feat(WEB): 전체 페이지 ?mode= URL 네비게이션 패턴 적용

- 등록(?mode=new), 상세(?mode=view), 수정(?mode=edit) URL 패턴 일괄 적용
- 중복 패턴 제거: /edit?mode=edit → ?mode=edit (16개 파일)
- 제목 일관성: {기능} 등록/상세/수정 패턴 적용
- 검수 체크리스트 문서 추가 (79개 페이지)
- UniversalListPage, IntegratedDetailTemplate 공통 컴포넌트 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-25 12:27:43 +09:00
parent 72f1accbe4
commit f6551c7e8b
162 changed files with 2907 additions and 480 deletions

View File

@@ -123,7 +123,7 @@ export default function BiddingDetailForm({
const result = await updateBidding(biddingId, formData);
if (result.success) {
toast.success('수정이 완료되었습니다.');
router.push(`/ko/construction/project/bidding/${biddingId}`);
router.push(`/ko/construction/project/bidding/${biddingId}?mode=view`);
router.refresh();
return { success: true };
} else {
@@ -524,9 +524,10 @@ export default function BiddingDetailForm({
);
// 템플릿 동적 설정
// Note: IntegratedDetailTemplate이 모드에 따라 '상세'/'수정' 자동 추가
const dynamicConfig = {
...biddingConfig,
title: isViewMode ? '입찰 상세' : '입찰 수정',
title: '입찰',
};
return (

View File

@@ -114,14 +114,14 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: Bidding) => {
router.push(`/ko/construction/project/bidding/${item.id}`);
router.push(`/ko/construction/project/bidding/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: Bidding) => {
router.push(`/ko/construction/project/bidding/${item.id}/edit`);
router.push(`/ko/construction/project/bidding/${item.id}?mode=edit`);
},
[router]
);

View File

@@ -114,7 +114,7 @@ export default function ContractDetailForm({
const result = await createContract(formData);
if (result.success && result.data) {
toast.success(isChangeContract ? '변경 계약서가 생성되었습니다.' : '계약이 생성되었습니다.');
router.push(`/ko/construction/project/contract/${result.data.id}`);
router.push(`/ko/construction/project/contract/${result.data.id}?mode=view`);
router.refresh();
return { success: true };
}
@@ -124,7 +124,7 @@ export default function ContractDetailForm({
const result = await updateContract(contractId, formData);
if (result.success) {
toast.success('수정이 완료되었습니다.');
router.push(`/ko/construction/project/contract/${contractId}`);
router.push(`/ko/construction/project/contract/${contractId}?mode=view`);
router.refresh();
return { success: true };
}
@@ -212,11 +212,12 @@ export default function ContractDetailForm({
}, []);
// 모드별 config 타이틀 동적 설정
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'상세' 자동 추가
const dynamicConfig = useMemo(() => {
if (isCreateMode) {
return {
...contractConfig,
title: isChangeContract ? '변경 계약서 생성' : '계약 등록',
title: isChangeContract ? '변경 계약서' : '계약',
actions: {
...contractConfig.actions,
showDelete: false, // create 모드에서는 삭제 버튼 없음
@@ -512,7 +513,7 @@ export default function ContractDetailForm({
<>
<IntegratedDetailTemplate
config={dynamicConfig}
mode={mode === 'create' ? 'new' : mode}
mode={mode}
initialData={{}}
itemId={contractId}
isLoading={false}

View File

@@ -122,14 +122,14 @@ export default function ContractListClient({ initialData = [], initialStats }: C
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: Contract) => {
router.push(`/ko/construction/project/contract/${item.id}`);
router.push(`/ko/construction/project/contract/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: Contract) => {
router.push(`/ko/construction/project/contract/${item.id}/edit`);
router.push(`/ko/construction/project/contract/${item.id}?mode=edit`);
},
[router]
);

View File

@@ -97,7 +97,7 @@ export default function EstimateDetailForm({
}, [router]);
const handleEdit = useCallback(() => {
router.push(`/ko/construction/project/bidding/estimates/${estimateId}/edit`);
router.push(`/ko/construction/project/bidding/estimates/${estimateId}?mode=edit`);
}, [router, estimateId]);
// ===== 저장/삭제 핸들러 =====
@@ -123,7 +123,7 @@ export default function EstimateDetailForm({
if (result.success) {
toast.success('수정이 완료되었습니다.');
setShowSaveDialog(false);
router.push(`/ko/construction/project/bidding/estimates/${estimateId}`);
router.push(`/ko/construction/project/bidding/estimates/${estimateId}?mode=view`);
router.refresh();
} else {
toast.error(result.error || '저장에 실패했습니다.');
@@ -191,7 +191,7 @@ export default function EstimateDetailForm({
toast.success('입찰이 등록되었습니다.');
setShowBiddingDialog(false);
// 입찰 상세 페이지로 이동
router.push(`/ko/construction/project/bidding/${result.data.id}`);
router.push(`/ko/construction/project/bidding/${result.data.id}?mode=view`);
} else {
toast.error(result.error || '입찰 등록에 실패했습니다.');
}
@@ -668,11 +668,12 @@ export default function EstimateDetailForm({
]);
// Edit 모드용 config (타이틀 변경)
// Note: IntegratedDetailTemplate이 모드에 따라 '상세'/'수정' 자동 추가
const currentConfig = useMemo(() => {
if (isEditMode) {
return {
...estimateConfig,
title: '견적 수정',
title: '견적',
description: '견적 정보를 수정합니다',
};
}

View File

@@ -104,14 +104,14 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: Estimate) => {
router.push(`/ko/construction/project/bidding/estimates/${item.id}`);
router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: Estimate) => {
router.push(`/ko/construction/project/bidding/estimates/${item.id}/edit`);
router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=edit`);
},
[router]
);

View File

@@ -50,7 +50,7 @@ export function EstimateDocumentModal({
const handleEdit = useCallback(() => {
if (estimateId) {
onClose();
router.push(`/ko/construction/project/bidding/estimates/${estimateId}/edit`);
router.push(`/ko/construction/project/bidding/estimates/${estimateId}?mode=edit`);
}
}, [estimateId, onClose, router]);

View File

@@ -101,7 +101,7 @@ export default function HandoverReportDetailForm({
const result = await updateHandoverReport(reportId, formData);
if (result.success) {
toast.success('수정이 완료되었습니다.');
router.push(`/ko/construction/project/contract/handover-report/${reportId}`);
router.push(`/ko/construction/project/contract/handover-report/${reportId}?mode=view`);
router.refresh();
return { success: true };
}

View File

@@ -129,14 +129,14 @@ export default function HandoverReportListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(report: HandoverReport) => {
router.push(`/ko/construction/project/contract/handover-report/${report.id}`);
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(report: HandoverReport) => {
router.push(`/ko/construction/project/contract/handover-report/${report.id}/edit`);
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=edit`);
},
[router]
);

View File

@@ -49,7 +49,7 @@ export function HandoverReportDocumentModal({
// 수정
const handleEdit = () => {
onOpenChange(false);
router.push(`/ko/construction/project/contract/handover-report/${report.id}/edit`);
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=edit`);
};
// 삭제

View File

@@ -604,10 +604,11 @@ export default function IssueDetailForm({ issue, mode = 'view' }: IssueDetailFor
);
// 템플릿 모드 및 동적 설정
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'상세' 자동 추가
const templateMode = isCreateMode ? 'create' : mode;
const dynamicConfig = {
...issueConfig,
title: isCreateMode ? '이슈 등록' : '이슈 상세',
title: isViewMode ? '이슈 상세' : '이슈',
};
return (

View File

@@ -107,20 +107,20 @@ export default function IssueManagementListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: Issue) => {
router.push(`/ko/construction/project/issue-management/${item.id}`);
router.push(`/ko/construction/project/issue-management/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: Issue) => {
router.push(`/ko/construction/project/issue-management/${item.id}/edit`);
router.push(`/ko/construction/project/issue-management/${item.id}?mode=edit`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/project/issue-management/new');
router.push('/ko/construction/project/issue-management?mode=new');
}, [router]);
// 철회 다이얼로그 열기

View File

@@ -58,8 +58,8 @@ export default function ItemDetailClient({
}: ItemDetailClientProps) {
const router = useRouter();
// 모드 계산
const mode = isNewMode ? 'new' : isEditMode ? 'edit' : 'view';
// 모드 계산 (IntegratedDetailTemplate은 'create'를 기대)
const mode = isNewMode ? 'create' : isEditMode ? 'edit' : 'view';
const isViewMode = mode === 'view';
// 폼 데이터
@@ -161,17 +161,16 @@ export default function ItemDetailClient({
);
// 동적 config (mode에 따라 title 변경)
// Note: IntegratedDetailTemplate 제목 로직:
// - create 모드: ${config.title} 등록
// - view 모드: config.title (접미사 없음)
// - edit 모드: ${config.title} 수정
const dynamicConfig = useMemo(() => {
const titleMap: Record<string, string> = {
new: '품목 등록',
edit: '품목 수정',
view: '품목 상세',
};
return {
...itemConfig,
title: titleMap[mode] || itemConfig.title,
title: isViewMode ? '품목 상세' : '품목',
};
}, [mode]);
}, [isViewMode]);
// onSubmit 핸들러 (Promise 반환)
const handleFormSubmit = useCallback(async () => {
@@ -187,11 +186,11 @@ export default function ItemDetailClient({
setIsSaving(true);
try {
if (mode === 'new') {
if (mode === 'create') {
const result = await createItem(formData);
if (result.success && result.data) {
toast.success('품목이 등록되었습니다.');
router.push(`/ko/construction/order/base-info/items/${result.data.id}`);
router.push(`/ko/construction/order/base-info/items/${result.data.id}?mode=view`);
return { success: true };
} else {
toast.error(result.error || '품목 등록에 실패했습니다.');
@@ -201,7 +200,7 @@ export default function ItemDetailClient({
const result = await updateItem(itemId, formData);
if (result.success) {
toast.success('품목이 수정되었습니다.');
router.push(`/ko/construction/order/base-info/items/${itemId}`);
router.push(`/ko/construction/order/base-info/items/${itemId}?mode=view`);
return { success: true };
} else {
toast.error(result.error || '품목 수정에 실패했습니다.');

View File

@@ -210,13 +210,13 @@ export default function ItemManagementClient({
const handleRowClick = useCallback(
(item: Item) => {
router.push(`/ko/construction/order/base-info/items/${item.id}`);
router.push(`/ko/construction/order/base-info/items/${item.id}?mode=view`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/order/base-info/items/new');
router.push('/ko/construction/order/base-info/items?mode=new');
}, [router]);
const handleEdit = useCallback(

View File

@@ -181,7 +181,7 @@ export default function LaborDetailClient({
const result = await createLabor(formData);
if (result.success && result.data) {
toast.success('노임이 등록되었습니다.');
router.push(`/ko/construction/order/base-info/labor/${result.data.id}`);
router.push(`/ko/construction/order/base-info/labor/${result.data.id}?mode=view`);
} else {
toast.error(result.error || '노임 등록에 실패했습니다.');
}

View File

@@ -98,7 +98,7 @@ export default function LaborManagementClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(labor: Labor) => {
router.push(`/ko/construction/order/base-info/labor/${labor.id}`);
router.push(`/ko/construction/order/base-info/labor/${labor.id}?mode=view`);
},
[router]
);
@@ -111,7 +111,7 @@ export default function LaborManagementClient({
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/order/base-info/labor/new');
router.push('/ko/construction/order/base-info/labor?mode=new');
}, [router]);
// ===== UniversalListPage Config =====

View File

@@ -582,7 +582,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
{/* 이슈 보고 카드 */}
<Card
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => router.push(`/ko/construction/project/issue-management/new?orderId=${detail.orderId}`)}
onClick={() => router.push(`/ko/construction/project/issue-management?mode=new&orderId=${detail.orderId}`)}
>
<CardContent className="pt-6 pb-6">
<div className="space-y-2">
@@ -694,10 +694,18 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
</div>
);
// 동적 config 설정
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
// view 모드에서 "시공 상세"로 표시하려면 직접 설정 필요
const dynamicConfig = {
...constructionConfig,
title: isViewMode ? '시공 상세' : '시공',
};
return (
<>
<IntegratedDetailTemplate
config={constructionConfig}
config={dynamicConfig}
mode={mode}
initialData={{}}
itemId={id}

View File

@@ -161,14 +161,14 @@ export default function ConstructionManagementListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: ConstructionManagement) => {
router.push(`/ko/construction/project/construction-management/${item.id}`);
router.push(`/ko/construction/project/construction-management/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: ConstructionManagement) => {
router.push(`/ko/construction/project/construction-management/${item.id}/edit`);
router.push(`/ko/construction/project/construction-management/${item.id}?mode=edit`);
},
[router]
);
@@ -184,7 +184,7 @@ export default function ConstructionManagementListClient({
const handleCalendarEventClick = useCallback((event: ScheduleEvent) => {
if (event.data) {
router.push(`/ko/construction/project/construction-management/${event.id}`);
router.push(`/ko/construction/project/construction-management/${event.id}?mode=view`);
}
}, [router]);

View File

@@ -2,7 +2,7 @@
import { useState, useMemo, useCallback, useEffect, Fragment } from 'react';
import { useRouter } from 'next/navigation';
import { FolderKanban, Pencil, ClipboardList, PlayCircle, CheckCircle2 } from 'lucide-react';
import { FolderKanban, ClipboardList, PlayCircle, CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
@@ -240,22 +240,14 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
const handleRowClick = useCallback(
(project: Project) => {
router.push(`/ko/construction/project/management/${project.id}`);
},
[router]
);
const handleEdit = useCallback(
(e: React.MouseEvent, projectId: string) => {
e.stopPropagation();
router.push(`/ko/construction/project/management/${projectId}/edit`);
router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`);
},
[router]
);
const handleGanttProjectClick = useCallback(
(project: Project) => {
router.push(`/ko/construction/project/management/${project.id}`);
router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`);
},
[router]
);
@@ -510,13 +502,12 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
<TableHead className="w-[120px] text-right"> </TableHead>
<TableHead className="w-[180px] text-center"> </TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_tr]:h-14 [&_tr]:min-h-[56px] [&_tr]:max-h-[56px]">
{paginatedData.length === 0 ? (
<TableRow>
<TableCell colSpan={14} className="h-24 text-center">
<TableCell colSpan={13} className="h-24 text-center">
.
</TableCell>
</TableRow>
@@ -553,18 +544,6 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
<TableCell className="text-center">
{getStatusBadge(project.status, project.hasUrgentIssue)}
</TableCell>
<TableCell className="text-center">
{isSelected && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => handleEdit(e, project.id)}
>
<Pencil className="h-4 w-4" />
</Button>
)}
</TableCell>
</TableRow>
);
})

View File

@@ -20,8 +20,8 @@ import { OrderDialogs } from './dialogs/OrderDialogs';
import { OrderDocumentModal } from './modals/OrderDocumentModal';
interface OrderDetailFormProps {
mode: 'view' | 'edit';
orderId: string;
mode: 'view' | 'edit' | 'create';
orderId?: string;
initialData?: OrderDetail;
}
@@ -94,7 +94,7 @@ export default function OrderDetailForm({
const result = await updateOrder(orderId, formData);
if (result.success) {
toast.success('수정이 완료되었습니다.');
router.push(`/ko/construction/order/order-management/${orderId}`);
router.push(`/ko/construction/order/order-management/${orderId}?mode=view`);
router.refresh();
return { success: true };
}
@@ -225,10 +225,18 @@ export default function OrderDetailForm({
</div>
);
// 동적 config 설정
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
// view 모드에서 "발주 상세"로 표시하려면 직접 설정 필요
const dynamicConfig = {
...orderConfig,
title: isViewMode ? '발주 상세' : '발주',
};
return (
<>
<IntegratedDetailTemplate
config={orderConfig}
config={dynamicConfig}
mode={mode}
initialData={{}}
itemId={orderId}

View File

@@ -113,6 +113,13 @@ export default function OrderManagementListClient({
const calendarEvents: ScheduleEvent[] = useMemo(() => {
return allOrders
.filter((order) => {
// 유효한 날짜가 있는 항목만 달력에 표시
// periodStart/periodEnd가 빈 문자열이면 parseISO가 Invalid Date를 반환하여
// 모든 이벤트가 일요일(0번 컬럼)에 표시되는 버그 방지
if (!order.periodStart || !order.periodEnd) {
return false;
}
// 현장 필터 (달력용)
if (calendarSiteFilters.length > 0) {
const matchingSite = MOCK_SITES.find((s) => order.siteName.includes(s.label.split(' ')[0]));
@@ -152,20 +159,20 @@ export default function OrderManagementListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(order: Order) => {
router.push(`/ko/construction/order/order-management/${order.id}`);
router.push(`/ko/construction/order/order-management/${order.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(order: Order) => {
router.push(`/ko/construction/order/order-management/${order.id}/edit`);
router.push(`/ko/construction/order/order-management/${order.id}?mode=edit`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/order/order-management/new');
router.push('/ko/construction/order/order-management?mode=new');
}, [router]);
// 달력 이벤트 핸들러
@@ -179,7 +186,7 @@ export default function OrderManagementListClient({
const handleCalendarEventClick = useCallback((event: ScheduleEvent) => {
if (event.data) {
router.push(`/ko/construction/order/order-management/${event.id}`);
router.push(`/ko/construction/order/order-management/${event.id}?mode=view`);
}
}, [router]);
@@ -366,6 +373,10 @@ export default function OrderManagementListClient({
// 달력 날짜 필터
if (selectedCalendarDate) {
// periodStart/periodEnd가 빈 문자열이면 필터링에서 제외
if (!item.periodStart || !item.periodEnd) {
return false;
}
const orderStart = startOfDay(parseISO(item.periodStart));
const orderEnd = startOfDay(parseISO(item.periodEnd));
const selected = startOfDay(selectedCalendarDate);

View File

@@ -155,7 +155,7 @@ export function OrderManagementUnified({ initialData }: OrderManagementUnifiedPr
const handleCalendarEventClick = useCallback((event: ScheduleEvent) => {
if (event.data) {
router.push(`/ko/construction/order/order-management/${event.id}`);
router.push(`/ko/construction/order/order-management/${event.id}?mode=view`);
}
}, [router]);

View File

@@ -136,8 +136,11 @@ function transformOrder(apiOrder: ApiOrder): Order {
plannedDeliveryDate: apiOrder.delivery_date || '',
actualDeliveryDate: apiOrder.actual_delivery_date || null,
status: transformStatus(apiOrder.status_code),
periodStart: apiOrder.received_at || '',
periodEnd: apiOrder.delivery_date || '',
// 달력 표시용 기간: received_at ~ delivery_date
// received_at이 없으면 delivery_date를 시작일로 사용 (단일 날짜 이벤트)
// delivery_date도 없으면 created_at을 사용
periodStart: apiOrder.received_at || apiOrder.delivery_date || apiOrder.created_at.split('T')[0],
periodEnd: apiOrder.delivery_date || apiOrder.received_at || apiOrder.created_at.split('T')[0],
createdAt: apiOrder.created_at,
updatedAt: apiOrder.updated_at,
};

View File

@@ -158,7 +158,7 @@ export function useOrderDetailForm({
}, [router]);
const handleEdit = useCallback(() => {
router.push(`/ko/construction/order/order-management/${orderId}/edit`);
router.push(`/ko/construction/order/order-management/${orderId}?mode=edit`);
}, [router, orderId]);
const handleCancel = useCallback(() => {
@@ -239,7 +239,7 @@ export function useOrderDetailForm({
const result = await duplicateOrder(orderId);
if (result.success && result.newId) {
toast.success('발주가 복제되었습니다.');
router.push(`/ko/construction/order/order-management/${result.newId}/edit`);
router.push(`/ko/construction/order/order-management/${result.newId}?mode=edit`);
} else {
toast.error(result.error || '복제에 실패했습니다.');
}

View File

@@ -50,7 +50,7 @@ export function OrderDocumentModal({
// 수정
const handleEdit = () => {
onOpenChange(false);
router.push(`/ko/construction/order/order-management/${order.id}/edit`);
router.push(`/ko/construction/order/order-management/${order.id}?mode=edit`);
};
// 삭제

View File

@@ -541,7 +541,9 @@ export function orderDetailToFormData(detail: OrderDetail): OrderDetailFormData
// orderItems를 카테고리별로 그룹핑
const categoryMap = new Map<string, OrderDetailCategory>();
detail.orderItems.forEach((item) => {
// orderItems가 없거나 undefined일 경우 빈 배열로 처리
const orderItems = detail.orderItems || [];
orderItems.forEach((item) => {
// 임시로 'default' 카테고리 사용 (실제로는 item에 categoryId가 있어야 함)
const categoryId = 'default';
const categoryName = '기본 카테고리';
@@ -558,27 +560,27 @@ export function orderDetailToFormData(detail: OrderDetail): OrderDetailFormData
});
return {
orderNumber: detail.orderNumber,
orderCompanyId: detail.orderCompanyId,
orderType: detail.orderType,
status: detail.status,
orderManager: detail.orderManager,
deliveryAddress: detail.deliveryAddress,
partnerId: detail.partnerId,
partnerName: detail.partnerName,
siteName: detail.siteName,
contractNumber: detail.contractNumber,
contractId: detail.contractId,
constructionPMId: detail.constructionPMId,
constructionPM: detail.constructionPM,
constructionManagers: detail.constructionManagers,
workTeamLeader: detail.workTeamLeader,
constructionStartDate: detail.constructionStartDate,
orderNumber: detail.orderNumber || '',
orderCompanyId: detail.orderCompanyId || '',
orderType: detail.orderType || 'steel_bar',
status: detail.status || 'waiting',
orderManager: detail.orderManager || '',
deliveryAddress: detail.deliveryAddress || '',
partnerId: detail.partnerId || '',
partnerName: detail.partnerName || '',
siteName: detail.siteName || '',
contractNumber: detail.contractNumber || '',
contractId: detail.contractId || '',
constructionPMId: detail.constructionPMId || '',
constructionPM: detail.constructionPM || '',
constructionManagers: detail.constructionManagers || [],
workTeamLeader: detail.workTeamLeader || '',
constructionStartDate: detail.constructionStartDate || '',
constructionEndDate: '', // Order 인터페이스에는 없으므로 빈 값
orderCategories: Array.from(categoryMap.values()),
memo: detail.memo,
periodStart: detail.periodStart,
periodEnd: detail.periodEnd,
memo: detail.memo || '',
periodStart: detail.periodStart || '',
periodEnd: detail.periodEnd || '',
};
}

View File

@@ -233,18 +233,19 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
}, []);
// 동적 Config (모드별 타이틀/설명)
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'수정' 자동 추가
const dynamicConfig = useMemo(() => {
if (isNewMode) {
return {
...partnerConfig,
title: '거래처 등록',
title: '거래처',
description: '새로운 거래처를 등록합니다',
};
}
if (isEditMode) {
return {
...partnerConfig,
title: '거래처 수정',
title: '거래처',
description: '거래처 정보를 수정합니다',
};
}
@@ -628,7 +629,7 @@ export default function PartnerForm({ mode, partnerId, initialData }: PartnerFor
return (
<IntegratedDetailTemplate
config={dynamicConfig}
mode={mode}
mode={mode === 'new' ? 'create' : mode}
initialData={{}}
itemId={partnerId}
isLoading={false}

View File

@@ -70,18 +70,18 @@ export default function PartnerListClient({ initialData = [], initialStats }: Pa
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: Partner) => {
router.push(`/ko/construction/project/bidding/partners/${item.id}`);
router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=view`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/project/bidding/partners/new');
router.push('/ko/construction/project/bidding/partners?mode=new');
}, [router]);
const handleEdit = useCallback(
(item: Partner) => {
router.push(`/ko/construction/project/bidding/partners/${item.id}/edit`);
router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=edit`);
},
[router]
);

View File

@@ -173,7 +173,7 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro
if (result.success) {
toast.success('단가가 수정되었습니다.');
router.push(`/ko/construction/order/base-info/pricing/${id}`);
router.push(`/ko/construction/order/base-info/pricing/${id}?mode=view`);
} else {
toast.error(result.error || '수정에 실패했습니다.');
}
@@ -209,7 +209,7 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro
// 수정 페이지로 이동
const handleEdit = useCallback(() => {
if (id) {
router.push(`/ko/construction/order/base-info/pricing/${id}/edit`);
router.push(`/ko/construction/order/base-info/pricing/${id}?mode=edit`);
}
}, [id, router]);
@@ -218,7 +218,7 @@ export default function PricingDetailClient({ id, mode }: PricingDetailClientPro
if (isCreateMode) {
router.push('/ko/construction/order/base-info/pricing');
} else if (isEditMode && id) {
router.push(`/ko/construction/order/base-info/pricing/${id}`);
router.push(`/ko/construction/order/base-info/pricing/${id}?mode=view`);
}
}, [isCreateMode, isEditMode, id, router]);

View File

@@ -109,7 +109,7 @@ export default function PricingDetailClientV2({
(newMode: DetailMode) => {
if (newMode === 'edit' && pricingId) {
// edit 모드로 변경 시 별도 페이지로 이동 (기존 라우트 구조 유지)
router.push(`/ko/construction/order/base-info/pricing/${pricingId}/edit`);
router.push(`/ko/construction/order/base-info/pricing/${pricingId}?mode=edit`);
} else {
setMode(newMode);
}

View File

@@ -99,20 +99,20 @@ export default function PricingListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(pricing: Pricing) => {
router.push(`/ko/construction/order/base-info/pricing/${pricing.id}`);
router.push(`/ko/construction/order/base-info/pricing/${pricing.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(pricing: Pricing) => {
router.push(`/ko/construction/order/base-info/pricing/${pricing.id}/edit`);
router.push(`/ko/construction/order/base-info/pricing/${pricing.id}?mode=edit`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/order/base-info/pricing/new');
router.push('/ko/construction/order/base-info/pricing?mode=new');
}, [router]);
// ===== UniversalListPage Config =====

View File

@@ -146,10 +146,18 @@ export default function ProgressBillingDetailForm({
</div>
);
// 동적 config 설정
// IntegratedDetailTemplate: create → "{title} 등록", view → "{title}", edit → "{title} 수정"
// view 모드에서 "기성청구 상세"로 표시하려면 직접 설정 필요
const dynamicConfig = {
...progressBillingConfig,
title: isViewMode ? '기성청구 상세' : '기성청구',
};
return (
<>
<IntegratedDetailTemplate
config={progressBillingConfig}
config={dynamicConfig}
mode={mode}
initialData={{}}
itemId={billingId}

View File

@@ -90,14 +90,14 @@ export default function ProgressBillingManagementListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: ProgressBilling) => {
router.push(`/ko/construction/billing/progress-billing-management/${item.id}`);
router.push(`/ko/construction/billing/progress-billing-management/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: ProgressBilling) => {
router.push(`/ko/construction/billing/progress-billing-management/${item.id}/edit`);
router.push(`/ko/construction/billing/progress-billing-management/${item.id}?mode=edit`);
},
[router]
);

View File

@@ -63,7 +63,7 @@ export function useProgressBillingDetailForm({
}, [router]);
const handleEdit = useCallback(() => {
router.push('/construction/billing/progress-billing-management/' + billingId + '/edit');
router.push('/construction/billing/progress-billing-management/' + billingId + '?mode=edit');
}, [router, billingId]);
const handleCancel = useCallback(() => {

View File

@@ -395,11 +395,12 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site
}, []);
// 동적 config (모드에 따른 title 변경)
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'수정' 자동 추가
const dynamicConfig = useMemo(() => {
if (isNewMode) {
return {
...siteBriefingConfig,
title: '현장설명회 등록',
title: '현장설명회',
description: '새로운 현장설명회를 등록합니다',
actions: {
...siteBriefingConfig.actions,
@@ -410,7 +411,7 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site
if (isEditMode) {
return {
...siteBriefingConfig,
title: '현장설명회 수정',
title: '현장설명회',
description: '현장설명회 정보를 수정합니다',
};
}
@@ -901,7 +902,7 @@ export default function SiteBriefingForm({ mode, briefingId, initialData }: Site
<>
<IntegratedDetailTemplate
config={dynamicConfig}
mode={mode}
mode={mode === 'new' ? 'create' : mode}
initialData={{}}
itemId={briefingId}
isLoading={false}

View File

@@ -101,20 +101,20 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: SiteBriefing) => {
router.push(`/ko/construction/project/bidding/site-briefings/${item.id}`);
router.push(`/ko/construction/project/bidding/site-briefings/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: SiteBriefing) => {
router.push(`/ko/construction/project/bidding/site-briefings/${item.id}/edit`);
router.push(`/ko/construction/project/bidding/site-briefings/${item.id}?mode=edit`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/project/bidding/site-briefings/new');
router.push('/ko/construction/project/bidding/site-briefings?mode=new');
}, [router]);
// ===== UniversalListPage Config (최소 버전) =====

View File

@@ -83,14 +83,14 @@ export default function SiteManagementListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(site: Site) => {
router.push(`/ko/construction/order/site-management/${site.id}`);
router.push(`/ko/construction/order/site-management/${site.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(site: Site) => {
router.push(`/ko/construction/order/site-management/${site.id}/edit`);
router.push(`/ko/construction/order/site-management/${site.id}?mode=edit`);
},
[router]
);

View File

@@ -129,15 +129,11 @@ export default function StructureReviewDetailForm({
}, []);
// 동적 config (mode에 따라 title 변경)
// Note: IntegratedDetailTemplate이 모드에 따라 '등록'/'수정'/'상세' 자동 추가
const dynamicConfig = useMemo(() => {
const titleMap: Record<string, string> = {
new: '구조검토 등록',
edit: '구조검토 수정',
view: '구조검토 상세',
};
return {
...structureReviewConfig,
title: titleMap[mode] || structureReviewConfig.title,
title: '구조검토',
};
}, [mode]);

View File

@@ -97,20 +97,20 @@ export default function StructureReviewListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: StructureReview) => {
router.push(`/ko/construction/order/structure-review/${item.id}`);
router.push(`/ko/construction/order/structure-review/${item.id}?mode=view`);
},
[router]
);
const handleEdit = useCallback(
(item: StructureReview) => {
router.push(`/ko/construction/order/structure-review/${item.id}/edit`);
router.push(`/ko/construction/order/structure-review/${item.id}?mode=edit`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/construction/order/structure-review/new');
router.push('/ko/construction/order/structure-review?mode=new');
}, [router]);
// ===== UniversalListPage Config =====

View File

@@ -11,9 +11,8 @@
* - 삭제 기능 (deleteConfirmMessage로 AlertDialog 대체)
*/
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Zap, Pencil, Trash2, FileText, CheckCircle, Clock } from 'lucide-react';
import { useState, useMemo, useEffect } from 'react';
import { Zap, Trash2, FileText, CheckCircle, Clock } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { TableCell, TableRow } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
@@ -80,8 +79,6 @@ export default function UtilityManagementListClient({
initialData = [],
initialStats,
}: UtilityManagementListClientProps) {
const router = useRouter();
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
const [activeStatTab, setActiveStatTab] = useState<'all' | 'waiting' | 'complete'>('all');
const [startDate, setStartDate] = useState<string>('');
@@ -99,21 +96,6 @@ export default function UtilityManagementListClient({
}
}, [initialStats]);
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: Utility) => {
router.push(`/ko/construction/project/utility-management/${item.id}`);
},
[router]
);
const handleEdit = useCallback(
(item: Utility) => {
router.push(`/ko/construction/project/utility-management/${item.id}/edit`);
},
[router]
);
// ===== UniversalListPage Config =====
const config: UniversalListConfig<Utility> = useMemo(
() => ({
@@ -349,10 +331,9 @@ export default function UtilityManagementListClient({
) => (
<TableRow
key={item.id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleRowClick(item)}
className="hover:bg-muted/50"
>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="text-center">
<Checkbox checked={handlers.isSelected} onCheckedChange={handlers.onToggle} />
</TableCell>
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
@@ -372,30 +353,14 @@ export default function UtilityManagementListClient({
</TableCell>
<TableCell className="text-center">
{handlers.isSelected && (
<div className="flex items-center justify-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => {
e.stopPropagation();
handleEdit(item);
}}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={(e) => {
e.stopPropagation();
handlers.onDelete?.(item);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={() => handlers.onDelete?.(item)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</TableCell>
</TableRow>
@@ -416,7 +381,6 @@ export default function UtilityManagementListClient({
badgeVariant="secondary"
isSelected={handlers.isSelected}
onToggle={handlers.onToggle}
onClick={() => handleRowClick(item)}
details={[
{ label: '거래처', value: item.partnerName },
{ label: '공사PM', value: item.constructionPM },
@@ -425,7 +389,7 @@ export default function UtilityManagementListClient({
/>
),
}),
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit]
[startDate, endDate, activeStatTab, stats]
);
return <UniversalListPage config={config} initialData={initialData} />;

View File

@@ -99,14 +99,14 @@ export default function WorkerStatusListClient({
// ===== 핸들러 =====
const handleRowClick = useCallback(
(item: WorkerStatus) => {
router.push(`/ko/construction/project/worker-status/${item.id}`);
router.push(`/ko/construction/project/worker-status/${item.id}?mode=view`);
},
[router]
);
const handleViewDetail = useCallback(
(item: WorkerStatus) => {
router.push(`/ko/construction/project/worker-status/${item.id}`);
router.push(`/ko/construction/project/worker-status/${item.id}?mode=view`);
},
[router]
);