From ca5a9325c6c7260b43d99c4d5ec934a0d9b14d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 12 Mar 2026 21:48:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B8=89=EC=97=AC=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20+=20=EC=84=A4=EB=B9=84=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=8B=A0=EA=B7=9C=20+=20=ED=8C=9D=EC=97=85=EA=B4=80?= =?UTF-8?q?=EB=A6=AC/=EC=B9=B4=EB=93=9C=EA=B4=80=EB=A6=AC/=EA=B0=80?= =?UTF-8?q?=EA=B2=A9=ED=91=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 급여관리: 상세/등록 다이얼로그 리팩토링, actions/types 확장 - 설비관리: 설비현황/점검/수리 4개 페이지 신규 추가 - 팝업관리: PopupDetail/PopupForm 개선 - 카드관리: CardForm 개선 - IntegratedListTemplateV2, SearchFilter, useColumnSettings 개선 - CLAUDE.md: 페이지 모드 라우팅 패턴 규칙 추가 - 공통 페이지 패턴 가이드 확장 --- CLAUDE.md | 85 + claudedocs/dev/[REF] all-pages-test-urls.md | 12 + .../guides/[GUIDE] common-page-patterns.md | 347 +++- package.json | 1 + .../quality/equipment-inspections/page.tsx | 12 + .../quality/equipment-repairs/new/page.tsx | 22 + .../quality/equipment-repairs/page.tsx | 22 + .../quality/equipment-status/page.tsx | 12 + .../quality/equipment/[id]/page.tsx | 19 + .../quality/equipment/import/page.tsx | 12 + .../quality/equipment/new/page.tsx | 22 + .../(protected)/quality/equipment/page.tsx | 22 + src/components/auth/LoginPage.tsx | 8 +- src/components/hr/CardManagement/CardForm.tsx | 27 +- .../SalaryManagement/SalaryDetailDialog.tsx | 1160 ++++++----- .../SalaryRegistrationDialog.tsx | 718 ++++--- src/components/hr/SalaryManagement/actions.ts | 768 +++++--- src/components/hr/SalaryManagement/index.tsx | 1743 ++++++++++------- src/components/hr/SalaryManagement/types.ts | 206 +- .../SalaryDetailDialog.tsx | 524 +++++ .../SalaryRegistrationDialog.tsx | 571 ++++++ .../actions.ts | 349 ++++ .../index.tsx | 793 ++++++++ .../SalaryManagement_backup_20260312/types.ts | 100 + src/components/organisms/SearchFilter.tsx | 6 +- src/components/pricing/PricingFormClient.tsx | 32 +- .../quality/EquipmentInspection/index.tsx | 481 +++++ .../EquipmentManagement/EquipmentDetail.tsx | 1164 +++++++++++ .../EquipmentManagement/EquipmentForm.tsx | 561 ++++++ .../EquipmentManagement/EquipmentImport.tsx | 211 ++ .../quality/EquipmentManagement/actions.ts | 588 ++++++ .../quality/EquipmentManagement/index.tsx | 343 ++++ .../quality/EquipmentManagement/types.ts | 352 ++++ .../quality/EquipmentRepair/RepairForm.tsx | 268 +++ .../quality/EquipmentRepair/index.tsx | 284 +++ .../quality/EquipmentStatus/index.tsx | 202 ++ .../settings/PopupManagement/PopupDetail.tsx | 35 +- .../settings/PopupManagement/PopupForm.tsx | 35 +- .../templates/IntegratedListTemplateV2.tsx | 26 +- src/hooks/useColumnSettings.ts | 8 +- 40 files changed, 10284 insertions(+), 1867 deletions(-) create mode 100644 src/app/[locale]/(protected)/quality/equipment-inspections/page.tsx create mode 100644 src/app/[locale]/(protected)/quality/equipment-repairs/new/page.tsx create mode 100644 src/app/[locale]/(protected)/quality/equipment-repairs/page.tsx create mode 100644 src/app/[locale]/(protected)/quality/equipment-status/page.tsx create mode 100644 src/app/[locale]/(protected)/quality/equipment/[id]/page.tsx create mode 100644 src/app/[locale]/(protected)/quality/equipment/import/page.tsx create mode 100644 src/app/[locale]/(protected)/quality/equipment/new/page.tsx create mode 100644 src/app/[locale]/(protected)/quality/equipment/page.tsx create mode 100644 src/components/hr/SalaryManagement_backup_20260312/SalaryDetailDialog.tsx create mode 100644 src/components/hr/SalaryManagement_backup_20260312/SalaryRegistrationDialog.tsx create mode 100644 src/components/hr/SalaryManagement_backup_20260312/actions.ts create mode 100644 src/components/hr/SalaryManagement_backup_20260312/index.tsx create mode 100644 src/components/hr/SalaryManagement_backup_20260312/types.ts create mode 100644 src/components/quality/EquipmentInspection/index.tsx create mode 100644 src/components/quality/EquipmentManagement/EquipmentDetail.tsx create mode 100644 src/components/quality/EquipmentManagement/EquipmentForm.tsx create mode 100644 src/components/quality/EquipmentManagement/EquipmentImport.tsx create mode 100644 src/components/quality/EquipmentManagement/actions.ts create mode 100644 src/components/quality/EquipmentManagement/index.tsx create mode 100644 src/components/quality/EquipmentManagement/types.ts create mode 100644 src/components/quality/EquipmentRepair/RepairForm.tsx create mode 100644 src/components/quality/EquipmentRepair/index.tsx create mode 100644 src/components/quality/EquipmentStatus/index.tsx diff --git a/CLAUDE.md b/CLAUDE.md index d963b583..18adf690 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -271,6 +271,89 @@ function buildCoverageMap(items, spanKey) { --- +## 페이지 모드 라우팅 패턴 (mode=new/edit/view) +**Priority**: 🔴 + +### 라우팅 규칙 +- **별도 `/new` 경로 금지** → `?mode=new` 쿼리파라미터 사용 +- **별도 `/edit` 경로 금지** → `?mode=edit` 쿼리파라미터 사용 +- 목록과 등록/수정을 **같은 page.tsx에서 분기** + +```typescript +// ✅ 올바른 패턴: page.tsx에서 mode 분기 +export default function SomePage() { + const searchParams = useSearchParams(); + const mode = searchParams.get('mode'); + + if (mode === 'new') return ; + return ; +} + +// ✅ 상세+수정: [id] 경로에서 mode 분기 +export default function SomeDetailPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view'; + + return ; +} +``` + +```typescript +// ❌ 금지 패턴 +router.push('/some-page/new') // → router.push('/some-page?mode=new') +router.push('/some-page/123/edit') // → router.push('/some-page/123?mode=edit') +``` + +### 등록/수정/상세 페이지 헤더 +| 위치 | 요소 | +|------|------| +| 상단 좌측 | 페이지 제목 (`

`) | +| 상단 우측 | `← 목록으로` 링크 (`Button variant="link"`) | + +```typescript +// ✅ 표준 헤더 +
+

페이지 제목

+ +
+``` + +### 하단 Sticky 액션 바 (필수) +폼 콘텐츠 아래에 **sticky bottom bar**로 버튼 배치. 취소는 좌측, 주요 액션은 우측. + +| 모드 | 좌측 | 우측 | +|------|------|------| +| 등록 (new) | `X 취소` | `💾 저장` | +| 상세 (view) | `X 취소` (목록으로) | `✏️ 수정` | +| 수정 (edit) | `X 취소` | `💾 저장` | + +```typescript +// ✅ 표준 하단 Sticky 액션 바 +
+
+ + +
+
+``` + +**규칙:** +- Card 내부에 버튼 넣지 않음 → sticky 하단 바 사용 +- 아이콘 포함: 취소(`X`), 저장(`Save`), 수정(`Pencil`) +- 상세(view) 모드에서 "취소"는 목록으로 이동, "수정"은 `?mode=edit` 전환 + +--- + ## Design Popup Policy **Priority**: 🟡 @@ -448,6 +531,7 @@ url: `${API_URL}/api/v1/items?${params.toString()}` |-----------|----------| | 검색 모달/선택 팝업 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "검색 모달" 섹션 | | 리스트/목록 페이지 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "리스트 페이지" 섹션 | +| **IntegratedListTemplateV2 적용/리팩토링** | `claudedocs/guides/[GUIDE] common-page-patterns.md` → **"IntegratedListTemplateV2 표준 적용"** 섹션 | | 상세/수정/등록 페이지 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "상세/폼 페이지" 섹션 | | 새 organisms 필요 | `src/components/organisms/index.ts` 먼저 확인 → 없으면 생성 | @@ -455,6 +539,7 @@ url: `${API_URL}/api/v1/items?${params.toString()}` - 새 파일 만들기 전 `organisms/`, `molecules/` export 목록 확인 - 검색+선택 모달 → `SearchableSelectionModal` 사용 (직접 Dialog 조합 금지) - 리스트 페이지 → `UniversalListPage` 또는 organisms 조합 +- **IntegratedListTemplateV2 사용 시 → 컬럼 설정(`useColumnSettings` + `ColumnSettingsPopover`), 모바일 카드(`renderMobileCard`), 체크박스(`Set`), 테이블 내 필터(`tableHeaderActions`) 필수 적용** - 상세/폼 → Card + 기존 패턴 따르기 --- diff --git a/claudedocs/dev/[REF] all-pages-test-urls.md b/claudedocs/dev/[REF] all-pages-test-urls.md index b72bc602..b6203559 100644 --- a/claudedocs/dev/[REF] all-pages-test-urls.md +++ b/claudedocs/dev/[REF] all-pages-test-urls.md @@ -136,10 +136,18 @@ http://localhost:3000/ko/material/stock-status # 🆕 재고현황 |--------|-----|------| | **검사관리** | `/ko/quality/inspections` | 🆕 NEW | | **실적신고관리** | `/ko/quality/performance-reports` | 🆕 NEW | +| **설비 등록대장** | `/ko/quality/equipment` | 🆕 NEW | +| **설비 현황** | `/ko/quality/equipment-status` | 🆕 NEW | +| **일상점검표** | `/ko/quality/equipment-inspections` | 🆕 NEW | +| **수리이력** | `/ko/quality/equipment-repairs` | 🆕 NEW | ``` http://localhost:3000/ko/quality/inspections # 🆕 검사관리 http://localhost:3000/ko/quality/performance-reports # 🆕 실적신고관리 +http://localhost:3000/ko/quality/equipment # 🆕 설비 등록대장 +http://localhost:3000/ko/quality/equipment-status # 🆕 설비 현황 +http://localhost:3000/ko/quality/equipment-inspections # 🆕 일상점검표 +http://localhost:3000/ko/quality/equipment-repairs # 🆕 수리이력 ``` --- @@ -519,6 +527,10 @@ http://localhost:3000/ko/dev/editable-table # Editable Table 테스트 // Quality (품질관리) '/quality/inspections' // 검사관리 (🆕 NEW) '/quality/performance-reports' // 실적신고관리 (🆕 NEW) +'/quality/equipment' // 설비 등록대장 (🆕 NEW) +'/quality/equipment-status' // 설비 현황 (🆕 NEW) +'/quality/equipment-inspections' // 일상점검표 (🆕 NEW) +'/quality/equipment-repairs' // 수리이력 (🆕 NEW) // Outbound (출고관리) '/outbound/shipments' // 출하관리 (🆕 NEW) diff --git a/claudedocs/guides/[GUIDE] common-page-patterns.md b/claudedocs/guides/[GUIDE] common-page-patterns.md index 067f76c7..b2cabba0 100644 --- a/claudedocs/guides/[GUIDE] common-page-patterns.md +++ b/claudedocs/guides/[GUIDE] common-page-patterns.md @@ -9,9 +9,10 @@ 1. [공통 컴포넌트 맵](#1-공통-컴포넌트-맵) 2. [검색 모달 (SearchableSelectionModal)](#2-검색-모달) 3. [리스트 페이지](#3-리스트-페이지) -4. [상세/폼 페이지](#4-상세폼-페이지) -5. [API 연동 패턴](#5-api-연동-패턴) -6. [페이지 라우팅 구조](#6-페이지-라우팅-구조) +4. [IntegratedListTemplateV2 표준 적용](#4-integratedlisttemplatev2-표준-적용) +5. [상세/폼 페이지](#5-상세폼-페이지) +6. [API 연동 패턴](#6-api-연동-패턴) +7. [페이지 라우팅 구조](#7-페이지-라우팅-구조) --- @@ -304,7 +305,320 @@ export function MyList() { --- -## 4. 상세/폼 페이지 +## 4. IntegratedListTemplateV2 표준 적용 + +### 개요 + +`IntegratedListTemplateV2`는 프로젝트의 표준 리스트 페이지 템플릿으로, 아래 기능을 한 번에 제공한다: +- PageLayout + PageHeader (아이콘/제목/설명) +- 날짜/검색/버튼 헤더 영역 +- 통계 카드 +- 테이블 (체크박스/번호/데이터/작업) + 페이지네이션 +- 모바일 카드 뷰 자동 전환 +- 컬럼 설정 (표시/숨기기/리사이즈) + +**위치**: `src/components/templates/IntegratedListTemplateV2.tsx` + +### 🔴 적용 시 필수 체크리스트 + +IntegratedListTemplateV2를 사용하는 페이지를 만들거나 리팩토링할 때, **아래 항목을 반드시 확인**한다. + +| # | 항목 | 설명 | 필수 | +|---|------|------|:----:| +| 1 | **컬럼 설정** | `useColumnSettings` + `ColumnSettingsPopover` + `columnSettings` prop | ✅ | +| 2 | **검색** | `searchValue` + `onSearchChange` + `searchPlaceholder` | ✅ | +| 3 | **체크박스 선택** | `selectedItems (Set)` + `onToggleSelection` + `onToggleSelectAll` + `getItemId` | ✅ | +| 4 | **페이지네이션** | `pagination` (currentPage, totalPages, totalItems, itemsPerPage, onPageChange) | ✅ | +| 5 | **모바일 카드** | `renderMobileCard` + `MobileCard` / `InfoField` 사용 | ✅ | +| 6 | **테이블 행** | `renderTableRow` (TableRow + TableCell 조합) | ✅ | +| 7 | **헤더 레이아웃** | 순서: `[검색] [날짜/연월] --- [액션버튼] [등록버튼]` | ✅ | +| 8 | **통계 카드** | `stats` 배열 (label, value, icon, iconColor) | 권장 | +| 9 | **테이블 내 필터** | `tableHeaderActions` (부서, 상태 등 Select) | 필요 시 | +| 10 | **탭** | `tabsContent` (커스텀) 또는 `tabs` + `activeTab` + `onTabChange` | 필요 시 | + +### 컬럼 설정 (필수 패턴) + +**매번 빠뜨리지 않도록 3가지 세트로 기억한다:** + +```typescript +// 1️⃣ Hook 선언 +import { useColumnSettings } from '@/hooks/useColumnSettings'; +import { ColumnSettingsPopover } from '@/components/molecules/ColumnSettingsPopover'; + +const TABLE_COLUMNS: TableColumn[] = [ + { key: 'no', label: '번호', className: 'text-center w-[60px]' }, + { key: 'name', label: '이름', copyable: true }, + // ... + { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, +]; + +const { + visibleColumns, // → tableColumns prop에 전달 + allColumnsWithVisibility, // → ColumnSettingsPopover에 전달 + columnWidths, // → columnSettings.columnWidths + setColumnWidth, // → columnSettings.onColumnResize + toggleColumnVisibility, // → ColumnSettingsPopover.onToggle + resetSettings, // → ColumnSettingsPopover.onReset + hasHiddenColumns, // → ColumnSettingsPopover.hasHiddenColumns +} = useColumnSettings({ + pageId: 'my-page-id', // Zustand 저장 키 (고유값) + columns: TABLE_COLUMNS, + alwaysVisibleKeys: ['no', 'name', 'actions'], // 숨기기 불가 컬럼 +}); +``` + +```tsx +// 2️⃣ 템플릿에 전달 + + ), + }} + // ... +/> +``` + +### 헤더 레이아웃 순서 + +표준 레이아웃은 아래 순서를 따른다: + +``` +[아이콘] 페이지 제목 + 설명 텍스트 + +[검색창] [날짜/연월 셀렉트] --- [액션버튼들] [+ 등록 버튼] + +[탭: 목록 | 설정] (tabsContent, 필요 시) + +[통계카드 ...] (stats) + +[전체 N건 | N개 선택됨] [부서 필터] [상태 필터] [컬럼 설정] (tableHeaderActions) +[테이블] +[페이지네이션] +``` + +**날짜 대신 연월 셀렉트가 필요한 경우:** +```typescript +dateRangeSelector={{ + enabled: true, + hideDateInputs: true, // 날짜 입력 숨김 + showPresets: false, // 프리셋 버튼 숨김 + extraActions: ( // 대신 연월 셀렉트 배치 +
+ + +
+ ), +}} +``` + +### 테이블 내 필터 (tableHeaderActions) + +테이블 카드 내부 "전체 N건" 오른쪽에 필터 셀렉트를 배치한다: + +```tsx +const tableHeaderActionsNode = ( +
+ + +
+); + +// 사용 + +``` + +### 모바일 카드 (renderMobileCard) + +```tsx +import { MobileCard, InfoField } from '@/components/organisms/MobileCard'; + +const renderMobileCard = useCallback(( + item: MyItem, + _index: number, + _globalIndex: number, + isSelected: boolean, + onToggle: () => void, +) => ( + , + , + ]} + isSelected={isSelected} + onToggleSelection={onToggle} + onClick={() => handleDetailOpen(item.id)} + /> +), []); +``` + +### 체크박스 선택 (Set\) + +IntegratedListTemplateV2는 **문자열 ID** (`Set`)를 요구한다: + +```typescript +// ✅ 올바른 패턴 +const [selectedIds, setSelectedIds] = useState>(new Set()); + +const toggleSelection = useCallback((id: string) => { + setSelectedIds(prev => { + const next = new Set(prev); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); +}, []); + +const toggleSelectAll = useCallback(() => { + setSelectedIds(prev => + prev.size === data.length + ? new Set() + : new Set(data.map(item => String(item.id))) + ); +}, [data]); + +// 사용 + String(item.id)} +/> +``` + +### 전체 스켈레톤 예제 + +```tsx +'use client'; + +import { IntegratedListTemplateV2, type TableColumn } from '@/components/templates/IntegratedListTemplateV2'; +import { useColumnSettings } from '@/hooks/useColumnSettings'; +import { ColumnSettingsPopover } from '@/components/molecules/ColumnSettingsPopover'; +import { MobileCard, InfoField } from '@/components/organisms/MobileCard'; +import { TableRow, TableCell } from '@/components/ui/table'; +import { Checkbox } from '@/components/ui/checkbox'; +import { MyIcon } from 'lucide-react'; + +const TABLE_COLUMNS: TableColumn[] = [ + { key: 'no', label: '번호', className: 'text-center w-[60px]' }, + { key: 'name', label: '이름', copyable: true }, + { key: 'status', label: '상태', className: 'text-center w-[80px]' }, + { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, +]; + +export function MyListPage() { + // 컬럼 설정 (필수) + const { + visibleColumns, allColumnsWithVisibility, columnWidths, + setColumnWidth, toggleColumnVisibility, resetSettings, hasHiddenColumns, + } = useColumnSettings({ + pageId: 'my-page', + columns: TABLE_COLUMNS, + alwaysVisibleKeys: ['no', 'name', 'actions'], + }); + + // 선택 (Set) + const [selectedIds, setSelectedIds] = useState>(new Set()); + // ... toggleSelection, toggleSelectAll 구현 + + return ( + + // 헤더 + title="페이지 제목" + description="설명" + icon={MyIcon} + + // 헤더 액션 + headerActions={} + createButton={{ label: '등록', onClick: handleCreate }} + + // 검색 + searchValue={search} + onSearchChange={setSearch} + searchPlaceholder="검색..." + + // 통계 + stats={[{ label: '전체', value: totalCount, icon: Users, iconColor: 'text-blue-600' }]} + + // 테이블 필터 + tableHeaderActions={filterNode} + + // 테이블 + 컬럼 설정 + tableColumns={visibleColumns} + columnSettings={{ + columnWidths, + onColumnResize: setColumnWidth, + settingsPopover: ( + + ), + }} + + // 데이터 + data={items} + selectedItems={selectedIds} + onToggleSelection={toggleSelection} + onToggleSelectAll={toggleSelectAll} + getItemId={(item) => String(item.id)} + + // 렌더링 + renderTableRow={renderTableRow} + renderMobileCard={renderMobileCard} + + // 페이지네이션 + pagination={{ + currentPage, totalPages, totalItems: totalCount, + itemsPerPage: PAGE_SIZE, onPageChange: setCurrentPage, + }} + + // 로딩 + isLoading={isLoading} + /> + ); +} +``` + +--- + +## 5. 상세/폼 페이지 ### 표준 구조 @@ -405,18 +719,37 @@ export function MyDetail({ id, mode }: DetailProps) { ### 상세/폼 페이지 공통 규칙 - **모드**: `view` | `edit` | `new` 3가지 +- **라우팅**: `?mode=new` / `?mode=edit` 쿼리파라미터 사용 (별도 `/new`, `/edit` 경로 금지) +- **page.tsx 분기**: 목록 page.tsx에서 `searchParams.get('mode')` 로 등록 폼 분기 - **Hook 규칙**: 모든 hook은 최상단, 조건부 return은 그 아래 - **레이아웃**: `Card > CardHeader + CardContent` 섹션 단위 - **필드 그리드**: `grid grid-cols-1 md:grid-cols-2 gap-4` - **disabled**: view 모드에서 모든 입력 비활성화 - **알림**: `toast.success()` / `toast.error()` (sonner) -- **네비게이션**: `router.back()` 또는 `router.push()` - **로딩**: Skeleton 컴포넌트 사용 - **Select 버그 대응**: ` updateAllowanceItem(idx, 'name', e.target.value)} + className="w-32 h-8 text-sm" + /> + updateAllowanceItem(idx, 'amount', v ?? 0)} + className="flex-1 h-8 text-right" + /> + + + ) : ( + + ) + ))} + + +
+ 총 지급액 + {formatCurrency(grossSalary)}원 +
+
+ 과세표준 (식대 제외) + {formatCurrency(Math.max(0, grossSalary - bonus))}원 +
+ + + + {/* ===== 공제 항목 ===== */} +
+

공제 항목

+
+

4대보험

+ + + + + + +

세금

+ + + + {/* 추가 공제 */} + {(deductions.length > 0 || isEditing) && } + {isEditing && ( +
+ 추가 공제 + +
+ )} + {deductions.map((item, idx) => ( + isEditing ? ( +
+ updateDeductionItem(idx, 'name', e.target.value)} + className="w-32 h-8 text-sm" + /> + updateDeductionItem(idx, 'amount', v ?? 0)} + className="flex-1 h-8 text-right" + /> + +
+ ) : ( + + ) + ))} + + +
+ 총 공제액 + -{formatCurrency(totalDeductions)}원 +
+
+
+ + {/* ===== 합계 ===== */} +
+
+
+ 총 지급액 + + {formatCurrency(grossSalary)}원 + +
+
+ 총 공제액 + + -{formatCurrency(totalDeductions)}원 + +
+
+ 실수령액 + + {formatCurrency(netSalary)}원 + +
+
+
+ + {/* ===== 비고 ===== */} +
+ 비고 + {isEditing ? ( +