feat(api): 세션·통계·설정 API 연동 기반을 추가
맥락: - 실제 세션 엔진과 통계·설정 저장을 백엔드와 연결할 프론트 API 경계를 먼저 정리할 필요가 있었다. 변경사항: - focus session, stats, preferences API 계층과 타입을 추가하고 메서드 주석에 Backend Codex 지시 사항을 작성했다. - /space를 현재 세션 조회, 시작, 일시정지, 재개, 다시 시작, 완료, 종료 API 흐름에 연결하고 API 실패 시 로컬 미리보기 fallback을 유지했다. - /stats와 /settings를 API 기반 fetch/save 구조로 전환하고 auth/apiClient를 보강했다. - React 19 규칙에 맞게 관련 훅과 HUD/시트 구현을 정리해 lint/build가 통과하도록 보정했다. 검증: - npm run lint - npm run build 세션-상태: 프론트에서 세션·통계·설정 API를 호출할 준비가 된 상태 세션-다음: 백엔드가 주석에 맞춘 엔드포인트와 응답 스키마를 구현하도록 협업 세션-리스크: 실제 서버 응답 필드명이 현재 타입과 다르면 프론트 매핑 조정이 추가로 필요
This commit is contained in:
@@ -10,9 +10,15 @@ import {
|
||||
interface SpaceTimerHudWidgetProps {
|
||||
timerLabel: string;
|
||||
goal: string;
|
||||
timeDisplay?: string;
|
||||
className?: string;
|
||||
sessionPhase?: 'focus' | 'break' | null;
|
||||
playbackState?: 'running' | 'paused' | null;
|
||||
isControlsDisabled?: boolean;
|
||||
isImmersionMode?: boolean;
|
||||
onPlaybackStateChange?: (state: 'running' | 'paused') => void;
|
||||
onStartClick?: () => void;
|
||||
onPauseClick?: () => void;
|
||||
onResetClick?: () => void;
|
||||
onGoalCompleteRequest?: () => void;
|
||||
}
|
||||
|
||||
@@ -25,13 +31,24 @@ const HUD_ACTIONS = [
|
||||
export const SpaceTimerHudWidget = ({
|
||||
timerLabel,
|
||||
goal,
|
||||
timeDisplay = '25:00',
|
||||
className,
|
||||
sessionPhase = 'focus',
|
||||
playbackState = 'running',
|
||||
isControlsDisabled = false,
|
||||
isImmersionMode = false,
|
||||
onPlaybackStateChange,
|
||||
onStartClick,
|
||||
onPauseClick,
|
||||
onResetClick,
|
||||
onGoalCompleteRequest,
|
||||
}: SpaceTimerHudWidgetProps) => {
|
||||
const { isBreatheMode, triggerRestart } = useRestart30s();
|
||||
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.';
|
||||
const modeLabel = isBreatheMode
|
||||
? RECOVERY_30S_MODE_LABEL
|
||||
: sessionPhase === 'break'
|
||||
? 'Break'
|
||||
: 'Focus';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -62,7 +79,7 @@ export const SpaceTimerHudWidget = ({
|
||||
isImmersionMode ? 'text-white/90' : 'text-white/88',
|
||||
)}
|
||||
>
|
||||
{isBreatheMode ? RECOVERY_30S_MODE_LABEL : 'Focus'}
|
||||
{modeLabel}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -70,7 +87,7 @@ export const SpaceTimerHudWidget = ({
|
||||
isImmersionMode ? 'text-white/90' : 'text-white/92',
|
||||
)}
|
||||
>
|
||||
25:00
|
||||
{timeDisplay}
|
||||
</span>
|
||||
<span className={cn('text-[11px]', isImmersionMode ? 'text-white/65' : 'text-white/65')}>
|
||||
{timerLabel}
|
||||
@@ -98,20 +115,31 @@ export const SpaceTimerHudWidget = ({
|
||||
key={action.id}
|
||||
type="button"
|
||||
title={action.label}
|
||||
disabled={isControlsDisabled}
|
||||
onClick={() => {
|
||||
if (action.id === 'start') {
|
||||
onPlaybackStateChange?.('running');
|
||||
onStartClick?.();
|
||||
}
|
||||
|
||||
if (action.id === 'pause') {
|
||||
onPlaybackStateChange?.('paused');
|
||||
onPauseClick?.();
|
||||
}
|
||||
|
||||
if (action.id === 'reset') {
|
||||
onResetClick?.();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-8 w-8 items-center justify-center rounded-full border text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80',
|
||||
'inline-flex h-8 w-8 items-center justify-center rounded-full border text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80 disabled:cursor-not-allowed disabled:opacity-45',
|
||||
isImmersionMode
|
||||
? 'border-white/14 bg-black/26 text-white/82 hover:bg-black/34'
|
||||
: 'border-white/14 bg-black/26 text-white/84 hover:bg-black/34',
|
||||
action.id === 'start' && playbackState === 'running'
|
||||
? 'border-sky-200/42 bg-sky-200/18 text-white'
|
||||
: '',
|
||||
action.id === 'pause' && playbackState === 'paused'
|
||||
? 'border-amber-200/42 bg-amber-200/16 text-white'
|
||||
: '',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden>{action.icon}</span>
|
||||
|
||||
Reference in New Issue
Block a user