Skip to content

Commit 81df29e

Browse files
Merge pull request #244 from LeeTaegyung/Next-이태경-sprint9-r
[이태경] Sprint9/Refactor
2 parents 48c4a5f + 2bf93df commit 81df29e

File tree

8 files changed

+58
-49
lines changed

8 files changed

+58
-49
lines changed

src/app/actions.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use server";
2+
3+
import { revalidateTag } from "next/cache";
4+
5+
import { createTodoItem } from "@/features/todo/services/todoApi";
6+
7+
export async function createTodoItemAction(_: any, formData: FormData) {
8+
const name = formData.get("name")?.toString();
9+
10+
if (!name) return;
11+
12+
try {
13+
const data = await createTodoItem(name);
14+
15+
revalidateTag("todoList");
16+
17+
return data;
18+
} catch (err) {
19+
console.log(err);
20+
return;
21+
}
22+
}

src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import localFont from "next/font/local";
2+
23
import type { Metadata } from "next";
4+
35
import "./globals.css";
46
import Header from "@/components/Header";
57

src/features/todo/components/TodoAddForm.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,32 @@
11
"use client";
22

3+
import { ChangeEvent, useActionState, useEffect, useState } from "react";
4+
5+
import { createTodoItemAction } from "@/app/actions";
36
import Button from "@/components/Button";
4-
import { createTodoItem } from "@/features/todo/services/todoApi";
5-
import { useRouter } from "next/navigation";
6-
import { ChangeEvent, FormEvent, useRef, useState } from "react";
77

88
const TodoAddForm = () => {
9+
const [state, formAction, isPending] = useActionState(
10+
createTodoItemAction,
11+
null
12+
);
913
const [todoText, setTodoText] = useState("");
10-
const loadingRef = useRef<boolean>(false);
11-
const router = useRouter();
12-
const isValid = !(todoText.trim().length > 0) || loadingRef.current;
14+
const isValid = todoText.trim().length === 0 || isPending;
1315

1416
const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
1517
setTodoText(e.target.value);
1618

17-
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
18-
e.preventDefault();
19-
if (loadingRef.current) return; // 중복 요청 방지
20-
try {
21-
loadingRef.current = true;
22-
await createTodoItem(todoText.trim());
23-
router.refresh(); // 서버컴포넌트 새로고침
24-
} catch (error) {
25-
console.error(error);
26-
} finally {
27-
setTodoText(""); // input 초기화
28-
loadingRef.current = false;
19+
useEffect(() => {
20+
if (state) {
21+
setTodoText("");
2922
}
30-
};
23+
}, [state]);
3124

3225
return (
33-
<form className="flex gap-5" onSubmit={handleSubmit}>
26+
<form className="flex gap-5" action={formAction}>
3427
<input
3528
type="text"
29+
name="name"
3630
value={todoText}
3731
onChange={handleChange}
3832
placeholder="할 일을 입력해주세요"

src/features/todo/components/TodoListArea.tsx

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
"use client";
22

3-
import DoneEmpty from "@/features/todo/components/DoneEmpty";
3+
import { startTransition, useOptimistic, useRef } from "react";
4+
5+
import DoneEmpty from "@/app/_components/Empty/DoneEmpty";
6+
import TodoEmpty from "@/app/_components/Empty/TodoEmpty";
47
import TodoContent from "@/features/todo/components/TodoContent";
5-
import TodoEmpty from "@/features/todo/components/TodoEmpty";
68
import { updateTodoItem } from "@/features/todo/services/todoApi";
79
import { TodoItemType } from "@/types/todoTypes";
8-
import { startTransition, useEffect, useOptimistic, useState } from "react";
910

1011
interface Props {
1112
data: TodoItemType[];
1213
onUpdate?: (id: number) => void;
1314
}
1415

1516
const TodoListArea = ({ data }: Props) => {
16-
const [todoAll, setTodoAll] = useState(data);
17+
const initialDataRef = useRef<TodoItemType[]>(data);
1718
const [optimisticState, toggleOptimisticState] = useOptimistic<
1819
TodoItemType[],
1920
number
20-
>(todoAll, (currentState, id) => {
21+
>(initialDataRef.current, (currentState, id) => {
2122
return currentState.map((todo) =>
2223
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
2324
);
@@ -28,30 +29,23 @@ const TodoListArea = ({ data }: Props) => {
2829
// 낙관적 업데이트
2930
toggleOptimisticState(id);
3031

31-
const targetCheck = todoAll.find((todo) => todo.id === id)?.isCompleted;
32+
const targetCheck = optimisticState.find(
33+
(todo) => todo.id === id
34+
)?.isCompleted;
3235
const updateData = { isCompleted: !targetCheck };
3336

34-
const prevTodoAll = todoAll;
35-
3637
try {
37-
setTodoAll((prevTodoAll) =>
38-
prevTodoAll.map((todo) =>
39-
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
40-
)
41-
);
4238
await updateTodoItem(id, updateData);
39+
const updateTodo = initialDataRef.current.map((todo) =>
40+
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
41+
);
42+
initialDataRef.current = updateTodo;
4343
} catch (error) {
4444
console.error(error);
45-
46-
setTodoAll(prevTodoAll);
4745
}
4846
});
4947
};
5048

51-
useEffect(() => {
52-
setTodoAll(data);
53-
}, [data]);
54-
5549
const todoData = optimisticState.filter((item) => !item.isCompleted);
5650
const doneData = optimisticState.filter((item) => item.isCompleted);
5751

src/features/todo/services/todoApi.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
2-
PatchTodoResponse,
3-
PostTodoResponse,
2+
TodoResponseType,
43
TodoItemType,
54
UpdateTodoData,
65
} from "@/types/todoTypes";
@@ -10,7 +9,9 @@ const API_END_POINT = process.env.NEXT_PUBLIC_SERVER_COMMON_END_POINT;
109
const BASE_URL = `${API_URL}${API_END_POINT}`;
1110

1211
export async function getTodoList(): Promise<TodoItemType[]> {
13-
const res = await fetch(`${BASE_URL}/items`);
12+
const res = await fetch(`${BASE_URL}/items`, {
13+
next: { tags: ["todoList"] },
14+
});
1415

1516
if (!res.ok) throw new Error(res.statusText);
1617

@@ -19,7 +20,7 @@ export async function getTodoList(): Promise<TodoItemType[]> {
1920
return data;
2021
}
2122

22-
export async function createTodoItem(name: string): Promise<PostTodoResponse> {
23+
export async function createTodoItem(name: string): Promise<TodoResponseType> {
2324
const res = await fetch(`${BASE_URL}/items`, {
2425
method: "POST",
2526
headers: {
@@ -38,7 +39,7 @@ export async function createTodoItem(name: string): Promise<PostTodoResponse> {
3839
export async function updateTodoItem(
3940
id: number,
4041
updateData: UpdateTodoData
41-
): Promise<PatchTodoResponse> {
42+
): Promise<TodoResponseType> {
4243
const res = await fetch(`${BASE_URL}/items/${id}`, {
4344
method: "PATCH",
4445
headers: {

src/types/todoTypes.d.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,11 @@ export interface UpdateTodoData {
1111
isCompleted?: boolean;
1212
}
1313

14-
interface TodoResponseBase {
14+
export interface TodoResponseType {
1515
id: number;
1616
tenantId: string;
1717
name: string;
1818
memo: string | null;
1919
imageUrl: string | null;
2020
isCompleted: boolean;
2121
}
22-
23-
export interface PostTodoResponse extends TodoResponseBase {}
24-
25-
export interface PatchTodoResponse extends TodoResponseBase {}

0 commit comments

Comments
 (0)