From b2f4a397e39b9cb192147b5fa4e8d531f56a69af Mon Sep 17 00:00:00 2001 From: theorose49 Date: Tue, 30 Jun 2026 16:15:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(calendar):=20=EA=B5=AC=EC=84=B1=EC=9B=90?= =?UTF-8?q?=EB=B3=84=20=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20+=20=EB=8B=A4=EB=A5=B8=20=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=EC=9B=90=20=EC=9D=BC=EC=A0=95=20=EB=B3=B4=EA=B8=B0(=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=84=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일정 소유자별 체크박스로 표시/숨김(기본 전체), 칩에 타인 일정은 이름 이니셜 표기 - 남의 일정 클릭 시 읽기 전용 모달(수정/삭제 불가), 내 일정만 편집 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pages/Calendar.tsx | 72 +++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/src/pages/Calendar.tsx b/src/pages/Calendar.tsx index 7669fde..5e8feb5 100644 --- a/src/pages/Calendar.tsx +++ b/src/pages/Calendar.tsx @@ -1,7 +1,9 @@ import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Plus, Trash2, ChevronLeft, ChevronRight } from "lucide-react"; +import { Plus, Trash2, ChevronLeft, ChevronRight, Users } from "lucide-react"; import { getEvents, createEvent, updateEvent, deleteEvent, getProjects } from "@/lib/api"; +import { useAuth } from "@/context/Auth"; +import { useDirectory } from "@/lib/directory"; import { Card, Button, PageHeader, Modal, Field, Input, Select, Textarea, LoadingState, } from "@/components/ui"; @@ -20,14 +22,32 @@ function eventColor(e: { category: string; projectId: string }): string { } const ymd = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; -// 전체 캘린더 — 개인 일정을 분류(프로젝트/기타/개인)별 색으로 관리. +// 전체 캘린더 — 직접 넣는 일정만. 다른 구성원 일정도 보이고 구성원별 체크박스로 필터. export function CalendarPage() { const now = new Date(); + const { me } = useAuth(); + const myEmail = me?.user.email ?? ""; + const { nameOf } = useDirectory(); const [ym, setYm] = useState({ y: now.getFullYear(), m: now.getMonth() }); const [editing, setEditing] = useState | null>(null); + const [selected, setSelected] = useState | null>(null); // null = 전체 표시 const evQ = useQuery({ queryKey: ["events"], queryFn: getEvents }); const events = evQ.data ?? []; + // 일정 소유자 목록(나 우선) + const owners = useMemo(() => { + const s = new Set(events.map((e) => e.ownerEmail)); + if (myEmail) s.add(myEmail); + return Array.from(s).sort((a, b) => (a === myEmail ? -1 : b === myEmail ? 1 : nameOf(a).localeCompare(nameOf(b)))); + }, [events, myEmail, nameOf]); + const isOn = (email: string) => selected === null || selected.has(email); + const toggleOwner = (email: string) => + setSelected((prev) => { + const next = new Set(prev ?? owners); + if (next.has(email)) next.delete(email); else next.add(email); + return next; + }); + const first = new Date(ym.y, ym.m, 1); const startDow = first.getDay(); const daysInMonth = new Date(ym.y, ym.m + 1, 0).getDate(); @@ -36,8 +56,8 @@ export function CalendarPage() { const eventsOn = useMemo(() => { const map = new Map(); for (const e of events) { + if (!(selected === null || selected.has(e.ownerEmail))) continue; const s = e.start, end = e.end || e.start; - // start..end 사이 모든 날짜에 배치 let d = new Date(s); const last = new Date(end); if (Number.isNaN(d.getTime())) continue; @@ -48,16 +68,33 @@ export function CalendarPage() { } } return map; - }, [events]); + }, [events, selected]); const prevMonth = () => setYm((s) => ({ y: s.m === 0 ? s.y - 1 : s.y, m: s.m === 0 ? 11 : s.m - 1 })); const nextMonth = () => setYm((s) => ({ y: s.m === 11 ? s.y + 1 : s.y, m: s.m === 11 ? 0 : s.m + 1 })); return (
- } onClick={() => setEditing({ category: "etc", start: ymd(now) })}>일정 추가} /> + {/* 구성원 필터 (체크박스) */} + {owners.length > 1 && ( + +
+ 구성원 + {owners.map((email) => ( + + ))} +
+
+ )} +
@@ -83,8 +120,8 @@ export function CalendarPage() { {dayEvents.slice(0, 4).map((e) => ( ))} {dayEvents.length > 4 &&
+{dayEvents.length - 4}
} @@ -96,7 +133,6 @@ export function CalendarPage() { })}
)} - {/* 범례 */}
기타 개인 @@ -104,14 +140,15 @@ export function CalendarPage() {
- {editing && setEditing(null)} />} + {editing && setEditing(null)} />}
); } -function EventModal({ init, onClose }: { init: Partial; onClose: () => void }) { +function EventModal({ init, myEmail, ownerName, onClose }: { init: Partial; myEmail: string; ownerName: string; onClose: () => void }) { const qc = useQueryClient(); - const projQ = useQuery({ queryKey: ["projects", "mine"], queryFn: () => getProjects({ scope: "mine" }) }); + 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 ?? "", @@ -127,6 +164,19 @@ function EventModal({ init, onClose }: { init: Partial; onClose: }); const del = useMutation({ mutationFn: () => deleteEvent(init.id!), onSuccess: () => { invalidate(); onClose(); } }); + if (readOnly) { + return ( + 닫기}> +
+
{init.title}
+
{ownerName} 님의 일정
+
{init.start}{init.end && init.end !== init.start ? ` ~ ${init.end}` : ""}
+ {init.memo &&

{init.memo}

} +
+
+ ); + } + return (