@@ -39,15 +32,14 @@ export default function HomePage() {
- {/* Divider + Section: Timed sessions */}
@@ -61,18 +53,8 @@ export default function HomePage() {
- startSession("sprint")}
- />
- startSession("deepwork")}
- />
+ go("sprint")} />
+ go("deepwork")} />
@@ -82,12 +64,10 @@ export default function HomePage() {
function ModeTile({
title,
meta,
- desc,
onClick,
}: {
title: string;
meta: string;
- desc: string;
onClick: () => void;
}) {
return (
@@ -101,7 +81,6 @@ function ModeTile({
{title}
{meta}
-
{desc}
);
}
diff --git a/src/app/session/end/page.tsx b/src/app/session/end/page.tsx
new file mode 100644
index 0000000..90d7981
--- /dev/null
+++ b/src/app/session/end/page.tsx
@@ -0,0 +1,93 @@
+// app/session/end/page.tsx
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import { useMemo } from "react";
+
+type Mode = "freeflow" | "sprint" | "deepwork";
+
+const BG = "#E9EEF6";
+const BORDER = "#C9D7F5";
+const PRIMARY = "#2F6FED";
+const PRIMARY_HOVER = "#295FD1";
+
+function clampMode(v: string | null): Mode {
+ if (v === "sprint" || v === "deepwork" || v === "freeflow") return v;
+ return "freeflow";
+}
+
+function modeLabel(mode: Mode) {
+ if (mode === "sprint") return "스프린트";
+ if (mode === "deepwork") return "딥워크";
+ return "프리플로우";
+}
+
+function hhmmss(total: number) {
+ const s = Math.max(0, Math.floor(total));
+ const hh = String(Math.floor(s / 3600)).padStart(2, "0");
+ const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
+ const ss = String(s % 60).padStart(2, "0");
+ return `${hh}:${mm}:${ss}`;
+}
+
+export default function SessionEndPage() {
+ const router = useRouter();
+ const params = useSearchParams();
+
+ const mode = useMemo(() => clampMode(params.get("mode")), [params]);
+ const elapsed = useMemo(() => {
+ const v = Number(params.get("elapsed") ?? "0");
+ return Number.isFinite(v) ? v : 0;
+ }, [params]);
+
+ return (
+
+
+
+
+
+
+ {modeLabel(mode)}
+
+
+ {hhmmss(elapsed)}
+
+
세션이 종료됐어요
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/session/page.tsx b/src/app/session/page.tsx
new file mode 100644
index 0000000..0f04172
--- /dev/null
+++ b/src/app/session/page.tsx
@@ -0,0 +1,465 @@
+// app/session/page.tsx
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import { useEffect, useMemo, useRef, useState } from "react";
+
+type Mode = "freeflow" | "sprint" | "deepwork";
+type PresenceStatus = "focus" | "away";
+
+type Participant = {
+ id: string;
+ status: PresenceStatus;
+ lastSeen: number; // ms
+ isSelf: boolean;
+};
+
+const BG = "#E9EEF6";
+const BORDER = "#C9D7F5";
+const PRIMARY = "#2F6FED";
+const PRIMARY_HOVER = "#295FD1";
+const HOVER = "#F1F5FF";
+
+function clampMode(v: string | null): Mode {
+ if (v === "sprint" || v === "deepwork" || v === "freeflow") return v;
+ return "freeflow";
+}
+
+function modeLabel(mode: Mode) {
+ if (mode === "sprint") return "스프린트";
+ if (mode === "deepwork") return "딥워크";
+ return "프리플로우";
+}
+
+function modeDurationSeconds(mode: Mode) {
+ if (mode === "sprint") return 25 * 60;
+ if (mode === "deepwork") return 90 * 60;
+ return null; // freeflow
+}
+
+function formatHHMMSS(totalSeconds: number) {
+ const s = Math.max(0, Math.floor(totalSeconds));
+ const hh = String(Math.floor(s / 3600)).padStart(2, "0");
+ const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
+ const ss = String(s % 60).padStart(2, "0");
+ return `${hh}:${mm}:${ss}`;
+}
+
+/**
+ * 로컬 Presence (Supabase 교체 전용)
+ * - 같은 브라우저에서 여러 탭/창을 열면 "인원 점"이 늘어남
+ * - BroadcastChannel 미지원 환경은 localStorage 이벤트로 fallback
+ *
+ * 교체 포인트:
+ * const { participants } = useLocalPresence(roomKey, status)
+ * 를
+ * const { participants } = useSupabasePresence(roomKey, status)
+ * 같은 형태로 바꾸면 UI는 그대로 유지 가능
+ */
+function useLocalPresence(roomKey: string, status: PresenceStatus) {
+ const selfId = useMemo(() => {
+ // 탭 단위 고유 ID
+ const key = `hushroom:selfId:${roomKey}`;
+ const existing = sessionStorage.getItem(key);
+ if (existing) return existing;
+
+ // 짧고 충돌 확률 낮은 ID
+ const id = (crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`)
+ .replace(/[^a-zA-Z0-9]/g, "")
+ .slice(0, 16);
+
+ sessionStorage.setItem(key, id);
+ return id;
+ }, [roomKey]);
+
+ const [participants, setParticipants] = useState
(() => [
+ { id: selfId, status, lastSeen: Date.now(), isSelf: true },
+ ]);
+
+ const participantsRef = useRef