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:
유병철
2026-02-20 13:26:27 +09:00
parent 6d934b4418
commit 012a661a19
68 changed files with 535 additions and 346 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 = [

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
/**
* 일일 일보 요약 데이터

View File

@@ -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>

View File

@@ -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 }[] = [

View File

@@ -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>

View File

@@ -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 }[] = [

View File

@@ -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>

View File

@@ -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개 (기획서: 전체 상품권, 보유 상품권, 사용 상품권, 접대비 해당)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>