화면 복사기능 수정
This commit is contained in:
parent
5f026e88ab
commit
b74cb94191
|
|
@ -2101,55 +2101,109 @@ export class ScreenManagementService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 화면에 연결된 모달 화면들을 자동 감지
|
||||
* 버튼 컴포넌트의 popup 액션에서 targetScreenId를 추출
|
||||
* 화면에 연결된 모달/화면들을 재귀적으로 자동 감지
|
||||
* - 버튼 컴포넌트: popup/modal/edit/openModalWithData 액션의 targetScreenId
|
||||
* - 조건부 컨테이너: sections[].screenId (조건별 화면 할당)
|
||||
* - 중첩된 화면들도 모두 감지 (재귀)
|
||||
*/
|
||||
async detectLinkedModalScreens(
|
||||
screenId: number
|
||||
): Promise<{ screenId: number; screenName: string; screenCode: string }[]> {
|
||||
// 화면의 모든 레이아웃 조회
|
||||
const layouts = await query<any>(
|
||||
`SELECT layout_id, properties
|
||||
FROM screen_layouts
|
||||
WHERE screen_id = $1
|
||||
AND component_type = 'component'
|
||||
AND properties IS NOT NULL`,
|
||||
[screenId]
|
||||
);
|
||||
console.log(`\n🔍 [재귀 감지 시작] 화면 ID: ${screenId}`);
|
||||
|
||||
const allLinkedScreenIds = new Set<number>();
|
||||
const visited = new Set<number>(); // 무한 루프 방지
|
||||
const queue: number[] = [screenId]; // BFS 큐
|
||||
|
||||
const linkedScreenIds = new Set<number>();
|
||||
// BFS로 연결된 모든 화면 탐색
|
||||
while (queue.length > 0) {
|
||||
const currentScreenId = queue.shift()!;
|
||||
|
||||
// 이미 방문한 화면은 스킵 (순환 참조 방지)
|
||||
if (visited.has(currentScreenId)) {
|
||||
console.log(`⏭️ 이미 방문한 화면 스킵: ${currentScreenId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.add(currentScreenId);
|
||||
console.log(`\n📋 현재 탐색 중인 화면: ${currentScreenId} (깊이: ${visited.size})`);
|
||||
|
||||
// 각 레이아웃에서 버튼의 popup/modal/edit 액션 확인
|
||||
for (const layout of layouts) {
|
||||
try {
|
||||
const properties = layout.properties;
|
||||
|
||||
// 버튼 컴포넌트인지 확인
|
||||
if (properties?.componentType === "button" || properties?.componentType?.startsWith("button-")) {
|
||||
const action = properties?.componentConfig?.action;
|
||||
// 현재 화면의 모든 레이아웃 조회
|
||||
const layouts = await query<any>(
|
||||
`SELECT layout_id, properties
|
||||
FROM screen_layouts
|
||||
WHERE screen_id = $1
|
||||
AND component_type = 'component'
|
||||
AND properties IS NOT NULL`,
|
||||
[currentScreenId]
|
||||
);
|
||||
|
||||
console.log(` 📦 레이아웃 개수: ${layouts.length}`);
|
||||
|
||||
// 각 레이아웃에서 연결된 화면 ID 확인
|
||||
for (const layout of layouts) {
|
||||
try {
|
||||
const properties = layout.properties;
|
||||
|
||||
// popup, modal, edit 액션이고 targetScreenId가 있는 경우
|
||||
// edit 액션도 수정 폼 모달을 열기 때문에 포함
|
||||
if ((action?.type === "popup" || action?.type === "modal" || action?.type === "edit") && action?.targetScreenId) {
|
||||
const targetScreenId = parseInt(action.targetScreenId);
|
||||
if (!isNaN(targetScreenId)) {
|
||||
linkedScreenIds.add(targetScreenId);
|
||||
console.log(`🔗 연결된 모달 화면 발견: screenId=${targetScreenId}, actionType=${action.type} (레이아웃 ${layout.layout_id})`);
|
||||
// 1. 버튼 컴포넌트의 액션 확인
|
||||
if (properties?.componentType === "button" || properties?.componentType?.startsWith("button-")) {
|
||||
const action = properties?.componentConfig?.action;
|
||||
|
||||
const modalActionTypes = ["popup", "modal", "edit", "openModalWithData"];
|
||||
if (modalActionTypes.includes(action?.type) && action?.targetScreenId) {
|
||||
const targetScreenId = parseInt(action.targetScreenId);
|
||||
if (!isNaN(targetScreenId) && targetScreenId !== currentScreenId) {
|
||||
// 메인 화면이 아닌 경우에만 추가
|
||||
if (targetScreenId !== screenId) {
|
||||
allLinkedScreenIds.add(targetScreenId);
|
||||
}
|
||||
// 아직 방문하지 않은 화면이면 큐에 추가
|
||||
if (!visited.has(targetScreenId)) {
|
||||
queue.push(targetScreenId);
|
||||
console.log(` 🔗 [버튼] 연결된 화면 발견: ${targetScreenId} (action: ${action.type}) → 큐에 추가`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. conditional-container 컴포넌트의 sections 확인
|
||||
if (properties?.componentType === "conditional-container") {
|
||||
const sections = properties?.componentConfig?.sections || [];
|
||||
|
||||
for (const section of sections) {
|
||||
if (section?.screenId) {
|
||||
const sectionScreenId = parseInt(section.screenId);
|
||||
if (!isNaN(sectionScreenId) && sectionScreenId !== currentScreenId) {
|
||||
// 메인 화면이 아닌 경우에만 추가
|
||||
if (sectionScreenId !== screenId) {
|
||||
allLinkedScreenIds.add(sectionScreenId);
|
||||
}
|
||||
// 아직 방문하지 않은 화면이면 큐에 추가
|
||||
if (!visited.has(sectionScreenId)) {
|
||||
queue.push(sectionScreenId);
|
||||
console.log(` 🔗 [조건부컨테이너] 연결된 화면 발견: ${sectionScreenId} (condition: ${section.condition}) → 큐에 추가`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(` ⚠️ 레이아웃 ${layout.layout_id} 파싱 오류:`, error);
|
||||
}
|
||||
} catch (error) {
|
||||
// JSON 파싱 오류 등은 무시하고 계속 진행
|
||||
console.warn(`레이아웃 ${layout.layout_id} 파싱 오류:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ [재귀 감지 완료] 총 방문한 화면: ${visited.size}개, 연결된 화면: ${allLinkedScreenIds.size}개`);
|
||||
console.log(` 방문한 화면 ID: [${Array.from(visited).join(", ")}]`);
|
||||
console.log(` 연결된 화면 ID: [${Array.from(allLinkedScreenIds).join(", ")}]`);
|
||||
|
||||
// 감지된 화면 ID들의 정보 조회
|
||||
if (linkedScreenIds.size === 0) {
|
||||
if (allLinkedScreenIds.size === 0) {
|
||||
console.log(`ℹ️ 연결된 화면이 없습니다.`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const screenIds = Array.from(linkedScreenIds);
|
||||
const screenIds = Array.from(allLinkedScreenIds);
|
||||
const placeholders = screenIds.map((_, i) => `$${i + 1}`).join(", ");
|
||||
|
||||
const linkedScreens = await query<any>(
|
||||
|
|
@ -2161,6 +2215,11 @@ export class ScreenManagementService {
|
|||
screenIds
|
||||
);
|
||||
|
||||
console.log(`\n📋 최종 감지된 화면 목록:`);
|
||||
linkedScreens.forEach((s: any) => {
|
||||
console.log(` - ${s.screen_name} (ID: ${s.screen_id}, 코드: ${s.screen_code})`);
|
||||
});
|
||||
|
||||
return linkedScreens.map((s) => ({
|
||||
screenId: s.screen_id,
|
||||
screenName: s.screen_name,
|
||||
|
|
@ -2430,23 +2489,23 @@ export class ScreenManagementService {
|
|||
for (const layout of layouts) {
|
||||
try {
|
||||
const properties = layout.properties;
|
||||
let needsUpdate = false;
|
||||
|
||||
// 버튼 컴포넌트인지 확인
|
||||
// 1. 버튼 컴포넌트의 targetScreenId 업데이트
|
||||
if (
|
||||
properties?.componentType === "button" ||
|
||||
properties?.componentType?.startsWith("button-")
|
||||
) {
|
||||
const action = properties?.componentConfig?.action;
|
||||
|
||||
// targetScreenId가 있는 액션 (popup, modal, edit)
|
||||
// targetScreenId가 있는 액션 (popup, modal, edit, openModalWithData)
|
||||
const modalActionTypes = ["popup", "modal", "edit", "openModalWithData"];
|
||||
if (
|
||||
(action?.type === "popup" ||
|
||||
action?.type === "modal" ||
|
||||
action?.type === "edit") &&
|
||||
modalActionTypes.includes(action?.type) &&
|
||||
action?.targetScreenId
|
||||
) {
|
||||
const oldScreenId = parseInt(action.targetScreenId);
|
||||
console.log(`🔍 버튼 발견: layout ${layout.layout_id}, action=${action.type}, targetScreenId=${oldScreenId}`);
|
||||
console.log(`🔍 [버튼] 발견: layout ${layout.layout_id}, action=${action.type}, targetScreenId=${oldScreenId}`);
|
||||
|
||||
// 매핑에 있으면 업데이트
|
||||
if (screenIdMapping.has(oldScreenId)) {
|
||||
|
|
@ -2456,31 +2515,63 @@ export class ScreenManagementService {
|
|||
// properties 업데이트
|
||||
properties.componentConfig.action.targetScreenId =
|
||||
newScreenId.toString();
|
||||
needsUpdate = true;
|
||||
|
||||
// 데이터베이스 업데이트
|
||||
await query(
|
||||
`UPDATE screen_layouts
|
||||
SET properties = $1
|
||||
WHERE layout_id = $2`,
|
||||
[JSON.stringify(properties), layout.layout_id]
|
||||
);
|
||||
|
||||
updateCount++;
|
||||
console.log(
|
||||
`🔗 버튼 targetScreenId 업데이트: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id})`
|
||||
`🔗 [버튼] targetScreenId 업데이트 준비: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id})`
|
||||
);
|
||||
} else {
|
||||
console.log(`⚠️ 매핑 없음: ${oldScreenId} (업데이트 건너뜀)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. conditional-container 컴포넌트의 sections[].screenId 업데이트
|
||||
if (properties?.componentType === "conditional-container") {
|
||||
const sections = properties?.componentConfig?.sections || [];
|
||||
|
||||
for (const section of sections) {
|
||||
if (section?.screenId) {
|
||||
const oldScreenId = parseInt(section.screenId);
|
||||
console.log(`🔍 [조건부컨테이너] section 발견: layout ${layout.layout_id}, condition=${section.condition}, screenId=${oldScreenId}`);
|
||||
|
||||
// 매핑에 있으면 업데이트
|
||||
if (screenIdMapping.has(oldScreenId)) {
|
||||
const newScreenId = screenIdMapping.get(oldScreenId)!;
|
||||
console.log(`✅ 매핑 발견: ${oldScreenId} → ${newScreenId}`);
|
||||
|
||||
// section.screenId 업데이트
|
||||
section.screenId = newScreenId;
|
||||
needsUpdate = true;
|
||||
|
||||
console.log(
|
||||
`🔗 [조건부컨테이너] screenId 업데이트 준비: ${oldScreenId} → ${newScreenId} (layout ${layout.layout_id}, condition=${section.condition})`
|
||||
);
|
||||
} else {
|
||||
console.log(`⚠️ 매핑 없음: ${oldScreenId} (업데이트 건너뜀)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 업데이트가 필요한 경우 DB 저장
|
||||
if (needsUpdate) {
|
||||
await query(
|
||||
`UPDATE screen_layouts
|
||||
SET properties = $1
|
||||
WHERE layout_id = $2`,
|
||||
[JSON.stringify(properties), layout.layout_id]
|
||||
);
|
||||
updateCount++;
|
||||
console.log(`💾 레이아웃 ${layout.layout_id} 업데이트 완료`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`❌ 레이아웃 ${layout.layout_id} 업데이트 오류:`, error);
|
||||
// 개별 레이아웃 오류는 무시하고 계속 진행
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 총 ${updateCount}개 버튼의 targetScreenId 업데이트 완료`);
|
||||
console.log(`✅ 총 ${updateCount}개 레이아웃의 연결된 화면 ID 업데이트 완료 (버튼 + 조건부컨테이너)`);
|
||||
return updateCount;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,429 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Plus, X, Calculator } from "lucide-react";
|
||||
import { CalculationNode, CalculationStep, AdditionalFieldDefinition } from "./types";
|
||||
|
||||
interface CalculationBuilderProps {
|
||||
steps: CalculationStep[];
|
||||
availableFields: AdditionalFieldDefinition[];
|
||||
onChange: (steps: CalculationStep[]) => void;
|
||||
}
|
||||
|
||||
export const CalculationBuilder: React.FC<CalculationBuilderProps> = ({
|
||||
steps,
|
||||
availableFields,
|
||||
onChange,
|
||||
}) => {
|
||||
const [previewValues, setPreviewValues] = useState<Record<string, number>>({});
|
||||
|
||||
// 새 단계 추가
|
||||
const addStep = () => {
|
||||
const newStep: CalculationStep = {
|
||||
id: `step_${Date.now()}`,
|
||||
label: `단계 ${steps.length + 1}`,
|
||||
expression: {
|
||||
type: "field",
|
||||
fieldName: "",
|
||||
},
|
||||
};
|
||||
onChange([...steps, newStep]);
|
||||
};
|
||||
|
||||
// 단계 삭제
|
||||
const removeStep = (stepId: string) => {
|
||||
onChange(steps.filter((s) => s.id !== stepId));
|
||||
};
|
||||
|
||||
// 단계 업데이트
|
||||
const updateStep = (stepId: string, updates: Partial<CalculationStep>) => {
|
||||
onChange(
|
||||
steps.map((s) => (s.id === stepId ? { ...s, ...updates } : s))
|
||||
);
|
||||
};
|
||||
|
||||
// 간단한 표현식 렌더링
|
||||
const renderSimpleExpression = (step: CalculationStep) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 왼쪽 항 */}
|
||||
<Select
|
||||
value={step.expression.type === "field" ? step.expression.fieldName || "" : step.expression.type}
|
||||
onValueChange={(value) => {
|
||||
if (value === "previous") {
|
||||
updateStep(step.id, {
|
||||
expression: { type: "previous" },
|
||||
});
|
||||
} else if (value === "constant") {
|
||||
updateStep(step.id, {
|
||||
expression: { type: "constant", value: 0 },
|
||||
});
|
||||
} else {
|
||||
updateStep(step.id, {
|
||||
expression: { type: "field", fieldName: value },
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 flex-1 text-xs">
|
||||
<SelectValue placeholder="항목 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="previous">이전 결과</SelectItem>
|
||||
<SelectItem value="constant">상수값</SelectItem>
|
||||
{availableFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{step.expression.type === "constant" && (
|
||||
<Input
|
||||
type="number"
|
||||
value={step.expression.value || 0}
|
||||
onChange={(e) => {
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
...step.expression,
|
||||
value: parseFloat(e.target.value) || 0,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="h-8 w-24 text-xs"
|
||||
placeholder="값"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 연산 추가 버튼 */}
|
||||
{step.expression.type !== "operation" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const currentExpression = step.expression;
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
type: "operation",
|
||||
operator: "+",
|
||||
left: currentExpression,
|
||||
right: { type: "constant", value: 0 },
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
연산 추가
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 연산식 */}
|
||||
{step.expression.type === "operation" && (
|
||||
<div className="space-y-2 border-l-2 border-primary pl-3 ml-2">
|
||||
{renderOperationExpression(step)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 연산식 렌더링
|
||||
const renderOperationExpression = (step: CalculationStep) => {
|
||||
if (step.expression.type !== "operation") return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 왼쪽 항 */}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{renderNodeLabel(step.expression.left)}
|
||||
</div>
|
||||
|
||||
{/* 연산자 */}
|
||||
<Select
|
||||
value={step.expression.operator || "+"}
|
||||
onValueChange={(value) => {
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
...step.expression,
|
||||
operator: value as any,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-16 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="+">+</SelectItem>
|
||||
<SelectItem value="-">-</SelectItem>
|
||||
<SelectItem value="*">×</SelectItem>
|
||||
<SelectItem value="/">÷</SelectItem>
|
||||
<SelectItem value="%">%</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 오른쪽 항 */}
|
||||
<Select
|
||||
value={
|
||||
step.expression.right?.type === "field"
|
||||
? step.expression.right.fieldName || ""
|
||||
: step.expression.right?.type || ""
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
if (value === "constant") {
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
...step.expression,
|
||||
right: { type: "constant", value: 0 },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
...step.expression,
|
||||
right: { type: "field", fieldName: value },
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 flex-1 text-xs">
|
||||
<SelectValue placeholder="항목 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="constant">상수값</SelectItem>
|
||||
{availableFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{step.expression.right?.type === "constant" && (
|
||||
<Input
|
||||
type="number"
|
||||
value={step.expression.right.value || 0}
|
||||
onChange={(e) => {
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
...step.expression,
|
||||
right: {
|
||||
...step.expression.right!,
|
||||
value: parseFloat(e.target.value) || 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="h-7 w-24 text-xs"
|
||||
placeholder="값"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 노드 라벨 표시
|
||||
const renderNodeLabel = (node?: CalculationNode): string => {
|
||||
if (!node) return "";
|
||||
|
||||
switch (node.type) {
|
||||
case "field":
|
||||
const field = availableFields.find((f) => f.name === node.fieldName);
|
||||
return field?.label || node.fieldName || "필드";
|
||||
case "constant":
|
||||
return String(node.value || 0);
|
||||
case "previous":
|
||||
return "이전 결과";
|
||||
case "operation":
|
||||
const left = renderNodeLabel(node.left);
|
||||
const right = renderNodeLabel(node.right);
|
||||
const op = node.operator === "*" ? "×" : node.operator === "/" ? "÷" : node.operator;
|
||||
return `(${left} ${op} ${right})`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// 함수 적용 UI
|
||||
const renderFunctionStep = (step: CalculationStep) => {
|
||||
if (step.expression.type !== "function") return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={step.expression.functionName || "round"}
|
||||
onValueChange={(value) => {
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
type: "function",
|
||||
functionName: value as any,
|
||||
params: [{ type: "previous" }],
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-32 text-xs">
|
||||
<SelectValue placeholder="함수 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="round">반올림</SelectItem>
|
||||
<SelectItem value="floor">절삭</SelectItem>
|
||||
<SelectItem value="ceil">올림</SelectItem>
|
||||
<SelectItem value="abs">절댓값</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{(step.expression.functionName === "round" ||
|
||||
step.expression.functionName === "floor" ||
|
||||
step.expression.functionName === "ceil") && (
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground">단위:</span>
|
||||
<Select
|
||||
value={
|
||||
step.expression.params?.[1]?.type === "field"
|
||||
? step.expression.params[1].fieldName || ""
|
||||
: String(step.expression.params?.[1]?.value || "1")
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
const isField = availableFields.some((f) => f.name === value);
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
...step.expression,
|
||||
params: [
|
||||
{ type: "previous" },
|
||||
isField
|
||||
? { type: "field", fieldName: value }
|
||||
: { type: "constant", value: parseFloat(value) },
|
||||
],
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-32 text-xs">
|
||||
<SelectValue placeholder="단위" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
<SelectItem value="1000">1,000</SelectItem>
|
||||
{availableFields.map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-semibold">계산식 빌더</Label>
|
||||
<Button variant="outline" size="sm" onClick={addStep} className="h-7 text-xs">
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
단계 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{steps.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-6 text-center">
|
||||
<Calculator className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
계산 단계를 추가하여 계산식을 만드세요
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{steps.map((step, idx) => (
|
||||
<Card key={step.id} className="border-primary/30">
|
||||
<CardHeader className="pb-2 pt-3 px-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xs font-medium">
|
||||
{step.label || `단계 ${idx + 1}`}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
value={step.label}
|
||||
onChange={(e) => updateStep(step.id, { label: e.target.value })}
|
||||
placeholder={`단계 ${idx + 1}`}
|
||||
className="h-6 w-24 text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeStep(step.id)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3 px-3">
|
||||
{step.expression.type === "function"
|
||||
? renderFunctionStep(step)
|
||||
: renderSimpleExpression(step)}
|
||||
|
||||
{/* 함수 적용 버튼 */}
|
||||
{step.expression.type !== "function" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
updateStep(step.id, {
|
||||
expression: {
|
||||
type: "function",
|
||||
functionName: "round",
|
||||
params: [{ type: "previous" }, { type: "constant", value: 1 }],
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="h-7 text-xs mt-2"
|
||||
>
|
||||
함수 적용
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 미리보기 */}
|
||||
{steps.length > 0 && (
|
||||
<Card className="bg-muted/30">
|
||||
<CardContent className="py-3 px-3">
|
||||
<div className="text-xs">
|
||||
<span className="font-semibold">계산식:</span>
|
||||
<div className="mt-1 font-mono text-muted-foreground">
|
||||
{steps.map((step, idx) => (
|
||||
<div key={step.id}>
|
||||
{idx + 1}. {renderNodeLabel(step.expression)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -258,10 +258,32 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
// 🆕 저장 요청 시에만 데이터 전달 (이벤트 리스너 방식)
|
||||
useEffect(() => {
|
||||
const handleSaveRequest = () => {
|
||||
// component.id를 문자열로 안전하게 변환
|
||||
const componentKey = String(component.id || "selected_items");
|
||||
|
||||
console.log("🔔 [SelectedItemsDetailInput] beforeFormSave 이벤트 수신!", {
|
||||
itemsCount: items.length,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
componentId: component.id,
|
||||
componentIdType: typeof component.id,
|
||||
componentKey,
|
||||
});
|
||||
|
||||
if (items.length > 0 && onFormDataChange) {
|
||||
const dataToSave = { [component.id || "selected_items"]: items };
|
||||
console.log("📝 [SelectedItemsDetailInput] 저장 요청 시 데이터 전달:", dataToSave);
|
||||
const dataToSave = { [componentKey]: items };
|
||||
console.log("📝 [SelectedItemsDetailInput] 저장 요청 시 데이터 전달:", {
|
||||
key: componentKey,
|
||||
itemsCount: items.length,
|
||||
fullData: dataToSave,
|
||||
firstItem: items[0],
|
||||
});
|
||||
onFormDataChange(dataToSave);
|
||||
} else {
|
||||
console.warn("⚠️ [SelectedItemsDetailInput] 저장 데이터 전달 실패:", {
|
||||
hasItems: items.length > 0,
|
||||
hasCallback: !!onFormDataChange,
|
||||
itemsLength: items.length,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -342,6 +364,14 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
|
||||
// 🆕 그룹별 필드 변경 핸들러: itemId + groupId + entryId + fieldName
|
||||
const handleFieldChange = useCallback((itemId: string, groupId: string, entryId: string, fieldName: string, value: any) => {
|
||||
console.log("📝 [handleFieldChange] 필드 값 변경:", {
|
||||
itemId,
|
||||
groupId,
|
||||
entryId,
|
||||
fieldName,
|
||||
value,
|
||||
});
|
||||
|
||||
setItems((prevItems) => {
|
||||
return prevItems.map((item) => {
|
||||
if (item.id !== itemId) return item;
|
||||
|
|
@ -357,6 +387,12 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
|||
[fieldName]: value,
|
||||
};
|
||||
|
||||
console.log("✅ [handleFieldChange] Entry 업데이트:", {
|
||||
beforeKeys: Object.keys(updatedEntries[existingEntryIndex]),
|
||||
afterKeys: Object.keys(updatedEntry),
|
||||
updatedEntry,
|
||||
});
|
||||
|
||||
// 🆕 가격 관련 필드가 변경되면 자동 계산
|
||||
if (componentConfig.autoCalculation) {
|
||||
const { inputFields, targetField } = componentConfig.autoCalculation;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSecondLevelMenus, getCategoryColumns, getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||
import { CalculationBuilder } from "./CalculationBuilder";
|
||||
|
||||
export interface SelectedItemsDetailInputConfigPanelProps {
|
||||
config: SelectedItemsDetailInputConfig;
|
||||
|
|
@ -1127,132 +1128,223 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
<div className="space-y-3 rounded-lg border p-3 sm:p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-semibold sm:text-sm">자동 계산 설정</Label>
|
||||
<Checkbox
|
||||
id="enable-auto-calc"
|
||||
checked={!!config.autoCalculation}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleChange("autoCalculation", {
|
||||
targetField: "calculated_price",
|
||||
inputFields: {
|
||||
basePrice: "current_unit_price",
|
||||
discountType: "discount_type",
|
||||
discountValue: "discount_value",
|
||||
roundingType: "rounding_type",
|
||||
roundingUnit: "rounding_unit_value",
|
||||
},
|
||||
calculationType: "price",
|
||||
});
|
||||
} else {
|
||||
handleChange("autoCalculation", undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
id="enable-auto-calc"
|
||||
checked={!!config.autoCalculation}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
handleChange("autoCalculation", {
|
||||
targetField: "",
|
||||
mode: "template",
|
||||
inputFields: {
|
||||
basePrice: "",
|
||||
discountType: "",
|
||||
discountValue: "",
|
||||
roundingType: "",
|
||||
roundingUnit: "",
|
||||
},
|
||||
calculationType: "price",
|
||||
valueMapping: {},
|
||||
calculationSteps: [],
|
||||
});
|
||||
} else {
|
||||
handleChange("autoCalculation", undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.autoCalculation && (
|
||||
<div className="space-y-2 border-t pt-2">
|
||||
{/* 계산 모드 선택 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] sm:text-xs">계산 결과 필드</Label>
|
||||
<Input
|
||||
value={config.autoCalculation.targetField || ""}
|
||||
onChange={(e) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
targetField: e.target.value,
|
||||
})}
|
||||
placeholder="calculated_price"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<Label className="text-[10px] sm:text-xs">계산 방식</Label>
|
||||
<Select
|
||||
value={config.autoCalculation.mode || "template"}
|
||||
onValueChange={(value: "template" | "custom") => {
|
||||
handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
mode: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="template">템플릿 (가격 계산)</SelectItem>
|
||||
<SelectItem value="custom">커스텀 (계산식 빌더)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] sm:text-xs">기준 단가 필드</Label>
|
||||
<Input
|
||||
value={config.autoCalculation.inputFields?.basePrice || ""}
|
||||
onChange={(e) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
basePrice: e.target.value,
|
||||
},
|
||||
})}
|
||||
placeholder="current_unit_price"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
{/* 템플릿 모드 */}
|
||||
{config.autoCalculation.mode === "template" && (
|
||||
<>
|
||||
{/* 계산 필드 선택 */}
|
||||
<div className="space-y-2 border-t pt-2 mt-2">
|
||||
<Label className="text-[10px] font-semibold sm:text-xs">계산에 사용할 필드 선택</Label>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* 계산 결과 필드 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] sm:text-[10px]">계산 결과 필드</Label>
|
||||
<Select
|
||||
value={config.autoCalculation.targetField || ""}
|
||||
onValueChange={(value) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
targetField: value,
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.additionalFields || []).map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] sm:text-xs">할인 방식</Label>
|
||||
<Input
|
||||
value={config.autoCalculation.inputFields?.discountType || ""}
|
||||
onChange={(e) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
discountType: e.target.value,
|
||||
},
|
||||
})}
|
||||
placeholder="discount_type"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
{/* 기준 단가 필드 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] sm:text-[10px]">기준 단가 필드</Label>
|
||||
<Select
|
||||
value={config.autoCalculation.inputFields?.basePrice || ""}
|
||||
onValueChange={(value) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
basePrice: value,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.additionalFields || []).map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] sm:text-xs">할인값</Label>
|
||||
<Input
|
||||
value={config.autoCalculation.inputFields?.discountValue || ""}
|
||||
onChange={(e) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
discountValue: e.target.value,
|
||||
},
|
||||
})}
|
||||
placeholder="discount_value"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* 할인 방식 필드 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] sm:text-[10px]">할인 방식</Label>
|
||||
<Select
|
||||
value={config.autoCalculation.inputFields?.discountType || ""}
|
||||
onValueChange={(value) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
discountType: value,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.additionalFields || []).map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 할인값 필드 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] sm:text-[10px]">할인값</Label>
|
||||
<Select
|
||||
value={config.autoCalculation.inputFields?.discountValue || ""}
|
||||
onValueChange={(value) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
discountValue: value,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.additionalFields || []).map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* 반올림 방식 필드 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] sm:text-[10px]">반올림 방식</Label>
|
||||
<Select
|
||||
value={config.autoCalculation.inputFields?.roundingType || ""}
|
||||
onValueChange={(value) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
roundingType: value,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.additionalFields || []).map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 반올림 단위 필드 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[9px] sm:text-[10px]">반올림 단위</Label>
|
||||
<Select
|
||||
value={config.autoCalculation.inputFields?.roundingUnit || ""}
|
||||
onValueChange={(value) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
roundingUnit: value,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.additionalFields || []).map((field) => (
|
||||
<SelectItem key={field.name} value={field.name}>
|
||||
{field.label || field.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] sm:text-xs">반올림 방식</Label>
|
||||
<Input
|
||||
value={config.autoCalculation.inputFields?.roundingType || ""}
|
||||
onChange={(e) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
roundingType: e.target.value,
|
||||
},
|
||||
})}
|
||||
placeholder="rounding_type"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] sm:text-xs">반올림 단위</Label>
|
||||
<Input
|
||||
value={config.autoCalculation.inputFields?.roundingUnit || ""}
|
||||
onChange={(e) => handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
inputFields: {
|
||||
...config.autoCalculation.inputFields,
|
||||
roundingUnit: e.target.value,
|
||||
},
|
||||
})}
|
||||
placeholder="rounding_unit_value"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[9px] text-amber-600 sm:text-[10px]">
|
||||
💡 위 필드명들이 추가 입력 필드에 있어야 자동 계산이 작동합니다
|
||||
</p>
|
||||
|
||||
{/* 카테고리 값 매핑 */}
|
||||
<div className="space-y-3 border-t pt-3 mt-3">
|
||||
<Label className="text-[10px] font-semibold sm:text-xs">카테고리 값 매핑</Label>
|
||||
|
|
@ -1591,6 +1683,24 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
💡 1단계: 메뉴 선택 → 2단계: 카테고리 선택 → 3단계: 값 매핑
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 커스텀 모드 (계산식 빌더) */}
|
||||
{config.autoCalculation.mode === "custom" && (
|
||||
<div className="space-y-2 border-t pt-2 mt-2">
|
||||
<CalculationBuilder
|
||||
steps={config.autoCalculation.calculationSteps || []}
|
||||
availableFields={config.additionalFields || []}
|
||||
onChange={(steps) => {
|
||||
handleChange("autoCalculation", {
|
||||
...config.autoCalculation,
|
||||
calculationSteps: steps,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,37 +58,67 @@ export interface FieldGroup {
|
|||
displayItems?: DisplayItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 계산식 노드 타입
|
||||
*/
|
||||
export type CalculationNodeType = "field" | "constant" | "operation" | "function" | "previous";
|
||||
|
||||
export interface CalculationNode {
|
||||
type: CalculationNodeType;
|
||||
// field: 필드명
|
||||
fieldName?: string;
|
||||
// constant: 상수값
|
||||
value?: number;
|
||||
// operation: 연산
|
||||
operator?: "+" | "-" | "*" | "/" | "%" | "^";
|
||||
left?: CalculationNode;
|
||||
right?: CalculationNode;
|
||||
// function: 함수
|
||||
functionName?: "round" | "floor" | "ceil" | "abs" | "max" | "min";
|
||||
params?: CalculationNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 계산 단계
|
||||
*/
|
||||
export interface CalculationStep {
|
||||
id: string;
|
||||
label: string;
|
||||
expression: CalculationNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 자동 계산 설정
|
||||
*/
|
||||
export interface AutoCalculationConfig {
|
||||
/** 계산 대상 필드명 (예: calculated_price) */
|
||||
targetField: string;
|
||||
/** 계산에 사용할 입력 필드들 */
|
||||
inputFields: {
|
||||
basePrice: string; // 기본 단가 필드명
|
||||
discountType: string; // 할인 방식 필드명
|
||||
discountValue: string; // 할인값 필드명
|
||||
roundingType: string; // 반올림 방식 필드명
|
||||
roundingUnit: string; // 반올림 단위 필드명
|
||||
/** 🆕 계산 방식 */
|
||||
mode: "template" | "custom";
|
||||
|
||||
/** 템플릿 모드 (기존 방식) */
|
||||
inputFields?: {
|
||||
basePrice: string;
|
||||
discountType: string;
|
||||
discountValue: string;
|
||||
roundingType: string;
|
||||
roundingUnit: string;
|
||||
};
|
||||
/** 계산 함수 타입 */
|
||||
calculationType: "price" | "custom";
|
||||
/** 🆕 카테고리 값 → 연산 매핑 */
|
||||
calculationType?: "price" | "custom";
|
||||
valueMapping?: {
|
||||
/** 할인 방식 매핑 */
|
||||
discountType?: {
|
||||
[valueCode: string]: "none" | "rate" | "amount"; // 예: { "CATEGORY_544740": "rate" }
|
||||
[valueCode: string]: "none" | "rate" | "amount";
|
||||
};
|
||||
/** 반올림 방식 매핑 */
|
||||
roundingType?: {
|
||||
[valueCode: string]: "none" | "round" | "floor" | "ceil";
|
||||
};
|
||||
/** 반올림 단위 매핑 (숫자로 변환) */
|
||||
roundingUnit?: {
|
||||
[valueCode: string]: number; // 예: { "10": 10, "100": 100 }
|
||||
[valueCode: string]: number;
|
||||
};
|
||||
};
|
||||
|
||||
/** 커스텀 모드 (계산식 빌더) */
|
||||
calculationSteps?: CalculationStep[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -230,6 +230,16 @@ export class ButtonActionExecutor {
|
|||
|
||||
const selectedItemsKeys = Object.keys(context.formData).filter(key => {
|
||||
const value = context.formData[key];
|
||||
console.log(`🔍 [handleSave] 필터링 체크 - ${key}:`, {
|
||||
isArray: Array.isArray(value),
|
||||
length: Array.isArray(value) ? value.length : 0,
|
||||
firstItem: Array.isArray(value) && value.length > 0 ? {
|
||||
keys: Object.keys(value[0] || {}),
|
||||
hasOriginalData: !!value[0]?.originalData,
|
||||
hasFieldGroups: !!value[0]?.fieldGroups,
|
||||
actualValue: value[0],
|
||||
} : null
|
||||
});
|
||||
return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups;
|
||||
});
|
||||
|
||||
|
|
@ -238,6 +248,7 @@ export class ButtonActionExecutor {
|
|||
return await this.handleBatchSave(config, context, selectedItemsKeys);
|
||||
} else {
|
||||
console.log("⚠️ [handleSave] SelectedItemsDetailInput 데이터 감지 실패 - 일반 저장 진행");
|
||||
console.log("⚠️ [handleSave] formData 전체 내용:", context.formData);
|
||||
}
|
||||
|
||||
// 폼 유효성 검사
|
||||
|
|
|
|||
Loading…
Reference in New Issue