-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/component/tech stack2/DEVING-49 기술스택 선택 컴포넌트 #29
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
Changes from all commits
5f4eab5
0a8550a
265a6c1
bfaa059
65eea8c
b6ea3e4
76ed79c
0dc88f6
aabf041
1021b9a
a305938
2a2c6d7
246e61e
d544e90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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[]>([]); | ||
|
|
||
| // 기술 선택 변경 핸들러 | ||
| const handleSelectionChange = (selection: string[]) => { | ||
| setSelectedTechs(selection); | ||
| console.log('Selected technologies:', selection); | ||
|
|
||
| // 여기서 필요한 상태 업데이트 또는 API 호출 등을 수행할 수 있습니다. | ||
| // 예: 선택된 기술 정보를 서버에 전송 | ||
| }; | ||
|
|
||
| return ( | ||
| <div> | ||
| <TechSelector | ||
| maxSelections={5} | ||
| onSelectionChange={handleSelectionChange} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion CSS 클래스명 오류와 시맨틱 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default SelectedTechList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default TabButton; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; |
There was a problem hiding this comment.
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