diff --git a/backend/src/schemas/schedule.js b/backend/src/schemas/schedule.js index efd01e1..bc4614e 100644 --- a/backend/src/schemas/schedule.js +++ b/backend/src/schemas/schedule.js @@ -25,7 +25,8 @@ export const scheduleResponse = { properties: { id: { type: 'integer' }, title: { type: 'string' }, - datetime: { type: 'string' }, + date: { type: 'string', format: 'date' }, + time: { type: 'string', nullable: true }, category: scheduleCategory, members: { type: 'array', items: scheduleMember }, createdAt: { type: 'string', format: 'date-time' }, diff --git a/backend/src/services/meilisearch/index.js b/backend/src/services/meilisearch/index.js index fc601f6..308c10b 100644 --- a/backend/src/services/meilisearch/index.js +++ b/backend/src/services/meilisearch/index.js @@ -8,7 +8,6 @@ import Inko from 'inko'; import config, { CATEGORY_IDS } from '../../config/index.js'; import { createLogger } from '../../utils/logger.js'; -import { buildDatetime } from '../schedule.js'; const inko = new Inko(); const logger = createLogger('Meilisearch'); @@ -166,7 +165,8 @@ function formatScheduleResponse(hit) { return { id: hit.id, title: hit.title, - datetime: buildDatetime(hit.date, hit.time), + date: hit.date, + time: hit.time || null, category: { id: hit.category_id, name: hit.category_name, diff --git a/backend/src/services/schedule.js b/backend/src/services/schedule.js index e3d06ad..4b70a72 100644 --- a/backend/src/services/schedule.js +++ b/backend/src/services/schedule.js @@ -69,7 +69,8 @@ export function formatSchedule(rawSchedule, members = []) { return { id: rawSchedule.id, title: rawSchedule.title, - datetime: buildDatetime(rawSchedule.date, rawSchedule.time), + date: normalizeDate(rawSchedule.date), + time: rawSchedule.time || null, category: { id: rawSchedule.category_id, name: rawSchedule.category_name, @@ -220,7 +221,8 @@ export async function getScheduleDetail(db, id, getXProfile = null) { const result = { id: s.id, title: s.title, - datetime: buildDatetime(s.date, s.time), + date: normalizeDate(s.date), + time: s.time || null, category: { id: s.category_id, name: s.category_name, @@ -324,7 +326,8 @@ export async function getMonthlySchedules(db, year, month) { schedules.push({ id: `birthday-${member.id}`, title: `HAPPY ${member.name_en} DAY`, - datetime: birthdayDate.toISOString().split('T')[0], + date: birthdayDate.toISOString().split('T')[0], + time: null, category: { id: CATEGORY_IDS.BIRTHDAY, name: '생일', @@ -338,7 +341,7 @@ export async function getMonthlySchedules(db, year, month) { } // 날짜순 정렬 - schedules.sort((a, b) => a.datetime.localeCompare(b.datetime)); + schedules.sort((a, b) => a.date.localeCompare(b.date)); return { schedules }; } diff --git a/docs/api.md b/docs/api.md index d5ece60..07429b3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -50,25 +50,24 @@ Base URL: `/api` **월별 조회 응답:** ```json { - "2026-01-18": { - "categories": [ - { "id": 2, "name": "유튜브", "color": "#ff0033", "count": 3 } - ], - "schedules": [ - { - "id": 123, - "title": "...", - "time": "19:00:00", - "category": { "id": 2, "name": "유튜브", "color": "#ff0033" }, - "source": { - "name": "fromis_9", - "url": "https://www.youtube.com/watch?v=VIDEO_ID" - } - } - ] - } + "schedules": [ + { + "id": 123, + "title": "...", + "date": "2026-01-18", + "time": "19:00:00", + "category": { "id": 2, "name": "유튜브", "color": "#ff0033" }, + "source": { + "name": "fromis_9", + "url": "https://www.youtube.com/watch?v=VIDEO_ID" + }, + "members": ["송하영"] + } + ] } ``` +※ `time`: 시간이 없는 일정은 `null`, 00:00 시간은 `"00:00:00"`으로 반환 +``` **source 객체 (카테고리별):** - YouTube (category_id=2): `{ name: "채널명", url: "https://www.youtube.com/..." }` @@ -77,20 +76,22 @@ Base URL: `/api` **다가오는 일정 응답 (startDate):** ```json -[ - { - "id": 123, - "title": "...", - "date": "2026-01-18", - "time": "19:00:00", - "category_id": 2, - "category_name": "유튜브", - "category_color": "#ff0033", - "members": [{ "name": "송하영" }] - } -] +{ + "schedules": [ + { + "id": 123, + "title": "...", + "date": "2026-01-18", + "time": "19:00:00", + "category": { "id": 2, "name": "유튜브", "color": "#ff0033" }, + "source": { "name": "fromis_9", "url": "https://..." }, + "members": ["송하영"] + } + ] +} ``` -※ 현재 활동 멤버 전원인 경우 `[{ "name": "프로미스나인" }]` 반환 (탈퇴 멤버 제외) +※ 현재 활동 멤버 전원인 경우 `["프로미스나인"]` 반환 (탈퇴 멤버 제외) +※ `time`: 시간이 없는 일정은 `null`, 00:00 시간은 `"00:00:00"`으로 반환 **검색 응답:** ```json @@ -99,7 +100,8 @@ Base URL: `/api` { "id": 123, "title": "...", - "datetime": "2026-01-18T19:00:00", + "date": "2026-01-18", + "time": "19:00:00", "category": { "id": 2, "name": "유튜브", "color": "#ff0033" }, "source": { "name": "fromis_9", "url": "https://..." }, "members": ["송하영"], @@ -112,6 +114,8 @@ Base URL: `/api` "hasMore": true } ``` +※ `time`: 시간이 없는 일정은 `null`, 00:00 시간은 `"00:00:00"`으로 반환 +``` ### GET /schedules/categories 카테고리 목록 조회 diff --git a/frontend/src/api/admin/schedules.js b/frontend/src/api/admin/schedules.js index d7855c8..455a50e 100644 --- a/frontend/src/api/admin/schedules.js +++ b/frontend/src/api/admin/schedules.js @@ -5,29 +5,17 @@ import { fetchAuthApi, fetchFormData } from '@/api/client'; /** * API 응답을 프론트엔드 형식으로 변환 - * - datetime → date, time 분리 * - category 객체 → category_id, category_name, category_color 플랫화 * - members 배열 → member_names 문자열 */ function transformSchedule(schedule) { const category = schedule.category || {}; - // datetime에서 date와 time 분리 - let date = ''; - let time = null; - if (schedule.datetime) { - const parts = schedule.datetime.split('T'); - date = parts[0]; - time = parts[1] || null; - } - // members 배열을 문자열로 (기존 코드 호환성) const memberNames = Array.isArray(schedule.members) ? schedule.members.join(',') : ''; return { ...schedule, - date, - time, category_id: category.id, category_name: category.name, category_color: category.color, diff --git a/frontend/src/api/public/schedules.js b/frontend/src/api/public/schedules.js index 18a9739..b43748d 100644 --- a/frontend/src/api/public/schedules.js +++ b/frontend/src/api/public/schedules.js @@ -2,27 +2,16 @@ * 스케줄 API */ import { fetchApi, fetchAuthApi, fetchFormData } from '@/api/client'; -import { getTodayKST, dayjs } from '@/utils'; +import { getTodayKST } from '@/utils'; /** * API 응답을 프론트엔드 형식으로 변환 - * - datetime → date, time 분리 * - category 객체 → category_id, category_name, category_color 플랫화 * - members 배열 → member_names 문자열 */ function transformSchedule(schedule) { const category = schedule.category || {}; - // datetime에서 date와 time 분리 - let date = ''; - let time = null; - if (schedule.datetime) { - const dt = dayjs(schedule.datetime); - date = dt.format('YYYY-MM-DD'); - // datetime에 T가 포함되어 있으면 시간이 있는 것 - time = schedule.datetime.includes('T') ? dt.format('HH:mm:ss') : null; - } - // members 배열을 문자열로 (기존 코드 호환성) const memberNames = Array.isArray(schedule.members) ? schedule.members.join(',') @@ -30,8 +19,6 @@ function transformSchedule(schedule) { return { ...schedule, - date, - time, category_id: category.id, category_name: category.name, category_color: category.color, diff --git a/frontend/src/components/mobile/schedule/ScheduleSearchCard.jsx b/frontend/src/components/mobile/schedule/ScheduleSearchCard.jsx index 527b091..0f6f93d 100644 --- a/frontend/src/components/mobile/schedule/ScheduleSearchCard.jsx +++ b/frontend/src/components/mobile/schedule/ScheduleSearchCard.jsx @@ -14,7 +14,7 @@ const ScheduleSearchCard = memo(function ScheduleSearchCard({ delay = 0, className = '', }) { - const scheduleDate = new Date(schedule.date || schedule.datetime); + const scheduleDate = new Date(schedule.date); const categoryInfo = getCategoryInfo(schedule); const timeStr = getScheduleTime(schedule); const displayMembers = getDisplayMembers(schedule); diff --git a/frontend/src/components/pc/admin/schedule/AdminScheduleCard.jsx b/frontend/src/components/pc/admin/schedule/AdminScheduleCard.jsx index 3f30a0a..12d3884 100644 --- a/frontend/src/components/pc/admin/schedule/AdminScheduleCard.jsx +++ b/frontend/src/components/pc/admin/schedule/AdminScheduleCard.jsx @@ -12,7 +12,7 @@ function AdminScheduleCard({ onDelete, className = '', }) { - const scheduleDate = new Date(schedule.date || schedule.datetime); + const scheduleDate = new Date(schedule.date); const today = new Date(); const currentYear = today.getFullYear(); const currentMonth = today.getMonth(); diff --git a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx index a35ce68..3490ad2 100644 --- a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx +++ b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx @@ -5,7 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-react'; import Linkify from 'react-linkify'; import { getSchedule } from '@/api'; -import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTime } from '@/utils'; +import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTimeWithTime } from '@/utils'; /** * 전체화면 시 자동 가로 회전 훅 (숏츠가 아닐 때만) @@ -88,7 +88,7 @@ function MobileYoutubeSection({ schedule }) {