feature/screen-management #337
|
|
@ -2282,6 +2282,7 @@ export class NodeFlowExecutionService {
|
|||
UPDATE ${targetTable}
|
||||
SET ${setClauses.join(", ")}
|
||||
WHERE ${updateWhereConditions}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
logger.info(`🔄 UPDATE 실행:`, {
|
||||
|
|
@ -2292,8 +2293,14 @@ export class NodeFlowExecutionService {
|
|||
values: updateValues,
|
||||
});
|
||||
|
||||
await txClient.query(updateSql, updateValues);
|
||||
const updateResult = await txClient.query(updateSql, updateValues);
|
||||
updatedCount++;
|
||||
|
||||
// 🆕 UPDATE 결과를 입력 데이터에 병합 (다음 노드에서 id 등 사용 가능)
|
||||
if (updateResult.rows && updateResult.rows[0]) {
|
||||
Object.assign(data, updateResult.rows[0]);
|
||||
logger.info(` 📦 UPDATE 결과 병합: id=${updateResult.rows[0].id}`);
|
||||
}
|
||||
} else {
|
||||
// 3-B. 없으면 INSERT
|
||||
const columns: string[] = [];
|
||||
|
|
@ -2340,6 +2347,7 @@ export class NodeFlowExecutionService {
|
|||
const insertSql = `
|
||||
INSERT INTO ${targetTable} (${columns.join(", ")})
|
||||
VALUES (${placeholders})
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
logger.info(`➕ INSERT 실행:`, {
|
||||
|
|
@ -2348,8 +2356,14 @@ export class NodeFlowExecutionService {
|
|||
conflictKeyValues,
|
||||
});
|
||||
|
||||
await txClient.query(insertSql, values);
|
||||
const insertResult = await txClient.query(insertSql, values);
|
||||
insertedCount++;
|
||||
|
||||
// 🆕 INSERT 결과를 입력 데이터에 병합 (다음 노드에서 id 등 사용 가능)
|
||||
if (insertResult.rows && insertResult.rows[0]) {
|
||||
Object.assign(data, insertResult.rows[0]);
|
||||
logger.info(` 📦 INSERT 결과 병합: id=${insertResult.rows[0].id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2357,11 +2371,10 @@ export class NodeFlowExecutionService {
|
|||
`✅ UPSERT 완료 (내부 DB): ${targetTable}, INSERT ${insertedCount}건, UPDATE ${updatedCount}건`
|
||||
);
|
||||
|
||||
return {
|
||||
insertedCount,
|
||||
updatedCount,
|
||||
totalCount: insertedCount + updatedCount,
|
||||
};
|
||||
// 🔥 다음 노드에 전달할 데이터 반환
|
||||
// dataArray에는 Object.assign으로 UPSERT 결과(id 등)가 이미 병합되어 있음
|
||||
// 카운트 정보도 함께 반환하여 기존 호환성 유지
|
||||
return dataArray;
|
||||
};
|
||||
|
||||
// 🔥 클라이언트가 전달되었으면 사용, 아니면 독립 트랜잭션 생성
|
||||
|
|
|
|||
|
|
@ -27,13 +27,14 @@ interface EmbeddedScreenProps {
|
|||
onSelectionChanged?: (selectedRows: any[]) => void;
|
||||
position?: SplitPanelPosition; // 분할 패널 내 위치 (left/right)
|
||||
initialFormData?: Record<string, any>; // 🆕 수정 모드에서 전달되는 초기 데이터
|
||||
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (수정 모드에서 원본 데이터 추적용)
|
||||
}
|
||||
|
||||
/**
|
||||
* 임베드된 화면 컴포넌트
|
||||
*/
|
||||
export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenProps>(
|
||||
({ embedding, onSelectionChanged, position, initialFormData }, ref) => {
|
||||
({ embedding, onSelectionChanged, position, initialFormData, groupedData }, ref) => {
|
||||
const [layout, setLayout] = useState<ComponentData[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -430,6 +431,8 @@ export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenPro
|
|||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={companyCode}
|
||||
groupedData={groupedData}
|
||||
initialData={initialFormData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@ interface ScreenSplitPanelProps {
|
|||
screenId?: number;
|
||||
config?: any; // 설정 패널에서 오는 config (leftScreenId, rightScreenId, splitRatio, resizable)
|
||||
initialFormData?: Record<string, any>; // 🆕 수정 모드에서 전달되는 초기 데이터
|
||||
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (수정 모드에서 원본 데이터 추적용)
|
||||
}
|
||||
|
||||
/**
|
||||
* 분할 패널 컴포넌트
|
||||
* 순수하게 화면 분할 기능만 제공합니다.
|
||||
*/
|
||||
export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSplitPanelProps) {
|
||||
export function ScreenSplitPanel({ screenId, config, initialFormData, groupedData }: ScreenSplitPanelProps) {
|
||||
// config에서 splitRatio 추출 (기본값 50)
|
||||
const configSplitRatio = config?.splitRatio ?? 50;
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
|
|||
{/* 좌측 패널 */}
|
||||
<div style={{ width: `${splitRatio}%` }} className="h-full flex-shrink-0 overflow-hidden border-r">
|
||||
{hasLeftScreen ? (
|
||||
<EmbeddedScreen embedding={leftEmbedding!} position="left" initialFormData={initialFormData} />
|
||||
<EmbeddedScreen embedding={leftEmbedding!} position="left" initialFormData={initialFormData} groupedData={groupedData} />
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center bg-muted/30">
|
||||
<p className="text-muted-foreground text-sm">좌측 화면을 선택하세요</p>
|
||||
|
|
@ -157,7 +158,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
|
|||
{/* 우측 패널 */}
|
||||
<div style={{ width: `${100 - splitRatio}%` }} className="h-full flex-shrink-0 overflow-hidden">
|
||||
{hasRightScreen ? (
|
||||
<EmbeddedScreen embedding={rightEmbedding!} position="right" initialFormData={initialFormData} />
|
||||
<EmbeddedScreen embedding={rightEmbedding!} position="right" initialFormData={initialFormData} groupedData={groupedData} />
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center bg-muted/30">
|
||||
<p className="text-muted-foreground text-sm">우측 화면을 선택하세요</p>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
editModalTitle: String(config.action?.editModalTitle || ""),
|
||||
editModalDescription: String(config.action?.editModalDescription || ""),
|
||||
targetUrl: String(config.action?.targetUrl || ""),
|
||||
groupByColumn: String(config.action?.groupByColumns?.[0] || ""),
|
||||
});
|
||||
|
||||
const [screens, setScreens] = useState<ScreenOption[]>([]);
|
||||
|
|
@ -97,6 +98,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
const [modalTargetColumns, setModalTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState<Record<number, boolean>>({});
|
||||
const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// 🆕 그룹화 컬럼 선택용 상태
|
||||
const [currentTableColumns, setCurrentTableColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [groupByColumnOpen, setGroupByColumnOpen] = useState(false);
|
||||
const [groupByColumnSearch, setGroupByColumnSearch] = useState("");
|
||||
const [modalSourceSearch, setModalSourceSearch] = useState<Record<number, string>>({});
|
||||
const [modalTargetSearch, setModalTargetSearch] = useState<Record<number, string>>({});
|
||||
|
||||
|
|
@ -130,6 +136,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
editModalTitle: String(latestAction.editModalTitle || ""),
|
||||
editModalDescription: String(latestAction.editModalDescription || ""),
|
||||
targetUrl: String(latestAction.targetUrl || ""),
|
||||
groupByColumn: String(latestAction.groupByColumns?.[0] || ""),
|
||||
});
|
||||
|
||||
// 🆕 제목 블록 초기화
|
||||
|
|
@ -327,6 +334,35 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
loadColumns();
|
||||
}, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]);
|
||||
|
||||
// 🆕 현재 테이블 컬럼 로드 (그룹화 컬럼 선택용)
|
||||
useEffect(() => {
|
||||
if (!currentTableName) return;
|
||||
|
||||
const loadCurrentTableColumns = async () => {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${currentTableName}/columns`);
|
||||
if (response.data.success) {
|
||||
let columnData = response.data.data;
|
||||
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
|
||||
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
|
||||
|
||||
if (Array.isArray(columnData)) {
|
||||
const columns = columnData.map((col: any) => ({
|
||||
name: col.name || col.columnName,
|
||||
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
||||
}));
|
||||
setCurrentTableColumns(columns);
|
||||
console.log(`✅ 현재 테이블 ${currentTableName} 컬럼 로드 성공:`, columns.length, "개");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("현재 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadCurrentTableColumns();
|
||||
}, [currentTableName]);
|
||||
|
||||
// 🆕 openModalWithData 소스/타겟 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
const actionType = config.action?.type;
|
||||
|
|
@ -1529,6 +1565,106 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-group-by-column">그룹화 컬럼</Label>
|
||||
<Popover open={groupByColumnOpen} onOpenChange={setGroupByColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={groupByColumnOpen}
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{localInputs.groupByColumn ? (
|
||||
<span>
|
||||
{localInputs.groupByColumn}
|
||||
{currentTableColumns.find((col) => col.name === localInputs.groupByColumn)?.label &&
|
||||
currentTableColumns.find((col) => col.name === localInputs.groupByColumn)?.label !== localInputs.groupByColumn && (
|
||||
<span className="ml-1 text-muted-foreground">
|
||||
({currentTableColumns.find((col) => col.name === localInputs.groupByColumn)?.label})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">컬럼을 선택하세요</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center border-b px-3 py-2">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<Input
|
||||
placeholder="컬럼명 또는 라벨 검색..."
|
||||
value={groupByColumnSearch}
|
||||
onChange={(e) => setGroupByColumnSearch(e.target.value)}
|
||||
className="border-0 p-0 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-auto">
|
||||
{currentTableColumns.length === 0 ? (
|
||||
<div className="p-3 text-sm text-muted-foreground">
|
||||
{currentTableName ? "컬럼을 불러오는 중..." : "테이블이 설정되지 않았습니다"}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 선택 해제 옵션 */}
|
||||
<div
|
||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
|
||||
onClick={() => {
|
||||
setLocalInputs((prev) => ({ ...prev, groupByColumn: "" }));
|
||||
onUpdateProperty("componentConfig.action.groupByColumns", undefined);
|
||||
setGroupByColumnOpen(false);
|
||||
setGroupByColumnSearch("");
|
||||
}}
|
||||
>
|
||||
<Check className={cn("mr-2 h-4 w-4", !localInputs.groupByColumn ? "opacity-100" : "opacity-0")} />
|
||||
<span className="text-muted-foreground">선택 안 함</span>
|
||||
</div>
|
||||
{/* 컬럼 목록 */}
|
||||
{currentTableColumns
|
||||
.filter((col) => {
|
||||
if (!groupByColumnSearch) return true;
|
||||
const search = groupByColumnSearch.toLowerCase();
|
||||
return (
|
||||
col.name.toLowerCase().includes(search) ||
|
||||
col.label.toLowerCase().includes(search)
|
||||
);
|
||||
})
|
||||
.map((col) => (
|
||||
<div
|
||||
key={col.name}
|
||||
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
|
||||
onClick={() => {
|
||||
setLocalInputs((prev) => ({ ...prev, groupByColumn: col.name }));
|
||||
onUpdateProperty("componentConfig.action.groupByColumns", [col.name]);
|
||||
setGroupByColumnOpen(false);
|
||||
setGroupByColumnSearch("");
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", localInputs.groupByColumn === col.name ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label !== col.name && (
|
||||
<span className="text-xs text-muted-foreground">{col.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
여러 행을 하나의 그룹으로 묶어서 수정할 때 사용합니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class ScreenSplitPanelRenderer extends AutoRegisteringComponentRenderer {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { component, style = {}, componentConfig, config, screenId, formData } = this.props as any;
|
||||
const { component, style = {}, componentConfig, config, screenId, formData, groupedData } = this.props as any;
|
||||
|
||||
// componentConfig 또는 config 또는 component.componentConfig 사용
|
||||
const finalConfig = componentConfig || config || component?.componentConfig || {};
|
||||
|
|
@ -77,6 +77,7 @@ class ScreenSplitPanelRenderer extends AutoRegisteringComponentRenderer {
|
|||
screenId={screenId || finalConfig.screenId}
|
||||
config={finalConfig}
|
||||
initialFormData={formData} // 🆕 수정 데이터 전달
|
||||
groupedData={groupedData} // 🆕 그룹 데이터 전달 (수정 모드에서 원본 데이터 추적용)
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2392,6 +2392,44 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
fetchLabels();
|
||||
}, [columnUniqueValues, categoryLabelCache]);
|
||||
|
||||
// 🆕 데이터에서 CATEGORY_ 코드를 찾아 라벨 미리 로드 (테이블 셀 렌더링용)
|
||||
useEffect(() => {
|
||||
if (data.length === 0) return;
|
||||
|
||||
const categoryCodesToFetch = new Set<string>();
|
||||
|
||||
// 모든 데이터 행에서 CATEGORY_ 코드 수집
|
||||
data.forEach((row) => {
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
if (value && typeof value === "string") {
|
||||
// 콤마로 구분된 다중 값도 처리
|
||||
const codes = value.split(",").map((v) => v.trim());
|
||||
codes.forEach((code) => {
|
||||
if (code.startsWith("CATEGORY_") && !categoryLabelCache[code]) {
|
||||
categoryCodesToFetch.add(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (categoryCodesToFetch.size === 0) return;
|
||||
|
||||
// API로 라벨 조회
|
||||
const fetchLabels = async () => {
|
||||
try {
|
||||
const response = await getCategoryLabelsByCodes(Array.from(categoryCodesToFetch));
|
||||
if (response.success && response.data && Object.keys(response.data).length > 0) {
|
||||
setCategoryLabelCache((prev) => ({ ...prev, ...response.data }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("CATEGORY_ 라벨 조회 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLabels();
|
||||
}, [data, categoryLabelCache]);
|
||||
|
||||
// 🆕 헤더 필터 토글
|
||||
const toggleHeaderFilter = useCallback((columnName: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
|
|
@ -4548,10 +4586,36 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
case "boolean":
|
||||
return value ? "예" : "아니오";
|
||||
default:
|
||||
return String(value);
|
||||
// 🆕 CATEGORY_ 코드 자동 변환 (inputType이 category가 아니어도)
|
||||
const strValue = String(value);
|
||||
if (strValue.startsWith("CATEGORY_")) {
|
||||
// rowData에서 _label 필드 찾기
|
||||
if (rowData) {
|
||||
const labelFieldCandidates = [
|
||||
`${column.columnName}_label`,
|
||||
`${column.columnName}_name`,
|
||||
`${column.columnName}_value_label`,
|
||||
];
|
||||
for (const labelField of labelFieldCandidates) {
|
||||
if (rowData[labelField] && rowData[labelField] !== "") {
|
||||
return String(rowData[labelField]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// categoryMappings에서 찾기
|
||||
const mapping = categoryMappings[column.columnName];
|
||||
if (mapping && mapping[strValue]) {
|
||||
return mapping[strValue].label;
|
||||
}
|
||||
// categoryLabelCache에서 찾기 (필터용 캐시)
|
||||
if (categoryLabelCache[strValue]) {
|
||||
return categoryLabelCache[strValue];
|
||||
}
|
||||
}
|
||||
return strValue;
|
||||
}
|
||||
},
|
||||
[columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings],
|
||||
[columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings, categoryLabelCache],
|
||||
);
|
||||
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -304,6 +304,9 @@ export interface ButtonActionContext {
|
|||
selectedLeftData?: Record<string, any>;
|
||||
refreshRightPanel?: () => void;
|
||||
};
|
||||
|
||||
// 🆕 저장된 데이터 (저장 후 제어 실행 시 플로우에 전달)
|
||||
savedData?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1036,10 +1039,11 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
|
||||
// 🆕 공통 필드 병합 + 사용자 정보 추가
|
||||
// 공통 필드를 먼저 넣고, 개별 항목 데이터로 덮어씀 (개별 항목이 우선)
|
||||
// 개별 항목 데이터를 먼저 넣고, 공통 필드로 덮어씀 (공통 필드가 우선)
|
||||
// 이유: 사용자가 공통 필드(출고상태 등)를 변경하면 모든 항목에 적용되어야 함
|
||||
const dataWithMeta: Record<string, unknown> = {
|
||||
...commonFields, // 범용 폼 모달의 공통 필드 (order_no, manager_id 등)
|
||||
...dataToSave, // RepeaterFieldGroup의 개별 항목 데이터
|
||||
...commonFields, // 범용 폼 모달의 공통 필드 (outbound_status 등) - 공통 필드가 우선!
|
||||
created_by: context.userId,
|
||||
updated_by: context.userId,
|
||||
company_code: context.companyCode,
|
||||
|
|
@ -1251,7 +1255,49 @@ export class ButtonActionExecutor {
|
|||
// 🔥 저장 성공 후 연결된 제어 실행 (dataflowTiming이 'after'인 경우)
|
||||
if (config.enableDataflowControl && config.dataflowConfig) {
|
||||
console.log("🎯 저장 후 제어 실행 시작:", config.dataflowConfig);
|
||||
await this.executeAfterSaveControl(config, context);
|
||||
|
||||
// 테이블 섹션 데이터 파싱 (comp_로 시작하는 필드에 JSON 배열이 있는 경우)
|
||||
// 입고 화면 등에서 품목 목록이 comp_xxx 필드에 JSON 문자열로 저장됨
|
||||
const formData: Record<string, any> = (saveResult.data || context.formData || {}) as Record<string, any>;
|
||||
let parsedSectionData: any[] = [];
|
||||
|
||||
// comp_로 시작하는 필드에서 테이블 섹션 데이터 찾기
|
||||
const compFieldKey = Object.keys(formData).find(key =>
|
||||
key.startsWith("comp_") && typeof formData[key] === "string"
|
||||
);
|
||||
|
||||
if (compFieldKey) {
|
||||
try {
|
||||
const sectionData = JSON.parse(formData[compFieldKey]);
|
||||
if (Array.isArray(sectionData) && sectionData.length > 0) {
|
||||
// 공통 필드와 섹션 데이터 병합
|
||||
parsedSectionData = sectionData.map((item: any) => {
|
||||
// 섹션 데이터에서 불필요한 내부 필드 제거
|
||||
const { _isNewItem, _targetTable, _existingRecord, ...cleanItem } = item;
|
||||
// 공통 필드(comp_ 필드 제외) + 섹션 아이템 병합
|
||||
const commonFields: Record<string, any> = {};
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (!key.startsWith("comp_") && !key.endsWith("_numberingRuleId")) {
|
||||
commonFields[key] = formData[key];
|
||||
}
|
||||
});
|
||||
return { ...commonFields, ...cleanItem };
|
||||
});
|
||||
console.log(`📦 [handleSave] 테이블 섹션 데이터 파싱 완료: ${parsedSectionData.length}건`, parsedSectionData[0]);
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn("⚠️ [handleSave] 테이블 섹션 데이터 파싱 실패:", parseError);
|
||||
}
|
||||
}
|
||||
|
||||
// 저장된 데이터를 context에 추가하여 플로우에 전달
|
||||
const contextWithSavedData = {
|
||||
...context,
|
||||
savedData: formData,
|
||||
// 파싱된 섹션 데이터가 있으면 selectedRowsData로 전달
|
||||
selectedRowsData: parsedSectionData.length > 0 ? parsedSectionData : context.selectedRowsData,
|
||||
};
|
||||
await this.executeAfterSaveControl(config, contextWithSavedData);
|
||||
}
|
||||
} else {
|
||||
throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
|
||||
|
|
@ -3643,8 +3689,20 @@ export class ButtonActionExecutor {
|
|||
// 노드 플로우 실행 API
|
||||
const { executeNodeFlow } = await import("@/lib/api/nodeFlows");
|
||||
|
||||
// 데이터 소스 준비
|
||||
const sourceData: any = context.formData || {};
|
||||
// 데이터 소스 준비: context-data 모드는 배열을 기대함
|
||||
// 우선순위: selectedRowsData > savedData > formData
|
||||
// - selectedRowsData: 테이블 섹션에서 저장된 하위 항목들 (item_code, inbound_qty 등 포함)
|
||||
// - savedData: 저장 API 응답 데이터
|
||||
// - formData: 폼에 입력된 데이터
|
||||
let sourceData: any[];
|
||||
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
|
||||
sourceData = context.selectedRowsData;
|
||||
console.log("📦 [다중제어] selectedRowsData 사용:", sourceData.length, "건");
|
||||
} else {
|
||||
const savedData = context.savedData || context.formData || {};
|
||||
sourceData = Array.isArray(savedData) ? savedData : [savedData];
|
||||
console.log("📦 [다중제어] savedData/formData 사용:", sourceData.length, "건");
|
||||
}
|
||||
|
||||
let allSuccess = true;
|
||||
const results: Array<{ flowId: number; flowName: string; success: boolean; message?: string }> = [];
|
||||
|
|
@ -3751,8 +3809,20 @@ export class ButtonActionExecutor {
|
|||
// 노드 플로우 실행 API 호출
|
||||
const { executeNodeFlow } = await import("@/lib/api/nodeFlows");
|
||||
|
||||
// 데이터 소스 준비
|
||||
const sourceData: any = context.formData || {};
|
||||
// 데이터 소스 준비: context-data 모드는 배열을 기대함
|
||||
// 우선순위: selectedRowsData > savedData > formData
|
||||
// - selectedRowsData: 테이블 섹션에서 저장된 하위 항목들 (item_code, inbound_qty 등 포함)
|
||||
// - savedData: 저장 API 응답 데이터
|
||||
// - formData: 폼에 입력된 데이터
|
||||
let sourceData: any[];
|
||||
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
|
||||
sourceData = context.selectedRowsData;
|
||||
console.log("📦 [단일제어] selectedRowsData 사용:", sourceData.length, "건");
|
||||
} else {
|
||||
const savedData = context.savedData || context.formData || {};
|
||||
sourceData = Array.isArray(savedData) ? savedData : [savedData];
|
||||
console.log("📦 [단일제어] savedData/formData 사용:", sourceData.length, "건");
|
||||
}
|
||||
|
||||
// repeat-screen-modal 데이터가 있으면 병합
|
||||
const repeatScreenModalKeys = Object.keys(context.formData || {}).filter((key) =>
|
||||
|
|
@ -3765,7 +3835,8 @@ export class ButtonActionExecutor {
|
|||
console.log("📦 노드 플로우에 전달할 데이터:", {
|
||||
flowId,
|
||||
dataSourceType: controlDataSource,
|
||||
sourceData,
|
||||
sourceDataCount: sourceData.length,
|
||||
sourceDataSample: sourceData[0],
|
||||
});
|
||||
|
||||
const result = await executeNodeFlow(flowId, {
|
||||
|
|
|
|||
Loading…
Reference in New Issue