import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { getOptionsArray } from '../../../utils'; import { SelectInput, SearchBarAlert } from '../../../styles/Components'; import { FormInput, FormInputSuffix, FormInputSuffixWrapper, FormLabel, FormRowGroup, FormStatusBar, FormStatusLabel, FormStatusWarning, } from '../../../styles/ModuleComponents'; import { CheckBox, SingleDatePicker, SingleTimePicker } from '../../common'; import Button from '../../common/button/Button'; import styled from 'styled-components'; import ImageUploadBtn from '../../ServiceManage/ImageUploadBtn'; const CaliForm = ({ config, // 폼 설정 JSON mode, // 'create', 'update', 'view' 중 하나 initialData, // 초기 데이터 externalData, // 외부 데이터(옵션 등) onSubmit, // 제출 핸들러 onCancel, // 취소 핸들러 className, // 추가 CSS 클래스 onFieldValidation, // 필드 유효성 검사 콜백 formRef // 폼 ref }) => { const { t } = useTranslation(); const [formData, setFormData] = useState({ ...(config?.initData || {}), ...(initialData || {}) }); const [errors, setErrors] = useState({}); const [isFormValid, setIsFormValid] = useState(false); // 필드 변경 핸들러 const handleFieldChange = (fieldId, value) => { setFormData(prev => ({ ...prev, [fieldId]: value })); }; // 날짜 변경 핸들러 const handleDateChange = (fieldId, date) => { if (!date) return; setFormData(prev => ({ ...prev, [fieldId]: date })); }; // 시간 변경 핸들러 const handleTimeChange = (fieldId, time) => { if (!time) return; const newDateTime = formData[fieldId] ? new Date(formData[fieldId]) : new Date(); newDateTime.setHours(time.getHours(), time.getMinutes(), 0, 0); setFormData(prev => ({ ...prev, [fieldId]: newDateTime })); }; // 폼 유효성 검사 useEffect(() => { const validateForm = () => { const newErrors = {}; let isValid = true; if (!config) return false; // 필수 필드 검사 const requiredFields = config.fields .filter(f => f.visibleOn.includes(mode) && f.validations?.includes("required") ) .map(f => f.id); requiredFields.forEach(fieldId => { if (!formData[fieldId] && formData[fieldId] !== 0) { newErrors[fieldId] = t('REQUIRED_FIELD'); isValid = false; } }); // 조건부 유효성 검사 if (config.validations && config.validations[mode]) { for (const validation of config.validations[mode]) { const conditionResult = evaluateCondition(validation.condition, { ...formData, current_time: new Date().getTime() }); if (conditionResult) { // 전체 폼 검증 오류 newErrors._form = t(validation.message); isValid = false; } } } setErrors(newErrors); setIsFormValid(isValid); if (onFieldValidation) { onFieldValidation(isValid, newErrors); } return isValid; }; validateForm(); }, [config, formData, mode, t, onFieldValidation]); // 간단한 조건식 평가 함수 const evaluateCondition = (conditionStr, context) => { try { const fn = new Function(...Object.keys(context), `return ${conditionStr}`); return fn(...Object.values(context)); } catch (e) { console.error('Error evaluating condition:', e); return false; } }; // 필드 렌더링 const renderField = (field) => { const isEditable = field.editableOn.includes(mode); const value = formData[field.id] !== undefined ? formData[field.id] : ''; const hasError = errors[field.id]; switch (field.type) { case 'text': return (
handleFieldChange(field.id, e.target.value)} disabled={!isEditable} width={field.width} className={hasError ? 'error' : ''} /> {hasError &&
{hasError}
}
); case 'number': return (
handleFieldChange(field.id, Number(e.target.value))} disabled={!isEditable} width={field.width} min={field.min} max={field.max} step={field.step || 1} className={hasError ? 'error' : ''} /> {hasError &&
{hasError}
}
); case 'select': let options = []; if (field.optionsKey) { // 옵션 설정에서 가져오기 options = getOptionsArray(field.optionsKey); } else if (field.dataSource && externalData) { // 외부 데이터 소스 사용 const dataSource = externalData[field.dataSource] || []; options = dataSource.map(item => ({ value: item[field.valueField], label: field.displayFormat ? field.displayFormat.replace('{value}', item[field.valueField]) .replace('{display}', item[field.displayField]) : `${item[field.displayField]}(${item[field.valueField]})` })); } else if (field.options) { options = field.options; } return (
handleFieldChange(field.id, e.target.value)} disabled={!isEditable} width={field.width} className={hasError ? 'error' : ''} > {options.map((option, index) => ( ))} {hasError &&
{hasError}
}
); case 'datePicker': return (
handleDateChange(field.id, date)} selectedDate={value} minDate={field.minDate} maxDate={field.maxDate} className={hasError ? 'error' : ''} /> {hasError &&
{hasError}
}
); case 'timePicker': return (
handleTimeChange(field.id, time)} className={hasError ? 'error' : ''} /> {hasError &&
{hasError}
}
); case 'status': let statusText = ""; if (field.optionsKey && formData[field.statusField]) { const statusOptions = getOptionsArray(field.optionsKey); const statusItem = statusOptions.find(item => item.value === formData[field.statusField]); statusText = statusItem ? statusItem.name : "등록"; } return (
{field.label}: {statusText} {mode === 'update' && field.warningMessage && ( {t(field.warningMessage)} )}
); case 'dateTimeRange': return (
handleDateChange(field.startDateField, date)} selectedDate={formData[field.startDateField]} /> handleTimeChange(field.startDateField, time)} /> handleDateChange(field.endDateField, date)} selectedDate={formData[field.endDateField]} /> handleTimeChange(field.endDateField, time)} />
{hasError &&
{hasError}
}
); case 'imageUpload': const imageLanguage = field.language; const imageList = formData.image_list || []; const imageData = imageList.find(img => img.language === imageLanguage) || { content: '' }; return (
{imageLanguage} { const updatedImageList = [...imageList]; const index = updatedImageList.findIndex(img => img.language === imageLanguage); if (index !== -1) { updatedImageList[index] = { ...updatedImageList[index], content: fileName }; } else { updatedImageList.push({ language: imageLanguage, content: fileName }); } handleFieldChange('image_list', updatedImageList); }} onFileDelete={() => { const updatedImageList = [...imageList]; const index = updatedImageList.findIndex(img => img.language === imageLanguage); if (index !== -1) { updatedImageList[index] = { ...updatedImageList[index], content: '' }; handleFieldChange('image_list', updatedImageList); } }} fileName={imageData.content} disabled={!isEditable} /> {hasError &&
{hasError}
}
); case 'checkbox': return (
handleFieldChange(field.id, e.target.checked)} disabled={!isEditable} /> {hasError &&
{hasError}
}
); case 'textWithSuffix': const linkLanguage = field.suffix; const linkList = formData.link_list || []; const linkData = linkList.find(link => link.language === linkLanguage) || { content: '' }; return (
{field.label && {field.label}} { const updatedLinkList = [...linkList]; const index = updatedLinkList.findIndex(link => link.language === linkLanguage); if (index !== -1) { updatedLinkList[index] = { ...updatedLinkList[index], content: e.target.value }; } else { updatedLinkList.push({ language: linkLanguage, content: e.target.value }); } handleFieldChange('link_list', updatedLinkList); }} disabled={!isEditable} width={field.width} suffix="true" /> {linkLanguage} {hasError &&
{hasError}
}
); default: return null; } }; // 조건부 렌더링을 위한 필드 필터링 const getVisibleFields = () => { if (!config) return []; return config.fields.filter(field => { if (!field.visibleOn.includes(mode)) return false; // 조건부 표시 필드 처리 if (field.conditional) { const { field: condField, operator, value } = field.conditional; if (operator === "==" && formData[condField] !== value) return false; if (operator === "!=" && formData[condField] === value) return false; } return true; }); }; // 그리드 기반 필드 렌더링 const renderGridFields = () => { if (!config) return null; const visibleFields = getVisibleFields(); const { rows, columns } = config.grid; // 그리드 레이아웃 생성 return (
{visibleFields.map((field) => { const { row, col, width } = field.position; return (
{field.label}{field.validations?.includes("required") && *} {renderField(field)}
); })}
); }; // 버튼 렌더링 const renderButtons = () => { if (!config || !config.actions || !config.actions[mode]) return null; return (
{config.actions[mode].map(action => (
); }; if (!config) return
로딩 중...
; return (
{renderGridFields()} {errors._form && ( {errors._form} )}
{renderButtons()}
); }; export default CaliForm; const LanguageWrapper = styled.div` width: ${props => props.width || '100%'}; //margin-bottom: 20px; padding-bottom: 20px; padding-left: 90px; &:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } `; const LanguageLabel = styled.h4` color: #444; margin: 0 0 10px 20px; font-size: 16px; font-weight: 500; `;