From a2c443fff15ebc9c585e2403bf97cfdaa71aadcb Mon Sep 17 00:00:00 2001 From: Seongbin Kim Date: Mon, 13 Jul 2026 19:16:03 +0900 Subject: [PATCH] =?UTF-8?q?feat(web):=20=ED=9D=91=EB=B0=B1=20=ED=8F=AC?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=20=EC=9B=8C=ED=81=AC=EC=8A=A4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=8B=9C=EC=95=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/components.json | 17 ++ apps/web/src/App.css | 80 ++++++ .../charts/AvailableRestTimeChart/chart.tsx | 57 +++- .../charts/AvailableRestTimeChart/points.tsx | 10 +- .../web/src/components/charts/DayRatioBar.tsx | 6 +- .../components/charts/ProductivePaceChart.tsx | 66 +++-- apps/web/src/components/days/DayNavigator.tsx | 37 ++- .../src/components/texts/ProductiveToggle.tsx | 18 +- .../src/components/texts/TextLogContainer.tsx | 206 +++++++++----- apps/web/src/components/texts/TimeSummary.tsx | 37 +-- apps/web/src/components/ui/badge.tsx | 16 ++ apps/web/src/components/ui/button.tsx | 42 +++ apps/web/src/components/ui/card.tsx | 43 +++ apps/web/src/components/ui/input.tsx | 23 ++ .../features/AvailableRestTimeChartArea.tsx | 9 +- .../src/features/ProductivePaceChartArea.tsx | 41 ++- apps/web/src/lib/utils.ts | 3 + apps/web/src/pages/LogWriterPage.tsx | 269 ++++++++++++++---- 18 files changed, 770 insertions(+), 210 deletions(-) create mode 100644 apps/web/components.json create mode 100644 apps/web/src/components/ui/badge.tsx create mode 100644 apps/web/src/components/ui/button.tsx create mode 100644 apps/web/src/components/ui/card.tsx create mode 100644 apps/web/src/components/ui/input.tsx create mode 100644 apps/web/src/lib/utils.ts diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 0000000..f9a8534 --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/App.css", + "baseColor": "neutral", + "cssVariables": false + }, + "aliases": { + "components": "./src/components", + "utils": "./src/lib/utils", + "ui": "./src/components/ui" + } +} diff --git a/apps/web/src/App.css b/apps/web/src/App.css index 8363bb1..2e17660 100644 --- a/apps/web/src/App.css +++ b/apps/web/src/App.css @@ -2,6 +2,86 @@ @tailwind components; @tailwind utilities; +@layer base { + html, + body, + #root { + min-height: 100%; + } + + body { + margin: 0; + } + + ::selection { + background: #0a0a0a; + color: #fff; + } +} + +.monochrome-shell { + font-family: 'Avenir Next', 'Century Gothic', sans-serif; +} + +.monochrome-shell > div > section, +.monochrome-shell > div > main { + animation: focus-rise 420ms ease-out both; +} + +.monochrome-shell > div > section:nth-of-type(2) { + animation-delay: 80ms; +} + +.monochrome-shell > div > main { + animation-delay: 140ms; +} + +.monochrome-shell header .dropdown > .btn, +.monochrome-shell header .btn-circle { + min-height: 2.25rem; + height: 2.25rem; + width: 2.25rem; + border-radius: 0; + background: transparent; + color: #0a0a0a; +} + +.monochrome-shell header .dropdown-content { + border: 1px solid #171717; + border-radius: 0; + background: #fff; + color: #171717; +} + +.monochrome-log-editor { + background-image: repeating-linear-gradient( + to bottom, + transparent 0, + transparent 27px, + rgb(0 0 0 / 7%) 27px, + rgb(0 0 0 / 7%) 28px + ); + background-position-y: 15px; +} + +@keyframes focus-rise { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .monochrome-shell > div > section, + .monochrome-shell > div > main { + animation: none; + } +} + @keyframes shake { 0% { transform: translateX(0); diff --git a/apps/web/src/components/charts/AvailableRestTimeChart/chart.tsx b/apps/web/src/components/charts/AvailableRestTimeChart/chart.tsx index bf4e2dd..8d8915e 100644 --- a/apps/web/src/components/charts/AvailableRestTimeChart/chart.tsx +++ b/apps/web/src/components/charts/AvailableRestTimeChart/chart.tsx @@ -2,6 +2,7 @@ import { Area, AreaChart, CartesianGrid, + ReferenceLine, ResponsiveContainer, Tooltip, XAxis, @@ -31,46 +32,78 @@ export const AvailableRestTimeChart = ({ const gradientId = 'availableRestTimeSplitColor'; + if (data.length < 2) { + return ( +
+ 기록을 두 개 이상 추가하면 시간 잔고가 표시됩니다 +
+ ); + } + return ( - + - + `${value}`} /> [`${value}분`, '초과 휴식']} + formatter={(value: number | string) => [`${value}분`, '소비 − 생산']} + contentStyle={{ + border: '1px solid #171717', + borderRadius: 0, + boxShadow: 'none', + fontSize: 11, + }} /> + - - + + {getPoints(data)} diff --git a/apps/web/src/components/charts/AvailableRestTimeChart/points.tsx b/apps/web/src/components/charts/AvailableRestTimeChart/points.tsx index 0016284..05290f2 100644 --- a/apps/web/src/components/charts/AvailableRestTimeChart/points.tsx +++ b/apps/web/src/components/charts/AvailableRestTimeChart/points.tsx @@ -11,7 +11,7 @@ export const getPoints = (data: ChartDataPoint[]) => { const points: ReactElement[] = []; if (highPoint) { - const color = highPoint.need >= 0 ? 'red' : 'green'; + const color = highPoint.need >= 0 ? '#171717' : '#737373'; points.push( { } if (lowPoint) { - const color = lowPoint.need >= 0 ? 'red' : 'green'; + const color = lowPoint.need >= 0 ? '#171717' : '#737373'; points.push( = 0 ? 'red' : 'green'; + const color = currPoint.need >= 0 ? '#171717' : '#737373'; const position = currPoint.need >= 0 ? 'top' : 'bottom'; return { point: currPoint, shouldShow, color, position }; diff --git a/apps/web/src/components/charts/DayRatioBar.tsx b/apps/web/src/components/charts/DayRatioBar.tsx index 4812d71..807f43d 100644 --- a/apps/web/src/components/charts/DayRatioBar.tsx +++ b/apps/web/src/components/charts/DayRatioBar.tsx @@ -8,7 +8,7 @@ export const DayRatioBar = ({ logs }: { logs: Log[] }) => { if (isEmpty || totalDuration <= 0) { return ( -
+
); } @@ -36,14 +36,14 @@ export const DayRatioBar = ({ logs }: { logs: Log[] }) => { return (
{blocks.map((block, idx) => (
{ + if (data.length < 2) { + return ( +
+ 기록을 두 개 이상 추가하면 페이스가 표시됩니다 +
+ ); + } + return ( - + - + - - o.pace} unit="min/h" - stroke="darkgreen" - fill="green" + stroke="#171717" + strokeWidth={1.5} + fill="#d4d4d4" /> diff --git a/apps/web/src/components/days/DayNavigator.tsx b/apps/web/src/components/days/DayNavigator.tsx index ecb1d13..23bcc39 100644 --- a/apps/web/src/components/days/DayNavigator.tsx +++ b/apps/web/src/components/days/DayNavigator.tsx @@ -1,6 +1,7 @@ import { useDispatch } from 'react-redux'; import { goToNextDate, goToPrevDate, goToToday } from '../../store/logs'; +import { Button } from '../ui/button'; const PREV_DAY_BUTTON_TEXT = '←'; const TODAY_BUTTON_TEXT = '오늘'; @@ -13,19 +14,33 @@ export const DayNavigator = () => { const handleTomorrowButton = () => dispatch(goToNextDate()); return ( -
- - + - + {TODAY_BUTTON_TEXT} + +
); }; diff --git a/apps/web/src/components/texts/ProductiveToggle.tsx b/apps/web/src/components/texts/ProductiveToggle.tsx index b1c9016..72cbf89 100644 --- a/apps/web/src/components/texts/ProductiveToggle.tsx +++ b/apps/web/src/components/texts/ProductiveToggle.tsx @@ -14,10 +14,10 @@ export const ProductiveToggle = ({ onKeyDown, checkboxRef, }: ProductiveToggleProps) => ( -