Files
sam-react-prod/src/components/business/construction/order-management/modals/OrderDocumentModal.tsx
유병철 cbb38d48b9 refactor(WEB): 전체 actions.ts에 공통 API 유틸 적용
- buildApiUrl / executePaginatedAction 패턴으로 전환 (40+ actions 파일)
- 직접 URLSearchParams 조립 → buildApiUrl 유틸 사용
- 수동 페이지네이션 메타 변환 → executePaginatedAction 자동 처리
- HandoverReportDocumentModal, OrderDocumentModal 개선
- 급여관리 SalaryManagement 코드 개선
- CLAUDE.md Server Action 공통 유틸 규칙 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:59:59 +09:00

338 lines
14 KiB
TypeScript

'use client';
import { useState } from 'react';
import { DocumentViewer, DocumentHeader } from '@/components/document-system';
import { toast } from 'sonner';
import { useRouter } from 'next/navigation';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import type { OrderDetail, OrderDetailItem } from '../types';
import { deleteOrder } from '../actions';
// 날짜 포맷팅
function formatDate(dateStr: string | null): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
}
// 카테고리별로 품목 그룹화
function groupItemsByCategory(items: OrderDetailItem[]): Map<string, OrderDetailItem[]> {
const grouped = new Map<string, OrderDetailItem[]>();
items.forEach((item) => {
// name 필드를 카테고리로 사용 (실제로는 categoryName 필드가 있어야 함)
const categoryKey = item.name || '기타';
if (!grouped.has(categoryKey)) {
grouped.set(categoryKey, []);
}
grouped.get(categoryKey)!.push(item);
});
return grouped;
}
// 이미지 포함 여부 확인
function hasImage(items: OrderDetailItem[]): boolean {
return items.some((item) => item.imageUrl && item.imageUrl.trim() !== '');
}
interface OrderDocumentModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
order: OrderDetail;
onSuccess?: () => void;
}
export function OrderDocumentModal({
open,
onOpenChange,
order,
onSuccess,
}: OrderDocumentModalProps) {
const router = useRouter();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
// 수정
const handleEdit = () => {
onOpenChange(false);
router.push(`/ko/construction/order/order-management/${order.id}?mode=edit`);
};
// 삭제
const handleDelete = () => {
setShowDeleteDialog(true);
};
const handleConfirmDelete = async () => {
const result = await deleteOrder(order.id);
if (result.success) {
toast.success('발주서가 삭제되었습니다.');
setShowDeleteDialog(false);
onOpenChange(false);
onSuccess?.();
} else {
toast.error(result.error || '삭제에 실패했습니다.');
}
};
// 카테고리별 그룹화
const groupedItems = groupItemsByCategory(order.orderItems || []);
const categories = Array.from(groupedItems.entries());
return (
<>
<DocumentViewer
title="발주서"
subtitle="발주서 상세"
preset="construction"
open={open}
onOpenChange={onOpenChange}
onEdit={handleEdit}
onDelete={handleDelete}
>
<div className="p-8">
{/* 상단: 제목 (공통 컴포넌트) */}
<DocumentHeader
title="발주서"
documentCode={order.orderNumber}
subtitle={`작성일자: ${formatDate(order.orderDate)}`}
layout="simple"
/>
{/* 기본 정보 테이블 */}
<table className="w-full border-collapse border border-gray-300 text-sm mb-8">
<tbody>
{/* 출고일 / 작업반장 */}
<tr>
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-center w-28 font-medium">
</th>
<td className="border border-gray-300 px-4 py-3">
{formatDate(order.plannedDeliveryDate)}
</td>
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-center w-32 font-medium">
</th>
<td className="border border-gray-300 px-4 py-3">
{order.workTeamLeader || '-'}
</td>
</tr>
{/* 현장명 / 작업반장 연락처 */}
<tr>
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-center font-medium">
</th>
<td className="border border-gray-300 px-4 py-3">{order.siteName || '-'}</td>
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-center font-medium">
</th>
<td className="border border-gray-300 px-4 py-3">-</td>
</tr>
{/* 화물 도착지 / 발주담당자 */}
<tr>
<th rowSpan={2} className="border border-gray-300 px-4 py-3 bg-gray-50 text-center font-medium align-middle">
</th>
<td rowSpan={2} className="border border-gray-300 px-4 py-3 align-middle">{order.deliveryAddress || '-'}</td>
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-center font-medium">
</th>
<td className="border border-gray-300 px-4 py-3">
{order.orderManager || '-'}
</td>
</tr>
{/* 발주담당자 연락처 */}
<tr>
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-center font-medium">
</th>
<td className="border border-gray-300 px-4 py-3">-</td>
</tr>
</tbody>
</table>
{/* 카테고리별 발주 품목 */}
{categories.map(([categoryName, items]) => {
const showImageLayout = hasImage(items);
return (
<div key={categoryName} className="mb-8">
{/* 카테고리 헤더 */}
<div className="flex items-center gap-2 mb-4">
<span className="font-bold text-lg"> {categoryName}</span>
</div>
{showImageLayout ? (
// 이미지 포함 시 2열 레이아웃
<div className="space-y-4">
{items.map((item) => (
<div key={item.id} className="grid grid-cols-2 gap-4 border border-gray-300">
{/* 좌측: 이미지 영역 */}
<div className="p-4 flex flex-col">
<table className="w-full border-collapse text-sm mb-4">
<tbody>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center w-20">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.name || '-'}
</td>
</tr>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.product || '-'}
</td>
</tr>
</tbody>
</table>
{/* 이미지 표시 영역 */}
<div className="flex-1 border border-gray-200 rounded flex items-center justify-center min-h-[150px] bg-gray-50">
{item.imageUrl ? (
<img
src={item.imageUrl}
alt={item.name || '품목 이미지'}
className="max-w-full max-h-[200px] object-contain"
/>
) : (
<span className="text-gray-400">IMG</span>
)}
</div>
</div>
{/* 우측: 상세 정보 테이블 */}
<div className="p-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center w-20">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.name || '-'}
</td>
</tr>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.product || '-'}
</td>
</tr>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.width || '-'}
</td>
</tr>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.height || '-'}
</td>
</tr>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.quantity} {item.unit}
</td>
</tr>
<tr>
<th className="border border-gray-300 px-3 py-2 bg-gray-50 text-center">
</th>
<td className="border border-gray-300 px-3 py-2">
{item.remark || '-'}
</td>
</tr>
</tbody>
</table>
</div>
</div>
))}
</div>
) : (
// 이미지 없을 시 일반 테이블 레이아웃
<table className="w-full border-collapse border border-gray-300 text-sm">
<thead>
<tr className="bg-gray-50">
<th className="border border-gray-300 px-3 py-2 text-center"></th>
<th className="border border-gray-300 px-3 py-2 text-center"></th>
<th className="border border-gray-300 px-3 py-2 text-center w-20"></th>
<th className="border border-gray-300 px-3 py-2 text-center w-20"></th>
<th className="border border-gray-300 px-3 py-2 text-center w-20"></th>
<th className="border border-gray-300 px-3 py-2 text-center w-16"></th>
<th className="border border-gray-300 px-3 py-2 text-center"></th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id}>
<td className="border border-gray-300 px-3 py-2">{item.name || '-'}</td>
<td className="border border-gray-300 px-3 py-2">{item.product || '-'}</td>
<td className="border border-gray-300 px-3 py-2 text-center">
{item.width || '-'}
</td>
<td className="border border-gray-300 px-3 py-2 text-center">
{item.height || '-'}
</td>
<td className="border border-gray-300 px-3 py-2 text-center">
{item.quantity}
</td>
<td className="border border-gray-300 px-3 py-2 text-center">
{item.unit || '-'}
</td>
<td className="border border-gray-300 px-3 py-2">{item.remark || '-'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
})}
{/* 품목이 없는 경우 */}
{categories.length === 0 && (
<div className="text-center text-gray-400 py-8 border border-gray-200 rounded">
.
</div>
)}
{/* 비고 영역 - 내용이 있을 때만 표시 */}
{order.memo && (
<div className="mt-8">
<div className="flex items-center gap-2 mb-4">
<span className="font-bold text-lg"> </span>
</div>
<div className="whitespace-pre-wrap text-sm">
{order.memo}
</div>
</div>
)}
</div>
</DocumentViewer>
<DeleteConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
description="이 발주서를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
onConfirm={handleConfirmDelete}
/>
</>
);
}