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