TODO UX 개선
This commit is contained in:
@@ -83,7 +83,7 @@
|
|||||||
|
|
||||||
.todo-board {
|
.todo-board {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
@@ -247,6 +247,129 @@
|
|||||||
color: #f9b6b1;
|
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 ──────────────────────────────────────────────────────── */
|
/* ── Responsive ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -257,6 +380,20 @@
|
|||||||
.todo-col {
|
.todo-col {
|
||||||
min-height: 120px;
|
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) {
|
@media (max-width: 480px) {
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { getTodos, addTodo, updateTodo, deleteTodo, clearTodos } from '../../api';
|
import { getTodos, addTodo, updateTodo, deleteTodo, clearTodos } from '../../api';
|
||||||
import './Todo.css';
|
import './Todo.css';
|
||||||
|
|
||||||
const COLUMNS = [
|
const ACTIVE_COLUMNS = [
|
||||||
{ id: 'todo', label: '할 일' },
|
{ id: 'todo', label: '할 일' },
|
||||||
{ id: 'in_progress', label: '진행 중' },
|
{ id: 'in_progress', label: '진행 중' },
|
||||||
|
];
|
||||||
|
const ALL_COLUMNS = [
|
||||||
|
...ACTIVE_COLUMNS,
|
||||||
{ id: 'done', label: '완료' },
|
{ id: 'done', label: '완료' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const emptyForm = { title: '', description: '' };
|
const emptyForm = { title: '', description: '' };
|
||||||
|
|
||||||
|
const toDateStr = (iso) => {
|
||||||
|
if (!iso) return '';
|
||||||
|
return new Date(iso).toISOString().slice(0, 10); // YYYY-MM-DD
|
||||||
|
};
|
||||||
|
|
||||||
const Todo = () => {
|
const Todo = () => {
|
||||||
const [todos, setTodos] = useState([]);
|
const [todos, setTodos] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -19,6 +27,7 @@ const Todo = () => {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [dragging, setDragging] = useState(null);
|
const [dragging, setDragging] = useState(null);
|
||||||
const [dragOver, setDragOver] = useState(null);
|
const [dragOver, setDragOver] = useState(null);
|
||||||
|
const [doneDate, setDoneDate] = useState(''); // '' = 전체
|
||||||
const dragItem = useRef(null);
|
const dragItem = useRef(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
@@ -54,7 +63,11 @@ const Todo = () => {
|
|||||||
|
|
||||||
const handleMove = async (id, newStatus) => {
|
const handleMove = async (id, newStatus) => {
|
||||||
setTodos((prev) =>
|
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 {
|
try {
|
||||||
await updateTodo(id, { status: newStatus, updated_at: new Date().toISOString() });
|
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 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) => (
|
||||||
|
<div
|
||||||
|
key={todo.id}
|
||||||
|
className={`todo-card${dragging === todo.id ? ' is-dragging' : ''}`}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => onDragStart(e, todo)}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
>
|
||||||
|
<p className="todo-card__title">{todo.title}</p>
|
||||||
|
{todo.description && (
|
||||||
|
<p className="todo-card__desc">{todo.description}</p>
|
||||||
|
)}
|
||||||
|
<div className="todo-card__footer">
|
||||||
|
<span className="todo-card__date">
|
||||||
|
{formatDate(todo.created_at)}
|
||||||
|
</span>
|
||||||
|
<div className="todo-card__actions">
|
||||||
|
{ALL_COLUMNS.filter((c) => c.id !== colId).map((c) => (
|
||||||
|
<button
|
||||||
|
key={c.id}
|
||||||
|
type="button"
|
||||||
|
className="todo-card__btn"
|
||||||
|
title={`${c.label}으로 이동`}
|
||||||
|
onClick={() => handleMove(todo.id, c.id)}
|
||||||
|
>
|
||||||
|
{c.id === 'todo' ? '↩' : c.id === 'in_progress' ? '▶' : '✓'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="todo-card__btn todo-card__btn--danger"
|
||||||
|
title="삭제"
|
||||||
|
onClick={() => handleDelete(todo.id)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="todo-page">
|
<div className="todo-page">
|
||||||
{/* 추가 버튼 & 완료 비우기 */}
|
{/* 툴바 */}
|
||||||
<div className="todo-toolbar">
|
<div className="todo-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -167,9 +242,9 @@ const Todo = () => {
|
|||||||
{error && <p className="todo-error">{error}</p>}
|
{error && <p className="todo-error">{error}</p>}
|
||||||
{loading && todos.length === 0 && <p className="todo-loading">불러오는 중...</p>}
|
{loading && todos.length === 0 && <p className="todo-loading">불러오는 중...</p>}
|
||||||
|
|
||||||
{/* 보드 */}
|
{/* 활성 보드 (할 일 + 진행 중) */}
|
||||||
<div className="todo-board">
|
<div className="todo-board">
|
||||||
{COLUMNS.map((col) => {
|
{ACTIVE_COLUMNS.map((col) => {
|
||||||
const items = byStatus(col.id);
|
const items = byStatus(col.id);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -186,53 +261,70 @@ const Todo = () => {
|
|||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<p className="todo-col__empty">드래그하여 이동</p>
|
<p className="todo-col__empty">드래그하여 이동</p>
|
||||||
)}
|
)}
|
||||||
{items.map((todo) => (
|
{items.map((todo) => renderCard(todo, col.id))}
|
||||||
<div
|
|
||||||
key={todo.id}
|
|
||||||
className={`todo-card${dragging === todo.id ? ' is-dragging' : ''}`}
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => onDragStart(e, todo)}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
>
|
|
||||||
<p className="todo-card__title">{todo.title}</p>
|
|
||||||
{todo.description && (
|
|
||||||
<p className="todo-card__desc">{todo.description}</p>
|
|
||||||
)}
|
|
||||||
<div className="todo-card__footer">
|
|
||||||
<span className="todo-card__date">
|
|
||||||
{todo.created_at
|
|
||||||
? new Date(todo.created_at).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })
|
|
||||||
: ''}
|
|
||||||
</span>
|
|
||||||
<div className="todo-card__actions">
|
|
||||||
{COLUMNS.filter((c) => c.id !== col.id).map((c) => (
|
|
||||||
<button
|
|
||||||
key={c.id}
|
|
||||||
type="button"
|
|
||||||
className="todo-card__btn"
|
|
||||||
title={`${c.label}으로 이동`}
|
|
||||||
onClick={() => handleMove(todo.id, c.id)}
|
|
||||||
>
|
|
||||||
{c.id === 'todo' ? '↩' : c.id === 'in_progress' ? '▶' : '✓'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="todo-card__btn todo-card__btn--danger"
|
|
||||||
title="삭제"
|
|
||||||
onClick={() => handleDelete(todo.id)}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 완료 패널 */}
|
||||||
|
<div
|
||||||
|
className={`todo-done-panel${dragOver === 'done' ? ' is-drag-over' : ''}`}
|
||||||
|
onDragOver={(e) => onDragOver(e, 'done')}
|
||||||
|
onDrop={(e) => onDrop(e, 'done')}
|
||||||
|
>
|
||||||
|
{/* 완료 패널 헤더 */}
|
||||||
|
<div className="todo-done-panel__head">
|
||||||
|
<div className="todo-done-panel__title-row">
|
||||||
|
<span className="todo-col__title">완료</span>
|
||||||
|
<span className="todo-col__count">{doneTodos.length}</span>
|
||||||
|
{doneDates.length > 0 && doneDate === '' && (
|
||||||
|
<span className="todo-done-panel__total-hint">
|
||||||
|
전체 {todos.filter(t => t.status === 'done').length}건
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* 날짜 필터 */}
|
||||||
|
<div className="todo-done-panel__filter">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`todo-date-btn${doneDate === '' ? ' is-active' : ''}`}
|
||||||
|
onClick={() => setDoneDate('')}
|
||||||
|
>
|
||||||
|
전체
|
||||||
|
</button>
|
||||||
|
{doneDates.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d}
|
||||||
|
type="button"
|
||||||
|
className={`todo-date-btn${doneDate === d ? ' is-active' : ''}`}
|
||||||
|
onClick={() => setDoneDate(d)}
|
||||||
|
>
|
||||||
|
{new Date(d + 'T00:00:00').toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="todo-date-input"
|
||||||
|
value={doneDate}
|
||||||
|
onChange={(e) => setDoneDate(e.target.value)}
|
||||||
|
title="날짜 직접 선택"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 완료 카드 그리드 */}
|
||||||
|
<div className="todo-done-panel__body">
|
||||||
|
{doneTodos.length === 0 ? (
|
||||||
|
<p className="todo-col__empty">
|
||||||
|
{doneDate ? '해당 날짜에 완료된 항목이 없습니다' : '드래그하여 이동'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
doneTodos.map((todo) => renderCard(todo, 'done'))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user