You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

221 lines
7.3 KiB
TypeScript

// frontend/components/calendar/EvEditor.tsx — 새 일정 생성/편집 모달
"use client";
import { useState } from "react";
import { Icon } from "@/components/Icon";
import { calendarApi } from "@/lib/calendar/api";
import { addHour } from "@/lib/calendar/time";
import type { Calendar, CalEvent, EventWrite } from "@/lib/calendar/types";
// 쓰기 가능한 캘린더(Google = gcal-*, Outlook = ocal-*, 로컬 = local).
function writableCalendars(cals: Calendar[]): Calendar[] {
const ok = cals.filter(
(c) => c.id.startsWith("gcal-") || c.id.startsWith("ocal-") || c.id === "local",
);
return ok.length ? ok : cals;
}
export function EvEditor({
calendars,
initial,
defaultDate,
defaultTime,
onClose,
onSaved,
onDeleted,
}: {
calendars: Calendar[];
initial?: CalEvent | null; // 있으면 편집, 없으면 생성
defaultDate: string; // 생성 시 기본 날짜(ISO)
defaultTime?: string; // 생성 시 기본 시작시각("HH:MM", 슬롯 클릭)
onClose: () => void;
onSaved: () => void;
onDeleted?: () => void;
}) {
const editing = !!initial;
const writable = writableCalendars(calendars);
const [title, setTitle] = useState(initial?.title ?? "");
const [date, setDate] = useState(initial?.date || defaultDate);
const [allDay, setAllDay] = useState(editing ? !initial?.start : false);
const [start, setStart] = useState(initial?.start || defaultTime || "09:00");
const [end, setEnd] = useState(
initial?.end || (defaultTime ? addHour(defaultTime, 1) : "10:00"),
);
const [loc, setLoc] = useState(initial?.loc ?? "");
const [note, setNote] = useState(initial?.note ?? "");
const [cal, setCal] = useState(initial?.cal || writable[0]?.id || "");
const [people, setPeople] = useState((initial?.people ?? []).join(", "));
const [rrule, setRrule] = useState(initial?.rrule ?? "");
const [reminder, setReminder] = useState<number>(initial?.reminders?.[0] ?? -1); // -1 = 없음
const [busy, setBusy] = useState(false);
const [err, setErr] = useState("");
const save = async () => {
if (!title.trim()) {
setErr("제목을 입력하세요");
return;
}
setBusy(true);
setErr("");
const body: EventWrite = {
title: title.trim(),
date,
start: allDay ? "" : start,
end: allDay ? "" : end,
loc: loc.trim(),
note: note.trim(),
people: people
.split(",")
.map((p) => p.trim())
.filter(Boolean),
cal,
rrule,
reminders: reminder >= 0 ? [reminder] : [],
};
try {
if (editing && initial) await calendarApi.updateEvent(initial.id, body);
else await calendarApi.createEvent(body);
onSaved();
} catch (e) {
setErr(e instanceof Error ? e.message : "저장하지 못했어요");
setBusy(false);
}
};
const del = async () => {
if (!initial) return;
setBusy(true);
setErr("");
try {
await calendarApi.deleteEvent(initial.id);
onDeleted?.();
} catch (e) {
setErr(e instanceof Error ? e.message : "삭제하지 못했어요");
setBusy(false);
}
};
return (
<>
<div className="dp-backdrop" onClick={busy ? undefined : onClose} />
<div className="eed" role="dialog" aria-modal="true" aria-label={editing ? "일정 편집" : "새 일정"}>
<div className="eed-head">
<h2>{editing ? "일정 편집" : "새 일정"}</h2>
<button className="eed-x" aria-label="닫기" onClick={onClose} disabled={busy}>
<Icon name="x" />
</button>
</div>
<label className="eed-field">
<span></span>
<input
autoFocus
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="일정 제목"
/>
</label>
<label className="eed-field">
<span></span>
<input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
</label>
<label className="eed-check">
<input type="checkbox" checked={allDay} onChange={(e) => setAllDay(e.target.checked)} />
<span></span>
</label>
{!allDay && (
<div className="eed-times">
<label className="eed-field">
<span></span>
<input type="time" value={start} onChange={(e) => setStart(e.target.value)} />
</label>
<label className="eed-field">
<span></span>
<input type="time" value={end} onChange={(e) => setEnd(e.target.value)} />
</label>
</div>
)}
<label className="eed-field">
<span></span>
<input value={loc} onChange={(e) => setLoc(e.target.value)} placeholder="(선택)" />
</label>
<label className="eed-field">
<span></span>
<select value={cal} onChange={(e) => setCal(e.target.value)}>
{writable.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
</label>
<div className="eed-times">
<label className="eed-field">
<span></span>
<select value={rrule} onChange={(e) => setRrule(e.target.value)}>
<option value=""> </option>
<option value="FREQ=DAILY"></option>
<option value="FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"> ()</option>
<option value="FREQ=WEEKLY"></option>
<option value="FREQ=MONTHLY"></option>
<option value="FREQ=YEARLY"></option>
</select>
</label>
<label className="eed-field">
<span></span>
<select value={reminder} onChange={(e) => setReminder(Number(e.target.value))}>
<option value={-1}></option>
<option value={0}> </option>
<option value={5}>5 </option>
<option value={10}>10 </option>
<option value={15}>15 </option>
<option value={30}>30 </option>
<option value={60}>1 </option>
</select>
</label>
</div>
<label className="eed-field">
<span></span>
<input
value={people}
onChange={(e) => setPeople(e.target.value)}
placeholder="이메일 — 쉼표로 구분 (선택)"
/>
</label>
<label className="eed-field">
<span></span>
<textarea value={note} onChange={(e) => setNote(e.target.value)} rows={2} placeholder="(선택)" />
</label>
{err && (
<div className="eed-err" role="alert">
{err}
</div>
)}
<div className="eed-acts">
{editing && (
<button className="eed-del" onClick={del} disabled={busy}>
<Icon name="trash" />
</button>
)}
<span className="eed-spacer" />
<button className="eed-cancel" onClick={onClose} disabled={busy}>
</button>
<button className="eed-save" onClick={save} disabled={busy}>
{busy ? "저장 중…" : editing ? "저장" : "추가"}
</button>
</div>
</div>
</>
);
}