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

@@ -97,6 +97,8 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
// 날짜 범위
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
// 검색어
const [searchQuery, setSearchQuery] = useState('');
// Stats 데이터
const [stats, setStats] = useState<BiddingStats | null>(initialStats || null);
@@ -220,6 +222,7 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
// 커스텀 필터 함수 (activeStatTab + filterValues 기반)
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'waiting' && item.status !== 'waiting') return false;
@@ -291,6 +294,11 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션: 날짜 선택기
dateRangeSelector: {
enabled: true,
@@ -410,8 +418,8 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit]
[startDate, endDate, searchQuery, activeStatTab, stats, handleRowClick, handleEdit]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -106,6 +106,7 @@ export default function ContractListClient({ initialData = [], initialStats }: C
const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [stats, setStats] = useState<ContractStats | null>(initialStats || null);
// Stats 로드
@@ -235,6 +236,7 @@ export default function ContractListClient({ initialData = [], initialStats }: C
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'pending' && item.status !== 'pending') return false;
@@ -304,6 +306,11 @@ export default function ContractListClient({ initialData = [], initialStats }: C
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -433,8 +440,8 @@ export default function ContractListClient({ initialData = [], initialStats }: C
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit]
[startDate, endDate, searchQuery, activeStatTab, stats, handleRowClick, handleEdit]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -68,6 +68,8 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
// 날짜 범위
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
// 검색어
const [searchQuery, setSearchQuery] = useState('');
// Stats 데이터
const [stats, setStats] = useState<EstimateStats | null>(initialStats || null);
// 필터 옵션 데이터
@@ -210,6 +212,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
// 커스텀 필터 함수 (activeStatTab + filterValues 기반)
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'pending' && item.status !== 'pending') return false;
@@ -274,6 +277,11 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션: 날짜 선택기
dateRangeSelector: {
enabled: true,
@@ -389,8 +397,8 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
/>
),
}),
[startDate, endDate, activeStatTab, stats, partnerOptions, estimatorOptions, handleRowClick, handleEdit]
[startDate, endDate, searchQuery, activeStatTab, stats, partnerOptions, estimatorOptions, handleRowClick, handleEdit]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -113,6 +113,7 @@ export default function HandoverReportListClient({
const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all');
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [searchQuery, setSearchQuery] = useState('');
const [stats, setStats] = useState<HandoverReportStats | null>(initialStats || null);
// Stats 로드
@@ -234,6 +235,7 @@ export default function HandoverReportListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'pending' && item.status !== 'pending') return false;
@@ -297,6 +299,11 @@ export default function HandoverReportListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -410,8 +417,8 @@ export default function HandoverReportListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit]
[startDate, endDate, searchQuery, activeStatTab, stats, handleRowClick, handleEdit]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -89,6 +89,7 @@ export default function IssueManagementListClient({
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stats, setStats] = useState<IssueStats | null>(initialStats || null);
const [searchQuery, setSearchQuery] = useState('');
const [withdrawDialogOpen, setWithdrawDialogOpen] = useState(false);
const [itemsToWithdraw, setItemsToWithdraw] = useState<Set<string>>(new Set());
const [clearSelectionFn, setClearSelectionFn] = useState<(() => void) | null>(null);
@@ -271,6 +272,7 @@ export default function IssueManagementListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab !== 'all' && item.status !== activeStatTab) return false;
@@ -346,6 +348,11 @@ export default function IssueManagementListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -494,12 +501,12 @@ export default function IssueManagementListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit, handleCreate, handleWithdrawClick]
[startDate, endDate, searchQuery, activeStatTab, stats, handleRowClick, handleEdit, handleCreate, handleWithdrawClick]
);
return (
<>
<UniversalListPage config={config} initialData={initialData} />
<UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />
{/* 철회 확인 다이얼로그 */}
<ConfirmDialog

View File

@@ -555,6 +555,7 @@ export default function ItemManagementClient({
onStartDateChange: setStartDate,
onEndDateChange: setEndDate,
},
hideSearch: true,
// 등록 버튼
createButton: {

View File

@@ -59,6 +59,7 @@ export default function LaborManagementClient({
const [startDate, setStartDate] = useState(format(startOfYear(today), 'yyyy-MM-dd'));
const [endDate, setEndDate] = useState(format(endOfYear(today), 'yyyy-MM-dd'));
const [stats, setStats] = useState<LaborStats>(initialStats ?? { total: 0, active: 0 });
const [searchQuery, setSearchQuery] = useState('');
// Stats 로드
useEffect(() => {
@@ -211,6 +212,7 @@ export default function LaborManagementClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// 구분 필터
const categoryFilter = filterValues.category as string;
@@ -242,6 +244,11 @@ export default function LaborManagementClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -361,6 +368,7 @@ export default function LaborManagementClient({
[
startDate,
endDate,
searchQuery,
stats,
handleRowClick,
handleEdit,
@@ -371,5 +379,5 @@ export default function LaborManagementClient({
]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -82,6 +82,7 @@ export default function ConstructionManagementListClient({
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stats, setStats] = useState<ConstructionManagementStats | null>(initialStats || null);
const [searchQuery, setSearchQuery] = useState('');
// 달력 관련 상태
const [selectedCalendarDate, setSelectedCalendarDate] = useState<Date | null>(null);
@@ -289,6 +290,7 @@ export default function ConstructionManagementListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'in_progress' && item.status !== 'in_progress') return false;
@@ -379,6 +381,11 @@ export default function ConstructionManagementListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -530,6 +537,7 @@ export default function ConstructionManagementListClient({
[
startDate,
endDate,
searchQuery,
activeStatTab,
stats,
selectedCalendarDate,
@@ -550,5 +558,5 @@ export default function ConstructionManagementListClient({
]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -104,12 +104,23 @@ export default function ProjectDetailClient({ projectId }: ProjectDetailClientPr
icon={FolderKanban}
/>
{/* 기간 선택 (달력 + 프리셋 버튼) */}
{/* 기간 선택 + 검색 영역 */}
<DateRangeSelector
startDate={filterStartDate}
endDate={filterEndDate}
onStartDateChange={setFilterStartDate}
onEndDateChange={setFilterEndDate}
extraActions={
<div className="relative w-full sm:w-[300px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="프로젝트 검색 (현장명, 거래처, 계약번호)"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
}
/>
{/* 상태 카드 */}
@@ -155,17 +166,6 @@ export default function ProjectDetailClient({ projectId }: ProjectDetailClientPr
</Card>
</div>
{/* 검색 영역 */}
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="프로젝트 검색 (현장명, 거래처, 계약번호)"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
{/* 칸반 보드 */}
<Card>
<CardContent className="p-4 min-h-[600px]">

View File

@@ -89,6 +89,7 @@ export default function OrderManagementListClient({
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [searchQuery, setSearchQuery] = useState('');
// 달력 관련 상태
const [selectedCalendarDate, setSelectedCalendarDate] = useState<Date | null>(null);
@@ -304,6 +305,7 @@ export default function OrderManagementListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// 거래처 필터 (다중선택)
const partnerFilters = filterValues.partners as string[];
@@ -423,6 +425,11 @@ export default function OrderManagementListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -584,6 +591,7 @@ export default function OrderManagementListClient({
[
startDate,
endDate,
searchQuery,
selectedCalendarDate,
calendarEvents,
calendarBadges,
@@ -606,5 +614,5 @@ export default function OrderManagementListClient({
]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -188,6 +188,7 @@ export default function PartnerListClient({ initialData = [], initialStats }: Pa
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// 악성채권 필터
const badDebtFilter = filterValues.badDebt as string;

View File

@@ -75,6 +75,7 @@ export default function ProgressBillingManagementListClient({
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stats, setStats] = useState<ProgressBillingStats | null>(initialStats || null);
const [searchQuery, setSearchQuery] = useState('');
// Stats 로드
useEffect(() => {
@@ -188,6 +189,7 @@ export default function ProgressBillingManagementListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'contractWaiting' &&
@@ -239,6 +241,11 @@ export default function ProgressBillingManagementListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -350,8 +357,8 @@ export default function ProgressBillingManagementListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit]
[startDate, endDate, searchQuery, activeStatTab, stats, handleRowClick, handleEdit]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -97,6 +97,7 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
// 날짜 범위
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// ===== 핸들러 =====
const handleRowClick = useCallback(
@@ -187,6 +188,7 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
// 커스텀 필터 함수 (activeStatTab 필터링 포함)
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'scheduled' && item.attendanceStatus !== 'scheduled') return false;
@@ -216,6 +218,11 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 날짜 범위 선택기
dateRangeSelector: {
enabled: true,
@@ -350,8 +357,8 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
);
},
}),
[handleRowClick, handleEdit, handleCreate, activeStatTab, startDate, endDate]
[handleRowClick, handleEdit, handleCreate, activeStatTab, startDate, endDate, searchQuery]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -68,6 +68,7 @@ export default function SiteManagementListClient({
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stats, setStats] = useState<SiteStats | null>(initialStats || null);
const [searchQuery, setSearchQuery] = useState('');
// Stats 로드
useEffect(() => {
@@ -183,6 +184,7 @@ export default function SiteManagementListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'construction' && item.status !== 'active') return false;
@@ -228,6 +230,11 @@ export default function SiteManagementListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -337,8 +344,8 @@ export default function SiteManagementListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit]
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit, searchQuery]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -82,6 +82,7 @@ export default function StructureReviewListClient({
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stats, setStats] = useState<StructureReviewStats | null>(initialStats || null);
const [searchQuery, setSearchQuery] = useState('');
// Stats 로드
useEffect(() => {
@@ -201,6 +202,7 @@ export default function StructureReviewListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'pending' && item.status !== 'pending') return false;
@@ -246,6 +248,11 @@ export default function StructureReviewListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -376,8 +383,8 @@ export default function StructureReviewListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit, handleCreate]
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit, handleCreate, searchQuery]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -84,6 +84,7 @@ export default function UtilityManagementListClient({
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stats, setStats] = useState<UtilityStats | null>(initialStats || null);
const [searchQuery, setSearchQuery] = useState('');
// Stats 로드
useEffect(() => {
@@ -212,6 +213,7 @@ export default function UtilityManagementListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터
if (activeStatTab === 'waiting' && item.status !== 'scheduled' && item.status !== 'issued') return false;
@@ -279,6 +281,11 @@ export default function UtilityManagementListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -389,8 +396,8 @@ export default function UtilityManagementListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats]
[startDate, endDate, activeStatTab, stats, searchQuery]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}

View File

@@ -84,6 +84,7 @@ export default function WorkerStatusListClient({
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [stats, setStats] = useState<WorkerStatusStats | null>(initialStats || null);
const [searchQuery, setSearchQuery] = useState('');
// Stats 로드
useEffect(() => {
@@ -220,6 +221,7 @@ export default function WorkerStatusListClient({
// 커스텀 필터 함수
customFilterFn: (items, filterValues) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// Stats 탭 필터 (계약상태)
if (activeStatTab !== 'all' && item.contractStatus !== activeStatTab) return false;
@@ -300,6 +302,11 @@ export default function WorkerStatusListClient({
return sorted;
},
// 검색창 (공통 컴포넌트에서 자동 생성)
hideSearch: true,
searchValue: searchQuery,
onSearchChange: setSearchQuery,
// 공통 헤더 옵션
dateRangeSelector: {
enabled: true,
@@ -415,8 +422,8 @@ export default function WorkerStatusListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleViewDetail]
[startDate, endDate, activeStatTab, stats, handleRowClick, handleViewDetail, searchQuery]
);
return <UniversalListPage config={config} initialData={initialData} />;
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
}