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:
@@ -1,20 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
DEFAULT_PRESET_OPTIONS,
|
||||
NOTIFICATION_INTENSITY_OPTIONS,
|
||||
} from '@/shared/config/settingsOptions';
|
||||
import { useUserFocusPreferences } from '@/features/preferences';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
|
||||
export const SettingsPanelWidget = () => {
|
||||
const [reduceMotion, setReduceMotion] = useState(false);
|
||||
const [notificationIntensity, setNotificationIntensity] =
|
||||
useState<(typeof NOTIFICATION_INTENSITY_OPTIONS)[number]>('기본');
|
||||
const [defaultPresetId, setDefaultPresetId] = useState<
|
||||
(typeof DEFAULT_PRESET_OPTIONS)[number]['id']
|
||||
>(DEFAULT_PRESET_OPTIONS[0].id);
|
||||
const {
|
||||
preferences,
|
||||
isLoading,
|
||||
isSaving,
|
||||
error,
|
||||
saveStateLabel,
|
||||
updatePreferences,
|
||||
} = useUserFocusPreferences();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_82%_0%,rgba(167,204,237,0.42),transparent_50%),radial-gradient(circle_at_12%_8%,rgba(191,219,254,0.4),transparent_46%),linear-gradient(170deg,#f8fafc_0%,#eef4fb_54%,#e8f1fa_100%)] text-brand-dark">
|
||||
@@ -30,6 +32,27 @@ export const SettingsPanelWidget = () => {
|
||||
</header>
|
||||
|
||||
<div className="space-y-4">
|
||||
<section className="rounded-xl border border-brand-dark/12 bg-white/78 p-4 backdrop-blur-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-brand-dark">Focus Preferences API</h2>
|
||||
<p className="mt-1 text-sm text-brand-dark/64">
|
||||
{isLoading
|
||||
? '저장된 설정을 불러오는 중이에요.'
|
||||
: isSaving
|
||||
? '변경 사항을 저장하는 중이에요.'
|
||||
: '변경 즉시 서버에 저장합니다.'}
|
||||
</p>
|
||||
</div>
|
||||
{saveStateLabel ? (
|
||||
<span className="rounded-full border border-brand-dark/14 bg-white/75 px-2.5 py-1 text-[11px] text-brand-dark/72">
|
||||
{saveStateLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? <p className="mt-3 text-sm text-rose-500">{error}</p> : null}
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-brand-dark/12 bg-white/78 p-4 backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
@@ -41,11 +64,15 @@ export const SettingsPanelWidget = () => {
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={reduceMotion}
|
||||
onClick={() => setReduceMotion((current) => !current)}
|
||||
aria-checked={preferences.reduceMotion}
|
||||
onClick={() => {
|
||||
void updatePreferences({
|
||||
reduceMotion: !preferences.reduceMotion,
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex w-16 items-center rounded-full border px-1 py-1 transition-colors',
|
||||
reduceMotion
|
||||
preferences.reduceMotion
|
||||
? 'border-brand-primary/45 bg-brand-soft/60'
|
||||
: 'border-brand-dark/20 bg-white/85',
|
||||
)}
|
||||
@@ -53,7 +80,7 @@ export const SettingsPanelWidget = () => {
|
||||
<span
|
||||
className={cn(
|
||||
'h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200 motion-reduce:transition-none',
|
||||
reduceMotion ? 'translate-x-9' : 'translate-x-0',
|
||||
preferences.reduceMotion ? 'translate-x-9' : 'translate-x-0',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
@@ -68,10 +95,12 @@ export const SettingsPanelWidget = () => {
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => setNotificationIntensity(option)}
|
||||
onClick={() => {
|
||||
void updatePreferences({ notificationIntensity: option });
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1.5 text-xs transition-colors',
|
||||
notificationIntensity === option
|
||||
preferences.notificationIntensity === option
|
||||
? 'border-brand-primary/45 bg-brand-soft/60 text-brand-dark'
|
||||
: 'border-brand-dark/18 bg-white/75 text-brand-dark/78 hover:bg-white',
|
||||
)}
|
||||
@@ -90,10 +119,12 @@ export const SettingsPanelWidget = () => {
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => setDefaultPresetId(preset.id)}
|
||||
onClick={() => {
|
||||
void updatePreferences({ defaultPresetId: preset.id });
|
||||
}}
|
||||
className={cn(
|
||||
'w-full rounded-lg border px-3 py-2 text-left text-sm transition-colors',
|
||||
defaultPresetId === preset.id
|
||||
preferences.defaultPresetId === preset.id
|
||||
? 'border-brand-primary/45 bg-brand-soft/58 text-brand-dark'
|
||||
: 'border-brand-dark/16 bg-white/72 text-brand-dark/82 hover:bg-white',
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user