From 9937ed33daf9304f6bf72e691fdf00a5fa6b0a83 Mon Sep 17 00:00:00 2001 From: yerimi00 Date: Tue, 9 Sep 2025 14:30:16 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(calendar)=20:=20=EC=9B=94=EB=B3=84=20?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20api=20=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/event.ts | 187 ++++++++++++++++++++- src/apis/event.type.ts | 76 +++++++++ src/app/calendar/components/Calendar.tsx | 36 ++-- src/app/calendar/components/EventCheck.tsx | 34 ++-- src/app/calendar/page.tsx | 25 ++- src/components/ui/calendar.tsx | 24 +-- 6 files changed, 337 insertions(+), 45 deletions(-) diff --git a/src/apis/event.ts b/src/apis/event.ts index a65248d..4b91f89 100644 --- a/src/apis/event.ts +++ b/src/apis/event.ts @@ -1,4 +1,16 @@ -import { CategoryProps, CheckListType, LeftEventCount, MonthlyChecklistType } from "./event.type"; +import { + CategoryProps, + CheckListType, + LeftEventCount, + MonthlyChecklistType, + CalendarEvent, + CalendarResponseDTO, + TempEvent, + CreateEventRequest, + UpdateEventRequest, + FixEventRequest, + CheckStatusUpdate +} from "./event.type"; import instance from "./instance"; export const getMajorEventChecklist = async (params?: { year?: number; category?: string }): Promise => { @@ -45,4 +57,175 @@ export const getCountLeftEvent = async(): Promise=>{ console.error("남은 과행사 개수 조회 실패", error); throw error; } -} \ No newline at end of file +} + +// ========== Calendar 관련 API 함수들 ========== + +// 월별 달력 조회 +export const getCalendarEvents = async (year: number, month: number): Promise => { + try { + const response = await instance.get("/api/v1/major-event/calendar", { + params: { year, month } + }); + console.log("월별 달력 조회 성공", response); + return response.data; + } catch (error) { + console.error("월별 달력 조회 실패", error); + throw error; + } +}; + +// 임시 행사 저장 +export const saveTempEvent = async (eventData: CreateEventRequest): Promise => { + try { + const formData = new FormData(); + formData.append("eventName", eventData.eventName); + formData.append("category", eventData.category); + formData.append("location", eventData.location); + formData.append("notice", eventData.notice); + formData.append("googleFormLink", eventData.googleFormLink); + formData.append("startDate", eventData.startDate); + formData.append("endDate", eventData.endDate); + formData.append("time", eventData.time); + + // 이미지 파일들 추가 + eventData.cardNewsImages.forEach((image, index) => { + formData.append("cardNewsImages", image); + }); + + const response = await instance.post("/api/v1/major-event/temp", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + console.log("임시 행사 저장 성공", response); + return response.data; + } catch (error) { + console.error("임시 행사 저장 실패", error); + throw error; + } +}; + +// 임시 행사 수정 +export const updateTempEvent = async (tempEventId: number, eventData: UpdateEventRequest): Promise => { + try { + const formData = new FormData(); + formData.append("eventName", eventData.eventName); + formData.append("category", eventData.category); + formData.append("location", eventData.location); + formData.append("notice", eventData.notice); + formData.append("googleFormLink", eventData.googleFormLink); + formData.append("startDate", eventData.startDate); + formData.append("endDate", eventData.endDate); + formData.append("time", eventData.time); + formData.append("majorEventId", eventData.majorEventId.toString()); + + // 기존 이미지 URL들 추가 + eventData.existingImageUrls.forEach((url, index) => { + formData.append("existingImageUrls", url); + }); + + // 새 이미지 파일들 추가 + eventData.newImages.forEach((image, index) => { + formData.append("newImages", image); + }); + + const response = await instance.patch(`/api/v1/major-event/temp/${tempEventId}`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + console.log("임시 행사 수정 성공", response); + return response.data; + } catch (error) { + console.error("임시 행사 수정 실패", error); + throw error; + } +}; + +// 임시 행사 삭제 +export const deleteTempEvent = async (tempEventId: number): Promise => { + try { + const response = await instance.delete(`/api/v1/major-event/temp/${tempEventId}`); + console.log("임시 행사 삭제 성공", response); + } catch (error) { + console.error("임시 행사 삭제 실패", error); + throw error; + } +}; + +// 임시 행사 최종 제출 (확정) +export const submitAllTempEvents = async (tempEventIds: number[]): Promise => { + try { + const response = await instance.post("/api/v1/major-event/fix", { + tempEventIds + }); + console.log("임시 행사 최종 제출 성공", response); + return response.data; + } catch (error) { + console.error("임시 행사 최종 제출 실패", error); + throw error; + } +}; + +// 확정된 행사 수정 +export const updateMajorEvent = async (majorEventId: number, eventData: UpdateEventRequest): Promise => { + try { + const formData = new FormData(); + formData.append("eventName", eventData.eventName); + formData.append("category", eventData.category); + formData.append("location", eventData.location); + formData.append("notice", eventData.notice); + formData.append("googleFormLink", eventData.googleFormLink); + formData.append("startDate", eventData.startDate); + formData.append("endDate", eventData.endDate); + formData.append("time", eventData.time); + formData.append("majorEventId", eventData.majorEventId.toString()); + + // 기존 이미지 URL들 추가 + eventData.existingImageUrls.forEach((url, index) => { + formData.append("existingImageUrls", url); + }); + + // 새 이미지 파일들 추가 + eventData.newImages.forEach((image, index) => { + formData.append("newImages", image); + }); + + const response = await instance.put(`/api/v1/major-event/${majorEventId}`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + console.log("확정된 행사 수정 성공", response); + return response.data; + } catch (error) { + console.error("확정된 행사 수정 실패", error); + throw error; + } +}; + +// 확정된 행사 삭제 +export const deleteMajorEvent = async (majorEventId: number): Promise => { + try { + const response = await instance.delete(`/api/v1/major-event/${majorEventId}`); + console.log("확정된 행사 삭제 성공", response); + } catch (error) { + console.error("확정된 행사 삭제 실패", error); + throw error; + } +}; + +// 체크리스트 항목 상태 변경 +export const updateChecklistItemStatus = async (itemId: number, isChecked: boolean): Promise => { + try { + const response = await instance.put(`/api/v1/major-event/checklists/${itemId}`, { + isChecked + }); + console.log("체크리스트 항목 상태 변경 성공", response); + return response.data; + } catch (error) { + console.error("체크리스트 항목 상태 변경 실패", error); + throw error; + } +}; \ No newline at end of file diff --git a/src/apis/event.type.ts b/src/apis/event.type.ts index 454512e..f7759bb 100644 --- a/src/apis/event.type.ts +++ b/src/apis/event.type.ts @@ -47,4 +47,80 @@ export interface MonthlyChecklistType { // 할일별 export interface LeftEventCount{ count: number; // 남은 과행사 개수 +} + +// Calendar 관련 타입 정의 +export interface CalendarEvent { + id: number; + eventName: string; + startDate: string; + endDate: string; + eventStatus: "TEMPORARY" | "FIXED"; +} + +// API 스키마에 맞는 CalendarResponseDTO 타입 정의 +export interface CalendarResponseDTO { + id: number; + eventName: string; + startDate: string; + endDate: string; + eventStatus: "TEMPORARY" | "FIXED"; + category: string; + location: string; + notice: string; + googleFormLink: string; + time: string; + cardNewsImageUrls: string[]; +} + +// 임시 행사 관련 타입 정의 +export interface TempEvent { + tempEventId: number; + eventName: string; + category: string; + startDate: string; + endDate: string; + time: string; + location: string; + notice: string; + googleFormLink: string; + cardNewsImageUrls: string[]; +} + +// 행사 생성 요청 타입 +export interface CreateEventRequest { + eventName: string; + category: string; + location: string; + notice: string; + googleFormLink: string; + startDate: string; + endDate: string; + time: string; + cardNewsImages: File[]; +} + +// 행사 수정 요청 타입 +export interface UpdateEventRequest { + eventName: string; + category: string; + location: string; + notice: string; + googleFormLink: string; + startDate: string; + endDate: string; + time: string; + majorEventId: number; + existingImageUrls: string[]; + newImages: File[]; +} + +// 행사 확정 요청 타입 +export interface FixEventRequest { + tempEventIds: number[]; +} + +// 체크리스트 상태 업데이트 타입 +export interface CheckStatusUpdate { + isChecked: boolean; } \ No newline at end of file diff --git a/src/app/calendar/components/Calendar.tsx b/src/app/calendar/components/Calendar.tsx index 83cc137..6c14147 100644 --- a/src/app/calendar/components/Calendar.tsx +++ b/src/app/calendar/components/Calendar.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from "react"; import { Calendar } from "@/components/ui/calendar"; -import { getMajorEvent, getAnotherMajorEvent } from "@/mock/calendar/api"; +import { getCalendarEvents } from "@/apis/event"; +import { CalendarResponseDTO } from "@/apis/event.type"; import { majorEventItem } from "@/mock/calendar/api"; interface CalendarEvent { @@ -17,45 +18,44 @@ interface CalendarComponentProps { onDateSelect?: (date: Date | undefined) => void; selectedEvents?: majorEventItem[]; showOnlySelected?: boolean; + onMonthChange?: (month: Date) => void; } export default function CalendarComponent({ onDateSelect, selectedEvents = [], showOnlySelected = false, + onMonthChange, }: CalendarComponentProps) { const [events, setEvents] = useState([]); const [selectedDate, setSelectedDate] = useState(undefined); + const [currentDate, setCurrentDate] = useState(new Date()); // 데이터 불러오기 useEffect(() => { const fetchEvents = async () => { try { - const [majorEvents, anotherEvents] = await Promise.all([ - getMajorEvent(), - getAnotherMajorEvent(), - ]); + const year = currentDate.getFullYear(); + const month = currentDate.getMonth() + 1; // getMonth()는 0부터 시작하므로 +1 + + const calendarEvents = await getCalendarEvents(year, month); - const parseEvent = (item: majorEventItem): CalendarEvent => ({ + const parseEvent = (item: CalendarResponseDTO): CalendarEvent => ({ id: item.id, title: item.eventName, - date: item.date, + date: item.startDate, location: item.location, - buttonText: "신청하기", + buttonText: item.eventStatus === "FIXED" ? "확정" : "임시", }); - const allEvents: CalendarEvent[] = [ - ...(majorEvents || []).map(parseEvent), - ...(anotherEvents || []).map(parseEvent), - ]; - + const allEvents: CalendarEvent[] = calendarEvents.map(parseEvent); setEvents(allEvents); } catch (e) { console.error("행사 데이터 불러오기 실패", e); } }; fetchEvents(); - }, []); + }, [currentDate]); // 표시할 이벤트 결정 (선택된 이벤트만 표시하거나 모든 이벤트 표시) const displayEvents = showOnlySelected @@ -79,6 +79,11 @@ export default function CalendarComponent({ onDateSelect?.(date); }; + const handleMonthChange = (newMonth: Date) => { + setCurrentDate(newMonth); + onMonthChange?.(newMonth); + }; + return (
@@ -86,7 +91,8 @@ export default function CalendarComponent({ mode="single" selected={selectedDate} onSelect={handleDateSelect} - month={new Date()} + onMonthChange={handleMonthChange} + month={currentDate} modifiers={{ event: isEventDay, }} diff --git a/src/app/calendar/components/EventCheck.tsx b/src/app/calendar/components/EventCheck.tsx index 2ac02fb..c96dd5e 100644 --- a/src/app/calendar/components/EventCheck.tsx +++ b/src/app/calendar/components/EventCheck.tsx @@ -1,11 +1,9 @@ "use client"; import { useEffect, useState, useRef } from "react"; -import { - getMajorEvent, - getAnotherMajorEvent, - majorEventItem, -} from "@/mock/calendar/api"; +import { getCalendarEvents } from "@/apis/event"; +import { CalendarResponseDTO } from "@/apis/event.type"; +import { majorEventItem } from "@/mock/calendar/api"; import { FaCirclePlus } from "react-icons/fa6"; import { useRouter } from "next/navigation"; @@ -13,12 +11,14 @@ interface EventCheckProps { selectedDate?: Date; onSelectedEventsChange?: (events: majorEventItem[]) => void; showOnlySelected?: boolean; + currentMonth?: Date; } export default function EventCheck({ selectedDate, onSelectedEventsChange, showOnlySelected = false, + currentMonth = new Date(), }: EventCheckProps) { const router = useRouter(); const [events, setEvents] = useState([]); @@ -39,19 +39,27 @@ export default function EventCheck({ useEffect(() => { const fetchEvents = async () => { try { - const [majorEvents, anotherEvents] = await Promise.all([ - getMajorEvent(), - getAnotherMajorEvent(), - ]); - - const allEvents = [...(majorEvents || []), ...(anotherEvents || [])]; - setEvents(allEvents); + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth() + 1; // getMonth()는 0부터 시작하므로 +1 + + const calendarEvents = await getCalendarEvents(year, month); + + // API 데이터를 majorEventItem 형태로 변환 + const convertedEvents: majorEventItem[] = calendarEvents.map((event: CalendarResponseDTO) => ({ + id: event.id, + eventName: event.eventName, + date: event.startDate, + location: event.location, + description: event.eventStatus === "FIXED" ? "확정된 행사" : "임시 행사", + })); + + setEvents(convertedEvents); } catch (e) { console.error("행사 데이터 불러오기 실패", e); } }; fetchEvents(); - }, []); + }, [currentMonth]); // 선택된 날짜에 해당하는 이벤트 필터링 useEffect(() => { diff --git a/src/app/calendar/page.tsx b/src/app/calendar/page.tsx index a870f41..4516220 100644 --- a/src/app/calendar/page.tsx +++ b/src/app/calendar/page.tsx @@ -1,26 +1,37 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import TitleAndDescription from "@/components/TitleAndDescription"; import CalendarComponent from "./components/Calendar"; import EventCheck from "./components/EventCheck"; import Notification from "@/components/ui/notification"; import { majorEventItem } from "@/mock/calendar/api"; +import { submitAllTempEvents } from "@/apis/event"; export default function Calendar() { const [selectedDate, setSelectedDate] = useState(undefined); const [selectedEvents, setSelectedEvents] = useState([]); const [showOnlySelected, setShowOnlySelected] = useState(false); const [showNotification, setShowNotification] = useState(false); + const [currentMonth, setCurrentMonth] = useState(new Date()); const handleSelectedEventsChange = (events: majorEventItem[]) => { setSelectedEvents(events); }; - const handleFixButtonClick = () => { + const handleFixButtonClick = async () => { if (selectedEvents.length > 0) { - setShowOnlySelected(true); - setShowNotification(true); + try { + // 선택된 이벤트들의 ID를 추출하여 API 호출 + const tempEventIds = selectedEvents.map(event => event.id); + await submitAllTempEvents(tempEventIds); + + setShowOnlySelected(true); + setShowNotification(true); + } catch (error) { + console.error("행사 확정 실패:", error); + // 에러 처리 로직 추가 가능 + } } }; @@ -28,6 +39,10 @@ export default function Calendar() { setShowOnlySelected(false); }; + const handleMonthChange = (month: Date) => { + setCurrentMonth(month); + }; + return (
@@ -46,6 +61,7 @@ export default function Calendar() { onDateSelect={setSelectedDate} selectedEvents={selectedEvents} showOnlySelected={showOnlySelected} + onMonthChange={handleMonthChange} />
@@ -55,6 +71,7 @@ export default function Calendar() { selectedDate={selectedDate} onSelectedEventsChange={handleSelectedEventsChange} showOnlySelected={showOnlySelected} + currentMonth={currentMonth} />
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 2bb55f8..966fdf6 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -7,6 +7,7 @@ interface CalendarProps { mode?: "single"; selected?: Date | undefined; onSelect?: (date: Date | undefined) => void; + onMonthChange?: (date: Date) => void; month?: Date; modifiers?: { event?: (date: Date) => boolean; @@ -21,6 +22,7 @@ export function Calendar({ mode = "single", selected, onSelect, + onMonthChange, month = new Date(), modifiers, modifiersClassNames, @@ -96,11 +98,11 @@ export function Calendar({ >
- + cat.sort === selectedCategory)?.item : undefined} + /> ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 72f933e..4a83c4f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,7 +15,10 @@ export default function RootLayout({ }>) { return ( - +
{children}