From 40116ec84c0e64b9d4e83acf36cc9390c6db4789 Mon Sep 17 00:00:00 2001 From: imnyang Date: Fri, 6 Mar 2026 23:32:46 +0900 Subject: [PATCH] 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 --- .github/workflows/main.yml | 35 ++++++++++ Dockerfile | 49 ++++++++++++++ app/.gitignore | 34 ++++++++++ app/README.md | 15 +++++ app/bun.lock | 70 ++++++++++++++++++++ app/index.ts | 48 ++++++++++++++ app/lib/comcigan.ts | 7 ++ app/lib/discord.ts | 77 ++++++++++++++++++++++ app/lib/meal.ts | 128 +++++++++++++++++++++++++++++++++++++ app/package.json | 16 +++++ app/run.sh | 3 + app/test.ts | 7 ++ app/tsconfig.json | 29 +++++++++ cron | 1 + 14 files changed, 519 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 Dockerfile create mode 100644 app/.gitignore create mode 100644 app/README.md create mode 100644 app/bun.lock create mode 100644 app/index.ts create mode 100644 app/lib/comcigan.ts create mode 100644 app/lib/discord.ts create mode 100644 app/lib/meal.ts create mode 100644 app/package.json create mode 100644 app/run.sh create mode 100644 app/test.ts create mode 100644 app/tsconfig.json create mode 100644 cron diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f786642 --- /dev/null +++ b/.github/workflows/main.yml @@ -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 }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0b58fb8 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/app/.gitignore @@ -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 diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..e0531d3 --- /dev/null +++ b/app/README.md @@ -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. diff --git a/app/bun.lock b/app/bun.lock new file mode 100644 index 0000000..c32286d --- /dev/null +++ b/app/bun.lock @@ -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=="], + } +} diff --git a/app/index.ts b/app/index.ts new file mode 100644 index 0000000..e46a79e --- /dev/null +++ b/app/index.ts @@ -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); +}); \ No newline at end of file diff --git a/app/lib/comcigan.ts b/app/lib/comcigan.ts new file mode 100644 index 0000000..a7d81df --- /dev/null +++ b/app/lib/comcigan.ts @@ -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) +} diff --git a/app/lib/discord.ts b/app/lib/discord.ts new file mode 100644 index 0000000..ffc087c --- /dev/null +++ b/app/lib/discord.ts @@ -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}`); + } +} \ No newline at end of file diff --git a/app/lib/meal.ts b/app/lib/meal.ts new file mode 100644 index 0000000..f2cc62b --- /dev/null +++ b/app/lib/meal.ts @@ -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(//gi, '\n'), + date: MLSV_YMD, + // @ts-ignore + + kcal: data.mealServiceDietInfo[1].row[0].CAL_INFO, + }; + +} + +export async function NameToEmoji(name: string): Promise { + 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; +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..338fabf --- /dev/null +++ b/app/package.json @@ -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" + } +} diff --git a/app/run.sh b/app/run.sh new file mode 100644 index 0000000..e2ddac2 --- /dev/null +++ b/app/run.sh @@ -0,0 +1,3 @@ +cd /code/app + +./run \ No newline at end of file diff --git a/app/test.ts b/app/test.ts new file mode 100644 index 0000000..a23436b --- /dev/null +++ b/app/test.ts @@ -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)) diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/app/tsconfig.json @@ -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 + } +} diff --git a/cron b/cron new file mode 100644 index 0000000..dfc0fd1 --- /dev/null +++ b/cron @@ -0,0 +1 @@ +0 6 * * * /code/app/run.sh >> /code/app/temp/logs/run-$(date +%Y%m%d-%H%M%S).log 2>&1