feat: Enhance V2Repeater and configuration panel with source detail auto-fetching

- Added support for automatic fetching of detail rows from the master data in the V2Repeater component, improving data management.
- Introduced a new configuration option in the V2RepeaterConfigPanel to enable source detail auto-fetching, allowing users to specify detail table and foreign key settings.
- Enhanced the V2Repeater component to handle entity joins for loading data, optimizing data retrieval processes.
- Updated the V2RepeaterProps and V2RepeaterConfig interfaces to include new properties for grouped data and source detail configuration, ensuring type safety and clarity in component usage.
- Improved logging for data loading processes to provide better insights during development and debugging.
This commit is contained in:
kjs
2026-02-28 14:33:18 +09:00
parent b1831ada04
commit e16d76936b
11 changed files with 858 additions and 200 deletions

View File

@@ -224,23 +224,38 @@ export function UniversalFormModalComponent({
// 설정 병합
const config: UniversalFormModalConfig = useMemo(() => {
const componentConfig = component?.config || {};
// V2 레이아웃에서 overrides 전체가 config로 전달되는 경우
// 실제 설정이 propConfig.componentConfig에 이중 중첩되어 있을 수 있음
const nestedPropConfig = propConfig?.componentConfig;
const hasFlatPropConfig = propConfig?.modal !== undefined || propConfig?.sections !== undefined;
const effectivePropConfig = hasFlatPropConfig
? propConfig
: (nestedPropConfig?.modal ? nestedPropConfig : propConfig);
const nestedCompConfig = componentConfig?.componentConfig;
const hasFlatCompConfig = componentConfig?.modal !== undefined || componentConfig?.sections !== undefined;
const effectiveCompConfig = hasFlatCompConfig
? componentConfig
: (nestedCompConfig?.modal ? nestedCompConfig : componentConfig);
return {
...defaultConfig,
...propConfig,
...componentConfig,
...effectivePropConfig,
...effectiveCompConfig,
modal: {
...defaultConfig.modal,
...propConfig?.modal,
...componentConfig.modal,
...effectivePropConfig?.modal,
...effectiveCompConfig?.modal,
},
saveConfig: {
...defaultConfig.saveConfig,
...propConfig?.saveConfig,
...componentConfig.saveConfig,
...effectivePropConfig?.saveConfig,
...effectiveCompConfig?.saveConfig,
afterSave: {
...defaultConfig.saveConfig.afterSave,
...propConfig?.saveConfig?.afterSave,
...componentConfig.saveConfig?.afterSave,
...effectivePropConfig?.saveConfig?.afterSave,
...effectiveCompConfig?.saveConfig?.afterSave,
},
},
};
@@ -295,6 +310,7 @@ export function UniversalFormModalComponent({
const hasInitialized = useRef(false);
// 마지막으로 초기화된 데이터의 ID를 추적 (수정 모달에서 다른 항목 선택 시 재초기화 필요)
const lastInitializedId = useRef<string | undefined>(undefined);
const tableSectionLoadedRef = useRef(false);
// 초기화 - 최초 마운트 시 또는 initialData가 변경되었을 때 실행
useEffect(() => {
@@ -316,7 +332,7 @@ export function UniversalFormModalComponent({
if (hasInitialized.current && lastInitializedId.current === currentIdString) {
// 생성 모드에서 데이터가 새로 전달된 경우는 재초기화 필요
if (!createModeDataHash || capturedInitialData.current) {
// console.log("[UniversalFormModal] 초기화 스킵 - 이미 초기화됨");
// console.log("[UniversalFormModal] 초기화 스킵 - 이미 초기화됨", { currentIdString });
// 🆕 채번 플래그가 true인데 formData에 값이 없으면 재생성 필요
// (컴포넌트 remount로 인해 state가 초기화된 경우)
return;
@@ -350,21 +366,13 @@ export function UniversalFormModalComponent({
// console.log("[UniversalFormModal] 초기 데이터 캡처:", capturedInitialData.current);
}
// console.log("[UniversalFormModal] initializeForm 호출 예정");
// console.log("[UniversalFormModal] initializeForm 호출 예정", { currentIdString });
hasInitialized.current = true;
tableSectionLoadedRef.current = false;
initializeForm();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialData]); // initialData 전체 변경 시 재초기화
// config 변경 시에만 재초기화 (initialData 변경은 무시) - 채번규칙 제외
useEffect(() => {
if (!hasInitialized.current) return; // 최초 초기화 전이면 스킵
// console.log("[useEffect config 변경] 재초기화 스킵 (채번 중복 방지)");
// initializeForm(); // 주석 처리 - config 변경 시 재초기화 안 함 (채번 중복 방지)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
// 컴포넌트 unmount 시 채번 플래그 초기화
useEffect(() => {
return () => {
@@ -728,9 +736,13 @@ export function UniversalFormModalComponent({
// 🆕 테이블 섹션(type: "table") 디테일 데이터 로드 (마스터-디테일 구조)
// 수정 모드일 때 디테일 테이블에서 데이터 가져오기
if (effectiveInitialData) {
console.log("[initializeForm] 테이블 섹션 디테일 로드 시작", {
sectionsCount: config.sections.length,
effectiveInitialDataKeys: Object.keys(effectiveInitialData),
// console.log("[initializeForm] 테이블 섹션 디테일 로드 시작", { sectionsCount: config.sections.length });
console.warn("[initializeForm] 테이블 섹션 순회 시작:", {
sectionCount: config.sections.length,
tableSections: config.sections.filter(s => s.type === "table").map(s => s.id),
hasInitialData: !!effectiveInitialData,
initialDataKeys: effectiveInitialData ? Object.keys(effectiveInitialData).slice(0, 10) : [],
});
for (const section of config.sections) {
@@ -739,16 +751,14 @@ export function UniversalFormModalComponent({
}
const tableConfig = section.tableConfig;
// editConfig는 타입에 정의되지 않았지만 런타임에 존재할 수 있음
const editConfig = (tableConfig as any).editConfig;
const saveConfig = tableConfig.saveConfig;
console.log(`[initializeForm] 테이블 섹션 ${section.id} 검사:`, {
hasEditConfig: !!editConfig,
loadOnEdit: editConfig?.loadOnEdit,
hasSaveConfig: !!saveConfig,
console.warn(`[initializeForm] 테이블 섹션 ${section.id}:`, {
editConfig,
targetTable: saveConfig?.targetTable,
linkColumn: editConfig?.linkColumn,
masterField: editConfig?.linkColumn?.masterField,
masterValue: effectiveInitialData?.[editConfig?.linkColumn?.masterField],
});
// 수정 모드 로드 설정 확인 (기본값: true)
@@ -1073,6 +1083,25 @@ export function UniversalFormModalComponent({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]); // initialData는 의존성에서 제거 (capturedInitialData.current 사용)
// config 변경 시 테이블 섹션 데이터 로드 보완
// initializeForm은 initialData useEffect에서 호출되지만, config(화면 설정)이
// 비동기 로드로 늦게 도착하면 테이블 섹션 로드를 놓칠 수 있음
useEffect(() => {
if (!hasInitialized.current) return;
const hasTableSection = config.sections.some(s => s.type === "table" && s.tableConfig?.saveConfig?.targetTable);
if (!hasTableSection) return;
const editData = capturedInitialData.current || initialData;
if (!editData || Object.keys(editData).length === 0) return;
if (tableSectionLoadedRef.current) return;
tableSectionLoadedRef.current = true;
initializeForm();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.sections, initializeForm]);
// 반복 섹션 아이템 생성
const createRepeatItem = (section: FormSectionConfig, index: number): RepeatSectionItem => {
const item: RepeatSectionItem = {