화면 복사기능 구현
This commit is contained in:
parent
b5edef274f
commit
3c86b22a99
|
|
@ -108,6 +108,41 @@ export const deleteScreen = async (
|
|||
}
|
||||
};
|
||||
|
||||
// 화면 복사
|
||||
export const copyScreen = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { screenName, screenCode, description } = req.body;
|
||||
const { companyCode, userId } = req.user as any;
|
||||
|
||||
const copiedScreen = await screenManagementService.copyScreen(
|
||||
parseInt(id),
|
||||
{
|
||||
screenName,
|
||||
screenCode,
|
||||
description,
|
||||
companyCode,
|
||||
createdBy: userId,
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: copiedScreen,
|
||||
message: "화면이 복사되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("화면 복사 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "화면 복사에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블 목록 조회 (모든 테이블)
|
||||
export const getTables = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
createScreen,
|
||||
updateScreen,
|
||||
deleteScreen,
|
||||
copyScreen,
|
||||
getTables,
|
||||
getTableInfo,
|
||||
getTableColumns,
|
||||
|
|
@ -28,6 +29,7 @@ router.get("/screens/:id", getScreen);
|
|||
router.post("/screens", createScreen);
|
||||
router.put("/screens/:id", updateScreen);
|
||||
router.delete("/screens/:id", deleteScreen);
|
||||
router.post("/screens/:id/copy", copyScreen);
|
||||
|
||||
// 화면 코드 자동 생성
|
||||
router.get("/generate-screen-code/:companyCode", generateScreenCode);
|
||||
|
|
|
|||
|
|
@ -14,8 +14,18 @@ import {
|
|||
WebType,
|
||||
WidgetData,
|
||||
} from "../types/screen";
|
||||
|
||||
import { generateId } from "../utils/generateId";
|
||||
|
||||
// 화면 복사 요청 인터페이스
|
||||
interface CopyScreenRequest {
|
||||
screenName: string;
|
||||
screenCode: string;
|
||||
description?: string;
|
||||
companyCode: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
// 백엔드에서 사용할 테이블 정보 타입
|
||||
interface TableInfo {
|
||||
tableName: string;
|
||||
|
|
@ -968,6 +978,120 @@ export class ScreenManagementService {
|
|||
|
||||
return `${companyCode}_${paddedNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면 복사 (화면 정보 + 레이아웃 모두 복사)
|
||||
*/
|
||||
async copyScreen(
|
||||
sourceScreenId: number,
|
||||
copyData: CopyScreenRequest
|
||||
): Promise<ScreenDefinition> {
|
||||
// 트랜잭션으로 처리
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
// 1. 원본 화면 정보 조회
|
||||
const sourceScreen = await tx.screen_definitions.findFirst({
|
||||
where: {
|
||||
screen_id: sourceScreenId,
|
||||
company_code: copyData.companyCode,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sourceScreen) {
|
||||
throw new Error("복사할 화면을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 2. 화면 코드 중복 체크
|
||||
const existingScreen = await tx.screen_definitions.findFirst({
|
||||
where: {
|
||||
screen_code: copyData.screenCode,
|
||||
company_code: copyData.companyCode,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingScreen) {
|
||||
throw new Error("이미 존재하는 화면 코드입니다.");
|
||||
}
|
||||
|
||||
// 3. 새 화면 생성
|
||||
const newScreen = await tx.screen_definitions.create({
|
||||
data: {
|
||||
screen_code: copyData.screenCode,
|
||||
screen_name: copyData.screenName,
|
||||
description: copyData.description || sourceScreen.description,
|
||||
company_code: copyData.companyCode,
|
||||
table_name: sourceScreen.table_name,
|
||||
is_active: sourceScreen.is_active,
|
||||
created_by: copyData.createdBy,
|
||||
created_date: new Date(),
|
||||
updated_by: copyData.createdBy,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// 4. 원본 화면의 레이아웃 정보 조회
|
||||
const sourceLayouts = await tx.screen_layouts.findMany({
|
||||
where: {
|
||||
screen_id: sourceScreenId,
|
||||
},
|
||||
orderBy: { display_order: "asc" },
|
||||
});
|
||||
|
||||
// 5. 레이아웃이 있다면 복사
|
||||
if (sourceLayouts.length > 0) {
|
||||
try {
|
||||
// ID 매핑 맵 생성
|
||||
const idMapping: { [oldId: string]: string } = {};
|
||||
|
||||
// 새로운 컴포넌트 ID 미리 생성
|
||||
sourceLayouts.forEach((layout) => {
|
||||
idMapping[layout.component_id] = generateId();
|
||||
});
|
||||
|
||||
// 각 레이아웃 컴포넌트 복사
|
||||
for (const sourceLayout of sourceLayouts) {
|
||||
const newComponentId = idMapping[sourceLayout.component_id];
|
||||
const newParentId = sourceLayout.parent_id
|
||||
? idMapping[sourceLayout.parent_id]
|
||||
: null;
|
||||
|
||||
await tx.screen_layouts.create({
|
||||
data: {
|
||||
screen_id: newScreen.screen_id,
|
||||
component_type: sourceLayout.component_type,
|
||||
component_id: newComponentId,
|
||||
parent_id: newParentId,
|
||||
position_x: sourceLayout.position_x,
|
||||
position_y: sourceLayout.position_y,
|
||||
width: sourceLayout.width,
|
||||
height: sourceLayout.height,
|
||||
properties: sourceLayout.properties as any,
|
||||
display_order: sourceLayout.display_order,
|
||||
created_date: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("레이아웃 복사 중 오류:", error);
|
||||
// 레이아웃 복사 실패해도 화면 생성은 유지
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 생성된 화면 정보 반환
|
||||
return {
|
||||
screenId: newScreen.screen_id,
|
||||
screenCode: newScreen.screen_code,
|
||||
screenName: newScreen.screen_name,
|
||||
description: newScreen.description || "",
|
||||
companyCode: newScreen.company_code,
|
||||
tableName: newScreen.table_name,
|
||||
isActive: newScreen.is_active,
|
||||
createdBy: newScreen.created_by || undefined,
|
||||
createdDate: newScreen.created_date,
|
||||
updatedBy: newScreen.updated_by || undefined,
|
||||
updatedDate: newScreen.updated_date,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 서비스 인스턴스 export
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Loader2, Copy } from "lucide-react";
|
||||
import { ScreenDefinition } from "@/types/screen";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface CopyScreenModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
sourceScreen: ScreenDefinition | null;
|
||||
onCopySuccess: () => void;
|
||||
}
|
||||
|
||||
export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopySuccess }: CopyScreenModalProps) {
|
||||
const [screenName, setScreenName] = useState("");
|
||||
const [screenCode, setScreenCode] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
const [isCopying, setIsCopying] = useState(false);
|
||||
|
||||
// 모달이 열릴 때 초기값 설정
|
||||
useEffect(() => {
|
||||
if (isOpen && sourceScreen) {
|
||||
setScreenName(`${sourceScreen.screenName} (복사본)`);
|
||||
setDescription(sourceScreen.description || "");
|
||||
// 화면 코드 자동 생성
|
||||
generateNewScreenCode();
|
||||
}
|
||||
}, [isOpen, sourceScreen]);
|
||||
|
||||
// 새로운 화면 코드 자동 생성
|
||||
const generateNewScreenCode = async () => {
|
||||
if (!sourceScreen?.companyCode) return;
|
||||
|
||||
try {
|
||||
const newCode = await screenApi.generateScreenCode(sourceScreen.companyCode);
|
||||
setScreenCode(newCode);
|
||||
} catch (error) {
|
||||
console.error("화면 코드 생성 실패:", error);
|
||||
toast.error("화면 코드 생성에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 화면 복사 실행
|
||||
const handleCopy = async () => {
|
||||
if (!sourceScreen) return;
|
||||
|
||||
// 입력값 검증
|
||||
if (!screenName.trim()) {
|
||||
toast.error("화면명을 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!screenCode.trim()) {
|
||||
toast.error("화면 코드 생성에 실패했습니다. 잠시 후 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCopying(true);
|
||||
|
||||
// 화면 복사 API 호출
|
||||
await screenApi.copyScreen(sourceScreen.screenId, {
|
||||
screenName: screenName.trim(),
|
||||
screenCode: screenCode.trim(),
|
||||
description: description.trim(),
|
||||
});
|
||||
|
||||
toast.success("화면이 성공적으로 복사되었습니다.");
|
||||
onCopySuccess();
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
console.error("화면 복사 실패:", error);
|
||||
const errorMessage = error.response?.data?.message || "화면 복사에 실패했습니다.";
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsCopying(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 모달 닫기
|
||||
const handleClose = () => {
|
||||
setScreenName("");
|
||||
setScreenCode("");
|
||||
setDescription("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Copy className="h-5 w-5" />
|
||||
화면 복사
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{sourceScreen?.screenName} 화면을 복사합니다. 화면 구성도 함께 복사됩니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 원본 화면 정보 */}
|
||||
<div className="rounded-md bg-gray-50 p-3">
|
||||
<h4 className="mb-2 text-sm font-medium text-gray-700">원본 화면 정보</h4>
|
||||
<div className="space-y-1 text-sm text-gray-600">
|
||||
<div>
|
||||
<span className="font-medium">화면명:</span> {sourceScreen?.screenName}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">화면코드:</span> {sourceScreen?.screenCode}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">회사코드:</span> {sourceScreen?.companyCode}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 새 화면 정보 입력 */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="screenName">새 화면명 *</Label>
|
||||
<Input
|
||||
id="screenName"
|
||||
value={screenName}
|
||||
onChange={(e) => setScreenName(e.target.value)}
|
||||
placeholder="복사될 화면의 이름을 입력하세요"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="screenCode">새 화면코드 (자동생성)</Label>
|
||||
<Input
|
||||
id="screenCode"
|
||||
value={screenCode}
|
||||
readOnly
|
||||
className="mt-1 bg-gray-50"
|
||||
placeholder="화면 코드가 자동으로 생성됩니다"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="화면 설명을 입력하세요 (선택사항)"
|
||||
className="mt-1"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isCopying}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleCopy} disabled={isCopying}>
|
||||
{isCopying ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
복사 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
복사하기
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette } from "
|
|||
import { ScreenDefinition } from "@/types/screen";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import CreateScreenModal from "./CreateScreenModal";
|
||||
import CopyScreenModal from "./CopyScreenModal";
|
||||
|
||||
interface ScreenListProps {
|
||||
onScreenSelect: (screen: ScreenDefinition) => void;
|
||||
|
|
@ -30,6 +31,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [isCopyOpen, setIsCopyOpen] = useState(false);
|
||||
const [screenToCopy, setScreenToCopy] = useState<ScreenDefinition | null>(null);
|
||||
|
||||
// 화면 목록 로드 (실제 API)
|
||||
useEffect(() => {
|
||||
|
|
@ -58,6 +61,20 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
|
||||
const filteredScreens = screens; // 서버 필터 기준 사용
|
||||
|
||||
// 화면 목록 다시 로드
|
||||
const reloadScreens = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const resp = await screenApi.getScreens({ page: currentPage, size: 20, searchTerm });
|
||||
setScreens(resp.data || []);
|
||||
setTotalPages(Math.max(1, Math.ceil((resp.total || 0) / 20)));
|
||||
} catch (e) {
|
||||
console.error("화면 목록 조회 실패", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScreenSelect = (screen: ScreenDefinition) => {
|
||||
onScreenSelect(screen);
|
||||
};
|
||||
|
|
@ -75,8 +92,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
};
|
||||
|
||||
const handleCopy = (screen: ScreenDefinition) => {
|
||||
// 복사 모달 열기
|
||||
console.log("복사:", screen);
|
||||
setScreenToCopy(screen);
|
||||
setIsCopyOpen(true);
|
||||
};
|
||||
|
||||
const handleView = (screen: ScreenDefinition) => {
|
||||
|
|
@ -84,6 +101,11 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
console.log("미리보기:", screen);
|
||||
};
|
||||
|
||||
const handleCopySuccess = () => {
|
||||
// 복사 성공 후 화면 목록 다시 로드
|
||||
reloadScreens();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
|
|
@ -239,6 +261,14 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
|||
setScreens((prev) => [created, ...prev]);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 화면 복사 모달 */}
|
||||
<CopyScreenModal
|
||||
isOpen={isCopyOpen}
|
||||
onClose={() => setIsCopyOpen(false)}
|
||||
sourceScreen={screenToCopy}
|
||||
onCopySuccess={handleCopySuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -54,6 +54,8 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
|
|||
canGroup = false,
|
||||
canUngroup = false,
|
||||
}) => {
|
||||
// 데이터테이블 설정 탭 상태를 여기서 관리
|
||||
const [dataTableActiveTab, setDataTableActiveTab] = useState("basic");
|
||||
// 최신 값들의 참조를 유지
|
||||
const selectedComponentRef = useRef(selectedComponent);
|
||||
const onUpdatePropertyRef = useRef(onUpdateProperty);
|
||||
|
|
@ -170,6 +172,8 @@ export const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
|
|||
key={`datatable-${selectedComponent.id}-${selectedComponent.columns.length}-${selectedComponent.filters.length}-${JSON.stringify(selectedComponent.columns.map((c) => c.id))}-${JSON.stringify(selectedComponent.filters.map((f) => f.columnName))}`}
|
||||
component={selectedComponent as DataTableComponent}
|
||||
tables={tables}
|
||||
activeTab={dataTableActiveTab}
|
||||
onTabChange={setDataTableActiveTab}
|
||||
onUpdateComponent={(updates) => {
|
||||
console.log("🔄 DataTable 컴포넌트 업데이트:", updates);
|
||||
console.log("🔄 업데이트 항목들:", Object.keys(updates));
|
||||
|
|
|
|||
|
|
@ -79,6 +79,19 @@ export const screenApi = {
|
|||
const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 화면 복사 (화면정보 + 레이아웃 모두 복사)
|
||||
copyScreen: async (
|
||||
sourceScreenId: number,
|
||||
copyData: {
|
||||
screenName: string;
|
||||
screenCode: string;
|
||||
description?: string;
|
||||
},
|
||||
): Promise<ScreenDefinition> => {
|
||||
const response = await apiClient.post(`/screen-management/screens/${sourceScreenId}/copy`, copyData);
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
// 템플릿 관련 API
|
||||
|
|
|
|||
Loading…
Reference in New Issue