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