Merge pull request 'feature/screen-management' (#283) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/283
This commit is contained in:
commit
309d4be31d
|
|
@ -1,13 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
||||||
|
|
@ -183,15 +177,66 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
||||||
} else {
|
} else {
|
||||||
// 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정
|
// 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정
|
||||||
// 1순위: 이벤트로 전달된 splitPanelParentData (탭 안에서 열린 모달)
|
// 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 전달해야 함
|
||||||
// 2순위: splitPanelContext에서 직접 가져온 데이터 (분할 패널 내에서 열린 모달)
|
// 모든 필드를 전달하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생
|
||||||
const parentData =
|
// 예: 설비의 manufacturer가 소모품의 manufacturer로 들어감
|
||||||
|
|
||||||
|
// parentDataMapping에서 명시된 필드만 추출
|
||||||
|
const parentDataMapping = splitPanelContext?.parentDataMapping || [];
|
||||||
|
|
||||||
|
// 부모 데이터 소스
|
||||||
|
const rawParentData =
|
||||||
splitPanelParentData && Object.keys(splitPanelParentData).length > 0
|
splitPanelParentData && Object.keys(splitPanelParentData).length > 0
|
||||||
? splitPanelParentData
|
? splitPanelParentData
|
||||||
: splitPanelContext?.getMappedParentData() || {};
|
: splitPanelContext?.selectedLeftData || {};
|
||||||
|
|
||||||
|
// 🔧 신규 등록 모드에서는 연결에 필요한 필드만 전달
|
||||||
|
const parentData: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 필수 연결 필드: company_code (멀티테넌시)
|
||||||
|
if (rawParentData.company_code) {
|
||||||
|
parentData.company_code = rawParentData.company_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentDataMapping에 정의된 필드만 전달
|
||||||
|
for (const mapping of parentDataMapping) {
|
||||||
|
const sourceValue = rawParentData[mapping.sourceColumn];
|
||||||
|
if (sourceValue !== undefined && sourceValue !== null) {
|
||||||
|
parentData[mapping.targetColumn] = sourceValue;
|
||||||
|
console.log(
|
||||||
|
`🔗 [ScreenModal] 매핑 필드 전달: ${mapping.sourceColumn} → ${mapping.targetColumn} = ${sourceValue}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentDataMapping이 비어있으면 연결 필드 자동 감지 (equipment_code, xxx_code, xxx_id 패턴)
|
||||||
|
if (parentDataMapping.length === 0) {
|
||||||
|
const linkFieldPatterns = ["_code", "_id"];
|
||||||
|
const excludeFields = [
|
||||||
|
"id",
|
||||||
|
"company_code",
|
||||||
|
"created_date",
|
||||||
|
"updated_date",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"writer",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rawParentData)) {
|
||||||
|
if (excludeFields.includes(key)) continue;
|
||||||
|
if (value === undefined || value === null) continue;
|
||||||
|
|
||||||
|
// 연결 필드 패턴 확인
|
||||||
|
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
||||||
|
if (isLinkField) {
|
||||||
|
parentData[key] = value;
|
||||||
|
console.log(`🔗 [ScreenModal] 연결 필드 자동 감지: ${key} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(parentData).length > 0) {
|
if (Object.keys(parentData).length > 0) {
|
||||||
console.log("🔗 [ScreenModal] 분할 패널 부모 데이터 초기값 설정:", parentData);
|
console.log("🔗 [ScreenModal] 분할 패널 부모 데이터 초기값 설정 (연결 필드만):", parentData);
|
||||||
setFormData(parentData);
|
setFormData(parentData);
|
||||||
} else {
|
} else {
|
||||||
setFormData({});
|
setFormData({});
|
||||||
|
|
@ -604,19 +649,15 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<DialogTitle className="text-base">{modalState.title}</DialogTitle>
|
<DialogTitle className="text-base">{modalState.title}</DialogTitle>
|
||||||
{modalState.description && !loading && (
|
{modalState.description && !loading && (
|
||||||
<DialogDescription className="text-muted-foreground text-xs">
|
<DialogDescription className="text-muted-foreground text-xs">{modalState.description}</DialogDescription>
|
||||||
{modalState.description}
|
|
||||||
</DialogDescription>
|
|
||||||
)}
|
)}
|
||||||
{loading && (
|
{loading && (
|
||||||
<DialogDescription className="text-xs">
|
<DialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</DialogDescription>
|
||||||
{loading ? "화면을 불러오는 중입니다..." : ""}
|
|
||||||
</DialogDescription>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-transparent">
|
<div className="flex-1 overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return item[exactKey];
|
return item[exactKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 2-1️⃣ item_id 패턴 시도 (백엔드가 item_id_xxx 형식으로 반환하는 경우)
|
||||||
|
// 예: item_info.item_name → item_id_item_name
|
||||||
|
const idPatternKey = `${tableName.replace("_info", "_id").replace("_mng", "_id")}_${fieldName}`;
|
||||||
|
if (item[idPatternKey] !== undefined) {
|
||||||
|
return item[idPatternKey];
|
||||||
|
}
|
||||||
|
|
||||||
// 3️⃣ 별칭 패턴: 소스컬럼_name (기본 표시 컬럼용)
|
// 3️⃣ 별칭 패턴: 소스컬럼_name (기본 표시 컬럼용)
|
||||||
// 예: item_code_name (item_name의 별칭)
|
// 예: item_code_name (item_name의 별칭)
|
||||||
if (fieldName === "item_name" || fieldName === "name") {
|
if (fieldName === "item_name" || fieldName === "name") {
|
||||||
|
|
@ -107,6 +114,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
if (item[aliasKey] !== undefined) {
|
if (item[aliasKey] !== undefined) {
|
||||||
return item[aliasKey];
|
return item[aliasKey];
|
||||||
}
|
}
|
||||||
|
// 🆕 item_id_name 패턴도 시도
|
||||||
|
const idAliasKey = `${tableName.replace("_info", "_id").replace("_mng", "_id")}_name`;
|
||||||
|
if (item[idAliasKey] !== undefined) {
|
||||||
|
return item[idAliasKey];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4️⃣ entityColumnMap에서 매핑 찾기 (화면 설정에서 지정된 경우)
|
// 4️⃣ entityColumnMap에서 매핑 찾기 (화면 설정에서 지정된 경우)
|
||||||
|
|
@ -1023,7 +1035,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const uniqueValues = new Set<string>();
|
const uniqueValues = new Set<string>();
|
||||||
|
|
||||||
leftData.forEach((item) => {
|
leftData.forEach((item) => {
|
||||||
// 🆕 조인 컬럼 처리 (item_info.standard → item_code_standard)
|
// 🆕 조인 컬럼 처리 (item_info.standard → item_code_standard 또는 item_id_standard)
|
||||||
let value: any;
|
let value: any;
|
||||||
|
|
||||||
if (columnName.includes(".")) {
|
if (columnName.includes(".")) {
|
||||||
|
|
@ -1035,10 +1047,21 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const exactKey = `${inferredSourceColumn}_${fieldName}`;
|
const exactKey = `${inferredSourceColumn}_${fieldName}`;
|
||||||
value = item[exactKey];
|
value = item[exactKey];
|
||||||
|
|
||||||
// 기본 별칭 패턴 시도 (item_code_name)
|
// 🆕 item_id 패턴 시도
|
||||||
|
if (value === undefined) {
|
||||||
|
const idPatternKey = `${refTable.replace("_info", "_id").replace("_mng", "_id")}_${fieldName}`;
|
||||||
|
value = item[idPatternKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 별칭 패턴 시도 (item_code_name 또는 item_id_name)
|
||||||
if (value === undefined && (fieldName === "item_name" || fieldName === "name")) {
|
if (value === undefined && (fieldName === "item_name" || fieldName === "name")) {
|
||||||
const aliasKey = `${inferredSourceColumn}_name`;
|
const aliasKey = `${inferredSourceColumn}_name`;
|
||||||
value = item[aliasKey];
|
value = item[aliasKey];
|
||||||
|
// item_id_name 패턴도 시도
|
||||||
|
if (value === undefined) {
|
||||||
|
const idAliasKey = `${refTable.replace("_info", "_id").replace("_mng", "_id")}_name`;
|
||||||
|
value = item[idAliasKey];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 일반 컬럼
|
// 일반 컬럼
|
||||||
|
|
|
||||||
|
|
@ -681,13 +681,52 @@ export class ButtonActionExecutor {
|
||||||
console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
|
console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
|
||||||
|
|
||||||
// 🆕 분할 패널 부모 데이터 병합 (좌측 화면에서 선택된 데이터)
|
// 🆕 분할 패널 부모 데이터 병합 (좌측 화면에서 선택된 데이터)
|
||||||
const splitPanelData = context.splitPanelParentData || {};
|
// 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 병합해야 함
|
||||||
if (Object.keys(splitPanelData).length > 0) {
|
// 모든 필드를 병합하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생
|
||||||
console.log("🔗 [handleSave] 분할 패널 부모 데이터 병합:", splitPanelData);
|
// 예: 설비의 manufacturer가 소모품의 manufacturer로 들어감
|
||||||
|
const rawSplitPanelData = context.splitPanelParentData || {};
|
||||||
|
|
||||||
|
// INSERT 모드에서는 연결에 필요한 필드만 추출
|
||||||
|
const cleanedSplitPanelData: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 필수 연결 필드: company_code (멀티테넌시)
|
||||||
|
if (rawSplitPanelData.company_code) {
|
||||||
|
cleanedSplitPanelData.company_code = rawSplitPanelData.company_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연결 필드 패턴으로 자동 감지 (equipment_code, xxx_code, xxx_id 패턴)
|
||||||
|
const linkFieldPatterns = ["_code", "_id"];
|
||||||
|
const excludeFields = [
|
||||||
|
"id",
|
||||||
|
"company_code",
|
||||||
|
"created_date",
|
||||||
|
"updated_date",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"writer",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rawSplitPanelData)) {
|
||||||
|
if (excludeFields.includes(key)) continue;
|
||||||
|
if (value === undefined || value === null) continue;
|
||||||
|
|
||||||
|
// 연결 필드 패턴 확인
|
||||||
|
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
||||||
|
if (isLinkField) {
|
||||||
|
cleanedSplitPanelData[key] = value;
|
||||||
|
console.log(`🔗 [handleSave] INSERT 모드 - 연결 필드만 병합: ${key} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(rawSplitPanelData).length > 0) {
|
||||||
|
console.log("🧹 [handleSave] 원본 분할 패널 부모 데이터:", Object.keys(rawSplitPanelData));
|
||||||
|
console.log("🧹 [handleSave] 정리된 분할 패널 부모 데이터 (연결 필드만):", cleanedSplitPanelData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataWithUserInfo = {
|
const dataWithUserInfo = {
|
||||||
...splitPanelData, // 분할 패널 부모 데이터 먼저 적용
|
...cleanedSplitPanelData, // 정리된 분할 패널 부모 데이터 먼저 적용
|
||||||
...formData, // 폼 데이터가 우선 (덮어쓰기 가능)
|
...formData, // 폼 데이터가 우선 (덮어쓰기 가능)
|
||||||
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
||||||
created_by: writerValue, // created_by는 항상 로그인한 사람
|
created_by: writerValue, // created_by는 항상 로그인한 사람
|
||||||
|
|
@ -695,6 +734,12 @@ export class ButtonActionExecutor {
|
||||||
company_code: formData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
|
company_code: formData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🔧 formData에서도 id 제거 (신규 INSERT이므로)
|
||||||
|
if ("id" in dataWithUserInfo && !formData.id) {
|
||||||
|
console.log("🗑️ [handleSave] INSERT 모드 - dataWithUserInfo에서 id 제거:", dataWithUserInfo.id);
|
||||||
|
delete dataWithUserInfo.id;
|
||||||
|
}
|
||||||
|
|
||||||
// _numberingRuleId 필드 제거 (실제 저장하지 않음)
|
// _numberingRuleId 필드 제거 (실제 저장하지 않음)
|
||||||
for (const key of Object.keys(dataWithUserInfo)) {
|
for (const key of Object.keys(dataWithUserInfo)) {
|
||||||
if (key.endsWith("_numberingRuleId")) {
|
if (key.endsWith("_numberingRuleId")) {
|
||||||
|
|
@ -1578,14 +1623,16 @@ export class ButtonActionExecutor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모달 액션 처리
|
* 모달 액션 처리
|
||||||
|
* 🔧 modal 액션은 항상 신규 등록(INSERT) 모드로 동작
|
||||||
|
* edit 액션만 수정(UPDATE) 모드로 동작해야 함
|
||||||
*/
|
*/
|
||||||
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
// 모달 열기 로직
|
// 모달 열기 로직
|
||||||
console.log("모달 열기:", {
|
console.log("모달 열기 (신규 등록 모드):", {
|
||||||
title: config.modalTitle,
|
title: config.modalTitle,
|
||||||
size: config.modalSize,
|
size: config.modalSize,
|
||||||
targetScreenId: config.targetScreenId,
|
targetScreenId: config.targetScreenId,
|
||||||
selectedRowsData: context.selectedRowsData,
|
// 🔧 selectedRowsData는 modal 액션에서 사용하지 않음 (신규 등록이므로)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.targetScreenId) {
|
if (config.targetScreenId) {
|
||||||
|
|
@ -1602,10 +1649,11 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 선택된 행 데이터 수집
|
// 🔧 modal 액션은 신규 등록이므로 selectedData를 전달하지 않음
|
||||||
const selectedData = context.selectedRowsData || [];
|
// selectedData가 있으면 ScreenModal에서 originalData로 인식하여 UPDATE 모드로 동작하게 됨
|
||||||
console.log("📦 [handleModal] 선택된 데이터:", selectedData);
|
// edit 액션만 selectedData/editData를 사용하여 UPDATE 모드로 동작
|
||||||
console.log("📦 [handleModal] 분할 패널 부모 데이터:", context.splitPanelParentData);
|
console.log("📦 [handleModal] 신규 등록 모드 - selectedData 전달하지 않음");
|
||||||
|
console.log("📦 [handleModal] 분할 패널 부모 데이터 (초기값으로 사용):", context.splitPanelParentData);
|
||||||
|
|
||||||
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
||||||
const modalEvent = new CustomEvent("openScreenModal", {
|
const modalEvent = new CustomEvent("openScreenModal", {
|
||||||
|
|
@ -1614,10 +1662,11 @@ export class ButtonActionExecutor {
|
||||||
title: config.modalTitle || "화면",
|
title: config.modalTitle || "화면",
|
||||||
description: description,
|
description: description,
|
||||||
size: config.modalSize || "md",
|
size: config.modalSize || "md",
|
||||||
// 🆕 선택된 행 데이터 전달
|
// 🔧 신규 등록이므로 selectedData/selectedIds를 전달하지 않음
|
||||||
selectedData: selectedData,
|
// edit 액션에서만 이 데이터를 사용
|
||||||
selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
|
selectedData: [],
|
||||||
// 🆕 분할 패널 부모 데이터 전달 (탭 안 모달에서 사용)
|
selectedIds: [],
|
||||||
|
// 🆕 분할 패널 부모 데이터 전달 (탭 안 모달에서 초기값으로 사용)
|
||||||
splitPanelParentData: context.splitPanelParentData || {},
|
splitPanelParentData: context.splitPanelParentData || {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -2663,7 +2712,7 @@ export class ButtonActionExecutor {
|
||||||
const { executeNodeFlow } = await import("@/lib/api/nodeFlows");
|
const { executeNodeFlow } = await import("@/lib/api/nodeFlows");
|
||||||
|
|
||||||
// 데이터 소스 준비
|
// 데이터 소스 준비
|
||||||
let sourceData: any = context.formData || {};
|
const sourceData: any = context.formData || {};
|
||||||
|
|
||||||
// repeat-screen-modal 데이터가 있으면 병합
|
// repeat-screen-modal 데이터가 있으면 병합
|
||||||
const repeatScreenModalKeys = Object.keys(context.formData || {}).filter((key) =>
|
const repeatScreenModalKeys = Object.keys(context.formData || {}).filter((key) =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue