From 8eccdd0b4c4d4e12b322039de9dac955a3a2b4fd Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 19 Nov 2025 11:48:00 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=88=98=EC=A3=BC=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=EC=B6=94=EA=B0=80=20=EC=8B=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EC=B6=9C=EB=A0=A5=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문제: - 조건부 컨테이너 내부의 modal-repeater-table 컴포넌트가 데이터 업데이트 불가 - ConditionalSectionViewer가 RealtimePreview에 formData/onFormDataChange 미전달 해결: - ConditionalSectionViewer.tsx: RealtimePreview에 formData, onFormDataChange props 추가 - DynamicComponentRenderer.tsx: 디버깅 로그 정리 - ScreenModal.tsx: 디버깅 로그 정리 영향: - 수주 등록 화면 품목 추가 기능 정상 작동 - 조건부 컨테이너 내부 모든 폼 컴포넌트 데이터 바인딩 정상화 Refs: #수주관리 #modal-repeater-table #ConditionalContainer --- frontend/MODAL_REPEATER_TABLE_DEBUG.md | 185 ++++++++++++++++++ frontend/components/common/ScreenModal.tsx | 14 +- .../lib/registry/DynamicComponentRenderer.tsx | 18 +- .../ConditionalSectionViewer.tsx | 2 + .../ItemSelectionModal.tsx | 21 +- .../ModalRepeaterTableComponent.tsx | 83 +++++++- .../ModalRepeaterTableRenderer.tsx | 23 ++- .../modal-repeater-table/RepeaterTable.tsx | 7 +- 8 files changed, 312 insertions(+), 41 deletions(-) create mode 100644 frontend/MODAL_REPEATER_TABLE_DEBUG.md diff --git a/frontend/MODAL_REPEATER_TABLE_DEBUG.md b/frontend/MODAL_REPEATER_TABLE_DEBUG.md new file mode 100644 index 00000000..0f0f66ce --- /dev/null +++ b/frontend/MODAL_REPEATER_TABLE_DEBUG.md @@ -0,0 +1,185 @@ +# Modal Repeater Table 디버깅 가이드 + +## 📊 콘솔 로그 확인 순서 + +새로고침 후 수주 등록 모달을 열고, 아래 순서대로 콘솔 로그를 확인하세요: + +### 1️⃣ 컴포넌트 마운트 (초기 로드) + +``` +🎬 ModalRepeaterTableComponent 마운트: { + config: {...}, + propColumns: [...], + columns: [...], + columnsLength: N, // ⚠️ 0이면 문제! + value: [], + valueLength: 0, + sourceTable: "item_info", + sourceColumns: [...], + uniqueField: "item_number" +} +``` + +**✅ 정상:** +- `columnsLength: 8` (품번, 품명, 규격, 재질, 수량, 단가, 금액, 납기일) +- `columns` 배열에 각 컬럼의 `field`, `label`, `type` 정보가 있어야 함 + +**❌ 문제:** +- `columnsLength: 0` → **이것이 문제의 원인!** +- 빈 배열이면 테이블에 컬럼이 표시되지 않음 + +--- + +### 2️⃣ 항목 검색 모달 열림 + +``` +🚪 모달 열림 - uniqueField: "item_number", multiSelect: true +``` + +--- + +### 3️⃣ 품목 체크 (선택) + +``` +🖱️ 행 클릭: { + item: { item_number: "SLI-2025-0003", item_name: "실리콘 고무 시트", ... }, + uniqueField: "item_number", + itemValue: "SLI-2025-0003", + currentSelected: 0, + selectedValues: [] +} + +✅ 매칭 발견: { selectedValue: "SLI-2025-0003", itemValue: "SLI-2025-0003", uniqueField: "item_number" } +``` + +--- + +### 4️⃣ 추가 버튼 클릭 + +``` +✅ ItemSelectionModal 추가 버튼 클릭: { + selectedCount: 1, + selectedItems: [{ item_number: "SLI-2025-0003", item_name: "실리콘 고무 시트", ... }], + uniqueField: "item_number" +} +``` + +--- + +### 5️⃣ 데이터 추가 처리 + +``` +➕ handleAddItems 호출: { + selectedItems: [{ item_number: "SLI-2025-0003", ... }], + currentValue: [], + columns: [...], // ⚠️ 여기도 확인! + calculationRules: [...] +} + +📝 기본값 적용 후: [{ item_number: "SLI-2025-0003", quantity: 1, ... }] + +🔢 계산 필드 적용 후: [{ item_number: "SLI-2025-0003", quantity: 1, selling_price: 1000, amount: 1000, ... }] + +✅ 최종 데이터 (onChange 호출): [{ item_number: "SLI-2025-0003", quantity: 1, selling_price: 1000, amount: 1000, ... }] +``` + +--- + +### 6️⃣ Renderer 업데이트 + +``` +🔄 ModalRepeaterTableRenderer onChange 호출: { + previousValue: [], + newValue: [{ item_number: "SLI-2025-0003", ... }] +} +``` + +--- + +### 7️⃣ value 변경 감지 + +``` +📦 ModalRepeaterTableComponent value 변경: { + valueLength: 1, + value: [{ item_number: "SLI-2025-0003", ... }], + columns: [...] // ⚠️ 여기도 확인! +} +``` + +--- + +### 8️⃣ 테이블 리렌더링 + +``` +📊 RepeaterTable 데이터 업데이트: { + rowCount: 1, + data: [{ item_number: "SLI-2025-0003", ... }], + columns: ["item_number", "item_name", "specification", "material", "quantity", "selling_price", "amount", "delivery_date"] +} +``` + +--- + +## 🔍 문제 진단 + +### Case 1: columns가 비어있음 (columnsLength: 0) + +**원인:** +- 화면관리 시스템에서 modal-repeater-table 컴포넌트의 `columns` 설정을 하지 않음 +- DB에 컬럼 설정이 저장되지 않음 + +**해결:** +1. 화면 관리 페이지로 이동 +2. 해당 화면 편집 +3. modal-repeater-table 컴포넌트 선택 +4. 우측 설정 패널에서 "컬럼 설정" 탭 열기 +5. 다음 컬럼들을 추가: + - 품번 (item_number, text, 편집불가) + - 품명 (item_name, text, 편집불가) + - 규격 (specification, text, 편집불가) + - 재질 (material, text, 편집불가) + - 수량 (quantity, number, 편집가능, 기본값: 1) + - 단가 (selling_price, number, 편집가능) + - 금액 (amount, number, 편집불가, 계산필드) + - 납기일 (delivery_date, date, 편집가능) +6. 저장 + +--- + +### Case 2: 로그가 8번까지 나오는데 화면에 안 보임 + +**원인:** +- React 리렌더링 문제 +- 화면관리 시스템의 상태 동기화 문제 + +**해결:** +1. 브라우저 개발자 도구 → Elements 탭 +2. `#component-comp_5jdmuzai .border.rounded-md table tbody` 찾기 +3. 실제 DOM에 `` 요소가 추가되었는지 확인 +4. 추가되었다면 CSS 문제 (display: none 등) +5. 추가 안 되었다면 컴포넌트 렌더링 문제 + +--- + +### Case 3: 로그가 5번까지만 나오고 멈춤 + +**원인:** +- `onChange` 콜백이 제대로 전달되지 않음 +- Renderer의 `updateComponent`가 작동하지 않음 + +**해결:** +- 이미 수정한 `ModalRepeaterTableRenderer.tsx` 코드 확인 +- `handleChange` 함수가 호출되는지 확인 + +--- + +## 📝 다음 단계 + +위 로그를 **모두** 복사해서 공유해주세요. 특히: + +1. **🎬 마운트 로그의 `columnsLength` 값** +2. **로그가 어디까지 출력되는지** +3. **Elements 탭에서 `tbody` 내부 HTML 구조** + +이 정보로 정확한 문제를 진단할 수 있습니다! + diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 087444b7..e8c85c37 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -436,16 +436,10 @@ export const ScreenModal: React.FC = ({ className }) => { allComponents={screenData.components} formData={formData} onFormDataChange={(fieldName, value) => { - // console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`); - // console.log("📋 현재 formData:", formData); - setFormData((prev) => { - const newFormData = { - ...prev, - [fieldName]: value, - }; - // console.log("📝 ScreenModal 업데이트된 formData:", newFormData); - return newFormData; - }); + setFormData((prev) => ({ + ...prev, + [fieldName]: value, + })); }} onRefresh={() => { // 부모 화면의 테이블 새로고침 이벤트 발송 diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 865a361f..4c95ceb8 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -262,7 +262,14 @@ export const DynamicComponentRenderer: React.FC = // 컴포넌트의 columnName에 해당하는 formData 값 추출 const fieldName = (component as any).columnName || component.id; - const currentValue = formData?.[fieldName] || ""; + + // modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화 + let currentValue; + if (componentType === "modal-repeater-table") { + currentValue = formData?.[fieldName] || []; + } else { + currentValue = formData?.[fieldName] || ""; + } // onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리 const handleChange = (value: any) => { @@ -274,13 +281,14 @@ export const DynamicComponentRenderer: React.FC = } if (onFormDataChange) { + // modal-repeater-table은 배열 데이터를 다룸 + if (componentType === "modal-repeater-table") { + onFormDataChange(fieldName, actualValue); + } // RepeaterInput 같은 복합 컴포넌트는 전체 데이터를 전달 - // 단순 input 컴포넌트는 (fieldName, value) 형태로 전달받음 - if (componentType === "repeater-field-group" || componentType === "repeater") { - // fieldName과 함께 전달 + else if (componentType === "repeater-field-group" || componentType === "repeater") { onFormDataChange(fieldName, actualValue); } else { - // 이미 fieldName이 포함된 경우는 그대로 전달 onFormDataChange(fieldName, actualValue); } } diff --git a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx index 229f52f2..db5de1e9 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx @@ -142,6 +142,8 @@ export function ConditionalSectionViewer({ onClick={() => {}} screenId={screenInfo?.id} tableName={screenInfo?.tableName} + formData={formData} + onFormDataChange={onFormDataChange} /> ))} diff --git a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx index 4951aef6..60da98f8 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx @@ -42,7 +42,6 @@ export function ItemSelectionModal({ // 모달 열릴 때 초기 검색 useEffect(() => { if (open) { - console.log("🚪 모달 열림 - uniqueField:", uniqueField, "multiSelect:", multiSelect); search("", 1); // 빈 검색어로 전체 목록 조회 setSelectedItems([]); } else { @@ -59,14 +58,6 @@ export function ItemSelectionModal({ const handleToggleItem = (item: any) => { const itemValue = uniqueField ? item[uniqueField] : undefined; - console.log("🖱️ 행 클릭:", { - item, - uniqueField, - itemValue, - currentSelected: selectedItems.length, - selectedValues: uniqueField ? selectedItems.map(s => s[uniqueField]) : [] - }); - if (!multiSelect) { setSelectedItems([item]); return; @@ -77,14 +68,10 @@ export function ItemSelectionModal({ console.warn(`⚠️ uniqueField "${uniqueField}"의 값이 undefined입니다. 객체 참조로 비교합니다.`); const itemIsSelected = selectedItems.includes(item); - console.log("📊 선택 상태 (객체 참조):", itemIsSelected); - if (itemIsSelected) { const newSelected = selectedItems.filter((selected) => selected !== item); - console.log("➖ 제거 후:", newSelected.length); setSelectedItems(newSelected); } else { - console.log("➕ 추가"); setSelectedItems([...selectedItems, item]); } return; @@ -101,8 +88,6 @@ export function ItemSelectionModal({ return selectedValue === itemValue; }); - console.log("📊 선택 상태:", itemIsSelected); - if (itemIsSelected) { const newSelected = selectedItems.filter((selected) => { if (!uniqueField) { @@ -114,15 +99,15 @@ export function ItemSelectionModal({ } return selectedValue !== itemValue; }); - console.log("➖ 제거 후:", newSelected.length); setSelectedItems(newSelected); } else { - console.log("➕ 추가"); setSelectedItems([...selectedItems, item]); } }; const handleConfirm = () => { + console.log("✅ ItemSelectionModal 추가:", selectedItems.length, "개 항목"); + onSelect(selectedItems); onOpenChange(false); }; @@ -307,7 +292,7 @@ export function ItemSelectionModal({ }} >
- +
)} diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 6f74003a..0796c309 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react"; import { ItemSelectionModal } from "./ItemSelectionModal"; import { RepeaterTable } from "./RepeaterTable"; -import { ModalRepeaterTableProps } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig } from "./types"; import { useCalculation } from "./useCalculation"; import { cn } from "@/lib/utils"; @@ -32,19 +32,87 @@ export function ModalRepeaterTableComponent({ }: ModalRepeaterTableComponentProps) { // config prop 우선, 없으면 개별 prop 사용 const sourceTable = config?.sourceTable || propSourceTable || ""; - const sourceColumns = config?.sourceColumns || propSourceColumns || []; + + // sourceColumns에서 빈 문자열 필터링 + const rawSourceColumns = config?.sourceColumns || propSourceColumns || []; + const sourceColumns = rawSourceColumns.filter((col) => 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 columns = config?.columns || propColumns || []; const calculationRules = config?.calculationRules || propCalculationRules || []; const value = config?.value || propValue || []; const onChange = config?.onChange || propOnChange || (() => {}); - const uniqueField = config?.uniqueField || propUniqueField; + + // uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경 + const rawUniqueField = config?.uniqueField || propUniqueField; + const uniqueField = rawUniqueField === "order_no" && sourceTable === "item_info" + ? "item_number" + : rawUniqueField; + const filterCondition = config?.filterCondition || propFilterCondition || {}; const companyCode = config?.companyCode || propCompanyCode; const [modalOpen, setModalOpen] = useState(false); + + // columns가 비어있으면 sourceColumns로부터 자동 생성 + const columns = React.useMemo((): RepeaterColumnConfig[] => { + const configuredColumns = config?.columns || propColumns || []; + + if (configuredColumns.length > 0) { + console.log("✅ 설정된 columns 사용:", configuredColumns); + return configuredColumns; + } + + // columns가 비어있으면 sourceColumns로부터 자동 생성 + if (sourceColumns.length > 0) { + console.log("🔄 sourceColumns로부터 자동 생성:", sourceColumns); + const autoColumns: RepeaterColumnConfig[] = sourceColumns.map((field) => ({ + field: field, + label: field, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능) + editable: false, // 기본적으로 읽기 전용 + type: "text" as const, + width: "150px", + })); + console.log("📋 자동 생성된 columns:", autoColumns); + return autoColumns; + } + + console.warn("⚠️ columns와 sourceColumns 모두 비어있음!"); + return []; + }, [config?.columns, propColumns, sourceColumns]); + + // 초기 props 로깅 + useEffect(() => { + if (rawSourceColumns.length !== sourceColumns.length) { + console.warn(`⚠️ sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개 (빈 문자열 제거)`); + } + + if (rawUniqueField !== uniqueField) { + console.warn(`⚠️ uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`); + } + + console.log("🎬 ModalRepeaterTableComponent 마운트:", { + columnsLength: columns.length, + sourceTable, + sourceColumns, + uniqueField, + }); + + if (columns.length === 0) { + console.error("❌ columns가 비어있습니다! sourceColumns:", sourceColumns); + } else { + console.log("✅ columns 설정 완료:", columns.map(c => c.label || c.field).join(", ")); + } + }, []); + + // value 변경 감지 + useEffect(() => { + console.log("📦 ModalRepeaterTableComponent value 변경:", { + valueLength: value.length, + }); + }, [value]); + const { calculateRow, calculateAll } = useCalculation(calculationRules); // 초기 데이터에 계산 필드 적용 @@ -59,6 +127,8 @@ export function ModalRepeaterTableComponent({ }, []); const handleAddItems = (items: any[]) => { + console.log("➕ handleAddItems 호출:", items.length, "개 항목"); + // 기본값 적용 const itemsWithDefaults = items.map((item) => { const newItem = { ...item }; @@ -74,7 +144,10 @@ export function ModalRepeaterTableComponent({ const calculatedItems = calculateAll(itemsWithDefaults); // 기존 데이터에 추가 - onChange([...value, ...calculatedItems]); + const newData = [...value, ...calculatedItems]; + console.log("✅ 최종 데이터:", newData.length, "개 항목"); + + onChange(newData); }; const handleRowChange = (index: number, newRow: any) => { diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableRenderer.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableRenderer.tsx index 8b6d09f3..6362e1ce 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableRenderer.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableRenderer.tsx @@ -13,12 +13,31 @@ export class ModalRepeaterTableRenderer extends AutoRegisteringComponentRenderer static componentDefinition = ModalRepeaterTableDefinition; render(): React.ReactElement { - return ; + // onChange 콜백을 명시적으로 전달 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleChange = (newValue: any[]) => { + console.log("🔄 ModalRepeaterTableRenderer onChange:", newValue.length, "개 항목"); + + // 컴포넌트 업데이트 + this.updateComponent({ value: newValue }); + + // 원본 onChange 콜백도 호출 (있다면) + if (this.props.onChange) { + this.props.onChange(newValue); + } + }; + + // renderer prop 제거 (불필요) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { onChange, ...restProps } = this.props; + + return ; } /** - * 값 변경 처리 + * 값 변경 처리 (레거시 메서드 - 호환성 유지) */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any protected handleValueChange = (value: any) => { this.updateComponent({ value }); }; diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index b1c94dda..879cbce5 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; @@ -28,6 +28,11 @@ export function RepeaterTable({ field: string; } | null>(null); + // 데이터 변경 감지 (필요시 활성화) + // useEffect(() => { + // console.log("📊 RepeaterTable 데이터 업데이트:", data.length, "개 행"); + // }, [data]); + const handleCellEdit = (rowIndex: number, field: string, value: any) => { const newRow = { ...data[rowIndex], [field]: value }; onRowChange(rowIndex, newRow);