refactor(frontend): API 레이어에 데이터 변환 로직 통합

- schedules.js에 transformSchedule 함수 추가
  - datetime → date, time 분리
  - category 객체 → category_id, category_name, category_color 플랫화
  - members 배열 → member_names 문자열 변환
- PC Schedule.jsx의 검색 로직을 searchSchedules 함수 사용으로 변경
- 중복 변환 로직 제거로 코드 간소화

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 23:33:32 +09:00
parent e136f3c74b
commit 55096c8e43
2 changed files with 47 additions and 49 deletions

View file

@ -4,40 +4,62 @@
import { fetchApi } from "../index"; import { fetchApi } from "../index";
import { getTodayKST } from "../../utils/date"; import { getTodayKST } from "../../utils/date";
// 일정 목록 조회 (월별) /**
export async function getSchedules(year, month) { * API 응답을 프론트엔드 형식으로 변환
const data = await fetchApi(`/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 fetchApi(`/api/schedules?year=${year}&month=${month}`);
return (data.schedules || []).map(transformSchedule);
} }
// 다가오는 일정 조회 (오늘 이후) // 다가오는 일정 조회 (오늘 이후)
export async function getUpcomingSchedules(limit = 3) { export async function getUpcomingSchedules(limit = 3) {
const todayStr = getTodayKST(); const todayStr = getTodayKST();
return fetchApi(`/api/schedules?startDate=${todayStr}&limit=${limit}`); const data = await fetchApi(`/api/schedules?startDate=${todayStr}&limit=${limit}`);
return (data.schedules || []).map(transformSchedule);
} }
// 일정 검색 (Meilisearch) // 일정 검색 (Meilisearch)
export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) { export async function searchSchedules(query, { offset = 0, limit = 20 } = {}) {
return fetchApi( const data = await fetchApi(
`/api/schedules?search=${encodeURIComponent( `/api/schedules?search=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}`
query
)}&offset=${offset}&limit=${limit}`
); );
return {
...data,
schedules: (data.schedules || []).map(transformSchedule),
};
} }
// 일정 상세 조회 // 일정 상세 조회

View file

@ -7,7 +7,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import confetti from 'canvas-confetti'; import confetti from 'canvas-confetti';
import { getTodayKST } from '../../../utils/date'; import { getTodayKST } from '../../../utils/date';
import { getSchedules } from '../../../api/public/schedules'; import { getSchedules, searchSchedules } from '../../../api/public/schedules';
import useScheduleStore from '../../../stores/useScheduleStore'; import useScheduleStore from '../../../stores/useScheduleStore';
// HTML // HTML
@ -257,11 +257,7 @@ function Schedule() {
} = useInfiniteQuery({ } = useInfiniteQuery({
queryKey: ['scheduleSearch', searchTerm], queryKey: ['scheduleSearch', searchTerm],
queryFn: async ({ pageParam = 0 }) => { queryFn: async ({ pageParam = 0 }) => {
const response = await fetch( return 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) {
@ -272,30 +268,10 @@ function Schedule() {
enabled: !!searchTerm && isSearchMode, enabled: !!searchTerm && isSearchMode,
}); });
// Flatten search results and normalize format to match monthly data // Flatten search results (already transformed by API layer)
const searchResults = useMemo(() => { const searchResults = useMemo(() => {
if (!searchData?.pages) return []; if (!searchData?.pages) return [];
return searchData.pages.flatMap(page => return searchData.pages.flatMap(page => page.schedules);
page.schedules.map(s => {
// datetime date + time
const dateTime = s.datetime || '';
const [date, time] = dateTime.includes('T')
? dateTime.split('T')
: [dateTime, null];
return {
...s,
date,
time,
// category flat
category_id: s.category?.id,
category_name: s.category?.name,
category_color: s.category?.color,
// members ( )
member_names: Array.isArray(s.members) ? s.members.map(m => m.name).join(',') : s.member_names,
};
})
);
}, [searchData]); }, [searchData]);
const searchTotal = searchData?.pages?.[0]?.total || 0; const searchTotal = searchData?.pages?.[0]?.total || 0;