home 화면 todo list 보이게 추가

This commit is contained in:
2026-03-06 02:43:55 +09:00
parent b9aeb2ff3e
commit bbc9bf36f9
3 changed files with 389 additions and 91 deletions

View File

@@ -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 (
<div className="home">
<section className="home-hero">
@@ -47,20 +58,21 @@ const Home = () => {
<div className="home-hero__card">
<p className="home-hero__card-eyebrow">이번 집중 테마</p>
<div className="home-hero__card-body">
<h2>느린 기록, 깊은 회고</h2>
<p>
빠르게 업데이트하는 대신, 번쯤 되돌아보며 기록하는 목표로
합니다. 글은 매주 편씩 추가될 예정이에요.
</p>
<h2>{theme.theme}</h2>
<p>{theme.desc}</p>
</div>
<div className="home-hero__stats">
<div className="home-hero__stat">
<p className="stat-label">게시 </p>
<p className="stat-value">{posts.length}<span className="stat-unit"></span></p>
<p className="stat-label">전체 태스크</p>
<p className="stat-value">
{totalTasks}<span className="stat-unit"></span>
</p>
</div>
<div className="home-hero__stat">
<p className="stat-label">다음 업데이트</p>
<p className="stat-value stat-value--sm">이번 주말</p>
<p className="stat-label">진행 / 완료</p>
<p className="stat-value stat-value--sm">
{inProgress}<span className="stat-unit"> / </span>{doneTasks}
</p>
</div>
</div>
</div>
@@ -114,34 +126,13 @@ const Home = () => {
</div>
</section>
{/* ── TODO 보드 ──────────────────────────────────────────── */}
<section className="home-section">
<div className="home-section__header">
<h2>최근 개발</h2>
<p>최근 7 완료 태스크를 보여줍니다.</p>
</div>
<div className="home-dev-log">
{recentDev.length === 0 ? (
<p className="home-dev-log__empty">완료된 태스크가 없습니다.</p>
) : (
recentDev.map((todo) => (
<div key={todo.id} className="home-dev-log__item">
<span className="home-dev-log__dot" />
<div className="home-dev-log__content">
<p className="home-dev-log__title">{todo.title}</p>
{todo.description && (
<p className="home-dev-log__desc">{todo.description}</p>
)}
</div>
<span className="home-dev-log__date">
{new Date(todo.updated_at).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })}
</span>
</div>
))
)}
<Link to="/todo" className="home-dev-log__link">
Todo 보드 열기
</Link>
<h2>TODO</h2>
<p>계획 · 진행 · 완료 태스크를 한눈에 확인합니다.</p>
</div>
<TodoBoard todosByStatus={todosByStatus} />
</section>
<section className="home-section">
@@ -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 (
<div className="home-todo-wrapper">
{canScrollLeft && (
<button
className="home-todo-nav home-todo-nav--left"
onClick={() => scroll(-1)}
aria-label="왼쪽으로"
></button>
)}
{canScrollRight && (
<button
className="home-todo-nav home-todo-nav--right"
onClick={() => scroll(1)}
aria-label="오른쪽으로"
></button>
)}
<div className="home-todo-board" ref={boardRef}>
{TODO_COLUMNS.map((col) => {
const items = todosByStatus[col.id] ?? [];
return (
<div key={col.id} className="home-todo-col">
<div className="home-todo-col__head">
<span
className="home-todo-col__dot"
style={{ background: col.color, boxShadow: `0 0 6px ${col.color}` }}
/>
<span className="home-todo-col__label">{col.label}</span>
<span className="home-todo-col__count">{items.length}</span>
</div>
<div className="home-todo-col__body">
{items.length === 0 ? (
<p className="home-todo-col__empty">태스크가 없습니다.</p>
) : (
items.map((todo) => (
<div key={todo.id} className="home-todo-card">
<p className="home-todo-card__title">{todo.title}</p>
{todo.description && (
<p className="home-todo-card__desc">{todo.description}</p>
)}
<p className="home-todo-card__date">
{todo.updated_at
? new Date(todo.updated_at).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })
: ''}
</p>
</div>
))
)}
</div>
</div>
);
})}
</div>
<div className="home-todo-footer">
<Link to="/todo" className="home-todo-footer__link">
Todo 보드 열기
</Link>
</div>
</div>
);
};
export default Home;