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:
parent
55096c8e43
commit
3922d5c6f7
2 changed files with 200 additions and 114 deletions
|
|
@ -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),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일정 상세 조회
|
// 일정 상세 조회
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue