릴리스: 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

@@ -44,6 +44,7 @@ const isTemplateRequestModalOpen = ref(false)
const isTemplateUpdateModalOpen = ref(false)
const templateRequestDraftTitle = ref('')
const templateRequestDraftDescription = ref('')
const templateRequestSaveToMyTierList = ref(true)
const isDeleteModalOpen = ref(false)
const isGroupDeleteModalOpen = ref(false)
const pendingRemoveGroupId = ref('')
@@ -119,14 +120,7 @@ const canRequestTemplateCreate = computed(
const canRequestTemplateUpdate = computed(
() => canEdit.value && !isNewTierList.value && gameId.value !== 'freeform' && customItems.value.length > 0
)
const templateRequestChecks = computed(() => [
{
id: 'title',
label: '티어표 이름(게임 이름)을 직접 입력했는지',
passed: !!(title.value || '').trim() && (title.value || '').trim() !== (gameName.value || '').trim(),
},
])
const canSubmitTemplateCreateRequest = computed(() => templateRequestChecks.value.every((item) => item.passed) && !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
const canSubmitTemplateCreateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
const canSubmitTemplateUpdateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
const templateRequestTargetLabel = computed(() => (gameId.value === 'freeform' ? '새로운 템플릿' : (gameName.value || gameId.value || '선택한 게임')))
@@ -547,8 +541,9 @@ function closeSaveModal() {
}
function resetTemplateRequestDrafts() {
templateRequestDraftTitle.value = ''
templateRequestDraftDescription.value = ''
templateRequestDraftTitle.value = (title.value || '').trim()
templateRequestDraftDescription.value = (description.value || '').trim()
templateRequestSaveToMyTierList.value = true
}
function openTemplateRequestModal() {
@@ -624,27 +619,54 @@ async function toggleFavorite() {
}
async function requestTemplate(type) {
if (isNewTierList.value) {
toast.error('요청 전에 먼저 티어표를 저장해주세요.')
return
}
try {
isRequestingTemplate.value = true
const persisted = await persistTierList({ showModal: false })
await api.requestTierListTemplate(persisted.savedTierListId, {
await uploadPendingCustomItems()
const uploadedThumbnailSrc = await uploadPendingThumbnail()
const response = await api.requestTierListTemplate({
type,
sourceTierListId: tierListId.value !== 'new' ? tierListId.value : '',
gameId: gameId.value,
requestTitle: templateRequestDraftTitle.value.trim(),
requestDescription: templateRequestDraftDescription.value.trim(),
thumbnailSrc: uploadedThumbnailSrc || thumbnailSrc.value || '',
isPublic: !!isPublic.value,
showCharacterNames: !!showCharacterNames.value,
saveToMyTierList: !!templateRequestSaveToMyTierList.value,
groups: groups.value.map((group) => ({
id: group.id,
name: group.name,
itemIds: [...group.itemIds],
})),
boardItems: Object.values(itemsById.value),
})
const savedTierList = response?.savedTierList
if (savedTierList) {
title.value = savedTierList.title || title.value
description.value = savedTierList.description || ''
updatedAt.value = Number(savedTierList.updatedAt || Date.now())
authorName.value = savedTierList.authorName || effectiveAuthorName.value
authorAccountName.value = savedTierList.authorAccountName || authorAccountName.value
favoriteCount.value = Number(savedTierList.favoriteCount || favoriteCount.value || 0)
isFavorited.value = !!savedTierList.isFavorited
if (tierListId.value === 'new' && savedTierList.id) {
await router.replace(`/editor/${gameId.value}/${savedTierList.id}`)
}
}
if (type === 'create') closeTemplateRequestModal()
if (type === 'update') closeTemplateUpdateModal()
toast.success(type === 'create' ? '템플릿 등록 요청을 보냈어요.' : '템플릿 업데이트 요청을 보냈어요.')
toast.success(
type === 'create'
? templateRequestSaveToMyTierList.value
? '템플릿 등록 요청과 내 티어표 저장을 함께 완료했어요.'
: '템플릿 등록 요청을 보냈어요.'
: templateRequestSaveToMyTierList.value
? '템플릿 업데이트 요청과 내 티어표 저장을 함께 완료했어요.'
: '템플릿 업데이트 요청을 보냈어요.'
)
} catch (e) {
if (e?.status === 400 && e?.data?.error === 'title_required') {
toast.error('템플릿 등록 요청 전에는 티어표 이름을 직접 입력해주세요.')
return
}
if (e?.status === 409) {
toast.error('이미 처리 대기 중인 같은 요청이 있어요.')
return
@@ -773,18 +795,7 @@ onUnmounted(() => {
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateRequestTitle">
<div id="templateRequestTitle" class="modalCard__title">템플릿 등록 요청</div>
<div class="modalCard__desc">
여러 사용자가 비슷한 주제로 요청할 있으니, 관리자에게 전달되기 전에 아래 조건을 먼저 확인해주세.
</div>
<div class="requestChecklist">
<div
v-for="check in templateRequestChecks"
:key="check.id"
class="requestChecklist__item"
:class="{ 'requestChecklist__item--passed': check.passed }"
>
<span class="requestChecklist__label">{{ check.label }}</span>
<span class="requestChecklist__icon">{{ check.passed ? '완료' : '확인 필요' }}</span>
</div>
티어표에 적어둔 제목과 설명이 있다면 그대로 가져와 두었어요. 비어 있다면 여기서 바로 작성해도 괜찮아.
</div>
<div class="requestChecklist__hint">
제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 있어요.
@@ -792,15 +803,23 @@ onUnmounted(() => {
</div>
<div class="templateRequestDraft">
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 제목</span>
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 등록 요청" />
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/80</span>
<span class="templateRequestDraft__label">티어표 제목</span>
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="120" placeholder="예: 템플릿 등록 요청" />
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/120</span>
</label>
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 설명</span>
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/240</span>
<span class="templateRequestDraft__label">티어표 설명</span>
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="1000" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/1000</span>
</label>
<label class="toggleSwitch">
<input v-model="templateRequestSaveToMyTierList" type="checkbox" />
<span class="toggleSwitch__label"> 티어 리스트에도 저장</span>
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
</label>
<div class="templateRequestDraft__note">
저장을 끄면 요청 시점 스냅샷만 관리자에게 전달되고, 티어 리스트에는 별도로 남기지 않아요.
</div>
</div>
<div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeTemplateRequestModal">취소</button>
@@ -815,7 +834,7 @@ onUnmounted(() => {
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateUpdateTitle">
<div id="templateUpdateTitle" class="modalCard__title">템플릿 요청하기</div>
<div class="modalCard__desc">
{{ templateRequestTargetLabel }} 직접 추가한 아이템을 포함해 달라고 관리자에게 요청 보냅니다.
{{ templateRequestTargetLabel }} 직접 추가한 아이템을 포함해 달라고 관리자에게 요청합니다. 현재 티어표 제목과 설명은 그대로 가져와 두었어요.
</div>
<div class="modalCard__note">
모두가 사용하는 기본 템플릿이니 개인적인 항목이 아닌 공통된 항목만 추가한 신청해주세요.
@@ -823,15 +842,23 @@ onUnmounted(() => {
</div>
<div class="templateRequestDraft">
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 제목</span>
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 업데이트 요청" />
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/80</span>
<span class="templateRequestDraft__label">티어표 제목</span>
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="120" placeholder="예: 템플릿 업데이트 요청" />
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/120</span>
</label>
<label class="templateRequestDraft__field">
<span class="templateRequestDraft__label">요청 설명</span>
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/240</span>
<span class="templateRequestDraft__label">티어표 설명</span>
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="1000" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/1000</span>
</label>
<label class="toggleSwitch">
<input v-model="templateRequestSaveToMyTierList" type="checkbox" />
<span class="toggleSwitch__label"> 티어 리스트에도 저장</span>
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
</label>
<div class="templateRequestDraft__note">
저장을 끄면 관리자 확인용 요청 스냅샷만 남고, 현재 작업 중인 티어표는 따로 저장하지 않아요.
</div>
</div>
<div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeTemplateUpdateModal">요청 취소</button>
@@ -1449,6 +1476,11 @@ onUnmounted(() => {
font-size: 12px;
color: rgba(255, 255, 255, 0.46);
}
.templateRequestDraft__note {
font-size: 12px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
}
.templateRequestDraft__input {
width: 100%;
padding: 14px 0;