Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
commit
a2637f4dbb
|
|
@ -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={() => {}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 || "버튼"}
|
||||
|
|
|
|||
|
|
@ -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):", {
|
||||
|
|
|
|||
|
|
@ -1253,21 +1253,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;
|
||||
})()}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
<SelectItem value="cancel">취소</SelectItem>
|
||||
<SelectItem value="delete">삭제</SelectItem>
|
||||
<SelectItem value="edit">수정</SelectItem>
|
||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||
<SelectItem value="add">추가</SelectItem>
|
||||
<SelectItem value="search">검색</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" />
|
||||
</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-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)" }}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center border-b px-3 py-2">
|
||||
|
|
@ -434,12 +500,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
선택된 데이터가 이 폼 화면에 자동으로 로드되어 수정할 수 있습니다
|
||||
선택된 데이터가 복사되며, 품목코드는 자동으로 초기화됩니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="edit-mode">수정 모드</Label>
|
||||
<Label htmlFor="copy-mode">복사 모드</Label>
|
||||
<Select
|
||||
value={localSelects.editMode}
|
||||
onValueChange={(value) => {
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
<SelectItem value="save">저장</SelectItem>
|
||||
<SelectItem value="delete">삭제</SelectItem>
|
||||
<SelectItem value="edit">편집</SelectItem>
|
||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||
<SelectItem value="modal">모달 열기</SelectItem>
|
||||
<SelectItem value="control">제어 흐름</SelectItem>
|
||||
|
|
@ -553,6 +554,159 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
</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" && (
|
||||
<div className="mt-4 space-y-4">
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ export const ButtonDataflowConfigPanel: React.FC<ButtonDataflowConfigPanelProps>
|
|||
save: "저장",
|
||||
delete: "삭제",
|
||||
edit: "수정",
|
||||
copy: "복사",
|
||||
add: "추가",
|
||||
search: "검색",
|
||||
reset: "초기화",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters";
|
||||
import { SingleTableWithSticky } from "./SingleTableWithSticky";
|
||||
|
|
@ -274,7 +279,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(new Set());
|
||||
|
||||
// 그룹 설정 관련 상태
|
||||
const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false);
|
||||
const [groupByColumns, setGroupByColumns] = useState<string[]>([]);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
|
|
@ -1106,16 +1110,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);
|
||||
|
|
@ -1284,17 +1288,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}));
|
||||
}, [visibleColumns, visibleFilterColumns, columnLabels]);
|
||||
|
||||
// 그룹 설정 저장
|
||||
const saveGroupSettings = useCallback(() => {
|
||||
// 그룹 설정 자동 저장 (localStorage)
|
||||
useEffect(() => {
|
||||
if (!groupSettingKey) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns));
|
||||
setIsGroupSettingOpen(false);
|
||||
toast.success("그룹 설정이 저장되었습니다");
|
||||
} catch (error) {
|
||||
console.error("그룹 설정 저장 실패:", error);
|
||||
toast.error("설정 저장에 실패했습니다");
|
||||
}
|
||||
}, [groupSettingKey, groupByColumns]);
|
||||
|
||||
|
|
@ -1545,10 +1546,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
>
|
||||
<ChevronsRight className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
|
||||
<span className="text-muted-foreground ml-2 text-[10px] sm:ml-4 sm:text-xs">
|
||||
전체 {totalItems.toLocaleString()}개
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 우측 새로고침 버튼 */}
|
||||
|
|
@ -1610,7 +1607,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
onClearFilters={handleClearAdvancedFilters}
|
||||
/>
|
||||
</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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -1629,15 +1631,84 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<Settings className="mr-2 h-4 w-4" />
|
||||
필터 설정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsGroupSettingOpen(true)}
|
||||
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||
>
|
||||
<Layers className="mr-2 h-4 w-4" />
|
||||
그룹 설정
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
|
|
@ -1717,7 +1788,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
onClearFilters={handleClearAdvancedFilters}
|
||||
/>
|
||||
</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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -1736,15 +1812,84 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<Settings className="mr-2 h-4 w-4" />
|
||||
필터 설정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsGroupSettingOpen(true)}
|
||||
className="w-full flex-shrink-0 sm:mt-1 sm:w-auto"
|
||||
>
|
||||
<Layers className="mr-2 h-4 w-4" />
|
||||
그룹 설정
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-shrink-0 w-full sm:w-auto sm:mt-1"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
|
|
@ -2209,68 +2354,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
</DialogContent>
|
||||
</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
|
||||
isOpen={isTableOptionsOpen}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export type ButtonActionType =
|
|||
| "save" // 저장
|
||||
| "delete" // 삭제
|
||||
| "edit" // 편집
|
||||
| "copy" // 복사 (품목코드 초기화)
|
||||
| "navigate" // 페이지 이동
|
||||
| "modal" // 모달 열기
|
||||
| "control" // 제어 흐름
|
||||
|
|
@ -132,6 +133,9 @@ export class ButtonActionExecutor {
|
|||
case "delete":
|
||||
return await this.handleDelete(config, context);
|
||||
|
||||
case "copy":
|
||||
return await this.handleCopy(config, context);
|
||||
|
||||
case "navigate":
|
||||
return this.handleNavigate(config, context);
|
||||
|
||||
|
|
@ -248,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> = {};
|
||||
|
||||
|
|
@ -269,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}`);
|
||||
|
|
@ -299,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,
|
||||
|
|
@ -341,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) {
|
||||
|
|
@ -886,6 +891,197 @@ export class ButtonActionExecutor {
|
|||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 닫기 액션 처리
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export type ButtonActionType =
|
|||
| "cancel"
|
||||
| "delete"
|
||||
| "edit"
|
||||
| "copy" // 복사 (품목코드 초기화)
|
||||
| "add"
|
||||
// 검색 및 초기화
|
||||
| "search"
|
||||
|
|
|
|||
Loading…
Reference in New Issue