Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -276,6 +277,8 @@

"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-GraLbYqOJcmW1qY3osB+2YIiD62nVf2/bVLHZmrb4t/YSUwE03l7TwcDJl08T/Tm3SVhepX8RQkpzWbag/Sb4w=="],

"@wojtekmaj/date-utils": ["@wojtekmaj/[email protected]", "", {}, "sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww=="],

"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],

"acorn-jsx": ["[email protected]", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
Expand Down Expand Up @@ -350,6 +353,8 @@

"cliui": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],

"color": ["[email protected]", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],

"color-convert": ["[email protected]", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
Expand Down Expand Up @@ -506,6 +511,8 @@

"get-tsconfig": ["[email protected]", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],

"get-user-locale": ["[email protected]", "", { "dependencies": { "mem": "^8.0.0" } }, "sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ=="],

"glob": ["[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
Expand Down Expand Up @@ -670,8 +677,12 @@

"loose-envify": ["[email protected]", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],

"map-age-cleaner": ["[email protected]", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="],

"math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],

"mem": ["[email protected]", "", { "dependencies": { "map-age-cleaner": "^0.1.3", "mimic-fn": "^3.1.0" } }, "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA=="],

"memoirist": ["[email protected]", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="],

"memory-pager": ["[email protected]", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="],
Expand All @@ -684,6 +695,8 @@

"mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],

"mimic-fn": ["[email protected]", "", {}, "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="],

"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],

"minimist": ["[email protected]", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
Expand Down Expand Up @@ -736,6 +749,8 @@

"own-keys": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="],

"p-limit": ["[email protected]", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],

"p-locate": ["[email protected]", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
Expand Down Expand Up @@ -774,6 +789,8 @@

"react": ["[email protected]", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],

"react-calendar": ["[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],

"react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
Expand Down Expand Up @@ -920,6 +937,8 @@

"uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],

"warning": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="],

"webidl-conversions": ["[email protected]", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],

"whatwg-url": ["[email protected]", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
179 changes: 107 additions & 72 deletions frontend/src/app/activity/[id]/belong/page.tsx
Original file line number Diff line number Diff line change
@@ -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>(`/activity/${id}`);
const { data: boardInfo } = await instance.get<BoardInfo[]>(`/board/list/activity/${id}`);
};
}

return (
<div className="w-full py-4 flex flex-col gap-4">
<div className="px-4 flex flex-row gap-3 items-center">
<BackButton>
<Icons.Back
size={24}
className="fill-dark"
/>
</BackButton>
<p className="text-2xl font-bold">내 소속 동아리</p>
</div>
const Club = ({ params }: ClubProps) => {
const [info, setInfo] = useState<Activity | null>(null);
const [boardInfo, setBoardInfo] = useState<BoardInfo[]>([]);
const [events, setEvents] = useState<any[]>([]);

<div className="px-4 flex flex-row items-center justify-between">
<div className="flex flex-row items-center justify-start gap-5">
<img
src={info.logo_url}
alt="동아리 로고"
className="w-24 h-24 object-cover rounded-full bg-white"
/>
<div className="flex flex-col gap-1">
<p className="font-bold text-2xl text-dark">{info.name}</p>
<p className="font-bold text-dark/40">{info.small_type}</p>
</div>
</div>
<div className="flex flex-col gap-1 justify-center items-end">
{
info.homepage_url ? (
<Link
href={info.homepage_url}
target="_blank"
rel="noopener noreferrer"
>
<p className="text-dark/40 font-bold underline">홈페이지</p>
</Link>
) : null
}
{
info.instagram ? (
<Link
href={`https://instagram.com/${info.instagram}`}
target="_blank"
rel="noopener noreferrer"
>
<p className="text-dark/40 font-bold underline">인스타그램</p>
</Link>
) : null
useEffect(() => {
const fetchData = async () => {
try {
const { id } = params;
const [infoRes, boardRes, eventsRes] = await Promise.all([
instance.get<Activity>(`/activity/${id}`),
instance.get<BoardInfo[]>(`/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 <div>Loading...</div>;
}

return (
<div className="w-full py-4 flex flex-col gap-4">
<div className="px-4 flex flex-row gap-3 items-center">
<BackButton>
<Icons.Back
size={24}
className="fill-dark"
/>
</BackButton>
<p className="text-2xl font-bold">내 소속 동아리</p>
</div>

<div className="px-4 flex flex-row items-center justify-between">
<div className="flex flex-row items-center justify-start gap-5">
<img
src={info.logo_url}
alt="동아리 로고"
className="w-24 h-24 object-cover rounded-full bg-white"
/>
<div className="flex flex-col gap-1">
<p className="font-bold text-2xl text-dark">{info.name}</p>
<p className="font-bold text-dark/40">{info.small_type}</p>
</div>
</div>

<div className="flex flex-col gap-1 justify-center items-end">
{
info.homepage_url ? (
<Link
href={info.homepage_url}
target="_blank"
rel="noopener noreferrer"
>
<p className="text-dark/40 font-bold underline">홈페이지</p>
</Link>
) : null
}
{
info.instagram ? (
<Link
href={`https://instagram.com/${info.instagram}`}
target="_blank"
rel="noopener noreferrer"
>
<p className="text-dark/40 font-bold underline">인스타그램</p>
</Link>
) : null
}
</div>
</div>
{
boardInfo.map((board) => (
<React.Fragment key={board._id}>
<div className="flex flex-row items-center justify-between px-4">
<p className="text-2xl font-bold text-dark mt-4">{board.name}</p>
<p className="text-dark/40 font-bold underline cursor-pointer">전체보기</p>
</div>
</React.Fragment>
))
}

{
boardInfo.map((board) => (
<React.Fragment key={board._id}>
<div className="flex flex-row items-center justify-between px-4">
<p className="text-2xl font-bold text-dark mt-4">{board.name}</p>
<p className="text-dark/40 font-bold underline cursor-pointer">전체보기</p>
</div>
</React.Fragment>
))
}
<div className="px-4">
<CustomCalendar timetables={[{
timetable_id: params.id,
name: info.name,
color: info.key_color || "#000000",
events: events
}]} />

<SetColor color={info.key_color} />
</div>
);
}
catch {
return redirect("/main");
}

</div>
);
};

export default Club;
87 changes: 87 additions & 0 deletions frontend/src/components/calendar/CalendarGrid.tsx
Original file line number Diff line number Diff line change
@@ -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<CalendarGridProps> = ({
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 (
<div className="grid grid-cols-7 gap-y-2 text-center">
{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 (
<div key={day.format("YYYY-MM-DD")} className="relative">
<button
onClick={() => onSelectDate(day)}
className={[
"h-10 w-full rounded-full flex items-center justify-center text-sm relative",
!isCurrentMonth && "text-gray-300",
isCurrentMonth && day.day() === 0 && "text-red-500",
isCurrentMonth && day.day() === 6 && "text-blue-500",
isSelected && "bg-gray-100",
isToday && "font-bold"
].filter(Boolean).join(" ")}
>
{day.format("D")}
</button>
{events.length > 0 && (
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 flex gap-0.5">
{events.slice(0, 3).map((event, index) => (
<div
key={event.event_id}
className="w-1 h-1 rounded-full"
style={{ backgroundColor: event.color }}
/>
))}
{events.length > 3 && (
<div className="w-1 h-1 rounded-full bg-gray-400" />
)}
</div>
)}
</div>
);
})}
</div>
);
};

export default CalendarGrid;
Loading