diff --git a/resources/views/rd/planning-design/index.blade.php b/resources/views/rd/planning-design/index.blade.php index 163713f8..5a5e5a27 100644 --- a/resources/views/rd/planning-design/index.blade.php +++ b/resources/views/rd/planning-design/index.blade.php @@ -320,6 +320,176 @@ .pc-cm-item.danger:hover { background: #fef2f2; } .pc-cm-sep { height: 1px; background: #e5e7eb; margin: 4px 0; } +/* ===== Kanban Board ===== */ +.pc-kanban { + display: flex; gap: 12px; flex: 1; padding: 16px; overflow-x: auto; background: #f1f5f9; +} +.pc-kanban-col { + min-width: 260px; width: 260px; flex-shrink: 0; + background: #e8ecf1; border-radius: 10px; display: flex; flex-direction: column; max-height: 100%; +} +.pc-kanban-col-header { + display: flex; align-items: center; gap: 8px; padding: 10px 12px; flex-shrink: 0; +} +.pc-kanban-col-dot { + width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; +} +.pc-kanban-col-title { + font-size: 12px; font-weight: 700; color: #374151; flex: 1; +} +.pc-kanban-col-count { + font-size: 10px; font-weight: 600; color: #94a3b8; background: #fff; + padding: 1px 7px; border-radius: 9999px; +} +.pc-kanban-col-body { + flex: 1; overflow-y: auto; padding: 4px 8px 8px; display: flex; flex-direction: column; gap: 6px; + min-height: 60px; +} +.pc-kanban-col-body.drag-over { background: rgba(99,102,241,0.08); border-radius: 0 0 10px 10px; } +.pc-kanban-card { + background: #fff; border-radius: 8px; padding: 10px 12px; cursor: grab; + border: 1px solid #e2e8f0; transition: box-shadow 0.15s, border-color 0.15s; + border-left: 3px solid var(--card-color, #6366f1); +} +.pc-kanban-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); } +.pc-kanban-card.dragging { opacity: 0.4; } +.pc-kanban-card-title { + font-size: 12px; font-weight: 600; color: #1e293b; margin-bottom: 4px; + display: flex; align-items: center; gap: 6px; +} +.pc-kanban-card-title .emoji { font-size: 14px; } +.pc-kanban-card-desc { + font-size: 11px; color: #64748b; line-height: 1.4; + overflow: hidden; max-height: 36px; text-overflow: ellipsis; + margin-bottom: 6px; +} +.pc-kanban-card-meta { + display: flex; align-items: center; gap: 6px; flex-wrap: wrap; +} +.pc-kanban-card-tag { + font-size: 9px; padding: 1px 6px; border-radius: 9999px; + background: #f1f5f9; color: #64748b; font-weight: 500; +} +.pc-kanban-card-priority { + font-size: 9px; padding: 1px 6px; border-radius: 9999px; font-weight: 600; + margin-left: auto; +} +.pc-kanban-card-priority.high { background: #fef2f2; color: #ef4444; } +.pc-kanban-card-priority.critical { background: #fef2f2; color: #dc2626; } +.pc-kanban-card-priority.medium { background: #fffbeb; color: #d97706; } +.pc-kanban-card-priority.low { background: #f0fdf4; color: #22c55e; } + +/* ===== Node Detail Modal ===== */ +.pc-modal-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 200; + display: flex; align-items: center; justify-content: center; +} +.pc-modal { + background: #fff; border-radius: 12px; width: 640px; max-width: 92vw; + max-height: 85vh; overflow-y: auto; box-shadow: 0 24px 48px rgba(0,0,0,0.2); +} +.pc-modal-header { + display: flex; align-items: center; gap: 12px; padding: 20px 24px 12px; + border-bottom: 1px solid #f1f5f9; +} +.pc-modal-header .emoji { font-size: 24px; } +.pc-modal-header input { + flex: 1; font-size: 18px; font-weight: 700; border: none; outline: none; color: #1e293b; +} +.pc-modal-header input::placeholder { color: #cbd5e1; } +.pc-modal-close { + width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent; + color: #94a3b8; cursor: pointer; display: flex; align-items: center; justify-content: center; +} +.pc-modal-close:hover { background: #f1f5f9; color: #374151; } +.pc-modal-body { padding: 16px 24px 24px; } +.pc-modal-row { display: flex; gap: 12px; margin-bottom: 14px; } +.pc-modal-label { + width: 72px; flex-shrink: 0; font-size: 11px; font-weight: 600; color: #94a3b8; + padding-top: 7px; text-align: right; +} +.pc-modal-field { flex: 1; } +.pc-modal-field input, .pc-modal-field select, .pc-modal-field textarea { + width: 100%; padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 8px; + font-size: 13px; outline: none; transition: border-color 0.15s; +} +.pc-modal-field input:focus, .pc-modal-field select:focus, .pc-modal-field textarea:focus { + border-color: var(--pc-indigo); +} +.pc-modal-field textarea { resize: vertical; min-height: 80px; line-height: 1.6; } + +/* Checklist */ +.pc-checklist { list-style: none; padding: 0; margin: 0; } +.pc-checklist-item { + display: flex; align-items: center; gap: 8px; padding: 5px 0; + border-bottom: 1px solid #f8fafc; +} +.pc-checklist-item input[type="checkbox"] { accent-color: var(--pc-indigo); width: 15px; height: 15px; cursor: pointer; } +.pc-checklist-item .text { + flex: 1; font-size: 13px; color: #374151; border: none; outline: none; background: transparent; +} +.pc-checklist-item .text.done { text-decoration: line-through; color: #94a3b8; } +.pc-checklist-item .remove { + width: 20px; height: 20px; border: none; background: transparent; + color: #cbd5e1; cursor: pointer; font-size: 14px; border-radius: 4px; + display: flex; align-items: center; justify-content: center; +} +.pc-checklist-item .remove:hover { color: #ef4444; background: #fef2f2; } +.pc-checklist-add { + display: flex; align-items: center; gap: 6px; padding: 6px 0; + font-size: 12px; color: var(--pc-indigo); cursor: pointer; font-weight: 500; +} +.pc-checklist-add:hover { color: #4f46e5; } +.pc-checklist-progress { + height: 4px; background: #e2e8f0; border-radius: 2px; overflow: hidden; margin-bottom: 8px; +} +.pc-checklist-progress-bar { + height: 100%; background: var(--pc-emerald); border-radius: 2px; transition: width 0.3s; +} + +/* ===== List/Table View ===== */ +.pc-list-view { + flex: 1; overflow: auto; background: #fff; +} +.pc-list-table { + width: 100%; border-collapse: collapse; font-size: 12px; +} +.pc-list-table th { + position: sticky; top: 0; background: #f8fafc; padding: 8px 12px; + text-align: left; font-weight: 700; color: #64748b; font-size: 10px; + text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid #e2e8f0; + cursor: pointer; user-select: none; white-space: nowrap; +} +.pc-list-table th:hover { color: #374151; } +.pc-list-table th .sort-icon { font-size: 10px; margin-left: 2px; } +.pc-list-table td { + padding: 8px 12px; border-bottom: 1px solid #f1f5f9; color: #374151; + vertical-align: middle; +} +.pc-list-table tr:hover td { background: #f8fafc; } +.pc-list-table tr.selected td { background: #eef2ff; } +.pc-list-status-badge { + display: inline-flex; align-items: center; gap: 4px; font-size: 11px; + padding: 2px 8px; border-radius: 9999px; font-weight: 500; +} + +/* ===== Filter Bar ===== */ +.pc-filter-bar { + display: flex; align-items: center; gap: 8px; padding: 6px 16px; + background: #fff; border-bottom: 1px solid #e2e8f0; flex-shrink: 0; +} +.pc-filter-bar select, .pc-filter-bar input { + padding: 4px 8px; border: 1px solid #e2e8f0; border-radius: 6px; + font-size: 11px; outline: none; background: #fff; color: #374151; +} +.pc-filter-bar select:focus, .pc-filter-bar input:focus { border-color: var(--pc-indigo); } +.pc-filter-label { font-size: 10px; font-weight: 600; color: #94a3b8; } +.pc-filter-clear { + font-size: 11px; color: var(--pc-indigo); cursor: pointer; font-weight: 500; + background: none; border: none; padding: 4px 8px; +} +.pc-filter-clear:hover { text-decoration: underline; } + /* Phase Swimlane (Timeline View) */ .pc-swimlane { position: absolute; top: 0; @@ -358,9 +528,11 @@
- + + +
@@ -512,6 +684,14 @@ +
+
담당자
+ +
+
+
마감일
+ +
태그 (콤마 구분)
긴급
-
+
+
@@ -559,8 +741,41 @@
- {{-- Canvas --}} + {{-- Main Content Area --}} +
+ + {{-- Filter Bar --}} +
+ 필터: + + + + + + +
+ + {{-- Canvas (free / timeline / flow) --}}
+ @mousedown.stop="onNodeMouseDown($event, node)" + @dblclick.stop="openNodeModal(node)">
@@ -653,8 +869,197 @@ {{-- Minimap --}}
+ + {{-- Kanban View --}} +
+ +
+ + {{-- List/Table View --}} +
+ + + + + + + + + + + + + + + + +
제목 유형 상태 우선순위 담당자 마감일 체크
+
노드가 없습니다
+
+ +
{{-- /Main Content Area --}}
+ {{-- Node Detail Modal --}} + + {{-- Context Menu --}}
복제
@@ -695,6 +1100,26 @@ function planningCanvas() { // Selection selectedNode: null, selectedConnection: null, + modalNode: null, + + // Filters + filterText: '', + filterStatus: '', + filterPriority: '', + filterType: '', + + // List sort + listSortKey: 'title', + listSortDir: 'asc', + + // Kanban + kanbanColumns: [ + { key: 'todo', label: '대기', color: '#94a3b8' }, + { key: 'progress', label: '진행중', color: '#3b82f6' }, + { key: 'review', label: '검토중', color: '#f59e0b' }, + { key: 'done', label: '완료', color: '#10b981' }, + ], + _kanbanDragNodeId: null, // Drag State dragging: false, @@ -767,6 +1192,47 @@ function planningCanvas() { ], }, + get allPaletteItems() { + return [ + ...this.paletteItems.planning, + ...this.paletteItems.analysis, + ...this.paletteItems.structure, + ...this.paletteItems.output, + ]; + }, + + get filteredNodes() { + return this.nodes.filter(n => { + if (this.filterStatus && n.status !== this.filterStatus) return false; + if (this.filterPriority && n.priority !== this.filterPriority) return false; + if (this.filterType && n.type !== this.filterType) return false; + if (this.filterText) { + const q = this.filterText.toLowerCase(); + if (!(n.title||'').toLowerCase().includes(q) && + !(n.description||'').toLowerCase().includes(q) && + !(n.assignee||'').toLowerCase().includes(q) && + !(n.tags||[]).some(t => t.toLowerCase().includes(q))) return false; + } + return true; + }); + }, + + get sortedFilteredNodes() { + const items = [...this.filteredNodes]; + const key = this.listSortKey; + const dir = this.listSortDir === 'asc' ? 1 : -1; + const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + const statusOrder = { todo: 0, progress: 1, review: 2, done: 3 }; + items.sort((a, b) => { + let va = a[key] || '', vb = b[key] || ''; + if (key === 'priority') { va = priorityOrder[va] ?? 9; vb = priorityOrder[vb] ?? 9; } + else if (key === 'status') { va = statusOrder[va] ?? 9; vb = statusOrder[vb] ?? 9; } + else { va = String(va).toLowerCase(); vb = String(vb).toLowerCase(); } + return va < vb ? -dir : va > vb ? dir : 0; + }); + return items; + }, + init() { this.loadSavedProjects(); const currentId = localStorage.getItem(CURRENT_KEY); @@ -811,7 +1277,6 @@ function planningCanvas() { localStorage.setItem(STORAGE_KEY, JSON.stringify(projects)); localStorage.setItem(CURRENT_KEY, this.currentProjectId); this.loadSavedProjects(); - if (typeof showToast === 'function') showToast('저장되었습니다.', 'success'); }, autoSave() { @@ -882,6 +1347,9 @@ function planningCanvas() { status: 'todo', priority: 'medium', tags: [], + assignee: '', + dueDate: '', + checklist: [], x: x, y: y, }; @@ -1091,7 +1559,7 @@ function planningCanvas() { switchView(mode) { this.viewMode = mode; if (mode === 'timeline') this.layoutTimeline(); - if (mode === 'flow') this.layoutFlow(); + else if (mode === 'flow') this.layoutFlow(); }, layoutTimeline() { @@ -1180,6 +1648,15 @@ function planningCanvas() { if ((e.ctrlKey || e.metaKey) && e.key === 'y') { e.preventDefault(); this.redoAction(); } if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); this.saveProject(); } if ((e.ctrlKey || e.metaKey) && e.key === 'd') { e.preventDefault(); this.duplicateNode(); } + if ((e.ctrlKey || e.metaKey) && e.key === 'f') { + if (this.viewMode === 'kanban' || this.viewMode === 'list') { + e.preventDefault(); + this.$nextTick(() => { + const input = document.querySelector('.pc-filter-bar input[type="text"]'); + if (input) input.focus(); + }); + } + } }, // ===== Context Menu ===== @@ -1198,6 +1675,87 @@ function planningCanvas() { document.getElementById('contextMenu').classList.remove('show'); }, + // ===== Kanban ===== + kanbanNodesFor(statusKey) { + return this.filteredNodes.filter(n => (n.status || 'todo') === statusKey); + }, + + onKanbanDragStart(e, node) { + this._kanbanDragNodeId = node.id; + e.target.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + }, + + onKanbanDrop(e, statusKey) { + if (!this._kanbanDragNodeId) return; + const node = this.nodes.find(n => n.id === this._kanbanDragNodeId); + if (node) { + node.status = statusKey; + if (this.selectedNode?.id === node.id) this.selectedNode = node; + if (this.modalNode?.id === node.id) this.modalNode = node; + this.pushHistory(); + this.autoSave(); + } + this._kanbanDragNodeId = null; + }, + + // ===== Node Detail Modal ===== + openNodeModal(node) { + this.modalNode = node; + this.selectedNode = node; + if (!this.modalNode.checklist) this.modalNode.checklist = []; + if (!this.modalNode.assignee) this.modalNode.assignee = ''; + if (!this.modalNode.dueDate) this.modalNode.dueDate = ''; + }, + + closeNodeModal() { + this.modalNode = null; + this.pushHistory(); + this.autoSave(); + }, + + onModalChange() { + this.autoSave(); + }, + + addChecklistItem() { + if (!this.modalNode.checklist) this.modalNode.checklist = []; + this.modalNode.checklist.push({ text: '', done: false }); + }, + + checklistProgress(node) { + if (!node.checklist || node.checklist.length === 0) return 0; + return Math.round(node.checklist.filter(c => c.done).length / node.checklist.length * 100); + }, + + // ===== List View ===== + sortList(key) { + if (this.listSortKey === key) { + this.listSortDir = this.listSortDir === 'asc' ? 'desc' : 'asc'; + } else { + this.listSortKey = key; + this.listSortDir = 'asc'; + } + }, + + sortIcon(key) { + if (this.listSortKey !== key) return ''; + return this.listSortDir === 'asc' ? '▲' : '▼'; + }, + + // ===== Multi-Select ===== + multiSelected: [], + + toggleMultiSelect(node, e) { + if (e.shiftKey) { + const idx = this.multiSelected.findIndex(n => n.id === node.id); + if (idx >= 0) this.multiSelected.splice(idx, 1); + else this.multiSelected.push(node); + } else { + this.multiSelected = [node]; + } + }, + // ===== Helpers ===== toggleSidebar() { this.sidebarOpen = !this.sidebarOpen; }, @@ -1208,6 +1766,11 @@ function planningCanvas() { return map[status] || '#94a3b8'; }, + statusLabel(status) { + const map = { todo: '대기', progress: '진행중', review: '검토중', done: '완료' }; + return map[status] || status; + }, + priorityLabel(p) { const map = { low: '낮음', medium: '보통', high: '높음', critical: '긴급' }; return map[p] || ''; @@ -1218,6 +1781,20 @@ function planningCanvas() { const d = new Date(iso); return (d.getMonth() + 1) + '/' + d.getDate() + ' ' + d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0'); }, + + formatShortDate(dateStr) { + if (!dateStr) return ''; + const d = new Date(dateStr); + return (d.getMonth() + 1) + '/' + d.getDate(); + }, + + isOverdue(dateStr) { + if (!dateStr) return false; + const d = new Date(dateStr); + const today = new Date(); + today.setHours(0,0,0,0); + return d < today; + }, }; }