[agent-pipeline] pipe-20260329112709-ncml round-2

This commit is contained in:
DDD1542
2026-03-29 23:12:23 +09:00
parent a5f4cd5ba9
commit 1405a19d1c
13 changed files with 96 additions and 93 deletions
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { ComponentRendererProps } from "../../types";
import { ComponentRendererProps } from "@/types/component";
import { TextDisplayConfig } from "./types";
import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
@@ -116,8 +116,8 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// 🆕 채번 규칙이 설정되어 있으면 항상 _numberingRuleId를 formData에 설정
// (값 생성 성공 여부와 관계없이, 저장 시점에 allocateCode를 호출하기 위함)
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
const ruleId = testAutoGeneration.options.numberingRuleId;
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numbering_rule_id) {
const ruleId = testAutoGeneration.options.numbering_rule_id;
if (ruleId && ruleId !== "undefined" && ruleId !== "null" && ruleId !== "") {
const ruleIdKey = `${component.columnName}_numberingRuleId`;
// formData에 아직 설정되지 않은 경우에만 설정
@@ -135,7 +135,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// 채번 규칙은 비동기로 처리
if (testAutoGeneration.type === "numbering_rule") {
const ruleId = testAutoGeneration.options?.numberingRuleId;
const ruleId = testAutoGeneration.options?.numbering_rule_id;
if (ruleId && ruleId !== "undefined" && ruleId !== "null") {
try {
const { previewNumberingCode } = await import("@/lib/api/numberingRule");
@@ -154,7 +154,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}
} else {
// 기타 타입은 동기로 처리
generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
generatedValue = await AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
isGeneratingRef.current = false;
}
@@ -168,13 +168,13 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
onFormDataChange(component.columnName, generatedValue);
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numbering_rule_id) {
const ruleIdKey = `${component.columnName}_numberingRuleId`;
onFormDataChange(ruleIdKey, testAutoGeneration.options.numberingRuleId);
onFormDataChange(ruleIdKey, testAutoGeneration.options.numbering_rule_id);
}
}
}
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
} else if (!autoGeneratedValue) {
// 디자인 모드에서도 미리보기용 자동생성 값 표시
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
setAutoGeneratedValue(previewValue);
@@ -844,7 +844,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// 채번 규칙 ID 복구
if (isInteractive && onFormDataChange && component.columnName) {
const ruleId = testAutoGeneration.options?.numberingRuleId;
const ruleId = testAutoGeneration.options?.numbering_rule_id;
if (ruleId) {
const ruleIdKey = `${component.columnName}_numberingRuleId`;
onFormDataChange(ruleIdKey, ruleId);
@@ -34,7 +34,7 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
// useState 초기값에서 저장된 값 복원 (우선순위: 저장된 값 > menuObjid prop)
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
return config.autoGeneration?.selectedMenuObjid || menuObjid;
return (config.autoGeneration as any)?.selectedMenuObjid || menuObjid;
});
const [loadingMenus, setLoadingMenus] = useState(false);
@@ -250,14 +250,14 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
<span className="text-destructive">*</span>
</Label>
<Select
value={config.autoGeneration?.options?.numberingRuleId || ""}
value={config.autoGeneration?.options?.numbering_rule_id || ""}
onValueChange={(value) => {
const currentConfig = config.autoGeneration!;
handleChange("autoGeneration", {
...currentConfig,
options: {
...currentConfig.options,
numberingRuleId: value,
numbering_rule_id: value,
},
});
}}
+10 -10
View File
@@ -4168,12 +4168,12 @@ export class ButtonActionExecutor {
}
const extendedContext: ExtendedControlContext = {
formData: context.formData || {},
selectedRows: context.selectedRows || [],
selectedRowsData: context.selectedRowsData || [],
flowSelectedData: context.flowSelectedData || [],
flowSelectedStepId: context.flowSelectedStepId,
controlDataSource,
form_data: context.formData || {},
selected_rows: context.selectedRows || [],
selected_rows_data: context.selectedRowsData || [],
flow_selected_data: context.flowSelectedData || [],
flow_selected_step_id: context.flowSelectedStepId,
control_data_source: controlDataSource,
};
// 🔥 새로운 버튼 액션 실행 시스템 사용
@@ -4395,10 +4395,10 @@ export class ButtonActionExecutor {
}
const extendedContext: ExtendedControlContext = {
formData: context.formData || {},
selectedRows: context.selectedRows || [],
selectedRowsData: context.selectedRowsData || [],
controlDataSource,
form_data: context.formData || {},
selected_rows: context.selectedRows || [],
selected_rows_data: context.selectedRowsData || [],
control_data_source: controlDataSource,
};
// 🔥 다중 제어 지원 (flowControls 배열)
+13 -13
View File
@@ -87,16 +87,16 @@ export const validateFormData = async (
const widgetComponents = components.filter((c) => c.type === "widget") as WidgetComponent[];
for (const component of widgetComponents) {
const fieldName = component.columnName || component.id;
const fieldName = component.column_name || component.id;
const value = formData[fieldName];
if (value !== undefined && value !== null && value !== "") {
const fieldValidation = validateFieldValue(
fieldName,
value,
component.widgetType,
component.webTypeConfig,
component.validationRules,
component.widget_type,
component.web_type_config,
component.validation_rules,
);
if (!fieldValidation.isValid && fieldValidation.error) {
@@ -137,11 +137,11 @@ export const validateFormSchema = (
const invalidTypes: { field: string; expected: WebType; actual: string }[] = [];
const suggestions: string[] = [];
const columnMap = new Map(tableColumns.map((col) => [col.columnName, col]));
const columnMap = new Map(tableColumns.map((col) => [col.column_name, col]));
const widgetComponents = components.filter((c) => c.type === "widget") as WidgetComponent[];
for (const component of widgetComponents) {
const fieldName = component.columnName;
const fieldName = component.column_name;
if (!fieldName) continue;
// 컬럼 존재 여부 확인
@@ -158,8 +158,8 @@ export const validateFormSchema = (
}
// 웹타입 일치 여부 확인
const componentWebType = normalizeWebType(component.widgetType);
const columnWebType = columnInfo.webType ? normalizeWebType(columnInfo.webType) : null;
const componentWebType = normalizeWebType(component.widget_type);
const columnWebType = columnInfo.web_type ? normalizeWebType(columnInfo.web_type) : null;
if (columnWebType && componentWebType !== columnWebType) {
invalidTypes.push({
@@ -170,11 +170,11 @@ export const validateFormSchema = (
}
// 웹타입 유효성 확인
if (!isValidWebType(component.widgetType)) {
if (!isValidWebType(component.widget_type)) {
invalidTypes.push({
field: fieldName,
expected: "text", // 기본값
actual: component.widgetType,
actual: component.widget_type,
});
}
}
@@ -199,7 +199,7 @@ export const validateRequiredFields = (
const widgetComponents = components.filter((c) => c.type === "widget") as WidgetComponent[];
for (const component of widgetComponents) {
const fieldName = component.columnName || component.id;
const fieldName = component.column_name || component.id;
// 수동 required + NOT NULL 메타데이터 기반 통합 체크
const isRequired = component.required || isColumnRequiredByMeta(tableName, fieldName);
if (!isRequired) continue;
@@ -627,9 +627,9 @@ const findSimilarColumns = (targetColumn: string, columns: ColumnInfo[], thresho
const similar: string[] = [];
for (const column of columns) {
const similarity = calculateStringSimilarity(targetColumn, column.columnName);
const similarity = calculateStringSimilarity(targetColumn, column.column_name);
if (similarity >= threshold) {
similar.push(column.columnName);
similar.push(column.column_name);
}
}
+1
View File
@@ -1,5 +1,6 @@
import { Position, Size } from "@/types/screen";
import { GridSettings } from "@/types/screen-management";
export type { GridSettings };
export interface GridInfo {
columnWidth: number;
@@ -171,28 +171,33 @@ export class ImprovedButtonActionExecutor {
return plan;
}
// 🔧 controlMode가 없으면 flowConfig/relationshipConfig 존재 여부로 자동 판단
const effectiveControlMode = dataflowConfig.controlMode
|| (dataflowConfig.flowConfig ? "flow" : null)
|| (dataflowConfig.relationshipConfig ? "relationship" : null)
// 🔧 control_mode가 없으면 flow_config/relationship_config 존재 여부로 자동 판단
const effectiveControlMode = dataflowConfig.control_mode
|| (dataflowConfig.flow_config ? "flow" : null)
|| (dataflowConfig.relationship_config ? "relationship" : null)
|| "none";
console.log("📋 실행 계획 생성:", {
controlMode: dataflowConfig.controlMode,
controlMode: dataflowConfig.control_mode,
effectiveControlMode,
hasFlowConfig: !!dataflowConfig.flowConfig,
hasRelationshipConfig: !!dataflowConfig.relationshipConfig,
hasFlowConfig: !!dataflowConfig.flow_config,
hasRelationshipConfig: !!dataflowConfig.relationship_config,
enableDataflowControl: buttonConfig.enable_dataflow_control,
});
// 관계 기반 제어
if (effectiveControlMode === "relationship" && dataflowConfig.relationshipConfig) {
if (effectiveControlMode === "relationship" && dataflowConfig.relationship_config) {
const control: ControlConfig = {
type: "relationship",
relationshipConfig: dataflowConfig.relationshipConfig,
relationshipConfig: {
relationshipId: dataflowConfig.relationship_config.relationship_id,
relationshipName: dataflowConfig.relationship_config.relationship_name,
executionTiming: dataflowConfig.relationship_config.execution_timing,
contextData: dataflowConfig.relationship_config.context_data,
},
};
switch (dataflowConfig.relationshipConfig.executionTiming) {
switch (dataflowConfig.relationship_config.execution_timing) {
case "before":
plan.beforeControls.push(control);
break;
@@ -207,15 +212,20 @@ export class ImprovedButtonActionExecutor {
}
// 🆕 플로우 기반 제어
if (effectiveControlMode === "flow" && dataflowConfig.flowConfig) {
if (effectiveControlMode === "flow" && dataflowConfig.flow_config) {
const control: ControlConfig = {
type: "flow",
flowConfig: dataflowConfig.flowConfig,
flowConfig: {
flowId: dataflowConfig.flow_config.flow_id,
flowName: dataflowConfig.flow_config.flow_name,
executionTiming: dataflowConfig.flow_config.execution_timing,
contextData: dataflowConfig.flow_config.context_data,
},
};
console.log("📋 플로우 제어 설정:", dataflowConfig.flowConfig);
console.log("📋 플로우 제어 설정:", dataflowConfig.flow_config);
switch (dataflowConfig.flowConfig.executionTiming) {
switch (dataflowConfig.flow_config.execution_timing) {
case "before":
plan.beforeControls.push(control);
break;
@@ -1074,7 +1084,7 @@ export class ImprovedButtonActionExecutor {
console.error("🔄 실행 오류 처리 시작:", error.message);
// 롤백이 필요한 경우 처리
const rollbackNeeded = buttonConfig.dataflow_config?.executionOptions?.rollbackOnError;
const rollbackNeeded = (buttonConfig.dataflow_config?.execution_options as any)?.rollback_on_error;
if (rollbackNeeded) {
console.log("🔄 롤백 처리 시작...");
-10
View File
@@ -377,16 +377,6 @@ function getDefaultText(key: string): string {
"menu.management.user": "사용자",
"menu.management.user.description": "일반 사용자 업무 메뉴",
"menu.list.title": "메뉴 목록",
"filter.search.placeholder": "메뉴명 검색...",
"filter.reset": "초기화",
"button.add.top.level": "최상위 메뉴 추가",
"button.delete.selected": "선택 삭제",
"table.header.menu.name": "메뉴명",
"table.header.sequence": "순서",
"table.header.company": "회사",
"table.header.menu.url": "URL",
"table.header.status": "상태",
"table.header.actions": "작업",
};
return defaultTexts[key] || "";
@@ -419,6 +419,7 @@ export function applyMultilangMappings(
const updateComponent = (comp: ComponentData): ComponentData => {
const anyComp = comp as any;
const config = anyComp.componentConfig || anyComp.config;
const compType = anyComp.componentType || anyComp.type;
let updated = { ...comp } as any;
// 기본 컴포넌트 라벨 매핑 확인
+2 -2
View File
@@ -42,7 +42,7 @@ export interface FlowExecutionResult {
* 노드 플로우 실행 함수
*/
export async function executeButtonWithFlow(
flowConfig: ButtonDataflowConfig["flowConfig"],
flowConfig: ButtonDataflowConfig["flow_config"],
context: ButtonExecutionContext,
originalAction?: () => Promise<void>,
): Promise<FlowExecutionResult> {
@@ -50,7 +50,7 @@ export async function executeButtonWithFlow(
throw new Error("플로우 설정이 없습니다.");
}
const { flowId, flowName, executionTiming = "before" } = flowConfig;
const { flow_id: flowId, flow_name: flowName, execution_timing: executionTiming = "before" } = flowConfig!;
logger.info(`🚀 노드 플로우 실행 시작:`, {
flowId,
+20 -20
View File
@@ -26,17 +26,17 @@ export function generateSmartDefaults(
if (fullWidthComponents.includes(componentId) || fullWidthComponents.includes(componentType)) {
return {
desktop: {
gridColumns: 12, // 전체 너비
grid_columns: 12, // 전체 너비
order: 1,
hide: false,
},
tablet: {
gridColumns: 8, // 전체 너비
grid_columns: 8, // 전체 너비
order: 1,
hide: false,
},
mobile: {
gridColumns: 4, // 전체 너비
grid_columns: 4, // 전체 너비
order: 1,
hide: false,
},
@@ -53,17 +53,17 @@ export function generateSmartDefaults(
return {
desktop: {
gridColumns: desktopColumns,
grid_columns: desktopColumns,
order: 1,
hide: false,
},
tablet: {
gridColumns: tabletColumns,
grid_columns: tabletColumns,
order: 1,
hide: false,
},
mobile: {
gridColumns: mobileColumns,
grid_columns: mobileColumns,
order: 1,
hide: false,
},
@@ -73,17 +73,17 @@ export function generateSmartDefaults(
else if (componentWidthPercent <= 10) {
return {
desktop: {
gridColumns: 1, // 12컬럼 중 1개 (~8%)
grid_columns: 1, // 12컬럼 중 1개 (~8%)
order: 1,
hide: false,
},
tablet: {
gridColumns: 1, // 8컬럼 중 1개 (~12.5%)
grid_columns: 1, // 8컬럼 중 1개 (~12.5%)
order: 1,
hide: false,
},
mobile: {
gridColumns: 1, // 4컬럼 중 1개 (25%)
grid_columns: 1, // 4컬럼 중 1개 (25%)
order: 1,
hide: false,
},
@@ -93,17 +93,17 @@ export function generateSmartDefaults(
else if (componentWidthPercent <= 25) {
return {
desktop: {
gridColumns: 3, // 12컬럼 중 3개 (25%)
grid_columns: 3, // 12컬럼 중 3개 (25%)
order: 1,
hide: false,
},
tablet: {
gridColumns: 2, // 8컬럼 중 2개 (25%)
grid_columns: 2, // 8컬럼 중 2개 (25%)
order: 1,
hide: false,
},
mobile: {
gridColumns: 1, // 4컬럼 중 1개 (25%)
grid_columns: 1, // 4컬럼 중 1개 (25%)
order: 1,
hide: false,
},
@@ -113,17 +113,17 @@ export function generateSmartDefaults(
else if (componentWidthPercent <= 50) {
return {
desktop: {
gridColumns: 6, // 12컬럼 중 6개 (50%)
grid_columns: 6, // 12컬럼 중 6개 (50%)
order: 1,
hide: false,
},
tablet: {
gridColumns: 4, // 8컬럼 중 4개 (50%)
grid_columns: 4, // 8컬럼 중 4개 (50%)
order: 1,
hide: false,
},
mobile: {
gridColumns: 4, // 4컬럼 전체 (100%)
grid_columns: 4, // 4컬럼 전체 (100%)
order: 1,
hide: false,
},
@@ -133,17 +133,17 @@ export function generateSmartDefaults(
else {
return {
desktop: {
gridColumns: 12, // 전체 너비
grid_columns: 12, // 전체 너비
order: 1,
hide: false,
},
tablet: {
gridColumns: 8, // 전체 너비
grid_columns: 8, // 전체 너비
order: 1,
hide: false,
},
mobile: {
gridColumns: 4, // 전체 너비
grid_columns: 4, // 전체 너비
order: 1,
hide: false,
},
@@ -162,13 +162,13 @@ export function ensureResponsiveConfig(component: ComponentData, screenWidth?: n
return {
...component,
responsive_config: {
designerPosition: {
designer_position: {
x: component.position.x,
y: component.position.y,
width: component.size.width,
height: component.size.height,
},
useSmartDefaults: true,
use_smart_defaults: true,
responsive: generateSmartDefaults(component, screenWidth),
},
};
+5 -4
View File
@@ -21,10 +21,11 @@ export function convertWebTypeConfigToAutoGeneration(component: ComponentData):
}
// 이미 auto_generation이 올바르게 설정되어 있으면 변환 불필요
const anyComp = component as any;
if (
component.auto_generation &&
component.auto_generation.type === config.autoValueType &&
component.auto_generation.options?.numbering_rule_id === config.numberingRuleId
anyComp.auto_generation &&
anyComp.auto_generation.type === config.autoValueType &&
anyComp.auto_generation.options?.numbering_rule_id === config.numberingRuleId
) {
return component;
}
@@ -46,7 +47,7 @@ export function convertWebTypeConfigToAutoGeneration(component: ComponentData):
return {
...component,
auto_generation,
};
} as unknown as ComponentData;
}
/**
+7 -7
View File
@@ -25,7 +25,7 @@ export function generateZPL(layout: BarcodeLabelLayout): string {
"^LH0,0",
];
const sorted = [...components].sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
const sorted = [...components].sort((a, b) => (a.z_index ?? 0) - (b.z_index ?? 0));
for (const c of sorted) {
const x = pxToDots(c.x);
@@ -34,25 +34,25 @@ export function generateZPL(layout: BarcodeLabelLayout): string {
const h = pxToDots(c.height);
if (c.type === "text") {
const fontH = Math.max(10, Math.min(120, (c.fontSize || 10) * 4)); // 대략적 변환
const fontH = Math.max(10, Math.min(120, (c.font_size || 10) * 4)); // 대략적 변환
const fontW = Math.round(fontH * 0.6);
lines.push(`^FO${x},${y}`);
lines.push(`^A0N,${fontH},${fontW}`);
lines.push(`^FD${escapeZPL(c.content || "")}^FS`);
} else if (c.type === "barcode") {
if (c.barcodeType === "QR") {
if (c.barcode_type === "QR") {
const size = Math.min(w, h);
const qrSize = Math.max(1, Math.min(10, Math.round(size / 20)));
lines.push(`^FO${x},${y}`);
lines.push(`^BQN,2,${qrSize}`);
lines.push(`^FDQA,${escapeZPL(c.barcodeValue || "")}^FS`);
lines.push(`^FDQA,${escapeZPL(c.barcode_value || "")}^FS`);
} else {
// CODE128: ^BC, CODE39: ^B3
const mod = c.barcodeType === "CODE39" ? "^B3N" : "^BCN";
const showText = c.showBarcodeText !== false ? "Y" : "N";
const mod = c.barcode_type === "CODE39" ? "^B3N" : "^BCN";
const showText = c.show_barcode_text !== false ? "Y" : "N";
lines.push(`^FO${x},${y}`);
lines.push(`${mod},${Math.max(20, h - 10)},${showText},N,N`);
lines.push(`^FD${escapeZPL(c.barcodeValue || "")}^FS`);
lines.push(`^FD${escapeZPL(c.barcode_value || "")}^FS`);
}
}
// 이미지/선/사각형은 ZPL에서 비트맵 또는 ^GB 등으로 확장 가능 (생략)