2026-01-05 22:16:02 +09:00
|
|
|
import express from "express";
|
|
|
|
|
import pool from "../lib/db.js";
|
2026-01-06 08:22:43 +09:00
|
|
|
import { searchSchedules } from "../services/meilisearch.js";
|
2026-01-05 22:16:02 +09:00
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
2026-01-06 08:22:43 +09:00
|
|
|
// 공개 일정 목록 조회 (검색 포함)
|
2026-01-05 22:16:02 +09:00
|
|
|
router.get("/", async (req, res) => {
|
|
|
|
|
try {
|
2026-01-09 21:38:29 +09:00
|
|
|
const { search, startDate, endDate, limit, year, month } = req.query;
|
2026-01-06 08:22:43 +09:00
|
|
|
|
|
|
|
|
// 검색어가 있으면 Meilisearch 사용
|
|
|
|
|
if (search && search.trim()) {
|
2026-01-06 19:48:43 +09:00
|
|
|
const offset = parseInt(req.query.offset) || 0;
|
|
|
|
|
const pageLimit = parseInt(req.query.limit) || 20;
|
|
|
|
|
const results = await searchSchedules(search.trim(), {
|
|
|
|
|
offset,
|
|
|
|
|
limit: pageLimit,
|
|
|
|
|
});
|
|
|
|
|
return res.json({
|
|
|
|
|
schedules: results.hits,
|
|
|
|
|
total: results.total,
|
|
|
|
|
offset: results.offset,
|
|
|
|
|
limit: results.limit,
|
|
|
|
|
hasMore: results.offset + results.hits.length < results.total,
|
|
|
|
|
});
|
2026-01-06 08:22:43 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-06 12:04:27 +09:00
|
|
|
// 날짜 필터 및 제한 조건 구성
|
|
|
|
|
let whereClause = "WHERE 1=1";
|
|
|
|
|
const params = [];
|
|
|
|
|
|
2026-01-09 21:38:29 +09:00
|
|
|
// 년/월 필터링 (월별 데이터 로딩용)
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 12:04:27 +09:00
|
|
|
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)}` : "";
|
|
|
|
|
|
2026-01-06 08:22:43 +09:00
|
|
|
// 검색어 없으면 DB에서 전체 조회
|
2026-01-06 12:04:27 +09:00
|
|
|
const [schedules] = await pool.query(
|
|
|
|
|
`
|
2026-01-05 22:16:02 +09:00
|
|
|
SELECT
|
|
|
|
|
s.id,
|
|
|
|
|
s.title,
|
|
|
|
|
s.description,
|
|
|
|
|
s.date,
|
|
|
|
|
s.time,
|
|
|
|
|
s.category_id,
|
|
|
|
|
s.source_url,
|
2026-01-06 00:27:35 +09:00
|
|
|
s.source_name,
|
2026-01-05 22:16:02 +09:00
|
|
|
s.location_name,
|
|
|
|
|
c.name as category_name,
|
2026-01-06 00:27:35 +09:00
|
|
|
c.color as category_color,
|
|
|
|
|
GROUP_CONCAT(m.name ORDER BY m.id SEPARATOR ',') as member_names
|
2026-01-05 22:16:02 +09:00
|
|
|
FROM schedules s
|
|
|
|
|
LEFT JOIN schedule_categories c ON s.category_id = c.id
|
2026-01-06 00:27:35 +09:00
|
|
|
LEFT JOIN schedule_members sm ON s.id = sm.schedule_id
|
|
|
|
|
LEFT JOIN members m ON sm.member_id = m.id
|
2026-01-06 12:04:27 +09:00
|
|
|
${whereClause}
|
2026-01-06 00:27:35 +09:00
|
|
|
GROUP BY s.id
|
2026-01-06 12:04:27 +09:00
|
|
|
ORDER BY s.date ASC, s.time ASC
|
|
|
|
|
${limitClause}
|
|
|
|
|
`,
|
|
|
|
|
params
|
|
|
|
|
);
|
2026-01-05 22:16:02 +09:00
|
|
|
|
|
|
|
|
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: "일정을 찾을 수 없습니다." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json(schedules[0]);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("일정 조회 오류:", error);
|
|
|
|
|
res.status(500).json({ error: "일정 조회 중 오류가 발생했습니다." });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-06 08:22:43 +09:00
|
|
|
// 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,
|
2026-01-06 08:46:10 +09:00
|
|
|
GROUP_CONCAT(m.name ORDER BY m.id SEPARATOR ',') as member_names
|
2026-01-06 08:22:43 +09:00
|
|
|
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: "동기화 중 오류가 발생했습니다." });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-05 22:16:02 +09:00
|
|
|
export default router;
|