[FEAT] 온보딩 페이지 퍼블리싱#22
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthrough로그인 페이지와 회원가입 선택 화면을 추가하고, 디자이너/강사 회원가입의 3단계/2단계 플로우를 구현합니다. 회원가입 검증, 이메일 인증 타이머, 약관 관리를 위한 상수와 상태 관리 훅을 제공하며, 은행 선택 드롭다운, 타입 버튼 등의 공통 UI 컴포넌트를 추가합니다. Changes온보딩 페이지 퍼블리싱
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
src/components/common/dropdown/BankDropdown.tsx (1)
97-97: ⚡ Quick win인라인 style 대신 Tailwind 클래스로 통일하는 게 좋습니다
maxHeight를 인라인으로 주입하기보다 Tailwind 유틸리티(또는 해당 값을 감싼 유틸 클래스)로 맞추면 스타일 규칙 일관성이 좋아집니다.As per coding guidelines, "인라인 style 속성보다 Tailwind 클래스 사용을 권장해주세요."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/common/dropdown/BankDropdown.tsx` at line 97, The div in BankDropdown uses an inline style (style={{ maxHeight: BANK_DROPDOWN_MAX_HEIGHT }}) which violates the guideline; remove the inline style and apply a Tailwind utility or a small CSS utility class instead — either convert the constant BANK_DROPDOWN_MAX_HEIGHT to a matching Tailwind class (e.g., max-h-... or arbitrary value like max-h-[...]), or create a dedicated utility class (e.g., .bank-dropdown-max-h) and add that class to the div with the existing "overflow-y-auto" so all styling is controlled via classes in the BankDropdown component.src/app/signup/designer/step1/page.tsx (1)
173-185: ⚡ Quick win약관 모달에 기본 대화상자 접근성 속성이 필요합니다.
스크린리더/키보드 사용자 관점에서
role="dialog",aria-modal, 제목 연결(aria-labelledby), 닫기 버튼aria-label을 추가하는 편이 좋습니다.제안 수정
- <section + <section + role="dialog" + aria-modal="true" + aria-labelledby="designer-terms-modal-title" className="rounded-12 flex h-[792px] w-[612px] flex-col gap-6 bg-white px-6 py-8" onClick={event => event.stopPropagation()} > <header className="border-gray-20 flex items-center justify-between gap-4"> - <h2 className="text-heading2-sb text-black">{selectedTerm.modalTitle}</h2> + <h2 id="designer-terms-modal-title" className="text-heading2-sb text-black"> + {selectedTerm.modalTitle} + </h2> <button type="button" + aria-label="약관 모달 닫기" className="text-gray-80 flex size-6 shrink-0 cursor-pointer items-center justify-center hover:text-black" onClick={() => setSelectedTermId(null)} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/signup/designer/step1/page.tsx` around lines 173 - 185, The modal <section> is missing basic dialog accessibility attributes; add role="dialog" and aria-modal="true" to the section, give the <h2> (selectedTerm.modalTitle) a unique id (e.g., modal-title-<termId>) and set aria-labelledby on the section to that id, and add an accessible label to the Close button (e.g., aria-label="Close dialog") where setSelectedTermId(null) is used; ensure the existing onClick={event => event.stopPropagation()} remains and that CloseIcon remains unchanged.src/app/signup/designer/step3/page.tsx (1)
69-75: ⚡ Quick win계좌번호 입력에 숫자 키패드 힌트를 추가하면 UX가 좋아집니다.
숫자만 허용 중이므로
inputMode="numeric"를 주면 모바일 입력이 더 자연스럽습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/signup/designer/step3/page.tsx` around lines 69 - 75, Add inputMode="numeric" to the InputField usage to hint numeric keyboard on mobile: update the JSX where InputField is rendered (the instance with label="계좌번호", value={accountNumber}, onChange={handleAccountNumberChange}) to include inputMode="numeric"; if InputField doesn't accept or forward unknown props, update the InputField component's props interface to include inputMode?: string and forward it to the underlying <input> (or native input element) so the prop actually takes effect.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/signup/designer/step1/page.tsx`:
- Line 50: isNextEnabled currently only checks phoneNumber existence so
incomplete inputs like "010-1" pass; update the condition to validate that
phoneNumber contains exactly 11 digits by stripping non-digits and checking
length === 11 (e.g., compute digits = phoneNumber.replace(/\D/g, '').length) and
use that in the isNextEnabled expression along with the existing name.trim() and
isAllAgreed checks so the next button only enables when phoneNumber has 11
numeric digits.
In `@src/app/signup/designer/step2/page.tsx`:
- Around line 150-155: The submit Button currently always invokes
router.push("/signup/designer/step3") even when form.isSubmitEnabled is false;
update the onClick handler on the Button (and/or add a guard in the surrounding
click handler) to only call router.push when form.isSubmitEnabled is true (e.g.,
onClick={() => { if (!form.isSubmitEnabled) return;
router.push("/signup/designer/step3"); }}), matching the conditional guard used
in Step1/Step3 flows and preventing bypass of Step2 validation; reference the
Button component, its onClick and router.push usage to locate the change.
In `@src/app/signup/designer/step3/page.tsx`:
- Around line 43-49: The handler handlePortfolioFilesAdded currently only
filters by extension and count; add a size check to enforce the 30MB limit by
filtering files with file.size <= PORTFOLIO_MAX_FILE_SIZE (or define
PORTFOLIO_MAX_FILE_SIZE = 30 * 1024 * 1024 if missing) before slicing by
remainingCount, so only files that pass isPortfolioFile and the size constraint
are passed to handleFilesAdded; ensure any user-facing message/constant used
elsewhere is consistent with this limit.
In `@src/app/signup/instructor/step1/page.tsx`:
- Around line 169-186: Wrap the modal semantics onto the modal container: add
role="dialog" and aria-modal="true" to the section element (or outer modal
container) that renders the content, link the heading used by
selectedTerm.modalTitle via an id (e.g., set an id on the h2 and reference it
with aria-labelledby on the dialog) so the title is announced, and give the
CloseIcon button an accessible name (aria-label or aria-labelledby) instead of
relying on visual-only content; adjust references around setSelectedTermId,
selectedTerm.modalTitle and CloseIcon to locate and update the attributes.
- Around line 87-108: The term-toggle buttons currently only change visually;
update the button elements used for "모두 동의합니다" (onClick=toggleAllTerms, using
isAllAgreed) and each term button (onClick={() => toggleTerm(id)}, using
checkedTerms[id]) to include role="checkbox" and aria-checked set to the boolean
state (aria-checked={isAllAgreed} and aria-checked={checkedTerms[id]}
respectively), so assistive tech receives the checked state; ensure these
attributes are applied to the same element that handles the click (the button
wrapping the CheckIcon) and keep existing handlers (toggleAllTerms/toggleTerm)
and visual markup unchanged.
In `@src/app/signup/page.tsx`:
- Around line 37-45: The icons for the user-type cards are swapped: the card
with type="디자이너" currently uses UserTypeInstructorIcon and the card with
type="강사/교사" uses UserTypeDesignerIcon; update the JSX so the card whose type
prop is "designer" uses UserTypeDesignerIcon and the card whose type prop is
"instructor" uses UserTypeInstructorIcon, and verify each card's isSelected
(selectedType === "designer"/"instructor") and onClick
(setSelectedType("designer"/"instructor")) still match their respective type
strings; check the UserTypeBtn instances, UserTypeDesignerIcon,
UserTypeInstructorIcon, selectedType and setSelectedType references to make the
swap.
In `@src/components/common/Header.tsx`:
- Around line 31-38: The login/signup links are only rendered when isLoggedIn is
false but that flag is initialized to true causing the CTAs to be hidden; update
the Header component to derive authentication from the real auth state (e.g.,
useAuth(), authContext, or a prop like isAuthenticated) or, if this is a
temporary stub, change the initial value of isLoggedIn to false so the
onboarding CTA appears by default; locate the isLoggedIn variable/state used in
Header and adjust its initialization or replace it with the real auth check so
the Link elements for "/login" and "/signup" render correctly.
In `@src/constants/signup.ts`:
- Around line 100-103: The object with id "2" has mismatched text between label
("강사 약관 2") and modalTitle ("강사 약관 제목 3"); update the modalTitle in the signup
constants entry (the object containing id: "2", label, modalTitle, content) so
its modalTitle matches the label (e.g., "강사 약관 2") to avoid user confusion.
In `@src/lib/hooks/useSignupStep2Form.ts`:
- Around line 99-103: handleUserIdCheck currently calls
checkSignupUserIdAvailability(userId) and sets setUserIdCheckStatus based solely
on availability, allowing empty/short IDs to be marked "available"; update
handleUserIdCheck to first validate the userId length (use the same length rules
used elsewhere in this hook) and early-return or set status to "invalid" when it
fails, only calling checkSignupUserIdAvailability if length is valid; also
update the isSubmitEnabled logic to include the same length validation (in
addition to name/email/password checks and the userIdCheckStatus) so submission
cannot be enabled for empty/short IDs. Ensure you reference handleUserIdCheck,
checkSignupUserIdAvailability, setUserIdCheckStatus and the isSubmitEnabled
computation when making these changes.
- Around line 125-133: handleVerificationCodeChange currently sets verified
purely on code match, allowing success after the timer expires; update the
handler to also check the remaining timer before marking verified: after
computing nextCode and calling setVerificationCode, only call
setEmailVerificationStatus("verified") and setVerificationTimer(0) if
nextCode.trim() === SIGNUP_MOCK_EMAIL_VERIFICATION_CODE AND the
verificationTimer (or a derived remaining time state) indicates the timer has
not expired (e.g., verificationTimer > 0); if expired, ensure you reject the
match path (do not change status) and optionally set an expired state.
---
Nitpick comments:
In `@src/app/signup/designer/step1/page.tsx`:
- Around line 173-185: The modal <section> is missing basic dialog accessibility
attributes; add role="dialog" and aria-modal="true" to the section, give the
<h2> (selectedTerm.modalTitle) a unique id (e.g., modal-title-<termId>) and set
aria-labelledby on the section to that id, and add an accessible label to the
Close button (e.g., aria-label="Close dialog") where setSelectedTermId(null) is
used; ensure the existing onClick={event => event.stopPropagation()} remains and
that CloseIcon remains unchanged.
In `@src/app/signup/designer/step3/page.tsx`:
- Around line 69-75: Add inputMode="numeric" to the InputField usage to hint
numeric keyboard on mobile: update the JSX where InputField is rendered (the
instance with label="계좌번호", value={accountNumber},
onChange={handleAccountNumberChange}) to include inputMode="numeric"; if
InputField doesn't accept or forward unknown props, update the InputField
component's props interface to include inputMode?: string and forward it to the
underlying <input> (or native input element) so the prop actually takes effect.
In `@src/components/common/dropdown/BankDropdown.tsx`:
- Line 97: The div in BankDropdown uses an inline style (style={{ maxHeight:
BANK_DROPDOWN_MAX_HEIGHT }}) which violates the guideline; remove the inline
style and apply a Tailwind utility or a small CSS utility class instead — either
convert the constant BANK_DROPDOWN_MAX_HEIGHT to a matching Tailwind class
(e.g., max-h-... or arbitrary value like max-h-[...]), or create a dedicated
utility class (e.g., .bank-dropdown-max-h) and add that class to the div with
the existing "overflow-y-auto" so all styling is controlled via classes in the
BankDropdown component.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e5f41130-a22e-44e5-9b1a-d7d694c1f936
⛔ Files ignored due to path filters (7)
src/assets/icons/icon_step_one_designer.svgis excluded by!**/*.svgsrc/assets/icons/icon_step_one_instructor.svgis excluded by!**/*.svgsrc/assets/icons/icon_step_three_designer.svgis excluded by!**/*.svgsrc/assets/icons/icon_step_two_designer.svgis excluded by!**/*.svgsrc/assets/icons/icon_step_two_instructor.svgis excluded by!**/*.svgsrc/assets/icons/icon_user_type_designer.svgis excluded by!**/*.svgsrc/assets/icons/icon_user_type_instructor.svgis excluded by!**/*.svg
📒 Files selected for processing (14)
src/app/login/page.tsxsrc/app/signup/designer/step1/page.tsxsrc/app/signup/designer/step2/page.tsxsrc/app/signup/designer/step3/page.tsxsrc/app/signup/instructor/step1/page.tsxsrc/app/signup/instructor/step2/page.tsxsrc/app/signup/page.tsxsrc/assets/icons/index.tssrc/components/common/Header.tsxsrc/components/common/dropdown/BankDropdown.tsxsrc/components/common/input/InputField.tsxsrc/components/signup/UserTypeBtn.tsxsrc/constants/signup.tssrc/lib/hooks/useSignupStep2Form.ts
|
|
||
| const selectedTerm = DESIGNER_TERMS.find(({ id }) => id === selectedTermId); | ||
| const isAllAgreed = DESIGNER_TERMS.every(({ id }) => checkedTerms[id]); | ||
| const isNextEnabled = isAllAgreed && name.trim().length > 0 && phoneNumber.trim().length > 0; |
There was a problem hiding this comment.
전화번호 미완성 상태에서도 다음 단계로 진행됩니다.
현재 isNextEnabled가 전화번호의 “존재 여부”만 확인해서 010-1 같은 입력도 통과합니다. 최소한 숫자 11자리 완성 여부로 게이트하는 게 안전합니다.
제안 수정
- const isNextEnabled = isAllAgreed && name.trim().length > 0 && phoneNumber.trim().length > 0;
+ const phoneDigits = phoneNumber.replace(/\D/g, "");
+ const isNextEnabled =
+ isAllAgreed &&
+ name.trim().length > 0 &&
+ phoneDigits.length === SIGNUP_MAX_PHONE_NUMBER_LENGTH;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/signup/designer/step1/page.tsx` at line 50, isNextEnabled currently
only checks phoneNumber existence so incomplete inputs like "010-1" pass; update
the condition to validate that phoneNumber contains exactly 11 digits by
stripping non-digits and checking length === 11 (e.g., compute digits =
phoneNumber.replace(/\D/g, '').length) and use that in the isNextEnabled
expression along with the existing name.trim() and isAllAgreed checks so the
next button only enables when phoneNumber has 11 numeric digits.
| <Button | ||
| className="w-[232px]" | ||
| type="button" | ||
| variant={form.isSubmitEnabled ? "medium_primary" : "medium_disabled"} | ||
| onClick={() => router.push("/signup/designer/step3")} | ||
| > |
There was a problem hiding this comment.
제출 조건이 충족되지 않아도 Step3로 이동됩니다.
Line 154에서 항상 라우팅해서 Step2 검증을 우회할 수 있습니다. Step1/Step3처럼 조건 가드를 넣어 주세요.
제안 수정
<Button
className="w-[232px]"
type="button"
variant={form.isSubmitEnabled ? "medium_primary" : "medium_disabled"}
- onClick={() => router.push("/signup/designer/step3")}
+ onClick={() => {
+ if (form.isSubmitEnabled) router.push("/signup/designer/step3");
+ }}
>
다음
</Button>📝 Committable suggestion
‼️ 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.
| <Button | |
| className="w-[232px]" | |
| type="button" | |
| variant={form.isSubmitEnabled ? "medium_primary" : "medium_disabled"} | |
| onClick={() => router.push("/signup/designer/step3")} | |
| > | |
| <Button | |
| className="w-[232px]" | |
| type="button" | |
| variant={form.isSubmitEnabled ? "medium_primary" : "medium_disabled"} | |
| onClick={() => { | |
| if (form.isSubmitEnabled) router.push("/signup/designer/step3"); | |
| }} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/signup/designer/step2/page.tsx` around lines 150 - 155, The submit
Button currently always invokes router.push("/signup/designer/step3") even when
form.isSubmitEnabled is false; update the onClick handler on the Button (and/or
add a guard in the surrounding click handler) to only call router.push when
form.isSubmitEnabled is true (e.g., onClick={() => { if (!form.isSubmitEnabled)
return; router.push("/signup/designer/step3"); }}), matching the conditional
guard used in Step1/Step3 flows and preventing bypass of Step2 validation;
reference the Button component, its onClick and router.push usage to locate the
change.
| const handlePortfolioFilesAdded = (files: File[]) => { | ||
| const remainingCount = PORTFOLIO_MAX_FILE_COUNT - uploadedFiles.length; | ||
| if (remainingCount <= 0) return; | ||
|
|
||
| const portfolioFiles = files.filter(isPortfolioFile).slice(0, remainingCount); | ||
| if (portfolioFiles.length > 0) handleFilesAdded(portfolioFiles); | ||
| }; |
There was a problem hiding this comment.
포트폴리오 파일 크기(30MB) 제한 검증이 누락되어 있습니다.
현재는 확장자/개수만 제한하고 있어, 안내 문구와 실제 동작이 불일치합니다. handlePortfolioFilesAdded에서 크기 필터를 같이 적용해 주세요.
제안 수정
const PORTFOLIO_MAX_FILE_COUNT = 3;
const PORTFOLIO_ALLOWED_EXTENSIONS = [".pdf", ".png"];
+const PORTFOLIO_MAX_FILE_SIZE_BYTES = 30 * 1024 * 1024;
const isPortfolioFile = (file: File) => {
const fileName = file.name.toLowerCase();
- return PORTFOLIO_ALLOWED_EXTENSIONS.some(extension => fileName.endsWith(extension));
+ const isAllowedExtension = PORTFOLIO_ALLOWED_EXTENSIONS.some(extension =>
+ fileName.endsWith(extension),
+ );
+ const isAllowedSize = file.size <= PORTFOLIO_MAX_FILE_SIZE_BYTES;
+ return isAllowedExtension && isAllowedSize;
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/signup/designer/step3/page.tsx` around lines 43 - 49, The handler
handlePortfolioFilesAdded currently only filters by extension and count; add a
size check to enforce the 30MB limit by filtering files with file.size <=
PORTFOLIO_MAX_FILE_SIZE (or define PORTFOLIO_MAX_FILE_SIZE = 30 * 1024 * 1024 if
missing) before slicing by remainingCount, so only files that pass
isPortfolioFile and the size constraint are passed to handleFilesAdded; ensure
any user-facing message/constant used elsewhere is consistent with this limit.
| <button | ||
| type="button" | ||
| className="flex w-full cursor-pointer items-center gap-4 text-left" | ||
| onClick={toggleAllTerms} | ||
| > | ||
| <CheckIcon isChecked={isAllAgreed} /> | ||
| <span className="text-heading3-sb text-gray-90">모두 동의합니다</span> | ||
| </button> | ||
|
|
||
| <div className="bg-gray-20 h-px w-full" /> | ||
|
|
||
| <div className="flex flex-col gap-3"> | ||
| {INSTRUCTOR_TERMS.map(({ id, label }) => ( | ||
| <div key={id} className="flex items-center justify-between gap-4"> | ||
| <button | ||
| type="button" | ||
| className="flex min-w-0 cursor-pointer items-center gap-4 text-left" | ||
| onClick={() => toggleTerm(id)} | ||
| > | ||
| <CheckIcon isChecked={checkedTerms[id]} /> | ||
| <span className="text-heading3-m text-gray-90">{label}</span> | ||
| </button> |
There was a problem hiding this comment.
약관 동의 토글에 체크 상태 접근성 속성이 필요합니다.
현재는 시각적으로만 체크 상태가 바뀌고, 보조기기에는 상태가 전달되지 않습니다. 커스텀 체크 버튼에 role="checkbox"와 aria-checked를 추가해 주세요.
♿ 제안 코드
- <button
- type="button"
- className="flex w-full cursor-pointer items-center gap-4 text-left"
- onClick={toggleAllTerms}
- >
+ <button
+ type="button"
+ role="checkbox"
+ aria-checked={isAllAgreed}
+ className="flex w-full cursor-pointer items-center gap-4 text-left"
+ onClick={toggleAllTerms}
+ >
...
- <button
- type="button"
- className="flex min-w-0 cursor-pointer items-center gap-4 text-left"
- onClick={() => toggleTerm(id)}
- >
+ <button
+ type="button"
+ role="checkbox"
+ aria-checked={checkedTerms[id]}
+ aria-label={`${label} 동의`}
+ className="flex min-w-0 cursor-pointer items-center gap-4 text-left"
+ onClick={() => toggleTerm(id)}
+ >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/signup/instructor/step1/page.tsx` around lines 87 - 108, The
term-toggle buttons currently only change visually; update the button elements
used for "모두 동의합니다" (onClick=toggleAllTerms, using isAllAgreed) and each term
button (onClick={() => toggleTerm(id)}, using checkedTerms[id]) to include
role="checkbox" and aria-checked set to the boolean state
(aria-checked={isAllAgreed} and aria-checked={checkedTerms[id]} respectively),
so assistive tech receives the checked state; ensure these attributes are
applied to the same element that handles the click (the button wrapping the
CheckIcon) and keep existing handlers (toggleAllTerms/toggleTerm) and visual
markup unchanged.
| <div | ||
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-8" | ||
| onClick={() => setSelectedTermId(null)} | ||
| > | ||
| <section | ||
| className="rounded-12 flex h-[792px] w-[612px] flex-col gap-6 bg-white px-6 py-8" | ||
| onClick={event => event.stopPropagation()} | ||
| > | ||
| <header className="border-gray-20 flex items-center justify-between gap-4"> | ||
| <h2 className="text-heading2-sb text-black">{selectedTerm.modalTitle}</h2> | ||
| <button | ||
| type="button" | ||
| className="text-gray-80 flex size-6 shrink-0 cursor-pointer items-center justify-center hover:text-black" | ||
| onClick={() => setSelectedTermId(null)} | ||
| > | ||
| <CloseIcon aria-hidden="true" className="size-6" /> | ||
| </button> | ||
| </header> |
There was a problem hiding this comment.
약관 상세 모달에 dialog 접근성 시맨틱이 누락되었습니다.
모달 컨테이너에 role="dialog", aria-modal, 제목 연결을 추가하고 닫기 버튼에 접근 가능한 이름을 부여해 주세요.
♿ 제안 코드
- <section
- className="rounded-12 flex h-[792px] w-[612px] flex-col gap-6 bg-white px-6 py-8"
- onClick={event => event.stopPropagation()}
- >
+ <section
+ role="dialog"
+ aria-modal="true"
+ aria-labelledby="instructor-term-modal-title"
+ className="rounded-12 flex h-[792px] w-[612px] flex-col gap-6 bg-white px-6 py-8"
+ onClick={event => event.stopPropagation()}
+ >
...
- <h2 className="text-heading2-sb text-black">{selectedTerm.modalTitle}</h2>
+ <h2 id="instructor-term-modal-title" className="text-heading2-sb text-black">
+ {selectedTerm.modalTitle}
+ </h2>
...
- <button
- type="button"
- className="text-gray-80 flex size-6 shrink-0 cursor-pointer items-center justify-center hover:text-black"
- onClick={() => setSelectedTermId(null)}
- >
+ <button
+ type="button"
+ aria-label="약관 모달 닫기"
+ className="text-gray-80 flex size-6 shrink-0 cursor-pointer items-center justify-center hover:text-black"
+ onClick={() => setSelectedTermId(null)}
+ >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/signup/instructor/step1/page.tsx` around lines 169 - 186, Wrap the
modal semantics onto the modal container: add role="dialog" and
aria-modal="true" to the section element (or outer modal container) that renders
the content, link the heading used by selectedTerm.modalTitle via an id (e.g.,
set an id on the h2 and reference it with aria-labelledby on the dialog) so the
title is announced, and give the CloseIcon button an accessible name (aria-label
or aria-labelledby) instead of relying on visual-only content; adjust references
around setSelectedTermId, selectedTerm.modalTitle and CloseIcon to locate and
update the attributes.
| icon={<UserTypeInstructorIcon className="size-20" />} | ||
| type="디자이너" | ||
| description="외주를 맡고 싶어요!" | ||
| isSelected={selectedType === "designer"} | ||
| onClick={() => setSelectedType("designer")} | ||
| /> | ||
| <UserTypeBtn | ||
| icon={<UserTypeDesignerIcon className="size-20" />} | ||
| type="강사/교사" |
There was a problem hiding this comment.
회원 유형 아이콘이 선택 타입과 반대로 매핑되어 있습니다.
Line 37~48에서 designer 선택 카드에 강사 아이콘, instructor 선택 카드에 디자이너 아이콘이 들어가 있어 유형 인지가 뒤바뀝니다.
수정 제안
- icon={<UserTypeInstructorIcon className="size-20" />}
+ icon={<UserTypeDesignerIcon className="size-20" />}
@@
- icon={<UserTypeDesignerIcon className="size-20" />}
+ icon={<UserTypeInstructorIcon className="size-20" />}📝 Committable suggestion
‼️ 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.
| icon={<UserTypeInstructorIcon className="size-20" />} | |
| type="디자이너" | |
| description="외주를 맡고 싶어요!" | |
| isSelected={selectedType === "designer"} | |
| onClick={() => setSelectedType("designer")} | |
| /> | |
| <UserTypeBtn | |
| icon={<UserTypeDesignerIcon className="size-20" />} | |
| type="강사/교사" | |
| icon={<UserTypeDesignerIcon className="size-20" />} | |
| type="디자이너" | |
| description="외주를 맡고 싶어요!" | |
| isSelected={selectedType === "designer"} | |
| onClick={() => setSelectedType("designer")} | |
| /> | |
| <UserTypeBtn | |
| icon={<UserTypeInstructorIcon className="size-20" />} | |
| type="강사/교사" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/signup/page.tsx` around lines 37 - 45, The icons for the user-type
cards are swapped: the card with type="디자이너" currently uses
UserTypeInstructorIcon and the card with type="강사/교사" uses UserTypeDesignerIcon;
update the JSX so the card whose type prop is "designer" uses
UserTypeDesignerIcon and the card whose type prop is "instructor" uses
UserTypeInstructorIcon, and verify each card's isSelected (selectedType ===
"designer"/"instructor") and onClick (setSelectedType("designer"/"instructor"))
still match their respective type strings; check the UserTypeBtn instances,
UserTypeDesignerIcon, UserTypeInstructorIcon, selectedType and setSelectedType
references to make the swap.
| <Link href="/login" className="flex cursor-pointer flex-row items-center gap-1"> | ||
| <ProfileCircleIcon className="text-gray-70 hover:text-gray-80 size-6" /> | ||
| <span className="text-body2-m text-gray-80 hover:text-gray-90">로그인</span> | ||
| </button> | ||
| <button type="button" className="flex cursor-pointer flex-row items-center gap-1"> | ||
| </Link> | ||
| <Link href="/signup" className="flex cursor-pointer flex-row items-center gap-1"> | ||
| <EnterIcon className="text-gray-70 hover:text-gray-80 size-6" /> | ||
| <span className="text-body2-m text-gray-80 hover:text-gray-90">회원가입</span> | ||
| </button> | ||
| </Link> |
There was a problem hiding this comment.
로그인/회원가입 링크 분기가 초기 렌더에서 사실상 비활성입니다.
Line 31~38의 링크는 isLoggedIn이 false일 때만 보이는데, 현재 초기값이 true라 온보딩 진입 CTA가 기본 화면에서 노출되지 않습니다. 인증 상태 기반 값으로 대체하거나, 임시라면 기본값을 false로 두는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/common/Header.tsx` around lines 31 - 38, The login/signup
links are only rendered when isLoggedIn is false but that flag is initialized to
true causing the CTAs to be hidden; update the Header component to derive
authentication from the real auth state (e.g., useAuth(), authContext, or a prop
like isAuthenticated) or, if this is a temporary stub, change the initial value
of isLoggedIn to false so the onboarding CTA appears by default; locate the
isLoggedIn variable/state used in Header and adjust its initialization or
replace it with the real auth check so the Link elements for "/login" and
"/signup" render correctly.
| id: "2", | ||
| label: "강사 약관 2", | ||
| modalTitle: "강사 약관 제목 3", | ||
| content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2", |
There was a problem hiding this comment.
약관 제목 문구 불일치 수정 필요
label은 "강사 약관 2"인데 modalTitle은 "강사 약관 제목 3"으로 표시되어 사용자에게 혼동을 줍니다. 같은 항목 번호로 맞춰주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/constants/signup.ts` around lines 100 - 103, The object with id "2" has
mismatched text between label ("강사 약관 2") and modalTitle ("강사 약관 제목 3"); update
the modalTitle in the signup constants entry (the object containing id: "2",
label, modalTitle, content) so its modalTitle matches the label (e.g., "강사 약관
2") to avoid user confusion.
| const handleUserIdCheck = () => { | ||
| const isAvailable = checkSignupUserIdAvailability(userId); | ||
| setUserIdCheckStatus(isAvailable ? "available" : "duplicated"); | ||
| }; | ||
|
|
There was a problem hiding this comment.
아이디 길이 검증 없이 중복확인/제출 가능해집니다
현재는 길이가 유효하지 않아도 handleUserIdCheck로 available 상태가 될 수 있고, isSubmitEnabled도 길이 조건을 직접 보지 않아 빈/짧은 아이디로 제출 조건을 통과할 수 있습니다. 중복확인과 제출 조건 모두 길이 검증을 강제해야 합니다.
제안 수정안
const handleUserIdCheck = () => {
+ if (!isUserIdLengthValid) {
+ setUserIdCheckStatus("idle");
+ return;
+ }
const isAvailable = checkSignupUserIdAvailability(userId);
setUserIdCheckStatus(isAvailable ? "available" : "duplicated");
};
...
isSubmitEnabled:
- isUserIdAvailable && isPasswordValid && isPasswordConfirmValid && isEmailVerified,
+ isUserIdLengthValid &&
+ isUserIdAvailable &&
+ isPasswordValid &&
+ isPasswordConfirmValid &&
+ isEmailVerified,Also applies to: 162-163
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/hooks/useSignupStep2Form.ts` around lines 99 - 103, handleUserIdCheck
currently calls checkSignupUserIdAvailability(userId) and sets
setUserIdCheckStatus based solely on availability, allowing empty/short IDs to
be marked "available"; update handleUserIdCheck to first validate the userId
length (use the same length rules used elsewhere in this hook) and early-return
or set status to "invalid" when it fails, only calling
checkSignupUserIdAvailability if length is valid; also update the
isSubmitEnabled logic to include the same length validation (in addition to
name/email/password checks and the userIdCheckStatus) so submission cannot be
enabled for empty/short IDs. Ensure you reference handleUserIdCheck,
checkSignupUserIdAvailability, setUserIdCheckStatus and the isSubmitEnabled
computation when making these changes.
| const handleVerificationCodeChange = (event: ChangeEvent<HTMLInputElement>) => { | ||
| const nextCode = event.target.value; | ||
|
|
||
| setVerificationCode(nextCode); | ||
|
|
||
| if (nextCode.trim() === SIGNUP_MOCK_EMAIL_VERIFICATION_CODE) { | ||
| setEmailVerificationStatus("verified"); | ||
| setVerificationTimer(0); | ||
| } |
There was a problem hiding this comment.
인증 타이머 만료 후에도 코드가 인증됩니다
인증번호 일치만으로 verified 처리되어, 타이머가 0이어도 인증이 성공합니다. 만료 이후에는 인증 성공 처리되지 않도록 조건을 추가해야 합니다.
제안 수정안
const handleVerificationCodeChange = (event: ChangeEvent<HTMLInputElement>) => {
const nextCode = event.target.value;
setVerificationCode(nextCode);
- if (nextCode.trim() === SIGNUP_MOCK_EMAIL_VERIFICATION_CODE) {
+ if (
+ isVerificationTimerActive &&
+ emailVerificationStatus === "sent" &&
+ nextCode.trim() === SIGNUP_MOCK_EMAIL_VERIFICATION_CODE
+ ) {
setEmailVerificationStatus("verified");
setVerificationTimer(0);
}
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/hooks/useSignupStep2Form.ts` around lines 125 - 133,
handleVerificationCodeChange currently sets verified purely on code match,
allowing success after the timer expires; update the handler to also check the
remaining timer before marking verified: after computing nextCode and calling
setVerificationCode, only call setEmailVerificationStatus("verified") and
setVerificationTimer(0) if nextCode.trim() ===
SIGNUP_MOCK_EMAIL_VERIFICATION_CODE AND the verificationTimer (or a derived
remaining time state) indicates the timer has not expired (e.g.,
verificationTimer > 0); if expired, ensure you reject the match path (do not
change status) and optionally set an expired state.
| export { default as StepOneDesigner } from "@/assets/icons/icon_step_one_designer.svg"; | ||
| export { default as StepOneInstructor } from "@/assets/icons/icon_step_one_instructor.svg"; | ||
| export { default as StepThreeDesigner } from "@/assets/icons/icon_step_three_designer.svg"; | ||
| export { default as StepTwoDesigner } from "@/assets/icons/icon_step_two_designer.svg"; | ||
| export { default as StepTwoInstructor } from "@/assets/icons/icon_step_two_instructor.svg"; |
There was a problem hiding this comment.
다른 아이콘들이랑 컨벤션 맞춰서 끝에 Icon 붙여서 내보내면 좋을 것 같아요 !!
| /> | ||
| <InputField | ||
| label="전화번호" | ||
| inputMode="numeric" | ||
| maxLength={SIGNUP_MAX_PHONE_NUMBER_LENGTH + 2} | ||
| placeholder="010-0000-0000" | ||
| type="tel" | ||
| value={phoneNumber} | ||
| onClear={() => setPhoneNumber("")} | ||
| onChange={handlePhoneNumberChange} | ||
| /> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
여기 placeholder 피그마에는 전화번호를 입력해주세요 인데 디자인팀과 변경 합의된걸까요?!
There was a problem hiding this comment.
맵핑할 때 알아보려고 잠시 변경해놓은 부분인데 그대로 올린 것 같습니다..! 수정하겠습니닷!!
| }, | ||
| default: { | ||
| button: "border-gray-20 bg-gray-10 hover:border-purple-40 hover:bg-purple-40", | ||
| icon: "text-gray-60 group-hover:text-white", | ||
| type: "text-gray-60 group-hover:text-white", | ||
| description: "text-gray-60 group-hover:text-white", | ||
| }, |
There was a problem hiding this comment.
UserTypeBtn 호버액션 조금만 더 부드럽게 해주시면 좋을 것 같아용 지금 너무 팍팍 바뀌는 느낌이 나서 수정하면 좋을 것 같습니다
| }; | ||
|
|
||
| return ( | ||
| <div className={cn("relative w-full", className)} ref={dropdownRef}> | ||
| <button | ||
| aria-expanded={isOpen} | ||
| className={cn( | ||
| "rounded-8 border-gray-30 flex h-14 w-full cursor-pointer items-center justify-between border bg-white p-4 text-left outline-none", | ||
| "hover:bg-gray-10 focus-visible:bg-gray-10", | ||
| disabled && "bg-gray-10 cursor-not-allowed text-gray-50", | ||
| )} | ||
| disabled={disabled} | ||
| type="button" | ||
| onClick={() => setIsOpen(prev => !prev)} | ||
| > | ||
| <span |
| <div className="flex w-full items-center justify-between"> | ||
| <h1 className="text-title2-b text-black">회원가입</h1> | ||
| <StepOneInstructor className="h-8 w-[138px] shrink-0" /> | ||
| </div> |
| </div> | ||
|
|
||
| <InputField | ||
| label="이름" | ||
| maxLength={SIGNUP_MAX_NAME_LENGTH} | ||
| placeholder="이름을 입력해주세요" | ||
| value={name} | ||
| onClear={() => setName("")} | ||
| onChange={handleNameChange} | ||
| /> | ||
| <InputField | ||
| label="전화번호" | ||
| inputMode="numeric" | ||
| maxLength={SIGNUP_MAX_PHONE_NUMBER_LENGTH + 2} | ||
| placeholder="010-0000-0000" | ||
| type="tel" | ||
| value={phoneNumber} | ||
| onClear={() => setPhoneNumber("")} | ||
| onChange={handlePhoneNumberChange} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex w-full items-start justify-between"> |
There was a problem hiding this comment.
위의 코드들을 수정하고 보니 지금은 생기지 않는 것 같습니다! 저도 border가 생기면서 높이가 조금 늘어난 부분 때문일 것으로 예상했는데 이 현상이 발생하지 않아서 수정된 코드에서 확인부탁드립니다!!





📢 PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
파일 구조
회원가입
input을 생성하고 타이머 작동input은 인증번호가 올바르지 않으면 계속 에러 메세지 보여줌. 백엔드에서 인증번호를 4자리로 설정하였으나 입력input에 글자 수 제한을 걸지 않음input를 입력 그대로 비활성화📸 스크린샷 or 실행영상
1.mov
2.mov
3.mov
🎸 기타 사항 or 추가 코멘트
Summary by CodeRabbit
릴리스 노트