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:
208
src/app/[locale]/(protected)/items/[id]/edit/page.tsx
Normal file
208
src/app/[locale]/(protected)/items/[id]/edit/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
195
src/app/[locale]/(protected)/items/[id]/page.tsx
Normal file
195
src/app/[locale]/(protected)/items/[id]/page.tsx
Normal 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} 품목 정보`,
|
||||
};
|
||||
}
|
||||
28
src/app/[locale]/(protected)/items/create/page.tsx
Normal file
28
src/app/[locale]/(protected)/items/create/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
162
src/app/[locale]/(protected)/items/page.tsx
Normal file
162
src/app/[locale]/(protected)/items/page.tsx
Normal 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: '품목 목록 조회 및 관리',
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} 품목 정보`,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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: '품목 목록 조회 및 관리',
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user