- 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>
176 lines
5.8 KiB
TypeScript
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,
|
|
};
|
|
} |