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,419 @@
/**
* 품목기준관리 Zustand Store
*
* 하이브리드 로딩 전략:
* 1단계: Zustand (메모리 캐시)
* 2단계: sessionStorage (브라우저 세션)
* 3단계: API/Redis (백엔드 캐시, 10분 TTL)
* 4단계: Database (영구 저장소)
*/
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { shallow } from 'zustand/shallow';
import type { PageConfig, PageType, DynamicFormData } from '@/types/master-data';
import { fetchPageConfigByType, invalidatePageConfigCache } from '@/lib/api/master-data';
// ===== Store 타입 정의 =====
interface MasterDataStore {
// === State ===
// 페이지 구성 캐시 (페이지 타입별)
pageConfigs: Record<PageType, PageConfig | null>;
// 로딩 상태
loading: Record<PageType, boolean>;
// 에러 상태
errors: Record<PageType, string | null>;
// 선택된 페이지 타입
selectedPageType: PageType | null;
// 폼 데이터 (임시 저장)
formData: Record<string, DynamicFormData>;
// === Actions ===
// 페이지 구성 가져오기 (하이브리드 로딩)
fetchPageConfig: (pageType: PageType) => Promise<PageConfig | null>;
// 페이지 구성 캐시 무효화
invalidateConfig: (pageType: PageType) => void;
// 모든 캐시 무효화
invalidateAllConfigs: () => void;
// 선택된 페이지 타입 설정
setSelectedPageType: (pageType: PageType | null) => void;
// 폼 데이터 관리
setFormData: (id: string, data: DynamicFormData) => void;
getFormData: (id: string) => DynamicFormData | undefined;
clearFormData: (id: string) => void;
// 로딩/에러 상태
setLoading: (pageType: PageType, loading: boolean) => void;
setError: (pageType: PageType, error: string | null) => void;
// 초기화
reset: () => void;
}
// ===== 초기 상태 =====
const initialState = {
pageConfigs: {} as Record<PageType, PageConfig | null>,
loading: {} as Record<PageType, boolean>,
errors: {} as Record<PageType, string | null>,
selectedPageType: null,
formData: {},
};
// ===== sessionStorage 유틸리티 =====
const STORAGE_PREFIX = 'page_config_';
const STORAGE_TIMESTAMP_SUFFIX = '_timestamp';
const CACHE_TTL = 10 * 60 * 1000; // 10분
/**
* sessionStorage에서 페이지 구성 가져오기
*/
function getConfigFromSessionStorage(pageType: PageType): PageConfig | null {
if (typeof window === 'undefined') return null;
try {
const cachedData = window.sessionStorage.getItem(`${STORAGE_PREFIX}${pageType}`);
const timestamp = window.sessionStorage.getItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`);
if (!cachedData || !timestamp) return null;
// TTL 확인
const cacheAge = Date.now() - parseInt(timestamp, 10);
if (cacheAge > CACHE_TTL) {
// 만료된 캐시 삭제
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}`);
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`);
return null;
}
return JSON.parse(cachedData);
} catch (error) {
console.error(`[sessionStorage] Failed to get config for ${pageType}:`, error);
return null;
}
}
/**
* sessionStorage에 페이지 구성 저장
*/
function setConfigToSessionStorage(pageType: PageType, config: PageConfig): void {
if (typeof window === 'undefined') return;
try {
window.sessionStorage.setItem(`${STORAGE_PREFIX}${pageType}`, JSON.stringify(config));
window.sessionStorage.setItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`, Date.now().toString());
} catch (error) {
console.error(`[sessionStorage] Failed to set config for ${pageType}:`, error);
}
}
/**
* sessionStorage에서 페이지 구성 삭제
*/
function removeConfigFromSessionStorage(pageType: PageType): void {
if (typeof window === 'undefined') return;
try {
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}`);
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${pageType}${STORAGE_TIMESTAMP_SUFFIX}`);
} catch (error) {
console.error(`[sessionStorage] Failed to remove config for ${pageType}:`, error);
}
}
// ===== Store 생성 =====
export const useMasterDataStore = create<MasterDataStore>()(
devtools(
(set, get) => ({
// Initial state
...initialState,
// ===== 하이브리드 로딩 전략 =====
fetchPageConfig: async (pageType: PageType) => {
const { pageConfigs, loading, errors } = get();
// 🔒 이미 로딩 중이면 중복 요청 방지
if (loading[pageType]) {
console.log(`⏳ [Already Loading] ${pageType}`);
return pageConfigs[pageType] || null;
}
// 🔒 최근에 에러가 발생했으면 재시도 방지 (캐시 무효화 전까지)
if (errors[pageType] && pageConfigs[pageType] === null) {
console.log(`🚫 [Skip - Previous Error] ${pageType}: ${errors[pageType]}`);
return null;
}
// 1⃣ 메모리 캐시 확인 (Zustand)
if (pageConfigs[pageType]) {
console.log(`✅ [Cache Hit - Memory] ${pageType}`);
return pageConfigs[pageType];
}
// 2⃣ sessionStorage 확인
const cachedConfig = getConfigFromSessionStorage(pageType);
if (cachedConfig) {
console.log(`✅ [Cache Hit - Session] ${pageType}`);
// 메모리에 저장
set(
(state) => ({
pageConfigs: {
...state.pageConfigs,
[pageType]: cachedConfig,
},
}),
false,
'fetchPageConfig/cacheHit'
);
return cachedConfig;
}
// 3⃣ API 요청 (Redis/Database)
console.log(`🌐 [API Request] ${pageType}`);
try {
get().setLoading(pageType, true);
get().setError(pageType, null);
const config = await fetchPageConfigByType(pageType);
if (!config) {
console.warn(`⚠️ [Config Not Found] ${pageType}`);
get().setError(pageType, '페이지 구성을 찾을 수 없습니다.');
// 에러 상태지만 null을 캐시에 저장하여 재시도 방지
set(
(state) => ({
pageConfigs: {
...state.pageConfigs,
[pageType]: null,
},
}),
false,
'fetchPageConfig/notFound'
);
return null;
}
// 캐시 저장 (메모리 + sessionStorage)
set(
(state) => ({
pageConfigs: {
...state.pageConfigs,
[pageType]: config,
},
}),
false,
'fetchPageConfig/success'
);
setConfigToSessionStorage(pageType, config);
console.log(`✅ [Config Loaded] ${pageType}`, config);
return config;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '페이지 구성 조회 실패';
console.error(`❌ [API Error] ${pageType}:`, error);
get().setError(pageType, errorMessage);
// 에러 상태를 캐시에 저장하여 재시도 방지
set(
(state) => ({
pageConfigs: {
...state.pageConfigs,
[pageType]: null,
},
}),
false,
'fetchPageConfig/error'
);
return null;
} finally {
get().setLoading(pageType, false);
}
},
// ===== 캐시 무효화 =====
invalidateConfig: (pageType: PageType) => {
const { pageConfigs } = get();
const newConfigs = { ...pageConfigs };
delete newConfigs[pageType];
set(
{ pageConfigs: newConfigs },
false,
'invalidateConfig'
);
removeConfigFromSessionStorage(pageType);
console.log(`🗑️ [Cache Invalidated] ${pageType}`);
},
invalidateAllConfigs: () => {
const pageTypes: PageType[] = ['item-master', 'quotation', 'sales-order', 'formula', 'pricing'];
pageTypes.forEach((pageType) => {
removeConfigFromSessionStorage(pageType);
});
set(
{ pageConfigs: {} },
false,
'invalidateAllConfigs'
);
// 서버 캐시도 무효화
invalidatePageConfigCache().catch((error) => {
console.error('[Cache Invalidation Error]:', error);
});
console.log('🗑️ [All Caches Invalidated]');
},
// ===== 페이지 타입 선택 =====
setSelectedPageType: (pageType) =>
set(
{ selectedPageType: pageType },
false,
'setSelectedPageType'
),
// ===== 폼 데이터 관리 =====
setFormData: (id, data) =>
set(
(state) => ({
formData: {
...state.formData,
[id]: data,
},
}),
false,
'setFormData'
),
getFormData: (id) => {
return get().formData[id];
},
clearFormData: (id) =>
set(
(state) => {
const newFormData = { ...state.formData };
delete newFormData[id];
return { formData: newFormData };
},
false,
'clearFormData'
),
// ===== 로딩/에러 상태 =====
setLoading: (pageType, loading) =>
set(
(state) => ({
loading: {
...state.loading,
[pageType]: loading,
},
}),
false,
'setLoading'
),
setError: (pageType, error) =>
set(
(state) => ({
errors: {
...state.errors,
[pageType]: error,
},
}),
false,
'setError'
),
// ===== 초기화 =====
reset: () =>
set(
initialState,
false,
'reset'
),
}),
{
name: 'MasterDataStore',
}
)
);
// ===== Selector Hooks (성능 최적화) =====
/**
* 특정 페이지 타입의 구성만 구독
*/
export const usePageConfig = (pageType: PageType) =>
useMasterDataStore((state) => state.pageConfigs[pageType]);
/**
* 로딩 상태만 구독
*/
export const usePageConfigLoading = (pageType: PageType) =>
useMasterDataStore((state) => state.loading[pageType]);
/**
* 에러 상태만 구독
*/
export const usePageConfigError = (pageType: PageType) =>
useMasterDataStore((state) => state.errors[pageType]);
/**
* 선택된 페이지 타입만 구독
*/
export const useSelectedPageType = () =>
useMasterDataStore((state) => state.selectedPageType);
/**
* 액션만 가져오기 (리렌더링 방지)
*/
export const useMasterDataActions = () =>
useMasterDataStore(
(state) => ({
fetchPageConfig: state.fetchPageConfig,
invalidateConfig: state.invalidateConfig,
invalidateAllConfigs: state.invalidateAllConfigs,
setSelectedPageType: state.setSelectedPageType,
setFormData: state.setFormData,
getFormData: state.getFormData,
clearFormData: state.clearFormData,
reset: state.reset,
}),
shallow
);
// ===== 타입 추출 =====
export type { MasterDataStore };