refactor(i18n): 사용자 문구 참조를 중앙화

This commit is contained in:
2026-03-10 13:32:37 +09:00
parent 92a509ebb6
commit 1717f335f0
44 changed files with 433 additions and 515 deletions

View File

@@ -8,6 +8,7 @@ import {
import type { SceneTheme } from '@/entities/scene';
import { getSceneCardBackgroundStyle, type SceneAssetMap } from '@/entities/media';
import { SOUND_PRESETS, type TimerPreset } from '@/entities/session';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
import { Toggle } from '@/shared/ui';
@@ -58,6 +59,7 @@ export const ControlCenterSheetWidget = ({
onSelectProFeature,
onLockedClick,
}: ControlCenterSheetWidgetProps) => {
const { controlCenter } = copy.space;
const reducedMotion = useReducedMotion();
const isPro = plan === 'pro';
const interactiveMotionClass = reducedMotion
@@ -74,7 +76,7 @@ export const ControlCenterSheetWidget = ({
return (
<div className="space-y-4">
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
<SectionTitle title="Background" description={selectedScene?.name ?? '기본 배경'} />
<SectionTitle title={controlCenter.sectionTitles.background} description={selectedScene?.name ?? copy.common.defaultBackground} />
<div
className={cn(
'-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 snap-x snap-mandatory scrollbar-none',
@@ -116,7 +118,7 @@ export const ControlCenterSheetWidget = ({
</section>
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
<SectionTitle title="Time" description={selectedTimerLabel} />
<SectionTitle title={controlCenter.sectionTitles.time} description={selectedTimerLabel} />
<div className="grid grid-cols-3 gap-2">
{timerPresets.slice(0, 3).map((preset) => {
const selected = preset.label === selectedTimerLabel;
@@ -145,8 +147,8 @@ export const ControlCenterSheetWidget = ({
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
<SectionTitle
title="Sound"
description={SOUND_PRESETS.find((preset) => preset.id === selectedSoundPresetId)?.label ?? '기본'}
title={controlCenter.sectionTitles.sound}
description={SOUND_PRESETS.find((preset) => preset.id === selectedSoundPresetId)?.label ?? copy.common.default}
/>
<div className="grid grid-cols-3 gap-2">
{SOUND_PRESETS.slice(0, 6).map((preset) => {
@@ -175,12 +177,12 @@ export const ControlCenterSheetWidget = ({
</section>
<div className="space-y-1.5 rounded-xl border border-white/12 bg-white/[0.03] px-3 py-2.5">
<p className="text-[11px] text-white/58">: {sceneRecommendedSoundLabel} · {sceneRecommendedTimerLabel}</p>
<p className="text-[10px] text-white/48"> .</p>
<p className="text-[11px] text-white/58">{controlCenter.recommendation(sceneRecommendedSoundLabel, sceneRecommendedTimerLabel)}</p>
<p className="text-[10px] text-white/48">{controlCenter.recommendationHint}</p>
</div>
<section className="space-y-2 rounded-2xl border border-white/12 bg-black/18 p-3 backdrop-blur-md">
<SectionTitle title="Packs" description="확장/개인화" />
<SectionTitle title={controlCenter.sectionTitles.packs} description={controlCenter.packsDescription} />
<div className="space-y-1.5">
{PRO_FEATURE_CARDS.map((feature) => {
const locked = !isPro;
@@ -213,13 +215,13 @@ export const ControlCenterSheetWidget = ({
<section className="space-y-2 rounded-2xl border border-white/12 bg-black/18 px-3 py-2.5 backdrop-blur-md">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-[11px] text-white/72"> </p>
<p className="mt-0.5 text-[10px] text-white/52"> .</p>
<p className="text-[11px] text-white/72">{controlCenter.autoHideTitle}</p>
<p className="mt-0.5 text-[10px] text-white/52">{controlCenter.autoHideDescription}</p>
</div>
<Toggle
checked={autoHideControls}
onChange={onAutoHideControlsChange}
ariaLabel="컨트롤 자동 숨김"
ariaLabel={controlCenter.autoHideAriaLabel}
className="shrink-0"
/>
</div>

View File

@@ -5,10 +5,12 @@ import {
DEFAULT_PRESET_OPTIONS,
NOTIFICATION_INTENSITY_OPTIONS,
} from '@/shared/config/settingsOptions';
import { copy } from '@/shared/i18n';
import { useUserFocusPreferences } from '@/features/preferences';
import { cn } from '@/shared/lib/cn';
export const SettingsPanelWidget = () => {
const { settings } = copy;
const {
preferences,
isLoading,
@@ -22,12 +24,12 @@ export const SettingsPanelWidget = () => {
<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">
<div className="mx-auto w-full max-w-4xl px-4 pb-10 pt-6 sm:px-6">
<header className="mb-6 flex items-center justify-between rounded-xl border border-brand-dark/12 bg-white/72 px-4 py-3 backdrop-blur-sm">
<h1 className="text-xl font-semibold">Settings</h1>
<h1 className="text-xl font-semibold">{settings.title}</h1>
<Link
href="/app"
className="rounded-lg border border-brand-dark/16 px-3 py-1.5 text-xs text-brand-dark/82 transition hover:bg-white/90"
>
{copy.common.hub}
</Link>
</header>
@@ -35,13 +37,13 @@ export const SettingsPanelWidget = () => {
<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>
<h2 className="text-base font-semibold text-brand-dark">{settings.focusPreferencesApi}</h2>
<p className="mt-1 text-sm text-brand-dark/64">
{isLoading
? '저장된 설정을 불러오는 중이에요.'
? settings.loading
: isSaving
? '변경 사항을 저장하는 중이에요.'
: '변경 즉시 서버에 저장합니다.'}
? settings.saving
: settings.synced}
</p>
</div>
{saveStateLabel ? (
@@ -56,9 +58,9 @@ export const SettingsPanelWidget = () => {
<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>
<h2 className="text-base font-semibold text-brand-dark">Reduce Motion</h2>
<h2 className="text-base font-semibold text-brand-dark">{settings.reduceMotionTitle}</h2>
<p className="mt-1 text-sm text-brand-dark/64">
. (UI )
{settings.reduceMotionDescription}
</p>
</div>
<button
@@ -88,8 +90,8 @@ export const SettingsPanelWidget = () => {
</section>
<section className="rounded-xl border border-brand-dark/12 bg-white/78 p-4 backdrop-blur-sm">
<h2 className="text-base font-semibold text-brand-dark"> </h2>
<p className="mt-1 text-sm text-brand-dark/64"> / .</p>
<h2 className="text-base font-semibold text-brand-dark">{settings.notificationIntensityTitle}</h2>
<p className="mt-1 text-sm text-brand-dark/64">{settings.notificationIntensityDescription}</p>
<div className="mt-3 flex flex-wrap gap-2">
{NOTIFICATION_INTENSITY_OPTIONS.map((option) => (
<button
@@ -112,8 +114,8 @@ export const SettingsPanelWidget = () => {
</section>
<section className="rounded-xl border border-brand-dark/12 bg-white/78 p-4 backdrop-blur-sm">
<h2 className="text-base font-semibold text-brand-dark"> </h2>
<p className="mt-1 text-sm text-brand-dark/64"> .</p>
<h2 className="text-base font-semibold text-brand-dark">{settings.defaultPresetTitle}</h2>
<p className="mt-1 text-sm text-brand-dark/64">{settings.defaultPresetDescription}</p>
<div className="mt-3 space-y-2">
{DEFAULT_PRESET_OPTIONS.map((preset) => (
<button

View File

@@ -2,6 +2,7 @@
import type { FormEvent } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
interface GoalCompleteSheetProps {
@@ -12,12 +13,7 @@ interface GoalCompleteSheetProps {
onClose: () => void;
}
const GOAL_SUGGESTIONS = [
'리뷰 코멘트 2개 처리',
'문서 1문단 다듬기',
'이슈 1개 정리',
'메일 2개 회신',
];
const GOAL_SUGGESTIONS = copy.space.goalComplete.suggestions;
export const GoalCompleteSheet = ({
open,
@@ -53,10 +49,10 @@ export const GoalCompleteSheet = ({
const trimmed = currentGoal.trim();
if (!trimmed) {
return '다음 한 조각을 적어보세요';
return copy.space.goalComplete.placeholderFallback;
}
return `예: ${trimmed}`;
return copy.space.goalComplete.placeholderExample(trimmed);
}, [currentGoal]);
const canConfirm = draft.trim().length > 0;
@@ -82,14 +78,14 @@ export const GoalCompleteSheet = ({
<section className="pointer-events-auto w-[min(460px,94vw)] rounded-2xl border border-white/12 bg-black/26 px-3.5 py-3 text-white shadow-[0_14px_30px_rgba(2,6,23,0.28)] backdrop-blur-md">
<header className="flex items-start justify-between gap-2">
<div>
<h3 className="text-sm font-semibold text-white/92">. ?</h3>
<p className="mt-0.5 text-[11px] text-white/58"> , .</p>
<h3 className="text-sm font-semibold text-white/92">{copy.space.goalComplete.title}</h3>
<p className="mt-0.5 text-[11px] text-white/58">{copy.space.goalComplete.description}</p>
</div>
<button
type="button"
onClick={onClose}
className="inline-flex h-6 w-6 items-center justify-center rounded-full border border-white/16 bg-white/[0.05] text-[11px] text-white/72 transition-colors hover:bg-white/[0.12]"
aria-label="닫기"
aria-label={copy.space.goalComplete.closeAriaLabel}
>
</button>
@@ -120,17 +116,17 @@ export const GoalCompleteSheet = ({
<footer className="mt-3 flex items-center justify-end gap-2">
<button
type="button"
onClick={onRest}
className="rounded-full border border-white/18 bg-white/[0.05] px-3 py-1.5 text-xs text-white/74 transition-colors hover:bg-white/[0.11]"
>
</button>
onClick={onRest}
className="rounded-full border border-white/18 bg-white/[0.05] px-3 py-1.5 text-xs text-white/74 transition-colors hover:bg-white/[0.11]"
>
{copy.space.goalComplete.restButton}
</button>
<button
type="submit"
disabled={!canConfirm}
className="rounded-full border border-sky-200/42 bg-sky-300/84 px-3.5 py-1.5 text-xs font-semibold text-slate-900 transition-colors hover:bg-sky-300 disabled:cursor-not-allowed disabled:border-white/16 disabled:bg-white/[0.08] disabled:text-white/48"
>
{copy.space.goalComplete.confirmButton}
</button>
</footer>
</form>

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { copy } from '@/shared/i18n';
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
import { GoalCompleteSheet } from './GoalCompleteSheet';
@@ -44,7 +45,7 @@ export const SpaceFocusHudWidget = ({
const visibleRef = useRef(false);
const playbackStateRef = useRef<'running' | 'paused'>(playbackState);
const restReminderTimerRef = useRef<number | null>(null);
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '집중을 시작해요.';
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback;
useEffect(() => {
return () => {
@@ -58,7 +59,7 @@ export const SpaceFocusHudWidget = ({
useEffect(() => {
if (visible && !visibleRef.current && playbackState === 'running') {
onStatusMessage({
message: `이번 한 조각 · ${normalizedGoal}`,
message: copy.space.focusHud.goalToast(normalizedGoal),
});
}
@@ -68,7 +69,7 @@ export const SpaceFocusHudWidget = ({
useEffect(() => {
if (playbackStateRef.current === 'paused' && playbackState === 'running' && visible) {
onStatusMessage({
message: `이번 한 조각 · ${normalizedGoal}`,
message: copy.space.focusHud.goalToast(normalizedGoal),
});
}
@@ -115,7 +116,7 @@ export const SpaceFocusHudWidget = ({
}
restReminderTimerRef.current = window.setTimeout(() => {
onStatusMessage({ message: '5분이 지났어요. 다음 한 조각으로 돌아와요.' });
onStatusMessage({ message: copy.space.focusHud.restReminder });
restReminderTimerRef.current = null;
}, 5 * 60 * 1000);
}}

View File

@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
import type { SceneAssetMap } from '@/entities/media';
import type { SceneTheme } from '@/entities/scene';
import type { GoalChip, SoundPreset, TimerPreset } from '@/entities/session';
import { copy } from '@/shared/i18n';
import { SceneSelectCarousel } from '@/features/scene-select';
import { SessionGoalField } from '@/features/session-goal';
import { Button } from '@/shared/ui';
@@ -84,6 +85,7 @@ export const SpaceSetupDrawerWidget = ({
onStart,
resumeHint,
}: SpaceSetupDrawerWidgetProps) => {
const { setup } = copy.space;
const [openPopover, setOpenPopover] = useState<RitualPopover | null>(null);
const panelRef = useRef<HTMLDivElement | null>(null);
@@ -95,7 +97,7 @@ export const SpaceSetupDrawerWidget = ({
return (
soundPresets.find((preset) => preset.id === selectedSoundPresetId)?.label ??
soundPresets[0]?.label ??
'기본'
copy.common.default
);
}, [selectedSoundPresetId, soundPresets]);
@@ -148,21 +150,21 @@ export const SpaceSetupDrawerWidget = ({
return (
<section
className="fixed left-1/2 top-1/2 z-40 w-[min(428px,92vw)] -translate-x-1/2 -translate-y-1/2"
aria-label="집중 시작 패널"
aria-label={setup.panelAriaLabel}
>
<div
ref={panelRef}
className="rounded-3xl border border-white/14 bg-[linear-gradient(160deg,rgba(15,23,42,0.68)_0%,rgba(8,13,27,0.56)_100%)] p-4 text-white shadow-[0_22px_52px_rgba(2,6,23,0.38)] backdrop-blur-2xl sm:p-5"
>
<header className="mb-3 space-y-1">
<p className="text-[10px] uppercase tracking-[0.18em] text-white/48">Ritual</p>
<h1 className="text-[1.45rem] font-semibold leading-tight text-white"> .</h1>
<p className="text-xs text-white/60"> HUD의 .</p>
<p className="text-[10px] uppercase tracking-[0.18em] text-white/48">{setup.eyebrow}</p>
<h1 className="text-[1.45rem] font-semibold leading-tight text-white">{setup.title}</h1>
<p className="text-xs text-white/60">{setup.description}</p>
</header>
{resumeHint ? (
<div className="mb-3 rounded-2xl border border-white/14 bg-black/22 px-3 py-2.5">
<p className="text-[11px] text-white/62"> </p>
<p className="text-[11px] text-white/62">{setup.resumeTitle}</p>
<p className="mt-1 truncate text-sm text-white/88">{resumeHint.goal}</p>
<div className="mt-2 flex items-center justify-end gap-1.5">
<button
@@ -170,14 +172,14 @@ export const SpaceSetupDrawerWidget = ({
onClick={resumeHint.onStartFresh}
className="rounded-full border border-white/16 bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/72 transition-colors hover:bg-white/[0.1]"
>
{setup.startFresh}
</button>
<button
type="button"
onClick={resumeHint.onResume}
className="rounded-full border border-sky-200/34 bg-sky-200/14 px-2.5 py-1 text-[11px] text-white/90 transition-colors hover:bg-sky-200/22"
>
{setup.resumePrepare}
</button>
</div>
</div>
@@ -186,19 +188,19 @@ export const SpaceSetupDrawerWidget = ({
<div className="relative mb-3">
<div className="flex flex-wrap gap-1.5">
<SummaryChip
label="배경"
value={selectedScene?.name ?? '기본 배경'}
label={setup.sceneLabel}
value={selectedScene?.name ?? copy.common.defaultBackground}
open={openPopover === 'space'}
onClick={() => togglePopover('space')}
/>
<SummaryChip
label="타이머"
label={setup.timerLabel}
value={selectedTimerLabel}
open={openPopover === 'timer'}
onClick={() => togglePopover('timer')}
/>
<SummaryChip
label="사운드"
label={setup.soundLabel}
value={selectedSoundLabel}
open={openPopover === 'sound'}
onClick={() => togglePopover('sound')}
@@ -289,7 +291,7 @@ export const SpaceSetupDrawerWidget = ({
/>
<div className="space-y-1.5 pt-1">
{!canStart ? <p className="text-[10px] text-white/56"> .</p> : null}
{!canStart ? <p className="text-[10px] text-white/56">{setup.readyHint}</p> : null}
<Button
type="submit"
form="space-setup-ritual-form"
@@ -299,7 +301,7 @@ export const SpaceSetupDrawerWidget = ({
'h-10 rounded-xl !bg-sky-300/84 !text-slate-900 shadow-[0_8px_16px_rgba(125,211,252,0.24)] hover:!bg-sky-300 disabled:!bg-white/10 disabled:!text-white/42',
)}
>
{setup.openFocusScreen}
</Button>
</div>
</form>

View File

@@ -1,6 +1,7 @@
'use client';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
@@ -100,7 +101,7 @@ export const SpaceSideSheet = ({
{dismissible ? (
<button
type="button"
aria-label="시트 닫기"
aria-label={copy.modal.closeAriaLabel}
onClick={onClose}
className={cn(
'fixed inset-0 z-40 bg-slate-950/14 backdrop-blur-[1px] transition-opacity',
@@ -147,7 +148,7 @@ export const SpaceSideSheet = ({
<button
type="button"
onClick={onClose}
aria-label="닫기"
aria-label={copy.modal.closeButton}
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-white/14 bg-white/6 text-[12px] text-white/72 transition-colors hover:bg-white/12 hover:text-white"
>

View File

@@ -1,5 +1,6 @@
'use client';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
import {
RECOVERY_30S_MODE_LABEL,
@@ -26,11 +27,7 @@ interface SpaceTimerHudWidgetProps {
onGoalCompleteRequest?: () => void;
}
const HUD_ACTIONS = [
{ id: 'start', label: '시작', icon: '▶' },
{ id: 'pause', label: '일시정지', icon: '⏸' },
{ id: 'reset', label: '리셋', icon: '↺' },
] as const;
const HUD_ACTIONS = copy.space.timerHud.actions;
export const SpaceTimerHudWidget = ({
timerLabel,
@@ -51,14 +48,14 @@ export const SpaceTimerHudWidget = ({
onGoalCompleteRequest,
}: SpaceTimerHudWidgetProps) => {
const { isBreatheMode, triggerRestart } = useRestart30s();
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.';
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.timerHud.goalFallback;
const modeLabel = isBreatheMode
? RECOVERY_30S_MODE_LABEL
: !hasActiveSession
? 'Ready'
? copy.space.timerHud.readyMode
: sessionPhase === 'break'
? 'Break'
: 'Focus';
? copy.space.timerHud.breakMode
: copy.space.timerHud.focusMode;
return (
<div
@@ -105,7 +102,7 @@ export const SpaceTimerHudWidget = ({
</div>
<div className="mt-1.5 flex min-w-0 items-center gap-2">
<p className={cn('min-w-0 truncate text-sm', isImmersionMode ? 'text-white/88' : 'text-white/86')}>
<span className="text-white/62"> · </span>
<span className="text-white/62">{copy.space.timerHud.goalPrefix}</span>
<span className="text-white/90">{normalizedGoal}</span>
</p>
<button
@@ -113,7 +110,7 @@ export const SpaceTimerHudWidget = ({
onClick={onGoalCompleteRequest}
className="shrink-0 rounded-full border border-white/16 bg-white/[0.04] px-2 py-0.5 text-[10px] text-white/70 transition-colors hover:bg-white/[0.1] hover:text-white/86"
>
{copy.space.timerHud.completeButton}
</button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { cn } from '@/shared/lib/cn';
import { copy } from '@/shared/i18n';
import { formatThoughtCount, RAIL_ICON } from './constants';
interface FocusRightRailProps {
@@ -25,8 +26,8 @@ export const FocusRightRail = ({
<div className="flex flex-col gap-1">
<button
type="button"
aria-label="인박스 열기"
title="인박스"
aria-label={copy.space.inbox.openInboxAriaLabel}
title={copy.space.inbox.openInboxTitle}
onClick={onOpenInbox}
className="relative inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
>
@@ -39,8 +40,8 @@ export const FocusRightRail = ({
</button>
<button
type="button"
aria-label="Quick Controls 열기"
title="Quick Controls"
aria-label={copy.space.rightRail.openQuickControlsAriaLabel}
title={copy.space.rightRail.openQuickControlsTitle}
onClick={onOpenControlCenter}
className="inline-flex h-8 w-8 items-center justify-center rounded-xl border border-white/12 bg-white/[0.03] text-white/82 transition-colors hover:bg-white/10"
>

View File

@@ -4,6 +4,7 @@ import type { SceneAssetMap } from '@/entities/media';
import type { PlanTier } from '@/entities/plan';
import type { SceneTheme } from '@/entities/scene';
import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/session';
import { copy } from '@/shared/i18n';
import { ExitHoldButton } from '@/features/exit-hold';
import { ManagePlanSheetContent, PaywallSheetContent } from '@/features/paywall-sheet';
import { PlanPill } from '@/features/plan-pill';
@@ -75,6 +76,7 @@ export const SpaceToolsDockWidget = ({
onStatusMessage,
onExitRequested,
}: SpaceToolsDockWidgetProps) => {
const { toolsDock, controlCenter } = copy.space;
const [openPopover, setOpenPopover] = useState<SpaceAnchorPopoverId | null>(null);
const [utilityPanel, setUtilityPanel] = useState<SpaceUtilityPanelId | null>(null);
const [autoHideControls, setAutoHideControls] = useState(true);
@@ -86,7 +88,7 @@ export const SpaceToolsDockWidget = ({
const selectedSoundLabel = useMemo(() => {
return (
SOUND_PRESETS.find((preset) => preset.id === selectedPresetId)?.label ?? SOUND_PRESETS[0]?.label ?? '기본'
SOUND_PRESETS.find((preset) => preset.id === selectedPresetId)?.label ?? SOUND_PRESETS[0]?.label ?? copy.common.default
);
}, [selectedPresetId]);
@@ -233,11 +235,11 @@ export const SpaceToolsDockWidget = ({
setNoteDraft('');
onStatusMessage({
message: '인박스에 저장됨',
message: toolsDock.inboxSaved,
durationMs: 4200,
priority: 'undo',
action: {
label: '실행취소',
label: toolsDock.undo,
onClick: () => {
const removed = onDeleteThought(addedThought.id);
@@ -245,7 +247,7 @@ export const SpaceToolsDockWidget = ({
return;
}
onStatusMessage({ message: '저장 취소됨' });
onStatusMessage({ message: toolsDock.inboxSaveUndone });
},
},
});
@@ -263,14 +265,14 @@ export const SpaceToolsDockWidget = ({
}
onStatusMessage({
message: '삭제됨',
message: toolsDock.deleted,
durationMs: 4200,
priority: 'undo',
action: {
label: '실행취소',
label: toolsDock.undo,
onClick: () => {
onRestoreThought(removedThought);
onStatusMessage({ message: '삭제를 취소했어요.' });
onStatusMessage({ message: toolsDock.deleteUndone });
},
},
});
@@ -280,19 +282,19 @@ export const SpaceToolsDockWidget = ({
const snapshot = onClearInbox();
if (snapshot.length === 0) {
onStatusMessage({ message: '비울 항목이 없어요.' });
onStatusMessage({ message: toolsDock.emptyToClear });
return;
}
onStatusMessage({
message: '모두 비워짐',
message: toolsDock.clearedAll,
durationMs: 4200,
priority: 'undo',
action: {
label: '실행취소',
label: toolsDock.undo,
onClick: () => {
onRestoreThoughts(snapshot);
onStatusMessage({ message: '복원했어요.' });
onStatusMessage({ message: toolsDock.restored });
},
},
});
@@ -304,28 +306,28 @@ export const SpaceToolsDockWidget = ({
return;
}
onStatusMessage({ message: 'NORMAL 플랜 사용 중 · 잠금 항목에서만 업그레이드할 수 있어요.' });
onStatusMessage({ message: toolsDock.normalPlanInfo });
};
const handleLockedClick = (source: string) => {
onStatusMessage({ message: `${source}은(는) PRO 기능이에요.` });
onStatusMessage({ message: toolsDock.proFeatureLocked(source) });
openUtilityPanel('paywall');
};
const handleSelectProFeature = (featureId: string) => {
const label =
featureId === 'scene-packs'
? 'Scene Packs'
? toolsDock.featureLabels.scenePacks
: featureId === 'sound-packs'
? 'Sound Packs'
: 'Profiles';
? toolsDock.featureLabels.soundPacks
: toolsDock.featureLabels.profiles;
onStatusMessage({ message: `${label} 준비 중(더미)` });
onStatusMessage({ message: toolsDock.proFeaturePending(label) });
};
const handleStartPro = () => {
setPlan('pro');
onStatusMessage({ message: '결제(더미)' });
onStatusMessage({ message: toolsDock.purchaseMock });
openUtilityPanel('control-center');
};
@@ -370,7 +372,7 @@ export const SpaceToolsDockWidget = ({
{isFocusMode && openPopover ? (
<button
type="button"
aria-label="팝오버 닫기"
aria-label={toolsDock.popoverCloseAria}
onClick={() => setOpenPopover(null)}
className="fixed inset-0 z-30"
/>
@@ -417,7 +419,7 @@ export const SpaceToolsDockWidget = ({
className="inline-flex items-center gap-1.5 rounded-full border border-white/14 bg-black/24 px-2.5 py-1.5 text-[11px] text-white/88 backdrop-blur-md transition-opacity hover:opacity-100"
>
<span aria-hidden className="text-white/82">{ANCHOR_ICON.notes}</span>
<span>Notes {formatThoughtCount(thoughtCount)}</span>
<span>{toolsDock.notesButton} {formatThoughtCount(thoughtCount)}</span>
<span aria-hidden className="text-white/60"></span>
</button>
@@ -485,7 +487,7 @@ export const SpaceToolsDockWidget = ({
<SpaceSideSheet
open={isFocusMode && utilityPanel !== null}
title={utilityPanel ? UTILITY_PANEL_TITLE[utilityPanel] : ''}
subtitle={utilityPanel === 'control-center' ? '배경 · 타이머 · 사운드를 그 자리에서 바꿔요.' : undefined}
subtitle={utilityPanel === 'control-center' ? controlCenter.sideSheetSubtitle : undefined}
headerAction={
utilityPanel === 'control-center' ? (
<PlanPill plan={plan} onClick={handlePlanPillClick} />
@@ -539,8 +541,8 @@ export const SpaceToolsDockWidget = ({
{utilityPanel === 'manage-plan' ? (
<ManagePlanSheetContent
onClose={() => setUtilityPanel(null)}
onManage={() => onStatusMessage({ message: '구독 관리(더미)' })}
onRestore={() => onStatusMessage({ message: '구매 복원(더미)' })}
onManage={() => onStatusMessage({ message: toolsDock.manageSubscriptionMock })}
onRestore={() => onStatusMessage({ message: toolsDock.restorePurchaseMock })}
/>
) : null}
</SpaceSideSheet>

View File

@@ -1,4 +1,5 @@
import type { SpaceUtilityPanelId } from '../model/types';
import { copy } from '@/shared/i18n';
export const ANCHOR_ICON = {
sound: (
@@ -66,10 +67,10 @@ export const RAIL_ICON = {
};
export const UTILITY_PANEL_TITLE: Record<SpaceUtilityPanelId, string> = {
'control-center': 'Quick Controls',
inbox: '인박스',
paywall: 'PRO',
'manage-plan': '플랜 관리',
'control-center': copy.space.toolsDock.utilityPanelTitle['control-center'],
inbox: copy.space.toolsDock.utilityPanelTitle.inbox,
paywall: copy.space.toolsDock.utilityPanelTitle.paywall,
'manage-plan': copy.space.toolsDock.utilityPanelTitle['manage-plan'],
};
export const formatThoughtCount = (count: number) => {

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { RecentThought } from '@/entities/session';
import { copy } from '@/shared/i18n';
import { InboxList } from '@/features/inbox';
interface InboxToolPanelProps {
@@ -20,13 +21,13 @@ export const InboxToolPanel = ({
return (
<div className="relative space-y-3.5">
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-white/58"> </p>
<p className="text-xs text-white/58">{copy.space.inbox.readOnly}</p>
<button
type="button"
onClick={() => setConfirmOpen(true)}
className="rounded-full border border-white/20 bg-white/8 px-2.5 py-1 text-[11px] text-white/74 transition-colors hover:bg-white/14 hover:text-white"
>
{copy.space.inbox.clearAll}
</button>
</div>
<InboxList
@@ -38,15 +39,15 @@ export const InboxToolPanel = ({
{confirmOpen ? (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-2xl bg-slate-950/70 px-3 backdrop-blur-sm">
<div className="w-full max-w-[272px] rounded-2xl border border-white/14 bg-slate-900/92 p-3.5 shadow-xl shadow-slate-950/45">
<p className="text-sm font-medium text-white/92"> ?</p>
<p className="mt-1 text-[11px] text-white/60"> .</p>
<p className="text-sm font-medium text-white/92">{copy.space.inbox.clearConfirmTitle}</p>
<p className="mt-1 text-[11px] text-white/60">{copy.space.inbox.clearConfirmDescription}</p>
<div className="mt-3 flex justify-end gap-1.5">
<button
type="button"
onClick={() => setConfirmOpen(false)}
className="rounded-full border border-white/20 bg-white/8 px-2.5 py-1 text-[11px] text-white/74 transition-colors hover:bg-white/14 hover:text-white"
>
{copy.common.cancel}
</button>
<button
type="button"
@@ -56,7 +57,7 @@ export const InboxToolPanel = ({
}}
className="rounded-full border border-rose-200/34 bg-rose-200/16 px-2.5 py-1 text-[11px] text-rose-100/92 transition-colors hover:bg-rose-200/24"
>
{copy.space.inbox.clearButton}
</button>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import { useState } from 'react';
import type { SceneTheme } from '@/entities/scene';
import type { TimerPreset } from '@/entities/session';
import { copy } from '@/shared/i18n';
import { DEFAULT_PRESET_OPTIONS } from '@/shared/config/settingsOptions';
import { cn } from '@/shared/lib/cn';
@@ -23,6 +24,7 @@ export const SettingsToolPanel = ({
onSelectScene,
onSelectTimer,
}: SettingsToolPanelProps) => {
const { settingsPanel } = copy.space;
const [reduceMotion, setReduceMotion] = useState(false);
const [defaultPresetId, setDefaultPresetId] = useState<
(typeof DEFAULT_PRESET_OPTIONS)[number]['id']
@@ -33,8 +35,8 @@ export const SettingsToolPanel = ({
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium text-white">Reduce Motion</p>
<p className="mt-1 text-xs text-white/58"> .</p>
<p className="text-sm font-medium text-white">{settingsPanel.reduceMotion}</p>
<p className="mt-1 text-xs text-white/58">{settingsPanel.reduceMotionDescription}</p>
</div>
<button
type="button"
@@ -59,8 +61,8 @@ export const SettingsToolPanel = ({
</section>
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
<p className="text-sm font-medium text-white"></p>
<p className="mt-1 text-xs text-white/58"> scene을 .</p>
<p className="text-sm font-medium text-white">{settingsPanel.background}</p>
<p className="mt-1 text-xs text-white/58">{settingsPanel.backgroundDescription}</p>
<div className="mt-2 flex flex-wrap gap-2">
{scenes.slice(0, 4).map((scene) => {
const selected = scene.id === selectedSceneId;
@@ -85,8 +87,8 @@ export const SettingsToolPanel = ({
</section>
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
<p className="text-sm font-medium text-white"> </p>
<p className="mt-1 text-xs text-white/58"> .</p>
<p className="text-sm font-medium text-white">{settingsPanel.timerPreset}</p>
<p className="mt-1 text-xs text-white/58">{settingsPanel.timerPresetDescription}</p>
<div className="mt-2 flex flex-wrap gap-2">
{timerPresets.slice(0, 3).map((preset) => {
const selected = preset.label === selectedTimerLabel;
@@ -111,7 +113,7 @@ export const SettingsToolPanel = ({
</section>
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
<p className="text-sm font-medium text-white"> </p>
<p className="text-sm font-medium text-white">{settingsPanel.defaultPreset}</p>
<div className="mt-2 flex flex-wrap gap-2">
{DEFAULT_PRESET_OPTIONS.map((preset) => (
<button

View File

@@ -1,11 +1,12 @@
import { TODAY_STATS, WEEKLY_STATS } from '@/entities/session';
import { copy } from '@/shared/i18n';
export const StatsToolPanel = () => {
const previewStats = [TODAY_STATS[0], TODAY_STATS[1], WEEKLY_STATS[0], WEEKLY_STATS[2]];
return (
<div className="space-y-4">
<p className="text-xs text-white/58"> 7 .</p>
<p className="text-xs text-white/58">{copy.space.statsPanel.description}</p>
<section className="grid gap-2.5 sm:grid-cols-2">
{previewStats.map((stat) => (
@@ -19,7 +20,7 @@ export const StatsToolPanel = () => {
<section className="rounded-2xl border border-white/14 bg-white/6 p-3.5">
<div className="h-28 rounded-xl border border-dashed border-white/20 bg-[linear-gradient(180deg,rgba(148,163,184,0.14),rgba(148,163,184,0.02))]" />
<p className="mt-2 text-[11px] text-white/54"> </p>
<p className="mt-2 text-[11px] text-white/54">{copy.space.statsPanel.graphPlaceholder}</p>
</section>
</div>
);

View File

@@ -1,3 +1,5 @@
import { copy } from '@/shared/i18n';
interface QuickNotesPopoverProps {
noteDraft: string;
onDraftChange: (value: string) => void;
@@ -16,7 +18,7 @@ export const QuickNotesPopover = ({
className="mb-2 w-[min(320px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none"
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', left: 0 }}
>
<p className="text-[11px] text-white/56"> </p>
<p className="text-[11px] text-white/56">{copy.space.quickNotes.title}</p>
<div className="mt-2 flex gap-1.5">
<input
value={noteDraft}
@@ -29,7 +31,7 @@ export const QuickNotesPopover = ({
event.preventDefault();
onDraftEnter();
}}
placeholder="떠오른 생각을 잠깐 주차…"
placeholder={copy.space.quickNotes.placeholder}
className="h-8 min-w-0 flex-1 rounded-lg border border-white/14 bg-white/[0.04] px-2.5 text-xs text-white placeholder:text-white/38 focus:border-sky-200/42 focus:outline-none"
/>
<button
@@ -37,10 +39,10 @@ export const QuickNotesPopover = ({
onClick={onSubmit}
className="h-8 rounded-lg border border-sky-200/34 bg-sky-200/14 px-2.5 text-xs text-white/88"
>
{copy.space.quickNotes.submit}
</button>
</div>
<p className="mt-2 text-[11px] text-white/52"> .</p>
<p className="mt-2 text-[11px] text-white/52">{copy.space.quickNotes.hint}</p>
</div>
);
};

View File

@@ -1,6 +1,7 @@
import { cn } from '@/shared/lib/cn';
import type { SoundPreset } from '@/entities/session';
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
import { copy } from '@/shared/i18n';
interface QuickSoundPopoverProps {
selectedSoundLabel: string;
@@ -32,14 +33,14 @@ export const QuickSoundPopover = ({
className="mb-2 w-[min(288px,calc(100vw-2rem))] rounded-2xl border border-white/14 bg-slate-950/74 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none"
style={{ position: 'absolute', bottom: 'calc(100% + 0.5rem)', right: 0 }}
>
<p className="text-[11px] text-white/56"> </p>
<p className="text-[11px] text-white/56">{copy.space.quickSound.currentSound}</p>
<p className="mt-1 truncate text-sm font-medium text-white/88">{selectedSoundLabel}</p>
<div className="mt-3 rounded-xl border border-white/14 bg-white/[0.04] px-2.5 py-2">
<div className="flex items-center gap-2">
<button
type="button"
aria-label={isSoundMuted ? '음소거 해제' : '음소거'}
aria-label={isSoundMuted ? copy.space.quickSound.unmuteAriaLabel : copy.space.quickSound.muteAriaLabel}
onClick={onToggleMute}
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-white/16 bg-white/[0.05] text-xs text-white/80 transition-colors hover:bg-white/[0.12]"
>
@@ -53,7 +54,7 @@ export const QuickSoundPopover = ({
value={soundVolume}
onChange={(event) => onVolumeChange(Number(event.target.value))}
onKeyDown={onVolumeKeyDown}
aria-label="사운드 볼륨"
aria-label={copy.space.quickSound.volumeAriaLabel}
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-white/18 accent-sky-200"
/>
<span className="w-9 text-right text-[11px] text-white/66">
@@ -62,7 +63,7 @@ export const QuickSoundPopover = ({
</div>
</div>
<p className="mt-3 text-[11px] text-white/56"> </p>
<p className="mt-3 text-[11px] text-white/56">{copy.space.quickSound.quickSwitch}</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{quickSoundPresets.map((preset) => {
const selected = preset.id === selectedPresetId;

View File

@@ -22,6 +22,7 @@ import {
} from '@/entities/session';
import { useFocusSessionEngine } from '@/features/focus-session';
import { useSoundPlayback, useSoundPresetSelection } from '@/features/sound-preset';
import { copy } from '@/shared/i18n';
import { useHudStatusLine } from '@/shared/lib/useHudStatusLine';
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
@@ -436,7 +437,7 @@ export const SpaceWorkspaceWidget = () => {
setPendingSessionEntryPoint(entryPoint);
setPreviewPlaybackState('paused');
setWorkspaceMode('focus');
queuedFocusStatusMessageRef.current = '준비 완료 · 시작 버튼을 눌러 집중을 시작해요.';
queuedFocusStatusMessageRef.current = copy.space.workspace.readyToStart;
};
const startFocusFlow = async () => {
@@ -462,7 +463,7 @@ export const SpaceWorkspaceWidget = () => {
setPreviewPlaybackState('paused');
pushStatusLine({
message: '세션을 시작하지 못했어요. 잠시 후 다시 시도해 주세요.',
message: copy.space.workspace.startFailed,
});
};
@@ -488,7 +489,7 @@ export const SpaceWorkspaceWidget = () => {
if (!resumedSession) {
pushStatusLine({
message: '세션을 다시 시작하지 못했어요.',
message: copy.space.workspace.resumeFailed,
});
}
};
@@ -498,7 +499,7 @@ export const SpaceWorkspaceWidget = () => {
if (!didAbandon) {
pushStatusLine({
message: '세션 종료를 완료하지 못했어요.',
message: copy.space.workspace.abandonFailed,
});
return;
}
@@ -518,7 +519,7 @@ export const SpaceWorkspaceWidget = () => {
if (!pausedSession) {
pushStatusLine({
message: '세션을 일시정지하지 못했어요.',
message: copy.space.workspace.pauseFailed,
});
}
};
@@ -532,13 +533,13 @@ export const SpaceWorkspaceWidget = () => {
if (!restartedSession) {
pushStatusLine({
message: '현재 페이즈를 다시 시작하지 못했어요.',
message: copy.space.workspace.restartFailed,
});
return;
}
pushStatusLine({
message: '현재 페이즈를 처음부터 다시 시작했어요.',
message: copy.space.workspace.restarted,
});
};
@@ -557,7 +558,7 @@ export const SpaceWorkspaceWidget = () => {
if (!completedSession) {
pushStatusLine({
message: '현재 세션 완료를 서버에 반영하지 못했어요.',
message: copy.space.workspace.goalCompleteSyncFailed,
});
return;
}
@@ -568,7 +569,7 @@ export const SpaceWorkspaceWidget = () => {
setPendingSessionEntryPoint('goal-complete');
setPreviewPlaybackState('paused');
pushStatusLine({
message: '다음 한 조각 준비 완료 · 시작 버튼을 눌러 이어가요.',
message: copy.space.workspace.nextGoalReady,
});
};

View File

@@ -1,6 +1,7 @@
'use client';
import Link from 'next/link';
import { copy } from '@/shared/i18n';
import { useFocusStats } from '@/features/stats';
const StatSection = ({
@@ -42,45 +43,46 @@ const formatMinutes = (minutes: number) => {
};
export const StatsOverviewWidget = () => {
const { stats } = copy;
const { summary, isLoading, error, source, refetch } = useFocusStats();
const todayItems = [
{
id: 'today-focus',
label: '오늘 집중 시간',
label: stats.todayFocus,
value: formatMinutes(summary.today.focusMinutes),
delta: source === 'api' ? 'API' : 'Mock',
delta: source === 'api' ? stats.apiLabel : stats.mockLabel,
},
{
id: 'today-cycles',
label: '완료한 사이클',
value: `${summary.today.completedCycles}`,
label: stats.completedCycles,
value: `${summary.today.completedCycles}${stats.countUnit}`,
delta: `${summary.today.completedCycles > 0 ? '+' : ''}${summary.today.completedCycles}`,
},
{
id: 'today-entry',
label: '입장 횟수',
value: `${summary.today.sessionEntries}`,
delta: source === 'api' ? '동기화됨' : '임시값',
label: stats.sessionEntries,
value: `${summary.today.sessionEntries}${stats.countUnit}`,
delta: source === 'api' ? stats.syncedApi : stats.temporary,
},
];
const weeklyItems = [
{
id: 'week-focus',
label: '최근 7일 집중 시간',
label: stats.last7DaysFocus,
value: formatMinutes(summary.last7Days.focusMinutes),
delta: source === 'api' ? '실집계' : '목업',
delta: source === 'api' ? stats.actualAggregate : stats.mockAggregate,
},
{
id: 'week-best-day',
label: '최고 몰입일',
label: stats.bestDay,
value: summary.last7Days.bestDayLabel,
delta: formatMinutes(summary.last7Days.bestDayFocusMinutes),
},
{
id: 'week-consistency',
label: '연속 달성',
value: `${summary.last7Days.streakDays}`,
delta: summary.last7Days.streakDays > 0 ? '유지 중' : '시작 전',
label: stats.streak,
value: `${summary.last7Days.streakDays}${stats.dayUnit}`,
delta: summary.last7Days.streakDays > 0 ? stats.streakActive : stats.streakStart,
},
];
@@ -88,12 +90,12 @@ export const StatsOverviewWidget = () => {
<div className="min-h-screen bg-[radial-gradient(circle_at_18%_0%,rgba(167,204,237,0.45),transparent_50%),radial-gradient(circle_at_88%_8%,rgba(191,219,254,0.4),transparent_42%),linear-gradient(170deg,#f8fafc_0%,#eef4fb_52%,#e9f1fa_100%)] text-brand-dark">
<div className="mx-auto w-full max-w-6xl px-4 pb-10 pt-6 sm:px-6">
<header className="mb-6 flex items-center justify-between rounded-xl border border-brand-dark/12 bg-white/72 px-4 py-3 backdrop-blur-sm">
<h1 className="text-xl font-semibold">Stats</h1>
<h1 className="text-xl font-semibold">{stats.title}</h1>
<Link
href="/app"
className="rounded-lg border border-brand-dark/16 px-3 py-1.5 text-xs text-brand-dark/82 transition hover:bg-white/90"
>
{copy.common.hub}
</Link>
</header>
@@ -102,13 +104,13 @@ export const StatsOverviewWidget = () => {
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-xs font-medium text-brand-dark/72">
{source === 'api' ? 'API 통계 사용 중' : 'API 실패로 mock 통계 표시 중'}
{source === 'api' ? stats.sourceApi : stats.sourceMock}
</p>
{error ? (
<p className="mt-1 text-xs text-rose-500">{error}</p>
) : (
<p className="mt-1 text-xs text-brand-dark/56">
{isLoading ? '통계를 불러오는 중이에요.' : '화면 진입 시 최신 요약을 동기화합니다.'}
{isLoading ? stats.loading : stats.synced}
</p>
)}
</div>
@@ -119,16 +121,16 @@ export const StatsOverviewWidget = () => {
}}
className="rounded-lg border border-brand-dark/16 px-3 py-1.5 text-xs text-brand-dark/82 transition hover:bg-white/90"
>
{stats.refresh}
</button>
</div>
</section>
<StatSection title="오늘" items={todayItems} />
<StatSection title="최근 7일" items={weeklyItems} />
<StatSection title={stats.today} items={todayItems} />
<StatSection title={stats.last7Days} items={weeklyItems} />
<section className="space-y-3">
<h2 className="text-lg font-semibold text-brand-dark"> </h2>
<h2 className="text-lg font-semibold text-brand-dark">{stats.chartTitle}</h2>
<div className="rounded-xl border border-dashed border-brand-dark/18 bg-white/65 p-5">
<div className="h-52 rounded-lg border border-brand-dark/12 bg-[linear-gradient(180deg,rgba(148,163,184,0.15),rgba(148,163,184,0.04))] p-4">
{summary.trend.length > 0 ? (
@@ -141,7 +143,7 @@ export const StatsOverviewWidget = () => {
<div
className="w-full rounded-md bg-brand-primary/55"
style={{ height: `${barHeight}%` }}
title={`${point.date} · ${point.focusMinutes}`}
title={stats.barTitle(point.date, point.focusMinutes)}
/>
<span className="text-[10px] text-brand-dark/56">
{point.date.slice(5)}
@@ -154,8 +156,8 @@ export const StatsOverviewWidget = () => {
</div>
<p className="mt-3 text-xs text-brand-dark/56">
{summary.trend.length > 0
? 'trend 응답으로 간단한 막대 그래프를 렌더링합니다.'
: 'trend 응답이 비어 있어 플레이스홀더 상태입니다.'}
? stats.chartWithTrend
: stats.chartWithoutTrend}
</p>
</div>
</section>