이건 Python이 잘못했다다
This commit is contained in:
parent
2d2e546673
commit
ca946990e1
41 changed files with 3081 additions and 964 deletions
39
app/lib/discord.ts
Normal file
39
app/lib/discord.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { getMealInfo } from "./meal";
|
||||
|
||||
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL as string;
|
||||
|
||||
export async function Discord(MLSV_YMD: string) {
|
||||
const mealInfo = await getMealInfo(MLSV_YMD);
|
||||
//const isVTS = vts.VTS임(MLSV_YMD);
|
||||
|
||||
const data = {
|
||||
// To include VTS info, uncomment and define isVTS:
|
||||
// content: `${MLSV_YMD.slice(0, 4)}년 ${MLSV_YMD.slice(4, 6)}월 ${MLSV_YMD.slice(6, 8)}일 급식 정보${isVTS ? " | with V.T.S." : ""}`,
|
||||
content: `${MLSV_YMD.slice(0, 4)}년 ${MLSV_YMD.slice(
|
||||
4,
|
||||
6
|
||||
)}월 ${MLSV_YMD.slice(6, 8)}일 급식 정보`,
|
||||
username: "@today.isangjeong",
|
||||
embeds: [
|
||||
{
|
||||
title: "🏫 | 인천상정중학교",
|
||||
description: `🔥 ${mealInfo.kcal}\n\n${mealInfo.meal}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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, code ${response.status}`);
|
||||
}
|
||||
}
|
||||
151
app/lib/image.ts
Normal file
151
app/lib/image.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import path from "path";
|
||||
import { join } from "path";
|
||||
import { createCanvas, loadImage, GlobalFonts } from "@napi-rs/canvas";
|
||||
import { getMealInfo } from "./meal"; // 이 함수의 내용은 제공되지 않았으므로 그대로 둡니다.
|
||||
import { getAllSchedules } from "./schedule";
|
||||
|
||||
GlobalFonts.registerFromPath('./template/Pretendard-Bold.ttf', 'Pretendard Bold')
|
||||
|
||||
export class CreateImage {
|
||||
static async PostMeal(MLSV_YMD: string) {
|
||||
const mealInfo = await getMealInfo(MLSV_YMD);
|
||||
const img = await loadImage(path.join("./template/skeleton.png"));
|
||||
const canvas = createCanvas(img.width, img.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
ctx.font = "56px Pretendard Bold";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.textAlign = "left";
|
||||
|
||||
const lines = mealInfo.meal.split("\n").reverse();
|
||||
lines.forEach((line, i) => {
|
||||
ctx.fillText(line, 75, 930 - i * 60);
|
||||
});
|
||||
|
||||
ctx.font = "24px Pretendard Bold";
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText(
|
||||
`${MLSV_YMD.slice(0, 4)}년 ${MLSV_YMD.slice(4, 6)}월 ${MLSV_YMD.slice(
|
||||
6,
|
||||
8
|
||||
)}일`,
|
||||
945,
|
||||
110
|
||||
);
|
||||
ctx.fillStyle = "#89CAFF";
|
||||
ctx.fillText(mealInfo.kcal, 945, 220);
|
||||
|
||||
/*if (vts.VTS임(MLSV_YMD)) {
|
||||
ctx.fillStyle = "#CDAD94";
|
||||
ctx.fillText("with V.T.S.", 830, 225);
|
||||
}*/ // VTS 관련 코드 주석 처리
|
||||
|
||||
const outPath = path.join("./temp", `${MLSV_YMD}.png`); // 경로 수정: "./temp/" -> "./temp"
|
||||
const buffer = canvas.toBuffer("image/png");
|
||||
|
||||
try {
|
||||
await Bun.write(outPath, buffer);
|
||||
console.log("🍲 | Meal Info Image Saved");
|
||||
} catch (error) {
|
||||
console.error("Error saving meal info image:", error);
|
||||
}
|
||||
}
|
||||
|
||||
static async PostSchedule() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = (today.getMonth() + 1).toString().padStart(2, "0");
|
||||
|
||||
// 일정 데이터 불러오기
|
||||
const allSchedules = await getAllSchedules();
|
||||
const events = allSchedules[month] || [];
|
||||
|
||||
// 이미지 로드
|
||||
const image = await loadImage(join("./template/skeleton_schoolevent.png"));
|
||||
const canvas = createCanvas(image.width, image.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// 배경 그리기
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
// 폰트 설정
|
||||
const eventFont = "48px Pretendard Bold";
|
||||
const detailFont = "24px Pretendard Bold";
|
||||
ctx.font = eventFont;
|
||||
ctx.fillStyle = "white";
|
||||
ctx.textAlign = "left";
|
||||
|
||||
// 행사 내용 그리기
|
||||
let yPosition = 930;
|
||||
for (const event of events.slice().reverse()) {
|
||||
ctx.fillText(`${event.date} : ${event.desc}`, 75, yPosition);
|
||||
yPosition -= 60;
|
||||
}
|
||||
|
||||
// 상단 월 정보
|
||||
ctx.font = detailFont;
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText(`${year}년 ${parseInt(month)}월`, 945, 110);
|
||||
|
||||
// 파일 저장
|
||||
const outputFilePath = join("./", `temp/schedule-${year}-${month}.png`);
|
||||
const buffer = canvas.toBuffer("image/png");
|
||||
require("fs").writeFileSync(outputFilePath, buffer);
|
||||
console.log("📅 | School Event Image Saved");
|
||||
|
||||
return outputFilePath;
|
||||
}
|
||||
|
||||
static async ConvertToStory(filePath: string) {
|
||||
const inPath = path.join("./",`${filePath}`);
|
||||
let img;
|
||||
try {
|
||||
img = await loadImage(inPath);
|
||||
} catch (error) {
|
||||
console.error(`Error loading image for story: ${inPath}`, error);
|
||||
// Post 메서드가 먼저 호출되어 이미지가 생성되었는지 확인 필요
|
||||
// 또는 에러 처리를 통해 사용자에게 알림
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const width = img.width;
|
||||
const height = img.height;
|
||||
const aspectRatio = 9 / 16;
|
||||
const currentRatio = width / height;
|
||||
|
||||
let newWidth = width;
|
||||
let newHeight = height;
|
||||
|
||||
if (currentRatio < aspectRatio) {
|
||||
newWidth = Math.floor(height * aspectRatio);
|
||||
} else {
|
||||
newHeight = Math.floor(width / aspectRatio);
|
||||
}
|
||||
|
||||
const canvas = createCanvas(newWidth, newHeight);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.fillStyle = "#0A0A0A"; // 배경색
|
||||
ctx.fillRect(0, 0, newWidth, newHeight);
|
||||
|
||||
// 원본 이미지를 새 캔버스 중앙에 맞추어 약간 작게(85%) 그립니다.
|
||||
const resizedWidth = Math.floor(width * 0.85);
|
||||
const resizedHeight = Math.floor(height * 0.85);
|
||||
|
||||
const offsetX = Math.floor((newWidth - resizedWidth) / 2);
|
||||
const offsetY = Math.floor((newHeight - resizedHeight) / 2);
|
||||
|
||||
ctx.drawImage(img, offsetX, offsetY, resizedWidth, resizedHeight);
|
||||
|
||||
const outPath = path.join("./", `${filePath}-story.png`);
|
||||
const buffer = canvas.toBuffer("image/png");
|
||||
|
||||
try {
|
||||
await Bun.write(outPath, buffer);
|
||||
console.log("🔄️ | Story Convert Success");
|
||||
} catch (error) {
|
||||
console.error("Error saving story info image:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
app/lib/instagram.ts
Normal file
65
app/lib/instagram.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import path from 'path';
|
||||
import { IgApiClient } from "igramapi";
|
||||
|
||||
const ig = new IgApiClient();
|
||||
|
||||
export async function Login() {
|
||||
ig.state.generateDevice(process.env.IG_USERNAME!);
|
||||
ig.state.appUserAgent
|
||||
|
||||
try {
|
||||
await ig.simulate.preLoginFlow();
|
||||
const loggedInUser = await ig.account.login(
|
||||
process.env.IG_USERNAME!,
|
||||
process.env.IG_PASSWORD!
|
||||
);
|
||||
process.nextTick(async () => await ig.simulate.postLoginFlow());
|
||||
const currentUser = await ig.account.currentUser();
|
||||
|
||||
return { loggedInUser, currentUser };
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.name === "IgCheckpointError") {
|
||||
console.error(
|
||||
"Instagram checkpoint required. Please verify your account in the Instagram app or handle the challenge."
|
||||
);
|
||||
// Optionally, you can trigger challenge handling here
|
||||
// await ig.challenge.auto(true); // Requesting sms-code or click "It was me" button
|
||||
} else {
|
||||
console.error("🔒 | Login failed:", error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export class Upload {
|
||||
static async Post(filePath: string, caption: string) {
|
||||
try {
|
||||
const ImagePath = path.resolve(`${filePath}`);
|
||||
const bunFile = Bun.file(ImagePath);
|
||||
const ImageBuffer = Buffer.from(await bunFile.arrayBuffer());
|
||||
|
||||
await ig.publish.photo({
|
||||
file: ImageBuffer,
|
||||
caption: caption,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error uploading post:", error);
|
||||
}
|
||||
}
|
||||
|
||||
static async Story(filePath: string, MLSV_YMD: string) {
|
||||
try {
|
||||
const ImagePath = path.resolve(`${filePath}`);
|
||||
const bunFile = Bun.file(ImagePath);
|
||||
const ImageBuffer = Buffer.from(await bunFile.arrayBuffer());
|
||||
|
||||
await ig.publish.story({
|
||||
file: ImageBuffer,
|
||||
caption: `#인천상정중학교 #상정중학교 #급식 \n${MLSV_YMD}일자 급식`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error uploading post:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
app/lib/meal.ts
Normal file
25
app/lib/meal.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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(/\(.*?\)/g, '').trim());
|
||||
const result = cleanedLines.join('\n');
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getMealInfo(MLSV_YMD: string): Promise<{ meal: string; date: string, kcal: string }> {
|
||||
const url = `https://open.neis.go.kr/hub/mealServiceDietInfo?Type=json&ATPT_OFCDC_SC_CODE=E10&SD_SCHUL_CODE=7331071&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: removeNutritionInfo(DDISH_NM.replace(/<br\s*\/?>/gi, '\n')),
|
||||
date: MLSV_YMD,
|
||||
// @ts-ignore
|
||||
|
||||
kcal: data.mealServiceDietInfo[1].row[0].CAL_INFO,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
105
app/lib/schedule.ts
Normal file
105
app/lib/schedule.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { writeFileSync, existsSync, readFileSync } from "fs";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
const CACHE_FILE = "./temp/scheduleCache.json";
|
||||
|
||||
const URLS = {
|
||||
section1:
|
||||
"https://isangjeong.icems.kr/schdList.do?section=1&m=021101&s=isangjeong",
|
||||
section2:
|
||||
"https://isangjeong.icems.kr/schdList.do?section=2&m=021101&s=isangjeong",
|
||||
};
|
||||
|
||||
const MONTH_XPATH_MAP: Record<
|
||||
string,
|
||||
{ section: keyof typeof URLS; divIndex: number }
|
||||
> = {
|
||||
"03": { section: "section1", divIndex: 2 },
|
||||
"04": { section: "section1", divIndex: 3 },
|
||||
"05": { section: "section1", divIndex: 4 },
|
||||
"06": { section: "section1", divIndex: 5 },
|
||||
"07": { section: "section1", divIndex: 6 },
|
||||
"08": { section: "section1", divIndex: 7 },
|
||||
"09": { section: "section2", divIndex: 2 },
|
||||
"10": { section: "section2", divIndex: 3 },
|
||||
"11": { section: "section2", divIndex: 4 },
|
||||
"12": { section: "section2", divIndex: 5 },
|
||||
"01": { section: "section2", divIndex: 6 },
|
||||
"02": { section: "section2", divIndex: 7 },
|
||||
};
|
||||
|
||||
async function fetchSectionSchedule(
|
||||
url: string,
|
||||
divIndexes: { month: string; index: number }[]
|
||||
): Promise<Record<string, { date: string; desc: string }[]>> {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; today.isangjeong/1.0)",
|
||||
},
|
||||
});
|
||||
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const sectionSchedule: Record<string, { date: string; desc: string }[]> = {};
|
||||
|
||||
for (const { month, index } of divIndexes) {
|
||||
const selector = `#all-scroll > div > form > div > div:nth-of-type(2) > div:nth-of-type(${index}) > dl > dd > ul`;
|
||||
const items: { date: string; desc: string }[] = [];
|
||||
|
||||
$(selector).find("li").each((_, li) => {
|
||||
const text = $(li).text().trim().replace(/\u00A0/g, " ");
|
||||
if (text) {
|
||||
const [date, desc] = text.split(" : ");
|
||||
if (date && desc) {
|
||||
items.push({ date: date.trim(), desc: desc.trim() });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sectionSchedule[month] = items;
|
||||
}
|
||||
|
||||
return sectionSchedule;
|
||||
}
|
||||
|
||||
export async function getAllSchedules(
|
||||
refresh = false
|
||||
): Promise<Record<string, { date: string; desc: string }[]>> {
|
||||
if (!refresh && existsSync(CACHE_FILE)) {
|
||||
const cached = readFileSync(CACHE_FILE, "utf-8");
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
const sectionMap: Record<keyof typeof URLS, { month: string; index: number }[]> = {
|
||||
section1: [],
|
||||
section2: [],
|
||||
};
|
||||
|
||||
for (const [month, { section, divIndex }] of Object.entries(MONTH_XPATH_MAP)) {
|
||||
sectionMap[section].push({ month, index: divIndex });
|
||||
}
|
||||
|
||||
// 병렬로 section1, section2 요청
|
||||
const promises = (Object.keys(sectionMap) as (keyof typeof URLS)[]).map(
|
||||
async (section) => {
|
||||
const url = URLS[section];
|
||||
try {
|
||||
const sectionSchedule = await fetchSectionSchedule(url, sectionMap[section]);
|
||||
console.log(`✅ ${section} 완료`);
|
||||
return sectionSchedule;
|
||||
} catch (err) {
|
||||
console.error(`❌ ${section} 실패:`, err);
|
||||
return Object.fromEntries(sectionMap[section].map(({ month }) => [month, []]));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const schedule = Object.assign({}, ...results);
|
||||
|
||||
writeFileSync(CACHE_FILE, JSON.stringify(schedule, null, 2), "utf-8");
|
||||
console.log(`📦 캐시 저장됨 → ${CACHE_FILE}`);
|
||||
return schedule;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue