Compare commits

...

5 Commits

12 changed files with 771 additions and 229 deletions

View File

@ -303,8 +303,10 @@ export default function ScreenViewPage() {
style={{ style={{
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: "top left", transformOrigin: "top left",
width: containerWidth > 0 ? `${containerWidth / scale}px` : "100%", width: `${screenWidth}px`,
minWidth: containerWidth > 0 ? `${containerWidth / scale}px` : "100%", 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); const topLevelComponents = layout.components.filter((component) => !component.parentId);
// 버튼은 scale에 맞춰 위치만 조정하면 됨 (scale = 1.0이면 그대로, scale < 1.0이면 왼쪽으로) // 화면 관리에서 설정한 해상도를 사용하므로 widthOffset 계산 불필요
// 하지만 x=0 컴포넌트는 width: 100%로 확장되므로, 그만큼 버튼을 오른쪽으로 이동 // 모든 컴포넌트는 원본 위치 그대로 사용
const leftmostComponent = topLevelComponents.find((c) => c.position.x === 0); const widthOffset = 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,
});
}
const buttonGroups: Record<string, any[]> = {}; const buttonGroups: Record<string, any[]> = {};
const processedButtonIds = new Set<string>(); const processedButtonIds = new Set<string>();
@ -393,37 +378,11 @@ export default function ScreenViewPage() {
<> <>
{/* 일반 컴포넌트들 */} {/* 일반 컴포넌트들 */}
{regularComponents.map((component) => { {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 ( return (
<RealtimePreview <RealtimePreview
key={component.id} key={component.id}
component={adjustedComponent} component={component}
isSelected={false} isSelected={false}
isDesignMode={false} isDesignMode={false}
onClick={() => {}} onClick={() => {}}

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useState, useEffect, useMemo } from "react"; import React, { useState, useEffect, useRef } from "react";
import { import {
ResizableDialog, ResizableDialog,
ResizableDialogContent, ResizableDialogContent,
@ -8,6 +8,8 @@ import {
ResizableDialogTitle, ResizableDialogTitle,
ResizableDialogDescription, ResizableDialogDescription,
} from "@/components/ui/resizable-dialog"; } from "@/components/ui/resizable-dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
import { screenApi } from "@/lib/api/screen"; import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/types/screen"; import { ComponentData } from "@/types/screen";
@ -53,6 +55,19 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 폼 데이터 상태 추가 // 폼 데이터 상태 추가
const [formData, setFormData] = useState<Record<string, any>>({}); 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[]) => { const calculateScreenDimensions = (components: ComponentData[]) => {
if (components.length === 0) { if (components.length === 0) {
@ -124,16 +139,43 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
}); });
setScreenData(null); setScreenData(null);
setFormData({}); 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("openScreenModal", handleOpenModal as EventListener);
window.addEventListener("closeSaveModal", handleCloseModal); window.addEventListener("closeSaveModal", handleCloseModal);
window.addEventListener("saveSuccessInModal", handleSaveSuccess);
return () => { return () => {
window.removeEventListener("openScreenModal", handleOpenModal as EventListener); window.removeEventListener("openScreenModal", handleOpenModal as EventListener);
window.removeEventListener("closeSaveModal", handleCloseModal); window.removeEventListener("closeSaveModal", handleCloseModal);
window.removeEventListener("saveSuccessInModal", handleSaveSuccess);
}; };
}, []); }, []); // 의존성 제거 (ref 사용으로 최신 상태 참조)
// 화면 데이터 로딩 // 화면 데이터 로딩
useEffect(() => { useEffect(() => {
@ -160,8 +202,25 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
if (screenInfo && layoutData) { if (screenInfo && layoutData) {
const components = layoutData.components || []; 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); setScreenDimensions(dimensions);
setScreenData({ setScreenData({
@ -235,39 +294,39 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 1순위: screenId (가장 안정적) // 1순위: screenId (가장 안정적)
if (modalState.screenId) { if (modalState.screenId) {
newModalId = `screen-modal-${modalState.screenId}`; newModalId = `screen-modal-${modalState.screenId}`;
console.log("🔑 ScreenModal modalId 생성:", { // console.log("🔑 ScreenModal modalId 생성:", {
method: "screenId", // method: "screenId",
screenId: modalState.screenId, // screenId: modalState.screenId,
result: newModalId, // result: newModalId,
}); // });
} }
// 2순위: 테이블명 // 2순위: 테이블명
else if (screenData?.screenInfo?.tableName) { else if (screenData?.screenInfo?.tableName) {
newModalId = `screen-modal-table-${screenData.screenInfo.tableName}`; newModalId = `screen-modal-table-${screenData.screenInfo.tableName}`;
console.log("🔑 ScreenModal modalId 생성:", { // console.log("🔑 ScreenModal modalId 생성:", {
method: "tableName", // method: "tableName",
tableName: screenData.screenInfo.tableName, // tableName: screenData.screenInfo.tableName,
result: newModalId, // result: newModalId,
}); // });
} }
// 3순위: 화면명 // 3순위: 화면명
else if (screenData?.screenInfo?.screenName) { else if (screenData?.screenInfo?.screenName) {
newModalId = `screen-modal-name-${screenData.screenInfo.screenName}`; newModalId = `screen-modal-name-${screenData.screenInfo.screenName}`;
console.log("🔑 ScreenModal modalId 생성:", { // console.log("🔑 ScreenModal modalId 생성:", {
method: "screenName", // method: "screenName",
screenName: screenData.screenInfo.screenName, // screenName: screenData.screenInfo.screenName,
result: newModalId, // result: newModalId,
}); // });
} }
// 4순위: 제목 // 4순위: 제목
else if (modalState.title) { else if (modalState.title) {
const titleId = modalState.title.replace(/\s+/g, '-'); const titleId = modalState.title.replace(/\s+/g, "-");
newModalId = `screen-modal-title-${titleId}`; newModalId = `screen-modal-title-${titleId}`;
console.log("🔑 ScreenModal modalId 생성:", { // console.log("🔑 ScreenModal modalId 생성:", {
method: "title", // method: "title",
title: modalState.title, // title: modalState.title,
result: newModalId, // result: newModalId,
}); // });
} }
if (newModalId) { if (newModalId) {
@ -325,11 +384,12 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
}} }}
> >
{screenData.components.map((component) => { {screenData.components.map((component) => {
// 컴포넌트 위치를 offset만큼 조정 (왼쪽 상단으로 정렬) // 화면 관리 해상도를 사용하는 경우 offset 조정 불필요
const offsetX = screenDimensions?.offsetX || 0; const offsetX = screenDimensions?.offsetX || 0;
const offsetY = screenDimensions?.offsetY || 0; const offsetY = screenDimensions?.offsetY || 0;
const adjustedComponent = { // offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : {
...component, ...component,
position: { position: {
...component.position, ...component.position,
@ -345,14 +405,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
allComponents={screenData.components} allComponents={screenData.components}
formData={formData} formData={formData}
onFormDataChange={(fieldName, value) => { onFormDataChange={(fieldName, value) => {
console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`); // console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
console.log("📋 현재 formData:", formData); // console.log("📋 현재 formData:", formData);
setFormData((prev) => { setFormData((prev) => {
const newFormData = { const newFormData = {
...prev, ...prev,
[fieldName]: value, [fieldName]: value,
}; };
console.log("📝 ScreenModal 업데이트된 formData:", newFormData); // console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
return newFormData; return newFormData;
}); });
}} }}
@ -370,6 +430,29 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div> </div>
)} )}
</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> </ResizableDialogContent>
</ResizableDialog> </ResizableDialog>
); );

View File

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

View File

@ -241,7 +241,17 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
return "100%"; 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") { if (component.componentConfig?.type === "table-list") {
const width = `${Math.max(size?.width || 120, 120)}px`; const width = `${Math.max(size?.width || 120, 120)}px`;
console.log("📏 [getWidth] 픽셀 사용 (table-list):", { console.log("📏 [getWidth] 픽셀 사용 (table-list):", {

View File

@ -1253,21 +1253,6 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
zIndex: component.position.z || 1, 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; return style;
})()} })()}
> >

View File

@ -242,6 +242,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
<SelectItem value="cancel"></SelectItem> <SelectItem value="cancel"></SelectItem>
<SelectItem value="delete"></SelectItem> <SelectItem value="delete"></SelectItem>
<SelectItem value="edit"></SelectItem> <SelectItem value="edit"></SelectItem>
<SelectItem value="copy"> ( )</SelectItem>
<SelectItem value="add"></SelectItem> <SelectItem value="add"></SelectItem>
<SelectItem value="search"></SelectItem> <SelectItem value="search"></SelectItem>
<SelectItem value="reset"></SelectItem> <SelectItem value="reset"></SelectItem>
@ -386,6 +387,71 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </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={modalSearchTerm}
onChange={(e) => setModalSearchTerm(e.target.value)}
className="border-0 p-0 focus-visible:ring-0"
/>
</div>
<div className="max-h-[200px] overflow-auto">
{(() => {
const filteredScreens = filterScreens(modalSearchTerm);
if (screensLoading) {
return <div className="p-3 text-sm text-gray-500"> ...</div>;
}
if (filteredScreens.length === 0) {
return <div className="p-3 text-sm text-gray-500"> .</div>;
}
return filteredScreens.map((screen, index) => (
<div
key={`edit-screen-${screen.id}-${index}`}
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
onClick={() => {
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
setModalScreenOpen(false);
setModalSearchTerm("");
}}
>
<span className="text-sm">{screen.name}</span>
</div>
));
})()}
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
)}
{/* 복사 액션 설정 */}
{localSelects.actionType === "copy" && (
<div className="mt-4 space-y-4 rounded-lg border bg-blue-50 p-4">
<h4 className="text-sm font-medium text-gray-700"> ( )</h4>
<div>
<Label htmlFor="copy-screen"> </Label>
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={modalScreenOpen}
className="h-10 w-full justify-between"
disabled={screensLoading}
>
{config.action?.targetScreenId
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
"복사 폼 화면을 선택하세요..."
: "복사 폼 화면을 선택하세요..."}
<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)" }}> <PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center border-b px-3 py-2"> <div className="flex items-center border-b px-3 py-2">
@ -434,12 +500,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
,
</p> </p>
</div> </div>
<div> <div>
<Label htmlFor="edit-mode"> </Label> <Label htmlFor="copy-mode"> </Label>
<Select <Select
value={localSelects.editMode} value={localSelects.editMode}
onValueChange={(value) => { onValueChange={(value) => {

View File

@ -263,6 +263,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
<SelectItem value="save"></SelectItem> <SelectItem value="save"></SelectItem>
<SelectItem value="delete"></SelectItem> <SelectItem value="delete"></SelectItem>
<SelectItem value="edit"></SelectItem> <SelectItem value="edit"></SelectItem>
<SelectItem value="copy"> ( )</SelectItem>
<SelectItem value="navigate"> </SelectItem> <SelectItem value="navigate"> </SelectItem>
<SelectItem value="modal"> </SelectItem> <SelectItem value="modal"> </SelectItem>
<SelectItem value="control"> </SelectItem> <SelectItem value="control"> </SelectItem>
@ -553,6 +554,159 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</div> </div>
)} )}
{/* 복사 액션 설정 */}
{(component.componentConfig?.action?.type || "save") === "copy" && (
<div className="mt-4 space-y-4 rounded-lg border bg-blue-50 p-4">
<h4 className="text-sm font-medium text-foreground"> ( )</h4>
<div>
<Label htmlFor="copy-screen"> </Label>
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }}
disabled={screensLoading}
>
{config.action?.targetScreenId
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
"복사 폼 화면을 선택하세요..."
: "복사 폼 화면을 선택하세요..."}
<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={modalSearchTerm}
onChange={(e) => setModalSearchTerm(e.target.value)}
className="border-0 p-0 focus-visible:ring-0"
/>
</div>
<div className="max-h-[200px] overflow-auto">
{(() => {
const filteredScreens = filterScreens(modalSearchTerm);
if (screensLoading) {
return <div className="p-3 text-sm text-muted-foreground"> ...</div>;
}
if (filteredScreens.length === 0) {
return <div className="p-3 text-sm text-muted-foreground"> .</div>;
}
return filteredScreens.map((screen, index) => (
<div
key={`copy-screen-${screen.id}-${index}`}
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
onClick={() => {
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
setModalScreenOpen(false);
setModalSearchTerm("");
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{screen.name}</span>
{screen.description && <span className="text-xs text-muted-foreground">{screen.description}</span>}
</div>
</div>
));
})()}
</div>
</div>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-muted-foreground">
,
</p>
</div>
<div>
<Label htmlFor="copy-mode"> </Label>
<Select
value={component.componentConfig?.action?.editMode || "modal"}
onValueChange={(value) => {
onUpdateProperty("componentConfig.action.editMode", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="복사 모드 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="modal"> </SelectItem>
<SelectItem value="navigate"> </SelectItem>
</SelectContent>
</Select>
</div>
{(component.componentConfig?.action?.editMode || "modal") === "modal" && (
<>
<div>
<Label htmlFor="copy-modal-title"> </Label>
<Input
id="copy-modal-title"
placeholder="모달 제목을 입력하세요 (예: 데이터 복사)"
value={localInputs.editModalTitle}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, editModalTitle: newValue }));
onUpdateProperty("componentConfig.action.editModalTitle", newValue);
onUpdateProperty("webTypeConfig.editModalTitle", newValue);
}}
/>
<p className="mt-1 text-xs text-muted-foreground"> </p>
</div>
<div>
<Label htmlFor="copy-modal-description"> </Label>
<Input
id="copy-modal-description"
placeholder="모달 설명을 입력하세요 (예: 선택한 데이터를 복사합니다)"
value={localInputs.editModalDescription}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, editModalDescription: newValue }));
onUpdateProperty("componentConfig.action.editModalDescription", newValue);
onUpdateProperty("webTypeConfig.editModalDescription", newValue);
}}
/>
<p className="mt-1 text-xs text-muted-foreground"> </p>
</div>
<div>
<Label htmlFor="copy-modal-size"> </Label>
<Select
value={component.componentConfig?.action?.modalSize || "md"}
onValueChange={(value) => {
onUpdateProperty("componentConfig.action.modalSize", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="모달 크기 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm"> (Small)</SelectItem>
<SelectItem value="md"> (Medium)</SelectItem>
<SelectItem value="lg"> (Large)</SelectItem>
<SelectItem value="xl"> (Extra Large)</SelectItem>
<SelectItem value="full"> (Full)</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
</div>
)}
{/* 테이블 이력 보기 액션 설정 */} {/* 테이블 이력 보기 액션 설정 */}
{(component.componentConfig?.action?.type || "save") === "view_table_history" && ( {(component.componentConfig?.action?.type || "save") === "view_table_history" && (
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">

View File

@ -188,6 +188,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
save: "저장", save: "저장",
delete: "삭제", delete: "삭제",
edit: "수정", edit: "수정",
copy: "복사",
add: "추가", add: "추가",
search: "검색", search: "검색",
reset: "초기화", reset: "초기화",

View File

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

View File

@ -35,6 +35,11 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters";
import { SingleTableWithSticky } from "./SingleTableWithSticky"; import { SingleTableWithSticky } from "./SingleTableWithSticky";
@ -274,7 +279,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(new Set()); const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(new Set());
// 그룹 설정 관련 상태 // 그룹 설정 관련 상태
const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false);
const [groupByColumns, setGroupByColumns] = useState<string[]>([]); const [groupByColumns, setGroupByColumns] = useState<string[]>([]);
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set()); const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
@ -1106,16 +1110,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const mapping = categoryMappings[column.columnName]; const mapping = categoryMappings[column.columnName];
const categoryData = mapping?.[String(value)]; const categoryData = mapping?.[String(value)];
console.log(`🎨 [카테고리 배지] ${column.columnName}:`, { // console.log(`🎨 [카테고리 배지] ${column.columnName}:`, {
value, // value,
stringValue: String(value), // stringValue: String(value),
mapping, // mapping,
categoryData, // categoryData,
hasMapping: !!mapping, // hasMapping: !!mapping,
hasCategoryData: !!categoryData, // hasCategoryData: !!categoryData,
allCategoryMappings: categoryMappings, // 전체 매핑 확인 // allCategoryMappings: categoryMappings, // 전체 매핑 확인
categoryMappingsKeys: Object.keys(categoryMappings), // categoryMappingsKeys: Object.keys(categoryMappings),
}); // });
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상 // 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상
const displayLabel = categoryData?.label || String(value); const displayLabel = categoryData?.label || String(value);
@ -1284,17 +1288,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
})); }));
}, [visibleColumns, visibleFilterColumns, columnLabels]); }, [visibleColumns, visibleFilterColumns, columnLabels]);
// 그룹 설정 저장 // 그룹 설정 자동 저장 (localStorage)
const saveGroupSettings = useCallback(() => { useEffect(() => {
if (!groupSettingKey) return; if (!groupSettingKey) return;
try { try {
localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns)); localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns));
setIsGroupSettingOpen(false);
toast.success("그룹 설정이 저장되었습니다");
} catch (error) { } catch (error) {
console.error("그룹 설정 저장 실패:", error); console.error("그룹 설정 저장 실패:", error);
toast.error("설정 저장에 실패했습니다");
} }
}, [groupSettingKey, groupByColumns]); }, [groupSettingKey, groupByColumns]);
@ -1545,10 +1546,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
> >
<ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" /> <ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" />
</Button> </Button>
<span className="text-muted-foreground ml-2 text-[10px] sm:ml-4 sm:text-xs">
{totalItems.toLocaleString()}
</span>
</div> </div>
{/* 우측 새로고침 버튼 */} {/* 우측 새로고침 버튼 */}
@ -1610,7 +1607,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onClearFilters={handleClearAdvancedFilters} onClearFilters={handleClearAdvancedFilters}
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex items-center gap-2">
{/* 전체 개수 */}
<div className="hidden sm:block text-sm text-muted-foreground whitespace-nowrap">
<span className="font-semibold text-foreground">{totalItems.toLocaleString()}</span>
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -1629,15 +1631,84 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
</Button> </Button>
<Popover>
<PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setIsGroupSettingOpen(true)} className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
> >
<Layers className="mr-2 h-4 w-4" /> <Layers className="mr-2 h-4 w-4" />
{groupByColumns.length > 0 && (
<span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground">
{groupByColumns.length}
</span>
)}
</Button> </Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="space-y-3 p-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold"> </h4>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 컬럼 목록 */}
<div className="max-h-[300px] space-y-2 overflow-y-auto">
{visibleColumns
.filter((col) => col.columnName !== "__checkbox__")
.map((col) => (
<div
key={col.columnName}
className="flex items-center gap-3 rounded p-2 hover:bg-muted/50"
>
<Checkbox
id={`group-dropdown-${col.columnName}`}
checked={groupByColumns.includes(col.columnName)}
onCheckedChange={() => toggleGroupColumn(col.columnName)}
/>
<Label
htmlFor={`group-dropdown-${col.columnName}`}
className="flex-1 cursor-pointer text-xs font-normal"
>
{columnLabels[col.columnName] || col.displayName || col.columnName}
</Label>
</div>
))}
</div>
{/* 선택된 그룹 안내 */}
{groupByColumns.length > 0 && (
<div className="rounded bg-muted/30 p-2 text-xs text-muted-foreground">
<span className="font-semibold text-foreground">
{groupByColumns.map((col) => columnLabels[col] || col).join(" → ")}
</span>
</div>
)}
{/* 초기화 버튼 */}
{groupByColumns.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={() => {
setGroupByColumns([]);
if (groupSettingKey) {
localStorage.removeItem(groupSettingKey);
}
toast.success("그룹 설정이 초기화되었습니다");
}}
className="w-full text-xs"
>
</Button>
)}
</div>
</PopoverContent>
</Popover>
</div> </div>
</div> </div>
</div> </div>
@ -1717,7 +1788,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onClearFilters={handleClearAdvancedFilters} onClearFilters={handleClearAdvancedFilters}
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex items-center gap-2">
{/* 전체 개수 */}
<div className="hidden sm:block text-sm text-muted-foreground whitespace-nowrap">
<span className="font-semibold text-foreground">{totalItems.toLocaleString()}</span>
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -1736,15 +1812,84 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
</Button> </Button>
<Popover>
<PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setIsGroupSettingOpen(true)} className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
> >
<Layers className="mr-2 h-4 w-4" /> <Layers className="mr-2 h-4 w-4" />
{groupByColumns.length > 0 && (
<span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground">
{groupByColumns.length}
</span>
)}
</Button> </Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="space-y-3 p-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold"> </h4>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 컬럼 목록 */}
<div className="max-h-[300px] space-y-2 overflow-y-auto">
{visibleColumns
.filter((col) => col.columnName !== "__checkbox__")
.map((col) => (
<div
key={col.columnName}
className="flex items-center gap-3 rounded p-2 hover:bg-muted/50"
>
<Checkbox
id={`group-dropdown-2-${col.columnName}`}
checked={groupByColumns.includes(col.columnName)}
onCheckedChange={() => toggleGroupColumn(col.columnName)}
/>
<Label
htmlFor={`group-dropdown-2-${col.columnName}`}
className="flex-1 cursor-pointer text-xs font-normal"
>
{columnLabels[col.columnName] || col.displayName || col.columnName}
</Label>
</div>
))}
</div>
{/* 선택된 그룹 안내 */}
{groupByColumns.length > 0 && (
<div className="rounded bg-muted/30 p-2 text-xs text-muted-foreground">
<span className="font-semibold text-foreground">
{groupByColumns.map((col) => columnLabels[col] || col).join(" → ")}
</span>
</div>
)}
{/* 초기화 버튼 */}
{groupByColumns.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={() => {
setGroupByColumns([]);
if (groupSettingKey) {
localStorage.removeItem(groupSettingKey);
}
toast.success("그룹 설정이 초기화되었습니다");
}}
className="w-full text-xs"
>
</Button>
)}
</div>
</PopoverContent>
</Popover>
</div> </div>
</div> </div>
</div> </div>
@ -2209,68 +2354,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* 그룹 설정 다이얼로그 */}
<Dialog open={isGroupSettingOpen} onOpenChange={setIsGroupSettingOpen}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
. .
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
{/* 컬럼 목록 */}
<div className="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
{visibleColumns
.filter((col) => col.columnName !== "__checkbox__")
.map((col) => (
<div key={col.columnName} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
<Checkbox
id={`group-${col.columnName}`}
checked={groupByColumns.includes(col.columnName)}
onCheckedChange={() => toggleGroupColumn(col.columnName)}
/>
<Label
htmlFor={`group-${col.columnName}`}
className="flex-1 cursor-pointer text-xs font-normal sm:text-sm"
>
{columnLabels[col.columnName] || col.displayName || col.columnName}
</Label>
</div>
))}
</div>
{/* 선택된 그룹 안내 */}
<div className="text-muted-foreground bg-muted/30 rounded p-3 text-xs">
{groupByColumns.length === 0 ? (
<span> </span>
) : (
<span>
:{" "}
<span className="text-primary font-semibold">
{groupByColumns.map((col) => columnLabels[col] || col).join(" → ")}
</span>
</span>
)}
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => setIsGroupSettingOpen(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button onClick={saveGroupSettings} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 테이블 옵션 모달 */} {/* 테이블 옵션 모달 */}
<TableOptionsModal <TableOptionsModal
isOpen={isTableOptionsOpen} isOpen={isTableOptionsOpen}

View File

@ -14,6 +14,7 @@ export type ButtonActionType =
| "save" // 저장 | "save" // 저장
| "delete" // 삭제 | "delete" // 삭제
| "edit" // 편집 | "edit" // 편집
| "copy" // 복사 (품목코드 초기화)
| "navigate" // 페이지 이동 | "navigate" // 페이지 이동
| "modal" // 모달 열기 | "modal" // 모달 열기
| "control" // 제어 흐름 | "control" // 제어 흐름
@ -132,6 +133,9 @@ export class ButtonActionExecutor {
case "delete": case "delete":
return await this.handleDelete(config, context); return await this.handleDelete(config, context);
case "copy":
return await this.handleCopy(config, context);
case "navigate": case "navigate":
return this.handleNavigate(config, context); return this.handleNavigate(config, context);
@ -248,19 +252,19 @@ export class ButtonActionExecutor {
const writerValue = context.userId; const writerValue = context.userId;
const companyCodeValue = context.companyCode || ""; const companyCodeValue = context.companyCode || "";
console.log("👤 [buttonActions] 사용자 정보:", { // console.log("👤 [buttonActions] 사용자 정보:", {
userId: context.userId, // userId: context.userId,
userName: context.userName, // userName: context.userName,
companyCode: context.companyCode, // ✅ 회사 코드 // companyCode: context.companyCode, // ✅ 회사 코드
formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값 // formDataWriter: formData.writer, // ✅ 폼에서 입력한 writer 값
formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값 // formDataCompanyCode: formData.company_code, // ✅ 폼에서 입력한 company_code 값
defaultWriterValue: writerValue, // defaultWriterValue: writerValue,
companyCodeValue, // ✅ 최종 회사 코드 값 // companyCodeValue, // ✅ 최종 회사 코드 값
}); // });
// 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가) // 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)
console.log("🔍 채번 규칙 할당 체크 시작"); // console.log("🔍 채번 규칙 할당 체크 시작");
console.log("📦 현재 formData:", JSON.stringify(formData, null, 2)); // console.log("📦 현재 formData:", JSON.stringify(formData, null, 2));
const fieldsWithNumbering: Record<string, string> = {}; const fieldsWithNumbering: Record<string, string> = {};
@ -269,26 +273,26 @@ export class ButtonActionExecutor {
if (key.endsWith("_numberingRuleId") && value) { if (key.endsWith("_numberingRuleId") && value) {
const fieldName = key.replace("_numberingRuleId", ""); const fieldName = key.replace("_numberingRuleId", "");
fieldsWithNumbering[fieldName] = value as string; fieldsWithNumbering[fieldName] = value as string;
console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`); // console.log(`🎯 발견: ${fieldName} → 규칙 ${value}`);
} }
} }
console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering); // console.log("📋 채번 규칙이 설정된 필드:", fieldsWithNumbering);
console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length); // console.log("📊 필드 개수:", Object.keys(fieldsWithNumbering).length);
// 각 필드에 대해 실제 코드 할당 // 각 필드에 대해 실제 코드 할당
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
try { try {
console.log(`🎫 ${fieldName} 필드에 채번 규칙 ${ruleId} 할당 시작`); // console.log(`🎫 ${fieldName} 필드에 채번 규칙 ${ruleId} 할당 시작`);
const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
const response = await allocateNumberingCode(ruleId); const response = await allocateNumberingCode(ruleId);
console.log(`📡 API 응답 (${fieldName}):`, response); // console.log(`📡 API 응답 (${fieldName}):`, response);
if (response.success && response.data) { if (response.success && response.data) {
const generatedCode = response.data.generatedCode; const generatedCode = response.data.generatedCode;
formData[fieldName] = generatedCode; formData[fieldName] = generatedCode;
console.log(`${fieldName} = ${generatedCode} (할당 완료)`); // console.log(`✅ ${fieldName} = ${generatedCode} (할당 완료)`);
} else { } else {
console.error(`❌ 채번 규칙 할당 실패 (${fieldName}):`, response.error); console.error(`❌ 채번 규칙 할당 실패 (${fieldName}):`, response.error);
toast.error(`${fieldName} 채번 규칙 할당 실패: ${response.error}`); toast.error(`${fieldName} 채번 규칙 할당 실패: ${response.error}`);
@ -299,8 +303,8 @@ export class ButtonActionExecutor {
} }
} }
console.log("✅ 채번 규칙 할당 완료"); // console.log("✅ 채번 규칙 할당 완료");
console.log("📦 최종 formData:", JSON.stringify(formData, null, 2)); // console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
const dataWithUserInfo = { const dataWithUserInfo = {
...formData, ...formData,
@ -341,8 +345,9 @@ export class ButtonActionExecutor {
context.onRefresh?.(); context.onRefresh?.();
context.onFlowRefresh?.(); context.onFlowRefresh?.();
// 저장 성공 후 EditModal 닫기 이벤트 발생 // 저장 성공 후 이벤트 발생
window.dispatchEvent(new CustomEvent("closeEditModal")); window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal 닫기
window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal 연속 등록 모드 처리
return true; return true;
} catch (error) { } catch (error) {
@ -886,6 +891,197 @@ export class ButtonActionExecutor {
window.location.href = editUrl; window.location.href = editUrl;
} }
/**
* ( )
*/
private static async handleCopy(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
const { selectedRowsData, flowSelectedData } = context;
// 플로우 선택 데이터 우선 사용
let dataToCopy = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData;
console.log("📋 handleCopy - 데이터 소스 확인:", {
hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0),
flowSelectedDataLength: flowSelectedData?.length || 0,
hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0),
selectedRowsDataLength: selectedRowsData?.length || 0,
dataToCopyLength: dataToCopy?.length || 0,
});
// 선택된 데이터가 없는 경우
if (!dataToCopy || dataToCopy.length === 0) {
toast.error("복사할 항목을 선택해주세요.");
return false;
}
// 복사 화면이 설정되지 않은 경우
if (!config.targetScreenId) {
toast.error("복사 폼 화면이 설정되지 않았습니다. 버튼 설정에서 복사 폼 화면을 선택해주세요.");
return false;
}
console.log(`📋 복사 액션 실행: ${dataToCopy.length}개 항목`, {
dataToCopy,
targetScreenId: config.targetScreenId,
editMode: config.editMode,
});
if (dataToCopy.length === 1) {
// 단일 항목 복사
const rowData = dataToCopy[0];
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) {
const originalValue = copiedData[field];
const ruleIdKey = `${field}_numberingRuleId`;
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;
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(`복사본이 생성되었습니다. 품목코드는 저장 시 자동으로 생성됩니다.`);
} else {
console.warn("⚠️ 품목코드 필드를 찾을 수 없습니다. 전체 데이터를 복사합니다.");
console.warn("⚠️ 사용 가능한 필드:", Object.keys(copiedData));
toast.info("복사본이 생성됩니다.");
}
console.log("📋 복사된 데이터:", copiedData);
await this.openCopyForm(config, copiedData, context);
} else {
// 다중 항목 복사 - 현재는 단일 복사만 지원
toast.error("현재 단일 항목 복사만 지원됩니다. 하나의 항목만 선택해주세요.");
return false;
}
return true;
} catch (error: any) {
console.error("❌ 복사 액션 실행 중 오류:", error);
toast.error(`복사 중 오류가 발생했습니다: ${error.message || "알 수 없는 오류"}`);
return false;
}
}
/**
* ( )
*/
private static async openCopyForm(
config: ButtonActionConfig,
rowData: any,
context: ButtonActionContext,
): Promise<void> {
try {
const editMode = config.editMode || "modal";
console.log("📋 openCopyForm 실행:", { editMode, targetScreenId: config.targetScreenId });
switch (editMode) {
case "modal":
// 모달로 복사 폼 열기 (편집 모달 재사용)
console.log("📋 모달로 복사 폼 열기");
await this.openEditModal(config, rowData, context);
break;
case "navigate":
// 새 페이지로 이동
console.log("📋 새 페이지로 복사 화면 이동");
this.navigateToCopyScreen(config, rowData, context);
break;
default:
// 기본값: 모달
console.log("📋 기본 모달로 복사 폼 열기");
this.openEditModal(config, rowData, context);
}
} catch (error: any) {
console.error("❌ openCopyForm 실행 중 오류:", error);
throw error;
}
}
/**
*
*/
private static navigateToCopyScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
const copyUrl = `/screens/${config.targetScreenId}?mode=copy`;
console.log("🔄 복사 화면으로 이동:", copyUrl);
// 복사할 데이터를 sessionStorage에 저장
sessionStorage.setItem("copyData", JSON.stringify(rowData));
window.location.href = copyUrl;
}
/** /**
* *
*/ */

View File

@ -55,6 +55,7 @@ export type ButtonActionType =
| "cancel" | "cancel"
| "delete" | "delete"
| "edit" | "edit"
| "copy" // 복사 (품목코드 초기화)
| "add" | "add"
// 검색 및 초기화 // 검색 및 초기화
| "search" | "search"