From 012a661a196869c5fa3b7fe64ad6094d40270e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Fri, 20 Feb 2026 13:26:27 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20=ED=9A=8C=EA=B3=84/=EA=B2=B0?= =?UTF-8?q?=EC=9E=AC/=EA=B1=B4=EC=84=A4=20=EB=93=B1=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=ED=99=94=203=EC=B0=A8=20=EB=B0=8F=20=EA=B2=80=EC=83=89/?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=9C=A0=ED=8B=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - search.ts: 범용 검색 유틸리티 추출 (텍스트/날짜/상태 필터링) - status-config.ts: 상태 설정 공통 유틸 추가 - 회계 모듈 types 간소화 및 컬럼 설정 공통 패턴 적용 - 회계 page.tsx 통일 (bad-debt/bills/deposits/sales 등 9개) - 결재함(승인/기안/참조) 공통 패턴 적용 - 건설 모듈 견적/인수인계/이슈/기성 등 코드 정리 - IntegratedListTemplateV2 개선 - LanguageSelect/ThemeSelect 정리 - 체크리스트 문서 업데이트 Co-Authored-By: Claude Opus 4.6 --- ...-19] code-dedup-commonization-checklist.md | 103 +++++++++++++ .../accounting/bad-debt-collection/page.tsx | 4 +- .../(protected)/accounting/bills/page.tsx | 4 +- .../(protected)/accounting/deposits/page.tsx | 4 +- .../accounting/expected-expenses/page.tsx | 4 +- .../accounting/gift-certificates/page.tsx | 4 +- .../(protected)/accounting/sales/page.tsx | 4 +- .../accounting/tax-invoice-issuance/page.tsx | 4 +- .../(protected)/accounting/vendors/page.tsx | 4 +- .../accounting/withdrawals/page.tsx | 4 +- .../(protected)/board/[boardCode]/page.tsx | 4 +- .../(protected)/boards/[boardCode]/page.tsx | 4 +- src/components/LanguageSelect.tsx | 4 +- src/components/ThemeSelect.tsx | 4 +- .../accounting/BadDebtCollection/index.tsx | 24 +-- .../accounting/BadDebtCollection/types.ts | 39 +---- .../BankTransactionInquiry/index.tsx | 4 +- .../BillManagement/BillManagementClient.tsx | 8 +- .../accounting/BillManagement/index.tsx | 4 +- .../CardTransactionInquiry/index.tsx | 15 +- .../accounting/DailyReport/types.ts | 14 +- .../accounting/DepositManagement/index.tsx | 40 ++--- .../accounting/DepositManagement/types.ts | 22 +-- .../ExpectedExpenseManagement/index.tsx | 21 +-- .../ExpectedExpenseManagement/types.ts | 19 +-- .../AccountSubjectSettingModal.tsx | 2 +- .../GiftCertificateManagement/index.tsx | 17 +-- .../accounting/PurchaseManagement/index.tsx | 2 +- .../accounting/ReceivablesStatus/index.tsx | 10 +- .../accounting/SalesManagement/index.tsx | 39 +++-- .../accounting/TaxInvoiceIssuance/index.tsx | 8 +- .../accounting/TaxInvoiceManagement/index.tsx | 2 +- .../VendorManagementClient.tsx | 43 ++---- .../accounting/WithdrawalManagement/index.tsx | 40 ++--- src/components/approval/ApprovalBox/index.tsx | 4 +- src/components/approval/DraftBox/index.tsx | 4 +- .../approval/ReferenceBox/index.tsx | 4 +- .../board/BoardManagement/BoardForm.tsx | 2 +- .../CEODashboard/modals/DetailModal.tsx | 2 +- .../CEODashboard/sections/CalendarSection.tsx | 4 +- .../sections/EnhancedSections.tsx | 18 +-- .../common/modals/ElectronicApprovalModal.tsx | 6 +- .../sections/EstimateDetailTableSection.tsx | 12 +- .../HandoverReportDetailForm.tsx | 2 +- .../issue-management/IssueDetailForm.tsx | 2 +- .../management/ProjectKanbanBoard.tsx | 4 +- .../management/ProjectListClient.tsx | 4 +- .../tables/ProgressBillingItemTable.tsx | 2 +- .../common/ScheduleCalendar/MonthView.tsx | 2 +- .../common/ScheduleCalendar/ScheduleBar.tsx | 4 +- .../AttendanceInfoDialog.tsx | 18 +-- .../AttendanceManagement/ReasonInfoDialog.tsx | 4 +- src/components/hr/CardManagement/index.tsx | 4 +- .../SalaryManagement/SalaryDetailDialog.tsx | 2 +- .../InventoryAdjustmentDialog.tsx | 2 +- .../molecules/YearQuarterFilter.tsx | 2 +- .../PriceDistributionDetail.tsx | 2 +- .../process-management/RuleModal.tsx | 4 +- .../InspectionManagement/InspectionList.tsx | 4 +- .../quotes/QuoteManagementClient.tsx | 4 +- .../reports/ComprehensiveAnalysis/index.tsx | 2 +- .../settings/AccountManagement/index.tsx | 4 +- .../AttendanceSettingsManagement/index.tsx | 2 +- .../settings/NotificationSettings/index.tsx | 2 +- .../PaymentHistoryManagement/index.tsx | 2 +- .../templates/IntegratedListTemplateV2.tsx | 2 +- src/lib/utils/search.ts | 139 ++++++++++++++++++ src/lib/utils/status-config.ts | 78 ++++++++++ 68 files changed, 535 insertions(+), 346 deletions(-) create mode 100644 src/lib/utils/search.ts diff --git a/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md b/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md index 12e741e1..868a7cf0 100644 --- a/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md +++ b/claudedocs/[REF-2026-02-19] code-dedup-commonization-checklist.md @@ -375,3 +375,106 @@ Phase 3 (Phase 2 완료 후): | WP-4 → WP-1 | WP-1에서 수정한 날짜 초기값 패턴을 useDateRange 훅 설계에 반영 | | WP-5 → WP-2 | WP-2에서 통일된 formatAmount를 공통 컴포넌트에서 import | | WP-6 → WP-4 | useDateRange가 getTodayString()을 내부 사용하므로 훅 완성 후 적용 | + +--- + +## WP-10: 검색 필터 유틸 search.ts ✅ 완료 (2026-02-20) + +**심각도**: 🟢 MEDIUM (코드 일관성) +**난이도**: 중간 | **신규 파일**: 1개 | **적용 파일**: 9개 + +### 10-A: search.ts 유틸 생성 ✅ + +**위치**: `src/lib/utils/search.ts` (~70줄) +- [x] `filterByText(data, query, fields, options?)` — 텍스트 검색 (case-insensitive 기본) +- [x] `filterByEnum(data, field, value, allValue?)` — enum 필터 ('all' short-circuit) +- [x] `filterByDateRange(data, field, startDate?, endDate?)` — 날짜 범위 필터 +- [x] `applyFilters(data, filters)` — 필터 체이닝 파이프라인 +- [x] `textFilter`, `enumFilter`, `dateRangeFilter` — 팩토리 함수 (applyFilters용) + +### 10-B: 회계 모듈 적용 (9파일) ✅ + +| # | 파일 | 변경 내용 | 상태 | +|---|------|----------|------| +| 1 | `VendorManagement/VendorManagementClient.tsx` | useMemo 체인 → `applyFilters` (text 3필드 + enum 4개) | ✅ | +| 2 | `BadDebtCollection/index.tsx` | customFilterFn → `applyFilters` (enum 2개) | ✅ | +| 3 | `ExpectedExpenseManagement/index.tsx` | useMemo 체인 → `applyFilters` (text 3필드 + enum 1개) | ✅ | +| 4 | `SalesManagement/index.tsx` | customFilterFn → `applyFilters` (enum 2개 + issuance 인라인 유지) | ✅ | +| 5 | `DepositManagement/index.tsx` | customFilterFn → `applyFilters` (text 4필드 + enum 2개) | ✅ | +| 6 | `WithdrawalManagement/index.tsx` | customFilterFn → `applyFilters` (text 4필드 + enum 2개) | ✅ | +| 7 | `CardTransactionInquiry/index.tsx` | filteredData useMemo → `filterByEnum` (1개) | ✅ | +| 8 | `ReceivablesStatus/index.tsx` | filteredData useMemo → `filterByText` (1필드) | ✅ | +| 9 | `GiftCertificateManagement/index.tsx` | customFilterFn → `applyFilters` (enum 2개) | ✅ | + +**스킵**: BillManagement — 서버 사이드 필터링 (API params), 클라이언트 사이드 필터 없음 + +### 검증 +- [x] `npx tsc --noEmit` 통과 (기존 orders/actions.ts 1건 외 에러 0건) + +--- + +## WP-11: 상태 설정 채택 확대 ✅ 완료 (2026-02-20) + +**심각도**: 🟢 MEDIUM (코드 일관성) +**난이도**: 중간 | **수정 파일**: status-config.ts + types.ts 4개 + +### 11-A: 도메인별 상태 설정 추가 (status-config.ts) ✅ + +7개 회계 도메인 설정 추가: +- [x] `BAD_DEBT_COLLECTION_STATUS_CONFIG` (collecting, legalAction, recovered, badDebt) +- [x] `TAX_INVOICE_STATUS_CONFIG` (pending, journalized, error) +- [x] `BILL_STATUS_CONFIG` (stored~dishonored 8개) +- [x] `SALES_STATUS_CONFIG` (monthlyClose, lastMonth, agreed, outstanding) +- [x] `DEPOSIT_STATUS_CONFIG` (inputWaiting~confirmed 7개) +- [x] `PAYMENT_STATUS_CONFIG` (pending, partial, paid, overdue) +- [x] `MATCH_STATUS_CONFIG` (matched, unmatched) + +### 11-B: types.ts 인라인 상수 마이그레이션 (4파일) ✅ + +| # | 파일 | 제거한 인라인 상수 | 대체 | +|---|------|-------------------|------| +| 1 | `BadDebtCollection/types.ts` | COLLECTION_STATUS_LABELS, STATUS_BADGE_STYLES, STATUS_FILTER_OPTIONS, STATUS_SELECT_OPTIONS | `BAD_DEBT_COLLECTION_STATUS_CONFIG` re-export | +| 2 | `ExpectedExpenseManagement/types.ts` | PAYMENT_STATUS_LABELS, PAYMENT_STATUS_FILTER_OPTIONS | `PAYMENT_STATUS_CONFIG` re-export | +| 3 | `DailyReport/types.ts` | MATCH_STATUS_LABELS, MATCH_STATUS_COLORS | `MATCH_STATUS_CONFIG` re-export | +| 4 | `DepositManagement/types.ts` | DEPOSIT_STATUS_LABELS, DEPOSIT_STATUS_COLORS | `DEPOSIT_STATUS_CONFIG` re-export | + +**스킵 (shape 비호환):** +- SalesManagement — `SALES_STATUS_LABELS`에 'all' 키 포함, STATUS_LABELS와 shape 불일치 +- TaxInvoiceManagement — `INVOICE_STATUS_MAP`가 `{label, color}` 결합 형태, 분리하면 더 복잡 +- WithdrawalManagement — WITHDRAWAL_TYPE 설정 미추가 (status가 아닌 type 분류) +- BillManagement — 수취/발행 분기 헬퍼 함수 복잡 + +### 검증 +- [x] `npx tsc --noEmit` 통과 (기존 orders/actions.ts 1건 외 에러 0건) +- [x] types.ts re-export로 소비 컴포넌트 import 경로 변경 불필요 + +--- + +## WP-12: Loading Skeleton 전환 (GenericPageSkeleton → ListPageSkeleton) ✅ 완료 (2026-02-20) + +**심각도**: 🟢 MEDIUM (UX 개선) +**난이도**: 낮음 | **파일 수**: 9 | **예상 변경량**: 각 2줄 (import + JSX) + +### 현황 +회계 모듈 9개 page.tsx가 `GenericPageSkeleton`(form 레이아웃 고정)을 사용하여 실제 리스트 페이지 구조(필터+통계카드+테이블)와 불일치. +이미 `ListPageSkeleton`이 존재하지만 회계 모듈에서 미사용. + +### 수정 완료 (9파일) + +| # | 파일 | props | 상태 | +|---|------|-------|------| +| 1 | `accounting/deposits/page.tsx` | `showStats statsCount={4} tableColumns={7}` | ✅ | +| 2 | `accounting/sales/page.tsx` | `showStats statsCount={4} tableColumns={10}` | ✅ | +| 3 | `accounting/withdrawals/page.tsx` | `showStats statsCount={4} tableColumns={7}` | ✅ | +| 4 | `accounting/bad-debt-collection/page.tsx` | `showDateRange={false} showCreateButton={false} showStats statsCount={4} tableColumns={8}` | ✅ | +| 5 | `accounting/bills/page.tsx` | `tableColumns={9}` | ✅ | +| 6 | `accounting/expected-expenses/page.tsx` | `showStats statsCount={2} tableColumns={7}` | ✅ | +| 7 | `accounting/vendors/page.tsx` | `showDateRange={false} showStats statsCount={3} tableColumns={10}` | ✅ | +| 8 | `accounting/tax-invoice-issuance/page.tsx` | `showStats statsCount={4} tableColumns={10}` | ✅ | +| 9 | `accounting/gift-certificates/page.tsx` | `showStats statsCount={4} tableColumns={8}` | ✅ | + +### 검증 +- [x] `npx tsc --noEmit` 통과 (기존 orders/actions.ts 1건 외 에러 0건) +- [x] `GenericPageSkeleton` 회계 모듈 내 잔존 0건 +- [x] 브라우저 화면 검수: Slow 3G + CPU 20x 쓰로틀링으로 스켈레톤 캡처 확인 +- [x] vendors 페이지 showCreateButton 수정 (화면 검수에서 발견) diff --git a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx index 98707d18..a59937c4 100644 --- a/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx +++ b/src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx @@ -12,7 +12,7 @@ import { useSearchParams } from 'next/navigation'; import { BadDebtCollection, BadDebtDetailClientV2 } from '@/components/accounting/BadDebtCollection'; import { getBadDebts, getBadDebtSummary } from '@/components/accounting/BadDebtCollection/actions'; import type { BadDebtSummary } from '@/components/accounting/BadDebtCollection/types'; -import { GenericPageSkeleton } from '@/components/ui/skeleton'; +import { ListPageSkeleton } from '@/components/ui/skeleton'; const DEFAULT_SUMMARY: BadDebtSummary = { totalCount: 0, @@ -50,7 +50,7 @@ export default function BadDebtCollectionPage() { return ; } - if (isLoading) return ; + if (isLoading) return ; return ( ; } - if (isLoading) return ; + if (isLoading) return ; return ( ; - if (isLoading) return ; + if (isLoading) return ; return ( setIsLoading(false)); }, []); - if (isLoading) return ; + if (isLoading) return ; return ( ; + if (isLoading) return ; return ( ; - if (isLoading) return ; + if (isLoading) return ; return ( setIsLoading(false)); }, [mode, id]); - if (isLoading) return ; + if (isLoading) return ; if (mode === 'edit' && id) { return ( diff --git a/src/app/[locale]/(protected)/accounting/vendors/page.tsx b/src/app/[locale]/(protected)/accounting/vendors/page.tsx index 32295ea0..cc09ac96 100644 --- a/src/app/[locale]/(protected)/accounting/vendors/page.tsx +++ b/src/app/[locale]/(protected)/accounting/vendors/page.tsx @@ -5,7 +5,7 @@ import { useSearchParams } from 'next/navigation'; import { VendorManagement } from '@/components/accounting/VendorManagement'; import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail'; import { getClients } from '@/components/accounting/VendorManagement/actions'; -import { GenericPageSkeleton } from '@/components/ui/skeleton'; +import { ListPageSkeleton } from '@/components/ui/skeleton'; export default function VendorsPage() { const searchParams = useSearchParams(); @@ -32,7 +32,7 @@ export default function VendorsPage() { return ; } - if (isLoading) return ; + if (isLoading) return ; return ( ; - if (isLoading) return ; + if (isLoading) return ; return ( - + @@ -392,7 +392,7 @@ function BoardListContent({ boardCode }: { boardCode: string }) { - +
diff --git a/src/components/ThemeSelect.tsx b/src/components/ThemeSelect.tsx index d1ab2e41..1a2e206e 100644 --- a/src/components/ThemeSelect.tsx +++ b/src/components/ThemeSelect.tsx @@ -29,7 +29,7 @@ export function ThemeSelect({ native = true }: ThemeSelectProps) { // 네이티브 select if (native) { return ( -
+
@@ -56,7 +56,7 @@ export function ThemeSelect({ native = true }: ThemeSelectProps) { // Radix UI 모달 select return ( - + @@ -290,7 +282,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec {/* 상태 필터 */} - + @@ -427,7 +427,7 @@ export function BillManagementClient({ tableHeaderActions: (
- + @@ -350,7 +350,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem {/* 거래처명 필터 */} - + @@ -392,7 +389,7 @@ export function CardTransactionInquiry() { - + @@ -373,7 +357,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
{/* 거래처 필터 */} - + @@ -401,7 +385,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan {/* 정렬 */} - + @@ -942,7 +937,7 @@ export function ExpectedExpenseManagement({ {/* 정렬 필터 (최신순/등록순) */} - + diff --git a/src/components/accounting/GiftCertificateManagement/index.tsx b/src/components/accounting/GiftCertificateManagement/index.tsx index 91fbf4fb..1ba7e593 100644 --- a/src/components/accounting/GiftCertificateManagement/index.tsx +++ b/src/components/accounting/GiftCertificateManagement/index.tsx @@ -47,6 +47,7 @@ import { } from './actions'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { formatNumber as formatAmount } from '@/lib/utils/amount'; +import { applyFilters, enumFilter } from '@/lib/utils/search'; import { useDateRange } from '@/hooks'; // ===== 테이블 컬럼 정의 (체크박스/No. 제외) ===== @@ -201,7 +202,7 @@ export function GiftCertificateManagement() { value={statusFilter} onValueChange={setStatusFilter} > - + @@ -218,7 +219,7 @@ export function GiftCertificateManagement() { value={entertainmentFilter} onValueChange={setEntertainmentFilter} > - + @@ -234,14 +235,10 @@ export function GiftCertificateManagement() { // 클라이언트 사이드 커스텀 필터 (상태 + 접대비) customFilterFn: (items) => { - let filtered = items; - if (statusFilter !== 'all') { - filtered = filtered.filter((item) => item.status === statusFilter); - } - if (entertainmentFilter !== 'all') { - filtered = filtered.filter((item) => item.entertainmentExpense === entertainmentFilter); - } - return filtered; + return applyFilters(items, [ + enumFilter('status', statusFilter), + enumFilter('entertainmentExpense', entertainmentFilter), + ]); }, // 통계 카드 4개 (기획서: 전체 상품권, 보유 상품권, 사용 상품권, 접대비 해당) diff --git a/src/components/accounting/PurchaseManagement/index.tsx b/src/components/accounting/PurchaseManagement/index.tsx index 54fefb30..e1ce96f1 100644 --- a/src/components/accounting/PurchaseManagement/index.tsx +++ b/src/components/accounting/PurchaseManagement/index.tsx @@ -368,7 +368,7 @@ export function PurchaseManagement() {
계정과목명 - + diff --git a/src/components/accounting/TaxInvoiceIssuance/index.tsx b/src/components/accounting/TaxInvoiceIssuance/index.tsx index ac008561..1ce81da4 100644 --- a/src/components/accounting/TaxInvoiceIssuance/index.tsx +++ b/src/components/accounting/TaxInvoiceIssuance/index.tsx @@ -244,7 +244,7 @@ export function TaxInvoiceIssuancePage({ value={filters.dateType} onValueChange={(v) => updateFilter('dateType', v)} > - + @@ -298,7 +298,7 @@ export function TaxInvoiceIssuancePage({ value={filters.status} onValueChange={(v) => updateFilter('status', v)} > - + @@ -314,7 +314,7 @@ export function TaxInvoiceIssuancePage({ value={filters.sortBy} onValueChange={(v) => updateFilter('sortBy', v)} > - + @@ -330,7 +330,7 @@ export function TaxInvoiceIssuancePage({ value={filters.sortOrder} onValueChange={(v) => updateFilter('sortOrder', v)} > - + diff --git a/src/components/accounting/TaxInvoiceManagement/index.tsx b/src/components/accounting/TaxInvoiceManagement/index.tsx index 1bdfe2ca..9be3dd2d 100644 --- a/src/components/accounting/TaxInvoiceManagement/index.tsx +++ b/src/components/accounting/TaxInvoiceManagement/index.tsx @@ -284,7 +284,7 @@ export function TaxInvoiceManagement() { {/* Row1: 일자타입 + 날짜범위 + 분기 버튼 + 조회 */}
- + @@ -464,7 +447,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana {/* 신용등급 필터 */} - + @@ -492,7 +475,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana {/* 악성채권 필터 */} setSortOption(value as SortOption)}> - + diff --git a/src/components/accounting/WithdrawalManagement/index.tsx b/src/components/accounting/WithdrawalManagement/index.tsx index 61effab7..e8c75642 100644 --- a/src/components/accounting/WithdrawalManagement/index.tsx +++ b/src/components/accounting/WithdrawalManagement/index.tsx @@ -70,6 +70,7 @@ import { } from './types'; import { deleteWithdrawal, updateWithdrawalTypes, getWithdrawals } from './actions'; import { formatNumber } from '@/lib/utils/amount'; +import { applyFilters, textFilter, enumFilter } from '@/lib/utils/search'; import { toast } from 'sonner'; import { useDateRange } from '@/hooks'; import { @@ -291,32 +292,13 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra // 검색창 숨김 (dateRangeSelector extraActions로 렌더링) hideSearch: true, - // 커스텀 필터 함수 (검색 + 필터) + // 커스텀 필터 함수 customFilterFn: (items) => { - return items.filter((item) => { - // 검색어 필터 - if (searchQuery) { - const search = searchQuery.toLowerCase(); - const matchesSearch = - item.recipientName.toLowerCase().includes(search) || - item.accountName.toLowerCase().includes(search) || - item.note.toLowerCase().includes(search) || - item.vendorName.toLowerCase().includes(search); - if (!matchesSearch) return false; - } - - // 거래처 필터 - if (vendorFilter !== 'all' && item.vendorName !== vendorFilter) { - return false; - } - - // 출금유형 필터 - if (withdrawalTypeFilter !== 'all' && item.withdrawalType !== withdrawalTypeFilter) { - return false; - } - - return true; - }); + return applyFilters(items, [ + textFilter(searchQuery, ['recipientName', 'accountName', 'note', 'vendorName']), + enumFilter('vendorName', vendorFilter), + enumFilter('withdrawalType', withdrawalTypeFilter), + ]); }, // 커스텀 정렬 함수 @@ -344,7 +326,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
계정과목명 - + @@ -398,7 +380,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra {/* 출금유형 필터 */} setSortOption(value as SortOption)}> - + diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index e932f853..98b5d8af 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -609,7 +609,7 @@ export function ApprovalBox() { value={filterOption} onValueChange={(value) => setFilterOption(value as FilterOption)} > - + @@ -625,7 +625,7 @@ export function ApprovalBox() { value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)} > - + diff --git a/src/components/approval/DraftBox/index.tsx b/src/components/approval/DraftBox/index.tsx index 7086823d..0d4d0f67 100644 --- a/src/components/approval/DraftBox/index.tsx +++ b/src/components/approval/DraftBox/index.tsx @@ -588,7 +588,7 @@ export function DraftBox() { value={filterOption} onValueChange={(value) => setFilterOption(value as FilterOption)} > - + @@ -604,7 +604,7 @@ export function DraftBox() { value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)} > - + diff --git a/src/components/approval/ReferenceBox/index.tsx b/src/components/approval/ReferenceBox/index.tsx index e1a719a2..e3af2a5b 100644 --- a/src/components/approval/ReferenceBox/index.tsx +++ b/src/components/approval/ReferenceBox/index.tsx @@ -396,7 +396,7 @@ export function ReferenceBox() {
{/* 필터 셀렉트박스 */} setSortOption(value as SortOption)}> - + diff --git a/src/components/board/BoardManagement/BoardForm.tsx b/src/components/board/BoardManagement/BoardForm.tsx index 1cd4d3e8..a11b6331 100644 --- a/src/components/board/BoardManagement/BoardForm.tsx +++ b/src/components/board/BoardManagement/BoardForm.tsx @@ -145,7 +145,7 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) { value={formData.target} onValueChange={(value) => handleTargetChange(value as BoardTarget)} > - + diff --git a/src/components/business/CEODashboard/modals/DetailModal.tsx b/src/components/business/CEODashboard/modals/DetailModal.tsx index d8a0657d..9c2d54c1 100644 --- a/src/components/business/CEODashboard/modals/DetailModal.tsx +++ b/src/components/business/CEODashboard/modals/DetailModal.tsx @@ -563,7 +563,7 @@ const TableSection = ({ config }: { config: TableConfig }) => { value={filters[filter.key]} onValueChange={(value) => handleFilterChange(filter.key, value)} > - + diff --git a/src/components/business/CEODashboard/sections/CalendarSection.tsx b/src/components/business/CEODashboard/sections/CalendarSection.tsx index e215855f..9ac361f8 100644 --- a/src/components/business/CEODashboard/sections/CalendarSection.tsx +++ b/src/components/business/CEODashboard/sections/CalendarSection.tsx @@ -244,7 +244,7 @@ export function CalendarSection({ value={deptFilter} onValueChange={(value) => setDeptFilter(value as CalendarDeptFilterType)} > - + @@ -261,7 +261,7 @@ export function CalendarSection({ value={taskFilter} onValueChange={(value) => setTaskFilter(value as CalendarTaskFilterType)} > - + diff --git a/src/components/business/CEODashboard/sections/EnhancedSections.tsx b/src/components/business/CEODashboard/sections/EnhancedSections.tsx index 84d9ef29..7c22fe03 100644 --- a/src/components/business/CEODashboard/sections/EnhancedSections.tsx +++ b/src/components/business/CEODashboard/sections/EnhancedSections.tsx @@ -82,7 +82,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor {/* 카드 1: 현금성 자산 */}
@@ -114,7 +114,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor {/* 카드 2: 외국환 */}
@@ -148,7 +148,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor {/* 카드 3: 입금 */}
@@ -180,7 +180,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor {/* 카드 4: 출금 */}
@@ -329,7 +329,7 @@ export function EnhancedStatusBoardSection({ items, itemSettings }: EnhancedStat
handleItemClick(item.path)} > {/* 아이콘 + 라벨 */} @@ -401,7 +401,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon {/* 카드 1: 매입 */}
onCardClick?.(data.cards[0]?.id || 'me1')} >
@@ -426,7 +426,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon {/* 카드 2: 카드 */}
onCardClick?.(data.cards[1]?.id || 'me2')} >
@@ -451,7 +451,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon {/* 카드 3: 발행어음 */}
onCardClick?.(data.cards[2]?.id || 'me3')} >
@@ -476,7 +476,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon {/* 카드 4: 총 예상 지출 합계 (강조 - 인라인 스타일) */}
onCardClick?.(data.cards[3]?.id || 'me4')} >
diff --git a/src/components/business/construction/common/modals/ElectronicApprovalModal.tsx b/src/components/business/construction/common/modals/ElectronicApprovalModal.tsx index c2909366..9288a144 100644 --- a/src/components/business/construction/common/modals/ElectronicApprovalModal.tsx +++ b/src/components/business/construction/common/modals/ElectronicApprovalModal.tsx @@ -161,7 +161,7 @@ export function ElectronicApprovalModal({ value={person.department || undefined} onValueChange={(val) => onChange(person.id, 'department', val)} > - + @@ -177,7 +177,7 @@ export function ElectronicApprovalModal({ value={person.position || undefined} onValueChange={(val) => onChange(person.id, 'position', val)} > - + @@ -193,7 +193,7 @@ export function ElectronicApprovalModal({ value={person.name || undefined} onValueChange={(val) => onChange(person.id, 'name', val)} > - + diff --git a/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx b/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx index 79b77466..34f72779 100644 --- a/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx +++ b/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx @@ -346,7 +346,7 @@ export function EstimateDetailTableSection({ onValueChange={(val) => onItemChange(item.id, 'material', val)} disabled={isViewMode} > - + @@ -463,7 +463,7 @@ export function EstimateDetailTableSection({ onValueChange={(val) => onItemChange(item.id, 'coating', Number(val))} disabled={isViewMode} > - + @@ -482,7 +482,7 @@ export function EstimateDetailTableSection({ onValueChange={(val) => onItemChange(item.id, 'mounting', Number(val))} disabled={isViewMode} > - + @@ -501,7 +501,7 @@ export function EstimateDetailTableSection({ onValueChange={(val) => onItemChange(item.id, 'controller', Number(val))} disabled={isViewMode} > - + @@ -520,7 +520,7 @@ export function EstimateDetailTableSection({ onValueChange={(val) => onItemChange(item.id, 'widthConstruction', Number(val))} disabled={isViewMode} > - + @@ -539,7 +539,7 @@ export function EstimateDetailTableSection({ onValueChange={(val) => onItemChange(item.id, 'heightConstruction', Number(val))} disabled={isViewMode} > - + diff --git a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx index 1ee41bc5..8e48ae55 100644 --- a/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx +++ b/src/components/business/construction/handover-report/HandoverReportDetailForm.tsx @@ -410,7 +410,7 @@ export default function HandoverReportDetailForm({ value={manager.name} onValueChange={(value) => handleManagerChange(manager.id, 'name', value)} > - + diff --git a/src/components/business/construction/issue-management/IssueDetailForm.tsx b/src/components/business/construction/issue-management/IssueDetailForm.tsx index b8150881..6b8074ab 100644 --- a/src/components/business/construction/issue-management/IssueDetailForm.tsx +++ b/src/components/business/construction/issue-management/IssueDetailForm.tsx @@ -432,7 +432,7 @@ export default function IssueDetailForm({ issue, mode = 'view' }: IssueDetailFor onValueChange={(value) => handleSelectChange('status')(value as IssueStatus)} disabled={isReadOnly} > - + diff --git a/src/components/business/construction/management/ProjectKanbanBoard.tsx b/src/components/business/construction/management/ProjectKanbanBoard.tsx index f496baf9..e8321b36 100644 --- a/src/components/business/construction/management/ProjectKanbanBoard.tsx +++ b/src/components/business/construction/management/ProjectKanbanBoard.tsx @@ -145,7 +145,7 @@ export default function ProjectKanbanBoard({ {/* 필터 영역 */}
{ setStatusFilter(v); setCurrentPage(1); }}> - + @@ -449,7 +449,7 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr {/* 정렬 */} - + @@ -175,7 +175,7 @@ export function CardManagement() { - + diff --git a/src/components/molecules/YearQuarterFilter.tsx b/src/components/molecules/YearQuarterFilter.tsx index 8589affc..4835de66 100644 --- a/src/components/molecules/YearQuarterFilter.tsx +++ b/src/components/molecules/YearQuarterFilter.tsx @@ -67,7 +67,7 @@ export function YearQuarterFilter({ return (
- + diff --git a/src/components/process-management/RuleModal.tsx b/src/components/process-management/RuleModal.tsx index 37a9f44e..d6a82f67 100644 --- a/src/components/process-management/RuleModal.tsx +++ b/src/components/process-management/RuleModal.tsx @@ -243,7 +243,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc
- + @@ -193,7 +193,7 @@ export function InspectionList() { - + @@ -324,7 +324,7 @@ export function QuoteManagementClient({ {/* 상태 필터 */} - + diff --git a/src/components/settings/AccountManagement/index.tsx b/src/components/settings/AccountManagement/index.tsx index 3fe8d250..f3827391 100644 --- a/src/components/settings/AccountManagement/index.tsx +++ b/src/components/settings/AccountManagement/index.tsx @@ -252,7 +252,7 @@ export function AccountManagement() { tableHeaderActions: (
setSortOption(value as SortOption)}> - // + // // // // diff --git a/src/components/templates/IntegratedListTemplateV2.tsx b/src/components/templates/IntegratedListTemplateV2.tsx index e5406062..42b901f6 100644 --- a/src/components/templates/IntegratedListTemplateV2.tsx +++ b/src/components/templates/IntegratedListTemplateV2.tsx @@ -498,7 +498,7 @@ export function IntegratedListTemplateV2({ value={(filterValues[field.key] as string) || 'all'} onValueChange={(value) => onFilterChange(field.key, value)} > - + diff --git a/src/lib/utils/search.ts b/src/lib/utils/search.ts new file mode 100644 index 00000000..b3c759f3 --- /dev/null +++ b/src/lib/utils/search.ts @@ -0,0 +1,139 @@ +/** + * Search & Filter Utilities + * + * 클라이언트 사이드 검색/필터링 공통 유틸리티. + * useMemo 내부의 반복적인 .filter() 체인을 선언적 파이프라인으로 대체. + * + * @example + * const filtered = useMemo(() => applyFilters(data, [ + * textFilter(searchQuery, ['vendorName', 'vendorCode', 'businessNumber']), + * enumFilter('category', categoryFilter), + * enumFilter('status', statusFilter), + * ]), [data, searchQuery, categoryFilter, statusFilter]); + */ + +// ===== Types ===== + +/** 필터 함수 타입 */ +export type FilterFn = (data: T[]) => T[]; + +/** 텍스트 검색 옵션 */ +export interface TextFilterOptions { + /** 대소문자 구분 여부 (기본값: false = case-insensitive) */ + caseSensitive?: boolean; +} + +// ===== Core Filter Functions ===== + +/** + * 텍스트 검색 필터 + * + * 빈 query는 전체 반환 (short-circuit). + * 기본 case-insensitive (한글에는 영향 없음). + */ +export function filterByText( + data: T[], + query: string, + fields: (keyof T & string)[], + options?: TextFilterOptions +): T[] { + if (!query || query.trim() === '') return data; + + const caseSensitive = options?.caseSensitive ?? false; + const normalizedQuery = caseSensitive ? query : query.toLowerCase(); + + return data.filter((item) => + fields.some((field) => { + const value = item[field]; + if (value == null) return false; + const str = String(value); + return caseSensitive ? str.includes(normalizedQuery) : str.toLowerCase().includes(normalizedQuery); + }) + ); +} + +/** + * Enum 필터 (단일 값 매칭) + * + * allValue와 일치하면 전체 반환 (short-circuit). + */ +export function filterByEnum( + data: T[], + field: keyof T & string, + value: string, + allValue: string = 'all' +): T[] { + if (value === allValue) return data; + return data.filter((item) => String(item[field]) === value); +} + +/** + * 날짜 범위 필터 (YYYY-MM-DD 문자열 비교) + * + * startDate, endDate 모두 없으면 전체 반환. + */ +export function filterByDateRange( + data: T[], + field: keyof T & string, + startDate?: string, + endDate?: string +): T[] { + if (!startDate && !endDate) return data; + + return data.filter((item) => { + const dateValue = String(item[field] ?? ''); + if (!dateValue) return false; + if (startDate && dateValue < startDate) return false; + if (endDate && dateValue > endDate) return false; + return true; + }); +} + +// ===== Pipeline ===== + +/** + * 필터 파이프라인 + * + * 여러 필터 함수를 순차 적용. 각 필터는 빈 데이터에 대해 short-circuit. + */ +export function applyFilters(data: T[], filters: FilterFn[]): T[] { + return filters.reduce((result, filter) => { + if (result.length === 0) return result; + return filter(result); + }, data); +} + +// ===== Factory Functions (for applyFilters) ===== + +/** + * textFilter 팩토리 — applyFilters용 + */ +export function textFilter( + query: string, + fields: (keyof T & string)[], + options?: TextFilterOptions +): FilterFn { + return (data) => filterByText(data, query, fields, options); +} + +/** + * enumFilter 팩토리 — applyFilters용 + */ +export function enumFilter( + field: keyof T & string, + value: string, + allValue: string = 'all' +): FilterFn { + return (data) => filterByEnum(data, field, value, allValue); +} + +/** + * dateRangeFilter 팩토리 — applyFilters용 + */ +export function dateRangeFilter( + field: keyof T & string, + startDate?: string, + endDate?: string +): FilterFn { + return (data) => filterByDateRange(data, field, startDate, endDate); +} diff --git a/src/lib/utils/status-config.ts b/src/lib/utils/status-config.ts index 5b387e6a..b92e09ae 100644 --- a/src/lib/utils/status-config.ts +++ b/src/lib/utils/status-config.ts @@ -314,4 +314,82 @@ export const RECEIVING_STATUS_CONFIG = createStatusConfig({ rejected: { label: '반품', style: 'destructive' }, }, { includeAll: true }); +// ============================================================ +// 회계 도메인 상태 설정 +// ============================================================ + +/** + * 악성채권 추심 상태 (커스텀 border 스타일) + */ +export const BAD_DEBT_COLLECTION_STATUS_CONFIG = createStatusConfig({ + collecting: { label: '추심중', style: 'border-orange-300 text-orange-600 bg-orange-50' }, + legalAction: { label: '법적조치', style: 'border-red-300 text-red-600 bg-red-50' }, + recovered: { label: '회수완료', style: 'border-green-300 text-green-600 bg-green-50' }, + badDebt: { label: '대손처리', style: 'border-gray-300 text-gray-600 bg-gray-50' }, +}, { includeAll: true }); + +/** + * 세금계산서 상태 + */ +export const TAX_INVOICE_STATUS_CONFIG = createStatusConfig({ + pending: { label: '미분개', style: 'bg-yellow-100 text-yellow-700' }, + journalized: { label: '분개완료', style: 'bg-green-100 text-green-700' }, + error: { label: '오류', style: 'bg-red-100 text-red-700' }, +}); + +/** + * 어음 상태 (수취/발행 공통 색상) + */ +export const BILL_STATUS_CONFIG = createStatusConfig({ + stored: { label: '보관중', style: 'info' }, + maturityAlert: { label: '만기입금(7일전)', style: 'warning' }, + maturityResult: { label: '만기결과', style: 'orange' }, + paymentComplete: { label: '결제완료', style: 'success' }, + collectionRequest: { label: '추심의뢰', style: 'purple' }, + collectionComplete: { label: '추심완료', style: 'bg-teal-100 text-teal-800' }, + suing: { label: '추소중', style: 'destructive' }, + dishonored: { label: '부도', style: 'muted' }, +}); + +/** + * 매출 상태 + */ +export const SALES_STATUS_CONFIG = createStatusConfig({ + monthlyClose: { label: '당월마감', style: 'success' }, + lastMonth: { label: '전월', style: 'default' }, + agreed: { label: '합의', style: 'info' }, + outstanding: { label: '미수', style: 'destructive' }, +}, { includeAll: true }); + +/** + * 입금 상태 + */ +export const DEPOSIT_STATUS_CONFIG = createStatusConfig({ + inputWaiting: { label: '입력대기', style: 'warning' }, + requesting: { label: '신청중', style: 'info' }, + rejected: { label: '반려', style: 'destructive' }, + pending: { label: '보류', style: 'default' }, + incomplete: { label: '미완', style: 'orange' }, + error: { label: '오류', style: 'destructive' }, + confirmed: { label: '확정완료', style: 'success' }, +}); + +/** + * 지급 상태 (지출 예상 내역) + */ +export const PAYMENT_STATUS_CONFIG = createStatusConfig({ + pending: { label: '미지급', style: 'warning' }, + partial: { label: '부분지급', style: 'orange' }, + paid: { label: '지급완료', style: 'success' }, + overdue: { label: '연체', style: 'destructive' }, +}, { includeAll: true }); + +/** + * 매칭 상태 (일일일보) + */ +export const MATCH_STATUS_CONFIG = createStatusConfig({ + matched: { label: '매칭', style: 'bg-green-100 text-green-700' }, + unmatched: { label: '비매칭', style: 'bg-orange-100 text-orange-700' }, +}); + export default createStatusConfig;