Skip to content

Conversation

@shinwokkang
Copy link
Contributor

💡 To Reviewers

  • 별도 추가 라이브러리 없음: 현재까지는 Next.js/React 기본 기능과 Tailwind CSS만 사용되었습니다.

  • 아키텍처 주요 사항:

    • SignupContext를 도입하여 Step 간 데이터 파편화 문제를 해결하고 전역에서 폼 데이터를 관리합니다.
    • SignupStepLayout을 공통 컴포넌트로 분리하여 모든 페이지의 레이아웃 중복을 제거했습니다.
    • 모바일/태블릿/데스크탑 3단계 반응형 전략(3-Tier Responsive)을 적용했습니다.

🔥 작업 내용

1. 회원가입 전체 플로우 오케스트레이션 구현

  • SignupPage (/signup/page.tsx)에서 Step 상태(terms -> complete)를 관리하며 컴포넌트 전환 구현.

2. 단계별 컴포넌트 UI 및 로직 구현 (반응형 완비)

  • 약관 동의 (TermsAgreement): 전체 동의/개별 동의 로직 및 유효성 검사.
  • 이메일 인증 (EmailVerification): 타이머 로직, 인증번호 입력 UI, Toast 알림 연동.
  • 비밀번호 입력 (PasswordEntry): 정규식 유효성 검사(복잡도), 비밀번호 일치 확인, 커스텀 라벨링.
  • 프로필 설정 (ProfileSetup): 닉네임 중복 확인 UI, 입력 폼 구현.
  • 프로필 이미지 (ProfileImage):
    • 파일 업로드 및 미리보기(useMemo 최적화).
    • 모바일 환경에서의 Stepper 분기 처리 (이미지 업로드 -> 관심사 선택).
  • 가입 완료 (SignupComplete): 최종 입력 데이터 표시 및 메인 이동 버튼.

3. 전역 상태 관리 (SignupContext)

  • 모든 단계의 입력값(formData)을 한곳에서 관리 및 업데이트하는 로직 구현.
  • useMemo를 활용하여 File 객체의 Blob URL 메모리 누수 방지 처리.

🤔 추후 작업 예정

  • UX 고도화: 새로고침 시 데이터 유지(SessionStorage), 뒤로 가기 시 단계 동기화(Query Param).
  • 성능 최적화: 고해상도 이미지 업로드 시 클라이언트 측 압축 로직 추가.
  • API 연동: 실제 백엔드 API (multipart/form-data) 연동 및 로딩 처리.

📸 작업 결과 (스크린샷)

  • X

🔗 관련 이슈

@shinwokkang shinwokkang self-assigned this Jan 24, 2026
@shinwokkang shinwokkang added ✨ feat 새로운 기능 추가 ♻️ refactor 리팩토링 labels Jan 24, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @shinwokkang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 회원가입 전체 플로우에 대한 반응형 UI를 구현하고, 각 단계별 컴포넌트의 로직을 개선하는 데 중점을 두었습니다. SignupContext를 활용한 전역 폼 데이터 관리와 SignupStepLayout을 통한 공통 레이아웃 적용으로 아키텍처를 정비했으며, 모바일 환경에서의 사용자 경험을 향상시키기 위한 다양한 반응형 디자인 패턴이 적용되었습니다. 또한, API 에러 처리 방식이 개선되어 견고한 프론트엔드 시스템 구축에 기여합니다.

Highlights

  • 반응형 UI 구현: 회원가입 플로우의 모든 단계에 걸쳐 모바일, 태블릿, 데스크탑 3단계 반응형 UI가 적용되었습니다. JoinLayout 및 각 스텝 컴포넌트(EmailVerification, PasswordEntry, ProfileSetup, ProfileImage, SignupComplete, TermsAgreement, TermsItem, TermsList)에 Tailwind CSS를 활용한 반응형 스타일이 추가되었습니다.
  • 회원가입 플로우 로직 개선: 이메일 인증, 비밀번호 입력, 프로필 설정, 프로필 이미지/관심사 선택 등 각 회원가입 단계의 UI 로직이 구현 및 개선되었습니다. 특히 비밀번호 유효성 검사 피드백과 모바일 환경에서의 프로필 이미지/관심사 선택 단계 분기 처리가 추가되었습니다.
  • 에러 핸들링 개선: src/lib/api/client.ts 파일에 ApiError 클래스를 도입하여 API 호출 시 발생하는 에러를 보다 구조적으로 처리할 수 있도록 변경되었습니다.
  • 신규 회원가입 약관 동의 컴포넌트 추가: 회원가입 약관 동의를 위한 SignupTerms.tsx 컴포넌트와 관련 CSS 모듈(SignupTerms.module.css)이 새로 추가되었습니다. 이 컴포넌트는 필수/선택 약관 동의 및 전체 동의 기능을 포함합니다.
  • 로그인 모달에서 회원가입 페이지로 이동 기능 추가: 로그인 모달(LoginModal.tsx)에서 '회원가입하러가기' 링크 클릭 시 /signup 경로로 이동하도록 useRouter를 사용하여 기능이 추가되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

전반적으로 회원가입 플로우에 반응형 UI를 훌륭하게 구현해주셨습니다. Tailwind의 반응형 prefix를 사용하여 코드가 깔끔하고 가독성이 좋습니다. 특히 usePasswordEntry.ts에서 useEffect 대신 파생 상태(derived state)를 사용하도록 리팩토링한 점은 성능과 코드 명확성 측면에서 매우 훌륭한 개선입니다. 또한 api/client.ts에서 커스텀 ApiError를 throw 하도록 변경하여 오류 처리를 개선한 점도 코드를 더욱 견고하게 만들어주는 좋은 변경입니다. 코드 구조 및 구현 세부 사항에 대한 몇 가지 개선 제안 사항이 있습니다. 주로 재사용성과 성능에 관한 내용이니, 남겨드린 개별 코멘트들을 확인 부탁드립니다.

Comment on lines +1 to +107
"use client";

import React, { useState } from "react";
import Image from "next/image";
import styles from "./SignupTerms.module.css";
import LoginLogo from "@/components/base-ui/Login/LoginLogo";

const TERMS_DATA = [
{
id: "privacy",
label: "서비스 이용을 위한 필수 개인정보 수집·이용 동의",
required: true,
},
{ id: "terms", label: "책모 이용약관 동의", required: true },
{ id: "thirdParty", label: "개인정보 제3자 제공 동의", required: false },
{
id: "marketing",
label: "마케팅 및 이벤트 정보 수신 동의",
required: false,
},
];

export default function SignupTerms() {
const [agreements, setAgreements] = useState<Record<string, boolean>>({
privacy: false,
terms: false,
thirdParty: false,
marketing: false,
});

const handleCheck = (id: string) => {
setAgreements((prev) => ({ ...prev, [id]: !prev[id] }));
};

const handleAllCheck = () => {
const allChecked = Object.values(agreements).every(Boolean);
const newStatus = !allChecked;
const newAgreements = Object.keys(agreements).reduce((acc, key) => {
acc[key] = newStatus;
return acc;
}, {} as Record<string, boolean>);
setAgreements(newAgreements);
};

const isAllRequiredChecked = TERMS_DATA.filter((t) => t.required).every(
(t) => agreements[t.id]
);

const isAllChecked = Object.values(agreements).every(Boolean);

return (
<div className={styles.container}>
{/* 닫기 버튼 */}
<button type="button" className={styles.closeIcon}>
<Image src="/cancle_button.svg" alt="닫기" width={24} height={24} />
</button>

{/* 헤더 */}
<div className={styles.header}>
<LoginLogo />
<h2 className={styles.title}>약관 동의</h2>
</div>

{/* 약관 목록 */}
<div className={styles.termsContainer}>
{TERMS_DATA.map((term) => (
<div
key={term.id}
className={styles.termItem}
onClick={() => handleCheck(term.id)}
>
<span className={styles.termLabel}>
{term.label} ({term.required ? "필수" : "선택"})
</span>
<input
type="checkbox"
checked={agreements[term.id]}
readOnly
className={styles.checkbox}
/>
</div>
))}
<div className={styles.divider} />
<div className={styles.termItem} onClick={handleAllCheck}>
<span className={styles.termLabel} style={{ fontWeight: 600 }}>
전체동의
</span>
<input
type="checkbox"
checked={isAllChecked}
readOnly
className={styles.checkbox}
/>
</div>
</div>

{/* 다음 버튼 */}
<button
type="button"
className={styles.nextButton}
disabled={!isAllRequiredChecked}
>
다음
</button>
</div>
);
}

Choose a reason for hiding this comment

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

high

새로 추가된 SignupTerms.tsx 컴포넌트와 SignupTerms.module.css 파일이 TermsAgreement.tsx와 기능적으로 매우 유사해 보입니다. TermsAgreement.tsx는 Tailwind CSS를 사용하고 회원가입 단계의 일부로 잘 통합되어 있는 반면, SignupTerms.tsx는 CSS Modules를 사용하고 있어 구현 방식에 차이가 있습니다. 코드베이스의 일관성을 유지하고 중복을 피하기 위해, 이 컴포넌트가 꼭 필요한지 확인해주시고, 만약 TermsAgreement.tsx로 대체될 수 있다면 이 파일들은 제거하는 것을 고려해보는 것이 좋겠습니다.

- Mobile: Fixed w-[352px], Fluid px-[24px]
- Desktop: Fixed w-[766px], Strict Padding px-[56px] py-[99px]
*/}
<div className="relative flex flex-col items-center w-[352px] h-[720px] px-[24px] md:w-[766px] md:h-auto md:min-h-0 md:px-[56px] md:py-[99px] md:gap-0 rounded-xl bg-white shadow-lg mx-auto">

Choose a reason for hiding this comment

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

medium

모바일 뷰에서 h-[720px]로 고정 높이를 사용하고 계신데, 이 경우 화면이 작은 모바일 기기나 사용자가 시스템 폰트 크기를 키웠을 때 콘텐츠가 잘리거나 레이아웃이 깨질 위험이 있습니다. min-h-[720px]를 사용하시거나, 높이를 고정하지 않고 콘텐츠에 따라 유연하게 조절되도록 하는 것이 더 안전한 방법일 것 같습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

이렇게 바꾸면 더 좋을듯해요!

Comment on lines 106 to 116
{showToast && (
<Toast
message="인증이 완료되었습니다."
onClose={() => setShowToast(false)}
/>
<div
className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 inline-flex justify-center items-center h-auto py-4 px-8 md:h-[88px] md:px-[138px] bg-[#31111D99] rounded-[24px] backdrop-blur-[1px] transition-opacity duration-300 ${
isToastVisible ? "opacity-100" : "opacity-0"
}`}
>
<span className="text-[#FFF] text-[16px] md:text-[18px] font-medium leading-[135%] tracking-[-0.018px] whitespace-nowrap">
인증이 완료되었습니다.
</span>
</div>
)}

Choose a reason for hiding this comment

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

medium

인증 완료 시 보여주는 Toast UI를 이 컴포넌트 내에 직접 구현하셨네요. 기존에 components/common/Toast 컴포넌트를 사용하고 계셨는데, 이를 대체한 특별한 이유가 있으신가요? 현재 구현은 EmailVerification 컴포넌트에 종속적이라 다른 곳에서 재사용하기 어렵습니다. 또한, 애니메이션을 위한 타이머 로직이 useEmailVerification 훅에 포함되어 있어 UI와 로직이 분리되는 효과가 줄어듭니다. 기존 Toast 컴포넌트를 확장하여 사용하시거나, 이 새로운 Toast를 별도의 재사용 가능한 컴포넌트로 분리하는 것을 고려해보시면 좋겠습니다.

Comment on lines +8 to +17
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^&*]).{6,12}$/;

setIsValid(isPasswordValid && isMatch && password.length > 0);
}, [password, confirmPassword]);
// Derived State (No useEffect)
const isComplexityValid = passwordRegex.test(password);
const isMatch = password === confirmPassword;
const isValid =
isComplexityValid &&
isMatch &&
password.length > 0 &&
confirmPassword.length > 0;

Choose a reason for hiding this comment

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

medium

useEffect를 사용하지 않고 파생 상태(derived state)로 유효성 검사 로직을 변경하신 점이 매우 좋습니다. 코드가 더 선언적이고 예측 가능해졌습니다. 훌륭한 리팩토링입니다!

Comment on lines +27 to +37
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};

// Initial check
checkMobile();

window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);

Choose a reason for hiding this comment

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

medium

useEffectresize 이벤트를 사용해 모바일 여부를 판단하고 계십니다. 이 방법은 화면 크기가 변경될 때마다 리렌더링을 유발할 수 있어 성능에 미세한 영향을 줄 수 있습니다. window.matchMedia API를 사용하면 미디어 쿼리의 상태 변경을 직접 감지할 수 있어 더 효율적입니다. 아래와 같이 수정하는 것을 고려해보세요.

useEffect(() => {
  const mediaQuery = window.matchMedia("(max-width: 767px)");

  const handleMediaChange = (e) => {
    setIsMobile(e.matches);
  };

  // 컴포넌트 마운트 시 초기 상태 설정
  handleMediaChange(mediaQuery);

  // 리스너 등록
  mediaQuery.addEventListener("change", handleMediaChange);

  // 클린업 함수에서 리스너 제거
  return () => {
    mediaQuery.removeEventListener("change", handleMediaChange);
  };
}, []);

Comment on lines 21 to 25
<span className="text-[#353535] text-[19.861px] font-normal leading-[15.605px]">
{label} ({required ? "필수" : "선택"})
<span className="text-[14px] md:text-[clamp(16px,2.5vw,19.861px)]">
{label} ({required ? "필수" : "선택"})
</span>
</span>

Choose a reason for hiding this comment

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

medium

<span> 태그가 중첩되어 사용되었는데, 바깥쪽 <span>에 적용된 스타일(text-[19.861px], leading-[15.605px])이 안쪽 <span>의 스타일로 인해 덮어쓰여지고 있습니다. 불필요한 중첩 구조를 제거하고 코드를 간결하게 만들 수 있을 것 같습니다.

      <span className="text-[#353535] font-normal text-[14px] md:text-[clamp(16px,2.5vw,19.861px)]">
          {label} ({required ? "필수" : "선택"})
        </span>

@@ -1,4 +1,5 @@
// 이메일 UI 비즈니스 로직
// c:\Users\shinwookKang\Desktop\CheckMo\FE\src\components\base-ui\Join\steps\useEmailVerification.tsx

Choose a reason for hiding this comment

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

medium

주석에 개발자 개인의 로컬 파일 경로가 포함되어 있습니다. 이는 다른 팀원들에게 혼란을 줄 수 있고 불필요한 정보이므로 제거하는 것이 좋습니다.

// 비즈니스 로직에서 catch 할 수 있도록 그대로 반환하거나 throw
// 현재 구조상 useLoginForm 등에서 data.isSuccess를 체크하므로 data를 반환
return data as T;
throw new ApiError(errorMessage, errorCode, data);

Choose a reason for hiding this comment

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

medium

API 요청 실패 시 data를 반환하는 대신 커스텀 에러인 ApiError를 throw하도록 변경하신 점이 매우 좋습니다. try...catch 구문을 통해 에러를 일관되게 처리할 수 있게 되어 코드의 안정성과 예측 가능성이 크게 향상되었습니다. 훌륭한 개선입니다.

@bini0918 bini0918 self-requested a review January 24, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가 ♻️ refactor 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants