기능추가

This commit is contained in:
leeheejin 2025-11-06 17:32:29 +09:00
parent b3cd771b99
commit ead3433f3e
8 changed files with 232 additions and 147 deletions

View File

@ -303,8 +303,10 @@ export default function ScreenViewPage() {
style={{
transform: `scale(${scale})`,
transformOrigin: "top left",
width: containerWidth > 0 ? `${containerWidth / scale}px` : "100%",
minWidth: containerWidth > 0 ? `${containerWidth / scale}px` : "100%",
width: `${screenWidth}px`,
height: `${screenHeight}px`,
minWidth: `${screenWidth}px`,
minHeight: `${screenHeight}px`,
}}
>
{/* 최상위 컴포넌트들 렌더링 */}
@ -312,26 +314,9 @@ export default function ScreenViewPage() {
// 🆕 플로우 버튼 그룹 감지 및 처리
const topLevelComponents = layout.components.filter((component) => !component.parentId);
// 버튼은 scale에 맞춰 위치만 조정하면 됨 (scale = 1.0이면 그대로, scale < 1.0이면 왼쪽으로)
// 하지만 x=0 컴포넌트는 width: 100%로 확장되므로, 그만큼 버튼을 오른쪽으로 이동
const leftmostComponent = topLevelComponents.find((c) => c.position.x === 0);
let widthOffset = 0;
if (leftmostComponent && containerWidth > 0) {
const originalWidth = leftmostComponent.size?.width || screenWidth;
const actualWidth = containerWidth / scale;
widthOffset = Math.max(0, actualWidth - originalWidth);
console.log("📊 widthOffset 계산:", {
containerWidth,
scale,
screenWidth,
originalWidth,
actualWidth,
widthOffset,
leftmostType: leftmostComponent.type,
});
}
// 화면 관리에서 설정한 해상도를 사용하므로 widthOffset 계산 불필요
// 모든 컴포넌트는 원본 위치 그대로 사용
const widthOffset = 0;
const buttonGroups: Record<string, any[]> = {};
const processedButtonIds = new Set<string>();
@ -393,37 +378,11 @@ export default function ScreenViewPage() {
<>
{/* 일반 컴포넌트들 */}
{regularComponents.map((component) => {
// 버튼인 경우 위치 조정 (테이블이 늘어난 만큼 오른쪽으로 이동)
const isButton =
(component.type === "component" &&
["button-primary", "button-secondary"].includes((component as any).componentType)) ||
(component.type === "widget" && (component as any).widgetType === "button");
const adjustedComponent =
isButton && widthOffset > 0
? {
...component,
position: {
...component.position,
x: component.position.x + widthOffset,
},
}
: component;
// 버튼일 경우 로그 출력
if (isButton) {
console.log("🔘 버튼 위치 조정:", {
label: component.label,
originalX: component.position.x,
adjustedX: component.position.x + widthOffset,
widthOffset,
});
}
// 화면 관리 해상도를 사용하므로 위치 조정 불필요
return (
<RealtimePreview
key={component.id}
component={adjustedComponent}
component={component}
isSelected={false}
isDesignMode={false}
onClick={() => {}}

View File

@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect, useRef } from "react";
import {
ResizableDialog,
ResizableDialogContent,
@ -8,6 +8,8 @@ import {
ResizableDialogTitle,
ResizableDialogDescription,
} from "@/components/ui/resizable-dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/types/screen";
@ -52,6 +54,19 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 폼 데이터 상태 추가
const [formData, setFormData] = useState<Record<string, any>>({});
// 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록)
const continuousModeRef = useRef(false);
const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음)
// localStorage에서 연속 모드 상태 복원
useEffect(() => {
const savedMode = localStorage.getItem("screenModal_continuousMode");
if (savedMode === "true") {
continuousModeRef.current = true;
// console.log("🔄 연속 모드 복원: true");
}
}, []);
// 화면의 실제 크기 계산 함수
const calculateScreenDimensions = (components: ComponentData[]) => {
@ -124,16 +139,43 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
});
setScreenData(null);
setFormData({});
continuousModeRef.current = false;
localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장
// console.log("🔄 연속 모드 초기화: false");
};
// 저장 성공 이벤트 처리 (연속 등록 모드 지원)
const handleSaveSuccess = () => {
const isContinuousMode = continuousModeRef.current;
// console.log("💾 저장 성공 이벤트 수신");
// console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode);
// console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode"));
if (isContinuousMode) {
// 연속 모드: 폼만 초기화하고 모달은 유지
// console.log("✅ 연속 모드 활성화 - 폼만 초기화");
// 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨)
setFormData({});
toast.success("저장되었습니다. 계속 입력하세요.");
} else {
// 일반 모드: 모달 닫기
// console.log("❌ 일반 모드 - 모달 닫기");
handleCloseModal();
}
};
window.addEventListener("openScreenModal", handleOpenModal as EventListener);
window.addEventListener("closeSaveModal", handleCloseModal);
window.addEventListener("saveSuccessInModal", handleSaveSuccess);
return () => {
window.removeEventListener("openScreenModal", handleOpenModal as EventListener);
window.removeEventListener("closeSaveModal", handleCloseModal);
window.removeEventListener("saveSuccessInModal", handleSaveSuccess);
};
}, []);
}, []); // 의존성 제거 (ref 사용으로 최신 상태 참조)
// 화면 데이터 로딩
useEffect(() => {
@ -160,8 +202,25 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
if (screenInfo && layoutData) {
const components = layoutData.components || [];
// 화면의 실제 크기 계산
const dimensions = calculateScreenDimensions(components);
// 화면 관리에서 설정한 해상도 사용 (우선순위)
const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution;
let dimensions;
if (screenResolution && screenResolution.width && screenResolution.height) {
// 화면 관리에서 설정한 해상도 사용
dimensions = {
width: screenResolution.width,
height: screenResolution.height,
offsetX: 0,
offsetY: 0,
};
console.log("✅ 화면 관리 해상도 사용:", dimensions);
} else {
// 해상도 정보가 없으면 자동 계산
dimensions = calculateScreenDimensions(components);
console.log("⚠️ 자동 계산된 크기 사용:", dimensions);
}
setScreenDimensions(dimensions);
setScreenData({
@ -235,39 +294,39 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 1순위: screenId (가장 안정적)
if (modalState.screenId) {
newModalId = `screen-modal-${modalState.screenId}`;
console.log("🔑 ScreenModal modalId 생성:", {
method: "screenId",
screenId: modalState.screenId,
result: newModalId,
});
// console.log("🔑 ScreenModal modalId 생성:", {
// method: "screenId",
// screenId: modalState.screenId,
// result: newModalId,
// });
}
// 2순위: 테이블명
else if (screenData?.screenInfo?.tableName) {
newModalId = `screen-modal-table-${screenData.screenInfo.tableName}`;
console.log("🔑 ScreenModal modalId 생성:", {
method: "tableName",
tableName: screenData.screenInfo.tableName,
result: newModalId,
});
// console.log("🔑 ScreenModal modalId 생성:", {
// method: "tableName",
// tableName: screenData.screenInfo.tableName,
// result: newModalId,
// });
}
// 3순위: 화면명
else if (screenData?.screenInfo?.screenName) {
newModalId = `screen-modal-name-${screenData.screenInfo.screenName}`;
console.log("🔑 ScreenModal modalId 생성:", {
method: "screenName",
screenName: screenData.screenInfo.screenName,
result: newModalId,
});
// console.log("🔑 ScreenModal modalId 생성:", {
// method: "screenName",
// screenName: screenData.screenInfo.screenName,
// result: newModalId,
// });
}
// 4순위: 제목
else if (modalState.title) {
const titleId = modalState.title.replace(/\s+/g, '-');
const titleId = modalState.title.replace(/\s+/g, "-");
newModalId = `screen-modal-title-${titleId}`;
console.log("🔑 ScreenModal modalId 생성:", {
method: "title",
title: modalState.title,
result: newModalId,
});
// console.log("🔑 ScreenModal modalId 생성:", {
// method: "title",
// title: modalState.title,
// result: newModalId,
// });
}
if (newModalId) {
@ -325,11 +384,12 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
}}
>
{screenData.components.map((component) => {
// 컴포넌트 위치를 offset만큼 조정 (왼쪽 상단으로 정렬)
// 화면 관리 해상도를 사용하는 경우 offset 조정 불필요
const offsetX = screenDimensions?.offsetX || 0;
const offsetY = screenDimensions?.offsetY || 0;
const adjustedComponent = {
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : {
...component,
position: {
...component.position,
@ -345,14 +405,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
allComponents={screenData.components}
formData={formData}
onFormDataChange={(fieldName, value) => {
console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
console.log("📋 현재 formData:", formData);
// console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
// console.log("📋 현재 formData:", formData);
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
// console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
return newFormData;
});
}}
@ -370,6 +430,29 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div>
)}
</div>
{/* 연속 등록 모드 체크박스 */}
<div className="border-t px-4 py-3">
<div className="flex items-center gap-2">
<Checkbox
id="continuous-mode"
checked={continuousModeRef.current}
onCheckedChange={(checked) => {
const isChecked = checked === true;
continuousModeRef.current = isChecked;
localStorage.setItem("screenModal_continuousMode", String(isChecked));
setForceUpdate((prev) => prev + 1); // 체크박스 UI 업데이트를 위한 강제 리렌더링
// console.log("🔄 연속 모드 변경:", isChecked);
}}
/>
<Label
htmlFor="continuous-mode"
className="text-sm font-normal cursor-pointer select-none"
>
( )
</Label>
</div>
</div>
</ResizableDialogContent>
</ResizableDialog>
);

View File

@ -127,6 +127,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
if (e.key === "Enter" && !e.shiftKey) {
const target = e.target as HTMLElement;
// 한글 조합 중이면 무시 (한글 입력 문제 방지)
if ((e as any).isComposing || e.keyCode === 229) {
return;
}
// textarea는 제외 (여러 줄 입력)
if (target.tagName === "TEXTAREA") {
return;
@ -166,10 +171,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
const nextElement = focusableArray[currentIndex + 1];
nextElement.focus();
// input이면 전체 선택
if (nextElement.tagName === "INPUT") {
(nextElement as HTMLInputElement).select();
}
// select() 제거: 한글 입력 시 이전 필드의 마지막 글자가 복사되는 버그 방지
}
}
}
@ -543,13 +545,15 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
variant={(config?.variant as any) || "default"}
size={(config?.size as any) || "default"}
disabled={config?.disabled}
className="h-full w-full"
style={{
// 컴포넌트 스타일 먼저 적용
// 컴포넌트 스타일 적용
...comp.style,
// 설정값이 있으면 우선 적용
backgroundColor: config?.backgroundColor || comp.style?.backgroundColor,
color: config?.textColor || comp.style?.color,
// 부모 컨테이너 크기에 맞춤
width: '100%',
height: '100%',
}}
>
{label || "버튼"}

View File

@ -241,7 +241,17 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
return "100%";
}
// 3순위: size.width (픽셀)
// 3순위: size.width (픽셀) - 버튼의 경우 항상 픽셀 사용
if (isButtonComponent && size?.width) {
const width = `${size.width}px`;
console.log("🔘 [getWidth] 버튼 픽셀 사용:", {
componentId: id,
label: component.label,
width,
});
return width;
}
if (component.componentConfig?.type === "table-list") {
const width = `${Math.max(size?.width || 120, 120)}px`;
console.log("📏 [getWidth] 픽셀 사용 (table-list):", {

View File

@ -1267,21 +1267,6 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
zIndex: component.position.z || 1,
};
// 버튼 타입일 때 디버깅 (widget 타입 또는 component 타입 모두 체크)
if (
(component.type === "widget" && (component as any).widgetType === "button") ||
(component.type === "component" && (component as any).componentType?.includes("button"))
) {
console.log("🔘 ScreenList 버튼 외부 div 스타일:", {
id: component.id,
label: component.label,
position: component.position,
size: component.size,
componentStyle: component.style,
appliedStyle: style,
});
}
return style;
})()}
>

View File

@ -344,8 +344,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
window.dispatchEvent(new CustomEvent("closeEditModal"));
}
// ScreenModal은 항상 닫기
window.dispatchEvent(new CustomEvent("closeSaveModal"));
// ScreenModal은 연속 등록 모드를 지원하므로 saveSuccessInModal 이벤트 발생
window.dispatchEvent(new CustomEvent("saveSuccessInModal"));
}, 100);
}
}

View File

@ -1107,16 +1107,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const mapping = categoryMappings[column.columnName];
const categoryData = mapping?.[String(value)];
console.log(`🎨 [카테고리 배지] ${column.columnName}:`, {
value,
stringValue: String(value),
mapping,
categoryData,
hasMapping: !!mapping,
hasCategoryData: !!categoryData,
allCategoryMappings: categoryMappings, // 전체 매핑 확인
categoryMappingsKeys: Object.keys(categoryMappings),
});
// console.log(`🎨 [카테고리 배지] ${column.columnName}:`, {
// value,
// stringValue: String(value),
// mapping,
// categoryData,
// hasMapping: !!mapping,
// hasCategoryData: !!categoryData,
// allCategoryMappings: categoryMappings, // 전체 매핑 확인
// categoryMappingsKeys: Object.keys(categoryMappings),
// });
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
const displayLabel = categoryData?.label || String(value);

View File

@ -252,19 +252,19 @@ export class ButtonActionExecutor {
const writerValue = context.userId;
const companyCodeValue = context.companyCode || "";
console.log("👤 [buttonActions] 사용자 정보:", {
userId: context.userId,
userName: context.userName,
companyCode: context.companyCode, // ✅ 회사 코드
formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값
formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값
defaultWriterValue: writerValue,
companyCodeValue, // ✅ 최종 회사 코드 값
});
// console.log("👤 [buttonActions] 사용자 정보:", {
// userId: context.userId,
// userName: context.userName,
// companyCode: context.companyCode, // ✅ 회사 코드
// formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값
// formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값
// defaultWriterValue: writerValue,
// companyCodeValue, // ✅ 최종 회사 코드 값
// });
// 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)
console.log("🔍 채번 규칙 할당 체크 시작");
console.log("📦 현재 formData:", JSON.stringify(formData, null, 2));
// console.log("🔍 채번 규칙 할당 체크 시작");
// console.log("📦 현재 formData:", JSON.stringify(formData, null, 2));
const fieldsWithNumbering: Record<string, string> = {};
@ -273,26 +273,26 @@ export class ButtonActionExecutor {
if (key.endsWith("_numberingRuleId") && value) {
const fieldName = key.replace("_numberingRuleId", "");
fieldsWithNumbering[fieldName] = value as string;
console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`);
// console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`);
}
}
console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering);
console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length);
// console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering);
// console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length);
// 각 필드에 대해 실제 코드 할당
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
try {
console.log(`🎫 ${fieldName} 필드에 채번 규칙 ${ruleId} 할당 시작`);
// console.log(`🎫 ${fieldName} 필드에 채번 규칙 ${ruleId} 할당 시작`);
const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
const response = await allocateNumberingCode(ruleId);
console.log(`📡 API 응답 (${fieldName}):`, response);
// console.log(`📡 API 응답 (${fieldName}):`, response);
if (response.success && response.data) {
const generatedCode = response.data.generatedCode;
formData[fieldName] = generatedCode;
console.log(`${fieldName} = ${generatedCode} (할당 완료)`);
// console.log(`✅ ${fieldName} = ${generatedCode} (할당 완료)`);
} else {
console.error(`❌ 채번 규칙 할당 실패 (${fieldName}):`, response.error);
toast.error(`${fieldName} 채번 규칙 할당 실패: ${response.error}`);
@ -303,8 +303,8 @@ export class ButtonActionExecutor {
}
}
console.log("✅ 채번 규칙 할당 완료");
console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
// console.log("✅ 채번 규칙 할당 완료");
// console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
const dataWithUserInfo = {
...formData,
@ -345,8 +345,9 @@ export class ButtonActionExecutor {
context.onRefresh?.();
context.onFlowRefresh?.();
// 저장 성공 후 EditModal 닫기 이벤트 발생
window.dispatchEvent(new CustomEvent("closeEditModal"));
// 저장 성공 후 이벤트 발생
window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal 닫기
window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal 연속 등록 모드 처리
return true;
} catch (error) {
@ -932,45 +933,88 @@ export class ButtonActionExecutor {
console.log("📋 단일 항목 복사:", rowData);
console.log("📋 원본 데이터 키 목록:", Object.keys(rowData));
// 품목코드 필드 초기화 (여러 가능한 필드명 확인)
// 복사 시 제거할 필드들
const copiedData = { ...rowData };
const fieldsToRemove = [
// ID 필드 (새 레코드 생성)
"id",
"ID",
// 날짜 필드 (자동 생성)
"created_date",
"createdDate",
"updated_date",
"updatedDate",
"created_at",
"createdAt",
"updated_at",
"updatedAt",
"reg_date",
"regDate",
"mod_date",
"modDate",
];
// 제거할 필드 삭제
fieldsToRemove.forEach((field) => {
if (copiedData[field] !== undefined) {
delete copiedData[field];
console.log(`🗑️ 필드 제거: ${field}`);
}
});
// 품목코드 필드 초기화 (여러 가능한 필드명 확인)
const itemCodeFields = [
"item_code",
"itemCode",
"item_no",
"itemNo",
"item_number",
"itemNumber",
"품목코드",
"품번",
"code",
];
// 품목코드 필드를 찾아서 초기화
// 품목코드 필드를 찾아서 무조건 공백으로 초기화
let resetFieldName = "";
for (const field of itemCodeFields) {
if (copiedData[field] !== undefined) {
// 품목코드 필드를 빈 문자열로 초기화
// (저장 시점에 채번 규칙이 자동으로 적용됨)
copiedData[field] = "";
// 채번 규칙 ID도 함께 저장 (formData에 있을 경우)
const originalValue = copiedData[field];
const ruleIdKey = `${field}_numberingRuleId`;
if (rowData[ruleIdKey]) {
const hasNumberingRule = rowData[ruleIdKey] !== undefined && rowData[ruleIdKey] !== null && rowData[ruleIdKey] !== "";
// 품목코드를 무조건 공백으로 초기화
copiedData[field] = "";
// 채번 규칙 ID가 있으면 복사 (저장 시 자동 생성)
if (hasNumberingRule) {
copiedData[ruleIdKey] = rowData[ruleIdKey];
console.log(`✅ 품목코드 초기화 (채번 규칙 있음): ${field} (기존값: ${originalValue})`);
console.log(`📋 채번 규칙 ID 복사: ${ruleIdKey} = ${rowData[ruleIdKey]}`);
} else {
console.log(`✅ 품목코드 초기화 (수동 입력 필요): ${field} (기존값: ${originalValue})`);
}
resetFieldName = field;
console.log(`✅ 품목코드 필드 초기화: ${field} (기존값: ${rowData[field]})`);
break;
}
}
// 작성자 정보를 현재 사용자로 변경
const writerFields = ["writer", "creator", "reg_user", "regUser", "created_by", "createdBy"];
writerFields.forEach((field) => {
if (copiedData[field] !== undefined && context.userId) {
copiedData[field] = context.userId;
console.log(`👤 작성자 변경: ${field} = ${context.userId}`);
}
});
if (resetFieldName) {
toast.success(`품목코드(${resetFieldName})가 초기화되었습니다. 저장 시 자동으로 새 코드가 생성됩니다.`);
toast.success(`복사본이 생성되었습니다. 품목코드는 저장 시 자동으로 생성됩니다.`);
} else {
console.warn("⚠️ 품목코드 필드를 찾을 수 없습니다. 전체 데이터를 복사합니다.");
console.warn("⚠️ 사용 가능한 필드:", Object.keys(copiedData));
toast.info("복사본이 생성됩니다. (품목코드 필드를 찾을 수 없음)");
toast.info("복사본이 생성됩니다.");
}
console.log("📋 복사된 데이터:", copiedData);