diff --git a/resources/views/rd/planning-design/index.blade.php b/resources/views/rd/planning-design/index.blade.php
index 5a5e5a27..26584332 100644
--- a/resources/views/rd/planning-design/index.blade.php
+++ b/resources/views/rd/planning-design/index.blade.php
@@ -785,21 +785,8 @@
@contextmenu.prevent="showContextMenu($event)">
- {{-- SVG Connections --}}
-
+ {{-- SVG Connections (프로그래밍 렌더링 — Alpine template은 SVG 네임스페이스 미지원) --}}
+
{{-- Timeline Swimlanes --}}
@@ -1120,6 +1107,7 @@ function planningCanvas() {
{ key: 'done', label: '완료', color: '#10b981' },
],
_kanbanDragNodeId: null,
+ _connTick: 0,
// Drag State
dragging: false,
@@ -1403,6 +1391,84 @@ function planningCanvas() {
}
},
+ // ===== Connection Rendering (SVG namespace 직접 제어) =====
+ renderConnections() {
+ // _connTick 읽기 → x-effect 의존성 등록 (노드 이동 시 리렌더 트리거)
+ void this._connTick;
+ void this.connections.length;
+ void this.selectedConnection;
+ void this.drawingConnection;
+ void this.tempConnectionPath;
+
+ const svg = this.$refs.connSvg;
+ if (!svg) return;
+ // 현재 내용 클리어
+ svg.innerHTML = '';
+
+ // defs (arrowhead marker)
+ const NS = 'http://www.w3.org/2000/svg';
+ const defs = document.createElementNS(NS, 'defs');
+
+ const marker = document.createElementNS(NS, 'marker');
+ marker.setAttribute('id', 'arrowhead');
+ marker.setAttribute('markerWidth', '10');
+ marker.setAttribute('markerHeight', '7');
+ marker.setAttribute('refX', '9');
+ marker.setAttribute('refY', '3.5');
+ marker.setAttribute('orient', 'auto');
+ const poly = document.createElementNS(NS, 'polygon');
+ poly.setAttribute('points', '0 0, 10 3.5, 0 7');
+ poly.setAttribute('fill', '#94a3b8');
+ marker.appendChild(poly);
+
+ const markerSel = document.createElementNS(NS, 'marker');
+ markerSel.setAttribute('id', 'arrowhead-sel');
+ markerSel.setAttribute('markerWidth', '10');
+ markerSel.setAttribute('markerHeight', '7');
+ markerSel.setAttribute('refX', '9');
+ markerSel.setAttribute('refY', '3.5');
+ markerSel.setAttribute('orient', 'auto');
+ const polySel = document.createElementNS(NS, 'polygon');
+ polySel.setAttribute('points', '0 0, 10 3.5, 0 7');
+ polySel.setAttribute('fill', '#6366f1');
+ markerSel.appendChild(polySel);
+
+ defs.appendChild(marker);
+ defs.appendChild(markerSel);
+ svg.appendChild(defs);
+
+ // 기존 연결선
+ const self = this;
+ this.connections.forEach(conn => {
+ const d = this.getConnectionPath(conn);
+ if (!d) return;
+ const path = document.createElementNS(NS, 'path');
+ path.setAttribute('d', d);
+ const isSel = this.selectedConnection?.id === conn.id;
+ path.setAttribute('stroke', isSel ? '#6366f1' : '#94a3b8');
+ path.setAttribute('stroke-width', isSel ? '3' : '2');
+ path.setAttribute('fill', 'none');
+ path.setAttribute('marker-end', isSel ? 'url(#arrowhead-sel)' : 'url(#arrowhead)');
+ path.style.pointerEvents = 'stroke';
+ path.style.cursor = 'pointer';
+ path.addEventListener('click', () => { self.selectConnection(conn); });
+ svg.appendChild(path);
+ });
+
+ // 드래그 중인 임시 연결선
+ if (this.drawingConnection && this.tempConnectionPath) {
+ const temp = document.createElementNS(NS, 'path');
+ temp.setAttribute('d', this.tempConnectionPath);
+ temp.setAttribute('stroke', '#6366f1');
+ temp.setAttribute('stroke-width', '2');
+ temp.setAttribute('stroke-dasharray', '6 3');
+ temp.setAttribute('fill', 'none');
+ temp.setAttribute('marker-end', 'url(#arrowhead-sel)');
+ temp.style.pointerEvents = 'none';
+ svg.appendChild(temp);
+ }
+ },
+
// ===== Connection Operations =====
startConnection(e, node, port) {
if (this.tool !== 'connect' && this.tool !== 'select') return;
@@ -1465,6 +1531,7 @@ function planningCanvas() {
const rect = wrap.getBoundingClientRect();
this.dragNode.x = (e.clientX - rect.left - this.panX * this.zoom) / this.zoom - this.dragOffsetX;
this.dragNode.y = (e.clientY - rect.top - this.panY * this.zoom) / this.zoom - this.dragOffsetY;
+ this._connTick++;
}
if (this.drawingConnection && this.connSource) {
const wrap = document.getElementById('canvasWrap');