Skip to content

Conversation

@clyde-yoonjae
Copy link
Contributor

@clyde-yoonjae clyde-yoonjae commented Feb 25, 2025

📝 주요 작업 내용

모임리스트페이지, 마이페이지, 모임생성페이지 에서 사용되는 공통컴포넌트 작업입니다.
해당 컴포넌트를 생성하기 위한 세부 컴포넌트 와 통합컴포넌트 같이 작업하였습니다.

📺 스크린샷

기본 디자인
image
5개 전부 선택되었을 때 - 아이콘 버튼 hover시, title로 최대5개 까지 선택 문구 보입니다.
image

모바일 반응형 - 선택리스트 아키노만 보여지고 카테고리 수정하였습니다.
image

🔗 참고 사항

현재 마크업 진행하시는 팀원들 빠르게 사용위해서 dev로 병합 pr 올렸습니다.

💬 리뷰 요구사항

타입을 제외한 커밋 순 대로 봐주시면 감사합니다.

📃 관련 이슈

DEVING-49

Summary by CodeRabbit

  • 신규 기능
    • 다양한 기술 선택 및 관리 기능이 활성화된 새로운 페이지가 추가되었습니다.
    • 카테고리별 필터링, 선택 초기화, 최대 5개 선택 제한 등 사용자 인터페이스가 향상되었습니다.
    • 일부 기술 아이콘의 색상이 업데이트되어 시각적 일관성과 가독성이 개선되었습니다.

@vercel
Copy link

vercel bot commented Feb 25, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
moim-fe ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 25, 2025 0:33am
moim-fe-1kc6 ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 25, 2025 0:33am

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2025

Walkthrough

이번 PR은 기술 스택 미리보기 페이지와 관련된 다양한 React 컴포넌트 및 유틸리티 함수들을 새로 도입하고 수정하는 작업입니다. 주요 변경 사항으로는 기술 선택 기능 구현을 위한 Page, TechSelector, 여러 보조 컴포넌트들(예: CategoryTabs, TechButton 등), 커스텀 훅 useTechSelection의 추가와 관련 타입 및 유틸리티 함수의 도입, 그리고 특정 아이콘의 색상 조정이 포함됩니다.

Changes

파일 경로 변경 요약
src/app/preview/.../page.tsx 새로운 Page 컴포넌트 추가: useState를 이용해 선택한 기술(문자열 배열)을 관리하며 TechSelector 컴포넌트를 렌더링.
src/components/ui/Icon/IconData.ts 'Next', 'Express', 'Django' 아이콘의 색상 값 수정: 기존 색상을 흰색 혹은 변경된 녹색으로 업데이트.
src/components/ui/tech-stack/TechSelector.tsx 새로운 TechSelector 컴포넌트 추가: 최대 선택 개수(기본 5) 및 선택사항 변경 콜백을 지원하며 내부 상태와 useTechSelection 훅 활용.
src/components/ui/tech-stack/tech-stack-components/[CategoryTabs, SelectedTechButton, SelectedTechList, TabButton, TechButton, TechButtonList].tsx 기술 선택 UI를 지원하는 여러 컴포넌트 추가: 카테고리 탭 전환, 선택된 기술 표시, 버튼 기반 기술 선택 및 상호작용 관리 기능 구현.
src/hooks/useTechSelection.ts 기술 선택 관련 상태와 액션(선택, 초기화, 제거)을 관리하는 커스텀 훅 추가.
src/types/techStack.ts
src/util/getIconDetail.ts
기술 스택 관련 타입 선언 및 아이콘 관리 유틸리티 함수(getIconColor, getIconComponent, getIconsByCategory, isLightColor) 추가.

Sequence Diagram(s)

sequenceDiagram
    participant U as 사용자
    participant P as Page
    participant TS as TechSelector
    participant H as useTechSelection
    participant TB as TechButton

    U->>P: 페이지 요청
    P->>TS: TechSelector 렌더링
    TS->>H: 초기 선택 상태 설정
    U->>TB: 기술 버튼 클릭
    TB->>H: 선택/해제 처리 요청
    H->>TS: 상태 업데이트 후 콜백 실행
    TS->>P: 선택 업데이트 알림
Loading

Possibly related PRs

  • Feat/component/tech stack/DEVING-41 #22: Page 컴포넌트와 TechSelector 도입을 통한 기술 선택 컴포넌트 구현과 관련된 변경으로, 두 PR 모두 기술 선택 기능 구현에 초점을 맞추고 있음.

Suggested reviewers

  • dbswl701
  • Lee-Dong-Seok

Poem

나는 귀여운 토끼, 코딩이 즐거워요,
새 컴포넌트들이 춤추듯 추가되어요.
버튼과 탭이 경쾌하게 노래하죠,
코드 속 논리의 리듬에 맞춰 뛰어요.
버그 없이 순수한 기능의 꽃,
오늘도 나는 행복하게 홉홉! 🐰
~ 토끼 드림 ~

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 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. (Beta)
  • @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.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

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.

@github-actions
Copy link

CI Status Report

검사 결과

  • Lint: ✅ success
  • Format: ✅ success
  • Type Check: ✅ success
  • Tests: ✅ success
  • Build: ✅ success

✅ 모든 검사가 통과되었습니다.

@clyde-yoonjae clyde-yoonjae changed the title Feat/component/tech stack2/DEVING-49 기술스택선택컴포넌트 Feat/component/tech stack2/DEVING-49 기술스택 선택 컴포넌트 Feb 25, 2025
@codecov
Copy link

codecov bot commented Feb 25, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

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

🧹 Nitpick comments (19)
src/components/ui/tech-stack/tech-stack-components/TechButton.tsx (3)

25-40: 버튼 접근성 향상이 필요합니다.

버튼 컴포넌트의 접근성을 높이기 위해 aria 속성을 추가하는 것이 좋습니다.

<button
  className={`flex items-center gap-1 rounded-full border px-2 py-1
text-xs transition-all hover:shadow-md lg:gap-2 lg:px-3 lg:py-1.5 lg:text-sm
${isClicked ? 'bg-white' : ''}
${isMaxReached ? 'cursor-not-allowed opacity-50' : ''}`}
  onClick={() => onClick(name)}
  disabled={isMaxReached}
  title={isMaxReached ? '최대 5개까지만 선택할 수 있습니다' : ''}
+ aria-pressed={isClicked}
+ aria-label={`${name} 기술 선택하기`}
>

27-29: transition-all보다 구체적인 속성 전환 지정이 필요합니다.

성능 최적화를 위해 transition-all 대신 실제로 전환되는 구체적인 속성만 지정하는 것이 좋습니다.

- text-xs transition-all hover:shadow-md lg:gap-2 lg:px-3 lg:py-1.5 lg:text-sm
+ text-xs transition-[background-color,opacity,box-shadow,color] duration-200 hover:shadow-md lg:gap-2 lg:px-3 lg:py-1.5 lg:text-sm

35-38: 인라인 스타일 대신 className을 사용하는 것이 좋습니다.

인라인 스타일 대신 Tailwind의 text-[color] 또는 동적 클래스를 사용하는 것이 일관성 있는 스타일링을 위해 좋습니다.

- <p style={{ color: iconColor }} className="font-medium">
+ <p className={`font-medium`} style={{ color: iconColor }}>

참고: 완전히 인라인 스타일을 제거하려면 동적 색상 적용을 위한 별도의 유틸리티 함수나 방식이 필요할 수 있습니다.

src/app/preview/tech-stack3/page.tsx (2)

19-25: 불필요한 div 래퍼를 제거하는 것이 좋습니다.

TechSelector 주변의 div 래퍼가 추가적인 스타일이나 기능을 제공하지 않는다면 제거할 수 있습니다.

return (
-  <div>
    <TechSelector
      maxSelections={5}
      onSelectionChange={handleSelectionChange}
    />
-  </div>
);

14-17: 코드 주석 스타일 개선이 필요합니다.

주석이 명확하지만, JSDoc 스타일을 사용하면 기능과 가능한 확장 부분을 더 명확하게 문서화할 수 있습니다.

-  // 여기서 필요한 상태 업데이트 또는 API 호출 등을 수행할 수 있습니다.
-  // 예: 선택된 기술 정보를 서버에 전송
+  /**
+   * TODO: 여기에 선택된 기술 정보를 처리하는 로직 추가
+   * - 상태 업데이트
+   * - API 호출 (예: 선택된 기술 정보를 서버에 전송)
+   */
src/components/ui/tech-stack/tech-stack-components/TabButton.tsx (2)

18-22: 하드코딩된 색상 값을 상수나 테마 변수로 분리하는 것이 좋습니다.

하드코딩된 색상 값(#C586C0)은 상수나 테마 변수로 분리하는 것이 일관성과 유지보수성을 높입니다.

- ? 'border-b-2 border-[#C586C0] text-[#C586C0]'
+ ? 'border-b-2 border-primary text-primary'

이 경우 Tailwind 설정에 primary 색상을 정의하거나, 컴포넌트에 색상 props를 추가하는 방법도 고려할 수 있습니다.


18-22: Tailwind 클래스를 더 체계적으로 구성하는 것이 좋습니다.

가독성을 높이기 위해 Tailwind 클래스를 레이아웃, 스타일링, 반응형 순으로 그룹화하는 것이 좋습니다.

- className={`px-2 py-2 font-medium sm:px-4 ${
+ className={`px-2 py-2 sm:px-4 font-medium ${

또는 @apply 지시문을 사용하여 클래스를 더 체계적으로 구성할 수도 있습니다.

src/types/techStack.ts (2)

4-7: JSDoc 형식으로 주석을 작성하는 것이 좋습니다.

주석을 JSDoc 형식으로 변경하면 더 나은 IDE 지원과 문서화가 가능합니다.

- // 클릭된 버튼 상태를 위한 타입
+ /**
+  * 클릭된 버튼의 상태를 추적하기 위한 타입
+  * 키는 버튼 이름, 값은 클릭 상태를 나타냄
+  */
export type ClickedButtonsState = {
  [key: string]: boolean;
};

9-14: IconComponent 타입에 더 많은 유연성 추가가 필요합니다.

IconComponent 타입이 잘 정의되어 있지만, 더 많은 유연성을 위해 추가 속성을 허용하는 것이 좋을 수 있습니다.

- export type IconComponent = React.ComponentType<{
+ export type IconComponent = React.ComponentType<{
  size?: number;
  color?: string;
  className?: string;
+ [key: string]: any; // 추가 props를 허용
}>;

참고: 타입 안전성을 유지하기 위해서는 구체적인 추가 속성을 정의하는 것이 더 좋을 수 있습니다.

src/components/ui/tech-stack/tech-stack-components/TechButtonList.tsx (1)

22-44: 스크롤바 스타일링에 접근성 개선이 필요합니다.

스크롤바 커스터마이징은 잘 되어 있지만, 몇 가지 개선할 점이 있습니다:

  1. 스크롤바 컬러 값을 Tailwind 색상 변수로 통일하면 좋겠습니다
  2. 스크롤바 커스터마이징에 대한 주석 설명이 있으면 유지보수가 쉬워질 것 같습니다
  3. 접근성을 위해 키보드 탐색이 가능하도록 역할(role) 속성을 추가하는 것이 좋겠습니다
-        style={{
-          scrollbarWidth: 'thin',
-          scrollbarColor: '#a0aec0 transparent',
-        }}
+        style={{
+          scrollbarWidth: 'thin',
+          scrollbarColor: 'var(--color-gray-500) transparent',
+        }}
+        role="list"
+        aria-label="기술 스택 목록"
src/components/ui/tech-stack/tech-stack-components/SelectedTechButton.tsx (1)

20-39: 삭제 버튼 스타일 및 접근성 개선이 필요합니다.

삭제 버튼 구현에 몇 가지 개선할 점이 있습니다:

  1. Tailwind 클래스 순서가 일관성이 없습니다 (hover:bg-gray-200 ml-1 - 마진이 먼저 와야 합니다)
  2. X 아이콘의 텍스트 색상이 흰색인데, 배경에 따라 대비가 충분하지 않을 수 있습니다
      <button
        onClick={() => onRemove(name)}
-        className="hover:bg-gray-200 ml-1 cursor-pointer rounded-full p-1"
+        className="ml-1 cursor-pointer rounded-full p-1 hover:bg-gray-200"
        aria-label={`${name} 선택 해제`}
      >
-        <X size={12} className="text-white" />
+        <X size={12} className="text-gray-600" />
      </button>
src/util/getIconDetail.ts (3)

14-18: getIconColor 함수의 오류 처리 개선이 필요합니다.

아이콘 색상을 가져오는 함수에서 아이콘을 찾지 못했을 때 기본 색상을 반환하는 것은 좋지만, 개발 및 디버깅을 위해 경고 메시지를 로깅하는 것이 좋을 것 같습니다.

export const getIconColor = (iconName: string): string => {
  const icon = ICON_LIST.find((icon) => icon.name === iconName);
+  if (!icon) {
+    console.warn(`아이콘을 찾을 수 없습니다: ${iconName}. 기본 색상을 사용합니다.`);
+  }
  return icon ? icon.color : '#000000';
};

31-64: getIconsByCategory 함수의 중복 코드 리팩토링이 필요합니다.

각 카테고리별로 비슷한 코드가 반복되고 있습니다. 코드 중복을 줄이고 유지보수성을 향상시키기 위해 리팩토링이 필요합니다.

export const getIconsByCategory = (
  activeCategory: CategoryType,
): IconWithComponent[] => {
+  // 모든 아이콘을 반환하는 경우
  if (activeCategory === 'all') {
    return ICON_LIST.map((icon) => ({
      ...icon,
      component: AllIcons[`${icon.name}Icon` as keyof typeof AllIcons],
    }));
  }
  
+  // 특정 카테고리의 아이콘만 필터링
+  const filteredIcons = ICON_LIST.filter((icon) => icon.category === activeCategory);
+  
+  // 카테고리에 따른 아이콘 레지스트리 선택
+  let iconRegistry;
+  switch(activeCategory) {
+    case 'frontend': iconRegistry = frontend; break;
+    case 'backend': iconRegistry = backend; break;
+    case 'design': iconRegistry = design; break;
+    default: return [];
+  }
+  
+  // 필터링된 아이콘에 컴포넌트 추가
+  return filteredIcons.map((icon) => ({
+    ...icon,
+    component: iconRegistry[`${icon.name}Icon` as keyof typeof iconRegistry],
+  }));
-  switch (activeCategory) {
-    case 'all':
-      return ICON_LIST.map((icon) => ({
-        ...icon,
-        component: AllIcons[`${icon.name}Icon` as keyof typeof AllIcons],
-      }));
-    case 'frontend':
-      return ICON_LIST.filter((icon) => icon.category === 'frontend').map(
-        (icon) => ({
-          ...icon,
-          component: frontend[`${icon.name}Icon` as keyof typeof frontend],
-        }),
-      );
-    case 'backend':
-      return ICON_LIST.filter((icon) => icon.category === 'backend').map(
-        (icon) => ({
-          ...icon,
-          component: backend[`${icon.name}Icon` as keyof typeof backend],
-        }),
-      );
-    case 'design':
-      return ICON_LIST.filter((icon) => icon.category === 'design').map(
-        (icon) => ({
-          ...icon,
-          component: design[`${icon.name}Icon` as keyof typeof design],
-        }),
-      );
-    default:
-      return [];
-  }
};

67-69: isLightColor 함수의 개선이 필요합니다.

현재 함수는 정확히 '#FFFFFF' 문자열만 확인하는데, 이는 다른 형식의 흰색(예: 'rgb(255,255,255)', '#fff')이나 매우 밝은 색상을 감지하지 못합니다. 더 강력한 검사 로직이 필요합니다.

export const isLightColor = (color: string): boolean => {
-  return color === '#FFFFFF';
+  // HEX 코드를 RGB로 변환하는 함수
+  const hexToRgb = (hex: string): number[] => {
+    // 축약형 변환 (#fff -> #ffffff)
+    const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+    const fullHex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
+    
+    // RGB 값 추출
+    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex);
+    return result 
+      ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
+      : [0, 0, 0];
+  };
+  
+  // 정확히 흰색인 경우
+  if (color.toUpperCase() === '#FFFFFF' || color.toLowerCase() === '#fff') {
+    return true;
+  }
+  
+  // RGB 형식인 경우 처리
+  if (color.startsWith('rgb')) {
+    const rgbMatch = color.match(/\d+/g);
+    if (rgbMatch && rgbMatch.length >= 3) {
+      const [r, g, b] = rgbMatch.map(Number);
+      // 매우 밝은 색상 검사 (예: 모든 채널이 240 이상)
+      return r >= 240 && g >= 240 && b >= 240;
+    }
+  }
+  
+  // HEX 형식인 경우 처리
+  if (color.startsWith('#')) {
+    const [r, g, b] = hexToRgb(color);
+    // 매우 밝은 색상 검사
+    return r >= 240 && g >= 240 && b >= 240;
+  }
+  
+  return false;
};
src/components/ui/tech-stack/tech-stack-components/CategoryTabs.tsx (2)

18-27: 카테고리 배열은 컴포넌트 외부로 이동하는 것이 좋습니다.

현재 categories 배열은 컴포넌트 내부에 정의되어 있어 렌더링마다 새로 생성됩니다. 이 배열을 컴포넌트 외부로 이동하거나 useMemo로 감싸면 불필요한 재생성을 방지할 수 있습니다.

-const CategoryTabs = ({
-  activeCategory,
-  onCategoryChange,
-  onReset,
-}: CategoryTabsProps): JSX.Element => {
-  const categories: Array<{
-    id: CategoryType;
-    label: string;
-    smallText: string;
-  }> = [
-    { id: 'all', label: '전체', smallText: 'All' },
-    { id: 'frontend', label: '프론트엔드', smallText: 'Front' },
-    { id: 'backend', label: '백엔드', smallText: 'Back' },
-    { id: 'design', label: '디자인', smallText: 'UI/UX' },
-  ];

+const categories: Array<{
+  id: CategoryType;
+  label: string;
+  smallText: string;
+}> = [
+  { id: 'all', label: '전체', smallText: 'All' },
+  { id: 'frontend', label: '프론트엔드', smallText: 'Front' },
+  { id: 'backend', label: '백엔드', smallText: 'Back' },
+  { id: 'design', label: '디자인', smallText: 'UI/UX' },
+];
+
+const CategoryTabs = ({
+  activeCategory,
+  onCategoryChange,
+  onReset,
+}: CategoryTabsProps): JSX.Element => {

32-41: onClick 핸들러에 useCallback 적용 고려

TabButton의 onClick 핸들러가 매 렌더링마다 새로 생성됩니다. 컴포넌트가 자주 리렌더링되는 경우 useCallback을 사용하여 성능을 최적화할 수 있습니다.

+import React, { useCallback } from 'react';

// ... 코드 생략 ...

+  const handleCategoryChange = useCallback(
+    (categoryId: CategoryType) => {
+      onCategoryChange(categoryId);
+    },
+    [onCategoryChange]
+  );

  return (
    <div className="flex items-center justify-between border-b">
      <div className="flex text-white">
        {categories.map((category) => (
          <TabButton
            key={category.id}
            active={activeCategory === category.id}
-           onClick={() => onCategoryChange(category.id)}
+           onClick={() => handleCategoryChange(category.id)}
            smallText={category.smallText}
          >
            {category.label}
          </TabButton>
        ))}
      </div>
src/components/ui/tech-stack/TechSelector.tsx (1)

38-40: 컴포넌트 스타일의 재사용성 고려

현재 배경색과 최소 높이를 컴포넌트 내부에서 하드코딩하고 있습니다. 이 컴포넌트가 여러 페이지에서 재사용된다면, 이러한 스타일 속성은 props로 받거나 외부에서 제공하는 것이 더 유연한 사용을 가능하게 할 수 있습니다.

-  return (
-    <div className="bg-gray-50 min-h-screen p-10">
-      <div className="mx-auto max-w-6xl">

+  return (
+    <div className="p-10">
+      <div className="mx-auto max-w-6xl">
src/hooks/useTechSelection.ts (2)

22-30: 선택된 아이콘 목록 계산 최적화

getSelectedIconNames 함수에서 filter와 map을 연속해서 사용하는 부분은 성능 최적화를 위해 reduce로 변경할 수 있습니다. 이는 배열을 한 번만 순회하도록 합니다.

-  // 선택된 아이콘 이름 목록 가져오기
-  const getSelectedIconNames = (): string[] => {
-    return Object.entries(clickedButtons)
-      .filter(([, isClicked]) => isClicked)
-      .map(([name]) => name);
-  };
-
-  const selectedNames = getSelectedIconNames();
-  const selectedCount = selectedNames.length;

+  // 선택된 아이콘 이름 목록 가져오기
+  const getSelectedIconNames = (): string[] => {
+    return Object.entries(clickedButtons).reduce<string[]>((acc, [name, isClicked]) => {
+      if (isClicked) acc.push(name);
+      return acc;
+    }, []);
+  };
+
+  const selectedNames = getSelectedIconNames();
+  const selectedCount = selectedNames.length;

50-54: ESLint 비활성화 주석 개선 가능

eslint-disable-next-line 주석을 사용하는 대신, 모든 종속성을 명시적으로 추가하는 것이 좋습니다. 이 경우, 빈 배열 대신 [selectedNames]를 종속성으로 추가할 수 있습니다.

-  // 컴포넌트 마운트 시 초기 선택 상태 설정
-  useEffect(() => {
-    prevSelectionRef.current = selectedNames;
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);

+  // 컴포넌트 마운트 시 초기 선택 상태 설정
+  useEffect(() => {
+    if (prevSelectionRef.current.length === 0) {
+      prevSelectionRef.current = selectedNames;
+    }
+  }, [selectedNames]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9264dd8 and d544e90.

📒 Files selected for processing (12)
  • src/app/preview/tech-stack3/page.tsx (1 hunks)
  • src/components/ui/Icon/IconData.ts (3 hunks)
  • src/components/ui/tech-stack/TechSelector.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/CategoryTabs.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/SelectedTechButton.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/SelectedTechList.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/TabButton.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/TechButton.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/TechButtonList.tsx (1 hunks)
  • src/hooks/useTechSelection.ts (1 hunks)
  • src/types/techStack.ts (1 hunks)
  • src/util/getIconDetail.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/components/ui/Icon/IconData.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: check
🔇 Additional comments (16)
src/components/ui/tech-stack/tech-stack-components/TechButton.tsx (1)

1-44: 반응형 디자인 접근 방식이 잘 구현되었습니다.

Tailwind의 lg: 접두사를 사용하여 다양한 화면 크기에 맞게 컴포넌트를 조정한 점이 좋습니다. 모바일 및 데스크톱 환경 모두에서 적절한 크기와 간격이 유지됩니다.

src/app/preview/tech-stack3/page.tsx (1)

6-18: 상태 관리 접근 방식이 명확하게 구현되었습니다.

상위 컴포넌트에서 상태를 관리하고 하위 컴포넌트에 props로 전달하는 방식이 잘 구현되었습니다. 이는 React의 단방향 데이터 흐름 원칙을 잘 따르고 있습니다.

src/components/ui/tech-stack/tech-stack-components/TabButton.tsx (1)

24-27: 반응형 디자인 접근 방식이 잘 구현되었습니다.

hiddensm:inline 클래스를 사용한 반응형 디자인 접근 방식이 좋습니다. 화면 크기에 따라 다른 텍스트를 표시하는 방법이 간결하게 구현되었습니다.

src/types/techStack.ts (2)

16-19: IconWithComponent 인터페이스가 잘 설계되었습니다.

기존 IconConfig를 확장하여 component 속성을 추가한 접근 방식이 명확하고 타입 안전성을 잘 유지하고 있습니다.


21-22: CategoryType의 유니온 타입 접근 방식이 효과적입니다.

기존 카테고리 타입에 'all' 옵션을 추가한 유니온 타입 접근 방식이 타입 안전성을 유지하면서도 필요한 유연성을 제공합니다.

src/components/ui/tech-stack/tech-stack-components/TechButtonList.tsx (1)

45-60: 기술 버튼 목록 렌더링이 잘 구현되어 있습니다.

기술 스택 아이콘을 맵핑하여 버튼 컴포넌트로 렌더링하는 구현이 깔끔하게 잘 되어 있습니다. isMaxReached 로직을 통해 최대 선택 개수를 효과적으로 제한하고 있습니다.

src/components/ui/tech-stack/tech-stack-components/SelectedTechButton.tsx (1)

5-18: 컴포넌트 구조와 반응형 디자인이 잘 구현되어 있습니다.

기술 스택 버튼 컴포넌트의 구조가 명확하고, 텍스트를 모바일에서는 숨기고 데스크톱에서만 표시하는 반응형 디자인이 잘 적용되어 있습니다. 또한 aria-label을 사용하여 접근성도 고려한 점이 좋습니다.

src/components/ui/tech-stack/tech-stack-components/SelectedTechList.tsx (1)

16-18: 선택된 항목이 없을 때의 처리가 잘 되어 있습니다.

선택된 항목이 없을 때 최소 높이를 유지하는 빈 div를 반환하여 레이아웃이 급격하게 변하는 것을 방지한 점이 좋습니다.

src/components/ui/tech-stack/tech-stack-components/CategoryTabs.tsx (1)

44-53: 리셋 버튼도 useCallback 적용 및 접근성 개선

리셋 버튼의 onClick 핸들러도 마찬가지로 useCallback을 적용하면 좋습니다. 또한 title 속성에 한글을 사용하고 있는데, 이는 매우 좋은 접근성 제공 방식입니다.

src/components/ui/tech-stack/TechSelector.tsx (4)

1-9: 좋은 임포트 구조 및 컴포넌트 분리

컴포넌트 구성이 잘 되어 있고, 임포트 구조도 명확합니다. 커스텀 훅과 유틸리티 함수를 활용하여 관심사를 잘 분리하였습니다.


42-46: 컴포넌트 구조와 데이터 흐름이 깔끔합니다

선택된 기술 목록을 표시하는 SelectedTechList 컴포넌트에 필요한 props를 잘 전달하고 있습니다. 이렇게 컴포넌트를 분리하면 코드의 가독성과 유지보수성이 향상됩니다.


49-53: 간결한 카테고리 탭 구현

카테고리 탭 컴포넌트로 필요한 props를 명확하게 전달하고 있습니다. 활성 카테고리 상태 관리와 상태 변경 함수를 잘 분리했습니다.


56-62: 기술 버튼 목록 컴포넌트 구현이 적절합니다

TechButtonList 컴포넌트로 필요한 데이터와 콜백 함수를 잘 전달하고 있습니다. 컴포넌트 분리와 관심사 분리가 잘 이루어졌습니다.

src/hooks/useTechSelection.ts (3)

18-19: useRef 훅 사용이 적절합니다

이전 선택 값을 저장하기 위해 useRef를 사용한 것은 좋은 방법입니다. 불필요한 리렌더링을 방지하면서 값을 기억할 수 있습니다.


31-48: 선택 변경 감지 로직이 효율적입니다

선택 변경이 실제로 있을 때만 외부 콜백을 호출하는 로직이 잘 구현되어 있습니다. 선택 배열의 길이와 내용을 비교하여 정확하게 변경을 감지합니다.


57-80: 버튼 클릭 핸들러 로직이 명확합니다

handleButtonClick 함수의 로직이 명확하고 가독성이 좋습니다. 이미 선택된 항목은 해제하고, 최대 선택 개수를 초과하지 않도록 검사하는 로직이 잘 구현되어 있습니다.

Comment on lines +11 to +17
const handleSelectionChange = (selection: string[]) => {
setSelectedTechs(selection);
console.log('Selected technologies:', selection);

// 여기서 필요한 상태 업데이트 또는 API 호출 등을 수행할 수 있습니다.
// 예: 선택된 기술 정보를 서버에 전송
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

프로덕션 코드에서 console.log 제거가 필요합니다.

프로덕션 환경에서는 console.log 문을 제거하는 것이 좋습니다. 디버깅 코드를 남겨두면 성능에 영향을 미칠 수 있으며 민감한 정보가 노출될 가능성이 있습니다.

const handleSelectionChange = (selection: string[]) => {
  setSelectedTechs(selection);
-  console.log('Selected technologies:', selection);
  
  // 여기서 필요한 상태 업데이트 또는 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.

Suggested change
const handleSelectionChange = (selection: string[]) => {
setSelectedTechs(selection);
console.log('Selected technologies:', selection);
// 여기서 필요한 상태 업데이트 또는 API 호출 등을 수행할 수 있습니다.
// 예: 선택된 기술 정보를 서버에 전송
};
const handleSelectionChange = (selection: string[]) => {
setSelectedTechs(selection);
// 여기서 필요한 상태 업데이트 또는 API 호출 등을 수행할 수 있습니다.
// 예: 선택된 기술 정보를 서버에 전송
};

Comment on lines +16 to +28
return (
<button
className={`px-2 py-2 font-medium sm:px-4 ${
active
? 'border-b-2 border-[#C586C0] text-[#C586C0]'
: 'hover:text-Cgray500'
}`}
onClick={onClick}
>
<span className="hidden sm:inline">{children}</span>
<span className="sm:hidden">{smallText}</span>
</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 속성과 role을 추가하여 접근성을 향상시킬 필요가 있습니다.

<button
  className={`px-2 py-2 font-medium sm:px-4 ${
    active
      ? 'border-b-2 border-[#C586C0] text-[#C586C0]'
      : 'hover:text-Cgray500'
  }`}
  onClick={onClick}
+ role="tab"
+ aria-selected={active}
+ tabIndex={active ? 0 : -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.

Suggested change
return (
<button
className={`px-2 py-2 font-medium sm:px-4 ${
active
? 'border-b-2 border-[#C586C0] text-[#C586C0]'
: 'hover:text-Cgray500'
}`}
onClick={onClick}
>
<span className="hidden sm:inline">{children}</span>
<span className="sm:hidden">{smallText}</span>
</button>
);
return (
<button
className={`px-2 py-2 font-medium sm:px-4 ${
active
? 'border-b-2 border-[#C586C0] text-[#C586C0]'
: 'hover:text-Cgray500'
}`}
onClick={onClick}
role="tab"
aria-selected={active}
tabIndex={active ? 0 : -1}
>
<span className="hidden sm:inline">{children}</span>
<span className="sm:hidden">{smallText}</span>
</button>
);

Comment on lines +20 to +34
return (
<div className="min-h-8">
<div className="bg-gray-102 mb-2 flex flex-col gap-2 rounded-md">
<div className="flex flex-wrap gap-2">
{selectedNames.map((name) => (
<SelectedTechButton
key={name}
name={name}
color={getIconColor(name)}
onRemove={onRemove}
/>
))}
</div>
</div>
</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

CSS 클래스명 오류와 시맨틱 HTML 사용 개선이 필요합니다.

선택된 기술 목록을 표시하는 컴포넌트에 몇 가지 개선할 점이 있습니다:

  1. bg-gray-102 클래스는 Tailwind CSS에서 유효하지 않습니다. 일반적으로 50 또는 100 단위로 증가합니다(예: bg-gray-100, bg-gray-200).
  2. 목록을 표시할 때 ulli 같은 시맨틱 HTML 요소를 사용하면 접근성이 향상됩니다.
  return (
    <div className="min-h-8">
-      <div className="bg-gray-102 mb-2 flex flex-col gap-2 rounded-md">
-        <div className="flex flex-wrap gap-2">
+      <div className="mb-2 flex flex-col gap-2 rounded-md bg-gray-100">
+        <ul className="flex flex-wrap gap-2" role="list" aria-label="선택된 기술 목록">
          {selectedNames.map((name) => (
+           <li key={name}>
            <SelectedTechButton
-              key={name}
              name={name}
              color={getIconColor(name)}
              onRemove={onRemove}
            />
+           </li>
          ))}
-        </div>
+        </ul>
      </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.

Suggested change
return (
<div className="min-h-8">
<div className="bg-gray-102 mb-2 flex flex-col gap-2 rounded-md">
<div className="flex flex-wrap gap-2">
{selectedNames.map((name) => (
<SelectedTechButton
key={name}
name={name}
color={getIconColor(name)}
onRemove={onRemove}
/>
))}
</div>
</div>
</div>
return (
<div className="min-h-8">
<div className="mb-2 flex flex-col gap-2 rounded-md bg-gray-100">
<ul className="flex flex-wrap gap-2" role="list" aria-label="선택된 기술 목록">
{selectedNames.map((name) => (
<li key={name}>
<SelectedTechButton
name={name}
color={getIconColor(name)}
onRemove={onRemove}
/>
</li>
))}
</ul>
</div>
</div>
);

Comment on lines +96 to +104
return {
clickedButtons,
selectedCount,
selectedNames,
handleButtonClick,
handleReset,
handleRemoveSelection,
setClickedButtons,
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

setClickedButtons 직접 노출 검토

hook의 반환 값에 setClickedButtons를 직접 포함시키는 것은 캡슐화를 깨뜨릴 수 있습니다. 사용처에서 실수로 이 함수를 직접 호출하면 hook의 내부 로직을 우회할 수 있습니다. 필요한 경우가 아니라면 제거하는 것이 좋습니다.

-  return {
-    clickedButtons,
-    selectedCount,
-    selectedNames,
-    handleButtonClick,
-    handleReset,
-    handleRemoveSelection,
-    setClickedButtons,
-  };

+  return {
+    clickedButtons,
+    selectedCount,
+    selectedNames,
+    handleButtonClick,
+    handleReset,
+    handleRemoveSelection,
+  };
📝 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
return {
clickedButtons,
selectedCount,
selectedNames,
handleButtonClick,
handleReset,
handleRemoveSelection,
setClickedButtons,
};
return {
clickedButtons,
selectedCount,
selectedNames,
handleButtonClick,
handleReset,
handleRemoveSelection,
};

Copy link
Contributor

@lee1nna lee1nna left a comment

Choose a reason for hiding this comment

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

CategoryTabs는 categories 랑 primary 색상 값 props로 받아서 다른 곳에서도 사용할 수 있도록 (ex. 마이페이지 탭) 추후 리팩토링 되어도 좋을 것 같아용
고생하셨습니다 !!! 👍

Copy link
Contributor

@Lee-Dong-Seok Lee-Dong-Seok left a comment

Choose a reason for hiding this comment

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

고생하셨습니다👍👍👍👍👍👍👍👍

@clyde-yoonjae clyde-yoonjae merged commit 3cb7965 into dev Feb 26, 2025
11 checks passed
@clyde-yoonjae clyde-yoonjae deleted the feat/component/tech-stack2/DEVING-49 branch February 26, 2025 09:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants