|
추가된 항목이 없습니다
@@ -672,7 +675,7 @@ export function RepeaterTable({
/>
|
{/* 데이터 컬럼들 */}
- {columns.map((col) => (
+ {visibleColumns.map((col) => (
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..b01d6b09 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,414 @@ 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 () => {
+ if (!sourceTableName) {
+ setCategoryColumns({});
+ return;
+ }
+
+ try {
+ const { getCategoryColumns } = await import("@/lib/api/tableCategoryValue");
+ const result = await getCategoryColumns(sourceTableName);
+
+ 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;
+ }
+ });
+ 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;
+
+ // 소스 테이블에서 해당 컬럼이 카테고리 타입인지 확인
+ if (sourceTableName && categoryColumns[actualFieldName]) {
+ try {
+ setLoadingOptions(true);
+ const { getCategoryValues } = await import("@/lib/api/tableCategoryValue");
+ const result = await getCategoryValues(sourceTableName, actualFieldName, false);
+ 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,
+ }));
+ 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: {
@@ -669,6 +1079,14 @@ function ColumnSettingItem({
/>
필수
+
|