Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/app/preview/tech-stack3/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import TechSelector from '@/components/ui/tech-stack/TechSelector';
import React, { useState } from 'react';

export default function Page(): JSX.Element {
// 상위 컴포넌트에서 선택된 기술 목록 관리
const [selectedTechs, setSelectedTechs] = useState<string[]>([]);

Check warning on line 8 in src/app/preview/tech-stack3/page.tsx

View workflow job for this annotation

GitHub Actions / check

'selectedTechs' is assigned a value but never used. Allowed unused vars must match /^_/u

// 기술 선택 변경 핸들러
const handleSelectionChange = (selection: string[]) => {
setSelectedTechs(selection);
console.log('Selected technologies:', selection);

Check warning on line 13 in src/app/preview/tech-stack3/page.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected console statement

// 여기서 필요한 상태 업데이트 또는 API 호출 등을 수행할 수 있습니다.
// 예: 선택된 기술 정보를 서버에 전송
};
Comment on lines +11 to +17
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 호출 등을 수행할 수 있습니다.
// 예: 선택된 기술 정보를 서버에 전송
};


return (
<div>
<TechSelector
maxSelections={5}
onSelectionChange={handleSelectionChange}
/>
</div>
);
}
6 changes: 3 additions & 3 deletions src/components/ui/Icon/IconData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const ICON_LIST: IconConfig[] = [
},
{
name: 'Next',
color: '#000000',
color: '#FFFFFF',
path: getPath(
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z"/></svg>',
),
Expand Down Expand Up @@ -127,7 +127,7 @@ export const ICON_LIST: IconConfig[] = [
},
{
name: 'Express',
color: '#000000',
color: '#FFFFFF',
path: getPath(
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Express</title><path d="M24 18.588a1.529 1.529 0 01-1.895-.72l-3.45-4.771-.5-.667-4.003 5.444a1.466 1.466 0 01-1.802.708l5.158-6.92-4.798-6.251a1.595 1.595 0 011.9.666l3.576 4.83 3.596-4.81a1.435 1.435 0 011.788-.668L21.708 7.9l-2.522 3.283a.666.666 0 000 .994l4.804 6.412zM.002 11.576l.42-2.075c1.154-4.103 5.858-5.81 9.094-3.27 1.895 1.489 2.368 3.597 2.275 5.973H1.116C.943 16.447 4.005 19.009 7.92 17.7a4.078 4.078 0 002.582-2.876c.207-.666.548-.78 1.174-.588a5.417 5.417 0 01-2.589 3.957 6.272 6.272 0 01-7.306-.933 6.575 6.575 0 01-1.64-3.858c0-.235-.08-.455-.134-.666A88.33 88.33 0 010 11.577zm1.127-.286h9.654c-.06-3.076-2.001-5.258-4.59-5.278-2.882-.04-4.944 2.094-5.071 5.264z"/></svg>',
),
Expand Down Expand Up @@ -159,7 +159,7 @@ export const ICON_LIST: IconConfig[] = [
},
{
name: 'Django',
color: '#092E20',
color: '#098620',
path: getPath(
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Django</title><path d="M11.146 0h3.924v18.166c-2.013.382-3.491.535-5.096.535-4.791 0-7.288-2.166-7.288-6.32 0-4.002 2.65-6.6 6.753-6.6.637 0 1.121.05 1.707.203zm0 9.143a3.894 3.894 0 00-1.325-.204c-1.988 0-3.134 1.223-3.134 3.365 0 2.09 1.096 3.236 3.109 3.236.433 0 .79-.025 1.35-.102V9.142zM21.314 6.06v9.098c0 3.134-.229 4.638-.917 5.937-.637 1.249-1.478 2.039-3.211 2.905l-3.644-1.733c1.733-.815 2.574-1.53 3.109-2.625.561-1.121.739-2.421.739-5.835V6.059h3.924zM17.39.021h3.924v4.026H17.39z"/></svg>',
),
Expand Down
68 changes: 68 additions & 0 deletions src/components/ui/tech-stack/TechSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import useTechSelection from '@/hooks/useTechSelection';
import { getIconColor, getIconsByCategory } from '@/util/getIconDetail';
import React, { useState } from 'react';
import { CategoryType } from 'types/techStack';

import CategoryTabs from './tech-stack-components/CategoryTabs';
import SelectedTechList from './tech-stack-components/SelectedTechList';
import TechButtonList from './tech-stack-components/TechButtonList';

interface TechSelectorProps {
maxSelections?: number;
onSelectionChange?: (selection: string[]) => void;
}

const TechSelector = ({
maxSelections = 5,
onSelectionChange,
}: TechSelectorProps): JSX.Element => {
// 현재 선택된 카테고리 상태
const [activeCategory, setActiveCategory] = useState<CategoryType>('all');

// 기술 선택 로직 훅 사용
const {
clickedButtons,
selectedCount,
selectedNames,
handleButtonClick,
handleReset,
handleRemoveSelection,
} = useTechSelection({
maxSelections,
onSelectionChange,
});

// 현재 카테고리의 아이콘 가져오기
const activeIcons = getIconsByCategory(activeCategory);

return (
<div className="bg-gray-50 min-h-screen p-10">
<div className="mx-auto max-w-6xl">
{/* 선택된 기술 목록 */}
<SelectedTechList
selectedNames={selectedNames}
getIconColor={getIconColor}
onRemove={handleRemoveSelection}
/>

{/* 카테고리 탭 */}
<CategoryTabs
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
onReset={handleReset}
/>

{/* 기술 버튼 목록 */}
<TechButtonList
icons={activeIcons}
clickedButtons={clickedButtons}
selectedCount={selectedCount}
maxSelections={maxSelections}
onButtonClick={handleButtonClick}
/>
</div>
</div>
);
};

export default TechSelector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { RotateCcw } from 'lucide-react';
import React from 'react';
import { CategoryType } from 'types/techStack';

import TabButton from './TabButton';

interface CategoryTabsProps {
activeCategory: CategoryType;
onCategoryChange: (category: CategoryType) => void;
onReset: () => void;
}

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' },
];

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)}
smallText={category.smallText}
>
{category.label}
</TabButton>
))}
</div>

<div className="flex items-center gap-2">
<button
onClick={onReset}
className="hover:bg-gray-700/30 flex items-center gap-1 rounded-full px-2 py-1 text-white sm:px-3"
title="초기화"
>
<RotateCcw size={16} />
<span className="hidden sm:inline">초기화</span>
</button>
</div>
</div>
);
};

export default CategoryTabs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getIconComponent } from '@/util/getIconDetail';
import { X } from 'lucide-react';
import React from 'react';

interface SelectedTechButtonProps {
name: string;
color: string;
onRemove: (name: string) => void;
}

const SelectedTechButton = ({
name,
color,
onRemove,
}: SelectedTechButtonProps): JSX.Element => {
// 현재 기술의 아이콘 컴포넌트
const TechIcon = getIconComponent(name);

return (
<div className="flex items-center gap-1 rounded-full border border-main bg-Cgray100 px-2 py-1 shadow-sm">
<span className="flex-shrink-0">
<TechIcon size={14} color={color} />
</span>
<span
style={{ color }}
className="hidden cursor-default text-xs font-medium sm:inline-block"
>
{name}
</span>

{/* 삭제 버튼 */}
<button
onClick={() => onRemove(name)}
className="hover:bg-gray-200 ml-1 cursor-pointer rounded-full p-1"
aria-label={`${name} 선택 해제`}
>
<X size={12} className="text-white" />
</button>
</div>
);
};

export default SelectedTechButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import SelectedTechButton from './SelectedTechButton';

interface SelectedTechListProps {
selectedNames: string[];
getIconColor: (name: string) => string;
onRemove: (name: string) => void;
}

const SelectedTechList = ({
selectedNames,
getIconColor,
onRemove,
}: SelectedTechListProps): JSX.Element => {
if (selectedNames.length === 0) {
return <div className="min-h-8"></div>;
}

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>
Comment on lines +20 to +34
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>
);

);
};

export default SelectedTechList;
31 changes: 31 additions & 0 deletions src/components/ui/tech-stack/tech-stack-components/TabButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';

interface TabButtonProps {
active: boolean;
onClick: () => void;
children: React.ReactNode;
smallText: string;
}

const TabButton = ({
active,
onClick,
children,
smallText,
}: TabButtonProps): JSX.Element => {
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>
);
Comment on lines +16 to +28
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>
);

};

export default TabButton;
43 changes: 43 additions & 0 deletions src/components/ui/tech-stack/tech-stack-components/TechButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { isLightColor } from '@/util/getIconDetail';
import React from 'react';
import { IconComponent } from 'types/techStack';

interface TechButtonProps {
icon: IconComponent;
name: string;
color: string;
isClicked: boolean;
isMaxReached: boolean;
onClick: (name: string) => void;
}

const TechButton = ({
icon: Icon,
name,
color,
isClicked,
isMaxReached,
onClick,
}: TechButtonProps): JSX.Element => {
// 색상이 흰색이면 클릭 시 검정색으로 변경
const iconColor = isClicked && isLightColor(color) ? '#000000' : color;

return (
<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개까지만 선택할 수 있습니다' : ''}
>
<Icon size={16} color={iconColor} />
<p style={{ color: iconColor }} className="font-medium">
{name}
</p>
</button>
);
};

export default TechButton;
Loading
Loading