릴리스: v1.4.42 홈 템플릿 정렬 및 클릭 배치 지원

This commit is contained in:
2026-04-03 10:44:37 +09:00
parent bf726b6161
commit 04e9a0420a
6 changed files with 159 additions and 6 deletions

View File

@@ -30,6 +30,9 @@ const templates = computed(() => {
const rankA = a.displayRank == null ? Number.MAX_SAFE_INTEGER : a.displayRank
const rankB = b.displayRank == null ? Number.MAX_SAFE_INTEGER : b.displayRank
if (rankA !== rankB) return rankA - rankB
if (Number(a.createdAt || 0) !== Number(b.createdAt || 0)) {
return Number(b.createdAt || 0) - Number(a.createdAt || 0)
}
return (a.name || '').localeCompare(b.name || '', 'ko')
})
})

View File

@@ -74,6 +74,8 @@ const isFavorited = ref(false)
const isRequestingTemplate = ref(false)
const isDeleting = ref(false)
const poolSearchQuery = ref('')
const selectedItemId = ref('')
const recentDragFinishedAt = ref(0)
const boardEl = ref(null)
const exportBoardEl = ref(null)
@@ -122,7 +124,7 @@ const canSwitchToEditMode = computed(() => isOwnTierList.value && hasSavedTierLi
const copiedFromLabel = computed(() => {
if (!sourceTierListId.value) return ''
const parts = []
if (sourceSnapshotTitle.value) parts.push(`원본 ${sourceSnapshotTitle.value}`)
if (sourceSnapshotTitle.value) parts.push(sourceSnapshotTitle.value)
if (sourceSnapshotAuthor.value) parts.push(sourceSnapshotAuthor.value)
return parts.join(' · ') || '복사해 온 티어표'
})
@@ -287,6 +289,87 @@ function removeItemFromGroup(groupId, columnIndex, itemId) {
targetGroup.cells = nextCells
syncGroupItemIds(targetGroup)
pool.value = [itemId, ...pool.value.filter((id) => id !== itemId)]
if (selectedItemId.value === itemId) selectedItemId.value = ''
}
function shouldIgnoreItemClick() {
return Date.now() - recentDragFinishedAt.value < 180
}
function getItemLocation(itemId) {
if (!itemId) return { type: null, groupId: '', columnIndex: -1, index: -1 }
const poolIndex = pool.value.findIndex((id) => id === itemId)
if (poolIndex >= 0) {
return { type: 'pool', groupId: '', columnIndex: -1, index: poolIndex }
}
for (const group of groups.value) {
for (let columnIndex = 0; columnIndex < columns.value.length; columnIndex += 1) {
const index = getGroupCellIds(group, columnIndex).findIndex((id) => id === itemId)
if (index >= 0) {
return { type: 'group', groupId: group.id, columnIndex, index }
}
}
}
return { type: null, groupId: '', columnIndex: -1, index: -1 }
}
function detachItemById(itemId) {
if (!itemId) return
pool.value = pool.value.filter((id) => id !== itemId)
groups.value.forEach((group) => {
group.cells = (group.cells || []).map((cell) => (cell || []).filter((id) => id !== itemId))
syncGroupItemIds(group)
})
}
function selectItemByClick(itemId) {
if (!canEdit.value || !itemId || shouldIgnoreItemClick()) return
selectedItemId.value = selectedItemId.value === itemId ? '' : itemId
}
function placeSelectedItemInGroup(groupId, columnIndex) {
if (!canEdit.value || !selectedItemId.value || !groupId || !Number.isInteger(columnIndex)) return
if (shouldIgnoreItemClick()) return
const targetGroup = groups.value.find((group) => group.id === groupId)
if (!targetGroup) return
const selectedId = selectedItemId.value
const currentLocation = getItemLocation(selectedId)
const sameTarget =
currentLocation.type === 'group' &&
currentLocation.groupId === groupId &&
currentLocation.columnIndex === columnIndex
if (sameTarget) {
selectedItemId.value = ''
return
}
detachItemById(selectedId)
const nextCells = [...targetGroup.cells]
nextCells[columnIndex] = [...getGroupCellIds(targetGroup, columnIndex), selectedId]
targetGroup.cells = nextCells
syncGroupItemIds(targetGroup)
selectedItemId.value = ''
}
function moveSelectedItemToPool() {
if (!canEdit.value || !selectedItemId.value || shouldIgnoreItemClick()) return
const selectedId = selectedItemId.value
const currentLocation = getItemLocation(selectedId)
if (currentLocation.type === 'pool') {
selectedItemId.value = ''
return
}
detachItemById(selectedId)
pool.value = [selectedId, ...pool.value]
selectedItemId.value = ''
}
function setGroupDropEl(groupId, columnIndex, el) {
@@ -360,6 +443,12 @@ async function initSortables() {
draggable: '[data-item-id]',
ghostClass: 'ghost',
chosenClass: 'chosen',
onStart: () => {
selectedItemId.value = ''
},
onEnd: () => {
recentDragFinishedAt.value = Date.now()
},
onSort: () => normalizeSort(poolEl.value),
onAdd: () => normalizeSort(poolEl.value),
})
@@ -371,6 +460,12 @@ async function initSortables() {
draggable: '[data-item-id]',
ghostClass: 'ghost',
chosenClass: 'chosen',
onStart: () => {
selectedItemId.value = ''
},
onEnd: () => {
recentDragFinishedAt.value = Date.now()
},
onSort: () => normalizeSort(el),
onAdd: () => normalizeSort(el),
})
@@ -1213,7 +1308,7 @@ onUnmounted(() => {
</template>
</div>
<div v-if="sourceTierListId" class="editorMain__sourceNote">
<span>복사</span>
<span></span>
<button class="editorMain__sourceLink" type="button" @click="router.push(editorPath(templateId, sourceTierListId))">{{ copiedFromLabel }}</button>
</div>
</div>
@@ -1294,10 +1389,19 @@ onUnmounted(() => {
:data-group-id="g.id"
:data-column-index="columnIndex"
:ref="(el) => setGroupDropEl(g.id, columnIndex, el)"
:class="{ 'row__drop--clickTarget': canEdit && !!selectedItemId }"
@click="placeSelectedItemInGroup(g.id, columnIndex)"
>
<div v-if="columns.length > 1" class="row__columnBadge">{{ column.name || ' ' + (columnIndex + 1) }}</div>
<div v-if="!isExporting" class="row__empty" v-show="getGroupCellIds(g, columnIndex).length === 0">여기로 드래그해서 배치</div>
<div v-for="id in getGroupCellIds(g, columnIndex)" :key="id" class="cell" :data-item-id="id">
<div
v-for="id in getGroupCellIds(g, columnIndex)"
:key="id"
class="cell"
:class="{ 'cell--selected': selectedItemId === id }"
:data-item-id="id"
@click.stop="selectItemByClick(id)"
>
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
<div v-if="showCharacterNames" class="itemNameOverlay">{{ itemsById[id]?.label || id }}</div>
<button
@@ -1351,7 +1455,7 @@ onUnmounted(() => {
<div class="sidebar__count">{{ visiblePoolCount }} / {{ pool.length }}</div>
</div>
<div class="sidebar__hint">
{{ canEdit ? '등록된 아이템 리스트입니다. 드래그해서 표에 넣을 수 있습니다.' : '공개 티어표는 보기 전용입니다.' }}
{{ canEdit ? '아이템을 드래그하거나, 클릭으로 선택한 뒤 원하는 셀/풀을 클릭해서 옮길 수 있어요.' : '공개 티어표는 보기 전용입니다.' }}
</div>
<input
v-model="poolSearchQuery"
@@ -1360,13 +1464,24 @@ onUnmounted(() => {
maxlength="60"
placeholder="아이템 이름 검색"
/>
<div ref="poolEl" class="pool" data-list-type="pool">
<div
ref="poolEl"
class="pool"
:class="{ 'pool--clickTarget': canEdit && !!selectedItemId }"
data-list-type="pool"
@click.self="moveSelectedItemToPool"
>
<div
v-for="id in pool"
:key="id"
class="poolItem"
:class="{ 'poolItem--readonly': !canEdit, 'poolItem--hidden': !isPoolItemVisible(id) }"
:class="{
'poolItem--readonly': !canEdit,
'poolItem--hidden': !isPoolItemVisible(id),
'poolItem--selected': selectedItemId === id,
}"
:data-item-id="id"
@click.stop="selectItemByClick(id)"
>
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
<div class="poolItem__label">{{ itemsById[id]?.label || id }}</div>
@@ -2214,6 +2329,11 @@ onUnmounted(() => {
overflow: hidden;
position: relative;
}
.row__drop--clickTarget {
cursor: copy;
border-color: rgba(96, 165, 250, 0.42);
box-shadow: inset 0 0 0 1px rgba(96, 165, 250, 0.12);
}
.row__empty {
opacity: 0.6;
font-size: 13px;
@@ -2227,6 +2347,11 @@ onUnmounted(() => {
display: inline-flex;
flex: 0 0 auto;
position: relative;
cursor: pointer;
border-radius: 12px;
}
.cell--selected {
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.92), 0 0 0 6px rgba(96, 165, 250, 0.18);
}
.itemNameOverlay {
position: absolute;
@@ -2606,6 +2731,9 @@ onUnmounted(() => {
gap: 10px;
align-content: start;
}
.pool--clickTarget {
cursor: copy;
}
.poolItem {
min-width: 0;
display: grid;
@@ -2617,11 +2745,17 @@ onUnmounted(() => {
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.10);
background: var(--theme-pill-bg);
cursor: pointer;
}
.poolItem--readonly {
cursor: default;
opacity: 0.58;
filter: grayscale(0.25) brightness(0.78);
}
.poolItem--selected {
border-color: rgba(96, 165, 250, 0.58);
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.92), 0 0 0 6px rgba(96, 165, 250, 0.18);
}
.poolItem .thumb {
width: 100%;
max-width: var(--thumb-size, 80px);