From 2c4b1e2e3a7045f35702aec848e8219294736929 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 22 Mar 2026 12:19:02 +0900 Subject: [PATCH] =?UTF-8?q?TODO=20UX=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/todo/Todo.css | 139 +++++++++++++++++++++++++++++- src/pages/todo/Todo.jsx | 186 ++++++++++++++++++++++++++++++---------- 2 files changed, 277 insertions(+), 48 deletions(-) diff --git a/src/pages/todo/Todo.css b/src/pages/todo/Todo.css index caa5980..54f6f31 100644 --- a/src/pages/todo/Todo.css +++ b/src/pages/todo/Todo.css @@ -83,7 +83,7 @@ .todo-board { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; } @@ -247,6 +247,129 @@ color: #f9b6b1; } +/* ── Done Panel ──────────────────────────────────────────────────────── */ + +.todo-done-panel { + background: rgba(10, 18, 45, 0.6); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + display: flex; + flex-direction: column; + gap: 0; + transition: border-color 0.2s ease, background 0.2s ease; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.todo-done-panel.is-drag-over { + border-color: rgba(244, 114, 182, 0.4); + background: rgba(244, 114, 182, 0.04); +} + +.todo-done-panel__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px 12px; + border-bottom: 1px solid var(--line); + flex-wrap: wrap; +} + +.todo-done-panel__title-row { + display: flex; + align-items: center; + gap: 8px; +} + +.todo-done-panel__total-hint { + font-size: 11px; + color: var(--text-muted); + opacity: 0.6; +} + +/* ── 날짜 필터 ────────────────────────────────────────────────────────── */ + +.todo-done-panel__filter { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + flex: 1; + justify-content: flex-end; +} + +.todo-date-btn { + font-size: 11px; + padding: 3px 10px; + border-radius: 999px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.04); + color: var(--text-muted); + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; + white-space: nowrap; + font-family: inherit; + line-height: 1.6; +} + +.todo-date-btn:hover { + background: rgba(244, 114, 182, 0.1); + border-color: rgba(244, 114, 182, 0.3); + color: #f472b6; +} + +.todo-date-btn.is-active { + background: rgba(244, 114, 182, 0.18); + border-color: rgba(244, 114, 182, 0.5); + color: #f472b6; + font-weight: 600; +} + +.todo-date-input { + font-size: 11px; + padding: 3px 8px; + border-radius: 8px; + border: 1px solid var(--line); + background: rgba(0, 0, 0, 0.25); + color: var(--text-muted); + cursor: pointer; + font-family: inherit; + outline: none; + transition: border-color 0.15s ease; + height: 26px; +} + +.todo-date-input:focus { + border-color: rgba(244, 114, 182, 0.4); + color: var(--text-bright); +} + +/* 완료 패널 카드 그리드 */ +.todo-done-panel__body { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 10px; + padding: 12px; +} + +.todo-done-panel__body .todo-col__empty { + grid-column: 1 / -1; +} + +.todo-done-panel__body .todo-card { + opacity: 0.75; +} + +.todo-done-panel__body .todo-card:hover { + opacity: 1; +} + +.todo-done-panel__body .todo-card__title { + text-decoration: line-through; + text-decoration-color: rgba(244, 114, 182, 0.4); +} + /* ── Responsive ──────────────────────────────────────────────────────── */ @media (max-width: 768px) { @@ -257,6 +380,20 @@ .todo-col { min-height: 120px; } + + .todo-done-panel__head { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .todo-done-panel__filter { + justify-content: flex-start; + } + + .todo-done-panel__body { + grid-template-columns: 1fr; + } } @media (max-width: 480px) { diff --git a/src/pages/todo/Todo.jsx b/src/pages/todo/Todo.jsx index b7063f8..62ba2a8 100644 --- a/src/pages/todo/Todo.jsx +++ b/src/pages/todo/Todo.jsx @@ -2,14 +2,22 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { getTodos, addTodo, updateTodo, deleteTodo, clearTodos } from '../../api'; import './Todo.css'; -const COLUMNS = [ +const ACTIVE_COLUMNS = [ { id: 'todo', label: '할 일' }, { id: 'in_progress', label: '진행 중' }, +]; +const ALL_COLUMNS = [ + ...ACTIVE_COLUMNS, { id: 'done', label: '완료' }, ]; const emptyForm = { title: '', description: '' }; +const toDateStr = (iso) => { + if (!iso) return ''; + return new Date(iso).toISOString().slice(0, 10); // YYYY-MM-DD +}; + const Todo = () => { const [todos, setTodos] = useState([]); const [loading, setLoading] = useState(false); @@ -19,6 +27,7 @@ const Todo = () => { const [saving, setSaving] = useState(false); const [dragging, setDragging] = useState(null); const [dragOver, setDragOver] = useState(null); + const [doneDate, setDoneDate] = useState(''); // '' = 전체 const dragItem = useRef(null); const load = useCallback(async () => { @@ -54,7 +63,11 @@ const Todo = () => { const handleMove = async (id, newStatus) => { setTodos((prev) => - prev.map((t) => (t.id === id ? { ...t, status: newStatus } : t)) + prev.map((t) => + t.id === id + ? { ...t, status: newStatus, updated_at: new Date().toISOString() } + : t + ) ); try { await updateTodo(id, { status: newStatus, updated_at: new Date().toISOString() }); @@ -110,9 +123,71 @@ const Todo = () => { const byStatus = (status) => todos.filter((t) => t.status === status); + /* ── 완료 필터 ─────────────────────────────────────────────────── */ + const doneTodos = todos.filter((t) => { + if (t.status !== 'done') return false; + if (!doneDate) return true; + const ref = t.updated_at || t.created_at; + return toDateStr(ref) === doneDate; + }); + + /* 완료 항목에서 날짜 목록 추출 (필터 select용) */ + const doneDates = [...new Set( + todos + .filter((t) => t.status === 'done') + .map((t) => toDateStr(t.updated_at || t.created_at)) + .filter(Boolean) + )].sort((a, b) => b.localeCompare(a)); // 최신순 + + const formatDate = (iso) => { + if (!iso) return ''; + return new Date(iso).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }); + }; + + const renderCard = (todo, colId) => ( +
onDragStart(e, todo)} + onDragEnd={onDragEnd} + > +

{todo.title}

+ {todo.description && ( +

{todo.description}

+ )} +
+ + {formatDate(todo.created_at)} + +
+ {ALL_COLUMNS.filter((c) => c.id !== colId).map((c) => ( + + ))} + +
+
+
+ ); + return (
- {/* 추가 버튼 & 완료 비우기 */} + {/* 툴바 */}
- ))} - -
-
- - ))} + {items.map((todo) => renderCard(todo, col.id))} ); })} + + {/* 완료 패널 */} +
onDragOver(e, 'done')} + onDrop={(e) => onDrop(e, 'done')} + > + {/* 완료 패널 헤더 */} +
+
+ 완료 + {doneTodos.length} + {doneDates.length > 0 && doneDate === '' && ( + + 전체 {todos.filter(t => t.status === 'done').length}건 + + )} +
+ {/* 날짜 필터 */} +
+ + {doneDates.map((d) => ( + + ))} + setDoneDate(e.target.value)} + title="날짜 직접 선택" + /> +
+
+ + {/* 완료 카드 그리드 */} +
+ {doneTodos.length === 0 ? ( +

+ {doneDate ? '해당 날짜에 완료된 항목이 없습니다' : '드래그하여 이동'} +

+ ) : ( + doneTodos.map((todo) => renderCard(todo, 'done')) + )} +
+
); };