Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map

This commit is contained in:
dohyeons
2025-12-09 10:36:49 +09:00
119 changed files with 7589 additions and 2305 deletions

View File

@@ -424,7 +424,7 @@ export default function CopyScreenModal({
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-[95vw] sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogContent className="max-w-[95vw] sm:max-w-[700px] max-h-[90vh] overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Copy className="h-5 w-5" />

View File

@@ -2,12 +2,12 @@
import { useEffect, useMemo, useState, useRef } from "react";
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -271,21 +271,11 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
};
return (
<ResizableDialog open={open} onOpenChange={onOpenChange}>
<ResizableDialogContent
className="sm:max-w-lg"
defaultWidth={600}
defaultHeight={700}
minWidth={500}
minHeight={600}
maxWidth={900}
maxHeight={900}
modalId="create-screen"
userId={user?.userId}
>
<ResizableDialogHeader>
<ResizableDialogTitle> </ResizableDialogTitle>
</ResizableDialogHeader>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
@@ -603,15 +593,15 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
)}
</div>
<ResizableDialogFooter className="mt-4">
<DialogFooter className="mt-4">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
</Button>
<Button onClick={handleSubmit} disabled={!isValid || submitting} variant="default">
</Button>
</ResizableDialogFooter>
</ResizableDialogContent>
</ResizableDialog>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -2,13 +2,13 @@
import React, { useState, useEffect } from "react";
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogDescription,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/types/screen";
@@ -678,14 +678,17 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
// 실제 모달 크기 = 컨텐츠 + 헤더
const headerHeight = 60; // DialogHeader
const totalHeight = screenDimensions.height + headerHeight;
// 실제 모달 크기 = 컨텐츠 + 헤더 + gap + padding
const headerHeight = 52; // DialogHeader (타이틀 + border-b + py-3)
const dialogGap = 16; // DialogContent gap-4
const extraPadding = 24; // 추가 여백 (안전 마진)
const totalHeight = screenDimensions.height + headerHeight + dialogGap + extraPadding;
return {
className: "overflow-hidden p-0",
style: {
width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`,
width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 좌우 패딩 추가
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`,
maxWidth: "98vw",
maxHeight: "95vh",
@@ -696,32 +699,24 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const modalStyle = getModalStyle();
return (
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
<ResizableDialogContent
className={`${modalStyle.className} ${className || ""}`}
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
<DialogContent
className={`${modalStyle.className} ${className || ""} max-w-none`}
style={modalStyle.style}
defaultWidth={800}
defaultHeight={600}
minWidth={600}
minHeight={400}
maxWidth={1400}
maxHeight={1000}
modalId={modalState.screenId ? `edit-modal-${modalState.screenId}` : undefined}
userId={user?.userId}
>
<ResizableDialogHeader className="shrink-0 border-b px-4 py-3">
<DialogHeader className="shrink-0 border-b px-4 py-3">
<div className="flex items-center gap-2">
<ResizableDialogTitle className="text-base">{modalState.title || "데이터 수정"}</ResizableDialogTitle>
<DialogTitle className="text-base">{modalState.title || "데이터 수정"}</DialogTitle>
{modalState.description && !loading && (
<ResizableDialogDescription className="text-muted-foreground text-xs">{modalState.description}</ResizableDialogDescription>
<DialogDescription className="text-muted-foreground text-xs">{modalState.description}</DialogDescription>
)}
{loading && (
<ResizableDialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</ResizableDialogDescription>
<DialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</DialogDescription>
)}
</div>
</ResizableDialogHeader>
</DialogHeader>
<div className="flex flex-1 items-center justify-center overflow-auto">
<div className="flex flex-1 items-center justify-center overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-transparent">
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@@ -812,8 +807,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
</div>
)}
</div>
</ResizableDialogContent>
</ResizableDialog>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useState, useCallback, useEffect } from "react";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -352,9 +352,9 @@ export const FileAttachmentDetailModal: React.FC<FileAttachmentDetailModalProps>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden">
<DialogHeader>
<div className="flex items-center justify-between">
<ResizableDialogTitle className="text-xl font-semibold">
<DialogTitle className="text-xl font-semibold">
- {component.label || component.id}
</ResizableDialogTitle>
</DialogTitle>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>

View File

@@ -2471,7 +2471,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
{/* 기존 데이터 추가 모달 (제거 예정 - SaveModal로 대체됨) */}
<Dialog open={false} onOpenChange={() => {}}>
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
<DialogContent className={`max-h-[80vh] overflow-hidden ${getModalSizeClass()}`}>
<DialogHeader>
<DialogTitle>{component.addModalConfig?.title || "새 데이터 추가"}</DialogTitle>
<DialogDescription>
@@ -2517,7 +2517,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
{/* 기존 데이터 수정 모달 (제거 예정 - SaveModal로 대체됨) */}
<Dialog open={false} onOpenChange={() => {}}>
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
<DialogContent className={`max-h-[80vh] overflow-hidden ${getModalSizeClass()}`}>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> .</DialogDescription>
@@ -2773,7 +2773,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
{/* 파일 관리 모달 */}
<Dialog open={showFileManagementModal} onOpenChange={setShowFileManagementModal}>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogContent className="max-h-[80vh] max-w-4xl overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Folder className="h-5 w-5" />

View File

@@ -8,7 +8,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Dialog, DialogContent, DialogHeader, DialogTitle, ResizableDialog, ResizableDialogContent, ResizableDialogHeader } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { CalendarIcon, File, Upload, X } from "lucide-react";
import { format } from "date-fns";
import { ko } from "date-fns/locale";
@@ -441,6 +441,39 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
);
}
// 🆕 렉 구조 컴포넌트 처리
if (comp.type === "component" && componentType === "rack-structure") {
const { RackStructureComponent } = require("@/lib/registry/components/rack-structure/RackStructureComponent");
const componentConfig = (comp as any).componentConfig || {};
// config가 중첩되어 있을 수 있음: componentConfig.config 또는 componentConfig 직접
const rackConfig = componentConfig.config || componentConfig;
console.log("🏗️ 렉 구조 컴포넌트 렌더링:", {
componentType,
componentConfig,
rackConfig,
fieldMapping: rackConfig.fieldMapping,
formData,
});
return (
<div className="h-full w-full overflow-auto">
<RackStructureComponent
config={rackConfig}
formData={formData}
tableName={tableName}
onChange={(locations: any[]) => {
console.log("📦 렉 구조 위치 데이터 변경:", locations.length, "개");
// 컴포넌트의 columnName을 키로 사용
const fieldKey = (comp as any).columnName || "_rackStructureLocations";
updateFormData(fieldKey, locations);
}}
isPreview={false}
/>
</div>
);
}
const { widgetType, label, placeholder, required, readonly, columnName } = comp;
const fieldName = columnName || comp.id;
const currentValue = formData[fieldName] || "";

View File

@@ -3,8 +3,7 @@
import React, { useState, useCallback, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader } from "@/components/ui/resizable-dialog";
import { DialogTitle, DialogHeader } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useAuth } from "@/hooks/useAuth";
import { uploadFilesAndCreateData } from "@/lib/api/file";
import { toast } from "sonner";
@@ -119,17 +118,19 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
const [popupFormData, setPopupFormData] = useState<Record<string, any>>({});
// 🆕 분할 패널에서 매핑된 부모 데이터 가져오기
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
const splitPanelMappedData = React.useMemo(() => {
if (splitPanelContext) {
if (splitPanelContext && !splitPanelContext.disableAutoDataTransfer) {
return splitPanelContext.getMappedParentData();
}
return {};
}, [splitPanelContext, splitPanelContext?.selectedLeftData]);
}, [splitPanelContext, splitPanelContext?.selectedLeftData, splitPanelContext?.disableAutoDataTransfer]);
// formData 결정 (외부에서 전달받은 것이 있으면 우선 사용, 분할 패널 데이터도 병합)
const formData = React.useMemo(() => {
const baseData = externalFormData || localFormData;
// 분할 패널 매핑 데이터가 있으면 병합 (기존 값이 없는 경우에만)
// disableAutoDataTransfer가 true이면 자동 병합 안함
if (Object.keys(splitPanelMappedData).length > 0) {
const merged = { ...baseData };
for (const [key, value] of Object.entries(splitPanelMappedData)) {
@@ -776,17 +777,15 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
{/* 팝업 화면 렌더링 */}
{popupScreen && (
<ResizableDialog open={!!popupScreen} onOpenChange={() => setPopupScreen(null)}>
<ResizableDialogContent
className="overflow-hidden p-0"
defaultWidth={popupScreen.size === "small" ? 600 : popupScreen.size === "large" ? 1400 : 1000}
defaultHeight={800}
minWidth={500}
minHeight={400}
maxWidth={1600}
maxHeight={1200}
modalId={`popup-screen-${popupScreen.screenId}`}
userId={user?.userId || "guest"}
<Dialog open={!!popupScreen} onOpenChange={() => setPopupScreen(null)}>
<DialogContent
className="overflow-hidden p-0 max-w-none"
style={{
width: popupScreen.size === "small" ? "600px" : popupScreen.size === "large" ? "1400px" : "1000px",
height: "800px",
maxWidth: "95vw",
maxHeight: "90vh",
}}
>
<DialogHeader>
<DialogTitle>{popupScreen.title}</DialogTitle>
@@ -820,8 +819,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
))}
</div>
)}
</ResizableDialogContent>
</ResizableDialog>
</DialogContent>
</Dialog>
)}
</>
);

View File

@@ -2,13 +2,13 @@
import React, { useState, useEffect, useRef } from "react";
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogDescription,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@@ -345,26 +345,26 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
return (
<>
<ResizableDialog open={isOpen} onOpenChange={onClose}>
<ResizableDialogContent className="max-w-2xl">
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
{assignmentSuccess ? (
// 성공 화면
<>
<ResizableDialogHeader>
<ResizableDialogTitle className="flex items-center gap-2">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100">
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
{assignmentMessage.includes("나중에") ? "화면 저장 완료" : "화면 할당 완료"}
</ResizableDialogTitle>
<ResizableDialogDescription>
</DialogTitle>
<DialogDescription>
{assignmentMessage.includes("나중에")
? "화면이 성공적으로 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다."
: "화면이 성공적으로 메뉴에 할당되었습니다."}
</ResizableDialogDescription>
</ResizableDialogHeader>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="rounded-lg border bg-green-50 p-4">
@@ -386,7 +386,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
</div>
</div>
<ResizableDialogFooter>
<DialogFooter>
<Button
onClick={() => {
// 타이머 정리
@@ -407,19 +407,19 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
<Monitor className="mr-2 h-4 w-4" />
</Button>
</ResizableDialogFooter>
</DialogFooter>
</>
) : (
// 기본 할당 화면
<>
<ResizableDialogHeader>
<ResizableDialogTitle className="flex items-center gap-2">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
</ResizableDialogTitle>
<ResizableDialogDescription>
</DialogTitle>
<DialogDescription>
.
</ResizableDialogDescription>
</DialogDescription>
{screenInfo && (
<div className="bg-accent mt-2 rounded-lg border p-3">
<div className="flex items-center gap-2">
@@ -432,7 +432,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
{screenInfo.description && <p className="mt-1 text-sm text-blue-700">{screenInfo.description}</p>}
</div>
)}
</ResizableDialogHeader>
</DialogHeader>
<div className="space-y-4">
{/* 메뉴 선택 (검색 기능 포함) */}
@@ -550,7 +550,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
)}
</div>
<ResizableDialogFooter className="flex gap-2">
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={handleAssignLater} disabled={assigning}>
<X className="mr-2 h-4 w-4" />
@@ -572,22 +572,22 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
</>
)}
</Button>
</ResizableDialogFooter>
</DialogFooter>
</>
)}
</ResizableDialogContent>
</ResizableDialog>
</DialogContent>
</Dialog>
{/* 화면 교체 확인 대화상자 */}
<ResizableDialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
<ResizableDialogContent className="max-w-md">
<ResizableDialogHeader>
<ResizableDialogTitle className="flex items-center gap-2">
<Dialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Monitor className="h-5 w-5 text-orange-600" />
</ResizableDialogTitle>
<ResizableDialogDescription> .</ResizableDialogDescription>
</ResizableDialogHeader>
</DialogTitle>
<DialogDescription> .</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 기존 화면 목록 */}
@@ -628,7 +628,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
</div>
</div>
<ResizableDialogFooter className="flex gap-2">
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={() => setShowReplaceDialog(false)} disabled={assigning}>
</Button>
@@ -652,9 +652,9 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
</>
)}
</Button>
</ResizableDialogFooter>
</ResizableDialogContent>
</ResizableDialog>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useState, createContext, useContext } from "react";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Monitor, Tablet, Smartphone } from "lucide-react";
import { ComponentData } from "@/types/screen";
@@ -76,7 +76,7 @@ export const ResponsivePreviewModal: React.FC<ResponsivePreviewModalProps> = ({
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-h-[95vh] max-w-[95vw] p-0">
<DialogHeader className="border-b px-6 pt-6 pb-4">
<ResizableDialogTitle> </ResizableDialogTitle>
<DialogTitle> </DialogTitle>
{/* 디바이스 선택 버튼들 */}
<div className="mt-4 flex gap-2">

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, ResizableDialogTitle } from "@/components/ui/resizable-dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { X, Save, Loader2 } from "lucide-react";
import { toast } from "sonner";
@@ -232,22 +232,19 @@ export const SaveModal: React.FC<SaveModalProps> = ({
const dynamicSize = calculateDynamicSize();
return (
<ResizableDialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
<ResizableDialogContent
modalId={`save-modal-${screenId}`}
<Dialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
<DialogContent
style={{
width: `${dynamicSize.width}px`,
height: `${dynamicSize.height}px`, // 화면관리 설정 크기 그대로 사용
minWidth: "400px",
minHeight: "300px",
}}
defaultWidth={600} // 폴백용 기본값
defaultHeight={400} // 폴백용 기본값
minWidth={400}
minHeight={300}
className="gap-0 p-0"
className="gap-0 p-0 max-w-none"
>
<ResizableDialogHeader className="border-b px-6 py-4 flex-shrink-0">
<DialogHeader className="border-b px-6 py-4 flex-shrink-0">
<div className="flex items-center justify-between">
<ResizableDialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</ResizableDialogTitle>
<DialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</DialogTitle>
<div className="flex items-center gap-2">
<Button onClick={handleSave} disabled={isSaving} size="sm" className="gap-2">
{isSaving ? (
@@ -267,7 +264,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
</Button>
</div>
</div>
</ResizableDialogHeader>
</DialogHeader>
<div className="overflow-auto p-6 flex-1">
{loading ? (
@@ -376,7 +373,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
<div className="text-muted-foreground py-12 text-center"> .</div>
)}
</div>
</ResizableDialogContent>
</ResizableDialog>
</DialogContent>
</Dialog>
);
};

View File

@@ -2239,10 +2239,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
calculatedWidth: `${Math.round(widthPercent * 100) / 100}%`,
});
// 🆕 라벨을 기반으로 기본 columnName 생성 (한글 → 스네이크 케이스)
// 예: "창고코드" → "warehouse_code" 또는 그대로 유지
const generateDefaultColumnName = (label: string): string => {
// 한글 라벨의 경우 그대로 사용 (나중에 사용자가 수정 가능)
// 영문의 경우 스네이크 케이스로 변환
if (/[가-힣]/.test(label)) {
// 한글이 포함된 경우: 공백을 언더스코어로, 소문자로 변환
return label.replace(/\s+/g, "_").toLowerCase();
}
// 영문의 경우: 카멜케이스/파스칼케이스를 스네이크 케이스로 변환
return label
.replace(/([a-z])([A-Z])/g, "$1_$2")
.replace(/\s+/g, "_")
.toLowerCase();
};
const newComponent: ComponentData = {
id: generateComponentId(),
type: "component", // ✅ 새 컴포넌트 시스템 사용
label: component.name,
columnName: generateDefaultColumnName(component.name), // 🆕 기본 columnName 자동 생성
widgetType: component.webType,
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
position: snappedPosition,

View File

@@ -91,6 +91,14 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
const [mappingSourceSearch, setMappingSourceSearch] = useState<Record<number, string>>({});
const [mappingTargetSearch, setMappingTargetSearch] = useState<Record<number, string>>({});
// 🆕 openModalWithData 전용 필드 매핑 상태
const [modalSourceColumns, setModalSourceColumns] = useState<Array<{ name: string; label: string }>>([]);
const [modalTargetColumns, setModalTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState<Record<number, boolean>>({});
const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState<Record<number, boolean>>({});
const [modalSourceSearch, setModalSourceSearch] = useState<Record<number, string>>({});
const [modalTargetSearch, setModalTargetSearch] = useState<Record<number, string>>({});
// 🎯 플로우 위젯이 화면에 있는지 확인
const hasFlowWidget = useMemo(() => {
const found = allComponents.some((comp: any) => {
@@ -318,6 +326,88 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
loadColumns();
}, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]);
// 🆕 openModalWithData 소스/타겟 테이블 컬럼 로드
useEffect(() => {
const actionType = config.action?.type;
if (actionType !== "openModalWithData") return;
const loadModalMappingColumns = async () => {
// 소스 테이블: 현재 화면의 분할 패널 또는 테이블에서 감지
// allComponents에서 split-panel-layout 또는 table-list 찾기
let sourceTableName: string | null = null;
for (const comp of allComponents) {
const compType = comp.componentType || (comp as any).componentConfig?.type;
if (compType === "split-panel-layout" || compType === "screen-split-panel") {
// 분할 패널의 좌측 테이블명
sourceTableName = (comp as any).componentConfig?.leftPanel?.tableName ||
(comp as any).componentConfig?.leftTableName;
break;
}
if (compType === "table-list") {
sourceTableName = (comp as any).componentConfig?.tableName;
break;
}
}
// 소스 테이블 컬럼 로드
if (sourceTableName) {
try {
const response = await apiClient.get(`/table-management/tables/${sourceTableName}/columns`);
if (response.data.success) {
let columnData = response.data.data;
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
if (Array.isArray(columnData)) {
const columns = columnData.map((col: any) => ({
name: col.name || col.columnName,
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
}));
setModalSourceColumns(columns);
console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드:`, columns.length);
}
}
} catch (error) {
console.error("소스 테이블 컬럼 로드 실패:", error);
}
}
// 타겟 화면의 테이블 컬럼 로드
const targetScreenId = config.action?.targetScreenId;
if (targetScreenId) {
try {
// 타겟 화면 정보 가져오기
const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`);
if (screenResponse.data.success && screenResponse.data.data) {
const targetTableName = screenResponse.data.data.tableName;
if (targetTableName) {
const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`);
if (columnResponse.data.success) {
let columnData = columnResponse.data.data;
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
if (Array.isArray(columnData)) {
const columns = columnData.map((col: any) => ({
name: col.name || col.columnName,
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
}));
setModalTargetColumns(columns);
console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드:`, columns.length);
}
}
}
}
} catch (error) {
console.error("타겟 화면 테이블 컬럼 로드 실패:", error);
}
}
};
loadModalMappingColumns();
}, [config.action?.type, config.action?.targetScreenId, allComponents]);
// 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준)
useEffect(() => {
const fetchScreens = async () => {
@@ -1024,6 +1114,194 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
SelectedItemsDetailInput
</p>
</div>
{/* 🆕 필드 매핑 설정 (소스 컬럼 → 타겟 컬럼) */}
<div className="space-y-2 border-t pt-4">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium"> ()</Label>
<Button
type="button"
variant="outline"
size="sm"
className="h-6 text-[10px]"
onClick={() => {
const currentMappings = config.action?.fieldMappings || [];
const newMapping = { sourceField: "", targetField: "" };
onUpdateProperty("componentConfig.action.fieldMappings", [...currentMappings, newMapping]);
}}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<p className="text-[10px] text-muted-foreground">
.
<br />
: warehouse_code warehouse_id ( ID에 )
</p>
{/* 컬럼 로드 상태 표시 */}
{modalSourceColumns.length > 0 || modalTargetColumns.length > 0 ? (
<div className="text-[10px] text-muted-foreground bg-muted/50 p-2 rounded">
: {modalSourceColumns.length} / : {modalTargetColumns.length}
</div>
) : (
<div className="text-[10px] text-amber-600 bg-amber-50 p-2 rounded dark:bg-amber-950/20">
.
</div>
)}
{(config.action?.fieldMappings || []).length === 0 ? (
<div className="rounded-md border border-dashed p-3 text-center">
<p className="text-xs text-muted-foreground">
.
</p>
</div>
) : (
<div className="space-y-2">
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
<div key={index} className="flex items-center gap-2 rounded-md border bg-background p-2">
{/* 소스 필드 선택 (Combobox) */}
<div className="flex-1">
<Popover
open={modalSourcePopoverOpen[index] || false}
onOpenChange={(open) => setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs"
>
{mapping.sourceField
? modalSourceColumns.find((c) => c.name === mapping.sourceField)?.label || mapping.sourceField
: "소스 컬럼 선택"}
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput
placeholder="컬럼 검색..."
className="h-8 text-xs"
value={modalSourceSearch[index] || ""}
onValueChange={(value) => setModalSourceSearch((prev) => ({ ...prev, [index]: value }))}
/>
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
{modalSourceColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.label} ${col.name}`}
onSelect={() => {
const mappings = [...(config.action?.fieldMappings || [])];
mappings[index] = { ...mappings[index], sourceField: col.name };
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: false }));
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
mapping.sourceField === col.name ? "opacity-100" : "opacity-0"
)}
/>
<span>{col.label}</span>
{col.label !== col.name && (
<span className="ml-1 text-muted-foreground">({col.name})</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<span className="text-xs text-muted-foreground"></span>
{/* 타겟 필드 선택 (Combobox) */}
<div className="flex-1">
<Popover
open={modalTargetPopoverOpen[index] || false}
onOpenChange={(open) => setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-full justify-between text-xs"
>
{mapping.targetField
? modalTargetColumns.find((c) => c.name === mapping.targetField)?.label || mapping.targetField
: "타겟 컬럼 선택"}
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput
placeholder="컬럼 검색..."
className="h-8 text-xs"
value={modalTargetSearch[index] || ""}
onValueChange={(value) => setModalTargetSearch((prev) => ({ ...prev, [index]: value }))}
/>
<CommandList>
<CommandEmpty className="py-2 text-center text-xs"> </CommandEmpty>
<CommandGroup>
{modalTargetColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.label} ${col.name}`}
onSelect={() => {
const mappings = [...(config.action?.fieldMappings || [])];
mappings[index] = { ...mappings[index], targetField: col.name };
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: false }));
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
mapping.targetField === col.name ? "opacity-100" : "opacity-0"
)}
/>
<span>{col.label}</span>
{col.label !== col.name && (
<span className="ml-1 text-muted-foreground">({col.name})</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 삭제 버튼 */}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:bg-destructive/10"
onClick={() => {
const mappings = [...(config.action?.fieldMappings || [])];
mappings.splice(index, 1);
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
}}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
)}

View File

@@ -584,20 +584,23 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
</div>
<div className="space-y-3">
{selectedComponent.type === "widget" && (
{(selectedComponent.type === "widget" || selectedComponent.type === "component") && (
<>
<div className="space-y-1.5">
<Label htmlFor="columnName" className="text-xs font-medium">
( )
()
</Label>
<Input
id="columnName"
value={selectedComponent.columnName || ""}
readOnly
placeholder="데이터베이스 컬럼명"
className="bg-muted/50 text-muted-foreground h-8"
title="컬럼명은 변경할 수 없습니다"
onChange={(e) => onUpdateProperty("columnName", e.target.value)}
placeholder="formData에서 사용할 필드명"
className="h-8"
title="분할 패널에서 데이터를 전달받을 때 사용되는 필드명입니다"
/>
<p className="text-muted-foreground text-xs">
</p>
</div>
<div className="space-y-1.5">

View File

@@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { ResizableDialog, ResizableDialogContent, ResizableDialogDescription, DialogFooter, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { toast } from "sonner";
import { useAuth } from "@/hooks/useAuth";