- eslint.config.mjs 규칙 강화 및 정리 - 전역 unused import/변수 제거 (312개 파일) - next.config.ts, middleware, proxy route 개선 - CopyableCell molecule 추가 - 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리 - IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선 - execute-server-action 에러 핸들링 보강
273 lines
12 KiB
TypeScript
273 lines
12 KiB
TypeScript
/**
|
|
* 품목 상세 보기 컴포넌트 (View Mode)
|
|
*
|
|
* API 연동: GET /api/proxy/items/{id} (id 기반 통일)
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { notFound } from 'next/navigation';
|
|
import ItemDetailClient from '@/components/items/ItemDetailClient';
|
|
import type { ItemMaster, ItemType, ProductCategory, PartType, PartUsage } from '@/types/item';
|
|
import { DetailPageSkeleton } from '@/components/ui/skeleton';
|
|
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
|
|
|
// Materials 타입 (SM, RM, CS는 Material 테이블 사용)
|
|
const MATERIAL_TYPES = ['SM', 'RM', 'CS'];
|
|
|
|
/**
|
|
* API 응답을 ItemMaster 타입으로 변환
|
|
*/
|
|
function mapApiResponseToItemMaster(data: Record<string, unknown>): ItemMaster {
|
|
// attributes 객체 추출 (조립부품 등의 동적 필드가 여기에 저장됨)
|
|
const attributes = (data.attributes || {}) as Record<string, unknown>;
|
|
// details 객체 추출 (PT 부품의 상세 정보가 여기에 저장됨)
|
|
const details = (data.details || {}) as Record<string, unknown>;
|
|
|
|
|
|
return {
|
|
id: String(data.id || ''),
|
|
// 백엔드 필드 매핑:
|
|
// - Product: code, name, product_type
|
|
// - Material: material_code, name, material_type (또는 type_code)
|
|
itemCode: String(data.code || data.material_code || data.item_code || data.itemCode || ''),
|
|
itemName: String(data.name || data.item_name || data.itemName || ''),
|
|
itemType: (data.product_type || data.material_type || data.type_code || data.item_type || data.itemType || 'FG') as ItemType,
|
|
unit: String(data.unit || 'EA'),
|
|
specification: data.specification ? String(data.specification) : undefined,
|
|
isActive: Boolean(data.is_active ?? data.isActive ?? true),
|
|
category1: data.category1 ? String(data.category1) : undefined,
|
|
category2: data.category2 ? String(data.category2) : undefined,
|
|
category3: data.category3 ? String(data.category3) : undefined,
|
|
salesPrice: data.sales_price ? Number(data.sales_price) : undefined,
|
|
purchasePrice: data.purchase_price ? Number(data.purchase_price) : undefined,
|
|
marginRate: data.margin_rate ? Number(data.margin_rate) : undefined,
|
|
processingCost: data.processing_cost ? Number(data.processing_cost) : undefined,
|
|
laborCost: data.labor_cost ? Number(data.labor_cost) : undefined,
|
|
installCost: data.install_cost ? Number(data.install_cost) : undefined,
|
|
productCategory: data.product_category ? (data.product_category as ProductCategory) : undefined,
|
|
lotAbbreviation: data.lot_abbreviation ? String(data.lot_abbreviation) : undefined,
|
|
note: data.note ? String(data.note) : undefined,
|
|
description: data.description ? String(data.description) : undefined,
|
|
safetyStock: data.safety_stock ? Number(data.safety_stock) : undefined,
|
|
leadTime: data.lead_time ? Number(data.lead_time) : undefined,
|
|
currentRevision: data.current_revision ? Number(data.current_revision) : 0,
|
|
isFinal: Boolean(data.is_final ?? false),
|
|
createdAt: String(data.created_at || data.createdAt || ''),
|
|
updatedAt: data.updated_at ? String(data.updated_at) : undefined,
|
|
// 부품 관련 - details, data, attributes 순으로 찾음
|
|
partType: (details.part_type || data.part_type || attributes.part_type) ? ((details.part_type || data.part_type || attributes.part_type) as PartType) : undefined,
|
|
partUsage: (details.part_usage || data.part_usage || attributes.part_usage) ? ((details.part_usage || data.part_usage || attributes.part_usage) as PartUsage) : undefined,
|
|
installationType: (data.installation_type || attributes.installation_type) ? String(data.installation_type || attributes.installation_type) : undefined,
|
|
assemblyType: (data.assembly_type || attributes.assembly_type) ? String(data.assembly_type || attributes.assembly_type) : undefined,
|
|
assemblyLength: (data.assembly_length || attributes.assembly_length || attributes.length) ? String(data.assembly_length || attributes.assembly_length || attributes.length) : undefined,
|
|
material: (data.material || attributes.material) ? String(data.material || attributes.material) : undefined,
|
|
sideSpecWidth: (data.side_spec_width || attributes.side_spec_width) ? String(data.side_spec_width || attributes.side_spec_width) : undefined,
|
|
sideSpecHeight: (data.side_spec_height || attributes.side_spec_height) ? String(data.side_spec_height || attributes.side_spec_height) : undefined,
|
|
guideRailModelType: (data.guide_rail_model_type || attributes.guide_rail_model_type) ? String(data.guide_rail_model_type || attributes.guide_rail_model_type) : undefined,
|
|
guideRailModel: (data.guide_rail_model || attributes.guide_rail_model) ? String(data.guide_rail_model || attributes.guide_rail_model) : undefined,
|
|
length: (data.length || attributes.length) ? String(data.length || attributes.length) : undefined,
|
|
// BOM (있으면)
|
|
bom: Array.isArray(data.bom) ? data.bom.map((bomItem: Record<string, unknown>, index: number) => ({
|
|
id: String(bomItem.id || bomItem.child_item_id || `bom-${index}`),
|
|
childItemCode: String(bomItem.child_item_code || bomItem.childItemCode || ''),
|
|
childItemName: String(bomItem.child_item_name || bomItem.childItemName || ''),
|
|
quantity: Number(bomItem.quantity || 1),
|
|
unit: String(bomItem.unit || 'EA'),
|
|
unitPrice: bomItem.unit_price ? Number(bomItem.unit_price) : undefined,
|
|
quantityFormula: bomItem.quantity_formula ? String(bomItem.quantity_formula) : undefined,
|
|
isBending: Boolean(bomItem.is_bending ?? false),
|
|
})) : undefined,
|
|
// 파일 관련 필드 (PT - 절곡/조립 부품)
|
|
// bending_diagram: data 또는 attributes에서 찾음
|
|
bendingDiagram: (() => {
|
|
const diagram = data.bending_diagram || attributes.bending_diagram;
|
|
return diagram ? String(diagram) : undefined;
|
|
})(),
|
|
// bending_diagram 파일 ID (프록시 이미지 로드용)
|
|
bendingDiagramFileId: (() => {
|
|
const files = data.files as { bending_diagram?: Array<{ id: number }> } | undefined;
|
|
const arr = files?.bending_diagram;
|
|
if (arr && arr.length > 0) return arr[arr.length - 1].id;
|
|
return undefined;
|
|
})(),
|
|
// bending_details: details.bending_details에서 찾음 (API 응답 구조)
|
|
bendingDetails: (() => {
|
|
const bendingDetails = details.bending_details || data.bending_details || attributes.bending_details;
|
|
return Array.isArray(bendingDetails) ? bendingDetails : undefined;
|
|
})(),
|
|
// 파일 관련 필드 (FG - 제품) - 배열의 마지막 파일 = 최신 파일
|
|
specificationFile: (() => {
|
|
const files = data.files as { specification_file?: Array<{ file_path: string }> } | undefined;
|
|
const arr = files?.specification_file;
|
|
if (arr && arr.length > 0) return arr[arr.length - 1].file_path;
|
|
return undefined;
|
|
})(),
|
|
specificationFileName: (() => {
|
|
const files = data.files as { specification_file?: Array<{ file_name: string }> } | undefined;
|
|
const arr = files?.specification_file;
|
|
if (arr && arr.length > 0) return arr[arr.length - 1].file_name;
|
|
return undefined;
|
|
})(),
|
|
specificationFileId: (() => {
|
|
const files = data.files as { specification_file?: Array<{ id: number }> } | undefined;
|
|
const arr = files?.specification_file;
|
|
if (arr && arr.length > 0) return arr[arr.length - 1].id;
|
|
return undefined;
|
|
})(),
|
|
certificationFile: (() => {
|
|
const files = data.files as { certification_file?: Array<{ file_path: string }> } | undefined;
|
|
const arr = files?.certification_file;
|
|
if (arr && arr.length > 0) return arr[arr.length - 1].file_path;
|
|
return undefined;
|
|
})(),
|
|
certificationFileName: (() => {
|
|
const files = data.files as { certification_file?: Array<{ file_name: string }> } | undefined;
|
|
const arr = files?.certification_file;
|
|
if (arr && arr.length > 0) return arr[arr.length - 1].file_name;
|
|
return undefined;
|
|
})(),
|
|
certificationFileId: (() => {
|
|
const files = data.files as { certification_file?: Array<{ id: number }> } | undefined;
|
|
const arr = files?.certification_file;
|
|
if (arr && arr.length > 0) return arr[arr.length - 1].id;
|
|
return undefined;
|
|
})(),
|
|
certificationNumber: data.certification_number ? String(data.certification_number) : undefined,
|
|
certificationStartDate: data.certification_start_date ? String(data.certification_start_date) : undefined,
|
|
certificationEndDate: data.certification_end_date ? String(data.certification_end_date) : undefined,
|
|
};
|
|
}
|
|
|
|
interface ItemDetailViewProps {
|
|
itemCode: string;
|
|
itemType: string;
|
|
itemId: string;
|
|
}
|
|
|
|
/**
|
|
* 품목 상세 보기 컴포넌트
|
|
*/
|
|
export function ItemDetailView({ itemCode, itemType, itemId }: ItemDetailViewProps) {
|
|
const [item, setItem] = useState<ItemMaster | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchItem = async () => {
|
|
if (!itemCode) {
|
|
setError('잘못된 품목 ID입니다.');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
|
|
// 모든 품목: GET /api/proxy/items/{id} (id 기반 통일)
|
|
if (!itemId) {
|
|
setError('품목 ID가 없습니다.');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
// 2025-12-15: 백엔드에서 id만으로 조회 가능 (item_type 불필요)
|
|
const isMaterial = MATERIAL_TYPES.includes(itemType);
|
|
const queryParams = new URLSearchParams();
|
|
if (!isMaterial) {
|
|
queryParams.append('include_bom', 'true');
|
|
}
|
|
|
|
const queryString = queryParams.toString();
|
|
const response = await fetch(`/api/proxy/items/${itemId}${queryString ? `?${queryString}` : ''}`);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
setError('품목을 찾을 수 없습니다.');
|
|
} else {
|
|
const errorData = await response.json().catch(() => null);
|
|
setError(errorData?.message || `오류 발생 (${response.status})`);
|
|
}
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.data) {
|
|
let mappedItem = mapApiResponseToItemMaster(result.data);
|
|
|
|
// BOM 데이터 별도 API 호출 (expandBomItems로 품목 정보 포함)
|
|
// GET /api/proxy/items/{id}/bom - 품목 정보가 확장된 BOM 데이터 반환
|
|
if (!isMaterial) {
|
|
try {
|
|
const bomResponse = await fetch(`/api/proxy/items/${itemId}/bom`);
|
|
const bomResult = await bomResponse.json();
|
|
|
|
if (bomResult.success && bomResult.data && Array.isArray(bomResult.data)) {
|
|
const expandedBomData = bomResult.data as Array<Record<string, unknown>>;
|
|
|
|
mappedItem = {
|
|
...mappedItem,
|
|
bom: expandedBomData.map((bomItem, index) => ({
|
|
id: String(bomItem.id || bomItem.child_item_id || `bom-${index}`),
|
|
childItemCode: String(bomItem.child_item_code || ''),
|
|
childItemName: String(bomItem.child_item_name || ''),
|
|
quantity: Number(bomItem.quantity || 1),
|
|
unit: String(bomItem.unit || 'EA'),
|
|
unitPrice: bomItem.unit_price ? Number(bomItem.unit_price) : undefined,
|
|
quantityFormula: bomItem.quantity_formula ? String(bomItem.quantity_formula) : undefined,
|
|
isBending: Boolean(bomItem.is_bending ?? false),
|
|
})),
|
|
};
|
|
}
|
|
} catch (bomErr) {
|
|
console.error('[ItemDetailView] BOM 조회 실패:', bomErr);
|
|
}
|
|
}
|
|
|
|
setItem(mappedItem);
|
|
} else {
|
|
setError(result.message || '품목 정보를 불러올 수 없습니다.');
|
|
}
|
|
} catch (err) {
|
|
console.error('[ItemDetailView] Error:', err);
|
|
setError('품목 정보를 불러오는 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchItem();
|
|
}, [itemCode, itemType, itemId]);
|
|
|
|
// 로딩 상태
|
|
if (isLoading) {
|
|
return <DetailPageSkeleton sections={2} fieldsPerSection={6} />;
|
|
}
|
|
|
|
// 에러 상태
|
|
if (error) {
|
|
return (
|
|
<ServerErrorPage
|
|
title="품목 정보를 불러올 수 없습니다"
|
|
message={error}
|
|
showBackButton={true}
|
|
showHomeButton={true}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// 품목 없음
|
|
if (!item) {
|
|
notFound();
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<ItemDetailClient item={item} />
|
|
</div>
|
|
);
|
|
}
|