-
Notifications
You must be signed in to change notification settings - Fork 2
[feat] 회원가입 반응형 UI #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 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. Comment |
Summary of ChangesHello @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를 구현하고, 각 단계별 컴포넌트의 로직을 개선하는 데 중점을 두었습니다. Highlights
🧠 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 AssistThe 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
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 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
|
There was a problem hiding this 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 하도록 변경하여 오류 처리를 개선한 점도 코드를 더욱 견고하게 만들어주는 좋은 변경입니다. 코드 구조 및 구현 세부 사항에 대한 몇 가지 개선 제안 사항이 있습니다. 주로 재사용성과 성능에 관한 내용이니, 남겨드린 개별 코멘트들을 확인 부탁드립니다.
| "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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
새로 추가된 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"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 바꾸면 더 좋을듯해요!
| {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> | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인증 완료 시 보여주는 Toast UI를 이 컴포넌트 내에 직접 구현하셨네요. 기존에 components/common/Toast 컴포넌트를 사용하고 계셨는데, 이를 대체한 특별한 이유가 있으신가요? 현재 구현은 EmailVerification 컴포넌트에 종속적이라 다른 곳에서 재사용하기 어렵습니다. 또한, 애니메이션을 위한 타이머 로직이 useEmailVerification 훅에 포함되어 있어 UI와 로직이 분리되는 효과가 줄어듭니다. 기존 Toast 컴포넌트를 확장하여 사용하시거나, 이 새로운 Toast를 별도의 재사용 가능한 컴포넌트로 분리하는 것을 고려해보시면 좋겠습니다.
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| useEffect(() => { | ||
| const checkMobile = () => { | ||
| setIsMobile(window.innerWidth < 768); | ||
| }; | ||
|
|
||
| // Initial check | ||
| checkMobile(); | ||
|
|
||
| window.addEventListener("resize", checkMobile); | ||
| return () => window.removeEventListener("resize", checkMobile); | ||
| }, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEffect와 resize 이벤트를 사용해 모바일 여부를 판단하고 계십니다. 이 방법은 화면 크기가 변경될 때마다 리렌더링을 유발할 수 있어 성능에 미세한 영향을 줄 수 있습니다. 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);
};
}, []);| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @@ -1,4 +1,5 @@ | |||
| // 이메일 UI 비즈니스 로직 | |||
| // c:\Users\shinwookKang\Desktop\CheckMo\FE\src\components\base-ui\Join\steps\useEmailVerification.tsx | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // 비즈니스 로직에서 catch 할 수 있도록 그대로 반환하거나 throw | ||
| // 현재 구조상 useLoginForm 등에서 data.isSuccess를 체크하므로 data를 반환 | ||
| return data as T; | ||
| throw new ApiError(errorMessage, errorCode, data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 To Reviewers
별도 추가 라이브러리 없음: 현재까지는 Next.js/React 기본 기능과 Tailwind CSS만 사용되었습니다.
아키텍처 주요 사항:
SignupContext를 도입하여 Step 간 데이터 파편화 문제를 해결하고 전역에서 폼 데이터를 관리합니다.SignupStepLayout을 공통 컴포넌트로 분리하여 모든 페이지의 레이아웃 중복을 제거했습니다.🔥 작업 내용
1. 회원가입 전체 플로우 오케스트레이션 구현
SignupPage(/signup/page.tsx)에서 Step 상태(terms->complete)를 관리하며 컴포넌트 전환 구현.2. 단계별 컴포넌트 UI 및 로직 구현 (반응형 완비)
TermsAgreement): 전체 동의/개별 동의 로직 및 유효성 검사.EmailVerification): 타이머 로직, 인증번호 입력 UI, Toast 알림 연동.PasswordEntry): 정규식 유효성 검사(복잡도), 비밀번호 일치 확인, 커스텀 라벨링.ProfileSetup): 닉네임 중복 확인 UI, 입력 폼 구현.ProfileImage):useMemo최적화).SignupComplete): 최종 입력 데이터 표시 및 메인 이동 버튼.3. 전역 상태 관리 (
SignupContext)formData)을 한곳에서 관리 및 업데이트하는 로직 구현.useMemo를 활용하여File객체의 Blob URL 메모리 누수 방지 처리.🤔 추후 작업 예정
multipart/form-data) 연동 및 로딩 처리.📸 작업 결과 (스크린샷)
🔗 관련 이슈