diff --git a/my-app/components/todo/todo-detail-title.module.css b/my-app/components/todo/todo-detail-header.module.css similarity index 70% rename from my-app/components/todo/todo-detail-title.module.css rename to my-app/components/todo/todo-detail-header.module.css index f7d328d4..2d32a04f 100644 --- a/my-app/components/todo/todo-detail-title.module.css +++ b/my-app/components/todo/todo-detail-header.module.css @@ -1,4 +1,4 @@ -.todoDetailTitle { +.todoDetailHeader { background-color: white; border: 2px solid var(--color-state-900); border-radius: 24px; @@ -8,21 +8,31 @@ align-items: center; gap: 16px; height: 64px; - text-decoration: underline; } -.todoDetailTitle.checked { +.todoDetailHeader.checked { background-color: var(--color-violet-200); } -.todoDetailTitle input { - field-sizing: content; +.todoTitle, +.todoTitleMirror { + font-size: 20px; + font-weight: 800; + line-height: 100%; + text-decoration: underline; +} + +.todoTitle { padding: 0; background: none; border: none; outline: none; - font-size: 20px; - font-weight: 700; - line-height: 100%; +} + +.todoTitleMirror { + position: absolute; + visibility: hidden; + white-space: pre; + pointer-events: none; } .checkImage { diff --git a/my-app/components/todo/todo-detail-header.tsx b/my-app/components/todo/todo-detail-header.tsx new file mode 100644 index 00000000..ebe76218 --- /dev/null +++ b/my-app/components/todo/todo-detail-header.tsx @@ -0,0 +1,60 @@ +import { ChangeEvent, useEffect, useRef } from "react"; +import styles from "./todo-detail-header.module.css"; + +export default function TodoDetailHeader({ + name, + isCompleted, + onNameChange, + onCompletedChange, +}: { + name: string; + isCompleted: boolean; + onNameChange: (newName: string) => void; + onCompletedChange: (newCompleted: boolean) => void; +}) { + const spanRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (spanRef.current && inputRef.current) { + const newWidth = spanRef.current.offsetWidth + 4; + inputRef.current.style.width = `${newWidth}px`; + } + }, [inputRef.current?.value, spanRef.current]); + + let className = styles.todoDetailHeader; + if (isCompleted) { + className += ` ${styles.checked}`; + } + + const checkImage = isCompleted + ? "/images/checkbox-checked.svg" + : "/images/checkbox.svg"; + + const handleClick = () => { + onCompletedChange(!isCompleted); + }; + + const handleChange = (event: ChangeEvent) => { + onNameChange(event.target.value); + }; + + return ( +
+
+ check +
+
+ + + {name} + +
+
+ ); +} diff --git a/my-app/components/todo/todo-detail-image-preview.tsx b/my-app/components/todo/todo-detail-image-preview.tsx index 65494a45..25ed2886 100644 --- a/my-app/components/todo/todo-detail-image-preview.tsx +++ b/my-app/components/todo/todo-detail-image-preview.tsx @@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from "react"; import TodoDetailImageButton from "./todo-detail-image-button"; import styles from "./todo-detail-image-preview.module.css"; +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + export default function TodoDetailImagePreview({ imageUrl, onChange, @@ -20,8 +22,7 @@ export default function TodoDetailImagePreview({ const handlePreviewChanged = (file: File | null, reset: () => void) => { if (!file) return; - const maxSize = 5 * 1024 * 1024; - if (file.size > maxSize) { + if (file.size > MAX_FILE_SIZE) { alert("파일 크기는 5MB 이하여야 합니다."); reset(); return; diff --git a/my-app/components/todo/todo-detail-title.tsx b/my-app/components/todo/todo-detail-title.tsx deleted file mode 100644 index 5bc34ed5..00000000 --- a/my-app/components/todo/todo-detail-title.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ChangeEvent } from "react"; -import styles from "./todo-detail-title.module.css"; - -export default function TodoDetailTitle({ - name, - isCompleted, - onNameChange, - onCompletedChange, -}: { - name: string; - isCompleted: boolean; - onNameChange: (newName: string) => void; - onCompletedChange: (newCompleted: boolean) => void; -}) { - let className = styles.todoDetailTitle; - if (isCompleted) { - className += ` ${styles.checked}`; - } - - const checkImage = isCompleted - ? "/images/checkbox-checked.svg" - : "/images/checkbox.svg"; - - const handleClick = () => { - onCompletedChange(!isCompleted); - }; - - const handleChange = (event: ChangeEvent) => { - const span = document.createElement("span"); - span.textContent = event.target.value; - onNameChange(event.target.value); - }; - - return ( -
-
- check -
- -
- ); -} diff --git a/my-app/libs/apis/revalidate.ts b/my-app/libs/apis/revalidate.ts new file mode 100644 index 00000000..06b4fd00 --- /dev/null +++ b/my-app/libs/apis/revalidate.ts @@ -0,0 +1,9 @@ +export async function revalidate(paths: string[]) { + await fetch("/api/revalidate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ paths }), + }); +} diff --git a/my-app/pages/api/revalidate/index.ts b/my-app/pages/api/revalidate/index.ts new file mode 100644 index 00000000..b2d248c4 --- /dev/null +++ b/my-app/pages/api/revalidate/index.ts @@ -0,0 +1,24 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + try { + const { paths } = req.body; + + if (!paths || !Array.isArray(paths) || paths.length === 0) { + return res.status(400).json({ message: "Paths are required" }); + } + + await Promise.all(paths.map((path) => res.revalidate(path))); + + return res.json({ revalidated: true }); + } catch (err) { + return res.status(500).json({ message: "Error revalidating" }); + } +} diff --git a/my-app/pages/index.tsx b/my-app/pages/index.tsx index 6441cd88..ba844b2c 100644 --- a/my-app/pages/index.tsx +++ b/my-app/pages/index.tsx @@ -6,6 +6,7 @@ import TodoList from "@/components/todo/todo-list"; import TodoListItem from "@/components/todo/todo-list-item"; import { TodoStatus } from "@/components/todo/todo-status"; import { useAsyncCall } from "@/hooks/use-async-call"; +import { revalidate } from "@/libs/apis/revalidate"; import { addTodo, getTodos, toggleTodo } from "@/libs/apis/todo"; import styles from "@/styles/home.module.css"; import type { Todo } from "@/types"; @@ -17,7 +18,7 @@ import { useState, } from "react"; -export async function getServerSideProps() { +export async function getStaticProps() { const todos = await getTodos(); return { props: { todos } }; } @@ -43,18 +44,24 @@ export default function Home({ todos: initialTodos }: { todos: Todo[] }) { const handleAddClick: MouseEventHandler = async (event) => { event.preventDefault(); - execute(async () => { + await execute(async () => { const newTodo = await addTodo(inputValue); if (!newTodo) return; + + await revalidate(["/", `/items/${newTodo.id}`]); + setTodos((prevTodos) => [...prevTodos, newTodo]); setInputValue(""); }); }; const handleTodoChange = async (todo: Todo) => { - execute(async () => { + await execute(async () => { const updatedTodo = await toggleTodo(todo); if (!updatedTodo) return; + + await revalidate(["/", `/items/${todo.id}`]); + setTodos((prevTodos) => { const index = prevTodos.findIndex( (prevTodo) => prevTodo.id === todo.id diff --git a/my-app/pages/items/[id]/index.tsx b/my-app/pages/items/[id]/index.tsx index 7a32ed26..4e301cdc 100644 --- a/my-app/pages/items/[id]/index.tsx +++ b/my-app/pages/items/[id]/index.tsx @@ -1,18 +1,27 @@ import Button from "@/components/button/button"; import { ButtonType } from "@/components/button/button-type"; import Portal from "@/components/portal/portal"; +import TodoDetailHeader from "@/components/todo/todo-detail-header"; import TodoDetailImagePreview from "@/components/todo/todo-detail-image-preview"; -import TodoDetailTitle from "@/components/todo/todo-detail-title"; import { useAsyncCall } from "@/hooks/use-async-call"; import { uploadImage } from "@/libs/apis/image"; -import { deleteTodo, editTodo, getTodo } from "@/libs/apis/todo"; +import { revalidate } from "@/libs/apis/revalidate"; +import { deleteTodo, editTodo, getTodo, getTodos } from "@/libs/apis/todo"; import styles from "@/styles/item.module.css"; import type { Todo } from "@/types"; -import { GetServerSidePropsContext } from "next"; +import { GetStaticPropsContext } from "next"; import { useRouter } from "next/router"; import { ChangeEvent, useMemo, useState } from "react"; -export async function getServerSideProps(context: GetServerSidePropsContext) { +export async function getStaticPaths() { + const todos = await getTodos(); + const paths = todos.map((todo) => ({ + params: { id: todo.id.toString() }, + })); + return { paths, fallback: false }; +} + +export async function getStaticProps(context: GetStaticPropsContext) { const { id } = context.params!; const todo = await getTodo(Number(id)); @@ -34,10 +43,6 @@ type TodoValuesState = Pick; export default function Page({ todo }: { todo: Todo }) { const router = useRouter(); - if (!todo) { - return
Todo not found
; - } - const [todoValues, setTodoValues] = useState({ name: todo.name, memo: todo.memo, @@ -96,7 +101,8 @@ export default function Page({ todo }: { todo: Todo }) { const result = await editTodo(todo.id, updateValues); if (result) { - router.replace("/"); + await revalidate(["/", `/items/${todo.id}`]); + router.push("/"); } }); }; @@ -121,7 +127,7 @@ export default function Page({ todo }: { todo: Todo }) { <>
-