refactor(frontend): 관리자 일정 API도 새 형식에 맞게 업데이트

- admin/schedules.js에 transformSchedule 함수 추가
- AdminSchedule.jsx의 검색 로직을 schedulesApi.searchSchedules 사용으로 변경
- 직접 fetch 호출 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 23:35:17 +09:00
parent 55096c8e43
commit 3922d5c6f7
2 changed files with 200 additions and 114 deletions

View file

@ -3,32 +3,55 @@
*/ */
import { fetchAdminApi, fetchAdminFormData } from "../index"; import { fetchAdminApi, fetchAdminFormData } from "../index";
// 일정 목록 조회 (월별) /**
export async function getSchedules(year, month) { * API 응답을 프론트엔드 형식으로 변환
const data = await fetchAdminApi(`/api/schedules?year=${year}&month=${month}`); * - datetime date, time 분리
* - category 객체 category_id, category_name, category_color 플랫화
// 날짜별 그룹화된 응답을 플랫 배열로 변환 * - members 배열 member_names 문자열
const schedules = []; */
for (const [date, dayData] of Object.entries(data)) { function transformSchedule(schedule) {
for (const schedule of dayData.schedules) {
const category = schedule.category || {}; const category = schedule.category || {};
schedules.push({
// 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, ...schedule,
date, date,
time,
category_id: category.id, category_id: category.id,
category_name: category.name, category_name: category.name,
category_color: category.color, category_color: category.color,
}); member_names: memberNames,
};
} }
}
return schedules; // 일정 목록 조회 (월별)
export async function getSchedules(year, month) {
const data = await fetchAdminApi(`/api/schedules?year=${year}&month=${month}`);
return (data.schedules || []).map(transformSchedule);
} }
// 일정 검색 (Meilisearch) // 일정 검색 (Meilisearch)
export async function searchSchedules(query) { export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) {
return fetchAdminApi( const data = await fetchAdminApi(
`/api/admin/schedules/search?q=${encodeURIComponent(query)}` `/api/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}`
); );
return {
...data,
schedules: (data.schedules || []).map(transformSchedule),
};
} }
// 일정 상세 조회 // 일정 상세 조회

View file

@ -45,6 +45,64 @@ const getMemberList = (schedule) => {
return []; 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 // ID
const CATEGORY_IDS = { const CATEGORY_IDS = {
YOUTUBE: 2, YOUTUBE: 2,
@ -73,11 +131,13 @@ const ScheduleItem = memo(function ScheduleItem({
navigate, navigate,
openDeleteDialog openDeleteDialog
}) { }) {
const scheduleDate = new Date(schedule.date); const scheduleDate = getScheduleDate(schedule);
const isBirthday = schedule.is_birthday || String(schedule.id).startsWith('birthday-'); 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 categoryInfo = getCategoryInfo(schedule, categories);
const categoryName = categories.find(c => c.id === schedule.category_id)?.name || '미분류'; const categoryColor = getColorStyle(categoryInfo.color)?.style?.backgroundColor || categoryInfo.color || '#6b7280';
const memberList = getMemberList(schedule); const memberList = getMemberList(schedule);
const timeStr = getScheduleTime(schedule);
const catId = getCategoryId(schedule);
return ( return (
<motion.div <motion.div
@ -105,15 +165,15 @@ const ScheduleItem = memo(function ScheduleItem({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900">{decodeHtmlEntities(schedule.title)}</h3> <h3 className="font-semibold text-gray-900">{decodeHtmlEntities(schedule.title)}</h3>
<div className="flex items-center gap-3 mt-1 text-sm text-gray-500"> <div className="flex items-center gap-3 mt-1 text-sm text-gray-500">
{schedule.time && ( {timeStr && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Clock size={14} /> <Clock size={14} />
{schedule.time.slice(0, 5)} {timeStr}
</span> </span>
)} )}
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Tag size={14} /> <Tag size={14} />
{categoryName} {categoryInfo.name}
</span> </span>
{schedule.source?.name && ( {schedule.source?.name && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
@ -220,11 +280,7 @@ function AdminSchedule() {
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: ['adminScheduleSearch', searchTerm], queryKey: ['adminScheduleSearch', searchTerm],
queryFn: async ({ pageParam = 0 }) => { queryFn: async ({ pageParam = 0 }) => {
const response = await fetch( return schedulesApi.searchSchedules(searchTerm, { offset: pageParam, limit: SEARCH_LIMIT });
`/api/schedules?search=${encodeURIComponent(searchTerm)}&offset=${pageParam}&limit=${SEARCH_LIMIT}`
);
if (!response.ok) throw new Error('Search failed');
return response.json();
}, },
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
if (lastPage.hasMore) { if (lastPage.hasMore) {
@ -582,7 +638,7 @@ function AdminSchedule() {
if (selectedCategories.length === 0) { if (selectedCategories.length === 0) {
result = [...searchResults]; result = [...searchResults];
} else { } else {
result = searchResults.filter(s => selectedCategories.includes(s.category_id)); result = searchResults.filter(s => selectedCategories.includes(getCategoryId(s)));
} }
} else { } else {
// : // :
@ -622,11 +678,12 @@ function AdminSchedule() {
// //
// //
if (!(isSearchMode && searchTerm) && selectedDate) { if (!(isSearchMode && searchTerm) && selectedDate) {
const scheduleDate = formatDate(s.date); const sDate = getScheduleDate(s);
const scheduleDate = formatDate(sDate);
if (scheduleDate !== selectedDate) return; if (scheduleDate !== selectedDate) return;
} }
const catId = s.category_id; const catId = getCategoryId(s);
counts.set(catId, (counts.get(catId) || 0) + 1); counts.set(catId, (counts.get(catId) || 0) + 1);
total++; total++;
}); });
@ -1274,37 +1331,46 @@ function AdminSchedule() {
transform: `translateY(${virtualItem.start}px)`, transform: `translateY(${virtualItem.start}px)`,
}} }}
> >
{(() => {
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);
return (
<div className="p-5 hover:bg-gray-50 transition-colors group border-b border-gray-100"> <div className="p-5 hover:bg-gray-50 transition-colors group border-b border-gray-100">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-20 text-center flex-shrink-0"> <div className="w-20 text-center flex-shrink-0">
<div className="text-xs text-gray-400 mb-0.5"> <div className="text-xs text-gray-400 mb-0.5">
{new Date(schedule.date).getFullYear()}.{new Date(schedule.date).getMonth() + 1} {scheduleDate.getFullYear()}.{scheduleDate.getMonth() + 1}
</div> </div>
<div className="text-2xl font-bold text-gray-900"> <div className="text-2xl font-bold text-gray-900">
{new Date(schedule.date).getDate()} {scheduleDate.getDate()}
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{['일', '월', '화', '수', '목', '금', '토'][new Date(schedule.date).getDay()]}요일 {['일', '월', '화', '수', '목', '금', '토'][scheduleDate.getDay()]}요일
</div> </div>
</div> </div>
<div <div
className="w-1.5 rounded-full flex-shrink-0 self-stretch" className="w-1.5 rounded-full flex-shrink-0 self-stretch"
style={{ backgroundColor: getColorStyle(categories.find(c => c.id === schedule.category_id)?.color)?.style?.backgroundColor || '#6b7280' }} style={{ backgroundColor: categoryColor }}
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900">{decodeHtmlEntities(schedule.title)}</h3> <h3 className="font-semibold text-gray-900">{decodeHtmlEntities(schedule.title)}</h3>
<div className="flex items-center gap-3 mt-1 text-sm text-gray-500"> <div className="flex items-center gap-3 mt-1 text-sm text-gray-500">
{schedule.time && ( {timeStr && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Clock size={14} /> <Clock size={14} />
{schedule.time.slice(0, 5)} {timeStr}
</span> </span>
)} )}
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Tag size={14} /> <Tag size={14} />
{categories.find(c => c.id === schedule.category_id)?.name || '미분류'} {categoryInfo.name}
</span> </span>
{schedule.source?.name && ( {schedule.source?.name && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
@ -1313,10 +1379,7 @@ function AdminSchedule() {
</span> </span>
)} )}
</div> </div>
{(() => { {memberList.length > 0 && (
const memberList = getMemberList(schedule);
if (memberList.length === 0) return null;
return (
<div className="flex flex-wrap gap-1.5 mt-2"> <div className="flex flex-wrap gap-1.5 mt-2">
{memberList.map((name, i) => ( {memberList.map((name, i) => (
<span key={i} className="px-2 py-0.5 bg-primary/10 text-primary text-xs font-medium rounded-full"> <span key={i} className="px-2 py-0.5 bg-primary/10 text-primary text-xs font-medium rounded-full">
@ -1324,8 +1387,7 @@ function AdminSchedule() {
</span> </span>
))} ))}
</div> </div>
); )}
})()}
</div> </div>
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
@ -1341,7 +1403,7 @@ function AdminSchedule() {
</a> </a>
)} )}
<button <button
onClick={() => navigate(getEditPath(schedule.id, schedule.category_id))} onClick={() => navigate(getEditPath(schedule.id, catId))}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors text-gray-500" className="p-2 hover:bg-gray-200 rounded-lg transition-colors text-gray-500"
> >
<Edit2 size={18} /> <Edit2 size={18} />
@ -1355,7 +1417,8 @@ function AdminSchedule() {
</div> </div>
</div> </div>
</div> </div>
</div> );
})()}</div>
); );
})} })}
</div> </div>