feat: modal-repeater-table 배열 데이터 저장 기능 구현
- 백엔드: 배열 객체 형식 Repeater 데이터 처리 로직 추가 - 백엔드: Repeater 저장 시 company_code 자동 주입 - 백엔드: 부모 테이블 데이터 자동 병합 (targetTable = tableName) - 프론트엔드: beforeFormSave 이벤트로 formData 주입 - 프론트엔드: _targetTable 메타데이터 전달 - 프론트엔드: ComponentRendererProps 상속 및 Renderer 단순화 멀티테넌시 및 부모-자식 관계 자동 처리로 복잡한 배열 데이터 저장 안정성 확보
This commit is contained in:
@@ -9,9 +9,26 @@ import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./
|
||||
import { useCalculation } from "./useCalculation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
|
||||
interface ModalRepeaterTableComponentProps extends Partial<ModalRepeaterTableProps> {
|
||||
// ✅ ComponentRendererProps 상속으로 필수 props 자동 확보
|
||||
export interface ModalRepeaterTableComponentProps extends ComponentRendererProps {
|
||||
config?: ModalRepeaterTableProps;
|
||||
// ModalRepeaterTableProps의 개별 prop들도 지원 (호환성)
|
||||
sourceTable?: string;
|
||||
sourceColumns?: string[];
|
||||
sourceSearchFields?: string[];
|
||||
targetTable?: string;
|
||||
modalTitle?: string;
|
||||
modalButtonText?: string;
|
||||
multiSelect?: boolean;
|
||||
columns?: RepeaterColumnConfig[];
|
||||
calculationRules?: any[];
|
||||
value?: any[];
|
||||
onChange?: (newData: any[]) => void;
|
||||
uniqueField?: string;
|
||||
filterCondition?: Record<string, any>;
|
||||
companyCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,10 +139,25 @@ async function fetchReferenceValue(
|
||||
}
|
||||
|
||||
export function ModalRepeaterTableComponent({
|
||||
// ComponentRendererProps (자동 전달)
|
||||
component,
|
||||
isDesignMode = false,
|
||||
isSelected = false,
|
||||
isInteractive = false,
|
||||
onClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
className,
|
||||
style,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
|
||||
// ModalRepeaterTable 전용 props
|
||||
config,
|
||||
sourceTable: propSourceTable,
|
||||
sourceColumns: propSourceColumns,
|
||||
sourceSearchFields: propSourceSearchFields,
|
||||
targetTable: propTargetTable,
|
||||
modalTitle: propModalTitle,
|
||||
modalButtonText: propModalButtonText,
|
||||
multiSelect: propMultiSelect,
|
||||
@@ -136,36 +168,55 @@ export function ModalRepeaterTableComponent({
|
||||
uniqueField: propUniqueField,
|
||||
filterCondition: propFilterCondition,
|
||||
companyCode: propCompanyCode,
|
||||
className,
|
||||
|
||||
...props
|
||||
}: ModalRepeaterTableComponentProps) {
|
||||
// ✅ config 또는 component.config 또는 개별 prop 우선순위로 병합
|
||||
const componentConfig = {
|
||||
...config,
|
||||
...component?.config,
|
||||
};
|
||||
|
||||
// config prop 우선, 없으면 개별 prop 사용
|
||||
const sourceTable = config?.sourceTable || propSourceTable || "";
|
||||
const sourceTable = componentConfig?.sourceTable || propSourceTable || "";
|
||||
const targetTable = componentConfig?.targetTable || propTargetTable;
|
||||
|
||||
// sourceColumns에서 빈 문자열 필터링
|
||||
const rawSourceColumns = config?.sourceColumns || propSourceColumns || [];
|
||||
const sourceColumns = rawSourceColumns.filter((col) => col && col.trim() !== "");
|
||||
const rawSourceColumns = componentConfig?.sourceColumns || propSourceColumns || [];
|
||||
const sourceColumns = rawSourceColumns.filter((col: string) => col && col.trim() !== "");
|
||||
|
||||
const sourceSearchFields = config?.sourceSearchFields || propSourceSearchFields || [];
|
||||
const modalTitle = config?.modalTitle || propModalTitle || "항목 검색";
|
||||
const modalButtonText = config?.modalButtonText || propModalButtonText || "품목 검색";
|
||||
const multiSelect = config?.multiSelect ?? propMultiSelect ?? true;
|
||||
const calculationRules = config?.calculationRules || propCalculationRules || [];
|
||||
const value = config?.value || propValue || [];
|
||||
const onChange = config?.onChange || propOnChange || (() => {});
|
||||
const sourceSearchFields = componentConfig?.sourceSearchFields || propSourceSearchFields || [];
|
||||
const modalTitle = componentConfig?.modalTitle || propModalTitle || "항목 검색";
|
||||
const modalButtonText = componentConfig?.modalButtonText || propModalButtonText || "품목 검색";
|
||||
const multiSelect = componentConfig?.multiSelect ?? propMultiSelect ?? true;
|
||||
const calculationRules = componentConfig?.calculationRules || propCalculationRules || [];
|
||||
|
||||
// ✅ value는 formData[columnName] 우선, 없으면 prop 사용
|
||||
const columnName = component?.columnName;
|
||||
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||
|
||||
// ✅ onChange 래퍼 (기존 onChange 콜백만 호출, formData는 beforeFormSave에서 처리)
|
||||
const handleChange = (newData: any[]) => {
|
||||
// 기존 onChange 콜백 호출 (호환성)
|
||||
const externalOnChange = componentConfig?.onChange || propOnChange;
|
||||
if (externalOnChange) {
|
||||
externalOnChange(newData);
|
||||
}
|
||||
};
|
||||
|
||||
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
||||
const rawUniqueField = config?.uniqueField || propUniqueField;
|
||||
const rawUniqueField = componentConfig?.uniqueField || propUniqueField;
|
||||
const uniqueField = rawUniqueField === "order_no" && sourceTable === "item_info"
|
||||
? "item_number"
|
||||
: rawUniqueField;
|
||||
|
||||
const filterCondition = config?.filterCondition || propFilterCondition || {};
|
||||
const companyCode = config?.companyCode || propCompanyCode;
|
||||
const filterCondition = componentConfig?.filterCondition || propFilterCondition || {};
|
||||
const companyCode = componentConfig?.companyCode || propCompanyCode;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
||||
const columns = React.useMemo((): RepeaterColumnConfig[] => {
|
||||
const configuredColumns = config?.columns || propColumns || [];
|
||||
const configuredColumns = componentConfig?.columns || propColumns || [];
|
||||
|
||||
if (configuredColumns.length > 0) {
|
||||
console.log("✅ 설정된 columns 사용:", configuredColumns);
|
||||
@@ -188,7 +239,7 @@ export function ModalRepeaterTableComponent({
|
||||
|
||||
console.warn("⚠️ columns와 sourceColumns 모두 비어있음!");
|
||||
return [];
|
||||
}, [config?.columns, propColumns, sourceColumns]);
|
||||
}, [componentConfig?.columns, propColumns, sourceColumns]);
|
||||
|
||||
// 초기 props 로깅
|
||||
useEffect(() => {
|
||||
@@ -221,6 +272,59 @@ export function ModalRepeaterTableComponent({
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
// 🆕 저장 요청 시에만 데이터 전달 (beforeFormSave 이벤트 리스너)
|
||||
useEffect(() => {
|
||||
const handleSaveRequest = async (event: Event) => {
|
||||
const componentKey = columnName || component?.id || "modal_repeater_data";
|
||||
|
||||
console.log("🔔 [ModalRepeaterTable] beforeFormSave 이벤트 수신!", {
|
||||
componentKey,
|
||||
itemsCount: value.length,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
columnName,
|
||||
componentId: component?.id,
|
||||
targetTable,
|
||||
});
|
||||
|
||||
if (value.length === 0) {
|
||||
console.warn("⚠️ [ModalRepeaterTable] 저장할 데이터 없음");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 targetTable 메타데이터를 배열 항목에 추가
|
||||
const dataWithTargetTable = targetTable
|
||||
? value.map(item => ({
|
||||
...item,
|
||||
_targetTable: targetTable, // 백엔드가 인식할 메타데이터
|
||||
}))
|
||||
: value;
|
||||
|
||||
// ✅ CustomEvent의 detail에 데이터 추가
|
||||
if (event instanceof CustomEvent && event.detail) {
|
||||
event.detail.formData[componentKey] = dataWithTargetTable;
|
||||
console.log("✅ [ModalRepeaterTable] context.formData에 데이터 추가 완료:", {
|
||||
key: componentKey,
|
||||
itemCount: dataWithTargetTable.length,
|
||||
targetTable: targetTable || "미설정 (화면 설계에서 설정 필요)",
|
||||
sampleItem: dataWithTargetTable[0],
|
||||
});
|
||||
}
|
||||
|
||||
// 기존 onFormDataChange도 호출 (호환성)
|
||||
if (onFormDataChange) {
|
||||
onFormDataChange(componentKey, dataWithTargetTable);
|
||||
console.log("✅ [ModalRepeaterTable] onFormDataChange 호출 완료");
|
||||
}
|
||||
};
|
||||
|
||||
// 저장 버튼 클릭 시 데이터 수집
|
||||
window.addEventListener("beforeFormSave", handleSaveRequest as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener);
|
||||
};
|
||||
}, [value, columnName, component?.id, onFormDataChange, targetTable]);
|
||||
|
||||
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
||||
|
||||
// 초기 데이터에 계산 필드 적용
|
||||
@@ -338,7 +442,8 @@ export function ModalRepeaterTableComponent({
|
||||
const newData = [...value, ...calculatedItems];
|
||||
console.log("✅ 최종 데이터:", newData.length, "개 항목");
|
||||
|
||||
onChange(newData);
|
||||
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
||||
handleChange(newData);
|
||||
};
|
||||
|
||||
const handleRowChange = (index: number, newRow: any) => {
|
||||
@@ -348,12 +453,16 @@ export function ModalRepeaterTableComponent({
|
||||
// 데이터 업데이트
|
||||
const newData = [...value];
|
||||
newData[index] = calculatedRow;
|
||||
onChange(newData);
|
||||
|
||||
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
||||
handleChange(newData);
|
||||
};
|
||||
|
||||
const handleRowDelete = (index: number) => {
|
||||
const newData = value.filter((_, i) => i !== index);
|
||||
onChange(newData);
|
||||
|
||||
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
||||
handleChange(newData);
|
||||
};
|
||||
|
||||
// 컬럼명 -> 라벨명 매핑 생성
|
||||
@@ -382,7 +491,7 @@ export function ModalRepeaterTableComponent({
|
||||
<RepeaterTable
|
||||
columns={columns}
|
||||
data={value}
|
||||
onDataChange={onChange}
|
||||
onDataChange={handleChange}
|
||||
onRowChange={handleRowChange}
|
||||
onRowDelete={handleRowDelete}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user