Repository: gsainfoteam/idp-fe
Pull request #23: feat: implement authorize page
Author: login: rhseung
Created: 2025-04-07T14:08:01Z | Merged: 2025-04-08T01:29:48Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **새로운 기능**
  - 회원가입 과정에 검증 코드 전송 및 입력 기능이 추가되어 인증 절차가 강화되었습니다.
  - 등록 완료 후, 가입 완료를 안내하는 새로운 화면이 도입되어 로그인으로 쉽게 전환할 수 있습니다.
  - Ziggle 서비스를 위한 OAuth 인증 화면과 사용자 동의 옵션이 새롭게 제공됩니다.

- **개선 사항**
  - 입력 검증과 오류 안내 메시지가 강화되어 사용자가 보다 명확한 정보를 얻을 수 있습니다.
  - 다국어 번역 업데이트로 UI 텍스트가 재구성되어 전반적인 사용자 경험이 향상되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/features/auth/components/register-form.tsx b/src/features/auth/components/register-form.tsx
index ff2a80ee..056471eb 100644
--- a/src/features/auth/components/register-form.tsx
+++ b/src/features/auth/components/register-form.tsx
@@ -58,8 +58,8 @@ export function RegisterForm({
                 }}
               >
                 {isCodeSent === 'sent'
-                  ? t('register.buttons.resendCode')
-                  : t('register.buttons.sendCode')}
+                  ? t('register.buttons.resend_code')
+                  : t('register.buttons.send_code')}
               </Button>
             )}
           />
@@ -89,7 +89,7 @@ export function RegisterForm({
                   setVerifying(false);
                 }}
               >
-                {t('register.buttons.verifyCode')}
+                {t('register.buttons.verify_code')}
               </Button>
             }
           />
@@ -110,10 +110,10 @@ export function RegisterForm({
           {...register('password')}
         />
         <Input
-          label={t('register.fields.passwordConfirm')}
+          label={t('register.fields.password_confirm')}
           error={formState.errors.passwordConfirm?.message}
           type="password"
-          placeholder={t('register.placeholders.passwordConfirm')}
+          placeholder={t('register.placeholders.password_confirm')}
           required
           {...register('passwordConfirm')}
         />
@@ -121,7 +121,7 @@ export function RegisterForm({
       <div className="h-8" />
       <div>
         <div className="text-title-3 mb-2.5 text-neutral-900">
-          {t('register.titles.defaultInfo')}
+          {t('register.titles.default_info')}
         </div>
         <Input
           label={t('register.fields.name')}
@@ -133,10 +133,10 @@ export function RegisterForm({
           {...register('name')}
         />
         <Input
-          label={t('register.fields.studentId')}
+          label={t('register.fields.student_id')}
           error={formState.errors.studentId?.message}
           type="text"
-          placeholder={t('register.placeholders.studentId', {
+          placeholder={t('register.placeholders.student_id', {
             form: `${new Date().getFullYear()}0000`,
           })}
           required
@@ -144,10 +144,10 @@ export function RegisterForm({
           {...register('studentId')}
         />
         <Input
-          label={t('register.fields.phoneNumber')}
+          label={t('register.fields.phone_number')}
           error={formState.errors.phoneNumber?.message}
           type="tel"
-          placeholder={t('register.placeholders.phoneNumber')}
+          placeholder={t('register.placeholders.phone_number')}
           required
           {...register('phoneNumber')}
         />
diff --git a/src/features/auth/frames/register-done-frame.tsx b/src/features/auth/frames/register-done-frame.tsx
index 0aae1ee3..4d378089 100644
--- a/src/features/auth/frames/register-done-frame.tsx
+++ b/src/features/auth/frames/register-done-frame.tsx
@@ -9,14 +9,14 @@ export function RegisterDoneFrame() {
   return (
     <div className="flex min-h-screen items-center justify-center">
       <div className="flex w-full max-w-[400px] flex-col px-5 py-6">
-        <div className="text-title-1 mb-4">{t(`register-done.title`)}</div>
+        <div className="text-title-1 mb-4">{t(`register_done.title`)}</div>
         <div className="text-title-3 text-neutral-900">
-          {t(`register-done.description`)}
+          {t(`register_done.description`)}
         </div>
         <div className="h-100" />
         <Link to="/auth/login">
           <Button variant="primary" type="button">
-            {t(`register-done.buttons.login`)}
+            {t(`register_done.buttons.login`)}
           </Button>
         </Link>
       </div>
diff --git a/src/features/auth/hooks/use-register-form.ts b/src/features/auth/hooks/use-register-form.ts
index 43b4f1b7..c4b85e3c 100644
--- a/src/features/auth/hooks/use-register-form.ts
+++ b/src/features/auth/hooks/use-register-form.ts
@@ -21,7 +21,7 @@ export const createSchema = (t: TFunction) =>
       name: z.string().min(1, t('register.errors.name')),
       studentId: z.string().length(
         8,
-        t('register.errors.studentId', {
+        t('register.errors.student_id', {
           form: `${new Date().getFullYear()}0000`,
         }),
       ),
@@ -29,12 +29,12 @@ export const createSchema = (t: TFunction) =>
         .string()
         .regex(
           /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/,
-          t('register.errors.phoneNumber'),
+          t('register.errors.phone_number'),
         ),
       verificationJwtToken: z.string().default(''),
     })
     .refine((data) => data.password === data.passwordConfirm, {
-      message: t('register.errors.passwordConfirm'),
+      message: t('register.errors.password_confirm'),
       path: ['passwordConfirm'],
     });
 
@@ -55,7 +55,7 @@ export const useRegisterForm = () => {
         switch (err.response?.status) {
           case 409:
             form.setError('email', {
-              message: t('register.errors.emailAlreadyExists'),
+              message: t('register.errors.email_already_exists'),
             });
             break;
           default:
@@ -84,9 +84,9 @@ export const useRegisterForm = () => {
     } catch (err) {
       if (err instanceof AxiosError) {
         switch (err.response?.status) {
-          case 400:
+          case 403:
             form.setError('code', {
-              message: t('register.errors.invalidCode'),
+              message: t('register.errors.invalid_code'),
             });
             break;
           default:
@@ -106,14 +106,14 @@ export const useRegisterForm = () => {
     } catch (err) {
       if (err instanceof AxiosError) {
         switch (err.response?.status) {
-          case 400:
+          case 403:
             form.setError('code', {
-              message: t('register.errors.invalidCode'),
+              message: t('register.errors.invalid_code'),
             });
             break;
           case 409:
             form.setError('email', {
-              message: t('register.errors.emailAlreadyExists'),
+              message: t('register.errors.email_already_exists'),
             });
             break;
           default:
diff --git a/src/features/oauth/components/authorize-ziggle-form.tsx b/src/features/oauth/components/authorize-ziggle-form.tsx
new file mode 100644
index 00000000..eb52e1d0
--- /dev/null
+++ b/src/features/oauth/components/authorize-ziggle-form.tsx
@@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { ConsentFormSchema } from '../hooks/use-consent-form';
+
+import { Button, Checkbox } from '@/features/core';
+
+const ziggleRequiredScopes = ['email', 'name'] as const;
+const ziggleOptionalScopes = ['student_id', 'phone_number'] as const;
+
+export function AuthorizeForm({
+  onSubmit,
+}: {
+  onSubmit: (e?: React.SyntheticEvent) => void;
+}) {
+  const { register, setValue, watch, getValues } =
+    useFormContext<ConsentFormSchema>();
+  const { t } = useTranslation();
+  const [allAgree, setAllAgree] = useState(false);
+
+  setValue('client_id', 'ziggle2023');
+  ziggleRequiredScopes.forEach((scope) => setValue(scope, true));
+
+  useEffect(() => {
+    setAllAgree(ziggleOptionalScopes.every((scope) => watch(scope)));
+    console.log('allAgree', allAgree); // TESTING
+  }, [watch, allAgree]);
+
+  return (
+    <div className="flex flex-col">
+      <Checkbox
+        defaultChecked={allAgree}
+        onChange={() => {
+          ziggleOptionalScopes.forEach((scope) => setValue(scope, !allAgree));
+          setAllAgree((prev) => !prev);
+          console.log(getValues()); // TESTING
+        }}
+      >
+        <div className="font-bold">{t('authorize.checkboxes.all_agree')}</div>
+      </Checkbox>
+      <div className="h-2.5" />
+      <div className="rounded-lg border border-neutral-200 px-5 py-4">
+        <div className="text-body-2 mb-1 text-neutral-800">
+          {t('authorize.labels.required')}
+        </div>
+        <div className="flex flex-col gap-1 pl-1">
+          {ziggleRequiredScopes.map((scope) => (
+            <Checkbox key={scope} disabled {...register(scope)}>
+              {t(`authorize.checkboxes.${scope}`)}
+            </Checkbox>
+          ))}
+        </div>
+        <div className="h-2.5" />
+        <div className="text-body-2 mb-1 text-neutral-800">
+          {t('authorize.labels.optional')}
+        </div>
+        <div className="flex flex-col gap-1 pl-1">
+          {ziggleOptionalScopes.map((scope) => (
+            <Checkbox key={scope} {...register(scope)}>
+              {t(`authorize.checkboxes.${scope}`)}
+            </Checkbox>
+          ))}
+        </div>
+      </div>
+      <div className="h-10" />
+      <div className="flex gap-2.5">
+        <Button variant="secondary">{t('authorize.buttons.cancel')}</Button>
+        <Button variant="primary" onClick={onSubmit}>
+          {t('authorize.buttons.continue')}
+        </Button>
+      </div>
+    </div>
+  );
+}
diff --git a/src/features/oauth/frames/authorize-frame.tsx b/src/features/oauth/frames/authorize-frame.tsx
index f5e5f92b..79c17e86 100644
--- a/src/features/oauth/frames/authorize-frame.tsx
+++ b/src/features/oauth/frames/authorize-frame.tsx
@@ -24,7 +24,7 @@ export function AuthorizeFrame() {
             setChecked((prev) => prev.map(() => !prev.every(Boolean)))
           }
         >
-          <div className="font-bold">{t('authorize.checkboxes.all-agree')}</div>
+          <div className="font-bold">{t('authorize.checkboxes.all_agree')}</div>
         </Checkbox>
         <div className="h-2.5" />
         <div className="rounded-lg border border-neutral-200 px-5 py-4">
@@ -50,7 +50,7 @@ export function AuthorizeFrame() {
                 setChecked((prev) => prev.map((v, i) => (i === 0 ? !v : v)))
               }
             >
-              {t('authorize.checkboxes.studentId')}
+              {t('authorize.checkboxes.student_id')}
             </Checkbox>
             <Checkbox
               checked={checked[1]}
@@ -58,7 +58,7 @@ export function AuthorizeFrame() {
                 setChecked((prev) => prev.map((v, i) => (i === 1 ? !v : v)))
               }
             >
-              {t('authorize.checkboxes.phoneNumber')}
+              {t('authorize.checkboxes.phone_number')}
             </Checkbox>
           </div>
         </div>
diff --git a/src/features/oauth/frames/authorize-ziggle-frame.tsx b/src/features/oauth/frames/authorize-ziggle-frame.tsx
new file mode 100644
index 00000000..6bda182b
--- /dev/null
+++ b/src/features/oauth/frames/authorize-ziggle-frame.tsx
@@ -0,0 +1,30 @@
+import { FormProvider } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { AuthorizeForm } from '../components/authorize-ziggle-form';
+import { useAuthorizeForm } from '../hooks/use-consent-form';
+
+export function AuthorizeFrame() {
+  const { form, onSubmit } = useAuthorizeForm();
+  const { t } = useTranslation();
+
+  return (
+    <div className="flex min-h-screen items-center justify-center">
+      <div className="flex w-full max-w-[400px] flex-col px-5 py-8">
+        <div className="text-title-1 whitespace-pre-wrap">
+          {t('authorize.title')}
+        </div>
+        <div className="h-8" />
+        <div className="text-body-2 text-neutral-800">
+          {t('authorize.description')}
+        </div>
+        <div className="h-1" />
+        <FormProvider {...form}>
+          <form>
+            <AuthorizeForm onSubmit={onSubmit} />
+          </form>
+        </FormProvider>
+      </div>
+    </div>
+  );
+}
diff --git a/src/features/oauth/hooks/use-consent-form.ts b/src/features/oauth/hooks/use-consent-form.ts
new file mode 100644
index 00000000..54a12c1b
--- /dev/null
+++ b/src/features/oauth/hooks/use-consent-form.ts
@@ -0,0 +1,57 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { TFunction } from 'i18next';
+import { useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { z, ZodBoolean } from 'zod';
+
+import { consent } from '../services/use-consent';
+
+export const ClientScopeList = [
+  'profile',
+  'name',
+  'email',
+  'phone_number',
+  'student_id',
+] as const;
+export type ClientScopeType = (typeof ClientScopeList)[number];
+
+export const TokenScopeList = ['openid', 'offline_access'] as const;
+export type TokenScopeType = (typeof TokenScopeList)[number];
+
+export const ScopeList = [...ClientScopeList, ...TokenScopeList];
+export type ScopeType = (typeof ScopeList)[number];
+
+export const createSchema = (t: TFunction) =>
+  z.object({
+    client_id: z.string().min(1, t('authorize.errors.client_id')), // TODO: i18next
+    ...ScopeList.reduce(
+      (acc, scope) => {
+        acc[scope] = z.boolean().default(false);
+        return acc;
+      },
+      {} as Record<ScopeType, z.ZodDefault<ZodBoolean>>,
+    ),
+  });
+
+export type ConsentFormSchema = z.infer<ReturnType<typeof createSchema>>;
+
+export const useAuthorizeForm = () => {
+  const { t } = useTranslation();
+  const form = useForm({
+    resolver: zodResolver(createSchema(t)),
+    mode: 'onBlur',
+  });
+
+  const onSubmit = form.handleSubmit(async (data) => {
+    console.log({
+      client_id: data.client_id,
+      scope: ScopeList.filter((scope) => data[scope]).join(' '),
+    }); // TESTING
+    await consent({
+      client_id: data.client_id,
+      scope: ScopeList.filter((scope) => data[scope]).join(' '),
+    });
+  });
+
+  return { form, onSubmit };
+};
diff --git a/src/features/oauth/index.ts b/src/features/oauth/index.ts
index cbd16fca..e497541b 100644
--- a/src/features/oauth/index.ts
+++ b/src/features/oauth/index.ts
@@ -1 +1 @@
-export * from './frames/authorize-frame';
+export * from './frames/authorize-ziggle-frame';
diff --git a/src/features/oauth/services/use-consent.ts b/src/features/oauth/services/use-consent.ts
new file mode 100644
index 00000000..4f4e4148
--- /dev/null
+++ b/src/features/oauth/services/use-consent.ts
@@ -0,0 +1,11 @@
+import { api } from '@/features/core';
+
+export type ConsentRequestBody = {
+  client_id: string;
+  scope: string;
+};
+
+export const consent = async (requestBody: ConsentRequestBody) => {
+  const res = await api.post<void>('/oauth/consent', requestBody);
+  return res.data;
+};
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 2cf2ac39..643726a6 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -4,45 +4,45 @@
     "titles": {
       "email": "Email",
       "password": "Password",
-      "defaultInfo": "Basic Information"
+      "default_info": "Basic Information"
     },
     "fields": {
       "email": "GIST Email",
       "code": "Verification Code",
       "password": "Password",
-      "passwordConfirm": "Confirm Password",
+      "password_confirm": "Confirm Password",
       "name": "Name",
-      "studentId": "Student ID",
-      "phoneNumber": "Phone Number"
+      "student_id": "Student ID",
+      "phone_number": "Phone Number"
     },
     "placeholders": {
       "email": "m@gm.gist.ac.kr",
       "code": "Verification Code",
       "password": "Password",
-      "passwordConfirm": "Confirm Password",
+      "password_confirm": "Confirm Password",
       "name": "Kim Jin-i",
-      "studentId": "{{form}}",
-      "phoneNumber": "010-0000-0000"
+      "student_id": "{{form}}",
+      "phone_number": "010-0000-0000"
     },
     "errors": {
-      "emailAlreadyExists": "This email is already registered",
+      "email_already_exists": "This email is already registered",
       "email": "Please use a GIST email (@gm.gist.ac.kr, @gist.ac.kr)",
-      "invalidCode": "The verification code is incorrect",
+      "invalid_code": "The verification code is incorrect",
       "code": "The verification code could not be verified",
       "password": "Password must be at least 12 characters long",
-      "passwordConfirm": "Passwords do not match",
+      "password_confirm": "Passwords do not match",
       "name": "Please enter your name",
-      "studentId": "Please enter in the {{form}} format.",
-      "phoneNumber": "Please enter a valid phone number"
+      "student_id": "Please enter in the {{form}} format.",
+      "phone_number": "Please enter a valid phone number"
     },
     "buttons": {
-      "sendCode": "Send Verification Code",
-      "resendCode": "Resend Verification Code",
-      "verifyCode": "Verify",
+      "send_code": "Send Verification Code",
+      "resend_code": "Resend Verification Code",
+      "verify_code": "Verify",
       "next": "Next"
     }
   },
-  "register-done": {
+  "register_done": {
     "title": "Register",
     "description": "Registration is complete",
     "buttons": {
@@ -72,11 +72,14 @@
       "optional": "Optional"
     },
     "checkboxes": {
-      "all-agree": "Agree to All",
+      "all_agree": "Agree to All",
       "email": "Email",
       "name": "Name",
-      "studentId": "Student ID",
-      "phoneNumber": "Phone Number"
+      "student_id": "Student ID",
+      "phone_number": "Phone Number"
+    },
+    "errors": {
+      "client_id": "Client ID is required"
     },
     "buttons": {
       "cancel": "Cancel",
diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json
index 3805b98c..02a26bf5 100644
--- a/src/locales/ko/translation.json
+++ b/src/locales/ko/translation.json
@@ -4,45 +4,45 @@
     "titles": {
       "email": "이메일",
       "password": "비밀번호",
-      "defaultInfo": "기본정보"
+      "default_info": "기본정보"
     },
     "fields": {
       "email": "GIST 이메일",
       "code": "인증번호",
       "password": "비밀번호",
-      "passwordConfirm": "비밀번호 확인",
+      "password_confirm": "비밀번호 확인",
       "name": "이름",
-      "studentId": "학번",
-      "phoneNumber": "전화번호"
+      "student_id": "학번",
+      "phone_number": "전화번호"
     },
     "placeholders": {
       "email": "m@gm.gist.ac.kr",
       "code": "인증번호",
       "password": "비밀번호",
-      "passwordConfirm": "비밀번호 확인",
+      "password_confirm": "비밀번호 확인",
       "name": "김지니",
-      "studentId": "{{form}}",
-      "phoneNumber": "010-0000-0000"
+      "student_id": "{{form}}",
+      "phone_number": "010-0000-0000"
     },
     "errors": {
-      "emailAlreadyExists": "해당 이메일은 이미 가입되어 있습니다",
+      "email_already_exists": "해당 이메일은 이미 가입되어 있습니다",
       "email": "지스트 메일(@gm.gist.ac.kr, @gist.ac.kr)을 사용해주세요",
-      "invalidCode": "인증번호가 잘못 되었습니다",
+      "invalid_code": "인증번호가 잘못 되었습니다",
       "code": "인증번호가 확인되지 않았습니다",
       "password": "비밀번호는 12자리 이상이어야 합니다",
-      "passwordConfirm": "비밀번호가 일치하지 않습니다",
+      "password_confirm": "비밀번호가 일치하지 않습니다",
       "name": "이름을 입력해주세요",
-      "studentId": "{{form}}형태로 입력해주세요.",
-      "phoneNumber": "올바른 전화번호를 입력해주세요"
+      "student_id": "{{form}}형태로 입력해주세요.",
+      "phone_number": "올바른 전화번호를 입력해주세요"
     },
     "buttons": {
-      "sendCode": "인증번호 발송",
-      "resendCode": "인증번호 재발송",
-      "verifyCode": "확인",
+      "send_code": "인증번호 발송",
+      "resend_code": "인증번호 재발송",
+      "verify_code": "확인",
       "next": "다음으로"
     }
   },
-  "register-done": {
+  "register_done": {
     "title": "회원가입",
     "description": "회원가입이 완료되었습니다",
     "buttons": {
@@ -72,11 +72,14 @@
       "optional": "선택"
     },
     "checkboxes": {
-      "all-agree": "전체 동의하기",
+      "all_agree": "전체 동의하기",
       "email": "이메일",
       "name": "이름",
-      "studentId": "학번",
-      "phoneNumber": "전화번호"
+      "student_id": "학번",
+      "phone_number": "전화번호"
+    },
+    "errors": {
+      "client_id": "클라이언트 ID가 필요합니다"
     },
     "buttons": {
       "cancel": "취소",


Reviews:
user: login: rhseung
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T14:13:04Z
user: login: 2paperstar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T14:14:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (17)</summary><blockquote>

<details>
<summary>src/routes/authorize.tsx (1)</summary><blockquote>

`1-11`: **인증 페이지 라우트가 적절하게 구현되었습니다**

`@tanstack/react-router`를 사용하여 인증 경로를 명확하게 정의했고, 컴포넌트 구조도 간결합니다. `AuthorizeFrame` 컴포넌트를 통해 실제 인증 로직이 잘 캡슐화되어 있습니다.


다만, 이 인증 페이지의 구체적인 목적이나 사용 사례를 문서화하는 것이 좋을 것 같습니다. 예를 들어, 주석으로 "OAuth 클라이언트 인증을 위한 페이지" 등을 추가하는 것이 미래에 이 코드를 유지보수할 때 도움이 될 것입니다.

</blockquote></details>
<details>
<summary>src/routes/auth/register/index.tsx (1)</summary><blockquote>

`9-9`: **경로 변경이 적절하게 이루어졌습니다**

경로를 '/auth/register'에서 '/auth/register/'로 변경하여 후행 슬래시(trailing slash)를 추가했습니다. 이는 라우트 구조상 하위 경로(`/auth/register/done`)와의 일관성을 유지하는 데 도움이 됩니다.


라우트 구조에 대한 설명을 주석으로 추가하면 팀원들이 후행 슬래시의 중요성과 이 경로가 다른 등록 관련 경로와 어떻게 연결되는지 이해하는 데 도움이 될 것입니다.

</blockquote></details>
<details>
<summary>src/features/oauth/services/use-consent.ts (1)</summary><blockquote>

`8-11`: **에러 처리 로직 추가를 고려해보세요.**

API 호출 시 발생할 수 있는 오류에 대한 처리가 없습니다. 사용자 경험 향상을 위해 적절한 에러 처리 로직을 추가하는 것이 좋겠습니다.


```diff
export const consent = async (requestBody: ConsentRequestBody) => {
-  const res = await api.post<void>('/oauth/consent', requestBody);
-  return res.data;
+  try {
+    const res = await api.post<void>('/oauth/consent', requestBody);
+    return res.data;
+  } catch (error) {
+    // 에러 로깅 또는 적절한 에러 객체 반환
+    console.error('OAuth consent request failed:', error);
+    throw error;
+  }
};
```

</blockquote></details>
<details>
<summary>src/features/auth/services/send-verification-code.ts (1)</summary><blockquote>

`9-14`: **에러 처리 및 반환 값 개선을 고려하세요.**

현재 함수는 에러 처리가 없으며, 반환 타입이 `void`로 지정되어 있습니다. 사용자 경험 향상을 위해 에러 처리 로직을 추가하고, 의미 있는 응답 값을 반환하는 것이 좋겠습니다.


```diff
export const sendVerificationCode = async (
  requestBody: SendVerificationRequestBody,
) => {
-  const res = await api.post<void>('/verify/email', requestBody);
-  return res.data;
+  try {
+    const res = await api.post<{ success: boolean }>('/verify/email', requestBody);
+    return res.data;
+  } catch (error) {
+    console.error('Failed to send verification code:', error);
+    throw error;
+  }
};
```

</blockquote></details>
<details>
<summary>src/features/auth/frames/register-done-frame.tsx (1)</summary><blockquote>

`9-24`: **반응형 레이아웃 구성이 좋습니다만, 일부 개선 사항이 있습니다.**

등록 완료 화면의 레이아웃이 잘 구성되어 있습니다. Tailwind CSS를 활용하여 반응형 디자인을 구현한 점이 좋습니다. 다만, 16번 줄의 `<div className="h-100" />` 부분에서 높이 값에 단위가 없습니다.


```diff
-        <div className="h-100" />
+        <div className="h-[100px]" />
```

또는 Tailwind의 표준 간격 클래스를 사용할 수도 있습니다:

```diff
-        <div className="h-100" />
+        <div className="my-10" />
```

</blockquote></details>
<details>
<summary>src/features/core/components/language-switcher.tsx (2)</summary><blockquote>

`15-23`: **버튼 UI 구현이 잘 되었으나 사용자 경험 개선이 가능합니다.**

현재 선택된 언어와 드롭다운 상태를 시각적으로 표시하는 방법이 직관적입니다. 다만, 접근성과 사용자 경험을 위해 화살표 기호(▲/▼) 대신 더 명확한 아이콘을 사용하는 것을 고려해볼 수 있습니다.


화살표 아이콘에 대해 더 현대적인 접근을 고려해보세요:
```diff
-        {`${i18n.language === 'en' ? 'English' : '한국어'} ${showDropdown ? '▲' : '▼'}`}
+        <span>{i18n.language === 'en' ? 'English' : '한국어'}</span>
+        <span className="ml-2">{showDropdown ? '▲' : '▼'}</span>
```

---

`24-42`: **드롭다운 UI가 잘 구현되었으나 접근성 개선이 필요합니다.**

드롭다운 메뉴와 언어 선택 버튼들이 잘 구현되어 있습니다. 그러나 사용자 경험 개선을 위한 몇 가지 사항을 고려해볼 수 있습니다:

1. 드롭다운 외부를 클릭했을 때 드롭다운이 닫히도록 하는 기능이 없습니다.
2. 키보드 접근성을 위한 키 이벤트 처리가 없습니다.



외부 클릭 시 드롭다운을 닫는 로직을 추가하는 것이 좋습니다:

```tsx
useEffect(() => {
  if (!showDropdown) return;
  
  const handleClickOutside = (event) => {
    if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
      setShowDropdown(false);
    }
  };
  
  document.addEventListener('mousedown', handleClickOutside);
  return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showDropdown]);

// ref를 추가하여 드롭다운 요소를 참조
const dropdownRef = useRef(null);

// 반환 부분에 ref 추가
return (
  <div className="relative inline-block text-sm font-medium">
    {/* ... */}
    {showDropdown && (
      <div 
        ref={dropdownRef}
        className="absolute right-0 bottom-full z-20 mb-2 w-30 rounded-lg border border-neutral-300 bg-white px-3 py-2"
      >
      {/* ... */}
```

</blockquote></details>
<details>
<summary>src/features/core/components/input.tsx (2)</summary><blockquote>

`12-13`: **TODO 항목 처리 필요**

두 개의 TODO 주석이 있습니다. 이러한 기능을 구현하거나 별도의 이슈로 관리하여 추적하는 것이 좋겠습니다.

해당 기능을 구현하는 코드를 작성해드릴까요? 또는 이러한 기능 개발을 위한 별도의 이슈를 생성해 드릴까요?

---

`15-61`: **접근성 개선 필요**

Input 컴포넌트의 구현은 잘 되어 있지만, 접근성(a11y) 측면에서 개선이 필요합니다. `aria-*` 속성이나 `id`와 `aria-describedby`를 통해 라벨과의 연결성을 높이는 것이 좋겠습니다.


다음과 같이 접근성을 개선하는 것을 제안합니다:

```diff
export const Input = forwardRef<HTMLInputElement, InputProps>(
  (
    {
      label,
      error,
      inputClassName,
      suffix,
      required = false,
      disabled = false,
      className,
+     id,
      ...props
    },
    ref,
  ) => {
+   const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
+   const errorId = `${inputId}-error`;
    return (
      <div className={className}>
        {label && (
-         <div className="text-label-1 mb-1 flex text-neutral-900">
+         <label htmlFor={inputId} className="text-label-1 mb-1 flex text-neutral-900">
            {label}
            {required && <div className="text-red-500">*</div>}
-         </div>
+         </label>
        )}
        <div className="flex items-center gap-2">
          <input
+           id={inputId}
            disabled={disabled}
            ref={ref}
+           aria-invalid={!!error}
+           aria-describedby={error ? errorId : undefined}
            className={cn(
              'w-full rounded-lg px-4 py-3',
              'text-body-1 text-neutral-950 placeholder:text-neutral-400',
              'focus:border-primary-400 border border-neutral-400 bg-white shadow-none focus:border focus:outline-none',
              error &&
                'border-red-500 bg-white shadow-[inset_0_0_0_1px_theme(colors.red.500)]',
              disabled &&
                'border border-neutral-400 bg-neutral-100 text-neutral-600',
              inputClassName,
            )}
            {...props}
          />
          {suffix}
        </div>
        {typeof error === 'string' && error.trim().length > 0 && (
-         <div className="text-label-1 mt-1 px-1 text-red-500">{error}</div>
+         <div id={errorId} className="text-label-1 mt-1 px-1 text-red-500">{error}</div>
        )}
      </div>
    );
  },
);
```

</blockquote></details>
<details>
<summary>src/features/auth/hooks/use-login-form.ts (1)</summary><blockquote>

`26-37`: **Promise 체인 대신 async/await 구문을 사용하도록 개선되었습니다.**

Promise 체인 방식에서 더 가독성이 좋은 async/await 구문으로 변경한 것은 좋은 개선입니다. 에러 처리 로직도 try/catch 블록으로 잘 구조화되었습니다.


다만, 디버깅 목적의 console.log가 있습니다:
```diff
- console.log(await login(data)); // TEST: debug
+ const result = await login(data);
+ // 필요한 경우 result를 처리
```

</blockquote></details>
<details>
<summary>src/features/oauth/components/authorize-ziggle-form.tsx (2)</summary><blockquote>

`25-28`: **디버깅 코드가 프로덕션 코드에 남아있습니다.**

`console.log('allAgree', allAgree);` 주석에 "TESTING"이라고 표시되어 있어 개발용 디버깅 코드로 보입니다. 프로덕션 코드에서는 제거하는 것이 좋습니다.

```diff
useEffect(() => {
  setAllAgree(ziggleOptionalScopes.every((scope) => watch(scope)));
- console.log('allAgree', allAgree); // TESTING
}, [watch, allAgree]);
```

---

`34-38`: **디버깅 코드가 남아있습니다.**

여기서도 `console.log(getValues());` 문이 "TESTING" 주석과 함께 존재합니다. 프로덕션 코드에서는 제거하는 것이 좋습니다.

```diff
onChange={() => {
  ziggleOptionalScopes.forEach((scope) => setValue(scope, !allAgree));
  setAllAgree((prev) => !prev);
- console.log(getValues()); // TESTING
}}
```

</blockquote></details>
<details>
<summary>src/features/core/components/button.tsx (1)</summary><blockquote>

`129-139`: **로딩 상태 표시기가 조건부로 렌더링됩니다.**

버튼 변형과 로딩 상태에 따라 적절한 로딩 표시기를 조건부로 렌더링하는 로직이 추가되었습니다. 삼항 연산자의 중첩이 조금 복잡하지만, 기능적으로는 적절히 구현되었습니다.


조금 더 가독성을 높이기 위해 다음과 같이 리팩토링을 고려해볼 수 있습니다:

```diff
- className={
-   variant === 'default'
-     ? 'bg-neutral-600'
-     : variant === 'primary'
-       ? 'bg-neutral-200'
-       : 'bg-primary-600'
- }
+ className={
+   {
+     'default': 'bg-neutral-600',
+     'primary': 'bg-neutral-200',
+     'secondary': 'bg-primary-600'
+   }[variant] || 'bg-neutral-600'
+ }
```

</blockquote></details>
<details>
<summary>src/features/oauth/hooks/use-consent-form.ts (1)</summary><blockquote>

`45-54`: **onSubmit 함수 구현**

`onSubmit` 함수 구현이 잘 되었으나, 47-49번 줄의 console.log는 테스트 목적으로 사용된 것 같습니다.

```diff
-    console.log({
-      client_id: data.client_id,
-      scope: ScopeList.filter((scope) => data[scope]).join(' '),
-    }); // TESTING
```
릴리스 전에 console.log 문을 제거하는 것이 좋습니다.

</blockquote></details>
<details>
<summary>src/features/auth/hooks/use-register-form.ts (3)</summary><blockquote>

`50-68`: **인증 코드 전송 로직을 onSendVerificationCode로 분리하여 가독성이 뛰어납니다.**  
다만 default 분기에서 발생할 수 있는 기타 에러(예: 5xx)에 대한 사용자 알림도 추가 고려하면 좋겠습니다.

---

`70-100`: **onVerifyCode 함수를 통해 인증번호 검증 단계를 명확히 분리했습니다.**  
403 에러만 처리하는 로직 외에도 그 외 에러(예: 4xx/5xx)에 대한 사용자 메시지를 추가하면 더욱 안정적입니다.

---

`102-128`: **onRegister 로직에서 403과 409 에러를 각각 처리해주는 구조가 효과적입니다.**  
마찬가지로 default 케이스에서 사용자 공지(알림창, 에러 메시지 등)를 보완하면 완성도가 높아질 것 같습니다.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between edc0056c321ee918c560f8d574c67295ead25cfd and 42d20d567daa2a50db16ff83e2c1cb697481e5d9.

</details>

<details>
<summary>📒 Files selected for processing (33)</summary>

* `.prettierrc` (1 hunks)
* `src/features/auth/components/input.tsx` (0 hunks)
* `src/features/auth/components/login-form.tsx` (1 hunks)
* `src/features/auth/components/register-form.tsx` (1 hunks)
* `src/features/auth/frames/login-frame.tsx` (3 hunks)
* `src/features/auth/frames/register-done-frame.tsx` (1 hunks)
* `src/features/auth/frames/register-frame.tsx` (1 hunks)
* `src/features/auth/hooks/use-login-form.ts` (2 hunks)
* `src/features/auth/hooks/use-register-form.ts` (2 hunks)
* `src/features/auth/index.ts` (1 hunks)
* `src/features/auth/services/get-token.ts` (1 hunks)
* `src/features/auth/services/send-verification-code.ts` (1 hunks)
* `src/features/auth/services/use-register.ts` (1 hunks)
* `src/features/core/components/button.tsx` (4 hunks)
* `src/features/core/components/input.tsx` (1 hunks)
* `src/features/core/components/language-switcher.tsx` (1 hunks)
* `src/features/core/index.ts` (1 hunks)
* `src/features/core/utils/constants.ts` (1 hunks)
* `src/features/oauth/components/authorize-ziggle-form.tsx` (1 hunks)
* `src/features/oauth/frames/authorize-ziggle-frame.tsx` (1 hunks)
* `src/features/oauth/hooks/use-consent-form.ts` (1 hunks)
* `src/features/oauth/index.ts` (1 hunks)
* `src/features/oauth/services/use-consent.ts` (1 hunks)
* `src/features/test/frames/component-test-frame.tsx` (5 hunks)
* `src/features/test/index.ts` (1 hunks)
* `src/locales/en/translation.json` (1 hunks)
* `src/locales/ko/translation.json` (1 hunks)
* `src/locales/language-switcher.tsx` (0 hunks)
* `src/routes/__root.tsx` (1 hunks)
* `src/routes/auth/register/done.tsx` (1 hunks)
* `src/routes/auth/register/index.tsx` (1 hunks)
* `src/routes/authorize.tsx` (1 hunks)
* `src/routes/component-test.tsx` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* src/locales/language-switcher.tsx
* src/features/auth/components/input.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (17)</summary>

<details>
<summary>src/routes/authorize.tsx (1)</summary><blockquote>

<details>
<summary>src/features/oauth/frames/authorize-ziggle-frame.tsx (1)</summary>

* `AuthorizeFrame` (7-30)

</details>

</blockquote></details>
<details>
<summary>src/routes/auth/register/done.tsx (2)</summary><blockquote>

<details>
<summary>src/features/auth/frames/register-done-frame.tsx (1)</summary>

* `RegisterDoneFrame` (6-25)

</details>
<details>
<summary>src/routes/auth/register/index.tsx (1)</summary>

* `Route` (9-11)

</details>

</blockquote></details>
<details>
<summary>src/features/auth/services/send-verification-code.ts (2)</summary><blockquote>

<details>
<summary>src/features/auth/hooks/use-register-form.ts (1)</summary>

* `RegisterFormSchema` (41-41)

</details>
<details>
<summary>src/features/core/services/api.ts (1)</summary>

* `api` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/features/auth/services/use-register.ts (3)</summary><blockquote>

<details>
<summary>src/features/auth/hooks/use-register-form.ts (1)</summary>

* `RegisterFormSchema` (41-41)

</details>
<details>
<summary>src/features/auth/services/get-token.ts (1)</summary>

* `VerifyResponse` (3-5)

</details>
<details>
<summary>src/features/core/services/api.ts (1)</summary>

* `api` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/features/core/components/language-switcher.tsx (1)</summary><blockquote>

<details>
<summary>src/features/core/components/button.tsx (1)</summary>

* `Button` (96-144)

</details>

</blockquote></details>
<details>
<summary>src/features/oauth/services/use-consent.ts (1)</summary><blockquote>

<details>
<summary>src/features/core/services/api.ts (1)</summary>

* `api` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/routes/component-test.tsx (1)</summary><blockquote>

<details>
<summary>src/features/test/frames/component-test-frame.tsx (1)</summary>

* `ComponentTestFrame` (3-205)

</details>

</blockquote></details>
<details>
<summary>src/features/auth/services/get-token.ts (1)</summary><blockquote>

<details>
<summary>src/features/core/services/api.ts (1)</summary>

* `api` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/features/auth/frames/register-done-frame.tsx (1)</summary><blockquote>

<details>
<summary>src/features/core/components/button.tsx (1)</summary>

* `Button` (96-144)

</details>

</blockquote></details>
<details>
<summary>src/features/auth/frames/login-frame.tsx (2)</summary><blockquote>

<details>
<summary>src/features/auth/components/login-form.tsx (1)</summary>

* `LoginForm` (8-31)

</details>
<details>
<summary>src/features/core/components/button.tsx (1)</summary>

* `Button` (96-144)

</details>

</blockquote></details>
<details>
<summary>src/features/core/components/input.tsx (1)</summary><blockquote>

<details>
<summary>src/features/core/utils/cn.ts (1)</summary>

* `cn` (29-31)

</details>

</blockquote></details>
<details>
<summary>src/features/oauth/frames/authorize-ziggle-frame.tsx (2)</summary><blockquote>

<details>
<summary>src/features/oauth/hooks/use-consent-form.ts (1)</summary>

* `useAuthorizeForm` (38-57)

</details>
<details>
<summary>src/features/oauth/components/authorize-ziggle-form.tsx (1)</summary>

* `AuthorizeForm` (12-75)

</details>

</blockquote></details>
<details>
<summary>src/routes/auth/register/index.tsx (1)</summary><blockquote>

<details>
<summary>src/routes/auth/register/done.tsx (1)</summary>

* `Route` (9-11)

</details>

</blockquote></details>
<details>
<summary>src/features/auth/frames/register-frame.tsx (3)</summary><blockquote>

<details>
<summary>src/features/auth/hooks/use-register-form.ts (1)</summary>

* `useRegisterForm` (43-131)

</details>
<details>
<summary>src/features/auth/components/register-form.tsx (1)</summary>

* `RegisterForm` (9-157)

</details>
<details>
<summary>src/features/core/components/button.tsx (1)</summary>

* `Button` (96-144)

</details>

</blockquote></details>
<details>
<summary>src/features/test/frames/component-test-frame.tsx (3)</summary><blockquote>

<details>
<summary>src/features/core/components/button.tsx (1)</summary>

* `Button` (96-144)

</details>
<details>
<summary>src/features/core/components/input.tsx (1)</summary>

* `Input` (15-61)

</details>
<details>
<summary>src/features/core/components/checkbox.tsx (1)</summary>

* `Checkbox` (7-43)

</details>

</blockquote></details>
<details>
<summary>src/features/auth/hooks/use-register-form.ts (3)</summary><blockquote>

<details>
<summary>src/features/auth/services/send-verification-code.ts (1)</summary>

* `sendVerificationCode` (9-14)

</details>
<details>
<summary>src/features/auth/services/get-token.ts (1)</summary>

* `getJWTToken` (13-16)

</details>
<details>
<summary>src/features/auth/services/use-register.ts (1)</summary>

* `register` (7-21)

</details>

</blockquote></details>
<details>
<summary>src/features/core/components/button.tsx (2)</summary><blockquote>

<details>
<summary>src/features/core/utils/cn.ts (1)</summary>

* `cn` (29-31)

</details>
<details>
<summary>src/features/core/components/loading-ellipse.tsx (1)</summary>

* `LoadingEllipse` (3-14)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (81)</summary><blockquote>

<details>
<summary>src/features/oauth/index.ts (1)</summary>

`1-1`: **OAuth 기능에 대한 내보내기 설정이 적절합니다.**

모듈의 접근성을 높이기 위해 배럴 내보내기(barrel export) 패턴을 사용한 것은 좋은 방식입니다. 이를 통해 다른 파일에서 `@/features/oauth`로 간단하게 AuthorizeFrame 및 관련 컴포넌트들을 가져올 수 있습니다.

</details>
<details>
<summary>src/features/test/index.ts (1)</summary>

`1-1`: **테스트 기능에 대한 내보내기 설정이 적절합니다.**

이전 파일과 마찬가지로 배럴 내보내기 패턴을 사용하여 `ComponentTestFrame` 컴포넌트를 손쉽게 가져올 수 있게 했습니다. 이런 방식은 코드의 가독성과 유지보수성을 향상시킵니다.

</details>
<details>
<summary>.prettierrc (1)</summary>

`4-5`: **Tailwind CSS 함수 설정이 적절합니다.**

Prettier 설정에 `tailwindFunctions` 속성을 추가한 것은 좋은 개선입니다. 이를 통해 `cn`과 `cva` 함수 내에서 사용되는 Tailwind 클래스 문자열이 Prettier에 의해 올바르게 포맷팅됩니다.

- `cn`: 일반적으로 클래스 이름을 조합하는 유틸리티 함수
- `cva`: "class variance authority"로, 변형 기반 컴포넌트를 Tailwind로 생성하는 데 사용

이 설정은 코드 스타일의 일관성을 유지하는 데 도움이 됩니다.

</details>
<details>
<summary>src/routes/__root.tsx (1)</summary>

`3-3`: **좋은 리팩토링입니다!**

상대 경로에서 절대 경로(`@/features/core`)로 변경하여 가져오기 경로를 개선했습니다. 이는 프로젝트 구조를 더 명확하게 하고 깊은 상대 경로를 피하는 데 도움이 됩니다.

</details>
<details>
<summary>src/routes/auth/register/done.tsx (1)</summary>

`1-11`: **회원가입 완료 페이지 라우트가 잘 구현되었습니다**

`RegisterDoneFrame` 컴포넌트를 활용하여 회원가입 완료 페이지를 적절하게 구현했습니다. 라우트 구조도 `/auth/register/done`으로 직관적이고, 이는 회원가입 단계에서의 위치를 명확하게 나타냅니다.

</details>
<details>
<summary>src/features/oauth/services/use-consent.ts (1)</summary>

`1-11`: **유용한 OAuth 동의 서비스 구현이 확인됩니다.**

전반적으로 코드가 간결하고 타입 정의가 명확합니다. `ConsentRequestBody` 타입과 `consent` 함수의 구현이 직관적입니다.

</details>
<details>
<summary>src/features/auth/index.ts (1)</summary>

`2-2`: **register-done-frame 추가 적절합니다.**

`register-done-frame` 컴포넌트를 내보내는 것은 등록 완료 화면 구현에 필요한 변경으로 보입니다. 코드 구조가 개선되었습니다.

</details>
<details>
<summary>src/routes/component-test.tsx (2)</summary>

`3-3`: **컴포넌트 임포트 경로 변경이 적절합니다.**

`ComponentTestFrame`의 임포트 경로를 `@/features/auth`에서 `@/features/test`로 변경한 것은 컴포넌트의 역할에 맞게 모듈 구조를 개선한 것으로 보입니다.

---

`9-9`: **라우트 경로 변경이 적절합니다.**

라우트 경로를 `/auth/component-test`에서 `/component-test`로 변경한 것은 컴포넌트 테스트가 인증 기능에 국한되지 않는다는 점을 고려할 때 적절한 변경입니다.

</details>
<details>
<summary>src/features/auth/services/send-verification-code.ts (2)</summary>

`1-7`: **인터페이스 정의가 적절합니다.**

`SendVerificationRequestBody` 인터페이스의 정의가 명확하며, 기존 `RegisterFormSchema`의 타입을 재활용하여 타입 일관성을 유지하고 있습니다.

---

`9-14`: **이메일 검증 코드 전송 기능 구현이 적절합니다.**

`sendVerificationCode` 함수의 구현이 간결하고 직관적입니다. API 호출 시 필요한 데이터 구조를 잘 활용하고 있습니다.

</details>
<details>
<summary>src/features/auth/frames/register-done-frame.tsx (3)</summary>

`1-5`: **적절한 임포트 구성입니다.**

필요한 모듈들을 명확하게 임포트했습니다. React Router의 `Link` 컴포넌트를 활용하여 페이지 네비게이션을 구현하고, 국제화를 위한 `useTranslation` 훅과 재사용 가능한 `Button` 컴포넌트를 적절히 임포트했습니다.

---

`6-8`: **함수 컴포넌트와 훅 사용이 잘 되었습니다.**

`RegisterDoneFrame` 컴포넌트가 명확하게 정의되었고, `useTranslation` 훅을 사용하여 국제화 기능을 적절히 구현했습니다.

---

`17-22`: **버튼과 링크 구현이 적절합니다.**

로그인 페이지로 이동하는 버튼이 잘 구현되어 있습니다. Link 컴포넌트로 버튼을 감싸서 네비게이션을 구현한 것이 적절합니다. 번역 키를 사용하여 버튼 텍스트를 다국어로 표시한 것도 좋은 접근입니다.

</details>
<details>
<summary>src/features/core/components/language-switcher.tsx (2)</summary>

`1-6`: **기본 임포트와 구조가 깔끔합니다.**

필요한 모듈들을 깔끔하게 임포트했습니다. React의 `useState` 훅과 i18n 라이브러리, 그리고 커스텀 `Button` 컴포넌트를 적절히 활용하고 있습니다.

---

`7-14`: **언어 전환 로직이 잘 구현되었습니다.**

상태 관리와 언어 전환 함수가 명확하게 구현되어 있습니다. `showDropdown` 상태를 통해 드롭다운 표시 여부를 제어하고, `handleLanguageChange` 함수를 통해 언어 변경 및 드롭다운 닫기를 처리하는 방식이 적절합니다.

</details>
<details>
<summary>src/features/auth/services/use-register.ts (4)</summary>

`3-4`: **새로운 인터페이스 임포트가 적절합니다.**

`VerifyResponse` 인터페이스를 임포트하여 등록 프로세스에 검증 토큰 정보를 포함할 수 있도록 한 것은 좋은 접근입니다.

---

`7-9`: **함수 시그니처 업데이트가 명확합니다.**

`register` 함수가 이제 `RegisterFormSchema`와 `VerifyResponse`를 결합한 요청 본문을 받도록 수정되었습니다. 이는 등록 프로세스에 검증 단계가 추가되었음을 의미합니다.

---

`14-17`: **전화번호 처리 로직이 개선되었습니다.**

요청 본문을 수정하여 전화번호 형식을 표준화하는 로직이 개선되었습니다. 정규식을 통해 전화번호 구성 요소를 추출하고 이를 조합하는 방식으로 변경되었습니다.

---

`19-19`: **API 응답 타입 변경이 적절합니다.**

API 엔드포인트의 응답 타입이 특정 타입에서 `void`로 변경되었습니다. 이는 API가 이제 더 이상 응답 데이터를 반환하지 않는다는 것을 의미합니다. 이러한 변경이 의도적이라면 적절한 수정입니다.

</details>
<details>
<summary>src/features/core/index.ts (2)</summary>

`1-6`: **코어 컴포넌트 익스포트 추가가. 잘 구성되었습니다.**

다양한 UI 컴포넌트들을 코어 모듈에서 익스포트하는 것은 코드 재사용성과 모듈화를 위한 좋은 방법입니다. 백 버튼, 기본 버튼, 체크박스, 입력 필드, 언어 전환기 및 로딩 표시 컴포넌트를 코어 기능으로 노출한 것이 적절합니다.

---

`9-9`: **유틸리티 상수 익스포트 추가가 적절합니다.**

유틸리티 상수를 익스포트하여 애플리케이션 전체에서 사용할 수 있도록 한 것은 좋은 접근입니다. 이를 통해 코드 중복을 줄이고 일관된 값을 유지할 수 있습니다.

</details>
<details>
<summary>src/features/auth/services/get-token.ts (2)</summary>

`3-5`: **인터페이스 정의가 명확합니다.**

VerifyResponse 인터페이스가 API 응답 구조를 명확하게 정의하고 있습니다. 이는 타입 안전성을 보장하는 좋은 접근 방식입니다.

---

`7-11`: **요청 본문 인터페이스 설계가 적절합니다.**

VerifyRequestBody 인터페이스에서 hint 필드를 리터럴 타입 'email'로 제한한 것은 타입 안전성 측면에서 좋은 설계입니다.

</details>
<details>
<summary>src/features/auth/components/login-form.tsx (4)</summary>

`2-2`: **국제화 적용이 잘 되었습니다.**

useTranslation 훅을 사용하여 국제화를 적용한 것은 좋은 접근 방식입니다. 다양한 언어를 지원하는 애플리케이션 개발에 적합합니다.



Also applies to: 10-10

---

`6-6`: **컴포넌트 임포트 경로 변경이 적절합니다.**

Input 컴포넌트를 코어 기능 디렉토리에서 가져오도록 변경한 것은 컴포넌트 재사용성을 높이는 좋은 구조 변경입니다.

---

`8-8`: **인터페이스 간소화가 잘 되었습니다.**

LoginForm 컴포넌트에서 disabled 프롭을 제거하여 인터페이스를 간소화한 것은 좋은 변경입니다.

---

`16-19`: **폼 상태 기반 비활성화 처리가 개선되었습니다.**

Input 컴포넌트의 disabled 상태를 외부 프롭 대신 formState.isSubmitting을 사용하도록 변경하고, placeholder 텍스트를 번역 함수를 통해 가져오는 것으로 개선되었습니다.



Also applies to: 24-27

</details>
<details>
<summary>src/features/core/components/input.tsx (1)</summary>

`5-10`: **인터페이스 설계가 유연합니다.**

InputProps 인터페이스가 HTML 입력 속성을 확장하면서 추가적인 커스텀 속성을 정의하여 다양한 사용 사례에 유연하게 대응할 수 있습니다.

</details>
<details>
<summary>src/features/oauth/frames/authorize-ziggle-frame.tsx (2)</summary>

`11-28`: **응답형 레이아웃 구현이 우수합니다.**

응답형 레이아웃이 잘 구현되어 있으며, Tailwind CSS 클래스를 효과적으로 활용하여 스타일링하고 있습니다. 화면 중앙에 콘텐츠를 정렬하고 최대 너비를 제한하여 다양한 화면 크기에서 일관된 사용자 경험을 제공합니다.

---

`14-20`: **국제화 처리가 잘 적용되어 있습니다.**

타이틀과 설명에 국제화를 적용하여 다국어 지원을 준비한 것이 좋습니다. 특히 whitespace-pre-wrap을 사용하여 번역된 텍스트의 줄바꿈을 보존하는 부분이 세심하게 고려되었습니다.

</details>
<details>
<summary>src/features/auth/hooks/use-login-form.ts (1)</summary>

`10-10`: **타입 정의가 단순화되었습니다.**

`TFunction<'translation', undefined>` 대신 단순히 `TFunction`을 사용함으로써 타입 정의가 더 간결해졌습니다. 이 변경은 코드를 더 유연하게 만들지만, 프로젝트 전체에서 일관성 있게 적용되었는지 확인하세요.

</details>
<details>
<summary>src/features/oauth/components/authorize-ziggle-form.tsx (1)</summary>

`9-10`: **상수 값을 적절히 정의했습니다.**

필수 스코프와 선택적 스코프를 상수로 분리하여 정의한 접근 방식이 좋습니다. `as const`를 사용하여 타입 안전성을 확보한 것도 훌륭합니다.

</details>
<details>
<summary>src/features/auth/frames/login-frame.tsx (4)</summary>

`9-9`: **Button 임포트 경로가 개선되었습니다.**

상대 경로 대신 절대 경로(`@/features/core`)를 사용하여 Button 컴포넌트를 임포트하는 방식으로 개선되었습니다. 이는 코드 구조를 더 명확하게 만들고 유지보수성을 향상시킵니다.

---

`25-25`: **LoginForm의 disabled 속성이 제거되었습니다.**

LoginForm에서 disabled 속성을 제거한 것은 적절한 개선입니다. `src/features/auth/components/login-form.tsx`를 확인해보면, Input 컴포넌트들이 이미 `formState.isSubmitting`을 통해 disabled 상태를 관리하고 있으므로 이 변경은 코드 중복을 제거합니다.

---

`35-35`: **번역 키 구조가 개선되었습니다.**

번역 키가 `login.login`에서 `login.buttons.login`으로 변경되었습니다. 이는 관련 문자열을 더 논리적인 구조로 구성하여 유지보수성을 향상시킵니다.

---

`43-43`: **번역 키 구조가 일관되게 개선되었습니다.**

등록 버튼의 번역 키도 같은 방식으로 `login.register`에서 `login.buttons.register`로 일관되게 변경되었습니다. 이는 번역 관리를 더 체계적으로 만듭니다.

</details>
<details>
<summary>src/features/core/components/button.tsx (5)</summary>

`4-4`: **임포트 구문이 개선되었습니다.**

`cn`과 `LoadingEllipse`를 함께 임포트하여 코드가 더 간결해졌습니다. 절대 경로를 사용하는 것도 좋은 관행입니다.

---

`12-12`: **미해결 TODO 주석이 있습니다.**

TODO 주석이 코드에 남아 있습니다. 이 주석에서 언급된 개선 사항(`isPressed -> active:로 대체 가능`)을 구현하거나, 이슈 트래커에 기록하고 주석을 제거하는 것이 좋겠습니다.

---

`14-16`: **버튼 스타일이 확장되었습니다.**

새로운 'default' 변형이 추가되었고, 기존 변형들의 스타일이 개선되었습니다. 특히, Tailwind CSS 클래스들을 보다 체계적으로 정리한 것이 눈에 띕니다.



Also applies to: 18-26

---

`28-29`: **로딩 상태에 대한 처리가 추가되었습니다.**

버튼의 로딩 상태(`isLoading`)를 처리하는 로직이 추가되었고, 다양한 버튼 변형에 대한 복합 변형(compound variants)도 적절히 정의되었습니다. 이는 사용자 경험을 개선하는 좋은 변경입니다.



Also applies to: 31-71

---

`118-126`: **버튼의 비활성화 및 클래스 처리가 개선되었습니다.**

버튼이 로딩 중일 때도 비활성화되도록 처리된 것은 좋은 개선입니다. 또한 `cn` 함수를 사용하여 클래스들을 결합하는 방식도 더 깔끔해졌습니다.

</details>
<details>
<summary>src/features/test/frames/component-test-frame.tsx (6)</summary>

`1-1`: **컴포넌트 임포트 개선이 잘 되었습니다.**

코어 컴포넌트 임포트를 단일 경로로 통합하여 코드를 더 깔끔하게 정리했습니다.

---

`103-113`: **버튼 컴포넌트 테스트 섹션 개선 사항**

그리드 레이아웃을 5개 컬럼으로 변경하고 default 버튼 변형을 추가한 것이 좋습니다. 로딩 상태 버튼들을 자체 닫기 태그로 단순화한 것도
코드를 더 간결하게 만들었습니다.

---

`140-145`: **Input 컴포넌트에 suffix 추가**

Input 컴포넌트에 확인 버튼을 suffix로 추가한 것이 사용자 경험을 향상시킬 것입니다.

---

`153-163`: **두 번째 Input 컴포넌트에 suffix 추가**

disabled 상태의 Input 컴포넌트에도 suffix 추가가 잘 구현되었습니다.

---

`176-181`: **에러 상태 Input 컴포넌트에 suffix 추가**

에러 상태의 Input 컴포넌트에도 suffix를 추가하여 일관성 있는 UI를 제공하고 있습니다.

---

`188-194`: **Checkbox defaultChecked 속성 사용**

`checked` 대신 `defaultChecked`를 사용한 것은 올바른 변경입니다. 이렇게 하면 React의 제어 컴포넌트 패턴에 더 잘 맞습니다.

</details>
<details>
<summary>src/features/auth/frames/register-frame.tsx (3)</summary>

`11-12`: **훅 반환값 구조 개선**

`useRegisterForm` 훅에서 반환하는 값들을 더 세분화하여 코드 가독성과 유지보수성이 향상되었습니다.

---

`22-26`: **RegisterForm 컴포넌트 Props 전달 방식 개선**

`onSendVerificationCode`와 `onVerifyCode` 메서드를 직접 전달하는 방식으로 변경하여 컴포넌트 간 책임 분리가 명확해졌습니다.

---

`28-37`: **버튼 타입 변경 및 온클릭 핸들러 추가**

버튼 타입을 "button"으로 변경하고 비동기 onClick 핸들러를 추가한 것은 폼 제출 로직을 더 세밀하게 제어할 수 있게 해줍니다. 성공 시 라우터를 사용한 페이지 이동도 잘 구현되었습니다.

</details>
<details>
<summary>src/features/oauth/hooks/use-consent-form.ts (2)</summary>

`9-23`: **OAuth 스코프 상수 및 타입 정의**

클라이언트 스코프와 토큰 스코프를 상수와 타입으로 잘 정의했습니다. 타입 안전성을 보장하면서 스코프를 관리할 수 있는 좋은 접근법입니다.

---

`24-34`: **동적 스키마 생성 함수 구현**

`createSchema` 함수는 다국어 지원을 고려한 스키마 생성 로직을 잘 구현했습니다. 다만, 26번 줄의 TODO 주석은 추후 작업이 필요합니다.


i18next 설정이 완료되었는지 확인하고, TODO 주석을 해결해 주세요.

</details>
<details>
<summary>src/features/auth/components/register-form.tsx (6)</summary>

`9-15`: **컴포넌트 프롭스 인터페이스 개선**

컴포넌트의 프롭스 인터페이스가 명확하게 정의되었습니다. 비동기 콜백 함수를 타입과 함께 잘 정의했습니다.

---

`20-24`: **상태 관리 로직 추가**

인증 코드 전송 및 확인에 관한 상태를 관리하는 로직이 잘 구현되었습니다.

---

`26-27`: **국제화(i18n) 관련 TODO 코멘트**

버튼 너비와 관련된 국제화 이슈가 TODO로 남아있습니다.

이 TODO를 해결하기 위한 작업 계획이 있으신가요? 다국어 지원을 위해서는 고정 너비보다 콘텐츠에 맞게 조정되는 방식이 필요합니다.

---

`33-67`: **이메일 입력 및 인증 코드 전송 로직 개선**

이메일 입력과 인증 코드 전송 버튼 UI 및 로직이 잘 구현되었습니다. 특히 `Controller` 컴포넌트를 활용하여 상태에 따른 버튼 UI 변경이 효과적으로 처리되었습니다.

---

`68-97`: **인증 코드 확인 UI 및 로직 개선**

인증 코드 입력 필드와 확인 버튼이 조건부 렌더링으로 잘 구현되었습니다. 특히 `suffix` 속성을 활용하여 입력 필드와 버튼을 결합한 UI가 사용자 경험을 향상시킬 것입니다.

---

`139-141`: **학번 플레이스홀더 동적 생성**

현재 연도를 기반으로 학번 플레이스홀더를 동적으로 생성한 것은 좋은 접근법입니다.

</details>
<details>
<summary>src/locales/ko/translation.json (9)</summary>

`4-8`: **'titles' 섹션이 더 구조화되었습니다.**  
기존에 분산되었던 타이틀들을 묶어둠으로써 가독성이 좋아졌습니다.

---

`9-17`: **'fields' 객체 추가로 필드별 관리가 보기 쉽게 정리되었습니다.**  
각 필드가 명확히 구분되어 유지보수성이 높아집니다.

---

`18-26`: **'placeholders' 섹션 도입으로 사용자 입력 예시를 구체화했습니다.**  
직관적인 예시가 제공되어 사용자 경험 개선에 도움이 됩니다.

---

`28-37`: **오류 메시지들이 더욱 명확하고 세분화되었습니다.**  
사용자가 어떤 값을 잘못 입력했는지 빠르게 이해할 수 있어 좋습니다.

---

`38-43`: **버튼 라벨을 별도의 객체로 관리해 유지보수 편의성이 높아졌습니다.**

---

`45-50`: **'register_done' 섹션 분리로 회원가입 완료 화면을 명확히 안내합니다.**  
사용자가 회원가입이 종료되었음을 직관적으로 인지할 수 있습니다.

---

`53-56`: **로그인 플레이스홀더를 상세히 구분해 UX가 향상되었습니다.**  
사용자가 어떤 값을 입력해야 할지 한눈에 파악하기 쉽습니다.

---

`62-65`: **로그인 관련 버튼 텍스트가 간결해 사용자가 쉽게 인지할 수 있습니다.**

---

`67-88`: **'authorize' 섹션 추가로 사용자 동의 절차가 명확하게 구조화되었습니다.**  
필수·선택 정보를 분리해 투명성이 높아졌습니다.

</details>
<details>
<summary>src/features/auth/hooks/use-register-form.ts (4)</summary>

`2-2`: **AxiosError를 도입하여 에러 처리가 좀 더 구체화되었습니다.**  
axios 요청 실패 시 에러 타입을 보다 명확하게 구분할 수 있다는 점이 좋습니다.

---

`8-9`: **getJWTToken, sendVerificationCode 함수를 적절히 import하여 구조화되었습니다.**  
서비스 단에서 분리된 로직을 재사용하기 용이해 보입니다.

---

`12-39`: **새로운 createSchema로 유효성 검사가 한층 강화되었습니다.**  
'code' 필드를 추가하고, 비밀번호의 최소 길이를 12자로 지정하며, 학번·전화번호 형식을 정규식으로 엄격히 체크하므로 사용자 입력 품질이 향상됩니다.

---

`130-130`: **Hook 반환 객체가 명확해져 컴포넌트 사용 시 편리해졌습니다.**  
필요한 함수를 일괄 제공하므로 코드 가독성과 재사용성이 개선됩니다.

</details>
<details>
<summary>src/locales/en/translation.json (9)</summary>

`4-8`: **'titles' 섹션으로 영문 타이틀 관리를 체계화했습니다.**  
기존보다 항목이 명확히 분리되어 찾기 쉽습니다.

---

`9-17`: **'fields' 구성이 훨씬 명료해졌습니다.**  
영문 번역에서도 각 필드를 쉽게 유지보수할 수 있게 됩니다.

---

`18-26`: **placeholder 항목이 추가되어 사용자에게 예시를 효과적으로 제시합니다.**

---

`28-37`: **에러 메시지들이 명확한 어투로 변경되어 사용자 안내가 좋아졌습니다.**  
비밀번호 길이라는 중요한 정보를 분명히 전달해 주는 점이 인상적입니다.

---

`38-43`: **버튼 관련 문자열들을 하나의 섹션으로 모아 관리 효율을 높였습니다.**

---

`45-50`: **'register_done' 세부 섹션으로 성공적인 회원가입 안내를 분리해 가독성이 향상되었습니다.**

---

`53-56`: **로그인 섹션에 placeholder를 추가하여 직관적인 예시 입력을 유도합니다.**

---

`62-65`: **로그인 버튼 정의가 간결해 사용자 액션이 명확합니다.**

---

`67-88`: **'authorize' 섹션 추가로 권한 동의 프로세스가 구체적으로 드러납니다.**  
필수·선택 항목 구분이 명확해 UX가 개선되었습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:16:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/features/oauth/frames/authorize-frame.tsx (1)</summary><blockquote>

`6-73`: **전체적인 코드 구조와 기능 승인**

AuthorizeFrame 컴포넌트가 잘 구현되었습니다. 필수 항목(이메일, 이름)과 선택 항목(학번, 전화번호)에 대한 동의를 얻는 UI가 명확하게 설계되었으며, "모두 동의" 기능도 잘 작동합니다. 국제화(i18n)를 위한 번역 키도 일관된 규칙으로 적용되었습니다.

다음 사항을 고려해볼 수 있습니다:
- 취소 및 계속 버튼에 대한 onClick 핸들러 추가
- 사용자 동의 선택 상태를 상위 컴포넌트에 전달하는 방법 구현

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 42d20d567daa2a50db16ff83e2c1cb697481e5d9 and 3650f7ccb4d0f0a5e3f69cf452cda46f989258d9.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/features/oauth/frames/authorize-frame.tsx` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>src/features/oauth/frames/authorize-frame.tsx (1)</summary><blockquote>

<details>
<summary>src/features/core/components/checkbox.tsx (1)</summary>

* `Checkbox` (7-43)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/features/oauth/frames/authorize-frame.tsx (3)</summary>

`27-27`: **명명 규칙 개선: kebab-case에서 snake_case로 변경됨**

번역 키가 `authorize.checkboxes.all-agree`에서 `authorize.checkboxes.all_agree`로 변경되었습니다. 이는 번역 키의 일관된 명명 규칙(snake_case)을 따르기 위한 좋은 변경입니다. 다른 파일의 번역 리소스도 이 변경에 맞게 업데이트되었는지 확인하세요.

---

`53-53`: **명명 규칙 개선: camelCase에서 snake_case로 변경됨**

번역 키가 `authorize.checkboxes.studentId`에서 `authorize.checkboxes.student_id`로 변경되었습니다. 이는 번역 키의 일관된 명명 규칙(snake_case)을 따르기 위한 좋은 변경입니다. 이 변경은 코드베이스의 일관성을 향상시킵니다.

---

`61-61`: **명명 규칙 개선: camelCase에서 snake_case로 변경됨**

번역 키가 `authorize.checkboxes.phoneNumber`에서 `authorize.checkboxes.phone_number`로 변경되었습니다. 이는 번역 키의 일관된 명명 규칙(snake_case)을 따르기 위한 좋은 변경입니다. 이러한 일관된 접근 방식은 유지보수성을 향상시킵니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T01:27:35Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T01:29:33Z
user: login: 2paperstar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T01:52:54Z

Review comments:
diff_hunk: @@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { ConsentFormSchema } from '../hooks/use-consent-form';
+
+import { Button, Checkbox } from '@/features/core';
+
+const ziggleRequiredScopes = ['email', 'name'] as const;
+const ziggleOptionalScopes = ['student_id', 'phone_number'] as const;
+
+export function AuthorizeForm({
+  onSubmit,
+}: {
+  onSubmit: (e?: React.SyntheticEvent) => void;
+}) {
+  const { register, setValue, watch, getValues } =
+    useFormContext<ConsentFormSchema>();
+  const { t } = useTranslation();
+  const [allAgree, setAllAgree] = useState(false);
+
+  setValue('client_id', 'ziggle2023');
+  ziggleRequiredScopes.forEach((scope) => setValue(scope, true));
+
+  useEffect(() => {
+    setAllAgree(ziggleOptionalScopes.every((scope) => watch(scope)));
+    console.log('allAgree', allAgree); // TESTING
+  }, [watch, allAgree]);
+
path: src/features/oauth/components/authorize-ziggle-form.tsx
user: login: rhseung
body: @2paperstar 현재 전체 동의 체크박스를 누를 때만 달라지고, optional 체크박스를 전부 누르거나 해제 한다고 전체 동의 체크박스가 달라지지가 않습니다.
handleChange 함수도 만들어서 register 함수 내에 넣어보고, state 따로 만들어서도 해봤는데 오히려 체크 박스가 꼬이기만 해서 gg선언... 알려주십쇼
created_at: 2025-04-07T14:13:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { ConsentFormSchema } from '../hooks/use-consent-form';
+
+import { Button, Checkbox } from '@/features/core';
+
+const ziggleRequiredScopes = ['email', 'name'] as const;
+const ziggleOptionalScopes = ['student_id', 'phone_number'] as const;
+
+export function AuthorizeForm({
+  onSubmit,
+}: {
+  onSubmit: (e?: React.SyntheticEvent) => void;
+}) {
+  const { register, setValue, watch, getValues } =
+    useFormContext<ConsentFormSchema>();
+  const { t } = useTranslation();
+  const [allAgree, setAllAgree] = useState(false);
+
+  setValue('client_id', 'ziggle2023');
+  ziggleRequiredScopes.forEach((scope) => setValue(scope, true));
+
+  useEffect(() => {
+    setAllAgree(ziggleOptionalScopes.every((scope) => watch(scope)));
+    console.log('allAgree', allAgree); // TESTING
+  }, [watch, allAgree]);
+
path: src/features/oauth/components/authorize-ziggle-form.tsx
user: login: 2paperstar
body: 전체에서 watch를 하면 되는데 음.. 저 watch가 저렇게 쓸 수 있는지 모르겠네요.
created_at: 2025-04-07T14:14:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,2 @@
+export const REGEX_PHONE_NUMBER =
+  /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/;
path: src/features/core/utils/constants.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**전화번호 정규식 패턴 검토가 필요합니다.**

현재 정규식은 다음과 같은 형식을 지원합니다:
- 선택적 국가 코드 (+1, +82 등)
- 지역 코드 (괄호 포함 또는 미포함)
- 다양한 구분자 (공백, 점, 하이픈)
- 3-4자리 및 4자리 숫자 조합

그러나 이 정규식은 일부 국제 전화번호 형식(특히 자릿수가 다른 경우)을 지원하지 않을 수 있습니다. 애플리케이션이 국제적으로 사용될 경우 더 포괄적인 검증이 필요할 수 있습니다.



국제 전화번호 형식에 대한 더 포괄적인 지원이 필요한 경우, 다음과 같은 정규식을 고려해 보세요:

```diff
-export const REGEX_PHONE_NUMBER =
-  /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/;
+export const REGEX_PHONE_NUMBER =
+  /^(\+\d{1,4})?\s?(\(\d{1,4}\)|\d{1,4})[\s.-]?(\d{1,4})[\s.-]?(\d{1,9})$/;
```

아니면 `libphonenumber-js` 같은 라이브러리 사용을 고려해 보세요:

---

🌐 Web query:

```web
libphonenumber-js 라이브러리로 전화번호 유효성 검사
```

💡 Result:

libphonenumber-js 라이브러리를 사용한 전화번호 유효성 검사는 국가 코드 기반의 정확한 검증이 가능하며, 다음 주요 기능을 제공합니다.

### 기본 사용법
1. **라이브러리 설치**
```bash
npm install libphonenumber-js
```

2. **전화번호 파싱 및 검증**  
```javascript
import parsePhoneNumber from 'libphonenumber-js';

const phoneNumber = parsePhoneNumber('+821023456789', 'KR');
if (phoneNumber) {
  console.log('가능한 번호:', phoneNumber.isPossible()); // 길이 검증 [3][9]
  console.log('유효한 번호:', phoneNumber.isValid());    // 전체 유효성 검증 [7][9]
}
```

### 주요 기능
**형식 검증 함수**
```javascript
import { 
  isPossiblePhoneNumber, 
  isValidPhoneNumber 
} from 'libphonenumber-js';

// 기본 길이 검사
isPossiblePhoneNumber('010-1234-5678', 'KR'); // true [9]

// 전체 유효성 검사
isValidPhoneNumber('02-123-4567', 'KR'); // true [9]
```

**오류 처리**
```javascript
import { parsePhoneNumberWithError, ParseError } from 'libphonenumber-js';

try {
  const number = parsePhoneNumberWithError('invalid_number', 'KR');
} catch (error) {
  if (error instanceof ParseError) {
    console.error('파싱 오류:', error.message); // INVALID_COUNTRY 등 [9]
  }
}
```

### 고급 기능
**전화번호 형식 변환**
```javascript
const number = parsePhoneNumber('+821023456789');
console.log(number.formatNational());  // '010-2345-6789' [9]
console.log(number.formatInternational()); // '+82 10-2345-6789' [9]
```

**국가별 정보 추출**
```javascript
import { geocoder, timezone } from 'libphonenumber-js';

console.log(geocoder.descriptionForNumber(number, 'ko')); // '대한민국' [8]
console.log(timezone.timeZonesForNumber(number)); // ['Asia/Seoul'] [8]
```

### 주의사항
- `isPossible()`은 번호 길이만 검증하며, `isValid()`는 실제 번호 존재 여부까지 확인[7][9]
- 국가 코드 미지정 시 자동 감지가 실패할 수 있으므로 `defaultCountry` 파라미터 권장[3][9]
- 모바일 번호 전용 검증에는 `mobile` 버전 사용 가능[3][9]

이 라이브러리는 140개 이상 국가의 전화번호 형식을 지원하며, 정규식 기반 검증보다 더 정확한 결과를 제공합니다[7][9].

Citations:

- 1: https://recordsoflife.tistory.com/525
- 2: https://string.tistory.com/102
- 3: https://blog.naver.com/chandong83/221895488234
- 4: https://taedonn.tistory.com/17
- 5: https://honeystorage.tistory.com/132
- 6: https://blog.naver.com/on21life/221816166148
- 7: https://velog.io/@gktjdfhr2/%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8-%EC%9C%A0%ED%9A%A8%EC%84%B1%EC%9D%84-%EA%B2%80%EC%A6%9D%ED%95%B4%EB%B3%B4%EC%9E%90libphonenumber
- 8: https://minwook-shin.github.io/python-parsing-phone-numbers-using-phonenumbers/
- 9: https://www.npmjs.com/package/libphonenumber-js
- 10: https://myhoneyhouse.tistory.com/131

---



</details>

**전화번호 정규식 개선 및 국제번호 지원 고려**

- 현재 사용 중인 정규식은 기본적인 국가 코드, 지역번호 및 구분자 지원은 잘 처리하지만, 일부 국제 전화번호(예: 국가별 자릿수가 다른 번호)의 경우 충분한 커버리지를 제공하지 못할 수 있습니다.
- 제시된 대체 정규식은 범위를 확장하여 다양한 국가별 형식에도 대응할 수 있도록 한 예시입니다.
- 또한, `libphonenumber-js` 라이브러리를 사용하면 국가 코드 기반의 정밀 검증, 번호 형식 변환, 오류 처리 등 보다 포괄적인 기능을 제공하므로 국제적인 사용 환경에서는 라이브러리 도입을 적극 고려해 보시길 권장합니다.

```diff
-export const REGEX_PHONE_NUMBER =
-  /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/;
+export const REGEX_PHONE_NUMBER =
+  /^(\+\d{1,4})?\s?(\(\d{1,4}\)|\d{1,4})[\s.-]?(\d{1,4})[\s.-]?(\d{1,9})$/;
```

예시: `libphonenumber-js` 사용
```javascript
import parsePhoneNumber from 'libphonenumber-js';

const phoneNumber = parsePhoneNumber('+821023456789', 'KR');
if (phoneNumber && phoneNumber.isValid()) {
  console.log('전화번호가 유효합니다.');
}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T14:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,17 +1,21 @@
 import { RegisterFormSchema } from '../hooks/use-register-form';
 
+import { VerifyResponse } from './get-token';
+
 import { api } from '@/features/core';
 
-interface RegisterResponse {
-  accessToken: string;
-}
+export const register = async (
+  requestBody: RegisterFormSchema & VerifyResponse,
+) => {
+  const matched = requestBody.phoneNumber.match(
+    /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/,
+  )!;
path: src/features/auth/services/use-register.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**전화번호 정규식 처리에 안전성 문제가 있습니다.**

전화번호 파싱에 정규식을 사용한 것은 좋은 접근이지만, 정규식 매치 실패 시 발생할 수 있는 오류 처리가 없습니다. 현재 코드는 매치가 항상 성공할 것이라고 가정하고 있으며(`!` 연산자 사용), 이는 런타임 오류를 발생시킬 수 있습니다.



아래와 같이 코드를 수정하여 전화번호 형식이 올바르지 않은 경우를 처리하는 것을 권장합니다:

```diff
-  const matched = requestBody.phoneNumber.match(
-    /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/,
-  )!;
+  const matched = requestBody.phoneNumber.match(
+    /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/,
+  );
+
+  if (!matched) {
+    throw new Error('유효하지 않은 전화번호 형식입니다.');
+  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const matched = requestBody.phoneNumber.match(
    /^(\+\d{1,2})?\s?\(?(\d{3})\)?[\s.-]?(\d{3,4})[\s.-]?(\d{4})$/,
  );
  
  if (!matched) {
    throw new Error('유효하지 않은 전화번호 형식입니다.');
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T14:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,16 @@
+import { api } from '@/features/core';
+
+export interface VerifyResponse {
+  verificationJwtToken: string;
+}
+
+interface VerifyRequestBody {
+  subject: string;
+  code: string;
+  hint: 'email';
+}
+
+export const getJWTToken = async (requestBody: VerifyRequestBody) => {
+  const res = await api.post<VerifyResponse>('/verify', requestBody);
+  return res.data;
+};
path: src/features/auth/services/get-token.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**오류 처리 기능 추가 필요**

API 요청 시 발생할 수 있는 오류를 처리하는 로직이 없습니다. try-catch 블록을 추가하여 오류를 적절히 처리하는 것이 좋겠습니다.


다음과 같이 오류 처리 로직을 추가하는 것을 제안합니다:

```diff
export const getJWTToken = async (requestBody: VerifyRequestBody) => {
+  try {
    const res = await api.post<VerifyResponse>('/verify', requestBody);
    return res.data;
+  } catch (error) {
+    console.error('토큰 획득 실패:', error);
+    throw error;
+  }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export const getJWTToken = async (requestBody: VerifyRequestBody) => {
  try {
    const res = await api.post<VerifyResponse>('/verify', requestBody);
    return res.data;
  } catch (error) {
    console.error('토큰 획득 실패:', error);
    throw error;
  }
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T14:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+import { FormProvider } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { AuthorizeForm } from '../components/authorize-ziggle-form';
+import { useAuthorizeForm } from '../hooks/use-consent-form';
+
+export function AuthorizeFrame() {
+  const { form, onSubmit } = useAuthorizeForm();
+  const { t } = useTranslation();
+
+  return (
+    <div className="flex min-h-screen items-center justify-center">
+      <div className="flex w-full max-w-[400px] flex-col px-5 py-8">
+        <div className="text-title-1 whitespace-pre-wrap">
+          {t('authorize.title')}
+        </div>
+        <div className="h-8" />
+        <div className="text-body-2 text-neutral-800">
+          {t('authorize.description')}
+        </div>
+        <div className="h-1" />
+        <FormProvider {...form}>
+          <form>
+            <AuthorizeForm onSubmit={onSubmit} />
+          </form>
+        </FormProvider>
+      </div>
+    </div>
+  );
+}
path: src/features/oauth/frames/authorize-ziggle-frame.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**form 요소에 onSubmit 속성 추가 필요**

현재 form 요소에 onSubmit 속성이 없어, 엔터 키를 눌러 폼을 제출할 수 없습니다. 또한 기본 제출 이벤트를 방지하는 코드도 없어 페이지 새로고침이 발생할 수 있습니다.


다음과 같이 코드를 수정하는 것을 제안합니다:

```diff
<FormProvider {...form}>
-  <form>
+  <form onSubmit={onSubmit}>
    <AuthorizeForm onSubmit={onSubmit} />
  </form>
</FormProvider>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export function AuthorizeFrame() {
  const { form, onSubmit } = useAuthorizeForm();
  const { t } = useTranslation();

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="flex w-full max-w-[400px] flex-col px-5 py-8">
        <div className="text-title-1 whitespace-pre-wrap">
          {t('authorize.title')}
        </div>
        <div className="h-8" />
        <div className="text-body-2 text-neutral-800">
          {t('authorize.description')}
        </div>
        <div className="h-1" />
        <FormProvider {...form}>
          <form onSubmit={onSubmit}>
            <AuthorizeForm onSubmit={onSubmit} />
          </form>
        </FormProvider>
      </div>
    </div>
  );
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T14:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { ConsentFormSchema } from '../hooks/use-consent-form';
+
+import { Button, Checkbox } from '@/features/core';
+
+const ziggleRequiredScopes = ['email', 'name'] as const;
+const ziggleOptionalScopes = ['student_id', 'phone_number'] as const;
+
+export function AuthorizeForm({
+  onSubmit,
+}: {
+  onSubmit: (e?: React.SyntheticEvent) => void;
+}) {
+  const { register, setValue, watch, getValues } =
+    useFormContext<ConsentFormSchema>();
+  const { t } = useTranslation();
+  const [allAgree, setAllAgree] = useState(false);
+
+  setValue('client_id', 'ziggle2023');
+  ziggleRequiredScopes.forEach((scope) => setValue(scope, true));
path: src/features/oauth/components/authorize-ziggle-form.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**클라이언트 ID 하드코딩에 대한 고려가 필요합니다.**

클라이언트 ID 'ziggle2023'가 코드에 직접 하드코딩되어 있습니다. 이러한 값은 환경 변수나 설정 파일에서 가져오는 것이 더 유지보수하기 좋습니다.

```diff
- setValue('client_id', 'ziggle2023');
+ setValue('client_id', import.meta.env.VITE_CLIENT_ID || 'ziggle2023');
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  setValue('client_id', import.meta.env.VITE_CLIENT_ID || 'ziggle2023');
  ziggleRequiredScopes.forEach((scope) => setValue(scope, true));
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T14:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { ConsentFormSchema } from '../hooks/use-consent-form';
+
+import { Button, Checkbox } from '@/features/core';
+
+const ziggleRequiredScopes = ['email', 'name'] as const;
+const ziggleOptionalScopes = ['student_id', 'phone_number'] as const;
+
+export function AuthorizeForm({
+  onSubmit,
+}: {
+  onSubmit: (e?: React.SyntheticEvent) => void;
+}) {
+  const { register, setValue, watch, getValues } =
+    useFormContext<ConsentFormSchema>();
+  const { t } = useTranslation();
+  const [allAgree, setAllAgree] = useState(false);
+
+  setValue('client_id', 'ziggle2023');
+  ziggleRequiredScopes.forEach((scope) => setValue(scope, true));
+
+  useEffect(() => {
+    setAllAgree(ziggleOptionalScopes.every((scope) => watch(scope)));
+    console.log('allAgree', allAgree); // TESTING
+  }, [watch, allAgree]);
+
+  return (
+    <div className="flex flex-col">
+      <Checkbox
+        defaultChecked={allAgree}
+        onChange={() => {
+          ziggleOptionalScopes.forEach((scope) => setValue(scope, !allAgree));
+          setAllAgree((prev) => !prev);
+          console.log(getValues()); // TESTING
+        }}
+      >
+        <div className="font-bold">{t('authorize.checkboxes.all_agree')}</div>
+      </Checkbox>
+      <div className="h-2.5" />
+      <div className="rounded-lg border border-neutral-200 px-5 py-4">
+        <div className="text-body-2 mb-1 text-neutral-800">
+          {t('authorize.labels.required')}
+        </div>
+        <div className="flex flex-col gap-1 pl-1">
+          {ziggleRequiredScopes.map((scope) => (
+            <Checkbox key={scope} disabled {...register(scope)}>
+              {t(`authorize.checkboxes.${scope}`)}
+            </Checkbox>
+          ))}
+        </div>
+        <div className="h-2.5" />
+        <div className="text-body-2 mb-1 text-neutral-800">
+          {t('authorize.labels.optional')}
+        </div>
+        <div className="flex flex-col gap-1 pl-1">
+          {ziggleOptionalScopes.map((scope) => (
+            <Checkbox key={scope} {...register(scope)}>
+              {t(`authorize.checkboxes.${scope}`)}
+            </Checkbox>
+          ))}
+        </div>
+      </div>
+      <div className="h-10" />
+      <div className="flex gap-2.5">
+        <Button variant="secondary">{t('authorize.buttons.cancel')}</Button>
+        <Button variant="primary" onClick={onSubmit}>
path: src/features/oauth/components/authorize-ziggle-form.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**취소 버튼에 기능이 없습니다.**

취소 버튼에 클릭 핸들러가 구현되어 있지 않습니다. 사용자가 취소 버튼을 클릭했을 때 적절한 동작을 추가해야 합니다.

```diff
- <Button variant="secondary">{t('authorize.buttons.cancel')}</Button>
+ <Button 
+   variant="secondary" 
+   onClick={() => {/* 취소 로직 구현 - 예: 이전 페이지로 이동 */}}
+ >
+   {t('authorize.buttons.cancel')}
+ </Button>
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        <Button 
          variant="secondary" 
          onClick={() => {/* 취소 로직 구현 - 예: 이전 페이지로 이동 */}}
        >
          {t('authorize.buttons.cancel')}
        </Button>
        <Button variant="primary" onClick={onSubmit}>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T14:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { ConsentFormSchema } from '../hooks/use-consent-form';
+
+import { Button, Checkbox } from '@/features/core';
+
+const ziggleRequiredScopes = ['email', 'name'] as const;
+const ziggleOptionalScopes = ['student_id', 'phone_number'] as const;
+
+export function AuthorizeForm({
+  onSubmit,
+}: {
+  onSubmit: (e?: React.SyntheticEvent) => void;
+}) {
+  const { register, setValue, watch, getValues } =
+    useFormContext<ConsentFormSchema>();
+  const { t } = useTranslation();
+  const [allAgree, setAllAgree] = useState(false);
+
+  setValue('client_id', 'ziggle2023');
path: src/features/oauth/components/authorize-ziggle-form.tsx
user: login: 2paperstar
body: 원래는 search param에서 가져오는게 맞는데, 고려 안 한 PR인가요?
created_at: 2025-04-08T01:28:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,57 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { TFunction } from 'i18next';
+import { useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { z, ZodBoolean } from 'zod';
+
+import { consent } from '../services/use-consent';
+
+export const ClientScopeList = [
+  'profile',
+  'name',
+  'email',
+  'phone_number',
+  'student_id',
+] as const;
+export type ClientScopeType = (typeof ClientScopeList)[number];
+
+export const TokenScopeList = ['openid', 'offline_access'] as const;
+export type TokenScopeType = (typeof TokenScopeList)[number];
+
+export const ScopeList = [...ClientScopeList, ...TokenScopeList];
+export type ScopeType = (typeof ScopeList)[number];
+
+export const createSchema = (t: TFunction) =>
+  z.object({
path: src/features/oauth/hooks/use-consent-form.ts
user: login: 2paperstar
body: zod에 record 타입이라고 있어요
created_at: 2025-04-08T01:51:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+
+import { ConsentFormSchema } from '../hooks/use-consent-form';
+
+import { Button, Checkbox } from '@/features/core';
+
+const ziggleRequiredScopes = ['email', 'name'] as const;
+const ziggleOptionalScopes = ['student_id', 'phone_number'] as const;
+
+export function AuthorizeForm({
+  onSubmit,
+}: {
+  onSubmit: (e?: React.SyntheticEvent) => void;
+}) {
+  const { register, setValue, watch, getValues } =
+    useFormContext<ConsentFormSchema>();
+  const { t } = useTranslation();
+  const [allAgree, setAllAgree] = useState(false);
+
+  setValue('client_id', 'ziggle2023');
path: src/features/oauth/components/authorize-ziggle-form.tsx
user: login: 2paperstar
body: 그리고 setValue가 매 렌더마다 호출 되기 때문에 효율적인 코드가 아닌 것 같습니다
created_at: 2025-04-08T01:52:49Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-07T14:08:09Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough
이번 변경 사항은 사용자 등록 및 OAuth 인증 프로세스의 기능 개선에 중점을 두고 있습니다.  
- **사용자 등록**: `RegisterForm` 컴포넌트에 검증 코드 전송 및 확인을 위한 비동기 함수 Props가 추가되고, 관련 상태 관리와 조건부 렌더링이 도입되었습니다.  
- **유효성 검사 및 스키마**: 이메일, 비밀번호, 학번, 전화번호 등의 유효성 검사 로직이 강화되고, 새로운 필드(코드)가 포함되었습니다.  
- **OAuth 인증**: OAuth 관련 동의 및 인증 컴포넌트와 커스텀 훅, 서비스 함수가 추가되었습니다.  
- **국제화**: 등록, 로그인, 인증 관련 번역 파일들이 재구성되었습니다.

## Changes

| 파일들 | 변경 내용 |
| --- | --- |
| `src/features/auth/components/register-form.tsx` | `RegisterForm` 컴포넌트에 `onSendVerificationCode`, `onVerifyCode` Props 추가 및 새로운 상태 변수(`isCodeSent`, `isVerifying`, `isCodeValid`) 도입, 검증 코드 입력 필드와 에러 처리, 인터페이스와 레이아웃 개선 |
| `src/features/auth/frames/register-done-frame.tsx` | 등록 완료 메시지를 보여주고 로그인 페이지로 연결하는 새로운 `RegisterDoneFrame` 컴포넌트 추가 |
| `src/features/auth/hooks/use-register-form.ts` | 스키마 유효성 검사 강화(이메일 정규식, 비밀번호 최소 길이, 학번 및 전화번호 검증) 및 새로운 함수(`onSendVerificationCode`, `onVerifyCode`, `onRegister`) 추가, 반환값 업데이트 |
| `src/features/oauth/components/authorize-ziggle-form.tsx`,<br>`src/features/oauth/frames/authorize-ziggle-frame.tsx`,<br>`src/features/oauth/hooks/use-consent-form.ts` | OAuth 관련 동의 및 인증을 위한 새로운 컴포넌트(`AuthorizeForm`, `AuthorizeFrame`)와 커스텀 훅(`useAuthorizeForm`) 추가, 필요한 스코프와 체크박스 관리 도입 |
| `src/features/oauth/index.ts`,<br>`src/features/oauth/services/use-consent.ts` | OAuth 동의 관련 API 호출 기능과 타입 정의, 새로운 export 문 추가 |
| `src/locales/en/translation.json`,<br>`src/locales/ko/translation.json` | 등록, 로그인, OAuth 인증 관련 번역 문자열 구조 개편 및 새로운 섹션(`register_done`, `authorize`)과 키 추가 |
| `src/features/oauth/frames/authorize-frame.tsx` | 번역 키 네이밍 수정: `all-agree` → `all_agree`, `studentId` → `student_id`, `phoneNumber` → `phone_number` |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant 사용자 as User
    participant 양식 as RegisterForm
    participant 전송함수 as onSendVerificationCode
    participant 검증함수 as onVerifyCode
    participant 등록함수 as onRegister

    사용자->>양식: 이메일 및 비밀번호 입력
    양식->>전송함수: 검증 코드 전송 요청
    전송함수-->>양식: 전송 결과 반환 (코드 전송)
    Note over 양식: isCodeSent 업데이트

    사용자->>양식: 검증 코드 입력 후 제출
    양식->>검증함수: 코드 검증 요청
    검증함수-->>양식: 검증 결과 반환 (성공/실패)
    Note over 양식: isCodeValid 업데이트

    사용자->>양식: 최종 등록 제출
    양식->>등록함수: 등록 요청
    등록함수-->>양식: 등록 결과 반환 (성공/에러)
```

```mermaid
sequenceDiagram
    participant 사용자 as User
    participant 프레임 as AuthorizeFrame
    participant 양식 as AuthorizeForm
    participant 훅 as useAuthorizeForm
    participant 서비스 as ConsentService

    사용자->>프레임: OAuth 인증 페이지 방문
    프레임->>훅: 양식 상태 초기화 및 유효성 검사 준비
    프레임->>양식: 체크박스 및 동의 옵션 렌더링
    사용자->>양식: 필요한 항목 선택 및 제출
    양식->>서비스: 동의 API 호출 (POST /oauth/consent)
    서비스-->>양식: API 응답 (성공/실패)
```

## Possibly related PRs
- **gsainfoteam/idp-fe#13**: `RegisterForm`의 props 변경 및 검증 코드 전송 관련 수정 사항과 직접 연결됩니다.
- **gsainfoteam/idp-fe#6**: `RegisterForm` 컴포넌트의 에러 처리 및 필수 필드 업데이트와 관련된 코드 수정이 이루어졌습니다.
- **gsainfoteam/idp-fe#2**: 등록 폼 컴포넌트 수정에 있어 새로운 속성 도입 및 상태 관리 변경이 유사하게 적용되었습니다.

## Suggested reviewers
- 2paperstar

## Poem
> 귀여운 토끼가 코드를 뛰놀며,  
> 등록과 인증의 길을 밝혀요.  
> 검증의 코드가 반짝반짝 빛나고,  
> 동의와 함께 흐르는 줄기처럼!  
> 새로운 기능, 깔끔한 구조에  
> 토실토실 발자국 남기며 춤추네 🐰✨

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=gsainfoteam/idp-fe&utm_content=23):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: 2paperstar
created_at: 2025-04-08T00:55:36Z
body: @rhseung 요거 머지 한번 해주세요!
author_association: MEMBER
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-04-08T01:21:47Z
body: ## Deploying idp-fe with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>3650f7c</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://9d02a2ab.idp-fe-49s.pages.dev'>https://9d02a2ab.idp-fe-49s.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://idf-10.idp-fe-49s.pages.dev'>https://idf-10.idp-fe-49s.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/c9a1723f7c76afd75eb58d2241287fdf/pages/view/idp-fe/9d02a2ab-8c51-4d31-9b75-b5ff2b5ffcde)

author_association: NONE