Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into commonCodeMng
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Plus, ArrowLeft, ArrowRight, CheckCircle, Circle } from "lucide-react";
|
||||
import { Plus, ArrowLeft, ArrowRight, Circle } from "lucide-react";
|
||||
import ScreenList from "@/components/screen/ScreenList";
|
||||
import ScreenDesigner from "@/components/screen/ScreenDesigner";
|
||||
import TemplateManager from "@/components/screen/TemplateManager";
|
||||
@@ -62,69 +62,11 @@ export default function ScreenManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 단계별 진행 상태 확인
|
||||
const isStepCompleted = (step: Step) => {
|
||||
return stepHistory.includes(step);
|
||||
};
|
||||
|
||||
// 현재 단계가 마지막 단계인지 확인
|
||||
const isLastStep = currentStep === "template";
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">화면관리 시스템</h1>
|
||||
<p className="mt-2 text-gray-600">단계별로 화면을 관리하고 설계하세요</p>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{stepConfig[currentStep].description}</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 진행 표시 */}
|
||||
<div className="border-b bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{Object.entries(stepConfig).map(([step, config], index) => (
|
||||
<div key={step} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
onClick={() => goToStep(step as Step)}
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all ${
|
||||
currentStep === step
|
||||
? "border-blue-600 bg-blue-600 text-white"
|
||||
: isStepCompleted(step as Step)
|
||||
? "border-green-500 bg-green-500 text-white"
|
||||
: "border-gray-300 bg-white text-gray-400"
|
||||
} ${isStepCompleted(step as Step) ? "cursor-pointer hover:bg-green-600" : ""}`}
|
||||
>
|
||||
{isStepCompleted(step as Step) && currentStep !== step ? (
|
||||
<CheckCircle className="h-6 w-6" />
|
||||
) : (
|
||||
<span className="text-lg">{config.icon}</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="mt-2 text-center">
|
||||
<div
|
||||
className={`text-sm font-medium ${
|
||||
currentStep === step
|
||||
? "text-blue-600"
|
||||
: isStepCompleted(step as Step)
|
||||
? "text-green-600"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{config.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{index < Object.keys(stepConfig).length - 1 && (
|
||||
<div className={`mx-4 h-0.5 w-16 ${isStepCompleted(step as Step) ? "bg-green-500" : "bg-gray-300"}`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 화면 목록 단계 */}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ArrowLeft, Save, Loader2 } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { ScreenDefinition, LayoutData } from "@/types/screen";
|
||||
import { InteractiveScreenViewer } from "@/components/screen/InteractiveScreenViewer";
|
||||
@@ -21,7 +19,6 @@ export default function ScreenViewPage() {
|
||||
const [layout, setLayout] = useState<LayoutData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -59,30 +56,9 @@ export default function ScreenViewPage() {
|
||||
}
|
||||
}, [screenId]);
|
||||
|
||||
// 폼 데이터 저장 함수
|
||||
const handleSaveData = async () => {
|
||||
if (!screen) return;
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
console.log("저장할 데이터:", formData);
|
||||
console.log("화면 정보:", screen);
|
||||
|
||||
// 여기에 실제 데이터 저장 API 호출을 추가할 수 있습니다
|
||||
// await saveFormData(screen.tableName, formData);
|
||||
|
||||
toast.success("데이터가 성공적으로 저장되었습니다.");
|
||||
} catch (error) {
|
||||
console.error("데이터 저장 실패:", error);
|
||||
toast.error("데이터 저장에 실패했습니다.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-white">
|
||||
<div className="text-center">
|
||||
<Loader2 className="mx-auto h-8 w-8 animate-spin text-blue-600" />
|
||||
<p className="mt-2 text-gray-600">화면을 불러오는 중...</p>
|
||||
@@ -93,7 +69,7 @@ export default function ScreenViewPage() {
|
||||
|
||||
if (error || !screen) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-white">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
||||
<span className="text-2xl">⚠️</span>
|
||||
@@ -101,7 +77,6 @@ export default function ScreenViewPage() {
|
||||
<h2 className="mb-2 text-xl font-semibold text-gray-900">화면을 찾을 수 없습니다</h2>
|
||||
<p className="mb-4 text-gray-600">{error || "요청하신 화면이 존재하지 않습니다."}</p>
|
||||
<Button onClick={() => router.back()} variant="outline">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
이전으로 돌아가기
|
||||
</Button>
|
||||
</div>
|
||||
@@ -110,167 +85,108 @@ export default function ScreenViewPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="outline" size="sm" onClick={() => router.back()}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-gray-900">{screen.screenName}</h1>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{screen.screenCode}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{screen.tableName}
|
||||
</Badge>
|
||||
<Badge variant={screen.isActive === "Y" ? "default" : "secondary"} className="text-xs">
|
||||
{screen.isActive === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-500">생성일: {screen.createdDate.toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-screen w-screen bg-white">
|
||||
{layout && layout.components.length > 0 ? (
|
||||
// 캔버스 컴포넌트들만 표시 - 전체 화면 사용
|
||||
<div className="relative h-full w-full">
|
||||
{layout.components
|
||||
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함)
|
||||
.map((component) => {
|
||||
// 그룹 컴포넌트인 경우 특별 처리
|
||||
if (component.type === "group") {
|
||||
const groupChildren = layout.components.filter((child) => child.parentId === component.id);
|
||||
|
||||
{/* 메인 컨텐츠 영역 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{layout && layout.components.length > 0 ? (
|
||||
<div className="h-full p-6">
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>{screen.screenName}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
onClick={handleSaveData}
|
||||
disabled={saving}
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width}px`,
|
||||
height: `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.1)",
|
||||
border: (component as any).border || "2px dashed #3b82f6",
|
||||
borderRadius: (component as any).borderRadius || "8px",
|
||||
padding: "16px",
|
||||
}}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
저장
|
||||
</>
|
||||
{/* 그룹 제목 */}
|
||||
{(component as any).title && (
|
||||
<div className="mb-2 text-sm font-medium text-blue-700">{(component as any).title}</div>
|
||||
)}
|
||||
</Button>
|
||||
</CardTitle>
|
||||
{screen.description && <p className="text-sm text-gray-600">{screen.description}</p>}
|
||||
</CardHeader>
|
||||
<CardContent className="h-[calc(100%-5rem)] overflow-auto">
|
||||
{/* 실제 화면 렌더링 영역 */}
|
||||
<div className="relative h-full w-full bg-white">
|
||||
{layout.components
|
||||
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함)
|
||||
.map((component) => {
|
||||
// 그룹 컴포넌트인 경우 특별 처리
|
||||
if (component.type === "group") {
|
||||
const groupChildren = layout.components.filter((child) => child.parentId === component.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width}px`,
|
||||
height: `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.1)",
|
||||
border: (component as any).border || "2px dashed #3b82f6",
|
||||
borderRadius: (component as any).borderRadius || "8px",
|
||||
padding: "16px",
|
||||
}}
|
||||
>
|
||||
{/* 그룹 제목 */}
|
||||
{(component as any).title && (
|
||||
<div className="mb-2 text-sm font-medium text-blue-700">{(component as any).title}</div>
|
||||
)}
|
||||
|
||||
{/* 그룹 내 자식 컴포넌트들 렌더링 */}
|
||||
{groupChildren.map((child) => (
|
||||
<div
|
||||
key={child.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${child.position.x}px`,
|
||||
top: `${child.position.y}px`,
|
||||
width: `${child.size.width}px`,
|
||||
height: `${child.size.height}px`,
|
||||
zIndex: child.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={child}
|
||||
allComponents={layout.components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 일반 컴포넌트 렌더링
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width}px`,
|
||||
height: `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
{/* 그룹 내 자식 컴포넌트들 렌더링 */}
|
||||
{groupChildren.map((child) => (
|
||||
<div
|
||||
key={child.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${child.position.x}px`,
|
||||
top: `${child.position.y}px`,
|
||||
width: `${child.size.width}px`,
|
||||
height: `${child.size.height}px`,
|
||||
zIndex: child.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={child}
|
||||
allComponents={layout.components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={component}
|
||||
allComponents={layout.components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 일반 컴포넌트 렌더링
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width}px`,
|
||||
height: `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={component}
|
||||
allComponents={layout.components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-100">
|
||||
<span className="text-2xl">📄</span>
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-semibold text-gray-900">화면이 비어있습니다</h2>
|
||||
<p className="text-gray-600">이 화면에는 아직 설계된 컴포넌트가 없습니다.</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// 빈 화면일 때도 깔끔하게 표시
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-sm">
|
||||
<span className="text-2xl">📄</span>
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-semibold text-gray-900">화면이 비어있습니다</h2>
|
||||
<p className="text-gray-600">이 화면에는 아직 설계된 컴포넌트가 없습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user