);
diff --git a/apps/web/src/features/ProductivePaceChartArea.tsx b/apps/web/src/features/ProductivePaceChartArea.tsx
index 711fc2d..1e9dab6 100644
--- a/apps/web/src/features/ProductivePaceChartArea.tsx
+++ b/apps/web/src/features/ProductivePaceChartArea.tsx
@@ -2,6 +2,7 @@ import { ChangeEvent, useState } from 'react';
import { DayRatioBar } from '../components/charts/DayRatioBar';
import { ProductivePaceChart } from '../components/charts/ProductivePaceChart';
+import { Input } from '../components/ui/input';
import { DEFAULT_PACE_IN_MIN } from '../policies/userConfig';
import { avgPaceOf, Log } from '../utils/PaceUtil';
import { loadFromStorage, saveToStorage } from '../utils/StorageUtil';
@@ -28,25 +29,35 @@ export const Area_ProductivePaceChart = ({
};
return (
-
-
);
};
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
new file mode 100644
index 0000000..b6a600e
--- /dev/null
+++ b/apps/web/src/lib/utils.ts
@@ -0,0 +1,3 @@
+import { ClassValue, clsx } from 'clsx';
+
+export const cn = (...inputs: ClassValue[]) => clsx(inputs);
diff --git a/apps/web/src/pages/LogWriterPage.tsx b/apps/web/src/pages/LogWriterPage.tsx
index 4c27e6a..303910e 100644
--- a/apps/web/src/pages/LogWriterPage.tsx
+++ b/apps/web/src/pages/LogWriterPage.tsx
@@ -1,12 +1,26 @@
-import { Bell, Timer } from 'lucide-react';
+import {
+ Bell,
+ Command,
+ Database,
+ Focus,
+ Timer,
+ TrendingUp,
+} from 'lucide-react';
import { lazy, Suspense, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AuthHeader } from '../components/auth/AuthHeader';
import { ConflictDialog } from '../components/common/ConflictDialog';
-import { DataManagementButton } from '../components/dataManagement/DataManagementButton';
import { DayNavigator } from '../components/days/DayNavigator';
import { TextLogContainer } from '../components/texts/TextLogContainer';
+import { Badge } from '../components/ui/badge';
+import { Button } from '../components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from '../components/ui/card';
import { Area_AvailableRestTimeChart } from '../features/AvailableRestTimeChartArea';
import { Area_ProductivePaceChart } from '../features/ProductivePaceChartArea';
import {
@@ -17,6 +31,7 @@ import { SoundSettingsDialog } from '../features/soundSettings';
import { ThemeSelector } from '../features/theme/ThemeSelector';
import { RootState } from '../store';
import { triggerCurrentDateFetch } from '../store/logs';
+import { minutesToTimeString } from '../utils/DateUtil';
const DataManagementDialog = lazy(() =>
import('../features/dataManagement/DataManagementDialog').then((module) => ({
@@ -24,74 +39,230 @@ const DataManagementDialog = lazy(() =>
})),
);
+const Metric = ({
+ eyebrow,
+ value,
+ detail,
+}: {
+ eyebrow: string;
+ value: string;
+ detail: string;
+}) => (
+
+
+
+ {eyebrow}
+
+
+ {value}
+
+ {detail}
+
+
+);
+
export const LogWriterPage = () => {
- // 휴식 알림 시스템 활성화
useRestNotification();
const dispatch = useDispatch();
-
const [isSoundSettingsOpen, setIsSoundSettingsOpen] = useState(false);
const [isDataManagementOpen, setIsDataManagementOpen] = useState(false);
- // 바로 다음 컴포넌트이니 직접 주입, redux 의존성 낮추기 위함.
- const logsForCharts = useSelector(
- (state: RootState) => state.logs.logsForCharts,
+ const { currentDate, logsForCharts } = useSelector(
+ (state: RootState) => state.logs,
);
-
- const { currentDate } = useSelector((state: RootState) => state.logs);
const currentNotification = useSelector(
(state: RootState) => state.restNotification.currentNotification,
);
const isAuthenticated = useSelector(
(state: RootState) => state.auth.isAuthenticated,
);
+ const { remainingTime, isOvertime } = useRemainingTime(currentNotification);
- // 로그인 상태에서 현재 날짜를 서버와 즉시 동기화
useEffect(() => {
- if (isAuthenticated) {
- dispatch(triggerCurrentDateFetch());
- }
+ if (isAuthenticated) dispatch(triggerCurrentDateFetch());
}, [dispatch, isAuthenticated]);
- // 잔여 시간 계산
- const { remainingTime, isOvertime } = useRemainingTime(currentNotification);
+ const latest = logsForCharts.at(-1);
+ const productive = latest?.productive ?? 0;
+ const wasted = latest?.wasted ?? 0;
+ const total = productive + wasted;
+ const balance = productive - wasted;
+ const averagePace = logsForCharts.length
+ ? Math.round(
+ logsForCharts.reduce((sum, log) => sum + log.pace, 0) /
+ logsForCharts.length,
+ )
+ : 0;
+ const metrics = {
+ productive,
+ wasted,
+ balance,
+ averagePace,
+ ratio: total ? Math.round((productive / total) * 100) : 0,
+ entries: Math.max(logsForCharts.length - 1, 0),
+ };
return (
-
-
-
-
- [기록지] ({currentDate})
-
- {currentNotification && (
-
+
+
+
+
+ MY/COMMIT
+
+
{currentDate}
+
+ desktop focus edition
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Daily control surface
+
+
+
+ 오늘의 시간을
+
+ 의도대로 남기세요.
+
+
+ / 입력 포커스
+ ⌥1 생산
+ ⌥2 소비
+ ⌘P 명령
+
- )}
-
-
-
-
-
-
setIsDataManagementOpen(true)} />
-
-
-
-
-
+
+
+
+
+ productive ratio
+
+
+
+
+
+ {metrics.ratio}
+ %
+
+
+ {metrics.entries}개 구간 기록
+ {currentNotification ? (
+
+
+ {isOvertime ? '휴식 초과' : '휴식 잔여'} {remainingTime}
+
+ ) : (
+ 휴식 타이머 없음
+ )}
+
+
+
+
+
+
+
+
+ = 0 ? 'secured' : 'overtime'}
+ value={minutesToTimeString(Math.abs(metrics.balance))}
+ detail={metrics.balance >= 0 ? '확보한 시간' : '초과한 시간'}
+ />
+
+
+
+
+
+
+
+
활동 기록
+
+ 생각의 흐름을 끊지 않고 바로 추가
+
+
+
+
+
+
+
+
+
+
+
+
+
+
시간 잔고
+
+ 생산 시간에서 소비 시간을 차감한 흐름
+
+
+
+
+
+
+
+
+
+
+
+ 생산 페이스
+
+ 목표 대비 시간당 생산 분량
+
+
+
+
+
+
+
+
+
+