feat: 일시정지 버튼을 눌렀을 때 별의 움직임이 함께 멈춤

This commit is contained in:
2026-02-14 02:19:34 +09:00
parent 6640962573
commit f32b7ee615
8 changed files with 67 additions and 162 deletions

View File

@@ -1,5 +0,0 @@
- TASK_ID: 0023
- TASK_TITLE: 종료 버튼 길게 누르기 진행선 비주얼 어색함 개선(원형 대안 적용)
- TASK_SLUG: finish-hold-visual-alt-for-non-circle-button
- DATE: 2026-02-14
- FILES: src/app/debrief/page.tsx, src/features/flight-session/model/useFlightSession.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx, AGENTS.md

View File

@@ -1,130 +0,0 @@
# Changelog
> 규칙
>
> - 새 작업이 끝나면 맨 위(최신)에 추가한다.
> - 날짜 섹션이 없으면 새로 만든다.
> - 각 항목은 3~5줄 이내로 짧게.
> - “무엇이 바뀌었는지”와 “영향 범위(파일)”만 남긴다.
## 2026-02-14
- [0023] 종료 버튼 길게 누르기 진행선 비주얼 어색함 개선(원형 대안 적용)
- Summary: 종료 버튼의 원형 진행선을 제거하고 버튼 형태와 일치하는 수평 fill 진행 피드백으로 교체해 hold 인터랙션 시각 일관성을 개선
- Summary: 2초 hold 규칙, 조기 해제 시 즉시 초기화, 완료 시 회고 모달 오픈 등 기존 종료 플로우 동작은 유지
- Files: src/app/debrief/page.tsx, src/features/flight-session/model/useFlightSession.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx, AGENTS.md
- [0022] Flight 종료 버튼 2초 길게 누르기(hold-to-confirm) + 원형 진행선 + 종료 모달 오픈
- Summary: Flight 종료 버튼을 단순 클릭에서 2초 hold-to-confirm 상호작용으로 전환하고, 진행률을 conic-gradient 외곽 진행선으로 시각화
- Summary: 2초 미만 해제 시 진행 상태를 즉시 초기화하고 아무 종료 동작도 발생하지 않게 유지, 2초 도달 시 기존 회고 모달 오픈/저장 흐름 재사용
- Files: src/app/debrief/page.tsx, src/features/flight-session/model/useFlightSession.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx, AGENTS.md
- [0021] Flight 종료 후 회고를 모달로 전환하고 회고 폼 항목/아이콘 정리
- Summary: Flight HUD 종료 버튼을 페이지 이동 대신 회고 모달 오픈으로 전환하고 저장/취소 흐름을 화면 내에서 처리하도록 구성
- Summary: 회고 폼에서 Next 항목을 제거하고 상태 라벨의 이모지를 제거해 문구 톤을 정리, 저장 시 기존과 동일하게 히스토리 저장 후 /log 이동 유지
- Files: src/app/debrief/page.tsx, src/features/flight-session/model/useFlightSession.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx, AGENTS.md
- [0020] Flight 화면 "이번 항해 목표" 뷰 UI/UX 개선
- Summary: Flight HUD 목표 영역을 카드형 레이아웃으로 전환해 라벨(이번 항해 목표)과 본문을 분리하고 정보 위계를 명확화
- Summary: 긴 목표 문구 대응을 위해 줄바꿈과 최대 높이/세로 스크롤 처리 및 대비/여백/행간을 조정해 가독성을 개선
- Files: src/features/flight-session/model/useFlightSession.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx, AGENTS.md
- [0019] 오리온/쌍둥이자리 카운트다운 타이머를 HH:MM:SS로 표시
- Summary: 카운트다운 항로 타이머 포맷 로직을 HH:MM:SS 고정으로 단순화해 오리온/쌍둥이자리 초기값 및 진행 표시를 3단 자리수로 통일
- Summary: 타이머 감소 속도/종료 조건은 유지하고 표시 포맷만 조정해 카운트다운 동작 회귀를 방지
- Files: src/features/flight-session/model/useFlightSession.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx, AGENTS.md
## 2026-02-13
- [0018] 우주정거장(무제한 체류) 타이머 count-up 복구 및 HH:MM:SS 표시
- Summary: 우주정거장 항로(duration=0)에서 타이머를 카운트다운이 아닌 count-up으로 계산해 1초 단위 증가를 복구
- Summary: station 타이머 포맷을 HH:MM:SS로 고정하고 카운트다운 항로의 완료 판정 문구 분기를 분리해 회귀를 방지
- Files: src/features/flight-session/model/useFlightSession.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx, AGENTS.md
- [0017] Enter로 진행 — 탑승(미션 설정) & 회고(항해일지) 폼을 form submit로 처리
- Summary: 탑승 미션 폼을 form submit 구조로 전환해 Enter 입력이 “도킹 완료(출항)”과 동일한 로직을 호출하도록 정리
- Summary: 회고 화면을 form submit 기반으로 통일하고 상태 선택 버튼을 non-submit으로 고정해 Enter 저장 흐름을 안정화
- Files: src/app/debrief/page.tsx, src/features/boarding/ui/BoardingMissionForm.tsx
- [0016] Lobby 목표 설정 화면을 모달로 전환 + 메모 기능 제거(동작 유지)
- Summary: 로비의 “바로 출항” 동선을 페이지 이동(`/boarding`)에서 모달 기반 목표 설정으로 전환하고 ESC/배경클릭/취소 닫기를 지원
- Summary: boarding 생성 경로를 `features/boarding`으로 통합하고 메모 입력 UI/상태/저장 필드를 제거해 출항 로직을 단일화
- Files: src/app/boarding/page.tsx, src/widgets/lobby-routes/ui/LobbyRoutesPanel.tsx, src/features/boarding/index.ts, src/features/boarding/model/startVoyage.ts, src/features/boarding/ui/BoardingMissionForm.tsx, .cli/docs/architecture.md
- [0015] FSD 정착 2차 — 잔재 import 제거/중복 정리 + widgets public API 고정 + 분리작업 준비
- Summary: pages/widgets/features/shared import 경로를 정리해 `@/lib`, `@/types`, `@/components` 직접 참조를 제거하고 단일 소스를 shared·features로 통일
- Summary: widgets별 `index.ts` public API를 추가하고 Home/Flight 페이지를 widgets 조합 전용으로 고정, 구 브릿지 파일(components/lib/types) 정리
- Files: src/app/page.tsx, src/app/flight/page.tsx, src/app/boarding/page.tsx, src/app/debrief/page.tsx, src/app/log/page.tsx, src/app/log/[id]/page.tsx, src/app/settings/page.tsx, src/components/ui/button.tsx, src/components/ui/card.tsx, src/components/ui/dialog.tsx, src/components/ui/input.tsx, src/components/ui/separator.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts, src/lib/store.ts, src/lib/utils.ts, src/types/index.ts, src/shared/types/index.ts, src/shared/config/routes.ts, src/shared/config/starfield.ts, src/shared/lib/store.ts, src/shared/lib/cn.ts, src/shared/lib/math/number.ts, src/shared/lib/motion/prefersReducedMotion.ts, src/features/lobby-session/model/useLobbyRedirect.ts, src/features/lobby-starfield/index.ts, src/features/lobby-starfield/model/constellationData.ts, src/features/lobby-starfield/ui/ConstellationScene.tsx, src/features/lobby-starfield/ui/StarGlint.tsx, src/features/flight-starfield/index.ts, src/features/flight-starfield/model/types.ts, src/features/flight-starfield/model/starfieldModel.ts, src/features/flight-starfield/lib/projection.ts, src/features/flight-starfield/ui/FlightStarfieldCanvas.tsx, src/features/flight-session/model/useFlightSession.ts, src/widgets/lobby-background/index.ts, src/widgets/lobby-background/ui/LobbyBackgroundWidget.tsx, src/widgets/lobby-routes/index.ts, src/widgets/lobby-routes/ui/LobbyRoutesPanel.tsx, src/widgets/flight-background/index.ts, src/widgets/flight-background/ui/FlightBackgroundWidget.tsx, src/widgets/flight-hud/index.ts, src/widgets/flight-hud/ui/FlightHudWidget.tsx
- [0014] FSD 1차 구조로 리팩토링(동작 동일) — lobby/flight 시각 컴포넌트 및 로직 분리
- Summary: Home/Flight 페이지를 조합 전용으로 축소하고 배경/스타필드/HUD/리다이렉트 로직을 features·widgets 구조로 분리
- Summary: 로비 글린트/플라이트 캔버스 로직과 스타필드 타입·튜닝값·모션 유틸을 shared/config·shared/lib·features/model/lib로 정리
- Files: src/app/page.tsx, src/app/flight/page.tsx, src/shared/config/starfield.ts, src/shared/lib/math/number.ts, src/shared/lib/motion/prefersReducedMotion.ts, src/features/lobby-session/model/useLobbyRedirect.ts, src/features/lobby-starfield/model/constellationData.ts, src/features/lobby-starfield/ui/ConstellationScene.tsx, src/features/lobby-starfield/ui/StarGlint.tsx, src/features/flight-starfield/model/types.ts, src/features/flight-starfield/model/starfieldModel.ts, src/features/flight-starfield/lib/projection.ts, src/features/flight-starfield/ui/FlightStarfieldCanvas.tsx, src/features/flight-session/model/useFlightSession.ts, src/widgets/lobby-background/ui/LobbyBackgroundWidget.tsx, src/widgets/lobby-routes/ui/LobbyRoutesPanel.tsx, src/widgets/flight-background/ui/FlightBackgroundWidget.tsx, src/widgets/flight-hud/ui/FlightHudWidget.tsx
- [0013] Home 별자리 별(코어) 더 작게 + Flight 스타필드 속도 추가 감속
- Summary: Home 별 코어 반지름을 추가 축소하고 코어 대비를 높여 작은 별 중심 표현을 강화, glint 길이/피크는 소폭 감쇠
- Summary: Flight 스타필드 speed 티어를 한 단계 더 낮춰 워프감을 줄이고 조용한 전진/유영 느낌으로 재튜닝
- Files: .gitignore, src/app/flight/page.tsx, src/app/globals.css, src/app/page.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0012] Lobby 배경 별(코어) 더 작고 더 진하게 튜닝
- Summary: Home 별자리 코어 반지름을 15~35% 축소해 노드/버튼 느낌을 줄이고 작은 점 중심으로 정리
- Summary: 코어 opacity 티어를 상향하고 glint/bloom 피크를 소폭 감쇠해 “작고 진한 별 + 은은한 글린트”로 조정
- Files: .gitignore, src/app/flight/page.tsx, src/app/globals.css, src/app/page.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0011] Home 별 모양을 Flight 스타일로 통일 + glint 유지(십자/그라데이션/블룸, opacity only)
- Summary: Home 별자리 코어 반지름/밝기 티어를 flight 별 범위에 맞춰 재조정해 노드형 인상을 완화
- Summary: 기존 십자 글린트(그라데이션/블룸)는 유지하고 코어 애니메이션을 변수 기반 opacity-only로 정렬
- Files: .gitignore, src/app/flight/page.tsx, src/app/globals.css, src/app/page.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0010] Flight 스타필드 튜닝 — 중심 과집중 완화(넓은 스폰) + 속도 감속(유영 느낌)
- Summary: 소실점 기반 투영은 유지하되 스폰 반경을 넓은 링 중심 분포로 재조정해 중심 과집중을 완화
- Summary: 속도 티어를 감속 범위로 낮추고 꼬리 티어를 4~10px 하한 기준으로 조정해 유영 느낌을 강화
- Files: .gitignore, src/app/flight/page.tsx, src/app/globals.css, src/app/page.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0009] Flight 스타필드 가시성/전진감 개선 — “먼지 낙하” 제거 + 원근 전진(소실점) + 저밀도 유지
- Summary: Flight 배경을 z-투영 기반 소실점 스타필드로 전환해 수직 낙하 인상을 제거하고 전진감을 강화
- Summary: 별 가시성(밝기/반지름/꼬리 하한)을 상향하면서 저밀도 범위와 중심 UI 보호, reduced-motion 정지를 유지
- Files: .gitignore, src/app/flight/page.tsx, src/app/globals.css, src/app/page.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0008] Flight 스타필드(이미지 없이) — “조용한 우주” 점 중심 + 약한 전진감 + 교차 0 (저밀도 튜닝)
- Summary: Flight 배경을 단일 방향 미세 전진감 기반으로 재구성해 교차/X자 인상을 제거하고 점 중심 표현으로 조정
- Summary: 별 밀도를 데스크탑 18~45 / 모바일 12~30 범위로 제한하고 중앙 보호 베일 및 reduced-motion 정지 렌더를 적용
- Files: .gitignore, src/app/flight/page.tsx, src/app/globals.css, src/app/page.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0007] Flight 스타필드 방향 수정(중심→4모서리 대각) + 중심부 자연화 + 별 개수 축소
- Summary: 별 흐름을 화면 중심 영역에서 NE/NW/SE/SW 대각선으로 퍼져나가도록 재구성하고 수직 낙하 인상을 제거
- Summary: 중심부 베일 그라데이션과 리사이클 스폰을 적용하고 스타 수를 68개로 축소해 과밀감을 완화
- Files: .gitignore, src/app/globals.css, src/app/page.tsx, src/components/FlightBackground.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0006] Lobby 별(노드) UI를 “빛나는 별”로 개선(그라데이션 스파이크 + 블룸 + 강약) — 이동 없음
- Summary: 십자 스파이크를 중앙 강조/양끝 투명 그라데이션으로 정교화하고 별별 강약(피크/길이/크기)을 분산
- Summary: 블룸 1겹과 작은 코어 글로우를 적용하고 반짝임을 opacity-only 리듬으로 조정
- Files: .gitignore, src/app/globals.css, src/app/page.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0005] Lobby 별 반짝임을 “십자 글린트 + 블룸(이미지 참고)”로 개선(이동/transform 0)
- Summary: 별 글린트를 대각선 없이 십자(+)만 남기고, 선 양끝 투명 그라데이션으로 부드럽게 감쇠되도록 조정
- Summary: 코어/스파이크에 저강도 블룸 레이어를 추가하고 반짝임 리듬을 opacity-only 피크 형태로 재설계
- Files: .gitignore, src/app/globals.css, src/app/page.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0004] Lobby 별 반짝임을 “빛 번짐(십자/팔각 글린트)”으로 구현(이동 없음)
- Summary: 로비 별자리를 십자/팔각 글린트 기반 반짝임으로 변경하고 별자리 외 랜덤 별 생성 제거
- Summary: 별 반짝임을 opacity-only로 분산 타이밍 적용하고 reduced-motion에서 정지/저강도 처리
- Files: .gitignore, src/app/globals.css, src/app/page.tsx, src/components/LobbyBackground.tsx, src/lib/constants.ts
- [0003] Lobby twinkle를 “반짝임만”으로 고정(이동 제거) + 별자리 3개 유지
- Summary: 로비 배경의 별 반짝임(Twinkle)을 이동 없이 빛과 미세한 크기 변화만 있도록 수정
- Summary: 배경 스크롤/드리프트/스타필드 등 모든 이동 애니메이션 제거 및 3개 별자리 고정
- Files: src/app/globals.css
- [0002] Lobby 별자리 3개 고정 + 별 반짝임 구현 + 불필요 별/문구 제거
- Summary: 로비 하단 "3명 대기 중" 문구 제거 및 재발 방지
- Summary: 배경 별자리를 오리온/마차부/북두칠성 3개로 고정하고 개별 별 반짝임 구현
- Summary: 카드 내부 및 배경의 불필요한 랜덤 별 생성 로직 제거
- Files: src/app/page.tsx, src/app/globals.css, src/components/LobbyBackground.tsx
## 2026-02-12
- [0001] Lobby 카드 구성 단순화 + twinkle 강화
- Summary: 로비 화면을 우주정거장(CTA) 및 오리온/쌍둥이자리 2열 구성으로 변경
- Summary: 배경 및 카드 내 별 반짝임 효과(Twinkle) 강화 및 접근성(Reduced Motion) 적용
- Files: src/lib/constants.ts, src/components/LobbyBackground.tsx, src/app/page.tsx

View File

@@ -1,5 +0,0 @@
# input.md (자연어 초안)
## 원하는 목표
- 꾹 눌렀을 때 나오는 테두리가 원이 아니다보니 어색한 면이 있다. 어떻게 하면 어색해지지 않을지 고민해서 적절한 대안을 찾아라.

4
.gitignore vendored
View File

@@ -40,3 +40,7 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
.idea
.cli/tasks
.cli/_task_context.md
.cli/changelog.md
.cli/planner/input.md

View File

@@ -1,13 +1,16 @@
'use client';
import { useFlightSession } from '@/features/flight-session/model/useFlightSession';
import { FlightBackgroundWidget } from '@/widgets/flight-background';
import { FlightHudWidget } from '@/widgets/flight-hud';
export default function FlightPage() {
const session = useFlightSession();
return (
<div className="relative flex min-h-[calc(100vh-64px)] flex-1 flex-col items-center justify-center overflow-hidden p-6 text-white">
<FlightBackgroundWidget />
<FlightHudWidget />
<FlightBackgroundWidget isPaused={session.isPaused} />
<FlightHudWidget {...session} />
</div>
);
}

View File

@@ -16,11 +16,15 @@ import { getPrefersReducedMotionMediaQuery } from '@/shared/lib/motion/prefersRe
export function FlightStarfieldCanvas({
vanishYOffset = -68,
centerProtectRadius = 200,
isPaused = false,
}: {
vanishYOffset?: number;
centerProtectRadius?: number;
isPaused?: boolean;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const starsRef = useRef<FlightStar[]>([]);
const vanishXJitterRef = useRef<number | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
@@ -35,7 +39,10 @@ export function FlightStarfieldCanvas({
const motionQuery = getPrefersReducedMotionMediaQuery();
let prefersReducedMotion = motionQuery.matches;
const vanishXJitter = createFlightVanishXJitter();
if (vanishXJitterRef.current === null) {
vanishXJitterRef.current = createFlightVanishXJitter();
}
const vanishXJitter = vanishXJitterRef.current ?? 0;
const setCanvasSize = () => {
width = window.innerWidth;
@@ -58,7 +65,10 @@ export function FlightStarfieldCanvas({
);
setCanvasSize();
let stars: FlightStar[] = createStars();
if (starsRef.current.length === 0) {
starsRef.current = createStars();
}
let stars = starsRef.current;
const applyCenterProtection = (x: number, y: number, alpha: number) => {
const centerX = width / 2;
@@ -183,6 +193,7 @@ export function FlightStarfieldCanvas({
})
) {
stars[index] = createFlightStar({ width, height, isRespawn: true });
starsRef.current = stars;
return;
}
@@ -198,7 +209,18 @@ export function FlightStarfieldCanvas({
drawVignette();
};
const stopAnimation = () => {
if (!animationFrameId) return;
cancelAnimationFrame(animationFrameId);
animationFrameId = 0;
};
const render = () => {
if (prefersReducedMotion || isPaused) {
animationFrameId = 0;
return;
}
drawFrame(true);
animationFrameId = requestAnimationFrame(render);
};
@@ -211,8 +233,9 @@ export function FlightStarfieldCanvas({
const handleResize = () => {
setCanvasSize();
stars = createStars();
starsRef.current = stars;
if (prefersReducedMotion) {
if (prefersReducedMotion || isPaused) {
renderStatic();
}
};
@@ -221,18 +244,20 @@ export function FlightStarfieldCanvas({
prefersReducedMotion = event.matches;
if (prefersReducedMotion) {
cancelAnimationFrame(animationFrameId);
stopAnimation();
renderStatic();
return;
}
render();
if (!isPaused && !animationFrameId) {
render();
}
};
window.addEventListener('resize', handleResize);
motionQuery.addEventListener('change', handleMotionChange);
if (prefersReducedMotion) {
if (prefersReducedMotion || isPaused) {
renderStatic();
} else {
render();
@@ -241,9 +266,9 @@ export function FlightStarfieldCanvas({
return () => {
window.removeEventListener('resize', handleResize);
motionQuery.removeEventListener('change', handleMotionChange);
cancelAnimationFrame(animationFrameId);
stopAnimation();
};
}, [vanishYOffset, centerProtectRadius]);
}, [vanishYOffset, centerProtectRadius, isPaused]);
return <canvas ref={canvasRef} className="fixed inset-0 z-0 bg-black pointer-events-none" />;
}

View File

@@ -1,5 +1,11 @@
import { FlightStarfieldCanvas } from '@/features/flight-starfield';
export function FlightBackgroundWidget() {
return <FlightStarfieldCanvas vanishYOffset={-68} centerProtectRadius={200} />;
export function FlightBackgroundWidget({ isPaused }: { isPaused: boolean }) {
return (
<FlightStarfieldCanvas
vanishYOffset={-68}
centerProtectRadius={200}
isPaused={isPaused}
/>
);
}

View File

@@ -8,7 +8,6 @@ import {
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";
@@ -23,16 +22,24 @@ const statusOptions: { value: VoyageStatus; label: string; desc: string }[] = [
];
const FINISH_HOLD_MS = 1000;
export function FlightHudWidget() {
type FlightHudWidgetProps = {
voyage: Voyage | null;
isPaused: boolean;
formattedTime: string;
isCountdownCompleted: boolean;
handlePauseToggle: () => void;
handleFinish: () => Voyage | null;
};
export function FlightHudWidget({
voyage,
isPaused,
formattedTime,
isCountdownCompleted,
handlePauseToggle,
handleFinish,
}: FlightHudWidgetProps) {
const router = useRouter();
const {
voyage,
isPaused,
formattedTime,
isCountdownCompleted,
handlePauseToggle,
handleFinish,
} = useFlightSession();
const [isDebriefOpen, setIsDebriefOpen] = useState(false);
const [finishedVoyage, setFinishedVoyage] = useState<Voyage | null>(null);
const [status, setStatus] = useState<VoyageStatus | null>(null);