Compare commits

...

2 Commits

5 changed files with 69 additions and 11 deletions

View File

@@ -1,5 +1,12 @@
# 의사결정 이력
## 2026-03-27 v0.1.50
- 신규 티어표 저장 직후 요청 실패는 별도 요청용 티어표를 또 만드는 것보다, 방금 저장된 실제 티어표 ID를 그대로 이어받아 요청하는 편이 구조가 단순하고 안전하다고 판단했다.
## 2026-03-27 v0.1.49
- 템플릿 등록 요청 모달은 체크리스트 설명이 먼저 읽히고 상태가 우측에서 한눈에 보여야 하므로, 라벨 좌측·상태 우측 구조로 정리하기로 했다.
- 관리자 입장에서는 `요청 목록``저장된 전체 티어표 목록`이 서로 다른 성격이므로, 같은 화면 안에서도 서브 탭으로 분리해 맥락을 명확히 하는 편이 더 적합하다고 판단했다.
## 2026-03-27 v0.1.48
- 템플릿 등록 요청은 실패 원인이 불명확하면 혼란이 크므로, 요청 전에 체크리스트 모달로 조건을 먼저 확인시키고 조건이 맞을 때만 전송하게 하는 편이 낫다고 정리했다.
- freeform 템플릿 등록 요청은 제목이 곧 게임 이름 후보가 되므로, 기본값이 아닌 사용자가 직접 입력한 제목을 요구하기로 했다.

View File

@@ -124,7 +124,7 @@
- 사용자 업로드 커스텀 아이템은 관리자 화면의 아이템 관리 탭에서 검색, 페이지네이션, 다운로드할 수 있다.
- 사용자 커스텀 아이템은 선택한 게임의 기본 템플릿으로 복제해 가져올 수 있다.
- 커스텀 아이템은 사용 횟수(`usageCount`)를 표시하며, 미사용 항목만 필터링해 개별/일괄 삭제할 수 있다.
- 관리자 화면에는 별도 `티어표 관리` 탭이 있으며, 최근 티어표 전체를 제목/게임/작성자 기준으로 검색하고 공개 여부를 함께 확인할 수 있다.
- 관리자 화면에는 별도 `티어표 관리` 탭이 있으며, 내부에서 `템플릿 요청 관리 / 전체 티어표 관리`를 분리해 볼 수 있다.
- `티어표 관리` 탭의 추가 아이템은 작은 그리드 카드로 표시하고, 클릭 시 `기존 템플릿에 추가 / 새 템플릿 만들기` 모달을 통해 목적지를 선택한다.
- `티어표 관리` 탭에서는 티어표 안의 커스텀 아이템을 개별 또는 일괄로 기존 게임 템플릿에 복제할 수 있다.
- `freeform` 티어표는 관리자 화면에서 새 게임 ID/이름을 입력해 새로운 게임 템플릿으로 복제 생성할 수 있다.
@@ -150,6 +150,7 @@
- 제목 입력이 비어 있는 동안에는 무분별한 도배 방지를 위한 관리자 임의 삭제 가능성 안내 문구를 표시한다.
- `freeform` 티어표는 보드가 비어 있고 커스텀 아이템이 준비된 상태에서만 `템플릿 등록 요청`을 보낼 수 있다.
- `템플릿 등록 요청` 전에는 체크리스트 모달로 `제목 직접 입력`, `보드 비움 상태`를 확인하고 두 조건이 충족될 때만 전송할 수 있다.
- 신규 티어표를 막 저장한 직후에도, 템플릿 요청은 새로 발급된 실제 티어표 ID를 기준으로 이어서 처리한다.
- 기존 게임 티어표는 커스텀 아이템이 포함되어 있으면 `템플릿 업데이트 요청`을 보낼 수 있다.
- 티어표는 편집 화면에서 16:9 썸네일 이미지를 별도로 선택해 저장할 수 있고, 목록 카드에서는 그 이미지를 상단 대표 썸네일로 사용한다.
- 편집 화면 상단 헤더는 좌측 제목/설명, 우측 썸네일 카드 구조를 사용하며 모바일에서는 한 열로 접힌다.

View File

@@ -1,5 +1,13 @@
# 업데이트 로그
## 2026-03-27 v0.1.50
- **신규 티어표 등록 요청 타이밍 수정**: 막 저장한 티어표에서 곧바로 템플릿 등록 요청을 보낼 때도 `new`가 아닌 실제 저장된 티어표 ID로 이어서 요청하도록 수정해, 신규 작성 직후 요청 실패 문제를 해결
## 2026-03-27 v0.1.49
- **템플릿 등록 요청 모달 레이아웃 보정**: 체크리스트 문구 줄바꿈과 버튼 겹침 문제를 수정하고, 설명은 좌측·상태 배지는 우측에 배치되도록 요청 모달 레이아웃을 다시 정리
- **관리자 티어표 화면 분리**: `티어표 관리` 탭 안에서 `템플릿 요청 관리 / 전체 티어표 관리`를 서브 탭으로 분리해, 요청 목록과 저장된 전체 티어표 목록이 섞여 보이지 않도록 개선
- **관리자 안내 문구 보강**: 전체 티어표 목록은 요청과 별개로 저장된 티어표 전체를 보는 영역이라는 설명을 추가해 혼선을 줄이도록 보강
## 2026-03-27 v0.1.48
- **템플릿 등록 요청 체크리스트 모달 추가**: freeform 템플릿 등록 요청 전 `제목 직접 입력 여부`, `보드 비움 상태`를 확인하는 모달과 안내 문구를 추가하고, 조건이 맞을 때만 요청 버튼이 활성화되도록 조정
- **등록 요청 실패 원인 구체화**: 템플릿 등록 요청 실패 시 제목 미입력, 보드 비우지 않음, 커스텀 아이템 없음, 중복 대기 요청 같은 주요 원인을 토스트로 구체적으로 안내하도록 보강

View File

@@ -13,6 +13,7 @@ const toast = useToast()
const isAdmin = computed(() => !!auth.user?.isAdmin)
const activeTab = ref('games')
const tierlistsMode = ref('requests')
const gameMode = ref('existing')
const games = ref([])
@@ -105,11 +106,19 @@ function resetMessages() {
function setTab(tab) {
resetMessages()
activeTab.value = tab
if (tab === 'tierlists') {
tierlistsMode.value = 'requests'
}
if (tab === 'items' && !customItemTargetGameId.value && games.value.length) {
customItemTargetGameId.value = games.value[0].id
}
}
function setTierlistsMode(mode) {
resetMessages()
tierlistsMode.value = mode
}
async function refreshGames() {
try {
const data = await api.listGames()
@@ -1084,7 +1093,16 @@ async function saveFeaturedOrder() {
</template>
<template v-else-if="activeTab === 'tierlists'">
<div class="panel">
<div class="modeTabs modeTabs--admin">
<button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'requests' }" @click="setTierlistsMode('requests')">
템플릿 요청 관리
</button>
<button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'lists' }" @click="setTierlistsMode('lists')">
전체 티어표 관리
</button>
</div>
<div v-if="tierlistsMode === 'requests'" class="panel">
<div class="sectionHeader">
<div>
<div class="panel__title">사용자 템플릿 요청</div>
@@ -1131,11 +1149,11 @@ async function saveFeaturedOrder() {
</div>
</div>
<div class="panel">
<div v-else class="panel">
<div class="sectionHeader">
<div>
<div class="panel__title">전체 티어표 관리</div>
<div class="hint hint--tight">공개/비공개를 포함한 최근 티어표를 모두 확인하고, 추가 아이템을 기존 게임 템플릿으로 승격하거나 커스텀 티어표를 게임 템플릿으로 만들 있어요.</div>
<div class="hint hint--tight">공개/비공개를 포함한 최근 티어표를 모두 확인하고, 추가 아이템을 기존 게임 템플릿으로 승격하거나 커스텀 티어표를 게임 템플릿으로 만들 있어요. 여기는 요청 목록과 별개로 전체 저장 티어표를 보는 영역입니다.</div>
</div>
<button class="btn btn--ghost" @click="refreshAdminTierLists">새로고침</button>
</div>

View File

@@ -443,14 +443,17 @@ async function persistTierList({ showModal = false } = {}) {
await uploadPendingThumbnail()
const payload = buildPayload(tierListId.value && tierListId.value !== 'new' ? tierListId.value : null)
const res = await api.saveTierList(payload)
if (tierListId.value === 'new') history.replaceState({}, '', `/editor/${gameId.value}/${res.tierList.id}`)
const savedTierListId = res.tierList?.id || tierListId.value
if (tierListId.value === 'new' && res.tierList?.id) {
await router.replace(`/editor/${gameId.value}/${res.tierList.id}`)
}
updatedAt.value = Number(res.tierList?.updatedAt || Date.now())
authorName.value = res.tierList?.authorName || effectiveAuthorName.value
authorAccountName.value = res.tierList?.authorAccountName || authorAccountName.value
favoriteCount.value = Number(res.tierList?.favoriteCount || favoriteCount.value || 0)
isFavorited.value = !!res.tierList?.isFavorited
if (showModal) isSaveModalOpen.value = true
return res
return { ...res, savedTierListId }
}
async function save() {
@@ -514,8 +517,8 @@ async function requestTemplate(type) {
try {
isRequestingTemplate.value = true
await persistTierList({ showModal: false })
await api.requestTierListTemplate(tierListId.value, { type })
const persisted = await persistTierList({ showModal: false })
await api.requestTierListTemplate(persisted.savedTierListId, { type })
if (type === 'create') closeTemplateRequestModal()
toast.success(type === 'create' ? '템플릿 등록 요청을 보냈어요.' : '템플릿 업데이트 요청을 보냈어요.')
} catch (e) {
@@ -704,8 +707,8 @@ onUnmounted(() => {
class="requestChecklist__item"
:class="{ 'requestChecklist__item--passed': check.passed }"
>
<span class="requestChecklist__label">{{ check.label }}</span>
<span class="requestChecklist__icon">{{ check.passed ? '완료' : '확인 필요' }}</span>
<span>{{ check.label }}</span>
</div>
</div>
<div class="requestChecklist__hint">
@@ -1077,13 +1080,19 @@ onUnmounted(() => {
display: flex;
justify-content: flex-end;
margin-top: 8px;
flex-wrap: wrap;
}
.modalCard__actions .btn {
width: auto;
min-width: 120px;
}
.requestChecklist {
display: grid;
gap: 10px;
}
.requestChecklist__item {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
padding: 10px 12px;
@@ -1096,12 +1105,18 @@ onUnmounted(() => {
border-color: rgba(52, 211, 153, 0.24);
background: rgba(52, 211, 153, 0.1);
}
.requestChecklist__label {
min-width: 0;
overflow-wrap: anywhere;
}
.requestChecklist__icon {
flex: 0 0 auto;
min-width: 56px;
min-width: 68px;
font-size: 12px;
font-weight: 900;
opacity: 0.9;
text-align: right;
white-space: nowrap;
}
.requestChecklist__hint {
font-size: 13px;
@@ -1446,5 +1461,14 @@ onUnmounted(() => {
.descInput {
border-radius: 16px;
}
.requestChecklist__item {
grid-template-columns: 1fr;
}
.requestChecklist__icon {
text-align: left;
}
.modalCard__actions .btn {
width: 100%;
}
}
</style>