refactor: 일정 API 응답 형식에 맞게 프론트엔드 수정

- API 응답(날짜별 그룹화)을 플랫 배열로 변환하는 로직 추가
- 별도 카테고리 API 호출 제거, 일정 데이터에서 카테고리 추출
- PC/모바일 Schedule.jsx, AdminSchedule.jsx 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-17 17:52:15 +09:00
parent a5ed04e9af
commit b89255780e
6 changed files with 113 additions and 66 deletions

View file

@ -5,7 +5,7 @@ import { fetchAdminApi } from "../index";
// 카테고리 목록 조회
export async function getCategories() {
return fetchAdminApi("/api/admin/schedule-categories");
return fetchAdminApi("/api/schedules/categories");
}
// 카테고리 생성

View file

@ -5,7 +5,23 @@ import { fetchAdminApi, fetchAdminFormData } from "../index";
// 일정 목록 조회 (월별)
export async function getSchedules(year, month) {
return fetchAdminApi(`/api/admin/schedules?year=${year}&month=${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;
}
// 일정 검색 (Meilisearch)

View file

@ -6,7 +6,23 @@ import { getTodayKST } from "../../utils/date";
// 일정 목록 조회 (월별)
export async function getSchedules(year, month) {
return fetchApi(`/api/schedules?year=${year}&month=${month}`);
const data = await fetchApi(`/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;
}
// 다가오는 일정 조회 (오늘 이후)
@ -29,11 +45,6 @@ export async function getSchedule(id) {
return fetchApi(`/api/schedules/${id}`);
}
// 카테고리 목록 조회
export async function getCategories() {
return fetchApi("/api/schedule-categories");
}
// X 프로필 정보 조회
export async function getXProfile(username) {
return fetchApi(`/api/schedules/x-profile/${encodeURIComponent(username)}`);

View file

@ -7,7 +7,7 @@ import { useInView } from 'react-intersection-observer';
import { useVirtualizer } from '@tanstack/react-virtual';
import confetti from 'canvas-confetti';
import { getTodayKST } from '../../../utils/date';
import { getSchedules, getCategories, searchSchedules } from '../../../api/public/schedules';
import { getSchedules, searchSchedules } from '../../../api/public/schedules';
import useScheduleStore from '../../../stores/useScheduleStore';
//
@ -265,18 +265,27 @@ function MobileSchedule() {
const viewYear = selectedDate.getFullYear();
const viewMonth = selectedDate.getMonth() + 1;
//
const { data: categories = [] } = useQuery({
queryKey: ['scheduleCategories'],
queryFn: getCategories,
});
//
const { data: schedules = [], isLoading: loading } = useQuery({
queryKey: ['schedules', viewYear, viewMonth],
queryFn: () => getSchedules(viewYear, viewMonth),
});
//
const categories = useMemo(() => {
const categoryMap = new Map();
schedules.forEach(s => {
if (s.category_id && !categoryMap.has(s.category_id)) {
categoryMap.set(s.category_id, {
id: s.category_id,
name: s.category_name,
color: s.category_color,
});
}
});
return Array.from(categoryMap.values());
}, [schedules]);
// (calendarViewDate )
const calendarYear = calendarViewDate.getFullYear();
const calendarMonth = calendarViewDate.getMonth() + 1;

View file

@ -18,7 +18,6 @@ import useAdminAuth from '../../../hooks/useAdminAuth';
import useToast from '../../../hooks/useToast';
import { getTodayKST, formatDate } from '../../../utils/date';
import * as schedulesApi from '../../../api/admin/schedules';
import * as categoriesApi from '../../../api/admin/categories';
// HTML
const decodeHtmlEntities = (text) => {
@ -287,14 +286,27 @@ function AdminSchedule() {
const getDaysInMonth = (y, m) => new Date(y, m + 1, 0).getDate();
// (API )
const [categories, setCategories] = useState([
{ id: 'all', name: '전체', color: 'gray' }
]);
// (API )
const [schedules, setSchedules] = useState([]);
//
const categories = useMemo(() => {
const categoryMap = new Map();
schedules.forEach(s => {
if (s.category_id && !categoryMap.has(s.category_id)) {
categoryMap.set(s.category_id, {
id: s.category_id,
name: s.category_name,
color: s.category_color,
});
}
});
return [
{ id: 'all', name: '전체', color: 'gray' },
...Array.from(categoryMap.values())
];
}, [schedules]);
//
const colorMap = {
blue: 'bg-blue-500',
@ -367,9 +379,6 @@ function AdminSchedule() {
useEffect(() => {
if (!isAuthenticated) return;
//
fetchCategories();
// sessionStorage ( / )
const savedToast = sessionStorage.getItem('scheduleToast');
if (savedToast) {
@ -404,19 +413,6 @@ function AdminSchedule() {
};
//
const fetchCategories = async () => {
try {
const data = await categoriesApi.getCategories();
setCategories([
{ id: 'all', name: '전체', color: 'gray' },
...data
]);
} catch (error) {
console.error('카테고리 로드 오류:', error);
}
};
//
const fetchSchedules = async () => {
setLoading(true);

View file

@ -7,7 +7,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import { useInView } from 'react-intersection-observer';
import confetti from 'canvas-confetti';
import { getTodayKST } from '../../../utils/date';
import { getSchedules, getCategories, searchSchedules as searchSchedulesApi } from '../../../api/public/schedules';
import { getSchedules } from '../../../api/public/schedules';
import useScheduleStore from '../../../stores/useScheduleStore';
// HTML
@ -158,12 +158,6 @@ function Schedule() {
const [slideDirection, setSlideDirection] = useState(0);
const pickerRef = useRef(null);
// (useQuery)
const { data: categories = [] } = useQuery({
queryKey: ['scheduleCategories'],
queryFn: getCategories,
});
// (useQuery)
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
@ -172,6 +166,21 @@ function Schedule() {
queryFn: () => getSchedules(year, month + 1),
});
//
const categories = useMemo(() => {
const categoryMap = new Map();
schedules.forEach(s => {
if (s.category_id && !categoryMap.has(s.category_id)) {
categoryMap.set(s.category_id, {
id: s.category_id,
name: s.category_name,
color: s.category_color,
});
}
});
return Array.from(categoryMap.values());
}, [schedules]);
// ( )
useEffect(() => {
if (loading || schedules.length === 0) return;
@ -354,6 +363,8 @@ function Schedule() {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const schedule = scheduleDateMap.get(dateStr);
if (!schedule) return null;
// schedule
if (schedule.category_color) return schedule.category_color;
const cat = categories.find(c => c.id === schedule.category_id);
return cat?.color || '#4A7C59';
};
@ -420,11 +431,11 @@ function Schedule() {
//
const toggleCategory = (categoryId) => {
setSelectedCategories(prev =>
prev.includes(categoryId)
? prev.filter(id => id !== categoryId)
: [...prev, categoryId]
);
if (selectedCategories.includes(categoryId)) {
setSelectedCategories(selectedCategories.filter(id => id !== categoryId));
} else {
setSelectedCategories([...selectedCategories, categoryId]);
}
};
// (useMemo , )
@ -485,8 +496,10 @@ function Schedule() {
}
const catId = s.category_id;
counts.set(catId, (counts.get(catId) || 0) + 1);
total++;
if (catId) {
counts.set(catId, (counts.get(catId) || 0) + 1);
total++;
}
});
counts.set('total', total);
@ -553,16 +566,18 @@ function Schedule() {
return `${names.slice(0, 2).join(', ')}${names.length - 2}`;
};
//
const getCategoryColor = (categoryId) => {
// (schedule categories )
const getCategoryColor = useCallback((categoryId, schedule = null) => {
if (schedule?.category_color) return schedule.category_color;
const cat = categories.find(c => c.id === categoryId);
return cat?.color || '#808080';
};
}, [categories]);
const getCategoryName = (categoryId) => {
const getCategoryName = useCallback((categoryId, schedule = null) => {
if (schedule?.category_name) return schedule.category_name;
const cat = categories.find(c => c.id === categoryId);
return cat?.name || '';
};
}, [categories]);
// ( )
const sortedCategories = useMemo(() => {
@ -789,7 +804,7 @@ function Schedule() {
<span
key={idx}
className="w-1 h-1 rounded-full"
style={{ backgroundColor: getCategoryColor(schedule.category_id) }}
style={{ backgroundColor: getCategoryColor(schedule.category_id, schedule) }}
/>
))}
</span>
@ -1126,8 +1141,8 @@ function Schedule() {
if (!schedule) return null;
const formatted = formatDate(schedule.date);
const categoryColor = getCategoryColor(schedule.category_id);
const categoryName = getCategoryName(schedule.category_id);
const categoryColor = getCategoryColor(schedule.category_id, schedule);
const categoryName = getCategoryName(schedule.category_id, schedule);
return (
<div
@ -1235,8 +1250,8 @@ function Schedule() {
/* 일반 모드: 기존 렌더링 */
filteredSchedules.map((schedule, index) => {
const formatted = formatDate(schedule.date);
const categoryColor = getCategoryColor(schedule.category_id);
const categoryName = getCategoryName(schedule.category_id);
const categoryColor = getCategoryColor(schedule.category_id, schedule);
const categoryName = getCategoryName(schedule.category_id, schedule);
return (
<motion.div