refactor(WEB): 품목관리 경로 통합 - /items 삭제 및 /production/screen-production으로 일원화
- /items 폴더 삭제 (중복 경로 제거) - /production/screen-production에 신버전 DynamicItemForm 기반 페이지 적용 - 구버전 ItemForm 연결 제거로 등록/수정 오류 해결 - 컴포넌트 내부 경로 참조 /items → /production/screen-production 변경 - ItemListClient, ItemForm, ItemDetailClient, ItemDetailEdit, DynamicItemForm Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트
|
||||
* 기존 쿼리 파라미터(type, id)는 유지
|
||||
*/
|
||||
export default function ItemEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
// 기존 쿼리 파라미터 유지하면서 mode=edit 추가
|
||||
const type = searchParams.get('type') || 'FG';
|
||||
const itemId = searchParams.get('id') || '';
|
||||
|
||||
router.replace(`/items/${id}?type=${type}&id=${itemId}&mode=edit`);
|
||||
}, [id, router, searchParams]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 품목 상세/수정 페이지 (Client Component)
|
||||
* V2 패턴: ?mode=edit로 수정 모드 전환
|
||||
*/
|
||||
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ItemDetailView } from '@/components/items/ItemDetailView';
|
||||
import { ItemDetailEdit } from '@/components/items/ItemDetailEdit';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function ItemDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// URL에서 type, id, mode 쿼리 파라미터 읽기
|
||||
const itemType = searchParams.get('type') || 'FG';
|
||||
const itemId = searchParams.get('id') || '';
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
// 품목 코드 디코딩
|
||||
const itemCode = decodeURIComponent(id);
|
||||
|
||||
if (mode === 'edit') {
|
||||
return <ItemDetailEdit itemCode={itemCode} itemType={itemType} itemId={itemId} />;
|
||||
}
|
||||
|
||||
return <ItemDetailView itemCode={itemCode} itemType={itemType} itemId={itemId} />;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* 품목 등록 페이지
|
||||
*
|
||||
* DynamicItemForm을 사용하여 품목기준관리 데이터 기반 동적 폼 렌더링
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import DynamicItemForm from '@/components/items/DynamicItemForm';
|
||||
import type { DynamicFormData } from '@/components/items/DynamicItemForm/types';
|
||||
// 2025-12-16: options 관련 변환 로직 제거
|
||||
// 백엔드가 품목기준관리 field_key 매핑을 처리하므로 프론트에서 변환 불필요
|
||||
import { DuplicateCodeError } from '@/lib/api/error-handler';
|
||||
|
||||
// 기존 ItemForm (주석처리 - 롤백 시 사용)
|
||||
// import ItemForm from '@/components/items/ItemForm';
|
||||
// import type { CreateItemFormData } from '@/lib/utils/validation';
|
||||
|
||||
export default function CreateItemPage() {
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (data: DynamicFormData) => {
|
||||
setSubmitError(null);
|
||||
|
||||
// 필드명 변환: spec → specification (백엔드 API 규격)
|
||||
const submitData = { ...data };
|
||||
if (submitData.spec !== undefined) {
|
||||
submitData.specification = submitData.spec;
|
||||
delete submitData.spec;
|
||||
}
|
||||
|
||||
// 2025-12-15: item_type은 Request Body에서 필수 (ItemService.store validation)
|
||||
// product_type과 item_type을 동일하게 설정
|
||||
const itemType = submitData.product_type as string;
|
||||
submitData.item_type = itemType;
|
||||
|
||||
// API 호출 전 이미지 데이터 제거 (파일 업로드는 별도 API 사용)
|
||||
// bending_diagram이 base64 데이터인 경우 제거 (JSON에 포함시키면 안됨)
|
||||
if (submitData.bending_diagram && typeof submitData.bending_diagram === 'string' && (submitData.bending_diagram as string).startsWith('data:')) {
|
||||
delete submitData.bending_diagram;
|
||||
}
|
||||
// 시방서/인정서 파일 필드도 base64면 제거
|
||||
if (submitData.specification_file && typeof submitData.specification_file === 'string' && (submitData.specification_file as string).startsWith('data:')) {
|
||||
delete submitData.specification_file;
|
||||
}
|
||||
if (submitData.certification_file && typeof submitData.certification_file === 'string' && (submitData.certification_file as string).startsWith('data:')) {
|
||||
delete submitData.certification_file;
|
||||
}
|
||||
|
||||
// API 호출: POST /api/proxy/items
|
||||
// 백엔드에서 product_type에 따라 Product/Material 분기 처리
|
||||
const response = await fetch('/api/proxy/items', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
// 2025-12-11: 백엔드 중복 에러 처리 (DuplicateCodeException)
|
||||
// duplicate_id가 있으면 DuplicateCodeError throw → DynamicItemForm에서 다이얼로그 표시
|
||||
if (response.status === 400 && result.duplicate_id) {
|
||||
console.warn('[CreateItemPage] 품목코드 중복 에러:', result);
|
||||
throw new DuplicateCodeError(
|
||||
result.message || '해당 품목코드가 이미 존재합니다.',
|
||||
result.duplicate_id,
|
||||
result.duplicate_code
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage = result.message || '품목 등록에 실패했습니다.';
|
||||
console.error('[CreateItemPage] 품목 등록 실패:', errorMessage);
|
||||
setSubmitError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 성공 시 DynamicItemForm 내부에서 /items로 리다이렉트 처리됨
|
||||
// console.log('[CreateItemPage] 품목 등록 성공:', result.data);
|
||||
|
||||
// 생성된 품목 ID를 포함한 데이터 반환 (파일 업로드용)
|
||||
return { id: result.data.id, ...result.data };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{submitError && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-900">
|
||||
⚠️ {submitError}
|
||||
</div>
|
||||
)}
|
||||
<DynamicItemForm
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* 품목 관리 페이지
|
||||
*
|
||||
* 품목기준관리 API 연동
|
||||
* - 품목 목록: API에서 조회
|
||||
* - 테이블 컬럼: custom-tabs API에서 동적 구성
|
||||
*/
|
||||
|
||||
import ItemListClient from '@/components/items/ItemListClient';
|
||||
|
||||
/**
|
||||
* 품목 목록 페이지
|
||||
*/
|
||||
export default function ItemsPage() {
|
||||
return <ItemListClient />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메타데이터 설정
|
||||
*/
|
||||
export const metadata = {
|
||||
title: '품목 관리',
|
||||
description: '품목 목록 조회 및 관리',
|
||||
};
|
||||
@@ -1,213 +1,32 @@
|
||||
/**
|
||||
* 품목 수정 페이지
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import ItemForm from '@/components/items/ItemForm';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
import type { ItemMaster } from '@/types/item';
|
||||
import type { CreateItemFormData } from '@/lib/utils/validation';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
// Mock 데이터 (API 연동 전 임시)
|
||||
const mockItems: ItemMaster[] = [
|
||||
{
|
||||
id: '1',
|
||||
itemCode: 'KD-FG-001',
|
||||
itemName: '스크린 제품 A',
|
||||
itemType: 'FG',
|
||||
unit: 'EA',
|
||||
specification: '2000x2000',
|
||||
isActive: true,
|
||||
category1: '본체부품',
|
||||
category2: '가이드시스템',
|
||||
salesPrice: 150000,
|
||||
purchasePrice: 100000,
|
||||
marginRate: 33.3,
|
||||
processingCost: 20000,
|
||||
laborCost: 15000,
|
||||
installCost: 10000,
|
||||
productCategory: 'SCREEN',
|
||||
lotAbbreviation: 'KD',
|
||||
note: '스크린 제품 샘플입니다.',
|
||||
safetyStock: 10,
|
||||
leadTime: 7,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
updatedAt: '2025-01-12T00:00:00Z',
|
||||
bom: [
|
||||
{
|
||||
id: 'bom-1',
|
||||
childItemCode: 'KD-PT-001',
|
||||
childItemName: '가이드레일(벽면형)',
|
||||
quantity: 2,
|
||||
unit: 'EA',
|
||||
unitPrice: 35000,
|
||||
quantityFormula: 'H / 1000',
|
||||
},
|
||||
{
|
||||
id: 'bom-2',
|
||||
childItemCode: 'KD-PT-002',
|
||||
childItemName: '절곡품 샘플',
|
||||
quantity: 4,
|
||||
unit: 'EA',
|
||||
unitPrice: 30000,
|
||||
isBending: true,
|
||||
},
|
||||
{
|
||||
id: 'bom-3',
|
||||
childItemCode: 'KD-SM-001',
|
||||
childItemName: '볼트 M6x20',
|
||||
quantity: 20,
|
||||
unit: 'EA',
|
||||
unitPrice: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
itemCode: 'KD-PT-001',
|
||||
itemName: '가이드레일(벽면형)',
|
||||
itemType: 'PT',
|
||||
unit: 'EA',
|
||||
specification: '2438mm',
|
||||
isActive: true,
|
||||
category1: '본체부품',
|
||||
category2: '가이드시스템',
|
||||
category3: '가이드레일',
|
||||
salesPrice: 50000,
|
||||
purchasePrice: 35000,
|
||||
marginRate: 30,
|
||||
partType: 'ASSEMBLY',
|
||||
partUsage: 'GUIDE_RAIL',
|
||||
installationType: '벽면형',
|
||||
assemblyType: 'M',
|
||||
assemblyLength: '2438',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
itemCode: 'KD-PT-002',
|
||||
itemName: '절곡품 샘플',
|
||||
itemType: 'PT',
|
||||
unit: 'EA',
|
||||
specification: 'EGI 1.55T',
|
||||
isActive: true,
|
||||
partType: 'BENDING',
|
||||
material: 'EGI 1.55T',
|
||||
length: '2000',
|
||||
salesPrice: 30000,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
itemCode: 'KD-RM-001',
|
||||
itemName: 'SPHC-SD',
|
||||
itemType: 'RM',
|
||||
unit: 'KG',
|
||||
specification: '1.6T x 1219 x 2438',
|
||||
isActive: true,
|
||||
category1: '철강재',
|
||||
purchasePrice: 1500,
|
||||
material: 'SPHC-SD',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
itemCode: 'KD-SM-001',
|
||||
itemName: '볼트 M6x20',
|
||||
itemType: 'SM',
|
||||
unit: 'EA',
|
||||
specification: 'M6x20',
|
||||
isActive: true,
|
||||
category1: '구조재/부속품',
|
||||
category2: '볼트/너트',
|
||||
purchasePrice: 50,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
];
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function EditItemPage() {
|
||||
const params = useParams();
|
||||
/**
|
||||
* V2 하위 호환성: /[id]/edit → /[id]?mode=edit 리다이렉트
|
||||
* 기존 쿼리 파라미터(type, id)는 유지
|
||||
*/
|
||||
export default function ItemEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [item, setItem] = useState<ItemMaster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동 시 fetchItemByCode() 호출
|
||||
const fetchItem = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// params.id 타입 체크
|
||||
if (!params.id || typeof params.id !== 'string') {
|
||||
alert('잘못된 품목 ID입니다.');
|
||||
router.push('/items');
|
||||
return;
|
||||
}
|
||||
// 기존 쿼리 파라미터 유지하면서 mode=edit 추가
|
||||
const type = searchParams.get('type') || 'FG';
|
||||
const itemId = searchParams.get('id') || '';
|
||||
|
||||
// Mock: 데이터 조회
|
||||
const itemCode = decodeURIComponent(params.id);
|
||||
const foundItem = mockItems.find((item) => item.itemCode === itemCode);
|
||||
|
||||
if (foundItem) {
|
||||
setItem(foundItem);
|
||||
} else {
|
||||
alert('품목을 찾을 수 없습니다.');
|
||||
router.push('/items');
|
||||
}
|
||||
} catch {
|
||||
alert('품목 조회에 실패했습니다.');
|
||||
router.push('/items');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchItem();
|
||||
}, [params.id, router]);
|
||||
|
||||
const handleSubmit = async (data: CreateItemFormData) => {
|
||||
// TODO: API 연동 시 updateItem() 호출
|
||||
console.log('품목 수정 데이터:', data);
|
||||
|
||||
// Mock: 성공 메시지
|
||||
alert(`품목 "${data.itemName}" (${data.itemCode})이(가) 수정되었습니다.`);
|
||||
|
||||
// API 연동 예시:
|
||||
// const updatedItem = await updateItem(item.itemCode, data);
|
||||
// router.push(`/items/${updatedItem.itemCode}`);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<ServerErrorPage
|
||||
title="품목 정보를 불러올 수 없습니다"
|
||||
message="품목을 찾을 수 없습니다."
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
router.replace(`/production/screen-production/${id}?type=${type}&id=${itemId}&mode=edit`);
|
||||
}, [id, router, searchParams]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ItemForm mode="edit" initialData={item} onSubmit={handleSubmit} />
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +1,34 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 품목 상세 조회 페이지 (Client Component)
|
||||
* 품목 상세/수정 페이지 (Client Component)
|
||||
* V2 패턴: ?mode=edit로 수정 모드 전환
|
||||
*/
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import ItemDetailClient from '@/components/items/ItemDetailClient';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
import type { ItemMaster } from '@/types/item';
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ItemDetailView } from '@/components/items/ItemDetailView';
|
||||
import { ItemDetailEdit } from '@/components/items/ItemDetailEdit';
|
||||
|
||||
// Mock 데이터 (API 연동 전 임시)
|
||||
const mockItems: ItemMaster[] = [
|
||||
{
|
||||
id: '1',
|
||||
itemCode: 'KD-FG-001',
|
||||
itemName: '스크린 제품 A',
|
||||
itemType: 'FG',
|
||||
unit: 'EA',
|
||||
specification: '2000x2000',
|
||||
isActive: true,
|
||||
category1: '본체부품',
|
||||
category2: '가이드시스템',
|
||||
salesPrice: 150000,
|
||||
purchasePrice: 100000,
|
||||
marginRate: 33.3,
|
||||
processingCost: 20000,
|
||||
laborCost: 15000,
|
||||
installCost: 10000,
|
||||
productCategory: 'SCREEN',
|
||||
lotAbbreviation: 'KD',
|
||||
note: '스크린 제품 샘플입니다.',
|
||||
safetyStock: 10,
|
||||
leadTime: 7,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
updatedAt: '2025-01-12T00:00:00Z',
|
||||
bom: [
|
||||
{
|
||||
id: 'bom-1',
|
||||
childItemCode: 'KD-PT-001',
|
||||
childItemName: '가이드레일(벽면형)',
|
||||
quantity: 2,
|
||||
unit: 'EA',
|
||||
unitPrice: 35000,
|
||||
quantityFormula: 'H / 1000',
|
||||
},
|
||||
{
|
||||
id: 'bom-2',
|
||||
childItemCode: 'KD-PT-002',
|
||||
childItemName: '절곡품 샘플',
|
||||
quantity: 4,
|
||||
unit: 'EA',
|
||||
unitPrice: 30000,
|
||||
isBending: true,
|
||||
},
|
||||
{
|
||||
id: 'bom-3',
|
||||
childItemCode: 'KD-SM-001',
|
||||
childItemName: '볼트 M6x20',
|
||||
quantity: 20,
|
||||
unit: 'EA',
|
||||
unitPrice: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
itemCode: 'KD-PT-001',
|
||||
itemName: '가이드레일(벽면형)',
|
||||
itemType: 'PT',
|
||||
unit: 'EA',
|
||||
specification: '2438mm',
|
||||
isActive: true,
|
||||
category1: '본체부품',
|
||||
category2: '가이드시스템',
|
||||
category3: '가이드레일',
|
||||
salesPrice: 50000,
|
||||
purchasePrice: 35000,
|
||||
marginRate: 30,
|
||||
partType: 'ASSEMBLY',
|
||||
partUsage: 'GUIDE_RAIL',
|
||||
installationType: '벽면형',
|
||||
assemblyType: 'M',
|
||||
assemblyLength: '2438',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
itemCode: 'KD-PT-002',
|
||||
itemName: '절곡품 샘플',
|
||||
itemType: 'PT',
|
||||
unit: 'EA',
|
||||
specification: 'EGI 1.55T',
|
||||
isActive: true,
|
||||
partType: 'BENDING',
|
||||
material: 'EGI 1.55T',
|
||||
length: '2000',
|
||||
salesPrice: 30000,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
itemCode: 'KD-RM-001',
|
||||
itemName: 'SPHC-SD',
|
||||
itemType: 'RM',
|
||||
unit: 'KG',
|
||||
specification: '1.6T x 1219 x 2438',
|
||||
isActive: true,
|
||||
category1: '철강재',
|
||||
purchasePrice: 1500,
|
||||
material: 'SPHC-SD',
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
itemCode: 'KD-SM-001',
|
||||
itemName: '볼트 M6x20',
|
||||
itemType: 'SM',
|
||||
unit: 'EA',
|
||||
specification: 'M6x20',
|
||||
isActive: true,
|
||||
category1: '구조재/부속품',
|
||||
category2: '볼트/너트',
|
||||
purchasePrice: 50,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
createdAt: '2025-01-10T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 품목 상세 페이지
|
||||
*/
|
||||
export default function ItemDetailPage({
|
||||
params,
|
||||
}: {
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
}
|
||||
|
||||
export default function ItemDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [item, setItem] = useState<ItemMaster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
// API 연동 전 mock 데이터 사용
|
||||
const foundItem = mockItems.find(
|
||||
(item) => item.itemCode === decodeURIComponent(id)
|
||||
);
|
||||
setItem(foundItem || null);
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
// URL에서 type, id, mode 쿼리 파라미터 읽기
|
||||
const itemType = searchParams.get('type') || 'FG';
|
||||
const itemId = searchParams.get('id') || '';
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
|
||||
// 품목 코드 디코딩
|
||||
const itemCode = decodeURIComponent(id);
|
||||
|
||||
if (mode === 'edit') {
|
||||
return <ItemDetailEdit itemCode={itemCode} itemType={itemType} itemId={itemId} />;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<ServerErrorPage
|
||||
title="품목 정보를 불러올 수 없습니다"
|
||||
message="품목을 찾을 수 없습니다."
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ItemDetailClient item={item} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <ItemDetailView itemCode={itemCode} itemType={itemType} itemId={itemId} />;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,101 @@
|
||||
/**
|
||||
* 품목 등록 페이지
|
||||
*
|
||||
* DynamicItemForm을 사용하여 품목기준관리 데이터 기반 동적 폼 렌더링
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import ItemForm from '@/components/items/ItemForm';
|
||||
import type { CreateItemFormData } from '@/lib/utils/validation';
|
||||
import { useState } from 'react';
|
||||
import DynamicItemForm from '@/components/items/DynamicItemForm';
|
||||
import type { DynamicFormData } from '@/components/items/DynamicItemForm/types';
|
||||
// 2025-12-16: options 관련 변환 로직 제거
|
||||
// 백엔드가 품목기준관리 field_key 매핑을 처리하므로 프론트에서 변환 불필요
|
||||
import { DuplicateCodeError } from '@/lib/api/error-handler';
|
||||
|
||||
// 기존 ItemForm (주석처리 - 롤백 시 사용)
|
||||
// import ItemForm from '@/components/items/ItemForm';
|
||||
// import type { CreateItemFormData } from '@/lib/utils/validation';
|
||||
|
||||
export default function CreateItemPage() {
|
||||
const handleSubmit = async (data: CreateItemFormData) => {
|
||||
// TODO: API 연동 시 createItem() 호출
|
||||
console.log('품목 등록 데이터:', data);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
// Mock: 성공 메시지
|
||||
alert(`품목 "${data.itemName}" (${data.itemCode})이(가) 등록되었습니다.`);
|
||||
const handleSubmit = async (data: DynamicFormData) => {
|
||||
setSubmitError(null);
|
||||
|
||||
// API 연동 예시:
|
||||
// const newItem = await createItem(data);
|
||||
// router.push(`/items/${newItem.itemCode}`);
|
||||
// 필드명 변환: spec → specification (백엔드 API 규격)
|
||||
const submitData = { ...data };
|
||||
if (submitData.spec !== undefined) {
|
||||
submitData.specification = submitData.spec;
|
||||
delete submitData.spec;
|
||||
}
|
||||
|
||||
// 2025-12-15: item_type은 Request Body에서 필수 (ItemService.store validation)
|
||||
// product_type과 item_type을 동일하게 설정
|
||||
const itemType = submitData.product_type as string;
|
||||
submitData.item_type = itemType;
|
||||
|
||||
// API 호출 전 이미지 데이터 제거 (파일 업로드는 별도 API 사용)
|
||||
// bending_diagram이 base64 데이터인 경우 제거 (JSON에 포함시키면 안됨)
|
||||
if (submitData.bending_diagram && typeof submitData.bending_diagram === 'string' && (submitData.bending_diagram as string).startsWith('data:')) {
|
||||
delete submitData.bending_diagram;
|
||||
}
|
||||
// 시방서/인정서 파일 필드도 base64면 제거
|
||||
if (submitData.specification_file && typeof submitData.specification_file === 'string' && (submitData.specification_file as string).startsWith('data:')) {
|
||||
delete submitData.specification_file;
|
||||
}
|
||||
if (submitData.certification_file && typeof submitData.certification_file === 'string' && (submitData.certification_file as string).startsWith('data:')) {
|
||||
delete submitData.certification_file;
|
||||
}
|
||||
|
||||
// API 호출: POST /api/proxy/items
|
||||
// 백엔드에서 product_type에 따라 Product/Material 분기 처리
|
||||
const response = await fetch('/api/proxy/items', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
// 2025-12-11: 백엔드 중복 에러 처리 (DuplicateCodeException)
|
||||
// duplicate_id가 있으면 DuplicateCodeError throw → DynamicItemForm에서 다이얼로그 표시
|
||||
if (response.status === 400 && result.duplicate_id) {
|
||||
console.warn('[CreateItemPage] 품목코드 중복 에러:', result);
|
||||
throw new DuplicateCodeError(
|
||||
result.message || '해당 품목코드가 이미 존재합니다.',
|
||||
result.duplicate_id,
|
||||
result.duplicate_code
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage = result.message || '품목 등록에 실패했습니다.';
|
||||
console.error('[CreateItemPage] 품목 등록 실패:', errorMessage);
|
||||
setSubmitError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 성공 시 DynamicItemForm 내부에서 /items로 리다이렉트 처리됨
|
||||
// console.log('[CreateItemPage] 품목 등록 성공:', result.data);
|
||||
|
||||
// 생성된 품목 ID를 포함한 데이터 반환 (파일 업로드용)
|
||||
return { id: result.data.id, ...result.data };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ItemForm mode="create" onSubmit={handleSubmit} />
|
||||
{submitError && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-900">
|
||||
⚠️ {submitError}
|
||||
</div>
|
||||
)}
|
||||
<DynamicItemForm
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 품목 목록 페이지 (Client Component)
|
||||
* 품목 관리 페이지
|
||||
*
|
||||
* Next.js 15 App Router
|
||||
* 품목기준관리 API 연동
|
||||
* - 품목 목록: API에서 조회
|
||||
* - 테이블 컬럼: custom-tabs API에서 동적 구성
|
||||
*/
|
||||
|
||||
import ItemListClient from '@/components/items/ItemListClient';
|
||||
@@ -13,4 +13,12 @@ import ItemListClient from '@/components/items/ItemListClient';
|
||||
*/
|
||||
export default function ItemsPage() {
|
||||
return <ItemListClient />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메타데이터 설정
|
||||
*/
|
||||
export const metadata = {
|
||||
title: '품목 관리',
|
||||
description: '품목 목록 조회 및 관리',
|
||||
};
|
||||
@@ -419,7 +419,7 @@ export default function DynamicItemForm({
|
||||
}
|
||||
}
|
||||
|
||||
router.push('/items');
|
||||
router.push('/production/screen-production');
|
||||
router.refresh();
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -603,7 +603,7 @@ export default function DynamicItemForm({
|
||||
const itemType = duplicateCheckResult.duplicateItemType || selectedItemType || 'PT';
|
||||
const itemId = duplicateCheckResult.duplicateId;
|
||||
// code는 없으므로 id를 path에 사용 (edit 페이지에서 id 쿼리 파라미터로 조회)
|
||||
router.push(`/items/${itemId}/edit?type=${itemType}&id=${itemId}`);
|
||||
router.push(`/production/screen-production/${itemId}/edit?type=${itemType}&id=${itemId}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => router.push(`/items/${encodeURIComponent(item.itemCode)}/edit?type=${item.itemType}&id=${item.id}`)}
|
||||
onClick={() => router.push(`/production/screen-production/${encodeURIComponent(item.itemCode)}/edit?type=${item.itemType}&id=${item.id}`)}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
수정
|
||||
|
||||
@@ -377,7 +377,7 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
||||
<p className="text-destructive">{error}</p>
|
||||
<button
|
||||
onClick={() => router.push('/items')}
|
||||
onClick={() => router.push('/production/screen-production')}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
품목 목록으로 돌아가기
|
||||
@@ -392,7 +392,7 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
||||
<p className="text-muted-foreground">품목 정보를 불러올 수 없습니다.</p>
|
||||
<button
|
||||
onClick={() => router.push('/items')}
|
||||
onClick={() => router.push('/production/screen-production')}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
품목 목록으로 돌아가기
|
||||
|
||||
@@ -174,7 +174,7 @@ export default function ItemForm({ mode, initialData, onSubmit }: ItemFormProps)
|
||||
certificationFileName: certificationFile?.name,
|
||||
};
|
||||
await onSubmit(finalData);
|
||||
router.push('/items');
|
||||
router.push('/production/screen-production');
|
||||
router.refresh();
|
||||
} catch {
|
||||
alert('품목 저장에 실패했습니다.');
|
||||
|
||||
@@ -152,12 +152,12 @@ export default function ItemListClient() {
|
||||
|
||||
const handleView = (itemCode: string, itemType: string, itemId: string) => {
|
||||
// itemType을 query param으로 전달 (Materials 조회를 위해)
|
||||
router.push(`/items/${encodeURIComponent(itemCode)}?type=${itemType}&id=${itemId}`);
|
||||
router.push(`/production/screen-production/${encodeURIComponent(itemCode)}?type=${itemType}&id=${itemId}`);
|
||||
};
|
||||
|
||||
const handleEdit = (itemCode: string, itemType: string, itemId: string) => {
|
||||
// itemType을 query param으로 전달 (Materials 조회를 위해)
|
||||
router.push(`/items/${encodeURIComponent(itemCode)}/edit?type=${itemType}&id=${itemId}`);
|
||||
router.push(`/production/screen-production/${encodeURIComponent(itemCode)}/edit?type=${itemType}&id=${itemId}`);
|
||||
};
|
||||
|
||||
// 삭제 확인 다이얼로그 열기
|
||||
@@ -312,7 +312,7 @@ export default function ItemListClient() {
|
||||
// 등록 버튼 (createButton 사용 - headerActions 대신)
|
||||
createButton: {
|
||||
label: '품목 등록',
|
||||
onClick: () => router.push('/items/create'),
|
||||
onClick: () => router.push('/production/screen-production/create'),
|
||||
icon: Plus,
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user