[agent-pipeline] pipe-20260315131310-l8kw round-1
This commit is contained in:
parent
8ed7faf517
commit
232650bc07
|
|
@ -2058,6 +2058,119 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons
|
|||
});
|
||||
});
|
||||
|
||||
// 6. v2-repeater 컴포넌트에서 selectedTable/foreignKey 추출
|
||||
const v2RepeaterQuery = `
|
||||
SELECT DISTINCT
|
||||
sd.screen_id,
|
||||
sd.screen_name,
|
||||
sd.table_name as main_table,
|
||||
comp->'overrides'->>'type' as component_type,
|
||||
comp->'overrides'->>'selectedTable' as sub_table,
|
||||
comp->'overrides'->>'foreignKey' as foreign_key,
|
||||
comp->'overrides'->>'parentTable' as parent_table
|
||||
FROM screen_definitions sd
|
||||
JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id,
|
||||
jsonb_array_elements(slv2.layout_data->'components') as comp
|
||||
WHERE sd.screen_id = ANY($1)
|
||||
AND comp->'overrides'->>'type' = 'v2-repeater'
|
||||
AND comp->'overrides'->>'selectedTable' IS NOT NULL
|
||||
`;
|
||||
const v2RepeaterResult = await pool.query(v2RepeaterQuery, [screenIds]);
|
||||
v2RepeaterResult.rows.forEach((row: any) => {
|
||||
const screenId = row.screen_id;
|
||||
const mainTable = row.main_table;
|
||||
const subTable = row.sub_table;
|
||||
const foreignKey = row.foreign_key;
|
||||
if (!subTable || subTable === mainTable) return;
|
||||
if (!screenSubTables[screenId]) {
|
||||
screenSubTables[screenId] = {
|
||||
screenId,
|
||||
screenName: row.screen_name,
|
||||
mainTable: mainTable || '',
|
||||
subTables: [],
|
||||
};
|
||||
}
|
||||
const exists = screenSubTables[screenId].subTables.some(
|
||||
(st) => st.tableName === subTable
|
||||
);
|
||||
if (!exists) {
|
||||
screenSubTables[screenId].subTables.push({
|
||||
tableName: subTable,
|
||||
componentType: 'v2-repeater',
|
||||
relationType: 'rightPanelRelation',
|
||||
fieldMappings: foreignKey ? [{
|
||||
sourceField: 'id',
|
||||
targetField: foreignKey,
|
||||
sourceDisplayName: 'ID',
|
||||
targetDisplayName: foreignKey,
|
||||
}] : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
logger.info("v2-repeater 서브 테이블 추출 완료", {
|
||||
screenIds,
|
||||
v2RepeaterCount: v2RepeaterResult.rows.length,
|
||||
});
|
||||
|
||||
// 7. rightPanel.components 내부의 componentConfig.detailTable 추출 (v2-bom-tree 등)
|
||||
const v2DetailTableQuery = `
|
||||
SELECT DISTINCT
|
||||
sd.screen_id,
|
||||
sd.screen_name,
|
||||
sd.table_name as main_table,
|
||||
inner_comp->>'type' as component_type,
|
||||
inner_comp->'componentConfig'->>'detailTable' as sub_table,
|
||||
inner_comp->'componentConfig'->>'foreignKey' as foreign_key
|
||||
FROM screen_definitions sd
|
||||
JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id,
|
||||
jsonb_array_elements(slv2.layout_data->'components') as comp,
|
||||
jsonb_array_elements(
|
||||
COALESCE(
|
||||
comp->'overrides'->'rightPanel'->'components',
|
||||
comp->'overrides'->'leftPanel'->'components',
|
||||
'[]'::jsonb
|
||||
)
|
||||
) as inner_comp
|
||||
WHERE sd.screen_id = ANY($1)
|
||||
AND inner_comp->'componentConfig'->>'detailTable' IS NOT NULL
|
||||
`;
|
||||
const v2DetailTableResult = await pool.query(v2DetailTableQuery, [screenIds]);
|
||||
v2DetailTableResult.rows.forEach((row: any) => {
|
||||
const screenId = row.screen_id;
|
||||
const mainTable = row.main_table;
|
||||
const subTable = row.sub_table;
|
||||
const foreignKey = row.foreign_key;
|
||||
if (!subTable || subTable === mainTable) return;
|
||||
if (!screenSubTables[screenId]) {
|
||||
screenSubTables[screenId] = {
|
||||
screenId,
|
||||
screenName: row.screen_name,
|
||||
mainTable: mainTable || '',
|
||||
subTables: [],
|
||||
};
|
||||
}
|
||||
const exists = screenSubTables[screenId].subTables.some(
|
||||
(st) => st.tableName === subTable
|
||||
);
|
||||
if (!exists) {
|
||||
screenSubTables[screenId].subTables.push({
|
||||
tableName: subTable,
|
||||
componentType: row.component_type || 'v2-bom-tree',
|
||||
relationType: 'rightPanelRelation',
|
||||
fieldMappings: foreignKey ? [{
|
||||
sourceField: 'id',
|
||||
targetField: foreignKey,
|
||||
sourceDisplayName: 'ID',
|
||||
targetDisplayName: foreignKey,
|
||||
}] : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
logger.info("v2-bom-tree/detailTable 서브 테이블 추출 완료", {
|
||||
screenIds,
|
||||
v2DetailTableCount: v2DetailTableResult.rows.length,
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 저장 테이블 정보 추출
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -153,12 +153,12 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`group relative flex h-[240px] w-[240px] flex-col overflow-hidden rounded-[10px] border bg-card/80 backdrop-blur-sm transition-all cursor-pointer ${
|
||||
className={`group relative flex h-[240px] w-[240px] flex-col overflow-hidden rounded-[10px] border bg-card dark:bg-card/80 backdrop-blur-sm transition-all cursor-pointer ${
|
||||
isFocused
|
||||
? "border-primary/40 shadow-[0_0_0_1px_hsl(var(--primary)/0.4)] scale-[1.03]"
|
||||
: isFaded
|
||||
? "opacity-40 border-border/10 shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)]"
|
||||
: "border-border/10 shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)] hover:border-border/20 hover:shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)] hover:-translate-y-0.5"
|
||||
? "opacity-40 border-border/40 dark:border-border/10 shadow-[0_4px_24px_-8px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)]"
|
||||
: "border-border/40 dark:border-border/10 shadow-[0_4px_24px_-8px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)] hover:border-border/50 dark:hover:border-border/20 hover:shadow-[0_4px_24px_-8px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)] hover:-translate-y-0.5"
|
||||
}`}
|
||||
style={{
|
||||
filter: isFaded
|
||||
|
|
@ -175,23 +175,23 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
|||
type="target"
|
||||
position={Position.Left}
|
||||
id="left"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="right"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="bottom"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-all duration-300 group-hover:opacity-100 group-hover:shadow-[0_0_6px_hsl(var(--primary)/0.5)]"
|
||||
/>
|
||||
|
||||
{/* 헤더: 그라디언트 제거, 모노크롬 */}
|
||||
<div className="flex items-center gap-2 border-b border-border/10 bg-muted/30 px-3 py-2 transition-colors duration-300">
|
||||
<div className="flex items-center gap-2 border-b border-border/40 dark:border-border/10 bg-muted/50 dark:bg-muted/30 px-3 py-2 transition-colors duration-300">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-primary/10 text-primary">
|
||||
<Monitor className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
|
|
@ -199,7 +199,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
|||
<div className="truncate text-xs font-bold text-foreground">{label}</div>
|
||||
{tableName && <div className="truncate text-[9px] text-muted-foreground font-mono">{tableName}</div>}
|
||||
</div>
|
||||
{(isMain || isFocused) && <span className="flex h-2 w-2 rounded-full bg-foreground/8 animate-pulse" />}
|
||||
{(isMain || isFocused) && <span className="flex h-2 w-2 rounded-full bg-foreground/[0.12] dark:bg-foreground/8 animate-pulse" />}
|
||||
</div>
|
||||
|
||||
{/* 화면 미리보기 영역 (컴팩트) */}
|
||||
|
|
@ -207,7 +207,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
|||
{layoutSummary ? (
|
||||
<ScreenPreview layoutSummary={layoutSummary} screenType={screenType} />
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground/40">
|
||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground/70 dark:text-muted-foreground/40">
|
||||
{getScreenTypeIcon(screenType)}
|
||||
<span className="mt-1 text-[10px]">화면: {label}</span>
|
||||
</div>
|
||||
|
|
@ -215,7 +215,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
|||
</div>
|
||||
|
||||
{/* 푸터 (타입 칩 + 컴포넌트 수) */}
|
||||
<div className="flex items-center justify-between border-t border-border/10 bg-background/50 px-3 py-1.5">
|
||||
<div className="flex items-center justify-between border-t border-border/40 dark:border-border/10 bg-background dark:bg-background/50 px-3 py-1.5">
|
||||
<span className="text-[9px] font-medium px-[7px] py-[2px] rounded bg-primary/10 text-primary">{getScreenTypeLabel(screenType)}</span>
|
||||
<span className="text-[9px] text-muted-foreground">{layoutSummary?.totalComponents ?? 0}개 컴포넌트</span>
|
||||
</div>
|
||||
|
|
@ -267,37 +267,37 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
|||
// 그리드 화면 일러스트 (모노크롬)
|
||||
if (screenType === "grid") {
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 rounded-lg border border-border/5 bg-muted/10 p-3">
|
||||
<div className="flex h-full flex-col gap-2 rounded-lg border border-border/30 dark:border-border/5 bg-muted/30 dark:bg-muted/10 p-3">
|
||||
{/* 상단 툴바 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-16 rounded bg-foreground/10" />
|
||||
<div className="h-4 w-16 rounded bg-foreground/[0.15] dark:bg-foreground/10" />
|
||||
<div className="flex-1" />
|
||||
<div className="h-4 w-8 rounded bg-foreground/12" />
|
||||
<div className="h-4 w-8 rounded bg-foreground/12" />
|
||||
<div className="h-4 w-8 rounded bg-foreground/8" />
|
||||
<div className="h-4 w-8 rounded bg-foreground/[0.18] dark:bg-foreground/12" />
|
||||
<div className="h-4 w-8 rounded bg-foreground/[0.18] dark:bg-foreground/12" />
|
||||
<div className="h-4 w-8 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
</div>
|
||||
{/* 테이블 헤더 */}
|
||||
<div className="flex gap-1 rounded-t-md bg-foreground/12 px-2 py-2">
|
||||
<div className="flex gap-1 rounded-t-md bg-foreground/[0.18] dark:bg-foreground/12 px-2 py-2">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="h-2.5 flex-1 rounded bg-foreground/8" />
|
||||
<div key={i} className="h-2.5 flex-1 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
))}
|
||||
</div>
|
||||
{/* 테이블 행들 */}
|
||||
<div className="flex flex-1 flex-col gap-1 overflow-hidden">
|
||||
{[...Array(7)].map((_, i) => (
|
||||
<div key={i} className={`flex gap-1 rounded px-2 py-1.5 ${i % 2 === 0 ? "bg-muted/10" : "bg-card"}`}>
|
||||
<div key={i} className={`flex gap-1 rounded px-2 py-1.5 ${i % 2 === 0 ? "bg-muted/30 dark:bg-muted/10" : "bg-card"}`}>
|
||||
{[...Array(5)].map((_, j) => (
|
||||
<div key={j} className="h-2 flex-1 rounded bg-foreground/6" />
|
||||
<div key={j} className="h-2 flex-1 rounded bg-foreground/[0.1] dark:bg-foreground/6" />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-center gap-2 pt-1">
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/8" />
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/12" />
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/8" />
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/8" />
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/[0.18] dark:bg-foreground/12" />
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
<div className="h-2.5 w-4 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -306,18 +306,18 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
|||
// 폼 화면 일러스트 (모노크롬)
|
||||
if (screenType === "form") {
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-3 rounded-lg border border-border/5 bg-muted/10 p-3">
|
||||
<div className="flex h-full flex-col gap-3 rounded-lg border border-border/30 dark:border-border/5 bg-muted/30 dark:bg-muted/10 p-3">
|
||||
{/* 폼 필드들 */}
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<div className="h-2.5 w-14 rounded bg-foreground/8" />
|
||||
<div className="h-5 flex-1 rounded-md border border-border/5 bg-card" />
|
||||
<div className="h-2.5 w-14 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
<div className="h-5 flex-1 rounded-md border border-border/30 dark:border-border/5 bg-card" />
|
||||
</div>
|
||||
))}
|
||||
{/* 버튼 영역 */}
|
||||
<div className="mt-auto flex justify-end gap-2 border-t border-border/5 pt-3">
|
||||
<div className="h-5 w-14 rounded-md bg-foreground/8" />
|
||||
<div className="h-5 w-14 rounded-md bg-foreground/12" />
|
||||
<div className="mt-auto flex justify-end gap-2 border-t border-border/30 dark:border-border/5 pt-3">
|
||||
<div className="h-5 w-14 rounded-md bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
<div className="h-5 w-14 rounded-md bg-foreground/[0.18] dark:bg-foreground/12" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -326,23 +326,23 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
|||
// 대시보드 화면 일러스트 (모노크롬)
|
||||
if (screenType === "dashboard") {
|
||||
return (
|
||||
<div className="grid h-full grid-cols-2 gap-2 rounded-lg border border-border/5 bg-muted/10 p-3">
|
||||
<div className="grid h-full grid-cols-2 gap-2 rounded-lg border border-border/30 dark:border-border/5 bg-muted/30 dark:bg-muted/10 p-3">
|
||||
{/* 카드/차트들 */}
|
||||
<div className="rounded-lg bg-foreground/5 p-2">
|
||||
<div className="mb-2 h-2.5 w-10 rounded bg-foreground/10" />
|
||||
<div className="h-10 rounded-md bg-foreground/8" />
|
||||
<div className="rounded-lg bg-foreground/[0.08] dark:bg-foreground/5 p-2">
|
||||
<div className="mb-2 h-2.5 w-10 rounded bg-foreground/[0.15] dark:bg-foreground/10" />
|
||||
<div className="h-10 rounded-md bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
</div>
|
||||
<div className="rounded-lg bg-foreground/5 p-2">
|
||||
<div className="mb-2 h-2.5 w-10 rounded bg-foreground/10" />
|
||||
<div className="h-10 rounded-md bg-foreground/8" />
|
||||
<div className="rounded-lg bg-foreground/[0.08] dark:bg-foreground/5 p-2">
|
||||
<div className="mb-2 h-2.5 w-10 rounded bg-foreground/[0.15] dark:bg-foreground/10" />
|
||||
<div className="h-10 rounded-md bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
</div>
|
||||
<div className="col-span-2 rounded-lg bg-foreground/5 p-2">
|
||||
<div className="mb-2 h-2.5 w-12 rounded bg-foreground/10" />
|
||||
<div className="col-span-2 rounded-lg bg-foreground/[0.08] dark:bg-foreground/5 p-2">
|
||||
<div className="mb-2 h-2.5 w-12 rounded bg-foreground/[0.15] dark:bg-foreground/10" />
|
||||
<div className="flex h-14 items-end gap-1">
|
||||
{[...Array(10)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 rounded-t bg-foreground/10"
|
||||
className="flex-1 rounded-t bg-foreground/[0.15] dark:bg-foreground/10"
|
||||
style={{ height: `${25 + Math.random() * 75}%` }}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -355,13 +355,13 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
|||
// 액션 화면 일러스트 (모노크롬)
|
||||
if (screenType === "action") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 rounded-lg border border-border/5 bg-muted/10 p-3">
|
||||
<div className="rounded-full bg-foreground/5 p-4 text-muted-foreground">
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 rounded-lg border border-border/30 dark:border-border/5 bg-muted/30 dark:bg-muted/10 p-3">
|
||||
<div className="rounded-full bg-foreground/[0.08] dark:bg-foreground/5 p-4 text-muted-foreground">
|
||||
<MousePointer2 className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="h-7 w-16 rounded-md bg-foreground/12" />
|
||||
<div className="h-7 w-16 rounded-md bg-foreground/8" />
|
||||
<div className="h-7 w-16 rounded-md bg-foreground/[0.18] dark:bg-foreground/12" />
|
||||
<div className="h-7 w-16 rounded-md bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||
</div>
|
||||
<div className="text-xs font-medium text-muted-foreground">액션 화면</div>
|
||||
</div>
|
||||
|
|
@ -370,8 +370,8 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
|||
|
||||
// 기본 (알 수 없는 타입, 모노크롬)
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 rounded-lg border border-border/5 bg-muted/10 text-muted-foreground">
|
||||
<div className="rounded-full bg-foreground/5 p-4">
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 rounded-lg border border-border/30 dark:border-border/5 bg-muted/30 dark:bg-muted/10 text-muted-foreground">
|
||||
<div className="rounded-full bg-foreground/[0.08] dark:bg-foreground/5 p-4">
|
||||
{getScreenTypeIcon(screenType)}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{totalComponents}개 컴포넌트</span>
|
||||
|
|
@ -506,7 +506,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`group relative flex w-[260px] flex-col overflow-visible rounded-[10px] border bg-card/80 backdrop-blur-sm shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)] ${
|
||||
className={`group relative flex w-[260px] flex-col overflow-visible rounded-[10px] border bg-card dark:bg-card/80 backdrop-blur-sm shadow-[0_4px_24px_-8px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_24px_-8px_rgba(0,0,0,0.5)] ${
|
||||
// 1. 필터 테이블 (마스터-디테일의 디테일 테이블)
|
||||
isFilterTable
|
||||
? "border-primary/30 shadow-[0_0_0_1px_hsl(var(--primary)/0.3)]"
|
||||
|
|
@ -518,9 +518,9 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
? "border-primary/30 shadow-[0_0_0_1px_hsl(var(--primary)/0.3),0_0_24px_-8px_hsl(var(--primary)/0.1)] bg-card"
|
||||
// 4. 흐리게 처리
|
||||
: isFaded
|
||||
? "opacity-60 bg-card border-border/10"
|
||||
? "opacity-60 bg-card border-border/40 dark:border-border/10"
|
||||
// 5. 기본
|
||||
: "border-border/10 hover:border-border/20"
|
||||
: "border-border/40 dark:border-border/10 hover:border-border/50 dark:hover:border-border/20"
|
||||
}`}
|
||||
style={{
|
||||
filter: isFaded ? "grayscale(80%)" : "none",
|
||||
|
|
@ -548,7 +548,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
type="target"
|
||||
position={Position.Top}
|
||||
id="top"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* top source: 메인테이블 → 메인테이블 연결용 (위쪽으로 나가는 선) */}
|
||||
<Handle
|
||||
|
|
@ -556,25 +556,25 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
position={Position.Top}
|
||||
id="top_source"
|
||||
style={{ top: -4 }}
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="left"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="right"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="bottom"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/* bottom target: 메인테이블 ← 메인테이블 연결용 (아래에서 들어오는 선) */}
|
||||
<Handle
|
||||
|
|
@ -582,18 +582,18 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
position={Position.Bottom}
|
||||
id="bottom_target"
|
||||
style={{ bottom: -4 }}
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="!h-2 !w-2 !border-[1.5px] !border-card !bg-muted-foreground/70 dark:!bg-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
|
||||
{/* 헤더: 그라디언트 제거, bg-muted/30 + 아이콘 박스 */}
|
||||
<div className="flex items-center gap-2.5 px-3.5 py-2.5 border-b border-border/10 bg-muted/30 rounded-t-[10px] transition-colors duration-700 ease-in-out">
|
||||
<div className="flex items-center gap-2.5 px-3.5 py-2.5 border-b border-border/40 dark:border-border/10 bg-muted/50 dark:bg-muted/30 rounded-t-[10px] transition-colors duration-700 ease-in-out">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-[7px] bg-cyan-500/10 shrink-0">
|
||||
<Database className="h-3.5 w-3.5 text-cyan-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate text-[11px] font-semibold text-foreground font-mono">{label}</div>
|
||||
{/* 필터 관계에 따른 문구 변경 */}
|
||||
<div className="truncate text-[9px] font-mono text-muted-foreground/40 tracking-[-0.3px]">
|
||||
<div className="truncate text-[9px] font-mono text-muted-foreground/70 dark:text-muted-foreground/40 tracking-[-0.3px]">
|
||||
{isFilterSource
|
||||
? "마스터 테이블 (필터 소스)"
|
||||
: hasFilterRelation
|
||||
|
|
@ -602,7 +602,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
</div>
|
||||
</div>
|
||||
{hasActiveColumns && (
|
||||
<span className="text-[9px] font-mono text-muted-foreground/40 px-1.5 py-0.5 rounded bg-foreground/5 border border-border/10 tracking-[-0.3px] shrink-0">
|
||||
<span className="text-[9px] font-mono text-muted-foreground/70 dark:text-muted-foreground/40 px-1.5 py-0.5 rounded bg-foreground/[0.08] dark:bg-foreground/5 border border-border/40 dark:border-border/10 tracking-[-0.3px] shrink-0">
|
||||
{displayColumns.length} ref
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -747,7 +747,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
)}
|
||||
|
||||
{/* 타입 */}
|
||||
<span className="text-[8px] text-muted-foreground/30 font-mono tracking-[-0.3px]">{col.type}</span>
|
||||
<span className="text-[8px] text-muted-foreground/60 dark:text-muted-foreground/30 font-mono tracking-[-0.3px]">{col.type}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -767,21 +767,21 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
</div>
|
||||
|
||||
{/* 푸터: cols + PK/FK 카운트 */}
|
||||
<div className="flex items-center justify-between border-t border-border/10 px-3.5 py-1.5 bg-background/50">
|
||||
<span className="text-[9px] text-muted-foreground/40 font-mono tracking-[-0.3px]">
|
||||
<div className="flex items-center justify-between border-t border-border/40 dark:border-border/10 px-3.5 py-1.5 bg-background dark:bg-background/50">
|
||||
<span className="text-[9px] text-muted-foreground/70 dark:text-muted-foreground/40 font-mono tracking-[-0.3px]">
|
||||
{hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount} cols
|
||||
</span>
|
||||
<div className="flex gap-2.5 text-[9px] font-mono tracking-[-0.3px]">
|
||||
{columns?.some(c => c.isPrimaryKey) && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-1 h-1 rounded-full bg-amber-400" />
|
||||
<span className="text-muted-foreground/40">PK {columns.filter(c => c.isPrimaryKey).length}</span>
|
||||
<span className="text-muted-foreground/70 dark:text-muted-foreground/40">PK {columns.filter(c => c.isPrimaryKey).length}</span>
|
||||
</span>
|
||||
)}
|
||||
{columns?.some(c => c.isForeignKey) && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-1 h-1 rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground/40">FK {columns.filter(c => c.isForeignKey).length}</span>
|
||||
<span className="text-muted-foreground/70 dark:text-muted-foreground/40">FK {columns.filter(c => c.isForeignKey).length}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue