릴리스: v0.1.13 관리자 탭과 가변 티어 행 추가

This commit is contained in:
2026-03-19 16:58:17 +09:00
parent f6a031cfe4
commit b97d7eacda
10 changed files with 484 additions and 215 deletions

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, nextTick, onMounted, ref } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
@@ -38,12 +38,18 @@ const groupListEl = ref(null)
const poolEl = ref(null)
const groupDropEls = ref({})
const fileEl = ref(null)
const groupSortable = ref(null)
const poolSortable = ref(null)
const dropSortables = ref([])
const isNewTierList = computed(() => tierListId.value === 'new')
const canEdit = computed(() => !!auth.user && (!ownerId.value || ownerId.value === auth.user.id))
function setGroupDropEl(groupId, el) {
if (!el) return
if (!el) {
delete groupDropEls.value[groupId]
return
}
groupDropEls.value[groupId] = el
}
@@ -75,7 +81,9 @@ function resolveItemSrc(item) {
async function initSortables() {
if (!poolEl.value || !groupListEl.value) return
Sortable.create(groupListEl.value, {
destroySortables()
groupSortable.value = Sortable.create(groupListEl.value, {
animation: 160,
handle: '[data-group-handle]',
ghostClass: 'ghost',
@@ -88,7 +96,7 @@ async function initSortables() {
},
})
Sortable.create(poolEl.value, {
poolSortable.value = Sortable.create(poolEl.value, {
group: 'tier-items',
animation: 160,
draggable: '[data-item-id]',
@@ -98,7 +106,7 @@ async function initSortables() {
onAdd: () => normalizeSort(poolEl.value),
})
Object.entries(groupDropEls.value).forEach(([gid, el]) => {
dropSortables.value = Object.entries(groupDropEls.value).map(([gid, el]) =>
Sortable.create(el, {
group: 'tier-items',
animation: 160,
@@ -108,7 +116,56 @@ async function initSortables() {
onSort: () => normalizeSort(el),
onAdd: () => normalizeSort(el),
})
})
)
}
function destroySortables() {
if (groupSortable.value) {
groupSortable.value.destroy()
groupSortable.value = null
}
if (poolSortable.value) {
poolSortable.value.destroy()
poolSortable.value = null
}
dropSortables.value.forEach((instance) => instance.destroy())
dropSortables.value = []
}
async function syncSortables() {
await nextTick()
if (canEdit.value) {
await initSortables()
}
}
function createGroupName() {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const index = groups.value.length
if (index < alphabet.length) return alphabet[index]
return `Tier ${index + 1}`
}
async function addGroup() {
groups.value = [
...groups.value,
{
id: `g-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
name: createGroupName(),
itemIds: [],
},
]
await syncSortables()
}
async function removeGroup(groupId) {
if (groups.value.length <= 1) return
const target = groups.value.find((group) => group.id === groupId)
if (!target) return
pool.value = [...target.itemIds, ...pool.value]
groups.value = groups.value.filter((group) => group.id !== groupId)
delete groupDropEls.value[groupId]
await syncSortables()
}
function addCustomImage(file) {
@@ -278,6 +335,10 @@ onMounted(() => {
}
})()
})
onUnmounted(() => {
destroySortables()
})
</script>
<template>
@@ -314,11 +375,15 @@ onMounted(() => {
<section class="layout">
<div ref="boardEl" class="board">
<div v-if="canEdit" class="boardTools">
<button class="btn btn--ghost" @click="addGroup">티어 추가</button>
</div>
<div ref="groupListEl" class="rows">
<div v-for="g in groups" :key="g.id" class="row">
<div class="row__label">
<span class="grab" title="드래그로 순서 변경" data-group-handle></span>
<input v-model="g.name" class="groupName" :readonly="!canEdit" />
<button v-if="canEdit" class="rowRemoveBtn" :disabled="groups.length <= 1" @click="removeGroup(g.id)">삭제</button>
</div>
<div
class="row__drop"
@@ -471,6 +536,11 @@ onMounted(() => {
border-radius: 16px;
padding: 12px;
}
.boardTools {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.rows {
display: grid;
gap: 10px;
@@ -517,6 +587,20 @@ onMounted(() => {
outline: none;
min-width: 0;
}
.rowRemoveBtn {
padding: 6px 10px;
border-radius: 10px;
border: 1px solid rgba(239, 68, 68, 0.28);
background: rgba(239, 68, 68, 0.12);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
font-weight: 800;
flex: 0 0 auto;
}
.rowRemoveBtn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.row__drop {
border-radius: 14px;
background: rgba(0, 0, 0, 0.18);