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() {
|
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) {
|
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)
|
// 일정 검색 (Meilisearch)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,23 @@ import { getTodayKST } from "../../utils/date";
|
||||||
|
|
||||||
// 일정 목록 조회 (월별)
|
// 일정 목록 조회 (월별)
|
||||||
export async function getSchedules(year, month) {
|
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}`);
|
return fetchApi(`/api/schedules/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카테고리 목록 조회
|
|
||||||
export async function getCategories() {
|
|
||||||
return fetchApi("/api/schedule-categories");
|
|
||||||
}
|
|
||||||
|
|
||||||
// X 프로필 정보 조회
|
// X 프로필 정보 조회
|
||||||
export async function getXProfile(username) {
|
export async function getXProfile(username) {
|
||||||
return fetchApi(`/api/schedules/x-profile/${encodeURIComponent(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 { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import confetti from 'canvas-confetti';
|
import confetti from 'canvas-confetti';
|
||||||
import { getTodayKST } from '../../../utils/date';
|
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';
|
import useScheduleStore from '../../../stores/useScheduleStore';
|
||||||
|
|
||||||
// 폭죽 애니메이션 함수
|
// 폭죽 애니메이션 함수
|
||||||
|
|
@ -265,18 +265,27 @@ function MobileSchedule() {
|
||||||
const viewYear = selectedDate.getFullYear();
|
const viewYear = selectedDate.getFullYear();
|
||||||
const viewMonth = selectedDate.getMonth() + 1;
|
const viewMonth = selectedDate.getMonth() + 1;
|
||||||
|
|
||||||
// 카테고리 데이터 로드
|
|
||||||
const { data: categories = [] } = useQuery({
|
|
||||||
queryKey: ['scheduleCategories'],
|
|
||||||
queryFn: getCategories,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 월별 일정 데이터 로드
|
// 월별 일정 데이터 로드
|
||||||
const { data: schedules = [], isLoading: loading } = useQuery({
|
const { data: schedules = [], isLoading: loading } = useQuery({
|
||||||
queryKey: ['schedules', viewYear, viewMonth],
|
queryKey: ['schedules', viewYear, viewMonth],
|
||||||
queryFn: () => getSchedules(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 기준)
|
// 달력 표시용 일정 데이터 (calendarViewDate 기준)
|
||||||
const calendarYear = calendarViewDate.getFullYear();
|
const calendarYear = calendarViewDate.getFullYear();
|
||||||
const calendarMonth = calendarViewDate.getMonth() + 1;
|
const calendarMonth = calendarViewDate.getMonth() + 1;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import useAdminAuth from '../../../hooks/useAdminAuth';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import { getTodayKST, formatDate } from '../../../utils/date';
|
import { getTodayKST, formatDate } from '../../../utils/date';
|
||||||
import * as schedulesApi from '../../../api/admin/schedules';
|
import * as schedulesApi from '../../../api/admin/schedules';
|
||||||
import * as categoriesApi from '../../../api/admin/categories';
|
|
||||||
|
|
||||||
// HTML 엔티티 디코딩 함수
|
// HTML 엔티티 디코딩 함수
|
||||||
const decodeHtmlEntities = (text) => {
|
const decodeHtmlEntities = (text) => {
|
||||||
|
|
@ -287,14 +286,27 @@ function AdminSchedule() {
|
||||||
|
|
||||||
const getDaysInMonth = (y, m) => new Date(y, m + 1, 0).getDate();
|
const getDaysInMonth = (y, m) => new Date(y, m + 1, 0).getDate();
|
||||||
|
|
||||||
// 카테고리 목록 (API에서 로드)
|
|
||||||
const [categories, setCategories] = useState([
|
|
||||||
{ id: 'all', name: '전체', color: 'gray' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 일정 목록 (API에서 로드)
|
// 일정 목록 (API에서 로드)
|
||||||
const [schedules, setSchedules] = useState([]);
|
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 = {
|
const colorMap = {
|
||||||
blue: 'bg-blue-500',
|
blue: 'bg-blue-500',
|
||||||
|
|
@ -367,9 +379,6 @@ function AdminSchedule() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
|
|
||||||
// 카테고리 로드
|
|
||||||
fetchCategories();
|
|
||||||
|
|
||||||
// sessionStorage에서 토스트 메시지 확인 (일정 추가/수정 완료 시)
|
// sessionStorage에서 토스트 메시지 확인 (일정 추가/수정 완료 시)
|
||||||
const savedToast = sessionStorage.getItem('scheduleToast');
|
const savedToast = sessionStorage.getItem('scheduleToast');
|
||||||
if (savedToast) {
|
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 () => {
|
const fetchSchedules = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
||||||
|
|
@ -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, getCategories, searchSchedules as searchSchedulesApi } from '../../../api/public/schedules';
|
import { getSchedules } from '../../../api/public/schedules';
|
||||||
import useScheduleStore from '../../../stores/useScheduleStore';
|
import useScheduleStore from '../../../stores/useScheduleStore';
|
||||||
|
|
||||||
// HTML 엔티티 디코딩 함수
|
// HTML 엔티티 디코딩 함수
|
||||||
|
|
@ -158,12 +158,6 @@ function Schedule() {
|
||||||
const [slideDirection, setSlideDirection] = useState(0);
|
const [slideDirection, setSlideDirection] = useState(0);
|
||||||
const pickerRef = useRef(null);
|
const pickerRef = useRef(null);
|
||||||
|
|
||||||
// 카테고리 데이터 로드 (useQuery)
|
|
||||||
const { data: categories = [] } = useQuery({
|
|
||||||
queryKey: ['scheduleCategories'],
|
|
||||||
queryFn: getCategories,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 월별 일정 데이터 로드 (useQuery)
|
// 월별 일정 데이터 로드 (useQuery)
|
||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = currentDate.getMonth();
|
const month = currentDate.getMonth();
|
||||||
|
|
@ -172,6 +166,21 @@ function Schedule() {
|
||||||
queryFn: () => getSchedules(year, month + 1),
|
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(() => {
|
useEffect(() => {
|
||||||
if (loading || schedules.length === 0) return;
|
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 dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
const schedule = scheduleDateMap.get(dateStr);
|
const schedule = scheduleDateMap.get(dateStr);
|
||||||
if (!schedule) return null;
|
if (!schedule) return null;
|
||||||
|
// schedule에서 직접 색상 가져오기
|
||||||
|
if (schedule.category_color) return schedule.category_color;
|
||||||
const cat = categories.find(c => c.id === schedule.category_id);
|
const cat = categories.find(c => c.id === schedule.category_id);
|
||||||
return cat?.color || '#4A7C59';
|
return cat?.color || '#4A7C59';
|
||||||
};
|
};
|
||||||
|
|
@ -420,11 +431,11 @@ function Schedule() {
|
||||||
|
|
||||||
// 카테고리 토글
|
// 카테고리 토글
|
||||||
const toggleCategory = (categoryId) => {
|
const toggleCategory = (categoryId) => {
|
||||||
setSelectedCategories(prev =>
|
if (selectedCategories.includes(categoryId)) {
|
||||||
prev.includes(categoryId)
|
setSelectedCategories(selectedCategories.filter(id => id !== categoryId));
|
||||||
? prev.filter(id => id !== categoryId)
|
} else {
|
||||||
: [...prev, categoryId]
|
setSelectedCategories([...selectedCategories, categoryId]);
|
||||||
);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필터링된 스케줄 (useMemo로 성능 최적화, 시간순 정렬)
|
// 필터링된 스케줄 (useMemo로 성능 최적화, 시간순 정렬)
|
||||||
|
|
@ -485,8 +496,10 @@ function Schedule() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const catId = s.category_id;
|
const catId = s.category_id;
|
||||||
counts.set(catId, (counts.get(catId) || 0) + 1);
|
if (catId) {
|
||||||
total++;
|
counts.set(catId, (counts.get(catId) || 0) + 1);
|
||||||
|
total++;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
counts.set('total', total);
|
counts.set('total', total);
|
||||||
|
|
@ -553,16 +566,18 @@ function Schedule() {
|
||||||
return `${names.slice(0, 2).join(', ')} 외 ${names.length - 2}개`;
|
return `${names.slice(0, 2).join(', ')} 외 ${names.length - 2}개`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 카테고리 색상 가져오기
|
// 카테고리 색상 가져오기 (schedule 객체에서 직접 가져오거나 categories에서 조회)
|
||||||
const getCategoryColor = (categoryId) => {
|
const getCategoryColor = useCallback((categoryId, schedule = null) => {
|
||||||
|
if (schedule?.category_color) return schedule.category_color;
|
||||||
const cat = categories.find(c => c.id === categoryId);
|
const cat = categories.find(c => c.id === categoryId);
|
||||||
return cat?.color || '#808080';
|
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);
|
const cat = categories.find(c => c.id === categoryId);
|
||||||
return cat?.name || '';
|
return cat?.name || '';
|
||||||
};
|
}, [categories]);
|
||||||
|
|
||||||
// 정렬된 카테고리 목록 (메모이제이션으로 깜빡임 방지)
|
// 정렬된 카테고리 목록 (메모이제이션으로 깜빡임 방지)
|
||||||
const sortedCategories = useMemo(() => {
|
const sortedCategories = useMemo(() => {
|
||||||
|
|
@ -789,7 +804,7 @@ function Schedule() {
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
className="w-1 h-1 rounded-full"
|
className="w-1 h-1 rounded-full"
|
||||||
style={{ backgroundColor: getCategoryColor(schedule.category_id) }}
|
style={{ backgroundColor: getCategoryColor(schedule.category_id, schedule) }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1126,8 +1141,8 @@ function Schedule() {
|
||||||
if (!schedule) return null;
|
if (!schedule) return null;
|
||||||
|
|
||||||
const formatted = formatDate(schedule.date);
|
const formatted = formatDate(schedule.date);
|
||||||
const categoryColor = getCategoryColor(schedule.category_id);
|
const categoryColor = getCategoryColor(schedule.category_id, schedule);
|
||||||
const categoryName = getCategoryName(schedule.category_id);
|
const categoryName = getCategoryName(schedule.category_id, schedule);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -1235,8 +1250,8 @@ function Schedule() {
|
||||||
/* 일반 모드: 기존 렌더링 */
|
/* 일반 모드: 기존 렌더링 */
|
||||||
filteredSchedules.map((schedule, index) => {
|
filteredSchedules.map((schedule, index) => {
|
||||||
const formatted = formatDate(schedule.date);
|
const formatted = formatDate(schedule.date);
|
||||||
const categoryColor = getCategoryColor(schedule.category_id);
|
const categoryColor = getCategoryColor(schedule.category_id, schedule);
|
||||||
const categoryName = getCategoryName(schedule.category_id);
|
const categoryName = getCategoryName(schedule.category_id, schedule);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue