fix(modal-repeater-table): 품목 추가 시 UI 즉시 반영되지 않는 버그 수정
- value 상수를 localValue useState로 변경하여 내부 상태 관리 - useEffect로 외부 값(formData, propValue) 변경 시 동기화 - handleChange에서 setLocalValue 호출하여 즉각적인 UI 업데이트 - RepeaterTable, ItemSelectionModal 등 모든 참조를 localValue로 변경
This commit is contained in:
parent
0e4ecef336
commit
c1400081c6
|
|
@ -304,7 +304,24 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
|||
};
|
||||
|
||||
// 저장 버튼 클릭 시 - UPDATE 액션 실행
|
||||
const handleSave = async () => {
|
||||
const handleSave = async (saveData?: any) => {
|
||||
// universal-form-modal 등에서 자체 저장 완료 후 호출된 경우 스킵
|
||||
if (saveData?._saveCompleted) {
|
||||
console.log("[EditModal] 자체 저장 완료된 컴포넌트에서 호출됨 - 저장 스킵");
|
||||
|
||||
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
|
||||
if (modalState.onSave) {
|
||||
try {
|
||||
modalState.onSave();
|
||||
} catch (callbackError) {
|
||||
console.error("onSave 콜백 에러:", callbackError);
|
||||
}
|
||||
}
|
||||
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!screenData?.screenInfo?.tableName) {
|
||||
toast.error("테이블 정보가 없습니다.");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -193,7 +193,18 @@ export function ModalRepeaterTableComponent({
|
|||
|
||||
// ✅ value는 formData[columnName] 우선, 없으면 prop 사용
|
||||
const columnName = component?.columnName;
|
||||
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||
const externalValue = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||
|
||||
// 🆕 내부 상태로 데이터 관리 (즉시 UI 반영을 위해)
|
||||
const [localValue, setLocalValue] = useState<any[]>(externalValue);
|
||||
|
||||
// 🆕 외부 값(formData, propValue) 변경 시 내부 상태 동기화
|
||||
useEffect(() => {
|
||||
// 외부 값이 변경되었고, 내부 값과 다른 경우에만 동기화
|
||||
if (JSON.stringify(externalValue) !== JSON.stringify(localValue)) {
|
||||
setLocalValue(externalValue);
|
||||
}
|
||||
}, [externalValue]);
|
||||
|
||||
// ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출 + 납기일 일괄 적용)
|
||||
const handleChange = (newData: any[]) => {
|
||||
|
|
@ -249,6 +260,9 @@ export function ModalRepeaterTableComponent({
|
|||
}
|
||||
}
|
||||
|
||||
// 🆕 내부 상태 즉시 업데이트 (UI 즉시 반영) - 일괄 적용된 데이터로 업데이트
|
||||
setLocalValue(processedData);
|
||||
|
||||
// 기존 onChange 콜백 호출 (호환성)
|
||||
const externalOnChange = componentConfig?.onChange || propOnChange;
|
||||
if (externalOnChange) {
|
||||
|
|
@ -321,7 +335,7 @@ export function ModalRepeaterTableComponent({
|
|||
const handleSaveRequest = async (event: Event) => {
|
||||
const componentKey = columnName || component?.id || "modal_repeater_data";
|
||||
|
||||
if (value.length === 0) {
|
||||
if (localValue.length === 0) {
|
||||
console.warn("⚠️ [ModalRepeaterTable] 저장할 데이터 없음");
|
||||
return;
|
||||
}
|
||||
|
|
@ -332,7 +346,7 @@ export function ModalRepeaterTableComponent({
|
|||
.filter(col => col.mapping?.type === "source" && col.mapping?.sourceField)
|
||||
.map(col => col.field);
|
||||
|
||||
const filteredData = value.map((item: any) => {
|
||||
const filteredData = localValue.map((item: any) => {
|
||||
const filtered: Record<string, any> = {};
|
||||
|
||||
Object.keys(item).forEach((key) => {
|
||||
|
|
@ -389,16 +403,16 @@ export function ModalRepeaterTableComponent({
|
|||
return () => {
|
||||
window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener);
|
||||
};
|
||||
}, [value, columnName, component?.id, onFormDataChange, targetTable]);
|
||||
}, [localValue, columnName, component?.id, onFormDataChange, targetTable]);
|
||||
|
||||
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
||||
|
||||
// 초기 데이터에 계산 필드 적용
|
||||
useEffect(() => {
|
||||
if (value.length > 0 && calculationRules.length > 0) {
|
||||
const calculated = calculateAll(value);
|
||||
if (localValue.length > 0 && calculationRules.length > 0) {
|
||||
const calculated = calculateAll(localValue);
|
||||
// 값이 실제로 변경된 경우만 업데이트
|
||||
if (JSON.stringify(calculated) !== JSON.stringify(value)) {
|
||||
if (JSON.stringify(calculated) !== JSON.stringify(localValue)) {
|
||||
handleChange(calculated);
|
||||
}
|
||||
}
|
||||
|
|
@ -506,7 +520,7 @@ export function ModalRepeaterTableComponent({
|
|||
const calculatedItems = calculateAll(mappedItems);
|
||||
|
||||
// 기존 데이터에 추가
|
||||
const newData = [...value, ...calculatedItems];
|
||||
const newData = [...localValue, ...calculatedItems];
|
||||
console.log("✅ 최종 데이터:", newData.length, "개 항목");
|
||||
|
||||
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
||||
|
|
@ -518,7 +532,7 @@ export function ModalRepeaterTableComponent({
|
|||
const calculatedRow = calculateRow(newRow);
|
||||
|
||||
// 데이터 업데이트
|
||||
const newData = [...value];
|
||||
const newData = [...localValue];
|
||||
newData[index] = calculatedRow;
|
||||
|
||||
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
||||
|
|
@ -526,7 +540,7 @@ export function ModalRepeaterTableComponent({
|
|||
};
|
||||
|
||||
const handleRowDelete = (index: number) => {
|
||||
const newData = value.filter((_, i) => i !== index);
|
||||
const newData = localValue.filter((_, i) => i !== index);
|
||||
|
||||
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
||||
handleChange(newData);
|
||||
|
|
@ -543,7 +557,7 @@ export function ModalRepeaterTableComponent({
|
|||
{/* 추가 버튼 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{value.length > 0 && `${value.length}개 항목`}
|
||||
{localValue.length > 0 && `${localValue.length}개 항목`}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
|
|
@ -557,7 +571,7 @@ export function ModalRepeaterTableComponent({
|
|||
{/* Repeater 테이블 */}
|
||||
<RepeaterTable
|
||||
columns={columns}
|
||||
data={value}
|
||||
data={localValue}
|
||||
onDataChange={handleChange}
|
||||
onRowChange={handleRowChange}
|
||||
onRowDelete={handleRowDelete}
|
||||
|
|
@ -573,7 +587,7 @@ export function ModalRepeaterTableComponent({
|
|||
multiSelect={multiSelect}
|
||||
filterCondition={filterCondition}
|
||||
modalTitle={modalTitle}
|
||||
alreadySelected={value}
|
||||
alreadySelected={localValue}
|
||||
uniqueField={uniqueField}
|
||||
onSelect={handleAddItems}
|
||||
columnLabels={columnLabels}
|
||||
|
|
|
|||
|
|
@ -398,7 +398,13 @@ export function UniversalFormModalComponent({
|
|||
window.dispatchEvent(new CustomEvent("refreshParentData"));
|
||||
}
|
||||
|
||||
onSave?.(formData);
|
||||
// onSave 콜백은 저장 완료 알림용으로만 사용
|
||||
// 실제 저장은 이미 위에서 완료됨 (saveSingleRow 또는 saveMultipleRows)
|
||||
// EditModal 등 부모 컴포넌트의 저장 로직이 다시 실행되지 않도록
|
||||
// _saveCompleted 플래그를 포함하여 전달
|
||||
if (onSave) {
|
||||
onSave({ ...formData, _saveCompleted: true });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("저장 실패:", error);
|
||||
toast.error(error.message || "저장에 실패했습니다.");
|
||||
|
|
@ -447,9 +453,36 @@ export function UniversalFormModalComponent({
|
|||
const { multiRowSave } = config.saveConfig;
|
||||
if (!multiRowSave) return;
|
||||
|
||||
const { commonFields = [], repeatSectionId = "", typeColumn, mainTypeValue, subTypeValue, mainSectionFields } =
|
||||
let { commonFields = [], repeatSectionId = "", typeColumn, mainTypeValue, subTypeValue, mainSectionFields = [] } =
|
||||
multiRowSave;
|
||||
|
||||
// 공통 필드가 설정되지 않은 경우, 기본정보 섹션의 모든 필드를 공통 필드로 사용
|
||||
if (commonFields.length === 0) {
|
||||
const nonRepeatableSections = config.sections.filter((s) => !s.repeatable);
|
||||
commonFields = nonRepeatableSections.flatMap((s) => s.fields.map((f) => f.columnName));
|
||||
console.log("[UniversalFormModal] 공통 필드 자동 설정:", commonFields);
|
||||
}
|
||||
|
||||
// 반복 섹션 ID가 설정되지 않은 경우, 첫 번째 반복 섹션 사용
|
||||
if (!repeatSectionId) {
|
||||
const repeatableSection = config.sections.find((s) => s.repeatable);
|
||||
if (repeatableSection) {
|
||||
repeatSectionId = repeatableSection.id;
|
||||
console.log("[UniversalFormModal] 반복 섹션 자동 설정:", repeatSectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// 디버깅: 설정 확인
|
||||
console.log("[UniversalFormModal] 다중 행 저장 설정:", {
|
||||
commonFields,
|
||||
mainSectionFields,
|
||||
repeatSectionId,
|
||||
typeColumn,
|
||||
mainTypeValue,
|
||||
subTypeValue,
|
||||
});
|
||||
console.log("[UniversalFormModal] 현재 formData:", formData);
|
||||
|
||||
// 공통 필드 데이터 추출
|
||||
const commonData: Record<string, any> = {};
|
||||
for (const fieldName of commonFields) {
|
||||
|
|
@ -457,16 +490,18 @@ export function UniversalFormModalComponent({
|
|||
commonData[fieldName] = formData[fieldName];
|
||||
}
|
||||
}
|
||||
console.log("[UniversalFormModal] 추출된 공통 데이터:", commonData);
|
||||
|
||||
// 메인 섹션 필드 데이터 추출
|
||||
const mainSectionData: Record<string, any> = {};
|
||||
if (mainSectionFields) {
|
||||
if (mainSectionFields && mainSectionFields.length > 0) {
|
||||
for (const fieldName of mainSectionFields) {
|
||||
if (formData[fieldName] !== undefined) {
|
||||
mainSectionData[fieldName] = formData[fieldName];
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("[UniversalFormModal] 추출된 메인 섹션 데이터:", mainSectionData);
|
||||
|
||||
// 저장할 행들 준비
|
||||
const rowsToSave: Record<string, any>[] = [];
|
||||
|
|
|
|||
|
|
@ -467,6 +467,96 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
|
||||
{config.saveConfig.multiRowSave?.enabled && (
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
{/* 공통 필드 선택 */}
|
||||
<div>
|
||||
<Label className="text-[10px]">공통 필드 (모든 행에 저장)</Label>
|
||||
<div className="mt-1 max-h-32 overflow-y-auto border rounded p-1 space-y-1">
|
||||
{config.sections
|
||||
.filter((s) => !s.repeatable)
|
||||
.flatMap((s) => s.fields)
|
||||
.map((field) => (
|
||||
<label key={field.id} className="flex items-center gap-1 text-[10px] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.saveConfig.multiRowSave?.commonFields?.includes(field.columnName) || false}
|
||||
onChange={(e) => {
|
||||
const currentFields = config.saveConfig.multiRowSave?.commonFields || [];
|
||||
const newFields = e.target.checked
|
||||
? [...currentFields, field.columnName]
|
||||
: currentFields.filter((f) => f !== field.columnName);
|
||||
updateSaveConfig({
|
||||
multiRowSave: { ...config.saveConfig.multiRowSave, commonFields: newFields },
|
||||
});
|
||||
}}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{field.label} ({field.columnName})
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<HelpText>메인 행과 겸직 행 모두에 저장될 필드</HelpText>
|
||||
</div>
|
||||
|
||||
{/* 메인 섹션 필드 선택 */}
|
||||
<div>
|
||||
<Label className="text-[10px]">메인 섹션 필드 (메인 행에만 저장)</Label>
|
||||
<div className="mt-1 max-h-32 overflow-y-auto border rounded p-1 space-y-1">
|
||||
{config.sections
|
||||
.filter((s) => !s.repeatable)
|
||||
.flatMap((s) => s.fields)
|
||||
.filter((field) => !config.saveConfig.multiRowSave?.commonFields?.includes(field.columnName))
|
||||
.map((field) => (
|
||||
<label key={field.id} className="flex items-center gap-1 text-[10px] cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.saveConfig.multiRowSave?.mainSectionFields?.includes(field.columnName) || false}
|
||||
onChange={(e) => {
|
||||
const currentFields = config.saveConfig.multiRowSave?.mainSectionFields || [];
|
||||
const newFields = e.target.checked
|
||||
? [...currentFields, field.columnName]
|
||||
: currentFields.filter((f) => f !== field.columnName);
|
||||
updateSaveConfig({
|
||||
multiRowSave: { ...config.saveConfig.multiRowSave, mainSectionFields: newFields },
|
||||
});
|
||||
}}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{field.label} ({field.columnName})
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<HelpText>메인 행에만 저장될 필드 (공통 필드 제외)</HelpText>
|
||||
</div>
|
||||
|
||||
{/* 반복 섹션 선택 */}
|
||||
<div>
|
||||
<Label className="text-[10px]">반복 섹션</Label>
|
||||
<Select
|
||||
value={config.saveConfig.multiRowSave?.repeatSectionId || ""}
|
||||
onValueChange={(value) =>
|
||||
updateSaveConfig({
|
||||
multiRowSave: { ...config.saveConfig.multiRowSave, repeatSectionId: value },
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-[10px] mt-1">
|
||||
<SelectValue placeholder="반복 섹션 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.sections
|
||||
.filter((s) => s.repeatable)
|
||||
.map((section) => (
|
||||
<SelectItem key={section.id} value={section.id}>
|
||||
{section.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<HelpText>겸직 등 반복 데이터가 있는 섹션</HelpText>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<Label className="text-[10px]">구분 컬럼</Label>
|
||||
<Input
|
||||
|
|
|
|||
Loading…
Reference in New Issue