feat(WEB): 리스트 페이지 UI 레이아웃 표준화

- 공통 레이아웃 패턴 적용: [달력] → [프리셋] → [검색창] → [버튼들]
- beforeTableContent → headerActions + createButton 마이그레이션
- DateRangeSelector extraActions prop 활용하여 검색창 통합
- PricingListClient 테이블 행 클릭 → 상세 이동 기능 추가
- 회계 관련 페이지 (입금/출금/매입/매출/어음/카드/예상지출 등) 정리
- 건설 관련 페이지 검색 영역 정리
- 부모 메뉴 리다이렉트 컴포넌트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-26 22:04:36 +09:00
parent ff93ab7fa2
commit 1f6b592b9f
65 changed files with 1974 additions and 503 deletions

View File

@@ -22,8 +22,10 @@ import {
Save,
Trash2,
RefreshCw,
Search,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import {
@@ -103,6 +105,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
const [endDate, setEndDate] = useState('2025-09-03');
const [depositData, setDepositData] = useState<DepositRecord[]>(initialData);
const [isRefreshing, setIsRefreshing] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// 인라인 필터 상태 (tableHeaderActions에서 사용)
const [vendorFilter, setVendorFilter] = useState<string>('all');
@@ -262,7 +265,19 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
// 커스텀 필터 함수 (인라인 필터 사용)
customFilterFn: (items, filterValues) => {
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;
@@ -295,9 +310,16 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
searchPlaceholder: '입금자명, 계좌명, 적요, 거래처 검색...',
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
showPresets: true,
startDate,
endDate,
onStartDateChange: setStartDate,
@@ -325,14 +347,45 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
},
filterTitle: '입금 필터',
// 헤더 액션 (등록 버튼)
headerActions: () => (
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/deposits?mode=new')}>
<Plus className="w-4 h-4 mr-2" />
</Button>
// 헤더 액션: 계정과목명 Select + 저장 + 새로고침
headerActions: ({ selectedItems }) => (
<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]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{ACCOUNT_SUBJECT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => handleSaveAccountSubject(selectedItems)} size="sm">
<Save className="h-4 w-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isRefreshing}
>
<RefreshCw className={`h-4 w-4 mr-1 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? '조회중...' : '새로고침'}
</Button>
</div>
),
// 등록 버튼
createButton: {
label: '입금등록',
icon: Plus,
onClick: () => router.push('/ko/accounting/deposits?mode=new'),
},
// Stats 카드
computeStats: (): StatCard[] => [
{ label: '총 입금', value: `${stats.totalDeposit.toLocaleString()}`, icon: Banknote, iconColor: 'text-blue-500' },
@@ -341,44 +394,9 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
{ label: '입금유형 미설정', value: `${stats.depositTypeUnsetCount}`, icon: Banknote, iconColor: 'text-red-500' },
],
// beforeTableContent: 계정과목명 Select + 저장 버튼 + 새로고침
beforeTableContent: (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700"></span>
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="계정과목명 선택" />
</SelectTrigger>
<SelectContent>
{ACCOUNT_SUBJECT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
variant="outline"
onClick={handleRefresh}
disabled={isRefreshing}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? '조회중...' : '새로고침'}
</Button>
</div>
),
// tableHeaderActions: 3개 인라인 필터
tableHeaderActions: ({ selectedItems }) => (
tableHeaderActions: (
<div className="flex items-center gap-2 flex-wrap">
{/* 저장 버튼 */}
<Button onClick={() => handleSaveAccountSubject(selectedItems)} className="bg-blue-500 hover:bg-blue-600">
<Save className="h-4 w-4 mr-2" />
</Button>
{/* 거래처 필터 */}
<Select value={vendorFilter} onValueChange={setVendorFilter}>
<SelectTrigger className="w-[140px]">
@@ -556,6 +574,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
vendorOptions,
tableTotals,
isRefreshing,
searchQuery,
handleRowClick,
handleEdit,
handleRefresh,