Files
sam-react-prod/src/components/items/ItemDetailView.tsx
유병철 81affdc441 feat: ESLint 정리 및 전체 코드 품질 개선
- eslint.config.mjs 규칙 강화 및 정리
- 전역 unused import/변수 제거 (312개 파일)
- next.config.ts, middleware, proxy route 개선
- CopyableCell molecule 추가
- 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리
- IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선
- execute-server-action 에러 핸들링 보강
2026-03-11 10:27:10 +09:00

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>
);
}