From 417e1d297b52176aa7060af6bb22194deef71a05 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 31 Dec 2025 13:53:30 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8F=BC=20=EC=A1=B0=EA=B1=B4=EB=B3=84=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=8B=9D=20=EC=84=A4=EC=A0=95=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TableSectionRenderer.tsx | 58 ++- .../modals/TableSectionSettingsModal.tsx | 474 ++++++++++++++++-- .../components/universal-form-modal/types.ts | 24 + 3 files changed, 505 insertions(+), 51 deletions(-) diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index a1c0bd76..1242e1d2 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -16,7 +16,13 @@ import { ItemSelectionModal } from "../modal-repeater-table/ItemSelectionModal"; import { RepeaterColumnConfig, CalculationRule } from "../modal-repeater-table/types"; // 타입 정의 -import { TableSectionConfig, TableColumnConfig, TableJoinCondition, FormDataState } from "./types"; +import { + TableSectionConfig, + TableColumnConfig, + TableJoinCondition, + FormDataState, + TableCalculationRule, +} from "./types"; interface TableSectionRendererProps { sectionId: string; @@ -811,39 +817,69 @@ export function TableSectionRenderer({ }); }, [tableConfig.columns, dynamicSelectOptionsMap]); - // 계산 규칙 변환 - const calculationRules: CalculationRule[] = (tableConfig.calculations || []).map(convertToCalculationRule); + // 원본 계산 규칙 (조건부 계산 포함) + const originalCalculationRules: TableCalculationRule[] = useMemo( + () => tableConfig.calculations || [], + [tableConfig.calculations], + ); - // 계산 로직 + // 기본 계산 규칙 변환 (RepeaterTable용 - 조건부 계산이 없는 경우에 사용) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const calculationRules: CalculationRule[] = originalCalculationRules.map(convertToCalculationRule); + + // 조건부 계산 로직: 행의 조건 필드 값에 따라 적절한 계산식 선택 + const getFormulaForRow = useCallback((rule: TableCalculationRule, row: Record): string => { + // 조건부 계산이 활성화된 경우 + if (rule.conditionalCalculation?.enabled && rule.conditionalCalculation.conditionField) { + const conditionValue = row[rule.conditionalCalculation.conditionField]; + // 조건값과 일치하는 규칙 찾기 + const matchedRule = rule.conditionalCalculation.rules?.find((r) => r.conditionValue === conditionValue); + if (matchedRule) { + return matchedRule.formula; + } + // 일치하는 규칙이 없으면 기본 계산식 사용 + if (rule.conditionalCalculation.defaultFormula) { + return rule.conditionalCalculation.defaultFormula; + } + } + // 조건부 계산이 비활성화되었거나 기본값이 없으면 원래 계산식 사용 + return rule.formula; + }, []); + + // 계산 로직 (조건부 계산 지원) const calculateRow = useCallback( (row: any): any => { - if (calculationRules.length === 0) return row; + if (originalCalculationRules.length === 0) return row; const updatedRow = { ...row }; - for (const rule of calculationRules) { + for (const rule of originalCalculationRules) { try { - let formula = rule.formula; + // 조건부 계산에 따라 적절한 계산식 선택 + let formula = getFormulaForRow(rule, row); + + if (!formula) continue; + const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []; const dependencies = rule.dependencies.length > 0 ? rule.dependencies : fieldMatches; for (const dep of dependencies) { - if (dep === rule.result) continue; + if (dep === rule.resultField) continue; const value = parseFloat(row[dep]) || 0; formula = formula.replace(new RegExp(`\\b${dep}\\b`, "g"), value.toString()); } const result = new Function(`return ${formula}`)(); - updatedRow[rule.result] = result; + updatedRow[rule.resultField] = result; } catch (error) { console.error(`계산 오류 (${rule.formula}):`, error); - updatedRow[rule.result] = 0; + updatedRow[rule.resultField] = 0; } } return updatedRow; }, - [calculationRules], + [originalCalculationRules, getFormulaForRow], ); const calculateAll = useCallback( diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx index d82db59b..037707ca 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx @@ -24,6 +24,8 @@ import { TablePreFilter, TableModalFilter, TableCalculationRule, + ConditionalCalculationRule, + ConditionalCalculationConfig, LookupOption, LookupCondition, ConditionalTableOption, @@ -52,6 +54,429 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); +// 계산 규칙 편집 컴포넌트 (조건부 계산 지원) +interface CalculationRuleEditorProps { + calc: TableCalculationRule; + index: number; + columns: TableColumnConfig[]; + sourceTableName?: string; // 소스 테이블명 추가 + onUpdate: (updates: Partial) => void; + onRemove: () => void; +} + +const CalculationRuleEditor: React.FC = ({ + calc, + index, + columns, + sourceTableName, + onUpdate, + onRemove, +}) => { + const [categoryOptions, setCategoryOptions] = useState<{ value: string; label: string }[]>([]); + const [loadingOptions, setLoadingOptions] = useState(false); + const [categoryColumns, setCategoryColumns] = useState>({}); + + // 조건부 계산 활성화 여부 + const isConditionalEnabled = calc.conditionalCalculation?.enabled ?? false; + + // 소스 테이블의 카테고리 컬럼 정보 로드 + useEffect(() => { + const loadCategoryColumns = async () => { + console.log("[CalculationRuleEditor] sourceTableName:", sourceTableName); + + if (!sourceTableName) { + setCategoryColumns({}); + return; + } + + try { + const { getCategoryColumns } = await import("@/lib/api/tableCategoryValue"); + const result = await getCategoryColumns(sourceTableName); + console.log("[CalculationRuleEditor] getCategoryColumns 결과:", result); + + if (result && result.success && Array.isArray(result.data)) { + const categoryMap: Record = {}; + result.data.forEach((col: any) => { + // API 응답은 camelCase (columnName) + const colName = col.columnName || col.column_name; + if (colName) { + categoryMap[colName] = true; + } + }); + console.log("[CalculationRuleEditor] categoryMap:", categoryMap); + setCategoryColumns(categoryMap); + } + } catch (error) { + console.error("카테고리 컬럼 조회 실패:", error); + } + }; + + loadCategoryColumns(); + }, [sourceTableName]); + + // 조건 필드가 선택되었을 때 옵션 로드 (테이블 타입 관리의 카테고리 기준) + useEffect(() => { + const loadConditionOptions = async () => { + if (!isConditionalEnabled || !calc.conditionalCalculation?.conditionField) { + setCategoryOptions([]); + return; + } + + const conditionField = calc.conditionalCalculation.conditionField; + + // 소스 필드(sourceField)가 있으면 해당 필드명 사용, 없으면 field명 사용 + const selectedColumn = columns.find((col) => col.field === conditionField); + const actualFieldName = selectedColumn?.sourceField || conditionField; + + console.log("[loadConditionOptions] 조건 필드:", { + conditionField, + actualFieldName, + sourceTableName, + categoryColumnsKeys: Object.keys(categoryColumns), + isCategoryColumn: categoryColumns[actualFieldName], + }); + + // 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인 + if (sourceTableName && categoryColumns[actualFieldName]) { + try { + setLoadingOptions(true); + const { getCategoryValues } = await import("@/lib/api/tableCategoryValue"); + console.log("[loadConditionOptions] getCategoryValues 호출:", sourceTableName, actualFieldName); + const result = await getCategoryValues(sourceTableName, actualFieldName, false); + console.log("[loadConditionOptions] getCategoryValues 결과:", result); + if (result && result.success && Array.isArray(result.data)) { + const options = result.data.map((item: any) => ({ + // API 응답은 camelCase (valueCode, valueLabel) + value: item.valueCode || item.value_code || item.value, + label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value, + })); + console.log("[loadConditionOptions] 매핑된 옵션:", options); + setCategoryOptions(options); + } else { + setCategoryOptions([]); + } + } catch (error) { + console.error("카테고리 값 로드 실패:", error); + setCategoryOptions([]); + } finally { + setLoadingOptions(false); + } + return; + } + + // 카테고리 키가 직접 설정된 경우 (저장된 값) + const categoryKey = calc.conditionalCalculation?.conditionFieldCategoryKey; + if (categoryKey) { + try { + setLoadingOptions(true); + const [tableName, columnName] = categoryKey.split("."); + if (tableName && columnName) { + const { getCategoryValues } = await import("@/lib/api/tableCategoryValue"); + const result = await getCategoryValues(tableName, columnName, false); + if (result && result.success && Array.isArray(result.data)) { + setCategoryOptions( + result.data.map((item: any) => ({ + // API 응답은 camelCase (valueCode, valueLabel) + value: item.valueCode || item.value_code || item.value, + label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value, + })) + ); + } + } + } catch (error) { + console.error("카테고리 옵션 로드 실패:", error); + } finally { + setLoadingOptions(false); + } + return; + } + + // 그 외 타입은 옵션 없음 (직접 입력) + setCategoryOptions([]); + }; + + loadConditionOptions(); + }, [isConditionalEnabled, calc.conditionalCalculation?.conditionField, calc.conditionalCalculation?.conditionFieldCategoryKey, columns, sourceTableName, categoryColumns]); + + // 조건부 계산 토글 + const toggleConditionalCalculation = (enabled: boolean) => { + onUpdate({ + conditionalCalculation: enabled + ? { + enabled: true, + conditionField: "", + conditionFieldType: "static", + rules: [], + defaultFormula: calc.formula || "", + } + : undefined, + }); + }; + + // 조건 필드 변경 + const updateConditionField = (field: string) => { + const selectedColumn = columns.find((col) => col.field === field); + const actualFieldName = selectedColumn?.sourceField || field; + + // 컬럼의 타입과 옵션 확인 (테이블 타입 관리의 카테고리 기준) + let conditionFieldType: "static" | "code" | "table" = "static"; + let conditionFieldCategoryKey = ""; + + // 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인 + if (sourceTableName && categoryColumns[actualFieldName]) { + conditionFieldType = "code"; + conditionFieldCategoryKey = `${sourceTableName}.${actualFieldName}`; + } + + onUpdate({ + conditionalCalculation: { + ...calc.conditionalCalculation!, + conditionField: field, + conditionFieldType, + conditionFieldCategoryKey, + rules: [], // 필드 변경 시 규칙 초기화 + }, + }); + }; + + // 조건 규칙 추가 + const addConditionRule = () => { + const newRule: ConditionalCalculationRule = { + conditionValue: "", + formula: calc.formula || "", + }; + onUpdate({ + conditionalCalculation: { + ...calc.conditionalCalculation!, + rules: [...(calc.conditionalCalculation?.rules || []), newRule], + }, + }); + }; + + // 조건 규칙 업데이트 + const updateConditionRule = (ruleIndex: number, updates: Partial) => { + const newRules = [...(calc.conditionalCalculation?.rules || [])]; + newRules[ruleIndex] = { ...newRules[ruleIndex], ...updates }; + onUpdate({ + conditionalCalculation: { + ...calc.conditionalCalculation!, + rules: newRules, + }, + }); + }; + + // 조건 규칙 삭제 + const removeConditionRule = (ruleIndex: number) => { + onUpdate({ + conditionalCalculation: { + ...calc.conditionalCalculation!, + rules: (calc.conditionalCalculation?.rules || []).filter((_, i) => i !== ruleIndex), + }, + }); + }; + + // 기본 계산식 업데이트 + const updateDefaultFormula = (formula: string) => { + onUpdate({ + conditionalCalculation: { + ...calc.conditionalCalculation!, + defaultFormula: formula, + }, + }); + }; + + // 조건 필드로 사용 가능한 컬럼 (모든 컬럼) + const availableColumns = columns.filter((col) => col.field); + + return ( +
+ {/* 기본 계산 규칙 */} +
+ + = + onUpdate({ formula: e.target.value })} + placeholder="수식 (예: qty * unit_price)" + className="h-8 text-xs flex-1" + disabled={isConditionalEnabled} + /> + +
+ + {/* 조건부 계산 토글 */} +
+ + + {availableColumns.length === 0 && !isConditionalEnabled && ( + + (컬럼 설정에서 먼저 컬럼을 추가하세요) + + )} +
+ + {/* 조건부 계산 설정 */} + {isConditionalEnabled && ( +
+ {/* 조건 필드 선택 */} +
+ + +
+ + {/* 조건별 계산식 목록 */} + {calc.conditionalCalculation?.conditionField && ( +
+
+ + +
+ + {(calc.conditionalCalculation?.rules || []).map((rule, ruleIndex) => ( +
+ {/* 조건값 선택 */} + {categoryOptions.length > 0 ? ( + + ) : ( + + updateConditionRule(ruleIndex, { conditionValue: e.target.value }) + } + placeholder="조건값" + className="h-7 text-xs w-[120px]" + /> + )} + + + updateConditionRule(ruleIndex, { formula: e.target.value }) + } + placeholder="계산식" + className="h-7 text-xs flex-1" + /> + +
+ ))} + + {/* 기본 계산식 */} +
+ + (기본값) + + + updateDefaultFormula(e.target.value)} + placeholder="기본 계산식 (조건 미해당 시)" + className="h-7 text-xs flex-1" + /> +
+
+ )} + + {loadingOptions && ( +

옵션 로딩 중...

+ )} +
+ )} +
+ ); +}; + // 옵션 소스 설정 컴포넌트 (검색 가능한 Combobox) interface OptionSourceConfigProps { optionSource: { @@ -3034,46 +3459,15 @@ export function TableSectionSettingsModal({ {(tableConfig.calculations || []).map((calc, index) => ( -
- - = - updateCalculation(index, { formula: e.target.value })} - placeholder="수식 (예: quantity * unit_price)" - className="h-8 text-xs flex-1" - /> - -
+ updateCalculation(index, updates)} + onRemove={() => removeCalculation(index)} + /> ))} diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index a07feed6..3d8cac6c 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -604,6 +604,27 @@ export interface ColumnModeConfig { valueMapping: ValueMappingConfig; // 이 모드의 값 매핑 } +/** + * 조건부 계산 규칙 + * 특정 필드 값에 따라 다른 계산식 적용 + */ +export interface ConditionalCalculationRule { + conditionValue: string; // 조건 값 (예: "국내", "해외") + formula: string; // 해당 조건일 때 사용할 계산식 +} + +/** + * 조건부 계산 설정 + */ +export interface ConditionalCalculationConfig { + enabled: boolean; // 조건부 계산 활성화 여부 + conditionField: string; // 조건 기준 필드 (예: "sales_type") + conditionFieldType?: "static" | "code" | "table"; // 조건 필드의 옵션 타입 + conditionFieldCategoryKey?: string; // 카테고리 키 (예: "sales_order_mng.sales_type") + rules: ConditionalCalculationRule[]; // 조건별 계산 규칙 + defaultFormula?: string; // 조건에 해당하지 않을 때 기본 계산식 +} + /** * 테이블 계산 규칙 * 다른 컬럼 값을 기반으로 자동 계산 @@ -612,6 +633,9 @@ export interface TableCalculationRule { resultField: string; // 결과를 저장할 필드 formula: string; // 계산 공식 (예: "quantity * unit_price") dependencies: string[]; // 의존하는 필드들 + + // 조건부 계산 (선택사항) + conditionalCalculation?: ConditionalCalculationConfig; } // 다중 행 저장 설정