refactor(WEB): 회계/결재/건설 등 공통화 3차 및 검색/상태 유틸 추가
- search.ts: 범용 검색 유틸리티 추출 (텍스트/날짜/상태 필터링) - status-config.ts: 상태 설정 공통 유틸 추가 - 회계 모듈 types 간소화 및 컬럼 설정 공통 패턴 적용 - 회계 page.tsx 통일 (bad-debt/bills/deposits/sales 등 9개) - 결재함(승인/기안/참조) 공통 패턴 적용 - 건설 모듈 견적/인수인계/이슈/기성 등 코드 정리 - IntegratedListTemplateV2 개선 - LanguageSelect/ThemeSelect 정리 - 체크리스트 문서 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<T>(data, query, fields, options?)` — 텍스트 검색 (case-insensitive 기본)
|
||||
- [x] `filterByEnum<T>(data, field, value, allValue?)` — enum 필터 ('all' short-circuit)
|
||||
- [x] `filterByDateRange<T>(data, field, startDate?, endDate?)` — 날짜 범위 필터
|
||||
- [x] `applyFilters<T>(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 수정 (화면 검수에서 발견)
|
||||
|
||||
@@ -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 <BadDebtDetailClientV2 />;
|
||||
}
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showDateRange={false} showCreateButton={false} showStats statsCount={4} tableColumns={8} />;
|
||||
|
||||
return (
|
||||
<BadDebtCollection
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BillManagementClient } from '@/components/accounting/BillManagement/Bil
|
||||
import { BillDetail } from '@/components/accounting/BillManagement/BillDetail';
|
||||
import { getBills } from '@/components/accounting/BillManagement/actions';
|
||||
import type { BillRecord } from '@/components/accounting/BillManagement/types';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
@@ -46,7 +46,7 @@ export default function BillsPage() {
|
||||
return <BillDetail billId="new" mode="new" />;
|
||||
}
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton tableColumns={9} />;
|
||||
|
||||
return (
|
||||
<BillManagementClient
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { DepositManagement } from '@/components/accounting/DepositManagement';
|
||||
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
|
||||
import { getDeposits } from '@/components/accounting/DepositManagement/actions';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { useAccountingListPage } from '@/hooks';
|
||||
|
||||
export default function DepositsPage() {
|
||||
@@ -12,7 +12,7 @@ export default function DepositsPage() {
|
||||
);
|
||||
|
||||
if (mode === 'new') return <DepositDetailClientV2 initialMode="create" />;
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={4} tableColumns={7} />;
|
||||
|
||||
return (
|
||||
<DepositManagement
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ExpectedExpenseManagement } from '@/components/accounting/ExpectedExpenseManagement';
|
||||
import { getExpectedExpenses } from '@/components/accounting/ExpectedExpenseManagement/actions';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const DEFAULT_PAGINATION = {
|
||||
currentPage: 1,
|
||||
@@ -31,7 +31,7 @@ export default function ExpectedExpensesPage() {
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={2} tableColumns={7} />;
|
||||
|
||||
return (
|
||||
<ExpectedExpenseManagement
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GiftCertificateManagement } from '@/components/accounting/GiftCertifica
|
||||
import { GiftCertificateDetail } from '@/components/accounting/GiftCertificateManagement/GiftCertificateDetail';
|
||||
import { getGiftCertificateById } from '@/components/accounting/GiftCertificateManagement/actions';
|
||||
import type { GiftCertificateFormData } from '@/components/accounting/GiftCertificateManagement/types';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export default function GiftCertificatesPage() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -34,7 +34,7 @@ export default function GiftCertificatesPage() {
|
||||
}
|
||||
|
||||
if (mode === 'edit' && id) {
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={4} tableColumns={8} />;
|
||||
return (
|
||||
<GiftCertificateDetail
|
||||
mode="edit"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { SalesManagement } from '@/components/accounting/SalesManagement';
|
||||
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
|
||||
import { getSales } from '@/components/accounting/SalesManagement/actions';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { useAccountingListPage } from '@/hooks';
|
||||
|
||||
export default function SalesPage() {
|
||||
@@ -12,7 +12,7 @@ export default function SalesPage() {
|
||||
);
|
||||
|
||||
if (mode === 'new') return <SalesDetail mode="new" />;
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={4} tableColumns={10} />;
|
||||
|
||||
return (
|
||||
<SalesManagement
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@/components/accounting/TaxInvoiceIssuance/actions';
|
||||
import type { TaxInvoiceRecord, TaxInvoiceFormData, SupplierSettings } from '@/components/accounting/TaxInvoiceIssuance/types';
|
||||
import { createEmptyBusinessEntity } from '@/components/accounting/TaxInvoiceIssuance/types';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
type TaxInvoiceDetailData = TaxInvoiceFormData & {
|
||||
id: string;
|
||||
@@ -53,7 +53,7 @@ export default function TaxInvoiceIssuanceRoute() {
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [mode, id]);
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={4} tableColumns={10} />;
|
||||
|
||||
if (mode === 'edit' && id) {
|
||||
return (
|
||||
|
||||
@@ -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 <VendorDetail mode="new" />;
|
||||
}
|
||||
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showDateRange={false} showStats statsCount={3} tableColumns={10} />;
|
||||
|
||||
return (
|
||||
<VendorManagement
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement';
|
||||
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
|
||||
import { getWithdrawals } from '@/components/accounting/WithdrawalManagement/actions';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { useAccountingListPage } from '@/hooks';
|
||||
|
||||
export default function WithdrawalsPage() {
|
||||
@@ -12,7 +12,7 @@ export default function WithdrawalsPage() {
|
||||
);
|
||||
|
||||
if (mode === 'new') return <WithdrawalDetailClientV2 initialMode="create" />;
|
||||
if (isLoading) return <GenericPageSkeleton />;
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={4} tableColumns={7} />;
|
||||
|
||||
return (
|
||||
<WithdrawalManagement
|
||||
|
||||
@@ -380,7 +380,7 @@ function BoardListContent({ boardCode }: { boardCode: string }) {
|
||||
value={statusFilter}
|
||||
onValueChange={setStatusFilter}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -392,7 +392,7 @@ function BoardListContent({ boardCode }: { boardCode: string }) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={sortOption} onValueChange={setSortOption}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -389,7 +389,7 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) {
|
||||
value={statusFilter}
|
||||
onValueChange={setStatusFilter}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -401,7 +401,7 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={sortOption} onValueChange={setSortOption}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function LanguageSelect({ native = true }: LanguageSelectProps) {
|
||||
// 네이티브 select
|
||||
if (native) {
|
||||
return (
|
||||
<div className="relative w-[140px]">
|
||||
<div className="relative min-w-[140px] w-auto">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-10">
|
||||
<Globe className="w-4 h-4" />
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ export function LanguageSelect({ native = true }: LanguageSelectProps) {
|
||||
// Radix UI 모달 select
|
||||
return (
|
||||
<Select value={locale} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger className="w-[140px] rounded-xl border-border/50 bg-background/50 backdrop-blur">
|
||||
<SelectTrigger className="min-w-[140px] w-auto rounded-xl border-border/50 bg-background/50 backdrop-blur">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
<SelectValue>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function ThemeSelect({ native = true }: ThemeSelectProps) {
|
||||
// 네이티브 select
|
||||
if (native) {
|
||||
return (
|
||||
<div className="relative w-[140px]">
|
||||
<div className="relative min-w-[140px] w-auto">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-10">
|
||||
<CurrentIcon className={`w-4 h-4 ${currentTheme?.color}`} />
|
||||
</div>
|
||||
@@ -56,7 +56,7 @@ export function ThemeSelect({ native = true }: ThemeSelectProps) {
|
||||
// Radix UI 모달 select
|
||||
return (
|
||||
<Select value={theme} onValueChange={(value) => setTheme(value as "light" | "dark" | "senior")}>
|
||||
<SelectTrigger className="w-[140px] rounded-xl border-border/50 bg-background/50 backdrop-blur">
|
||||
<SelectTrigger className="min-w-[140px] w-auto rounded-xl border-border/50 bg-background/50 backdrop-blur">
|
||||
<div className="flex items-center gap-2">
|
||||
<CurrentIcon className={`w-4 h-4 ${currentTheme?.color}`} />
|
||||
<SelectValue>
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
SORT_OPTIONS,
|
||||
} from './types';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { applyFilters, enumFilter } from '@/lib/utils/search';
|
||||
import { deleteBadDebt, toggleBadDebt } from './actions';
|
||||
|
||||
// ===== 테이블 컬럼 정의 =====
|
||||
@@ -235,19 +236,10 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
// 커스텀 필터 함수
|
||||
customFilterFn: (items) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
let result = [...items];
|
||||
|
||||
// 거래처 필터
|
||||
if (vendorFilter !== 'all') {
|
||||
result = result.filter((item) => item.vendorId === vendorFilter);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (statusFilter !== 'all') {
|
||||
result = result.filter((item) => item.status === statusFilter);
|
||||
}
|
||||
|
||||
return result;
|
||||
return applyFilters([...items], [
|
||||
enumFilter('vendorId', vendorFilter),
|
||||
enumFilter('status', statusFilter),
|
||||
]);
|
||||
},
|
||||
|
||||
// 커스텀 정렬 함수
|
||||
@@ -275,7 +267,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectTrigger className="min-w-[150px] w-auto">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -290,7 +282,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
|
||||
{/* 상태 필터 */}
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -307,7 +299,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
value={sortOption}
|
||||
onValueChange={(value) => setSortOption(value as SortOption)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -88,38 +88,15 @@ export interface BadDebtRecord {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ===== 상태 라벨 =====
|
||||
export const COLLECTION_STATUS_LABELS: Record<CollectionStatus, string> = {
|
||||
collecting: '추심중',
|
||||
legalAction: '법적조치',
|
||||
recovered: '회수완료',
|
||||
badDebt: '대손처리',
|
||||
};
|
||||
// ===== 상태 설정 (status-config 기반) =====
|
||||
import { BAD_DEBT_COLLECTION_STATUS_CONFIG } from '@/lib/utils/status-config';
|
||||
|
||||
// ===== 상태 필터 옵션 =====
|
||||
export const STATUS_FILTER_OPTIONS = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'collecting', label: '추심중' },
|
||||
{ value: 'legalAction', label: '법적조치' },
|
||||
{ value: 'recovered', label: '회수완료' },
|
||||
{ value: 'badDebt', label: '대손처리' },
|
||||
] as const;
|
||||
|
||||
// ===== 상태 셀렉트 옵션 (상세 페이지용, 전체 제외) =====
|
||||
export const STATUS_SELECT_OPTIONS = [
|
||||
{ value: 'collecting', label: '추심중' },
|
||||
{ value: 'legalAction', label: '법적조치' },
|
||||
{ value: 'recovered', label: '회수완료' },
|
||||
{ value: 'badDebt', label: '대손처리' },
|
||||
] as const;
|
||||
|
||||
// ===== 상태 Badge 스타일 =====
|
||||
export const STATUS_BADGE_STYLES: Record<CollectionStatus, string> = {
|
||||
collecting: 'border-orange-300 text-orange-600 bg-orange-50',
|
||||
legalAction: 'border-red-300 text-red-600 bg-red-50',
|
||||
recovered: 'border-green-300 text-green-600 bg-green-50',
|
||||
badDebt: 'border-gray-300 text-gray-600 bg-gray-50',
|
||||
};
|
||||
export const COLLECTION_STATUS_LABELS = BAD_DEBT_COLLECTION_STATUS_CONFIG.STATUS_LABELS;
|
||||
export const STATUS_FILTER_OPTIONS = BAD_DEBT_COLLECTION_STATUS_CONFIG.STATUS_OPTIONS;
|
||||
export const STATUS_SELECT_OPTIONS = BAD_DEBT_COLLECTION_STATUS_CONFIG.STATUS_OPTIONS.filter(
|
||||
(o): o is { value: CollectionStatus; label: string } => o.value !== 'all'
|
||||
);
|
||||
export const STATUS_BADGE_STYLES = BAD_DEBT_COLLECTION_STATUS_CONFIG.STATUS_STYLES;
|
||||
|
||||
// ===== 정렬 옵션 =====
|
||||
export const SORT_OPTIONS = [
|
||||
|
||||
@@ -373,7 +373,7 @@ export function BankTransactionInquiry() {
|
||||
value={accountCategoryFilter}
|
||||
onValueChange={(v) => setAccountCategoryFilter(v as AccountCategoryFilter)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -390,7 +390,7 @@ export function BankTransactionInquiry() {
|
||||
value={financialInstitutionFilter}
|
||||
onValueChange={setFinancialInstitutionFilter}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="금융기관" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -405,7 +405,7 @@ export function BillManagementClient({
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -427,7 +427,7 @@ export function BillManagementClient({
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="거래처명" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -440,7 +440,7 @@ export function BillManagementClient({
|
||||
</Select>
|
||||
|
||||
<Select value={billTypeFilter} onValueChange={(value) => { setBillTypeFilter(value); loadData(1); }}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -453,7 +453,7 @@ export function BillManagementClient({
|
||||
</Select>
|
||||
|
||||
<Select value={statusFilter} onValueChange={(value) => { setStatusFilter(value); loadData(1); }}>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectTrigger className="min-w-[110px] w-auto">
|
||||
<SelectValue placeholder="보관중" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -305,7 +305,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 상태 필터 */}
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectTrigger className="min-w-[110px] w-auto">
|
||||
<SelectValue placeholder="보관중" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -350,7 +350,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
|
||||
{/* 거래처명 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="거래처명" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -54,6 +54,7 @@ import { ManualInputModal } from './ManualInputModal';
|
||||
import { JournalEntryModal } from './JournalEntryModal';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { filterByEnum } from '@/lib/utils/search';
|
||||
|
||||
// ===== 테이블 컬럼 정의 (체크박스/No. 제외 15개) =====
|
||||
const tableColumns = [
|
||||
@@ -177,11 +178,7 @@ export function CardTransactionInquiry() {
|
||||
|
||||
// ===== 필터링된 데이터 =====
|
||||
const filteredData = useMemo(() => {
|
||||
let result = [...data];
|
||||
if (cardFilter !== 'all') {
|
||||
result = result.filter(item => item.cardName === cardFilter);
|
||||
}
|
||||
return result;
|
||||
return filterByEnum(data, 'cardName', cardFilter);
|
||||
}, [data, cardFilter]);
|
||||
|
||||
// ===== 인라인 편집 핸들러 =====
|
||||
@@ -382,7 +379,7 @@ export function CardTransactionInquiry() {
|
||||
)}
|
||||
</Button>
|
||||
<Select value={cardFilter} onValueChange={setCardFilter}>
|
||||
<SelectTrigger className="w-[120px] h-8 text-sm">
|
||||
<SelectTrigger className="min-w-[120px] w-auto h-8 text-sm">
|
||||
<SelectValue placeholder="카드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -392,7 +389,7 @@ export function CardTransactionInquiry() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={sortOption} onValueChange={(v) => setSortOption(v as SortOption)}>
|
||||
<SelectTrigger className="w-[110px] h-8 text-sm">
|
||||
<SelectTrigger className="min-w-[110px] w-auto h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -489,7 +486,7 @@ export function CardTransactionInquiry() {
|
||||
value={getEditValue(item.id, 'deductionType', item.deductionType)}
|
||||
onValueChange={(v) => handleInlineEdit(item.id, 'deductionType', v)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs w-[88px]">
|
||||
<SelectTrigger className="h-7 text-xs min-w-[88px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -547,7 +544,7 @@ export function CardTransactionInquiry() {
|
||||
value={getEditValue(item.id, 'accountSubject', item.accountSubject) || 'none'}
|
||||
onValueChange={(v) => handleInlineEdit(item.id, 'accountSubject', v === 'none' ? '' : v)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs w-[90px]">
|
||||
<SelectTrigger className="h-7 text-xs min-w-[90px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -37,17 +37,11 @@ export interface DailyAccountItem {
|
||||
currency: 'KRW' | 'USD';
|
||||
}
|
||||
|
||||
/** 매칭 상태 라벨 */
|
||||
export const MATCH_STATUS_LABELS: Record<MatchStatus, string> = {
|
||||
matched: '매칭',
|
||||
unmatched: '비매칭',
|
||||
};
|
||||
/** 매칭 상태 설정 (status-config 기반) */
|
||||
import { MATCH_STATUS_CONFIG } from '@/lib/utils/status-config';
|
||||
|
||||
/** 매칭 상태 색상 */
|
||||
export const MATCH_STATUS_COLORS: Record<MatchStatus, string> = {
|
||||
matched: 'bg-green-100 text-green-700',
|
||||
unmatched: 'bg-orange-100 text-orange-700',
|
||||
};
|
||||
export const MATCH_STATUS_LABELS = MATCH_STATUS_CONFIG.STATUS_LABELS;
|
||||
export const MATCH_STATUS_COLORS = MATCH_STATUS_CONFIG.STATUS_STYLES;
|
||||
|
||||
/**
|
||||
* 일일 일보 요약 데이터
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
} from './types';
|
||||
import { deleteDeposit, updateDepositTypes, getDeposits } 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 {
|
||||
@@ -254,31 +255,14 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
);
|
||||
},
|
||||
|
||||
// 커스텀 필터 함수 (인라인 필터 사용)
|
||||
customFilterFn: (items, filterValues) => {
|
||||
// 커스텀 필터 함수
|
||||
customFilterFn: (items) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
return items.filter((item) => {
|
||||
// 검색어 필터
|
||||
if (searchQuery) {
|
||||
const search = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
item.depositorName.toLowerCase().includes(search) ||
|
||||
item.accountName.toLowerCase().includes(search) ||
|
||||
(item.note?.toLowerCase().includes(search) || false) ||
|
||||
(item.vendorName?.toLowerCase().includes(search) || false);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// 거래처 필터
|
||||
if (vendorFilter !== 'all' && item.vendorName !== vendorFilter) {
|
||||
return false;
|
||||
}
|
||||
// 입금유형 필터
|
||||
if (depositTypeFilter !== 'all' && item.depositType !== depositTypeFilter) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return applyFilters(items, [
|
||||
textFilter(searchQuery, ['depositorName', 'accountName', 'note', 'vendorName']),
|
||||
enumFilter('vendorName', vendorFilter),
|
||||
enumFilter('depositType', depositTypeFilter),
|
||||
]);
|
||||
},
|
||||
|
||||
// 커스텀 정렬 함수
|
||||
@@ -326,7 +310,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -373,7 +357,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="거래처" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -387,7 +371,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
|
||||
{/* 입금유형 필터 */}
|
||||
<Select value={depositTypeFilter} onValueChange={setDepositTypeFilter}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectTrigger className="min-w-[130px] w-auto">
|
||||
<SelectValue placeholder="입금유형" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -401,7 +385,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
|
||||
{/* 정렬 */}
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -69,25 +69,11 @@ export type DepositStatus =
|
||||
| 'error' // 오류
|
||||
| 'confirmed'; // 확정완료
|
||||
|
||||
export const DEPOSIT_STATUS_LABELS: Record<DepositStatus, string> = {
|
||||
inputWaiting: '입력대기',
|
||||
requesting: '신청중',
|
||||
rejected: '반려',
|
||||
pending: '보류',
|
||||
incomplete: '미완',
|
||||
error: '오류',
|
||||
confirmed: '확정완료',
|
||||
};
|
||||
// 입금 상태 설정 (status-config 기반)
|
||||
import { DEPOSIT_STATUS_CONFIG } from '@/lib/utils/status-config';
|
||||
|
||||
export const DEPOSIT_STATUS_COLORS: Record<DepositStatus, string> = {
|
||||
inputWaiting: 'bg-yellow-100 text-yellow-800',
|
||||
requesting: 'bg-blue-100 text-blue-800',
|
||||
rejected: 'bg-red-100 text-red-800',
|
||||
pending: 'bg-gray-100 text-gray-800',
|
||||
incomplete: 'bg-orange-100 text-orange-800',
|
||||
error: 'bg-red-100 text-red-800',
|
||||
confirmed: 'bg-green-100 text-green-800',
|
||||
};
|
||||
export const DEPOSIT_STATUS_LABELS = DEPOSIT_STATUS_CONFIG.STATUS_LABELS;
|
||||
export const DEPOSIT_STATUS_COLORS = DEPOSIT_STATUS_CONFIG.STATUS_STYLES;
|
||||
|
||||
// ===== 상태 탭 옵션 =====
|
||||
export const STATUS_TAB_OPTIONS: { value: DepositStatus | 'all'; label: string }[] = [
|
||||
|
||||
@@ -92,6 +92,7 @@ import {
|
||||
} from './types';
|
||||
import { extractUniqueOptions } from '../shared';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { applyFilters, textFilter, enumFilter } from '@/lib/utils/search';
|
||||
|
||||
// ===== 테이블 행 타입 (데이터 + 그룹 헤더 + 소계) =====
|
||||
type RowType = 'data' | 'monthHeader' | 'monthSubtotal' | 'totalExpense' | 'expectedBalance' | 'finalBalance';
|
||||
@@ -305,16 +306,10 @@ export function ExpectedExpenseManagement({
|
||||
|
||||
// ===== 필터링된 원본 데이터 =====
|
||||
const filteredRawData = useMemo(() => {
|
||||
let result = data.filter(item =>
|
||||
item.vendorName.includes(searchQuery) ||
|
||||
item.accountSubject.includes(searchQuery) ||
|
||||
item.note.includes(searchQuery)
|
||||
);
|
||||
|
||||
// 거래처 필터
|
||||
if (vendorFilter !== 'all') {
|
||||
result = result.filter(item => item.vendorName === vendorFilter);
|
||||
}
|
||||
const result = applyFilters(data, [
|
||||
textFilter(searchQuery, ['vendorName', 'accountSubject', 'note']),
|
||||
enumFilter('vendorName', vendorFilter),
|
||||
]);
|
||||
|
||||
// 정렬 적용
|
||||
switch (sortOption) {
|
||||
@@ -928,7 +923,7 @@ export function ExpectedExpenseManagement({
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[140px] h-8 text-sm">
|
||||
<SelectTrigger className="min-w-[140px] w-auto h-8 text-sm">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -942,7 +937,7 @@ export function ExpectedExpenseManagement({
|
||||
|
||||
{/* 정렬 필터 (최신순/등록순) */}
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-sm">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-8 text-sm">
|
||||
<SelectValue placeholder="최신순" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1208,7 +1203,7 @@ export function ExpectedExpenseManagement({
|
||||
value={formData.paymentStatus}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, paymentStatus: value as PaymentStatus }))}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectTrigger className="min-w-[200px] w-auto">
|
||||
<SelectValue placeholder="결제상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -76,22 +76,11 @@ export const TRANSACTION_TYPE_FILTER_OPTIONS = [
|
||||
{ value: 'other', label: '기타' },
|
||||
];
|
||||
|
||||
// 지급상태 레이블
|
||||
export const PAYMENT_STATUS_LABELS: Record<PaymentStatus, string> = {
|
||||
pending: '미지급',
|
||||
partial: '부분지급',
|
||||
paid: '지급완료',
|
||||
overdue: '연체',
|
||||
};
|
||||
// 지급상태 설정 (status-config 기반)
|
||||
import { PAYMENT_STATUS_CONFIG } from '@/lib/utils/status-config';
|
||||
|
||||
// 지급상태 필터 옵션
|
||||
export const PAYMENT_STATUS_FILTER_OPTIONS = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'pending', label: '미지급' },
|
||||
{ value: 'partial', label: '부분지급' },
|
||||
{ value: 'paid', label: '지급완료' },
|
||||
{ value: 'overdue', label: '연체' },
|
||||
];
|
||||
export const PAYMENT_STATUS_LABELS = PAYMENT_STATUS_CONFIG.STATUS_LABELS;
|
||||
export const PAYMENT_STATUS_FILTER_OPTIONS = PAYMENT_STATUS_CONFIG.STATUS_OPTIONS;
|
||||
|
||||
// 정렬 옵션
|
||||
export const SORT_OPTIONS: { value: SortOption; label: string }[] = [
|
||||
|
||||
@@ -259,7 +259,7 @@ export function AccountSubjectSettingModal({
|
||||
className="max-w-[250px] h-9 text-sm"
|
||||
/>
|
||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
||||
<SelectTrigger className="w-[100px] h-9 text-sm">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -218,7 +219,7 @@ export function GiftCertificateManagement() {
|
||||
value={entertainmentFilter}
|
||||
onValueChange={setEntertainmentFilter}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectTrigger className="min-w-[130px] w-auto">
|
||||
<SelectValue placeholder="접대비" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -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개 (기획서: 전체 상품권, 보유 상품권, 사용 상품권, 접대비 해당)
|
||||
|
||||
@@ -368,7 +368,7 @@ export function PurchaseManagement() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from './types';
|
||||
import { getReceivablesList, getReceivablesSummary, updateOverdueStatus, updateMemos, exportReceivablesExcel } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { filterByText } from '@/lib/utils/search';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
@@ -138,10 +139,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma
|
||||
|
||||
// ===== 필터링된 데이터 =====
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchQuery) return data;
|
||||
return data.filter(item =>
|
||||
item.vendorName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
return filterByText(data, searchQuery, ['vendorName']);
|
||||
}, [data, searchQuery]);
|
||||
|
||||
// ===== 정렬된 데이터 =====
|
||||
@@ -359,7 +357,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma
|
||||
value={String(selectedYear)}
|
||||
onValueChange={(value) => setSelectedYear(Number(value))}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="연도 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -379,7 +377,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma
|
||||
value={sortOption}
|
||||
onValueChange={(value) => setSortOption(value as SortOption)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="정렬 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
type SortDirection,
|
||||
} from '../shared';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { applyFilters, enumFilter } from '@/lib/utils/search';
|
||||
|
||||
// ===== 테이블 컬럼 정의 =====
|
||||
const tableColumns = [
|
||||
@@ -321,28 +322,22 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
// 검색은 searchFilter에서 처리하므로 여기서는 필터만 처리
|
||||
customFilterFn: (items, fv) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
return items.filter((item) => {
|
||||
const vendorVal = fv.vendor as string;
|
||||
const salesTypeVal = fv.salesType as string;
|
||||
const issuanceVal = fv.issuance as string;
|
||||
const issuanceVal = fv.issuance as string;
|
||||
|
||||
// 거래처 필터
|
||||
if (vendorVal && vendorVal !== 'all' && item.vendorName !== vendorVal) {
|
||||
return false;
|
||||
}
|
||||
// 매출유형 필터
|
||||
if (salesTypeVal && salesTypeVal !== 'all' && item.salesType !== salesTypeVal) {
|
||||
return false;
|
||||
}
|
||||
// 발행여부 필터
|
||||
if (issuanceVal === 'taxInvoicePending' && item.taxInvoiceIssued) {
|
||||
return false;
|
||||
}
|
||||
if (issuanceVal === 'transactionStatementPending' && item.transactionStatementIssued) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
let result = applyFilters(items, [
|
||||
enumFilter('vendorName', fv.vendor as string),
|
||||
enumFilter('salesType', fv.salesType as string),
|
||||
]);
|
||||
|
||||
// 발행여부 필터 (특수 로직 - enumFilter로 대체 불가)
|
||||
if (issuanceVal === 'taxInvoicePending') {
|
||||
result = result.filter(item => !item.taxInvoiceIssued);
|
||||
}
|
||||
if (issuanceVal === 'transactionStatementPending') {
|
||||
result = result.filter(item => !item.transactionStatementIssued);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// 커스텀 정렬 함수
|
||||
@@ -368,7 +363,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -244,7 +244,7 @@ export function TaxInvoiceIssuancePage({
|
||||
value={filters.dateType}
|
||||
onValueChange={(v) => updateFilter('dateType', v)}
|
||||
>
|
||||
<SelectTrigger className="w-full lg:w-[120px] h-9">
|
||||
<SelectTrigger className="w-full lg:min-w-[120px] lg:w-auto h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -298,7 +298,7 @@ export function TaxInvoiceIssuancePage({
|
||||
value={filters.status}
|
||||
onValueChange={(v) => updateFilter('status', v)}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[130px] h-9">
|
||||
<SelectTrigger className="w-full sm:min-w-[130px] sm:w-auto h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -314,7 +314,7 @@ export function TaxInvoiceIssuancePage({
|
||||
value={filters.sortBy}
|
||||
onValueChange={(v) => updateFilter('sortBy', v)}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[130px] h-9">
|
||||
<SelectTrigger className="w-full sm:min-w-[130px] sm:w-auto h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -330,7 +330,7 @@ export function TaxInvoiceIssuancePage({
|
||||
value={filters.sortOrder}
|
||||
onValueChange={(v) => updateFilter('sortOrder', v)}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[130px] h-9">
|
||||
<SelectTrigger className="w-full sm:min-w-[130px] sm:w-auto h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -284,7 +284,7 @@ export function TaxInvoiceManagement() {
|
||||
{/* Row1: 일자타입 + 날짜범위 + 분기 버튼 + 조회 */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-2">
|
||||
<Select value={dateType} onValueChange={setDateType}>
|
||||
<SelectTrigger className="w-full lg:w-[120px] h-9 text-sm">
|
||||
<SelectTrigger className="w-full lg:min-w-[120px] lg:w-auto h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { applyFilters, textFilter, enumFilter } from '@/lib/utils/search';
|
||||
import {
|
||||
Building2,
|
||||
Pencil,
|
||||
@@ -116,31 +117,13 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
|
||||
|
||||
// ===== 필터링된 데이터 =====
|
||||
const filteredData = useMemo(() => {
|
||||
let result = data.filter(item =>
|
||||
item.vendorName.includes(searchQuery) ||
|
||||
item.vendorCode.includes(searchQuery) ||
|
||||
item.businessNumber.includes(searchQuery)
|
||||
);
|
||||
|
||||
// 구분 필터
|
||||
if (categoryFilter !== 'all') {
|
||||
result = result.filter(item => item.category === categoryFilter);
|
||||
}
|
||||
|
||||
// 신용등급 필터
|
||||
if (creditRatingFilter !== 'all') {
|
||||
result = result.filter(item => item.creditRating === creditRatingFilter);
|
||||
}
|
||||
|
||||
// 거래등급 필터
|
||||
if (transactionGradeFilter !== 'all') {
|
||||
result = result.filter(item => item.transactionGrade === transactionGradeFilter);
|
||||
}
|
||||
|
||||
// 악성채권 필터
|
||||
if (badDebtFilter !== 'all') {
|
||||
result = result.filter(item => item.badDebtStatus === badDebtFilter);
|
||||
}
|
||||
const result = applyFilters(data, [
|
||||
textFilter(searchQuery, ['vendorName', 'vendorCode', 'businessNumber']),
|
||||
enumFilter('category', categoryFilter),
|
||||
enumFilter('creditRating', creditRatingFilter),
|
||||
enumFilter('transactionGrade', transactionGradeFilter),
|
||||
enumFilter('badDebtStatus', badDebtFilter),
|
||||
]);
|
||||
|
||||
// 정렬
|
||||
switch (sortOption) {
|
||||
@@ -450,7 +433,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 구분 필터 */}
|
||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -464,7 +447,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
|
||||
|
||||
{/* 신용등급 필터 */}
|
||||
<Select value={creditRatingFilter} onValueChange={setCreditRatingFilter}>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectTrigger className="min-w-[110px] w-auto">
|
||||
<SelectValue placeholder="신용등급" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -478,7 +461,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
|
||||
|
||||
{/* 거래등급 필터 */}
|
||||
<Select value={transactionGradeFilter} onValueChange={setTransactionGradeFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="거래등급" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -492,7 +475,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
|
||||
|
||||
{/* 악성채권 필터 */}
|
||||
<Select value={badDebtFilter} onValueChange={setBadDebtFilter}>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectTrigger className="min-w-[110px] w-auto">
|
||||
<SelectValue placeholder="악성채권" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -506,7 +489,7 @@ export function VendorManagementClient({ initialData, initialTotal }: VendorMana
|
||||
|
||||
{/* 정렬 */}
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectTrigger className="min-w-[150px] w-auto">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -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
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -384,7 +366,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="거래처" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -398,7 +380,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
|
||||
{/* 출금유형 필터 */}
|
||||
<Select value={withdrawalTypeFilter} onValueChange={setWithdrawalTypeFilter}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectTrigger className="min-w-[130px] w-auto">
|
||||
<SelectValue placeholder="출금유형" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -412,7 +394,7 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
|
||||
{/* 정렬 */}
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -609,7 +609,7 @@ export function ApprovalBox() {
|
||||
value={filterOption}
|
||||
onValueChange={(value) => setFilterOption(value as FilterOption)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="필터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -625,7 +625,7 @@ export function ApprovalBox() {
|
||||
value={sortOption}
|
||||
onValueChange={(value) => setSortOption(value as SortOption)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="정렬 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -588,7 +588,7 @@ export function DraftBox() {
|
||||
value={filterOption}
|
||||
onValueChange={(value) => setFilterOption(value as FilterOption)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="필터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -604,7 +604,7 @@ export function DraftBox() {
|
||||
value={sortOption}
|
||||
onValueChange={(value) => setSortOption(value as SortOption)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="정렬 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -396,7 +396,7 @@ export function ReferenceBox() {
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 필터 셀렉트박스 */}
|
||||
<Select value={filterOption} onValueChange={(value) => setFilterOption(value as FilterOption)}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="필터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -410,7 +410,7 @@ export function ReferenceBox() {
|
||||
|
||||
{/* 정렬 셀렉트박스 */}
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="정렬 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -145,7 +145,7 @@ export function BoardForm({ mode, board, onSubmit }: BoardFormProps) {
|
||||
value={formData.target}
|
||||
onValueChange={(value) => handleTargetChange(value as BoardTarget)}
|
||||
>
|
||||
<SelectTrigger id="target" className="w-[120px]">
|
||||
<SelectTrigger id="target" className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder="대상 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -563,7 +563,7 @@ const TableSection = ({ config }: { config: TableConfig }) => {
|
||||
value={filters[filter.key]}
|
||||
onValueChange={(value) => handleFilterChange(filter.key, value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-auto min-w-[80px] text-xs">
|
||||
<SelectTrigger className="h-8 w-auto min-w-[80px] w-auto text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -244,7 +244,7 @@ export function CalendarSection({
|
||||
value={deptFilter}
|
||||
onValueChange={(value) => setDeptFilter(value as CalendarDeptFilterType)}
|
||||
>
|
||||
<SelectTrigger className="w-[80px] h-8">
|
||||
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -261,7 +261,7 @@ export function CalendarSection({
|
||||
value={taskFilter}
|
||||
onValueChange={(value) => setTaskFilter(value as CalendarTaskFilterType)}
|
||||
>
|
||||
<SelectTrigger className="w-[80px] h-8">
|
||||
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
{/* 카드 1: 현금성 자산 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#ecfdf5', borderColor: '#a7f3d0' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border h-[110px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border min-h-[110px] flex flex-col"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -114,7 +114,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
{/* 카드 2: 외국환 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border h-[110px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border min-h-[110px] flex flex-col"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -148,7 +148,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
{/* 카드 3: 입금 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border h-[110px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border min-h-[110px] flex flex-col"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -180,7 +180,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
{/* 카드 4: 출금 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#fff1f2', borderColor: '#fecdd3' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border h-[110px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border min-h-[110px] flex flex-col"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -329,7 +329,7 @@ export function EnhancedStatusBoardSection({ items, itemSettings }: EnhancedStat
|
||||
<div
|
||||
key={item.id}
|
||||
style={{ backgroundColor: bgColor, borderColor: borderColor }}
|
||||
className="relative p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] hover:shadow-md h-[130px] flex flex-col"
|
||||
className="relative p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] hover:shadow-md min-h-[130px] flex flex-col"
|
||||
onClick={() => handleItemClick(item.path)}
|
||||
>
|
||||
{/* 아이콘 + 라벨 */}
|
||||
@@ -401,7 +401,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
{/* 카드 1: 매입 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#f5f3ff', borderColor: '#ddd6fe' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
onClick={() => onCardClick?.(data.cards[0]?.id || 'me1')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
@@ -426,7 +426,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
{/* 카드 2: 카드 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
onClick={() => onCardClick?.(data.cards[1]?.id || 'me2')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
@@ -451,7 +451,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
{/* 카드 3: 발행어음 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#fffbeb', borderColor: '#fde68a' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
onClick={() => onCardClick?.(data.cards[2]?.id || 'me3')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
@@ -476,7 +476,7 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
{/* 카드 4: 총 예상 지출 합계 (강조 - 인라인 스타일) */}
|
||||
<div
|
||||
style={{ backgroundColor: '#f43f5e', borderColor: '#f43f5e' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
onClick={() => onCardClick?.(data.cards[3]?.id || 'me4')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
|
||||
@@ -161,7 +161,7 @@ export function ElectronicApprovalModal({
|
||||
value={person.department || undefined}
|
||||
onValueChange={(val) => onChange(person.id, 'department', val)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="부서명" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -177,7 +177,7 @@ export function ElectronicApprovalModal({
|
||||
value={person.position || undefined}
|
||||
onValueChange={(val) => onChange(person.id, 'position', val)}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="직책명" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -193,7 +193,7 @@ export function ElectronicApprovalModal({
|
||||
value={person.name || undefined}
|
||||
onValueChange={(val) => onChange(person.id, 'name', val)}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="이름" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -346,7 +346,7 @@ export function EstimateDetailTableSection({
|
||||
onValueChange={(val) => onItemChange(item.id, 'material', val)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectTrigger className={`w-full min-w-[70px] w-auto ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -463,7 +463,7 @@ export function EstimateDetailTableSection({
|
||||
onValueChange={(val) => onItemChange(item.id, 'coating', Number(val))}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectTrigger className={`w-full min-w-[70px] w-auto ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -482,7 +482,7 @@ export function EstimateDetailTableSection({
|
||||
onValueChange={(val) => onItemChange(item.id, 'mounting', Number(val))}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectTrigger className={`w-full min-w-[70px] w-auto ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -501,7 +501,7 @@ export function EstimateDetailTableSection({
|
||||
onValueChange={(val) => onItemChange(item.id, 'controller', Number(val))}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectTrigger className={`w-full min-w-[70px] w-auto ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -520,7 +520,7 @@ export function EstimateDetailTableSection({
|
||||
onValueChange={(val) => onItemChange(item.id, 'widthConstruction', Number(val))}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className={`w-full min-w-[90px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectTrigger className={`w-full min-w-[90px] w-auto ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -539,7 +539,7 @@ export function EstimateDetailTableSection({
|
||||
onValueChange={(val) => onItemChange(item.id, 'heightConstruction', Number(val))}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className={`w-full min-w-[90px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectTrigger className={`w-full min-w-[90px] w-auto ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -410,7 +410,7 @@ export default function HandoverReportDetailForm({
|
||||
value={manager.name}
|
||||
onValueChange={(value) => handleManagerChange(manager.id, 'name', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectTrigger className="min-w-[150px] w-auto">
|
||||
<SelectValue placeholder="이름" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -432,7 +432,7 @@ export default function IssueDetailForm({ issue, mode = 'view' }: IssueDetailFor
|
||||
onValueChange={(value) => handleSelectChange('status')(value as IssueStatus)}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<SelectTrigger id="status" className="w-full md:w-[200px]">
|
||||
<SelectTrigger id="status" className="w-full md:min-w-[200px] md:w-auto">
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function ProjectKanbanBoard({
|
||||
{/* 필터 영역 */}
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Select value={selectedPartner} onValueChange={setSelectedPartner}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -159,7 +159,7 @@ export default function ProjectKanbanBoard({
|
||||
</Select>
|
||||
|
||||
<Select value={selectedSite} onValueChange={setSelectedSite}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -435,7 +435,7 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
|
||||
{/* 상태 필터 */}
|
||||
<Select value={statusFilter} onValueChange={(v) => { setStatusFilter(v); setCurrentPage(1); }}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -449,7 +449,7 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
|
||||
{/* 정렬 */}
|
||||
<Select value={sortBy} onValueChange={(v) => setSortBy(v as typeof sortBy)}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="최신순" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -120,7 +120,7 @@ export function ProgressBillingItemTable({
|
||||
value={item.product}
|
||||
onValueChange={(value) => onItemChange(item.id, 'product', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[80px]">
|
||||
<SelectTrigger className="min-w-[80px] w-auto">
|
||||
<SelectValue placeholder="제품 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -165,7 +165,7 @@ function WeekRow({
|
||||
// 셀 최소 높이 계산 (이벤트 행 수에 따라) - 더 넉넉하게 확보
|
||||
const segmentRowIndices = eventSegments.map(s => globalRowAssignments.get(s.event.id) || 0);
|
||||
const maxRowIndex = Math.max(0, ...segmentRowIndices);
|
||||
const rowHeight = Math.max(120, 48 + Math.min(maxRowIndex + 1, visibleRows) * 28 + 24);
|
||||
const rowHeight = Math.max(120, 48 + Math.min(maxRowIndex + 1, visibleRows) * 40 + 24);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -63,7 +63,7 @@ export function ScheduleBar({
|
||||
onClick(event);
|
||||
}}
|
||||
className={cn(
|
||||
'absolute h-5 px-2 text-xs font-medium truncate',
|
||||
'absolute h-6 px-2 text-xs font-medium truncate',
|
||||
'transition-all hover:opacity-80 hover:shadow-sm',
|
||||
'flex items-center cursor-pointer',
|
||||
colorClass,
|
||||
@@ -76,7 +76,7 @@ export function ScheduleBar({
|
||||
style={{
|
||||
width: `calc(${widthPercent}% - 4px)`,
|
||||
left: `calc(${leftPercent}% + 2px)`,
|
||||
top: `${rowIndex * 24 + 40}px`, // 날짜 영역(40px) 아래부터 시작 (간격 8px 추가)
|
||||
top: `${rowIndex * 36 + 40}px`, // 날짜 영역(40px) 아래부터 시작
|
||||
}}
|
||||
>
|
||||
{isStart && <span className="truncate">{event.title}</span>}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.employeeId}
|
||||
onValueChange={(value) => handleChange('employeeId', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectTrigger className="min-w-[200px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -139,7 +139,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.checkInHour}
|
||||
onValueChange={(value) => handleChange('checkInHour', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -154,7 +154,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.checkInMinute}
|
||||
onValueChange={(value) => handleChange('checkInMinute', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -176,7 +176,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.checkOutHour}
|
||||
onValueChange={(value) => handleChange('checkOutHour', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -191,7 +191,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.checkOutMinute}
|
||||
onValueChange={(value) => handleChange('checkOutMinute', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -213,7 +213,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.nightOvertimeHours}
|
||||
onValueChange={(value) => handleChange('nightOvertimeHours', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -228,7 +228,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.nightOvertimeMinutes}
|
||||
onValueChange={(value) => handleChange('nightOvertimeMinutes', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -250,7 +250,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.weekendOvertimeHours}
|
||||
onValueChange={(value) => handleChange('weekendOvertimeHours', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -265,7 +265,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.weekendOvertimeMinutes}
|
||||
onValueChange={(value) => handleChange('weekendOvertimeMinutes', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[90px]">
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -73,7 +73,7 @@ export function ReasonInfoDialog({
|
||||
value={formData.employeeId}
|
||||
onValueChange={(value) => handleChange('employeeId', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectTrigger className="min-w-[200px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -104,7 +104,7 @@ export function ReasonInfoDialog({
|
||||
value={formData.reasonType}
|
||||
onValueChange={(value) => handleChange('reasonType', value)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectTrigger className="min-w-[200px] w-auto">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -164,7 +164,7 @@ export function CardManagement() {
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={cardCompanyFilter} onValueChange={setCardCompanyFilter}>
|
||||
<SelectTrigger className="w-[130px] h-9">
|
||||
<SelectTrigger className="min-w-[130px] w-auto h-9">
|
||||
<SelectValue placeholder="카드사" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -175,7 +175,7 @@ export function CardManagement() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[100px] h-9">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-9">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -171,7 +171,7 @@ export function SalaryDetailDialog({
|
||||
value={editedStatus}
|
||||
onValueChange={(value) => handleStatusChange(value as PaymentStatus)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue>
|
||||
<Badge className={PAYMENT_STATUS_COLORS[editedStatus]}>
|
||||
{PAYMENT_STATUS_LABELS[editedStatus]}
|
||||
|
||||
@@ -148,7 +148,7 @@ export function InventoryAdjustmentDialog({ open, onOpenChange, onSave }: Props)
|
||||
총 <strong>{filteredItems.length}</strong>건
|
||||
</span>
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="w-[120px] h-8 text-sm">
|
||||
<SelectTrigger className="min-w-[120px] w-auto h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function YearQuarterFilter({
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2 flex-wrap min-w-0', className)}>
|
||||
<Select value={String(year)} onValueChange={(v) => onYearChange(Number(v))}>
|
||||
<SelectTrigger className="w-[100px] h-9 text-sm">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -356,7 +356,7 @@ export function PriceDistributionDetail({ id, mode: propMode }: Props) {
|
||||
<CardTitle className="text-base">단가표 목록</CardTitle>
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={gradeFilter} onValueChange={setGradeFilter}>
|
||||
<SelectTrigger className="h-8 w-[120px] text-sm">
|
||||
<SelectTrigger className="h-8 min-w-[120px] w-auto text-sm">
|
||||
<SelectValue placeholder="등급" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -243,7 +243,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={processFilter} onValueChange={setProcessFilter}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-xs">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-8 text-xs">
|
||||
<SelectValue placeholder="공정" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -260,7 +260,7 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, proc
|
||||
onValueChange={setCategoryFilter}
|
||||
disabled={categoryFilterOptions.length <= 1}
|
||||
>
|
||||
<SelectTrigger className="w-[100px] h-8 text-xs">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-8 text-xs">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -182,7 +182,7 @@ export function InspectionList() {
|
||||
() => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={calendarStatusFilter} onValueChange={setCalendarStatusFilter}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-xs">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -193,7 +193,7 @@ export function InspectionList() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={calendarInspectorFilter} onValueChange={setCalendarInspectorFilter}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-xs">
|
||||
<SelectTrigger className="min-w-[100px] w-auto h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -311,7 +311,7 @@ export function QuoteManagementClient({
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 제품분류 필터 */}
|
||||
<Select value={productCategoryFilter} onValueChange={setProductCategoryFilter}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="제품분류" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -324,7 +324,7 @@ export function QuoteManagementClient({
|
||||
|
||||
{/* 상태 필터 */}
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectTrigger className="min-w-[100px] w-auto">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -412,7 +412,7 @@ export default function ComprehensiveAnalysis({ initialData }: ComprehensiveAnal
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<SectionTitle title="오늘의 이슈" badge="warning" />
|
||||
<Select value={issueFilter} onValueChange={setIssueFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectTrigger className="min-w-[140px] w-auto">
|
||||
<SelectValue placeholder="필터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -252,7 +252,7 @@ export function AccountManagement() {
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
||||
<SelectTrigger className="w-[130px] h-9">
|
||||
<SelectTrigger className="min-w-[130px] w-auto h-9">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -262,7 +262,7 @@ export function AccountManagement() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={institutionFilter} onValueChange={setInstitutionFilter}>
|
||||
<SelectTrigger className="w-[150px] h-9">
|
||||
<SelectTrigger className="min-w-[150px] w-auto h-9">
|
||||
<SelectValue placeholder="금융기관" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -219,7 +219,7 @@ export function AttendanceSettingsManagement() {
|
||||
onValueChange={handleRadiusChange}
|
||||
disabled={!settings.gpsEnabled}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -77,7 +77,7 @@ function NotificationItemRow({ label, item, onChange, disabled }: NotificationIt
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] h-8">
|
||||
<SelectTrigger className="min-w-[140px] w-auto h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -189,7 +189,7 @@ export function PaymentHistoryManagement({
|
||||
// <div className="flex items-center gap-2 flex-wrap">
|
||||
// {/* 정렬 */}
|
||||
// <Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
// <SelectTrigger className="w-[120px]">
|
||||
// <SelectTrigger className="min-w-[120px] w-auto">
|
||||
// <SelectValue placeholder="정렬" />
|
||||
// </SelectTrigger>
|
||||
// <SelectContent>
|
||||
|
||||
@@ -498,7 +498,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
value={(filterValues[field.key] as string) || 'all'}
|
||||
onValueChange={(value) => onFilterChange(field.key, value)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="min-w-[120px] w-auto">
|
||||
<SelectValue placeholder={field.allOptionLabel || field.label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
139
src/lib/utils/search.ts
Normal file
139
src/lib/utils/search.ts
Normal file
@@ -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<T> = (data: T[]) => T[];
|
||||
|
||||
/** 텍스트 검색 옵션 */
|
||||
export interface TextFilterOptions {
|
||||
/** 대소문자 구분 여부 (기본값: false = case-insensitive) */
|
||||
caseSensitive?: boolean;
|
||||
}
|
||||
|
||||
// ===== Core Filter Functions =====
|
||||
|
||||
/**
|
||||
* 텍스트 검색 필터
|
||||
*
|
||||
* 빈 query는 전체 반환 (short-circuit).
|
||||
* 기본 case-insensitive (한글에는 영향 없음).
|
||||
*/
|
||||
export function filterByText<T>(
|
||||
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<T>(
|
||||
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<T>(
|
||||
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<T>(data: T[], filters: FilterFn<T>[]): 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<T>(
|
||||
query: string,
|
||||
fields: (keyof T & string)[],
|
||||
options?: TextFilterOptions
|
||||
): FilterFn<T> {
|
||||
return (data) => filterByText(data, query, fields, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* enumFilter 팩토리 — applyFilters용
|
||||
*/
|
||||
export function enumFilter<T>(
|
||||
field: keyof T & string,
|
||||
value: string,
|
||||
allValue: string = 'all'
|
||||
): FilterFn<T> {
|
||||
return (data) => filterByEnum(data, field, value, allValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* dateRangeFilter 팩토리 — applyFilters용
|
||||
*/
|
||||
export function dateRangeFilter<T>(
|
||||
field: keyof T & string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): FilterFn<T> {
|
||||
return (data) => filterByDateRange(data, field, startDate, endDate);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user