Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 명함 생성 도메인 구현 #73

Merged
merged 61 commits into from
Mar 18, 2025
Merged

Conversation

jangwonyoon
Copy link
Collaborator

@jangwonyoon jangwonyoon commented Mar 11, 2025

📌 개요

관련 이슈 : close #58

명함 생성 도메인

  • 새로운 명함 생성
  • 기존 명함이 있을때 명함 만들기는, 내 명함 페이지 API 연동 후 진행

기능

  • react hook form을 사용하여 multi-step-form을 다루고, 유효성 처리 및 에러처리를 다룹니다.
  • 검색 드롭다운
    • 드롭다운 한글 검색을 위해 es-hangle 패키지와, react-select 패키지를 사용하였습니다.
  • 인풋 태그
  • ts-pattern을 사용하여, 명함 생성 form의 단계를 구분지었습니다.
  • FormData 객체를 하용하여 multi-part-form 제출을 진행합니다.

화면

기능 스크린샷

스크린샷 2025-03-18 오후 3 12 47

스크린샷 2025-03-18 오후 3 13 04

스크린샷 2025-03-18 오후 3 15 10 스크린샷 2025-03-18 오후 3 15 23

스크린샷 2025-03-18 오후 3 16 28

스크린샷 2025-03-18 오후 3 16 37

리뷰어에게

  • React hook form 부분에 중복 코드가 많이 생겨, 추상화를 진행하였습니다.

  • 컴포넌트 추상화 : EditableCardField

  • hook 추상화 : useEditingStates

  • API 실패 및 성공 시, 에러 처리에 대한 논의가 추후에 필요해 보입니다.

✅ 체크사항

  • API 연동 확인
  • 기능이 정상적으로 동작하는지 확인
  • 코드 스타일 및 규칙 준수 확인
  • UI가 변경된 경우 스크린샷 첨부 여부 확인

Summary by CodeRabbit

  • 신규 기능

    • 다단계 카드 작성 폼이 개선되어 프로필 이미지 업로드, 닉네임, 직업 선택, 관심 분야 및 요약 입력 등의 항목이 업데이트되었습니다.
    • 검색 드롭다운과 애니메이션 태그 관리 기능이 도입되어 인터랙티브한 태그 입력 경험을 제공합니다.
    • 입력 필드에 오류 메시지 표시 및 클리어 버튼 기능이 추가되어 유효성 검증이 강화되었습니다.
    • 디자이너와 개발자 카드 생성 라우팅이 개선되었습니다.
  • UI 개선

    • 레이아웃 반응성 및 클릭 이벤트(포인터 이벤트) 처리가 개선되어 사용자 경험이 향상되었습니다.

Copy link

coderabbitai bot commented Mar 11, 2025

📝 Walkthrough

Walkthrough

이번 PR은 여러 컴포넌트와 유틸리티의 업데이트 및 신규 기능 추가를 포함합니다.

  • package.json에 새로운 의존성이 추가되었으며, 메타데이터 관련 레이아웃 수정이 이루어졌습니다.
  • 다단계 폼 컴포넌트들이 React Hook Form, Tanstack Query 등의 라이브러리와 통합되어 개선되었고, 형태 검증 스키마 및 유틸리티 함수들이 갱신되었습니다.
  • Zustand 스토어를 활용한 상태 관리, 드롭다운, 태그, 입력 및 텍스트 영역 컴포넌트 등 UI 전반에 걸친 개선이 진행되었습니다.

Changes

파일 또는 파일 그룹 변경 내용
package.json 새로운 의존성(@radix-ui/react-dropdown-menu, es-hangul, framer-motion, react-select, zustand) 추가 및 형식 수정
src/app/layout.tsx metadata 객체에서 viewport 속성 삭제, JSX를 React Fragment(<>...</>)로 감쌈
src/features/multi-step-form/components/AvartarImg.tsx,
Cardview.tsx,
EditableCardField.tsx
AvartarImg: React Hook Form의 Controller 통합, 파일 입력 로직 리팩토링
Cardview: 새 컴포넌트 및 타입 추가
EditableCardField: 편집 가능한 카드 필드 컴포넌트 도입 및 비동기 링크 스크래핑 처리 추가
src/features/multi-step-form/config/index.ts MAXIMUM_ADDFIELD_TAG_MAPPING 상수 추가
src/features/multi-step-form/hooks/queries/useCreateCard.ts,
useScrap.ts
API 호출을 위한 신규 커스텀 훅 추가 (카드 생성, 스크랩 링크 전송)
src/features/multi-step-form/hooks/useEditingStates.ts 필드 배열의 편집 상태 관리를 위한 커스텀 훅 추가
src/features/multi-step-form/schema/index.ts Zod 기반 다단계 폼 스키마 확장 (필드명 변경, 신규 필드 추가 및 유효성 검사 업데이트)
src/features/multi-step-form/ui/careerForm/firstStep.tsx,
fourthStep.tsx,
index.tsx,
secondStep.tsx,
thridStep.tsx
폼 필드명 변경, 컴포넌트 업데이트, 입력 컴포넌트 교체, 상태 및 유효성 검증 로직 강화
src/features/multi-step-form/utils/index.tsx URL 도메인 추출, 플랫폼 식별, FormData 생성 유틸리티 함수 추가
src/features/new-card/config/index.tsx,
hooks/queries/useRegisterQuery.ts,
ui/newCreateCardView.tsx
라우트 키 이름 변경(대문자 적용), 카드 등록 데이터 조회 및 신규 카드 생성 뷰 업데이트 (상태 관리 포함)
src/shared/hooks/useOnClickOutside.ts DOM 영역 외 클릭 감지를 위한 커스텀 훅 추가
src/shared/spacing/spacing.ts,
tailwind.config.ts
새로운 패딩(ms: 12px) 추가
src/shared/store/cardFormState.ts 태그와 작업(job) 상태를 관리하는 Zustand 스토어 신규 생성
src/shared/types/index.ts 제네릭 API 응답 타입 ApiResponseType<T> 추가
src/shared/ui/Input/index.tsx,
input.tsx,
tagInput.tsx
WrappedInput 컴포넌트에 에러, 클리어 버튼 등 추가 속성 적용 및 컴포넌트 리팩토링
src/shared/ui/dropDown/index.tsx,
dropDown/searchDropdown.tsx
Radix UI 기반 드롭다운 메뉴 및 react-select 기반 SearchDropdown 컴포넌트 신규 추가
src/shared/ui/label.tsx Radix Label Primitive를 활용한 새 Label 컴포넌트 추가
src/shared/ui/tag/motionTag.tsx,
tag.tsx
Framer Motion 기반 MotionTag 컴포넌트 추가 및 Tag 컴포넌트에 create-input 사이즈 옵션 추가, 버튼 마진 수정
src/shared/ui/tagInput.tsx 태그 입력 기능을 제공하는 TagInput 컴포넌트 신규 추가
src/shared/ui/textArea/index.tsx CVA를 활용하여 스타일 변형 가능한 새로운 Textarea 컴포넌트 추가

Sequence Diagram(s)

sequenceDiagram
  participant U as 사용자
  participant F as MultiStepFormView
  participant C as Form Controller
  participant D as createCareerFormData (utils)
  participant Q as useCreateCard 훅
  participant API as API 서버

  U->>F: 폼 작성 후 Next 버튼 클릭
  F->>C: 폼 유효성 검사 요청
  C-->>F: 유효성 검사 결과 반환
  F->>D: 폼 데이터 변환 요청
  D-->>F: 변환된 FormData 반환
  F->>Q: 카드 생성 API 호출
  Q->>API: POST 요청 전송
  API-->>Q: 응답 전송
  Q-->>F: 처리 결과 반환
Loading
sequenceDiagram
  participant U as 사용자
  participant T as TagBox
  participant Z as Zustand 스토어

  U->>T: 태그 클릭
  T->>Z: handleTagClick 호출
  Z-->>Z: tagArray 및 tagCount 업데이트
  Z-->>T: 업데이트된 상태 반환
  T-->>U: UI 갱신 반영
Loading

Possibly related PRs

Suggested labels

✅ Auto approve, 💄 style

Suggested reviewers

  • semnil5202
  • JaeIn1
  • youngju6143
  • evenway2025

Tip

⚡🧪 Multi-step agentic review comment chat (experimental)
  • We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
    - To enable this feature, set early_access to true under in the settings.
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jangwonyoon jangwonyoon changed the title Feature/#58 card function [feat] 명함 생성 도메인 구현 Mar 11, 2025
기존의 wrappedInput에 hook-form value, onChange를 전달 받을수 있게 한다.
@jangwonyoon jangwonyoon added the 🚀 feature 새로운 기능 개발 label Mar 11, 2025
3단계의 태그 내용을 가져와서 validation을 처리하기 위해 전역상태를 사용하였습니다.
register API의 드롭다운 content를 연동하기 위해 전역상태를 사용하여  payload 처리
이미지 File 타입, zod validation 수정
EditableCardField 컴포넌트를 만들어 응집도를 높이고, 관심사 분리를 진행하였습니다.
@jangwonyoon jangwonyoon marked this pull request as ready for review March 18, 2025 06:10
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🔭 Outside diff range comments (1)
src/features/multi-step-form/ui/careerForm/secondStep.tsx (1)

37-38: ⚠️ Potential issue

setTagCount의 사용에 문제가 있습니다

setTagCount는 실제로 incrementTagCount 함수이기 때문에 0으로 설정하는 목적으로 사용하면 안 됩니다. 상태 초기화에는 resetTagArray 함수를 사용하는 것이 더 적절합니다.

다음과 같이 수정하는 것이 좋겠습니다:

-              setTagCount(0);
-              setTagArray([]);
+              useCardFormStore.getState().resetTagArray();
🧹 Nitpick comments (42)
src/features/new-card/config/index.tsx (1)

13-14: 라우트 경로 명명 규칙 개선

직군 관련 라우트 키를 소문자(design, dev)에서 대문자(DESIGNER, DEVELOPER)로 변경한 것은 상수 네이밍 컨벤션에 더 적합합니다. 이 변경으로 상수임을 명확히 식별할 수 있게 되었습니다. 다만, CAREER_SELECTlistText 객체 내 키(line 7-8)도 함께 대문자로 변경하는 것이 일관성을 유지하는 데 도움이 될 것입니다.

CAREER_SELECTlistText 내부 키도 같은 네이밍 컨벤션을 따르도록 수정하는 것이 좋겠습니다:

export const CAREER_SELECT = {
  title: '직군을 선택해 주세요',
  description: '직군에 맞는 템플릿으로 내 명함을 만들 수 있어요.',
  listText: {
-    design: '디자인 직군',
-    dev: '개발 직군',
+    DESIGNER: '디자인 직군',
+    DEVELOPER: '개발 직군',
  },
};
src/features/multi-step-form/components/Cardview.tsx (1)

6-12: 타입 정의 개선 제안

onCloseClick 속성은 옵셔널하지만 실제로는 index !== 0일 때만 사용됩니다. 타입 정의를 더 명확하게 개선할 수 있습니다.

조건부 타입을 사용하여 더 엄격한 타입 체크를 구현할 수 있습니다:

  type CardViewProps = {
    index: number;
    title: string;
    link: string;
-   onCloseClick?: () => void;
+   onCloseClick: index extends 0 ? undefined : () => void;
    onClick: () => void;
  };

또는 간단하게:

  type CardViewProps = {
    index: number;
    title: string;
    link: string;
    onCloseClick?: () => void;
    onClick: () => void;
  };
+
+ // 컴포넌트 내부에서 실행 전 확인
+ const handleCloseClick = (e: React.MouseEvent) => {
+   e.stopPropagation();
+   if (onCloseClick) onCloseClick();
+ }
src/features/new-card/ui/newCreateCardView.tsx (2)

17-20: 디자이너 카드 생성 핸들러 개선

라우팅 로직이 개선되었고 상태 관리가 추가되었습니다. 하지만 코드 가독성을 더 높이기 위해 라우팅과 상태 관리 로직을 더 명확하게 분리하는 것이 좋을 수 있습니다.

const handleCreateDesignCard = () => {
-  router.push(ROUTE_PATH.DESIGNER);
-  setJob('DESIGNER');
+  const jobType = 'DESIGNER';
+  setJob(jobType);
+  router.push(ROUTE_PATH.DESIGNER);
};

22-25: 개발자 카드 생성 핸들러 개선

디자이너 핸들러와 마찬가지로, 이 함수도 가독성을 높이기 위해 리팩토링을 고려해 볼 수 있습니다.

const handleCreateDevCard = () => {
-  router.push(ROUTE_PATH.DEVELOPER);
-  setJob('DEVELOPER');
+  const jobType = 'DEVELOPER';
+  setJob(jobType);
+  router.push(ROUTE_PATH.DEVELOPER);
};
src/shared/hooks/useOnClickOutside.ts (2)

10-17: 이벤트 핸들러 구현의 안전성 확보

이벤트 핸들러가 ref 유효성과 내부 클릭 여부를 안전하게 확인합니다. 다만, 타입 단언(as Node)을 사용하는 대신 더 안전한 타입 가드 패턴을 고려할 수 있습니다.

const handleEvent = (event: MouseEvent | TouchEvent) => {
-  if (!ref.current || ref.current.contains(event.target as Node)) {
+  const target = event.target;
+  if (!ref.current || !target || ref.current.contains(target as Node)) {
    return;
  }
  callback(event);
};

19-27: 이벤트 리스너 관리의 최적화

이벤트 리스너 등록 및 정리 로직이 적절히 구현되어 있습니다. 다만, 성능 최적화를 위해 passive 옵션을 추가하는 것을 고려해 볼 수 있습니다.

// mousedown과 touchstart 이벤트 등록
-document.addEventListener('mousedown', handleEvent);
-document.addEventListener('touchstart', handleEvent);
+document.addEventListener('mousedown', handleEvent, { passive: true });
+document.addEventListener('touchstart', handleEvent, { passive: true });

// cleanup: 언마운트 시 이벤트 제거
return () => {
-  document.removeEventListener('mousedown', handleEvent);
-  document.removeEventListener('touchstart', handleEvent);
+  document.removeEventListener('mousedown', handleEvent, { passive: true });
+  document.removeEventListener('touchstart', handleEvent, { passive: true });
};
src/features/multi-step-form/config/index.ts (1)

22-22: 상수 값의 의미 명확화가 필요합니다.

MAXIMUM_ADD 상수가 추가되었지만, 이 값이 정확히 무엇을 제한하는지 명확하지 않습니다. 주석이나 더 구체적인 이름을 통해 이 상수의 목적을 명확히 하는 것이 좋겠습니다.

- export const MAXIMUM_ADD = 10;
+ // 한 명함에 추가할 수 있는 최대 태그 개수
+ export const MAXIMUM_TAG_ADD = 10;
src/shared/ui/tag/tag.tsx (2)

11-11: 새로운 TagSize 옵션이 추가되었습니다.

'create-input' 사이즈 옵션이 추가되었지만, JSDoc 문서는 업데이트되지 않았습니다. 다른 개발자들이 이 새로운 옵션을 인지할 수 있도록 문서를 갱신해주세요.

JSDoc(26줄)을 다음과 같이 업데이트해 주세요:

- @param {"sm" | "md" | "lg"} [props.size="md"] - 태그의 크기를 설정합니다.
+ @param {"sm" | "md" | "lg" | "create-input"} [props.size="md"] - 태그의 크기를 설정합니다.

65-65: Typography 변형 로직 업데이트가 필요합니다.

Typography 컴포넌트의 variant 선택 로직에 새로 추가된 'create-input' 사이즈 케이스가 처리되지 않았습니다. 'create-input'의 텍스트 스타일도 고려해야 합니다.

- <Typography variant={size === 'lg' ? 'body-5' : size === 'md' ? 'body-5' : 'caption-1'}>{message}</Typography>
+ <Typography variant={size === 'lg' ? 'body-5' : size === 'md' ? 'body-5' : size === 'create-input' ? 'caption-1' : 'caption-1'}>{message}</Typography>
src/features/multi-step-form/ui/careerForm/tagFormStep/ui/MotionTagBox.tsx (1)

14-27: 애니메이션 variant 주석 처리가 좋습니다.

애니메이션 variant에 대한 명확한 주석이 추가되어 코드 이해도를 높입니다. 다만, selectedtap 상태에 대한 주석도 추가하면 더 완벽할 것 같습니다.

다음과 같이 나머지 상태에 대한 주석도 추가하는 것이 좋겠습니다:

  // unselected: 태그가 부모 중앙에서 초기 오프셋(tag.initialPosition)만큼 떨어져 있다.
+ // selected: 태그가 부모 중앙에서 최종 오프셋(tag.endPosition)으로 이동한다.
+ // tap: 태그를 누를 때 살짝 축소되는 효과를 준다.
src/features/multi-step-form/hooks/queries/useScrap.ts (2)

21-32: postScapLink 함수 구현이 잘 되었습니다.

API 요청 구현이 적절하게 되어 있으며, 헤더와 쿼리 파라미터 설정도 잘 되어 있습니다. 다만, 함수 이름에 오타가 있습니다.

- const postScapLink = async ({ payload, type }: { payload: ScrapPayload; type: ScrapType }) => {
+ const postScrapLink = async ({ payload, type }: { payload: ScrapPayload; type: ScrapType }) => {

34-40: useScrap 훅의 에러 처리가 미흡합니다.

onSuccessonError 핸들러가 빈 함수로 구현되어 있습니다. 사용자에게 성공/실패 피드백을 제공하거나 에러 로깅을 추가하는 것이 좋겠습니다.

  return useMutation<ApiResponseType<ScrapResponse>, unknown, { payload: ScrapPayload; type: ScrapType }>({
    mutationFn: postScapLink,
-   onSuccess: () => {},
-   onError: () => {},
+   onSuccess: (data) => {
+     console.log('Scrap link posted successfully:', data);
+     // 성공 메시지 표시 또는 추가 작업 수행
+   },
+   onError: (error) => {
+     console.error('Failed to post scrap link:', error);
+     // 에러 메시지 표시 또는 실패 처리
+   },
  });
src/shared/ui/textArea/index.tsx (2)

8-31: 텍스트 영역의 스타일 변형이 효과적으로 구성되어 있습니다.

텍스트 에리어의 다양한 크기 변형과 에러 상태 스타일링이 잘 정의되어 있습니다. 단, 기본 크기가 'xs'로 설정되어 있어 높이가 매우 작을 수 있습니다(12px). 실제 사용 시 사용자 경험을 고려하여 기본값을 더 큰 사이즈로 변경하는 것이 좋을 수 있습니다.

  defaultVariants: {
-     size: 'xs',
+     size: 'md',
  },

39-69: 텍스트 영역 컴포넌트의 구현이 완성도 높습니다.

forwardRef를 사용하여 DOM 접근성을 제공하고, 문자 수 카운팅 및 에러 메시지 표시 기능을 통합한 점이 좋습니다. 하지만 currentTextLength 계산 시 null 값에 대한 안전한 처리가 되어 있지만, 보다 명확하게 할 수 있습니다.

-   const currentTextLength = value?.toString().length ?? 0;
+   const currentTextLength = value ? value.toString().length : 0;

또한 totalNumber prop에 대한 유효성 검사를 추가하면 좋을 것 같습니다.

src/shared/ui/Input/input.tsx (1)

26-29: InputBodyProps 타입 수정 및 export 처리가 적절합니다.

타입을 외부로 내보내고 error 속성을 추가한 것은 좋은 변경입니다. 다만, Textarea 컴포넌트처럼 errorMsg 속성도 추가하면 UX 관점에서 더 완성도 있을 것 같습니다.

export type InputBodyProps = React.ComponentProps<'input'> & {
  variant?: 'default' | 'withBtn'; // variant 타입 추가
  error?: boolean;
+  errorMsg?: string;
};

참고로, 주석은 가능하면 영어로 통일하는 것이 국제적인 코드베이스 관리에 도움이 됩니다.

export type InputBodyProps = React.ComponentProps<'input'> & {
-  variant?: 'default' | 'withBtn'; // variant 타입 추가
+  variant?: 'default' | 'withBtn'; // Added variant type
  error?: boolean;
};
src/shared/store/cardFormState.ts (2)

36-49: 태그 클릭 핸들러 구현이 효과적입니다.

태그 토글 기능이 간결하게 구현되어 있습니다. 다만, tagCounttagArray.length로 도출될 수 있어 상태 중복이 있습니다. 추후 확장성을 고려한다면 tagCount를 파생 값으로 계산하는 방법을 고려해볼 수 있습니다.

또한, 코드 내 한글 주석은 코드 베이스의 일관성을 위해 영어로 통일하는 것이 좋습니다.

-  // 태그 클릭 핸들러
+  // Tag click handler
  handleTagClick: (tagMessage: TagValue) => {

50-56: resetTagArray 메소드에 대한 주석이 한글로 되어 있습니다.

코드 일관성을 위해 영어 주석으로 변경하는 것이 좋습니다.

-  // 태그 카운트와 태그 배열 초기화
+  // Reset tag count and tag array
  resetTagArray: () =>
src/features/multi-step-form/ui/careerForm/firstStep.tsx (1)

90-90: 오타 수정 필요

"괸심"을 "관심"으로 수정해야 합니다.

-                  placeholder="어떤 분야에 괸심이 있나요?"
+                  placeholder="어떤 분야에 관심이 있나요?"
src/features/multi-step-form/components/EditableCardField.tsx (2)

101-101: 주석 중복 제거 필요

// // 스크래핑 함수 주석에서 이중 슬래시를 제거해야 합니다.

-  // // 스크래핑 함수
+  // 스크래핑 함수

148-157: 키 이벤트 처리 로직 개선 필요

handleKeyDown 함수에서 Enter 키가 눌렸을 때 편집 모드를 활성화하는 로직은 입력 필드에서 일반적으로 기대되는 동작(폼 제출)과 충돌할 수 있습니다. 현재 편집 모드 상태를 먼저 확인하는 것이 좋습니다.

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  // Enter key
  /**
   * 현재는 Enter키만 지원하지만 추후 다른 키도 지원할 수 있도록 수정 가능
   */
  if (e.key === 'Enter') {
    e.preventDefault();
+   if (!editingStates[index]) {
      enableEditing();
+   }
  }
};
src/features/new-card/hooks/queries/useRegisterQuery.ts (2)

26-32: API 호출 함수가 간결합니다.

_getRegister 함수가 간결하게 구현되어 있습니다. 단, 에러 처리 로직이 보이지 않습니다.

에러 처리를 위한 코드를 추가하면 좋을 것 같습니다:

const _getRegister = async (params: RegisterQueryParams) => {
-  const data = await client.get<ApiResponseType<RegisterResponse>>(`${CLIENT_SIDE_URL}/api/card/register`, {
-    params,
-  });
-
-  return data;
+  try {
+    const data = await client.get<ApiResponseType<RegisterResponse>>(`${CLIENT_SIDE_URL}/api/card/register`, {
+      params,
+    });
+    
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch register data:', error);
+    throw error;
+  }
};

34-49: useQuery 구성이 잘 되어 있습니다.

React Query의 useQuery 훅을 사용한 구현이 좋습니다. 특히:

  1. enabled 옵션을 통해 필요한 경우에만 쿼리가 실행되도록 했습니다.
  2. select 함수를 통해 응답 데이터를 필요한 형태로 변환해주어 컴포넌트에서 바로 사용할 수 있게 했습니다.

다만, 쿼리 실패 시 재시도 로직이나 에러 핸들링은 추가하는 것이 좋을 것 같습니다.

export const useRegisterQuery = (params: RegisterQueryParams) => {
  return useQuery({
    queryKey: [CARD_REGISTER, params],
    queryFn: () => _getRegister(params),
    enabled: !!params.job,
+   retry: 2,
+   retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
+   onError: (error) => {
+     console.error('Register query failed:', error);
+     // 필요시 에러 알림 표시
+   },
    select: (data) =>
      data.data.careers.map(({ detailJobEn, id, detailJobKr }) => {
        return {
          id: id,
          label: detailJobEn,
          keywords: detailJobKr,
          value: detailJobEn,
        };
      }),
  });
};
src/features/multi-step-form/components/AvartarImg.tsx (1)

23-56: 파일 업로드 처리가 효율적입니다.

Controller 컴포넌트를 사용하여 파일 입력을 폼과 통합한 접근 방식이 매우 효과적입니다. 특히:

  1. 파일 객체를 폼 상태에 저장하면서 미리보기용 Data URL을 별도로 관리하는 전략이 깔끔합니다.
  2. 주석을 통해 코드의 의도를 명확히 설명했습니다.

하지만 파일 크기 제한이나 이미지 타입 검증 등의 유효성 검사가 추가되면 좋을 것 같습니다.

onChange={(e) => {
  const file = e.target.files?.[0];
  if (file) {
+   // 파일 크기 검증 (예: 5MB 제한)
+   if (file.size > 5 * 1024 * 1024) {
+     alert('파일 크기는 5MB 이하여야 합니다.');
+     return;
+   }
+   
+   // 이미지 타입 검증
+   if (!file.type.startsWith('image/')) {
+     alert('이미지 파일만 업로드 가능합니다.');
+     return;
+   }
+   
    // 폼에는 File 객체를 onChange로 저장
    onChange(file);

    // 미리보기용 Data URL 생성
    const reader = new FileReader();
    reader.onloadend = () => {
      const result = reader.result as string;
      setAvatarSrc(result);
    };
+   reader.onerror = () => {
+     alert('파일을 읽는 중 오류가 발생했습니다.');
+   };
    reader.readAsDataURL(file);
  }
}}
src/shared/ui/tagInput.tsx (1)

120-150: UI 구조 및 스타일링이 적절함

태그 입력 UI가 명확하게 구성되어 있으며, 조건부 스타일링을 사용하여 에러 상태 표시 등을 처리하는 방식이 적절합니다. 태그와 입력 필드의 배치도 사용자가 이해하기 쉽게 되어 있습니다.

하지만 124번 줄의 주석 처리된 ref={containerRef}는 필요하지 않다면 제거하는 것이 좋을 것 같습니다.

- <div
-   // ref={containerRef}
-   className={cn(
...
src/shared/ui/dropDown/searchDropdown.tsx (1)

92-152: 커스텀 스타일 구현이 상세함

Select 컴포넌트의 여러 부분(control, option, menu 등)에 대한 스타일이 상세하게 구현되어 있습니다. 특히 커스텀 색상과 여백을 적용하여 디자인 요구사항을 충족시키는 방식이 좋습니다.

다만, 134-137 라인에서 테두리 색상을 배경색과 동일하게 설정하여 테두리를 숨기는 방식은 혼란을 줄 수 있습니다. 필요하지 않다면 border: 'none'으로 설정하는 것이 더 명확할 수 있습니다.

border: '1px solid',
-borderTopColor: '#202030',
-borderRightColor: '#202030',
-borderBottomColor: '#202030',
-borderLeftColor: '#202030',
+border: 'none',
src/shared/ui/tag/motionTag.tsx (3)

11-19: TagProps에 정의된 onClose 사용 여부 확인
TagProps 인터페이스에 onClose가 있지만, 아래 구현부에서 실제로 사용하지 않고 있습니다. 만약 닫기 기능이 필요 없다면 프로퍼티에서 제거하거나, 추후 기능 구현을 고려해 보세요.


38-43: 타이포그래피 매핑 적절성 확인
sm일 때 caption-1, 그 외 md, lgbody-5로 동일하게 매핑되어 있습니다. 디자인 가이드를 만족하는지, 추가로 미세조정이 필요한지 확인을 권장합니다.


45-69: 애니메이션 및 이벤트 처리
프레이머 모션의 variants를 적절히 설정해 태그에 동적 효과를 주는 것은 좋아 보입니다. 다만, onClose에 대한 로직이 누락되어 있어 클릭 이외의 닫기 기능이 필요한 경우 추가 구현이 필요합니다.

src/features/multi-step-form/ui/careerForm/thridStep.tsx (7)

1-20: react-hook-form과 Zustand 연계 점검
useFormContext, useFieldArray, useShallow를 동시에 사용하면서 폼 상태와 전역 상태를 명확히 구분해야 합니다. 현재로서는 충돌 우려는 없어 보이나, 폼 검사 로직이 다수 확장될 경우 복잡도가 높아질 수 있으므로 구조를 주기적으로 점검하시기 바랍니다.


22-50: useFieldArray로 배열 필드 관리
content, sns, project에 각각 useFieldArray를 적용하여 동적 폼 필드를 구성하는 것은 효율적입니다. 다만, 유사한 로직이 반복되므로 추후 리팩터링으로 공통화할 여지가 있어 보입니다.


52-74: 별도의 편집 상태 훅(useEditingStates) 활용
필드마다 편집 여부를 별도로 관리하는 구조는 유연하지만, 상태가 늘어날 경우 복잡도가 커질 수 있습니다. 공통 로직을 합치거나 모듈화하는 방안을 검토해 보세요.


161-175: 취미 필드
포맷과 에러 메시지 처리는 잘 되어 있으나, SNS 등과 마찬가지로 여러 취미를 추가할 수 있는 요구사항이 있을 경우 useFieldArray로 확장하는 방안도 고려해 보세요.


177-191: 최근 소식 필드
UX 측면에서 사용자에게 힌트를 더 주거나, 여백과 레이블 안내 등을 강화하면 입력 편의가 더 좋아질 수 있습니다.


193-230: 작성한 글(content) 배열 필드
handleFieldAppendhandleFieldRemove를 통해 배치, SNS 부분과 유사한 로직이 반복됩니다. 공통화 및 재사용 가능성을 염두에 두면 코드 유지보수에 도움이 됩니다.


231-273: 프로젝트(project) 배열 필드
배열 필드 로직이 일관된 패턴으로 유지되어 있습니다. 다만, 필드 생성을 위한 초기값이 여러 곳에서 설정되므로, 공통 상수나 factory 함수를 정의해 중복을 없앨 수 있겠습니다.

src/features/multi-step-form/ui/careerForm/tagFormStep/ui/TagBox.tsx (2)

12-13: 컴포넌트에서 외부 스토어 함수 직접 사용
Props로 전달하던 로직을 제거하고, 곧바로 Zustand 스토어의 함수를 사용하는 구조는 간결해졌습니다. 대신, 복잡한 로직이 스토어 쪽에 집중될 수 있으니 유지보수 시 잘 관리하세요.


17-50: 태그 위치 계산 로직
getTagPositions 내부에서 태그 선택 순서별로 위치를 지정하고 있으며, 중복되는 조건문이 많습니다. 이 로직을 객체 매핑 또는 배열 인덱스 활용으로 줄일 수 있으면 관리가 더욱 편리해질 것입니다.

src/features/multi-step-form/utils/index.tsx (2)

14-33: 도메인 패턴 매핑에 대한 확장 고려
현재 주요 플랫폼들만 대응하지만, youtu.be와 같은 짧은 도메인이나 기타 TLD를 처리해야 할 수도 있습니다. 향후 새로운 플랫폼 도메인 추가 시, 해당 배열에만 간단히 패턴을 추가하면 확장할 수 있어 유연성이 좋습니다.


89-90: 옵셔널 체이닝 사용 제안
content && content.every(...) 대신 content?.every(...) 형태로 옵셔널 체이닝을 적용하면 코드 가독성과 안전성이 조금 더 향상됩니다.

-if (content && content.every((contentItem) => contentItem.link !== '')) {
+if (content?.every((contentItem) => contentItem.link !== '')) {
  formData.append('content', JSON.stringify(content));
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 89-90: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/features/multi-step-form/ui/careerForm/index.tsx (1)

94-95: 테스트용 콘솔 코드
현재 주석 처리된 console.log(data)는 실제 배포 시 제거하거나 로깅을 활용한 별도의 디버깅 방식으로 대체할 수 있습니다.

src/features/multi-step-form/schema/index.ts (2)

30-38: 중복 검증 규칙 간소화 필요
min(1)과 뒤이어 refine((value) => value.length > 1)가 동시에 선언되어 있으나, 사실상 최소 길이가 2로 정의된 셈이라 중복 표현일 수 있습니다. min(2) 혹은 해당 refine만 사용하는 방식으로 검증 규칙을 일관성 있게 맞추는 것이 좋습니다.


67-67: 오타 수정 제안
에러 메시지에서 '최소 하나의 지역' 처럼 두 칸 공백이 발생하고 있습니다. 작은 부분이지만 UI/UX 면에서 혼란이 없도록 메시지를 정제하면 좋겠습니다.

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66d3062 and 83c3770.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (39)
  • package.json (2 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/features/multi-step-form/components/AvartarImg.tsx (1 hunks)
  • src/features/multi-step-form/components/Cardview.tsx (1 hunks)
  • src/features/multi-step-form/components/EditableCardField.tsx (1 hunks)
  • src/features/multi-step-form/config/index.ts (2 hunks)
  • src/features/multi-step-form/hooks/queries/useCreateCard.ts (1 hunks)
  • src/features/multi-step-form/hooks/queries/useScrap.ts (1 hunks)
  • src/features/multi-step-form/hooks/useEditingStates.ts (1 hunks)
  • src/features/multi-step-form/schema/index.ts (1 hunks)
  • src/features/multi-step-form/ui/careerForm/firstStep.tsx (2 hunks)
  • src/features/multi-step-form/ui/careerForm/fourthStep.tsx (2 hunks)
  • src/features/multi-step-form/ui/careerForm/index.tsx (3 hunks)
  • src/features/multi-step-form/ui/careerForm/secondStep.tsx (2 hunks)
  • src/features/multi-step-form/ui/careerForm/tagFormStep/config/config.ts (2 hunks)
  • src/features/multi-step-form/ui/careerForm/tagFormStep/ui/Ball.tsx (1 hunks)
  • src/features/multi-step-form/ui/careerForm/tagFormStep/ui/MotionTagBox.tsx (1 hunks)
  • src/features/multi-step-form/ui/careerForm/tagFormStep/ui/TagBox.tsx (1 hunks)
  • src/features/multi-step-form/ui/careerForm/thridStep.tsx (1 hunks)
  • src/features/multi-step-form/ui/index.tsx (2 hunks)
  • src/features/multi-step-form/utils/index.tsx (1 hunks)
  • src/features/new-card/config/index.tsx (1 hunks)
  • src/features/new-card/hooks/queries/useRegisterQuery.ts (1 hunks)
  • src/features/new-card/ui/newCreateCardView.tsx (1 hunks)
  • src/shared/hooks/useOnClickOutside.ts (1 hunks)
  • src/shared/spacing/spacing.ts (1 hunks)
  • src/shared/store/cardFormState.ts (1 hunks)
  • src/shared/types/index.ts (1 hunks)
  • src/shared/ui/Input/index.tsx (3 hunks)
  • src/shared/ui/Input/input.tsx (2 hunks)
  • src/shared/ui/Input/tagInput.tsx (3 hunks)
  • src/shared/ui/dropDown/index.tsx (1 hunks)
  • src/shared/ui/dropDown/searchDropdown.tsx (1 hunks)
  • src/shared/ui/label.tsx (1 hunks)
  • src/shared/ui/tag/motionTag.tsx (1 hunks)
  • src/shared/ui/tag/tag.tsx (3 hunks)
  • src/shared/ui/tagInput.tsx (1 hunks)
  • src/shared/ui/textArea/index.tsx (1 hunks)
  • tailwind.config.ts (1 hunks)
🧰 Additional context used
🧬 Code Definitions (14)
src/features/multi-step-form/hooks/queries/useCreateCard.ts (1)
src/shared/types/index.ts (1) (1)
  • ApiResponseType (1:6)
src/shared/ui/Input/index.tsx (1)
src/shared/ui/Input/input.tsx (1) (1)
  • InputBody (48:48)
src/features/multi-step-form/hooks/queries/useScrap.ts (1)
src/shared/types/index.ts (1) (1)
  • ApiResponseType (1:6)
src/features/multi-step-form/config/index.ts (1)
src/features/multi-step-form/schema/index.ts (1) (1)
  • CareerFormData (123:123)
src/features/multi-step-form/ui/careerForm/tagFormStep/ui/MotionTagBox.tsx (1)
src/features/multi-step-form/ui/careerForm/tagFormStep/config/config.ts (2) (2)
  • TagConfigItem (6:19)
  • tagConfig (21:107)
src/shared/ui/Input/input.tsx (1)
src/shared/lib/utils.ts (1) (1)
  • cn (4:6)
src/features/multi-step-form/ui/careerForm/fourthStep.tsx (1)
src/features/multi-step-form/ui/careerForm/tagFormStep/config/config.ts (1) (1)
  • tagConfig (21:107)
src/features/multi-step-form/ui/careerForm/secondStep.tsx (1)
src/shared/store/cardFormState.ts (1) (1)
  • useCardFormStore (17:56)
src/app/layout.tsx (2)
src/shared/lib/font.ts (1) (1)
  • pretendard (3:53)
src/shared/providers/index.tsx (1) (1)
  • Providers (8:15)
src/features/new-card/hooks/queries/useRegisterQuery.ts (1)
src/shared/types/index.ts (1) (1)
  • ApiResponseType (1:6)
src/shared/ui/Input/tagInput.tsx (1)
src/shared/ui/Input/input.tsx (2) (2)
  • InputBodyProps (26:29)
  • inputVariants (48:48)
src/features/multi-step-form/ui/careerForm/tagFormStep/ui/TagBox.tsx (2)
src/shared/store/cardFormState.ts (1) (1)
  • useCardFormStore (17:56)
src/features/multi-step-form/ui/careerForm/tagFormStep/config/config.ts (2) (2)
  • TagConfigItem (6:19)
  • tagConfig (21:107)
src/features/multi-step-form/ui/careerForm/firstStep.tsx (3)
src/features/multi-step-form/schema/index.ts (1) (1)
  • CareerFormData (123:123)
src/shared/store/cardFormState.ts (1) (1)
  • useCardFormStore (17:56)
src/shared/ui/dropDown/searchDropdown.tsx (1) (1)
  • SearchOptions (13:18)
src/features/multi-step-form/ui/careerForm/index.tsx (2)
src/features/multi-step-form/schema/index.ts (2) (2)
  • CareerFormData (123:123)
  • cardCreateSchema (3:121)
src/features/multi-step-form/config/index.ts (1) (1)
  • TOTAL_STEPS (20:20)
🪛 Biome (1.9.4)
src/features/multi-step-form/utils/index.tsx

[error] 89-90: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 94-95: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (110)
src/shared/spacing/spacing.ts (1)

17-17: 일관된 스페이싱 시스템 확장 👍

새로운 패딩 변형 ms를 추가하여 12px 값을 지원하도록 했습니다. 이는 xs(4px)와 sm(8px) 사이의 중간 값으로, 더 세밀한 레이아웃 제어가 가능해졌습니다. 이 변경은 tailwind.config.ts의 spacing 설정과도 일치하여 일관성을 유지했습니다.

tailwind.config.ts (1)

132-132: 중간 크기의 스페이싱 값 추가 👍

spacing 객체에 'ms' 값(12px)을 추가한 것은 좋은 결정입니다. 이는 sm(8px)과 md(16px) 사이의 세밀한 간격 제어가 필요한 UI 요소에 유용하게 사용될 수 있습니다. 이 변경은 spacingStyles의 padding 변형 추가와 일치하여 디자인 시스템의 일관성을 유지합니다.

src/shared/types/index.ts (1)

1-6: 표준화된 API 응답 타입 정의 👍

ApiResponseType<T> 제네릭 타입을 정의하여 API 응답 구조를 표준화한 것은 좋은 접근입니다. 이를 통해 응답 처리가 일관되고 타입 안전성이 향상됩니다. 상태, 메시지, 타임스탬프 및 실제 데이터를 포함하는 구조는 명확하고 이해하기 쉽습니다.

src/features/multi-step-form/ui/careerForm/tagFormStep/ui/Ball.tsx (1)

25-38: 사용자 상호작용 처리 개선

pointer-events-none 클래스를 추가하여 파도 효과를 가진 div 요소들이 마우스 이벤트를 캡처하지 않도록 변경한 것은 좋은 개선입니다. 이렇게 하면 사용자가 태그 구슬과 상호작용할 때 배경 요소가 간섭하지 않게 됩니다.

src/shared/ui/label.tsx (1)

1-19: 재사용 가능한 레이블 컴포넌트 구현 완료

Radix UI를 활용한 레이블 컴포넌트가 잘 구현되어 있습니다. forwardRef를 사용하여 레퍼런스를 전달하고, class-variance-authority를 통해 스타일 변형을 관리하는 패턴이 적절합니다.

package.json (4)

19-19: 새로운 드롭다운 메뉴 기능 추가에 필요한 종속성 추가

Radix UI 드롭다운 메뉴 라이브러리가 적절히 추가되었습니다. 이는 명함 생성 인터페이스에서 드롭다운 기능을 구현하는데 사용될 것으로 보입니다.


28-29: 한글 처리 및 애니메이션 기능을 위한 라이브러리 추가

명함 생성 도메인에 필요한 두 가지 중요한 라이브러리가 추가되었습니다:

  • es-hangul: 한글 텍스트 처리 라이브러리로, 이름이나 직함 등의 한글 데이터를 다루는데 유용합니다.
  • framer-motion: UI 애니메이션 라이브러리로, 사용자 경험을 향상시키는 부드러운 전환과 애니메이션을 구현할 수 있습니다.

37-37: 검색 가능한 드롭다운 구현을 위한 라이브러리 추가

react-select 라이브러리는 고급 선택 컴포넌트를 제공하여 명함 생성 시 직업, 회사명 등을 검색하고 선택할 수 있는 기능을 구현하는데 도움이 될 것입니다.


43-44: 상태 관리를 위한 Zustand 라이브러리 추가

Zustand 상태 관리 라이브러리가 적절히 추가되었습니다. 이는 명함 생성 과정에서 여러 단계의 폼 상태를 관리하는데 유용할 것입니다. React Context API보다 더 간결하고 성능이 좋은 대안입니다.

src/app/layout.tsx (1)

26-32: React Fragment로 레이아웃 컴포넌트 구조 개선

레이아웃 컴포넌트의 JSX 반환 구조를 React Fragment(<> ... <>)로 감싸는 것은 좋은 패턴입니다. 이는 실제 DOM에 추가 노드를 생성하지 않으면서 여러 요소를 그룹화할 수 있게 해줍니다. 향후 레이아웃 확장 시 유연성이 향상됩니다.

src/features/new-card/ui/newCreateCardView.tsx (2)

7-7: Zustand 스토어를 활용한 상태 관리 개선

Zustand 상태 관리 라이브러리를 활용하여 명함 생성 상태를 관리하는 방식은 적절합니다. useCardFormStoresetJob 함수를 통해 사용자가 선택한 직업 유형을 중앙 집중식으로 관리할 수 있게 되었습니다.

Also applies to: 14-14


29-36: UI 구조 및 사용자 경험 개선

헤더와 리스트의 레이아웃이 잘 구성되어 있습니다. 사용자에게 명확한 안내와 선택 옵션을 제공하는 방식이 적절합니다.

src/shared/hooks/useOnClickOutside.ts (1)

5-29: useOnClickOutside 훅 구현

외부 클릭을 감지하는 훅이 잘 구현되어 있습니다. 이 훅은 드롭다운, 모달 등의 UI 컴포넌트에서 외부 클릭 시 닫히는 기능을 구현하는 데 유용합니다. 코드에 적절한 주석이 포함되어 있고, 이벤트 리스너의 등록 및 정리가 올바르게 처리되었습니다.

src/features/multi-step-form/config/index.ts (1)

24-32: LGTM! Record 타입 활용이 적절합니다.

FIELD_TAG_MAPPING 객체는 Record 타입과 Pick 유틸리티를 활용하여 타입 안정성을 확보했습니다. 이것은 폼 데이터와 태그 값 간의 매핑을 명확하게 정의하여 유지보수성을 높입니다.

src/shared/ui/tag/tag.tsx (2)

48-48: 'create-input' 스타일 정의가 추가되었습니다.

새로운 사이즈 옵션에 대한 스타일 정의가 적절히 추가되었습니다. 다양한 사이즈 옵션을 제공함으로써 컴포넌트의 유연성이 향상되었습니다.


70-70: 버튼 여백 조정이 적용되었습니다.

버튼의 왼쪽 여백이 ml-1에서 ml-[2px]로 변경되었습니다. 이 미세한 조정으로 UI의 세부 사항이 개선되었습니다.

src/features/multi-step-form/ui/careerForm/tagFormStep/ui/MotionTagBox.tsx (1)

1-56: MotionTagBox 컴포넌트 구현이 잘 되었습니다.

Framer Motion 라이브러리를 활용하여 태그 애니메이션을 효과적으로 구현했습니다. Zustand 상태 관리와의 통합도 적절하게 처리되었습니다. 각 태그의 애니메이션 상태(선택/미선택)에 따른 변형이 명확하게 정의되어 있고, 인터랙션도 잘 구현되었습니다.

src/features/multi-step-form/hooks/queries/useScrap.ts (1)

8-19: 타입 정의가 명확합니다.

ScrapType, ScrapPayload, ScrapResponse 타입이 명확하게 정의되어 있어 API와의 인터페이스가 잘 구조화되어 있습니다.

src/shared/ui/Input/input.tsx (3)

16-18: 에러 상태를 위한 스타일 변형 추가가 적절합니다.

에러 표시를 위한 border 스타일이 명확하게 정의되어 있습니다.


32-37: 에러 속성이 inputVariants에 올바르게 적용되었습니다.

error 속성을 추출하고 className에 적용하는 방식이 적절합니다.


48-48: InputBody와 함께 inputVariants를 내보내는 것이 적절합니다.

이 변경으로 다른 컴포넌트에서 스타일 변형을 직접 활용할 수 있어 재사용성이 향상됩니다.

src/features/multi-step-form/ui/index.tsx (2)

28-39: handleStepBack의 의존성 배열이 [handleBack]으로 변경되었습니다.

이전에는 [currentStep]에 의존했지만 이제 [handleBack]에만 의존합니다. 이 함수도 currentStep을 참조하지만 의존성에서 제외되었습니다. 함수형 업데이트를 사용하므로 상태 업데이트에는 문제가 없을 수 있지만, 이 변경 사항으로 handleBack이 변경될 때만 handleStepBack이 재생성됩니다.


19-26:

✅ Verification successful

handleNextStep의 의존성 배열 변경에 대한 검토가 필요합니다.

기존에는 [currentStep]에 의존했지만 이제 빈 배열 []로 변경되었습니다. 이 함수는 currentStep을 참조하지만 의존성에 포함되지 않아 클로저 문제가 발생할 가능성이 있습니다. setCurrentStep은 함수형 업데이트를 사용하므로 안전하지만, 이러한 변경의 의도가 명확해야 합니다.


🌐 Web query:

React useCallback 함수형 업데이트와 의존성 배열 관계

💡 Result:

React에서 useCallback은 함수를 메모이제이션하여 불필요한 재생성과 리렌더링을 방지하는 Hook입니다. 함수형 업데이트setState에 함수를 전달해 최신 상태를 안전하게 참조하는 기법이며, 의존성 배열useCallback이 함수 재생성 시점을 결정하는 핵심 요소입니다. 두 개념의 관계는 다음과 같이 요약됩니다:


useCallback과 의존성 배열

  1. 메모이제이션된 함수 관리
    useCallback(() => { ... }, [deps])deps 배열의 값이 변경될 때만 함수를 재생성합니다.

    • 빈 배열 []: 컴포넌트 마운트 시점의 값에 의존[1][6].
    • 의존성 포함 [state]: 해당 state 변경 시마다 함수 재생성[4][5].
  2. 렌더링 최적화
    자식 컴포넌트에 함수를 전달할 때, 의존성 배열을 통해 함수 참조 안정성을 유지하여 불필요한 리렌더링 방지[8][9].


함수형 업데이트와의 관계

함수형 업데이트(setState(prev => ...))는 useCallback의 의존성 배열을 간소화하는 데 활용됩니다:

  • 최신 상태 접근
    함수형 업데이트는 setState에 전달된 콜백이 항상 최신 상태를 인자로 받기 때문에, useCallback의 의존성 배열에서 해당 상태를 제거할 수 있습니다[1][7].
    // 의존성 배열 없이 최신 data 참조
    const updateData = useCallback((newItem) => {
      setData((prevData) => [...prevData, newItem]);
    }, []);
  • 의존성 배열 축소
    상태 변경 로직을 함수형 업데이트로 처리하면 해당 상태를 의존성 배열에 포함시킬 필요가 사라집니다. 이로 인해 함수 재생성 빈도 감소[6][10].

주의 사항

상황 함수형 업데이트 미사용 함수형 업데이트 사용
상태 참조 의존성 배열에 상태 추가 필수 의존성 배열에서 상태 제외 가능
최신 값 초기 상태 고정 항상 최신 상태 참조
리렌더링 상태 변경 시 함수 재생성 동일 함수 참조 유지

예시:

// ❌ 상태를 의존성 배열에 추가해야 최신 값 참조
const handleSubmit = useCallback(() => {
  post('/api', { data });
}, [data]);  // data 변경 시마다 재생성

// ✅ 함수형 업데이트로 의존성 제거
const handleSubmit = useCallback(() => {
  setData((prevData) => [...prevData, newData]);  // prevData는 항상 최신
}, []);  // 재생성 없음

결론

  • 함수형 업데이트useCallback과 결합해 의존성 배열을 최소화하고 최신 상태를 안전하게 참조합니다.
  • 의존성 배열은 함수가 외부 상태에 의존할 때만 필수로 포함시키고, 그렇지 않으면 함수형 업데이트로 대체하여 성능을 최적화합니다[4][7][10].

Citations:


핵심: 현재 코드에서는 함수형 업데이트를 활용해 빈 의존성 배열 사용이 안전함이 확인되었습니다.

handleNextStep은 setCurrentStep에 함수형 업데이트 패턴을 사용하기 때문에, 이전 상태(currentStep)를 명시적으로 의존성 배열에 포함하지 않아도 항상 최신 상태를 참조합니다. 다만, 이 변경 의도가 명확하도록 추가 주석을 달거나, 관련 문서에 변경 사유를 간략히 기재하면 가독성과 유지보수에 도움이 될 것 같습니다.

src/shared/store/cardFormState.ts (1)

17-29: 기본 상태 및 job 설정 함수가 적절히 구현되어 있습니다.

기본값 설정과 setJob 함수의 구현이 좋습니다. typeof job === 'function' 체크로 함수형 업데이트를 지원하는 점이 유연합니다.

src/features/multi-step-form/hooks/useEditingStates.ts (1)

1-54: 잘 구현된 커스텀 훅입니다!

useEditingStates 훅은 필드 배열의 편집 상태를 관리하는 기능을 잘 구현했습니다. 상태 초기화, 필드 배열 변경에 대한 반응, 그리고 다양한 상태 관리 함수들이 체계적으로 구현되어 있습니다. JSDoc을 통한 문서화도 잘 되어 있어 사용법이 명확합니다.

src/features/multi-step-form/ui/careerForm/secondStep.tsx (4)

3-6: Zustand 스토어 통합 개선이 좋습니다!

Zustand를 사용한 상태 관리로의 전환은 코드 구조를 개선했습니다.


12-14: 타입 이름 변경 및 반환 타입 개선

SecondStepPropsType에서 SecondStepProps로의 이름 변경과 handleNextStep의 반환 타입 변경이 더 명확합니다.


17-18: 상태 관리 방식 개선

Zustand 스토어를 활용한 상태 관리가 잘 구현되었습니다.


27-27: TagBox 컴포넌트 사용 방식 개선

TagBox 컴포넌트에 props를 전달하지 않고 Zustand 스토어를 직접 사용하도록 변경한 것은 prop drilling을 줄이는 좋은 접근법입니다.

src/features/multi-step-form/ui/careerForm/firstStep.tsx (6)

5-12: 적절한 컴포넌트 및 쿼리 가져오기

필요한 컴포넌트와 훅을 잘 가져왔습니다. useRegisterQuery, useCardFormStore, SearchDropdown, TagInput, Textarea는 모두 리팩토링된 form에 필요한 요소들입니다.


24-28: 직군 데이터 가져오기 로직이 잘 구현되었습니다

Zustand 스토어의 job 상태를 기반으로 useRegisterQuery를 호출하여 career 옵션을 가져오는 구현이 잘 되어 있습니다.


41-55: 필드 이름 및 컴포넌트 업데이트

name 필드를 nickname으로 변경하고 관련 오류 처리도 적절하게 업데이트했습니다.


56-82: SearchDropdown 구현이 잘 되었습니다

detailJobId 필드에 대한 SearchDropdown 사용이 효과적으로 구현되었습니다. 선택된 옵션의 id만 추출하여 전달하는 로직이 명확하고, 현재 필드 값에 해당하는 옵션을 찾는 방식도 효율적입니다.


83-96: TagInput 컴포넌트 사용 개선

domain 필드를 interestDomain으로 변경하고 WrappedTagInput에서 TagInput으로 변경한 것은 좋은 개선입니다.


97-113: Textarea 컴포넌트 사용 개선

description 필드를 summary로 변경하고 WrappedInput에서 Textarea로 변경한 것은 적절한 수정입니다. 문자 수 제한 기능을 추가한 것도 좋은 개선입니다.

src/features/multi-step-form/components/EditableCardField.tsx (9)

4-11: 필요한 컴포넌트와 유틸리티 가져오기

필요한 컴포넌트와 유틸리티를 잘 가져왔습니다. WrappedInput, Label, 그리고 필요한 상수와 훅들이 적절하게 임포트되었습니다.


13-29: 타입 정의가 명확합니다

FieldDataFieldProps 인터페이스가 명확하게 정의되어 있어 코드의 타입 안전성이 보장됩니다.


30-45: Props 구조가 잘 정의되었습니다

EditableCardFieldProps 타입이 잘 정의되어 있어 컴포넌트의 사용법이 명확합니다.


46-67: JSDoc 문서화가 잘 되어 있습니다

컴포넌트의 목적과 각 props의 용도가 JSDoc을 통해 상세하게 문서화되어 있어 다른 개발자들이 이해하기 쉽습니다.


102-128: 스크래핑 로직이 잘 구현되었습니다

링크에 대한 스크래핑 로직과 오류 처리가 잘 구현되어 있습니다. 스크래핑에 실패했을 때도 적절한 대체 로직을 제공합니다.


129-146: 포커스 상실 처리 로직이 적절합니다

포커스를 잃었을 때 필드 업데이트와 편집 모드 비활성화 로직이 체계적으로 구현되어 있습니다.


159-191: 컴포넌트 구조가 잘 설계되었습니다

컴포넌트의 렌더링 로직이 조건에 따라 적절히 분기되어 있고, 필요한 props가 잘 전달되고 있습니다.


205-219: EditableCardInputView 컴포넌트의 메모이제이션 적용이 좋습니다

memo를 사용하여 EditableCardInputView 컴포넌트를 메모이제이션한 것은 불필요한 렌더링을 방지하는 좋은 최적화 방법입니다.


232-234: EditableCardEditView 컴포넌트 메모이제이션 적용이 좋습니다

EditableCardEditView 컴포넌트에도 memo를 적용하여 성능을 최적화한 것이 좋습니다.

src/features/multi-step-form/ui/careerForm/fourthStep.tsx (2)

4-4: 훌륭한 리팩토링!

React Hook Form을 활용하여 폼 상태 관리를 구현한 점이 좋습니다. useFormContext 훅을 사용해 상위 폼 컨텍스트에 접근하는 방식으로 컴포넌트 간 상태 공유를 효율적으로 처리했습니다.

Also applies to: 12-13, 19-20


49-76:

✅ Verification successful

Controller 컴포넌트 활용이 적절합니다.

Controller 컴포넌트를 사용하여 태그 선택 UI와 폼 상태를 연결한 방식이 깔끔합니다. 특히 tag.value.toUpperCase()를 통해 백엔드 API와의 일관성을 유지하려는 노력이 보입니다.

다만, 여기서 한 가지 검증이 필요합니다:


🏁 Script executed:

#!/bin/bash
# CareerFormData 스키마에서 previewInfoType 필드가 대문자 값을 기대하는지 확인

echo "Checking CareerFormData schema for previewInfoType field..."
cat src/features/multi-step-form/schema/index.ts | grep -A 10 "previewInfoType"

Length of output: 372


검증 완료 – Controller 컴포넌트 활용 및 대문자 변환이 적절합니다.

  • CareerFormData 스키마에서 previewInfoType 필드가 대문자 문자열(예: "PROJECT", "CONTENT", ...)을 기대하는 점과 코드에서 tag.value.toUpperCase()를 통해 이를 충족하고 있는 점이 확인되었습니다.
src/features/new-card/hooks/queries/useRegisterQuery.ts (2)

1-6: 적절한 import 구조입니다.

필요한 라이브러리와 타입들을 명확하게 가져오고 있습니다.


7-23: 타입 정의가 명확합니다.

API 응답과 요청 파라미터에 대한 타입 정의가 잘 되어 있습니다. CardJobType을 유니온 타입으로 정의하여 타입 안전성을 확보했습니다.

src/features/multi-step-form/ui/careerForm/tagFormStep/config/config.ts (3)

4-4: TagValue 타입 정의가 명확합니다.

TagValue 타입을 유니온 타입으로 정의하여 가능한 태그 값들을 명확하게 제한했습니다. 이는 타입 안전성을 높이는 좋은 접근 방식입니다.


9-10: TagConfigItem 인터페이스 확장이 적절합니다.

인터페이스에 value 속성과 위치 관련 속성(initialPosition, endPosition)을 추가한 것은 좋은 접근입니다. 특히 위치 속성에 대한 주석을 통해 의도를 명확히 했습니다.

Also applies to: 16-18


26-27: tagConfig 업데이트가 일관성 있게 적용되었습니다.

모든 태그 항목에 새로운 속성(value, initialPosition, endPosition)이 일관되게 추가되었습니다. 특히 value 값이 TagValue 타입과 일치하여 타입 안전성을 유지했습니다.

Also applies to: 39-40, 51-56, 63-68, 75-80, 87-92, 99-104

src/features/multi-step-form/components/AvartarImg.tsx (1)

1-5: React Hook Form 통합이 깔끔합니다.

'use client' 지시문 추가 및 필요한 모듈을 임포트하고 useFormContext를 사용하여 폼 컨텍스트에 접근하는 방식이 깔끔합니다.

Also applies to: 9-10, 12-13

src/shared/ui/Input/tagInput.tsx (7)

5-7: 적절한 유틸리티 가져오기가 추가됨

cn 유틸리티를 사용하여 클래스 이름을 조건부로 적용하는 것은 좋은 방법입니다. 이는 컴포넌트의 가독성과 유지보수성을 향상시킵니다.


19-26: 태그 추가 로직이 개선됨

trimmedValue 변수를 사용하여 입력 값을 정리하고, 이 값이 비어있지 않고 기존 태그에 없을 때만 추가하는 로직이 적절하게 구현되었습니다.


35-39: 간격 클래스 제거 및 컨테이너 구조 개선

gap 클래스를 제거하고 구조를 간소화하여 레이아웃이 개선되었습니다.


41-46: 태그 UI 구현 개선

태그 위치와 스타일이 적절하게 구현되었습니다. 태그 삭제 기능도 잘 동작할 것으로 보입니다.


51-58: TagInputBody 컴포넌트로 교체

기존 InputBody에서 새로운 TagInputBody 컴포넌트로 변경된 것이 확인됩니다. 이 변경은 태그 입력에 특화된 기능을 제공할 수 있게 합니다.


64-77: 새로운 TagInputBody 컴포넌트 구현

TagInputBody 컴포넌트가 적절하게 구현되었습니다. forwardRef를 사용하여 ref를 전달하고, inputVariants를 활용하여 스타일을 적용하는 방식이 좋습니다.


80-80: displayName 설정 추가

TagInputBody 컴포넌트에 displayName을 설정한 것은 디버깅과 개발 도구에서의 식별을 위해 좋은 방법입니다.

src/shared/ui/Input/index.tsx (8)

11-14: 추가된 Props 타입이 적절함

error, errorMsg, closeBtn, closeBtnClick 등의 새로운 속성이 추가되어 컴포넌트의 기능이 확장되었습니다. 각 속성의 목적이 명확하게 정의되어 있습니다.


31-37: 제어/비제어 상태 관리 개선

컴포넌트가 제어 컴포넌트(controlled)와 비제어 컴포넌트(uncontrolled) 모두를 지원하도록 개선되었습니다. React Hook Form과의 통합을 위한 좋은 접근 방식입니다.


38-45: handleChange 함수 추가

입력값 변경을 처리하는 함수가 추가되었습니다. 제어/비제어 상태에 따라 적절히 처리하는 로직이 잘 구현되어 있습니다.


47-54: handleClearInput 함수 개선

입력 필드 초기화 기능이 개선되었습니다. 제어/비제어 상태에 따라 적절히 처리하고, closeBtnClick 콜백을 호출하는 로직이 잘 구현되어 있습니다.


71-80: 컴포넌트 구조 개선

InputBody에 error 속성을 전달하고, 값과 이벤트 핸들러를 적절하게 설정하는 등 컴포넌트 구조가 개선되었습니다.


81-90: 닫기 버튼 추가 기능

closeBtn 속성이 true일 때 닫기 버튼을 렌더링하는 기능이 추가되었습니다. 사용자 경험 향상을 위한 좋은 기능입니다.


92-92: 오류 메시지 표시 기능 추가

errorMsg가 있을 때 오류 메시지를 표시하는 기능이 추가되었습니다. 사용자에게 피드백을 제공하는 중요한 기능입니다.


100-100: 기본 내보내기 설정

컴포넌트를 기본 내보내기로 설정하여 다른 컴포넌트에서 쉽게 가져와 사용할 수 있도록 했습니다.

src/shared/ui/tagInput.tsx (9)

3-10: 적절한 의존성 가져오기

필요한 훅과 컴포넌트를 적절히 가져왔습니다. 특히 useOnClickOutside 훅을 사용하여 외부 클릭을 처리하는 것은 좋은 UX를 제공합니다.


12-18: TagInputProps 타입 정의가 명확함

컴포넌트의 속성을 위한 타입 정의가 명확하게 되어 있습니다. 외부 제어를 위한 value와 onChange 속성을 포함하여 컴포넌트의 유연성을 높였습니다.


20-40: 자세한 컴포넌트 문서화

JSDoc 형식으로 컴포넌트와 속성에 대한
상세한 설명과 사용 예시를 제공한 것은 매우 좋은 방법입니다. 이는 다른 개발자가 컴포넌트를 이해하고 사용하는 데 큰 도움이 됩니다.


42-60: ref 전달 처리가 정교함

forwardRef를 사용하여 외부에서 전달받은 ref를 내부 ref에 연결하는 로직이 잘 구현되어 있습니다. 다양한 ref 유형(함수, 객체)을 모두 처리할 수 있습니다.


66-72: 한글 입력 처리를 위한 컴포지션 이벤트 관리

컴포지션 이벤트(composition event)를 처리하여 한글과 같은 조합형 문자 입력을 적절히 지원하는 것은 매우 중요한 기능입니다. 이는 한국어 사용자 경험을 크게 향상시킵니다.


78-90: 키보드 이벤트 처리가 종합적임

다양한 키(공백, 쉼표, 탭, 백스페이스, 엔터)의 이벤트를 처리하여 태그 추가 및 삭제 기능을 구현한 것은 사용자 경험을 향상시키는 좋은 접근 방식입니다.


92-111: 태그 관리 기능이 잘 구현됨

태그 추가 및 삭제 기능이 제어/비제어 상태를 모두 고려하여 잘 구현되어 있습니다. 빈 태그 추가 방지와 중복 태그 검사도 적절히 처리됩니다.


113-118: 외부 클릭 처리가 적절함

입력 필드 외부를 클릭했을 때, 입력값이 있으면 태그로 추가하는 기능이 구현되어 있습니다. 이는 사용자 편의성을 향상시킵니다.


151-157: displayName 설정 및 내보내기

컴포넌트에 displayName을 설정하고 기본 내보내기로 지정한 것은 좋은 방법입니다. 이는 디버깅과 다른 파일에서의 사용성을 향상시킵니다.

src/shared/ui/dropDown/searchDropdown.tsx (8)

3-11: 필요한 의존성 가져오기

필요한 라이브러리와 유틸리티 함수가 적절히 가져와졌습니다. 특히 es-hangul 패키지의 disassemble 함수를 사용하여 한글 검색 기능을 개선한 것은 좋은 접근 방식입니다.


13-32: 타입 정의가 명확함

SearchOptions, SearchDropdownProps, CombineSearchDropdownProps 타입이 명확하게 정의되어 있습니다. 특히 react-select와 react-hook-form의 속성을 결합한 방식이 적절합니다.


34-56: 컴포넌트 구조가 간결함

forwardRef를 사용하여 ref를 전달하고, 필요한 속성들을 Select 컴포넌트에 전달하는 구조가 간결하고 명확합니다. 오류 메시지 처리도 적절히 구현되어 있습니다.


61-78: 한글 검색 기능 개선

한글 자모를 분해하여 검색하는 customFilterOption 함수가 잘 구현되어 있습니다. 이는 한글 검색의 정확도를 크게 향상시키는 중요한 기능입니다. 레이블과 키워드 모두에서 검색하는 방식도 적절합니다.


80-90: 일관된 폰트 스타일 설정

재사용 가능한 fontStyle 객체를 만들어 스타일을 일관되게 적용한 것은 좋은 방법입니다. 이는 코드 중복을 줄이고 유지보수성을 향상시킵니다.


153-196: 스크롤바 및 UI 요소 스타일링

option, menuList, placeholder 등의 UI 요소에 대한 스타일링이 잘 구현되어 있습니다. 특히 스크롤바를 숨기는 CSS를 적용하여 깔끔한 UI를 제공하는 것은 좋은 접근 방식입니다.


197-209: 선택된 값 스타일링

singleValue에 대한 스타일링이 잘 구현되어 있습니다. 텍스트 넘침 처리와 폰트 설정이 적절합니다.


211-211: 컴포넌트 내보내기

컴포넌트를 기본 내보내기로 설정하여 다른 파일에서 쉽게 가져와 사용할 수 있도록 했습니다.

src/shared/ui/tag/motionTag.tsx (3)

1-2: 클라이언트 전용 설정 확인
이 컴포넌트가 브라우저 환경에서만 동작하도록 'use client' 지시어를 사용한 것은 적절해 보입니다.


3-8: 필요한 라이브러리와 유틸 함수 임포트
React, class-variance-authority, framer-motion, cn 유틸 등 필요한 의존성을 잘 가져오고 있습니다. 현재로서는 임포트 구조에 문제될 부분이 없어 보입니다.


21-36: cva 스타일 정의 시 중복 속성 주의
py-[10px]pb-[10px]가 함께 정의되어 있는데, 의도대로인지를 확인해 주세요. 또한 z-[2147483647]라는 매우 높은 z-index는 다른 UI 요소를 가리지 않을지 주의가 필요합니다.

src/features/multi-step-form/ui/careerForm/thridStep.tsx (5)

76-78: 선택된 태그(selectedTags) 활용
Zustand 스토어의 tagArray로 선택된 태그를 관리하는 로직이 명확합니다. 다른 스텝에서도 동일 로직이 재사용되는지 확인하여, 전역 상태를 일관성 있게 유지하세요.


81-104: 소속 정보 필드 렌더링
selectedTags.includes(...) 조건으로 보여주기 때문에, 소속 정보가 필요한 경우에만 입력 필드가 드러납니다. 불필요한 필드를 감춘다는 점에서 UX가 향상될 것으로 보입니다.


106-143: SNS 동적 입력 필드
SNS 필드에 append, remove 기능을 제공해 사용자 친화적으로 구성되었습니다. 오류 메시지를 errors.sns?.[idx]?.link?.message 형태로 잘 연동하여, 에러 관리를 간결히 처리하고 있습니다.


145-159: 활동 지역 필드
이 부분은 다른 필드와 구조가 유사하며, 에러 처리가 동일 패턴으로 적용되고 있습니다. 전체적인 일관성이 유지되어 좋아 보입니다.


277-277: UI 구조 닫힘
컴포넌트가 JSX 구조 및 useFormContext 리턴까지 정상적으로 마무리됩니다. 전체적으로 폼 기능에 누락된 부분은 없어 보입니다.

src/features/multi-step-form/ui/careerForm/tagFormStep/ui/TagBox.tsx (1)

2-5: Zustand와 shallow 사용을 통한 최적화
useShallow를 사용해 필요한 상태(tagArray, handleTagClick)만 구독하는 점은 불필요한 리렌더링을 줄일 수 있어 적절해 보입니다.

src/features/multi-step-form/utils/index.tsx (2)

8-12: URL 파싱 로직 적절함
extractDomainUsingRegex 함수는 일반적인 HTTP/HTTPS URL에 대한 도메인 추출에 충분히 유효해 보입니다. 다만, 다른 프로토콜(예: ftp:// 등)을 다뤄야 하는 경우에는 추가 처리가 필요할 수 있습니다.


35-50: 플랫폼 식별 함수 구조 적절
getPlatformFromUrl는 직관적인 구성으로 빠르게 플랫폼 식별을 수행합니다. 도메인 검사가 실패할 경우 'LINK'를 반환하는 처리가 명확히 되어 있어 가독성이 좋습니다.

src/features/multi-step-form/ui/careerForm/index.tsx (4)

27-27: 타입 정의 변경 확인
handleNextStep() => Promise<void>에서 () => void로 교체되었습니다. 사용 범위에서 비동기 처리 여부를 다시 확인해 보시기 바랍니다.


30-64: 초기값 설정 구조화
initialValues에서 각 필드를 명확히 초기화하여, 폼 구성 및 타입 유효성을 유지하고 있습니다. 배열 필드(sns, content, project)도 기본 요소를 포함해 둠으로써 실제 사용 시 예외 처리가 줄어들 수 있습니다.


128-143: 스텝 검증 후 제출 흐름
handleNextStep에서 현재 스텝에 해당하는 필드를 trigger()로 검증한 다음, 유효성 통과 시 스텝 이동 또는 최종 제출을 처리하는 로직이 간결하게 잘 구성되었습니다.


148-155: 버튼 상태 및 표시 텍스트
disabled={!isStepValid} 처리와 '다음', '제출' 레이블 구분이 명확합니다. 2단계에서 버튼이 표시되지 않는 로직도 UI 요구사항에 부합하면 괜찮습니다.

src/shared/ui/dropDown/index.tsx (11)

1-9: 적절한 의존성 관리와 설정

'use client' 지시어를 사용하여 클라이언트 컴포넌트로 정확히 지정했습니다. Radix UI와 필요한 아이콘, 유틸리티 함수들을 잘 가져왔습니다. Took-FE 프로젝트에서 명함 생성 도메인 구현을 위한 드롭다운 UI를 만들기 위한 기본 설정이 잘 되어있습니다.


10-20: 기본 드롭다운 컴포넌트 정의 좋습니다

Radix UI 기본 컴포넌트를 직접 재정의하는 방식으로 구조화되어 있어 유지보수가 용이합니다. 이는 향후 컴포넌트의 동작이나 스타일을 확장하기에 좋은 기반이 됩니다.


22-42: DropdownMenuSubTrigger 구현 완성도 높음

forwardRef를 사용하여 ref 전달을 지원하고, inset 속성을 통해 들여쓰기 옵션을 제공하는 접근 방식이 훌륭합니다. ChevronRight 아이콘으로 하위 메뉴 존재를 직관적으로 나타내고 있습니다.


43-57: DropdownMenuSubContent 애니메이션 및 스타일링 우수

상태 변화에 따른 애니메이션이 잘 구현되어 있으며, 다양한 위치(top, right, bottom, left)에 따른 반응형 스타일링이 포함되어 있습니다. 사용자 경험 측면에서 매우 좋은 구현입니다.


58-76: DropdownMenuContent의 Portal 사용 적절

Portal을 사용하여 드롭다운 메뉴가 다른 요소들 위에 렌더링되도록 구현한 점이 좋습니다. 스크롤 가능한 컨텐츠를 위한 overflow 설정과 최대 높이 제한도 사용성을 고려한 좋은 접근입니다.


96-118: DropdownMenuCheckboxItem 적절한 구조

체크박스 인디케이터가 절대 위치로 잘 배치되어 있으며, Check 아이콘 사용이 직관적입니다. checked 상태 처리도 적절하게 구현되어 있습니다.


119-140: DropdownMenuRadioItem 디자인 일관성 유지

RadioItem이 CheckboxItem과 일관된 구조를 유지하면서도 Circle 아이콘을 사용하여 라디오 버튼의 특성을 잘 반영하고 있습니다. 시각적 계층 구조가 잘 유지되고 있습니다.


141-154: DropdownMenuLabel 적절한 구현

라벨 컴포넌트가 단순하고 명확하게 구현되어 있으며, inset 옵션을 통한 유연성도 제공하고 있습니다. 폰트 스타일 속성(font-semibold)을 통해 시각적 계층 구조를 강화한 점이 좋습니다.


155-166: DropdownMenuSeparator 간결한 구현

구분선 컴포넌트가 간결하고 효과적으로 구현되어 있습니다. 음수 마진(-mx-1)을 사용하여 시각적으로 구분선이 더 넓게 보이도록 한 부분이 세심한 디자인 고려를 보여줍니다.


167-171: DropdownMenuShortcut 키보드 접근성 고려

키보드 단축키를 표시하기 위한 컴포넌트가 추가된 점이 접근성 측면에서 좋습니다. tracking-widest를 적용해 가독성을 높인 부분도 사용자 경험 향상에 도움이 됩니다.


172-188: 컴포넌트 내보내기 완전성

모든 드롭다운 관련 컴포넌트가 명확하게 내보내져 있어 사용자가 필요한 컴포넌트를 선택적으로 임포트할 수 있습니다. 명함 생성 도메인에서 필요한 다양한 드롭다운 기능을 유연하게 구성할 수 있을 것입니다.

Comment on lines +1 to +23
import { useMutation } from '@tanstack/react-query';

import { client } from '@/shared/apis/client';
import { CLIENT_SIDE_URL } from '@/shared/constants';
import { ApiResponseType } from '@/shared/types';

const createCard = async (payload: FormData) => {
const res = await client.post<ApiResponseType<null>>(`${CLIENT_SIDE_URL}/api/card`, payload);
return res;
};

// 카드 생성 API 호출
export const useCreateCard = () => {
return useMutation({
mutationFn: createCard,
onSuccess: (data) => {
console.log('카드 생성 성공', data);
},
onError: (error) => {
console.error('카드 생성 실패', error);
},
});
};
Copy link

@coderabbitai coderabbitai bot Mar 18, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 및 사용자 피드백 개선 필요

카드 생성 API 호출을 위한 훅 구현이 잘 되어 있습니다. 그러나 현재 에러 처리가 콘솔 로그에만 의존하고 있어 사용자에게 피드백을 제공하지 않습니다.

다음과 같이 사용자에게 알림을 제공하는 기능을 추가하는 것이 좋겠습니다:

- import { useMutation } from '@tanstack/react-query';
+ import { useMutation } from '@tanstack/react-query';
+ import { toast } from 'react-toastify'; // 또는 프로젝트에서 사용 중인 알림 라이브러리

  import { client } from '@/shared/apis/client';
  import { CLIENT_SIDE_URL } from '@/shared/constants';
  import { ApiResponseType } from '@/shared/types';

  const createCard = async (payload: FormData) => {
    const res = await client.post<ApiResponseType<null>>(`${CLIENT_SIDE_URL}/api/card`, payload);
    return res;
  };

  // 카드 생성 API 호출
  export const useCreateCard = () => {
    return useMutation({
      mutationFn: createCard,
      onSuccess: (data) => {
        console.log('카드 생성 성공', data);
+       toast.success('카드가 성공적으로 생성되었습니다.');
      },
      onError: (error) => {
        console.error('카드 생성 실패', error);
+       toast.error('카드 생성에 실패했습니다. 다시 시도해주세요.');
      },
    });
  };

또한 반환값에 isLoading 상태를 활용하여 UI에 로딩 상태를 표시하는 것도 고려해보세요.

📝 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.

Suggested change
import { useMutation } from '@tanstack/react-query';
import { client } from '@/shared/apis/client';
import { CLIENT_SIDE_URL } from '@/shared/constants';
import { ApiResponseType } from '@/shared/types';
const createCard = async (payload: FormData) => {
const res = await client.post<ApiResponseType<null>>(`${CLIENT_SIDE_URL}/api/card`, payload);
return res;
};
// 카드 생성 API 호출
export const useCreateCard = () => {
return useMutation({
mutationFn: createCard,
onSuccess: (data) => {
console.log('카드 생성 성공', data);
},
onError: (error) => {
console.error('카드 생성 실패', error);
},
});
};
import { useMutation } from '@tanstack/react-query';
import { toast } from 'react-toastify'; // 또는 프로젝트에서 사용 중인 알림 라이브러리
import { client } from '@/shared/apis/client';
import { CLIENT_SIDE_URL } from '@/shared/constants';
import { ApiResponseType } from '@/shared/types';
const createCard = async (payload: FormData) => {
const res = await client.post<ApiResponseType<null>>(`${CLIENT_SIDE_URL}/api/card`, payload);
return res;
};
// 카드 생성 API 호출
export const useCreateCard = () => {
return useMutation({
mutationFn: createCard,
onSuccess: (data) => {
console.log('카드 생성 성공', data);
toast.success('카드가 성공적으로 생성되었습니다.');
},
onError: (error) => {
console.error('카드 생성 실패', error);
toast.error('카드 생성에 실패했습니다. 다시 시도해주세요.');
},
});
};

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

현재 고려중인 사항입니다. 코드래빗 그대 ... 당신은 좋은 리뷰어입니다.

Copy link

Choose a reason for hiding this comment

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

@jangwonyoon 감사합니다! 좋은 리뷰어라고 해주셔서 기쁩니다. 고려 중이신 사항이라고 하니 천천히 검토해보시길 바랍니다. 추가적인 도움이 필요하시면 언제든지 말씀해주세요. 계속해서 좋은 코드 리뷰로 도움이 되도록 하겠습니다. 😊

Comment on lines +7 to +15
interface CardFormState {
tagArray: TagValue[];
setTagArray: Dispatch<React.SetStateAction<TagValue[]>>;
tagCount: number;
incrementTagCount: Dispatch<React.SetStateAction<number>>;
handleTagClick: (tagMessage: TagValue) => void;
job: CardJobType;
setJob: Dispatch<React.SetStateAction<CardJobType>>;
}
Copy link

@coderabbitai coderabbitai bot Mar 18, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

CardFormState 인터페이스에 일부 메소드가 누락되어 있습니다.

인터페이스에는 resetTagArray 메소드가 선언되어 있지 않지만 구현부에는 존재합니다. 사용되는 모든 메소드를 인터페이스에 정의하는 것이 타입 안전성을 높입니다.

interface CardFormState {
  tagArray: TagValue[];
  setTagArray: Dispatch<React.SetStateAction<TagValue[]>>;
  tagCount: number;
  incrementTagCount: Dispatch<React.SetStateAction<number>>;
  handleTagClick: (tagMessage: TagValue) => void;
  job: CardJobType;
  setJob: Dispatch<React.SetStateAction<CardJobType>>;
+  resetTagArray: () => void;
}
📝 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.

Suggested change
interface CardFormState {
tagArray: TagValue[];
setTagArray: Dispatch<React.SetStateAction<TagValue[]>>;
tagCount: number;
incrementTagCount: Dispatch<React.SetStateAction<number>>;
handleTagClick: (tagMessage: TagValue) => void;
job: CardJobType;
setJob: Dispatch<React.SetStateAction<CardJobType>>;
}
interface CardFormState {
tagArray: TagValue[];
setTagArray: Dispatch<React.SetStateAction<TagValue[]>>;
tagCount: number;
incrementTagCount: Dispatch<React.SetStateAction<number>>;
handleTagClick: (tagMessage: TagValue) => void;
job: CardJobType;
setJob: Dispatch<React.SetStateAction<CardJobType>>;
resetTagArray: () => void;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

추후 진행 예정입니다.

Copy link

Choose a reason for hiding this comment

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

@jangwonyoon 네, 알겠습니다. 추후에 구현하실 때 도움이 필요하시면 말씀해 주세요.

🐰

Comment on lines +25 to +29
incrementTagCount: () => {
set((state) => ({
tagCount: state.tagCount + 1,
}));
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

incrementTagCount의 타입과 구현이 일치하지 않습니다.

incrementTagCountDispatch<React.SetStateAction<number>>로 선언되어 있지만, 구현은 파라미터를 받지 않고 항상 1을 증가시킵니다. 타입과 구현을 일치시키거나, 함수의 용도에 맞게 타입을 변경하는 것이 좋습니다.

-  incrementTagCount: Dispatch<React.SetStateAction<number>>;
+  incrementTagCount: () => void;

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +56 to +70
{tagConfig.map((tag) => (
<div key={tag.id}>
<Tag
message={tag.message} // 화면에 표시할 텍스트는 label 사용
size="lg"
className={cn(
'transition-all duration-500 ease-in-out',
!tagArray.includes(tag.value) && tag.animation,
tag.className,
tagArray.includes(tag.value) ? getTagPositions(tag.position, tag) : tag.position,
)}
onClick={() => handleTagClick(tag.value)} // 클릭 시 value 전달
/>
</div>
))}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

태그 렌더링 및 클릭 이벤트
tagConfig.map으로 태그 목록을 순회하며, 클릭 시 handleTagClick(tag.value)를 전달해주는 흐름이 명확합니다. 다만, 태그의 애니메이션 클래스(tag.animation)와 getTagPositions가 여러 군데서 조건을 처리하니, UI가 복잡해질 경우 구조화가 필요할 수 있습니다.

Comment on lines +77 to +95
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center gap-2 text-sm outline-none transition-colors focus:bg-gray-500 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
spacingStyles({ padding: 'md' }),
inset && 'pl-8',
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DropdownMenuItem 포커스 스타일 대비 확인 필요

전반적인 구현은 좋으나, focus 상태의 스타일에서 focus:bg-gray-500focus:text-zinc-900의 조합은 배경색이 어둡고 텍스트 색상도 어두워 대비가 낮을 수 있습니다. 접근성 측면에서 이 부분을 확인해보세요.


🌐 Web query:

색상 대비 WCAG 기준으로 gray-500과 zinc-900의 대비는 충분한가요?

💡 Result:

그레이-500과 징크-900의 색상 대비는 WCAG 2.1 기준을 충족하지 않습니다. 두 색상의 대비율은 약 3.35:1로, 일반 텍스트에 필요한 최소 4.5:1 미만이기 때문입니다[1][3][5].

주요 기준 요약

  • 일반 텍스트 (최소 4.5:1): 미충족 (3.35:1)
  • 큰 텍스트/굵은 텍스트 (최소 3:1): 충족 (3.35:1)[3][5]
  • WCAG AAA 수준 (7:1): 미충족[3]

디자인 시스템별 가이드 참고

  • Adobe Spectrum에서는 Gray-500을 비활성 텍스트/장식용으로만 사용하며, 4.5:1 대비 미달로 인해 주요 콘텐츠에 권장하지 않습니다[3][6].
  • Material Design은 색상 계급 차이 50단계 이상에서 4.5:1 대비를 보장하는 시스템을 채택했습니다[3][6].

대비 개선 방안

  1. 텍스트 크기 증가: 18pt 이상 또는 14pt 굵은 글씨로 변경 시 3:1 기준 충족 가능[5][9].
  2. 색상 조정: Gray-600(#4B5563)으로 변경 시 징크-900과의 대비율이 4.6:1로 WCAG AA 충족[3].

도구 활용 팁

  • **Coolors Contrast Checker**에서 실시간 대비 측정 가능[3][6].
  • Chrome의 Lighthouse 접근성 감사 기능으로 페이지 전체 대비 문제 진단 가능[1].

"색상 대비는 정보 인지의 핵심 요소입니다. 연한 회색 계열은 저시력자에게 가독성 문제를 유발할 수 있습니다." – 웹 접근성 전문가[3][6]

Citations:


DropdownMenuItem 포커스 스타일 대비 보완 필요

현재 focus:bg-gray-500focus:text-zinc-900 조합은 WCAG 2.1 일반 텍스트 기준(최소 4.5:1)을 충족하지 않아(대비율 약 3.35:1) 접근성 측면에서 개선이 필요합니다.

  • 해결 제안: 배경색을 예를 들어 gray-600(#4B5563) 등 WCAG AA 기준(대비율 최소 4.5:1) 이상을 만족하는 색상으로 변경하거나, 텍스트 색상과의 명도 차이를 재검토해 보세요.
  • 대상 파일: src/shared/ui/dropDown/index.tsx (라인 77-95)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (14)
src/features/multi-step-form/utils/index.tsx (1)

52-102: FormData 생성 함수가 체계적으로 구현되었습니다.

필수 필드와 선택적 필드를 구분하여 처리하는 방식이 명확합니다. 배열 필드의 경우 유효성 검사 후 JSON 문자열로 변환하는 것이 적절합니다.

정적 분석 도구의 제안에 따라 89-90줄과 94-95줄의 코드를 옵셔널 체이닝으로 개선할 수 있습니다:

-  if (content && content.every((contentItem) => contentItem.link !== '')) {
+  if (content?.every((contentItem) => contentItem.link !== '')) {
     formData.append('content', JSON.stringify(content));
   }

-  if (project && project.every((projectItem) => projectItem.link !== '')) {
+  if (project?.every((projectItem) => projectItem.link !== '')) {
     formData.append('project', JSON.stringify(project));
   }

이렇게 하면 코드가 더 간결해지고 null/undefined 체크가 한 번에 이루어집니다.

🧰 Tools
🪛 Biome (1.9.4)

[error] 89-90: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 94-95: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/features/multi-step-form/ui/careerForm/index.tsx (7)

31-64: 초기값 설정 시 주의사항
initialValues에 모든 필드를 빈 문자열로 두면, 파일 업로드(profileImage)처럼 기본적으로 null이거나 undefined가 되어야 하는 경우와 혼동될 수 있습니다. 파일 입력이 기본적으로 없을 경우 null이나 undefined 할당이 더 명시적일 수 있으니 고려해 보세요.

-  profileImage: '',
+  profileImage: undefined,

71-71: mode: 'onBlur' 검증 모드 확인
onBlur 모드는 필드가 포커스를 잃을 때 즉시 검증이 이루어지므로, UX 상 빠른 피드백이 필요한지 확인해 보세요. 상황에 따라 onSubmit 또는 onChange가 더 나은 경우도 있습니다.


81-81: API 훅 네이밍 일관성
useCreateCard가 반환하는 mutate: createCardAPI는 사용성에 문제는 없지만, mutateCreateCard처럼 직관적인 네이밍도 고려해 보세요.


94-94: 최종 제출 로직 연동
createCardAPI(createCareerFormData(data)) 호출 시 반환값 에러 처리가 별도로 필요하다면, onError 콜백 등을 활용해 UI 상 응답 처리하는 것도 고려하세요.


100-124: 배열 필드 검증 로직
validateArrayFields가 배열 내 link가 빈 값인지 검사하는 로직이 명확합니다. 다만 다른 속성(title, imageUrl 등)에 대한 검증도 필요하다면 별도 추가 검토가 필요합니다.


148-148: 조건부 렌더링
{currentStep !== 2 && (...)}는 2단계에서 버튼이 사라지는 의도라면 정확하지만, 사용자 입장에서 혼란이 없도록 UI/UX도 같이 고려해주세요.


150-154: 다음/제출 버튼 비활성화 조건
disabled={!isStepValid} 로직은 직관적입니다. 필요 시, 단계별 상세 안내 메시지를 표시해 유저가 무엇을 수정해야 하는지 알 수 있도록 개선을 고려해 주세요.

src/features/multi-step-form/components/EditableCardField.tsx (6)

13-44: 타입 정의 명확성
FieldData, FieldProps, EditableCardFieldProps 등 세분화된 타입이 가독성에 유리합니다. 다만, type?: string;과 같이 어떤 값이 들어올지 모호한 경우, enum 또는 구체적 리터럴 타입으로 엄격히 제한해 볼 수도 있습니다.


68-82: 컴포넌트 구조 분리
EditableCardField에서 enableEditing, disableEditing 함수 등을 구현해 UI 상태를 분리한 점이 좋습니다. 재사용성도 고려해 볼 수 있겠네요.


83-84: 에러 처리 시 await 주의
mutateAsync: scapAPI를 호출할 때 await로 감싸는 점은 좋으나, 에러 핸들링 부문에서 사용자 알림이나 추가 로깅이 필요할 수 있습니다.


92-99: 편집 모드 토글
enableEditing, disableEditing 함수를 통해 인덱스별 편집 상태를 관리하는 로직이 깔끔합니다. 다만, 편집 중에 다른 필드로 넘어가거나 삭제되는 경우의 예외가 있는지 확인해 주세요.


101-127: 메타데이터 스크래핑 로직
스크래핑에 성공하면 updateField로 모든 데이터를 갱신하는 흐름이 명확합니다. 실패 시 기본 플랫폼 제목으로 세팅하는 예외 처리도 적절합니다. 다만, 스크래핑이 반복해서 발생하지 않도록 방어 로직이 필요한지 검토가 필요합니다.


148-157: Enter 키 이벤트 처리
handleKeyDown에서 Enter 누르면 enableEditing이 되도록 처리했는데, 일반적으로 Enter 시 편집을 종료하는 패턴이므로 UX 디자인 의도가 맞는지 확인이 필요합니다.

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83c3770 and 2784968.

📒 Files selected for processing (9)
  • src/features/multi-step-form/components/Cardview.tsx (1 hunks)
  • src/features/multi-step-form/components/EditableCardField.tsx (1 hunks)
  • src/features/multi-step-form/config/index.ts (2 hunks)
  • src/features/multi-step-form/hooks/queries/useScrap.ts (1 hunks)
  • src/features/multi-step-form/schema/index.ts (1 hunks)
  • src/features/multi-step-form/ui/careerForm/firstStep.tsx (2 hunks)
  • src/features/multi-step-form/ui/careerForm/index.tsx (3 hunks)
  • src/features/multi-step-form/utils/index.tsx (1 hunks)
  • src/shared/ui/tagInput.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/features/multi-step-form/hooks/queries/useScrap.ts
  • src/features/multi-step-form/components/Cardview.tsx
🧰 Additional context used
🧬 Code Definitions (5)
src/features/multi-step-form/config/index.ts (1)
src/features/multi-step-form/schema/index.ts (1) (1)
  • CareerFormData (123:123)
src/features/multi-step-form/ui/careerForm/firstStep.tsx (3)
src/features/multi-step-form/schema/index.ts (1) (1)
  • CareerFormData (123:123)
src/shared/store/cardFormState.ts (1) (1)
  • useCardFormStore (17:56)
src/shared/ui/dropDown/searchDropdown.tsx (1) (1)
  • SearchOptions (13:18)
src/features/multi-step-form/ui/careerForm/index.tsx (3)
src/features/multi-step-form/schema/index.ts (2) (2)
  • CareerFormData (123:123)
  • cardCreateSchema (3:121)
src/features/multi-step-form/utils/index.tsx (1) (1)
  • createCareerFormData (52:102)
src/features/multi-step-form/config/index.ts (1) (1)
  • TOTAL_STEPS (20:20)
src/features/multi-step-form/utils/index.tsx (1)
src/features/multi-step-form/schema/index.ts (1) (1)
  • CareerFormData (123:123)
src/features/multi-step-form/components/EditableCardField.tsx (3)
src/features/multi-step-form/hooks/queries/useScrap.ts (1) (1)
  • useScrap (34:40)
src/features/multi-step-form/utils/index.tsx (1) (1)
  • getPlatformFromUrl (40:50)
src/features/multi-step-form/config/index.ts (1) (1)
  • MAXIMUM_TAG_ADD (22:22)
🪛 Biome (1.9.4)
src/features/multi-step-form/utils/index.tsx

[error] 89-90: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 94-95: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (32)
src/features/multi-step-form/config/index.ts (2)

22-22: 태그 추가 제한을 명확하게 상수로 정의했네요!

최대 태그 추가 개수를 상수로 정의하여 코드의 가독성과 유지보수성을 높였습니다. 이 값이 UI나 다른 로직에서 일관되게 사용될 수 있어 좋습니다.


24-32: 폼 필드와 태그 값 간의 매핑이 잘 정의되었습니다.

Record 타입과 Pick을 사용해 타입 안전성을 확보한 방식이 좋습니다. 각 폼 필드가 해당하는 태그 값에 명확하게 매핑되어 있어, 코드의 일관성과 유지보수성이 향상됩니다.

src/features/multi-step-form/ui/careerForm/firstStep.tsx (6)

5-8: 관련 훅과 스토어 추가가 잘 이루어졌습니다.

Job 상태 관리를 위한 useCardFormStore와 직군 정보를 가져오기 위한 useRegisterQuery의 추가는 적절합니다. 컴포넌트 간 상태 공유와 API 통합에 좋은 접근법입니다.


24-28: Zustand를 활용한 상태 관리가 잘 구현되었습니다.

Job 상태를 추출하여 API 쿼리에 활용하는 방식이 깔끔합니다. 이러한 패턴은 컴포넌트 간 상태 공유를 효과적으로 만들어줍니다.


41-55: 필드 이름 변경이 일관되게 적용되었습니다.

name에서 nickname으로의 변경이 모든 관련 속성에 일관되게 적용되었습니다. 이름 변경은 더 명확한 의미를 제공하여 코드 가독성을 향상시킵니다.


56-82: 직군 선택을 위한 SearchDropdown 구현이 효과적입니다.

detailJobId 필드에 SearchDropdown 컴포넌트를 사용한 구현이 뛰어납니다. 특히:

  1. 선택된 옵션의 ID 추출 로직이 명확합니다.
  2. 에러 처리가 잘 구현되어 있습니다.
  3. 현재 값에 대한 옵션 찾기 로직이 간결합니다.

이러한 접근 방식은 사용자 경험을 향상시키고, 타입 안전성을 보장합니다.


83-96: TagInput 컴포넌트 사용으로 입력 경험이 개선되었습니다.

WrappedTagInput에서 TagInput으로의 전환은 좋은 변화입니다. 태그 입력에 특화된 컴포넌트를 사용함으로써 사용자 경험이 향상되었습니다.


97-113: Textarea 사용으로 더 나은 요약 입력 경험을 제공합니다.

WrappedInput에서 Textarea로의 변경은 한 줄 소개를 입력하는데 더 적합합니다. 특히 40자 제한을 설정한 것이 좋습니다. 에러 처리도 잘 구현되어 있습니다.

src/shared/ui/tagInput.tsx (6)

12-18: 태그 입력을 위한 Props 타입이 명확하게 정의되었습니다.

TagInputProps 타입이 잘 설계되어 있습니다. 외부 제어를 위한 value와 onChange 속성이 포함되어 있어 컴포넌트의 유연성을 높입니다.


20-40: 컴포넌트 문서화가 매우 상세합니다.

JSDoc을 활용한 문서화가 잘 되어 있습니다. 사용 예시까지 포함하여 다른 개발자들이 쉽게 사용할 수 있도록 설명되어 있습니다.


50-72: 한국어 입력을 위한 조합 이벤트 처리가 잘 구현되었습니다.

Composition 이벤트(한글 입력 시 발생하는 이벤트)를 적절히 처리하여 한국어 입력 시 발생할 수 있는 문제를 방지했습니다. 이는 국제화 지원에 중요한 부분입니다.


78-90: 키보드 인터랙션 처리가 탁월합니다.

여러 키(스페이스, 쉼표, 탭, 백스페이스, 엔터)를 통한 태그 추가/제거 기능이 사용자 경험을 향상시킵니다. Composition 상태 확인을 통해 한글 입력 중에 의도하지 않은 동작을 방지하는 것도 좋은 접근법입니다.


113-118: 외부 클릭 처리가 효과적으로 구현되었습니다.

useOnClickOutside 훅을 사용하여 외부 클릭 시 입력값을 태그로 추가하는 기능이 사용자 경험을 향상시킵니다. Composition 상태도 함께 확인하여 한글 입력 중에 의도하지 않은 태그 추가를 방지합니다.


120-149: UI 구현이 접근성과 스타일링을 잘 고려했습니다.

  1. 라벨 연결이 적절합니다.
  2. 에러 메시지 표시 기능이 잘 구현되어 있습니다.
  3. 태그 표시와 입력 필드의 조합이 사용자 친화적입니다.
  4. Focus 관리를 위한 tabIndex 설정과 스타일링이 잘 되어 있습니다.

전반적으로 사용자 경험과 접근성을 모두 고려한 훌륭한 구현입니다.

src/features/multi-step-form/utils/index.tsx (3)

8-12: URL에서 도메인을 추출하는 함수가 효과적으로 구현되었습니다.

정규 표현식을 사용하여 URL에서 도메인을 추출하는 함수가 잘 구현되어 있습니다. 'www.' 접두사를 선택적으로 제거하고 도메인을 소문자로 변환하여 일관된 결과를 제공합니다.


15-33: 플랫폼 패턴 테이블이 체계적으로 구성되었습니다.

다양한 플랫폼을 카테고리별로 정리한 룩업 테이블이 잘 구성되어 있습니다. 이런 방식은 새로운 플랫폼을 쉽게 추가할 수 있어 확장성이 좋습니다.


40-50: URL에서 플랫폼을 판별하는 함수가 명확하게 구현되었습니다.

도메인을 추출하고 패턴을 확인하는 로직이 명확합니다. 플랫폼을 대문자로 반환하고, 매칭되지 않는 경우 'LINK'를 반환하는 것이 일관된 응답 형식을 제공합니다.

src/features/multi-step-form/ui/careerForm/index.tsx (7)

25-28: handleNextStep 타입 정의 확인 필요
handleNextStep가 비동기 로직(async/await)을 사용하는 부분이 있으므로, 필요 시 () => Promise<void> 형태로 업데이트하는 것이 명시적이고 안정적일 수 있습니다. 현 코드 흐름에는 큰 문제 없지만, 향후 확장성을 고려해 타입을 확인해 주세요.


74-79: React Hook Form 디스트럭처링
handleSubmit, trigger, watch, errors 사용이 명확합니다. 그러나 trigger 사용 시 성능 저하가 있을 수 있으므로, 검증 필드 범위를 명확히 지정해서 불필요한 검증 호출이 일어나지 않도록 관리해 주세요.


83-83: 전역 상태 사용 체크
useCardFormStore를 통해 가져오는 validationTagArray가 동적으로 변할 가능성이 있다면, 폼 단계에 따라 적절히 재검증이 이뤄지는지 확인이 필요합니다.


85-90: 단계별 필드 분리
stepValidationFields 구성이 명확합니다. 단, 2단계와 4단계에 필드가 비어 있는 로직이 맞다면, 추후 변경되었을 때 유지보수에 유의해야 합니다.


98-98: watch 사용 주의
watch(stepValidationFields[currentStep])는 특정 필드만 모니터링하므로, 다른 필드 변화가 필요한 경우 watch() 인자나 구조를 유연하게 바꿀 수 있는지 검토해 주세요.


127-127: handleNextStep 함수 비동기 처리
이미 trigger로 검증 후 로직을 분기하고 있으므로, 불필요한 추가 예외 처리는 없어도 됩니다. 다만 API 통신 후 에러가 발생하면 단계를 넘어가지 못하도록 보강할 수 있는지 확인해 주세요.


130-144: 스텝 이동 로직
onNextStep()과 마지막 단계에서 handleSubmit(onSubmit)()로 분기하는 구조가 명확합니다. 다만 TOTAL_STEPS 변경 시 이 로직도 함께 수정되어야 하므로, 유지보수 시 주의 바랍니다.

src/features/multi-step-form/schema/index.ts (1)

4-120: 더욱 세분화된 검증 로직 적용
cardCreateSchema에서 각 필드를 구체적으로 유효성 검증하는 것이 인상적입니다. 배열 필드(sns, content, project)에서 link나 기타 속성에 대한 에러 메시지가 충분히 안내되고 있으니 유지보수 시 혼동이 없을 것 같습니다. 다만, 일부 문자열 길이 제한(예: organization, news)과 profileImage가 URL일 때, 실제로 외부 링크 이미지가 유효하지 않아도 통과될 위험이 있을 수 있으므로, 필요하다면 API 쪽에서 추가 검증이 이뤄지도록 확인해주세요.

src/features/multi-step-form/components/EditableCardField.tsx (7)

1-2: React Hook Form 의존성 타입 확인
FieldArrayWithId, UseFieldArrayUpdate 등을 사용 시 버전에 따라 타입 명시가 달라질 수 있으니, 동일 버전 유지에 유념하세요.


7-10: MAXIMUM_TAG_ADD 상수 사용
최대 개수 제한을 상수로 잘 분리했습니다. 다양한 곳에서 이 제한이 사용된다면, 유지보수 편의성이 높아집니다.


46-66: JS Doc 주석 활용
주석으로 각 프로퍼티의 의미가 명확하게 설명되어 있습니다. 추후 유지보수 시 팀원들이 쉽게 이해할 수 있으므로, 매우 바람직한 문서화입니다.


88-90: 플랫폼 추출 로직
parseLinkToTitle = getPlatformFromUrl(value) 호출이 명확합니다. 도메인에서 플랫폼을 인식할 때 오탐 가능성이 있는지 고려해 주세요.


129-146: 블러 이벤트 시점 처리
handleBlur에서 scapingTargetArr를 확인 후 스크래핑을 수행하는 구조가 직관적입니다. 그러나 링크가 변경된 뒤 즉시 블러가 발생하지 않는 경우도 있을 수 있으니, 사용자가 편집을 완전히 마쳤을 때만 스크래핑이 수행되도록 추가 고려해주세요.


159-192: 조건부 렌더링(!error && value && !editingStates[index])
에러 없는 상태이면서 값이 존재하고, 편집 모드가 아닌 경우 EditableCardEditView로 전환하는 로직이 좋습니다. 다만, 에러가 있는데도 값이 있을 때 어떻게 표시되는지 UX 시나리오를 한 번 더 점검해 주시면 좋겠습니다.


194-239: 컴포넌트 분리로 가독성 향상
EditableCardInputView, EditableCardEditViewmemo로 감싼 뒤 각각 분리하여 관리하는 구조가 훌륭합니다. 재렌더링 비용도 절감할 수 있으니 좋은 시도입니다.

@jangwonyoon jangwonyoon merged commit 77e91fb into develop Mar 18, 2025
6 checks passed
@jangwonyoon jangwonyoon deleted the feature/#58-card-function branch March 18, 2025 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🚀 feature 새로운 기능 개발
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[feat] 카드 생성 폼 기능 구현
2 participants