Files
invyone/frontend/components/control/ControlCardPanel.tsx
T
DDD1542 2f398ae0b3 chore: 제어모드 IDE 작업 + v2/legacy 레지스트리 컴포넌트 폐기
- 제어모드 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 후속 노트
2026-05-19 21:31:03 +09:00

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>
);
}