feat: 화면 서브 테이블 정보 조회 기능 추가
- 화면 그룹에 대한 서브 테이블 관계를 조회하는 API 및 라우트 구현 - 화면 그룹 목록에서 서브 테이블 정보를 포함하여 데이터 흐름을 시각화 - 프론트엔드에서 화면 선택 시 그룹 및 서브 테이블 정보 연동 기능 추가 - 화면 노드 및 관계 시각화 컴포넌트에 서브 테이블 정보 통합
This commit is contained in:
@@ -26,6 +26,11 @@ export interface ScreenNodeData {
|
||||
isMain?: boolean;
|
||||
// 레이아웃 요약 정보 (미리보기용)
|
||||
layoutSummary?: ScreenLayoutSummary;
|
||||
// 그룹 내 포커스 관련 속성
|
||||
isInGroup?: boolean; // 그룹 모드인지
|
||||
isFocused?: boolean; // 포커스된 화면인지
|
||||
isFaded?: boolean; // 흑백 처리할지
|
||||
screenRole?: string; // 화면 역할 (메인그리드, 등록폼 등)
|
||||
}
|
||||
|
||||
// 테이블 노드 데이터 인터페이스
|
||||
@@ -72,6 +77,29 @@ const getScreenTypeColor = (screenType?: string, isMain?: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 화면 역할(screenRole)에 따른 색상
|
||||
const getScreenRoleColor = (screenRole?: string) => {
|
||||
if (!screenRole) return "bg-slate-400";
|
||||
|
||||
// 역할명에 포함된 키워드로 색상 결정
|
||||
const role = screenRole.toLowerCase();
|
||||
|
||||
if (role.includes("그리드") || role.includes("grid") || role.includes("메인") || role.includes("main") || role.includes("list")) {
|
||||
return "bg-violet-500"; // 보라색 - 메인 그리드
|
||||
}
|
||||
if (role.includes("등록") || role.includes("폼") || role.includes("form") || role.includes("register") || role.includes("input")) {
|
||||
return "bg-blue-500"; // 파란색 - 등록 폼
|
||||
}
|
||||
if (role.includes("액션") || role.includes("action") || role.includes("이벤트") || role.includes("event") || role.includes("클릭")) {
|
||||
return "bg-rose-500"; // 빨간색 - 액션/이벤트
|
||||
}
|
||||
if (role.includes("상세") || role.includes("detail") || role.includes("popup") || role.includes("팝업")) {
|
||||
return "bg-amber-500"; // 주황색 - 상세/팝업
|
||||
}
|
||||
|
||||
return "bg-slate-400"; // 기본 회색
|
||||
};
|
||||
|
||||
// 화면 타입별 라벨
|
||||
const getScreenTypeLabel = (screenType?: string) => {
|
||||
switch (screenType) {
|
||||
@@ -88,12 +116,38 @@ const getScreenTypeLabel = (screenType?: string) => {
|
||||
|
||||
// ========== 화면 노드 (상단) - 미리보기 표시 ==========
|
||||
export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||
const { label, subLabel, isMain, tableName, layoutSummary } = data;
|
||||
const { label, subLabel, isMain, tableName, layoutSummary, isInGroup, isFocused, isFaded, screenRole } = data;
|
||||
const screenType = layoutSummary?.screenType || "form";
|
||||
const headerColor = getScreenTypeColor(screenType, isMain);
|
||||
|
||||
// 그룹 모드에서는 screenRole 기반 색상, 그렇지 않으면 screenType 기반 색상
|
||||
// isFocused일 때 색상 활성화, isFaded일 때 회색
|
||||
let headerColor: string;
|
||||
if (isInGroup) {
|
||||
if (isFaded) {
|
||||
headerColor = "bg-gray-300"; // 흑백 처리 - 더 확실한 회색
|
||||
} else {
|
||||
// 포커스되었거나 아직 아무것도 선택 안됐을 때: 역할별 색상
|
||||
headerColor = getScreenRoleColor(screenRole);
|
||||
}
|
||||
} else {
|
||||
headerColor = getScreenTypeColor(screenType, isMain);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative flex h-[320px] w-[260px] flex-col overflow-hidden rounded-lg border border-border bg-card shadow-md transition-all hover:shadow-lg hover:ring-2 hover:ring-primary/20">
|
||||
<div
|
||||
className={`group relative flex h-[320px] w-[260px] flex-col overflow-hidden rounded-lg border bg-card shadow-md transition-all cursor-pointer ${
|
||||
isFocused
|
||||
? "border-2 border-primary ring-4 ring-primary/50 shadow-xl scale-105"
|
||||
: isFaded
|
||||
? "border-gray-200 opacity-50"
|
||||
: "border-border hover:shadow-lg hover:ring-2 hover:ring-primary/20"
|
||||
}`}
|
||||
style={{
|
||||
filter: isFaded ? "grayscale(100%)" : "none",
|
||||
transition: "all 0.3s ease",
|
||||
transform: isFocused ? "scale(1.02)" : "scale(1)",
|
||||
}}
|
||||
>
|
||||
{/* Handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
@@ -115,10 +169,10 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||
/>
|
||||
|
||||
{/* 헤더 (컬러) */}
|
||||
<div className={`flex items-center gap-2 px-3 py-2 text-white ${headerColor}`}>
|
||||
<div className={`flex items-center gap-2 px-3 py-2 text-white ${headerColor} transition-colors duration-300`}>
|
||||
<Monitor className="h-4 w-4" />
|
||||
<span className="flex-1 truncate text-xs font-semibold">{label}</span>
|
||||
{isMain && <span className="flex h-2 w-2 rounded-full bg-white/80" />}
|
||||
{(isMain || isFocused) && <span className="flex h-2 w-2 rounded-full bg-white/80 animate-pulse" />}
|
||||
</div>
|
||||
|
||||
{/* 화면 미리보기 영역 (컴팩트) */}
|
||||
@@ -160,7 +214,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||
<div className="text-center text-[9px] text-slate-400 py-2">필드 정보 없음</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 (테이블 정보) */}
|
||||
<div className="flex items-center justify-between border-t border-border bg-muted/30 px-3 py-1.5">
|
||||
@@ -219,7 +273,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||
|
||||
// 그리드 화면 일러스트
|
||||
if (screenType === "grid") {
|
||||
return (
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 rounded-lg border border-slate-200 bg-gradient-to-b from-slate-50 to-white p-3">
|
||||
{/* 상단 툴바 */}
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -296,7 +350,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||
<div className="rounded-lg bg-amber-100 p-2 shadow-sm">
|
||||
<div className="mb-2 h-2.5 w-10 rounded bg-amber-400" />
|
||||
<div className="h-10 rounded-md bg-amber-300/80" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 rounded-lg bg-blue-100 p-2 shadow-sm">
|
||||
<div className="mb-2 h-2.5 w-12 rounded bg-blue-400" />
|
||||
<div className="flex h-14 items-end gap-1">
|
||||
@@ -313,7 +367,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||
<div className="absolute bottom-2 right-2 rounded-md bg-slate-800/80 px-2 py-1 text-[10px] font-medium text-white shadow-sm">
|
||||
{totalComponents}개
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,11 +377,11 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 rounded-lg border border-slate-200 bg-gradient-to-b from-slate-50 to-white p-3">
|
||||
<div className="rounded-full bg-slate-100 p-4 text-slate-400">
|
||||
<MousePointer2 className="h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="h-7 w-16 rounded-md bg-blue-500 shadow-sm" />
|
||||
<div className="h-7 w-16 rounded-md bg-slate-300 shadow-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-medium text-slate-400">액션 화면</div>
|
||||
{/* 컴포넌트 수 */}
|
||||
<div className="absolute bottom-2 right-2 rounded-md bg-slate-800/80 px-2 py-1 text-[10px] font-medium text-white shadow-sm">
|
||||
@@ -348,12 +402,15 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||
);
|
||||
};
|
||||
|
||||
// ========== 테이블 노드 (하단) - 컬럼 목록 표시 ==========
|
||||
// ========== 테이블 노드 (하단) - 컬럼 목록 표시 (컴팩트) ==========
|
||||
export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||
const { label, subLabel, isMain, columns } = data;
|
||||
// 최대 5개 컬럼만 표시
|
||||
const displayColumns = columns?.slice(0, 5) || [];
|
||||
const remainingCount = (columns?.length || 0) - 5;
|
||||
|
||||
return (
|
||||
<div className="group relative flex h-[320px] w-[260px] flex-col overflow-hidden rounded-lg border border-border bg-card shadow-md transition-all hover:shadow-lg hover:ring-2 hover:ring-emerald-500/20">
|
||||
<div className="group relative flex w-[260px] flex-col overflow-hidden rounded-lg border border-border bg-card shadow-md transition-all hover:shadow-lg hover:ring-2 hover:ring-emerald-500/20">
|
||||
{/* Handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
@@ -373,57 +430,63 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||
id="right"
|
||||
className="!h-2 !w-2 !border-2 !border-background !bg-emerald-500 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="bottom"
|
||||
className="!h-2 !w-2 !border-2 !border-background !bg-orange-500 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
|
||||
{/* 헤더 (초록색) */}
|
||||
<div className={`flex items-center gap-2 px-3 py-2 text-white ${isMain ? "bg-emerald-600" : "bg-slate-500"}`}>
|
||||
<Database className="h-4 w-4" />
|
||||
{/* 헤더 (초록색, 컴팩트) */}
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 text-white ${isMain ? "bg-emerald-600" : "bg-slate-500"}`}>
|
||||
<Database className="h-3.5 w-3.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate text-xs font-semibold">{label}</div>
|
||||
{subLabel && <div className="truncate text-[10px] opacity-80">{subLabel}</div>}
|
||||
<div className="truncate text-[11px] font-semibold">{label}</div>
|
||||
{subLabel && <div className="truncate text-[9px] opacity-80">{subLabel}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 목록 */}
|
||||
<div className="flex-1 overflow-hidden p-2">
|
||||
{columns && columns.length > 0 ? (
|
||||
<div className="flex h-full flex-col gap-0.5">
|
||||
{columns.map((col, idx) => (
|
||||
{/* 컬럼 목록 (컴팩트) */}
|
||||
<div className="p-1.5">
|
||||
{displayColumns.length > 0 ? (
|
||||
<div className="flex flex-col gap-px">
|
||||
{displayColumns.map((col, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center gap-1.5 rounded bg-slate-50 px-2 py-1 hover:bg-slate-100"
|
||||
className="flex items-center gap-1 rounded bg-slate-50 px-1.5 py-0.5 hover:bg-slate-100"
|
||||
>
|
||||
{/* PK/FK 아이콘 */}
|
||||
{col.isPrimaryKey && <Key className="h-3 w-3 text-amber-500" />}
|
||||
{col.isForeignKey && !col.isPrimaryKey && <Link2 className="h-3 w-3 text-blue-500" />}
|
||||
{!col.isPrimaryKey && !col.isForeignKey && <div className="w-3" />}
|
||||
{col.isPrimaryKey && <Key className="h-2.5 w-2.5 text-amber-500" />}
|
||||
{col.isForeignKey && !col.isPrimaryKey && <Link2 className="h-2.5 w-2.5 text-blue-500" />}
|
||||
{!col.isPrimaryKey && !col.isForeignKey && <div className="w-2.5" />}
|
||||
|
||||
{/* 컬럼명 */}
|
||||
<span className="flex-1 truncate font-mono text-[10px] font-medium text-slate-700">
|
||||
<span className="flex-1 truncate font-mono text-[9px] font-medium text-slate-700">
|
||||
{col.name}
|
||||
</span>
|
||||
|
||||
{/* 타입 */}
|
||||
<span className="text-[9px] text-slate-400">{col.type}</span>
|
||||
<span className="text-[8px] text-slate-400">{col.type}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{columns.length > 8 && (
|
||||
<div className="text-center text-[9px] text-slate-400">... 외 {columns.length - 8}개 컬럼</div>
|
||||
{remainingCount > 0 && (
|
||||
<div className="text-center text-[8px] text-slate-400 py-0.5">+ {remainingCount}개 더</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
||||
<Database className="h-6 w-6 text-slate-300" />
|
||||
<span className="mt-1 text-[9px] text-slate-400">컬럼 정보 없음</span>
|
||||
<div className="flex flex-col items-center justify-center py-2 text-muted-foreground">
|
||||
<Database className="h-4 w-4 text-slate-300" />
|
||||
<span className="mt-0.5 text-[8px] text-slate-400">컬럼 정보 없음</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex items-center justify-between border-t border-border bg-muted/30 px-3 py-1.5">
|
||||
<span className="text-[10px] text-muted-foreground">PostgreSQL</span>
|
||||
{/* 푸터 (컴팩트) */}
|
||||
<div className="flex items-center justify-between border-t border-border bg-muted/30 px-2 py-1">
|
||||
<span className="text-[9px] text-muted-foreground">PostgreSQL</span>
|
||||
{columns && (
|
||||
<span className="text-[10px] text-muted-foreground">{columns.length}개 컬럼</span>
|
||||
<span className="text-[9px] text-muted-foreground">{columns.length}개 컬럼</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user