From f290afa4f06498d36d5917c8aff806aa5fb93a2d Mon Sep 17 00:00:00 2001 From: Seongbin Kim Date: Mon, 13 Jul 2026 19:19:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(web):=20=EC=BA=84=20=EB=B2=A4=ED=86=A0=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=8A=A4=ED=8A=9C=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=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/package.json | 1 + apps/web/src/App.css | 117 +++++++ .../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 | 25 +- .../src/components/texts/TextLogContainer.tsx | 209 +++++++++---- apps/web/src/components/texts/TimeSummary.tsx | 37 +-- apps/web/src/components/ui/badge.tsx | 16 + apps/web/src/components/ui/button.tsx | 41 +++ apps/web/src/components/ui/card.tsx | 46 +++ apps/web/src/components/ui/input.tsx | 23 ++ .../features/AvailableRestTimeChartArea.tsx | 9 +- .../src/features/ProductivePaceChartArea.tsx | 41 ++- apps/web/src/lib/utils.ts | 4 + apps/web/src/pages/LogWriterPage.tsx | 296 +++++++++++++++--- pnpm-lock.yaml | 8 + 20 files changed, 851 insertions(+), 215 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..9061a56 --- /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": "stone", + "cssVariables": false + }, + "aliases": { + "components": "./src/components", + "utils": "./src/lib/utils", + "ui": "./src/components/ui" + } +} diff --git a/apps/web/package.json b/apps/web/package.json index 3ebed56..16ec7af 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,7 @@ "react-redux": "^9.2.0", "react-router-dom": "^6.15.0", "recharts": "^2.5.0", + "tailwind-merge": "^3.5.0", "xlsx": "^0.18.5" }, "devDependencies": { diff --git a/apps/web/src/App.css b/apps/web/src/App.css index 8363bb1..9d47b7c 100644 --- a/apps/web/src/App.css +++ b/apps/web/src/App.css @@ -2,6 +2,123 @@ @tailwind components; @tailwind utilities; +@layer base { + html, + body, + #root { + min-height: 100%; + } + + body { + margin: 0; + background: #edf0e6; + } + + ::selection { + background: #b9c8a3; + color: #28362c; + } +} + +.calm-shell { + font-family: 'Avenir Next', 'Trebuchet MS', sans-serif; +} + +.calm-shell .font-serif { + font-family: 'Iowan Old Style', 'Palatino Linotype', Palatino, serif; +} + +.calm-orb { + position: absolute; + border-radius: 9999px; + filter: blur(2px); + pointer-events: none; +} + +.calm-orb-one { + top: 70px; + right: -160px; + width: 430px; + height: 430px; + background: rgb(244 168 143 / 22%); +} + +.calm-orb-two { + bottom: 120px; + left: -180px; + width: 470px; + height: 470px; + background: rgb(185 200 163 / 33%); +} + +.calm-shell section > *, +.calm-shell main > * { + animation: calm-arrive 480ms cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.calm-shell section > *:nth-child(2), +.calm-shell main > *:nth-child(2) { + animation-delay: 70ms; +} + +.calm-shell main > *:nth-child(3) { + animation-delay: 120ms; +} + +.calm-shell main > *:nth-child(4) { + animation-delay: 170ms; +} + +.calm-shell header .btn-circle, +.calm-shell header .btn-sm { + min-height: 2.5rem; + height: 2.5rem; + border: 0; + border-radius: 9999px; + background: transparent; + color: #28362c; +} + +.calm-shell header .btn-circle { + width: 2.5rem; +} + +.calm-shell header .dropdown-content { + border: 1px solid rgb(40 54 44 / 15%); + border-radius: 1.5rem; + background: #fffdf6; + color: #28362c; +} + +.calm-log-editor { + background-image: repeating-linear-gradient( + to bottom, + transparent 0, + transparent 27px, + rgb(40 54 44 / 7%) 27px, + rgb(40 54 44 / 7%) 28px + ); + background-position-y: 16px; +} + +@keyframes calm-arrive { + from { + opacity: 0; + transform: translateY(10px) scale(0.995); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + .calm-shell section > *, + .calm-shell 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..905e2ed 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 rgba(40,54,44,.12)', + borderRadius: 18, + boxShadow: '0 14px 35px rgba(40,54,44,.10)', + 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..b808567 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 ? '#d87f62' : '#718466'; points.push( { } if (lowPoint) { - const color = lowPoint.need >= 0 ? 'red' : 'green'; + const color = lowPoint.need >= 0 ? '#d87f62' : '#718466'; points.push( = 0 ? 'red' : 'green'; + const color = currPoint.need >= 0 ? '#d87f62' : '#718466'; 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..f705203 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="#667a5d" + strokeWidth={1.5} + fill="#cbd8ba" /> diff --git a/apps/web/src/components/days/DayNavigator.tsx b/apps/web/src/components/days/DayNavigator.tsx index ecb1d13..60a197f 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..043af4c 100644 --- a/apps/web/src/components/texts/ProductiveToggle.tsx +++ b/apps/web/src/components/texts/ProductiveToggle.tsx @@ -5,31 +5,40 @@ interface ProductiveToggleProps { isProductive: boolean; setIsProductive: (value: boolean) => void; onKeyDown: (e: React.KeyboardEvent) => void; - checkboxRef: React.RefObject; } export const ProductiveToggle = ({ isProductive, setIsProductive, onKeyDown, - checkboxRef, }: ProductiveToggleProps) => ( -