diff --git a/src/features/todo/components/DoneEmpty.tsx b/src/app/_components/Empty/DoneEmpty.tsx similarity index 100% rename from src/features/todo/components/DoneEmpty.tsx rename to src/app/_components/Empty/DoneEmpty.tsx diff --git a/src/features/todo/components/TodoEmpty.tsx b/src/app/_components/Empty/TodoEmpty.tsx similarity index 100% rename from src/features/todo/components/TodoEmpty.tsx rename to src/app/_components/Empty/TodoEmpty.tsx diff --git a/src/app/actions.ts b/src/app/actions.ts new file mode 100644 index 00000000..17dbd40c --- /dev/null +++ b/src/app/actions.ts @@ -0,0 +1,22 @@ +"use server"; + +import { revalidateTag } from "next/cache"; + +import { createTodoItem } from "@/features/todo/services/todoApi"; + +export async function createTodoItemAction(_: any, formData: FormData) { + const name = formData.get("name")?.toString(); + + if (!name) return; + + try { + const data = await createTodoItem(name); + + revalidateTag("todoList"); + + return data; + } catch (err) { + console.log(err); + return; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2fb6a361..0964e1ba 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,7 @@ import localFont from "next/font/local"; + import type { Metadata } from "next"; + import "./globals.css"; import Header from "@/components/Header"; diff --git a/src/features/todo/components/TodoAddForm.tsx b/src/features/todo/components/TodoAddForm.tsx index 4092ebcb..4e8d47dc 100644 --- a/src/features/todo/components/TodoAddForm.tsx +++ b/src/features/todo/components/TodoAddForm.tsx @@ -1,38 +1,32 @@ "use client"; +import { ChangeEvent, useActionState, useEffect, useState } from "react"; + +import { createTodoItemAction } from "@/app/actions"; import Button from "@/components/Button"; -import { createTodoItem } from "@/features/todo/services/todoApi"; -import { useRouter } from "next/navigation"; -import { ChangeEvent, FormEvent, useRef, useState } from "react"; const TodoAddForm = () => { + const [state, formAction, isPending] = useActionState( + createTodoItemAction, + null + ); const [todoText, setTodoText] = useState(""); - const loadingRef = useRef(false); - const router = useRouter(); - const isValid = !(todoText.trim().length > 0) || loadingRef.current; + const isValid = todoText.trim().length === 0 || isPending; const handleChange = (e: ChangeEvent) => setTodoText(e.target.value); - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - if (loadingRef.current) return; // 중복 요청 방지 - try { - loadingRef.current = true; - await createTodoItem(todoText.trim()); - router.refresh(); // 서버컴포넌트 새로고침 - } catch (error) { - console.error(error); - } finally { - setTodoText(""); // input 초기화 - loadingRef.current = false; + useEffect(() => { + if (state) { + setTodoText(""); } - }; + }, [state]); return ( -
+ { - const [todoAll, setTodoAll] = useState(data); + const initialDataRef = useRef(data); const [optimisticState, toggleOptimisticState] = useOptimistic< TodoItemType[], number - >(todoAll, (currentState, id) => { + >(initialDataRef.current, (currentState, id) => { return currentState.map((todo) => todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo ); @@ -28,30 +29,23 @@ const TodoListArea = ({ data }: Props) => { // 낙관적 업데이트 toggleOptimisticState(id); - const targetCheck = todoAll.find((todo) => todo.id === id)?.isCompleted; + const targetCheck = optimisticState.find( + (todo) => todo.id === id + )?.isCompleted; const updateData = { isCompleted: !targetCheck }; - const prevTodoAll = todoAll; - try { - setTodoAll((prevTodoAll) => - prevTodoAll.map((todo) => - todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo - ) - ); await updateTodoItem(id, updateData); + const updateTodo = initialDataRef.current.map((todo) => + todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo + ); + initialDataRef.current = updateTodo; } catch (error) { console.error(error); - - setTodoAll(prevTodoAll); } }); }; - useEffect(() => { - setTodoAll(data); - }, [data]); - const todoData = optimisticState.filter((item) => !item.isCompleted); const doneData = optimisticState.filter((item) => item.isCompleted); diff --git a/src/features/todo/services/todoApi.ts b/src/features/todo/services/todoApi.ts index 0c3f888d..87597965 100644 --- a/src/features/todo/services/todoApi.ts +++ b/src/features/todo/services/todoApi.ts @@ -1,6 +1,5 @@ import { - PatchTodoResponse, - PostTodoResponse, + TodoResponseType, TodoItemType, UpdateTodoData, } from "@/types/todoTypes"; @@ -10,7 +9,9 @@ const API_END_POINT = process.env.NEXT_PUBLIC_SERVER_COMMON_END_POINT; const BASE_URL = `${API_URL}${API_END_POINT}`; export async function getTodoList(): Promise { - const res = await fetch(`${BASE_URL}/items`); + const res = await fetch(`${BASE_URL}/items`, { + next: { tags: ["todoList"] }, + }); if (!res.ok) throw new Error(res.statusText); @@ -19,7 +20,7 @@ export async function getTodoList(): Promise { return data; } -export async function createTodoItem(name: string): Promise { +export async function createTodoItem(name: string): Promise { const res = await fetch(`${BASE_URL}/items`, { method: "POST", headers: { @@ -38,7 +39,7 @@ export async function createTodoItem(name: string): Promise { export async function updateTodoItem( id: number, updateData: UpdateTodoData -): Promise { +): Promise { const res = await fetch(`${BASE_URL}/items/${id}`, { method: "PATCH", headers: { diff --git a/src/types/todoTypes.d.ts b/src/types/todoTypes.d.ts index b7f38299..25afe9af 100644 --- a/src/types/todoTypes.d.ts +++ b/src/types/todoTypes.d.ts @@ -11,7 +11,7 @@ export interface UpdateTodoData { isCompleted?: boolean; } -interface TodoResponseBase { +export interface TodoResponseType { id: number; tenantId: string; name: string; @@ -19,7 +19,3 @@ interface TodoResponseBase { imageUrl: string | null; isCompleted: boolean; } - -export interface PostTodoResponse extends TodoResponseBase {} - -export interface PatchTodoResponse extends TodoResponseBase {}