feat(calendar): 일정 추가 시 참여자 선택(프로젝트 작업자 일괄 추가) + 참여자 기준 필터
All checks were successful
build-and-push / build (push) Successful in 32s
All checks were successful
build-and-push / build (push) Successful in 32s
- 일정 모달에 참여자 추가(MemberSelect, '이 프로젝트 작업자 모두 추가'), 칩 표시/삭제 - 구성원 필터·표시를 소유자뿐 아니라 참여자 포함으로 확장 - 읽기 전용 모달에 참여자 표시 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b2f4a397e3
commit
bce5016387
@ -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<string>();
|
||||
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<string, CalendarEvent[]>();
|
||||
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<CalendarEvent>; 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<string[]>(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<Calen
|
||||
<div className="text-base font-bold text-ink">{init.title}</div>
|
||||
<div className="text-ink-secondary">{ownerName} 님의 일정</div>
|
||||
<div className="text-ink-secondary tabular">{init.start}{init.end && init.end !== init.start ? ` ~ ${init.end}` : ""}</div>
|
||||
{(init.participants?.length ?? 0) > 0 && (
|
||||
<div className="text-ink-secondary">참여자: {init.participants!.map((p) => nameOf(p)).join(", ")}</div>
|
||||
)}
|
||||
{init.memo && <p className="text-ink-secondary whitespace-pre-wrap">{init.memo}</p>}
|
||||
</div>
|
||||
</Modal>
|
||||
@ -206,6 +223,25 @@ function EventModal({ init, myEmail, ownerName, onClose }: { init: Partial<Calen
|
||||
<Field label="종료일 (하루면 비움)"><Input type="date" value={f.end} onChange={(e) => setF({ ...f, end: e.target.value })} /></Field>
|
||||
</div>
|
||||
<Field label="메모"><Textarea value={f.memo} onChange={(e) => setF({ ...f, memo: e.target.value })} /></Field>
|
||||
|
||||
{/* 참여자 — 이 사람들 캘린더에도 표시 */}
|
||||
<div>
|
||||
<span className="form-label">참여자 <span className="text-ink-muted font-normal">· 추가하면 그 구성원 캘린더에도 보입니다</span></span>
|
||||
<MemberSelect value="" onChange={(v) => v && addP(v)} placeholder="참여자 추가" />
|
||||
{f.category === "project" && f.projectId && (pmQ.data?.length ?? 0) > 0 && (
|
||||
<button type="button" onClick={addProjectMembers} className="mt-1.5 text-xs text-navy hover:underline">+ 이 프로젝트 작업자 모두 추가</button>
|
||||
)}
|
||||
{participants.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{participants.map((p) => (
|
||||
<span key={p} className="inline-flex items-center gap-1 text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5">
|
||||
{nameOf(p)}<button type="button" onClick={() => removeP(p)} className="hover:text-money-out">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-ink-muted">
|
||||
색상: <span className="w-4 h-4 rounded-full inline-block" style={{ background: f.category === "project" ? projectColor(f.projectId) : (CAT_COLOR[f.category] ?? "#475467") }} />
|
||||
{f.category === "project" ? "프로젝트별 색 자동" : "분류 색 자동"}
|
||||
|
||||
@ -308,6 +308,7 @@ export interface CalendarEvent {
|
||||
end: string;
|
||||
allDay: boolean;
|
||||
memo: string;
|
||||
participants: string[] | null; // 참여자 이메일 — 그들 캘린더에도 표시
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user