From 3922d5c6f77280a0734a80d03d09b3c52ec5c5de Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 21 Jan 2026 23:35:17 +0900 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=9D=BC=EC=A0=95=20API=EB=8F=84=20=EC=83=88=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin/schedules.js에 transformSchedule 함수 추가 - AdminSchedule.jsx의 검색 로직을 schedulesApi.searchSchedules 사용으로 변경 - 직접 fetch 호출 제거 Co-Authored-By: Claude Opus 4.5 --- frontend/src/api/admin/schedules.js | 61 +++-- frontend/src/pages/pc/admin/AdminSchedule.jsx | 253 +++++++++++------- 2 files changed, 200 insertions(+), 114 deletions(-) diff --git a/frontend/src/api/admin/schedules.js b/frontend/src/api/admin/schedules.js index 4a689bc..26fdbdc 100644 --- a/frontend/src/api/admin/schedules.js +++ b/frontend/src/api/admin/schedules.js @@ -3,32 +3,55 @@ */ import { fetchAdminApi, fetchAdminFormData } from "../index"; +/** + * 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, + member_names: memberNames, + }; +} + // 일정 목록 조회 (월별) export async function getSchedules(year, month) { const data = await fetchAdminApi(`/api/schedules?year=${year}&month=${month}`); - - // 날짜별 그룹화된 응답을 플랫 배열로 변환 - const schedules = []; - for (const [date, dayData] of Object.entries(data)) { - for (const schedule of dayData.schedules) { - const category = schedule.category || {}; - schedules.push({ - ...schedule, - date, - category_id: category.id, - category_name: category.name, - category_color: category.color, - }); - } - } - return schedules; + return (data.schedules || []).map(transformSchedule); } // 일정 검색 (Meilisearch) -export async function searchSchedules(query) { - return fetchAdminApi( - `/api/admin/schedules/search?q=${encodeURIComponent(query)}` +export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { + const data = await fetchAdminApi( + `/api/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}` ); + return { + ...data, + schedules: (data.schedules || []).map(transformSchedule), + }; } // 일정 상세 조회 diff --git a/frontend/src/pages/pc/admin/AdminSchedule.jsx b/frontend/src/pages/pc/admin/AdminSchedule.jsx index 8882ab0..d8505f5 100644 --- a/frontend/src/pages/pc/admin/AdminSchedule.jsx +++ b/frontend/src/pages/pc/admin/AdminSchedule.jsx @@ -45,6 +45,64 @@ const getMemberList = (schedule) => { return []; }; +// 일정 날짜 추출 (검색 결과와 일반 데이터 모두 처리) +const getScheduleDate = (schedule) => { + // datetime이 있으면 (검색 결과) + if (schedule.datetime) { + return new Date(schedule.datetime); + } + // date가 있으면 (일반 데이터) + if (schedule.date) { + return new Date(schedule.date); + } + return new Date(); +}; + +// 일정 시간 추출 (검색 결과와 일반 데이터 모두 처리) +const getScheduleTime = (schedule) => { + // time이 있으면 (일반 데이터) + if (schedule.time) { + return schedule.time.slice(0, 5); + } + // datetime에서 시간 추출 (검색 결과) + if (schedule.datetime && schedule.datetime.includes('T')) { + const timePart = schedule.datetime.split('T')[1]; + if (timePart) { + return timePart.slice(0, 5); + } + } + return null; +}; + +// 카테고리 ID 추출 (검색 결과와 일반 데이터 모두 처리) +const getCategoryId = (schedule) => { + // category_id가 있으면 (일반 데이터) + if (schedule.category_id !== undefined) { + return schedule.category_id; + } + // category.id가 있으면 (검색 결과) + if (schedule.category?.id !== undefined) { + return schedule.category.id; + } + return null; +}; + +// 카테고리 정보 추출 (검색 결과와 일반 데이터 모두 처리) +const getCategoryInfo = (schedule, categories) => { + const catId = getCategoryId(schedule); + // 검색 결과에 category 객체가 있으면 직접 사용 + if (schedule.category?.name && schedule.category?.color) { + return { + id: schedule.category.id, + name: schedule.category.name, + color: schedule.category.color + }; + } + // categories 배열에서 찾기 + const found = categories.find(c => c.id === catId); + return found || { id: catId, name: '미분류', color: '#6b7280' }; +}; + // 카테고리 ID 상수 const CATEGORY_IDS = { YOUTUBE: 2, @@ -73,11 +131,13 @@ const ScheduleItem = memo(function ScheduleItem({ navigate, openDeleteDialog }) { - const scheduleDate = new Date(schedule.date); + const scheduleDate = getScheduleDate(schedule); const isBirthday = schedule.is_birthday || String(schedule.id).startsWith('birthday-'); - const categoryColor = getColorStyle(categories.find(c => c.id === schedule.category_id)?.color)?.style?.backgroundColor || '#6b7280'; - const categoryName = categories.find(c => c.id === schedule.category_id)?.name || '미분류'; + const categoryInfo = getCategoryInfo(schedule, categories); + const categoryColor = getColorStyle(categoryInfo.color)?.style?.backgroundColor || categoryInfo.color || '#6b7280'; const memberList = getMemberList(schedule); + const timeStr = getScheduleTime(schedule); + const catId = getCategoryId(schedule); return (

{decodeHtmlEntities(schedule.title)}

- {schedule.time && ( + {timeStr && ( - {schedule.time.slice(0, 5)} + {timeStr} )} - {categoryName} + {categoryInfo.name} {schedule.source?.name && ( @@ -220,11 +280,7 @@ function AdminSchedule() { } = useInfiniteQuery({ queryKey: ['adminScheduleSearch', searchTerm], queryFn: async ({ pageParam = 0 }) => { - const response = await fetch( - `/api/schedules?search=${encodeURIComponent(searchTerm)}&offset=${pageParam}&limit=${SEARCH_LIMIT}` - ); - if (!response.ok) throw new Error('Search failed'); - return response.json(); + return schedulesApi.searchSchedules(searchTerm, { offset: pageParam, limit: SEARCH_LIMIT }); }, getNextPageParam: (lastPage) => { if (lastPage.hasMore) { @@ -582,7 +638,7 @@ function AdminSchedule() { if (selectedCategories.length === 0) { result = [...searchResults]; } else { - result = searchResults.filter(s => selectedCategories.includes(s.category_id)); + result = searchResults.filter(s => selectedCategories.includes(getCategoryId(s))); } } else { // 일반 모드: 로컬 필터링 @@ -617,20 +673,21 @@ function AdminSchedule() { const source = (isSearchMode && searchTerm) ? searchResults : schedules; const counts = new Map(); let total = 0; - + source.forEach(s => { // 검색 모드에서 검색어가 있을 때는 전체 대상 // 그 외에는 선택된 날짜 기준으로 필터링 if (!(isSearchMode && searchTerm) && selectedDate) { - const scheduleDate = formatDate(s.date); + const sDate = getScheduleDate(s); + const scheduleDate = formatDate(sDate); if (scheduleDate !== selectedDate) return; } - - const catId = s.category_id; + + const catId = getCategoryId(s); counts.set(catId, (counts.get(catId) || 0) + 1); total++; }); - + counts.set('total', total); return counts; }, [schedules, searchResults, isSearchMode, searchTerm, selectedDate]); @@ -1274,88 +1331,94 @@ function AdminSchedule() { transform: `translateY(${virtualItem.start}px)`, }} > -
-
-
-
- {new Date(schedule.date).getFullYear()}.{new Date(schedule.date).getMonth() + 1} -
-
- {new Date(schedule.date).getDate()} -
-
- {['일', '월', '화', '수', '목', '금', '토'][new Date(schedule.date).getDay()]}요일 -
-
+ {(() => { + const scheduleDate = getScheduleDate(schedule); + const categoryInfo = getCategoryInfo(schedule, categories); + const categoryColor = getColorStyle(categoryInfo.color)?.style?.backgroundColor || categoryInfo.color || '#6b7280'; + const timeStr = getScheduleTime(schedule); + const catId = getCategoryId(schedule); + const memberList = getMemberList(schedule); -
c.id === schedule.category_id)?.color)?.style?.backgroundColor || '#6b7280' }} - /> - -
-

{decodeHtmlEntities(schedule.title)}

-
- {schedule.time && ( - - - {schedule.time.slice(0, 5)} - - )} - - - {categories.find(c => c.id === schedule.category_id)?.name || '미분류'} - - {schedule.source?.name && ( - - - {schedule.source?.name} - - )} -
- {(() => { - const memberList = getMemberList(schedule); - if (memberList.length === 0) return null; - return ( -
- {memberList.map((name, i) => ( - - {name} - - ))} + return ( +
+
+
+
+ {scheduleDate.getFullYear()}.{scheduleDate.getMonth() + 1}
- ); - })()} -
+
+ {scheduleDate.getDate()} +
+
+ {['일', '월', '화', '수', '목', '금', '토'][scheduleDate.getDay()]}요일 +
+
-
- {schedule.source?.url && ( - e.stopPropagation()} - className="p-2 hover:bg-blue-100 rounded-lg transition-colors text-blue-500" - > - - - )} - - +
+ +
+

{decodeHtmlEntities(schedule.title)}

+
+ {timeStr && ( + + + {timeStr} + + )} + + + {categoryInfo.name} + + {schedule.source?.name && ( + + + {schedule.source?.name} + + )} +
+ {memberList.length > 0 && ( +
+ {memberList.map((name, i) => ( + + {name} + + ))} +
+ )} +
+ +
+ {schedule.source?.url && ( + e.stopPropagation()} + className="p-2 hover:bg-blue-100 rounded-lg transition-colors text-blue-500" + > + + + )} + + +
+
-
-
-
+ ); + })()}
); })}