fromis_9/backend/routes/schedules.js

226 lines
6.5 KiB
JavaScript
Raw Normal View History

import express from "express";
import pool from "../lib/db.js";
import { searchSchedules } from "../services/meilisearch.js";
import { saveSearchQuery, getSuggestions } from "../services/suggestions.js";
import { getXProfile } from "../services/x-bot.js";
const router = express.Router();
// 검색어 추천 API (Bi-gram 기반)
router.get("/suggestions", async (req, res) => {
try {
const { q, limit } = req.query;
if (!q || q.trim().length === 0) {
return res.json({ suggestions: [] });
}
const suggestions = await getSuggestions(q, parseInt(limit) || 10);
res.json({ suggestions });
} catch (error) {
console.error("추천 검색어 오류:", error);
res.status(500).json({ error: "추천 검색어 조회 중 오류가 발생했습니다." });
}
});
// 공개 일정 목록 조회 (검색 포함)
router.get("/", async (req, res) => {
try {
const { search, startDate, endDate, limit, year, month } = req.query;
// 검색어가 있으면 Meilisearch 사용
if (search && search.trim()) {
const offset = parseInt(req.query.offset) || 0;
const pageLimit = parseInt(req.query.limit) || 100;
// 첫 페이지 검색 시에만 검색어 저장 (bi-gram 학습)
if (offset === 0) {
saveSearchQuery(search.trim()).catch((err) =>
console.error("검색어 저장 실패:", err.message)
);
}
// Meilisearch에서 큰 limit으로 검색 (유사도 필터링 후 클라이언트 페이징)
const results = await searchSchedules(search.trim(), {
limit: 1000, // 내부적으로 1000개까지 검색
});
// 페이징 적용
const paginatedHits = results.hits.slice(offset, offset + pageLimit);
return res.json({
schedules: paginatedHits,
total: results.total,
offset: offset,
limit: pageLimit,
hasMore: offset + paginatedHits.length < results.total,
});
}
// 날짜 필터 및 제한 조건 구성
let whereClause = "WHERE 1=1";
const params = [];
// 년/월 필터링 (월별 데이터 로딩용)
if (year && month) {
whereClause += " AND YEAR(s.date) = ? AND MONTH(s.date) = ?";
params.push(parseInt(year), parseInt(month));
} else if (year) {
whereClause += " AND YEAR(s.date) = ?";
params.push(parseInt(year));
}
if (startDate) {
whereClause += " AND s.date >= ?";
params.push(startDate);
}
if (endDate) {
whereClause += " AND s.date <= ?";
params.push(endDate);
}
// limit 파라미터 처리
const limitClause = limit ? `LIMIT ${parseInt(limit)}` : "";
// 검색어 없으면 DB에서 전체 조회
const [schedules] = await pool.query(
`
SELECT
s.id,
s.title,
s.description,
s.date,
s.time,
s.category_id,
s.source_url,
s.source_name,
s.location_name,
c.name as category_name,
c.color as category_color,
GROUP_CONCAT(m.name ORDER BY m.id SEPARATOR ',') as member_names
FROM schedules s
LEFT JOIN schedule_categories c ON s.category_id = c.id
LEFT JOIN schedule_members sm ON s.id = sm.schedule_id
LEFT JOIN members m ON sm.member_id = m.id
${whereClause}
GROUP BY s.id
ORDER BY s.date ASC, s.time ASC
${limitClause}
`,
params
);
res.json(schedules);
} catch (error) {
console.error("일정 목록 조회 오류:", error);
res.status(500).json({ error: "일정 목록 조회 중 오류가 발생했습니다." });
}
});
// 카테고리 목록 조회
router.get("/categories", async (req, res) => {
try {
const [categories] = await pool.query(`
SELECT id, name, color, sort_order
FROM schedule_categories
ORDER BY sort_order ASC
`);
res.json(categories);
} catch (error) {
console.error("카테고리 조회 오류:", error);
res.status(500).json({ error: "카테고리 조회 중 오류가 발생했습니다." });
}
});
// 개별 일정 조회
router.get("/:id", async (req, res) => {
try {
const { id } = req.params;
const [schedules] = await pool.query(
`
SELECT
s.*,
c.name as category_name,
c.color as category_color
FROM schedules s
LEFT JOIN schedule_categories c ON s.category_id = c.id
WHERE s.id = ?
`,
[id]
);
if (schedules.length === 0) {
return res.status(404).json({ error: "일정을 찾을 수 없습니다." });
}
// 이미지 조회
const [images] = await pool.query(
`SELECT image_url FROM schedule_images WHERE schedule_id = ? ORDER BY sort_order ASC`,
[id]
);
const schedule = schedules[0];
schedule.images = images.map((img) => img.image_url);
res.json(schedule);
} catch (error) {
console.error("일정 조회 오류:", error);
res.status(500).json({ error: "일정 조회 중 오류가 발생했습니다." });
}
});
// Meilisearch 동기화 API
router.post("/sync-search", async (req, res) => {
try {
const { syncAllSchedules } = await import("../services/meilisearch.js");
// DB에서 모든 일정 조회
const [schedules] = await pool.query(`
SELECT
s.id,
s.title,
s.description,
s.date,
s.time,
s.category_id,
s.source_url,
s.source_name,
c.name as category_name,
c.color as category_color,
GROUP_CONCAT(m.name ORDER BY m.id SEPARATOR ',') as member_names
FROM schedules s
LEFT JOIN schedule_categories c ON s.category_id = c.id
LEFT JOIN schedule_members sm ON s.id = sm.schedule_id
LEFT JOIN members m ON sm.member_id = m.id
GROUP BY s.id
`);
const count = await syncAllSchedules(schedules);
res.json({ success: true, synced: count });
} catch (error) {
console.error("Meilisearch 동기화 오류:", error);
res.status(500).json({ error: "동기화 중 오류가 발생했습니다." });
}
});
// X 프로필 정보 조회
router.get("/x-profile/:username", async (req, res) => {
try {
const { username } = req.params;
const profile = await getXProfile(username);
if (!profile) {
return res.status(404).json({ error: "프로필을 찾을 수 없습니다." });
}
res.json(profile);
} catch (error) {
console.error("X 프로필 조회 오류:", error);
res.status(500).json({ error: "프로필 조회 중 오류가 발생했습니다." });
}
});
export default router;