Repository: moyeora-it/moyeora-it
Pull request #30: 자동 PR: feature-di-auth → dev
Author: login: rich-manager
Labels: name: auto-updated
description: 자동 업데이트된 PR
Created: 2025-05-20T06:35:01Z | Merged: 2025-05-21T05:18:12Z

feat: 아직 미사용기능 주석처리
feat: 회원가입 폼 생성
feat: 회원가입 폼 생성
feat: 회원가입 폼 생성
Merge branch 'dev' into feat-di-auth
fix: package 충돌 해결
Merge branch 'dev' into feat-di-auth
refactor: min(1)을 nonempty로 알아보기 쉽게 변경
feat: InputText를 텍스트관련 입력으로 타입을 제한, placeholder의 경우 placeholder="비밀번호"는 props로 받는 placeholder가 겹쳐져서 의미가 불필요하여 삭제
feature: InputText 컴포넌트 생성
Merge branch 'dev' into feat-di-auth-login
feature: 로그인 폼생성, 로그인 로직 생성
Merge branch 'dev' into feat-di-auth-login
refactor: 페이지 function 으로 변경
feat: 인증과 관련된 페이지 client컴포넌트로 변경
feat: 인증과 관련된 페이지 생성


Diff:
diff --git a/package.json b/package.json
index 54353d3a..cb2e9edb 100644
--- a/package.json
+++ b/package.json
@@ -6,11 +6,16 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint",
-    "format": "npx prettier --write ."
+    "lint": "next lint"
   },
   "dependencies": {
+    "@hookform/resolvers": "^5.0.1",
     "@radix-ui/react-avatar": "^1.1.9",
+    "@radix-ui/react-checkbox": "^1.3.1",
+    "@radix-ui/react-dialog": "^1.1.13",
+    "@radix-ui/react-label": "^2.1.6",
+    "@radix-ui/react-progress": "^1.1.6",
+    "@radix-ui/react-radio-group": "^1.3.6",
     "@radix-ui/react-slot": "^1.2.2",
     "@tanstack/react-query": "^5.76.1",
     "class-variance-authority": "^0.7.1",
@@ -18,10 +23,12 @@
     "lucide-react": "^0.511.0",
     "msw": "^2.8.4",
     "next": "15.3.2",
+    "next-themes": "^0.4.6",
     "postcss": "^8.5.3",
     "react": "^19.0.0",
     "react-dom": "^19.1.0",
     "react-hook-form": "^7.56.3",
+    "sonner": "^2.0.3",
     "tailwind-merge": "^3.3.0",
     "zod": "^3.24.4",
     "zustand": "^5.0.4"
@@ -36,7 +43,6 @@
     "@types/react-dom": "^19",
     "eslint": "^9",
     "eslint-config-next": "15.3.2",
-    "eslint-config-prettier": "^10.1.5",
     "jest": "^29.7.0",
     "tailwindcss": "^4.1.6",
     "tw-animate-css": "^1.3.0",
diff --git a/src/app/(auth)/find-email/page.tsx b/src/app/(auth)/find-email/page.tsx
new file mode 100644
index 00000000..6ca8ee36
--- /dev/null
+++ b/src/app/(auth)/find-email/page.tsx
@@ -0,0 +1,71 @@
+'use client';
+import { Button } from '@/components/ui/button';
+import { z } from 'zod';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Form } from '@/components/ui/form';
+import { useState } from 'react';
+import Link from 'next/link';
+import { InputTextField } from '@/components/molecules/input-text-field';
+
+const formSchema = z.object({
+	email: z.string().nonempty({ message: '이메일을 입력해주세요' }).email({
+		message: '유효한 이메일이 아닙니다.',
+	}),
+});
+
+export default function Page() {
+	const form = useForm<z.infer<typeof formSchema>>({
+		resolver: zodResolver(formSchema),
+		defaultValues: {
+			email: '',
+		},
+	});
+
+	const [isExisted, setIsExisted] = useState(false);
+	const [isNotExisted, setIsNotExisted] = useState(false);
+
+	// 이메일 찾기
+	const onSubmit = async (values: z.infer<typeof formSchema>) => {
+		console.log(values);
+		try {
+			// TODO: 이메일 찾기 로직 작성 /find-email
+			setIsExisted(true);
+		} catch (e) {
+			// TODO: 이메일 찾기 실패시 에러코드 맞춰서 설정해주기
+			setIsNotExisted(true);
+			console.log(e);
+		}
+	};
+
+	return (
+		<div>
+			<Form {...form}>
+				<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+					<InputTextField
+						form={form}
+						name="email"
+						label="이메일"
+						type="email"
+						placeholder="myemail@mail.com"
+					/>
+
+					{/* 성공시 로그인페이지로 갈지 비밀번호 찾기로 갈지 물어보기 */}
+					{isExisted && (
+						<div className="flex flex-col">
+							<p className="text-blue-900">해당 이메일이 존재합니다!</p>
+							<Link href="/login">로그인하러 가기</Link>
+							<Link href="/reset-password">비밀번호 찾기</Link>
+						</div>
+					)}
+
+					{isNotExisted && (
+						<p className="text-red-600">해당 이메일이 존재하지 않습니다</p>
+					)}
+					<Button>이메일 찾기</Button>
+				</form>
+			</Form>
+			<Link href="/login">로그인으로 돌아가기</Link>
+		</div>
+	);
+}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
new file mode 100644
index 00000000..68ecded0
--- /dev/null
+++ b/src/app/(auth)/login/page.tsx
@@ -0,0 +1,74 @@
+'use client';
+import { Button } from '@/components/ui/button';
+import { z } from 'zod';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Form } from '@/components/ui/form';
+import { useState } from 'react';
+import Link from 'next/link';
+import { InputTextField } from '@/components/molecules/input-text-field';
+
+const formSchema = z.object({
+	email: z.string().nonempty({ message: '이메일을 입력해주세요' }).email({
+		message: '유효한 이메일이 아닙니다.',
+	}),
+	password: z.string().nonempty({ message: '비밀번호를 입력해주세요' }),
+});
+
+export default function Page() {
+	const form = useForm<z.infer<typeof formSchema>>({
+		resolver: zodResolver(formSchema),
+		defaultValues: {
+			email: '',
+			password: '',
+		},
+	});
+	const [isLoginFailed, setIsLoginFailed] = useState(false);
+	// const login = useAuthStore((s) => s.login);
+
+	// 로그인
+	const onSubmit = async (values: z.infer<typeof formSchema>) => {
+		console.log(values);
+		try {
+			// TODO: 로그인 로직 작성 /login
+			// TODO: 로그인 성공 후 회원정보 불러오기 /me
+		} catch (e) {
+			// TODO: 로그인 실패시 에러코드 맞춰서 설정해주기
+			setIsLoginFailed(true);
+			console.log(e);
+		}
+	};
+
+	return (
+		<div>
+			<Form {...form}>
+				<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+					<InputTextField
+						form={form}
+						name="email"
+						label="이메일"
+						type="email"
+						placeholder="myemail@mail.com"
+					/>
+					<InputTextField
+						form={form}
+						name="password"
+						label="비밀번호"
+						type="password"
+						placeholder="나만의 비밀번호..."
+					/>
+
+					{isLoginFailed && (
+						<p className="text-red-600">로그인에 실패했습니다</p>
+					)}
+					<Button>로그인</Button>
+				</form>
+			</Form>
+			<div className="flex flex-col">
+				<Link href="/register">회원가입</Link>
+				<Link href="/find-email">이메일 찾기</Link>
+				<Link href="/reset-password">비밀번호 찾기</Link>
+			</div>
+		</div>
+	);
+}
diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx
new file mode 100644
index 00000000..1fe07b50
--- /dev/null
+++ b/src/app/(auth)/register/page.tsx
@@ -0,0 +1,193 @@
+'use client';
+import { Button } from '@/components/ui/button';
+import { z } from 'zod';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Form } from '@/components/ui/form';
+import { useState } from 'react';
+import useAuthStore from '@/stores/useAuthStore';
+import { Position, Skill } from '@/types/enums';
+import { InputTextField } from '@/components/molecules/input-text-field';
+import { FormRadioGroupField } from '@/components/molecules/input-radiogroup-field';
+import { FormCheckboxGroupField } from '@/components/molecules/input-checkbox-field';
+import Link from 'next/link';
+
+const positions = Object.keys(Position).filter((k) => isNaN(Number(k))) as [
+  string,
+  ...string[],
+];
+const skills = Object.keys(Skill).filter((k) => isNaN(Number(k))) as [
+  string,
+  ...string[],
+];
+
+// 회원가입에 쓰이는 이메일과 비밀번호 유효성
+// 규칙은 네이버와 똑같이 했습니다
+const registerFormSchema = z
+  .object({
+    email: z.string().nonempty({ message: '이메일을 입력해주세요' }).email({
+      message: '유효한 이메일이 아닙니다.',
+    }),
+    password: z
+      .string()
+      .nonempty({ message: '비밀번호를 입력해주세요' })
+      .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[\W_]).{8,}$/, {
+        message: '영어 대/소문자, 숫자, 특수문자를 혼합하여 8자리 이상',
+      }),
+    passwordConfirm: z.string().nonempty({
+      message: '비밀번호를 다시 입력해주세요',
+    }),
+  })
+  .superRefine(({ password, passwordConfirm }, ctx) => {
+    if (password && passwordConfirm && password !== passwordConfirm) {
+      ctx.addIssue({
+        path: ['passwordConfirm'],
+        message: '비밀번호가 일치하지 않습니다.',
+        code: z.ZodIssueCode.custom,
+      });
+    }
+  });
+
+// 회원가입 후 옵셔널 정보 유효성
+// 규칙을 네이버와 같이 했습니다
+const optionalFormSchema = z.object({
+  nickname: z
+    .string()
+    .nonempty({
+      message: '닉네임을 입력해주세요',
+    })
+    .min(2, '닉네임은 2~8자리여야 합니다.')
+    .max(8, '닉네임은 2~8자리여야 합니다.')
+    .regex(/^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$/, '한글, 영어, 숫자만 입력 가능합니다.'),
+  position: z.enum(positions, {
+    message: '포지션을 선택해주세요',
+  }),
+  skills: z.array(z.enum(skills)),
+});
+
+export default function Page() {
+  const registerForm = useForm<z.infer<typeof registerFormSchema>>({
+    resolver: zodResolver(registerFormSchema),
+    defaultValues: {
+      email: '',
+      password: '',
+      passwordConfirm: '',
+    },
+  });
+  const [isRegisterFailed, setIsRegisterFailed] = useState(false);
+  const { user } = useAuthStore();
+
+  const optionalForm = useForm<z.infer<typeof optionalFormSchema>>({
+    resolver: zodResolver(optionalFormSchema),
+    defaultValues: {
+      nickname: '',
+      position: '',
+      skills: [],
+    },
+  });
+
+  // 회원가입
+  const onRegisterSubmit = async (
+    values: z.infer<typeof registerFormSchema>,
+  ) => {
+    console.log(values);
+    try {
+      // TODO: 회원가입 로직 작성 /register
+      // TODO: 회원가입 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 회원가입 실패시 에러코드 맞춰서 설정해주기
+      setIsRegisterFailed(true);
+      console.log(e);
+    }
+  };
+
+  // 옵션 설정
+  const onOptionalSubmit = async (
+    values: z.infer<typeof optionalFormSchema>,
+  ) => {
+    console.log(values);
+    try {
+      //  TODO: 프로필 옵션 설정
+    } catch (e) {
+      // TODO: 프로필 에러 설정 //
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      {/* 회원가입 전 로그인이 되지 않은 상태의 경우 회원가입 폼을 출력 */}
+      {!user && (
+        <Form {...registerForm}>
+          <form
+            onSubmit={registerForm.handleSubmit(onRegisterSubmit)}
+            className="space-y-8"
+          >
+            <InputTextField
+              form={registerForm}
+              name="email"
+              placeholder="이메일"
+              type="email"
+              label="이메일"
+            />
+
+            <InputTextField
+              form={registerForm}
+              name="password"
+              placeholder="비밀번호"
+              type="password"
+              label="비밀번호"
+            />
+
+            <InputTextField
+              form={registerForm}
+              name="passwordConfirm"
+              label="비밀번호 확인"
+              placeholder="비밀번호 확인"
+              type="password"
+            />
+
+            {isRegisterFailed && (
+              <p className="text-red-600">회원가입에 실패했습니다</p>
+            )}
+            <Button type="submit">회원가입</Button>
+          </form>
+        </Form>
+      )}
+
+      {/* 회원가입 후 로그인상태에서 출력, 이메일은 있지만 아직 닉네임은 설정이 안됨 */}
+      {user && (
+        <Form {...optionalForm}>
+          <form
+            onSubmit={optionalForm.handleSubmit(onOptionalSubmit)}
+            className="space-y-8"
+          >
+            <InputTextField
+              form={optionalForm}
+              name="nickname"
+              label="닉네임"
+              type="text"
+              placeholder="닉네임"
+            />
+
+            <FormRadioGroupField
+              form={optionalForm}
+              name="position"
+              label="포지션"
+              options={positions}
+            />
+
+            <FormCheckboxGroupField
+              form={optionalForm}
+              name="skills"
+              label="기술 스택"
+              options={skills}
+            />
+            <Button>프로필 설정하기</Button>
+          </form>
+        </Form>
+      )}
+      <Link href="/login">로그인으로 돌아가기</Link>
+    </div>
+  );
+}
diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx
new file mode 100644
index 00000000..97eb4d24
--- /dev/null
+++ b/src/app/(auth)/reset-password/page.tsx
@@ -0,0 +1,70 @@
+'use client';
+import { Button } from '@/components/ui/button';
+import { z } from 'zod';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Form } from '@/components/ui/form';
+import { useState } from 'react';
+import Link from 'next/link';
+import { InputTextField } from '@/components/molecules/input-text-field';
+
+const formSchema = z.object({
+	email: z.string().nonempty({ message: '이메일을 입력해주세요' }).email({
+		message: '유효한 이메일이 아닙니다.',
+	}),
+});
+
+export default function Page() {
+	const form = useForm<z.infer<typeof formSchema>>({
+		resolver: zodResolver(formSchema),
+		defaultValues: {
+			email: '',
+		},
+	});
+
+	const [isSuccessEmailSend, setIsSuccessEmailSend] = useState(false);
+	const [isNotExisted, setIsNotExisted] = useState(false);
+
+	// 비밀번호 리셋
+	const onSubmit = async (values: z.infer<typeof formSchema>) => {
+		console.log(values);
+		try {
+			// TODO: 비밀번호 찾기 로직 작성 /find-email
+			setIsSuccessEmailSend(true);
+		} catch (e) {
+			// TODO: 비밀번호 찾기 실패시 에러코드 맞춰서 설정해주기
+			setIsNotExisted(true);
+			console.log(e);
+		}
+	};
+
+	return (
+		<div>
+			<Form {...form}>
+				<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+					<InputTextField
+						form={form}
+						name="email"
+						label="이메일"
+						type="email"
+						placeholder="myemail@mail.com"
+					/>
+
+					{/* 성공시 로그인페이지로 갈지 비밀번호 찾기로 갈지 물어보기 */}
+					{isSuccessEmailSend && (
+						<div className="flex flex-col">
+							<p className="text-blue-900">이메일을 확인해주세요!</p>
+							<Link href="/login">로그인하러 가기</Link>
+						</div>
+					)}
+
+					{isNotExisted && (
+						<p className="text-red-600">해당 이메일이 존재하지 않습니다</p>
+					)}
+					<Button>이메일 찾기</Button>
+				</form>
+			</Form>
+			<Link href="/login">로그인으로 돌아가기</Link>
+		</div>
+	);
+}
diff --git a/src/components/atoms/input-text/index.tsx b/src/components/atoms/input-text/index.tsx
new file mode 100644
index 00000000..c9b4a007
--- /dev/null
+++ b/src/components/atoms/input-text/index.tsx
@@ -0,0 +1,17 @@
+import { Input } from "@/components/ui/input";
+import * as React from "react";
+
+// 키보드로 직접 입력하는 type으로 제한
+export type InputTextProps = React.ComponentProps<"input"> & {
+  type?: "text" | "password" | "search" | "email" | "url" | "tel" | "number";
+};
+
+// 사용자가 키보드로 직접 텍스트를 입력할 수 있는 Input입니다
+// type을 입력하지 않을 경우 기본 타입은 text입니다
+export const InputText = ({
+  className,
+  type = "text",
+  ...props
+}: InputTextProps) => {
+  return <Input className={`${className} text-black`} type={type} {...props} />;
+};
diff --git a/src/components/molecules/input-checkbox-field/index.tsx b/src/components/molecules/input-checkbox-field/index.tsx
new file mode 100644
index 00000000..ea467d11
--- /dev/null
+++ b/src/components/molecules/input-checkbox-field/index.tsx
@@ -0,0 +1,80 @@
+// components/molecules/FormCheckboxGroupField.tsx
+
+import {
+  ControllerRenderProps,
+  FieldPath,
+  FieldValues,
+  UseFormReturn,
+} from "react-hook-form";
+import {
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { Checkbox } from "@/components/ui/checkbox";
+
+interface FormCheckboxGroupFieldProps<T extends FieldValues> {
+  form: UseFormReturn<T>;
+  name: FieldPath<T>;
+  label: string;
+  description?: string;
+  options: string[];
+}
+
+export const FormCheckboxGroupField = <T extends FieldValues>({
+  form,
+  name,
+  label,
+  description,
+  options,
+}: FormCheckboxGroupFieldProps<T>) => {
+  return (
+    <FormItem>
+      <div className="mb-4">
+        <FormLabel className="text-base">{label}</FormLabel>
+        {description && <FormDescription>{description}</FormDescription>}
+      </div>
+      {options.map((option) => (
+        <FormField
+          key={option}
+          control={form.control}
+          name={name}
+          render={({
+            field,
+          }: {
+            field: ControllerRenderProps<T, FieldPath<T>>;
+          }) => {
+            return (
+              <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                <FormControl>
+                  <Checkbox
+                    className="hidden"
+                    checked={field.value?.includes(option)}
+                    onCheckedChange={(checked) => {
+                      return checked
+                        ? field.onChange([...field.value, option])
+                        : field.onChange(
+                            field.value?.filter((v: string) => v !== option)
+                          );
+                    }}
+                  />
+                </FormControl>
+                <FormLabel
+                  className={`font-normal p-1 rounded text-gray-600 cursor-pointer bg-gray-300 ${
+                    form.watch(name).includes(option) && "bg-red-300"
+                  }`}
+                >
+                  {option}
+                </FormLabel>
+              </FormItem>
+            );
+          }}
+        />
+      ))}
+      <FormMessage />
+    </FormItem>
+  );
+};
diff --git a/src/components/molecules/input-radiogroup-field/index.tsx b/src/components/molecules/input-radiogroup-field/index.tsx
new file mode 100644
index 00000000..b777c77e
--- /dev/null
+++ b/src/components/molecules/input-radiogroup-field/index.tsx
@@ -0,0 +1,70 @@
+import {
+  ControllerRenderProps,
+  FieldPath,
+  FieldValues,
+  UseFormReturn,
+} from "react-hook-form";
+import {
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+
+interface FormRadioGroupFieldProps<T extends FieldValues> {
+  form: UseFormReturn<T>;
+  name: FieldPath<T>;
+  label: string;
+  options: string[];
+}
+
+export const FormRadioGroupField = <T extends FieldValues>({
+  form,
+  name,
+  label,
+  options,
+}: FormRadioGroupFieldProps<T>) => {
+  return (
+    <FormField
+      control={form.control}
+      name={name}
+      render={({
+        field,
+      }: {
+        field: ControllerRenderProps<T, FieldPath<T>>;
+      }) => (
+        <FormItem className="space-y-3">
+          <FormLabel>{label}</FormLabel>
+          <FormControl>
+            <RadioGroup
+              onValueChange={field.onChange}
+              defaultValue={field.value}
+              className="flex gap-4"
+            >
+              {options.map((option) => (
+                <FormItem
+                  key={option}
+                  className="flex items-center space-x-3 space-y-0"
+                >
+                  <FormControl>
+                    <RadioGroupItem value={option} className="hidden" />
+                  </FormControl>
+                  <FormLabel
+                    className={`font-normal p-1 rounded text-gray-600 cursor-pointer bg-gray-300 ${
+                      form.watch(name) === option && "bg-red-300"
+                    }`}
+                  >
+                    {option}
+                  </FormLabel>
+                </FormItem>
+              ))}
+            </RadioGroup>
+          </FormControl>
+          <FormMessage />
+        </FormItem>
+      )}
+    />
+  );
+};
diff --git a/src/components/molecules/input-text-field/index.tsx b/src/components/molecules/input-text-field/index.tsx
new file mode 100644
index 00000000..de8abd6d
--- /dev/null
+++ b/src/components/molecules/input-text-field/index.tsx
@@ -0,0 +1,41 @@
+import {
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { InputText } from "@/components/atoms/input-text";
+import { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
+
+type InputTextFieldProps<T extends FieldValues> = {
+  form: UseFormReturn<T>; // 제네릭으로 확장 가능
+  name: FieldPath<T>;
+  label: string;
+  placeholder?: string;
+  type?: "text" | "password" | "search" | "email" | "url" | "tel" | "number";
+};
+
+export const InputTextField = <T extends FieldValues>({
+  label,
+  type = "text",
+  name,
+  form,
+  placeholder,
+}: InputTextFieldProps<T>) => {
+  return (
+    <FormField
+      control={form.control}
+      name={name}
+      render={({ field }) => (
+        <FormItem>
+          <FormLabel>{label}</FormLabel>
+          <FormControl>
+            <InputText placeholder={placeholder} type={type} {...field} />
+          </FormControl>
+          <FormMessage />
+        </FormItem>
+      )}
+    />
+  );
+};
diff --git a/yarn.lock b/yarn.lock
index 05148ca8..51247b7c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -393,6 +393,13 @@
     "@eslint/core" "^0.13.0"
     levn "^0.4.1"
 
+"@hookform/resolvers@^5.0.1":
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-5.0.1.tgz#0a5e90310149e3ac5b017efcb5beb9bdbb711f38"
+  integrity sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==
+  dependencies:
+    "@standard-schema/utils" "^0.3.0"
+
 "@humanfs/core@^0.19.1":
   version "0.19.1"
   resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
@@ -951,6 +958,11 @@
   resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda"
   integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==
 
+"@radix-ui/primitive@1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65"
+  integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==
+
 "@radix-ui/react-avatar@^1.1.9":
   version "1.1.9"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.9.tgz#592b1ff78a5ad90bd8bff54935d88a6b6960520f"
@@ -962,6 +974,30 @@
     "@radix-ui/react-use-is-hydrated" "0.1.0"
     "@radix-ui/react-use-layout-effect" "1.1.1"
 
+"@radix-ui/react-checkbox@^1.3.1":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.1.tgz#c5c978ed49dcc8a81a8126bde9d547c7b928285b"
+  integrity sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==
+  dependencies:
+    "@radix-ui/primitive" "1.1.2"
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-context" "1.1.2"
+    "@radix-ui/react-presence" "1.1.4"
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-use-controllable-state" "1.2.2"
+    "@radix-ui/react-use-previous" "1.1.1"
+    "@radix-ui/react-use-size" "1.1.1"
+
+"@radix-ui/react-collection@1.1.6":
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.6.tgz#fecf74475e4660ee99c7eb1ebfa5ccfb1a219fe4"
+  integrity sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==
+  dependencies:
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-context" "1.1.2"
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-slot" "1.2.2"
+
 "@radix-ui/react-compose-refs@1.1.2":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30"
@@ -972,6 +1008,86 @@
   resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
   integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==
 
+"@radix-ui/react-dialog@^1.1.13":
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz#8c868a97ec70765efb125fd48708c9993c7ae683"
+  integrity sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==
+  dependencies:
+    "@radix-ui/primitive" "1.1.2"
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-context" "1.1.2"
+    "@radix-ui/react-dismissable-layer" "1.1.9"
+    "@radix-ui/react-focus-guards" "1.1.2"
+    "@radix-ui/react-focus-scope" "1.1.6"
+    "@radix-ui/react-id" "1.1.1"
+    "@radix-ui/react-portal" "1.1.8"
+    "@radix-ui/react-presence" "1.1.4"
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-slot" "1.2.2"
+    "@radix-ui/react-use-controllable-state" "1.2.2"
+    aria-hidden "^1.2.4"
+    react-remove-scroll "^2.6.3"
+
+"@radix-ui/react-direction@1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14"
+  integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==
+
+"@radix-ui/react-dismissable-layer@1.1.9":
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz#46e025ba6e6f403677e22fbb7d99b63cf7b32bca"
+  integrity sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==
+  dependencies:
+    "@radix-ui/primitive" "1.1.2"
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-use-callback-ref" "1.1.1"
+    "@radix-ui/react-use-escape-keydown" "1.1.1"
+
+"@radix-ui/react-focus-guards@1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz#4ec9a7e50925f7fb661394460045b46212a33bed"
+  integrity sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==
+
+"@radix-ui/react-focus-scope@1.1.6":
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz#a265c5f2c6fa4365cb16bdf4fee69e36b62f728a"
+  integrity sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==
+  dependencies:
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-use-callback-ref" "1.1.1"
+
+"@radix-ui/react-id@1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7"
+  integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==
+  dependencies:
+    "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-label@^2.1.6":
+  version "2.1.6"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.6.tgz#45ebd1381996d0311d199781bed15a092c7978dd"
+  integrity sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==
+  dependencies:
+    "@radix-ui/react-primitive" "2.1.2"
+
+"@radix-ui/react-portal@1.1.8":
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.8.tgz#0181e85bc0d8c67229dd8cf198204f5f4cc7c09c"
+  integrity sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==
+  dependencies:
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-presence@1.1.4":
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz#253ac0ad4946c5b4a9c66878335f5cf07c967ced"
+  integrity sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==
+  dependencies:
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-use-layout-effect" "1.1.1"
+
 "@radix-ui/react-primitive@2.1.2":
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz#03f64f957719c761d22c2f92cc43ffb64bd42cc8"
@@ -979,6 +1095,45 @@
   dependencies:
     "@radix-ui/react-slot" "1.2.2"
 
+"@radix-ui/react-progress@^1.1.6":
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.6.tgz#bec8368fffe28446895be48a4b85f71eb91709f6"
+  integrity sha512-QzN9a36nKk2eZKMf9EBCia35x3TT+SOgZuzQBVIHyRrmYYi73VYBRK3zKwdJ6az/F5IZ6QlacGJBg7zfB85liA==
+  dependencies:
+    "@radix-ui/react-context" "1.1.2"
+    "@radix-ui/react-primitive" "2.1.2"
+
+"@radix-ui/react-radio-group@^1.3.6":
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.6.tgz#36f7bdc64b10212fa029badc487b91804b0b34ce"
+  integrity sha512-1tfTAqnYZNVwSpFhCT273nzK8qGBReeYnNTPspCggqk1fvIrfVxJekIuBFidNivzpdiMqDwVGnQvHqXrRPM4Og==
+  dependencies:
+    "@radix-ui/primitive" "1.1.2"
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-context" "1.1.2"
+    "@radix-ui/react-direction" "1.1.1"
+    "@radix-ui/react-presence" "1.1.4"
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-roving-focus" "1.1.9"
+    "@radix-ui/react-use-controllable-state" "1.2.2"
+    "@radix-ui/react-use-previous" "1.1.1"
+    "@radix-ui/react-use-size" "1.1.1"
+
+"@radix-ui/react-roving-focus@1.1.9":
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz#37fcacb7dfcc9ea45401b2dd07bd97ccbb8911b2"
+  integrity sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==
+  dependencies:
+    "@radix-ui/primitive" "1.1.2"
+    "@radix-ui/react-collection" "1.1.6"
+    "@radix-ui/react-compose-refs" "1.1.2"
+    "@radix-ui/react-context" "1.1.2"
+    "@radix-ui/react-direction" "1.1.1"
+    "@radix-ui/react-id" "1.1.1"
+    "@radix-ui/react-primitive" "2.1.2"
+    "@radix-ui/react-use-callback-ref" "1.1.1"
+    "@radix-ui/react-use-controllable-state" "1.2.2"
+
 "@radix-ui/react-slot@1.2.2", "@radix-ui/react-slot@^1.2.2":
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz#18e6533e778a2051edc2ad0773da8e22f03f626a"
@@ -991,6 +1146,28 @@
   resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40"
   integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==
 
+"@radix-ui/react-use-controllable-state@1.2.2":
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190"
+  integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==
+  dependencies:
+    "@radix-ui/react-use-effect-event" "0.0.2"
+    "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-use-effect-event@0.0.2":
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907"
+  integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==
+  dependencies:
+    "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-use-escape-keydown@1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29"
+  integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==
+  dependencies:
+    "@radix-ui/react-use-callback-ref" "1.1.1"
+
 "@radix-ui/react-use-is-hydrated@0.1.0":
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz#544da73369517036c77659d7cdd019dc0f5ff9a0"
@@ -1003,6 +1180,18 @@
   resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e"
   integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==
 
+"@radix-ui/react-use-previous@1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz#1a1ad5568973d24051ed0af687766f6c7cb9b5b5"
+  integrity sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==
+
+"@radix-ui/react-use-size@1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37"
+  integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==
+  dependencies:
+    "@radix-ui/react-use-layout-effect" "1.1.1"
+
 "@rtsao/scc@^1.1.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
@@ -1032,6 +1221,11 @@
   dependencies:
     "@sinonjs/commons" "^3.0.0"
 
+"@standard-schema/utils@^0.3.0":
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
+  integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
+
 "@swc/counter@0.1.3":
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
@@ -1573,6 +1767,13 @@ argparse@^2.0.1:
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
+aria-hidden@^1.2.4:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a"
+  integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==
+  dependencies:
+    tslib "^2.0.0"
+
 aria-query@5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
@@ -2149,6 +2350,11 @@ detect-newline@^3.0.0:
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
   integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
 
+detect-node-es@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
+  integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
+
 diff-sequences@^29.6.3:
   version "29.6.3"
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
@@ -2378,11 +2584,6 @@ eslint-config-next@15.3.2:
     eslint-plugin-react "^7.37.0"
     eslint-plugin-react-hooks "^5.0.0"
 
-eslint-config-prettier@^10.1.5:
-  version "10.1.5"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
-  integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
-
 eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
   version "0.3.9"
   resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
@@ -2858,6 +3059,11 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@
     hasown "^2.0.2"
     math-intrinsics "^1.1.0"
 
+get-nonce@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
+  integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
+
 get-package-type@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@@ -4128,6 +4334,11 @@ negotiator@^1.0.0:
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
   integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
 
+next-themes@^0.4.6:
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
+  integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
+
 next@15.3.2:
   version "15.3.2"
   resolved "https://registry.yarnpkg.com/next/-/next-15.3.2.tgz#97510629e38a058dd154782a5c2ec9c9ab94d0d8"
@@ -4550,6 +4761,33 @@ react-is@^18.0.0:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
   integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
 
+react-remove-scroll-bar@^2.3.7:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
+  integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
+  dependencies:
+    react-style-singleton "^2.2.2"
+    tslib "^2.0.0"
+
+react-remove-scroll@^2.6.3:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.0.tgz#0a483417fe9919acd3e459ce92008640cf898669"
+  integrity sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==
+  dependencies:
+    react-remove-scroll-bar "^2.3.7"
+    react-style-singleton "^2.2.3"
+    tslib "^2.1.0"
+    use-callback-ref "^1.3.3"
+    use-sidecar "^1.1.3"
+
+react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
+  integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
+  dependencies:
+    get-nonce "^1.0.0"
+    tslib "^2.0.0"
+
 react@^19.0.0:
   version "19.1.0"
   resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
@@ -4884,6 +5122,11 @@ slash@^3.0.0:
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
+sonner@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.3.tgz#de7cdbc4b6a25ac3f0a9e0aed3748e0b3d6e092e"
+  integrity sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==
+
 source-map-js@^1.0.2, source-map-js@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
@@ -5158,7 +5401,7 @@ tsconfig-paths@^3.15.0:
     minimist "^1.2.6"
     strip-bom "^3.0.0"
 
-tslib@^2.4.0, tslib@^2.8.0:
+tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0:
   version "2.8.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -5327,6 +5570,21 @@ url-parse@^1.5.3:
     querystringify "^2.1.1"
     requires-port "^1.0.0"
 
+use-callback-ref@^1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
+  integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
+  dependencies:
+    tslib "^2.0.0"
+
+use-sidecar@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb"
+  integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==
+  dependencies:
+    detect-node-es "^1.1.0"
+    tslib "^2.0.0"
+
 use-sync-external-store@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"


Reviews:
user: login: rlarkdals1202
body: 확인했습니다!
state: APPROVED
author_association: NONE
submitted_at: 2025-05-20T06:42:33Z
user: login: e-sum-e
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T06:47:40Z
user: login: kkukileon305
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T07:18:51Z
user: login: e-sum-e
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T07:19:30Z
user: login: kkukileon305
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T07:26:13Z
user: login: kkukileon305
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T07:27:50Z
user: login: e-sum-e
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T07:28:44Z
user: login: hjleee93
body: enum 수정해서 다시 올려주시면 될거 같습니다. 
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T12:27:49Z
user: login: hjleee93
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-21T05:13:06Z

Review comments:
diff_hunk: @@ -0,0 +1,322 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import useAuthStore from "@/stores/useAuthStore";
+import { InputText } from "@/components/atoms/input-text";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+
+const positions: [string, ...string[]] = [
path: src/app/(auth)/register/page.tsx
user: login: e-sum-e
body: 이거 enum으로 하기로 했었던 거 같아요!
created_at: 2025-05-20T06:44:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,322 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import useAuthStore from "@/stores/useAuthStore";
+import { InputText } from "@/components/atoms/input-text";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+
+const positions: [string, ...string[]] = [
+  "PM",
+  "PL",
+  "AA",
+  "TA",
+  "DA",
+  "QA",
+  "FE",
+  "BE",
+  "FS",
+];
+const skills: [string, ...string[]] = [
+  "JAVA",
+  "JavaScript",
+  "HTML/CSS",
+  "React",
+  "Vue.js",
+  "Kotlin",
+  "Spring",
+];
+
+// 회원가입에 쓰이는 이메일과 비밀번호 유효성
+// 규칙은 네이버와 똑같이 했습니다
+const registerFormSchema = z
+  .object({
+    email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+      message: "유효한 이메일이 아닙니다.",
+    }),
+    password: z
+      .string()
+      .nonempty({ message: "비밀번호를 입력해주세요" })
+      .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[\W_]).{8,}$/, {
+        message: "영어 대/소문자, 숫자, 특수문자를 혼합하여 8자리 이상",
+      }),
+    passwordConfirm: z.string().nonempty({
+      message: "비밀번호를 다시 입력해주세요",
+    }),
+  })
+  .superRefine(({ password, passwordConfirm }, ctx) => {
+    if (password && passwordConfirm && password !== passwordConfirm) {
+      ctx.addIssue({
+        path: ["passwordConfirm"],
+        message: "비밀번호가 일치하지 않습니다.",
+        code: z.ZodIssueCode.custom,
+      });
+    }
+  });
+
+// 회원가입 후 옵셔널 정보 유효성
+// 규칙을 네이버와 같이 했습니다
+const optionalFormSchema = z.object({
+  nickname: z
+    .string()
+    .nonempty({
+      message: "닉네임을 입력해주세요",
+    })
+    .min(2, "닉네임은 2~8자리여야 합니다.")
+    .max(8, "닉네임은 2~8자리여야 합니다.")
+    .regex(/^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$/, "한글, 영어, 숫자만 입력 가능합니다."),
+  position: z.enum(positions, {
+    message: "포지션을 선택해주세요",
+  }),
+  skills: z.array(z.enum(skills)),
+});
+
+export default function Page() {
+  const registerForm = useForm<z.infer<typeof registerFormSchema>>({
+    resolver: zodResolver(registerFormSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+      passwordConfirm: "",
+    },
+  });
+  const [isRegisterFailed, setIsRegisterFailed] = useState(false);
+  const { user } = useAuthStore();
+
+  const optionalForm = useForm<z.infer<typeof optionalFormSchema>>({
+    resolver: zodResolver(optionalFormSchema),
+    defaultValues: {
+      nickname: "",
+      position: "",
+      skills: [],
+    },
+  });
+
+  // 회원가입
+  const onRegisterSubmit = async (
+    values: z.infer<typeof registerFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      // TODO: 회원가입 로직 작성 /register
+      // TODO: 회원가입 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 회원가입 실패시 에러코드 맞춰서 설정해주기
+      setIsRegisterFailed(true);
+      console.log(e);
+    }
+  };
+
+  // 옵션 설정
+  const onOptionalSubmit = async (
+    values: z.infer<typeof optionalFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      //  TODO: 프로필 옵션 설정
+    } catch (e) {
+      // TODO: 프로필 에러 설정 //
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      {/* 회원가입 전 로그인이 되지 않은 상태의 경우 회원가입 폼을 출력 */}
+      {!user && (
+        <Form {...registerForm}>
+          <form
+            onSubmit={registerForm.handleSubmit(onRegisterSubmit)}
+            className="space-y-8"
+          >
+            <FormField
path: src/app/(auth)/register/page.tsx
user: login: e-sum-e
body: FormField 단위로 component로 나누는 건 어떨까요?
created_at: 2025-05-20T06:46:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,94 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import { InputText } from "@/components/atoms/input-text";
+import Link from "next/link";
+
+const formSchema = z.object({
+  email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+    message: "유효한 이메일이 아닙니다.",
+  }),
+  password: z.string().nonempty({ message: "비밀번호를 입력해주세요" }),
+});
+
+export default function Page() {
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+    },
+  });
+  const [isLoginFailed, setIsLoginFailed] = useState(false);
+  // const login = useAuthStore((s) => s.login);
+
+  // 로그인
+  const onSubmit = async (values: z.infer<typeof formSchema>) => {
+    console.log(values);
+    try {
+      // TODO: 로그인 로직 작성 /login
+      // TODO: 로그인 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 로그인 실패시 에러코드 맞춰서 설정해주기
+      setIsLoginFailed(true);
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      <Form {...form}>
+        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+          <FormField
+            control={form.control}
+            name="email"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>이메일</FormLabel>
+                <FormControl>
+                  <InputText placeholder="이메일" type="email" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          <FormField
+            control={form.control}
+            name="password"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>비밀번호</FormLabel>
+                <FormControl>
+                  <InputText
+                    placeholder="비밀번호"
+                    type="password"
+                    {...field}
+                  />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {isLoginFailed && (
+            <p className="text-red-600">로그인에 실패했습니다</p>
+          )}
+          <Button type="submit">로그인</Button>
path: src/app/(auth)/login/page.tsx
user: login: e-sum-e
body: form에 달리는 button 하나인거 같은데 type 지정 해줘야하나요?
- form에 있는 button은 기본 타입이 submit인거로 알고 있어서요
created_at: 2025-05-20T06:47:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,322 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import useAuthStore from "@/stores/useAuthStore";
+import { InputText } from "@/components/atoms/input-text";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+
+const positions: [string, ...string[]] = [
+  "PM",
+  "PL",
+  "AA",
+  "TA",
+  "DA",
+  "QA",
+  "FE",
+  "BE",
+  "FS",
+];
+const skills: [string, ...string[]] = [
+  "JAVA",
+  "JavaScript",
+  "HTML/CSS",
+  "React",
+  "Vue.js",
+  "Kotlin",
+  "Spring",
+];
+
+// 회원가입에 쓰이는 이메일과 비밀번호 유효성
+// 규칙은 네이버와 똑같이 했습니다
+const registerFormSchema = z
+  .object({
+    email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+      message: "유효한 이메일이 아닙니다.",
+    }),
+    password: z
+      .string()
+      .nonempty({ message: "비밀번호를 입력해주세요" })
+      .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[\W_]).{8,}$/, {
+        message: "영어 대/소문자, 숫자, 특수문자를 혼합하여 8자리 이상",
+      }),
+    passwordConfirm: z.string().nonempty({
+      message: "비밀번호를 다시 입력해주세요",
+    }),
+  })
+  .superRefine(({ password, passwordConfirm }, ctx) => {
+    if (password && passwordConfirm && password !== passwordConfirm) {
+      ctx.addIssue({
+        path: ["passwordConfirm"],
+        message: "비밀번호가 일치하지 않습니다.",
+        code: z.ZodIssueCode.custom,
+      });
+    }
+  });
+
+// 회원가입 후 옵셔널 정보 유효성
+// 규칙을 네이버와 같이 했습니다
+const optionalFormSchema = z.object({
+  nickname: z
+    .string()
+    .nonempty({
+      message: "닉네임을 입력해주세요",
+    })
+    .min(2, "닉네임은 2~8자리여야 합니다.")
+    .max(8, "닉네임은 2~8자리여야 합니다.")
+    .regex(/^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$/, "한글, 영어, 숫자만 입력 가능합니다."),
+  position: z.enum(positions, {
+    message: "포지션을 선택해주세요",
+  }),
+  skills: z.array(z.enum(skills)),
+});
+
+export default function Page() {
+  const registerForm = useForm<z.infer<typeof registerFormSchema>>({
+    resolver: zodResolver(registerFormSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+      passwordConfirm: "",
+    },
+  });
+  const [isRegisterFailed, setIsRegisterFailed] = useState(false);
+  const { user } = useAuthStore();
+
+  const optionalForm = useForm<z.infer<typeof optionalFormSchema>>({
+    resolver: zodResolver(optionalFormSchema),
+    defaultValues: {
+      nickname: "",
+      position: "",
+      skills: [],
+    },
+  });
+
+  // 회원가입
+  const onRegisterSubmit = async (
+    values: z.infer<typeof registerFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      // TODO: 회원가입 로직 작성 /register
+      // TODO: 회원가입 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 회원가입 실패시 에러코드 맞춰서 설정해주기
+      setIsRegisterFailed(true);
+      console.log(e);
+    }
+  };
+
+  // 옵션 설정
+  const onOptionalSubmit = async (
+    values: z.infer<typeof optionalFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      //  TODO: 프로필 옵션 설정
+    } catch (e) {
+      // TODO: 프로필 에러 설정 //
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      {/* 회원가입 전 로그인이 되지 않은 상태의 경우 회원가입 폼을 출력 */}
+      {!user && (
+        <Form {...registerForm}>
+          <form
+            onSubmit={registerForm.handleSubmit(onRegisterSubmit)}
+            className="space-y-8"
+          >
+            <FormField
path: src/app/(auth)/register/page.tsx
user: login: kkukileon305
body: 이게 FormField안에 FormField로 들어가있어서 잘 모르겠네요...
shadcn문서에 있던걸 그대로 쓴거라서 나눌수 있을지 잘 모르겟네요
created_at: 2025-05-20T07:18:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,322 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import useAuthStore from "@/stores/useAuthStore";
+import { InputText } from "@/components/atoms/input-text";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+
+const positions: [string, ...string[]] = [
+  "PM",
+  "PL",
+  "AA",
+  "TA",
+  "DA",
+  "QA",
+  "FE",
+  "BE",
+  "FS",
+];
+const skills: [string, ...string[]] = [
+  "JAVA",
+  "JavaScript",
+  "HTML/CSS",
+  "React",
+  "Vue.js",
+  "Kotlin",
+  "Spring",
+];
+
+// 회원가입에 쓰이는 이메일과 비밀번호 유효성
+// 규칙은 네이버와 똑같이 했습니다
+const registerFormSchema = z
+  .object({
+    email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+      message: "유효한 이메일이 아닙니다.",
+    }),
+    password: z
+      .string()
+      .nonempty({ message: "비밀번호를 입력해주세요" })
+      .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[\W_]).{8,}$/, {
+        message: "영어 대/소문자, 숫자, 특수문자를 혼합하여 8자리 이상",
+      }),
+    passwordConfirm: z.string().nonempty({
+      message: "비밀번호를 다시 입력해주세요",
+    }),
+  })
+  .superRefine(({ password, passwordConfirm }, ctx) => {
+    if (password && passwordConfirm && password !== passwordConfirm) {
+      ctx.addIssue({
+        path: ["passwordConfirm"],
+        message: "비밀번호가 일치하지 않습니다.",
+        code: z.ZodIssueCode.custom,
+      });
+    }
+  });
+
+// 회원가입 후 옵셔널 정보 유효성
+// 규칙을 네이버와 같이 했습니다
+const optionalFormSchema = z.object({
+  nickname: z
+    .string()
+    .nonempty({
+      message: "닉네임을 입력해주세요",
+    })
+    .min(2, "닉네임은 2~8자리여야 합니다.")
+    .max(8, "닉네임은 2~8자리여야 합니다.")
+    .regex(/^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$/, "한글, 영어, 숫자만 입력 가능합니다."),
+  position: z.enum(positions, {
+    message: "포지션을 선택해주세요",
+  }),
+  skills: z.array(z.enum(skills)),
+});
+
+export default function Page() {
+  const registerForm = useForm<z.infer<typeof registerFormSchema>>({
+    resolver: zodResolver(registerFormSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+      passwordConfirm: "",
+    },
+  });
+  const [isRegisterFailed, setIsRegisterFailed] = useState(false);
+  const { user } = useAuthStore();
+
+  const optionalForm = useForm<z.infer<typeof optionalFormSchema>>({
+    resolver: zodResolver(optionalFormSchema),
+    defaultValues: {
+      nickname: "",
+      position: "",
+      skills: [],
+    },
+  });
+
+  // 회원가입
+  const onRegisterSubmit = async (
+    values: z.infer<typeof registerFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      // TODO: 회원가입 로직 작성 /register
+      // TODO: 회원가입 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 회원가입 실패시 에러코드 맞춰서 설정해주기
+      setIsRegisterFailed(true);
+      console.log(e);
+    }
+  };
+
+  // 옵션 설정
+  const onOptionalSubmit = async (
+    values: z.infer<typeof optionalFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      //  TODO: 프로필 옵션 설정
+    } catch (e) {
+      // TODO: 프로필 에러 설정 //
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      {/* 회원가입 전 로그인이 되지 않은 상태의 경우 회원가입 폼을 출력 */}
+      {!user && (
+        <Form {...registerForm}>
+          <form
+            onSubmit={registerForm.handleSubmit(onRegisterSubmit)}
+            className="space-y-8"
+          >
+            <FormField
path: src/app/(auth)/register/page.tsx
user: login: e-sum-e
body: > 이게 FormField안에 FormField로 들어가있어서 잘 모르겠네요... shadcn문서에 있던걸 그대로 쓴거라서 나눌수 있을지 잘 모르겟네요

그냥 다른 컴포넌트 분리하셨던거랑 동일하게 분리하시면 돼요! 저도 하고 있어요ㅋㅋㅋㅋ
created_at: 2025-05-20T07:19:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,94 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import { InputText } from "@/components/atoms/input-text";
+import Link from "next/link";
+
+const formSchema = z.object({
+  email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+    message: "유효한 이메일이 아닙니다.",
+  }),
+  password: z.string().nonempty({ message: "비밀번호를 입력해주세요" }),
+});
+
+export default function Page() {
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+    },
+  });
+  const [isLoginFailed, setIsLoginFailed] = useState(false);
+  // const login = useAuthStore((s) => s.login);
+
+  // 로그인
+  const onSubmit = async (values: z.infer<typeof formSchema>) => {
+    console.log(values);
+    try {
+      // TODO: 로그인 로직 작성 /login
+      // TODO: 로그인 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 로그인 실패시 에러코드 맞춰서 설정해주기
+      setIsLoginFailed(true);
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      <Form {...form}>
+        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+          <FormField
+            control={form.control}
+            name="email"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>이메일</FormLabel>
+                <FormControl>
+                  <InputText placeholder="이메일" type="email" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          <FormField
+            control={form.control}
+            name="password"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>비밀번호</FormLabel>
+                <FormControl>
+                  <InputText
+                    placeholder="비밀번호"
+                    type="password"
+                    {...field}
+                  />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {isLoginFailed && (
+            <p className="text-red-600">로그인에 실패했습니다</p>
+          )}
+          <Button type="submit">로그인</Button>
path: src/app/(auth)/login/page.tsx
user: login: kkukileon305
body: 버튼은 명확하게 적어주는게 좋지않을까요
created_at: 2025-05-20T07:26:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,94 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import { InputText } from "@/components/atoms/input-text";
+import Link from "next/link";
+
+const formSchema = z.object({
+  email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+    message: "유효한 이메일이 아닙니다.",
+  }),
+  password: z.string().nonempty({ message: "비밀번호를 입력해주세요" }),
+});
+
+export default function Page() {
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+    },
+  });
+  const [isLoginFailed, setIsLoginFailed] = useState(false);
+  // const login = useAuthStore((s) => s.login);
+
+  // 로그인
+  const onSubmit = async (values: z.infer<typeof formSchema>) => {
+    console.log(values);
+    try {
+      // TODO: 로그인 로직 작성 /login
+      // TODO: 로그인 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 로그인 실패시 에러코드 맞춰서 설정해주기
+      setIsLoginFailed(true);
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      <Form {...form}>
+        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+          <FormField
+            control={form.control}
+            name="email"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>이메일</FormLabel>
+                <FormControl>
+                  <InputText placeholder="이메일" type="email" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          <FormField
+            control={form.control}
+            name="password"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>비밀번호</FormLabel>
+                <FormControl>
+                  <InputText
+                    placeholder="비밀번호"
+                    type="password"
+                    {...field}
+                  />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {isLoginFailed && (
+            <p className="text-red-600">로그인에 실패했습니다</p>
+          )}
+          <Button type="submit">로그인</Button>
path: src/app/(auth)/login/page.tsx
user: login: kkukileon305
body: 라고생각했는데 다른버튼은 다 따로 적어주니깐 필요없는거같네요
created_at: 2025-05-20T07:27:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,94 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { useState } from "react";
+import { InputText } from "@/components/atoms/input-text";
+import Link from "next/link";
+
+const formSchema = z.object({
+  email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+    message: "유효한 이메일이 아닙니다.",
+  }),
+  password: z.string().nonempty({ message: "비밀번호를 입력해주세요" }),
+});
+
+export default function Page() {
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+    },
+  });
+  const [isLoginFailed, setIsLoginFailed] = useState(false);
+  // const login = useAuthStore((s) => s.login);
+
+  // 로그인
+  const onSubmit = async (values: z.infer<typeof formSchema>) => {
+    console.log(values);
+    try {
+      // TODO: 로그인 로직 작성 /login
+      // TODO: 로그인 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 로그인 실패시 에러코드 맞춰서 설정해주기
+      setIsLoginFailed(true);
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      <Form {...form}>
+        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+          <FormField
+            control={form.control}
+            name="email"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>이메일</FormLabel>
+                <FormControl>
+                  <InputText placeholder="이메일" type="email" {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          <FormField
+            control={form.control}
+            name="password"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>비밀번호</FormLabel>
+                <FormControl>
+                  <InputText
+                    placeholder="비밀번호"
+                    type="password"
+                    {...field}
+                  />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {isLoginFailed && (
+            <p className="text-red-600">로그인에 실패했습니다</p>
+          )}
+          <Button type="submit">로그인</Button>
path: src/app/(auth)/login/page.tsx
user: login: e-sum-e
body: 네 오히려 submit이 아닌 버튼에만 `type=button` 을 넣어주는 게 좋을거 같아요
created_at: 2025-05-20T07:28:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,187 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Form } from "@/components/ui/form";
+import { useState } from "react";
+import useAuthStore from "@/stores/useAuthStore";
+import { Position, Skill } from "@/types";
+import { InputTextField } from "@/components/molecules/input-text-field";
+import { FormRadioGroupField } from "@/components/molecules/input-radiogroup-field";
+import { FormCheckboxGroupField } from "@/components/molecules/input-checkbox-field";
+import Link from "next/link";
+
+const positions = Object.values(Position) as [string, ...string[]];
+const skills = Object.values(Skill) as [string, ...string[]];
+
+// 회원가입에 쓰이는 이메일과 비밀번호 유효성
+// 규칙은 네이버와 똑같이 했습니다
+const registerFormSchema = z
+  .object({
+    email: z.string().nonempty({ message: "이메일을 입력해주세요" }).email({
+      message: "유효한 이메일이 아닙니다.",
+    }),
+    password: z
+      .string()
+      .nonempty({ message: "비밀번호를 입력해주세요" })
+      .regex(/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[\W_]).{8,}$/, {
+        message: "영어 대/소문자, 숫자, 특수문자를 혼합하여 8자리 이상",
+      }),
+    passwordConfirm: z.string().nonempty({
+      message: "비밀번호를 다시 입력해주세요",
+    }),
+  })
+  .superRefine(({ password, passwordConfirm }, ctx) => {
+    if (password && passwordConfirm && password !== passwordConfirm) {
+      ctx.addIssue({
+        path: ["passwordConfirm"],
+        message: "비밀번호가 일치하지 않습니다.",
+        code: z.ZodIssueCode.custom,
+      });
+    }
+  });
+
+// 회원가입 후 옵셔널 정보 유효성
+// 규칙을 네이버와 같이 했습니다
+const optionalFormSchema = z.object({
+  nickname: z
+    .string()
+    .nonempty({
+      message: "닉네임을 입력해주세요",
+    })
+    .min(2, "닉네임은 2~8자리여야 합니다.")
+    .max(8, "닉네임은 2~8자리여야 합니다.")
+    .regex(/^[ㄱ-ㅎ가-힣a-zA-Z0-9]+$/, "한글, 영어, 숫자만 입력 가능합니다."),
+  position: z.enum(positions, {
+    message: "포지션을 선택해주세요",
+  }),
+  skills: z.array(z.enum(skills)),
+});
+
+export default function Page() {
+  const registerForm = useForm<z.infer<typeof registerFormSchema>>({
+    resolver: zodResolver(registerFormSchema),
+    defaultValues: {
+      email: "",
+      password: "",
+      passwordConfirm: "",
+    },
+  });
+  const [isRegisterFailed, setIsRegisterFailed] = useState(false);
+  const { user } = useAuthStore();
+
+  const optionalForm = useForm<z.infer<typeof optionalFormSchema>>({
+    resolver: zodResolver(optionalFormSchema),
+    defaultValues: {
+      nickname: "",
+      position: "",
+      skills: [],
+    },
+  });
+
+  // 회원가입
+  const onRegisterSubmit = async (
+    values: z.infer<typeof registerFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      // TODO: 회원가입 로직 작성 /register
+      // TODO: 회원가입 성공 후 회원정보 불러오기 /me
+    } catch (e) {
+      // TODO: 회원가입 실패시 에러코드 맞춰서 설정해주기
+      setIsRegisterFailed(true);
+      console.log(e);
+    }
+  };
+
+  // 옵션 설정
+  const onOptionalSubmit = async (
+    values: z.infer<typeof optionalFormSchema>
+  ) => {
+    console.log(values);
+    try {
+      //  TODO: 프로필 옵션 설정
+    } catch (e) {
+      // TODO: 프로필 에러 설정 //
+      console.log(e);
+    }
+  };
+
+  return (
+    <div>
+      {/* 회원가입 전 로그인이 되지 않은 상태의 경우 회원가입 폼을 출력 */}
+      {!user && (
+        <Form {...registerForm}>
+          <form
+            onSubmit={registerForm.handleSubmit(onRegisterSubmit)}
+            className="space-y-8"
+          >
+            <InputTextField
+              form={registerForm}
+              name="email"
+              placeholder="이메일"
+              type="email"
+              label="이메일"
+            />
+
+            <InputTextField
+              form={registerForm}
+              name="password"
+              placeholder="비밀번호"
+              type="password"
+              label="비밀번호"
+            />
+
+            <InputTextField
+              form={registerForm}
+              name="passwordConfirm"
+              label="비밀번호 확인"
+              placeholder="비밀번호 확인"
+              type="password"
+            />
+
+            {isRegisterFailed && (
+              <p className="text-red-600">회원가입에 실패했습니다</p>
+            )}
+            <Button type="submit">회원가입</Button>
+          </form>
+        </Form>
+      )}
+
+      {/* 회원가입 후 로그인상태에서 출력, 이메일은 있지만 아직 닉네임은 설정이 안됨 */}
+      {user && (
+        <Form {...optionalForm}>
path: src/app/(auth)/register/page.tsx
user: login: hjleee93
body: 이게 form이 좀 겹치는 부분이 잇는거 같은데 공통 컴포넌트로 구성 해보실 생각없으신가요 
created_at: 2025-05-20T12:26:42Z
author_association: COLLABORATOR
diff_hunk: @@ -1,9 +1,30 @@
 //전역 파일 타입 정리
path: src/types/index.ts
user: login: hjleee93
body: enum은 숫자로 설정이되어야해요 
created_at: 2025-05-20T12:27:21Z
author_association: COLLABORATOR

PR comments:
user: login: rich-manager
created_at: 2025-05-20T08:18:33Z
body: 새로운 커밋이 푸시되었습니다: feat: 로그인 폼에도 InputTextField 적용
author_association: NONE
user: login: rich-manager
created_at: 2025-05-20T08:19:49Z
body: 새로운 커밋이 푸시되었습니다: Merge branch 'dev' into feature-di-auth
author_association: NONE
user: login: rich-manager
created_at: 2025-05-20T09:31:05Z
body: 새로운 커밋이 푸시되었습니다: feat: 이메일 찾기, 비밀번호 찾기 폼 생성, 인증 각 페이지 Link 생성
author_association: NONE
user: login: rich-manager
created_at: 2025-05-21T02:03:25Z
body: 새로운 커밋이 푸시되었습니다: feat: 이넘을 숫자형태로 변경
author_association: NONE
user: login: hjleee93
created_at: 2025-05-21T02:06:56Z
body: conflict 납니다 수정해주세요 
author_association: COLLABORATOR
user: login: rich-manager
created_at: 2025-05-21T02:08:48Z
body: 새로운 커밋이 푸시되었습니다: refactor: 설정된 프리티어 적용
author_association: NONE
user: login: rich-manager
created_at: 2025-05-21T02:37:23Z
body: 새로운 커밋이 푸시되었습니다: Merge branch 'dev' into feature-di-auth
author_association: NONE
user: login: hjleee93
created_at: 2025-05-21T04:53:19Z
body: @kkukileon305 제가 충돌난거 수정했어요 
author_association: COLLABORATOR
user: login: rich-manager
created_at: 2025-05-21T04:53:41Z
body: 새로운 커밋이 푸시되었습니다: Merge branch 'dev' into feature-di-auth
author_association: NONE
user: login: rich-manager
created_at: 2025-05-21T04:58:02Z
body: 새로운 커밋이 푸시되었습니다: Merge branch 'feature-di-auth' of https://github.com/moyeora-it/moyeora-it into feature-di-auth
author_association: NONE