[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 (
|
return (
|
||||||
<div
|
<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
|
isFocused
|
||||||
? "border-primary/40 shadow-[0_0_0_1px_hsl(var(--primary)/0.4)] scale-[1.03]"
|
? "border-primary/40 shadow-[0_0_0_1px_hsl(var(--primary)/0.4)] scale-[1.03]"
|
||||||
: isFaded
|
: isFaded
|
||||||
? "opacity-40 border-border/10 shadow-[0_4px_24px_-8px_rgba(0,0,0,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/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"
|
: "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={{
|
style={{
|
||||||
filter: isFaded
|
filter: isFaded
|
||||||
|
|
@ -175,23 +175,23 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="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
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="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
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="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">
|
<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" />
|
<Monitor className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -199,7 +199,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||||
<div className="truncate text-xs font-bold text-foreground">{label}</div>
|
<div className="truncate text-xs font-bold text-foreground">{label}</div>
|
||||||
{tableName && <div className="truncate text-[9px] text-muted-foreground font-mono">{tableName}</div>}
|
{tableName && <div className="truncate text-[9px] text-muted-foreground font-mono">{tableName}</div>}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* 화면 미리보기 영역 (컴팩트) */}
|
{/* 화면 미리보기 영역 (컴팩트) */}
|
||||||
|
|
@ -207,7 +207,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||||
{layoutSummary ? (
|
{layoutSummary ? (
|
||||||
<ScreenPreview layoutSummary={layoutSummary} screenType={screenType} />
|
<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)}
|
{getScreenTypeIcon(screenType)}
|
||||||
<span className="mt-1 text-[10px]">화면: {label}</span>
|
<span className="mt-1 text-[10px]">화면: {label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -215,7 +215,7 @@ export const ScreenNode: React.FC<{ data: ScreenNodeData }> = ({ data }) => {
|
||||||
</div>
|
</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] 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>
|
<span className="text-[9px] text-muted-foreground">{layoutSummary?.totalComponents ?? 0}개 컴포넌트</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -267,37 +267,37 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||||
// 그리드 화면 일러스트 (모노크롬)
|
// 그리드 화면 일러스트 (모노크롬)
|
||||||
if (screenType === "grid") {
|
if (screenType === "grid") {
|
||||||
return (
|
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="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="flex-1" />
|
||||||
<div className="h-4 w-8 rounded 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/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/8" />
|
<div className="h-4 w-8 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||||
</div>
|
</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) => (
|
{[...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>
|
||||||
{/* 테이블 행들 */}
|
{/* 테이블 행들 */}
|
||||||
<div className="flex flex-1 flex-col gap-1 overflow-hidden">
|
<div className="flex flex-1 flex-col gap-1 overflow-hidden">
|
||||||
{[...Array(7)].map((_, i) => (
|
{[...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) => (
|
{[...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>
|
</div>
|
||||||
{/* 페이지네이션 */}
|
{/* 페이지네이션 */}
|
||||||
<div className="flex items-center justify-center gap-2 pt-1">
|
<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/[0.12] dark:bg-foreground/8" />
|
||||||
<div className="h-2.5 w-4 rounded bg-foreground/12" />
|
<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/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/8" />
|
<div className="h-2.5 w-4 rounded bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -306,18 +306,18 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||||
// 폼 화면 일러스트 (모노크롬)
|
// 폼 화면 일러스트 (모노크롬)
|
||||||
if (screenType === "form") {
|
if (screenType === "form") {
|
||||||
return (
|
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) => (
|
{[...Array(6)].map((_, i) => (
|
||||||
<div key={i} className="flex items-center gap-3">
|
<div key={i} className="flex items-center gap-3">
|
||||||
<div className="h-2.5 w-14 rounded bg-foreground/8" />
|
<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/5 bg-card" />
|
<div className="h-5 flex-1 rounded-md border border-border/30 dark:border-border/5 bg-card" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* 버튼 영역 */}
|
{/* 버튼 영역 */}
|
||||||
<div className="mt-auto flex justify-end gap-2 border-t border-border/5 pt-3">
|
<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/8" />
|
<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/12" />
|
<div className="h-5 w-14 rounded-md bg-foreground/[0.18] dark:bg-foreground/12" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -326,23 +326,23 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||||
// 대시보드 화면 일러스트 (모노크롬)
|
// 대시보드 화면 일러스트 (모노크롬)
|
||||||
if (screenType === "dashboard") {
|
if (screenType === "dashboard") {
|
||||||
return (
|
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="rounded-lg bg-foreground/[0.08] dark:bg-foreground/5 p-2">
|
||||||
<div className="mb-2 h-2.5 w-10 rounded bg-foreground/10" />
|
<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/8" />
|
<div className="h-10 rounded-md bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg bg-foreground/5 p-2">
|
<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/10" />
|
<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/8" />
|
<div className="h-10 rounded-md bg-foreground/[0.12] dark:bg-foreground/8" />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 rounded-lg bg-foreground/5 p-2">
|
<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/10" />
|
<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">
|
<div className="flex h-14 items-end gap-1">
|
||||||
{[...Array(10)].map((_, i) => (
|
{[...Array(10)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
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}%` }}
|
style={{ height: `${25 + Math.random() * 75}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -355,13 +355,13 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||||
// 액션 화면 일러스트 (모노크롬)
|
// 액션 화면 일러스트 (모노크롬)
|
||||||
if (screenType === "action") {
|
if (screenType === "action") {
|
||||||
return (
|
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="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/5 p-4 text-muted-foreground">
|
<div className="rounded-full bg-foreground/[0.08] dark:bg-foreground/5 p-4 text-muted-foreground">
|
||||||
<MousePointer2 className="h-10 w-10" />
|
<MousePointer2 className="h-10 w-10" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<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/[0.18] dark: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.12] dark:bg-foreground/8" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs font-medium text-muted-foreground">액션 화면</div>
|
<div className="text-xs font-medium text-muted-foreground">액션 화면</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -370,8 +370,8 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType:
|
||||||
|
|
||||||
// 기본 (알 수 없는 타입, 모노크롬)
|
// 기본 (알 수 없는 타입, 모노크롬)
|
||||||
return (
|
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="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/5 p-4">
|
<div className="rounded-full bg-foreground/[0.08] dark:bg-foreground/5 p-4">
|
||||||
{getScreenTypeIcon(screenType)}
|
{getScreenTypeIcon(screenType)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">{totalComponents}개 컴포넌트</span>
|
<span className="text-sm font-medium">{totalComponents}개 컴포넌트</span>
|
||||||
|
|
@ -506,7 +506,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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. 필터 테이블 (마스터-디테일의 디테일 테이블)
|
// 1. 필터 테이블 (마스터-디테일의 디테일 테이블)
|
||||||
isFilterTable
|
isFilterTable
|
||||||
? "border-primary/30 shadow-[0_0_0_1px_hsl(var(--primary)/0.3)]"
|
? "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"
|
? "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. 흐리게 처리
|
// 4. 흐리게 처리
|
||||||
: isFaded
|
: isFaded
|
||||||
? "opacity-60 bg-card border-border/10"
|
? "opacity-60 bg-card border-border/40 dark:border-border/10"
|
||||||
// 5. 기본
|
// 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={{
|
style={{
|
||||||
filter: isFaded ? "grayscale(80%)" : "none",
|
filter: isFaded ? "grayscale(80%)" : "none",
|
||||||
|
|
@ -548,7 +548,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Top}
|
position={Position.Top}
|
||||||
id="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: 메인테이블 → 메인테이블 연결용 (위쪽으로 나가는 선) */}
|
{/* top source: 메인테이블 → 메인테이블 연결용 (위쪽으로 나가는 선) */}
|
||||||
<Handle
|
<Handle
|
||||||
|
|
@ -556,25 +556,25 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
position={Position.Top}
|
position={Position.Top}
|
||||||
id="top_source"
|
id="top_source"
|
||||||
style={{ top: -4 }}
|
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
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
id="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
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
id="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
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="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: 메인테이블 ← 메인테이블 연결용 (아래에서 들어오는 선) */}
|
{/* bottom target: 메인테이블 ← 메인테이블 연결용 (아래에서 들어오는 선) */}
|
||||||
<Handle
|
<Handle
|
||||||
|
|
@ -582,18 +582,18 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
id="bottom_target"
|
id="bottom_target"
|
||||||
style={{ bottom: -4 }}
|
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 + 아이콘 박스 */}
|
{/* 헤더: 그라디언트 제거, 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">
|
<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" />
|
<Database className="h-3.5 w-3.5 text-cyan-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="truncate text-[11px] font-semibold text-foreground font-mono">{label}</div>
|
<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
|
{isFilterSource
|
||||||
? "마스터 테이블 (필터 소스)"
|
? "마스터 테이블 (필터 소스)"
|
||||||
: hasFilterRelation
|
: hasFilterRelation
|
||||||
|
|
@ -602,7 +602,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasActiveColumns && (
|
{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
|
{displayColumns.length} ref
|
||||||
</span>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -767,21 +767,21 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 푸터: cols + PK/FK 카운트 */}
|
{/* 푸터: cols + PK/FK 카운트 */}
|
||||||
<div className="flex items-center justify-between border-t border-border/10 px-3.5 py-1.5 bg-background/50">
|
<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/40 font-mono tracking-[-0.3px]">
|
<span className="text-[9px] text-muted-foreground/70 dark:text-muted-foreground/40 font-mono tracking-[-0.3px]">
|
||||||
{hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount} cols
|
{hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount} cols
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2.5 text-[9px] font-mono tracking-[-0.3px]">
|
<div className="flex gap-2.5 text-[9px] font-mono tracking-[-0.3px]">
|
||||||
{columns?.some(c => c.isPrimaryKey) && (
|
{columns?.some(c => c.isPrimaryKey) && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="w-1 h-1 rounded-full bg-amber-400" />
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{columns?.some(c => c.isForeignKey) && (
|
{columns?.some(c => c.isForeignKey) && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="w-1 h-1 rounded-full bg-primary" />
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue