fix(modal-repeater-table): 품목 추가 시 UI 즉시 반영되지 않는 버그 수정

- value 상수를 localValue useState로 변경하여 내부 상태 관리
- useEffect로 외부 값(formData, propValue) 변경 시 동기화
- handleChange에서 setLocalValue 호출하여 즉각적인 UI 업데이트
- RepeaterTable, ItemSelectionModal 등 모든 참조를 localValue로 변경
This commit is contained in:
SeongHyun Kim 2025-12-04 19:05:43 +09:00
parent 0e4ecef336
commit c1400081c6
4 changed files with 173 additions and 17 deletions

View File

@ -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;

View File

@ -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}

View File

@ -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>[] = [];

View File

@ -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