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:
kjs 2025-12-12 13:50:49 +09:00
commit 309d4be31d
3 changed files with 149 additions and 36 deletions

View File

@ -1,13 +1,7 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
@ -183,15 +177,66 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
} else {
// 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정
// 1순위: 이벤트로 전달된 splitPanelParentData (탭 안에서 열린 모달)
// 2순위: splitPanelContext에서 직접 가져온 데이터 (분할 패널 내에서 열린 모달)
const parentData =
// 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 전달해야 함
// 모든 필드를 전달하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생
// 예: 설비의 manufacturer가 소모품의 manufacturer로 들어감
// parentDataMapping에서 명시된 필드만 추출
const parentDataMapping = splitPanelContext?.parentDataMapping || [];
// 부모 데이터 소스
const rawParentData =
splitPanelParentData && Object.keys(splitPanelParentData).length > 0
? 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) {
console.log("🔗 [ScreenModal] 분할 패널 부모 데이터 초기값 설정:", parentData);
console.log("🔗 [ScreenModal] 분할 패널 부모 데이터 초기값 설정 (연결 필드만):", parentData);
setFormData(parentData);
} else {
setFormData({});
@ -604,19 +649,15 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
<div className="flex items-center gap-2">
<DialogTitle className="text-base">{modalState.title}</DialogTitle>
{modalState.description && !loading && (
<DialogDescription className="text-muted-foreground text-xs">
{modalState.description}
</DialogDescription>
<DialogDescription className="text-muted-foreground text-xs">{modalState.description}</DialogDescription>
)}
{loading && (
<DialogDescription className="text-xs">
{loading ? "화면을 불러오는 중입니다..." : ""}
</DialogDescription>
<DialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</DialogDescription>
)}
</div>
</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 ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">

View File

@ -100,6 +100,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
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 (기본 표시 컬럼용)
// 예: item_code_name (item_name의 별칭)
if (fieldName === "item_name" || fieldName === "name") {
@ -107,6 +114,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (item[aliasKey] !== undefined) {
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에서 매핑 찾기 (화면 설정에서 지정된 경우)
@ -1023,7 +1035,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const uniqueValues = new Set<string>();
leftData.forEach((item) => {
// 🆕 조인 컬럼 처리 (item_info.standard → item_code_standard)
// 🆕 조인 컬럼 처리 (item_info.standard → item_code_standard 또는 item_id_standard)
let value: any;
if (columnName.includes(".")) {
@ -1035,10 +1047,21 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const exactKey = `${inferredSourceColumn}_${fieldName}`;
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")) {
const aliasKey = `${inferredSourceColumn}_name`;
value = item[aliasKey];
// item_id_name 패턴도 시도
if (value === undefined) {
const idAliasKey = `${refTable.replace("_info", "_id").replace("_mng", "_id")}_name`;
value = item[idAliasKey];
}
}
} else {
// 일반 컬럼

View File

@ -681,13 +681,52 @@ export class ButtonActionExecutor {
console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
// 🆕 분할 패널 부모 데이터 병합 (좌측 화면에서 선택된 데이터)
const splitPanelData = context.splitPanelParentData || {};
if (Object.keys(splitPanelData).length > 0) {
console.log("🔗 [handleSave] 분할 패널 부모 데이터 병합:", splitPanelData);
// 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 병합해야 함
// 모든 필드를 병합하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생
// 예: 설비의 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 = {
...splitPanelData, // 분할 패널 부모 데이터 먼저 적용
...cleanedSplitPanelData, // 정리된 분할 패널 부모 데이터 먼저 적용
...formData, // 폼 데이터가 우선 (덮어쓰기 가능)
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
created_by: writerValue, // created_by는 항상 로그인한 사람
@ -695,6 +734,12 @@ export class ButtonActionExecutor {
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 필드 제거 (실제 저장하지 않음)
for (const key of Object.keys(dataWithUserInfo)) {
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> {
// 모달 열기 로직
console.log("모달 열기:", {
console.log("모달 열기 (신규 등록 모드):", {
title: config.modalTitle,
size: config.modalSize,
targetScreenId: config.targetScreenId,
selectedRowsData: context.selectedRowsData,
// 🔧 selectedRowsData는 modal 액션에서 사용하지 않음 (신규 등록이므로)
});
if (config.targetScreenId) {
@ -1602,10 +1649,11 @@ export class ButtonActionExecutor {
}
}
// 🆕 선택된 행 데이터 수집
const selectedData = context.selectedRowsData || [];
console.log("📦 [handleModal] 선택된 데이터:", selectedData);
console.log("📦 [handleModal] 분할 패널 부모 데이터:", context.splitPanelParentData);
// 🔧 modal 액션은 신규 등록이므로 selectedData를 전달하지 않음
// selectedData가 있으면 ScreenModal에서 originalData로 인식하여 UPDATE 모드로 동작하게 됨
// edit 액션만 selectedData/editData를 사용하여 UPDATE 모드로 동작
console.log("📦 [handleModal] 신규 등록 모드 - selectedData 전달하지 않음");
console.log("📦 [handleModal] 분할 패널 부모 데이터 (초기값으로 사용):", context.splitPanelParentData);
// 전역 모달 상태 업데이트를 위한 이벤트 발생
const modalEvent = new CustomEvent("openScreenModal", {
@ -1614,10 +1662,11 @@ export class ButtonActionExecutor {
title: config.modalTitle || "화면",
description: description,
size: config.modalSize || "md",
// 🆕 선택된 행 데이터 전달
selectedData: selectedData,
selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
// 🆕 분할 패널 부모 데이터 전달 (탭 안 모달에서 사용)
// 🔧 신규 등록이므로 selectedData/selectedIds를 전달하지 않음
// edit 액션에서만 이 데이터를 사용
selectedData: [],
selectedIds: [],
// 🆕 분할 패널 부모 데이터 전달 (탭 안 모달에서 초기값으로 사용)
splitPanelParentData: context.splitPanelParentData || {},
},
});
@ -2663,7 +2712,7 @@ export class ButtonActionExecutor {
const { executeNodeFlow } = await import("@/lib/api/nodeFlows");
// 데이터 소스 준비
let sourceData: any = context.formData || {};
const sourceData: any = context.formData || {};
// repeat-screen-modal 데이터가 있으면 병합
const repeatScreenModalKeys = Object.keys(context.formData || {}).filter((key) =>