diff --git a/bun.lock b/bun.lock index 64ad7b7..9e7990e 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "@tanstack/react-query-devtools": "^5.71.0", "next": "15.2.4", "react": "^19.0.0", + "react-calendar": "^5.1.0", "react-dom": "^19.0.0", }, "devDependencies": { @@ -276,6 +277,8 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-GraLbYqOJcmW1qY3osB+2YIiD62nVf2/bVLHZmrb4t/YSUwE03l7TwcDJl08T/Tm3SVhepX8RQkpzWbag/Sb4w=="], + "@wojtekmaj/date-utils": ["@wojtekmaj/date-utils@1.5.1", "", {}, "sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww=="], + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -350,6 +353,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -506,6 +511,8 @@ "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], + "get-user-locale": ["get-user-locale@2.3.2", "", { "dependencies": { "mem": "^8.0.0" } }, "sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -670,8 +677,12 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "map-age-cleaner": ["map-age-cleaner@0.1.3", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mem": ["mem@8.1.1", "", { "dependencies": { "map-age-cleaner": "^0.1.3", "mimic-fn": "^3.1.0" } }, "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA=="], + "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], @@ -684,6 +695,8 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-fn": ["mimic-fn@3.1.0", "", {}, "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -736,6 +749,8 @@ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "p-defer": ["p-defer@1.0.0", "", {}, "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -774,6 +789,8 @@ "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "react-calendar": ["react-calendar@5.1.0", "", { "dependencies": { "@wojtekmaj/date-utils": "^1.1.3", "clsx": "^2.0.0", "get-user-locale": "^2.2.1", "warning": "^4.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-09o/rQHPZGEi658IXAJtWfra1N69D1eFnuJ3FQm9qUVzlzNnos1+GWgGiUeSs22QOpNm32aoVFOimq0p3Ug9Eg=="], + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -920,6 +937,8 @@ "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="], + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], diff --git a/frontend/package.json b/frontend/package.json index ad7cf3f..f975f93 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@tanstack/react-query-devtools": "^5.71.0", "next": "15.2.4", "react": "^19.0.0", + "react-calendar": "^5.1.0", "react-dom": "^19.0.0" }, "devDependencies": { diff --git a/frontend/src/app/activity/[id]/belong/page.tsx b/frontend/src/app/activity/[id]/belong/page.tsx index a823212..b4f0faf 100644 --- a/frontend/src/app/activity/[id]/belong/page.tsx +++ b/frontend/src/app/activity/[id]/belong/page.tsx @@ -1,97 +1,132 @@ +"use client"; import Link from "next/link"; import { redirect } from "next/navigation"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { Activity, BoardInfo } from "@common/types/responses"; +import CustomCalendar from "@front/components/calendar/CustomCalendar"; import Icons from "@front/components/icons"; import instance from "@front/utils/instance"; import BackButton from "../backButton"; import SetColor from "../setColor"; -const Club = async ({ - params, -}: { - params: Promise<{ +interface ClubProps { + params: { id: string; - }>; - }) => { - try { - const { id } = await params; - const { data: info } = await instance.get(`/activity/${id}`); - const { data: boardInfo } = await instance.get(`/board/list/activity/${id}`); + }; +} - return ( -
-
- - - -

내 소속 동아리

-
+const Club = ({ params }: ClubProps) => { + const [info, setInfo] = useState(null); + const [boardInfo, setBoardInfo] = useState([]); + const [events, setEvents] = useState([]); -
-
- 동아리 로고 -
-

{info.name}

-

{info.small_type}

-
-
-
- { - info.homepage_url ? ( - -

홈페이지

- - ) : null - } - { - info.instagram ? ( - -

인스타그램

- - ) : null + useEffect(() => { + const fetchData = async () => { + try { + const { id } = params; + const [infoRes, boardRes, eventsRes] = await Promise.all([ + instance.get(`/activity/${id}`), + instance.get(`/board/list/activity/${id}`), + instance.get("/event/list", { + params: { + from: new Date().toISOString().split("T")[0], + to: new Date(new Date().setMonth(new Date().getMonth() + 1)).toISOString().split("T")[0], } + }) + ]); + + setInfo(infoRes.data); + setBoardInfo(boardRes.data); + setEvents(eventsRes.data.events || []); + } catch (error) { + redirect("/main"); + } + }; + + fetchData(); + }, [params]); + + if (!info) { + return
Loading...
; + } + + return ( +
+
+ + + +

내 소속 동아리

+
+ +
+
+ 동아리 로고 +
+

{info.name}

+

{info.small_type}

- +
+ { + info.homepage_url ? ( + +

홈페이지

+ + ) : null + } + { + info.instagram ? ( + +

인스타그램

+ + ) : null + } +
+
+ { + boardInfo.map((board) => ( + +
+

{board.name}

+

전체보기

+
+
+ )) + } - { - boardInfo.map((board) => ( - -
-

{board.name}

-

전체보기

-
-
- )) - } +
+
- ); - } - catch { - return redirect("/main"); - } + +
+ ); }; export default Club; \ No newline at end of file diff --git a/frontend/src/components/calendar/CalendarGrid.tsx b/frontend/src/components/calendar/CalendarGrid.tsx new file mode 100644 index 0000000..cbbc5ba --- /dev/null +++ b/frontend/src/components/calendar/CalendarGrid.tsx @@ -0,0 +1,87 @@ +"use client"; + +import dayjs from "dayjs"; +import React from "react"; + +interface Event { + timetable_id: string; + timetable_name: string; + event_id: string; + title: string; + startTime: string; + endTime: string; + location?: string; + color: string; + isAllDay?: boolean; +} + +interface CalendarGridProps { + currentDate: dayjs.Dayjs; + onSelectDate: (date: dayjs.Dayjs) => void; + selectedDate: dayjs.Dayjs; + getDateEvents: (date: dayjs.Dayjs) => Event[]; +} + +const CalendarGrid: React.FC = ({ + currentDate, + onSelectDate, + selectedDate, + getDateEvents, +}) => { + const startOfMonth = currentDate.startOf("month"); + const endOfMonth = currentDate.endOf("month"); + const startDate = startOfMonth.startOf("week"); + const endDate = endOfMonth.endOf("week"); + + const calendarDays: dayjs.Dayjs[] = []; + let day = startDate; + while (day.isBefore(endDate) || day.isSame(endDate, "day")) { + calendarDays.push(day); + day = day.add(1, "day"); + } + + return ( +
+ {calendarDays.map((day) => { + const isCurrentMonth = day.month() === currentDate.month(); + const isToday = day.format("YYYY-MM-DD") === dayjs().format("YYYY-MM-DD"); + const isSelected = day.format("YYYY-MM-DD") === selectedDate.format("YYYY-MM-DD"); + const events = getDateEvents(day); + + return ( +
+ + {events.length > 0 && ( +
+ {events.slice(0, 3).map((event, index) => ( +
+ ))} + {events.length > 3 && ( +
+ )} +
+ )} +
+ ); + })} +
+ ); +}; + +export default CalendarGrid; diff --git a/frontend/src/components/calendar/CalendarHeader.tsx b/frontend/src/components/calendar/CalendarHeader.tsx new file mode 100644 index 0000000..0c2102b --- /dev/null +++ b/frontend/src/components/calendar/CalendarHeader.tsx @@ -0,0 +1,34 @@ +"use client"; + +import dayjs from "dayjs"; +import "dayjs/locale/ko"; +import React from "react"; + +interface CalendarHeaderProps { + currentDate: dayjs.Dayjs; + onPrevMonth: () => void; + onNextMonth: () => void; +} + +const CalendarHeader: React.FC = ({ + currentDate, + onPrevMonth, + onNextMonth, +}) => { + dayjs.locale("ko"); + return ( +
+ +

+ {currentDate.format("M월")} +

+ +
+ ); +}; + +export default CalendarHeader; diff --git a/frontend/src/components/calendar/CustomCalendar.tsx b/frontend/src/components/calendar/CustomCalendar.tsx new file mode 100644 index 0000000..3c01096 --- /dev/null +++ b/frontend/src/components/calendar/CustomCalendar.tsx @@ -0,0 +1,102 @@ +"use client"; + +import dayjs from "dayjs"; +import React, { useState } from "react"; + +import CalendarGrid from "./CalendarGrid"; +import CalendarHeader from "./CalendarHeader"; +import DaysOfWeek from "./DaysOfWeek"; +import EventList from "./EventList"; + +interface Event { + timetable_id: string; + timetable_name: string; + event_id: string; + title: string; + startTime: string; + endTime: string; + location?: string; + color: string; + isAllDay?: boolean; +} + +interface Timetable { + timetable_id: string; + name: string; + color: string; + events: Event[]; +} + +interface CalendarProps { + timetables: Timetable[]; +} + +const CustomCalendar: React.FC = ({ timetables }) => { + const [currentDate, setCurrentDate] = useState(dayjs()); + const [selectedDate, setSelectedDate] = useState(dayjs()); + + const handlePrevMonth = () => { + setCurrentDate(currentDate.subtract(1, "month")); + }; + + const handleNextMonth = () => { + setCurrentDate(currentDate.add(1, "month")); + }; + + const handleSelectDate = (date: dayjs.Dayjs) => { + setSelectedDate(date); + }; + + // 모든 시간표의 이벤트를 하나의 배열로 합치기 + const allEvents = timetables.flatMap(timetable => + timetable.events.map(event => ({ + ...event, + timetable_name: timetable.name, + color: timetable.color + })) + ); + + // 선택된 날짜의 이벤트 필터링 + const selectedDateEvents = allEvents.filter(event => { + const eventStart = dayjs(event.startTime); + return ( + eventStart.date() === selectedDate.date() && + eventStart.month() === selectedDate.month() && + eventStart.year() === selectedDate.year() + ); + }); + + // 각 날짜의 이벤트 개수 계산 + const getDateEvents = (date: dayjs.Dayjs) => { + return allEvents.filter(event => { + const eventStart = dayjs(event.startTime); + return ( + eventStart.date() === date.date() && + eventStart.month() === date.month() && + eventStart.year() === date.year() + ); + }); + }; + + return ( +
+
+ + + + +
+
+ ); +}; + +export default CustomCalendar; diff --git a/frontend/src/components/calendar/DaysOfWeek.tsx b/frontend/src/components/calendar/DaysOfWeek.tsx new file mode 100644 index 0000000..a197a5e --- /dev/null +++ b/frontend/src/components/calendar/DaysOfWeek.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +const daysOfWeek = ["일", "월", "화", "수", "목", "금", "토"]; + +const DaysOfWeek: React.FC = () => { + return ( +
+ {daysOfWeek.map((day, index) => ( +
+ {day} +
+ ))} +
+ ); +}; + +export default DaysOfWeek; diff --git a/frontend/src/components/calendar/EventList.tsx b/frontend/src/components/calendar/EventList.tsx new file mode 100644 index 0000000..6a3b65d --- /dev/null +++ b/frontend/src/components/calendar/EventList.tsx @@ -0,0 +1,72 @@ +import dayjs from "dayjs"; +import React from "react"; + +interface Event { + timetable_id: string; + timetable_name: string; + event_id: string; + title: string; + startTime: string; + endTime: string; + location?: string; + color: string; + isAllDay?: boolean; +} + +interface EventListProps { + selectedDate: dayjs.Dayjs; + events: Event[]; +} + +const EventList: React.FC = ({ selectedDate, events }) => { + // 이벤트를 시간순으로 정렬 + const sortedEvents = [...events].sort((a, b) => { + if (a.isAllDay && !b.isAllDay) return -1; + if (!a.isAllDay && b.isAllDay) return 1; + return dayjs(a.startTime).unix() - dayjs(b.startTime).unix(); + }); + + return ( +
+

+ {selectedDate.format("M월 D일")} 일정 +

+
+ {sortedEvents.map((event) => ( +
+
+
+
+
+

{event.title}

+

{event.timetable_name}

+
+
+ {event.isAllDay ? ( +

하루종일

+ ) : ( +

+ {dayjs(event.startTime).format("HH:mm")} -{" "} + {dayjs(event.endTime).format("HH:mm")} +

+ )} + {event.location && ( +

{event.location}

+ )} +
+
+
+
+ ))} + {events.length === 0 && ( +

일정이 없습니다

+ )} +
+
+ ); +}; + +export default EventList; diff --git a/frontend/src/data/events.tsx b/frontend/src/data/events.tsx new file mode 100644 index 0000000..98c0918 --- /dev/null +++ b/frontend/src/data/events.tsx @@ -0,0 +1,29 @@ +export interface Event { + id: string; + title: string; + date: Date; + time?: string; + description?: string; +} + +// Sample events data for April 2025 +export const events: Event[] = [ + { + id: "1", + title: "1학년 캘린더 - 동아리 1차 서류 지원 마감", + date: new Date(2025, 3, 5), // April 5th, 2025 + description: "하루종일", + }, + { + id: "2", + title: "전체 캘린더 - 동아리 일정 테스트동아리", + date: new Date(2025, 3, 5), // April 5th, 2025 + description: "하루종일", + }, + { + id: "3", + title: "전체 캘린더 - 동아리 회의", + date: new Date(2025, 3, 5), // April 5th, 2025 + time: "18:00~19:00", + }, +];