From bbc9bf36f9c7981619c26845e7620d0280f7c4da Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 6 Mar 2026 02:43:55 +0900 Subject: [PATCH] =?UTF-8?q?home=20=ED=99=94=EB=A9=B4=20todo=20list=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EA=B2=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/heroConfig.js | 83 +++++++++++++++ src/pages/home/Home.css | 223 +++++++++++++++++++++++++++++++--------- src/pages/home/Home.jsx | 174 +++++++++++++++++++++++-------- 3 files changed, 389 insertions(+), 91 deletions(-) create mode 100644 src/data/heroConfig.js diff --git a/src/data/heroConfig.js b/src/data/heroConfig.js new file mode 100644 index 0000000..11f84fc --- /dev/null +++ b/src/data/heroConfig.js @@ -0,0 +1,83 @@ +/** + * 홈 히어로 카드 월별 테마 설정 + * 매달 month, theme, desc, nextUpdate 를 수정해 적용하세요. + */ +export const MONTHLY_THEMES = [ + { + month: 1, + theme: '새해 목표 설정', + desc: '연초를 맞아 올해 개발·기록 목표를 구체적으로 정리하고 실행 계획을 세웁니다.', + nextUpdate: '매주 일요일', + }, + { + month: 2, + theme: '코드 품질 개선', + desc: '리팩토링과 테스트 커버리지 향상에 집중합니다. 작은 개선도 꾸준히 쌓아갑니다.', + nextUpdate: '매주 토요일', + }, + { + month: 3, + theme: '웹 UI 고도화', + desc: '대시보드 형태의 UI를 사이버펑크 스타일로 전면 개편하고, 새 기능을 추가합니다.', + nextUpdate: '이번 주말', + }, + { + month: 4, + theme: '백엔드 성능 최적화', + desc: 'API 응답 속도와 데이터베이스 쿼리를 분석하고 병목을 개선하는 달입니다.', + nextUpdate: '이번 주말', + }, + { + month: 5, + theme: '인프라 자동화', + desc: 'Docker/Kubernetes 파이프라인을 정비하고 배포 자동화를 강화합니다.', + nextUpdate: '격주 일요일', + }, + { + month: 6, + theme: '여름 사이드 프로젝트', + desc: '새로운 기술 스택을 탐구하며 소규모 실험 프로젝트를 진행합니다.', + nextUpdate: '매주 금요일', + }, + { + month: 7, + theme: '기록과 문서화', + desc: '그동안 미뤄뒀던 개발 노트와 블로그 글 작성에 집중합니다.', + nextUpdate: '매주 화요일', + }, + { + month: 8, + theme: '보안 점검', + desc: '서비스 취약점을 점검하고 인증·인가 로직을 강화합니다.', + nextUpdate: '격주 토요일', + }, + { + month: 9, + theme: '모니터링 강화', + desc: '로그 수집과 알림 파이프라인을 개선해 운영 가시성을 높입니다.', + nextUpdate: '이번 주말', + }, + { + month: 10, + theme: '오픈소스 기여', + desc: '사용 중인 라이브러리에 이슈를 제보하거나 PR을 올려봅니다.', + nextUpdate: '매주 목요일', + }, + { + month: 11, + theme: '연말 회고 준비', + desc: '올 한 해의 개발 성과를 정리하고 내년 로드맵 초안을 그립니다.', + nextUpdate: '매주 일요일', + }, + { + month: 12, + theme: '느린 기록, 깊은 회고', + desc: '빠르게 달려온 한 해를 천천히 돌아보며 가장 의미 있었던 작업을 기록합니다.', + nextUpdate: '크리스마스 주간', + }, +]; + +export function getCurrentTheme() { + const month = new Date().getMonth() + 1; + return MONTHLY_THEMES.find((t) => t.month === month) ?? MONTHLY_THEMES[0]; +} diff --git a/src/pages/home/Home.css b/src/pages/home/Home.css index 7dbfbfb..0a2d6ac 100644 --- a/src/pages/home/Home.css +++ b/src/pages/home/Home.css @@ -367,91 +367,207 @@ padding-top: 4px; } -/* ── Dev Log ─────────────────────────────────────────────────────────── */ +/* ── TODO Board ──────────────────────────────────────────────────────── */ -.home-dev-log { - display: grid; - gap: 8px; +.home-todo-wrapper { + position: relative; } -.home-dev-log__empty { - margin: 0; - color: var(--text-muted); - font-size: 13px; - padding: 16px 0; +.home-todo-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 2; + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid var(--line-bright); + background: var(--surface-raised); + color: var(--text-bright); + font-size: 20px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease, border-color 0.2s ease; + box-shadow: var(--shadow-md); } -.home-dev-log__item { +.home-todo-nav:hover { + background: var(--bg-tertiary); + border-color: var(--neon-cyan); +} + +.home-todo-nav--left { left: -16px; } +.home-todo-nav--right { right: -16px; } + +.home-todo-board { + display: flex; + gap: 12px; + overflow-x: auto; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: var(--line) transparent; + padding-bottom: 4px; +} + +.home-todo-board::-webkit-scrollbar { + height: 4px; +} + +.home-todo-board::-webkit-scrollbar-track { + background: transparent; +} + +.home-todo-board::-webkit-scrollbar-thumb { + background: var(--line); + border-radius: 2px; +} + +.home-todo-col { + flex: 1 0 260px; + min-width: 0; + max-width: 340px; + scroll-snap-align: start; + display: flex; + flex-direction: column; border: 1px solid var(--line); - padding: 14px 18px; border-radius: var(--radius-md); background: var(--surface-card); - display: grid; - grid-template-columns: auto 1fr auto; - align-items: start; - gap: 14px; box-shadow: var(--shadow-card); - transition: border-color 0.2s ease, background 0.2s ease; + overflow: hidden; } -.home-dev-log__item:hover { - border-color: rgba(52, 211, 153, 0.25); - background: var(--surface-raised); -} - -.home-dev-log__dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: #34d399; - box-shadow: 0 0 6px rgba(52, 211, 153, 0.8); - margin-top: 7px; +.home-todo-col__head { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 14px; + border-bottom: 1px solid var(--line); + background: rgba(255, 255, 255, 0.02); flex-shrink: 0; } -.home-dev-log__content { +.home-todo-col__dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +.home-todo-col__label { + font-size: 12px; + font-weight: 600; + color: var(--text-bright); + letter-spacing: 0.04em; + text-transform: uppercase; + font-family: var(--font-display); + flex: 1; +} + +.home-todo-col__count { + font-size: 11px; + color: var(--text-muted); + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--line); + border-radius: 999px; + padding: 1px 7px; + font-family: var(--font-display); +} + +.home-todo-col__body { + flex: 1; + overflow-y: auto; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; + max-height: calc(40vh); + min-height: 60px; + scrollbar-width: thin; + scrollbar-color: var(--line) transparent; +} + +.home-todo-col__body::-webkit-scrollbar { + width: 3px; +} + +.home-todo-col__body::-webkit-scrollbar-thumb { + background: var(--line); + border-radius: 2px; +} + +.home-todo-col__empty { + margin: auto; + color: var(--text-muted); + font-size: 12px; + text-align: center; + padding: 16px 0; +} + +.home-todo-card { + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 11px 13px; + background: rgba(255, 255, 255, 0.02); display: grid; gap: 4px; + transition: border-color 0.2s ease, background 0.2s ease; } -.home-dev-log__title { +.home-todo-card:hover { + border-color: rgba(0, 212, 255, 0.18); + background: rgba(0, 212, 255, 0.03); +} + +.home-todo-card__title { margin: 0; + font-size: 13px; font-weight: 600; - font-size: 15px; color: var(--text-bright); letter-spacing: -0.01em; + line-height: 1.4; } -.home-dev-log__desc { +.home-todo-card__desc { margin: 0; - color: var(--text-dim); - font-size: 12px; - line-height: 1.6; -} - -.home-dev-log__date { font-size: 11px; - color: rgba(52, 211, 153, 0.7); - text-transform: uppercase; - letter-spacing: 0.08em; - white-space: nowrap; - padding-top: 4px; + color: var(--text-dim); + line-height: 1.55; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } -.home-dev-log__link { +.home-todo-card__date { + margin: 2px 0 0; + font-size: 10px; + color: var(--text-muted); + letter-spacing: 0.04em; +} + +.home-todo-footer { + display: flex; + justify-content: flex-end; + margin-top: 10px; +} + +.home-todo-footer__link { display: inline-flex; align-items: center; gap: 6px; - margin-top: 4px; font-size: 13px; color: #34d399; text-decoration: none; - padding: 8px 0; + padding: 6px 0; transition: opacity 0.2s ease; font-weight: 500; } -.home-dev-log__link:hover { +.home-todo-footer__link:hover { opacity: 0.75; } @@ -626,6 +742,19 @@ gap: 24px; } + .home-todo-col { + flex: 0 0 80vw; + max-width: 80vw; + } + + .home-todo-col__body { + max-height: 30vh; + } + + .home-todo-nav { + display: none; + } + .home-hero h1 { font-size: clamp(22px, 6vw, 32px); } diff --git a/src/pages/home/Home.jsx b/src/pages/home/Home.jsx index c4d45d4..565e1b5 100644 --- a/src/pages/home/Home.jsx +++ b/src/pages/home/Home.jsx @@ -1,31 +1,42 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import { navLinks } from '../../routes.jsx'; import { getBlogPosts } from '../../data/blog'; import { getTodos } from '../../api'; +import { getCurrentTheme } from '../../data/heroConfig'; import myPhoto from '../../assets/myPhoto.jpg'; import './Home.css'; -const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; +const TODO_COLUMNS = [ + { id: 'todo', label: '계획', color: 'var(--neon-purple)' }, + { id: 'in_progress', label: '진행 중', color: '#f59e0b' }, + { id: 'done', label: '완료', color: '#34d399' }, +]; const Home = () => { const posts = getBlogPosts().slice(0, 3); const highlights = navLinks.filter((link) => link.id !== 'home'); - const [recentDev, setRecentDev] = useState([]); + const theme = getCurrentTheme(); + + const [todosByStatus, setTodosByStatus] = useState({ todo: [], in_progress: [], done: [] }); useEffect(() => { getTodos() - .then((todos) => { - if (!Array.isArray(todos)) return; - const now = Date.now(); - const filtered = todos - .filter((t) => t.status === 'done' && t.updated_at && (now - new Date(t.updated_at).getTime()) <= SEVEN_DAYS_MS) - .slice(0, 5); - setRecentDev(filtered); + .then((data) => { + if (!Array.isArray(data)) return; + setTodosByStatus({ + todo: data.filter((t) => t.status === 'todo'), + in_progress: data.filter((t) => t.status === 'in_progress'), + done: data.filter((t) => t.status === 'done'), + }); }) .catch(() => { /* 조용히 실패 */ }); }, []); + const totalTasks = todosByStatus.todo.length + todosByStatus.in_progress.length + todosByStatus.done.length; + const doneTasks = todosByStatus.done.length; + const inProgress = todosByStatus.in_progress.length; + return (
@@ -47,20 +58,21 @@ const Home = () => {

이번 달 집중 테마

-

느린 기록, 깊은 회고

-

- 빠르게 업데이트하는 대신, 한 번쯤 되돌아보며 기록하는 걸 목표로 - 합니다. 글은 매주 한 편씩 추가될 예정이에요. -

+

{theme.theme}

+

{theme.desc}

-

게시 글

-

{posts.length}

+

전체 태스크

+

+ {totalTasks} +

-

다음 업데이트

-

이번 주말

+

진행 중 / 완료

+

+ {inProgress} / {doneTasks} +

@@ -114,34 +126,13 @@ const Home = () => {
+ {/* ── TODO 보드 ──────────────────────────────────────────── */}
-

최근 개발

-

최근 7일 내 완료된 태스크를 보여줍니다.

-
-
- {recentDev.length === 0 ? ( -

완료된 태스크가 없습니다.

- ) : ( - recentDev.map((todo) => ( -
- -
-

{todo.title}

- {todo.description && ( -

{todo.description}

- )} -
- - {new Date(todo.updated_at).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })} - -
- )) - )} - - Todo 보드 열기 → - +

TODO

+

계획 · 진행 중 · 완료 태스크를 한눈에 확인합니다.

+
@@ -205,4 +196,99 @@ const Home = () => { ); }; +/* ── TodoBoard ──────────────────────────────────────────────────────── */ + +const TodoBoard = ({ todosByStatus }) => { + const boardRef = useRef(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + const checkScroll = () => { + const el = boardRef.current; + if (!el) return; + setCanScrollLeft(el.scrollLeft > 4); + setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 4); + }; + + useEffect(() => { + checkScroll(); + const el = boardRef.current; + if (!el) return; + el.addEventListener('scroll', checkScroll, { passive: true }); + const ro = new ResizeObserver(checkScroll); + ro.observe(el); + return () => { el.removeEventListener('scroll', checkScroll); ro.disconnect(); }; + }, [todosByStatus]); + + const scroll = (dir) => { + const el = boardRef.current; + if (!el) return; + el.scrollBy({ left: dir * 280, behavior: 'smooth' }); + }; + + const isEmpty = TODO_COLUMNS.every((col) => todosByStatus[col.id].length === 0); + + return ( +
+ {canScrollLeft && ( + + )} + {canScrollRight && ( + + )} + +
+ {TODO_COLUMNS.map((col) => { + const items = todosByStatus[col.id] ?? []; + return ( +
+
+ + {col.label} + {items.length} +
+
+ {items.length === 0 ? ( +

태스크가 없습니다.

+ ) : ( + items.map((todo) => ( +
+

{todo.title}

+ {todo.description && ( +

{todo.description}

+ )} +

+ {todo.updated_at + ? new Date(todo.updated_at).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }) + : ''} +

+
+ )) + )} +
+
+ ); + })} +
+ +
+ + Todo 보드 열기 → + +
+
+ ); +}; + export default Home;