Repository: vivi-trip/vivitrip
Pull request #257: [chore/#256] 테스트 유저 로그인 페이지 생성
Author: login: JuhyeokC
Created: 2025-04-02T06:31:28Z | Merged: 2025-04-02T07:33:01Z

# Resolved: #256

## 🔨 작업내역

- 테스트 유저 로그인 라우터 생성

## 🔊 전달사항

- 전달내용

## 📌 이슈사항

- 이슈내용

## 👩‍🔧 오류내역

- 오류내용

## 🎨 예시이미지

- 예시이미지 첨부


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

- **새로운 기능**
  - 사용자 이메일과 비밀번호를 이용한 로그인 페이지가 추가되었습니다.
  - 테스트 사용자 선택 드롭다운 기능을 통해 미리 설정된 계정으로 자동 로그인할 수 있습니다.
  - 인증된 사용자는 로그인 페이지를 건너뛰고 메인 페이지로 자동 리디렉션됩니다.
  - 테스트 사용자 목록이 업데이트되어 새로운 사용자 계정이 추가되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/constants/testUsers.ts b/src/constants/testUsers.ts
index 1c241307..e62c198a 100644
--- a/src/constants/testUsers.ts
+++ b/src/constants/testUsers.ts
@@ -4,60 +4,35 @@ interface TestUserProps extends UserNickName, UserEmail, UserPassword {}
 
 const TEST_USERS: TestUserProps[] = [
   {
-    nickname: "example",
-    email: "example@email.com",
-    password: "password",
-  },
-  {
-    nickname: "codeit",
-    email: "codeit@code.it",
-    password: "qwerqwer",
+    nickname: "테스트계정1",
+    email: "lee@test.com",
+    password: "!ASDF12345",
   },
   {
-    nickname: "test",
-    email: "user@code.it",
-    password: "user@code.it",
+    nickname: "테스트계정2",
+    email: "123123123@naver.com",
+    password: "123123123Z!",
   },
   {
-    nickname: "호성효성",
+    nickname: "테스트계정3",
     email: "hosung@hyo.sung",
     password: "hosung@hyo.sung",
   },
   {
-    nickname: "미미미영",
+    nickname: "테스트계정4",
     email: "mimi@mi.yong",
     password: "mimi@mi.yong",
   },
   {
-    nickname: "하이유리",
+    nickname: "테스트계정5",
     email: "hi@yu.ri",
     password: "hi@yu.ri",
   },
   {
-    nickname: "LEE",
-    email: "lee@test.com",
-    password: "!ASDF12345",
-  },
-  {
-    nickname: "초이주혁",
+    nickname: "테스트계정6",
     email: "choi@ju.hyeok",
     password: "QWER123$",
   },
-  {
-    nickname: "jhchoi.net",
-    email: "test@jhchoi.net",
-    password: "!@#$QWER",
-  },
-  {
-    nickname: "choi.net",
-    email: "juhyeok@choi.net",
-    password: "!@#$QWER",
-  },
-  {
-    nickname: "체험테스트",
-    email: "123123123@naver.com",
-    password: "123123123Z!",
-  },
 ];
 
 export default TEST_USERS;
diff --git a/src/pages/sign-in/admin.tsx b/src/pages/sign-in/admin.tsx
new file mode 100644
index 00000000..84067514
--- /dev/null
+++ b/src/pages/sign-in/admin.tsx
@@ -0,0 +1,77 @@
+import Dropdown from "@/src/components/Dropdown";
+import Form from "@/src/components/Form";
+import Logo from "@/src/components/Logo";
+import OauthSign from "@/src/components/OauthSign";
+import SignOptionSection from "@/src/components/SignOptionSection";
+import PATH_NAMES from "@/src/constants/pathname";
+import TEST_USERS from "@/src/constants/testUsers";
+import SignPageWrap from "@/src/containers/SignPageWrap";
+import { useSignIn } from "@/src/queries/auth";
+import useUserStore from "@/src/stores/useUserStore";
+import type { SignInProps } from "@/src/types/user";
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+
+const SignInRoute = () => {
+  const router = useRouter();
+  const { userData } = useUserStore();
+  const { mutate: signinFn, isPending: isPendingSignin } = useSignIn();
+
+  const handleSignIn = async (data: SignInProps) => {
+    signinFn(data);
+  };
+
+  useEffect(() => {
+    if (userData) router.replace(PATH_NAMES.Root);
+  }, [userData, router]);
+
+  return (
+    <SignPageWrap>
+      <Form onSubmit={handleSignIn} className="gap-16">
+        <Form.Title className="flex-col gap-16">
+          <Logo size="lg" />
+        </Form.Title>
+
+        <Form.Field variant="email">
+          <Form.Label>이메일</Form.Label>
+          <Form.Input placeholder="이메일을 입력해 주세요" />
+        </Form.Field>
+
+        <Form.Field variant="password">
+          <Form.Label>비밀번호</Form.Label>
+          <Form.Input placeholder="8자 이상 입력해 주세요" />
+        </Form.Field>
+
+        <Form.Field variant="authPage" className="mt-20">
+          <Form.SubmitButton disabled={isPendingSignin}>
+            로그인
+          </Form.SubmitButton>
+        </Form.Field>
+      </Form>
+
+      <SignOptionSection action="in" />
+
+      <OauthSign action="in" />
+
+      <div className="flex justify-end">
+        <Dropdown>
+          <Dropdown.Trigger>테스트 유저 로그인</Dropdown.Trigger>
+          <Dropdown.Menu className="!bottom-full !left-auto !right-0 !top-auto !mb-4 !mt-0 flex flex-col gap-4 p-4">
+            {TEST_USERS.map((item) => {
+              return (
+                <Dropdown.Item
+                  key={`test_user_item_${item.email}`}
+                  className="!rounded border border-brand-300 bg-brand-200 px-24 py-2 text-brand-500"
+                  onClick={() => signinFn(item)}>
+                  {item.nickname}
+                </Dropdown.Item>
+              );
+            })}
+          </Dropdown.Menu>
+        </Dropdown>
+      </div>
+    </SignPageWrap>
+  );
+};
+
+export default SignInRoute;


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

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

<details>
<summary>src/pages/sign-in/admin.tsx (3)</summary><blockquote>

`45-49`: **로딩 상태 표시가 필요합니다.**

폼 제출 버튼이 로딩 중에 비활성화되는 것은 좋지만, 사용자에게 로딩 중임을 시각적으로 표시하는 것이 더 좋습니다.

```diff
<Form.Field variant="authPage" className="mt-20">
- <Form.SubmitButton disabled={isPendingSignin}>
-   로그인
+ <Form.SubmitButton disabled={isPendingSignin}>
+   {isPendingSignin ? '로그인 중...' : '로그인'}
  </Form.SubmitButton>
</Form.Field>
```

---

`59-70`: **테스트 유저 항목 클릭 시 오류 처리가 필요합니다.**

테스트 유저 로그인 시에도 로그인 실패 가능성이 있으므로 오류 처리 로직이 필요합니다. 또한 로딩 상태도 표시해야 합니다.

```diff
<Dropdown.Item
  key={`test_user_item_${item.email}`}
  className="!rounded border border-brand-300 bg-brand-200 px-24 py-2 text-brand-500"
- onClick={() => signinFn(item)}>
+ onClick={() => signinFn(item, {
+   onError: (error) => {
+     console.error(`${item.nickname} 로그인 오류:`, error);
+     // 오류 메시지 표시 로직 추가
+   }
+ })}>
- {item.nickname}
+ {isPendingSignin && item.email === currentAttemptEmail ? `${item.nickname} 로그인 중...` : item.nickname}
</Dropdown.Item>
```

이를 구현하려면 컴포넌트 상단에 현재 시도 중인 이메일을 추적하는 상태가 필요합니다:

```javascript
const [currentAttemptEmail, setCurrentAttemptEmail] = useState<string | null>(null);

// signinFn 호출 전에 현재 시도 중인 이메일 설정
const handleTestUserSignIn = (user: typeof TEST_USERS[0]) => {
  setCurrentAttemptEmail(user.email);
  signinFn(user, {
    onSettled: () => setCurrentAttemptEmail(null),
    // 기타 콜백...
  });
};
```

---

`58-59`: **드롭다운 접근성 개선이 필요합니다.**

테스트 유저 선택 드롭다운에 접근성 속성이 부족합니다. 스크린 리더 사용자를 위한 적절한 ARIA 속성을 추가하세요.

```diff
- <Dropdown.Trigger>테스트 유저 로그인</Dropdown.Trigger>
+ <Dropdown.Trigger aria-label="테스트 유저 선택하기" aria-haspopup="true">테스트 유저 로그인</Dropdown.Trigger>
- <Dropdown.Menu className="!bottom-full !left-auto !right-0 !top-auto !mb-4 !mt-0 flex flex-col gap-4 p-4">
+ <Dropdown.Menu className="!bottom-full !left-auto !right-0 !top-auto !mb-4 !mt-0 flex flex-col gap-4 p-4" aria-label="테스트 유저 목록">
```

</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 8e9f8e1107b8a9bbe02a3c49e0cd3350b6f10244 and 1fb3064aaefb8e2338ac7068dff24c3697a63509.

</details>

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

* `src/pages/sign-in/admin.tsx` (1 hunks)

</details>

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

<details>
<summary>src/pages/sign-in/admin.tsx (5)</summary>

`1-14`: **임포트 구조가 잘 정리되어 있습니다.**

필요한 컴포넌트와 훅을 체계적으로 임포트했습니다. 경로 별칭(@/src/)을 일관되게 사용하여 가독성을 높였습니다.

---

`15-19`: **상태 관리 및 훅 사용이 적절합니다.**

필요한 훅들을 잘 활용하여 라우팅과 인증 상태를 관리하고 있습니다. 비구조화 할당을 통해 필요한 함수와 상태를 명확하게 추출했습니다.

---

`24-27`: **인증 사용자 리디렉션 로직이 적절합니다.**

이미 인증된 사용자를 루트 경로로 리디렉션하는 로직이 잘 구현되어 있습니다.

---

`77-78`: **컴포넌트 내보내기가 적절합니다.**

컴포넌트를 기본 내보내기로 올바르게 설정했습니다.

---

`6-7`: <details>
<summary>❓ Verification inconclusive</summary>

**상수 임포트 확인이 필요합니다.**

PATH_NAMES과 TEST_USERS 상수가 임포트되었습니다. 특히 TEST_USERS의 형식이 SignInProps와 호환되는지 확인하세요.

---

🏁 Script executed:

```shell
#!/bin/bash
# TEST_USERS 상수와 SignInProps 타입 구조 확인
cat $(fd -g "testUsers.{ts,tsx,js,jsx}" -I)
cat $(fd -g "*user.{ts,tsx}" -I | grep -w "SignInProps")
```

Length of output: 1328

---



</details>

**상수 임포트 및 타입 호환성 확인**

- `TEST_USERS` 상수는 `src/constants/testUsers.ts`에서 정의되며, 각 사용자 객체는 `nickname`, `email`, `password` 속성을 포함합니다.
- 현재까지 `SignInProps`의 정의를 확인하지 못했으므로, 두 타입 간 필드와 데이터 타입이 일치하는지 추가 검증이 필요합니다.
- `SignInProps`의 요구 사항에 따라 데이터가 적절한지 확인 후, 필요 시 타입 수정이나 보완 부탁드립니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T06:35:17Z
user: login: hayuri1990
body: 테스트 유저 계정으로 로그인 잘 되고,
로그인 상태에서 다시 `http://localhost:3000/sign-in/admin` 접근 시 메인 페이지로도 잘 이동합니다.
고생하셨습니다👍

<br/>

<img src="https://github.com/user-attachments/assets/231dfad1-f0d1-4d8c-9a75-8fc52c7b7f20" width="70%"/>

다만, 사용하지 않는 테스트 유저 계정이 많아
추후 테스트 유저 계정 및 닉네임 정리가 필요할 것 같아요.

### 사용하지 않은 테스트 유저 계정
- example
- codeit
- test
- jhchoi.net
- choi.net

### 닉네임 정리 예시
- test > 테스트계정1
- LEE > 테스트계정2
- 체험테스트 > 테스트계정3
- 저희 닉네임 계정은 그대로 유지

혹시 모를 상황을 대비해 테스트 계정을 상단에 배치하고,
총 테스트 유저 계정은 6-7개 정도만 남겨 두어도 좋을 것 같습니다.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T07:05:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

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

<details>
<summary>src/constants/testUsers.ts (4)</summary><blockquote>

`17-20`: **보안 관행 개선 필요**

테스트 계정 이름은 일관성 있게 변경되었지만, 이메일과 비밀번호가 동일한 것은 보안상 좋지 않은 관행입니다. 테스트 환경이라도 실제 환경과 유사하게 구성하는 것이 좋습니다.

```diff
  {
    nickname: "테스트계정3",
    email: "hosung@hyo.sung",
-   password: "hosung@hyo.sung",
+   password: "Hosung123!",
  },
```

---

`22-25`: **이메일과 동일한 비밀번호 사용 주의**

이 테스트 계정도 이메일과 비밀번호가 동일합니다. 보안 관행을 지키기 위해 다른 비밀번호를 사용하는 것이 좋습니다.

```diff
  {
    nickname: "테스트계정4",
    email: "mimi@mi.yong",
-   password: "mimi@mi.yong",
+   password: "Mimi2023!",
  },
```

---

`27-30`: **이메일과 동일한 비밀번호 수정 필요**

테스트 계정의 이름은 일관되게 변경되었으나, 여기서도 이메일과 비밀번호가 동일합니다. 보안 모범 사례를 따르도록 수정이 필요합니다.

```diff
  {
    nickname: "테스트계정5",
    email: "hi@yu.ri",
-   password: "hi@yu.ri",
+   password: "Yuri2023$",
  },
```

---

`5-36`: **전체적인 테스트 계정 관리에 관한 제안**

테스트 계정들이 일관된 이름 체계로 변경되어 가독성이 향상되었습니다. 하지만 몇 가지 개선사항이 있습니다:

1. 일부 계정에서 이메일과 비밀번호가 동일한 문제가 있습니다
2. 테스트 목적별로 계정을 그룹화하거나 주석을 추가하여 각 계정의 용도를 명확히 하는 것이 좋습니다
3. 모든 테스트 계정에 강력한 비밀번호 패턴을 적용하는 것이 좋습니다

테스트 환경이라도 실제 환경과 유사하게 구성하면 테스트의 신뢰성이 높아집니다.

</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 1fb3064aaefb8e2338ac7068dff24c3697a63509 and c589e1a32bc16d03feb2fe93e000c64369b9c232.

</details>

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

* `src/constants/testUsers.ts` (1 hunks)

</details>

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

<details>
<summary>src/constants/testUsers.ts (3)</summary>

`7-9`: **테스트 사용자 정보 업데이트 확인**

테스트 계정 정보가 명확하게 정리되었습니다. 계정명이 "테스트계정1"로 변경되어 더욱 직관적이고, 비밀번호가 특수문자와 대문자를 포함하여 보안성이 향상되었습니다.

---

`12-14`: **적절한 테스트 계정 포맷**

한국 사용자 환경에 맞게 naver.com 도메인을 사용한 이메일로 변경된 점이 좋습니다. 비밀번호도 특수문자를 포함하고 있어 적절합니다.

---

`32-35`: **일관된 테스트 계정 명명 체계**

테스트 계정의 이름이 "테스트계정6"으로 변경되어 다른 계정들과 일관성을 유지하고 있습니다. 이 계정은 이미 이메일과 다른 비밀번호를 사용하고 있어 적절합니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T07:17:37Z
user: login: hayuri1990
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T07:17:53Z

Review comments:
diff_hunk: @@ -0,0 +1,77 @@
+import Dropdown from "@/src/components/Dropdown";
+import Form from "@/src/components/Form";
+import Logo from "@/src/components/Logo";
+import OauthSign from "@/src/components/OauthSign";
+import SignOptionSection from "@/src/components/SignOptionSection";
+import PATH_NAMES from "@/src/constants/pathname";
+import TEST_USERS from "@/src/constants/testUsers";
+import SignPageWrap from "@/src/containers/SignPageWrap";
+import { useSignIn } from "@/src/queries/auth";
+import useUserStore from "@/src/stores/useUserStore";
+import type { SignInProps } from "@/src/types/user";
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+
+const SignInRoute = () => {
+  const router = useRouter();
+  const { userData } = useUserStore();
+  const { mutate: signinFn, isPending: isPendingSignin } = useSignIn();
+
+  const handleSignIn = async (data: SignInProps) => {
+    signinFn(data);
+  };
+
+  useEffect(() => {
+    if (userData) router.replace(PATH_NAMES.Root);
+  }, [userData, router]);
+
+  return (
+    <SignPageWrap>
+      <Form onSubmit={handleSignIn} className="gap-16">
+        <Form.Title className="flex-col gap-16">
+          <Logo size="lg" />
+        </Form.Title>
+
+        <Form.Field variant="email">
+          <Form.Label>이메일</Form.Label>
+          <Form.Input placeholder="이메일을 입력해 주세요" />
+        </Form.Field>
+
+        <Form.Field variant="password">
+          <Form.Label>비밀번호</Form.Label>
+          <Form.Input placeholder="8자 이상 입력해 주세요" />
+        </Form.Field>
path: src/pages/sign-in/admin.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**폼 입력 필드에 유효성 검사가 필요합니다.**

이메일과 비밀번호 필드에 유효성 검사가 없습니다. 특히 비밀번호는 8자 이상이라는 안내가 있지만 실제 검증 로직은 없습니다. 폼 제출 전에 필수 유효성 검증을 추가하세요.

```diff
<Form.Field variant="email">
  <Form.Label>이메일</Form.Label>
- <Form.Input placeholder="이메일을 입력해 주세요" />
+ <Form.Input 
+   placeholder="이메일을 입력해 주세요" 
+   required 
+   pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
+ />
</Form.Field>

<Form.Field variant="password">
  <Form.Label>비밀번호</Form.Label>
- <Form.Input placeholder="8자 이상 입력해 주세요" />
+ <Form.Input 
+   placeholder="8자 이상 입력해 주세요" 
+   required 
+   minLength={8}
+ />
</Form.Field>
```


<!-- 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
        <Form.Field variant="email">
          <Form.Label>이메일</Form.Label>
          <Form.Input 
            placeholder="이메일을 입력해 주세요" 
            required 
            pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
          />
        </Form.Field>

        <Form.Field variant="password">
          <Form.Label>비밀번호</Form.Label>
          <Form.Input 
            placeholder="8자 이상 입력해 주세요" 
            required 
            minLength={8}
          />
        </Form.Field>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-02T06:35:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,77 @@
+import Dropdown from "@/src/components/Dropdown";
+import Form from "@/src/components/Form";
+import Logo from "@/src/components/Logo";
+import OauthSign from "@/src/components/OauthSign";
+import SignOptionSection from "@/src/components/SignOptionSection";
+import PATH_NAMES from "@/src/constants/pathname";
+import TEST_USERS from "@/src/constants/testUsers";
+import SignPageWrap from "@/src/containers/SignPageWrap";
+import { useSignIn } from "@/src/queries/auth";
+import useUserStore from "@/src/stores/useUserStore";
+import type { SignInProps } from "@/src/types/user";
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+
+const SignInRoute = () => {
+  const router = useRouter();
+  const { userData } = useUserStore();
+  const { mutate: signinFn, isPending: isPendingSignin } = useSignIn();
+
+  const handleSignIn = async (data: SignInProps) => {
+    signinFn(data);
+  };
+
path: src/pages/sign-in/admin.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**로그인 처리 함수에 오류 처리 로직이 필요합니다.**

signInFn 함수 호출 시 오류가 발생할 경우에 대한 처리가 없습니다. 사용자에게 적절한 피드백을 제공하는 오류 처리 로직을 추가하세요.

```diff
const handleSignIn = async (data: SignInProps) => {
-  signinFn(data);
+  signinFn(data, {
+    onError: (error) => {
+      // 로그인 실패 시 오류 처리 (예: 토스트 메시지 표시)
+      console.error('로그인 오류:', 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 handleSignIn = async (data: SignInProps) => {
    signinFn(data, {
      onError: (error) => {
        // 로그인 실패 시 오류 처리 (예: 토스트 메시지 표시)
        console.error('로그인 오류:', error);
      }
    });
  };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-02T06:35:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,77 @@
+import Dropdown from "@/src/components/Dropdown";
+import Form from "@/src/components/Form";
+import Logo from "@/src/components/Logo";
+import OauthSign from "@/src/components/OauthSign";
+import SignOptionSection from "@/src/components/SignOptionSection";
+import PATH_NAMES from "@/src/constants/pathname";
+import TEST_USERS from "@/src/constants/testUsers";
+import SignPageWrap from "@/src/containers/SignPageWrap";
+import { useSignIn } from "@/src/queries/auth";
+import useUserStore from "@/src/stores/useUserStore";
+import type { SignInProps } from "@/src/types/user";
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+
+const SignInRoute = () => {
+  const router = useRouter();
+  const { userData } = useUserStore();
+  const { mutate: signinFn, isPending: isPendingSignin } = useSignIn();
+
+  const handleSignIn = async (data: SignInProps) => {
+    signinFn(data);
+  };
+
+  useEffect(() => {
+    if (userData) router.replace(PATH_NAMES.Root);
+  }, [userData, router]);
+
+  return (
+    <SignPageWrap>
+      <Form onSubmit={handleSignIn} className="gap-16">
+        <Form.Title className="flex-col gap-16">
+          <Logo size="lg" />
+        </Form.Title>
+
+        <Form.Field variant="email">
+          <Form.Label>이메일</Form.Label>
+          <Form.Input placeholder="이메일을 입력해 주세요" />
+        </Form.Field>
+
+        <Form.Field variant="password">
+          <Form.Label>비밀번호</Form.Label>
+          <Form.Input placeholder="8자 이상 입력해 주세요" />
+        </Form.Field>
+
+        <Form.Field variant="authPage" className="mt-20">
+          <Form.SubmitButton disabled={isPendingSignin}>
+            로그인
+          </Form.SubmitButton>
+        </Form.Field>
+      </Form>
+
+      <SignOptionSection action="in" />
+
+      <OauthSign action="in" />
+
+      <div className="flex justify-end">
+        <Dropdown>
+          <Dropdown.Trigger>테스트 유저 로그인</Dropdown.Trigger>
+          <Dropdown.Menu className="!bottom-full !left-auto !right-0 !top-auto !mb-4 !mt-0 flex flex-col gap-4 p-4">
+            {TEST_USERS.map((item) => {
+              return (
+                <Dropdown.Item
+                  key={`test_user_item_${item.email}`}
+                  className="!rounded border border-brand-300 bg-brand-200 px-24 py-2 text-brand-500"
+                  onClick={() => signinFn(item)}>
+                  {item.nickname}
+                </Dropdown.Item>
+              );
+            })}
+          </Dropdown.Menu>
+        </Dropdown>
+      </div>
path: src/pages/sign-in/admin.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**테스트 유저 로그인 기능의 환경 제한이 필요합니다.**

테스트 유저 로그인 기능이 프로덕션 환경에서 노출되면 보안 위험이 될 수 있습니다. 개발 또는 테스트 환경에서만 이 기능이 표시되도록 환경 검사를 추가하세요.

```diff
+ // 환경 변수를 통해 현재 환경 확인
+ const isDevelopment = process.env.NODE_ENV !== 'production';

return (
  <SignPageWrap>
    {/* ... 기존 코드 ... */}
    
-   <div className="flex justify-end">
+   {isDevelopment && (
+     <div className="flex justify-end">
        <Dropdown>
          <Dropdown.Trigger>테스트 유저 로그인</Dropdown.Trigger>
          <Dropdown.Menu className="!bottom-full !left-auto !right-0 !top-auto !mb-4 !mt-0 flex flex-col gap-4 p-4">
            {TEST_USERS.map((item) => {
              return (
                <Dropdown.Item
                  key={`test_user_item_${item.email}`}
                  className="!rounded border border-brand-300 bg-brand-200 px-24 py-2 text-brand-500"
                  onClick={() => signinFn(item)}>
                  {item.nickname}
                </Dropdown.Item>
              );
            })}
          </Dropdown.Menu>
        </Dropdown>
-     </div>
+     </div>
+   )}
  </SignPageWrap>
);
```


<!-- 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 isDevelopment = process.env.NODE_ENV !== 'production';

return (
  <SignPageWrap>
    {/* ... 기존 코드 ... */}
    
    {isDevelopment && (
      <div className="flex justify-end">
        <Dropdown>
          <Dropdown.Trigger>테스트 유저 로그인</Dropdown.Trigger>
          <Dropdown.Menu className="!bottom-full !left-auto !right-0 !top-auto !mb-4 !mt-0 flex flex-col gap-4 p-4">
            {TEST_USERS.map((item) => {
              return (
                <Dropdown.Item
                  key={`test_user_item_${item.email}`}
                  className="!rounded border border-brand-300 bg-brand-200 px-24 py-2 text-brand-500"
                  onClick={() => signinFn(item)}>
                  {item.nickname}
                </Dropdown.Item>
              );
            })}
          </Dropdown.Menu>
        </Dropdown>
      </div>
    )}
  </SignPageWrap>
);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-02T06:35:16Z
author_association: NONE
path: src/constants/testUsers.ts
user: login: hayuri1990
body: 굿굿! 빠른 반영 감사합니다!
created_at: 2025-04-02T07:17:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-02T06:31:32Z
body: [vc]: #Z07yWmsMLK8cINCB/l/AFQhAdWNV6UqHS4dK7fu/l28=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ2aXZpdHJpcCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS92aXZpLXRyaXAtZjk0NTY5ZDYvdml2aXRyaXAvQXNaZ3Z3d2NzQnNib3M3SFN0aFptR0xKVzE4bSIsInByZXZpZXdVcmwiOiJ2aXZpdHJpcC1naXQtY2hvcmUtMjU2LXZpdmktdHJpcC1mOTQ1NjlkNi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InZpdml0cmlwLWdpdC1jaG9yZS0yNTYtdml2aS10cmlwLWY5NDU2OWQ2LnZlcmNlbC5hcHAifSwicm9vdERpcmVjdG9yeSI6bnVsbH1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **vivitrip** | ✅ Ready ([Inspect](https://vercel.com/vivi-trip-f94569d6/vivitrip/AsZgvwwcsBsbos7HSthZmGLJW18m)) | [Visit Preview](https://vivitrip-git-chore-256-vivi-trip-f94569d6.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/vivitrip-git-chore-256-vivi-trip-f94569d6.vercel.app?via=pr-comment-feedback-link) | Apr 2, 2025 7:16am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-02T06:31:40Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
이 PR은 새로운 `SignInRoute` React 컴포넌트를 추가하여 관리자를 위한 로그인 페이지를 구현합니다. 컴포넌트는 이메일과 패스워드 입력 폼, 테스트 사용자 선택 드롭다운, 그리고 인증 후 자동 리디렉션 기능을 포함합니다. Next.js의 `useRouter`와 커스텀 `useSignIn` 훅을 활용하여 로그인 처리와 네비게이션을 수행하며, 인증된 사용자는 자동으로 루트 경로로 리디렉션됩니다.

## Changes
| 파일                           | 변경 내용                                                                                      |
|--------------------------------|------------------------------------------------------------------------------------------------|
| src/pages/sign-in/admin.tsx    | 신규 `SignInRoute` 컴포넌트 추가: 로그인 폼, 테스트 사용자 드롭다운, 인증 후 자동 리디렉션 기능 구현 |
| src/constants/testUsers.ts      | `TEST_USERS` 배열 수정: 기존 테스트 사용자 정보를 한국어 닉네임과 새로운 이메일로 교체            |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as 사용자
    participant S as SignInRoute
    participant H as useSignIn 훅
    participant R as 라우터

    U->>S: 이메일/비밀번호 입력 후 폼 제출
    S->>H: handleSignIn(data) 호출
    H-->>S: 인증 결과 반환
    alt 인증 성공
        S->>R: 루트 경로로 리디렉션
    else 인증 실패
        S-->>U: 오류 메시지 표시
    end
```

## Assessment against linked issues
| Objective                                | Addressed | Explanation              |
|------------------------------------------|-----------|--------------------------|
| 테스트 로그인 라우터 생성 (#256)         | ✅       |                          |

## Possibly related PRs
- **vivi-trip/vivitrip#242**: 이전 PR에서 테스트 사용자 드롭다운 기능 변경이 이루어져, 코드 관련 내용이 연관됩니다.

## Suggested reviewers
- hayuri1990
- kanghyosung1

## Poem
> 귀여운 토끼가 노래하네,  
> 로그인 길에 꽃길을 뿌리고,  
> 드롭다운 속 친구들이 모여,  
> 사용자의 꿈을 이뤄주네 🐇,  
> 새 코드와 함께 달려가네!  
>  
> - 작성자 토끼

<!-- 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=vivi-trip/vivitrip&utm_content=257):

- 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: NONE