fix: [rd] 기획디자인 연결선 렌더링 수정 (SVG namespace 문제 해결)

- Alpine.js <template x-for>가 SVG 내부에서 path 요소 생성 불가 문제
- SVG 요소를 createElementNS로 직접 생성하는 renderConnections() 도입
- x-effect + _connTick 카운터로 노드 이동/연결 변경 시 자동 리렌더
This commit is contained in:
김보곤
2026-03-07 22:22:35 +09:00
parent 98f7b94516
commit b0a70481e8

View File

@@ -785,21 +785,8 @@
@contextmenu.prevent="showContextMenu($event)">
<div class="pc-canvas" id="canvas" :style="'transform: scale(' + zoom + ') translate(' + panX + 'px,' + panY + 'px)'">
{{-- SVG Connections --}}
<svg class="pc-connections" id="connectionsSvg">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8"/>
</marker>
</defs>
<template x-for="conn in connections" :key="conn.id">
<path class="pc-conn" :d="getConnectionPath(conn)" marker-end="url(#arrowhead)"
style="pointer-events: stroke; cursor: pointer;"
@click="selectConnection(conn)"
:style="selectedConnection?.id === conn.id ? 'stroke: #6366f1; stroke-width: 3;' : ''"/>
</template>
<path x-show="drawingConnection" class="pc-conn-active" :d="tempConnectionPath" marker-end="url(#arrowhead)"/>
</svg>
{{-- SVG Connections (프로그래밍 렌더링 Alpine template은 SVG 네임스페이스 미지원) --}}
<svg class="pc-connections" id="connectionsSvg" x-ref="connSvg" x-effect="renderConnections()"></svg>
{{-- Timeline Swimlanes --}}
<template x-if="viewMode === 'timeline'">
@@ -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');