|
|
|
@ -1,16 +1,27 @@
|
|
|
|
// frontend/components/calendar/CalendarClient.tsx — 일정 페이지 오케스트레이터 (Topbar 제외)
|
|
|
|
// frontend/components/calendar/CalendarClient.tsx — 일정 페이지 오케스트레이터 (실제 날짜 기반)
|
|
|
|
"use client";
|
|
|
|
"use client";
|
|
|
|
import { useEffect, useRef, useState, type MouseEvent } from "react";
|
|
|
|
import { useEffect, useRef, useState, type MouseEvent } from "react";
|
|
|
|
|
|
|
|
import { useParams } from "next/navigation";
|
|
|
|
import { Icon } from "@/components/Icon";
|
|
|
|
import { Icon } from "@/components/Icon";
|
|
|
|
|
|
|
|
import { useDialog } from "@/components/Dialog";
|
|
|
|
import { calendarApi } from "@/lib/calendar/api";
|
|
|
|
import { calendarApi } from "@/lib/calendar/api";
|
|
|
|
import { loadLS, saveLS } from "@/lib/calendar/store";
|
|
|
|
import { loadLS, saveLS } from "@/lib/calendar/store";
|
|
|
|
import type { CalEvent, WeekBundle } from "@/lib/calendar/types";
|
|
|
|
import {
|
|
|
|
|
|
|
|
addDays,
|
|
|
|
|
|
|
|
addMonths,
|
|
|
|
|
|
|
|
dayNum,
|
|
|
|
|
|
|
|
monthNum,
|
|
|
|
|
|
|
|
weekDatesOf,
|
|
|
|
|
|
|
|
yearNum,
|
|
|
|
|
|
|
|
} from "@/lib/calendar/time";
|
|
|
|
|
|
|
|
import type { Calendar, CalEvent, EventWrite, WeekBundle } from "@/lib/calendar/types";
|
|
|
|
import { MiniCal } from "./MiniCal";
|
|
|
|
import { MiniCal } from "./MiniCal";
|
|
|
|
import { CalToggleList } from "./CalToggleList";
|
|
|
|
import { CalToggleList } from "./CalToggleList";
|
|
|
|
import { WeekView } from "./WeekView";
|
|
|
|
import { WeekView } from "./WeekView";
|
|
|
|
import { MonthView } from "./MonthView";
|
|
|
|
import { MonthView } from "./MonthView";
|
|
|
|
import { DayView } from "./DayView";
|
|
|
|
import { DayView } from "./DayView";
|
|
|
|
import { EvPopover } from "./EvPopover";
|
|
|
|
import { EvPopover } from "./EvPopover";
|
|
|
|
|
|
|
|
import { EvEditor } from "./EvEditor";
|
|
|
|
import { MeetDrawer } from "./MeetDrawer";
|
|
|
|
import { MeetDrawer } from "./MeetDrawer";
|
|
|
|
|
|
|
|
|
|
|
|
const VIEWS = [
|
|
|
|
const VIEWS = [
|
|
|
|
@ -19,10 +30,10 @@ const VIEWS = [
|
|
|
|
{ id: "day", label: "일" },
|
|
|
|
{ id: "day", label: "일" },
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function countByDay(events: CalEvent[]): Record<number, number> {
|
|
|
|
function countByDate(events: CalEvent[]): Record<string, number> {
|
|
|
|
const m: Record<number, number> = {};
|
|
|
|
const m: Record<string, number> = {};
|
|
|
|
events.forEach((e) => {
|
|
|
|
events.forEach((e) => {
|
|
|
|
m[e.day] = (m[e.day] || 0) + 1;
|
|
|
|
m[e.date] = (m[e.date] || 0) + 1;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return m;
|
|
|
|
return m;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -51,21 +62,34 @@ function CalendarSkeleton() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function CalendarClient() {
|
|
|
|
export function CalendarClient() {
|
|
|
|
|
|
|
|
const { confirm, alert } = useDialog();
|
|
|
|
const [bundle, setBundle] = useState<WeekBundle | null>(null);
|
|
|
|
const [bundle, setBundle] = useState<WeekBundle | null>(null);
|
|
|
|
const [err, setErr] = useState(false);
|
|
|
|
const [err, setErr] = useState(false);
|
|
|
|
const [view, setView] = useState<string>("week");
|
|
|
|
const [view, setView] = useState<string>("week");
|
|
|
|
const [selDay, setSelDay] = useState<number>(8);
|
|
|
|
const [refDate, setRefDate] = useState<string>(""); // 네비게이션 기준일(ISO)
|
|
|
|
|
|
|
|
const [selDate, setSelDate] = useState<string>(""); // 일 뷰 선택일(ISO)
|
|
|
|
const [calOn, setCalOn] = useState<Record<string, boolean>>({});
|
|
|
|
const [calOn, setCalOn] = useState<Record<string, boolean>>({});
|
|
|
|
const [focusOn, setFocusOn] = useState<boolean>(true);
|
|
|
|
const [focusOn, setFocusOn] = useState<boolean>(true);
|
|
|
|
const [hydrated, setHydrated] = useState(false);
|
|
|
|
const [hydrated, setHydrated] = useState(false);
|
|
|
|
const [pop, setPop] = useState<{ ev: CalEvent; x: number; y: number } | null>(null);
|
|
|
|
const [pop, setPop] = useState<{ ev: CalEvent; x: number; y: number } | null>(null);
|
|
|
|
const [meetEv, setMeetEv] = useState<CalEvent | null>(null);
|
|
|
|
const [meetEv, setMeetEv] = useState<CalEvent | null>(null);
|
|
|
|
|
|
|
|
const [editor, setEditor] = useState<{
|
|
|
|
|
|
|
|
ev: CalEvent | null;
|
|
|
|
|
|
|
|
date?: string;
|
|
|
|
|
|
|
|
time?: string;
|
|
|
|
|
|
|
|
} | null>(null);
|
|
|
|
const shellRef = useRef<HTMLDivElement>(null);
|
|
|
|
const shellRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
const didMount = useRef(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 클라이언트에서만 localStorage 복원 (SSR 안정)
|
|
|
|
// URL 로 열린 일정을 표현 — /calendar/<id> 의 <id> 세그먼트(옵셔널 캐치올).
|
|
|
|
|
|
|
|
// 주소 갱신은 router 대신 history.replaceState(얕은 갱신) — router.replace 는 이 라우트를
|
|
|
|
|
|
|
|
// 리마운트시켜 주간 네비게이션·상태가 날아가기 때문.
|
|
|
|
|
|
|
|
const params = useParams();
|
|
|
|
|
|
|
|
const routeId = Array.isArray(params.slug) ? params.slug[0] : undefined;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 클라이언트에서만 localStorage 복원 (SSR 안정). 날짜는 항상 오늘에서 시작(stale 방지).
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
setView(loadLS("view", "week"));
|
|
|
|
setView(loadLS("view", "week"));
|
|
|
|
setSelDay(loadLS("selDay", 8));
|
|
|
|
|
|
|
|
setCalOn(loadLS<Record<string, boolean>>("calOn", {}));
|
|
|
|
setCalOn(loadLS<Record<string, boolean>>("calOn", {}));
|
|
|
|
setFocusOn(loadLS("focusOn", true));
|
|
|
|
setFocusOn(loadLS("focusOn", true));
|
|
|
|
setHydrated(true);
|
|
|
|
setHydrated(true);
|
|
|
|
@ -80,17 +104,32 @@ export function CalendarClient() {
|
|
|
|
setCalOn((cur) =>
|
|
|
|
setCalOn((cur) =>
|
|
|
|
Object.keys(cur).length ? cur : Object.fromEntries(b.calendars.map((c) => [c.id, c.on])),
|
|
|
|
Object.keys(cur).length ? cur : Object.fromEntries(b.calendars.map((c) => [c.id, c.on])),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
setRefDate((cur) => cur || b.today_date);
|
|
|
|
|
|
|
|
setSelDate((cur) => cur || b.today_date);
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.catch(() => setErr(true));
|
|
|
|
.catch(() => setErr(true));
|
|
|
|
};
|
|
|
|
};
|
|
|
|
useEffect(load, []);
|
|
|
|
useEffect(load, []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 백엔드 자동 풀링이 적재한 새 일정을 주기적으로(10초)·포커스 시 반영(네비게이션 상태는 유지).
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
const refetch = () => {
|
|
|
|
|
|
|
|
calendarApi
|
|
|
|
|
|
|
|
.week()
|
|
|
|
|
|
|
|
.then((b) => setBundle(b))
|
|
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const id = setInterval(refetch, 10_000);
|
|
|
|
|
|
|
|
window.addEventListener("focus", refetch);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
|
|
clearInterval(id);
|
|
|
|
|
|
|
|
window.removeEventListener("focus", refetch);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
if (hydrated) saveLS("view", view);
|
|
|
|
if (hydrated) saveLS("view", view);
|
|
|
|
}, [view, hydrated]);
|
|
|
|
}, [view, hydrated]);
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
if (hydrated) saveLS("selDay", selDay);
|
|
|
|
|
|
|
|
}, [selDay, hydrated]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
if (hydrated) saveLS("calOn", calOn);
|
|
|
|
if (hydrated) saveLS("calOn", calOn);
|
|
|
|
}, [calOn, hydrated]);
|
|
|
|
}, [calOn, hydrated]);
|
|
|
|
@ -104,6 +143,33 @@ export function CalendarClient() {
|
|
|
|
return () => cancelAnimationFrame(id);
|
|
|
|
return () => cancelAnimationFrame(id);
|
|
|
|
}, [bundle]);
|
|
|
|
}, [bundle]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// URL → 일정: routeId 가 가리키는 일정이 있으면 그 주로 이동 + 팝오버 오픈(직접 입력·뒤로가기·딥링크).
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
if (!bundle) return; // 번들 로드 후에만 탐색 가능
|
|
|
|
|
|
|
|
if (!routeId) {
|
|
|
|
|
|
|
|
setPop((p) => (p ? null : p)); // 주소에서 빠지면 팝오버 닫기
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pop?.ev.id === routeId) return; // 이미 열려 있음
|
|
|
|
|
|
|
|
const ev = bundle.events.find((e) => e.id === routeId);
|
|
|
|
|
|
|
|
if (!ev) return; // 없는 id → 무시(잘못된 주소)
|
|
|
|
|
|
|
|
setRefDate(ev.date);
|
|
|
|
|
|
|
|
setSelDate(ev.date);
|
|
|
|
|
|
|
|
setPop({ ev, x: Math.max(window.innerWidth / 2 - 150, 16), y: 140 });
|
|
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
}, [routeId, bundle]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 일정 → URL: 팝오버로 연 일정에 주소를 맞춘다(초기 마운트는 위 effect 가 처리하므로 skip).
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
if (!didMount.current) {
|
|
|
|
|
|
|
|
didMount.current = true;
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = pop?.ev.id;
|
|
|
|
|
|
|
|
const target = id ? `/calendar/${id}` : "/calendar";
|
|
|
|
|
|
|
|
if (window.location.pathname !== target) window.history.replaceState(null, "", target);
|
|
|
|
|
|
|
|
}, [pop]);
|
|
|
|
|
|
|
|
|
|
|
|
if (err && !bundle) {
|
|
|
|
if (err && !bundle) {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div className="calpage">
|
|
|
|
<div className="calpage">
|
|
|
|
@ -117,7 +183,20 @@ export function CalendarClient() {
|
|
|
|
|
|
|
|
|
|
|
|
if (!bundle) return <CalendarSkeleton />;
|
|
|
|
if (!bundle) return <CalendarSkeleton />;
|
|
|
|
|
|
|
|
|
|
|
|
const calMap = Object.fromEntries(bundle.calendars.map((c) => [c.id, c]));
|
|
|
|
const today = bundle.today_date;
|
|
|
|
|
|
|
|
const ref = refDate || today;
|
|
|
|
|
|
|
|
const sel = selDate || today;
|
|
|
|
|
|
|
|
const weekDates = weekDatesOf(ref);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const calMap: Record<string, Calendar> = Object.fromEntries(
|
|
|
|
|
|
|
|
bundle.calendars.map((c) => [c.id, c]),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
// 안전망: 이벤트가 참조하는 캘린더가 목록에 없어도 렌더가 깨지지 않도록 기본값 보강.
|
|
|
|
|
|
|
|
for (const e of bundle.events) {
|
|
|
|
|
|
|
|
if (!calMap[e.cal]) {
|
|
|
|
|
|
|
|
calMap[e.cal] = { id: e.cal, name: e.cal || "기타", tone: "ink", on: true, count: 0 };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
const events = bundle.events.filter((e) => calOn[e.cal] !== false);
|
|
|
|
const events = bundle.events.filter((e) => calOn[e.cal] !== false);
|
|
|
|
const focusBlocks = focusOn ? bundle.focus_blocks : [];
|
|
|
|
const focusBlocks = focusOn ? bundle.focus_blocks : [];
|
|
|
|
const openEv = (ev: CalEvent, e: MouseEvent) =>
|
|
|
|
const openEv = (ev: CalEvent, e: MouseEvent) =>
|
|
|
|
@ -126,26 +205,130 @@ export function CalendarClient() {
|
|
|
|
setPop(null);
|
|
|
|
setPop(null);
|
|
|
|
setMeetEv(ev);
|
|
|
|
setMeetEv(ev);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const afterWrite = () => {
|
|
|
|
|
|
|
|
setEditor(null);
|
|
|
|
|
|
|
|
setPop(null);
|
|
|
|
|
|
|
|
load();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteEvent = async (ev: CalEvent) => {
|
|
|
|
|
|
|
|
setPop(null);
|
|
|
|
|
|
|
|
const ok = await confirm({
|
|
|
|
|
|
|
|
title: "일정 삭제",
|
|
|
|
|
|
|
|
message: `‘${ev.title}’ 일정을 삭제할까요?`,
|
|
|
|
|
|
|
|
confirmText: "삭제",
|
|
|
|
|
|
|
|
tone: "danger",
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await calendarApi.deleteEvent(ev.id);
|
|
|
|
|
|
|
|
load();
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
alert({ title: "삭제 실패", message: e instanceof Error ? e.message : "삭제하지 못했어요" });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
// 빈 시간 슬롯 클릭 → 해당 날짜·시각으로 생성 모달 오픈.
|
|
|
|
|
|
|
|
const openSlot = (date: string, time: string) =>
|
|
|
|
|
|
|
|
setEditor({ ev: null, date, time });
|
|
|
|
|
|
|
|
// 팝오버 "내 작업에 추가" → 회의 액션 전체를 작업으로 변환.
|
|
|
|
|
|
|
|
const addToTasks = async (ev: CalEvent) => {
|
|
|
|
|
|
|
|
await calendarApi.materializeAll(ev.id);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
// 드래그 이동/리사이즈 → 날짜·시각 변경을 서버에 반영(낙관적 갱신 후 재로드).
|
|
|
|
|
|
|
|
const evToWrite = (ev: CalEvent, patch: Partial<EventWrite>): EventWrite => ({
|
|
|
|
|
|
|
|
title: ev.title,
|
|
|
|
|
|
|
|
date: ev.date,
|
|
|
|
|
|
|
|
start: ev.start,
|
|
|
|
|
|
|
|
end: ev.end,
|
|
|
|
|
|
|
|
loc: ev.loc,
|
|
|
|
|
|
|
|
note: ev.note,
|
|
|
|
|
|
|
|
people: ev.people,
|
|
|
|
|
|
|
|
cal: ev.cal,
|
|
|
|
|
|
|
|
rrule: ev.rrule,
|
|
|
|
|
|
|
|
reminders: ev.reminders,
|
|
|
|
|
|
|
|
...patch,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const moveEvent = (ev: CalEvent, patch: { date: string; start: string; end: string }) => {
|
|
|
|
|
|
|
|
setBundle(
|
|
|
|
|
|
|
|
(b) =>
|
|
|
|
|
|
|
|
b && {
|
|
|
|
|
|
|
|
...b,
|
|
|
|
|
|
|
|
events: b.events.map((e) =>
|
|
|
|
|
|
|
|
e.id === ev.id
|
|
|
|
|
|
|
|
? { ...e, ...patch, day: Number(patch.date.slice(8, 10)) || e.day }
|
|
|
|
|
|
|
|
: e,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
calendarApi
|
|
|
|
|
|
|
|
.updateEvent(ev.id, evToWrite(ev, patch))
|
|
|
|
|
|
|
|
.then(load)
|
|
|
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
|
|
|
alert({
|
|
|
|
|
|
|
|
title: "이동 실패",
|
|
|
|
|
|
|
|
message: e instanceof Error ? e.message : "일정을 옮기지 못했어요",
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
load();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
// 초대 응답(RSVP) → 서버 반영 + 팝오버 즉시 갱신.
|
|
|
|
|
|
|
|
const rsvp = async (ev: CalEvent, status: "accepted" | "declined" | "tentative") => {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const updated = await calendarApi.rsvp(ev.id, status);
|
|
|
|
|
|
|
|
setPop((p) => (p && p.ev.id === ev.id ? { ...p, ev: updated } : p));
|
|
|
|
|
|
|
|
load();
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
alert({
|
|
|
|
|
|
|
|
title: "응답 실패",
|
|
|
|
|
|
|
|
message: e instanceof Error ? e.message : "응답을 보내지 못했어요",
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const nav = (dir: number) => {
|
|
|
|
|
|
|
|
if (view === "month") setRefDate(addMonths(ref, dir));
|
|
|
|
|
|
|
|
else if (view === "day") {
|
|
|
|
|
|
|
|
const nd = addDays(sel, dir);
|
|
|
|
|
|
|
|
setSelDate(nd);
|
|
|
|
|
|
|
|
setRefDate(nd);
|
|
|
|
|
|
|
|
} else setRefDate(addDays(ref, dir * 7));
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const goToday = () => {
|
|
|
|
|
|
|
|
setRefDate(today);
|
|
|
|
|
|
|
|
setSelDate(today);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const pickDay = (iso: string) => {
|
|
|
|
|
|
|
|
setSelDate(iso);
|
|
|
|
|
|
|
|
setRefDate(iso);
|
|
|
|
|
|
|
|
setView("day");
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ws = weekDates[0];
|
|
|
|
|
|
|
|
const we = weekDates[6];
|
|
|
|
const title =
|
|
|
|
const title =
|
|
|
|
view === "month" ? "2026년 6월" : view === "day" ? `6월 ${selDay}일` : "6월 7일 – 13일";
|
|
|
|
view === "month"
|
|
|
|
|
|
|
|
? `${yearNum(ref)}년 ${monthNum(ref)}월`
|
|
|
|
|
|
|
|
: view === "day"
|
|
|
|
|
|
|
|
? `${monthNum(sel)}월 ${dayNum(sel)}일`
|
|
|
|
|
|
|
|
: monthNum(ws) === monthNum(we)
|
|
|
|
|
|
|
|
? `${monthNum(ws)}월 ${dayNum(ws)}일 – ${dayNum(we)}일`
|
|
|
|
|
|
|
|
: `${monthNum(ws)}월 ${dayNum(ws)}일 – ${monthNum(we)}월 ${dayNum(we)}일`;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div className="calpage" ref={shellRef}>
|
|
|
|
<div className="calpage" ref={shellRef}>
|
|
|
|
<div className="twork">
|
|
|
|
<div className="twork">
|
|
|
|
<aside className="subnav">
|
|
|
|
<aside className="subnav">
|
|
|
|
<div className="sn-title">일정</div>
|
|
|
|
<div className="sn-title">일정</div>
|
|
|
|
<button className="sn-new">
|
|
|
|
<button className="sn-new" onClick={() => setEditor({ ev: null })}>
|
|
|
|
<Icon name="plus" /> 새 일정
|
|
|
|
<Icon name="plus" /> 새 일정
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<MiniCal
|
|
|
|
<MiniCal
|
|
|
|
sel={selDay}
|
|
|
|
monthRef={ref}
|
|
|
|
today={bundle.today}
|
|
|
|
sel={sel}
|
|
|
|
|
|
|
|
today={today}
|
|
|
|
weekdays={bundle.weekdays}
|
|
|
|
weekdays={bundle.weekdays}
|
|
|
|
eventsByDay={countByDay(events)}
|
|
|
|
eventsByDate={countByDate(events)}
|
|
|
|
onSel={(n) => {
|
|
|
|
onSel={pickDay}
|
|
|
|
setSelDay(n);
|
|
|
|
onMonth={(dir) => setRefDate(addMonths(ref, dir))}
|
|
|
|
if (view !== "day") setView("day");
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
<div className="sn-label">내 캘린더</div>
|
|
|
|
<div className="sn-label">내 캘린더</div>
|
|
|
|
<CalToggleList
|
|
|
|
<CalToggleList
|
|
|
|
@ -164,14 +347,14 @@ export function CalendarClient() {
|
|
|
|
|
|
|
|
|
|
|
|
<main className="cmain">
|
|
|
|
<main className="cmain">
|
|
|
|
<div className="chead">
|
|
|
|
<div className="chead">
|
|
|
|
<button className="ch-arrow" aria-label="이전">
|
|
|
|
<button className="ch-arrow" aria-label="이전" onClick={() => nav(-1)}>
|
|
|
|
<Icon name="chev" className="ic flip" />
|
|
|
|
<Icon name="chev" className="ic flip" />
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button className="ch-arrow" aria-label="다음">
|
|
|
|
<button className="ch-arrow" aria-label="다음" onClick={() => nav(1)}>
|
|
|
|
<Icon name="chev" />
|
|
|
|
<Icon name="chev" />
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<h1 className="ch-title">{title}</h1>
|
|
|
|
<h1 className="ch-title">{title}</h1>
|
|
|
|
<button className="ch-today" onClick={() => setSelDay(bundle.today)}>
|
|
|
|
<button className="ch-today" onClick={goToday}>
|
|
|
|
오늘
|
|
|
|
오늘
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<span className="ch-spacer" />
|
|
|
|
<span className="ch-spacer" />
|
|
|
|
@ -198,39 +381,28 @@ export function CalendarClient() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="ai-strip">
|
|
|
|
|
|
|
|
<div className="ai-ic">
|
|
|
|
|
|
|
|
<Icon name="spark" />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
|
|
|
오늘 일정 중 <b>14:00 분기 전략 미팅</b>이 가장 중요해요. 미팅 사이 빈 시간엔 가벼운
|
|
|
|
|
|
|
|
일을, <b>오후 15:10 빈 블록</b>엔 ‘분기 리포트’ 딥 워크를 자동으로 배치해뒀어요. 16:00
|
|
|
|
|
|
|
|
‘치과 예약’은 1:1과 가까워 15분 앞당기길 추천드려요.
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{view === "week" && (
|
|
|
|
{view === "week" && (
|
|
|
|
<WeekView
|
|
|
|
<WeekView
|
|
|
|
events={events}
|
|
|
|
events={events}
|
|
|
|
focusBlocks={focusBlocks}
|
|
|
|
focusBlocks={focusBlocks}
|
|
|
|
calMap={calMap}
|
|
|
|
calMap={calMap}
|
|
|
|
week={bundle.week}
|
|
|
|
weekDates={weekDates}
|
|
|
|
weekdays={bundle.weekdays}
|
|
|
|
weekdays={bundle.weekdays}
|
|
|
|
today={bundle.today}
|
|
|
|
todayDate={today}
|
|
|
|
onOpen={openEv}
|
|
|
|
onOpen={openEv}
|
|
|
|
|
|
|
|
onSlotClick={openSlot}
|
|
|
|
|
|
|
|
onCommit={moveEvent}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
{view === "month" && (
|
|
|
|
{view === "month" && (
|
|
|
|
<MonthView
|
|
|
|
<MonthView
|
|
|
|
events={events}
|
|
|
|
events={events}
|
|
|
|
calMap={calMap}
|
|
|
|
calMap={calMap}
|
|
|
|
|
|
|
|
monthRef={ref}
|
|
|
|
weekdays={bundle.weekdays}
|
|
|
|
weekdays={bundle.weekdays}
|
|
|
|
today={bundle.today}
|
|
|
|
todayDate={today}
|
|
|
|
onOpen={openEv}
|
|
|
|
onOpen={openEv}
|
|
|
|
onSelDay={(n) => {
|
|
|
|
onSelDay={pickDay}
|
|
|
|
setSelDay(n);
|
|
|
|
|
|
|
|
setView("day");
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
{view === "day" && (
|
|
|
|
{view === "day" && (
|
|
|
|
@ -238,12 +410,12 @@ export function CalendarClient() {
|
|
|
|
events={events}
|
|
|
|
events={events}
|
|
|
|
focusBlocks={focusBlocks}
|
|
|
|
focusBlocks={focusBlocks}
|
|
|
|
calMap={calMap}
|
|
|
|
calMap={calMap}
|
|
|
|
day={selDay}
|
|
|
|
dayDate={sel}
|
|
|
|
week={bundle.week}
|
|
|
|
todayDate={today}
|
|
|
|
weekdays={bundle.weekdays}
|
|
|
|
|
|
|
|
today={bundle.today}
|
|
|
|
|
|
|
|
onOpen={openEv}
|
|
|
|
onOpen={openEv}
|
|
|
|
onMeet={openMeet}
|
|
|
|
onMeet={openMeet}
|
|
|
|
|
|
|
|
onSlotClick={openSlot}
|
|
|
|
|
|
|
|
onCommit={moveEvent}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
</main>
|
|
|
|
</main>
|
|
|
|
@ -257,11 +429,29 @@ export function CalendarClient() {
|
|
|
|
cal={calMap[pop.ev.cal]}
|
|
|
|
cal={calMap[pop.ev.cal]}
|
|
|
|
onClose={() => setPop(null)}
|
|
|
|
onClose={() => setPop(null)}
|
|
|
|
onMeet={openMeet}
|
|
|
|
onMeet={openMeet}
|
|
|
|
|
|
|
|
onEdit={(ev) => {
|
|
|
|
|
|
|
|
setPop(null);
|
|
|
|
|
|
|
|
setEditor({ ev });
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
onDelete={deleteEvent}
|
|
|
|
|
|
|
|
onAddToTasks={addToTasks}
|
|
|
|
|
|
|
|
onRsvp={rsvp}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
{meetEv && (
|
|
|
|
{meetEv && (
|
|
|
|
<MeetDrawer ev={meetEv} cal={calMap[meetEv.cal]} onClose={() => setMeetEv(null)} />
|
|
|
|
<MeetDrawer ev={meetEv} cal={calMap[meetEv.cal]} onClose={() => setMeetEv(null)} />
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
{editor && (
|
|
|
|
|
|
|
|
<EvEditor
|
|
|
|
|
|
|
|
calendars={bundle.calendars}
|
|
|
|
|
|
|
|
initial={editor.ev}
|
|
|
|
|
|
|
|
defaultDate={editor.date || sel}
|
|
|
|
|
|
|
|
defaultTime={editor.time}
|
|
|
|
|
|
|
|
onClose={() => setEditor(null)}
|
|
|
|
|
|
|
|
onSaved={afterWrite}
|
|
|
|
|
|
|
|
onDeleted={afterWrite}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|