Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .gemini/styleguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@
- Even if the code or comments are in English, your review and feedback MUST be in Korean.
- 코드가 영어로 작성되어 있더라도, 리뷰와 피드백은 반드시 한국어로 작성하세요.

Technology Stack:

- **이 프로젝트는 React 19와 Next.js 16을 사용합니다.**
- **모든 코드 리뷰는 반드시 React 19와 Next.js 16의 최신 기능과 Best Practice를 기준으로 작성되어야 합니다.**

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.

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

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

2.1 React 19 Core Features
Hooks & Compilation: React Compiler 도입을 고려하여, 불필요한 useMemo, useCallback은 제거를 권장하세요.

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

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

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

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

- **Next.js 16에서는 이러한 API들이 모두 Promise를 반환합니다. await 없이 사용하는 코드를 발견하면 반드시 지적하세요.**

Metadata API: <title> 태그를 직접 사용하기보다, Next.js의 metadata 객체나 generateMetadata 함수를 사용하여 SEO 태그를 관리하는지 확인하세요.

Caching Strategy: fetch의 캐싱 동작을 이해하고, 필요 시 cache: 'force-cache' 옵션이나 React 19의 'use cache' 디렉티브가 명시되었는지 확인하세요.
Expand Down
2 changes: 1 addition & 1 deletion app/_components/LoginActionSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function ScreenLoginActionSection() {

return (
<section className="flex flex-col items-center">
<BubbleDiv />
<BubbleDiv top={5} />
<KakaoLoginButton
className="mt-[1.6vh] mb-[0.49vh]"
onClick={handleKakaoLogin}
Expand Down
30 changes: 12 additions & 18 deletions app/login/_components/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,53 @@
"use client";
import BubbleDiv from "@/app/_components/BubbleDiv";
import Button from "@/components/ui/Button";
import FormInput from "@/components/ui/FormInput";
import { User } from "lucide-react";
import React, { useActionState } from "react";
import React, { useActionState, useState } from "react";
import Link from "next/link";
import { loginAction } from "@/lib/actions/loginAction";

const INPUT_STYLE = {
background:
"linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.24) 100%)",
};
const INPUT_CLASSNAME =
"all:unset box-border w-full border-b border-gray-300 px-2 py-[14.5px] leading-[19px] placeholder:text-[#B3B3B3]";

export const LoginForm = () => {
// React 19: useActionState로 폼 상태 및 팬딩 처리 관리
const [state, formAction, isPending] = useActionState(loginAction, {
success: false,
message: "",
});

const [email, setEmail] = useState("");

return (
<section className="mt-10 flex w-full flex-1 flex-col items-start gap-6">
<form className="flex w-full flex-col gap-4" action={formAction}>
<div className="flex w-full flex-col gap-2">
<label htmlFor="email" className="typo-14-500 text-gray-700">
아이디(이메일)
</label>
<input
<FormInput
id="email"
type="email"
name="email"
placeholder="이메일 입력"
required
autoComplete="email"
className={INPUT_CLASSNAME}
style={INPUT_STYLE}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
Comment on lines +26 to 35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

서버 액션과 useActionState를 사용하는 현재 구조에서는 입력 필드를 비제어(uncontrolled) 방식으로 사용하는 것이 더 효율적이고 코드도 간결해집니다. 이메일 필드를 useState로 제어하는 것은 불필요한 리렌더링을 유발하며, 비밀번호 필드와도 일관성이 맞지 않습니다. loginActionFormData를 통해 값을 처리하므로, valueonChange props를 제거하고 관련 useState도 삭제하는 것을 권장합니다.

Suggested change
<FormInput
id="email"
type="email"
name="email"
placeholder="이메일 입력"
required
autoComplete="email"
className={INPUT_CLASSNAME}
style={INPUT_STYLE}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<FormInput
id="email"
type="email"
name="email"
placeholder="이메일 입력"
required
autoComplete="email"
/>

</div>

<div className="relative mb-6 flex w-full flex-col gap-2">
<label htmlFor="password" className="typo-14-500 text-gray-700">
비밀번호
</label>
<input
<FormInput
id="password"
type="password"
name="password"
placeholder="비밀번호 입력"
required
autoComplete="current-password"
className={INPUT_CLASSNAME}
style={INPUT_STYLE}
/>
{!state.success && (
{!state.success && state.message && (
<span className="typo-12-400 text-color-flame-700 absolute bottom-[-25px] left-0">
* 이메일 혹은 비밀번호가 틀립니다
</span>
Expand All @@ -77,13 +71,13 @@ export const LoginForm = () => {
<BubbleDiv w={162} h={26} typo="typo-12-600" top={3}>
아직 계정이 없으신가요?!
</BubbleDiv>
<button
type="button"
<Link
href="/register"
className="typo-14-500 flex items-center gap-1 border-b-2 border-gray-500 text-gray-500"
>
<User />
이메일로 회원가입
</button>
</Link>
</div>
</section>
);
Expand Down
2 changes: 1 addition & 1 deletion app/login/_components/ScreenLocalLoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LoginForm } from "./LoginForm";

const ScreenLocalLoginPage = () => {
return (
<main className="flex h-dvh flex-col items-start px-4 pt-2 pb-[6.2vh]">
<main className="flex min-h-dvh flex-col items-start px-4 pt-2 pb-[6.2vh]">
<BackButton />
<LocalLoginIntro />
<LoginForm />
Expand Down
50 changes: 50 additions & 0 deletions components/ui/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import { cn } from "@/lib/utils";

interface FormInputProps {
id: string; // input 요소의 고유 식별자 (label의 htmlFor와 연결)
type: string; // input 타입 (예: text, email, password 등)
name: string; // form 데이터 전송 시 key 역할
placeholder: string; // 입력란에 표시되는 안내 텍스트
autoComplete?: string; // 브라우저 자동완성 속성값 (예: email, username, current-password, new-password, tel, off 등)
required?: boolean; // 필수 입력 여부
className?: string; // 추가 커스텀 클래스
style?: React.CSSProperties; // 인라인 스타일
value?: string; // input 값 (제어 컴포넌트로 사용 시)
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; // 값 변경 핸들러
}

const INPUT_STYLE = {
background:
"linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.24) 100%)",
};
const INPUT_CLASSNAME =
"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";

const FormInput = ({
id,
type,
name,
placeholder,
autoComplete,
required = false,
className = "",
style = {},
...rest
}: FormInputProps) => {
return (
<input
id={id}
type={type}
name={name}
placeholder={placeholder}
autoComplete={autoComplete}
required={required}
className={cn(INPUT_CLASSNAME, className)}
style={{ ...INPUT_STYLE, ...style }}
{...rest}
/>
Comment on lines 60 to 78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The FormInput component currently uses the object spread operator (...rest) to pass props to the underlying <input> element. This introduces a potential medium-severity Cross-Site Scripting (XSS) vulnerability, as malicious event handlers (e.g., onmouseover, onerror) or other dangerous attributes could be injected if the props are ever derived from user-controlled input. The provided code suggestion explicitly defines the allowed props, which is crucial for mitigating this XSS risk by preventing unintended attributes from being passed. For further improvements, consider enhancing FormInputProps by extending React.InputHTMLAttributes for better type definition and implementing React.forwardRef to allow ref forwarding, which would increase the component's versatility.

const FormInput = ({
  id,
  type,
  name,
  placeholder,
  autoComplete,
  required = false,
  className = "",
  style = {},
  value,
  onChange
}: FormInputProps) => {
  return (
    <input
      id={id}
      type={type}
      name={name}
      placeholder={placeholder}
      autoComplete={autoComplete}
      required={required}
      className={cn(INPUT_CLASSNAME, className)}
      style={{ ...INPUT_STYLE, ...style }}
      value={value}
      onChange={onChange}
    />
  );
};
References
  1. The suggested code includes a style prop, which allows for inline styles. This aligns with the guideline to use inline styles when precise values, such as for box-shadow or border, cannot be accurately represented by existing Tailwind utility classes.

);
};

export default FormInput;
Loading