Merge branch 'ycshin-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-03-10 14:52:33 +09:00
54 changed files with 6530 additions and 749 deletions

View File

@@ -13,7 +13,6 @@ import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement";
import { ComponentData } from "@/lib/types/screen";
import { useAuth } from "@/hooks/useAuth";
interface SaveModalProps {
isOpen: boolean;
onClose: () => void;
@@ -124,9 +123,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
// 테이블 타입관리의 NOT NULL 설정 기반으로 필수 여부 판단
const isColumnRequired = (columnName: string): boolean => {
if (!columnName || tableColumnsInfo.length === 0) return false;
const colInfo = tableColumnsInfo.find(
(c) => c.columnName.toLowerCase() === columnName.toLowerCase()
);
const colInfo = tableColumnsInfo.find((c) => c.columnName.toLowerCase() === columnName.toLowerCase());
if (!colInfo) return false;
// is_nullable가 "NO"이면 필수
return colInfo.isNullable === "NO" || colInfo.isNullable === "N";
@@ -141,8 +138,8 @@ export const SaveModal: React.FC<SaveModalProps> = ({
const label = component.label || component.style?.label || columnName;
// 기존 required 속성 (화면 디자이너에서 수동 설정한 것)
const manualRequired =
component.required === true ||
const manualRequired =
component.required === true ||
component.style?.required === true ||
component.componentConfig?.required === true;
@@ -176,13 +173,6 @@ export const SaveModal: React.FC<SaveModalProps> = ({
return;
}
// ✅ 필수 항목 검증
const validation = validateRequiredFields();
if (!validation.isValid) {
toast.error(`필수 항목을 입력해주세요: ${validation.missingFields.join(", ")}`);
return;
}
try {
setIsSaving(true);
@@ -210,7 +200,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
// 🆕 자동으로 작성자 정보 추가 (user.userId가 확실히 있음)
const writerValue = user.userId;
const companyCodeValue = user.companyCode || "";
console.log("👤 현재 사용자 정보:", {
userId: user.userId,
userName: userName,
@@ -255,11 +245,11 @@ export const SaveModal: React.FC<SaveModalProps> = ({
if (result.success) {
const masterRecordId = result.data?.id || dataToSave.id;
// 🆕 리피터 데이터 저장 이벤트 발생 (V2Repeater 컴포넌트가 리스닝)
window.dispatchEvent(
new CustomEvent("repeaterSave", {
detail: {
detail: {
parentId: masterRecordId,
masterRecordId,
mainFormData: dataToSave,
@@ -268,7 +258,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
}),
);
console.log("📋 [SaveModal] repeaterSave 이벤트 발생:", { masterRecordId, tableName });
// ✅ 저장 성공
toast.success(initialData ? "수정되었습니다!" : "저장되었습니다!");
@@ -300,29 +290,29 @@ export const SaveModal: React.FC<SaveModalProps> = ({
const calculateDynamicSize = () => {
if (!components.length) return { width: 800, height: 600 };
const maxX = Math.max(...components.map((c) => {
const x = c.position?.x || 0;
const width = typeof c.size?.width === 'number'
? c.size.width
: parseInt(String(c.size?.width || 200), 10);
return x + width;
}));
const maxY = Math.max(...components.map((c) => {
const y = c.position?.y || 0;
const height = typeof c.size?.height === 'number'
? c.size.height
: parseInt(String(c.size?.height || 40), 10);
return y + height;
}));
const maxX = Math.max(
...components.map((c) => {
const x = c.position?.x || 0;
const width = typeof c.size?.width === "number" ? c.size.width : parseInt(String(c.size?.width || 200), 10);
return x + width;
}),
);
const maxY = Math.max(
...components.map((c) => {
const y = c.position?.y || 0;
const height = typeof c.size?.height === "number" ? c.size.height : parseInt(String(c.size?.height || 40), 10);
return y + height;
}),
);
// 컨텐츠 영역 크기 (화면관리 설정 크기)
const contentWidth = Math.max(maxX, 400);
const contentHeight = Math.max(maxY, 300);
// 실제 모달 크기 = 컨텐츠 + 헤더
const headerHeight = 60; // DialogHeader
return {
width: contentWidth,
height: contentHeight + headerHeight, // 헤더 높이 포함
@@ -340,9 +330,9 @@ export const SaveModal: React.FC<SaveModalProps> = ({
minWidth: "400px",
minHeight: "300px",
}}
className="gap-0 p-0 max-w-none"
className="max-w-none gap-0 p-0"
>
<DialogHeader className="border-b px-6 py-4 flex-shrink-0">
<DialogHeader className="flex-shrink-0 border-b px-6 py-4">
<div className="flex items-center justify-between">
<DialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</DialogTitle>
<div className="flex items-center gap-2">
@@ -366,7 +356,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
</div>
</DialogHeader>
<div className="overflow-auto p-6 flex-1">
<div className="flex-1 overflow-auto p-6">
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
@@ -384,90 +374,92 @@ export const SaveModal: React.FC<SaveModalProps> = ({
<div className="relative" style={{ width: `${dynamicSize.width}px`, height: `${dynamicSize.height}px` }}>
{components.map((component, index) => {
// ✅ 격자 시스템 잔재 제거: size의 픽셀 값만 사용
const widthPx = typeof component.size?.width === 'number'
? component.size.width
: parseInt(String(component.size?.width || 200), 10);
const heightPx = typeof component.size?.height === 'number'
? component.size.height
: parseInt(String(component.size?.height || 40), 10);
const widthPx =
typeof component.size?.width === "number"
? component.size.width
: parseInt(String(component.size?.width || 200), 10);
const heightPx =
typeof component.size?.height === "number"
? component.size.height
: parseInt(String(component.size?.height || 40), 10);
// 디버깅: 실제 크기 확인
if (index === 0) {
console.log('🔍 SaveModal 컴포넌트 크기:', {
console.log("🔍 SaveModal 컴포넌트 크기:", {
componentId: component.id,
'size.width (원본)': component.size?.width,
'size.width 타입': typeof component.size?.width,
'widthPx (계산)': widthPx,
'style.width': component.style?.width,
"size.width (원본)": component.size?.width,
"size.width 타입": typeof component.size?.width,
"widthPx (계산)": widthPx,
"style.width": component.style?.width,
});
}
return (
<div
key={component.id}
style={{
position: "absolute",
top: component.position?.y || 0,
left: component.position?.x || 0,
width: `${widthPx}px`, // ✅ 픽셀 단위 강제
height: `${heightPx}px`, // ✅ 픽셀 단위 강제
zIndex: component.position?.z || 1000 + index,
}}
>
{component.type === "widget" ? (
<InteractiveScreenViewer
component={component}
allComponents={components}
formData={formData}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
}}
hideLabel={false}
menuObjid={menuObjid}
tableColumns={tableColumnsInfo as any}
/>
) : (
<DynamicComponentRenderer
component={{
...component,
style: {
...component.style,
labelDisplay: true,
},
}}
screenId={screenId}
tableName={screenData.tableName}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 (카테고리 스코프용)
userId={user?.userId} // ✅ 사용자 ID 전달
userName={user?.userName} // ✅ 사용자 이름 전달
companyCode={user?.companyCode} // ✅ 회사 코드 전달
formData={formData}
originalData={originalData}
onFormDataChange={(fieldName, value) => {
console.log("📝 SaveModal - formData 변경:", {
fieldName,
value,
componentType: component.type,
componentId: component.id,
});
setFormData((prev) => {
const newData = {
<div
key={component.id}
style={{
position: "absolute",
top: component.position?.y || 0,
left: component.position?.x || 0,
width: `${widthPx}px`, // ✅ 픽셀 단위 강제
height: `${heightPx}px`, // ✅ 픽셀 단위 강제
zIndex: component.position?.z || 1000 + index,
}}
>
{component.type === "widget" ? (
<InteractiveScreenViewer
component={component}
allComponents={components}
formData={formData}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({
...prev,
[fieldName]: value,
};
console.log("📦 새 formData:", newData);
return newData;
});
}}
mode="edit"
isInModal={true}
isInteractive={true}
/>
)}
</div>
}));
}}
hideLabel={false}
menuObjid={menuObjid}
tableColumns={tableColumnsInfo as any}
/>
) : (
<DynamicComponentRenderer
component={{
...component,
style: {
...component.style,
labelDisplay: true,
},
}}
screenId={screenId}
tableName={screenData.tableName}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 (카테고리 스코프용)
userId={user?.userId} // ✅ 사용자 ID 전달
userName={user?.userName} // ✅ 사용자 이름 전달
companyCode={user?.companyCode} // ✅ 회사 코드 전달
formData={formData}
originalData={originalData}
onFormDataChange={(fieldName, value) => {
console.log("📝 SaveModal - formData 변경:", {
fieldName,
value,
componentType: component.type,
componentId: component.id,
});
setFormData((prev) => {
const newData = {
...prev,
[fieldName]: value,
};
console.log("📦 새 formData:", newData);
return newData;
});
}}
mode="edit"
isInModal={true}
isInteractive={true}
/>
)}
</div>
);
})}
</div>