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:
암냥 2026-03-06 23:32:46 +09:00
commit 40116ec84c
No known key found for this signature in database
14 changed files with 519 additions and 0 deletions

128
app/lib/meal.ts Normal file
View 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;
}