Conversation
Walkthrough새로운 EditProfile 페이지와 SelectionGroup 컴포넌트를 추가하고 관련 상수 및 mock 데이터를 도입했으며, 기존 profile 경로의 EditProfile을 제거하고 lazy 경로를 갱신했고, 여러 컴포넌트·스타일·라우트에서 연관된 UI·상태/검증 변화를 적용했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Router
participant EditPage as EditProfilePage
participant SG as SelectionGroup
participant API as Server (placeholder)
Router->>EditPage: /edit-profile 요청
EditPage->>EditPage: 초기값 설정 (mockEditData)
User->>SG: 옵션 클릭
SG-->>EditPage: onSelect(label)
EditPage->>EditPage: 상태 업데이트, isDirty 재계산
alt isDirty == true
User->>EditPage: "매칭 조건 수정" 클릭
EditPage->>API: (placeholder) 저장 호출
API-->>EditPage: 응답 (예정)
EditPage-->>User: 제출중 UI (버튼 비활성)
else isDirty == false
User-->>EditPage: 버튼 비활성 (무시)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
MATEBALL-STORYBOOK |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/shared/components/header/header.tsx (1)
8-12: SSR/메모리 라우터 호환성: window.location 대신 useLocation().search 사용 필요
urlParams를window.location.search에서 파생하고 있어 메모리 라우터(Storybook)나 SSR 환경에서 라우터 상태와 불일치할 수 있습니다. 동일 파일에서도 이미useLocation을 사용 중이므로search를 함께 구조분해하여 일관되게 처리하세요.- const { pathname } = useLocation(); - const urlParams = new URLSearchParams(location.search); + const { pathname, search } = useLocation(); + const urlParams = new URLSearchParams(search);
🧹 Nitpick comments (10)
src/pages/edit-profile/mocks/mockEditData.ts (1)
1-6: mockEditData 필드 명칭을 페이지 상태 키와 일치하도록 리팩터 제안현재
edit-profile.tsx에서 아래와 같이 mock 데이터 필드와 페이지 상태 키가 달라서 매핑 부가 로직이 필요합니다. 이로 인해 다음과 같은 부분에서 번거로운 매핑 코드가 발생합니다.
- 상태 초기화 및 useState 훅
const [gender, setGender] = useState(mockEditData.genderPreference);(18행)const [mateTeam, setMateTeam] = useState(mockEditData.teamAllowed || '상관없어요');(19행)const [viewStyle, setViewStyle] = useState(mockEditData.style);(20행)- 초기값 비교(dirty 체크)
gender: mockEditData.genderPreference,(25행)mateTeam: mockEditData.teamAllowed || '상관없어요',(26행)viewStyle: mockEditData.style,(27행)목(mock) 데이터의 필드명을 페이지의 상태 키(
mateTeam/viewStyle/gender)와 동일하게 맞추면 매핑 코드를 제거하고 타입 안정성을 높일 수 있습니다. 제안하는 인터페이스와 mock 데이터 변경 예시입니다.-export const mockEditData = { - team: '두산', - teamAllowed: null, - style: '직관먹방러', - genderPreference: '여성', -}; +export interface EditProfileData { + team: string; + mateTeam: string | null; + viewStyle: string; + gender: '남성' | '여성' | '상관없음'; +} + +export const mockEditData: EditProfileData = { + team: '두산', + mateTeam: null, + viewStyle: '직관먹방러', + gender: '여성', +};이렇게 변경하면
edit-profile.tsx에서는 매핑 없이 바로 다음과 같이 사용할 수 있습니다.const [gender, setGender] = useState(mockEditData.gender); const [mateTeam, setMateTeam] = useState(mockEditData.mateTeam || '상관없어요'); const [viewStyle, setViewStyle] = useState(mockEditData.viewStyle); … initRef.current = { gender: mockEditData.gender, mateTeam: mockEditData.mateTeam || '상관없어요', viewStyle: mockEditData.viewStyle, };이 PR에 타입 정의를 추가하고 mock 데이터 필드명을 일관되게 변경하면 DX와 유지보수성이 크게 개선될 것입니다.
src/shared/routes/lazy.ts (1)
22-22: EditProfile 경로 변경 확인 및 코드 승인레거시 임포트 잔여 없음 확인되었습니다. 경로 수정이 적절하며, 코드 변경을 승인합니다.
선택 사항으로 웹팩 청크 네이밍 주석 추가를 권장드립니다.• 레거시 임포트 경로 없음 확인 (rg 결과)
• 선택적 청크 네이밍 예시:-export const EditProfile = lazy(() => import('@pages/edit-profile/edit-profile')); +export const EditProfile = lazy(() => + import(/* webpackChunkName: "page-edit-profile" */ '@pages/edit-profile/edit-profile'), +);src/pages/edit-profile/components/selection-group.tsx (3)
12-14: 선택 버튼의 접근성(a11y) 개선: group/pressed 속성 추가 제안시각적 상태만으로는 스크린리더가 선택 상태를 파악하기 어렵습니다. 컨테이너에 role="group"과 라벨, 각 버튼에 aria-pressed를 부여하는 것을 권장합니다.
적용 diff:
- <div className="flex-col gap-[1.6rem]"> + <div className="flex-col gap-[1.6rem]" role="group" aria-label={title}> ... - <Button + <Button key={key} label={label} variant={isSelected ? 'skyblue' : 'gray2'} className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem] text-gray-900" + aria-pressed={isSelected} onClick={() => onSelect(label)} />Button 컴포넌트가 임의의 aria-* 속성을 그대로 전달하는지 한 번만 확인 부탁드립니다.
Also applies to: 21-27
25-25: 텍스트 색상 우선순위 충돌 가능성 — variant에 맡기고 class의 색상 제거 권장skyblue variant가 "!text-main-900"를 강제하도록 수정된 것으로 보입니다. 여기서 className의 "text-gray-900"가 불필요하거나 gray2일 때만 적용되는 등 혼선을 줄 수 있습니다. 색상은 variant에서 일관되게 관리하는 편이 안전합니다.
적용 diff:
- className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem] text-gray-900" + className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem]"
3-8: 옵션 value/size 확장: 동일 라벨 중복과 사이즈 제어 대응
- 라벨이 동일한 옵션이 존재할 경우 선택/키 비교가 애매해질 수 있습니다. 객체 옵션에 value를 허용하고, 선택 비교 및 onSelect에는 value를 사용하면 안전합니다.
- 버튼 사이즈를 외부에서 제어할 수 있도록 prop을 추가하면 재사용성이 올라갑니다.
적용 diff:
interface SelectionGroupProps { title: string; - options: { id: number; label: string }[] | string[]; + options: ({ id: number; label: string; value?: string } | string)[]; selectedValue: string; onSelect: (value: string) => void; + buttonSize?: 'setting_M' | 'setting_L'; } ... - const key = typeof option === 'string' ? option : option.id; - const label = typeof option === 'string' ? option : option.label; - const isSelected = selectedValue === label; + const key = typeof option === 'string' ? option : option.id; + const label = typeof option === 'string' ? option : option.label; + const value = typeof option === 'string' ? option : option.value ?? option.label; + const isSelected = selectedValue === value; ... - <Button + <Button key={key} label={label} variant={isSelected ? 'skyblue' : 'gray2'} - className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem] text-gray-900" - onClick={() => onSelect(label)} + className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem]" + size={buttonSize ?? 'setting_M'} + onClick={() => onSelect(value)} />Also applies to: 15-18, 21-27
src/pages/edit-profile/edit-profile.tsx (5)
19-20: 하드코딩된 '상관없어요' 상수화 필요 — NO_TEAM_OPTION 사용 권장초기값과 비교값에 하드코딩된 문자열이 섞여 있으면 상수 변경/다국어 대응 시 isDirty 계산이 어긋날 수 있습니다. NO_TEAM_OPTION을 사용해 일관성을 확보하세요.
적용 diff:
- const [mateTeam, setMateTeam] = useState(mockEditData.teamAllowed || '상관없어요'); + const [mateTeam, setMateTeam] = useState(mockEditData.teamAllowed || NO_TEAM_OPTION); ... - mateTeam: mockEditData.teamAllowed || '상관없어요', + mateTeam: mockEditData.teamAllowed || NO_TEAM_OPTION,Also applies to: 26-27
78-86: '응원팀' 선택 영역도 a11y 속성 적용 권장SelectionGroup에는 pressed/group을 적용했지만, 로컬 구현한 '응원팀' 리스트에도 동일하게 적용하면 접근성이 좋아집니다.
적용 diff:
- <div className="flex flex-wrap gap-[0.8rem]"> + <div className="flex flex-wrap gap-[0.8rem]" role="group" aria-label="응원팀"> {TEAMS.map((option) => { const selected = team === option; return ( <Button key={option} label={option} variant={selected ? 'skyblue' : 'gray2'} - className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem] text-gray-900" + className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem]" + aria-pressed={selected} onClick={() => setTeam(option)} /> ); })} <Button label={NO_TEAM_OPTION} variant={team === NO_TEAM_OPTION ? 'skyblue' : 'gray2'} - className="cap_14_sb w-fit px-[1.6rem] py-[0.6rem] text-gray-900" + className="cap_14_sb w-fit px-[1.6rem] py-[0.6rem]" + aria-pressed={team === NO_TEAM_OPTION} onClick={() => setTeam(NO_TEAM_OPTION)} />Also applies to: 94-100
21-21: 네이밍 가독성: isSubmit → isSubmitting 권장불리언 상태는 진행형 네이밍이 직관적입니다. 변경 시 전역 영향이 적고, 코드 읽기가 좋아집니다.
적용 diff:
- const [isSubmit, setIsSubmit] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false);그리고 사용처:
- setIsSubmit(true); + setIsSubmitting(true); ... - disabled={!isDirty || isSubmit} + disabled={!isDirty || isSubmitting}Also applies to: 44-44, 129-129
62-69: '한 줄 소개' 입력 필드: 플레이스홀더/검증 메시지 분리 예정 사항 재확인현재 닉네임 상수(NICKNAME_PLACEHOLDER/RULE_MESSAGE)를 재사용 중인데, PR 설명대로 후속 PR에서 교체될 항목이면 OK입니다. 누락 방지를 위해 TODO를 명시해 두는 것을 권장합니다.
적용 diff(주석 추가):
<Input placeholder={NICKNAME_PLACEHOLDER} label="한 줄 소개" defaultMessage={NICKNAME_RULE_MESSAGE} />→ 추후 전용 상수로 교체 예정임을 주석으로 남겨 주세요.
3-5: 오타 가능성: PROFILE_SYNK_MATE → PROFILE_SYNC_MATE?상수명이 SYNC가 아닌 SYNK로 보입니다. 오타라면 조기 교정이 유지보수 비용을 줄입니다. 변경 시 import 경로 및 사용처 일괄 치환이 필요합니다.
Also applies to: 103-109, 110-116, 117-123
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (8)
src/pages/edit-profile/components/selection-group.tsx(1 hunks)src/pages/edit-profile/constants/edit-profile.ts(1 hunks)src/pages/edit-profile/edit-profile.tsx(1 hunks)src/pages/edit-profile/mocks/mockEditData.ts(1 hunks)src/pages/profile/edit-profile/edit-profile.tsx(0 hunks)src/shared/components/button/button/styles/button-variants.ts(1 hunks)src/shared/components/header/header.tsx(1 hunks)src/shared/routes/lazy.ts(1 hunks)
💤 Files with no reviewable changes (1)
- src/pages/profile/edit-profile/edit-profile.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: heesunee
PR: MATEBALL/MATEBALL-CLIENT#78
File: src/pages/sign-up/utils/age-calculate.ts:1-4
Timestamp: 2025-07-09T09:08:46.371Z
Learning: heesunee는 나이 계산 시 한국식 나이 시스템을 사용하므로, 정확한 생일 기반 계산보다는 출생연도 기준의 간단한 계산 방식을 선호합니다.
📚 Learning: 2025-06-29T18:02:42.616Z
Learnt from: heesunee
PR: MATEBALL/MATEBALL-CLIENT#7
File: src/shared/routes/Router.tsx:4-4
Timestamp: 2025-06-29T18:02:42.616Z
Learning: The Home component file in the MATEBALL-CLIENT project was renamed from Home.tsx to home.tsx, so the correct import path is 'pages/home/home'.
Applied to files:
src/shared/routes/lazy.ts
🧬 Code graph analysis (6)
src/pages/edit-profile/constants/edit-profile.ts (2)
src/pages/profile/edit-profile/edit-profile.tsx (1)
div(1-3)src/pages/onboarding/components/sync-support-team.tsx (1)
option(18-26)
src/shared/components/header/header.tsx (5)
src/shared/routes/routes-config.ts (1)
ROUTES(1-22)src/shared/components/header/header.stories.tsx (1)
Story(11-33)src/shared/components/header/utils/get-header.tsx (1)
pathname(5-96)src/shared/routes/layout.tsx (1)
useLocation(9-31)src/pages/profile/edit-profile/edit-profile.tsx (1)
div(1-3)
src/pages/edit-profile/components/selection-group.tsx (3)
src/pages/onboarding/components/group-role.tsx (2)
OnboardingStepProps(6-36)option(22-32)src/pages/onboarding/components/support-team.tsx (1)
option(17-25)src/pages/onboarding/components/sync-support-team.tsx (2)
option(18-26)OnboardingStepProps(6-30)
src/shared/components/button/button/styles/button-variants.ts (2)
src/shared/components/button/button/button.stories.tsx (6)
size(110-112)size(98-100)size(122-124)div(94-127)size(116-118)size(104-106)src/shared/components/button/button/button.tsx (2)
ButtonProps(20-49)ButtonProps(8-18)
src/pages/edit-profile/mocks/mockEditData.ts (3)
src/shared/types/match-types.ts (5)
matchDetailMateSimple(213-221)matchDetailMate(78-86)baseMate(8-15)singleMatchMate(64-72)singleMatchMate(29-38)src/pages/match/hooks/mapMatchData.ts (1)
mate(9-14)src/pages/sign-up/components/profile-card.tsx (1)
ProfileCardProps(3-7)
src/pages/edit-profile/edit-profile.tsx (2)
src/pages/profile/edit-profile/edit-profile.tsx (1)
div(1-3)src/pages/profile/profile.tsx (1)
useQuery(8-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: chromatic
🔇 Additional comments (4)
src/shared/components/header/header.tsx (1)
16-24: EditProfile 경로 배경 추가 적용 LGTM
isEditProfile을 흰 배경 그룹에 포함시켜 레이아웃 일관성을 확보한 점 좋습니다. 라우트 상수와도 정합적입니다.src/pages/edit-profile/components/selection-group.tsx (1)
10-33: 컴포넌트 추출 굿 — 공통 선택 UI의 재사용성↑옵션 배열을 받아 공통 버튼 그룹으로 렌더링하는 접근이 명확하고 간결합니다. 이후 온보딩/프로필 등 다양한 페이지에서 일관성 유지에 도움이 되겠습니다.
src/pages/edit-profile/edit-profile.tsx (2)
30-39: 더티 상태 계산 로직 타이밍·의존성 적절 — 현재 요구사항과 부합초기값을 ref로 스냅샷해 비교하는 방식이 간결하고, 성능 최적화로 useMemo를 둔 것도 무리가 없습니다. 후속으로 실제 API 데이터 수신 시 초기값 갱신만 놓치지 않으면 됩니다.
실제 사용자 데이터로 초기 로드되는 시점에 아래처럼 초기값을 동기화하는 useEffect를 고려해 주세요(예: userInfo가 로드될 때):
useEffect(() => { if (!userInfo) return; setTeam(userInfo.team ?? NO_TEAM_OPTION); setGender(userInfo.genderPreference ?? ''); setMateTeam(userInfo.teamAllowed ?? NO_TEAM_OPTION); setViewStyle(userInfo.style ?? ''); initialValue.current = { team: userInfo.team ?? NO_TEAM_OPTION, gender: userInfo.genderPreference ?? '', mateTeam: userInfo.teamAllowed ?? NO_TEAM_OPTION, viewStyle: userInfo.style ?? '', }; }, [userInfo]);
126-131: Button 컴포넌트의ariaLabel지원이 정상적으로 확인되었습니다.
src/shared/components/button/button/button.tsx에서ButtonProps에ariaLabel?: string이 선언되어 있고,- 렌더링 시
<button>요소의aria-label={ariaLabel ?? label}로 매핑됩니다.따라서
<Button ariaLabel="매칭 조건 수정"…/>사용 시 의도한 대로aria-label="매칭 조건 수정"속성이 적용되어 접근성 요구사항이 충족됩니다.
| @@ -0,0 +1 @@ | |||
| export const PROFILE_SYNK_MATE = ['같은 팀 메이트', '상관없어요']; | |||
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
상수명 오타(SYNK→SYNC) 및 타입 협소화(as const) 제안
- 상수명에 오타가 있어 보입니다: PROFILE_SYNK_MATE → PROFILE_SYNC_MATE. 팀 내부 명명 규칙(영문 대문자 스네이크 케이스)과도 맞추면 좋겠습니다.
- 선택지 배열은
as const로 고정하면, SelectionGroup 등에서 보다 안전한 유니온 타입을 얻을 수 있어 오타 방지 및 DX가 개선됩니다.
아래처럼 수정 제안드립니다:
-export const PROFILE_SYNK_MATE = ['같은 팀 메이트', '상관없어요'];
+export const PROFILE_SYNC_MATE = ['같은 팀 메이트', '상관없어요'] as const;
+export type ProfileSyncMateOption = typeof PROFILE_SYNC_MATE[number];호출부/임포트 경로의 식별자도 함께 교체가 필요합니다. 빠르게 점검하려면:
🏁 Script executed:
#!/bin/bash
# PROFILE_SYNK_MATE 참조 지점 확인
rg -n "PROFILE_SYNK_MATE" -SLength of output: 341
PROFILE_SYNK_MATE 상수명 오타 수정 및 타입 고정 제안
프로필 편집 페이지 상수명에 오타가 있고, 배열을 고정(as const)하여 안전한 유니온 타입을 사용하는 것을 제안드립니다. 다음 두 부분을 수정해주세요:
-
src/pages/edit-profile/constants/edit-profile.ts
PROFILE_SYNK_MATE→PROFILE_SYNC_MATE- 배열 뒤에
as const추가 - 선택지를 표현하는 타입 추가
-
src/pages/edit-profile/edit-profile.tsx
- import 구문에서
PROFILE_SYNK_MATE→PROFILE_SYNC_MATE로 변경 options={PROFILE_SYNK_MATE}→options={PROFILE_SYNC_MATE}로 변경
- import 구문에서
제안하는 코드 예시:
--- src/pages/edit-profile/constants/edit-profile.ts
@@
-export const PROFILE_SYNK_MATE = ['같은 팀 메이트', '상관없어요'];
+export const PROFILE_SYNC_MATE = ['같은 팀 메이트', '상관없어요'] as const;
+export type ProfileSyncMateOption = typeof PROFILE_SYNC_MATE[number];--- src/pages/edit-profile/edit-profile.tsx
@@
-import { PROFILE_SYNK_MATE } from '@pages/edit-profile/constants/edit-profile';
+import { PROFILE_SYNC_MATE } from '@pages/edit-profile/constants/edit-profile';
@@
- options={PROFILE_SYNK_MATE}
+ options={PROFILE_SYNC_MATE}위와 같이 수정하시면 오타도 해결되고, as const를 통한 안전한 타입 활용으로 개발자 경험도 개선됩니다.
📝 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.
| export const PROFILE_SYNK_MATE = ['같은 팀 메이트', '상관없어요']; | |
| // src/pages/edit-profile/constants/edit-profile.ts | |
| export const PROFILE_SYNC_MATE = ['같은 팀 메이트', '상관없어요'] as const; | |
| export type ProfileSyncMateOption = typeof PROFILE_SYNC_MATE[number]; |
🤖 Prompt for AI Agents
In src/pages/edit-profile/constants/edit-profile.ts (line 1), rename the
constant PROFILE_SYNK_MATE to PROFILE_SYNC_MATE, append `as const` to the
exported array to make it readonly, and add a narrow union type (e.g., type
ProfileSyncMate = typeof PROFILE_SYNC_MATE[number]) to represent allowed values;
then update src/pages/edit-profile/edit-profile.tsx import to use
PROFILE_SYNC_MATE and change any usage from options={PROFILE_SYNK_MATE} to
options={PROFILE_SYNC_MATE}.
| blue: 'bg-main-900 text-white', | ||
| gray: 'bg-gray-400 text-white', | ||
| skyblue: 'bg-main-200 text-main-900', | ||
| skyblue: '!text-main-900 bg-main-200', |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
skyblue 변형의 !important 사용은 disabled 스타일링을 깨뜨릴 수 있습니다
!text-main-900는 Tailwind의 우선순위를 강제로 올려, Button 컴포넌트의 disabled 상태에서 의도한 텍스트 컬러 오버라이드(disabled:text-*)가 앞으로도 동작하지 않게 됩니다. 접근성(비활성 대비) 관점에서도 리스크가 있습니다. 우선순위 충돌을 피하기 위해 ! 제거를 권장합니다.
권장 변경:
- skyblue: '!text-main-900 bg-main-200',
+ skyblue: 'text-main-900 bg-main-200',그리고 Button 레벨에서 비활성 텍스트 컬러를 명시해두면 더 안전합니다(참고: src/shared/components/button/button/button.tsx):
- disabled && 'cursor-not-allowed bg-gray-400',
+ disabled && 'cursor-not-allowed bg-gray-400 text-gray-200',검증 스크립트:
🏁 Script executed:
#!/bin/bash
# !text-main-900 사용 중인 곳 점검
rg -n "!\s*text-main-900" src | sed -n '1,120p'
# disabled 상태에서 텍스트 컬러 지정 여부 점검
rg -n "disabled:.*text-" src/shared/components/buttonLength of output: 215
skyblue 변형: ! 우선순위 제거 및 disabled 텍스트 컬러 추가 필요
검증 결과, !text-main-900는 오직 아래 한 곳에만 사용되고 있으며(disabled 상태에서 텍스트 오버라이드가 동작하지 않는 원인이 됩니다), Button 컴포넌트 레벨에서 disabled 텍스트 컬러 지정도 되어 있지 않습니다.
src/shared/components/button/button/styles/button-variants.ts:10- skyblue: '!text-main-900 bg-main-200', + skyblue: 'text-main-900 bg-main-200',
src/shared/components/button/button/button.tsx(disabled 처리 부분)- disabled && 'cursor-not-allowed bg-gray-400', + disabled && 'cursor-not-allowed bg-gray-400 text-gray-200',
이 변경을 통해 disabled 상태에서도 의도한 텍스트 컬러 오버라이드가 정상 동작하며, 접근성 대비(비활성 대비) 리스크를 해소할 수 있습니다.
🤖 Prompt for AI Agents
In src/shared/components/button/button/styles/button-variants.ts around line 10,
the skyblue variant currently uses a forced-priority class `!text-main-900`
which prevents disabled-state text color overrides; remove the leading `!` so it
becomes `text-main-900`. Additionally, update
src/shared/components/button/button/button.tsx in the disabled handling to
explicitly set a disabled text color class (e.g., `text-disabled-...` or the
appropriate theme token) when `disabled` is true so the disabled color takes
precedence; ensure the disabled class is applied after variant classes or via
conditional concatenation to guarantee override.
* feat: signup-page funnel apply (#303) * feat: 약관 동의 페이지 퍼블리싱 (#303) * feat: arrow-right-18 svg add (#303) * feat: input className 받게 확장 (#303) * feat: 한줄소개 관련 유효성 추가 (#303) * feat: input textarea 분기 추가 (#303) * feat: 한줄소개 글자수 실시간 반영 (#303) * feat: 서비스 이용약관 체크박스 로직 추가 (#303) * feat: 이용약관 다음으로 이동 추가 (#303) * style: 정보입력 페이지 pb 수정 (#303) * feat: 체크박스 row 컴포넌트로 분리 (#303) * feat: 이용약관 페이지 checkboxrow 컴포넌트로 교체 (#303) * fix: 체크박스 클릭 함수 icon으로 이동 (#303) * refactor: maxLength 추가 (#303) * feat: 한줄소개 공백 입력 제거 (#303) * feat: checkbox button으로 감싸게 수정 (#303) --------- Co-authored-by: heesunee <heesun729@uos.ac.kr>
Deploying mateball-client with
|
| Latest commit: |
688a784
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://58d4c7e3.mateball-client.pages.dev |
| Branch Preview URL: | https://feat--307-profile-edit.mateball-client.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/pages/edit-profile/edit-profile.tsx (1)
41-47: 저장 버튼 영구 비활성화 및 초기값 동기화 누락(기존 코멘트 동일 이슈)성공/실패와 무관하게 isSubmit이 해제되지 않고, 저장 성공 시 initialValue 갱신이 없어 isDirty가 계속 true로 남습니다. 중복 클릭 방지도 필요합니다.
다음처럼 비동기/예외/초기값 동기화를 포함해 주세요:
- const handleSaveClick = () => { - if (!isDirty) return; - - setIsSubmit(true); - - // TODO: 실제 API 호출 - }; + const handleSaveClick = async () => { + if (!isDirty || isSubmit) return; + setIsSubmit(true); + try { + // TODO: 실제 API 호출 + // await api.updateMatchingCondition({ team, gender, mateTeam, viewStyle }); + // 성공 시 초기값 동기화 + initialValue.current = { team, gender, mateTeam, viewStyle }; + // TODO: 성공 토스트/네비게이션 + } catch (e) { + // TODO: 오류 토스트/로깅 + } finally { + setIsSubmit(false); + } + };
🧹 Nitpick comments (5)
src/pages/edit-profile/edit-profile.tsx (5)
112-116: 라벨 문자열을 상태로 보관하면 i18n/카피 변경에 취약 — id 기반으로 전환 권장VIEWING_STYLE, GENDER는 id/label 구조입니다. 현재는 label을 상태로 들고 있어 카피 변경 시 비교/저장 로직이 깨질 수 있습니다.
권장 방향(참고용 – 컴포넌트 외부 변경 포함):
- 상태를 viewStyleId, genderId로 전환
- selectedValue/옵션 비교는 id 기준
- API 입출력도 id로 매핑
Also applies to: 118-123
89-89: 버튼 텍스트 색상 클래스 중복 가능성variant가 텍스트 색을 지정한다면 추가 text-gray-900은 불필요하거나 우선순위 충돌 소지가 있습니다.
- className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem] text-gray-900" + className="cap_14_sb w-auto px-[1.6rem] py-[0.6rem]"- className="cap_14_sb w-fit px-[1.6rem] py-[0.6rem] text-gray-900" + className="cap_14_sb w-fit px-[1.6rem] py-[0.6rem]"Also applies to: 97-97
23-29: 비동기 초기 데이터 반영 시 initialValue와 상태 동기화 필요실데이터를 API로 불러오는 순간 초기 렌더의 mock 기준 값과 어긋날 수 있습니다. 데이터 로드 시점에 상태와 initialValue를 함께 재설정하세요.
원하는 경우, 데이터 로딩 훅/이펙트 스니펫을 제안해 드리겠습니다.
58-60: '수정' 버튼 클릭 핸들러 미구현닉네임/한 줄 소개 ‘수정’ 버튼에 onClick이 없어 UX가 혼란스러울 수 있습니다. 구현 전이라면 숨기거나 disabled 처리하세요.
원하시면 임시 토스트/모달 연결용 핸들러를 추가해 드립니다.
Also applies to: 67-69
127-132: 저장 버튼 로딩 표시/접근성 개선제출 중 로딩 인디케이터 또는 aria-busy 제공을 권장합니다. Button 컴포넌트가 loading prop을 지원하면 활용하세요.
예:
<Button onClick={handleSaveClick} label="매칭 조건 수정" disabled={!isDirty || isSubmit} ariaLabel="매칭 조건 수정" + // loading={isSubmit} // Button에서 지원 시 + // aria-busy={isSubmit} // 내부에서 전달 가능 시 />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/pages/edit-profile/components/selection-group.tsx(1 hunks)src/pages/edit-profile/edit-profile.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/pages/edit-profile/components/selection-group.tsx
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: heesunee
PR: MATEBALL/MATEBALL-CLIENT#78
File: src/pages/sign-up/utils/age-calculate.ts:1-4
Timestamp: 2025-07-09T09:08:46.371Z
Learning: heesunee는 나이 계산 시 한국식 나이 시스템을 사용하므로, 정확한 생일 기반 계산보다는 출생연도 기준의 간단한 계산 방식을 선호합니다.
🧬 Code graph analysis (1)
src/pages/edit-profile/edit-profile.tsx (6)
src/shared/routes/lazy.ts (1)
EditProfile(22-22)src/pages/edit-profile/mocks/mockEditData.ts (1)
mockEditData(1-6)src/pages/sign-up/constants/validation.ts (1)
NICKNAME_PLACEHOLDER(21-21)src/pages/sign-up/constants/NOTICE.ts (1)
NICKNAME_RULE_MESSAGE(9-9)src/pages/onboarding/constants/onboarding.ts (4)
TEAMS(13-24)NO_TEAM_OPTION(26-26)VIEWING_STYLE(30-46)GENDER(48-63)src/pages/edit-profile/constants/edit-profile.ts (1)
PROFILE_SYNK_MATE(1-1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: chromatic
🔇 Additional comments (1)
src/pages/edit-profile/edit-profile.tsx (1)
72-72: flex-col만 있어서는 동작하지 않음 — flex 누락현재 className에 flex가 없어 flex 레이아웃이 적용되지 않습니다.
- <section className="flex-col pb-[5.6rem]"> + <section className="flex flex-col pb-[5.6rem]">- <div className="flex-col gap-[3.2rem]"> + <div className="flex flex-col gap-[3.2rem]">- <div className="flex-col gap-[1.6rem]"> + <div className="flex flex-col gap-[1.6rem]">Also applies to: 78-80
⛔ Skipped due to learnings
Learnt from: heesunee PR: MATEBALL/MATEBALL-CLIENT#102 File: src/shared/components/bottom-sheet/bottom-sheet-indicator.tsx:5-5 Timestamp: 2025-07-10T09:58:36.134Z Learning: MATEBALL-CLIENT 프로젝트의 `src/shared/styles/custom-utilities.css`에서 `.flex-col` 클래스는 이미 `apply flex flex-col;`을 포함하고 있어서 별도로 `flex` 클래스를 추가할 필요가 없습니다. CSS 클래스 관련 제안을 하기 전에 반드시 커스텀 유틸리티 클래스 파일을 먼저 확인해야 합니다.Learnt from: heesunee PR: MATEBALL/MATEBALL-CLIENT#77 File: src/shared/components/card/match-card/components/card-header.tsx:56-56 Timestamp: 2025-07-08T13:43:15.684Z Learning: MATEBALL-CLIENT 프로젝트에서는 `src/shared/styles/custom-utilities.css`에 커스텀 유틸리티 클래스들이 정의되어 있습니다. 특히 `.flex-row` 클래스는 이미 `apply flex flex-row;`를 포함하고 있어서 별도로 `flex` 클래스를 추가할 필요가 없습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/pages/edit-profile/mocks/mockEditData.ts (3)
1-6: 모크 데이터의 프로덕션 번들 포함 방지mock 파일이 페이지 초기값에 직접 import 되면 프로덕션 빌드에 포함될 수 있습니다. dev 전용으로 한정하거나 Storybook/테스트 경로로 이동을 권장합니다.
권장 사항:
- 경로 이동: src/mocks/edit-profile.ts 혹은 src/fixtures/edit-profile.ts
- dev 전용 가드: (import.meta.env.DEV) 분기에서만 모크 사용
- 실제 API 연동 시 모크 import 제거할 TODO 주석 추가
1-6: 이름 가독성/의도 명확화(nit): 명명 일관성 유지mockEditData 보다는 EditProfileMock 등 구체적 명명이 검색성/의도를 높입니다. 파일명도 mockEditData.ts → edit-profile.mock.ts 처럼 컨벤션 정렬을 제안합니다.
-export const mockEditData = { +export const editProfileMock = {
2-5: 상수/enum 정의 후 mockEditData에 적용
src/pages/edit-profile/mocks/mockEditData.ts (2–5행)에서 하드코딩된 문자열 대신 도메인 코드(TEAM_PREF,VIEW_STYLE,GENDER등)와 라벨 매핑 상수/enum을 참조하도록 변경하세요. 해당 상수가 아직 정의되지 않았으니src/constants/profile.ts등 적절한 위치에 먼저 정의가 필요합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/pages/edit-profile/edit-profile.tsx(1 hunks)src/pages/edit-profile/mocks/mockEditData.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/pages/edit-profile/edit-profile.tsx
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: heesunee
PR: MATEBALL/MATEBALL-CLIENT#78
File: src/pages/sign-up/utils/age-calculate.ts:1-4
Timestamp: 2025-07-09T09:08:46.371Z
Learning: heesunee는 나이 계산 시 한국식 나이 시스템을 사용하므로, 정확한 생일 기반 계산보다는 출생연도 기준의 간단한 계산 방식을 선호합니다.
🔇 Additional comments (1)
src/pages/edit-profile/mocks/mockEditData.ts (1)
1-6: mockEditData 키명은 코드와 일치합니다
mockEditData의 team, teamAllowed, style, genderPreference 키가 useState 초기값과 initialValue.current의 프로퍼티(team, gender, mateTeam, viewStyle)에 정확히 매핑되어 있어 제안된 키 변경은 오히려 코드를 깨뜨립니다.Likely an incorrect or invalid review comment.
| export const mockEditData = { | ||
| team: '응원하는 팀이 없어요.', | ||
| teamAllowed: '상관없어요', | ||
| style: '직관먹방러', | ||
| genderPreference: '여성', | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
타입 안전성 보강: 명시적 폼 타입으로 검증(satisfies) 적용
mock 객체에 명시적 타입을 부여해 키 누락/오타를 컴파일 타임에 차단하세요. 기존 폼 타입이 있다면 import 하여 satisfies를 쓰는 걸 권장합니다.
+// TODO: 실제 폼 타입 경로로 교체
+import type { EditProfileFormValues } from '../types';
-export const mockEditData = {
+export const mockEditData = {
team: '응원하는 팀이 없어요.',
- teamAllowed: '상관없어요',
- style: '직관먹방러',
- genderPreference: '여성',
+ mateTeam: '상관없어요',
+ viewStyle: '직관먹방러',
+ gender: '여성',
-};
+} satisfies EditProfileFormValues;만약 공용 타입이 없다면, 모크에 중복 정의하지 말고 공용 타입을 새로 추가해 재사용하도록 제안합니다.
📝 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.
| export const mockEditData = { | |
| team: '응원하는 팀이 없어요.', | |
| teamAllowed: '상관없어요', | |
| style: '직관먹방러', | |
| genderPreference: '여성', | |
| }; | |
| // TODO: 실제 폼 타입 경로로 교체 | |
| import type { EditProfileFormValues } from '../types'; | |
| export const mockEditData = { | |
| team: '응원하는 팀이 없어요.', | |
| mateTeam: '상관없어요', | |
| viewStyle: '직관먹방러', | |
| gender: '여성', | |
| } satisfies EditProfileFormValues; |
🤖 Prompt for AI Agents
In src/pages/edit-profile/mocks/mockEditData.ts around lines 1 to 6, the mock
object is untyped which allows key typos or missing properties; import the
existing form type (e.g., EditProfileFormValues) from the canonical types file
and apply TypeScript's satisfies operator to the mock (mockEditData satisfies
EditProfileFormValues) so the compiler validates keys and value shapes; if no
shared form type exists, add a reusable type definition in the appropriate
shared/types or feature types file and then import and use satisfies instead of
duplicating the type inline.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/pages/edit-profile/components/selection-group.tsx (2)
23-23: key 충돌 가능성 (문자 옵션 중복 시)문자 옵션에 중복 라벨이 들어오면 key 충돌 위험이 있습니다. 정적 옵션이더라도 안전장치로 idx를 접두사로 추가하세요.
- {options.map((option) => { + {options.map((option, idx) => { @@ - key={value} + key={`${value}-${idx}`}Also applies to: 30-30
1-2: 불필요한 리렌더 줄이기: memo 적용SelectionGroup은 순수 프리젠테이션 성격이라 memo로 리렌더 비용을 줄일 수 있습니다.
import Button from '@components/button/button/button'; import { cn } from '@libs/cn'; +import { memo } from 'react'; @@ -export default SelectionGroup; +export default memo(SelectionGroup);Also applies to: 49-49
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/pages/edit-profile/components/selection-group.tsx(1 hunks)src/pages/edit-profile/edit-profile.tsx(1 hunks)src/shared/components/button/button/styles/button-variants.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/pages/edit-profile/edit-profile.tsx
- src/shared/components/button/button/styles/button-variants.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: heesunee
PR: MATEBALL/MATEBALL-CLIENT#78
File: src/pages/sign-up/utils/age-calculate.ts:1-4
Timestamp: 2025-07-09T09:08:46.371Z
Learning: heesunee는 나이 계산 시 한국식 나이 시스템을 사용하므로, 정확한 생일 기반 계산보다는 출생연도 기준의 간단한 계산 방식을 선호합니다.
🧬 Code graph analysis (1)
src/pages/edit-profile/components/selection-group.tsx (1)
src/shared/libs/cn.ts (1)
cn(4-6)
🔇 Additional comments (1)
src/pages/edit-profile/components/selection-group.tsx (1)
6-9: 위 옵션들은 모두 string[] 형태로 선언되어 있어, id/label 구조를 사용하지 않습니다. 선택 그룹(SelectionGroup) 컴포넌트는 현재 문자열(label)을 값으로 사용하므로, i18n이나 중복 라벨 문제는 코드상 발생하지 않습니다.
–PROFILE_SYNK_MATE,VIEWING_STYLE,GENDER모두string[](숫자 id 미사용)
– 상태(mateTeam,viewStyle,gender)와selectedValue/onSelect시그니처도 모두string기반따라서 제안된 id 기반 확장은 옵션 객체 배열 사용 시에만 필요하며, 현재 사용처에는 적용되지 않습니다.
| disabled, | ||
| }: SelectionGroupProps) => { | ||
| return ( | ||
| <div className="flex-col gap-[1.6rem]"> |
There was a problem hiding this comment.
flex 누락으로 레이아웃/간격 깨짐
gap이 적용되지 않고 수직 레이아웃도 동작하지 않습니다. flex를 추가하세요.
- <div className="flex-col gap-[1.6rem]">
+ <div className="flex flex-col gap-[1.6rem]">📝 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.
| <div className="flex-col gap-[1.6rem]"> | |
| <div className="flex flex-col gap-[1.6rem]"> |
🤖 Prompt for AI Agents
In src/pages/edit-profile/components/selection-group.tsx around line 20, the
wrapper div only has column and gap classes so the flex layout isn't enabled;
add the missing flex class to enable flexbox and make gap and vertical layout
work (i.e., include the 'flex' utility alongside the existing column and gap
utilities on that div).
| return ( | ||
| <div className="flex-col gap-[1.6rem]"> | ||
| <p className="body_16_m">{title}</p> | ||
| <div className="flex flex-wrap gap-[0.8rem]"> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
선택 그룹 컨테이너에 접근성 역할 부여 (radiogroup)
단일 선택 그룹이면 radiogroup을 부여하고 제목으로 레이블링하세요.
- <div className="flex flex-wrap gap-[0.8rem]">
+ <div
+ className="flex flex-wrap gap-[0.8rem]"
+ role="radiogroup"
+ aria-label={title}
+ >📝 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.
| <div className="flex flex-wrap gap-[0.8rem]"> | |
| <div | |
| className="flex flex-wrap gap-[0.8rem]" | |
| role="radiogroup" | |
| aria-label={title} | |
| > |
🤖 Prompt for AI Agents
In src/pages/edit-profile/components/selection-group.tsx around line 22, the
selection group container lacks an accessibility role and label; for a
single-selection group add role="radiogroup" to the div and provide a label via
aria-labelledby (preferred) or aria-label: give the group title element a stable
id (e.g. id="selection-group-title") and set
aria-labelledby="selection-group-title" on the div so assistive tech sees the
radiogroup and its title.
| <Button | ||
| key={key} | ||
| label={label} | ||
| variant={disabled ? 'disabled' : isSelected ? 'skyblue' : 'gray2'} | ||
| className={cn( | ||
| 'cap_14_sb w-auto px-[1.6rem] py-[0.6rem]', | ||
| disabled && 'cursor-not-allowed', | ||
| )} | ||
| onClick={() => { | ||
| if (disabled) return; | ||
| onSelect(label); | ||
| }} | ||
| /> |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Form 내 의도치 않은 submit + 비활성화/선택 상태의 ARIA 미부여
Button이 form 내부에 있으면 기본 type=submit로 동작할 수 있습니다. 또한 비활성/선택 상태를 스크린리더가 인지하도록 role/aria/disabled를 명시하세요. Button 컴포넌트가 native props를 포워딩한다는 전제입니다.
<Button
- key={key}
+ key={key}
label={label}
variant={disabled ? 'disabled' : isSelected ? 'skyblue' : 'gray2'}
className={cn(
'cap_14_sb w-auto px-[1.6rem] py-[0.6rem]',
disabled && 'cursor-not-allowed',
)}
+ type="button"
+ role="radio"
+ aria-checked={isSelected}
+ aria-disabled={disabled || undefined}
+ disabled={disabled}
+ data-selected={isSelected || undefined}
onClick={() => {
if (disabled) return;
onSelect(label);
}}
/>검증용 스크립트(위 변경이 유효하려면 Button이 해당 props를 지원해야 합니다):
🏁 Script executed:
#!/bin/bash
# Button 컴포넌트가 HTMLButtonElement props(type/disabled/aria-*)를 포워딩하는지 확인
fd -a -t f -E node_modules '(?i)button\.(tsx|jsx)$'
rg -n -C2 -P 'export\s+(default\s+)?function\s+Button|type\s+ButtonProps|interface\s+ButtonProps' $(fd -t f -E node_modules '(?i)button\.(tsx|jsx)$')
rg -n -C2 -P '\bdisabled\b|\btype\??:\s*["'\'']?(button|submit)?' $(fd -t f -E node_modules '(?i)button\.(tsx|jsx)$')Length of output: 2836
Button 컴포넌트에 네이티브 버튼·ARIA 속성 포워딩 리팩터링 필요
- form 내부 버튼이 기본 submit 동작을 하지 않도록 type="button" 명시 전달
- disabled 상태가 네이티브 disabled 속성으로 반영되도록 disabled={disabled} 전달 (스크린리더는 aria-disabled 대신 native disabled를 우선 인식)
- role, aria-checked, aria-disabled, data-selected 등 추가 ARIA 속성을 전달하려면 ButtonProps를 React.ButtonHTMLAttributes 로 확장하고 {...rest} 을 내부 에 스프레드하여 포워딩 구현
🤖 Prompt for AI Agents
In src/pages/edit-profile/components/selection-group.tsx around lines 29 to 41,
the Button usage must forward native button/ARIA attributes: pass type="button"
to avoid implicit form submits, pass disabled={disabled} so the native disabled
attribute is set, and forward role, aria-checked, aria-disabled, data-selected,
etc. by extending ButtonProps with React.ButtonHTMLAttributes<HTMLButtonElement>
and collecting {...rest} in the parent; then spread {...rest} onto the internal
<button> inside Button (ensuring onClick still no-ops when disabled). Update
prop types and the Button implementation to accept and forward these attributes.
* style: footer 수정 (#305) * style: 마이페이지 제목, 버튼 이름 수정 (#305) * feat: divider 컴포넌트 추가 (#305) * feat: divider 추가 (#305) * feat: 문의하기, 의견 보내기 구글폼 연결 (#305) * feat: logout api 추가 (#305) * feat: 로그아웃 api 연결 (#305) * feat: 로그아웃 요청 성공시 reload 추가 (#305) * feat: 문의하기/의견보내기 링크 연결 (#305) * refactor: 링크 이동 a 태그로 변경 (#305) * feat: LOGOUT시 쿼리 삭제, 취소 (#305) * feat: 로그아웃 커서 추가 (#305) * feat: 로그아웃 api 실패시 로직 추가 (#307) * feat: logout api 실패 로직 추가 (#305) --------- Co-authored-by: heesunee <heesun729@uos.ac.kr>
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
src/pages/edit-profile/constants/edit-profile.ts (1)
1-1: as const 추가 및 유니온 타입 공개로 타입 안정성 강화SelectionGroup 옵션으로 쓰이므로 리터럴 고정과 선택지 타입을 외부에 노출하는 편이 안전합니다. 이전 코멘트의 follow-up입니다.
-export const PROFILE_SYNC_MATE = ['같은 팀 메이트', '상관없어요']; +export const PROFILE_SYNC_MATE = ['같은 팀 메이트', '상관없어요'] as const; +export type ProfileSyncMateOption = typeof PROFILE_SYNC_MATE[number];빠른 점검 스크립트(남아있을 수 있는 이전 식별자 사용처 확인):
#!/bin/bash set -euo pipefail # 기존 오타 식별자 사용 흔적이 있는지 점검 rg -n -S '\bPROFILE_SYNK_MATE\b' || echo "OK: no old identifier usages" # 새 상수 사용 컨텍스트 확인 rg -n -S '\bPROFILE_SYNC_MATE\b' -C2
🧹 Nitpick comments (18)
src/shared/components/card/match-card/styles/card-variants.ts (1)
12-12: 1px 아웃라인 제거 → 브라우저 기본 두께로 변동 가능. 스펙 고정 권장
outline-[1px]가 빠져 두께가 브라우저 기본값(환경에 따라 2px/medium 등)으로 렌더링될 수 있어 시각적 일관성이 깨질 수 있습니다. 기존 디자인이 1px을 의도했다면 두께를 명시해 주세요. 또한 outline을 유지한다면outline-offset도 명시하면 좋습니다.제안 1(권장: 현재 접근 유지):
- active: 'bg-main-200 outline outline-main-600', + active: 'bg-main-200 outline outline-[1px] outline-main-600', + // 필요 시: 'outline-offset-0' 추가로 오프셋 고정제안 2(대안: ring로 일관된 모양, rounded와 궁합 좋음):
- active: 'bg-main-200 outline outline-main-600', + active: 'bg-main-200 ring-1 ring-main-600',확인 요청:
- 피그마/디자인 토큰에서 활성 상태 테두리 두께가 1px인지 확인.
- macOS/iOS Safari와 Chrome에서 두께/모서리(rounded) 시각이 동일한지 스폿 체크.
src/pages/sign-up/constants/NOTICE.ts (1)
17-17: 문구 다듬기 제안: 더 자연스러운 안내 톤현재 문구는 명사형으로 끝나 어색합니다. 사용자 안내 톤에 맞춰 서술형으로 다듬는 것을 제안드립니다.
-export const INFORMATION_RULE_MESSAGE = '비방, 욕설 등 불쾌감을 줄 수 있는 내용 제한'; +export const INFORMATION_RULE_MESSAGE = '비방·욕설 등 타인에게 불쾌감을 줄 수 있는 내용은 제한돼요.';src/pages/sign-up/constants/validation.ts (1)
39-42: 리터럴 유니온 유지 위해 as const 권장SIGNUP_STEPS가 단순 string[]이 되면 추후 단계 분기에서 타입 이점이 사라집니다. as const로 고정하고 필요하면 유니온 타입을 노출하세요.
-export const SIGNUP_STEPS = ['AGREEMENT', 'INFORMATION']; +export const SIGNUP_STEPS = ['AGREEMENT', 'INFORMATION'] as const;추가로 단계 타입을 노출하려면:
export type SignupStep = typeof SIGNUP_STEPS[number];src/shared/components/input/input.tsx (1)
62-77: Textarea 전용 속성 노출(타이핑) 개선 제안현 타입은
InputHTMLAttributes<HTMLInputElement>기반이라 multiline 사용 시rows등 textarea 전용 속성을 넘기기 어렵습니다. 분기 유니온 타입으로 개선을 권장합니다.type SingleLineProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'onBlur'> & { multiline?: false; }; type MultiLineProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onBlur'> & { multiline: true; }; type InputProps = (SingleLineProps | MultiLineProps) & { label?: string; isError?: boolean; isValid?: boolean; hasLength?: boolean; maxLength?: number; defaultMessage?: string; validationMessage?: string; className?: string; length?: number; onBlur?: (e: React.FocusEvent<HTMLInputElement> | React.FocusEvent<HTMLTextAreaElement>) => void; };src/pages/sign-up/schema/validation-schema.ts (1)
42-42: information 필드 에러 메시지 로컬라이즈/일관화 제안nickname/birthYear와 달리 information은 min/max에 커스텀 메시지가 없어 기본(영문) 메시지가 노출될 수 있습니다. NOTICE/validation 상수에 맞춘 한글 메시지를 명시해 주세요.
다음과 같이 수정을 제안합니다:
- information: z.string().trim().min(INFORMATION_MIN_LENGTH).max(INFORMATION_MAX_LENGTH), + information: z + .string() + .trim() + .min(INFORMATION_MIN_LENGTH, { message: '최소 1자 이상 입력해주세요.' }) + .max(INFORMATION_MAX_LENGTH, { message: `최대 ${INFORMATION_MAX_LENGTH}자까지 입력 가능합니다.` }),src/pages/sign-up/components/checkbox-row.tsx (1)
14-33: 체크박스 역할 명시로 접근성 강화현재 커스텀 버튼이므로 role/aria-checked를 쓰면 스크린리더 호환성이 좋아집니다.
- <button + <button type="button" + role="checkbox" + aria-checked={checked}src/pages/sign-up/components/agreement-step.tsx (2)
16-20: 프로퍼티 next와 지역변수 next 이름 충돌(섀도잉) 피하기handleCheckAll 내부의 next가 props.next를 가립니다. 혼동을 줄이기 위해 변수명을 변경해 주세요.
- const handleCheckAll = () => { - const next = !isAllChecked; - setTerms(next); - setPrivacy(next); - }; + const handleCheckAll = () => { + const nextChecked = !isAllChecked; + setTerms(nextChecked); + setPrivacy(nextChecked); + };
33-67: flex 누락 가능성 — 컨테이너 레이아웃 확인여기도 'flex-col'만 사용되었습니다. 공통 유틸이 없다면 flex 추가가 필요합니다.
- <div className="h-full flex-col justify-between pt-[4.8rem] pb-[2.4rem]"> + <div className="h-full flex flex-col justify-between pt-[4.8rem] pb-[2.4rem]"> ... - <div className="flex-col gap-[2.4rem]"> + <div className="flex flex-col gap-[2.4rem]"> - <div className="flex-col gap-[0.8rem]"> + <div className="flex flex-col gap-[0.8rem]">src/pages/sign-up/components/signup-step.tsx (4)
171-177: 중복 제출 방지: 전송 중 버튼 비활성화isValid만으로는 빠른 연타 시 중복 호출이 가능할 수 있습니다. 뮤테이션 진행 중에도 비활성화해 주세요.
- <Button + <Button label="가입하기" className="w-full" ariaLabel="가입하기" type="submit" - disabled={!isValid} + disabled={ + !isValid || nicknameMutation.isPending || userInfoMutation.isPending + } />
54-55: 불필요한 nullish 병합 연산자문자열 length는 항상 number입니다. 간단히 정리 가능합니다.
- const informationLength = informationValue.length ?? 0; + const informationLength = informationValue.length;
105-106: 폼 컨테이너 flex 확인레이아웃 의도대로라면 flex 추가가 필요할 수 있습니다.
- className="h-full w-full flex-col justify-between gap-[4rem] px-[1.6rem] pt-[4rem] pb-[1.6rem]" + className="h-full w-full flex flex-col justify-between gap-[4rem] px-[1.6rem] pt-[4rem] pb-[1.6rem]"
56-82: information 값 서버 전송 여부 확인UI에서 정보를 수집하지만 USER_INFO/닉네임 API 전송 페이로드엔 포함되지 않습니다. 저장 대상이 맞는지 확인 필요합니다. 추후 전송이 필요하면 mutateAsync로 비동기 흐름을 평탄화하는 것도 고려해 주세요.
예시(참고용):
- const onSubmit = (data: NicknameFormValues) => { - nicknameMutation.mutate( - { nickname: data.nickname }, - { - onSuccess: () => { - userInfoMutation.mutate( - { - gender: data.gender, - birthYear: Number(data.birthYear), - }, - { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: authQueries.USER_STATUS().queryKey }); - navigate(ROUTES.HOME); - }, - onError: (error) => { - console.error(error); - }, - }, - ); - }, - onError: (error) => { - console.error(error); - }, - }, - ); - }; + const onSubmit = async (data: NicknameFormValues) => { + try { + await nicknameMutation.mutateAsync({ nickname: data.nickname }); + await userInfoMutation.mutateAsync({ + gender: data.gender, + birthYear: Number(data.birthYear), + // information: data.information, // 전송 대상이면 API 스키마에 맞춰 추가 + }); + await queryClient.invalidateQueries({ queryKey: authQueries.USER_STATUS().queryKey }); + navigate(ROUTES.HOME); + } catch (error) { + console.error(error); + } + };src/pages/profile/constants/link.ts (1)
1-3: 외부 링크 상수 환경 변수/설정 모듈 이동 권장
운영 중 URL 변경에 대비해 해당 상수를.env또는 중앙 설정 모듈로 옮기고, 정의부에 목적·소유 주체·변경 안내 주석을 추가해 주세요. 프로필 페이지의<a>요소에는 이미rel="noopener noreferrer"가 적용되어 있습니다.src/shared/components/divider/divider.tsx (2)
21-28: 접근성: 구분선 역할 명시시맨틱 분리를 위해 role="separator"와 aria-orientation을 부여하는 것을 권장합니다.
아래 패치 제안:
- <div + <div + role="separator" + aria-orientation={isHorizontal ? 'horizontal' : 'vertical'} className={clsx( isHorizontal ? `w-full ${margin || 'my-4'}` : `h-full ${margin || 'mx-4'}`, color, className, )} style={isHorizontal ? { height: `${thickness}rem` } : { width: `${thickness}rem` }} />
3-9: 유연성: style 병합 지원소비자가 길이/여백 등 세부 스타일을 미세 조정할 수 있도록 style prop을 받아 기존 계산 값과 병합하면 재사용성이 좋아집니다.
다음 변경을 적용해 주세요:
interface DividerProps { direction?: 'horizontal' | 'vertical'; color?: string; // tailwind 클래스 ex: bg-gray-200 thickness?: number; // rem 단위 숫자 ex: 0.1 -> 0.1rem margin?: string; // tailwind 마진 클래스 - className?: string; + className?: string; + style?: CSSProperties; } const Divider = ({ direction = 'horizontal', color = 'bg-gray-200', thickness = 0.1, margin, - className, + className, + style, }: DividerProps) => { const isHorizontal = direction === 'horizontal'; return ( <div className={clsx( isHorizontal ? `w-full ${margin || 'my-4'}` : `h-full ${margin || 'mx-4'}`, color, className, )} - style={isHorizontal ? { height: `${thickness}rem` } : { width: `${thickness}rem` }} + style={ + isHorizontal + ? { height: `${thickness}rem`, ...style } + : { width: `${thickness}rem`, ...style } + } /> );추가로 파일 상단에 타입 임포트가 필요합니다:
import type { CSSProperties } from 'react';Also applies to: 11-17, 21-28
src/shared/apis/user/user-mutations.ts (1)
22-34: 로그아웃 후 캐시 정리 범위·타입 정밀화 제안
- 현재
queryClient.removeQueries({ queryKey: USER_KEY.ALL })로 ‘user’ 키만 클리어됩니다. 로그아웃 직후 사용자 컨텍스트에 의존하는 다른 캐시(e.g. 경기·매칭 등)가 있다면 함께 제거하거나, 영향 범위 검토 후queryClient.clear()전역 클리어를 고려하세요.- 서버 로그아웃 API가 204 No Content를 반환한다면,
mutationOptions<void, Error, void>로 첫 번째 제네릭을void로 지정하는 편이 더 정확합니다.LOGOUT: () => - mutationOptions<responseTypes, Error, void>({ + mutationOptions<void, Error, void>({ mutationKey: USER_KEY.LOGOUT(), mutationFn: () => post(END_POINT.POST_AUTH_LOGOUT), onSuccess: async () => { await queryClient.cancelQueries({ queryKey: USER_KEY.ALL }); queryClient.removeQueries({ queryKey: USER_KEY.ALL }); }, onError: (err) => { console.error('로그아웃 실패', err); }, }),백엔드 API 문서나 스펙에서 로그아웃 엔드포인트의 HTTP 상태 코드 및 응답 바디 유무를 확인해 주세요.
src/pages/profile/profile.tsx (2)
14-18: 로그아웃 UX: 성공 후 리다이렉트 및 중복 클릭 방지
- 현재 성공 후 화면 전환이 없어, 캐시 제거 뒤 공백 화면이 될 수 있습니다.
- pending 동안 버튼 비활성화로 중복 요청 방지 권장.
아래처럼 onSuccess에서 리다이렉트하고 pending 상태를 노출/비활성화하세요:
const navigate = useNavigate(); const { data } = useQuery(userQueries.USER_INFO()); - const { mutate: logout } = useMutation(userMutations.LOGOUT()); + const logoutOptions = userMutations.LOGOUT(); + const { mutate: logout, isPending: isLogoutPending } = useMutation({ + ...logoutOptions, + onSuccess: async (data, variables, context) => { + await logoutOptions.onSuccess?.(data, variables, context); + navigate(ROUTES.LOGIN, { replace: true }); + }, + });
63-70: 로그아웃 버튼: 로딩/비활성화 상태 반영중복 클릭 방지 및 상태 표시가 필요합니다.
- <button + <button type="button" - onClick={() => logout()} + onClick={() => logout()} + disabled={isLogoutPending} aria-label="로그아웃" - className="cap_14_m cursor-pointer py-[0.8rem] text-gray-800" + aria-busy={isLogoutPending} + className="cap_14_m cursor-pointer py-[0.8rem] text-gray-800 disabled:opacity-50" > - <p>로그아웃</p> + <p>{isLogoutPending ? '로그아웃 중…' : '로그아웃'}</p> </button>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (2)
src/shared/assets/svgs/arrow-right-18.svgis excluded by!**/*.svgsrc/shared/assets/svgs/check-filled.svgis excluded by!**/*.svg
📒 Files selected for processing (24)
src/pages/edit-profile/constants/edit-profile.ts(1 hunks)src/pages/edit-profile/edit-profile.tsx(1 hunks)src/pages/profile/constants/link.ts(1 hunks)src/pages/profile/profile.tsx(2 hunks)src/pages/sign-up/components/agreement-step.tsx(1 hunks)src/pages/sign-up/components/checkbox-row.tsx(1 hunks)src/pages/sign-up/components/signup-step.tsx(4 hunks)src/pages/sign-up/constants/NOTICE.ts(1 hunks)src/pages/sign-up/constants/validation.ts(2 hunks)src/pages/sign-up/schema/validation-schema.ts(2 hunks)src/pages/sign-up/sign-up.tsx(1 hunks)src/shared/apis/user/user-mutations.ts(2 hunks)src/shared/components/button/button/styles/button-variants.ts(1 hunks)src/shared/components/card/match-card/styles/card-variants.ts(1 hunks)src/shared/components/divider/divider.tsx(1 hunks)src/shared/components/footer/constants/legal.ts(0 hunks)src/shared/components/footer/footer.tsx(2 hunks)src/shared/components/header/utils/get-header.tsx(1 hunks)src/shared/components/input/input.tsx(3 hunks)src/shared/components/input/styles/input-variants.ts(1 hunks)src/shared/constants/api.ts(1 hunks)src/shared/constants/icon-list.ts(1 hunks)src/shared/constants/query-key.ts(1 hunks)src/shared/styles/custom-utilities.css(0 hunks)
💤 Files with no reviewable changes (2)
- src/shared/components/footer/constants/legal.ts
- src/shared/styles/custom-utilities.css
🚧 Files skipped from review as they are similar to previous changes (2)
- src/pages/edit-profile/edit-profile.tsx
- src/shared/components/button/button/styles/button-variants.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: heesunee
PR: MATEBALL/MATEBALL-CLIENT#78
File: src/pages/sign-up/utils/age-calculate.ts:1-4
Timestamp: 2025-07-09T09:08:46.371Z
Learning: heesunee는 나이 계산 시 한국식 나이 시스템을 사용하므로, 정확한 생일 기반 계산보다는 출생연도 기준의 간단한 계산 방식을 선호합니다.
📚 Learning: 2025-07-09T17:24:19.755Z
Learnt from: heesunee
PR: MATEBALL/MATEBALL-CLIENT#94
File: src/pages/sign-up/sign-up.tsx:4-4
Timestamp: 2025-07-09T17:24:19.755Z
Learning: heesunee는 src/pages/sign-up/sign-up.tsx의 isNicknameStep 하드코딩된 값을 퍼널 패턴으로 카카오 통합과 함께 나중에 처리할 예정이므로, 이 부분에 대해 다시 언급하지 않아야 합니다.
Applied to files:
src/pages/sign-up/sign-up.tsxsrc/pages/sign-up/components/signup-step.tsx
🧬 Code graph analysis (7)
src/pages/sign-up/components/checkbox-row.tsx (1)
src/shared/libs/cn.ts (1)
cn(4-6)
src/pages/profile/profile.tsx (4)
src/shared/apis/user/user-queries.ts (1)
userQueries(7-21)src/shared/apis/user/user-mutations.ts (1)
userMutations(9-35)src/shared/routes/routes-config.ts (1)
ROUTES(1-22)src/pages/profile/constants/link.ts (2)
REQUEST_LINK(1-1)FEEDBACK_LINK(3-3)
src/pages/sign-up/sign-up.tsx (3)
src/shared/hooks/use-funnel.tsx (1)
useFunnel(14-78)src/pages/sign-up/constants/validation.ts (1)
SIGNUP_STEPS(39-39)src/shared/routes/routes-config.ts (1)
ROUTES(1-22)
src/shared/apis/user/user-mutations.ts (3)
src/shared/types/base-types.ts (1)
responseTypes(7-10)src/shared/constants/query-key.ts (1)
USER_KEY(2-9)src/shared/constants/api.ts (1)
END_POINT(3-39)
src/shared/components/input/input.tsx (1)
src/shared/libs/cn.ts (1)
cn(4-6)
src/pages/sign-up/schema/validation-schema.ts (1)
src/pages/sign-up/constants/validation.ts (2)
INFORMATION_MIN_LENGTH(41-41)INFORMATION_MAX_LENGTH(42-42)
src/pages/sign-up/components/signup-step.tsx (3)
src/shared/apis/user/user-mutations.ts (1)
userMutations(9-35)src/pages/sign-up/constants/validation.ts (3)
INFORMATION_PLACEHOLDER(25-26)INFORMATION_MAX_LENGTH(42-42)BIRTH_PLACEHOLDER(23-23)src/pages/sign-up/constants/NOTICE.ts (1)
INFORMATION_RULE_MESSAGE(17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: chromatic
🔇 Additional comments (19)
src/shared/components/footer/footer.tsx (2)
15-15: 간격 4.8rem → 2.4rem 축소 변경 LGTM디자인 의도에 부합해 보이며, 기존 레이아웃 안정성에도 문제 없어 보입니다.
1-1: MATCHING_PLATFORM_NOTICE 관련 잔여 참조 없음
전역 검색 결과 MATCHING_PLATFORM_NOTICE 사용처와 정의가 전혀 발견되지 않아 제거가 정상 완료된 것으로 확인했습니다.src/shared/constants/icon-list.ts (1)
6-6: ‘arrow-right-18’ 아이콘 SVG 에셋 존재 여부 확인 필요
아이콘 리스트에는 잘 반영됐습니다. 다만 실제arrow-right-18.svg파일(또는 스프라이트 심볼)이 프로젝트 내에 추가되어 있는지 확인해 주세요.src/shared/components/input/styles/input-variants.ts (1)
11-13: 아이콘 컬러 톤 상향(L500→L600) 변경 LGTM컴포넌트 전반의 시인성 일관성 관점에서 적절합니다. 멀티라인 모드에서의 색상 적용 로직은 input.tsx에서 별도로 다루니 거기서 확인하겠습니다.
src/pages/sign-up/constants/validation.ts (2)
23-24: 출생연도 플레이스홀더 톤 정리 LGTM한국식(출생연도 기반) 계산을 선호한다는 과거 러닝과도 일치합니다. 불필요한 중복 어구를 제거해 더 명확합니다. (retrieved_learnings 참조)
25-27: 정보 입력 길이 상수 도입 LGTM플레이스홀더/최대 글자수/스텝 상수 간 결합이 명확합니다. 스키마와 컴포넌트(input.tsx)의 maxLength가 동일 상수를 사용 중인지만 재확인 부탁드립니다.
Also applies to: 41-42
src/shared/components/input/input.tsx (1)
98-103: 멀티라인에서 상태색(에러/성공) 비활성화 의도 확인 필요multiline일 때 아이콘/메시지 색이 항상 gray-600으로 고정되어 검증 상태 피드백이 사라집니다. 의도된 UX인지 확인 부탁드립니다. 의도되지 않았다면
!multiline &&조건을 제거해 상태색을 적용하세요.- className={cn('text-gray-600', !multiline && iconColorClass)} + className={cn('text-gray-600', iconColorClass)} @@ - <p className={cn('cap_14_m text-gray-600', !multiline && iconColorClass)}> + <p className={cn('cap_14_m text-gray-600', iconColorClass)}>src/pages/sign-up/schema/validation-schema.ts (1)
42-42: information 필수 여부 확인 필요현재 min(1)로 필수 입력입니다. 기획 상 optional이라면 min(0) 또는 .optional()로 바꿔야 하고, UI의 disabled 조건도 조정되어야 합니다. 의도 확인 부탁드립니다.
src/pages/sign-up/sign-up.tsx (2)
11-21: flex 누락 가능성 — 레이아웃 의도 확인'flex-col'만 있고 'flex'가 없어 flex 컨테이너가 적용되지 않을 수 있습니다(프로젝트 유틸로 해결하는 게 아니라면). 부모/자식 컨테이너 모두 확인 부탁드립니다.
다음처럼 보수적으로 추가하는 것을 제안합니다:
- <div className="h-full flex-col bg-gray-white"> - <div className="flex-1"> + <div className="h-full flex flex-col bg-gray-white"> + <div className="flex-1 flex">
8-21: 퍼널 연동과 단계 구성은 적절합니다Agreement → Information 단계 전환 및 완료 경로 설정이 깔끔합니다.
src/pages/sign-up/components/agreement-step.tsx (1)
50-61: 약관/개인정보 상세 보기 동작 확인우측 화살표 아이콘이 있으나 상세 약관 페이지/모달로의 이동이 바인딩되어 있지 않습니다. 기획대로라면 클릭 시 이동/모달을 연결해 주세요.
src/pages/sign-up/components/signup-step.tsx (1)
150-163: 성별 버튼 클릭 시 폼 submit 위험성Button 컴포넌트의 기본 type이 'submit'이 아니라는 보장이 없다면 클릭 시 폼이 제출될 수 있습니다. 명시적으로 type="button"을 지정하거나 Button 기본값을 확인해 주세요.
- <Button + <Button + type="button" variant={genderValue === '여성' ? 'skyblueBorder' : 'gray2'} label="여성" icon="female" className="flex w-full gap-[0.4rem]" onClick={() => handleGenderClick('여성')} /> ... - <Button + <Button + type="button" variant={genderValue === '남성' ? 'skyblueBorder' : 'gray2'} label="남성" icon="male" className="flex w-full gap-[0.4rem]" onClick={() => handleGenderClick('남성')} />src/shared/constants/api.ts (1)
8-10: 로그아웃 호출부 CSRF 대응 확인 필요
post 호출은 axios 인스턴스의 withCredentials:true로 세션 쿠키 전송을 보장하지만, CSRF 토큰 헤더 자동 추가(request interceptor)가 구현되어 있지 않습니다. 서버 측 CSRF 보호 정책(토큰 예외 처리 등)을 확인해주세요.src/shared/constants/query-key.ts (1)
8-8: USER_KEY.LOGOUT 추가 — 문제없음
src/shared/apis/user/user-mutations.ts에서 정상적으로 사용되는 것을 확인했습니다. 뮤테이션 키 분리가 명확해지고 캐시 무효화 정책에도 도움이 됩니다.src/shared/apis/user/user-mutations.ts (1)
4-4: LGTM – QueryClient 주입 적절전역 queryClient를 직접 가져와 캐시 정리하는 선택 합리적입니다.
src/pages/profile/profile.tsx (4)
1-11: LGTM – 라우팅/링크/훅 의존성 추가 적절
- 외부 링크에 noopener/noreferrer 적용 OK
- 라우트 상수 및 react-query 훅 사용 일관성 OK
25-35: LGTM – 카드 그림자 제거디자인 의도에 맞게 !shadow-none 지정 적절합니다.
36-40: LGTM – 편집 버튼 내비게이션라벨과 라우팅 명확합니다.
42-62: LGTM – 외부 링크 섹션시맨틱/접근성 속성 적절합니다.
| <button | ||
| type="button" | ||
| className={cn( | ||
| 'flex w-full items-center justify-between gap-[0.8rem] p-[0.8rem] px-[1.6rem] text-left', | ||
| divider && 'border-gray-200 border-b', | ||
| className, | ||
| )} | ||
| > | ||
| <div className="flex items-center gap-[0.8rem]"> | ||
| <button type="button" className="cursor-pointer" onClick={onClick}> | ||
| <Icon name="check-filled" className={checked ? 'text-main-800' : 'text-gray-300'} /> | ||
| </button> | ||
| <span className="body_16_m">{label}</span> | ||
| </div> | ||
| {svg} | ||
| </button> |
There was a problem hiding this comment.
중첩 인터랙티브 요소(button 안에 button) — 접근성/브라우저 호환 이슈
button 요소 내부에 또 다른 button이 있어 HTML 규격 위반이며 키보드 포커스/클릭 버그가 날 수 있습니다. 한 레벨의 인터랙션으로 단순화하고 aria 상태를 부여해 주세요.
다음과 같이 수정을 제안합니다(행 전체 클릭 토글, aria-pressed 추가):
- <button
+ <button
type="button"
className={cn(
'flex w-full items-center justify-between gap-[0.8rem] p-[0.8rem] px-[1.6rem] text-left',
divider && 'border-gray-200 border-b',
className,
)}
+ onClick={onClick}
+ aria-pressed={checked}
>
<div className="flex items-center gap-[0.8rem]">
- <button type="button" className="cursor-pointer" onClick={onClick}>
- <Icon name="check-filled" className={checked ? 'text-main-800' : 'text-gray-300'} />
- </button>
+ <Icon name="check-filled" className={checked ? 'text-main-800' : 'text-gray-300'} />
<span className="body_16_m">{label}</span>
</div>
{svg}
</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 | |
| type="button" | |
| className={cn( | |
| 'flex w-full items-center justify-between gap-[0.8rem] p-[0.8rem] px-[1.6rem] text-left', | |
| divider && 'border-gray-200 border-b', | |
| className, | |
| )} | |
| > | |
| <div className="flex items-center gap-[0.8rem]"> | |
| <button type="button" className="cursor-pointer" onClick={onClick}> | |
| <Icon name="check-filled" className={checked ? 'text-main-800' : 'text-gray-300'} /> | |
| </button> | |
| <span className="body_16_m">{label}</span> | |
| </div> | |
| {svg} | |
| </button> | |
| <button | |
| type="button" | |
| className={cn( | |
| 'flex w-full items-center justify-between gap-[0.8rem] p-[0.8rem] px-[1.6rem] text-left', | |
| divider && 'border-gray-200 border-b', | |
| className, | |
| )} | |
| onClick={onClick} | |
| aria-pressed={checked} | |
| > | |
| <div className="flex items-center gap-[0.8rem]"> | |
| <Icon name="check-filled" className={checked ? 'text-main-800' : 'text-gray-300'} /> | |
| <span className="body_16_m">{label}</span> | |
| </div> | |
| {svg} | |
| </button> |
🤖 Prompt for AI Agents
In src/pages/sign-up/components/checkbox-row.tsx around lines 16 to 31, there is
a nested interactive element (a button inside a button) which violates HTML
specs and causes accessibility/keyboard issues; remove the inner button and
consolidate interaction on the outer button: move the onClick handler to the
outer button, add aria-pressed={checked} (and aria-label or aria-labelledby if
needed), replace the inner button with a non-interactive element (e.g., span or
svg wrapper) or role="img" so it’s not focusable, ensure the outer button
handles keyboard activation (Enter/Space) and visual focus styles remain, and
keep checked-driven class toggling for the icon.
| <Input | ||
| placeholder={INFORMATION_PLACEHOLDER} | ||
| className="h-[10.4rem]" | ||
| label="한 줄 소개" | ||
| defaultMessage={INFORMATION_RULE_MESSAGE} | ||
| multiline | ||
| maxLength={INFORMATION_MAX_LENGTH} | ||
| isError={!!errors.information} | ||
| isValid={isInformationValid} | ||
| onBlur={onInformationBlur} | ||
| ref={informationRef} | ||
| length={informationLength} | ||
| hasLength | ||
| {...informationInputProps} | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
information 입력의 유효성 메시지 미전달
nickname/birthYear와 달리 validationMessage가 없어 에러 텍스트가 표시되지 않습니다. 일관성을 위해 추가해 주세요.
<Input
placeholder={INFORMATION_PLACEHOLDER}
className="h-[10.4rem]"
label="한 줄 소개"
defaultMessage={INFORMATION_RULE_MESSAGE}
multiline
maxLength={INFORMATION_MAX_LENGTH}
isError={!!errors.information}
isValid={isInformationValid}
onBlur={onInformationBlur}
ref={informationRef}
+ validationMessage={errors.information?.message}
length={informationLength}
hasLength
{...informationInputProps}
/>🤖 Prompt for AI Agents
In src/pages/sign-up/components/signup-step.tsx around lines 121 to 135, the
Input for "한 줄 소개" is missing the validationMessage prop so validation errors
don't display; add validationMessage={errors.information?.message} (or the
equivalent message source used by nickname/birthYear) to the Input props so the
component shows error text consistently when errors.information is set.
| if (pathname === ROUTES.PROFILE) { | ||
| return <h1 className="head_20_sb text-gray-black">내 정보</h1>; | ||
| return <h1 className="head_20_sb text-gray-black">마이페이지</h1>; | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
헤더 카피 '마이페이지' 변경 — 문제없음
디자인/카피 가이드와의 정합성만 확인하면 됩니다. 동일 화면을 지칭하는 다른 곳(탭/ARIA 라벨/테스트 스냅샷)에서 '내 정보'가 남아있지 않은지만 한번 점검 부탁드립니다.
다음 스크립트로 기존 카피 잔존 여부를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
rg -n -S "내 정보" src | sed -n '1,200p'Length of output: 130
‘내 정보’ 탭 라벨도 ‘마이페이지’로 통일 필요
- src/shared/components/bottom-navigation/constants/bottom-navigation.ts:29에서
label: '내 정보'→label: '마이페이지'로 수정해주세요.
🤖 Prompt for AI Agents
In src/shared/components/header/utils/get-header.tsx around lines 62 to 64, the
header for the profile route returns "마이페이지" but the bottom navigation still
uses the label "내 정보"; update
src/shared/components/bottom-navigation/constants/bottom-navigation.ts at line
~29 to change label: '내 정보' to label: '마이페이지' so both header and
bottom-navigation use the same text; ensure spelling and spacing exactly match
the header string.
| import Icon from '@components/icon/icon'; | ||
| import { iconColorMap, inputClassMap } from '@components/input/styles/input-variants'; | ||
| import { cn } from '@libs/cn'; | ||
| import type React from 'react'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
ref 전달 방식 수정: props로 받지 말고 forwardRef 사용
현재 컴포넌트가 ref를 props로 받고 있어 외부에서 JSX ref를 전달해도 React가 스트립합니다. forwardRef로 전환해 실제 DOM ref 전달이 가능하도록 해야 합니다.
-import type React from 'react';
+import React from 'react';
@@
interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onBlur'> {
...
- ref?: React.Ref<HTMLInputElement>;
...
onBlur?: (e: React.FocusEvent<HTMLInputElement> | React.FocusEvent<HTMLTextAreaElement>) => void;
}
@@
-const Input = ({
+const Input = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, InputProps>((
isError,
isValid,
id,
label,
validationMessage,
defaultMessage,
length,
maxLength = 50,
onBlur,
- ref,
className,
hasLength = false,
multiline = false,
- ...props
-}: InputProps) => {
+ ...props
+}: InputProps, ref) => {
@@
- <textarea
+ <textarea
id={id}
ref={ref as React.Ref<HTMLTextAreaElement>}
@@
- <input
+ <input
id={id}
type="text"
className="flex-1 p-[1.6rem] text-gray-black placeholder:text-gray-500"
- ref={ref}
+ ref={ref as React.Ref<HTMLInputElement>}함수 끝 괄호는 });로 마무리됩니다. 기존 export default Input;은 그대로 유지 가능합니다.
Also applies to: 9-22, 24-39, 63-66, 82-84
| length, | ||
| maxLength = 50, | ||
| onBlur, |
There was a problem hiding this comment.
maxLength 속성이 DOM에 전달되지 않아 길이 제한이 동작하지 않습니다
props에서 maxLength를 구조분해 한 뒤, 실제 input/textarea에 전달하지 않아 브라우저 단의 입력 제한이 적용되지 않습니다. 표시(length/maxLength)만 있고 제어가 안 되는 상태입니다.
const Input = ({
...
- length,
- maxLength = 50,
+ length,
+ maxLength = 50,
onBlur,
- ref,
...
}: InputProps) => {
...
- {multiline ? (
+ {multiline ? (
<textarea
id={id}
- ref={ref as React.Ref<HTMLTextAreaElement>}
+ ref={ref as React.Ref<HTMLTextAreaElement>}
+ maxLength={maxLength}
className={cn(
'w-full bg-transparent text-gray-black outline-none placeholder:text-gray-500',
'resize-none whitespace-pre-wrap break-words',
'h-[10.4rem] p-[1.6rem]',
)}
onFocus={() => setIsFocused(true)}
onBlur={(e) => {
setIsFocused(false);
onBlur?.(e);
}}
{...(props as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
/>
) : (
<input
id={id}
type="text"
className="flex-1 p-[1.6rem] text-gray-black placeholder:text-gray-500"
- ref={ref}
+ ref={ref}
+ maxLength={maxLength}
onFocus={() => setIsFocused(true)}
onBlur={(e) => {
setIsFocused(false);
onBlur?.(e);
}}
{...props}
/>
)}Also applies to: 62-77, 78-90
🤖 Prompt for AI Agents
In src/shared/components/input/input.tsx around lines 31-33 (also apply same fix
for ranges 62-77 and 78-90), the maxLength prop is destructured from props but
not forwarded to the rendered input/textarea, so the browser's native length
restriction isn't applied; update the JSX to pass maxLength to the actual
<input> or <textarea> element (e.g., include maxLength={maxLength} among the
element props) and ensure TypeScript/prop types accept it so the DOM receives
the attribute and enforces the limit.
| className={cn( | ||
| 'body_16_m h-[5.6rem] w-full flex-row-between rounded-[12px] bg-gray-100 p-[1.6rem]', | ||
| 'body_16_m h-[5.6rem] w-full flex-row-between rounded-[12px] bg-gray-100 ', | ||
| borderClass, | ||
| className, | ||
| )} | ||
| > |
There was a problem hiding this comment.
멀티라인에서 컨테이너 고정 높이로 인해 UI 클리핑 가능성
컨테이너가 항상 h-[5.6rem]이어서 textarea 높이(10.4rem)와 충돌, 내용 클리핑/스크롤 비의도 가능성이 큽니다. 멀티라인 시 min-h로 전환하세요.
- className={cn(
- 'body_16_m h-[5.6rem] w-full flex-row-between rounded-[12px] bg-gray-100 ',
- borderClass,
- className,
- )}
+ className={cn(
+ 'body_16_m w-full rounded-[12px] bg-gray-100',
+ multiline ? 'min-h-[10.4rem]' : 'h-[5.6rem] flex-row-between',
+ borderClass,
+ className,
+ )}📝 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.
| className={cn( | |
| 'body_16_m h-[5.6rem] w-full flex-row-between rounded-[12px] bg-gray-100 p-[1.6rem]', | |
| 'body_16_m h-[5.6rem] w-full flex-row-between rounded-[12px] bg-gray-100 ', | |
| borderClass, | |
| className, | |
| )} | |
| > | |
| className={cn( | |
| 'body_16_m w-full rounded-[12px] bg-gray-100', | |
| multiline ? 'min-h-[10.4rem]' : 'h-[5.6rem] flex-row-between', | |
| borderClass, | |
| className, | |
| )} |
🤖 Prompt for AI Agents
In src/shared/components/input/input.tsx around lines 55 to 60, the container
currently uses a fixed height class h-[5.6rem] which causes clipping when
rendering a multiline textarea (e.g., 10.4rem content); change the layout to use
a min-height instead when multiline is enabled (replace h-[5.6rem] with
min-h-[5.6rem] or conditionally apply min-h-[...] only for multiline inputs),
update the className composition to detect the multiline prop and add the min-h
class instead of the fixed h- class, and ensure existing borderClass/className
merges remain unaffected so multiline inputs can expand without clipping.
* feat: 약관 동의 노션 페이지 연결 (#323) * feat: 새 페이지로 이동하게 수정 (#323) * feat: 약관 동의 api 추가 (#323) * feat: 사용자 정보 설정 api 수정 (#323) * feat: 회원가입 페이지 내부 api 수정 (#323) * feat: 회원가입 성공, 실패시 분기 추가 (#323) * feat: 약관 동의 api 추가 (#323) * refactor: 약관 동의 먼저 후 그다음 정보 보내게 수정 (#323) * refactor: 중첩 버튼 수정 (#323) * feat: SP1 마이페이지 수정 및 로그아웃 api 연결 (#306) * style: footer 수정 (#305) * style: 마이페이지 제목, 버튼 이름 수정 (#305) * feat: divider 컴포넌트 추가 (#305) * feat: divider 추가 (#305) * feat: 문의하기, 의견 보내기 구글폼 연결 (#305) * feat: logout api 추가 (#305) * feat: 로그아웃 api 연결 (#305) * feat: 로그아웃 요청 성공시 reload 추가 (#305) * feat: 문의하기/의견보내기 링크 연결 (#305) * refactor: 링크 이동 a 태그로 변경 (#305) * feat: LOGOUT시 쿼리 삭제, 취소 (#305) * feat: 로그아웃 커서 추가 (#305) * feat: 로그아웃 api 실패시 로직 추가 (#307) * feat: logout api 실패 로직 추가 (#305) --------- Co-authored-by: heesunee <heesun729@uos.ac.kr> * feat: SP1 매칭 정보 수정 페이지 퍼블리싱 (#308) * feat: 프로필 수정 상단 퍼블리싱 (#307) * feat: 프로필 수정 내 응원팀 추가 (#307) * feat: 매칭 조건 수정 리스트 추가 (#307) * feat: 조건 수정 초기값 반영 (#307) * feat: selectionGroup component 분리 (#307) * feat: 클릭시 로직 추가 (#307) * style: 불필요한 border-t 삭제 (#307) * feat: SP1 회원가입 페이지 디자인 반영사항 적용 (#304) * feat: signup-page funnel apply (#303) * feat: 약관 동의 페이지 퍼블리싱 (#303) * feat: arrow-right-18 svg add (#303) * feat: input className 받게 확장 (#303) * feat: 한줄소개 관련 유효성 추가 (#303) * feat: input textarea 분기 추가 (#303) * feat: 한줄소개 글자수 실시간 반영 (#303) * feat: 서비스 이용약관 체크박스 로직 추가 (#303) * feat: 이용약관 다음으로 이동 추가 (#303) * style: 정보입력 페이지 pb 수정 (#303) * feat: 체크박스 row 컴포넌트로 분리 (#303) * feat: 이용약관 페이지 checkboxrow 컴포넌트로 교체 (#303) * fix: 체크박스 클릭 함수 icon으로 이동 (#303) * refactor: maxLength 추가 (#303) * feat: 한줄소개 공백 입력 제거 (#303) * feat: checkbox button으로 감싸게 수정 (#303) --------- Co-authored-by: heesunee <heesun729@uos.ac.kr> * feat: 응원팀 없어요 클릭시 메이트의 응원팀 disabled (#307) * feat: 응원팀 없을때 기본 값 수정 (#307) * feat: button disabled style add (#307) * fix: 수정 취소 (#307) * fix: 수정 취소 (#307) * feat: SP1 마이페이지 수정 및 로그아웃 api 연결 (#306) * style: footer 수정 (#305) * style: 마이페이지 제목, 버튼 이름 수정 (#305) * feat: divider 컴포넌트 추가 (#305) * feat: divider 추가 (#305) * feat: 문의하기, 의견 보내기 구글폼 연결 (#305) * feat: logout api 추가 (#305) * feat: 로그아웃 api 연결 (#305) * feat: 로그아웃 요청 성공시 reload 추가 (#305) * feat: 문의하기/의견보내기 링크 연결 (#305) * refactor: 링크 이동 a 태그로 변경 (#305) * feat: LOGOUT시 쿼리 삭제, 취소 (#305) * feat: 로그아웃 커서 추가 (#305) * feat: 로그아웃 api 실패시 로직 추가 (#307) * feat: logout api 실패 로직 추가 (#305) --------- Co-authored-by: heesunee <heesun729@uos.ac.kr> * feat: 프로필 수정 상단 퍼블리싱 (#307) * feat: 프로필 수정 내 응원팀 추가 (#307) * feat: 매칭 조건 수정 리스트 추가 (#307) * feat: 조건 수정 초기값 반영 (#307) * feat: selectionGroup component 분리 (#307) * feat: 클릭시 로직 추가 (#307) * style: 불필요한 border-t 삭제 (#307) * feat: 응원팀 없어요 클릭시 메이트의 응원팀 disabled (#307) * feat: 응원팀 없을때 기본 값 수정 (#307) * feat: button disabled style add (#307) * fix: 수정 취소 (#307) * fix: 수정 취소 (#307) * feat: 프로필 수정 페이지 내 divider, 한줄 소개 input 수정(#307) * feat: 리뷰 반영 (#307) --------- Co-authored-by: heesunee <heesun729@uos.ac.kr> * fix: 오타 수정 (#323) * feat: 약관 동의 노션 페이지 연결 (#323) * feat: 새 페이지로 이동하게 수정 (#323) * feat: 약관 동의 api 추가 (#323) * feat: 사용자 정보 설정 api 수정 (#323) * feat: 회원가입 페이지 내부 api 수정 (#323) * feat: 회원가입 성공, 실패시 분기 추가 (#323) * feat: 약관 동의 api 추가 (#323) * refactor: 약관 동의 먼저 후 그다음 정보 보내게 수정 (#323) * refactor: 중첩 버튼 수정 (#323) * fix: 오타 수정 (#323) * fix: build error (#323) --------- Co-authored-by: heesunee <heesun729@uos.ac.kr>
#️⃣ Related Issue
Closes #307
💎 PR Point
📸 Screenshot
Tab-2025.8.25._15_40.mp4
Summary by CodeRabbit
New Features
Style
Refactor
Chores