견적 시스템: - QuoteRegistrationV2: 할인 모달, 거래명세서 모달, vatType 필드 추가 - DiscountModal: 할인율/할인금액 상호 계산 모달 - QuoteTransactionModal: 거래명세서 미리보기 모달 - LocationDetailPanel, LocationListPanel 개선 템플릿 기능: - UniversalListPage: 엑셀 다운로드 기능 추가 (전체/선택 다운로드) - DocumentViewer: PDF 생성 기능 개선 신규 API: - /api/pdf/generate: Puppeteer 기반 PDF 생성 엔드포인트 UI 개선: - 입력 컴포넌트 placeholder 스타일 개선 (opacity 50%) - 각종 리스트 컴포넌트 정렬/필터링 개선 패키지 추가: - html2canvas, jspdf, puppeteer, dom-to-image-more Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
638 lines
27 KiB
TypeScript
638 lines
27 KiB
TypeScript
/**
|
||
* 품목 상세 조회 Client Component
|
||
*
|
||
* 품목 정보를 읽기 전용으로 표시
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import { useRouter } from 'next/navigation';
|
||
import type { ItemMaster } from '@/types/item';
|
||
import { ITEM_TYPE_LABELS, PART_TYPE_LABELS, PART_USAGE_LABELS, PRODUCT_CATEGORY_LABELS } from '@/types/item';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Label } from '@/components/ui/label';
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from '@/components/ui/card';
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from '@/components/ui/table';
|
||
import { ArrowLeft, Edit, Package, FileImage, Download, FileText, Check, Calendar } from 'lucide-react';
|
||
import { downloadFileById } from '@/lib/utils/fileDownload';
|
||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||
import { useMenuStore } from '@/store/menuStore';
|
||
|
||
interface ItemDetailClientProps {
|
||
item: ItemMaster;
|
||
}
|
||
|
||
/**
|
||
* 품목 유형별 Badge 반환
|
||
*/
|
||
function getItemTypeBadge(itemType: string) {
|
||
const badges: Record<string, { className: string }> = {
|
||
FG: { className: 'bg-purple-50 text-purple-700 border-purple-200' },
|
||
PT: { className: 'bg-orange-50 text-orange-700 border-orange-200' },
|
||
SM: { className: 'bg-green-50 text-green-700 border-green-200' },
|
||
RM: { className: 'bg-blue-50 text-blue-700 border-blue-200' },
|
||
CS: { className: 'bg-gray-50 text-gray-700 border-gray-200' },
|
||
};
|
||
|
||
const config = badges[itemType] || { className: '' };
|
||
|
||
return (
|
||
<Badge variant="outline" className={config.className}>
|
||
{ITEM_TYPE_LABELS[itemType as keyof typeof ITEM_TYPE_LABELS]}
|
||
</Badge>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 조립품 품목코드 포맷팅 (하이픈 이후 제거)
|
||
*/
|
||
function formatItemCodeForAssembly(item: ItemMaster): string {
|
||
return item.itemCode;
|
||
}
|
||
|
||
/**
|
||
* 파일 다운로드 핸들러 (Blob 방식)
|
||
*/
|
||
async function handleFileDownload(fileId: number | undefined, fileName?: string): Promise<void> {
|
||
if (!fileId) return;
|
||
try {
|
||
await downloadFileById(fileId, fileName);
|
||
} catch (error) {
|
||
if (isNextRedirectError(error)) throw error;
|
||
console.error('[ItemDetailClient] 다운로드 실패:', error);
|
||
alert('파일 다운로드에 실패했습니다.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Storage 경로를 전체 URL로 변환 (전개도 이미지용)
|
||
* - 이미 전체 URL인 경우 그대로 반환
|
||
* - 상대 경로인 경우 API URL + /storage/ 붙여서 반환
|
||
*/
|
||
function getStorageUrl(path: string | undefined): string | null {
|
||
if (!path) return null;
|
||
// 이미 전체 URL인 경우
|
||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||
return path;
|
||
}
|
||
// 상대 경로인 경우
|
||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||
return `${apiUrl}/storage/${path}`;
|
||
}
|
||
|
||
export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||
const router = useRouter();
|
||
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
||
|
||
return (
|
||
<div className="space-y-6 pb-24">
|
||
{/* 헤더 */}
|
||
<div className="flex items-start gap-3">
|
||
<div className="p-2 bg-primary/10 rounded-lg hidden md:block">
|
||
<Package className="w-6 h-6 text-primary" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-xl md:text-2xl">품목 상세 정보</h1>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
등록된 품목 정보를 확인합니다
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 기본 정보 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>기본 정보</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
<div>
|
||
<Label className="text-muted-foreground">품목코드</Label>
|
||
<p className="mt-1">
|
||
<code className="text-sm bg-gray-100 px-2 py-1 rounded">
|
||
{formatItemCodeForAssembly(item)}
|
||
</code>
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground">품목명</Label>
|
||
<p className="mt-1 font-medium">{item.itemName}</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground">품목유형</Label>
|
||
<p className="mt-1">{getItemTypeBadge(item.itemType)}</p>
|
||
</div>
|
||
|
||
{item.itemType === "PT" && item.partType && (
|
||
<div>
|
||
<Label className="text-muted-foreground">부품 유형</Label>
|
||
<p className="mt-1">
|
||
<Badge variant="outline" className={
|
||
item.partType === 'ASSEMBLY' ? 'bg-blue-50 text-blue-700' :
|
||
item.partType === 'BENDING' ? 'bg-purple-50 text-purple-700' :
|
||
item.partType === 'PURCHASED' ? 'bg-green-50 text-green-700' :
|
||
'bg-gray-50 text-gray-700'
|
||
}>
|
||
{item.partType === 'ASSEMBLY' ? '조립 부품' :
|
||
item.partType === 'BENDING' ? '절곡 부품' :
|
||
item.partType === 'PURCHASED' ? '구매 부품' :
|
||
item.partType}
|
||
</Badge>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{item.itemType === "PT" && item.partType === "BENDING" && item.partUsage && (
|
||
<div>
|
||
<Label className="text-muted-foreground">용도</Label>
|
||
<p className="mt-1">
|
||
<Badge variant="outline" className="bg-indigo-50 text-indigo-700">
|
||
{item.partUsage === "GUIDE_RAIL" ? "가이드레일용" :
|
||
item.partUsage === "BOTTOM_FINISH" ? "하단마감재용" :
|
||
item.partUsage === "CASE" ? "케이스용" :
|
||
item.partUsage === "DOOR" ? "도어용" :
|
||
item.partUsage === "BRACKET" ? "브라켓용" :
|
||
item.partUsage === "GENERAL" ? "범용 (공통 부품)" :
|
||
item.partUsage}
|
||
</Badge>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{item.itemType !== "FG" && item.partType !== 'ASSEMBLY' && item.specification && (
|
||
<div>
|
||
<Label className="text-muted-foreground">규격</Label>
|
||
<p className="mt-1">{item.specification}</p>
|
||
</div>
|
||
)}
|
||
|
||
{item.itemType !== "FG" && (
|
||
<div>
|
||
<Label className="text-muted-foreground">단위</Label>
|
||
<p className="mt-1">
|
||
<Badge variant="secondary">{item.unit}</Badge>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 버전 정보 */}
|
||
<div className="md:col-span-2 lg:col-span-3 pt-4 border-t">
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div>
|
||
<Label className="text-muted-foreground">현재 버전</Label>
|
||
<div className="mt-1">
|
||
<Badge variant="secondary">V{item.currentRevision || 0}</Badge>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Label className="text-muted-foreground">수정 횟수</Label>
|
||
<p className="mt-1">{(item.revisions?.length || 0)}회</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<Label className="text-muted-foreground">등록일</Label>
|
||
<p className="mt-1 text-sm">{new Date(item.createdAt).toLocaleDateString('ko-KR')}</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 제품(FG) 전용 정보 - 내용이 있을 때만 표시 */}
|
||
{item.itemType === 'FG' && (item.productCategory || item.lotAbbreviation || item.note) && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>제품 정보</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
{item.productCategory && (
|
||
<div>
|
||
<Label className="text-muted-foreground">제품 카테고리</Label>
|
||
<p className="mt-1 font-medium">{PRODUCT_CATEGORY_LABELS[item.productCategory]}</p>
|
||
</div>
|
||
)}
|
||
{item.lotAbbreviation && (
|
||
<div>
|
||
<Label className="text-muted-foreground">로트 약자</Label>
|
||
<p className="mt-1 font-medium">{item.lotAbbreviation}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{item.note && (
|
||
<div className="mt-4">
|
||
<Label className="text-muted-foreground">비고</Label>
|
||
<p className="mt-1 text-sm whitespace-pre-wrap">{item.note}</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 조립 부품 세부 정보 */}
|
||
{item.itemType === 'PT' && item.partType === 'ASSEMBLY' && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Package className="h-5 w-5" />
|
||
조립 부품 세부 정보
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
{/* 품목명 - itemName 표시 */}
|
||
<div>
|
||
<Label className="text-muted-foreground">품목명</Label>
|
||
<p className="mt-1">
|
||
<Badge variant="outline" className="bg-indigo-50 text-indigo-700">
|
||
{item.itemName}
|
||
</Badge>
|
||
</p>
|
||
</div>
|
||
{/* 설치 유형 */}
|
||
<div>
|
||
<Label className="text-muted-foreground">설치 유형</Label>
|
||
<p className="mt-1">
|
||
{item.installationType ? (
|
||
<Badge variant="outline" className="bg-green-50 text-green-700">
|
||
{item.installationType === 'wall' ? '벽면형 (R)' :
|
||
item.installationType === 'side' ? '측면형 (S)' :
|
||
item.installationType === 'steel' ? '스틸 (B)' :
|
||
item.installationType === 'iron' ? '철재 (T)' :
|
||
item.installationType}
|
||
</Badge>
|
||
) : (
|
||
<span className="text-muted-foreground">-</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
{/* 마감 */}
|
||
<div>
|
||
<Label className="text-muted-foreground">마감</Label>
|
||
<p className="mt-1">
|
||
{item.assemblyType ? (
|
||
<code className="text-sm bg-gray-100 px-2 py-1 rounded">
|
||
{item.assemblyType}
|
||
</code>
|
||
) : (
|
||
<span className="text-muted-foreground">-</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
{/* 길이 */}
|
||
<div>
|
||
<Label className="text-muted-foreground">길이</Label>
|
||
<p className="mt-1 font-medium">
|
||
{item.assemblyLength || item.length ? `${item.assemblyLength || item.length}mm` : '-'}
|
||
</p>
|
||
</div>
|
||
{/* 측면 규격 */}
|
||
<div>
|
||
<Label className="text-muted-foreground">측면 규격</Label>
|
||
<p className="mt-1">
|
||
{item.sideSpecWidth && item.sideSpecHeight
|
||
? `${item.sideSpecWidth} × ${item.sideSpecHeight}mm`
|
||
: '-'}
|
||
</p>
|
||
</div>
|
||
{/* 재질 (있으면) */}
|
||
{item.material && (
|
||
<div>
|
||
<Label className="text-muted-foreground">재질</Label>
|
||
<p className="mt-1">
|
||
<Badge variant="outline" className="bg-indigo-50 text-indigo-700">
|
||
{item.material}
|
||
</Badge>
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 가이드레일 세부 정보 */}
|
||
{item.category3 === "가이드레일" && item.guideRailModelType && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>가이드레일 세부 정보</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
{item.guideRailModelType && (
|
||
<div>
|
||
<Label className="text-muted-foreground">모델유형</Label>
|
||
<p className="mt-1">
|
||
<Badge variant="outline" className="bg-orange-50 text-orange-700">
|
||
{item.guideRailModelType}
|
||
</Badge>
|
||
</p>
|
||
</div>
|
||
)}
|
||
{item.guideRailModel && (
|
||
<div>
|
||
<Label className="text-muted-foreground">모델선택</Label>
|
||
<p className="mt-1">
|
||
<Badge variant="outline" className="bg-orange-50 text-orange-700">
|
||
{item.guideRailModel}
|
||
</Badge>
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 절곡품/조립품 전개도 정보 - 조립부품은 항상 표시 */}
|
||
{item.itemType === 'PT' &&
|
||
(item.partType === 'BENDING' || item.partType === 'ASSEMBLY') && (
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
||
<FileImage className="h-4 w-4 md:h-5 md:w-5" />
|
||
{item.partType === 'ASSEMBLY' ? '조립품 전개도 (바라시)' : '절곡품 전개도 (바라시)'}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4 md:space-y-6 pt-0">
|
||
{/* 전개도 이미지 - 파일 ID가 있으면 프록시로 로드 */}
|
||
{(item.bendingDiagramFileId || item.bendingDiagram) ? (
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs md:text-sm">전개도 이미지</Label>
|
||
<div className="mt-2 p-2 md:p-4 border rounded-lg bg-gray-50">
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
src={item.bendingDiagramFileId
|
||
? `/api/proxy/files/${item.bendingDiagramFileId}/download`
|
||
: getStorageUrl(item.bendingDiagram) || ''
|
||
}
|
||
alt="전개도"
|
||
className="max-w-full h-auto max-h-64 md:max-h-96 mx-auto border rounded"
|
||
onError={(e) => {
|
||
const target = e.target as HTMLImageElement;
|
||
target.style.display = 'none';
|
||
target.parentElement?.insertAdjacentHTML(
|
||
'beforeend',
|
||
'<p class="text-center text-gray-500 py-8">이미지를 불러올 수 없습니다</p>'
|
||
);
|
||
}}
|
||
/>
|
||
</div>
|
||
{item.bendingDiagramFileId && (
|
||
<div className="mt-2 flex justify-end">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleFileDownload(item.bendingDiagramFileId, '전개도_이미지')}
|
||
>
|
||
<Download className="h-4 w-4 mr-1" />
|
||
다운로드
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-6 md:py-8 text-xs md:text-sm text-muted-foreground border rounded-lg bg-gray-50">
|
||
등록된 전개도 이미지가 없습니다.
|
||
</div>
|
||
)}
|
||
|
||
{/* 전개도 상세 데이터 */}
|
||
{item.bendingDetails && item.bendingDetails.length > 0 && (
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs md:text-sm">전개도 상세 데이터</Label>
|
||
<div className="mt-2 overflow-x-auto bg-white rounded border">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow className="bg-gray-100">
|
||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">번호</TableHead>
|
||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">입력</TableHead>
|
||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">연신율</TableHead>
|
||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">연신율계산후</TableHead>
|
||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">합계</TableHead>
|
||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">음영</TableHead>
|
||
<TableHead className="px-1 md:px-2 py-1 md:py-2 text-center">A각</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{item.bendingDetails.map((detail, detailIndex) => {
|
||
const calculated = detail.input + detail.elongation;
|
||
let sum = 0;
|
||
for (let i = 0; i <= detailIndex; i++) {
|
||
const d = item.bendingDetails![i];
|
||
sum += d.input + d.elongation;
|
||
}
|
||
|
||
return (
|
||
<TableRow key={detail.id} className={detail.shaded ? "bg-gray-200" : ""}>
|
||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.no}</TableCell>
|
||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.input}</TableCell>
|
||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.elongation}</TableCell>
|
||
<TableCell className="px-1 md:px-2 py-1 text-center bg-blue-50">{calculated}</TableCell>
|
||
<TableCell className="px-1 md:px-2 py-1 text-center bg-green-50 font-medium">{sum}</TableCell>
|
||
<TableCell className="px-1 md:px-2 py-1 text-center">
|
||
{detail.shaded ? <Check className="h-3 w-3 md:h-4 md:w-4 inline" /> : "-"}
|
||
</TableCell>
|
||
<TableCell className="px-1 md:px-2 py-1 text-center">{detail.aAngle || "-"}</TableCell>
|
||
</TableRow>
|
||
);
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
<div className="mt-2 p-2 md:p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||
<p className="text-xs md:text-sm">
|
||
<span className="font-medium">최종 전개 길이:</span>{" "}
|
||
<span className="text-base md:text-lg font-bold text-blue-700">
|
||
{item.bendingDetails.reduce((sum, d) => sum + d.input + d.elongation, 0)} mm
|
||
</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 제품(FG) 인정 정보 및 첨부 파일 */}
|
||
{item.itemType === 'FG' && (item.certificationNumber || item.specificationFile || item.certificationFile) && (
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
||
<FileText className="h-4 w-4 md:h-5 md:w-5" />
|
||
인정 정보 및 첨부 파일
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4 md:space-y-6 pt-0">
|
||
{/* 인정 정보 */}
|
||
{item.certificationNumber && (
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg border">
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs md:text-sm">인정번호</Label>
|
||
<p className="mt-1 font-medium">{item.certificationNumber}</p>
|
||
</div>
|
||
{item.certificationStartDate && (
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs md:text-sm flex items-center gap-1">
|
||
<Calendar className="h-3 w-3" />
|
||
유효기간 시작
|
||
</Label>
|
||
<p className="mt-1">{new Date(item.certificationStartDate).toLocaleDateString('ko-KR')}</p>
|
||
</div>
|
||
)}
|
||
{item.certificationEndDate && (
|
||
<div>
|
||
<Label className="text-muted-foreground text-xs md:text-sm flex items-center gap-1">
|
||
<Calendar className="h-3 w-3" />
|
||
유효기간 종료
|
||
</Label>
|
||
<p className="mt-1">{new Date(item.certificationEndDate).toLocaleDateString('ko-KR')}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 첨부 파일 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* 시방서 */}
|
||
<div className="p-4 border rounded-lg">
|
||
<Label className="text-muted-foreground text-xs md:text-sm">시방서</Label>
|
||
{item.specificationFile ? (
|
||
<div className="mt-2 flex items-center gap-2">
|
||
<FileText className="h-4 w-4 text-blue-600" />
|
||
<span className="text-sm truncate flex-1">
|
||
{item.specificationFileName || '시방서 파일'}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleFileDownload(item.specificationFileId, item.specificationFileName)}
|
||
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800"
|
||
>
|
||
<Download className="h-4 w-4" />
|
||
다운로드
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<p className="mt-2 text-sm text-muted-foreground">등록된 시방서가 없습니다.</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 인정서 */}
|
||
<div className="p-4 border rounded-lg">
|
||
<Label className="text-muted-foreground text-xs md:text-sm">인정서</Label>
|
||
{item.certificationFile ? (
|
||
<div className="mt-2 flex items-center gap-2">
|
||
<FileText className="h-4 w-4 text-green-600" />
|
||
<span className="text-sm truncate flex-1">
|
||
{item.certificationFileName || '인정서 파일'}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleFileDownload(item.certificationFileId, item.certificationFileName)}
|
||
className="inline-flex items-center gap-1 text-sm text-green-600 hover:text-green-800"
|
||
>
|
||
<Download className="h-4 w-4" />
|
||
다운로드
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<p className="mt-2 text-sm text-muted-foreground">등록된 인정서가 없습니다.</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* BOM 정보 - 절곡 부품은 제외 */}
|
||
{(item.itemType === 'FG' || (item.itemType === 'PT' && item.partType !== 'BENDING')) && item.bom && item.bom.length > 0 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Package className="h-5 w-5" />
|
||
부품 구성 (BOM)
|
||
</CardTitle>
|
||
<Badge variant="outline" className="bg-blue-50 text-blue-700">
|
||
총 {item.bom.length}개 품목
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="border rounded-lg overflow-hidden">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>번호</TableHead>
|
||
<TableHead>품목코드</TableHead>
|
||
<TableHead>품목명</TableHead>
|
||
<TableHead className="text-right">수량</TableHead>
|
||
<TableHead>단위</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{item.bom.map((line, index) => (
|
||
<TableRow key={line.id}>
|
||
<TableCell>{index + 1}</TableCell>
|
||
<TableCell>
|
||
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
|
||
{line.childItemCode}
|
||
</code>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex items-center gap-2">
|
||
{line.childItemName}
|
||
{line.isBending && (
|
||
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700">
|
||
절곡품
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell className="text-right">{line.quantity}</TableCell>
|
||
<TableCell>{line.unit}</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 하단 액션 버튼 (sticky) */}
|
||
<div className={`fixed bottom-6 ${sidebarCollapsed ? 'left-[156px]' : 'left-[316px]'} right-[48px] px-6 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 flex items-center justify-between`}>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => router.back()}
|
||
>
|
||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||
목록으로
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
onClick={() => router.push(`/production/screen-production/${encodeURIComponent(item.itemCode)}?mode=edit&type=${item.itemType}&id=${item.id}`)}
|
||
>
|
||
<Edit className="w-4 h-4 mr-2" />
|
||
수정
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |