# 공통 컴포넌트 가이드 ## 컴포넌트 계층 요약 ``` Templates → 페이지 전체 (IntegratedListTemplateV2) Organisms → 페이지 블록 (PageHeader, DataTable, SearchFilter ...) Molecules → 조합 단위 (FormField, StatusBadge, StandardDialog ...) UI → 원자 단위 (Button, Input, Select ...) ``` --- ## Templates ### IntegratedListTemplateV2 리스트 페이지를 위한 **올인원 템플릿**. 새 리스트 페이지 생성 시 이 템플릿 사용을 우선 검토합니다. **경로**: `src/components/templates/IntegratedListTemplateV2.tsx` **포함 기능**: - PageLayout + PageHeader (아이콘/제목/설명) - 검색 + 필터 + 날짜 선택 헤더 - 통계 카드 (StatCards) - 테이블 + 컬럼 설정 + 페이지네이션 - 모바일 카드 자동 전환 (반응형) - 체크박스 선택 (`Set`) **필수 적용 항목**: 1. 컬럼 설정 (`useColumnSettings` + `ColumnSettingsPopover`) 2. 모바일 카드 (`renderMobileCard`) 3. 체크박스 (`selectedItems: Set`) 4. 테이블 내 필터 (`tableHeaderActions`) **기본 사용법**: ```tsx import IntegratedListTemplateV2 from '@/components/templates/IntegratedListTemplateV2'; import { useColumnSettings } from '@/hooks/useColumnSettings'; import { ColumnSettingsPopover } from '@/components/molecules/ColumnSettingsPopover'; import { MobileCard, InfoField } from '@/components/organisms'; const columns = [ { key: 'itemName', label: '품목명', width: '200px' }, { key: 'itemCode', label: '품목코드', width: '150px' }, { key: 'status', label: '상태', width: '100px' }, ]; export default function ItemListPage() { const [selectedItems, setSelectedItems] = useState>(new Set()); const { visibleColumns, allColumnsWithVisibility, columnWidths, setColumnWidth, toggleColumnVisibility, resetSettings, hasHiddenColumns } = useColumnSettings({ pageId: 'item-list', columns }); return ( ), }} data={items} // 체크박스 selectedItems={selectedItems} onToggleSelection={(id) => { setSelectedItems(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }} onToggleSelectAll={() => { /* 전체 선택/해제 */ }} getItemId={(item) => item.id} // 테이블 행 renderTableRow={(item, index, globalIndex, isSelected, onToggle) => ( {globalIndex} {item.itemName} {item.itemCode} )} // 모바일 카드 (반응형) renderMobileCard={(item, index, globalIndex, isSelected, onToggle) => ( )} // 페이지네이션 pagination={{ currentPage: pagination.currentPage, totalPages: pagination.lastPage, totalItems: pagination.total, itemsPerPage: pagination.perPage, onPageChange: (page) => fetchData({ page }), }} isLoading={isLoading} // 등록 버튼 createButton={{ label: '품목 등록', onClick: () => router.push('?mode=new') }} /> ); } ``` --- ## Organisms **경로**: `src/components/organisms/` **import**: `import { PageHeader, DataTable, ... } from '@/components/organisms'` ### PageHeader ```tsx 등록} /> ``` | Prop | 타입 | 설명 | |------|------|------| | `title` | string \| ReactNode | 페이지 제목 (필수) | | `description?` | string | 부제목 | | `icon?` | LucideIcon | 좌측 아이콘 | | `actions?` | ReactNode | 우측 액션 버튼 | ### PageLayout ```tsx {children} ``` | Prop | 타입 | 기본값 | 설명 | |------|------|--------|------| | `maxWidth?` | "sm"\|"md"\|"lg"\|"xl"\|"2xl"\|"full" | "full" | 최대 너비 | ### StatCards ```tsx ``` ### SearchFilter ```tsx } /> ``` ### DataTable ```tsx }, ]} data={items} keyField="id" onRowClick={(row) => router.push(`/items/${row.id}`)} pagination={{ currentPage, totalPages, onPageChange }} /> ``` **Column type 종류**: `text`, `number`, `currency`, `date`, `datetime`, `status`, `badge`, `icon`, `actions`, `custom` ### SearchableSelectionModal 검색+선택 팝업이 필요할 때 사용. **직접 Dialog 조합 금지**. ```tsx open={isOpen} onOpenChange={setIsOpen} title="거래처 검색" fetchData={async (query) => { const result = await searchVendors({ search: query }); return result.success ? result.data : []; }} keyExtractor={(vendor) => vendor.id} mode="single" onSelect={(vendor) => handleVendorSelect(vendor)} searchPlaceholder="거래처명으로 검색" renderItem={(vendor, isSelected) => (
{vendor.name}
{vendor.code}
)} /> ``` | Prop | 필수 | 설명 | |------|:---:|------| | `open` | O | 모달 열기 상태 | | `onOpenChange` | O | 상태 변경 | | `title` | O | 모달 제목 | | `fetchData` | O | `(query: string) => Promise` | | `keyExtractor` | O | `(item: T) => string` | | `mode` | O | `'single'` \| `'multiple'` | | `onSelect` | O | 선택 콜백 | | `renderItem` | O | 아이템 렌더링 | | `searchMode?` | | `'debounce'`(기본) \| `'enter'` | | `loadOnOpen?` | | 열릴 때 자동 로드 | | `listWrapper?` | | 리스트 래퍼 (테이블 구조 등) | ### MobileCard / InfoField ```tsx router.push(`/items/${item.id}`)} /> ``` ### EmptyState / TableEmptyState ```tsx ``` --- ## Molecules **경로**: `src/components/molecules/` ### FormField (신규 폼 필수) `Label + Input + Error` 수동 조합 대신 사용. ```tsx import { FormField } from '@/components/molecules/FormField'; handleChange('companyName', value)} placeholder="회사명을 입력하세요" disabled={mode === 'view'} error={errors.companyName} /> ``` **지원 type**: `text`, `number`, `date`, `select`, `textarea`, `custom`, `password`, `phone`, `businessNumber`, `personalNumber`, `currency`, `quantity` **FormField로 대체하지 않는 경우**: - Select, DatePicker, ImageUpload 등 특수 컴포넌트 - 주소 검색(버튼+입력) 등 복합 레이아웃 - 편집/읽기 모드가 다른 커스텀 인터랙션 ### StatusBadge ```tsx import { StatusBadge } from '@/components/molecules/StatusBadge'; ``` **variant**: `default`, `success`, `warning`, `danger`, `info`, `secondary`, `outline` ### ColumnSettingsPopover `useColumnSettings` hook과 함께 사용: ```tsx ``` ### StandardDialog ```tsx } >

이 작업은 되돌릴 수 없습니다.

``` **size**: `sm`, `md`, `lg`, `xl`, `full`