Skip to content

Commit 440e44e

Browse files
authored
feat: 로그인 폼에 FormInput 컴포넌트 추가 및 상태 관리 개선
feat: 로그인 폼에 FormInput 컴포넌트 추가 및 상태 관리 개선
2 parents 001ca30 + 7d25582 commit 440e44e

9 files changed

Lines changed: 135 additions & 32 deletions

File tree

.env

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# API 서버 URL (기본값: 프로덕션 서버)
2+
NEXT_PUBLIC_API_URL=https://srv.comatching.site
3+
4+
# Firebase Config (예시 값 - 실제 값은 .env.local에 작성)
5+
NEXT_PUBLIC_FIREBASE_API_KEY=your-firebase-api-key
6+
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
7+
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
8+
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.firebasestorage.app
9+
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
10+
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id
11+
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=your-measurement-id
12+
NEXT_PUBLIC_FIREBASE_VAPID_KEY=your-vapid-key
13+
14+
# 로컬 개발 환경 설정은 .env.local 파일에 작성하세요

.gemini/styleguide.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
66
- Even if the code or comments are in English, your review and feedback MUST be in Korean.
77
- 코드가 영어로 작성되어 있더라도, 리뷰와 피드백은 반드시 한국어로 작성하세요.
88

9+
Technology Stack:
10+
11+
- **이 프로젝트는 React 19와 Next.js 16을 사용합니다.**
12+
- **모든 코드 리뷰는 반드시 React 19와 Next.js 16의 최신 기능과 Best Practice를 기준으로 작성되어야 합니다.**
13+
914
Persona: Act as a Senior Front-end Engineer specializing in Next.js 16 & React 19. Be precise, insightful, and strict about performance, web standards, and maintainability.
1015

1116
Tone: Polite but professional. (정중하되, 문제는 명확하게 지적하고 구체적인 해결책을 제시하세요.)
1217

1318
2. React 19 & Next.js 16 Architecture
14-
2.1 React 19 Core Features
15-
Hooks & Compilation: React Compiler 도입을 고려하여, 불필요한 useMemo, useCallback은 제거를 권장하세요.
19+
**CRITICAL: 이 프로젝트는 React 19와 Next.js 16을 사용합니다. 모든 리뷰는 이 버전의 최신 기능과 API를 기준으로 작성되어야 합니다.**
20+
21+
2.1 React 19 Core Features
22+
Hooks & Compilation: React Compiler 도입을 고려하여, 불필요한 useMemo, useCallback은 제거를 권장하세요.
1623

1724
Form Actions: useActionState(구 useFormState)와 useFormStatus를 활용하여 폼 상태와 로딩 UI를 선언적으로 관리하는지 확인하세요.
1825

@@ -24,9 +31,11 @@ forwardRef 대신 ref를 prop으로 직접 전달하는지 확인하세요.
2431

2532
use() 훅을 사용하여 Promise나 Context를 조건부로 읽어오는지 확인하세요.
2633

27-
2.2 Next.js Architecture (App Router)
34+
2.2 Next.js 16 Architecture (App Router)
2835
Async Request APIs: params, searchParams, cookies(), headers() 등이 반드시 await로 비동기 처리되었는지 엄격히 확인하세요 (Next.js 15+ 필수 사항).
2936

37+
- **Next.js 16에서는 이러한 API들이 모두 Promise를 반환합니다. await 없이 사용하는 코드를 발견하면 반드시 지적하세요.**
38+
3039
Metadata API: <title> 태그를 직접 사용하기보다, Next.js의 metadata 객체나 generateMetadata 함수를 사용하여 SEO 태그를 관리하는지 확인하세요.
3140

3241
Caching Strategy: fetch의 캐싱 동작을 이해하고, 필요 시 cache: 'force-cache' 옵션이나 React 19의 'use cache' 디렉티브가 명시되었는지 확인하세요.

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ yarn-debug.log*
3030
yarn-error.log*
3131
.pnpm-debug.log*
3232

33-
# env files (can opt-in for committing if needed)
34-
.env*
33+
# env files
34+
.env*.local
35+
.env.local
36+
.env.development.local
37+
.env.production.local
3538

3639
# vercel
3740
.vercel

app/_components/LoginActionSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function ScreenLoginActionSection() {
2222

2323
return (
2424
<section className="flex flex-col items-center">
25-
<BubbleDiv />
25+
<BubbleDiv top={5} />
2626
<KakaoLoginButton
2727
className="mt-[1.6vh] mb-[0.49vh]"
2828
onClick={handleKakaoLogin}

app/layout.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ export const metadata: Metadata = {
2626
type: "website",
2727
locale: "ko_KR",
2828
},
29-
viewport: {
30-
width: "device-width",
31-
initialScale: 1,
32-
maximumScale: 1,
33-
userScalable: false,
34-
},
29+
};
30+
31+
export const viewport = {
32+
width: "device-width",
33+
initialScale: 1,
34+
maximumScale: 1,
35+
userScalable: false,
3536
};
3637

3738
export default async function RootLayout({

app/login/_components/LoginForm.tsx

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,53 @@
11
"use client";
22
import BubbleDiv from "@/app/_components/BubbleDiv";
33
import Button from "@/components/ui/Button";
4+
import FormInput from "@/components/ui/FormInput";
45
import { User } from "lucide-react";
5-
import React, { useActionState } from "react";
6+
import React, { useActionState, useState } from "react";
67
import Link from "next/link";
78
import { loginAction } from "@/lib/actions/loginAction";
89

9-
const INPUT_STYLE = {
10-
background:
11-
"linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.24) 100%)",
12-
};
13-
const INPUT_CLASSNAME =
14-
"all:unset box-border w-full border-b border-gray-300 px-2 py-[14.5px] leading-[19px] placeholder:text-[#B3B3B3]";
15-
1610
export const LoginForm = () => {
1711
// React 19: useActionState로 폼 상태 및 팬딩 처리 관리
1812
const [state, formAction, isPending] = useActionState(loginAction, {
1913
success: false,
2014
message: "",
2115
});
2216

17+
const [email, setEmail] = useState("");
18+
2319
return (
2420
<section className="mt-10 flex w-full flex-1 flex-col items-start gap-6">
2521
<form className="flex w-full flex-col gap-4" action={formAction}>
2622
<div className="flex w-full flex-col gap-2">
2723
<label htmlFor="email" className="typo-14-500 text-gray-700">
2824
아이디(이메일)
2925
</label>
30-
<input
26+
<FormInput
3127
id="email"
3228
type="email"
3329
name="email"
3430
placeholder="이메일 입력"
3531
required
3632
autoComplete="email"
37-
className={INPUT_CLASSNAME}
38-
style={INPUT_STYLE}
33+
value={email}
34+
onChange={(e) => setEmail(e.target.value)}
3935
/>
4036
</div>
4137

4238
<div className="relative mb-6 flex w-full flex-col gap-2">
4339
<label htmlFor="password" className="typo-14-500 text-gray-700">
4440
비밀번호
4541
</label>
46-
<input
42+
<FormInput
4743
id="password"
4844
type="password"
4945
name="password"
5046
placeholder="비밀번호 입력"
5147
required
5248
autoComplete="current-password"
53-
className={INPUT_CLASSNAME}
54-
style={INPUT_STYLE}
5549
/>
56-
{!state.success && (
50+
{!state.success && state.message && (
5751
<span className="typo-12-400 text-color-flame-700 absolute bottom-[-25px] left-0">
5852
* 이메일 혹은 비밀번호가 틀립니다
5953
</span>
@@ -77,13 +71,13 @@ export const LoginForm = () => {
7771
<BubbleDiv w={162} h={26} typo="typo-12-600" top={3}>
7872
아직 계정이 없으신가요?!
7973
</BubbleDiv>
80-
<button
81-
type="button"
74+
<Link
75+
href="/register"
8276
className="typo-14-500 flex items-center gap-1 border-b-2 border-gray-500 text-gray-500"
8377
>
8478
<User />
8579
이메일로 회원가입
86-
</button>
80+
</Link>
8781
</div>
8882
</section>
8983
);

app/login/_components/ScreenLocalLoginPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { LoginForm } from "./LoginForm";
55

66
const ScreenLocalLoginPage = () => {
77
return (
8-
<main className="flex h-dvh flex-col items-start px-4 pt-2 pb-[6.2vh]">
8+
<main className="flex min-h-dvh flex-col items-start px-4 pt-2 pb-[6.2vh]">
99
<BackButton />
1010
<LocalLoginIntro />
1111
<LoginForm />

components/ui/FormInput.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from "react";
2+
import { cn } from "@/lib/utils";
3+
4+
// React.InputHTMLAttributes를 확장하여 모든 표준 input 속성을 타입 안전하게 지원
5+
interface FormInputProps extends Omit<
6+
React.InputHTMLAttributes<HTMLInputElement>,
7+
"id" | "type" | "name" | "placeholder"
8+
> {
9+
id: string; // input 요소의 고유 식별자 (label의 htmlFor와 연결)
10+
type: string; // input 타입 (예: text, email, password 등)
11+
name: string; // form 데이터 전송 시 key 역할
12+
placeholder: string; // 입력란에 표시되는 안내 텍스트
13+
}
14+
15+
const INPUT_STYLE = {
16+
background:
17+
"linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.24) 100%)",
18+
};
19+
const INPUT_CLASSNAME =
20+
"all:unset box-border w-full border-b border-gray-300 px-2 py-[14.5px] leading-[19px] typo-16-500 placeholder:text-[#B3B3B3] text-color-gray-900 outline-none";
21+
22+
// 안전한 속성 화이트리스트 (XSS 방지)
23+
const SAFE_INPUT_ATTRIBUTES = [
24+
"autoComplete",
25+
"required",
26+
"value",
27+
"defaultValue",
28+
"disabled",
29+
"readOnly",
30+
"maxLength",
31+
"minLength",
32+
"max",
33+
"min",
34+
"pattern",
35+
"step",
36+
"inputMode",
37+
"aria-label",
38+
"aria-describedby",
39+
"aria-invalid",
40+
"aria-required",
41+
"onChange",
42+
"onBlur",
43+
"onFocus",
44+
"onInput",
45+
"onKeyDown",
46+
"onKeyUp",
47+
"ref", // React 19에서 ref는 일반 prop
48+
] as const;
49+
50+
type SafeInputAttribute = (typeof SAFE_INPUT_ATTRIBUTES)[number];
51+
52+
// React 19: ref는 일반 prop으로 전달되므로 forwardRef 불필요
53+
const FormInput = ({
54+
id,
55+
type,
56+
name,
57+
placeholder,
58+
className = "",
59+
style = {},
60+
...rest
61+
}: FormInputProps) => {
62+
// 화이트리스트에 있는 안전한 속성만 추출
63+
const safeProps = Object.fromEntries(
64+
Object.entries(rest).filter(([key]) =>
65+
SAFE_INPUT_ATTRIBUTES.includes(key as SafeInputAttribute),
66+
),
67+
);
68+
69+
return (
70+
<input
71+
id={id}
72+
type={type}
73+
name={name}
74+
placeholder={placeholder}
75+
className={cn(INPUT_CLASSNAME, className)}
76+
style={{ ...INPUT_STYLE, ...style }}
77+
{...safeProps}
78+
/>
79+
);
80+
};
81+
82+
export default FormInput;

lib/status.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export async function getInitialMaintenanceStatus() {
22
try {
33
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/status`, {
4-
cache: "no-store",
4+
next: { revalidate: 60 }, // 60초마다 재검증 (정적 생성 가능)
55
});
66
if (!res.ok) return false;
77

0 commit comments

Comments
 (0)