diff --git a/README.md b/README.md index 38a37188f..e35b2b88e 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,8 @@ state 관리하고 있는 params과 usePageSize로 관리하는 pageSize의 변 - 직접 값을 업데이트해줄때는 (특수한 필드의 경우), useForm의 반환값중 setValue를 이용하면 값 업데이트와 벨리데이션이 작동한다. - react hook form에 컴포넌트를 연결할때에는 한겹의 어뎁터 레이어를 설정하여 컴포넌트 내부에서 react hook form의 의존성이 없도록 작성하는 방법을 사용하자. - watch를 통해 각 필드에 value값을 전달하면 하나의 필드가 업데이트 될때마다 전체가 리랜더링이 되어버린다. (Controller, useController 등을 통해서 전달해야함) + +### nextjs server component에서 try/catch문 안에서 redirect 사용하기 + +- 서버컴포넌트에서 try,catch문안에서 redirect사용시 문제가 됬던점에 대해서 정리했습니다. +- https://heavy-bear.tistory.com/16 diff --git a/package-lock.json b/package-lock.json index 71f303537..39be576e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.9.1", + "@tanstack/react-query": "^5.64.1", "axios": "^1.7.8", "clsx": "^2.1.1", "dayjs": "^1.11.13", @@ -1190,6 +1191,32 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", + "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", + "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.64.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", diff --git a/package.json b/package.json index 0320aea9a..570403eb7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.1", + "@tanstack/react-query": "^5.64.1", "axios": "^1.7.8", "clsx": "^2.1.1", "dayjs": "^1.11.13", diff --git a/src/app/(auth)/_components/AuthContainer.tsx b/src/app/(auth)/_components/AuthContainer.tsx index 16a1d4302..520383962 100644 --- a/src/app/(auth)/_components/AuthContainer.tsx +++ b/src/app/(auth)/_components/AuthContainer.tsx @@ -1,12 +1,11 @@ -import { ReactNode } from "react"; +import { PropsWithChildren } from "react"; import Link from "next/link"; import Image from "next/image"; import logo from "@assets/img/common/logo_full.svg"; import Oauth from "./Oauth"; import styles from "./AuthContainer.module.scss"; -interface AuthContainerProps { - children: ReactNode; +interface AuthContainerProps extends PropsWithChildren { mode?: "login" | "signup"; } diff --git a/src/app/(auth)/_components/LoginForm.tsx b/src/app/(auth)/_components/LoginForm.tsx index 22700608d..9eb48d8a6 100644 --- a/src/app/(auth)/_components/LoginForm.tsx +++ b/src/app/(auth)/_components/LoginForm.tsx @@ -1,22 +1,22 @@ "use client"; -import { useRouter } from "next/navigation"; -import { Form, FieldItem, Input } from "@components/Field"; +import { FieldItem, Input } from "@components/Field"; import { Button } from "@components/ui"; import useFormWithError from "@hooks/useFormWithError"; import { zodResolver } from "@hookform/resolvers/zod"; import { signinFormSchmea, SigninFormType } from "@schemas/auth"; import { FieldAdapter } from "@components/adaptor/rhf"; -import { signIn } from "next-auth/react"; +import { ServerForm } from "@/components/Field/ServerForm"; +import { useActionState } from "react"; +import action from "../login/action"; export default function LoginForm() { - const router = useRouter(); - + const [formStatus, formAction, isPending] = useActionState(action, { + message: "", + }); const { control, - formError, - handleSubmit, - formState: { isSubmitting, isValid }, + formState: { isValid }, } = useFormWithError({ mode: "onBlur", resolver: zodResolver(signinFormSchmea), @@ -26,29 +26,11 @@ export default function LoginForm() { }, }); - async function onSubmit(data: SigninFormType) { - try { - const respone = await signIn("credentials", { - redirect: false, - ...data, - }); - - if (respone?.error) { - throw new Error(respone?.code); - } - - alert("로그인에 성공했습니다."); - router.replace("/items"); - } catch (err) { - throw err; - } - } - return ( -
이메일 @@ -81,6 +63,6 @@ export default function LoginForm() { - + ); } diff --git a/src/app/(auth)/_components/SignupForm.tsx b/src/app/(auth)/_components/SignupForm.tsx index d6a9c1504..66ef2765c 100644 --- a/src/app/(auth)/_components/SignupForm.tsx +++ b/src/app/(auth)/_components/SignupForm.tsx @@ -1,22 +1,23 @@ "use client"; -import { useRouter } from "next/navigation"; -import { Form, FieldItem, Input } from "@components/Field"; +import { FieldItem, Input } from "@components/Field"; import { Button } from "@components/ui"; import useFormWithError from "@hooks/useFormWithError"; import { zodResolver } from "@hookform/resolvers/zod"; import { signupFormSchema, SignupFormType } from "@schemas/auth"; import { FieldAdapter } from "@components/adaptor/rhf"; -import { signUp } from "@/service/auth"; +import action from "../signup/action"; +import { useActionState } from "react"; +import { ServerForm } from "@/components/Field/ServerForm"; export default function SignupForm() { - const router = useRouter(); + const [formStatus, formAction, isPending] = useActionState(action, { + message: "", + }); const { control, - formError, - handleSubmit, - formState: { isSubmitting, isValid }, + formState: { isValid }, } = useFormWithError({ mode: "onBlur", resolver: zodResolver(signupFormSchema), @@ -28,21 +29,11 @@ export default function SignupForm() { }, }); - async function onSubmit(data: SignupFormType) { - try { - await signUp(data); - alert("회원가입에 성공했습니다. \n로그인 페이지로 이동합니다."); - router.replace("/login"); - } catch (err) { - throw err; - } - } - return ( -
이메일 @@ -101,6 +92,6 @@ export default function SignupForm() { - + ); } diff --git a/src/app/(auth)/login/action.ts b/src/app/(auth)/login/action.ts new file mode 100644 index 000000000..338b13501 --- /dev/null +++ b/src/app/(auth)/login/action.ts @@ -0,0 +1,42 @@ +"use server"; + +import { signIn } from "@/auth"; +import { signinFormSchmea } from "@/schemas/auth"; +import { CredentialsSignin } from "next-auth"; +import { isRedirectError } from "next/dist/client/components/redirect-error"; +import { redirect } from "next/navigation"; +export default async function action( + prevState: { message: string }, + formData: FormData +) { + const parsed = signinFormSchmea.safeParse(Object.fromEntries(formData)); + + if (!parsed.success) { + return { + message: "제출양식에 문제가 있습니다. 확인해주세요", + }; + } + + try { + await signIn("credentials", { + email: parsed.data.email, + password: parsed.data.password, + redirect: false, + }); + redirect("/"); + } catch (error) { + if (isRedirectError(error)) { + throw error; + } + + if (error instanceof CredentialsSignin) { + return { + message: `로그인 실패 : ${error.code}`, + }; + } + } + + return { + message: "로그인 성공", + }; +} diff --git a/src/app/(auth)/signup/action.ts b/src/app/(auth)/signup/action.ts new file mode 100644 index 000000000..55a281dda --- /dev/null +++ b/src/app/(auth)/signup/action.ts @@ -0,0 +1,47 @@ +"use server"; + +import { signupFormSchema } from "@/schemas/auth"; +import { signUp } from "@/service/auth"; +import { isAxiosError } from "axios"; +import { isRedirectError } from "next/dist/client/components/redirect-error"; +import { redirect } from "next/navigation"; + +export default async function action( + prevState: { message: string }, + formData: FormData +) { + const parsed = signupFormSchema.safeParse(Object.fromEntries(formData)); + + if (!parsed.success) { + return { + message: "제출양식에 문제가 있습니다. 확인해주세요", + }; + } + + try { + await signUp({ + email: parsed.data.email, + nickname: parsed.data.nickname, + password: parsed.data.password, + passwordConfirmation: parsed.data.passwordConfirmation, + }); + + redirect("/login"); + } catch (error) { + if (isRedirectError(error)) { + throw error; + } + + if (isAxiosError(error)) { + const message = + error.response?.data.message || "알 수 없는 에러가 발생했어요."; + return { + message: `회원가입 실패 : ${message}`, + }; + } + } + + return { + message: "회원가입 성공", + }; +} diff --git a/src/app/(common)/(board)/_components/ArticleForm.tsx b/src/app/(common)/(board)/_components/ArticleForm.tsx new file mode 100644 index 000000000..8365569fb --- /dev/null +++ b/src/app/(common)/(board)/_components/ArticleForm.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { Section } from "@components/Section"; +import { + FieldItem, + Form, + ImageUpload, + Input, + Textarea, +} from "@components/Field"; +import { Button } from "@components/ui"; +import useFormWithError from "@hooks/useFormWithError"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Article } from "@/types/article"; +import { FieldAdapter } from "@components/adaptor/rhf"; +import { useRouter } from "next/navigation"; +import useArticleActions from "./useArticleActions"; +import { ArticleFormSchema, ArticleFormType } from "@/schemas/article"; + +interface ArticleFormProps { + initialData?: Article; + mode?: "add" | "edit"; + articleId?: number; +} + +export default function ArticleForm({ + initialData, + mode = "add", + articleId, +}: ArticleFormProps) { + const router = useRouter(); + const { handleArticleAdd, handleArticleModify } = + useArticleActions(articleId); + const onFormSubmit = mode === "add" ? handleArticleAdd : handleArticleModify; + + const { + control, + formError, + handleSubmit, + formState: { isSubmitting, isValid }, + } = useFormWithError({ + mode: "onBlur", + resolver: zodResolver(ArticleFormSchema), + defaultValues: initialData || { + title: "", + content: "", + }, + }); + + async function onSubmit(data: ArticleFormType) { + try { + const result = await onFormSubmit(data); + const id = result?.id; + + alert( + mode === "add" ? "성공적으로 작성했습니다." : "성공적으로 수정했습니다." + ); + router.replace(id ? `/boards/${id}` : "boards"); + } catch (err) { + throw err; + } + } + + return ( +
+
+ + + + + + 제목 + ( + + )} + /> + + + 내용 + ( +