Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs
2025-11-05 16:38:30 +09:00
100 changed files with 15433 additions and 790 deletions

View File

@@ -4,11 +4,11 @@ import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
} from "@/components/ui/resizable-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -104,13 +104,13 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ResizableDialogTitle className="flex items-center gap-2">
<Copy className="h-5 w-5" />
</DialogTitle>
</ResizableDialogTitle>
<DialogDescription>
{sourceScreen?.screenName} . .
</DialogDescription>
</ResizableDialogDescription>
</DialogHeader>
<div className="space-y-4">
@@ -168,7 +168,7 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
</div>
</div>
<DialogFooter>
<ResizableDialogFooter>
<Button variant="outline" onClick={handleClose} disabled={isCopying}>
</Button>
@@ -185,7 +185,7 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
</>
)}
</Button>
</DialogFooter>
</ResizableDialogFooter>
</DialogContent>
</Dialog>
);

View File

@@ -1,7 +1,14 @@
"use client";
import { useEffect, useMemo, useState, useRef } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogDescription,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -213,11 +220,21 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<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>
<div className="space-y-4">
<div className="space-y-2">
@@ -412,15 +429,15 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
</div>
</div>
<DialogFooter className="mt-4">
<ResizableDialogFooter className="mt-4">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
</Button>
<Button onClick={handleSubmit} disabled={!isValid || submitting} variant="default">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</ResizableDialogFooter>
</ResizableDialogContent>
</ResizableDialog>
);
}

View File

@@ -1,12 +1,20 @@
"use client";
import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogDescription,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
import { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/types/screen";
import { toast } from "sonner";
import { dynamicFormApi } from "@/lib/api/dynamicForm";
import { useAuth } from "@/hooks/useAuth";
interface EditModalState {
isOpen: boolean;
@@ -23,6 +31,7 @@ interface EditModalProps {
}
export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const { user } = useAuth();
const [modalState, setModalState] = useState<EditModalState>({
isOpen: false,
screenId: null,
@@ -286,17 +295,28 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const modalStyle = getModalStyle();
return (
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
<DialogContent className={`${modalStyle.className} ${className || ""}`} style={modalStyle.style}>
<DialogHeader className="shrink-0 border-b px-4 py-3">
<DialogTitle className="text-base">{modalState.title || "데이터 수정"}</DialogTitle>
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
<ResizableDialogContent
className={`${modalStyle.className} ${className || ""}`}
style={modalStyle.style}
defaultWidth={800}
defaultHeight={600}
minWidth={600}
minHeight={400}
maxWidth={1400}
maxHeight={1000}
modalId={`edit-modal-${modalState.screenId}`}
userId={user?.userId}
>
<ResizableDialogHeader className="shrink-0 border-b px-4 py-3">
<ResizableDialogTitle className="text-base">{modalState.title || "데이터 수정"}</ResizableDialogTitle>
{modalState.description && !loading && (
<DialogDescription className="text-muted-foreground text-xs">{modalState.description}</DialogDescription>
<ResizableDialogDescription className="text-muted-foreground text-xs">{modalState.description}</ResizableDialogDescription>
)}
{loading && (
<DialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</DialogDescription>
<ResizableDialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</ResizableDialogDescription>
)}
</DialogHeader>
</ResizableDialogHeader>
<div className="flex flex-1 items-center justify-center overflow-auto">
{loading ? (
@@ -358,8 +378,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
</div>
)}
</div>
</DialogContent>
</Dialog>
</ResizableDialogContent>
</ResizableDialog>
);
};

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useState, useCallback, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-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">
<DialogTitle className="text-xl font-semibold">
<ResizableDialogTitle className="text-xl font-semibold">
- {component.label || component.id}
</DialogTitle>
</ResizableDialogTitle>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>

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 } from "@/components/ui/dialog";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog";
import { CalendarIcon, File, Upload, X } from "lucide-react";
import { format } from "date-fns";
import { ko } from "date-fns/locale";

View File

@@ -3,7 +3,7 @@
import React, { useState, useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useAuth } from "@/hooks/useAuth";
import { uploadFilesAndCreateData } from "@/lib/api/file";
import { toast } from "sonner";

View File

@@ -4,11 +4,11 @@ import React, { useState, useEffect, useRef } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
} from "@/components/ui/resizable-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 (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
<ResizableDialog open={isOpen} onOpenChange={onClose}>
<ResizableDialogContent className="max-w-2xl">
{assignmentSuccess ? (
// 성공 화면
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ResizableDialogHeader>
<ResizableDialogTitle 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("나중에") ? "화면 저장 완료" : "화면 할당 완료"}
</DialogTitle>
<DialogDescription>
</ResizableDialogTitle>
<ResizableDialogDescription>
{assignmentMessage.includes("나중에")
? "화면이 성공적으로 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다."
: "화면이 성공적으로 메뉴에 할당되었습니다."}
</DialogDescription>
</DialogHeader>
</ResizableDialogDescription>
</ResizableDialogHeader>
<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>
<DialogFooter>
<ResizableDialogFooter>
<Button
onClick={() => {
// 타이머 정리
@@ -407,19 +407,19 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
<Monitor className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</ResizableDialogFooter>
</>
) : (
// 기본 할당 화면
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ResizableDialogHeader>
<ResizableDialogTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
</DialogTitle>
<DialogDescription>
</ResizableDialogTitle>
<ResizableDialogDescription>
.
</DialogDescription>
</ResizableDialogDescription>
{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>
)}
</DialogHeader>
</ResizableDialogHeader>
<div className="space-y-4">
{/* 메뉴 선택 (검색 기능 포함) */}
@@ -572,22 +572,22 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
</>
)}
</Button>
</DialogFooter>
</ResizableDialogFooter>
</>
)}
</DialogContent>
</Dialog>
</ResizableDialogContent>
</ResizableDialog>
{/* 화면 교체 확인 대화상자 */}
<Dialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ResizableDialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
<ResizableDialogContent className="max-w-md">
<ResizableDialogHeader>
<ResizableDialogTitle className="flex items-center gap-2">
<Monitor className="h-5 w-5 text-orange-600" />
</DialogTitle>
<DialogDescription> .</DialogDescription>
</DialogHeader>
</ResizableDialogTitle>
<ResizableDialogDescription> .</ResizableDialogDescription>
</ResizableDialogHeader>
<div className="space-y-4">
{/* 기존 화면 목록 */}
@@ -652,9 +652,9 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</ResizableDialogFooter>
</ResizableDialogContent>
</ResizableDialog>
</>
);
};

View File

@@ -63,6 +63,11 @@ interface RealtimePreviewProps {
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
// 플로우 선택 데이터 전달용
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
// 테이블 정렬 정보 전달용
sortBy?: string;
sortOrder?: "asc" | "desc";
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
[key: string]: any; // 추가 props 허용
}
// 영역 레이아웃에 따른 아이콘 반환
@@ -105,7 +110,14 @@ const renderArea = (component: ComponentData, children?: React.ReactNode) => {
};
// 동적 웹 타입 위젯 렌더링 컴포넌트
const WidgetRenderer: React.FC<{ component: ComponentData; isDesignMode?: boolean }> = ({ component, isDesignMode = false }) => {
const WidgetRenderer: React.FC<{
component: ComponentData;
isDesignMode?: boolean;
sortBy?: string;
sortOrder?: "asc" | "desc";
tableDisplayData?: any[];
[key: string]: any;
}> = ({ component, isDesignMode = false, sortBy, sortOrder, tableDisplayData, ...restProps }) => {
// 위젯 컴포넌트가 아닌 경우 빈 div 반환
if (!isWidgetComponent(component)) {
return <div className="text-xs text-gray-500"> </div>;
@@ -154,6 +166,9 @@ const WidgetRenderer: React.FC<{ component: ComponentData; isDesignMode?: boolea
readonly: readonly,
isDesignMode,
isInteractive: !isDesignMode,
sortBy, // 🆕 정렬 정보
sortOrder, // 🆕 정렬 방향
tableDisplayData, // 🆕 화면 표시 데이터
}}
config={widget.webTypeConfig}
/>
@@ -225,6 +240,10 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onGroupToggle,
children,
onFlowSelectedDataChange,
sortBy,
sortOrder,
tableDisplayData, // 🆕 화면 표시 데이터
...restProps
}) => {
const { user } = useAuth();
const { type, id, position, size, style = {} } = component;
@@ -545,7 +564,14 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
{/* 위젯 타입 - 동적 렌더링 (파일 컴포넌트 제외) */}
{type === "widget" && !isFileComponent(component) && (
<div className="pointer-events-none h-full w-full">
<WidgetRenderer component={component} isDesignMode={isDesignMode} />
<WidgetRenderer
component={component}
isDesignMode={isDesignMode}
sortBy={sortBy}
sortOrder={sortOrder}
tableDisplayData={tableDisplayData}
{...restProps}
/>
</div>
)}

View File

@@ -54,6 +54,11 @@ interface RealtimePreviewProps {
// 폼 데이터 관련 props
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
// 테이블 정렬 정보
sortBy?: string;
sortOrder?: "asc" | "desc";
columnOrder?: string[];
}
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
@@ -109,6 +114,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onFlowSelectedDataChange,
refreshKey,
onRefresh,
sortBy,
sortOrder,
columnOrder,
flowRefreshKey,
onFlowRefresh,
formData,
@@ -396,6 +404,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onFlowRefresh={onFlowRefresh}
formData={formData}
onFormDataChange={onFormDataChange}
sortBy={sortBy}
sortOrder={sortOrder}
columnOrder={columnOrder}
/>
</div>

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useState, createContext, useContext } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-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">
<DialogTitle> </DialogTitle>
<ResizableDialogTitle> </ResizableDialogTitle>
{/* 디바이스 선택 버튼들 */}
<div className="mt-4 flex gap-2">

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle } from "@/components/ui/resizable-dialog";
import { Button } from "@/components/ui/button";
import { X, Save, Loader2 } from "lucide-react";
import { toast } from "sonner";
@@ -217,7 +217,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
<DialogContent className={`${modalSizeClasses[modalSize]} max-h-[90vh] gap-0 p-0`}>
<DialogHeader className="border-b px-6 py-4">
<div className="flex items-center justify-between">
<DialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</DialogTitle>
<ResizableDialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</ResizableDialogTitle>
<div className="flex items-center gap-2">
<Button onClick={handleSave} disabled={isSaving} size="sm" className="gap-2">
{isSaving ? (

View File

@@ -17,15 +17,15 @@ import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertResizableDialogContent,
AlertResizableDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertResizableDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash } from "lucide-react";
import { ScreenDefinition } from "@/types/screen";

View File

@@ -270,6 +270,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
<SelectItem value="excel_download"> </SelectItem>
<SelectItem value="excel_upload"> </SelectItem>
<SelectItem value="barcode_scan"> </SelectItem>
<SelectItem value="code_merge"> </SelectItem>
</SelectContent>
</Select>
</div>
@@ -838,6 +839,53 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</div>
)}
{/* 코드 병합 액션 설정 */}
{(component.componentConfig?.action?.type || "save") === "code_merge" && (
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
<h4 className="text-sm font-medium text-foreground">🔀 </h4>
<div>
<Label htmlFor="merge-column-name">
<span className="text-destructive">*</span>
</Label>
<Input
id="merge-column-name"
placeholder="예: item_code, product_id"
value={config.action?.mergeColumnName || ""}
onChange={(e) => onUpdateProperty("componentConfig.action.mergeColumnName", e.target.value)}
className="h-8 text-xs"
/>
<p className="mt-1 text-xs text-muted-foreground">
(: item_code). .
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="merge-show-preview"> </Label>
<p className="text-xs text-muted-foreground"> </p>
</div>
<Switch
id="merge-show-preview"
checked={config.action?.mergeShowPreview !== false}
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.mergeShowPreview", checked)}
/>
</div>
<div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950">
<p className="text-xs text-blue-900 dark:text-blue-100">
<strong> :</strong>
<br />
1.
<br />
2.
<br />
3. ,
</p>
</div>
</div>
)}
{/* 제어 기능 섹션 */}
<div className="mt-8 border-t border-border pt-6">
<ImprovedButtonControlConfigPanel component={component} onUpdateProperty={onUpdateProperty} />

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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ResizableDialog, ResizableDialogContent, ResizableDialogDescription, DialogFooter, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog";
import { toast } from "sonner";
import { useAuth } from "@/hooks/useAuth";