2f398ae0b3
- 제어모드 IDE: ControlCardPanel, control/ide/* (Canvas/LeftRail/RightRail/PanZoomStage/V3RuleNode 등), schemas, lib/api/control - 레지스트리 정리: aggregation-widget, status-count, section-card/paper, table-list(legacy/v2), tabs-widget 폐기 → table/_shared/ 로 통합 - InvLegacyButtonConfigPanel cp 마이그레이션 - canonical data view cleanup 후속 노트
218 lines
7.3 KiB
TypeScript
218 lines
7.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { Eye, Wrench, Save, FolderOpen, X, Database } from 'lucide-react';
|
|
import { useControlMode } from './hooks/useControlMode';
|
|
import {
|
|
getBusinessRuleList,
|
|
getBusinessRuleInfo,
|
|
insertBusinessRule,
|
|
updateBusinessRule,
|
|
} from '@/lib/api/businessRule';
|
|
import { toast } from 'sonner';
|
|
|
|
interface ControlCardPanelProps {
|
|
dashboardId: string;
|
|
card: Record<string, any>;
|
|
}
|
|
|
|
/**
|
|
* 선택된 카드의 부속 제어 패널
|
|
* - 카드가 좌측 상단(예: left:20, top:90, 320x240)으로 축소되면
|
|
* 이 패널이 카드 바로 오른쪽 (left:360, top:90 부근)에 floating
|
|
* - 패널 안: 카드명 / [읽기 | 편집] 토글 / 액션 / ✕ 닫기
|
|
*/
|
|
export function ControlCardPanel({ dashboardId, card }: ControlCardPanelProps) {
|
|
const {
|
|
mode,
|
|
setMode,
|
|
setSelectedCardId,
|
|
ruleNodes,
|
|
ruleConnections,
|
|
activeRuleId,
|
|
setActiveRuleId,
|
|
setRuleNodes,
|
|
setRuleConnections,
|
|
} = useControlMode();
|
|
const [ruleList, setRuleList] = useState<Record<string, any>[]>([]);
|
|
const [showRuleList, setShowRuleList] = useState(false);
|
|
|
|
const cardLabel =
|
|
card.title ?? card.TITLE ?? card.template_name ?? card.TEMPLATE_NAME ?? '제목 없음';
|
|
const cardTable =
|
|
card.primary_table ?? card.PRIMARY_TABLE ?? card.source_table ?? card.SOURCE_TABLE ?? null;
|
|
const cardType =
|
|
card.component_type ?? card.COMPONENT_TYPE ?? card.template_type ?? card.TEMPLATE_TYPE ?? null;
|
|
|
|
// 편집 모드에서만 규칙 목록 로드
|
|
useEffect(() => {
|
|
if (mode !== 'edit') return;
|
|
getBusinessRuleList(dashboardId)
|
|
.then((res) => setRuleList(res?.list ?? res?.data ?? []))
|
|
.catch(() => setRuleList([]));
|
|
}, [mode, dashboardId]);
|
|
|
|
const handleLoadRule = useCallback(
|
|
async (ruleId: string) => {
|
|
try {
|
|
const detail = await getBusinessRuleInfo(ruleId);
|
|
if (!detail) {
|
|
toast.error('규칙을 찾을 수 없습니다');
|
|
return;
|
|
}
|
|
setRuleNodes(detail.nodes ?? []);
|
|
setRuleConnections(detail.connections ?? []);
|
|
setActiveRuleId(ruleId);
|
|
setShowRuleList(false);
|
|
toast.success(`"${detail.name ?? ruleId}" 로드됨`);
|
|
} catch {
|
|
toast.error('규칙 로드 실패');
|
|
}
|
|
},
|
|
[setRuleNodes, setRuleConnections, setActiveRuleId],
|
|
);
|
|
|
|
const handleSave = async () => {
|
|
if (ruleNodes.length === 0) {
|
|
toast.warning('저장할 노드가 없습니다');
|
|
return;
|
|
}
|
|
try {
|
|
const data = {
|
|
name: `${cardLabel} 규칙 ${new Date().toLocaleString('ko-KR')}`,
|
|
nodes: ruleNodes,
|
|
connections: ruleConnections,
|
|
card_id: card.card_id ?? card.CARD_ID ?? card.id,
|
|
};
|
|
if (activeRuleId) {
|
|
await updateBusinessRule(activeRuleId, data);
|
|
toast.success('규칙 저장됨');
|
|
} else {
|
|
const result = await insertBusinessRule(dashboardId, data);
|
|
if (result?.rule_id) setActiveRuleId(result.rule_id);
|
|
toast.success('규칙 생성됨');
|
|
}
|
|
} catch {
|
|
toast.error('저장 실패');
|
|
}
|
|
};
|
|
|
|
const handleSourceDragStart = (e: React.DragEvent) => {
|
|
if (!cardTable) return;
|
|
e.dataTransfer.setData('text/plain', JSON.stringify({ kind: 'table', name: cardTable }));
|
|
e.dataTransfer.effectAllowed = 'copy';
|
|
};
|
|
|
|
return (
|
|
<div className="ctrl-card-panel">
|
|
{/* 헤더 — "제어" + ✕ 닫기 (카드명은 좌측 카드 자체에 이미 보이므로 중복 X) */}
|
|
<div className="ctrl-card-panel-head">
|
|
<div className="ctrl-card-panel-icon">⚡</div>
|
|
<div className="ctrl-card-panel-title-wrap">
|
|
<div className="ctrl-card-panel-title">제어</div>
|
|
{cardType && <div className="ctrl-card-panel-type">{cardType}</div>}
|
|
</div>
|
|
<button
|
|
className="ctrl-card-panel-close"
|
|
onClick={() => setSelectedCardId(null)}
|
|
title="제어 해제"
|
|
>
|
|
<X size={11} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 데이터 소스 칩 (드래그 가능, 편집 모드에서 룰 빌더로 추가) */}
|
|
{cardTable && (
|
|
<div className="ctrl-card-panel-source">
|
|
<span
|
|
className="ctrl-card-panel-source-chip"
|
|
draggable={mode === 'edit'}
|
|
onDragStart={mode === 'edit' ? handleSourceDragStart : undefined}
|
|
title={mode === 'edit' ? '드래그해서 룰 빌더에 추가' : '데이터 소스'}
|
|
>
|
|
<Database size={9} />
|
|
<span>{cardTable}</span>
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 모드 토글 — 카드 컨텍스트 안의 segmented */}
|
|
<div className="ctrl-card-panel-mode">
|
|
<button
|
|
className={`ctrl-card-panel-mode-btn${mode === 'view' ? ' on' : ''}`}
|
|
onClick={() => setMode('view')}
|
|
title="읽기 — 자동 트리 자람"
|
|
>
|
|
<Eye size={10} />
|
|
<span>읽기</span>
|
|
</button>
|
|
<button
|
|
className={`ctrl-card-panel-mode-btn${mode === 'edit' ? ' on' : ''}`}
|
|
onClick={() => setMode('edit')}
|
|
title="편집 — 팔레트에서 직접 작성"
|
|
>
|
|
<Wrench size={10} />
|
|
<span>편집</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* 편집 모드 액션 */}
|
|
{mode === 'edit' && (
|
|
<>
|
|
<div className="ctrl-card-panel-actions">
|
|
<div style={{ position: 'relative', flex: 1 }}>
|
|
<button
|
|
className="ctrl-card-panel-btn"
|
|
onClick={() => setShowRuleList(!showRuleList)}
|
|
disabled={ruleList.length === 0}
|
|
title="저장된 규칙 불러오기"
|
|
>
|
|
<FolderOpen size={10} />
|
|
<span>불러오기{ruleList.length > 0 ? ` (${ruleList.length})` : ''}</span>
|
|
</button>
|
|
{showRuleList && ruleList.length > 0 && (
|
|
<div className="ctrl-card-panel-dropdown">
|
|
{ruleList.map((rule) => {
|
|
const id = rule.rule_id ?? rule.RULE_ID;
|
|
const name = rule.name ?? rule.NAME ?? id;
|
|
const isActive = id === activeRuleId;
|
|
return (
|
|
<button
|
|
key={id}
|
|
className={`ctrl-card-panel-dropdown-item${isActive ? ' active' : ''}`}
|
|
onClick={() => handleLoadRule(id)}
|
|
>
|
|
{name}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
className="ctrl-card-panel-btn primary"
|
|
onClick={handleSave}
|
|
disabled={ruleNodes.length === 0}
|
|
title="현재 룰 저장"
|
|
>
|
|
<Save size={10} />
|
|
<span>저장</span>
|
|
</button>
|
|
</div>
|
|
{ruleNodes.length > 0 && (
|
|
<div className="ctrl-card-panel-status">
|
|
{ruleNodes.length}개 노드 · {ruleConnections.length}개 연결
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{mode === 'view' && (
|
|
<div className="ctrl-card-panel-hint">
|
|
우측에 데이터 흐름이 자동으로 펼쳐집니다
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|