diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 0796c309..e387d50e 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -128,20 +128,56 @@ export function ModalRepeaterTableComponent({ const handleAddItems = (items: any[]) => { console.log("➕ handleAddItems 호출:", items.length, "개 항목"); + console.log("📋 소스 데이터:", items); + + // 매핑 규칙에 따라 데이터 변환 + const mappedItems = items.map((sourceItem) => { + const newItem: any = {}; - // 기본값 적용 - const itemsWithDefaults = items.map((item) => { - const newItem = { ...item }; columns.forEach((col) => { + console.log(`🔄 컬럼 "${col.field}" 매핑 처리:`, col.mapping); + + // 1. 매핑 규칙이 있는 경우 + if (col.mapping) { + if (col.mapping.type === "source") { + // 소스 테이블 컬럼에서 복사 + const sourceField = col.mapping.sourceField; + if (sourceField && sourceItem[sourceField] !== undefined) { + newItem[col.field] = sourceItem[sourceField]; + console.log(` ✅ 소스 복사: ${sourceField} → ${col.field}:`, newItem[col.field]); + } else { + console.warn(` ⚠️ 소스 필드 "${sourceField}" 값이 없음`); + } + } else if (col.mapping.type === "reference") { + // 외부 테이블 참조 (TODO: API 호출 필요) + console.log(` ⏳ 참조 조회 필요: ${col.mapping.referenceTable}.${col.mapping.referenceField}`); + // 현재는 빈 값으로 설정 (나중에 API 호출로 구현) + newItem[col.field] = undefined; + } else if (col.mapping.type === "manual") { + // 사용자 입력 (빈 값) + newItem[col.field] = undefined; + console.log(` ✏️ 수동 입력 필드`); + } + } + // 2. 매핑 규칙이 없는 경우 - 소스 데이터에서 같은 필드명으로 복사 + else if (sourceItem[col.field] !== undefined) { + newItem[col.field] = sourceItem[col.field]; + console.log(` 📝 직접 복사: ${col.field}:`, newItem[col.field]); + } + + // 3. 기본값 적용 if (col.defaultValue !== undefined && newItem[col.field] === undefined) { newItem[col.field] = col.defaultValue; + console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue); } }); + + console.log("📦 변환된 항목:", newItem); return newItem; }); // 계산 필드 업데이트 - const calculatedItems = calculateAll(itemsWithDefaults); + const calculatedItems = calculateAll(mappedItems); // 기존 데이터에 추가 const newData = [...value, ...calculatedItems]; diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx index 3854c9ee..a952d845 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx @@ -9,7 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Plus, X, Check, ChevronsUpDown } from "lucide-react"; -import { ModalRepeaterTableProps } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule } from "./types"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { cn } from "@/lib/utils"; @@ -18,18 +18,158 @@ interface ModalRepeaterTableConfigPanelProps { onConfigChange: (config: Partial) => void; } +// 소스 컬럼 선택기 (동적 테이블별 컬럼 로드) +function SourceColumnSelector({ + sourceTable, + value, + onChange, +}: { + sourceTable: string; + value: string; + onChange: (value: string) => void; +}) { + const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const loadColumns = async () => { + if (!sourceTable) { + setColumns([]); + return; + } + + setIsLoading(true); + try { + const response = await tableManagementApi.getColumnList(sourceTable); + if (response.success && response.data) { + setColumns(response.data.columns); + } + } catch (error) { + console.error("컬럼 로드 실패:", error); + setColumns([]); + } finally { + setIsLoading(false); + } + }; + loadColumns(); + }, [sourceTable]); + + return ( + + ); +} + +// 참조 컬럼 선택기 (동적 테이블별 컬럼 로드) +function ReferenceColumnSelector({ + referenceTable, + value, + onChange, +}: { + referenceTable: string; + value: string; + onChange: (value: string) => void; +}) { + const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const loadColumns = async () => { + if (!referenceTable) { + setColumns([]); + return; + } + + setIsLoading(true); + try { + const response = await tableManagementApi.getColumnList(referenceTable); + if (response.success && response.data) { + setColumns(response.data.columns); + } + } catch (error) { + console.error("참조 컬럼 로드 실패:", error); + setColumns([]); + } finally { + setIsLoading(false); + } + }; + loadColumns(); + }, [referenceTable]); + + return ( + + ); +} + export function ModalRepeaterTableConfigPanel({ config, onConfigChange, }: ModalRepeaterTableConfigPanelProps) { - const [localConfig, setLocalConfig] = useState(config); - const [allTables, setAllTables] = useState([]); - const [tableColumns, setTableColumns] = useState([]); + // 초기 설정 정리: 계산 규칙과 컬럼 설정 동기화 + const cleanupInitialConfig = (initialConfig: Partial): Partial => { + // 계산 규칙이 없으면 모든 컬럼의 calculated 속성 제거 + if (!initialConfig.calculationRules || initialConfig.calculationRules.length === 0) { + const cleanedColumns = (initialConfig.columns || []).map((col) => { + const { calculated: _calc, ...rest } = col; + return { ...rest, editable: true }; + }); + return { ...initialConfig, columns: cleanedColumns }; + } + + // 계산 규칙이 있으면 결과 필드만 calculated=true + const resultFields = new Set(initialConfig.calculationRules.map((rule) => rule.result)); + const cleanedColumns = (initialConfig.columns || []).map((col) => { + if (resultFields.has(col.field)) { + // 계산 결과 필드는 calculated=true, editable=false + return { ...col, calculated: true, editable: false }; + } else { + // 나머지 필드는 calculated 제거, editable=true + const { calculated: _calc, ...rest } = col; + return { ...rest, editable: true }; + } + }); + + return { ...initialConfig, columns: cleanedColumns }; + }; + + const [localConfig, setLocalConfig] = useState(cleanupInitialConfig(config)); + const [allTables, setAllTables] = useState<{ tableName: string; displayName?: string }[]>([]); + const [tableColumns, setTableColumns] = useState<{ columnName: string; displayName?: string }[]>([]); + const [targetTableColumns, setTargetTableColumns] = useState<{ columnName: string; displayName?: string }[]>([]); const [isLoadingTables, setIsLoadingTables] = useState(false); const [isLoadingColumns, setIsLoadingColumns] = useState(false); + const [isLoadingTargetColumns, setIsLoadingTargetColumns] = useState(false); const [openTableCombo, setOpenTableCombo] = useState(false); + const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false); const [openUniqueFieldCombo, setOpenUniqueFieldCombo] = useState(false); + // config 변경 시 localConfig 동기화 (cleanupInitialConfig 적용) + useEffect(() => { + const cleanedConfig = cleanupInitialConfig(config); + setLocalConfig(cleanedConfig); + }, [config]); + // 전체 테이블 목록 로드 useEffect(() => { const loadTables = async () => { @@ -48,7 +188,7 @@ export function ModalRepeaterTableConfigPanel({ loadTables(); }, []); - // 선택된 테이블의 컬럼 목록 로드 + // 소스 테이블의 컬럼 목록 로드 useEffect(() => { const loadColumns = async () => { if (!localConfig.sourceTable) { @@ -72,9 +212,29 @@ export function ModalRepeaterTableConfigPanel({ loadColumns(); }, [localConfig.sourceTable]); + // 저장 테이블의 컬럼 목록 로드 useEffect(() => { - setLocalConfig(config); - }, [config]); + const loadTargetColumns = async () => { + if (!localConfig.targetTable) { + setTargetTableColumns([]); + return; + } + + setIsLoadingTargetColumns(true); + try { + const response = await tableManagementApi.getColumnList(localConfig.targetTable); + if (response.success && response.data) { + setTargetTableColumns(response.data.columns); + } + } catch (error) { + console.error("저장 테이블 컬럼 로드 실패:", error); + setTargetTableColumns([]); + } finally { + setIsLoadingTargetColumns(false); + } + }; + loadTargetColumns(); + }, [localConfig.targetTable]); const updateConfig = (updates: Partial) => { const newConfig = { ...localConfig, ...updates }; @@ -116,235 +276,887 @@ export function ModalRepeaterTableConfigPanel({ updateConfig({ sourceSearchFields: fields }); }; + // 🆕 저장 테이블 컬럼에서 반복 테이블 컬럼 추가 + const addRepeaterColumnFromTarget = (columnName: string) => { + const columns = localConfig.columns || []; + + // 이미 존재하는 컬럼인지 확인 + if (columns.some(col => col.field === columnName)) { + alert("이미 추가된 컬럼입니다."); + return; + } + + const targetCol = targetTableColumns.find(c => c.columnName === columnName); + + const newColumn: RepeaterColumnConfig = { + field: columnName, + label: targetCol?.displayName || columnName, + type: "text", + width: "150px", + editable: true, + mapping: { + type: "manual", + referenceTable: localConfig.targetTable, + referenceField: columnName, + }, + }; + + updateConfig({ columns: [...columns, newColumn] }); + }; + + // 🆕 반복 테이블 컬럼 삭제 + const removeRepeaterColumn = (index: number) => { + const columns = [...(localConfig.columns || [])]; + columns.splice(index, 1); + updateConfig({ columns }); + }; + + // 🆕 반복 테이블 컬럼 개별 수정 + const updateRepeaterColumn = (index: number, updates: Partial) => { + const columns = [...(localConfig.columns || [])]; + columns[index] = { ...columns[index], ...updates }; + updateConfig({ columns }); + }; + + // 🆕 계산 규칙 관리 + const addCalculationRule = () => { + const rules = localConfig.calculationRules || []; + const newRule: CalculationRule = { + result: "", + formula: "", + dependencies: [], + }; + updateConfig({ calculationRules: [...rules, newRule] }); + }; + + const updateCalculationRule = (index: number, updates: Partial) => { + const rules = [...(localConfig.calculationRules || [])]; + const oldRule = rules[index]; + const newRule = { ...oldRule, ...updates }; + + // 결과 필드가 변경된 경우 + if (updates.result !== undefined && oldRule.result !== updates.result) { + const columns = [...(localConfig.columns || [])]; + + // 이전 결과 필드의 calculated 속성 제거 + if (oldRule.result) { + const oldResultIndex = columns.findIndex(c => c.field === oldRule.result); + if (oldResultIndex !== -1) { + const { calculated: _calc, ...rest } = columns[oldResultIndex]; + columns[oldResultIndex] = { ...rest, editable: true }; + } + } + + // 새 결과 필드를 calculated=true, editable=false로 설정 + if (updates.result) { + const newResultIndex = columns.findIndex(c => c.field === updates.result); + if (newResultIndex !== -1) { + columns[newResultIndex] = { + ...columns[newResultIndex], + calculated: true, + editable: false, + }; + } + } + + rules[index] = newRule; + updateConfig({ calculationRules: rules, columns }); + return; + } + + rules[index] = newRule; + updateConfig({ calculationRules: rules }); + }; + + const removeCalculationRule = (index: number) => { + const rules = [...(localConfig.calculationRules || [])]; + const removedRule = rules[index]; + + // 결과 필드의 calculated 속성 제거 + if (removedRule.result) { + const columns = [...(localConfig.columns || [])]; + const resultIndex = columns.findIndex(c => c.field === removedRule.result); + if (resultIndex !== -1) { + const { calculated: _calc, ...rest } = columns[resultIndex]; + columns[resultIndex] = { ...rest, editable: true }; + } + rules.splice(index, 1); + updateConfig({ calculationRules: rules, columns }); + return; + } + + rules.splice(index, 1); + updateConfig({ calculationRules: rules }); + }; + return ( -
-
- - - +
+ {/* 소스/저장 테이블 설정 */} +
+
+

1. 소스 테이블 (데이터 검색)

+

+ 항목 검색 모달에서 어떤 테이블의 어떤 컬럼 정보를 보여줄 건지 설정 +

+
+ + {/* 소스 테이블 */} +
+ + + + + + + + + + 테이블을 찾을 수 없습니다. + + {allTables.map((table) => ( + { + updateConfig({ sourceTable: table.tableName }); + setOpenTableCombo(false); + }} + className="text-xs sm:text-sm" + > + +
+ {table.displayName || table.tableName} + {table.displayName && {table.tableName}} +
+
+ ))} +
+
+
+
+
+

+ 모달에서 검색할 데이터 테이블 +

+
+ +
+

2. 저장 테이블 (데이터 저장)

+

+ 반복 테이블에 입력된 정보를 어떤 테이블에 저장할 건지 선택 +

+
+ + {/* 저장 테이블 */} +
+ + + + + + + + + + 테이블을 찾을 수 없습니다. + + {allTables.map((table) => ( + { + updateConfig({ targetTable: table.tableName }); + setOpenTargetTableCombo(false); + }} + className="text-xs sm:text-sm" + > + +
+ {table.displayName || table.tableName} + {table.displayName && {table.tableName}} +
+
+ ))} +
+
+
+
+
+

+ 반복 테이블 데이터를 저장할 대상 테이블 +

+
+ + {/* 소스 컬럼 */} +
+
+ - - - - - - 테이블을 찾을 수 없습니다. - - {allTables.map((table) => ( - { - updateConfig({ sourceTable: table.tableName }); - setOpenTableCombo(false); - }} - className="text-xs sm:text-sm" - > - -
- {table.displayName || table.tableName} - {table.displayName && {table.tableName}} -
-
- ))} -
-
-
-
- -
- -
- - updateConfig({ modalTitle: e.target.value })} - placeholder="항목 검색 및 선택" - className="h-8 text-xs sm:h-10 sm:text-sm" - /> -
- -
- - updateConfig({ modalButtonText: e.target.value })} - placeholder="항목 검색" - className="h-8 text-xs sm:h-10 sm:text-sm" - /> -
- -
- - - - - - - - - - 필드를 찾을 수 없습니다. - - {tableColumns.map((column) => ( - { - updateConfig({ uniqueField: column.columnName }); - setOpenUniqueFieldCombo(false); - }} - className="text-xs sm:text-sm" - > - -
- {column.displayName || column.columnName} - {column.displayName && {column.columnName}} -
-
- ))} -
-
-
-
-
+
+

+ 모달 테이블에 표시할 컬럼들 +

+
+ {(localConfig.sourceColumns || []).map((column, index) => ( +
+ + +
+ ))} +
+
+ + {/* 검색 필드 */} +
+
+ + +
+

+ 모달에서 검색 가능한 필드들 +

+
+ {(localConfig.sourceSearchFields || []).map((field, index) => ( +
+ + +
+ ))} +
+
+ + {/* 중복 체크 필드 */} +
+ + + + + + + + + + 필드를 찾을 수 없습니다. + + {tableColumns.map((column) => ( + { + updateConfig({ uniqueField: column.columnName }); + setOpenUniqueFieldCombo(false); + }} + className="text-xs sm:text-sm" + > + +
+ {column.displayName || column.columnName} + {column.displayName && {column.columnName}} +
+
+ ))} +
+
+
+
+
+

+ 중복 추가를 방지할 고유 필드 (예: 품목 코드) +

+
-
-
- - - updateConfig({ multiSelect: checked }) - } + {/* 모달 설정 */} +
+

모달 설정

+ +
+ + updateConfig({ modalTitle: e.target.value })} + placeholder="항목 검색 및 선택" + className="h-8 text-xs sm:h-10 sm:text-sm" />
+ +
+ + updateConfig({ modalButtonText: e.target.value })} + placeholder="항목 검색" + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ +
+
+ + + updateConfig({ multiSelect: checked }) + } + /> +
+
-
-
- + {/* 반복 테이블 컬럼 관리 */} +
+
+

3. 반복 테이블 컬럼 (헤더 구성)

+

+ 반복 테이블 헤더에 표시할 컬럼 옵션 - 어떤 컬럼을 화면에 보여줄지 선택 +

+
+ + {/* 저장 테이블 컬럼 선택 */} + {localConfig.targetTable && targetTableColumns.length > 0 && ( +
+ +
+ {targetTableColumns.map((col) => ( + + ))} +
+
+ )} + + {/* 추가된 컬럼 목록 */} + {localConfig.columns && localConfig.columns.length > 0 && ( +
+ +
+ {localConfig.columns.map((col, index) => ( +
+ {col.label} + + ({col.mapping?.type === "source" ? "소스" : col.mapping?.type === "reference" ? "참조" : "수동입력"}) + + +
+ ))} +
+ + {/* 개별 컬럼 설정 - 세로 레이아웃 */} +
+ {localConfig.columns.map((col, index) => ( +
+
+

{col.label}

+ +
+ + {/* 세로 레이아웃: 모든 항목 동일한 너비/높이 */} +
+ {/* 1. 품목명 */} +
+ + updateRepeaterColumn(index, { label: e.target.value })} + placeholder="화면에 표시될 이름" + className="h-10 text-sm w-full" + /> +

+ 반복 테이블 헤더에 표시될 이름 +

+
+ + {/* 2. 컬럼명 */} +
+ + updateRepeaterColumn(index, { field: e.target.value })} + placeholder="데이터베이스 필드명" + className="h-10 text-sm w-full" + /> +

+ 저장 테이블의 실제 컬럼명 +

+
+ + {/* 3. 컬럼 타입 */} +
+ + +

+ 입력 필드 타입 +

+
+ + {/* 4. 매핑 설정 */} +
+ + +

+ {col.mapping?.type === "source" && "모달에서 선택한 항목의 값을 가져옴"} + {col.mapping?.type === "reference" && "다른 테이블의 값을 참조"} + {col.mapping?.type === "manual" && "사용자가 직접 입력"} +

+
+ + {/* 5. 소스 정보 */} +
+ + + {col.mapping?.type === "source" && ( +
+
+

소스 테이블에서 복사

+

+ 모달 검색에서 선택한 항목(소스 테이블)의 컬럼 값을 가져옴 +

+
+ + {/* 소스 테이블 선택 */} +
+ + +

+ 값을 가져올 소스 테이블 +

+
+ + {/* 소스 컬럼 선택 */} +
+ + updateRepeaterColumn(index, { + mapping: { + ...col.mapping, + type: "source", + sourceField: value + } as ColumnMapping + })} + /> +

+ 가져올 컬럼명 +

+
+
+ )} + + {col.mapping?.type === "reference" && ( +
+
+

외부 테이블 참조

+

+ 다른 테이블의 컬럼 값을 조인하여 가져옴 (조인 조건 필요) +

+
+ + {/* 참조 테이블 선택 */} +
+ + +

+ 참조할 외부 테이블 +

+
+ + {/* 참조 컬럼 선택 */} +
+ + updateRepeaterColumn(index, { + mapping: { + ...col.mapping, + type: "reference", + referenceField: value + } as ColumnMapping + })} + /> +

+ 가져올 컬럼명 +

+
+
+ )} + + {col.mapping?.type === "manual" && ( +
+

화면에서 입력 (수동)

+

+ 사용자가 반복 테이블에서 직접 입력 +

+
+ )} +
+
+
+ ))} +
+
+ )} +
+ + {/* 계산 규칙 (자동 계산) */} +
+
+

4. 계산 규칙 (자동 계산)

+

+ 반복 테이블 컬럼들을 조합하여 자동 계산 (예: 수량 x 단가 = 금액) +

+
+ +
+

중요 안내

+
    +
  • 결과 필드는 수정 불가 (자동 계산됨)
  • +
  • • 계산 공식에 사용된 컬럼은 수정 가능
  • +
  • • 예시: order_qty * unit_price = total_amount
  • +
+
+ + {localConfig.calculationRules && localConfig.calculationRules.length > 0 ? ( +
+ {localConfig.calculationRules.map((rule, index) => ( +
+
+
+
+ {index + 1} +
+

계산 규칙 {index + 1}

+
+ +
+ +
+ {/* 결과 필드 */} +
+ + +

+ 이 컬럼은 자동 계산되어 수정 불가능합니다 +

+
+ + {/* 계산 공식 */} +
+ + updateCalculationRule(index, { formula: e.target.value })} + placeholder="예: order_qty * unit_price" + className="h-10 text-sm w-full font-mono" + /> +

+ 사용 가능한 연산자: +, -, *, /, (), 필드명 사용 +

+
+ + {/* 현재 설정 표시 */} + {rule.result && rule.formula && ( +
+
+
+

현재 설정

+
+
+ + {localConfig.columns?.find((c) => c.field === rule.result)?.label || rule.result} + + = + + {rule.formula} + +
+
+ )} +
+
+ ))} +
+ ) : ( +
+
+ +
+

계산 규칙이 없습니다

+

+ 자동 계산이 필요한 컬럼이 있다면 규칙을 추가하세요 +

+ +
+ )} + + {localConfig.calculationRules && localConfig.calculationRules.length > 0 && ( -
-
- {(localConfig.sourceColumns || []).map((column, index) => ( -
- - -
- ))} -
-
- -
-
- - -
-
- {(localConfig.sourceSearchFields || []).map((field, index) => ( -
- - -
- ))} -
+ )}
+ {/* 설정 순서 안내 */}
-

💡 참고사항:

-
    -
  • 컬럼 설정은 별도 설정 패널에서 관리
  • -
  • 계산 규칙도 별도 설정 패널에서 관리
  • -
  • 여기서는 기본 설정만 구성
  • -
+

설정 순서:

+
    +
  1. 소스 테이블 (검색용): 모달에서 검색할 데이터 테이블
  2. +
  3. 저장 테이블 (저장용): 선택한 항목을 저장할 테이블
  4. +
  5. 반복 테이블 컬럼: 저장 테이블의 컬럼 선택 → 화면에 표시
  6. +
  7. 컬럼 매핑 설정: 각 컬럼의 초기값을 어디서 가져올지 설정
  8. +
  9. 계산 규칙: 자동 계산이 필요한 컬럼 설정 (선택사항)
  10. +
+ +
+

실제 사용 예시:

+
    +
  • 수주 등록 화면
  • +
  • - 소스 테이블: item_info (품목 검색)
  • +
  • - 저장 테이블: sales_order_mng (수주 저장)
  • +
  • - 컬럼: part_name, quantity, unit_price, amount...
  • +
  • - 매핑: item_name → part_name (소스에서 복사)
  • +
  • - 계산: amount = quantity * unit_price
  • +
+
); } - diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index 3cb310b5..047dddc8 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -9,6 +9,9 @@ export interface ModalRepeaterTableProps { sourceColumns: string[]; // 모달에 표시할 컬럼들 sourceSearchFields?: string[]; // 검색 가능한 필드들 + // 🆕 저장 대상 테이블 설정 + targetTable?: string; // 저장할 테이블 (예: "sales_order_mng") + // 모달 설정 modalTitle: string; // 모달 제목 (예: "품목 검색 및 선택") modalButtonText?: string; // 모달 열기 버튼 텍스트 (기본: "품목 검색") @@ -45,6 +48,41 @@ export interface RepeaterColumnConfig { required?: boolean; // 필수 입력 여부 defaultValue?: any; // 기본값 selectOptions?: { value: string; label: string }[]; // select일 때 옵션 + + // 🆕 컬럼 매핑 설정 + mapping?: ColumnMapping; // 이 컬럼의 데이터를 어디서 가져올지 설정 +} + +/** + * 컬럼 매핑 설정 + * 반복 테이블 컬럼이 어느 테이블의 어느 컬럼에서 값을 가져올지 정의 + */ +export interface ColumnMapping { + /** 매핑 타입 */ + type: "source" | "reference" | "manual"; + + /** 매핑 타입별 설정 */ + // type: "source" - 소스 테이블 (모달에서 선택한 항목)의 컬럼에서 가져오기 + sourceField?: string; // 소스 테이블의 컬럼명 (예: "item_name") + + // type: "reference" - 외부 테이블 참조 (조인) + referenceTable?: string; // 참조 테이블명 (예: "customer_item_mapping") + referenceField?: string; // 참조 테이블에서 가져올 컬럼 (예: "basic_price") + joinCondition?: JoinCondition[]; // 조인 조건 + + // type: "manual" - 사용자가 직접 입력 +} + +/** + * 조인 조건 정의 + */ +export interface JoinCondition { + /** 현재 테이블의 컬럼 (소스 테이블 또는 반복 테이블) */ + sourceField: string; + /** 참조 테이블의 컬럼 */ + targetField: string; + /** 비교 연산자 */ + operator?: "=" | "!=" | ">" | "<" | ">=" | "<="; } export interface CalculationRule { diff --git a/frontend/lib/registry/components/modal-repeater-table/useCalculation.ts b/frontend/lib/registry/components/modal-repeater-table/useCalculation.ts index 127d986e..1f6f69f2 100644 --- a/frontend/lib/registry/components/modal-repeater-table/useCalculation.ts +++ b/frontend/lib/registry/components/modal-repeater-table/useCalculation.ts @@ -16,15 +16,26 @@ export function useCalculation(calculationRules: CalculationRule[] = []) { for (const rule of calculationRules) { try { - // formula에서 필드명 추출 및 값으로 대체 + // formula에서 필드명 자동 추출 (영문자, 숫자, 언더스코어로 구성된 단어) let formula = rule.formula; + const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []; + + // 추출된 필드명들을 사용 (dependencies가 없으면 자동 추출 사용) + const dependencies = rule.dependencies && rule.dependencies.length > 0 + ? rule.dependencies + : fieldMatches; - for (const dep of rule.dependencies) { + // 필드명을 실제 값으로 대체 + for (const dep of dependencies) { + // 결과 필드는 제외 + if (dep === rule.result) continue; + const value = parseFloat(row[dep]) || 0; - formula = formula.replace(new RegExp(dep, "g"), value.toString()); + // 정확한 필드명만 대체 (단어 경계 사용) + formula = formula.replace(new RegExp(`\\b${dep}\\b`, "g"), value.toString()); } - // 계산 실행 (eval 대신 Function 사용) + // 계산 실행 (Function 사용) const result = new Function(`return ${formula}`)(); updatedRow[rule.result] = result; } catch (error) {