화면 복사기능 구현

This commit is contained in:
kjs 2025-09-03 18:23:47 +09:00
parent b5edef274f
commit 3c86b22a99
8 changed files with 1591 additions and 1055 deletions

View File

@ -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 {

View File

@ -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);

View File

@ -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

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -17,6 +17,8 @@ import { generateComponentId } from "@/lib/utils/generateId";
interface DataTableConfigPanelProps {
component: DataTableComponent;
tables: TableInfo[];
activeTab?: string;
onTabChange?: (tab: string) => void;
onUpdateComponent: (updates: Partial<DataTableComponent>) => void;
}
@ -32,7 +34,13 @@ const webTypeOptions: { value: WebType; label: string }[] = [
{ value: "tel", label: "전화번호" },
];
export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ component, tables, onUpdateComponent }) => {
export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({
component,
tables,
activeTab: externalActiveTab,
onTabChange,
onUpdateComponent,
}) => {
const [selectedTable, setSelectedTable] = useState<TableInfo | null>(null);
// 로컬 입력 상태 (실시간 타이핑용)
@ -93,6 +101,11 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
// 모달 설정 확장/축소 상태
const [isModalConfigOpen, setIsModalConfigOpen] = useState<Record<string, boolean>>({});
// 탭 상태 관리 (외부에서 받거나 로컬 상태 사용)
const [internalActiveTab, setInternalActiveTab] = useState("basic");
const activeTab = externalActiveTab || internalActiveTab;
const setActiveTab = onTabChange || setInternalActiveTab;
// 컴포넌트 변경 시 로컬 값 동기화
useEffect(() => {
console.log("🔄 DataTableConfig: 컴포넌트 변경 감지", {
@ -239,6 +252,11 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
const filterKey = `${filter.columnName}-${index}`;
if (!(filterKey in newFilterInputs)) {
newFilterInputs[filterKey] = filter.label || filter.columnName;
console.log("🆕 새 필터 로컬 상태 추가:", {
filterKey,
label: filter.label,
columnName: filter.columnName,
});
}
});
@ -248,10 +266,17 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
);
Object.keys(newFilterInputs).forEach((key) => {
if (!currentFilterKeys.has(key)) {
console.log("🗑️ 삭제된 필터 로컬 상태 제거:", { filterKey: key });
delete newFilterInputs[key];
}
});
console.log("📝 필터 로컬 상태 동기화 완료:", {
prevCount: Object.keys(prev).length,
newCount: Object.keys(newFilterInputs).length,
newKeys: Object.keys(newFilterInputs),
});
return newFilterInputs;
});
}, [
@ -421,7 +446,14 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
const tableColumn = selectedTable.columns.find((col) => col.columnName === column.columnName);
return (
tableColumn?.webType ||
getWidgetTypeFromColumn(tableColumn || { columnName: column.columnName, dataType: "text" })
getWidgetTypeFromColumn(
tableColumn || {
columnName: column.columnName,
dataType: "text",
tableName: "",
isNullable: true,
},
)
);
},
[selectedTable],
@ -824,9 +856,56 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
})),
});
// 먼저 로컬 상태를 업데이트하고
const filterKey = `${newFilter.columnName}-${component.filters.length}`;
setLocalFilterInputs((prev) => {
const newState = {
...prev,
[filterKey]: newFilter.label,
};
console.log("📝 필터 로컬 상태 업데이트:", {
filterKey,
newLabel: newFilter.label,
prevState: prev,
newState,
});
return newState;
});
// 그 다음 컴포넌트 상태를 업데이트
onUpdateComponent({ filters: updatedFilters });
console.log("✅ 필터 추가 완료 - onUpdateComponent 호출됨");
// 필터 추가 후 필터 탭으로 자동 이동
setActiveTab("filters");
console.log("🔍 필터 추가 후 탭 이동:", {
activeTab: "filters",
isExternalControl: !!onTabChange,
});
// 강제로 리렌더링을 트리거하기 위해 여러 방법 사용
setTimeout(() => {
setLocalFilterInputs((prev) => ({
...prev,
[filterKey]: newFilter.label,
}));
console.log("🔄 setTimeout에서 강제 로컬 상태 업데이트:", { filterKey, label: newFilter.label });
}, 0);
// 추가적인 강제 업데이트
setTimeout(() => {
setLocalFilterInputs((prev) => {
const updated = { ...prev, [filterKey]: newFilter.label };
console.log("🔄 두 번째 강제 업데이트:", { updated });
return updated;
});
}, 100);
console.log("✅ 필터 추가 완료 - 로컬 상태와 컴포넌트 모두 업데이트됨", {
filterKey,
newFilterLabel: newFilter.label,
switchedToTab: "filters",
});
}, [selectedTable, component.filters, onUpdateComponent]);
// 필터 삭제
@ -954,13 +1033,30 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
onUpdateComponent(updates);
// 컬럼 추가 후 컬럼 탭으로 자동 이동
setActiveTab("columns");
console.log("📋 컬럼 추가 후 탭 이동:", {
activeTab: "columns",
isExternalControl: !!onTabChange,
});
console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨");
},
[selectedTable, component.columns, component.filters, onUpdateComponent],
);
return (
<div className="max-h-[80vh] space-y-4 overflow-y-auto p-4">
<div className="max-h-[80vh] p-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="basic"> </TabsTrigger>
<TabsTrigger value="columns"> </TabsTrigger>
<TabsTrigger value="filters"> </TabsTrigger>
<TabsTrigger value="modal"> </TabsTrigger>
</TabsList>
<TabsContent value="basic" className="mt-4 max-h-[70vh] overflow-y-auto">
{/* 기본 설정 */}
<Card>
<CardHeader>
@ -1333,26 +1429,17 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
</div>
</CardContent>
</Card>
</TabsContent>
{/* 탭 설정 */}
<Tabs defaultValue="columns" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="columns" className="flex items-center space-x-1">
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-sm">
<Columns className="h-4 w-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="filters" className="flex items-center space-x-1">
<Filter className="h-4 w-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="pagination" className="flex items-center space-x-1">
<Table className="h-4 w-4" />
<span></span>
</TabsTrigger>
</TabsList>
{/* 컬럼 설정 */}
<TabsContent value="columns" className="space-y-4">
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium"> </h3>
<div className="flex items-center space-x-2">
@ -1386,9 +1473,10 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
</div>
</div>
<div className="max-h-96 space-y-3 overflow-y-auto">
<div className="max-h-96 space-y-3 overflow-x-hidden overflow-y-auto">
{component.columns.map((column, index) => (
<Card key={`${column.id}-${column.columnName}-${index}`} className="p-2">
<Card key={`${column.id}-${column.columnName}-${index}`} className="w-full p-2">
<div className="space-y-2">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
@ -1403,13 +1491,7 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
updateColumn(column.id, { visible: checked as boolean });
}}
/>
<span className="text-sm font-medium">{column.label}</span>
<Badge variant="outline" className="text-xs">
{column.columnName}
</Badge>
<Badge variant="secondary" className="text-xs">
{getColumnWebType(column)}
</Badge>
<span className="truncate text-sm font-medium">{column.label}</span>
</div>
<div className="flex items-center space-x-1">
<Button
@ -1441,11 +1523,22 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
</Button>
</div>
</div>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="max-w-[120px] truncate text-xs">
{column.columnName}
</Badge>
<Badge variant="secondary" className="max-w-[80px] truncate text-xs">
{getColumnWebType(column)}
</Badge>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={localColumnInputs[column.id] !== undefined ? localColumnInputs[column.id] : column.label}
value={
localColumnInputs[column.id] !== undefined ? localColumnInputs[column.id] : column.label
}
onChange={(e) => {
const newValue = e.target.value;
setLocalColumnInputs((prev) => ({ ...prev, [column.id]: newValue }));
@ -1464,7 +1557,7 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
@ -1492,6 +1585,7 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
</Select>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center space-x-1">
<Checkbox
id={`sortable-${column.id}`}
@ -1528,6 +1622,7 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
</Label>
</div>
</div>
</div>
{/* 웹 타입 상세 설정 */}
{isColumnDetailOpen[column.id] && (
@ -1579,7 +1674,9 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
if (checked) {
newRequiredFields = [...requiredFields, column.columnName];
} else {
newRequiredFields = requiredFields.filter((field) => field !== column.columnName);
newRequiredFields = requiredFields.filter(
(field) => field !== column.columnName,
);
}
onUpdateComponent({
@ -1598,7 +1695,9 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
<div className="flex items-center space-x-1">
<Checkbox
id={`hidden-${column.id}`}
checked={component.addModalConfig?.hiddenFields?.includes(column.columnName) || false}
checked={
component.addModalConfig?.hiddenFields?.includes(column.columnName) || false
}
onCheckedChange={(checked) => {
const hiddenFields = component.addModalConfig?.hiddenFields || [];
let newHiddenFields;
@ -1717,8 +1816,8 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
<Label className="text-xs"> </Label>
<Input
value={
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.customValue ||
""
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]
?.customValue || ""
}
onChange={(e) => {
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
@ -1753,10 +1852,19 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
</Card>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 필터 설정 */}
<TabsContent value="filters" className="space-y-4">
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-sm">
<Filter className="h-4 w-4" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium"> </h3>
<div className="flex items-center space-x-2">
@ -1837,11 +1945,19 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={
localFilterInputs[`${filter.columnName}-${index}`] !== undefined
? localFilterInputs[`${filter.columnName}-${index}`]
: filter.label
}
value={(() => {
const filterKey = `${filter.columnName}-${index}`;
const localValue = localFilterInputs[filterKey];
const finalValue = localValue !== undefined ? localValue : filter.label;
console.log("🎯 필터 입력 값 결정:", {
filterKey,
localValue,
filterLabel: filter.label,
finalValue,
allLocalInputs: Object.keys(localFilterInputs),
});
return finalValue;
})()}
onChange={(e) => {
const newValue = e.target.value;
const filterKey = `${filter.columnName}-${index}`;
@ -1896,7 +2012,8 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
.filter((col) => isFilterableWebType(getWidgetTypeFromColumn(col)))
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{getWebTypeIcon(getWidgetTypeFromColumn(col))} {col.columnLabel || col.columnName}
{getWebTypeIcon(getWidgetTypeFromColumn(col))}{" "}
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
@ -1953,11 +2070,22 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
})}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-sm">
<Settings className="h-4 w-4" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 페이지네이션 설정 */}
<TabsContent value="pagination" className="space-y-4">
<div className="space-y-4">
<h4 className="text-sm font-medium"> </h4>
<div className="flex items-center space-x-2">
<Checkbox
id="pagination-enabled"
@ -2067,6 +2195,14 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
</div>
)}
</div>
{/* 모달 설정은 여기에 추가 가능 */}
<div className="space-y-4">
<h4 className="text-sm font-medium"> </h4>
<p className="text-xs text-gray-500">/ .</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>

View File

@ -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));

View File

@ -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