diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index c4501f6b..d07ee67d 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -219,7 +219,10 @@ export function UniversalFormModalComponent({ (columnName: string, value: any) => { setFormData((prev) => { const newData = { ...prev, [columnName]: value }; - onChange?.(newData); + // onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용) + if (onChange) { + setTimeout(() => onChange(newData), 0); + } return newData; }); }, @@ -339,6 +342,26 @@ export function UniversalFormModalComponent({ [selectOptionsCache], ); + // 필수 필드 검증 + const validateRequiredFields = useCallback((): { valid: boolean; missingFields: string[] } => { + const missingFields: string[] = []; + + for (const section of config.sections) { + if (section.repeatable) continue; // 반복 섹션은 별도 검증 + + for (const field of section.fields) { + if (field.required && !field.hidden && !field.numberingRule?.hidden) { + const value = formData[field.columnName]; + if (value === undefined || value === null || value === "") { + missingFields.push(field.label || field.columnName); + } + } + } + } + + return { valid: missingFields.length === 0, missingFields }; + }, [config.sections, formData]); + // 저장 처리 const handleSave = useCallback(async () => { if (!config.saveConfig.tableName) { @@ -346,6 +369,13 @@ export function UniversalFormModalComponent({ return; } + // 필수 필드 검증 + const { valid, missingFields } = validateRequiredFields(); + if (!valid) { + toast.error(`필수 항목을 입력해주세요: ${missingFields.join(", ")}`); + return; + } + setSaving(true); try { @@ -375,7 +405,7 @@ export function UniversalFormModalComponent({ } finally { setSaving(false); } - }, [config, formData, repeatSections, onSave]); + }, [config, formData, repeatSections, onSave, validateRequiredFields]); // 단일 행 저장 const saveSingleRow = async () => { @@ -492,11 +522,23 @@ export function UniversalFormModalComponent({ } // 모든 행 저장 - for (const row of rowsToSave) { + console.log("[UniversalFormModal] 저장할 행들:", rowsToSave); + console.log("[UniversalFormModal] 저장 테이블:", config.saveConfig.tableName); + + for (let i = 0; i < rowsToSave.length; i++) { + const row = rowsToSave[i]; + console.log(`[UniversalFormModal] ${i + 1}번째 행 저장 시도:`, row); + + // 빈 객체 체크 + if (Object.keys(row).length === 0) { + console.warn(`[UniversalFormModal] ${i + 1}번째 행이 비어있습니다. 건너뜁니다.`); + continue; + } + const response = await apiClient.post(`/table-management/tables/${config.saveConfig.tableName}/add`, row); if (!response.data?.success) { - throw new Error(response.data?.message || "저장 실패"); + throw new Error(response.data?.message || `${i + 1}번째 행 저장 실패`); } } @@ -509,16 +551,15 @@ export function UniversalFormModalComponent({ toast.info("폼이 초기화되었습니다."); }, [initializeForm]); - // 필드 렌더링 - const renderField = (field: FormFieldConfig, value: any, onChangeHandler: (value: any) => void, fieldKey: string) => { - const isDisabled = field.disabled || (field.numberingRule?.enabled && !field.numberingRule?.editable); - const isHidden = field.numberingRule?.hidden; - - if (isHidden) { - return null; - } - - const fieldElement = (() => { + // 필드 요소 렌더링 (입력 컴포넌트만) + const renderFieldElement = ( + field: FormFieldConfig, + value: any, + onChangeHandler: (value: any) => void, + fieldKey: string, + isDisabled: boolean, + ) => { + return (() => { switch (field.fieldType) { case "textarea": return ( @@ -651,18 +692,46 @@ export function UniversalFormModalComponent({ ); } })(); + }; + + // 섹션의 열 수에 따른 기본 gridSpan 계산 + const getDefaultGridSpan = (sectionColumns: number = 2): number => { + // 12칸 그리드 기준: 1열=12, 2열=6, 3열=4, 4열=3 + return Math.floor(12 / sectionColumns); + }; + + // 필드 렌더링 (섹션 열 수 적용) + const renderFieldWithColumns = ( + field: FormFieldConfig, + value: any, + onChangeHandler: (value: any) => void, + fieldKey: string, + sectionColumns: number = 2, + ) => { + // 섹션 열 수에 따른 기본 gridSpan 계산 (섹션 열 수가 우선) + const defaultSpan = getDefaultGridSpan(sectionColumns); + // 섹션이 1열이면 무조건 12(전체 너비), 그 외에는 필드 설정 또는 기본값 사용 + const actualGridSpan = sectionColumns === 1 ? 12 : field.gridSpan || defaultSpan; + + const isDisabled = !!(field.disabled || (field.numberingRule?.enabled && !field.numberingRule?.editable)); + const isHidden = field.hidden || field.numberingRule?.hidden; + + if (isHidden) { + return null; + } + + const fieldElement = renderFieldElement(field, value, onChangeHandler, fieldKey, isDisabled); - // 체크박스는 라벨이 옆에 있으므로 별도 처리 if (field.fieldType === "checkbox") { return ( -