Files
sam-react-prod/src/components/items/ItemMasterDataManagement/hooks/useInitialDataLoading.ts
byeongcheolryu 172d06b697 feat: 품목기준관리 Zustand store 연동
- useInitialDataLoading에서 Zustand initFromApi() 호출 추가
- Context와 Zustand 병행 운영 구조 구축
- Zustand 초기화 실패 시 Context fallback 처리
- 점진적 마이그레이션 기반 마련

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 16:31:44 +09:00

176 lines
5.8 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import { useItemMasterStore } from '@/stores/item-master/useItemMasterStore';
import { itemMasterApi } from '@/lib/api/item-master';
import { getErrorMessage, ApiError } from '@/lib/api/error-handler';
import {
transformPagesResponse,
transformSectionsResponse,
transformSectionTemplatesResponse,
transformFieldsResponse,
transformCustomTabsResponse,
transformUnitOptionsResponse,
transformSectionTemplateFromSection,
} from '@/lib/api/transformers';
import { toast } from 'sonner';
import type { CustomTab } from './useTabManagement';
import type { MasterOption } from '../types';
// 타입 alias
type UnitOption = MasterOption;
export interface UseInitialDataLoadingReturn {
isInitialLoading: boolean;
error: string | null;
reload: () => Promise<void>;
}
interface UseInitialDataLoadingProps {
setCustomTabs: React.Dispatch<React.SetStateAction<CustomTab[]>>;
setUnitOptions: React.Dispatch<React.SetStateAction<UnitOption[]>>;
}
export function useInitialDataLoading({
setCustomTabs,
setUnitOptions,
}: UseInitialDataLoadingProps): UseInitialDataLoadingReturn {
const {
loadItemPages,
loadSectionTemplates,
loadItemMasterFields,
loadIndependentSections,
loadIndependentFields,
} = useItemMaster();
// ✅ 2025-12-24: Zustand store 연동
const initFromApi = useItemMasterStore((state) => state.initFromApi);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 초기 로딩이 이미 실행되었는지 추적하는 ref
const hasInitialLoadRun = useRef(false);
const loadInitialData = useCallback(async () => {
try {
setIsInitialLoading(true);
setError(null);
// ✅ Zustand store 초기화 (정규화된 상태로 저장)
// Context와 병행 운영 - 점진적 마이그레이션
try {
await initFromApi();
console.log('✅ [Zustand] Store initialized');
} catch (zustandError) {
// Zustand 초기화 실패해도 Context로 fallback
console.warn('⚠️ [Zustand] Init failed, falling back to Context:', zustandError);
}
const data = await itemMasterApi.init();
// 1. 페이지 데이터 로드 (섹션이 이미 포함되어 있음)
const transformedPages = transformPagesResponse(data.pages);
loadItemPages(transformedPages);
// 2. 독립 섹션 로드 (모든 재사용 가능 섹션)
if (data.sections && data.sections.length > 0) {
const transformedSections = transformSectionsResponse(data.sections);
loadIndependentSections(transformedSections);
console.log('✅ 독립 섹션 로드:', transformedSections.length);
}
// 3. 섹션 템플릿 로드
if (data.sectionTemplates && data.sectionTemplates.length > 0) {
const transformedTemplates = transformSectionTemplatesResponse(data.sectionTemplates);
loadSectionTemplates(transformedTemplates);
} else if (data.sections && data.sections.length > 0) {
const templates = data.sections
.filter((s: { is_template?: boolean }) => s.is_template)
.map(transformSectionTemplateFromSection);
if (templates.length > 0) {
loadSectionTemplates(templates);
}
}
// 4. 필드 로드
if (data.fields && data.fields.length > 0) {
const transformedFields = transformFieldsResponse(data.fields);
const independentOnlyFields = transformedFields.filter(
f => f.section_id === null || f.section_id === undefined
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
loadItemMasterFields(transformedFields as any);
loadIndependentFields(independentOnlyFields);
console.log('✅ 필드 로드:', {
total: transformedFields.length,
independent: independentOnlyFields.length,
});
}
// 5. 커스텀 탭 로드
if (data.customTabs && data.customTabs.length > 0) {
const transformedTabs = transformCustomTabsResponse(data.customTabs);
setCustomTabs(transformedTabs);
}
// 6. 단위 옵션 로드
if (data.unitOptions && data.unitOptions.length > 0) {
const transformedUnits = transformUnitOptionsResponse(data.unitOptions);
setUnitOptions(transformedUnits);
}
console.log('✅ Initial data loaded:', {
pages: data.pages?.length || 0,
sections: data.sections?.length || 0,
fields: data.fields?.length || 0,
customTabs: data.customTabs?.length || 0,
unitOptions: data.unitOptions?.length || 0,
});
} catch (err) {
if (err instanceof ApiError && err.errors) {
const errorMessages = Object.entries(err.errors)
.map(([field, messages]) => `${field}: ${messages.join(', ')}`)
.join('\n');
toast.error(errorMessages);
setError('입력값을 확인해주세요.');
} else {
const errorMessage = getErrorMessage(err);
setError(errorMessage);
toast.error(errorMessage);
}
console.error('❌ Failed to load initial data:', err);
} finally {
setIsInitialLoading(false);
}
}, [
loadItemPages,
loadSectionTemplates,
loadItemMasterFields,
loadIndependentSections,
loadIndependentFields,
setCustomTabs,
setUnitOptions,
]);
// 초기 로딩은 한 번만 실행 (의존성 배열의 함수들이 불안정해도 무한 루프 방지)
useEffect(() => {
if (hasInitialLoadRun.current) {
return;
}
hasInitialLoadRun.current = true;
loadInitialData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
isInitialLoading,
error,
reload: loadInitialData,
};
}