feat: 품목 관리 및 마스터 데이터 관리 시스템 구현

주요 기능:
- 품목 CRUD 기능 (생성, 조회, 수정)
- 품목 마스터 데이터 관리 시스템
- BOM(Bill of Materials) 관리 기능
- 도면 캔버스 기능
- 품목 속성 및 카테고리 관리
- 스크린 인쇄 생산 관리 페이지

기술 개선:
- localStorage SSR 호환성 수정 (9개 useState 초기화)
- Shadcn UI 컴포넌트 추가 (table, tabs, alert, drawer 등)
- DataContext 및 DeveloperModeContext 추가
- API 라우트 구현 (items, master-data)
- 타입 정의 및 유틸리티 함수 추가

빌드 테스트:  성공 (3.1초)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-11-18 14:17:52 +09:00
parent 21edc932d9
commit 63f5df7d7d
56 changed files with 23927 additions and 149 deletions

View File

@@ -0,0 +1,208 @@
/**
* 품목 수정 페이지
*/
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import ItemForm from '@/components/items/ItemForm';
import type { ItemMaster } from '@/types/item';
import type { CreateItemFormData } from '@/lib/utils/validation';
// 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 EditItemPage() {
const params = useParams();
const router = useRouter();
const [item, setItem] = useState<ItemMaster | null>(null);
const [isLoading, setIsLoading] = useState(true);
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;
}
// 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 (
<div className="py-6">
<div className="text-center py-8"> ...</div>
</div>
);
}
if (!item) {
return null;
}
return (
<div className="py-6">
<ItemForm mode="edit" initialData={item} onSubmit={handleSubmit} />
</div>
);
}

View File

@@ -0,0 +1,195 @@
/**
* 품목 상세 조회 페이지
*/
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import ItemDetailClient from '@/components/items/ItemDetailClient';
import type { ItemMaster } from '@/types/item';
// 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',
},
];
/**
* 품목 조회 함수
* TODO: API 연동 시 fetchItemByCode()로 교체
*/
async function getItemByCode(itemCode: string): Promise<ItemMaster | null> {
// API 연동 전 mock 데이터 반환
// const item = await fetchItemByCode(itemCode);
const item = mockItems.find(
(item) => item.itemCode === decodeURIComponent(itemCode)
);
return item || null;
}
/**
* 품목 상세 페이지
*/
export default async function ItemDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const item = await getItemByCode(id);
if (!item) {
notFound();
}
return (
<div className="py-6">
<Suspense fallback={<div className="text-center py-8"> ...</div>}>
<ItemDetailClient item={item} />
</Suspense>
</div>
);
}
/**
* 메타데이터 설정
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const item = await getItemByCode(id);
if (!item) {
return {
title: '품목을 찾을 수 없습니다',
};
}
return {
title: `${item.itemName} - 품목 상세`,
description: `${item.itemCode} 품목 정보`,
};
}

View File

@@ -0,0 +1,28 @@
/**
* 품목 등록 페이지
*/
'use client';
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);
// Mock: 성공 메시지
alert(`품목 "${data.itemName}" (${data.itemCode})이(가) 등록되었습니다.`);
// API 연동 예시:
// const newItem = await createItem(data);
// router.push(`/items/${newItem.itemCode}`);
};
return (
<div className="py-6">
<ItemForm mode="create" onSubmit={handleSubmit} />
</div>
);
}

View File

@@ -0,0 +1,162 @@
/**
* 품목 목록 페이지 (Server Component)
*
* Next.js 15 App Router
* 서버에서 데이터 fetching 후 Client Component로 전달
*/
import { Suspense } from 'react';
import ItemListClient from '@/components/items/ItemListClient';
import type { ItemMaster } from '@/types/item';
// 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,
productCategory: 'SCREEN',
lotAbbreviation: 'KD',
currentRevision: 0,
isFinal: false,
createdAt: '2025-01-10T00:00:00Z',
},
{
id: '2',
itemCode: 'KD-PT-001',
itemName: '가이드레일(벽면형)',
itemType: 'PT',
unit: 'EA',
specification: '2438mm',
isActive: true,
category1: '본체부품',
category2: '가이드시스템',
category3: '가이드레일',
salesPrice: 50000,
purchasePrice: 35000,
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',
},
{
id: '6',
itemCode: 'KD-CS-001',
itemName: '절삭유',
itemType: 'CS',
unit: 'L',
specification: '20L',
isActive: true,
purchasePrice: 30000,
currentRevision: 0,
isFinal: false,
createdAt: '2025-01-10T00:00:00Z',
},
{
id: '7',
itemCode: 'KD-FG-002',
itemName: '철재 제품 B',
itemType: 'FG',
unit: 'SET',
specification: '3000x2500',
isActive: false,
category1: '본체부품',
salesPrice: 200000,
productCategory: 'STEEL',
lotAbbreviation: 'KD',
currentRevision: 0,
isFinal: false,
createdAt: '2025-01-09T00:00:00Z',
},
];
/**
* 품목 목록 조회 함수
* TODO: API 연동 시 fetchItems()로 교체
*/
async function getItems(): Promise<ItemMaster[]> {
// API 연동 전 mock 데이터 반환
// const items = await fetchItems();
return mockItems;
}
/**
* 품목 목록 페이지
*/
export default async function ItemsPage() {
const items = await getItems();
return (
<div className="p-6">
<Suspense fallback={<div className="text-center py-8"> ...</div>}>
<ItemListClient items={items} />
</Suspense>
</div>
);
}
/**
* 메타데이터 설정
*/
export const metadata = {
title: '품목 관리',
description: '품목 목록 조회 및 관리',
};

View File

@@ -2,6 +2,8 @@
import { useAuthGuard } from '@/hooks/useAuthGuard';
import DashboardLayout from '@/layouts/DashboardLayout';
import { DataProvider } from '@/contexts/DataContext';
import { DeveloperModeProvider } from '@/contexts/DeveloperModeContext';
/**
* Protected Layout
@@ -9,13 +11,15 @@ import DashboardLayout from '@/layouts/DashboardLayout';
* Purpose:
* - Apply authentication guard to all protected pages
* - Apply common layout (sidebar, header) to all protected pages
* - Provide global context (DataProvider, DeveloperModeProvider)
* - Prevent browser back button cache issues
* - Centralized protection for all routes under (protected)
*
* Protected Routes:
* - /dashboard
* - /base/* (기초정보관리)
* - /system/* (시스템관리)
* - /items/* (품목관리)
* - /master-data/* (기준정보관리)
* - /production/* (생산관리)
* - All other authenticated pages
*/
export default function ProtectedLayout({
@@ -26,6 +30,12 @@ export default function ProtectedLayout({
// 🔒 모든 하위 페이지에 인증 보호 적용
useAuthGuard();
// 🎨 모든 하위 페이지에 공통 레이아웃 적용 (사이드바, 헤더)
return <DashboardLayout>{children}</DashboardLayout>;
// 🎨 모든 하위 페이지에 공통 레이아웃 및 Context 적용
return (
<DataProvider>
<DeveloperModeProvider>
<DashboardLayout>{children}</DashboardLayout>
</DeveloperModeProvider>
</DataProvider>
);
}

View File

@@ -0,0 +1,35 @@
/**
* 품목기준관리 페이지 (Item Master Data Management)
*
* 품목기준정보, 견적, 수주, 계산식, 단가 등의 기준정보를 관리하는 시스템
* 버전관리 시스템 포함
*/
import { Suspense } from 'react';
import { ItemMasterDataManagement } from '@/components/items/ItemMasterDataManagement';
import type { Metadata } from 'next';
/**
* 메타데이터 설정
*/
export const metadata: Metadata = {
title: '품목기준관리',
description: '품목기준정보 관리 시스템 - 품목, 견적, 수주, 계산식, 단가 등의 기준정보 및 버전관리',
};
export default function ItemMasterDataManagementPage() {
return (
<div>
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
}>
<ItemMasterDataManagement />
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,208 @@
/**
* 품목 수정 페이지
*/
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import ItemForm from '@/components/items/ItemForm';
import type { ItemMaster } from '@/types/item';
import type { CreateItemFormData } from '@/lib/utils/validation';
// 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 EditItemPage() {
const params = useParams();
const router = useRouter();
const [item, setItem] = useState<ItemMaster | null>(null);
const [isLoading, setIsLoading] = useState(true);
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;
}
// 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 (
<div className="py-6">
<div className="text-center py-8"> ...</div>
</div>
);
}
if (!item) {
return null;
}
return (
<div className="py-6">
<ItemForm mode="edit" initialData={item} onSubmit={handleSubmit} />
</div>
);
}

View File

@@ -0,0 +1,195 @@
/**
* 품목 상세 조회 페이지
*/
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import ItemDetailClient from '@/components/items/ItemDetailClient';
import type { ItemMaster } from '@/types/item';
// 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',
},
];
/**
* 품목 조회 함수
* TODO: API 연동 시 fetchItemByCode()로 교체
*/
async function getItemByCode(itemCode: string): Promise<ItemMaster | null> {
// API 연동 전 mock 데이터 반환
// const item = await fetchItemByCode(itemCode);
const item = mockItems.find(
(item) => item.itemCode === decodeURIComponent(itemCode)
);
return item || null;
}
/**
* 품목 상세 페이지
*/
export default async function ItemDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const item = await getItemByCode(id);
if (!item) {
notFound();
}
return (
<div className="py-6">
<Suspense fallback={<div className="text-center py-8"> ...</div>}>
<ItemDetailClient item={item} />
</Suspense>
</div>
);
}
/**
* 메타데이터 설정
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const item = await getItemByCode(id);
if (!item) {
return {
title: '품목을 찾을 수 없습니다',
};
}
return {
title: `${item.itemName} - 품목 상세`,
description: `${item.itemCode} 품목 정보`,
};
}

View File

@@ -0,0 +1,28 @@
/**
* 품목 등록 페이지
*/
'use client';
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);
// Mock: 성공 메시지
alert(`품목 "${data.itemName}" (${data.itemCode})이(가) 등록되었습니다.`);
// API 연동 예시:
// const newItem = await createItem(data);
// router.push(`/items/${newItem.itemCode}`);
};
return (
<div className="py-6">
<ItemForm mode="create" onSubmit={handleSubmit} />
</div>
);
}

View File

@@ -0,0 +1,162 @@
/**
* 품목 목록 페이지 (Server Component)
*
* Next.js 15 App Router
* 서버에서 데이터 fetching 후 Client Component로 전달
*/
import { Suspense } from 'react';
import ItemListClient from '@/components/items/ItemListClient';
import type { ItemMaster } from '@/types/item';
// 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,
productCategory: 'SCREEN',
lotAbbreviation: 'KD',
currentRevision: 0,
isFinal: false,
createdAt: '2025-01-10T00:00:00Z',
},
{
id: '2',
itemCode: 'KD-PT-001',
itemName: '가이드레일(벽면형)',
itemType: 'PT',
unit: 'EA',
specification: '2438mm',
isActive: true,
category1: '본체부품',
category2: '가이드시스템',
category3: '가이드레일',
salesPrice: 50000,
purchasePrice: 35000,
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',
},
{
id: '6',
itemCode: 'KD-CS-001',
itemName: '절삭유',
itemType: 'CS',
unit: 'L',
specification: '20L',
isActive: true,
purchasePrice: 30000,
currentRevision: 0,
isFinal: false,
createdAt: '2025-01-10T00:00:00Z',
},
{
id: '7',
itemCode: 'KD-FG-002',
itemName: '철재 제품 B',
itemType: 'FG',
unit: 'SET',
specification: '3000x2500',
isActive: false,
category1: '본체부품',
salesPrice: 200000,
productCategory: 'STEEL',
lotAbbreviation: 'KD',
currentRevision: 0,
isFinal: false,
createdAt: '2025-01-09T00:00:00Z',
},
];
/**
* 품목 목록 조회 함수
* TODO: API 연동 시 fetchItems()로 교체
*/
async function getItems(): Promise<ItemMaster[]> {
// API 연동 전 mock 데이터 반환
// const items = await fetchItems();
return mockItems;
}
/**
* 품목 목록 페이지
*/
export default async function ItemsPage() {
const items = await getItems();
return (
<div className="p-6">
<Suspense fallback={<div className="text-center py-8"> ...</div>}>
<ItemListClient items={items} />
</Suspense>
</div>
);
}
/**
* 메타데이터 설정
*/
export const metadata = {
title: '품목 관리',
description: '품목 목록 조회 및 관리',
};

View File

@@ -4,13 +4,14 @@ import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
/**
* Root Page - Redirects to Login
* Root Page - Redirects to Dashboard (Main Landing Page)
* Middleware will redirect to login if not authenticated
*/
export default function Home() {
const router = useRouter();
useEffect(() => {
router.replace('/login');
router.replace('/dashboard');
}, [router]);
return null;

View File

@@ -235,6 +235,19 @@
/*position: fixed;*/
}
/* 🔧 Fix DropdownMenu/Popover/Select positioning to prevent "flying in from far away" */
[data-radix-popper-content-wrapper] {
will-change: auto !important;
transition: none !important; /* 전역 transition 제거 - 날아오는 효과 방지 */
}
/* 🔧 Radix UI 컴포넌트의 slide 애니메이션만 제거, 위치 계산은 유지 */
[data-radix-dropdown-menu-content],
[data-radix-select-content],
[data-radix-popover-content] {
animation-name: none !important;
}
/* Clean glass utilities */
.clean-glass {
backdrop-filter: var(--clean-blur);
@@ -308,39 +321,47 @@
}
.sidebar-scroll::-webkit-scrollbar-thumb {
background: transparent;
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
transition: background 0.2s ease;
}
.dark .sidebar-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
.sidebar-scroll:hover::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
background: rgba(0, 0, 0, 0.2);
}
.dark .sidebar-scroll:hover::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.2);
}
.sidebar-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25) !important;
background: rgba(0, 0, 0, 0.3) !important;
}
.dark .sidebar-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25) !important;
background: rgba(255, 255, 255, 0.3) !important;
}
/* Firefox */
.sidebar-scroll {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
scrollbar-color: rgba(0, 0, 0, 0.1) transparent;
}
.dark .sidebar-scroll {
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.sidebar-scroll:hover {
scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
.dark .sidebar-scroll:hover {
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
}