From bce50163873adcbde4dbaf3c51a1fe4f9fb1c4d8 Mon Sep 17 00:00:00 2001 From: theorose49 Date: Tue, 30 Jun 2026 16:32:03 +0900 Subject: [PATCH] =?UTF-8?q?feat(calendar):=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=8B=9C=20=EC=B0=B8=EC=97=AC=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D(=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9E=90=20=EC=9D=BC=EA=B4=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)=20+=20=EC=B0=B8=EC=97=AC=EC=9E=90=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=ED=95=84=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일정 모달에 참여자 추가(MemberSelect, '이 프로젝트 작업자 모두 추가'), 칩 표시/삭제 - 구성원 필터·표시를 소유자뿐 아니라 참여자 포함으로 확장 - 읽기 전용 모달에 참여자 표시 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pages/Calendar.tsx | 48 ++++++++++++++++++++++++++++++++++++------ src/types.ts | 1 + 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/pages/Calendar.tsx b/src/pages/Calendar.tsx index 5e8feb5..6f24289 100644 --- a/src/pages/Calendar.tsx +++ b/src/pages/Calendar.tsx @@ -1,9 +1,10 @@ import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus, Trash2, ChevronLeft, ChevronRight, Users } from "lucide-react"; -import { getEvents, createEvent, updateEvent, deleteEvent, getProjects } from "@/lib/api"; +import { getEvents, createEvent, updateEvent, deleteEvent, getProjects, getProjectMembers } from "@/lib/api"; import { useAuth } from "@/context/Auth"; import { useDirectory } from "@/lib/directory"; +import { MemberSelect } from "@/components/MemberSelect"; import { Card, Button, PageHeader, Modal, Field, Input, Select, Textarea, LoadingState, } from "@/components/ui"; @@ -34,12 +35,19 @@ export function CalendarPage() { const evQ = useQuery({ queryKey: ["events"], queryFn: getEvents }); const events = evQ.data ?? []; - // 일정 소유자 목록(나 우선) + // 구성원 목록 = 일정 소유자 + 참여자 (나 우선) const owners = useMemo(() => { - const s = new Set(events.map((e) => e.ownerEmail)); + const s = new Set(); + for (const e of events) { + s.add(e.ownerEmail); + (e.participants ?? []).forEach((p) => s.add(p)); + } if (myEmail) s.add(myEmail); - return Array.from(s).sort((a, b) => (a === myEmail ? -1 : b === myEmail ? 1 : nameOf(a).localeCompare(nameOf(b)))); + return Array.from(s).filter(Boolean).sort((a, b) => (a === myEmail ? -1 : b === myEmail ? 1 : nameOf(a).localeCompare(nameOf(b)))); }, [events, myEmail, nameOf]); + // 이벤트가 선택된 구성원에 해당하는지(소유자 또는 참여자) + const eventShown = (e: CalendarEvent) => + selected === null || selected.has(e.ownerEmail) || (e.participants ?? []).some((p) => selected.has(p)); const isOn = (email: string) => selected === null || selected.has(email); const toggleOwner = (email: string) => setSelected((prev) => { @@ -56,7 +64,7 @@ export function CalendarPage() { const eventsOn = useMemo(() => { const map = new Map(); for (const e of events) { - if (!(selected === null || selected.has(e.ownerEmail))) continue; + if (!eventShown(e)) continue; const s = e.start, end = e.end || e.start; let d = new Date(s); const last = new Date(end); @@ -147,17 +155,23 @@ export function CalendarPage() { function EventModal({ init, myEmail, ownerName, onClose }: { init: Partial; myEmail: string; ownerName: string; onClose: () => void }) { const qc = useQueryClient(); + const { nameOf } = useDirectory(); const readOnly = !!init.id && init.ownerEmail !== myEmail; // 남의 일정은 보기 전용 const projQ = useQuery({ queryKey: ["projects", "mine"], queryFn: () => getProjects({ scope: "mine" }), enabled: !readOnly }); const [f, setF] = useState({ title: init.title ?? "", category: init.category ?? "etc", projectId: init.projectId ?? "", start: init.start ?? "", end: init.end ?? "", memo: init.memo ?? "", }); + const [participants, setParticipants] = useState(init.participants ?? []); + const pmQ = useQuery({ queryKey: ["pm", f.projectId], queryFn: () => getProjectMembers(f.projectId), enabled: !readOnly && f.category === "project" && !!f.projectId }); + const addP = (email: string) => { if (email && !participants.includes(email)) setParticipants((p) => [...p, email]); }; + const removeP = (email: string) => setParticipants((p) => p.filter((x) => x !== email)); + const addProjectMembers = () => setParticipants((p) => Array.from(new Set([...p, ...(pmQ.data ?? []).map((m) => m.memberEmail)]))); const isEdit = !!init.id; const invalidate = () => qc.invalidateQueries({ queryKey: ["events"] }); const save = useMutation({ mutationFn: () => { - const body = { ...f, color: f.category === "project" ? projectColor(f.projectId) : (CAT_COLOR[f.category] ?? "") }; + const body = { ...f, participants, color: f.category === "project" ? projectColor(f.projectId) : (CAT_COLOR[f.category] ?? "") }; return isEdit ? updateEvent(init.id!, body) : createEvent(body); }, onSuccess: () => { invalidate(); onClose(); }, @@ -171,6 +185,9 @@ function EventModal({ init, myEmail, ownerName, onClose }: { init: Partial{init.title}
{ownerName} 님의 일정
{init.start}{init.end && init.end !== init.start ? ` ~ ${init.end}` : ""}
+ {(init.participants?.length ?? 0) > 0 && ( +
참여자: {init.participants!.map((p) => nameOf(p)).join(", ")}
+ )} {init.memo &&

{init.memo}

} @@ -206,6 +223,25 @@ function EventModal({ init, myEmail, ownerName, onClose }: { init: Partial setF({ ...f, end: e.target.value })} />