화면 복사기능 구현
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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