메뉴 관리: parent_id 오류 안내·표시/폴더 제거·태그형 드래그 UX (v0.0.95)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-12 11:53:39 +09:00
parent bcff96aa4c
commit 8b8a80034d
9 changed files with 293 additions and 135 deletions

View File

@@ -38,6 +38,19 @@ const emit = defineEmits([
'remove'
])
/**
* 입력·버튼 위에서는 행 드래그를 시작하지 않는다.
* @param {DragEvent} event - 이벤트
* @returns {boolean} true면 드래그를 막는다
*/
const shouldBlockRowDrag = (event) => {
const el = event.target
if (!el || typeof el.closest !== 'function') {
return false
}
return Boolean(el.closest('input, button, textarea, select, a'))
}
/**
* 드래그 시작
* @param {DragEvent} event - 이벤트
@@ -45,6 +58,10 @@ const emit = defineEmits([
* @returns {void}
*/
const onDragStart = (event, itemId) => {
if (shouldBlockRowDrag(event)) {
event.preventDefault()
return
}
if (!event.dataTransfer) {
return
}
@@ -81,80 +98,117 @@ const onDrop = (event, itemId) => {
const onDragEnd = () => {
emit('drag-end')
}
/**
* 행 하이라이트 클래스(태그 관리 메인 태그 테이블과 동일 톤)
* @param {string} itemId - 항목 id
* @returns {string}
*/
const rowStateClass = (itemId) => {
const id = String(itemId)
if (props.dragOverId === id) {
return 'bg-[#f9f9f7]'
}
if (props.draggingId === id) {
return 'opacity-50'
}
return ''
}
</script>
<template>
<ul class="admin-nav-primary-branch space-y-2" :class="depth ? 'mt-1 border-l border-line pl-3' : ''">
<li
v-for="wrap in wraps"
:key="wrap.item.id"
class="admin-nav-primary-branch__row rounded border border-transparent bg-white px-2 py-2 transition-colors"
:class="dragOverId === wrap.item.id ? 'border-ink/20 bg-[#f5f5f2]' : draggingId === wrap.item.id ? 'opacity-60' : ''"
>
<div class="admin-nav-primary-branch__controls flex flex-wrap items-center gap-2" :style="{ paddingLeft: `${depth * 4}px` }">
<span
class="admin-nav-primary-branch__handle cursor-grab select-none text-muted active:cursor-grabbing"
draggable="true"
title="드래그하여 순서 변경"
@dragstart="onDragStart($event, wrap.item.id)"
@dragover="onDragOver($event, wrap.item.id)"
@drop="onDrop($event, wrap.item.id)"
@dragend="onDragEnd"
>
::
</span>
<label class="admin-nav-primary-branch__visible flex items-center gap-1 text-xs text-muted">
<input v-model="wrap.item.isVisible" class="h-4 w-4" type="checkbox">
표시
</label>
<label class="admin-nav-primary-branch__folder flex items-center gap-1 text-xs text-muted">
<input v-model="wrap.item.isFolder" class="h-4 w-4" type="checkbox">
폴더
</label>
<input
v-model="wrap.item.label"
class="admin-nav-primary-branch__label min-w-[120px] flex-1 rounded border border-line px-2 py-1.5 text-sm"
type="text"
placeholder="라벨"
required
>
<input
v-model="wrap.item.url"
class="admin-nav-primary-branch__url min-w-[160px] flex-1 rounded border border-line px-2 py-1.5 text-sm font-mono"
type="text"
placeholder="URL (# 또는 /경로)"
required
>
<button
class="admin-nav-primary-branch__add-child rounded border border-line px-2 py-1 text-xs font-semibold"
type="button"
@click="emit('add-child', wrap.item.id)"
>
하위
</button>
<button
class="admin-nav-primary-branch__remove rounded border border-red-200 px-2 py-1 text-xs font-semibold text-red-700"
type="button"
@click="emit('remove', wrap.item.id)"
>
삭제
</button>
</div>
<AdminNavPrimaryBranch
v-if="wrap.children.length"
class="admin-nav-primary-branch__children mt-2"
:wraps="wrap.children"
:depth="depth + 1"
:parent-key="String(wrap.item.id)"
:dragging-id="draggingId"
:drag-over-id="dragOverId"
@drag-start="emit('drag-start', $event)"
@drag-over="emit('drag-over', $event)"
@drag-end="emit('drag-end')"
@drop="emit('drop', $event)"
@add-child="emit('add-child', $event)"
@remove="emit('remove', $event)"
/>
</li>
</ul>
<div class="admin-nav-primary-branch" :class="depth ? 'mt-2 border-l border-line pl-3' : ''">
<div class="admin-nav-primary-branch__shell overflow-hidden rounded border border-line">
<table class="admin-nav-primary-branch__table w-full border-collapse text-left text-sm">
<thead v-if="depth === 0" class="admin-nav-primary-branch__head bg-[#f5f5f2] text-xs uppercase text-muted">
<tr>
<th class="admin-nav-primary-branch__cell px-4 py-3">
#
</th>
<th class="admin-nav-primary-branch__cell px-4 py-3">
라벨
</th>
<th class="admin-nav-primary-branch__cell px-4 py-3">
URL
</th>
<th class="admin-nav-primary-branch__cell px-4 py-3">
관리
</th>
</tr>
</thead>
<tbody class="admin-nav-primary-branch__body divide-y divide-line bg-white">
<template v-for="(wrap, index) in wraps" :key="wrap.item.id">
<tr
class="admin-nav-primary-branch__row cursor-move"
:class="rowStateClass(wrap.item.id)"
draggable="true"
@dragstart="onDragStart($event, wrap.item.id)"
@dragover="onDragOver($event, wrap.item.id)"
@drop="onDrop($event, wrap.item.id)"
@dragend="onDragEnd"
>
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle text-muted">
{{ index + 1 }}
</td>
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle">
<input
v-model="wrap.item.label"
class="admin-nav-primary-branch__label w-full min-w-[8rem] rounded border border-line px-3 py-2 text-sm outline-none focus:border-[#8e9cac]"
type="text"
placeholder="라벨"
required
>
</td>
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle">
<input
v-model="wrap.item.url"
class="admin-nav-primary-branch__url w-full min-w-[10rem] rounded border border-line px-3 py-2 font-mono text-sm outline-none focus:border-[#8e9cac]"
type="text"
placeholder="URL (# 또는 /경로)"
required
>
</td>
<td class="admin-nav-primary-branch__cell px-4 py-3 align-middle">
<div class="admin-nav-primary-branch__actions flex flex-wrap gap-2">
<button
class="admin-nav-primary-branch__add-child rounded border border-line px-3 py-1.5 text-xs font-semibold"
type="button"
@click="emit('add-child', wrap.item.id)"
>
하위
</button>
<button
class="admin-nav-primary-branch__remove rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700"
type="button"
@click="emit('remove', wrap.item.id)"
>
삭제
</button>
</div>
</td>
</tr>
<tr v-if="wrap.children.length" class="admin-nav-primary-branch__nest bg-white">
<td class="p-0" colspan="4">
<div class="admin-nav-primary-branch__nest-inner border-t border-line bg-[#fafaf8] px-2 py-3">
<AdminNavPrimaryBranch
:wraps="wrap.children"
:depth="depth + 1"
:parent-key="String(wrap.item.id)"
:dragging-id="draggingId"
:drag-over-id="dragOverId"
@drag-start="emit('drag-start', $event)"
@drag-over="emit('drag-over', $event)"
@drag-end="emit('drag-end')"
@drop="emit('drop', $event)"
@add-child="emit('add-child', $event)"
@remove="emit('remove', $event)"
/>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>