refactor: 일정 API 응답 형식에 맞게 프론트엔드 수정
- API 응답(날짜별 그룹화)을 플랫 배열로 변환하는 로직 추가 - 별도 카테고리 API 호출 제거, 일정 데이터에서 카테고리 추출 - PC/모바일 Schedule.jsx, AdminSchedule.jsx 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a5ed04e9af
commit
b89255780e
6 changed files with 113 additions and 66 deletions
|
|
@ -5,7 +5,7 @@ import { fetchAdminApi } from "../index";
|
|||
|
||||
// 카테고리 목록 조회
|
||||
export async function getCategories() {
|
||||
return fetchAdminApi("/api/admin/schedule-categories");
|
||||
return fetchAdminApi("/api/schedules/categories");
|
||||
}
|
||||
|
||||
// 카테고리 생성
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)}`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue