Add initial project setup with Docker, GitHub Actions, and meal timetable functionality
- Create Dockerfile for multi-stage build - Set up GitHub Actions workflow for CI/CD - Implement meal and timetable fetching logic - Add README and configuration files - Include cron job for scheduled execution - Establish .gitignore for build artifacts and environment files
This commit is contained in:
commit
40116ec84c
14 changed files with 519 additions and 0 deletions
35
.github/workflows/main.yml
vendored
Normal file
35
.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
name: Web Build
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
tags: |
|
||||||
|
ghcr.io/imnyang/today.sunrin:latest
|
||||||
|
ghcr.io/imnyang/today.sunrin:${{ github.run_id }}
|
||||||
49
Dockerfile
Normal file
49
Dockerfile
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
FROM oven/bun:alpine AS build
|
||||||
|
LABEL maintainer="@imnya"
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
COPY app /code/app
|
||||||
|
COPY .env /code/app/.env
|
||||||
|
|
||||||
|
RUN chmod +x /code/app/run.sh
|
||||||
|
|
||||||
|
WORKDIR /code/app
|
||||||
|
RUN mkdir -p /code/app/temp
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
RUN bun build index.ts --compile --minify --sourcemap --target bun --outfile ./run
|
||||||
|
|
||||||
|
FROM oven/bun:alpine AS runner
|
||||||
|
LABEL maintainer="@imnya"
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /code
|
||||||
|
RUN mkdir -p /code/app
|
||||||
|
|
||||||
|
# Copy the built files from the build stage
|
||||||
|
COPY --from=build /code/app/run /code/app/run
|
||||||
|
COPY --from=build /code/app/.env /code/app/.env
|
||||||
|
COPY --from=build /code/app/run.sh /code/app/run.sh
|
||||||
|
|
||||||
|
RUN mkdir -p /code/app/temp
|
||||||
|
|
||||||
|
# Set timezone to Asia/Seoul
|
||||||
|
RUN apk add --no-cache tzdata \
|
||||||
|
&& cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime \
|
||||||
|
&& echo "Asia/Seoul" > /etc/timezone \
|
||||||
|
&& apk del tzdata
|
||||||
|
|
||||||
|
# Cron job
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
RUN curl -Lo /code/app/supercronic https://github.com/aptible/supercronic/releases/latest/download/supercronic-linux-amd64 \
|
||||||
|
&& chmod +x /code/app/supercronic
|
||||||
|
|
||||||
|
RUN mkdir -p /code/app/temp/logs
|
||||||
|
|
||||||
|
COPY cron /code/app/cron
|
||||||
|
RUN chmod +x /code/app/cron
|
||||||
|
|
||||||
|
CMD ["/code/app/supercronic", "/code/app/cron"]
|
||||||
34
app/.gitignore
vendored
Normal file
34
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
15
app/README.md
Normal file
15
app/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# app
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||||
70
app/bun.lock
Normal file
70
app/bun.lock
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "app",
|
||||||
|
"dependencies": {
|
||||||
|
"@azure-rest/ai-inference": "^1.0.0-beta.6",
|
||||||
|
"@imnyang/comcigan.ts": "^0.3.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@azure-rest/ai-inference": ["@azure-rest/ai-inference@1.0.0-beta.6", "", { "dependencies": { "@azure-rest/core-client": "^2.1.0", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-lro": "^2.7.2", "@azure/core-rest-pipeline": "^1.18.2", "@azure/core-tracing": "^1.2.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-j5FrJDTHu2P2+zwFVe5j2edasOIhqkFj+VkDjbhGkQuOoIAByF0egRkgs0G1k03HyJ7bOOT9BkRF7MIgr/afhw=="],
|
||||||
|
|
||||||
|
"@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="],
|
||||||
|
|
||||||
|
"@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
|
||||||
|
|
||||||
|
"@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="],
|
||||||
|
|
||||||
|
"@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="],
|
||||||
|
|
||||||
|
"@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.23.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ=="],
|
||||||
|
|
||||||
|
"@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
|
||||||
|
|
||||||
|
"@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="],
|
||||||
|
|
||||||
|
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
|
||||||
|
|
||||||
|
"@imnyang/comcigan.ts": ["@imnyang/comcigan.ts@0.3.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "undici": "^6.23.0" } }, "sha512-IqOoqsrziSOZe0vUBvVjCysv8Ydz5uYBoSBiBtX3eopR2b+Em5W1C7mRJwrMa/9Tt2bgZxn30F3yq7w5yJzEdg=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
|
||||||
|
|
||||||
|
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.4", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ=="],
|
||||||
|
|
||||||
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||||
|
|
||||||
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/index.ts
Normal file
48
app/index.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Weekday } from "@imnyang/comcigan.ts";
|
||||||
|
import { Meal, Timetable } from "./lib/discord";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Tomorrow is 1st of the month
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const YYMMDD = tomorrow.toISOString().slice(0, 10).replace(/-/g, "").toString();
|
||||||
|
// const YYMMDD = "20250306";
|
||||||
|
const weekday = tomorrow.getDay() === 0 ? 6 : tomorrow.getDay() - 1;
|
||||||
|
// const weekday = 4;
|
||||||
|
|
||||||
|
console.log("📅 | date:", YYMMDD);
|
||||||
|
console.log("📅 | weekday:", weekday);
|
||||||
|
console.timeLog("⏱️ | sunrint");
|
||||||
|
await Timetable({
|
||||||
|
schoolId: 41896,
|
||||||
|
grade: 1,
|
||||||
|
classNum: 1,
|
||||||
|
weekday: Weekday.Friday,
|
||||||
|
WEBHOOK_URL: process.env.DISCORD_WEBHOOK_SUNRIN_URL as string
|
||||||
|
})
|
||||||
|
await Meal({
|
||||||
|
MLSV_YMD: YYMMDD,
|
||||||
|
ATPT_OFCDC_SC_CODE: "B10",
|
||||||
|
SD_SCHUL_CODE: "7010536",
|
||||||
|
username: "@today.sunrin",
|
||||||
|
schoolName: "선린인터넷고등학교",
|
||||||
|
WEBHOOK_URL: process.env.DISCORD_WEBHOOK_SUNRIN_URL as string
|
||||||
|
}); // 선린인고
|
||||||
|
console.timeEnd("⏱️ | sunrint");
|
||||||
|
|
||||||
|
console.timeLog("⏱️ | sangjeong");
|
||||||
|
await Meal({
|
||||||
|
MLSV_YMD: YYMMDD,
|
||||||
|
ATPT_OFCDC_SC_CODE: "B10",
|
||||||
|
SD_SCHUL_CODE: "7010536",
|
||||||
|
username: "@today.isangjeong",
|
||||||
|
schoolName: "상정고등학교",
|
||||||
|
WEBHOOK_URL: process.env.DISCORD_WEBHOOK_SANGJEONG_URL as string
|
||||||
|
}); // 상정고
|
||||||
|
console.timeEnd("⏱️ | sangjeong");
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Error in main function:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
7
app/lib/comcigan.ts
Normal file
7
app/lib/comcigan.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Comcigan, { School, Weekday } from '@imnyang/comcigan.ts'
|
||||||
|
|
||||||
|
const comcigan = new Comcigan()
|
||||||
|
|
||||||
|
export default async function getTimetable({ schoolId, grade, classNum, weekday }: { schoolId: number, grade: number, classNum: number, weekday: number }) {
|
||||||
|
return await comcigan.getTimetable(schoolId, grade, classNum, weekday, false)
|
||||||
|
}
|
||||||
77
app/lib/discord.ts
Normal file
77
app/lib/discord.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import getTimetable from "./comcigan";
|
||||||
|
import { getMealInfo, NameToEmoji, removeNutritionInfo } from "./meal";
|
||||||
|
|
||||||
|
export async function Meal({ MLSV_YMD, ATPT_OFCDC_SC_CODE, SD_SCHUL_CODE, username, schoolName, WEBHOOK_URL }: { MLSV_YMD: string, ATPT_OFCDC_SC_CODE: string, SD_SCHUL_CODE: string, username: string, schoolName: string, WEBHOOK_URL: string }) {
|
||||||
|
const mealInfo = await getMealInfo(MLSV_YMD, ATPT_OFCDC_SC_CODE, SD_SCHUL_CODE);
|
||||||
|
//const isVTS = vts.VTS임(MLSV_YMD);
|
||||||
|
|
||||||
|
const lines = removeNutritionInfo(mealInfo.meal).split("\n");
|
||||||
|
let emojis = (await NameToEmoji(lines.toString())).split(",");
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
content: `${MLSV_YMD.slice(0, 4)}년 ${MLSV_YMD.slice(4, 6)}월 ${MLSV_YMD.slice(6, 8)}일 급식 정보`,
|
||||||
|
username: username,
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: `🏫 | ${schoolName}`,
|
||||||
|
description: lines.map((line, index) => `${emojis[index] || "❓"} ${line}`).join("\n"),
|
||||||
|
footer: {
|
||||||
|
text: `🔥 ${mealInfo.kcal}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("🏓 | Sending Payload");
|
||||||
|
const response = await fetch(WEBHOOK_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("Error sending Discord webhook:", response.statusText);
|
||||||
|
} else {
|
||||||
|
console.log(`✨ | Payload successfully sent, code ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function Timetable({ schoolId, grade, classNum, weekday, WEBHOOK_URL }: { schoolId: number, grade: number, classNum: number, weekday: number, WEBHOOK_URL: string }) {
|
||||||
|
const timetableInfo = await getTimetable({ schoolId, grade, classNum, weekday });
|
||||||
|
|
||||||
|
console.log("🏓 | Timetable Info Retrieved", timetableInfo);
|
||||||
|
const data = {
|
||||||
|
content: `📅 | ${grade}학년 ${classNum}반 시간표 정보`,
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: `🏫 | 학교 : 선린인터넷고등학교`,
|
||||||
|
fields: (timetableInfo ?? []).map((item) => ({
|
||||||
|
name: `${item.subject}${item.changed ? (" *") : ""}`,
|
||||||
|
value: item.teacher,
|
||||||
|
inline: false,
|
||||||
|
})),
|
||||||
|
footer: {
|
||||||
|
text: `${["월", "화", "수", "목", "금"][weekday - 1]}요일 시간표 정보`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("🏓 | Sending Payload");
|
||||||
|
const response = await fetch(WEBHOOK_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("Error sending Discord webhook:", response.statusText);
|
||||||
|
} else {
|
||||||
|
console.log(`✨ | Payload successfully sent, code ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
128
app/lib/meal.ts
Normal file
128
app/lib/meal.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import ModelClient, { isUnexpected } from "@azure-rest/ai-inference";
|
||||||
|
import { AzureKeyCredential } from "@azure/core-auth";
|
||||||
|
|
||||||
|
const KEY = process.env.NEIS_API_KEY;
|
||||||
|
|
||||||
|
export function removeNutritionInfo(value: string): string {
|
||||||
|
const lines = value.trim().split('\n');
|
||||||
|
const cleanedLines = lines.map(line => line.replace(/\s*\([\d.,]+\)/g, '').trim());
|
||||||
|
const result = cleanedLines.join('\n');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nutritionList = [
|
||||||
|
"난류", "우유", "메밀", "땅콩", "대두", "밀", "고등어", "게", "새우", "돼지고기",
|
||||||
|
"복숭아", "토마토", "아황산류", "호두", "닭고기", "쇠고기", "오징어", "조개류(굴, 전복, 홍합 포함)", "잣"
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getNutritionInfo(value: string): string[][] {
|
||||||
|
const lines = value.trim().split('\n');
|
||||||
|
return lines.map(line => {
|
||||||
|
const indexes = line
|
||||||
|
.replace(/[()\s]/g, "")
|
||||||
|
.split(".")
|
||||||
|
.map(v => parseInt(v, 10) - 1)
|
||||||
|
.filter(i => i >= 0 && i < nutritionList.length);
|
||||||
|
return indexes
|
||||||
|
.map(i => nutritionList[i])
|
||||||
|
.filter((item): item is string => typeof item === "string");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getMealInfo(MLSV_YMD: string, ATPT_OFCDC_SC_CODE: string, SD_SCHUL_CODE: string): Promise<{ meal: string; date: string, kcal: string }> {
|
||||||
|
const url = `https://open.neis.go.kr/hub/mealServiceDietInfo?Type=json&ATPT_OFCDC_SC_CODE=${ATPT_OFCDC_SC_CODE}&SD_SCHUL_CODE=${SD_SCHUL_CODE}&MLSV_YMD=${MLSV_YMD}&KEY=${KEY}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
// @ts-ignore
|
||||||
|
const DDISH_NM = data.mealServiceDietInfo[1].row[0].DDISH_NM;
|
||||||
|
return {
|
||||||
|
meal: DDISH_NM.replace(/<br\s*\/?>/gi, '\n'),
|
||||||
|
date: MLSV_YMD,
|
||||||
|
// @ts-ignore
|
||||||
|
|
||||||
|
kcal: data.mealServiceDietInfo[1].row[0].CAL_INFO,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function NameToEmoji(name: string): Promise<string> {
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("GITHUB_TOKEN environment variable is not set.");
|
||||||
|
}
|
||||||
|
const endpoint = "https://models.github.ai/inference";
|
||||||
|
const model = "openai/gpt-5-mini";
|
||||||
|
|
||||||
|
const client = ModelClient(
|
||||||
|
endpoint,
|
||||||
|
new AzureKeyCredential(token),
|
||||||
|
);
|
||||||
|
|
||||||
|
const systemPrompt = `⚠️ 중요한 지침: 당신은 오직 이모지로만 응답하는 AI입니다. 다음 규칙을 예외 없이 철저히 준수해야 합니다. ⚠️
|
||||||
|
|
||||||
|
1. 핵심 임무:
|
||||||
|
|
||||||
|
어떤 단어나 문구든, 해당 항목에 대해 가장 정확하게 일치하는 단 하나의 이모지로 응답해야 합니다. 🎯
|
||||||
|
|
||||||
|
여러 개의 이모지를 혼합하거나, 텍스트와 함께 사용하는 것은 절대 금지입니다. 🚫
|
||||||
|
|
||||||
|
2. 다중 항목 처리:
|
||||||
|
|
||||||
|
쉼표(,)로 구분된 여러 항목(단어 또는 문구)이 입력되면, 응답도 정확히 같은 순서로 각 항목에 해당하는 이모지를 쉼표(,)로 구분하여 제시해야 합니다. 🔢
|
||||||
|
|
||||||
|
입력된 항목의 개수와 출력되는 이모지의 개수는 반드시 일치해야 합니다. ✅
|
||||||
|
|
||||||
|
3. 절대 금지 (매우 중요):
|
||||||
|
|
||||||
|
단일 항목에 두 개 이상의 이모지를 사용해서는 안 됩니다. (오직 한 개!) 1️⃣
|
||||||
|
|
||||||
|
응답에 어떤 종류의 텍스트(단어, 글자, 숫자, 설명, 주석 등)도 포함해서는 안 됩니다. 📝❌
|
||||||
|
|
||||||
|
요청된 형식의 이모지만 허용됩니다. 💯
|
||||||
|
|
||||||
|
4. 응답 형식:
|
||||||
|
|
||||||
|
[이모지1], [이모지2], [이모지3]... (이모지의 수는 입력된 항목의 수와 같아야 함) 🔄
|
||||||
|
|
||||||
|
🌟 예시 (이 규칙들을 완벽하게 준수):
|
||||||
|
|
||||||
|
Q: 현미찹쌀밥, 개, 축구
|
||||||
|
|
||||||
|
A: 🍚,🐶,⚽
|
||||||
|
|
||||||
|
Q: 행복, 슬픔, 놀람
|
||||||
|
|
||||||
|
A: 😊,😢,😮
|
||||||
|
|
||||||
|
Q: 안녕하세요, 반갑습니다 ✨
|
||||||
|
|
||||||
|
A: 👋,🤝
|
||||||
|
|
||||||
|
Q: 사랑, 평화, 자유
|
||||||
|
|
||||||
|
A: 🥰,☮️,🗽
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
const response = await client.path("/chat/completions").post({
|
||||||
|
body: {
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
{ role: "user", content: name }
|
||||||
|
],
|
||||||
|
temperature: 1.0,
|
||||||
|
top_p: 1.0,
|
||||||
|
model: model
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isUnexpected(response)) {
|
||||||
|
throw response.body.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const choices = response.body?.choices;
|
||||||
|
if (!choices || !choices[0]?.message?.content) {
|
||||||
|
throw new Error("No valid response from the model.");
|
||||||
|
}
|
||||||
|
return choices[0].message.content as string;
|
||||||
|
}
|
||||||
16
app/package.json
Normal file
16
app/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@azure-rest/ai-inference": "^1.0.0-beta.6",
|
||||||
|
"@imnyang/comcigan.ts": "^0.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
app/run.sh
Normal file
3
app/run.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
cd /code/app
|
||||||
|
|
||||||
|
./run
|
||||||
7
app/test.ts
Normal file
7
app/test.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Comcigan, { School, Weekday } from '@imnyang/comcigan.ts'
|
||||||
|
|
||||||
|
const comcigan = new Comcigan()
|
||||||
|
|
||||||
|
console.log(await School.fromName('선린인터넷고'))
|
||||||
|
|
||||||
|
console.log(await comcigan.getTimetable(41896, 1,1, Weekday.Friday))
|
||||||
29
app/tsconfig.json
Normal file
29
app/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
1
cron
Normal file
1
cron
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
0 6 * * * /code/app/run.sh >> /code/app/temp/logs/run-$(date +%Y%m%d-%H%M%S).log 2>&1
|
||||||
Loading…
Add table
Add a link
Reference in a new issue