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] 명함 상세 페이지 구현 #74

Merged
merged 42 commits into from
Mar 18, 2025
Merged

Conversation

JaeIn1
Copy link
Collaborator

@JaeIn1 JaeIn1 commented Mar 11, 2025

📌 개요

  • 명함 상세 페이지 구현
  • swagger api에 맞춰 코드 작성

📋 변경사항

  • 명함 상세 페이지 구현
  • 공통 컴포넌트 abbbar (명함 상세 페이지 요규사항에 맞게 hasBackground props 추가)

기능

  • 내 명함 상세 조회

화면

| 기능 | 스크린샷 |

2025-03-18.7.24.57.mov

전달사항

  • 현재 명함 상세 페이지의 헤더는 스크롤 시 blur 효과로 구분되어 있습니다.
  • blur 처리 외에 다른 방식으로 헤더를 구분할 수 있는 대안을 검토 중입니다.

✅ 체크사항

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

Summary by CodeRabbit

Summary by CodeRabbit

  • 새로운 기능

    • 카드 상세 페이지가 업데이트되어, 사용자 정보와 직업별 배경 및 아이콘이 반영된 동적 헤더와 탭 기반 콘텐츠 탐색 기능이 도입되었습니다.
    • 게시글 썸네일을 통한 상세 정보 확인과 소셜 아이콘 연동 기능으로 다양한 콘텐츠를 쉽게 이용할 수 있습니다.
    • 스크롤에 따른 내비게이션 영역의 자동 표시/숨김 인터랙션이 추가되었습니다.
    • 새로운 PostThumbnail 컴포넌트가 추가되어 썸네일을 클릭하면 링크를 새 탭에서 열 수 있습니다.
    • UnderlineTabs 컴포넌트가 추가되어 탭 탐색 기능이 향상되었습니다.
    • 새로운 CardDetailHeader, CardTabs, SNS 컴포넌트가 추가되어 카드 세부정보를 보다 효과적으로 표시합니다.
    • 새로운 useScrollPosition, useTabsActive, useCardDetailQuery 훅이 추가되어 사용자 경험을 개선합니다.
  • 스타일 개선

    • 레이아웃 간격 및 여백 옵션이 확장되어 보다 깔끔한 UI를 제공합니다.
    • 텍스트 오버플로우 제어를 위한 라인 클램프 기능이 도입되었습니다.

JaeIn1 added 29 commits March 9, 2025 04:20
@JaeIn1 JaeIn1 added the 🚀 feature 새로운 기능 개발 label Mar 11, 2025
@JaeIn1 JaeIn1 requested a review from evenway2025 as a code owner March 11, 2025 10:13
@JaeIn1 JaeIn1 linked an issue Mar 11, 2025 that may be closed by this pull request
2 tasks
Copy link

coderabbitai bot commented Mar 11, 2025

📝 Walkthrough

Walkthrough

이번 PR은 package.json 구성 업데이트와 함께 card-detail 관련 UI 컴포넌트, 커스텀 훅, 타입, 설정 파일을 추가 및 수정하였습니다. 신규 의존성 추가, 데이터 구조 단순화, 이미지 소스 변경, 레이아웃 및 스타일링 개선 등이 포함되어 있습니다. 또한 카드 상세 페이지 렌더링, 스크롤 및 탭 상호작용, API 데이터 페칭(현재 모킹) 등의 기능 흐름이 구현되었습니다.

Changes

File(s) Change Summary
package.json 신규 의존성(react-intersection-observer: ^9.16.0) 추가
src/features/card-detail/ui/cardDetailHeader.tsx, src/features/card-detail/ui/cardTabs.tsx, src/features/card-detail/ui/underlineTabs.tsx, src/features/card-detail/ui/sns.tsx, src/shared/ui/postThumbnail.tsx, src/app/(detail)/card-detail/[cardId]/page.tsx 신규 컴포넌트 추가: CardDetailHeader, CardTabs, UnderlineTabs, SNS, PostThumbnail
src/features/card-detail/ui/domain.tsx, src/features/card-detail/ui/hobby.tsx, src/features/card-detail/ui/posts.tsx, src/features/card-detail/ui/projects.tsx, src/features/card-detail/ui/recent.tsx 인터페이스 및 prop 타입 단순화 (객체 → 문자열 등) 및 렌더링 로직 수정
src/features/card-detail/config/tabs-config.ts, src/features/card-detail/config/jobs-config.ts, src/features/card-detail/config/sns-config.ts 탭, 직무, SNS 관련 타입, 인터페이스 및 상수(CARD_TABS, JOB_CONFIG, SNS_CONFIG) 정의 파일 추가
src/features/card-detail/hooks/query/useCardDetailQuery.ts, src/features/card-detail/hooks/useScrollPosition.ts, src/features/card-detail/hooks/useTabsActive.ts 카드 상세 정보 페칭, 요소 인터섹션 관찰, 탭 활성화 동기화를 위한 커스텀 훅 추가
src/shared/spacing/spacing.ts, tailwind.config.ts 신규 spacing variant 추가 및 Tailwind CSS 설정 업데이트
src/features/card-detail/types/cardDetail.ts 카드 상세 정보 관련 제네릭, DTO 타입 추가
src/features/card-detail/mocks/sample.ts 모킹 데이터 내 contentprojectimageUrl 필드를 '/icons/imageIcon.svg'로 수정
src/shared/ui/appbar.tsx Appbar 컴포넌트에 hasBackground 옵션 추가

Sequence Diagram(s)

sequenceDiagram
  participant U as 사용자
  participant P as Page 컴포넌트
  participant H as CardDetailHeader
  participant Q as useCardDetailQuery
  participant API as API (모킹 데이터)

  U->>P: 카드 상세 페이지 진입
  P->>H: 헤더 컴포넌트 렌더링
  H->>Q: 카드 정보 요청 (cardId 전달)
  Q->>API: 카드 상세 데이터 요청
  API-->>Q: 모킹 데이터 응답
  Q-->>H: 데이터 전달
  H->>U: 동적 헤더 렌더링 (JOB_CONFIG 적용)
Loading
sequenceDiagram
  participant U as 사용자
  participant T as CardTabs 컴포넌트
  participant S as useScrollPosition
  participant A as useTabsActive

  U->>T: 탭 클릭 또는 스크롤
  T->>S: 섹션 보임 상태 관찰 요청
  S-->>T: 인터섹션 상태 업데이트
  T->>A: 현재 뷰 상태 동기화 요청
  A-->>T: 활성 탭 상태 설정
  T->>U: 업데이트된 탭 및 콘텐츠 렌더링
Loading

Suggested labels

auto approve

Suggested reviewers

  • youngju6143
  • semnil5202
  • jangwonyoon
  • 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.

📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8528e09 and d09bbde.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • package.json (1 hunks)
  • tailwind.config.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tailwind.config.ts
🔇 Additional comments (1)
package.json (1)

33-33: 의존성 추가: react-intersection-observer 버전 "^9.16.0" 확인 필요

새롭게 추가된 의존성은 상세 명함 페이지에서 스크롤 위치 감지 및 뷰포트 처리를 위해 필요해 보입니다. 다만, 해당 버전이 프로젝트의 다른 라이브러리(예: Next.js, React)와 호환되는지 공식 문서나 릴리즈 노트를 통해 한번 더 확인해 주시면 좋겠습니다.


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

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (22)
src/features/home/components/BusinessCard/Background/AddCradBackground.tsx (1)

1-3: ⚠️ Potential issue

파일 이름에 오타가 있습니다.

현재 파일 이름이 AddCradBackground.tsx로 되어 있는데, 이는 AddCardBackground.tsx의 오타로 보입니다.

파일 이름을 AddCardBackground.tsx로 수정하는 것이 좋겠습니다.

src/features/home/providers/CardSwiperProvider.tsx (1)

1-4: 🛠️ Refactor suggestion

Swiper CSS가 import 되지 않았습니다.

Swiper 컴포넌트를 사용할 때는 필요한 CSS 파일도 함께 import 해야 합니다. 그렇지 않으면 Swiper가 제대로 스타일링되지 않을 수 있습니다.

 'use client';

 import { PropsWithChildren } from 'react';
 import { Swiper } from 'swiper/react';
+import 'swiper/css';
+import 'swiper/css/pagination';
📝 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.

'use client';

import { PropsWithChildren } from 'react';
import { Swiper } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/pagination';
src/features/home/components/BusinessCard/AddCard.tsx (1)

5-12: 🛠️ Refactor suggestion

컴포넌트에 클릭 핸들러가 없습니다.

AddCard 컴포넌트는 사용자가 클릭하여 새 카드를 추가할 수 있게 하는 목적으로 보이지만, 클릭 이벤트 핸들러가 없습니다. 클릭 시 적절한 동작을 수행하는 핸들러를 추가하고, 접근성을 위해 적절한 속성(role, aria-label 등)을 포함하는 것이 좋습니다.

- export const AddCard = () => {
+ export const AddCard = ({ onClick }: { onClick?: () => void }) => {
  return (
    <div 
+     onClick={onClick}
+     role="button"
+     tabIndex={0}
+     aria-label="새 명함 추가하기"
+     onKeyDown={(e) => e.key === 'Enter' && onClick?.()}
      className="bg-[rgba(255,255,255, 0.2)] relative flex h-[394px] w-[270px] flex-col items-center justify-center gap-2 overflow-hidden rounded-2xl">
      <Typography variant="body-1">추가하기</Typography>
      <CardPlusIcon />
    </div>
  );
};
📝 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 AddCard = ({ onClick }: { onClick?: () => void }) => {
  return (
    <div 
      onClick={onClick}
      role="button"
      tabIndex={0}
      aria-label="새 명함 추가하기"
      onKeyDown={(e) => e.key === 'Enter' && onClick?.()}
      className="bg-[rgba(255,255,255, 0.2)] relative flex h-[394px] w-[270px] flex-col items-center justify-center gap-2 overflow-hidden rounded-2xl">
      <Typography variant="body-1">추가하기</Typography>
      <CardPlusIcon />
    </div>
  );
};
src/app/layout.tsx (2)

14-19: 🛠️ Refactor suggestion

사용자 확대 제한에 대한 접근성 문제

뷰포트 설정에서 userScalable: falsemaximumScale: 1은 사용자가 페이지를 확대하는 기능을 제한합니다. 이는 시각 장애가 있는 사용자에게 접근성 문제를 일으킬 수 있습니다.

접근성 지침(WCAG)에 따르면 사용자의 확대/축소 기능을 제한하는 것은 피해야 합니다. 다음과 같이 변경하세요:

 export const viewport: Viewport = {
   width: 'device-width',
   initialScale: 1,
-  maximumScale: 1,
-  userScalable: false,
+  maximumScale: 5,
+  userScalable: true,
 };

이렇게 하면 사용자가 필요에 따라 페이지를 확대할 수 있어 접근성이 향상됩니다.

📝 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 viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  maximumScale: 5,
  userScalable: true,
};

11-12: ⚠️ Potential issue

메타데이터에 뷰포트 설정이 추가되었습니다.

메타데이터 객체에 뷰포트 설정이 추가되었습니다. 이는 나중에 추가된 별도의 뷰포트 설정과 중복될 수 있습니다.

메타데이터 내의 뷰포트 설정과 14-19줄에 있는 별도의 뷰포트 설정이 중복됩니다. Next.js 13 이상에서는 별도의 viewport 객체를 사용하는 것이 권장됩니다. 메타데이터에서 뷰포트 설정을 제거하세요:

 export const metadata: Metadata = {
   title: 'Took',
   description: 'Took name service',
-  viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
 };
📝 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 metadata: Metadata = {
   title: 'Took',
   description: 'Took name service',
 };
src/features/card-detail/hooks/useReverseScrollView.ts (1)

1-20: 🛠️ Refactor suggestion

메모리 누수와 성능 최적화 문제가 있습니다.

현재 구현은 몇 가지 중요한 문제점이 있습니다:

  1. 메모리 누수: useEffect 내에서 이벤트 리스너를 추가하지만, 컴포넌트가 언마운트될 때 이를 제거하는 정리(cleanup) 함수가 없습니다.
  2. 성능 최적화: 이벤트 핸들러 함수가 매 렌더링마다 재생성됩니다.
  3. 이벤트 처리: wheel 이벤트는 스크롤 중에 매우 자주 발생하여 빈번한 리렌더링을 유발할 수 있습니다.

아래와 같이 수정하는 것을 제안합니다:

-import { useEffect, useState } from 'react';
+import { useEffect, useState, useCallback } from 'react';

function useReverseScrollView() {
  const [isReverseScroll, setIsReverseScroll] = useState(false);

+  const handleWheel = useCallback((event: WheelEvent) => {
+    if (event.deltaY < 0) {
+      setIsReverseScroll(true);
+    } else if (event.deltaY > 0) {
+      setIsReverseScroll(false);
+      // 탭바 숨기기 등 처리
+    }
+  }, []);

  useEffect(() => {
-    window.addEventListener('wheel', (event) => {
-      if (event.deltaY < 0) {
-        setIsReverseScroll(true);
-      } else if (event.deltaY > 0) {
-        setIsReverseScroll(false);
-        // 탭바 숨기기 등 처리
-      }
-    });
+    window.addEventListener('wheel', handleWheel);
+    
+    return () => {
+      window.removeEventListener('wheel', handleWheel);
+    };
-  }, []);
+  }, [handleWheel]);

  return { isReverseScroll };
}

export default useReverseScrollView;

또한 성능 최적화를 위해 throttle이나 debounce 기법 적용을 고려해보세요:

import { useEffect, useState, useCallback } from 'react';
import { throttle } from 'lodash'; // lodash 설치 필요

function useReverseScrollView() {
  const [isReverseScroll, setIsReverseScroll] = useState(false);

  const handleWheel = useCallback(
    throttle((event: WheelEvent) => {
      if (event.deltaY < 0) {
        setIsReverseScroll(true);
      } else if (event.deltaY > 0) {
        setIsReverseScroll(false);
        // 탭바 숨기기 등 처리
      }
    }, 200),
    []
  );

  useEffect(() => {
    window.addEventListener('wheel', handleWheel);
    
    return () => {
      window.removeEventListener('wheel', handleWheel);
      handleWheel.cancel(); // throttle된 함수의 pending 호출 취소
    };
  }, [handleWheel]);

  return { isReverseScroll };
}

export default useReverseScrollView;
src/shared/ui/appbar.tsx (1)

69-69: ⚠️ Potential issue

Appbar 배경 스타일 로직 수정 필요

배경 스타일을 설정하는 조건부 코드에 문제가 있습니다. 아래 70-80번 라인에 더 상세한 조건부 코드가 있는데, 이 부분이 중복되거나 불완전하게 표시된 것 같습니다.

코드 중복 또는 불완전한 코드가 있는지 확인하고, 아래와 같이 배경 스타일 로직을 수정하는 것이 좋을 것 같습니다:

-      className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${
-        page === 'detail' ? 'bg-opacity-90' : page === 'create' ? 'bg-gray-black' : ''
-      }`}
+      className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${
+        page === 'detail' && hasBackground
+          ? 'bg-gray-black bg-opacity-90'
+          : page === 'detail'
+            ? 'backdrop-blur-sm'
+            : page === 'create'
+              ? 'bg-gray-black'
+              : ''
+      }`}
📝 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.

function Appbar({ page, hasBackground, onLeftClick, onRightClick }: appbarPropsType) {
  return (
    <div
      className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${
        page === 'detail' && hasBackground
          ? 'bg-gray-black bg-opacity-90'
          : page === 'detail'
            ? 'backdrop-blur-sm'
            : page === 'create'
              ? 'bg-gray-black'
              : ''
      }`}
    >
      {/* ...rest of your component... */}
    </div>
  );
}
src/features/card-detail/ui/sns.tsx (1)

26-33: 🛠️ Refactor suggestion

SNS 링크를 클릭 가능하게 만드세요.

현재 SNS 아이콘을 표시하고 있지만, 실제 sns.link로 이동할 수 있는 기능이 없습니다. 사용자가 아이콘을 클릭하여 해당 SNS 페이지로 이동할 수 있도록 하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안합니다:

 <div className="flex w-[56px] flex-col items-center" key={i}>
-  <div
-    className={`${spacingStyles({ padding: 'md' })} flex items-center justify-center rounded-full bg-opacity-purple-30`}
-  >
-    <Image src={snsConfig.iconPath} alt={snsConfig.iconAlt} width={24} height={24} />
-  </div>
+  <a 
+    href={sns.link} 
+    target="_blank" 
+    rel="noopener noreferrer" 
+    className="block"
+  >
+    <div
+      className={`${spacingStyles({ padding: 'md' })} flex items-center justify-center rounded-full bg-opacity-purple-30 cursor-pointer`}
+    >
+      <Image src={snsConfig.iconPath} alt={snsConfig.iconAlt} width={24} height={24} />
+    </div>
+  </a>
   <p className="mt-[7px] w-full truncate text-center text-caption-1">{sns.type}</p>
 </div>
📝 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 w-[56px] flex-col items-center" key={i}>
            <a 
              href={sns.link} 
              target="_blank" 
              rel="noopener noreferrer" 
              className="block"
            >
              <div
                className={`${spacingStyles({ padding: 'md' })} flex items-center justify-center rounded-full bg-opacity-purple-30 cursor-pointer`}
              >
                <Image src={snsConfig.iconPath} alt={snsConfig.iconAlt} width={24} height={24} />
              </div>
            </a>
            <p className="mt-[7px] w-full truncate text-center text-caption-1">{sns.type}</p>
          </div>
tailwind.config.ts (1)

141-141: 💡 Verification agent

🧩 Analysis chain

@tailwindcss/line-clamp 플러그인 사용을 재검토하세요.

Tailwind CSS v3.3부터 line-clamp 기능은 이미 Tailwind CSS 코어에 "line-clamp-{n}" 유틸리티로 포함되어 있습니다. 별도의 플러그인은 더 이상 필요하지 않으며 사용을 권장하지 않습니다.

프로젝트에서 사용 중인 Tailwind CSS 버전을 확인하고, 최신 버전을 사용 중이라면 이 플러그인을 제거하는 것이 좋습니다.


🌐 Web query:

Tailwind CSS line-clamp plugin deprecated?

💡 Result:

The @tailwindcss/line-clamp plugin is deprecated as of Tailwind CSS v3.3, as line-clamp utilities are now included natively in Tailwind CSS core[4][8]. Here's what you need to know:

Migration Steps

  1. Remove the plugin from your tailwind.config.js:
    // Remove this line:
    plugins: [require('@tailwindcss/line-clamp')]
  2. Update Tailwind CSS to v3.3 or later:
    npm install tailwindcss@latest

Key Changes

  • Existing line-clamp-{n} classes work identically without the plugin[4]
  • No configuration needed - line-clamp utilities are available by default[4]
  • Supported line counts remain 1-6 by default, with configuration options for additional lines[1]

Important Notes:

  • The implementation still uses -webkit-line-clamp under the hood for cross-browser support[6]
  • When removing line-clamp, you may need to explicitly reset related properties like display and overflow[1]
  • The npm package (@tailwindcss/line-clamp) hasn't been updated since March 2023[1], confirming its deprecated status

For new projects, simply use line-clamp-{n} classes directly without installing any additional packages[4]. Existing projects should follow the migration steps to ensure compatibility with future Tailwind CSS updates.

Citations:


핵심: Tailwind CSS v3.3 이상에서는 별도의 line-clamp 플러그인을 제거하세요.

Tailwind CSS v3.3부터는 line-clamp-{n} 유틸리티가 코어에 내장되어 있어 별도로 @tailwindcss/line-clamp 플러그인을 사용할 필요가 없습니다. 현재 tailwind.config.ts 파일의 141번째 줄에서 두 플러그인이 함께 사용되고 있는 것을 확인했습니다.

  • 프로젝트에서 Tailwind CSS v3.3 이상을 사용 중이라면, 아래와 같이 @tailwindcss/line-clamp 플러그인을 제거해 주세요.
- plugins: [require('tailwindcss-animate'), require('@tailwindcss/line-clamp')],
+ plugins: [require('tailwindcss-animate')],

Tailwind CSS의 최신 버전으로 업그레이드하는 것도 함께 검토해 주시기 바랍니다.

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

  plugins: [require('tailwindcss-animate')],
src/features/home/containers/CardContainer.tsx (1)

19-19: 🛠️ Refactor suggestion

데이터 로딩 및 오류 상태를 처리하세요.

useCardQuery 훅을 사용하여 데이터를 가져오고 있지만, 로딩 상태나 오류 상태에 대한 처리가 없습니다. 이로 인해 사용자 경험이 저하될 수 있습니다.

다음과 같이 로딩 및 오류 상태를 처리하는 것을 제안합니다:

-const { data } = useCardQuery();
+const { data, isLoading, error } = useCardQuery();
+
+if (isLoading) {
+  return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는 중...</div>;
+}
+
+if (error) {
+  return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는데 실패했습니다.</div>;
+}
+
+if (!data || data.length === 0) {
+  return (
+    <Swiper pagination modules={[Pagination]} className="h-[440px]">
+      <SwiperSlide>
+        <AddCard />
+      </SwiperSlide>
+    </Swiper>
+  );
+}
📝 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.

  const { data, isLoading, error } = useCardQuery();

  if (isLoading) {
    return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는 중...</div>;
  }

  if (error) {
    return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는데 실패했습니다.</div>;
  }

  if (!data || data.length === 0) {
    return (
      <Swiper pagination modules={[Pagination]} className="h-[440px]">
        <SwiperSlide>
          <AddCard />
        </SwiperSlide>
      </Swiper>
    );
  }
src/features/multi-step-form/ui/careerForm/secondStep.tsx (1)

40-42: ⚠️ Potential issue

다음 버튼에 onClick 핸들러가 누락되었습니다.

다음 버튼에 onClick 이벤트 핸들러가 없어 실제로 다음 단계로 진행하는 기능이 구현되어 있지 않습니다. 멀티 스텝 폼에서는 단계 간 이동 기능이 필수적입니다.

-<Button className="w-full" disabled={tagCount === 0}>
+<Button className="w-full" disabled={tagCount === 0} onClick={() => {
+  // 다음 단계로 진행하는 로직 구현
+}}>
  다음
</Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        <Button className="w-full" disabled={tagCount === 0} onClick={() => {
          // 다음 단계로 진행하는 로직 구현
        }}>
          다음
        </Button>
src/features/card-detail/hooks/query/useCardDetailQuery.ts (2)

11-16: 🛠️ Refactor suggestion

API 호출 시 오류 처리가 없습니다.

getCardDetail 함수에서 API 호출 실패 시 오류 처리가 구현되어 있지 않습니다. API 호출이 실패할 경우 적절한 오류 처리가 필요합니다.

const getCardDetail = async (id: string): Promise<CardDetailDto> => {
-  const data = await client.get<CardDetailDto>(`${CLIENT_SIDE_URL}/api/card/detail`, {
-    params: { id },
-  });
-  return data;
+  try {
+    const data = await client.get<CardDetailDto>(`${CLIENT_SIDE_URL}/api/card/detail`, {
+      params: { id },
+    });
+    return data;
+  } catch (error) {
+    console.error('카드 상세 정보를 가져오는 중 오류가 발생했습니다:', error);
+    throw error;
+  }
};
📝 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.

const getCardDetail = async (id: string): Promise<CardDetailDto> => {
  try {
    const data = await client.get<CardDetailDto>(`${CLIENT_SIDE_URL}/api/card/detail`, {
      params: { id },
    });
    return data;
  } catch (error) {
    console.error('카드 상세 정보를 가져오는 중 오류가 발생했습니다:', error);
    throw error;
  }
};

20-26: ⚠️ Potential issue

실제 API 응답 대신 항상 목업 데이터를 반환하고 있습니다.

현재 코드는 useQuery를 사용하여 API 요청을 수행하지만, 실제 응답 데이터를 무시하고(data: _) 항상 목업 데이터를 반환합니다. 이는 개발 단계에서는 유용할 수 있지만, 프로덕션 환경에서는 적합하지 않습니다. 실제 API 응답을 사용하도록 변경하거나, 환경 변수를 통해 조건부로 목업 데이터를 사용하도록 개선해야 합니다.

export const useCardDetailQuery = (id: string) => {
-  const { data: _ } = useQuery({
+  const { data, isLoading, error } = useQuery({
    queryKey: ['cardDetail', id],
    queryFn: () => getCardDetail(id),
  });

-  return { data: mockCardDetailData };
+  // 개발 환경에서만 목업 데이터 사용
+  if (process.env.NODE_ENV === 'development' && !data) {
+    return { data: mockCardDetailData, isLoading, error };
+  }
+  
+  return { data, isLoading, error };
};
📝 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 useCardDetailQuery = (id: string) => {
  const { data, isLoading, error } = useQuery({
    queryKey: ['cardDetail', id],
    queryFn: () => getCardDetail(id),
  });

  // 개발 환경에서만 목업 데이터 사용
  if (process.env.NODE_ENV === 'development' && !data) {
    return { data: mockCardDetailData, isLoading, error };
  }
  
  return { data, isLoading, error };
};
src/features/card-detail/hooks/sample.ts (1)

19-24: ⚠️ Potential issue

SNS 링크 배열의 중복된 ID를 수정해주세요.

snsLinks 배열에 중복된 ID('2')가 여러 번 사용되고 있습니다. 이는 React에서 리스트 렌더링 시 key 속성에 사용될 경우 문제를 일으킬 수 있습니다. 각 항목에 고유한 ID를 할당해주세요.

  snsLinks: [
    { id: '1', type: 'GitHub', url: 'https://github.com/jaeiny' },
    { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
-   { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
-   { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
-   { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
+   { id: '3', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
+   { id: '4', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
+   { id: '5', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
  ],
📝 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.

  snsLinks: [
    { id: '1', type: 'GitHub', url: 'https://github.com/jaeiny' },
    { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
    { id: '3', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
    { id: '4', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
    { id: '5', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' },
  ],
src/features/home/hooks/queries/useCardQuery.ts (2)

9-15: 🛠️ Refactor suggestion

_getCard 함수 타입 안전성 개선이 필요합니다.

any 타입 대신 구체적인 타입을 사용하는 것이 좋습니다. API 응답과 파라미터에 인터페이스를 정의하세요.

- const _getCard = async (params: any) => {
-   const data = await client.get<any>('/somewhere/client-side', {
+ interface CardParams {
+   userId?: string;
+   // 기타 필요한 파라미터들
+ }
+
+ interface CardResponse {
+   // API 응답에 맞는 타입 정의
+   // 예: id: string, name: string 등
+ }
+
+ const _getCard = async (params?: CardParams) => {
+   const data = await client.get<CardResponse>('/somewhere/client-side', {
      params,
    });

    return data;
  };
📝 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.

interface CardParams {
  userId?: string;
  // 기타 필요한 파라미터들
}

interface CardResponse {
  // API 응답에 맞는 타입 정의
  // 예: id: string, name: string 등
}

const _getCard = async (params?: CardParams) => {
  const data = await client.get<CardResponse>('/somewhere/client-side', {
    params,
  });

  return data;
};

17-24: ⚠️ Potential issue

useCardQuery에서 모의 데이터 사용에 대한 명확한 표시가 필요합니다.

API 호출 결과를 사용하지 않고 모의 데이터를 반환하는 것이 의도적인지 명확히 해야 합니다. 변수명 '_'도 목적이 불분명합니다.

export const useCardQuery = (params?: any) => {
-  const { data: _ } = useQuery({
+  // TODO: API 연동 완료 후 모의 데이터 대신 실제 데이터 사용하도록 변경
+  const { data: apiData } = useQuery({
    queryKey: [CARD_QUERY_KEY, params],
    queryFn: () => _getCard(params),
+    enabled: false, // API 연동 전까지 쿼리 비활성화
  });

  return { data: CARD_MOCK };
};
📝 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 useCardQuery = (params?: any) => {
  // TODO: API 연동 완료 후 모의 데이터 대신 실제 데이터 사용하도록 변경
  const { data: apiData } = useQuery({
    queryKey: [CARD_QUERY_KEY, params],
    queryFn: () => _getCard(params),
    enabled: false, // API 연동 전까지 쿼리 비활성화
  });

  return { data: CARD_MOCK };
};
src/features/card-detail/ui/projects.tsx (2)

13-13: ⚠️ Potential issue

디버깅용 console.log를 제거하세요.

프로덕션 코드에 console.log가 남아있으면 성능에 영향을 줄 수 있고 불필요한 정보가 노출될 수 있습니다.

- console.log('ProjectsProps  :' + data[0].imageUrl);
📝 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.

// (The console.log has been removed)

16-17: 🛠️ Refactor suggestion

React 키 값으로 인덱스 사용을 피하세요.

배열 인덱스를 키로 사용하면 항목 순서가 변경될 때 성능 문제가 발생할 수 있습니다. 가능하면 고유 ID를 사용하세요.

- {data.map((project, i) => (
-   <div key={i} className="flex flex-col overflow-hidden rounded-md">
+ {data.map((project) => (
+   <div key={project.id} className="flex flex-col overflow-hidden rounded-md">
📝 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.

      {data.map((project) => (
        <div key={project.id} className="flex flex-col overflow-hidden rounded-md">
src/shared/ui/Input/tagInput.tsx (1)

38-53: 🛠️ Refactor suggestion

태그 위치 설정을 개선해주세요.

현재 모든 태그가 동일한 위치(absolute left-3 top-1)에 렌더링되어 겹치게 됩니다. 태그들이 시각적으로 구분되도록 위치 지정 방식을 변경하는 것이 좋습니다.

        <div className="relative flex flex-wrap gap-2 mt-2">
          {tags.map((tag, index) => (
            <div
              key={index}
-             className="flex items-center bg-gray-200 rounded-lg px-2 py-1 absolute left-3 top-1"
+             className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1"
            >
              <span className="text-body-5 text-gray-700">{tag}</span>
              <span
                className="ml-2 cursor-pointer text-red-500"
                onClick={() => handleRemoveTag(tag)}
              >
                x
              </span>
            </div>
          ))}
        </div>
📝 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="relative flex flex-wrap gap-2 mt-2">
          {tags.map((tag, index) => (
            <div
              key={index}
              className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1"
            >
              <span className="text-body-5 text-gray-700">{tag}</span>
              <span
                className="ml-2 cursor-pointer text-red-500"
                onClick={() => handleRemoveTag(tag)}
              >
                x
              </span>
            </div>
          ))}
        </div>
src/features/home/hooks/prefetches/useCardPrefetch.ts (1)

8-10: 🛠️ Refactor suggestion

타입 안전성을 개선하고 실제 API 엔드포인트를 사용해주세요.

현재 함수가 'any' 타입의 매개변수를 사용하고 있고, API 엔드포인트가 '/somewhere/server-side'로 지정되어 있습니다. 구체적인 타입을 정의하고 실제 API 엔드포인트를 지정해주세요.

-const _getCardPrefetch = async (params: any, cookie: ReadonlyRequestCookies) => {
-  const data = await server.get<any>('/somewhere/server-side', {
+// 구체적인 타입 정의 추가
+type CardParams = {
+  id?: string;
+  // 필요한 다른 매개변수들 추가
+};
+
+type CardResponse = {
+  // API 응답 타입 정의
+  id: string;
+  // 필요한 다른 필드들 추가
+};
+
+const _getCardPrefetch = async (params: CardParams, cookie: ReadonlyRequestCookies) => {
+  const data = await server.get<CardResponse>('/api/cards', { // 실제 API 엔드포인트 사용
📝 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.

// 구체적인 타입 정의 추가
type CardParams = {
  id?: string;
  // 필요한 다른 매개변수들 추가
};

type CardResponse = {
  // API 응답 타입 정의
  id: string;
  // 필요한 다른 필드들 추가
};

const _getCardPrefetch = async (params: CardParams, cookie: ReadonlyRequestCookies) => {
  const data = await server.get<CardResponse>('/api/cards', { // 실제 API 엔드포인트 사용
    params,
  });
  // ... (rest of the function implementation)
};
src/features/card-detail/ui/cardDetailHeader.tsx (2)

12-16: 🛠️ Refactor suggestion

데이터 로딩 및 오류 처리 필요

현재 코드는 데이터 로딩 상태나 오류 상태를 처리하지 않고 있습니다. 이로 인해 데이터가 없는 경우 런타임 오류가 발생할 수 있습니다.

useCardDetailQuery에서 isLoading, error 상태도 함께 가져와서 처리하는 것을 권장합니다:

- const { data } = useCardDetailQuery(id as string);
+ const { data, isLoading, error } = useCardDetailQuery(id as string);
+
+ if (isLoading) {
+   return <div className="w-full h-[300px] flex items-center justify-center">로딩 중...</div>;
+ }
+
+ if (error || !data) {
+   return <div className="w-full h-[300px] flex items-center justify-center">데이터를 불러오는 데 실패했습니다.</div>;
+ }
📝 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.

const CardDetailHeader = () => {
  const { id } = useParams();
  const { data, isLoading, error } = useCardDetailQuery(id as string);

  if (isLoading) {
    return <div className="w-full h-[300px] flex items-center justify-center">로딩 중...</div>;
  }

  if (error || !data) {
    return <div className="w-full h-[300px] flex items-center justify-center">데이터를 불러오는 데 실패했습니다.</div>;
  }

  const userJob = (data?.data?.job as JobType) || 'DEVELOPER';
  
  // ... rest of the component code
}

44-60: ⚠️ Potential issue

옵셔널 체이닝 사용 불일치

코드 전체에서 옵셔널 체이닝(?.)의 사용이 일관되지 않습니다. 일부는 data?.data로 사용하고 일부는 data.data로 사용하여 잠재적인 null 참조 오류가 발생할 수 있습니다.

다음과 같이 모든 데이터 접근에서 일관된 옵셔널 체이닝을 사용하세요:

- <p className="title-1 line-clamp-1">{data.data.nickname}</p>
+ <p className="title-1 line-clamp-1">{data?.data?.nickname}</p>
  <div
    className={`${spacingStyles({ marginBottom: 'ms' })} line-clamp-1 flex w-full items-center text-title-3`}
  >
    <span className={`max-w-1/2 truncate ${spacingStyles({ marginRight: 'sm' })}`}>
-     {data?.data.detailJob}
+     {data?.data?.detailJob}
    </span>
    {data?.data && (
      <>
        <span className="flex-shrink-0 px-1">|</span>
        <span className={`max-w-1/2 truncate ${spacingStyles({ marginLeft: 'sm' })}`}>
-         {data?.data.organization}
+         {data?.data?.organization}
        </span>
      </>
    )}
  </div>
- <p className="body-5 line-clamp-2">{data?.data.summary}</p>
+ <p className="body-5 line-clamp-2">{data?.data?.summary}</p>
📝 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.

            <p className="title-1 line-clamp-1">{data?.data?.nickname}</p>
            <div
              className={`${spacingStyles({ marginBottom: 'ms' })} line-clamp-1 flex w-full items-center text-title-3`}
            >
              <span className={`max-w-1/2 truncate ${spacingStyles({ marginRight: 'sm' })}`}>
                {data?.data?.detailJob}
              </span>
              {data?.data && (
                <>
                  <span className="flex-shrink-0 px-1">|</span>
                  <span className={`max-w-1/2 truncate ${spacingStyles({ marginLeft: 'sm' })}`}>
                    {data?.data?.organization}
                  </span>
                </>
              )}
            </div>
            <p className="body-5 line-clamp-2">{data?.data?.summary}</p>

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (27)
src/features/home/types/index.ts (1)

1-1: 💡 Verification agent

🧩 Analysis chain

타입 이름에 오타가 있습니다.

JopType이라는 타입 이름에 오타가 있습니다. 올바른 단어는 JobType입니다.

-export type JopType = 'designer' | 'developer';
+export type JobType = 'designer' | 'developer';

이 타입을 사용하는 모든 파일에서도 수정이 필요할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# JopType 타입을 사용하는 모든 파일 찾기
rg "JopType" --type ts

Length of output: 798


타입 이름 오타 수정 필요 – JopType에서 JobType으로 모든 변경 적용

src/features/home/types/index.ts 파일 내 타입 이름에 오타가 있습니다. 아래와 같이 수정해야 합니다:

-export type JopType = 'designer' | 'developer';
+export type JobType = 'designer' | 'developer';

추가로, 이 타입을 사용하는 다른 파일들에서도 아래와 같이 JopTypeJobType으로 변경해야 합니다:

  • src/features/home/mocks/index.ts
    • import 구문과 타입 지정 부분에서 변경 필요
  • src/features/home/components/BusinessCard/CardTag.tsx
    • import 및 사용 부분 변경 필요
  • src/features/home/components/BusinessCard/Card.tsx
    • import 및 타입 사용 부분 변경 필요

해당 변경 사항을 전역적으로 적용하여 타입 불일치 문제를 해결해 주세요.

📝 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 type JobType = 'designer' | 'developer';
src/features/home/components/BusinessCard/Background/AddCradBackground.tsx (1)

1-3: ⚠️ Potential issue

파일명에 오타가 있습니다.

파일명이 AddCradBackground.tsx로 되어 있지만, 컴포넌트 이름은 AddCardBackground입니다.
파일명을 AddCardBackground.tsx로 수정하는 것이 좋겠습니다.

src/features/home/providers/CardSwiperProvider.tsx (1)

6-8: 🛠️ Refactor suggestion

Swiper 컴포넌트 래퍼가 필요합니다만 구성 옵션이 부족합니다.

현재 구현은 기본적인 Swiper 래핑만 제공하고 있어 유연성이 제한됩니다. 카드 디테일 페이지 구현을 위해 더 많은 설정이 필요할 수 있습니다.

다음과 같이 Swiper 구성 옵션을 추가하여 컴포넌트를 개선해보세요:

'use client';

import { PropsWithChildren } from 'react';
-import { Swiper } from 'swiper/react';
+import { Swiper, SwiperProps } from 'swiper/react';
+import { Pagination } from 'swiper/modules';
+import 'swiper/css';
+import 'swiper/css/pagination';

-export const CardSwiperProvider = ({ children }: PropsWithChildren) => {
+export const CardSwiperProvider = ({ 
+  children, 
+  swiperOptions = {} 
+}: PropsWithChildren<{ swiperOptions?: SwiperProps }>) => {
  return (
-    <Swiper>{children}</Swiper>
+    <Swiper
+      modules={[Pagination]}
+      spaceBetween={20}
+      slidesPerView={'auto'}
+      pagination={{ clickable: true }}
+      centeredSlides={true}
+      {...swiperOptions}
+    >
+      {children}
+    </Swiper>
  );
};
📝 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.

'use client';

import { PropsWithChildren } from 'react';
import { Swiper, SwiperProps } from 'swiper/react';
import { Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/pagination';

export const CardSwiperProvider = ({ 
  children, 
  swiperOptions = {} 
}: PropsWithChildren<{ swiperOptions?: SwiperProps }>) => {
  return (
    <Swiper
      modules={[Pagination]}
      spaceBetween={20}
      slidesPerView={'auto'}
      pagination={{ clickable: true }}
      centeredSlides={true}
      {...swiperOptions}
    >
      {children}
    </Swiper>
  );
};
src/app/layout.tsx (1)

11-19: 💡 Verification agent

🧩 Analysis chain

viewport 설정 접근성 문제 확인 필요

viewport 설정에 maximumScale: 1userScalable: false 속성이 추가되었습니다. 이 설정은 사용자가 페이지를 확대할 수 없게 만들어 일관된 디자인을 유지할 수 있지만, 시각적 장애가 있는 사용자들의 접근성에 문제를 일으킬 수 있습니다.

웹 접근성 지침(WCAG)에 따르면 사용자가 화면을 확대할 수 있어야 합니다. 따라서 이 설정을 재고하는 것이 좋습니다.

다음과 같이 수정하는 것을 권장합니다:

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
-  maximumScale: 1,
-  userScalable: false,
+  maximumScale: 5,
+  userScalable: true,
};

🌐 Web query:

웹 접근성 지침 WCAG에서 viewport maximumScale과 userScalable 설정에 대해 권장하는 사항은?

💡 Result:

웹 접근성 지침(WCAG)은 뷰포트의 maximum-scaleuser-scalable 설정에 대해 명확한 권장 사항을 제시합니다. 이는 저시력 사용자가 텍스트와 콘텐츠를 확대하여 사용할 수 있도록 보장하기 위한 것입니다.

주요 권장사항

  1. `user-scalable="no" 사용 금지

    • 이 설정은 사용자의 화면 확대/축소 기능을 완전히 비활성화합니다. WCAG 1.4.4(텍스트 크기 조정) 및 1.4.10(리플로우)에 따라 사용자가 콘텐츠를 최대 200%까지 확대할 수 있어야 합니다[1][3][7].
    • iOS 10 이후부터는 user-scalable="no"를 무시하도록 변경되었으나, 접근성 문제로 인해 해당 설정 자체를 피해야 합니다[2].
  2. maximum-scale 값은 최소 2.0 이상 설정

    • maximum-scale=1.0 또는 그 이하로 설정하면 확대가 제한되어 WCAG 1.4.4를 위반합니다. 최소 2.0 이상으로 설정해야 200% 확대가 가능합니다[3][7][9].
    • 일부 브라우저는 maximum-scale을 생략할 경우 기본값으로 높은 확대 수준을 허용하므로, 명시적으로 maximum-scale=2.0을 권장합니다[3][13].

WCAG 기준과의 연관성

  • 1.4.4 Resize Text (AA): 텍스트를 200%까지 확대할 때 콘텐츠 손실 없이 조정 가능해야 합니다[5][13].
  • 1.4.10 Reflow (AA): 400% 확대 시 수평 스크롤 없이 콘텐츠가 단일 열로 재배치되어야 합니다[6][11].

잘못된 설정 예시 및 해결 방안

<!-- 문제 있는 설정 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<!-- 권장 설정 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0">
  • user-scalable="no" 제거 및 maximum-scale=2.0으로 변경하면 WCAG를 준수할 수 있습니다[1][7][9].

배경 및 중요성

  • 저시력 사용자 지원: 화면 확대는 시각 장애인에게 필수적이며, 제한 시 웹 이용이 어려워집니다[1][2][7].
  • 법적 준수: 유럽 및 미국 등에서는 WCAG 준수가 법적 의무화되고 있습니다[8][9].
  • 반응형 디자인: 뷰포트 설정은 모바일 기기에서의 접근성과 직접적인 연관이 있습니다[6][11].

이러한 지침은 사용자 경험을 개선하고 법적 위험을 줄이는 데 필수적입니다.

Citations:


뷰포트 확대 제한 수정 필요

현재 viewport 설정은 maximumScale: 1userScalable: false로 되어 있어, 사용자가 콘텐츠를 확대할 수 없으므로 WCAG 접근성 기준(최소 2.0 이상의 확대 지원)에 부합하지 않습니다. 이에 따라 아래와 같이 수정해 주세요:

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
-  maximumScale: 1,
-  userScalable: false,
+  maximumScale: 2,
+  userScalable: true,
};

이 수정은 저시력 사용자가 페이지를 최대 200%까지 확대할 수 있도록 보장하여 접근성을 개선합니다.

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

  viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
};

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  maximumScale: 2,
  userScalable: true,
};
src/features/card-detail/ui/snsLink.tsx (3)

15-25: 🛠️ Refactor suggestion

SNS 링크 렌더링 로직에 개선이 필요합니다.

현재 구현에서는 모든 SNS 링크에 동일한 디자인 아이콘이 사용되고 있습니다. 각 SNS 유형에 따라 다른 아이콘을 표시하는 것이 더 적절해 보입니다.

다음과 같이 수정하는 것을 제안합니다:

{data.map((e, i) => {
  return (
    <div className="flex flex-col items-center" key={i}>
      <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30">
-       <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} />
+       <Image 
+         src={`/icons/${e.type.toLowerCase()}-icon.svg`} 
+         alt={`${e.type} 아이콘`} 
+         width={28} 
+         height={28} 
+       />
      </div>

      <span>{e.type}</span>
    </div>
  );
})}
📝 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.

{data.map((e, i) => {
  return (
    <div className="flex flex-col items-center" key={i}>
      <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30">
-       <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} />
+       <Image 
+         src={`/icons/${e.type.toLowerCase()}-icon.svg`} 
+         alt={`${e.type} 아이콘`} 
+         width={28} 
+         height={28} 
+       />
      </div>

      <span>{e.type}</span>
    </div>
  );
})}

17-24: 🛠️ Refactor suggestion

SNS 링크 클릭 기능 추가 필요

현재 구현에서는 SNS 링크를 보여주기만 하고 실제로 클릭 가능한 링크 기능이 없습니다. 사용자가 SNS 프로필로 바로 이동할 수 있도록 링크 기능을 추가하는 것이 좋겠습니다.

<div className="flex flex-col items-center" key={i}>
+ <a 
+   href={e.url} 
+   target="_blank" 
+   rel="noopener noreferrer" 
+   className="flex flex-col items-center"
+ >
    <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30">
      <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} />
    </div>

    <span>{e.type}</span>
+ </a>
</div>
📝 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-col items-center" key={i}>
  <a 
    href={e.url} 
    target="_blank" 
    rel="noopener noreferrer" 
    className="flex flex-col items-center"
  >
    <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30">
      <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} />
    </div>
    <span>{e.type}</span>
  </a>
</div>

6-6: 💡 Verification agent

🧩 Analysis chain

타입 정의의 출처 확인 필요

SNSLinkDto 타입을 현재 'sample' 파일에서 가져오고 있습니다. 이는 실제 프로덕션 코드에서는 적절하지 않을 수 있습니다. 실제 API 응답 타입을 사용하는 것이 좋겠습니다.


🌐 Web query:

Next.js 프로젝트에서 API 응답 타입 관리 모범 사례

💡 Result:

Next.js 프로젝트에서 API 응답 타입을 효과적으로 관리하기 위한 모범 사례는 다음과 같습니다:

1. 전역 타입 정의 활용

  • 공통 응답 구조 표준화: .d.ts 파일에 기본 응답 인터페이스를 정의해 프로젝트 전체에서 일관된 구조 사용[6][9]
// api.d.ts
declare interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}
  • 환경 변수 타입 안전성 강화: ProcessEnv 인터페이스 확장으로 환경 변수 접근 시 타입 검증[6][11]
declare namespace NodeJS {
  interface ProcessEnv {
    NEXT_PUBLIC_API_URL: string;
    DATABASE_URL: string;
  }
}

2. Next.js 기본 타입 시스템 확장

  • API 핸들러 타입 명시: NextApiResponse 제네릭으로 응답 구조 강제화[1][10]
import type { NextApiRequest, NextApiResponse } from 'next';

type UserData = {
  id: number;
  name: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<ApiResponse<UserData[]>>
) {
  // ...
}
  • 미들웨어 응답 확장: 커스텀 응답 타입으로 동적 파라미처 처리[3][17]
interface CustomResponse extends NextResponse {
  params?: { action: string };
}

3. 자동화 도구 통합

  • OpenAPI 기반 코드 생성: Orval을 이용해 스키마에서 타입 자동 생성[9]
// orval.config.ts
export default defineConfig({
  api: {
    input: './openapi.yaml',
    output: './src/api/generated'
  }
});
  • React Query 통합: 생성된 타입을 쿼리 훅에 자동 적용[9][14]
const { data } = useGetApiTodos();
// data는 자동으로 ApiResponse<Todo[]> 타입 추론

4. 에러 처리 표준화

  • 일관된 에러 응답 구조: HTTP 상태 코드와 연동된 에러 타입 정의[7][12]
interface ErrorResponse {
  errorCode: string;
  message: string;
  details?: Record<string, unknown>;
}
  • 타입 가드 구현: 런타임 유효성 검사 도구(zod 등)와 결합[3][5]
function isErrorResponse(res: unknown): res is ErrorResponse {
  return typeof res === 'object' && res !== null && 'errorCode' in res;
}

5. 모듈화 전략

  • 도메인 주도 타입 분리: 기능별 독립적인 타입 모듈 구성[6][15]
/src
  /types
    /auth.ts
    /product.ts
    /common.ts
  • API 계층 타입 격리: 프론트엔드와 백엔드 타입 의존성 분리[9][11]
// 백엔드 전용 타입
declare module "next/server" {
  interface NextResponse {
    customBackendProp: string;
  }
}

이러한 접근 방식은 타입 안전성과 개발 생산성 간의 균형을 유지하면서 다음 장점을 제공합니다:

  • API 변경 시 컴파일 단계에서 오류 감지
  • 자동 완성 기능을 통한 개발 경험 개선
  • 문서화 없이도 타입 정의를 통해 API 스펙 이해 가능
  • 프론트엔드/백엔드 팀 간 커뮤니케이션 효율화

특히 Next.js 14 이상 버전에서는 App Router와 Server Actions 도입으로 인해 타입 관리의 중요성이 더욱 강조되고 있으며[13][17], OpenAPI 기반의 자동화 도구 활용이 프로젝트 규모 확장 시 필수 요소로 부상하고 있습니다[9][14].

Citations:


SNSLinkDto 타입 정의 업데이트 필요

  • 현재 SNSLinkDto가 샘플 파일에서 가져온 타입으로 설정되어 있습니다. 이 방식은 프로덕션에서 실제 API 응답에 맞지 않을 수 있습니다.
  • Next.js 모범 사례에 따르면, API 응답 타입은 전역 타입 정의나 도메인별 분리된 파일(예: /src/types/apiResponses.d.ts)을 활용하여 프로젝트 전반에 일관되게 관리하는 것이 좋습니다.
  • 실제 API 응답에 기반한 타입 정의로 변경하여 API 변경 시 컴파일 단계에서 오류를 미리 감지할 수 있도록 수정해 주세요.
src/features/home/components/BusinessCard/CardTag.tsx (2)

4-4: ⚠️ Potential issue

JopType 오타 수정 필요

import 문에 'JopType'이라는 이름이 사용되었는데, 이는 'JobType'의 오타로 보입니다. 타입 이름을 올바르게 수정해주세요.

-import { JopType } from '@/features/home/types';
+import { JobType } from '@/features/home/types';
📝 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.

import { JobType } from '@/features/home/types';

9-11: ⚠️ Potential issue

Props 타입 정의 수정 필요

JopType 대신 JobType을 사용하도록 Props 타입 정의를 수정해주세요.

type Props = {
-  tagType: JopType;
+  tagType: JobType;
} & HTMLAttributes<HTMLDivElement>;
📝 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.

type Props = {
  tagType: JobType;
} & HTMLAttributes<HTMLDivElement>;
src/features/card-detail/mocks/sample.ts (1)

17-38: ⚠️ Potential issue

SNS 링크 중복 및 잘못된 URL 확인 필요

SNS 목록에 다음과 같은 문제가 있습니다:

  1. 인스타그램 링크가 GitHub URL로 설정되어 있습니다.
  2. 네이버 블로그가 중복으로 등록되어 있습니다.
  3. 모든 SNS의 링크가 동일한 GitHub URL로 설정되어 있습니다.

각 SNS 타입에 맞는 적절한 URL로 수정하고 중복을 제거해주세요.

sns: [
  {
    type: 'INSTAGRAM',
-   link: 'https://github.com/jaeiny',
+   link: 'https://instagram.com/jaeiny',
  },
  {
    type: 'GITHUB',
    link: 'https://github.com/jaeiny',
  },
  {
    type: 'LINKEDIN',
-   link: 'https://github.com/jaeiny',
+   link: 'https://linkedin.com/in/jaeiny',
  },
  {
    type: 'NAVER_BLOG',
-   link: 'https://github.com/jaeiny',
+   link: 'https://blog.naver.com/jaeiny',
  },
-  {
-   type: 'NAVER_BLOG',
-   link: 'https://github.com/jaeiny',
-  },
],
📝 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.

    sns: [
      {
        type: 'INSTAGRAM',
        link: 'https://instagram.com/jaeiny',
      },
      {
        type: 'GITHUB',
        link: 'https://github.com/jaeiny',
      },
      {
        type: 'LINKEDIN',
        link: 'https://linkedin.com/in/jaeiny',
      },
      {
        type: 'NAVER_BLOG',
        link: 'https://blog.naver.com/jaeiny',
      },
    ],
src/features/home/containers/CardContainer.tsx (1)

18-20: ⚠️ Potential issue

useCardQuery 데이터 로딩 상태 처리 필요

현재 코드는 데이터가 항상 있다고 가정하고 있습니다. 실제 API 호출 시 로딩 상태나 에러 상태에 대한 처리가 필요합니다.

export const CardContainer = () => {
-  const { data } = useCardQuery();
+  const { data, isLoading, error } = useCardQuery();
+
+  if (isLoading) {
+    return <div className="h-[440px] flex items-center justify-center">로딩 중...</div>;
+  }
+
+  if (error || !data) {
+    return <div className="h-[440px] flex items-center justify-center">데이터를 불러오는데 실패했습니다.</div>;
+  }

  return (
📝 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 CardContainer = () => {
  const { data, isLoading, error } = useCardQuery();

  if (isLoading) {
    return <div className="h-[440px] flex items-center justify-center">로딩 중...</div>;
  }

  if (error || !data) {
    return <div className="h-[440px] flex items-center justify-center">데이터를 불러오는데 실패했습니다.</div>;
  }

  return (
    // ... rest of the component JSX
  );
};
src/features/home/hooks/queries/useCardQuery.ts (2)

17-24: ⚠️ Potential issue

실제 API 응답 대신 목업 데이터 사용 중

현재 이 훅은 실제 API 호출 결과를 무시하고 항상 CARD_MOCK 목업 데이터를 반환합니다. 개발 완료 시 실제 API 응답을 사용하도록 수정이 필요합니다. 또한 에러 핸들링 로직이 없어 API 호출 실패 시 대응 방안이 없습니다.

export const useCardQuery = (params?: any) => {
-  const { data: _ } = useQuery({
+  const { data, error, isLoading } = useQuery({
    queryKey: [CARD_QUERY_KEY, params],
    queryFn: () => _getCard(params),
  });

-  return { data: CARD_MOCK };
+  // 개발 단계에서만 목업 데이터 사용
+  return { 
+    data: process.env.NODE_ENV === 'development' ? CARD_MOCK : data,
+    error,
+    isLoading
+  };
};
📝 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 useCardQuery = (params?: any) => {
  const { data, error, isLoading } = useQuery({
    queryKey: [CARD_QUERY_KEY, params],
    queryFn: () => _getCard(params),
  });

  // 개발 단계에서만 목업 데이터 사용
  return { 
    data: process.env.NODE_ENV === 'development' ? CARD_MOCK : data,
    error,
    isLoading
  };
};

9-15: 💡 Verification agent

🧩 Analysis chain

API 엔드포인트 하드코딩 및 타입 개선 필요

'/somewhere/client-side' 엔드포인트는 플레이스홀더로 보입니다. 실제 API 엔드포인트로 변경이 필요하며, params: any 타입도 더 구체적인 타입으로 정의하면 좋겠습니다.


🏁 Script executed:

#!/bin/bash
# API 엔드포인트 상수가 정의되어 있는지 확인
echo "API 엔드포인트 상수 검색 결과:"
rg -i "const.*url|CLIENT_SIDE_URL" --type ts

Length of output: 1975


API 엔드포인트 상수 사용 및 파라미터 타입 정의 필요

  • src/features/home/hooks/queries/useCardQuery.ts 파일에서 현재 하드코딩된 '/somewhere/client-side' 엔드포인트는 플레이스홀더로 보입니다.
    요청: src/shared/constants/index.ts에 정의된 CLIENT_SIDE_URL 혹은 실제 API 엔드포인트 상수를 사용해 주세요.
  • params: any로 정의된 부분은 임시로 사용한 타입으로 보입니다.
    요청: 해당 API의 요청 파라미터에 맞는 구체적인 타입(예: interface)을 정의하여 사용해 주세요.
src/features/card-detail/hooks/query/useCardDetailQuery.ts (1)

19-26: 🛠️ Refactor suggestion

목업 데이터 사용 및 환경 분리 필요

useCardDetailQuery 훅도 실제 API 응답을 무시하고 항상 목업 데이터를 반환합니다. 개발/프로덕션 환경에 따라 분리하고, 로딩 상태와 에러 상태도 반환하는 것이 좋겠습니다.

export const useCardDetailQuery = (id: string) => {
-  const { data: _ } = useQuery({
+  const { data, error, isLoading } = useQuery({
    queryKey: ['cardDetail', id],
    queryFn: () => getCardDetail(id),
  });

-  return { data: mockCardDetailData };
+  return { 
+    data: process.env.NODE_ENV === 'development' ? mockCardDetailData : data,
+    isLoading,
+    error 
+  };
};
📝 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 useCardDetailQuery = (id: string) => {
  const { data, error, isLoading } = useQuery({
    queryKey: ['cardDetail', id],
    queryFn: () => getCardDetail(id),
  });

  return { 
    data: process.env.NODE_ENV === 'development' ? mockCardDetailData : data,
    isLoading,
    error 
  };
};
src/features/card-detail/config/sns-config.ts (1)

29-32: 💡 Verification agent

🧩 Analysis chain

누락된 SNS 아이콘 및 임시 아이콘 사용

일부 SNS 아이콘(Facebook, Medium, ETC)이 누락되어 있고 임시로 'design-icon-white.svg'를 사용하고 있습니다. 실제 아이콘으로 업데이트가 필요합니다.

Also applies to: 58-61, 67-70


🏁 Script executed:

#!/bin/bash
# 실제 아이콘 파일 존재 여부 확인
echo "SNS 아이콘 파일 존재 여부 확인:"
fd -t f "facebook|medium|etc" --exec file {} \; public/icons/sns-icon/
echo "현재 사용 중인 임시 아이콘 파일 확인:"
file public/icons/design-icon-white.svg 2>/dev/null || echo "임시 아이콘 파일이 없습니다."

Length of output: 266


확인 결과: SNS 아이콘 관련 파일 누락 문제

  • 파일: src/features/card-detail/config/sns-config.ts (라인 29-32, 58-61, 67-70)
  • 이슈:
    • 설정에서 Facebook, Medium, ETC 등 일부 SNS 아이콘에 대해 임시로 design-icon-white.svg 경로를 사용하고 있으나, 해당 임시 아이콘 파일이 존재하지 않는 것으로 확인되었습니다.
    • public/icons/sns-icon/ 경로 내에서 실제 SNS 아이콘 파일(Facebook, Medium, ETC)도 찾을 수 없습니다.
  • 요청 사항:
    • 누락된 실제 SNS 아이콘 파일을 /public/icons/sns-icon/ 디렉토리에 추가하거나, 올바른 경로의 아이콘 파일로 업데이트해 주세요.
src/features/card-detail/ui/cardDetailHeader.tsx (1)

12-15: 🛠️ Refactor suggestion

데이터 페칭 로직 개선 필요

현재 샘플 훅을 사용하여 데이터를 가져오고 있으나, 로딩 상태나 에러 처리가 없습니다. 실제 API와 통합할 때는 적절한 상태 처리가 필요합니다.

 const CardDetailHeader = () => {
-  const { data: userData } = useUser();
-  const { data: userDataDetail } = useUserDetail();
+  const { data, isLoading, error } = useCardDetailQuery();
+  
+  if (isLoading) return <div className="w-full h-[200px] flex items-center justify-center">로딩 중...</div>;
+  if (error) return <div className="w-full h-[200px] flex items-center justify-center">에러가 발생했습니다</div>;
+  if (!data) return null;
+  
+  const { name, jobTitle, organization, summary, region } = data;

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

package.json (1)

2-2: 💡 Verification agent

🧩 Analysis chain

프로젝트 이름 변경 확인

프로젝트 이름이 "Took-FE"에서 "took-fe"로 변경되었습니다. 이 변경이 의도적인지, 그리고 다른 설정 파일(예: README, CI/CD 설정 등)에서도 일관되게 적용되었는지 확인해주세요.


🏁 Script executed:

#!/bin/bash
# 프로젝트 이름이 다른 파일에서도 일관되게 사용되는지 확인
echo "README.md에서 프로젝트 이름 확인:"
rg -i "took-?fe" README.md || echo "README.md에서 프로젝트 이름 언급 없음"

echo "\n.github 폴더 내 워크플로우 파일에서 프로젝트 이름 확인:"
rg -i "took-?fe" .github/ || echo ".github 폴더에서 프로젝트 이름 언급 없음"

Length of output: 525


프로젝트 이름 불일치 문제 – 수정 필요

  • package.json에서는 프로젝트 이름이 "took-fe"로 변경된 것으로 보입니다.
  • README.md에는 프로젝트 이름이 언급되지 않아, 업데이트가 누락된 것 같습니다.
  • .github/workflows 내 워크플로우 파일(pr-comment.yml, deploy-integration.yml)에서는 여전히 "Took-FE" 및 관련 명칭(Took-FE-develop, Took-FE-production)이 사용되고 있습니다.

위 사항들에 대해 프로젝트 이름을 일관되게 통일할 수 있도록 확인 및 수정 부탁드립니다.

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

40-42: ⚠️ Potential issue

Next 버튼에 onClick 핸들러가 없습니다

다음 단계로 이동하는 버튼에 onClick 핸들러가 구현되어 있지 않습니다. 멀티스텝 폼에서 다음 단계로 진행하기 위한 핸들러를 추가해야 합니다.

- <Button className="w-full" disabled={tagCount === 0}>
+ <Button className="w-full" disabled={tagCount === 0} onClick={handleNextStep}>
  다음
</Button>

다음 단계로 이동하는 핸들러 함수를 구현하고 추가하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        <Button className="w-full" disabled={tagCount === 0} onClick={handleNextStep}>
          다음
        </Button>

22-22: 💡 Verification agent

🧩 Analysis chain

TagBox에 선택된 태그를 처리하는 로직 확인 필요

TagBox 컴포넌트에 tagCount, tagArray, setTagCount, setTagArray를 props로 전달하고 있지만, 이 값들이 실제로 어떻게 사용되는지 확인할 수 없습니다. 태그 선택 시 다음 단계로 이 데이터를 전달하는 로직이 필요합니다.


🏁 Script executed:

#!/bin/bash
# TagBox 컴포넌트 구현을 확인
find . -name "TagBox.tsx" -o -name "TagBox.jsx" | xargs cat

Length of output: 1798


태그 선택 데이터 전달 로직 개선 요청

TagBox 컴포넌트에서는 클릭 시 선택된 태그에 대해 상태(tagCount, tagArray)의 추가/제거 처리가 올바르게 수행되고 있음이 확인되었습니다. 다만, 이 상태값이 이후 프로세스(예: 제출 또는 다음 단계로의 데이터 전달)에 활용되는 로직은 확인되지 않습니다.

  • 현재 TagBox는 선택된 태그를 갱신한 후 콘솔에 출력하는 용도로만 사용되고 있으며, 이후 단계에서 해당 데이터를 활용하는 로직이 누락된 것으로 보입니다.
  • 선택된 태그와 관련 상태 값을 후속 단계나 상위 컴포넌트로 전달하여 처리하는 로직이 추가 구현되어야 합니다.
src/features/card-detail/ui/projects.tsx (1)

16-17: 🛠️ Refactor suggestion

React 컴포넌트 리스트의 key로 인덱스 대신 고유 식별자를 사용하세요.

React에서 배열을 매핑할 때 인덱스를 key로 사용하는 것은 권장되지 않습니다. 프로젝트 목록의 순서가 변경되거나 항목이 추가/제거되는 경우 렌더링 문제가 발생할 수 있습니다. 가능하면 각 프로젝트의 고유 ID를 key로 사용하는 것이 좋습니다.

- {data.map((project, i) => (
-   <div key={i} className="flex flex-col overflow-hidden rounded-md">
+ {data.map((project) => (
+   <div key={project.id} className="flex flex-col overflow-hidden rounded-md">
📝 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.

      {data.map((project) => (
        <div key={project.id} className="flex flex-col overflow-hidden rounded-md">
src/shared/ui/Input/tagInput.tsx (2)

17-26: ⚠️ Potential issue

주석 처리된 코드 및 콘솔 로그 정리 필요

주석 처리된 코드와 콘솔 로그는 프로덕션 코드에서 제거하거나 구현을 완료해야 합니다. 현재 상태로는 handleKeyDown 함수가 실제로 태그를 추가하는 기능을 수행하지 않습니다.

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter' || e.key === ' ') {
-     console.log(tags, 'tags');
      e.preventDefault();
-     // const trimmedValue = inputValue.trim();
-     // if (trimmedValue && !tags.includes(trimmedValue)) {
-     //   setTags((prevTags) => [...prevTags, trimmedValue]);
-     //   setInputValue(''); // 태그 추가 후 인풋 초기화
-     // }
+     const trimmedValue = inputValue.trim();
+     if (trimmedValue && !tags.includes(trimmedValue)) {
+       setTags((prevTags) => [...prevTags, trimmedValue]);
+       setInputValue(''); // 태그 추가 후 인풋 초기화
+     }
    }
  };
📝 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.

    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        const trimmedValue = inputValue.trim();
        if (trimmedValue && !tags.includes(trimmedValue)) {
          setTags((prevTags) => [...prevTags, trimmedValue]);
          setInputValue(''); // 태그 추가 후 인풋 초기화
        }
      }
    };

38-52: 🛠️ Refactor suggestion

태그 표시 UI 개선 및 접근성 고려

현재 구현은 다음과 같은 문제점이 있습니다:

  1. 태그가 absolute 위치로 지정되어 있어 여러 태그가 추가될 경우 겹칠 수 있습니다.
  2. 태그 삭제 버튼("x")이 접근성을 고려하지 않았습니다.
  <div className="relative flex flex-wrap gap-2 mt-2">
    {tags.map((tag, index) => (
      <div
        key={index}
-       className="flex items-center bg-gray-200 rounded-lg px-2 py-1 absolute left-3 top-1"
+       className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1"
      >
        <span className="text-body-5 text-gray-700">{tag}</span>
-       <span
-         className="ml-2 cursor-pointer text-red-500"
+       <button
+         type="button"
+         aria-label={`${tag} 태그 삭제`}
+         className="ml-2 cursor-pointer text-red-500"
          onClick={() => handleRemoveTag(tag)}
        >
          x
-       </span>
+       </button>
      </div>
    ))}
  </div>
📝 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="relative flex flex-wrap gap-2 mt-2">
          {tags.map((tag, index) => (
            <div
              key={index}
              className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1"
            >
              <span className="text-body-5 text-gray-700">{tag}</span>
              <button
                type="button"
                aria-label={`${tag} 태그 삭제`}
                className="ml-2 cursor-pointer text-red-500"
                onClick={() => handleRemoveTag(tag)}
              >
                x
              </button>
            </div>
          ))}
        </div>
src/features/card-detail/ui/underlineTabs.tsx (1)

29-38: 🛠️ Refactor suggestion

접근성 개선 필요

탭 구현에 대한 키보드 탐색 및 ARIA 속성이 누락되어 있습니다. 접근성을 개선하기 위해 적절한 ARIA 역할과 속성을 추가하세요.

+ <div
+   role="tablist"
+   className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
+   style={{
+     scrollbarWidth: 'none',
+     msOverflowStyle: 'none',
+   }}
+ >
+   {tabs.map((tab) => (
+     <button
+       key={tab.id}
+       role="tab"
+       aria-selected={activeTab === tab.id}
+       aria-controls={`panel-${tab.id}`}
+       id={`tab-${tab.id}`}
        onClick={() => onChange(tab.id)}
        className={cn(
          `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
          activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
        )}
      >
        <Typography variant="body-3">{tab.label}</Typography>
      </button>
    ))}
  </div>
📝 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
  role="tablist"
  className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
  style={{
    scrollbarWidth: 'none',
    msOverflowStyle: 'none',
  }}
>
  {tabs.map((tab) => (
    <button
      key={tab.id}
      role="tab"
      aria-selected={activeTab === tab.id}
      aria-controls={`panel-${tab.id}`}
      id={`tab-${tab.id}`}
      onClick={() => onChange(tab.id)}
      className={cn(
        `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
        activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
      )}
    >
      <Typography variant="body-3">{tab.label}</Typography>
    </button>
  ))}
</div>
src/features/home/hooks/prefetches/useCardPrefetch.ts (2)

19-24: 🛠️ Refactor suggestion

타입 안전성 개선 필요

params 및 반환 데이터에 'any' 타입을 사용하고 있습니다. 타입 안전성을 높이기 위해 명확한 타입 정의가 필요합니다.

- export const getCardPrefetch = (params: any, queryClient: QueryClient, cookie: ReadonlyRequestCookies) => {
+ interface CardQueryParams {
+   id?: string;
+   // 필요한 다른 파라미터들 추가
+ }
+ 
+ interface CardType {
+   // 카드 데이터 타입 정의
+   id: string;
+   name: string;
+   // 기타 필요한 필드들
+ }
+ 
+ export const getCardPrefetch = (params: CardQueryParams, queryClient: QueryClient, cookie: ReadonlyRequestCookies) => {
  return queryClient.prefetchQuery({
    queryKey: [CARD_QUERY_KEY, params],
    queryFn: () => _getCardPrefetch(params, cookie),
  });
};
📝 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.

interface CardQueryParams {
  id?: string;
  // 필요한 다른 파라미터들 추가
}

interface CardType {
  // 카드 데이터 타입 정의
  id: string;
  name: string;
  // 기타 필요한 필드들
}

export const getCardPrefetch = (
  params: CardQueryParams,
  queryClient: QueryClient,
  cookie: ReadonlyRequestCookies
) => {
  return queryClient.prefetchQuery({
    queryKey: [CARD_QUERY_KEY, params],
    queryFn: () => _getCardPrefetch(params, cookie),
  });
};

8-17: ⚠️ Potential issue

API 엔드포인트 실제 경로로 수정 필요

'/somewhere/server-side'는 실제 API 엔드포인트가 아닌 임시 플레이스홀더로 보입니다. 실제 사용할 API 엔드포인트로 수정이 필요합니다.

const _getCardPrefetch = async (params: any, cookie: ReadonlyRequestCookies) => {
-  const data = await server.get<any>('/somewhere/server-side', {
+  const data = await server.get<CardType>('/api/cards', {
    params,
    headers: {
      Cookie: cookie?.toString(),
    },
  });

  return data;
};
📝 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.

const _getCardPrefetch = async (params: any, cookie: ReadonlyRequestCookies) => {
  const data = await server.get<CardType>('/api/cards', {
    params,
    headers: {
      Cookie: cookie?.toString(),
    },
  });

  return data;
};
src/features/multi-step-form/ui/careerForm/index.tsx (1)

51-66: 🛠️ Refactor suggestion

스텝별 검증 로직 구현

각 스텝에 필요한 필드만 검증하는 로직이 잘 구현되었습니다. 다만 !fieldsToValidate 체크는 검증할 필드가 없는 경우(예: 스텝 2~4) 조기 반환하게 되어 이후 단계로 진행되지 않을 수 있습니다.

다음과 같이 빈 배열인 경우에도 다음 단계로 진행되도록 수정하는 것이 좋겠습니다:

const handleNextStep = async () => {
  const fieldsToValidate = stepValidationFields[currentStep];
- if (!fieldsToValidate) return;
+ if (!fieldsToValidate) {
+   if (currentStep < TOTAL_STEPS) {
+     onNextStep();
+   } else {
+     // 마지막 단계인 경우 form 제출
+     handleSubmit(onSubmit)();
+   }
+   return;
+ }

  const valid = await trigger(fieldsToValidate);

  if (valid) {
    if (currentStep < TOTAL_STEPS) {
      onNextStep();
    } else {
      // 마지막 단계인 경우 form 제출
      handleSubmit(onSubmit)();
    }
  }
};
📝 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.

  // 각 스텝에 해당하는 필드만 trigger로 검증 후 다음 단계로 이동
  const handleNextStep = async () => {
    const fieldsToValidate = stepValidationFields[currentStep];
    if (!fieldsToValidate) {
      if (currentStep < TOTAL_STEPS) {
        onNextStep();
      } else {
        // 마지막 단계인 경우 form 제출
        handleSubmit(onSubmit)();
      }
      return;
    }

    const valid = await trigger(fieldsToValidate);

    if (valid) {
      if (currentStep < TOTAL_STEPS) {
        onNextStep();
      } else {
        // 마지막 단계인 경우 form 제출
        handleSubmit(onSubmit)();
      }
    }
  };
src/features/home/components/BusinessCard/Card.tsx (1)

90-111: ⚠️ Potential issue

CardTags 컴포넌트 구현

태그 표시 로직에 문제가 있습니다. 코드의 주석과 실제 구현이 일치하지 않습니다. 4개 이상의 태그가 있을 때 처음 3개를 표시하고 나머지 갯수를 "+n" 형태로 보여주려는 의도같은데, "4개 이상"과 "3개 이상"의 불일치가 있습니다.

다음과 같이 수정해주세요:

let convertedTags = tags;

- if (tags.length > 4) {
+ // 최대 3개의 태그를 표시하고, 나머지는 "+n" 형태로 표시
+ if (tags.length > 3) {
  convertedTags = [...tags.slice(0, 3), `+ ${tags.length - 3}`];
}
📝 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.

type CardTagsProps = {
  tags: string[];
  tagType: JopType;
};

export const CardTags = ({ tags, tagType, ...rest }: CardTagsProps & HTMLAttributes<HTMLDivElement>) => {
  let convertedTags = tags;

  // 최대 3개의 태그를 표시하고, 나머지는 "+n" 형태로 표시
  if (tags.length > 3) {
    convertedTags = [...tags.slice(0, 3), `+ ${tags.length - 3}`];
  }

  return (
    <div className="mt-[44px] flex flex-wrap items-center gap-1" {...rest}>
      {convertedTags.map((tag, index) => (
        <CardTag key={`${tag}-${index}`} tagType={tagType}>
          {tag}
        </CardTag>
      ))}
    </div>
  );
};

Copy link
Collaborator

@jangwonyoon jangwonyoon left a comment

Choose a reason for hiding this comment

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

고생 하셨습니다.

Comment on lines +9 to +20
const JOB_CONFIG: Record<JobType, JobConfig> = {
DEVELOPER: {
backgroundImage: '/images/card-detail/card-detail-develop.png',
iconPath: '/icons/developer-icon-white.svg',
iconAlt: '개발자 아이콘',
},
DESIGNER: {
backgroundImage: '/images/card-detail/card-detail-design.png',
iconPath: '/icons/designer-icon-white.svg',
iconAlt: '디자이너 아이콘',
},
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

@JaeIn1 config로 분리해서 관리한 부분이 너무 좋네요

Comment on lines 35 to 42
const sectionRefs = {
domains: useRef<HTMLDivElement>(null),
sns: useRef<HTMLDivElement>(null),
news: useRef<HTMLDivElement>(null),
hobby: useRef<HTMLDivElement>(null),
posts: useRef<HTMLDivElement>(null),
projects: useRef<HTMLDivElement>(null),
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

ref 를 객체로 관리한 부분이 인상적이네요. 좋습니다.

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 (6)
src/features/card-detail/hooks/useTabsActive.ts (2)

1-2: 파일명 주석 불일치
주석에서 "useTabSync.ts"라는 이름이 사용되고 있지만, 실제 파일명은 "useTabsActive.ts"입니다. 필요하다면 주석을 파일명과 일치시켜 가독성을 높이는 방안을 고려해 주세요.


17-35: 기본 탭 처리 로직 검토 필요
모든 섹션이 보이지 않는 경우(모두 false)가 발생한다면, 활성화할 탭이 없어질 수 있습니다. 사용성 측면에서 기본 탭을 설정하거나 예외 처리를 추가하는 것이 좋겠습니다.

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

69-75: 조건부 클래스 간소화 제안
page === 'detail' && hasBackground, page === 'create' 모두 같은 배경 클래스를 적용하므로 다음과 같이 조건식을 간단히 표현하면 코드를 더 읽기 쉽게 만들 수 있습니다.

- className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${
-   page === 'detail' && hasBackground ? 'bg-gray-black' : page === 'create' ? 'bg-gray-black' : ''
- }`}
+ className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${
+   (page === 'detail' && hasBackground) || page === 'create' ? 'bg-gray-black' : ''
+ }`}
src/features/card-detail/ui/cardTabs.tsx (3)

37-42: Intersection Observer 임계값 확인
threshold: 1을 사용하면 해당 섹션이 화면에 완전히 들어왔을 때만 inView가 true가 됩니다. 탭 전환이 다소 늦어진다고 느낄 수 있으므로, 사용자 경험에 맞는 적절한 임계값(예: 0.5 등)을 고려해 보세요.


80-96: 하드코딩된 스크롤 마진 주의
헤더 높이에 대응하기 위해 tabsScrollMargin를 100px로 고정했는데, 추후 헤더 높이가 바뀌면 오프셋이 어긋날 수 있습니다. 실제 헤더 높이를 동적으로 계산하거나 상수 한 곳에서만 관리하도록 개선하는 방안을 검토해 주세요.


100-179: 반복되는 여백 스타일 관리
px-[20px] 등 중복되는 여백 스타일이 여러 곳에서 반복됩니다. TailwindCSS의 커스텀 클래스나 상수화로 관리하여 유지보수성을 높일 수 있습니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d2bd3bc and 5f6c3e4.

📒 Files selected for processing (4)
  • src/app/(detail)/card-detail/[id]/page.tsx (1 hunks)
  • src/features/card-detail/hooks/useTabsActive.ts (1 hunks)
  • src/features/card-detail/ui/cardTabs.tsx (1 hunks)
  • src/shared/ui/appbar.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(detail)/card-detail/[id]/page.tsx
🔇 Additional comments (2)
src/shared/ui/appbar.tsx (1)

10-11: hasBackground 속성 도입
hasBackground를 통한 배경 처리 옵션이 명확하게 분리되어 UI 요구사항에 잘 대응합니다. 추가 수정 사항은 보이지 않습니다.

src/features/card-detail/ui/cardTabs.tsx (1)

45-55: 탭 동기화 로직 구현
useTabsActive를 활용하여 현재 보이는 섹션에 따라 탭 상태를 업데이트하는 로직이 명확하게 구성되었습니다. 별다른 문제 없이 잘 동작할 것으로 보입니다.

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 (3)
src/features/card-detail/ui/cardTabs.tsx (2)

67-78: ref 결합 로직의 개선 가능성이 있습니다.

현재 if-else 체인을 사용하여 섹션 ref를 설정하고 있지만, 이 부분은 객체 매핑을 통해 더 간결하게 작성할 수 있습니다.

- // 각 섹션에 맞는 ref 설정
- if (section === 'domains') domainsRef(el);
- else if (section === 'sns') snsRef(el);
- else if (section === 'news') newsRef(el);
- else if (section === 'hobby') hobbyRef(el);
- else if (section === 'posts') postsRef(el);
- else if (section === 'projects') projectsRef(el);
+ // 섹션에 맞는 ref 매핑
+ const refMapping = {
+   domains: domainsRef,
+   sns: snsRef,
+   news: newsRef,
+   hobby: hobbyRef,
+   posts: postsRef,
+   projects: projectsRef,
+ };
+ if (refMapping[section]) refMapping[section](el);

122-187: 비슷한 구조의 섹션 컴포넌트를 재사용 가능한 컴포넌트로 추출할 수 있습니다.

각 섹션이 매우 유사한 구조와 스타일을 가지고 있으므로, 이를 재사용 가능한 컴포넌트로 추출하면 코드의 중복을 줄이고 유지보수성을 높일 수 있습니다.

예를 들어, 다음과 같은 컴포넌트를 만들 수 있습니다:

type SectionProps = {
  id: TabId;
  title: string;
  children: React.ReactNode;
  combineRefs: (section: TabId) => (el: HTMLDivElement) => void;
  isLastSection?: boolean;
};

function CardSection({ id, title, children, combineRefs, isLastSection = false }: SectionProps) {
  return (
    <div
      ref={combineRefs(id)}
      id={id}
      className={`${spacingStyles({ paddingY: 'xl' })} ${
        isLastSection ? 'pb-[77px]' : 'border-b-[4px] border-gray-800'
      } px-[20px]`}
    >
      <Typography variant="body-1">{title}</Typography>
      {children}
    </div>
  );
}

이 컴포넌트를 사용하면 각 섹션 코드를 다음과 같이 간소화할 수 있습니다:

{data.data.interestDomain && (
  <CardSection
    id="domains"
    title="관심 도메인"
    combineRefs={combineRefs}
  >
    <DomainList data={data.data.interestDomain} />
  </CardSection>
)}
src/features/card-detail/types/cardDetail.ts (1)

15-29: ContentItemDto와 ProjectDto 타입이 중복됩니다.

ContentItemDto와 ProjectDto가 동일한 구조를 가지고 있습니다. 이 둘을 하나의 공통 타입으로 통합하여 코드 중복을 줄이고 일관성을 유지하는 것이 좋을 것 같습니다.

다음과 같이 수정할 수 있습니다:

// 콘텐츠 아이템 공통 타입 (블로그 포스트, 프로젝트 등)
+ export type ItemDto = {
+   title: string;
+   link: string;
+   imageUrl: string;
+   description: string;
+ };

- // 콘텐츠 아이템 (블로그 포스트 등) 타입
- export type ContentItemDto = {
-   title: string;
-   link: string;
-   imageUrl: string;
-   description: string;
- };

- // 프로젝트 타입
- export type ProjectDto = {
-   title: string;
-   link: string;
-   imageUrl: string;
-   description: string;
- };

+ // 콘텐츠 아이템 (블로그 포스트 등) 타입
+ export type ContentItemDto = ItemDto;
+ 
+ // 프로젝트 타입
+ export type ProjectDto = ItemDto;

이렇게 하면 나중에 특정 타입에 필드를 추가해야 할 경우 확장이 용이합니다:

// 예: 프로젝트에만 필요한 추가 필드가 있을 경우
export type ProjectDto = ItemDto & {
  teamSize?: number;
  duration?: string;
};
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6c3e4 and c980fe4.

📒 Files selected for processing (2)
  • src/features/card-detail/types/cardDetail.ts (1 hunks)
  • src/features/card-detail/ui/cardTabs.tsx (1 hunks)
🔇 Additional comments (8)
src/features/card-detail/ui/cardTabs.tsx (5)

4-6: 필요한 모든 hook이 적절히 임포트되었습니다.

React 및 react-intersection-observer의 hook을 적절히 가져오고 있어 좋습니다.


29-35: 스크롤 감지 로직이 잘 구현되어 있습니다.

스크롤 위치를 감지하여 Appbar의 표시 여부를 결정하는 로직이 깔끔하게 구현되어 있습니다.
threshold와 rootMargin 값의 주석 처리로 의도가 명확히 설명되어 있어 좋습니다.


57-65: ref 객체를 효율적으로 관리하고 있습니다.

여러 섹션에 대한 ref를 객체로 관리하여 코드의 가독성과 유지보수성을 높였습니다.
각 ref에 대한 타입 지정도 명확하게 되어있어 좋습니다.


111-111: Appbar 컴포넌트에 hasBackground 속성이 추가되었습니다.

PR 목표에 언급된 대로 명함 상세 페이지 요구사항을 충족하기 위해 Appbar 컴포넌트에 hasBackground 속성이 추가되었습니다.
이 변경으로 배경 스타일이 적절히 적용될 수 있습니다.


80-97:

❓ Verification inconclusive

스크롤 마진 값에 대한 더 유연한 접근이 필요합니다.

현재 하드코딩된 '100px' 값은 화면 크기에 따라 문제가 될 수 있습니다.
주석에도 언급되어 있듯이 임시 조정값이 포함되어 있습니다.

- const tabsScrollMargin = '100px';
+ // 헤더 높이와 추가 여백을 계산하는 방식으로 개선
+ const headerHeight = 64; // 헤더 실제 높이
+ const extraPadding = 36; // 추가 여백
+ const tabsScrollMargin = `${headerHeight + extraPadding}px`;

혹은 CSS 변수나 Tailwind 테마를 활용하여 일관된 스크롤 마진을 관리하는 것이 좋을 것 같습니다.


스크롤 마진 값 개선 요청

현재 스크롤 마진이 하드코딩된 "100px"로 설정되어 있어, 다양한 화면 크기에서 의도한 동작을 보장하기 어려울 수 있습니다. 제안드린 대로 헤더 높이(64px)와 추가 여백(36px)을 별도로 계산하여 적용하는 방식으로 수정하면, 코드의 가독성과 유지보수성이 향상될 것입니다.

  • 아래와 같이 수정한 후, 모든 화면 크기에서 스크롤 동작이 정상 동작하는지 추가 검증 부탁드립니다.
- const tabsScrollMargin = '100px';
+ // 헤더 높이와 추가 여백을 고려한 스크롤 마진 계산
+ const headerHeight = 64; // 헤더 실제 높이
+ const extraPadding = 36; // 추가 여백
+ const tabsScrollMargin = `${headerHeight + extraPadding}px`;
  • 또는 CSS 변수나 Tailwind 테마를 활용하여 스크롤 마진 값을 중앙집중적으로 관리하는 방법도 고려해 주세요.
src/features/card-detail/types/cardDetail.ts (3)

1-7: API 응답 타입이 적절하게 정의되어 있습니다.

API 응답에 대한 제네릭 타입이 잘 구성되어 있어 다양한 데이터 타입에 재사용할 수 있습니다.


47-48: 응답 타입 조합이 적절히 되어있습니다.

ApiResponse 제네릭 타입을 활용하여 CardDetailDto 타입을 정의한 것이 좋습니다. 이로써 API 응답 구조의 일관성을 유지할 수 있습니다.


31-45:

❓ Verification inconclusive

CardDetailResponse 타입의 구조가 잘 설계되어 있습니다.

명함 상세 정보에 필요한 모든 필드가 잘 정의되어 있고, 선택적 필드에는 물음표와 undefined를 함께 사용하여 명확하게 표시했습니다.

그러나 hobby와 news가 문자열로 정의된 반면, 다른 필드들은 배열로 정의되어 있어 일관성이 부족합니다. 데이터 구조를 더 일관되게 만드는 것을 고려해보세요.


🌐 Web query:

프론트엔드 애플리케이션에서 데이터 구조 일관성의 중요성

💡 Result:

프론트엔드 애플리케이션에서 데이터 구조 일관성은 시스템의 유지보수성, 확장성, 사용자 경험에 직접적인 영향을 미치는 핵심 요소입니다. 특히 복잡한 상태 관리와 다양한 데이터 소스의 통합이 요구되는 현대 웹 애플리케이션에서 이는 다음과 같은 측면에서 중요합니다.

시스템 유지보수성 향상

  • 의존성 관리: 일관된 데이터 구조는 컴포넌트 간 의존성을 최소화하며, 모듈 간 결합도를 낮춰 변경 사항이 시스템 전체에 미치는 영향을 제한합니다[9][10]
  • 코드 예측성 강화: 통일된 데이터 포맷(예: JSON Schema)을 사용하면 API 응답 처리 로직을 표준화할 수 있어 신규 개발자도 빠르게 코드베이스 이해 가능[11][2]
  • 디버깅 효율화: 데이터 변환 계층(repository layer)에서 일관성 유지 시 오류 발생 지점을 신속하게 특정 가능[3][6]

성능 최적화

  • 불필요한 재렌더링 방지: React 등의 프레임워크에서 상태 객체의 깊은 비교 대신 참조 비교만으로 변경 감지 가능[10]
  • 캐싱 메커니즘 활용: 정규화된 데이터 구조를 적용하면 Apollo Client나 React Query 같은 라이브러리의 캐시 관리 기능을 최대화[5][8]
// 정규화된 데이터 구조 예시
const normalizedData = {
  users: {
    '1': { id: 1, name: 'John', posts: [101] },
    '2': { id: 2, name: 'Jane', posts: [102] }
  },
  posts: {
    '101': { id: 101, content: '...' },
    '102': { id: 102, content: '...' }
  }
}

협업 효율성 증대

  • 팀 간 커뮤니케이션 표준화: 프론트엔드-백엔드 팀이 공유하는 Swagger/OpenAPI 스펙과의 구조 일치성 유지[7][8]
  • 컴포넌트 재사용성 향상: UI 컴포넌트가 기대하는 데이터 형식에 대한 명확한 계약 존재 시 독립적 개발 및 테스트 가능[2][11]

에러 방지 메커니즘

  • 런타임 유효성 검증: Zod나 Yup을 이용한 데이터 구조 검증 계층 구현 시 잘못된 데이터 입력 차단[3]
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string().min(2),
  email: z.string().email()
});

확장성 보장

  • 다층 아키텍처 지원: Clean Architecture에서 도메인 계층의 순수 데이터 모델 유지는 기술 스택 변경 시 최소 영향도 보장[3][11]
  • 마이크로 프론트엔드 호환: 독립적 배포가 가능한 서비스 간 데이터 교환 표준화[10][8]

사용자 경험 개선

  • 데이터 동기화 보장: Optimistic UI 구현 시 일관된 구조가 로컬 상태와 서버 상태의 충돌 해결 용이[5]
  • 로딩 상태 관리: Skeleton UI 표시를 위한 데이터 형식 사전 정의 가능[6][7]

프론트엔드에서 데이터 구조 일관성을 달성하기 위한 실제 구현 전략으로는 TypeScript 인터페이스 표준화[10], API 응답 변환 계층 도입[11], 상태 관리 라이브러리 선택 시 직렬화 가능성 고려[9] 등이 있습니다. 이는 단순히 기술적 완성도를 넘어 비즈니스 요구사항 변화에 신속히 대응할 수 있는 애자일한 시스템 구축의 토대가 됩니다[4][8].

Citations:


검증 결과, 타입 구조는 전반적으로 명확하게 정의되어 있습니다만, newshobby가 문자열로 지정된 점은 다른 선택적 필드(배열 타입)와의 일관성 측면에서 재검토가 필요해 보입니다.

  • 파일: src/features/card-detail/types/cardDetail.ts (Lines 31–45)
  • 제안:
    • 만약 해당 필드들이 여러 값을 다뤄야 한다면, 타입을 string[] 등 배열 형태로 변경하여 다른 필드와의 데이터 구조 일관성을 맞추는 방안을 고려해 주세요.
    • 단, 해당 필드들이 실제로 단일 값을 목적으로 한다면 현재 구현도 문제가 없으므로, 데이터 처리 로직과 UI 컴포넌트 간의 계약이 명확하게 일치하는지 확인해 주시기 바랍니다.

프론트엔드 애플리케이션에서는 데이터 구조 일관성이 유지보수성과 효율적인 디버깅에 큰 도움이 되므로, 이 부분에 대해 검토 후 필요 시 수정해주시길 권장합니다.

Copy link
Collaborator

@youngju6143 youngju6143 left a comment

Choose a reason for hiding this comment

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

수고 많으셨어요!!! 👍

<div className="sticky top-0 z-10 w-full">
{/* Appbar에 트랜지션 효과 추가 */}
<div
className={`transition-all duration-300 ease-in-out ${showAppbar ? 'max-h-16 opacity-100' : 'max-h-0 overflow-hidden opacity-0'}`}
Copy link
Collaborator

Choose a reason for hiding this comment

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

조건문이 있는 className은 cn 함수를 사용해서 구현하면 좋을 것 같아요!

className={cn('transition-all duration-300 ease-in-out', 
showAppbar ? 'max-h-16 opacity-100' : 'max-h-0 overflow-hidden opacity-0')}

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

@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 (3)
src/features/card-detail/ui/cardDetailHeader.tsx (1)

14-17: 기본 직군(DEVELOPER) 처리에 대한 유연성 제고

현재 userJob을 계산할 때 'DEVELOPER' 문자열이 기본값으로 설정되어 있습니다. 예기치 않은 직군 타입이 들어오는 경우를 대비하여, 별도 에러 처리를 하거나 다른 직군 타입에 대한 기본 동작을 정의해두면 더욱 안전한 코드가 될 수 있습니다.

src/features/card-detail/ui/cardTabs.tsx (2)

30-37: 교차점(threshold) 동작 범위 재검토

useScrollPosition 사용 시 threshold: 0 설정으로, 요소의 상단이 뷰포트에 일부만 보여도 감지됩니다. 의도대로 정확히 상단 교차 지점을 포착하고 싶은 경우, 임계값을 더 세밀하게 조정하거나 rootMargin을 상황에 맞추어 수정하는 방안을 고려하세요.


81-98: 탭 전환 로직에 대한 스크롤 위치 계산 보완

handleTabChange 내에서 scrollMarginTop을 하드코딩해 100px으로 지정했는데, 헤더의 높이가 달라질 경우 유지보수 측면에서 버그 발생 가능성이 있습니다. 헤더 노드의 실제 높이를 구해 동적으로 margin 값을 설정하면 더욱 유연하게 탭 전환을 처리할 수 있습니다.

  const handleTabChange = (tabId: TabId) => {
    setActiveTab(tabId);

-   const tabsScrollMargin = '100px';
+   const headerElement = document.querySelector('.card-detail-header');
+   const headerHeight = headerElement?.clientHeight ?? 64;
+   const extraMargin = 36; // 여유 영역
+   const tabsScrollMargin = `${headerHeight + extraMargin}px`;

    if (sectionRefs[tabId]?.current) {
      sectionRefs[tabId].current.style.scrollMarginTop = tabsScrollMargin;
      sectionRefs[tabId].current.scrollIntoView({
        behavior: 'smooth',
        block: 'start',
      });
    }
  };
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c980fe4 and 82c2494.

📒 Files selected for processing (4)
  • src/app/(detail)/card-detail/[cardId]/page.tsx (1 hunks)
  • src/features/card-detail/hooks/query/useCardDetailQuery.ts (1 hunks)
  • src/features/card-detail/ui/cardDetailHeader.tsx (1 hunks)
  • src/features/card-detail/ui/cardTabs.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/card-detail/hooks/query/useCardDetailQuery.ts
🔇 Additional comments (2)
src/app/(detail)/card-detail/[cardId]/page.tsx (1)

4-13: Page 컴포넌트의 에러 처리 및 상태 관리 확인 필요

페이지 전체가 CardDetailHeader 및 CardTabs 컴포넌트에 의존하여 데이터를 표시하고 있습니다. 만약 cardId가 유효하지 않거나, 쿼리로부터 데이터를 불러오는 동안 에러가 발생할 경우를 대비해 에러 처리나 로딩 상태를 추가로 고려해보면 좋겠습니다.

src/features/card-detail/ui/cardDetailHeader.tsx (1)

44-61: data?.data 사용 시 null 체크 로직 보강 제안

사용자 정보(nickname, organization, summary)를 렌더링할 때 data?.data를 통해 안전하게 접근 중이지만, 실제 API 호출 결과가 null 또는 빈 객체일 경우 UI가 정상적으로 표시되지 않을 수 있습니다. 로딩 상태나 에러 상태를 처리할 예외 로직을 추가로 고려해 보시면 좋겠습니다.

Copy link
Collaborator

@jangwonyoon jangwonyoon left a comment

Choose a reason for hiding this comment

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

@JaeIn1 몇가지 코멘트 남겼습니다. 정말 고생많으셨어요~~ 고생의 흔적이 보이네요.

Comment on lines +19 to +25
// 각 섹션의 가시성에 따라 활성 탭 설정
if (viewState.domains) setActiveTab('domains');
else if (viewState.sns) setActiveTab('sns');
else if (viewState.news) setActiveTab('news');
else if (viewState.hobby) setActiveTab('hobby');
else if (viewState.posts) setActiveTab('posts');
else if (viewState.projects) setActiveTab('projects');
Copy link
Collaborator

Choose a reason for hiding this comment

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

@JaeIn1 지금도 충분히 가독성이 좋다고 생각하지만 다른 관점도 제시해주기 위해 레퍼런스 하나 놓고 갑니다~~

ts에서 조건문 같은경우 ts-pattern이라는 강력한 도구가 있습니다. 또한 룩업 테이블라는 참조 연산할때 좋은 개념이 있는데 지금과는 조금 벗어날수도 있지만 한번 살펴 보시면 좋을듯합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jangwonyoon 오 처음 알았네요. 참고해서 한 번 학습해보겠습니다!!

Comment on lines +2 to +7
export type ApiResponse<T> = {
status: string;
message: string;
timestamp: string;
data: T;
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

@JaeIn1 제네릭으로 처리한 부분 좋습니다~

Comment on lines 59 to 79
const sectionRefs = {
domains: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
sns: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
news: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
hobby: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
posts: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
projects: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
};

// 기존 ref와 intersection observer ref를 결합하는 함수
const combineRefs = (section: TabId) => (el: HTMLDivElement) => {
sectionRefs[section].current = el;

// 각 섹션에 맞는 ref 설정
if (section === 'domains') domainsRef(el);
else if (section === 'sns') snsRef(el);
else if (section === 'news') newsRef(el);
else if (section === 'hobby') hobbyRef(el);
else if (section === 'posts') postsRef(el);
else if (section === 'projects') projectsRef(el);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

결과값을 참조하는 부분도 룩업 테이블을 통해 빠른 참조와 가독성있게 나타낼수 있어보입니다. 물론 지금도 충분합니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jangwonyoon 룩업 테이블 학습해보겠습니다!!

Copy link
Member

Choose a reason for hiding this comment

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

wow 고생하셨네요.. 짱짱
탭 이동 시에 anchor tag + hash change event 사용해도 좋을 것 같아요.
요런 방법도 있구나 정도!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Jungjjeong 오 생각해보니까 그 방법도 있었네요 다시 한 번 봐보겠습니다!!


function Posts({ data }: PostsProps) {
return (
<div className={`${spacingStyles({ marginTop: 'ms' })}`}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

마진이 들어가는곳에 명시적으로 spacingStyles 스타일 써주신 부분 좋습니다. 감사해요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jangwonyoon 👍👍

Copy link
Member

@Jungjjeong Jungjjeong left a comment

Choose a reason for hiding this comment

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

👍

export interface TabItem {
id: TabId;
label: string;
}
Copy link
Member

Choose a reason for hiding this comment

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

다른 컴포넌트에서 동일한 타입이 다시 선언된 것 같아요.
export의 의도가 다른 곳에서도 사용하는 것이라면, 재사용해도 될 것 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Jungjjeong 확인했습니다.. 반영해서 다시 올리겠습니다!!

</div>
</div>
</div>
</>
Copy link
Member

Choose a reason for hiding this comment

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

불필요한 fragment가 들어갔어요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Jungjjeong 엇 그러네요.. 수정하겠습니다!!

Comment on lines 59 to 79
const sectionRefs = {
domains: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
sns: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
news: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
hobby: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
posts: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
projects: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>,
};

// 기존 ref와 intersection observer ref를 결합하는 함수
const combineRefs = (section: TabId) => (el: HTMLDivElement) => {
sectionRefs[section].current = el;

// 각 섹션에 맞는 ref 설정
if (section === 'domains') domainsRef(el);
else if (section === 'sns') snsRef(el);
else if (section === 'news') newsRef(el);
else if (section === 'hobby') hobbyRef(el);
else if (section === 'posts') postsRef(el);
else if (section === 'projects') projectsRef(el);
};
Copy link
Member

Choose a reason for hiding this comment

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

wow 고생하셨네요.. 짱짱
탭 이동 시에 anchor tag + hash change event 사용해도 좋을 것 같아요.
요런 방법도 있구나 정도!

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 (4)
src/features/card-detail/ui/underlineTabs.tsx (2)

9-9: activeTab 타입을 TabId로 통일해주세요.
TabId 타입을 이미 import했음에도 activeTabstring으로 선언되어 있어 타입이 불일치합니다. onChangeTabId를 사용하는 만큼 activeTabTabId 타입으로 맞추면 오류를 줄이고 가독성이 향상됩니다.

-  activeTab: string;
+  activeTab: TabId;

14-37: 접근성(ARIA 속성) 관련 개선을 고려해주세요.
각 탭에 role="tab"를 부여하고, 탭들을 감싼 컨테이너에 role="tablist"를 지정하면 화면 읽기 도구 등에서 탭 구조를 더욱 명확히 인식할 수 있습니다. 또한, 선택된 탭에는 aria-selected="true"를, 나머지 탭에는 aria-selected="false"와 같은 속성을 설정해 접근성을 높일 수 있습니다.

 <div
+  role="tablist"
   className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
   style={{
     scrollbarWidth: 'none',
     msOverflowStyle: 'none',
   }}
 >
   {tabs.map((tab) => (
     <button
+      role="tab"
+      aria-selected={activeTab === tab.id}
       key={tab.id}
       onClick={() => onChange(tab.id)}
       className={cn(
         `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
         activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
       )}
     >
       <Typography variant="body-3">{tab.label}</Typography>
     </button>
   ))}
 </div>
src/features/card-detail/ui/cardTabs.tsx (1)

90-90: tabsScrollMargin 값에 대한 하드코딩을 최소화해주세요.
'100px'은 하드코딩된 스타일 값이므로, 추후 레이아웃이나 헤더 높이가 변경되면 유지보수가 어려울 수 있습니다. 상수나 테마 파일에서 관리해 가독성과 확장성을 높이는 방법을 고려해 보세요.

-    const tabsScrollMargin = '100px';
+    // 테마 상수 혹은 별도 변수로 재관리
+    const TABS_SCROLL_MARGIN = '100px';
     ...
     sectionRefs[tabId].current.style.scrollMarginTop = TABS_SCROLL_MARGIN;
src/features/card-detail/ui/cardDetailHeader.tsx (1)

35-39: Avatar 아이콘에 맞는 alt 텍스트 사용을 권장드립니다.
현재 alt가 "Settings"로 되어 있는데, 이 아이콘은 사용자 프로필을 위한 이미지이므로 "User Avatar" 등의 표현이 더욱 적합해 보입니다. 시각장애인 등을 고려한 적절한 대체 텍스트를 제공해주세요.

-  <Image src="/icons/avatarIcon.svg" alt="Settings" width="28" height="28" className="rounded-full" />
+  <Image src="/icons/avatarIcon.svg" alt="User Avatar" width="28" height="28" className="rounded-full" />
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 82c2494 and 16f076e.

📒 Files selected for processing (3)
  • src/features/card-detail/ui/cardDetailHeader.tsx (1 hunks)
  • src/features/card-detail/ui/cardTabs.tsx (1 hunks)
  • src/features/card-detail/ui/underlineTabs.tsx (1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
src/features/card-detail/ui/cardDetailHeader.tsx (1)
src/features/card-detail/hooks/query/useCardDetailQuery.ts (1) (1)
  • useCardDetailQuery (17:24)
🔇 Additional comments (4)
src/features/card-detail/ui/cardTabs.tsx (2)

28-28: 에러 처리 및 로딩 상태 처리가 필요합니다.
현재 useCardDetailQuery로 데이터를 불러올 때 로딩 및 에러 상황에 대한 처리가 보이지 않습니다. 데이터 요청이 실패하거나 늦어질 경우 UI가 깨질 수 있으니, 에러 상태 및 로딩 상태를 처리하는 로직을 추가하는 것을 권장드립니다.


38-43: threshold: 1 설정 의도 확인 요청
현재 각 useInView 훅이 threshold: 1로 되어 있어 요소가 뷰포트에 100% 보여야만 inViewtrue로 바뀝니다. 섹션이 크게 길어질 경우 조금만 스크롤해도 탭이 활성화되어야 하는 상황에서는 동작이 달라질 수 있으니, 의도한 동작인지 다시 한번 확인해 주세요.

src/features/card-detail/ui/cardDetailHeader.tsx (2)

14-14: 로딩 상태와 에러 처리를 추가해주세요.
useCardDetailQuery 호출 시 에러 발생 또는 로딩 중 상태가 고려되지 않은 것으로 보입니다. 데이터가 도착하기 전이나 에러가 발생했을 때, 상단에 오류 안내나 로딩 스피너 등을 표시하는 로직을 추가하면 사용자 경험을 향상할 수 있습니다.


16-16: job 값이 없는 경우 'DEVELOPER'로 설정되는 부분을 재확인해주세요.
백엔드에서 job이 빈 문자열 혹은 null로 내려올 경우 자동으로 'DEVELOPER'를 사용합니다. 의도된 기본값인지 다시 한번 검토 부탁드립니다. 실제로는 다른 직군인데 데이터가 비어 있을 때 잘못된 정보가 노출될 여지가 있습니다.

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: 1

🧹 Nitpick comments (3)
src/features/card-detail/ui/underlineTabs.tsx (3)

7-12: 인터페이스 타입 정의가 잘 되어 있습니다.

인터페이스 정의가 명확하게 되어 있으나, 한 가지 개선 사항이 있습니다. activeTabstring 타입으로 정의되어 있는데, 이를 더 타입 안전하게 TabId 타입으로 지정하는 것이 좋겠습니다.

interface UnderlineTabsProps {
  tabs: TabItem[];
-  activeTab: string;
+  activeTab: TabId;
  onChange: (tabId: TabId) => void;
  className?: string;
}

14-37: 스크롤 가능한 탭 구현이 잘 되어 있습니다.

컴포넌트 구현이 전반적으로 잘 되어 있지만, 몇 가지 개선할 점이 있습니다:

  1. 모든 탭 버튼에 mr-2 클래스가 적용되어 있는데, 마지막 탭에는 불필요한 여백이 생길 수 있습니다.
  2. 스크롤바는 숨겨져 있지만, 사용자에게 스크롤 가능하다는 표시가 없습니다.
  3. transition-all은 모든 CSS 속성에 대해 트랜지션을 적용하므로 성능에 영향을 줄 수 있습니다.

아래는 개선된 구현 예시입니다:

export const UnderlineTabs = ({ tabs, activeTab, onChange, className }: UnderlineTabsProps) => {
  return (
    <div
      className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
      style={{
        scrollbarWidth: 'none',
        msOverflowStyle: 'none',
      }}
    >
      {tabs.map((tab, index) => (
        <button
          key={tab.id}
          onClick={() => onChange(tab.id)}
          className={cn(
-           `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
+           `flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
+           index < tabs.length - 1 && 'mr-2',
            activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
          )}
        >
          <Typography variant="body-3">{tab.label}</Typography>
        </button>
      ))}
    </div>
  );
};

16-22: 스크롤 기능 개선 필요

현재 구현은 스크롤바를 숨기고 있지만, 사용자에게 가로 스크롤 가능하다는 시각적 힌트가 없습니다. 또한 웹 접근성 측면에서도 개선이 필요합니다.

<div
  className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
  style={{
    scrollbarWidth: 'none',
    msOverflowStyle: 'none',
  }}
+ role="tablist"
+ aria-orientation="horizontal"
>

또한 스크롤 인디케이터나 그라데이션 효과를 추가하는 것을 고려해보세요.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 16f076e and 58d0c74.

📒 Files selected for processing (2)
  • src/features/card-detail/config/tabs-config.ts (1 hunks)
  • src/features/card-detail/ui/underlineTabs.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/card-detail/config/tabs-config.ts
🧰 Additional context used
🧬 Code Definitions (1)
src/features/card-detail/ui/underlineTabs.tsx (1)
src/features/card-detail/config/tabs-config.ts (2) (2)
  • TabItem (2:5)
  • TabId (1:1)

Comment on lines 23 to 34
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onChange(tab.id)}
className={cn(
`mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
)}
>
<Typography variant="body-3">{tab.label}</Typography>
</button>
))}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

탭 버튼의 접근성 개선 필요

현재 탭 버튼에는 적절한 ARIA 속성이 누락되어 있습니다. 접근성을 개선하기 위해 WAI-ARIA 규칙을 따른 속성을 추가하는 것이 좋습니다.

{tabs.map((tab) => (
  <button
    key={tab.id}
    onClick={() => onChange(tab.id)}
+   role="tab"
+   aria-selected={activeTab === tab.id}
+   aria-controls={`panel-${tab.id}`}
+   id={`tab-${tab.id}`}
    className={cn(
      `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
      activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
    )}
  >
    <Typography variant="body-3">{tab.label}</Typography>
  </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.

Suggested change
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onChange(tab.id)}
className={cn(
`mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
)}
>
<Typography variant="body-3">{tab.label}</Typography>
</button>
))}
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onChange(tab.id)}
role="tab"
aria-selected={activeTab === tab.id}
aria-controls={`panel-${tab.id}`}
id={`tab-${tab.id}`}
className={cn(
`mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
)}
>
<Typography variant="body-3">{tab.label}</Typography>
</button>
))}

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: 1

🧹 Nitpick comments (3)
src/features/card-detail/ui/underlineTabs.tsx (3)

7-12: 타입 일관성 개선 필요

activeTab 프로퍼티가 string 타입으로 정의되어 있지만, 실제로는 TabId 타입과 비교하고 있습니다. 더 타입 안전한 코드를 위해 activeTabTabId 타입으로 정의하는 것이 좋습니다.

interface UnderlineTabsProps {
  tabs: TabItem[];
-  activeTab: string;
+  activeTab: TabId;
  onChange: (tabId: TabId) => void;
  className?: string;
}

18-21: 인라인 스타일 대신 CSS 클래스 사용 권장

인라인 스타일 대신 CSS 클래스를 사용하는 것이 코드 유지보수성과 일관성을 높이는 데 도움이 됩니다. 특히 스크롤바 숨김과 같은 스타일링은 재사용 가능한 클래스로 분리하는 것이 좋습니다.

<div
-  className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
-  style={{
-    scrollbarWidth: 'none',
-    msOverflowStyle: 'none',
-  }}
+  className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black hide-scrollbar', className)}
>

그리고 CSS 파일에 다음과 같은 클래스를 추가하세요:

.hide-scrollbar {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.hide-scrollbar::-webkit-scrollbar {
  display: none;
}

1-39: 키보드 탐색 기능 추가 필요

현재 구현에는 키보드 탐색 기능이 누락되어 있습니다. 접근성 표준에 따르면 사용자가 키보드로 탭 사이를 이동할 수 있어야 합니다(보통 방향키 사용).

키보드 탐색을 지원하기 위해 키보드 이벤트 핸들러를 추가하세요:

export const UnderlineTabs = ({ tabs, activeTab, onChange, className }: UnderlineTabsProps) => {
+  const handleKeyDown = (e: React.KeyboardEvent, tabId: TabId, index: number) => {
+    if (e.key === 'ArrowRight') {
+      e.preventDefault();
+      const nextIndex = (index + 1) % tabs.length;
+      onChange(tabs[nextIndex].id);
+      document.getElementById(`tab-${tabs[nextIndex].id}`)?.focus();
+    } else if (e.key === 'ArrowLeft') {
+      e.preventDefault();
+      const prevIndex = (index - 1 + tabs.length) % tabs.length;
+      onChange(tabs[prevIndex].id);
+      document.getElementById(`tab-${tabs[prevIndex].id}`)?.focus();
+    }
+  };

  return (
    <div
      role="tablist"
      aria-orientation="horizontal"
      className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
      style={{
        scrollbarWidth: 'none',
        msOverflowStyle: 'none',
      }}
    >
-      {tabs.map((tab) => (
+      {tabs.map((tab, index) => (
        <button
          role="tab"
          key={tab.id}
          onClick={() => onChange(tab.id)}
+         onKeyDown={(e) => handleKeyDown(e, tab.id, index)}
+         tabIndex={activeTab === tab.id ? 0 : -1}
          className={cn(
            `mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
            activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
          )}
        >
          <Typography variant="body-3">{tab.label}</Typography>
        </button>
      ))}
    </div>
  );
};
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 58d0c74 and 8528e09.

📒 Files selected for processing (1)
  • src/features/card-detail/ui/underlineTabs.tsx (1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
src/features/card-detail/ui/underlineTabs.tsx (1)
src/features/card-detail/config/tabs-config.ts (2) (2)
  • TabItem (2:5)
  • TabId (1:1)
🔇 Additional comments (1)
src/features/card-detail/ui/underlineTabs.tsx (1)

23-35: 탭 버튼의 접근성 개선 필요

현재 탭 버튼에는 적절한 ARIA 속성이 일부 누락되어 있습니다. 접근성을 개선하기 위해 WAI-ARIA 규칙을 따른 속성을 추가하는 것이 좋습니다.

{tabs.map((tab) => (
  <button
    role="tab"
    key={tab.id}
    onClick={() => onChange(tab.id)}
+   aria-selected={activeTab === tab.id}
+   aria-controls={`panel-${tab.id}`}
+   id={`tab-${tab.id}`}
    className={cn(
      `mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
      activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
    )}
  >
    <Typography variant="body-3">{tab.label}</Typography>
  </button>
))}

Comment on lines +16 to +36
<div
className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
{tabs.map((tab) => (
<button
role="tab"
key={tab.id}
onClick={() => onChange(tab.id)}
className={cn(
`mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
)}
>
<Typography variant="body-3">{tab.label}</Typography>
</button>
))}
</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

탭 컨테이너에 대한 접근성 마크업 추가 필요

탭 인터페이스의 접근성을 개선하기 위해 컨테이너 요소에 적절한 역할(role)과 ARIA 속성을 추가해야 합니다. 또한 탭 패널도 구현되어야 완전한 탭 인터페이스가 됩니다.

<div
+  role="tablist"
+  aria-orientation="horizontal"
  className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
  style={{
    scrollbarWidth: 'none',
    msOverflowStyle: 'none',
  }}
>
  {tabs.map((tab) => (
    <button
      role="tab"
      key={tab.id}
      onClick={() => onChange(tab.id)}
      className={cn(
        `mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
        activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
      )}
    >
      <Typography variant="body-3">{tab.label}</Typography>
    </button>
  ))}
</div>

추가로, 이 컴포넌트와 함께 사용될 탭 패널 컴포넌트도 필요합니다. 예를 들면:

// 탭 패널 컴포넌트 예시
{tabs.map((tab) => (
  <div
    key={tab.id}
    role="tabpanel"
    id={`panel-${tab.id}`}
    aria-labelledby={`tab-${tab.id}`}
    hidden={activeTab !== tab.id}
  >
    {/* 탭 내용 */}
  </div>
))}

@JaeIn1 JaeIn1 merged commit 3349761 into develop Mar 18, 2025
2 checks passed
@JaeIn1 JaeIn1 deleted the feat/#56-card-detail-design branch March 18, 2025 13:43
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.

명함 상세보기 디자인 및 구현
4 participants