추가: 티어표 아이템 우클릭 복제

This commit is contained in:
2026-04-03 15:26:34 +09:00
parent b16aa046e7
commit 4285883e28
6 changed files with 145 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
<script setup>
import { Teleport, computed, inject, nextTick, onUnmounted, ref, watch } from 'vue'
import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
@@ -79,6 +79,12 @@ const poolSearchQuery = ref('')
const selectedItemId = ref('')
const recentDragFinishedAt = ref(0)
const savedEditorSnapshot = ref('')
const itemContextMenu = ref({
open: false,
x: 0,
y: 0,
itemId: '',
})
let editorLoadToken = 0
const boardEl = ref(null)
@@ -329,6 +335,31 @@ function shouldIgnoreItemClick() {
return Date.now() - recentDragFinishedAt.value < 180
}
function closeItemContextMenu() {
if (!itemContextMenu.value.open) return
itemContextMenu.value = {
open: false,
x: 0,
y: 0,
itemId: '',
}
}
function openItemContextMenu(itemId, event) {
if (!canEdit.value || !itemId || !itemsById.value[itemId] || shouldIgnoreItemClick()) return
selectedItemId.value = itemId
const viewportWidth = typeof window === 'undefined' ? 0 : window.innerWidth
const viewportHeight = typeof window === 'undefined' ? 0 : window.innerHeight
const menuX = Number(event?.clientX || 0)
const menuY = Number(event?.clientY || 0)
itemContextMenu.value = {
open: true,
x: viewportWidth ? Math.max(12, Math.min(menuX, viewportWidth - 180)) : menuX,
y: viewportHeight ? Math.max(12, Math.min(menuY, viewportHeight - 72)) : menuY,
itemId,
}
}
function getItemLocation(itemId) {
if (!itemId) return { type: null, groupId: '', columnIndex: -1, index: -1 }
@@ -405,6 +436,43 @@ function moveSelectedItemToPool() {
selectedItemId.value = ''
}
function duplicateItemToPool() {
if (!canEdit.value || !itemContextMenu.value.itemId) return
const sourceItem = itemsById.value[itemContextMenu.value.itemId]
if (!sourceItem) {
closeItemContextMenu()
return
}
const clonedId = createClonedItemId()
itemsById.value = {
...itemsById.value,
[clonedId]: {
...sourceItem,
id: clonedId,
},
}
pool.value = [clonedId, ...pool.value]
selectedItemId.value = clonedId
closeItemContextMenu()
toast.success('복제본을 미사용 아이템 목록에 추가했어요.')
}
function handleGlobalContextMenu(event) {
if (!itemContextMenu.value.open) return
const target = event?.target
if (target?.closest?.('[data-item-context-menu]') || target?.closest?.('[data-item-id]')) return
closeItemContextMenu()
}
function handleGlobalPointerDown(event) {
if (!itemContextMenu.value.open) return
const target = event?.target
if (target?.closest?.('[data-item-context-menu]')) return
closeItemContextMenu()
}
function setGroupDropEl(groupId, columnIndex, el) {
const key = `${groupId}::${columnIndex}`
if (!el) {
@@ -544,6 +612,10 @@ function createCustomItemLabel(fileName = '') {
return (normalized || 'custom').slice(0, 60)
}
function createClonedItemId() {
return `dup-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
}
async function addGroup() {
groups.value = [
...groups.value,
@@ -1135,6 +1207,7 @@ function resetEditorStateForRoute() {
selectedItemId.value = ''
recentDragFinishedAt.value = 0
savedEditorSnapshot.value = ''
closeItemContextMenu()
resetTemplateRequestDrafts()
}
@@ -1240,7 +1313,21 @@ watch(
{ immediate: true }
)
onMounted(() => {
if (typeof window === 'undefined') return
window.addEventListener('pointerdown', handleGlobalPointerDown)
window.addEventListener('contextmenu', handleGlobalContextMenu)
window.addEventListener('blur', closeItemContextMenu)
window.addEventListener('scroll', closeItemContextMenu, true)
})
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('pointerdown', handleGlobalPointerDown)
window.removeEventListener('contextmenu', handleGlobalContextMenu)
window.removeEventListener('blur', closeItemContextMenu)
window.removeEventListener('scroll', closeItemContextMenu, true)
}
if (thumbnailPreviewUrl.value) URL.revokeObjectURL(thumbnailPreviewUrl.value)
destroySortables()
})
@@ -1556,6 +1643,7 @@ onUnmounted(() => {
:class="{ 'cell--selected': selectedItemId === id }"
:data-item-id="id"
@click.stop="selectItemByClick(id)"
@contextmenu.prevent.stop="openItemContextMenu(id, $event)"
>
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
<div v-if="showCharacterNames" class="itemNameOverlay">{{ itemsById[id]?.label || id }}</div>
@@ -1637,6 +1725,7 @@ onUnmounted(() => {
}"
:data-item-id="id"
@click.stop="selectItemByClick(id)"
@contextmenu.prevent.stop="openItemContextMenu(id, $event)"
>
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
<div class="poolItem__label">{{ itemsById[id]?.label || id }}</div>
@@ -1646,6 +1735,19 @@ onUnmounted(() => {
</div>
</div>
<div
v-if="itemContextMenu.open && canEdit"
class="itemContextMenu"
:style="{ left: `${itemContextMenu.x}px`, top: `${itemContextMenu.y}px` }"
data-item-context-menu
@click.stop
@contextmenu.prevent.stop
>
<button class="itemContextMenu__action" type="button" @click="duplicateItemToPool">
아이템 복제
</button>
</div>
</div>
</section>
@@ -2940,6 +3042,35 @@ onUnmounted(() => {
.poolItem--hidden {
display: none;
}
.itemContextMenu {
position: fixed;
z-index: 50;
min-width: 150px;
padding: 8px;
border-radius: 16px;
border: 1px solid var(--theme-border);
background: var(--theme-card-bg);
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.28);
}
.itemContextMenu__action {
width: 100%;
border: 0;
border-radius: 12px;
padding: 11px 12px;
background: transparent;
color: var(--theme-text);
font-size: 13px;
font-weight: 800;
text-align: left;
cursor: pointer;
}
.itemContextMenu__action:hover {
background: var(--theme-pill-bg);
}
.hidden {
display: none;
}