릴리스: v1.3.13 템플릿 요청 스냅샷과 저장 분리

This commit is contained in:
2026-04-01 11:50:54 +09:00
parent b2a838ff34
commit 7fe4eff7b7
7 changed files with 366 additions and 90 deletions

View File

@@ -1065,6 +1065,38 @@ function openAdminTierList(tierList) {
previewModalOpen.value = true
}
function previewRequestItemsById(preview) {
const items = Array.isArray(preview?.snapshotItems) ? preview.snapshotItems : []
return items.reduce((acc, item) => {
if (item?.id) acc[item.id] = item
return acc
}, {})
}
function previewRequestGroupItems(preview, group) {
const itemsById = previewRequestItemsById(preview)
return (group?.itemIds || []).map((itemId) => itemsById[itemId]).filter(Boolean)
}
function previewRequestPoolItems(preview) {
const groupedIds = new Set((preview?.snapshotGroups || []).flatMap((group) => group.itemIds || []))
return (preview?.snapshotItems || []).filter((item) => !groupedIds.has(item.id))
}
function openTemplateRequestPreview(request) {
previewTierList.value = {
id: request.id,
title: request.sourceTierListTitle || '템플릿 요청 미리보기',
description: request.sourceDescription || '',
thumbnailSrc: request.thumbnailSrc || '',
requestPreview: true,
snapshotGroups: request.snapshotGroups || [],
snapshotItems: request.snapshotItems || [],
snapshotShowCharacterNames: !!request.snapshotShowCharacterNames,
}
previewModalOpen.value = true
}
function closePreviewModal() {
previewModalOpen.value = false
previewTierList.value = null
@@ -1470,13 +1502,14 @@ async function saveFeaturedOrder() {
<div class="templateRequestCard__head">
<div>
<div class="templateRequestCard__title">{{ request.sourceTierListTitle }}</div>
<div v-if="request.sourceDescription" class="templateRequestCard__desc">{{ request.sourceDescription }}</div>
<div class="templateRequestCard__meta">
{{ templateRequestTypeLabel(request) }} · {{ request.requesterName }} · {{ fmt(request.createdAt) }}
</div>
<div class="templateRequestCard__meta">{{ templateRequestTargetLabel(request) }}</div>
</div>
<button class="btn btn--ghost btn--small" @click="openAdminTierList({ id: request.sourceTierListId, gameId: request.sourceGameId })">
원본 보기
<button class="btn btn--ghost btn--small" @click="openTemplateRequestPreview(request)">
요청 미리보기
</button>
</div>
@@ -1886,8 +1919,45 @@ async function saveFeaturedOrder() {
<div class="modalCard__title">{{ previewTierList?.title || '티어표 미리보기' }}</div>
<button class="btn btn--ghost btn--small" @click="closePreviewModal">닫기</button>
</div>
<div v-if="previewTierList?.requestPreview" class="requestPreview">
<img
v-if="previewTierList.thumbnailSrc"
class="requestPreview__thumb"
:src="toApiUrl(previewTierList.thumbnailSrc)"
:alt="previewTierList.title"
/>
<div v-if="previewTierList.description" class="requestPreview__desc">{{ previewTierList.description }}</div>
<div class="requestPreview__rows">
<div v-for="group in previewTierList.snapshotGroups" :key="group.id" class="requestPreview__row">
<div class="requestPreview__rowLabel">{{ group.name }}</div>
<div class="requestPreview__rowItems">
<div
v-for="item in previewRequestGroupItems(previewTierList, group)"
:key="item.id"
class="requestPreview__item"
>
<img class="requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
<div v-if="previewTierList.snapshotShowCharacterNames" class="requestPreview__itemLabel">{{ item.label }}</div>
</div>
</div>
</div>
</div>
<div v-if="previewRequestPoolItems(previewTierList).length" class="requestPreview__pool">
<div class="requestPreview__poolLabel">남은 아이템</div>
<div class="requestPreview__rowItems">
<div
v-for="item in previewRequestPoolItems(previewTierList)"
:key="item.id"
class="requestPreview__item requestPreview__item--muted"
>
<img class="requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
<div v-if="previewTierList.snapshotShowCharacterNames" class="requestPreview__itemLabel">{{ item.label }}</div>
</div>
</div>
</div>
</div>
<iframe
v-if="previewTierList"
v-else-if="previewTierList"
class="previewFrame"
:src="previewTierListUrl(previewTierList)"
title="티어표 미리보기"
@@ -3224,6 +3294,12 @@ async function saveFeaturedOrder() {
font-weight: 900;
font-size: 18px;
}
.templateRequestCard__desc {
margin-top: 6px;
color: rgba(255, 255, 255, 0.74);
line-height: 1.55;
white-space: pre-line;
}
.templateRequestCard__meta {
margin-top: 4px;
font-size: 13px;
@@ -3272,6 +3348,76 @@ async function saveFeaturedOrder() {
justify-content: flex-end;
flex-wrap: wrap;
}
.requestPreview {
display: grid;
gap: 18px;
}
.requestPreview__thumb {
width: 100%;
max-height: 240px;
object-fit: cover;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.requestPreview__desc {
color: rgba(255, 255, 255, 0.74);
line-height: 1.6;
white-space: pre-line;
}
.requestPreview__rows,
.requestPreview__pool {
display: grid;
gap: 12px;
}
.requestPreview__row {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
gap: 12px;
align-items: start;
}
.requestPreview__rowLabel,
.requestPreview__poolLabel {
font-size: 13px;
font-weight: 800;
color: rgba(255, 255, 255, 0.86);
}
.requestPreview__rowItems {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
gap: 10px;
}
.requestPreview__item {
position: relative;
overflow: hidden;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
min-height: 72px;
}
.requestPreview__item--muted {
opacity: 0.52;
filter: grayscale(0.2) brightness(0.78);
}
.requestPreview__itemThumb {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
display: block;
}
.requestPreview__itemLabel {
position: absolute;
left: 8px;
right: 8px;
bottom: 8px;
padding: 4px 6px;
border-radius: 8px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0.7));
font-size: 11px;
font-weight: 700;
text-align: center;
line-height: 1.3;
}
.tierAdminList {
margin-top: 14px;
display: grid;