todo리스트 조회 페이지 개선, 미완료 리스트 아래로 내려가는설정, 페이지네이션적용
All checks were successful
Build And Test / build-and-push (push) Successful in 1m42s
All checks were successful
Build And Test / build-and-push (push) Successful in 1m42s
This commit is contained in:
@ -8,16 +8,19 @@ import todoApi from "../api/todoApi";
|
|||||||
const FILTERS = [
|
const FILTERS = [
|
||||||
{ label: "오늘 할 일", value: "today" },
|
{ label: "오늘 할 일", value: "today" },
|
||||||
{ label: "중요", value: "important" },
|
{ label: "중요", value: "important" },
|
||||||
{ label: "계획된 일정", value: "scheduled" },
|
{ label: "미완료 작업", value: "incomplete" },
|
||||||
|
{ label: "완료된 작업", value: "completed" }, // ✅ 변경
|
||||||
{ label: "전체", value: "all" },
|
{ label: "전체", value: "all" },
|
||||||
{ label: "완료된 작업", value: "completed" }, // ✅ 추가됨
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 5; // ✅ 페이지당 항목 수
|
||||||
|
|
||||||
const TaskPage = () => {
|
const TaskPage = () => {
|
||||||
const [tasks, setTasks] = useState([]);
|
const [tasks, setTasks] = useState([]);
|
||||||
const [selectedFilter, setSelectedFilter] = useState("all");
|
|
||||||
const [selectedTag, setSelectedTag] = useState(null);
|
const [selectedTag, setSelectedTag] = useState(null);
|
||||||
const [selectedTask, setSelectedTask] = useState(null);
|
const [selectedTask, setSelectedTask] = useState(null);
|
||||||
|
const [selectedFilter, setSelectedFilter] = useState("all");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1); // ✅ 현재 페이지
|
||||||
|
|
||||||
const fetchTasks = useCallback(async () => {
|
const fetchTasks = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -34,20 +37,36 @@ const TaskPage = () => {
|
|||||||
|
|
||||||
const closeDetail = () => setSelectedTask(null);
|
const closeDetail = () => setSelectedTask(null);
|
||||||
|
|
||||||
const filteredTasks = tasks.filter((task) => {
|
const filteredTasks = tasks
|
||||||
if (selectedTag) return task.tags?.includes(selectedTag);
|
.filter((task) => {
|
||||||
if (selectedFilter === "today") {
|
if (selectedTag) return task.tags?.includes(selectedTag);
|
||||||
const today = new Date().toISOString().split("T")[0];
|
if (selectedFilter === "today") {
|
||||||
return task.due_date === today;
|
const today = new Date().toISOString().split("T")[0];
|
||||||
} else if (selectedFilter === "important") {
|
return task.due_date === today;
|
||||||
return task.tags?.includes("중요");
|
} else if (selectedFilter === "important") {
|
||||||
} else if (selectedFilter === "scheduled") {
|
return task.tags?.includes("중요");
|
||||||
return !!task.due_date;
|
} else if (selectedFilter === "incomplete") {
|
||||||
} else if (selectedFilter === "completed") {
|
return !task.is_completed;
|
||||||
return task.is_completed;
|
} else if (selectedFilter === "completed") {
|
||||||
}
|
return task.is_completed;
|
||||||
return true; // 전체
|
}
|
||||||
});
|
return true;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.is_completed && !b.is_completed) return 1;
|
||||||
|
if (!a.is_completed && b.is_completed) return -1;
|
||||||
|
if (!a.is_completed && !b.is_completed) {
|
||||||
|
return (a.due_date || "").localeCompare(b.due_date || "");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 페이지네이션 적용
|
||||||
|
const totalPages = Math.ceil(filteredTasks.length / ITEMS_PER_PAGE);
|
||||||
|
const paginatedTasks = filteredTasks.slice(
|
||||||
|
(currentPage - 1) * ITEMS_PER_PAGE,
|
||||||
|
currentPage * ITEMS_PER_PAGE
|
||||||
|
);
|
||||||
|
|
||||||
const allTags = [...new Set(tasks.flatMap((task) => task.tags || []))];
|
const allTags = [...new Set(tasks.flatMap((task) => task.tags || []))];
|
||||||
|
|
||||||
@ -77,7 +96,7 @@ const TaskPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50">
|
<div className="flex h-screen bg-gray-50">
|
||||||
{/* ✅ 왼쪽 사이드바 - 목록 + 태그 */}
|
{/* ✅ 왼쪽 사이드바 */}
|
||||||
<aside className="w-60 border-r border-gray-200 p-4 space-y-4 bg-white overflow-y-auto">
|
<aside className="w-60 border-r border-gray-200 p-4 space-y-4 bg-white overflow-y-auto">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold mb-2">📂 목록</h2>
|
<h2 className="text-lg font-bold mb-2">📂 목록</h2>
|
||||||
@ -87,6 +106,7 @@ const TaskPage = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedFilter(filter.value);
|
setSelectedFilter(filter.value);
|
||||||
setSelectedTag(null);
|
setSelectedTag(null);
|
||||||
|
setCurrentPage(1); // ✅ 필터 변경 시 페이지 초기화
|
||||||
}}
|
}}
|
||||||
className={`w-full text-left px-2 py-1 rounded hover:bg-gray-100 ${
|
className={`w-full text-left px-2 py-1 rounded hover:bg-gray-100 ${
|
||||||
selectedFilter === filter.value && !selectedTag
|
selectedFilter === filter.value && !selectedTag
|
||||||
@ -107,6 +127,7 @@ const TaskPage = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedTag(tag);
|
setSelectedTag(tag);
|
||||||
setSelectedFilter(null);
|
setSelectedFilter(null);
|
||||||
|
setCurrentPage(1); // ✅ 태그 선택 시 페이지 초기화
|
||||||
}}
|
}}
|
||||||
className={`w-full text-left px-2 py-1 rounded hover:bg-gray-100 ${
|
className={`w-full text-left px-2 py-1 rounded hover:bg-gray-100 ${
|
||||||
selectedTag === tag ? "bg-blue-100 font-semibold" : ""
|
selectedTag === tag ? "bg-blue-100 font-semibold" : ""
|
||||||
@ -118,7 +139,7 @@ const TaskPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 가운데 메인 영역 */}
|
{/* 가운데 메인 */}
|
||||||
<main className="flex-1 border-r border-gray-200 p-4 overflow-y-auto">
|
<main className="flex-1 border-r border-gray-200 p-4 overflow-y-auto">
|
||||||
<TodoForm onTaskCreated={fetchTasks} />
|
<TodoForm onTaskCreated={fetchTasks} />
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
@ -131,15 +152,62 @@ const TaskPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TodoList
|
<TodoList
|
||||||
tasks={filteredTasks}
|
tasks={paginatedTasks}
|
||||||
onSelectTask={setSelectedTask}
|
onSelectTask={setSelectedTask}
|
||||||
onToggleComplete={toggleComplete}
|
onToggleComplete={toggleComplete}
|
||||||
onTagClick={(tag) => {
|
onTagClick={(tag) => {
|
||||||
setSelectedTag(tag);
|
setSelectedTag(tag);
|
||||||
setSelectedFilter(null);
|
setSelectedFilter(null);
|
||||||
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
onDelete={deleteTask}
|
onDelete={deleteTask}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* ✅ 페이지네이션 개선 */}
|
||||||
|
<div className="flex justify-center mt-6 items-center space-x-2">
|
||||||
|
{/* 이전 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className={`px-3 py-1 rounded ${
|
||||||
|
currentPage === 1
|
||||||
|
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
||||||
|
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 페이지 번호 */}
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className={`px-3 py-1 rounded ${
|
||||||
|
currentPage === page
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 다음 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||||
|
}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className={`px-3 py-1 rounded ${
|
||||||
|
currentPage === totalPages
|
||||||
|
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
||||||
|
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* 오른쪽 상세 Drawer */}
|
{/* 오른쪽 상세 Drawer */}
|
||||||
@ -159,8 +227,8 @@ const TaskPage = () => {
|
|||||||
<TodoDetail
|
<TodoDetail
|
||||||
task={selectedTask}
|
task={selectedTask}
|
||||||
onUpdated={() => {
|
onUpdated={() => {
|
||||||
fetchTasks(); // 목록 갱신
|
fetchTasks();
|
||||||
setSelectedTask(null); // 상세 창 닫기 (필요에 따라 유지 가능)
|
setSelectedTask(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
192
src/pages/_stay_TaskPage.js
Normal file
192
src/pages/_stay_TaskPage.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
// ✅ src/pages/TaskPage.js
|
||||||
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
|
import TodoForm from "../components/Todo/TodoForm";
|
||||||
|
import TodoList from "../components/Todo/TodoList";
|
||||||
|
import TodoDetail from "../components/Todo/TodoDetail";
|
||||||
|
import todoApi from "../api/todoApi";
|
||||||
|
|
||||||
|
const FILTERS = [
|
||||||
|
{ label: "오늘 할 일", value: "today" },
|
||||||
|
{ label: "중요", value: "important" },
|
||||||
|
{ label: "미완료 작업", value: "incomplete" },
|
||||||
|
{ label: "완료된 작업", value: "completed" }, // ✅ 추가됨
|
||||||
|
{ label: "전체", value: "all" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TaskPage = () => {
|
||||||
|
const [tasks, setTasks] = useState([]);
|
||||||
|
const [selectedTag, setSelectedTag] = useState(null);
|
||||||
|
const [selectedTask, setSelectedTask] = useState(null);
|
||||||
|
const [selectedFilter, setSelectedFilter] = useState("all");
|
||||||
|
|
||||||
|
const fetchTasks = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await todoApi.get("/api/todo/tasks/");
|
||||||
|
setTasks(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("목록 불러오기 실패:", err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTasks();
|
||||||
|
}, [fetchTasks]);
|
||||||
|
|
||||||
|
const closeDetail = () => setSelectedTask(null);
|
||||||
|
|
||||||
|
const filteredTasks = tasks
|
||||||
|
.filter((task) => {
|
||||||
|
if (selectedTag) return task.tags?.includes(selectedTag);
|
||||||
|
if (selectedFilter === "today") {
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
return task.due_date === today;
|
||||||
|
} else if (selectedFilter === "important") {
|
||||||
|
return task.tags?.includes("중요");
|
||||||
|
} else if (selectedFilter === "incomplete") {
|
||||||
|
return !task.is_completed; // ✅ is_completed가 false인 경우만
|
||||||
|
} else if (selectedFilter === "completed") {
|
||||||
|
return task.is_completed;
|
||||||
|
}
|
||||||
|
return true; // 전체
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
// 완료된 작업은 아래로, 미완료 작업은 due_date 기준 오름차순
|
||||||
|
if (a.is_completed && !b.is_completed) return 1;
|
||||||
|
if (!a.is_completed && b.is_completed) return -1;
|
||||||
|
|
||||||
|
if (!a.is_completed && !b.is_completed) {
|
||||||
|
return (a.due_date || "").localeCompare(b.due_date || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // 둘 다 완료된 경우 순서 유지
|
||||||
|
});
|
||||||
|
|
||||||
|
const allTags = [...new Set(tasks.flatMap((task) => task.tags || []))];
|
||||||
|
|
||||||
|
const toggleComplete = async (taskId, is_completed) => {
|
||||||
|
try {
|
||||||
|
await todoApi.patch(`/api/todo/tasks/${taskId}/`, {
|
||||||
|
is_completed: !is_completed,
|
||||||
|
});
|
||||||
|
fetchTasks();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("완료 상태 변경 실패:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTask = async (taskId) => {
|
||||||
|
if (!window.confirm("정말로 이 작업을 삭제하시겠습니까?")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await todoApi.delete(`/api/todo/tasks/${taskId}/`);
|
||||||
|
if (selectedTask?.id === taskId) setSelectedTask(null);
|
||||||
|
fetchTasks();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("삭제 실패:", err);
|
||||||
|
alert("삭제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-gray-50">
|
||||||
|
{/* ✅ 왼쪽 사이드바 - 목록 + 태그 */}
|
||||||
|
<aside className="w-60 border-r border-gray-200 p-4 space-y-4 bg-white overflow-y-auto">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold mb-2">📂 목록</h2>
|
||||||
|
{FILTERS.map((filter) => (
|
||||||
|
<button
|
||||||
|
key={filter.value}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedFilter(filter.value);
|
||||||
|
setSelectedTag(null);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-2 py-1 rounded hover:bg-gray-100 ${
|
||||||
|
selectedFilter === filter.value && !selectedTag
|
||||||
|
? "bg-blue-100 font-semibold"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-md font-semibold mt-6 mb-2">🏷 태그</h2>
|
||||||
|
{allTags.map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTag(tag);
|
||||||
|
setSelectedFilter(null);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-2 py-1 rounded hover:bg-gray-100 ${
|
||||||
|
selectedTag === tag ? "bg-blue-100 font-semibold" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* 가운데 메인 영역 */}
|
||||||
|
<main className="flex-1 border-r border-gray-200 p-4 overflow-y-auto">
|
||||||
|
<TodoForm onTaskCreated={fetchTasks} />
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-bold">
|
||||||
|
📋{" "}
|
||||||
|
{(selectedTag && `태그: ${selectedTag}`) ||
|
||||||
|
FILTERS.find((f) => f.value === selectedFilter)?.label ||
|
||||||
|
"할 일"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TodoList
|
||||||
|
tasks={filteredTasks}
|
||||||
|
onSelectTask={setSelectedTask}
|
||||||
|
onToggleComplete={toggleComplete}
|
||||||
|
onTagClick={(tag) => {
|
||||||
|
setSelectedTag(tag);
|
||||||
|
setSelectedFilter(null);
|
||||||
|
}}
|
||||||
|
onDelete={deleteTask}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* 오른쪽 상세 Drawer */}
|
||||||
|
<section
|
||||||
|
className={`fixed top-0 right-0 h-full w-full md:w-1/2 lg:w-2/5 xl:w-1/3 bg-white shadow-lg border-l border-gray-300 transform transition-transform duration-300 ease-in-out z-50 ${
|
||||||
|
selectedTask ? "translate-x-0" : "translate-x-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={closeDetail}
|
||||||
|
className="absolute top-4 right-4 text-gray-600 hover:text-red-500 text-2xl font-bold"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
{selectedTask && (
|
||||||
|
<div className="p-6">
|
||||||
|
<TodoDetail
|
||||||
|
task={selectedTask}
|
||||||
|
onUpdated={() => {
|
||||||
|
fetchTasks(); // 목록 갱신
|
||||||
|
setSelectedTask(null); // 상세 창 닫기 (필요에 따라 유지 가능)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{selectedTask && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-30 z-40"
|
||||||
|
onClick={closeDetail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskPage;
|
Reference in New Issue
Block a user