refactor: 항해일지 작성 모달로 변경 및 꾹 눌러서 종료

This commit is contained in:
2026-02-14 02:05:43 +09:00
parent 8e9ba0431b
commit 6640962573
6 changed files with 284 additions and 844 deletions

View File

@@ -1,14 +1,125 @@
import { useFlightSession } from '@/features/flight-session/model/useFlightSession';
import { useRouter } from "next/navigation";
import { FormEvent, useEffect, useRef, useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useFlightSession } from "@/features/flight-session/model/useFlightSession";
import { saveCurrentVoyage, saveToHistory } from "@/shared/lib/store";
import { Voyage, VoyageStatus } from "@/shared/types";
const statusOptions: { value: VoyageStatus; label: string; desc: string }[] = [
{ value: "completed", label: "계획대로", desc: "목표를 달성했습니다" },
{ value: "partial", label: "부분 진행", desc: "절반의 성과를 만들었습니다" },
{
value: "reoriented",
label: "방향 재설정",
desc: "우선순위를 새로 정했습니다",
},
];
const FINISH_HOLD_MS = 1000;
export function FlightHudWidget() {
const router = useRouter();
const {
voyage,
isPaused,
formattedTime,
timeLeft,
isCountdownCompleted,
handlePauseToggle,
handleFinish,
} = useFlightSession();
const [isDebriefOpen, setIsDebriefOpen] = useState(false);
const [finishedVoyage, setFinishedVoyage] = useState<Voyage | null>(null);
const [status, setStatus] = useState<VoyageStatus | null>(null);
const [progress, setProgress] = useState("");
const [holdProgress, setHoldProgress] = useState(0);
const holdStartAtRef = useRef<number | null>(null);
const holdRafRef = useRef<number | null>(null);
const isHoldCompletedRef = useRef(false);
const openDebriefModal = () => {
const endedVoyage = handleFinish();
if (!endedVoyage) return;
setFinishedVoyage(endedVoyage);
setStatus(null);
setProgress("");
setIsDebriefOpen(true);
};
const handleDebriefSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!status || !finishedVoyage) return;
const finalVoyage: Voyage = {
...finishedVoyage,
status,
debriefProgress: progress,
};
saveToHistory(finalVoyage);
saveCurrentVoyage(null);
setIsDebriefOpen(false);
router.push("/log");
};
const resetHold = () => {
if (holdRafRef.current !== null) {
cancelAnimationFrame(holdRafRef.current);
holdRafRef.current = null;
}
holdStartAtRef.current = null;
isHoldCompletedRef.current = false;
setHoldProgress(0);
};
const openDebriefByHold = () => {
isHoldCompletedRef.current = true;
setHoldProgress(1);
openDebriefModal();
resetHold();
};
const tickHoldProgress = (timestamp: number) => {
if (holdStartAtRef.current === null) return;
const elapsed = timestamp - holdStartAtRef.current;
const nextProgress = Math.min(1, elapsed / FINISH_HOLD_MS);
setHoldProgress(nextProgress);
if (nextProgress >= 1) {
openDebriefByHold();
return;
}
holdRafRef.current = requestAnimationFrame(tickHoldProgress);
};
const startHoldToFinish = () => {
if (isDebriefOpen) return;
resetHold();
holdStartAtRef.current = performance.now();
holdRafRef.current = requestAnimationFrame(tickHoldProgress);
};
const cancelHoldToFinish = () => {
if (isHoldCompletedRef.current) return;
resetHold();
};
useEffect(() => {
return () => {
if (holdRafRef.current !== null) {
cancelAnimationFrame(holdRafRef.current);
}
};
}, []);
if (!voyage) return null;
@@ -16,18 +127,25 @@ export function FlightHudWidget() {
<>
<div className="absolute top-8 z-10 text-center">
<span className="rounded-full border border-indigo-500/30 bg-indigo-950/50 px-4 py-1.5 text-xs font-medium uppercase tracking-widest text-indigo-300 shadow-[0_0_15px_rgba(99,102,241,0.3)] backdrop-blur">
{voyage.routeName} · {isPaused ? '일시정지' : '순항 중'}
{voyage.routeName} · {isPaused ? "일시정지" : "순항 중"}
</span>
</div>
<div
className={`relative z-10 my-12 font-mono text-7xl font-light tracking-tighter tabular-nums drop-shadow-2xl transition-opacity duration-300 md:text-9xl ${isPaused ? 'opacity-50' : 'opacity-100'}`}
className={`relative z-10 my-12 font-mono text-7xl font-light tracking-tighter tabular-nums drop-shadow-2xl transition-opacity duration-300 md:text-9xl ${isPaused ? "opacity-50" : "opacity-100"}`}
>
{formattedTime}
</div>
<div className="relative z-10 mb-24 max-w-2xl px-4 text-center text-xl font-medium leading-relaxed text-slate-200 drop-shadow-md md:text-2xl">
&ldquo;{voyage.missionText}&rdquo;
<div className="relative z-10 mb-24 w-full max-w-2xl px-4">
<section className="rounded-2xl border border-slate-600/60 bg-slate-950/55 p-4 text-left shadow-[0_16px_40px_rgba(2,6,23,0.35)] backdrop-blur md:p-6">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400 md:text-xs">
</p>
<p className="mt-2 max-h-32 overflow-y-auto break-words whitespace-pre-wrap pr-1 text-base leading-relaxed text-slate-100 md:text-lg">
{voyage.missionText}
</p>
</section>
</div>
<div className="absolute bottom-12 z-10 flex gap-6">
@@ -35,15 +153,111 @@ export function FlightHudWidget() {
onClick={handlePauseToggle}
className="rounded-full border border-slate-600 bg-slate-900/50 px-8 py-3 text-sm font-bold uppercase tracking-wide text-slate-300 backdrop-blur transition-all hover:border-slate-400 hover:bg-slate-800/80 hover:text-white"
>
{isPaused ? '다시 시작' : '일시정지'}
</button>
<button
onClick={handleFinish}
className="rounded-full bg-slate-100 px-8 py-3 text-sm font-bold uppercase tracking-wide text-slate-900 shadow-lg shadow-white/10 transition-all hover:bg-indigo-500 hover:text-white"
>
{timeLeft === 0 ? '도착 (회고)' : '항해 종료'}
{isPaused ? "다시 시작" : "일시정지"}
</button>
<div className="relative">
<button
type="button"
onPointerDown={startHoldToFinish}
onPointerUp={cancelHoldToFinish}
onPointerLeave={cancelHoldToFinish}
onPointerCancel={cancelHoldToFinish}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
startHoldToFinish();
}
}}
onKeyUp={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
cancelHoldToFinish();
}
}}
onBlur={cancelHoldToFinish}
className="relative overflow-hidden rounded-full border border-slate-200 bg-slate-100 px-8 py-3 text-sm font-bold uppercase tracking-wide text-slate-900 shadow-lg shadow-white/10 transition-all hover:border-indigo-300 hover:text-slate-950"
>
<span
aria-hidden
className="absolute inset-0 origin-left rounded-full bg-indigo-400/45 transition-transform duration-75"
style={{ transform: `scaleX(${holdProgress})` }}
/>
<span className="relative z-10">
{isCountdownCompleted ? "도착 (회고)" : "항해 종료"}
</span>
</button>
</div>
</div>
<Dialog open={isDebriefOpen} onOpenChange={setIsDebriefOpen}>
<DialogContent className="max-h-[90vh] overflow-y-auto border-slate-700/80 bg-slate-950 text-slate-100">
<DialogHeader>
<DialogTitle className="text-white">
</DialogTitle>
<DialogDescription className="text-slate-400">
.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleDebriefSubmit} className="space-y-6">
<section>
<label className="mb-3 block text-sm font-medium text-slate-300">
</label>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{statusOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setStatus(opt.value)}
className={`rounded-xl border p-4 text-left transition-all ${
status === opt.value
? "border-indigo-500 bg-indigo-900/40 ring-1 ring-indigo-500"
: "border-slate-800 bg-slate-900/50 hover:bg-slate-800"
}`}
>
<div className="mb-1 font-bold text-slate-200">
{opt.label}
</div>
<div className="text-xs text-slate-500">{opt.desc}</div>
</button>
))}
</div>
</section>
<section>
<label className="mb-2 block text-sm font-medium text-slate-300">
</label>
<input
type="text"
value={progress}
onChange={(event) => setProgress(event.target.value)}
placeholder="예: 기획안 목차 구성 완료"
className="w-full rounded-lg border border-slate-800 bg-slate-900/30 px-4 py-3 text-slate-200 outline-none transition-all focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
/>
</section>
<div className="grid grid-cols-2 gap-3 pt-1">
<button
type="button"
onClick={() => setIsDebriefOpen(false)}
className="rounded-xl border border-slate-700 bg-slate-900/60 px-4 py-3 font-semibold text-slate-300 transition-colors hover:border-slate-500 hover:text-white"
>
</button>
<button
type="submit"
disabled={!status}
className="rounded-xl bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-500 disabled:bg-slate-800 disabled:text-slate-500"
>
</button>
</div>
</form>
</DialogContent>
</Dialog>
</>
);
}