릴리스: v1.4.25 topic 응답/요청 키 정리

This commit is contained in:
2026-04-02 20:51:03 +09:00
parent 257d50f9c5
commit 932b4e35a7
20 changed files with 188 additions and 199 deletions

View File

@@ -23,7 +23,7 @@ const router = useRouter()
const auth = useAuthStore()
const { toasts, dismissToast } = useToast()
const RIGHT_RAIL_COPYRIGHT_URL = 'https://zenn.town/@murabito'
const currentTopicId = computed(() => route.params.topicId || route.params.gameId || '')
const currentTopicId = computed(() => route.params.topicId || '')
const leftRailCollapsed = ref(false)
const rightRailOpen = ref(true)

View File

@@ -65,7 +65,7 @@ function setThumbFileElement(el) {
<div class="hint hint--tight">
{{
props.activeTemplateRequest.type === 'create'
? (props.activeTemplateRequest.targetGameId
? (props.activeTemplateRequest.targetTopicId
? '이미 연결된 신규 템플릿이 있어요. 필요한 아이템만 골라 저장하세요.'
: '새 템플릿을 한 번 만든 뒤 필요한 아이템만 골라 저장하세요.')
: '필요한 아이템만 남긴 뒤 기본 아이템 추가 버튼으로 반영하세요.'
@@ -76,8 +76,8 @@ function setThumbFileElement(el) {
<span class="pill pill--accent">{{ props.activeTemplateRequest.type === 'create' ? '신규 템플릿 요청' : '기존 템플릿 업데이트' }}</span>
<span class="pill">요청 아이템 {{ props.stagedRequestDraftCount }}</span>
<span v-if="props.appliedRequestItemCount" class="pill pill--soft">이미 반영 {{ props.appliedRequestItemCount }}</span>
<span v-if="props.activeTemplateRequest.type === 'create' && (props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId)" class="pill pill--soft">
연결된 템플릿 · {{ props.activeTemplateRequest.targetGameName || props.activeTemplateRequest.targetGameId }}
<span v-if="props.activeTemplateRequest.type === 'create' && (props.activeTemplateRequest.targetTopicName || props.activeTemplateRequest.targetTopicId)" class="pill pill--soft">
연결된 템플릿 · {{ props.activeTemplateRequest.targetTopicName || props.activeTemplateRequest.targetTopicId }}
</span>
</div>
</div>
@@ -92,7 +92,7 @@ function setThumbFileElement(el) {
요청 티어표 보기
</a>
<button
v-if="props.activeTemplateRequest.type === 'create' && !props.activeTemplateRequest.targetGameId"
v-if="props.activeTemplateRequest.type === 'create' && !props.activeTemplateRequest.targetTopicId"
class="btn btn--ghost btn--small"
type="button"
@click="props.openTemplateCreateModal"

View File

@@ -53,18 +53,18 @@ const props = defineProps({
<template v-if="request.type === 'create'">
<label class="templateRequestField">
<span class="templateRequestField__label">템플릿 이름</span>
<input v-model="request.draftGameName" class="input" placeholder="새 템플릿 이름" />
<input v-model="request.draftTopicName" class="input" placeholder="새 템플릿 이름" />
</label>
<label class="templateRequestField">
<span class="templateRequestField__label">템플릿 ID</span>
<input v-model="request.draftGameId" class="input" placeholder="임시 템플릿 ID" />
<input v-model="request.draftTopicId" class="input" placeholder="임시 템플릿 ID" />
</label>
</template>
<template v-else>
<div class="templateRequestCard__thumbLabel">템플릿 이름</div>
<div class="templateRequestCard__thumbValue">{{ request.draftGameName || request.sourceGameName || '-' }}</div>
<div class="templateRequestCard__thumbValue">{{ request.draftTopicName || request.sourceTopicName || '-' }}</div>
<div class="templateRequestCard__thumbLabel">템플릿 ID</div>
<div class="templateRequestCard__thumbValue">{{ request.draftGameId || request.sourceGameId || '-' }}</div>
<div class="templateRequestCard__thumbValue">{{ request.draftTopicId || request.sourceTopicId || '-' }}</div>
</template>
</div>
</div>
@@ -89,8 +89,8 @@ const props = defineProps({
<div class="tierAdminCard__stats">
<span class="pill">추가 아이템 {{ request.items?.length || 0 }}</span>
<span v-if="request.type === 'create' && (request.targetGameName || request.targetGameId)" class="pill pill--soft">
연결됨 · {{ request.targetGameName || request.targetGameId }}
<span v-if="request.type === 'create' && (request.targetTopicName || request.targetTopicId)" class="pill pill--soft">
연결됨 · {{ request.targetTopicName || request.targetTopicId }}
</span>
<span class="pill" :class="{ 'pill--accent': request.status === 'reviewing' }">{{ props.templateRequestStatusLabel(request) }}</span>
</div>
@@ -109,7 +109,7 @@ const props = defineProps({
{{
request.isHandling
? '이동중...'
: request.type === 'create' && (request.targetGameName || request.targetGameId)
: request.type === 'create' && (request.targetTopicName || request.targetTopicId)
? '연결된 템플릿 열기'
: '확인하기'
}}
@@ -148,7 +148,7 @@ const props = defineProps({
<div class="tierAdminCard__title">{{ tierList.title }}</div>
<div v-if="tierList.description" class="tierAdminCard__desc">{{ tierList.description }}</div>
<div class="tierAdminCard__meta">
{{ tierList.topicName || tierList.gameName || tierList.topicId || tierList.gameId }} · {{ props.tierListAuthorDisplayName(tierList) }}
{{ tierList.topicName || tierList.topicId }} · {{ props.tierListAuthorDisplayName(tierList) }}
</div>
<div class="tierAdminCard__meta">{{ props.fmt(tierList.updatedAt) }}</div>
</div>
@@ -170,7 +170,7 @@ const props = defineProps({
</div>
<div class="tierAdminSection__actions">
<button class="btn btn--ghost btn--small" @click="props.openTierListImportModal(tierList, tierList.extraItems)">추가 아이템 전체 가져오기</button>
<button v-if="(tierList.topicId || tierList.gameId) === 'freeform'" class="btn btn--primary btn--small" @click="props.openTierListImportModal(tierList, tierList.extraItems)">
<button v-if="tierList.topicId === 'freeform'" class="btn btn--primary btn--small" @click="props.openTierListImportModal(tierList, tierList.extraItems)">
템플릿으로 가져오기
</button>
</div>

View File

@@ -167,7 +167,7 @@ export function useAdminCustomItems({
try {
item.isPromoting = true
await api.promoteAdminTemplateItem(item.id, { gameId: customItemModalTargetTemplateId.value })
await api.promoteAdminTemplateItem(item.id, { topicId: customItemModalTargetTemplateId.value })
const targetTemplateName =
templates.value.find((template) => template.id === customItemModalTargetTemplateId.value)?.name || customItemModalTargetTemplateId.value
if (selectedTemplateId.value === customItemModalTargetTemplateId.value) await loadTemplate()

View File

@@ -70,8 +70,8 @@ export function useAdminFeaturedGames({
async function saveFeaturedOrder() {
resetMessages()
try {
const data = await api.updateAdminTemplateDisplayOrder({ gameIds: featuredTemplateIds.value })
templates.value = data.games || []
const data = await api.updateAdminTemplateDisplayOrder({ topicIds: featuredTemplateIds.value })
templates.value = data.templates || []
featuredTemplateIds.value = templates.value
.filter((template) => template.displayRank != null)
.sort((a, b) => a.displayRank - b.displayRank)

View File

@@ -153,8 +153,8 @@ export function useAdminGameManager({
}
async function createTemplate(options = {}) {
const nextGameId = typeof options.gameId === 'string' ? options.gameId.trim() : newTemplateId.value.trim()
const nextGameName = typeof options.gameName === 'string' ? options.gameName.trim() : newTemplateName.value.trim()
const nextTopicId = typeof options.topicId === 'string' ? options.topicId.trim() : newTemplateId.value.trim()
const nextTopicName = typeof options.topicName === 'string' ? options.topicName.trim() : newTemplateName.value.trim()
const preserveUploadState = !!options.preserveUploadState
resetMessages()
try {
@@ -163,8 +163,8 @@ export function useAdminGameManager({
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: nextGameId,
name: nextGameName,
id: nextTopicId,
name: nextTopicName,
isPublic: !!newTemplateIsPublic.value,
thumbnailSrc: activeTemplateRequest.value?.type === 'create' ? (activeTemplateRequest.value?.thumbnailSrc || '') : '',
}),
@@ -175,19 +175,19 @@ export function useAdminGameManager({
const createdTemplate = data.template || {}
if (activeTemplateRequest.value?.type === 'create' && activeTemplateRequest.value?.id) {
const linkData = await api.linkAdminTemplateRequestGame(activeTemplateRequest.value.id, {
gameId: createdTemplate.id,
topicId: createdTemplate.id,
})
activeTemplateRequest.value = {
...activeTemplateRequest.value,
targetGameId: linkData.request?.targetGameId || createdTemplate.id,
targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName,
targetTopicId: linkData.request?.targetTopicId || createdTemplate.id,
targetTopicName: linkData.request?.targetTopicName || createdTemplate.name || nextTopicName,
}
const requestIndex = templateRequests.value.findIndex((entry) => entry.id === activeTemplateRequest.value.id)
if (requestIndex >= 0) {
templateRequests.value.splice(requestIndex, 1, {
...templateRequests.value[requestIndex],
targetGameId: linkData.request?.targetGameId || createdTemplate.id,
targetGameName: linkData.request?.targetGameName || createdTemplate.name || nextGameName,
targetTopicId: linkData.request?.targetTopicId || createdTemplate.id,
targetTopicName: linkData.request?.targetTopicName || createdTemplate.name || nextTopicName,
})
}
}
@@ -258,16 +258,16 @@ export function useAdminGameManager({
}
try {
if (!selectedTemplateId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) {
const draftGameId = (activeTemplateRequest.value?.draftGameId || '').trim()
const draftGameName = (activeTemplateRequest.value?.draftGameName || '').trim()
if (!draftGameId || !draftGameName) {
if (!selectedTemplateId.value && activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetTopicId) {
const draftTopicId = (activeTemplateRequest.value?.draftTopicId || '').trim()
const draftTopicName = (activeTemplateRequest.value?.draftTopicName || '').trim()
if (!draftTopicId || !draftTopicName) {
error.value = '먼저 신규 템플릿의 이름과 템플릿 ID를 저장해주세요.'
return
}
await createTemplate({
gameId: draftGameId,
gameName: draftGameName,
topicId: draftTopicId,
topicName: draftTopicName,
preserveUploadState: true,
})
}
@@ -301,7 +301,7 @@ export function useAdminGameManager({
for (const requestId of requestIds) {
const draftsForRequest = requestDrafts.filter((entry) => entry.requestId === requestId)
const result = await api.promoteAdminTemplateRequestItems(requestId, {
gameId: selectedTemplateId.value,
topicId: selectedTemplateId.value,
itemIds: draftsForRequest.map((entry) => entry.itemId).filter(Boolean),
itemSrcs: draftsForRequest.map((entry) => entry.src).filter(Boolean),
itemLabels: draftsForRequest.reduce((acc, entry) => {
@@ -327,7 +327,7 @@ export function useAdminGameManager({
error.value = `요청 아이템을 템플릿 기본 아이템으로 옮기지 못했어요.${detail}`
return
}
if (apiError === 'game_not_found') {
if (apiError === 'topic_not_found') {
error.value = '선택한 템플릿을 찾지 못했어요.'
return
}

View File

@@ -21,14 +21,14 @@ export function useAdminTemplateRequests({
type: request.type,
status: request.status,
thumbnailSrc: request.thumbnailSrc || '',
draftGameId: request.draftTopicId || request.draftGameId || '',
draftGameName: request.draftTopicName || request.draftGameName || '',
draftGameIsPublic: !!(request.draftTopicIsPublic ?? request.draftGameIsPublic),
draftTopicId: request.draftTopicId || '',
draftTopicName: request.draftTopicName || '',
draftTopicIsPublic: !!request.draftTopicIsPublic,
sourceTierListId: request.sourceTierListId || '',
sourceGameId: request.sourceTopicId || request.sourceGameId || '',
sourceTopicId: request.sourceTopicId || '',
sourceTierListTitle: request.sourceTierListTitle || '',
targetGameId: request.targetTopicId || request.targetGameId || '',
targetGameName: request.targetTopicName || request.targetGameName || '',
targetTopicId: request.targetTopicId || '',
targetTopicName: request.targetTopicName || '',
requesterName: request.requesterName || '',
}
}
@@ -38,8 +38,8 @@ export function useAdminTemplateRequests({
}
function templateRequestSourceUrl(request) {
if (!request?.sourceGameId || !request?.sourceTierListId) return ''
return editorPath(request.sourceGameId, request.sourceTierListId, { preview: true })
if (!request?.sourceTopicId || !request?.sourceTierListId) return ''
return editorPath(request.sourceTopicId, request.sourceTierListId, { preview: true })
}
function templateRequestReviewHint(request) {
@@ -55,9 +55,9 @@ export function useAdminTemplateRequests({
const syncedRequest = {
...request,
...(data.request || {}),
draftGameId: request.draftGameId || '',
draftGameName: request.draftGameName || '',
draftGameIsPublic: !!request.draftGameIsPublic,
draftTopicId: request.draftTopicId || '',
draftTopicName: request.draftTopicName || '',
draftTopicIsPublic: !!request.draftTopicIsPublic,
}
Object.assign(request, syncedRequest)
request.status = syncedRequest.status || 'reviewing'
@@ -65,18 +65,18 @@ export function useAdminTemplateRequests({
setTab('game-admin')
if (request.type === 'create') {
const linkedGameId = syncedRequest.targetGameId || ''
if (linkedGameId) {
await selectAdminTemplate(linkedGameId)
const linkedTopicId = syncedRequest.targetTopicId || ''
if (linkedTopicId) {
await selectAdminTemplate(linkedTopicId)
} else {
openTemplateCreateModal()
newTemplateId.value = (syncedRequest.draftGameId || '').trim()
newTemplateName.value = (syncedRequest.draftGameName || '').trim()
newTemplateId.value = (syncedRequest.draftTopicId || '').trim()
newTemplateName.value = (syncedRequest.draftTopicName || '').trim()
}
mergeRequestItemsIntoDrafts(syncedRequest)
} else {
const nextGameId = syncedRequest.targetGameId || syncedRequest.sourceGameId || ''
if (nextGameId) await selectAdminTemplate(nextGameId)
const nextTopicId = syncedRequest.targetTopicId || syncedRequest.sourceTopicId || ''
if (nextTopicId) await selectAdminTemplate(nextTopicId)
mergeRequestItemsIntoDrafts(syncedRequest)
}
success.value = '요청 아이템을 템플릿 관리 화면으로 가져왔어요. 필요한 항목만 골라서 추가해 주세요.'

View File

@@ -74,12 +74,10 @@ export const api = {
request(
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}`
),
listAdminTierLists: ({ q = '', topicId = '', gameId = '', page = 1, limit = 50 } = {}) =>
request(
`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId || gameId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`
),
getAdminTierListStats: ({ q = '', topicId = '', gameId = '' } = {}) =>
request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId || gameId)}`),
listAdminTierLists: ({ q = '', topicId = '', page = 1, limit = 50 } = {}) =>
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
getAdminTierListStats: ({ q = '', topicId = '' } = {}) =>
request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}`),
updateAdminTierList: (tierListId, payload) =>
request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'PATCH', body: payload }),
deleteAdminTierList: (tierListId) =>

View File

@@ -424,11 +424,11 @@ watch(
(name) => {
activeTab.value = tabFromAdminRoute(name)
if (name === 'adminGames') {
const nextGameId = typeof route.query.gameId === 'string' ? route.query.gameId : ''
if (nextGameId && nextGameId !== selectedTemplateId.value) {
selectedTemplateId.value = nextGameId
const nextTopicId = typeof route.query.topicId === 'string' ? route.query.topicId : ''
if (nextTopicId && nextTopicId !== selectedTemplateId.value) {
selectedTemplateId.value = nextTopicId
queueMicrotask(() => {
if (selectedTemplateId.value === nextGameId) void loadTemplate()
if (selectedTemplateId.value === nextTopicId) void loadTemplate()
})
}
return
@@ -436,8 +436,8 @@ watch(
if (name === 'adminTierlists') {
const nextMode = route.query.mode === 'all' ? 'all' : 'requests'
if (tierlistsMode.value !== nextMode) tierlistsMode.value = nextMode
const nextTierListGameId = typeof route.query.gameId === 'string' ? route.query.gameId : ''
if (adminTierListGameId.value !== nextTierListGameId) adminTierListGameId.value = nextTierListGameId
const nextTierListTopicId = typeof route.query.topicId === 'string' ? route.query.topicId : ''
if (adminTierListGameId.value !== nextTierListTopicId) adminTierListGameId.value = nextTierListTopicId
}
},
{ immediate: true }
@@ -447,7 +447,7 @@ watch(
() => selectedTemplateId.value,
(templateId) => {
if (route.name !== 'adminGames') return
syncAdminRouteQuery({ gameId: templateId || undefined })
syncAdminRouteQuery({ topicId: templateId || undefined })
}
)
@@ -465,16 +465,16 @@ watch(
if (route.name !== 'adminTierlists') return
syncAdminRouteQuery({
mode: mode === 'all' ? 'all' : undefined,
gameId: mode === 'all' && adminTierListGameId.value ? adminTierListGameId.value : undefined,
topicId: mode === 'all' && adminTierListGameId.value ? adminTierListGameId.value : undefined,
})
}
)
watch(
() => adminTierListGameId.value,
(gameId) => {
(topicId) => {
if (route.name !== 'adminTierlists' || tierlistsMode.value !== 'all') return
syncAdminRouteQuery({ gameId: gameId || undefined })
syncAdminRouteQuery({ topicId: topicId || undefined })
}
)
@@ -733,7 +733,7 @@ function setTab(tab) {
if (nextRouteName && route.name !== nextRouteName) {
const nextQuery =
tab === 'game-admin'
? { gameId: selectedTemplateId.value || undefined }
? { topicId: selectedTemplateId.value || undefined }
: tab === 'tierlists' && tierlistsMode.value === 'all'
? { mode: 'all' }
: {}
@@ -758,10 +758,10 @@ function setTierlistsMode(mode) {
function openTemplateCreateModal() {
resetMessages()
if (activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetGameId) {
newTemplateId.value = activeTemplateRequest.value?.draftGameId || ''
newTemplateName.value = activeTemplateRequest.value?.draftGameName || ''
newTemplateIsPublic.value = !!activeTemplateRequest.value?.draftGameIsPublic
if (activeTemplateRequest.value?.type === 'create' && !activeTemplateRequest.value?.targetTopicId) {
newTemplateId.value = activeTemplateRequest.value?.draftTopicId || ''
newTemplateName.value = activeTemplateRequest.value?.draftTopicName || ''
newTemplateIsPublic.value = !!activeTemplateRequest.value?.draftTopicIsPublic
} else {
newTemplateId.value = ''
newTemplateName.value = ''
@@ -822,7 +822,7 @@ async function refreshAdminTierLists() {
try {
const data = await api.listAdminTierLists({
q: adminTierListQuery.value,
gameId: adminTierListGameId.value,
topicId: adminTierListGameId.value,
page: adminTierListPage.value,
limit: adminTierListLimit.value,
})
@@ -839,7 +839,7 @@ async function refreshAdminTierLists() {
async function refreshAdminTierListStats() {
if (!auth.user?.isAdmin) return
try {
const data = await api.getAdminTierListStats({ q: adminTierListQuery.value, gameId: adminTierListGameId.value })
const data = await api.getAdminTierListStats({ q: adminTierListQuery.value, topicId: adminTierListGameId.value })
adminTierListStats.value = {
total: data.total || 0,
publicCount: data.publicCount || 0,
@@ -857,7 +857,7 @@ async function refreshSelectedTemplateTierListStats(templateId = '') {
}
try {
const data = await api.getAdminTierListStats({ gameId: templateId })
const data = await api.getAdminTierListStats({ topicId: templateId })
selectedTemplateTierListStats.value = {
total: data.total || 0,
publicCount: data.publicCount || 0,
@@ -874,15 +874,15 @@ async function refreshTemplateRequests() {
const data = await api.listAdminTemplateRequests()
templateRequests.value = (data.requests || []).map((request) => ({
...request,
draftGameId:
draftTopicId:
request.type === 'create'
? ('tmpl-' + String(request.id || 'request').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 12).toLowerCase())
: request.targetGameId || request.sourceGameId || '',
draftGameName:
: request.targetTopicId || request.sourceTopicId || '',
draftTopicName:
request.type === 'create'
? `${request.sourceTierListTitle || request.sourceGameName || '새 템플릿'}`
: request.targetGameName || request.sourceGameName || '',
draftGameIsPublic: false,
? `${request.sourceTierListTitle || request.sourceTopicName || '새 템플릿'}`
: request.targetTopicName || request.sourceTopicName || '',
draftTopicIsPublic: false,
}))
} catch (e) {
error.value = '템플릿 요청 목록을 불러오지 못했어요.'
@@ -1306,8 +1306,8 @@ function submitAdminTierListSearch() {
refreshAdminTierLists()
}
function setAdminTierListGameId(gameId) {
adminTierListGameId.value = gameId || ''
function setAdminTierListGameId(topicId) {
adminTierListGameId.value = topicId || ''
adminTierListPage.value = 1
refreshAdminTierLists()
}
@@ -1551,8 +1551,8 @@ function closePreviewModal() {
}
function previewTierListUrl(tierList) {
if (!tierList?.gameId || !tierList?.id) return ''
return editorPath(tierList.gameId, tierList.id, { preview: true })
if (!tierList?.topicId || !tierList?.id) return ''
return editorPath(tierList.topicId, tierList.id, { preview: true })
}
function openTierListImportModal(tierList, items) {
@@ -1567,9 +1567,9 @@ function openTierListImportModal(tierList, items) {
importModalItems.value = nextItems
importModalMode.value = 'existing'
importModalTargetTemplateId.value = ''
importModalNewTemplateId.value = tierList.gameId === 'freeform' ? '' : `${tierList.gameId}-copy`
importModalNewTemplateId.value = tierList.topicId === 'freeform' ? '' : `${tierList.topicId}-copy`
importModalNewTemplateName.value =
tierList.gameId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.gameName || tierList.gameId} 파생 템플릿`
tierList.topicId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.topicName || tierList.topicId} 파생 템플릿`
importModalOpen.value = true
}
@@ -1597,26 +1597,26 @@ async function confirmTierListImport() {
}
const data = await api.promoteAdminTierListItems(tierList.id, {
gameId: importModalTargetTemplateId.value,
topicId: importModalTargetTemplateId.value,
itemIds,
})
if (selectedTemplateId.value === importModalTargetTemplateId.value) await loadTemplate()
success.value = `${data.items?.length || 0}개의 아이템을 기존 템플릿에 추가했어요.`
} else {
const nextGameId = (importModalNewTemplateId.value || '').trim()
const nextGameName = (importModalNewTemplateName.value || '').trim()
if (!nextGameId || !nextGameName) {
const nextTopicId = (importModalNewTemplateId.value || '').trim()
const nextTopicName = (importModalNewTemplateName.value || '').trim()
if (!nextTopicId || !nextTopicName) {
error.value = '새 템플릿 ID와 이름을 모두 입력해주세요.'
return
}
const data = await api.createAdminTemplateFromTierList(tierList.id, {
gameId: nextGameId,
name: nextGameName,
topicId: nextTopicId,
name: nextTopicName,
itemIds,
})
await refreshTemplates()
success.value = `"${data.template?.name || nextGameName}" 템플릿을 생성했어요.`
success.value = `"${data.template?.name || nextTopicName}" 템플릿을 생성했어요.`
}
closeTierListImportModal()
@@ -1631,12 +1631,12 @@ function templateRequestTypeLabel(request) {
function templateRequestTargetLabel(request) {
if (request.type === 'create') {
if (request.targetGameName || request.targetGameId) {
return `연결된 템플릿 · ${request.targetGameName || request.targetGameId}`
if (request.targetTopicName || request.targetTopicId) {
return `연결된 템플릿 · ${request.targetTopicName || request.targetTopicId}`
}
return '연결된 템플릿 없음'
}
return request.targetGameName || request.targetGameId || request.sourceGameName
return request.targetTopicName || request.targetTopicId || request.sourceTopicName
}
const displayThumbnailUrl = computed(() => {
@@ -2095,7 +2095,7 @@ function userAvatarFallback(user) {
<div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__title">티어표 관리</div>
<div class="modalCard__desc">
{{ modalTargetAdminTierList ? `${modalTargetAdminTierList.gameName || modalTargetAdminTierList.gameId} · ${tierListAuthorDisplayName(modalTargetAdminTierList)}` : '' }}
{{ modalTargetAdminTierList ? `${modalTargetAdminTierList.topicName || modalTargetAdminTierList.topicId} · ${tierListAuthorDisplayName(modalTargetAdminTierList)}` : '' }}
</div>
<div class="modalCard__form">
<label class="field">

View File

@@ -48,7 +48,7 @@ async function loadFavorites() {
}
function openTierList(tierList) {
router.push(editorPath(tierList.topicId || tierList.gameId, tierList.id))
router.push(editorPath(tierList.topicId, tierList.id))
}
onMounted(loadFavorites)

View File

@@ -10,7 +10,7 @@ const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const topicId = computed(() => route.params.topicId || route.params.gameId)
const topicId = computed(() => route.params.topicId)
const topicName = ref('')
const tierLists = ref([])
const error = ref('')

View File

@@ -60,7 +60,7 @@ onMounted(async () => {
})
function openList(t) {
router.push(editorPath(t.topicId || t.gameId, t.id))
router.push(editorPath(t.topicId, t.id))
}
</script>

View File

@@ -38,7 +38,7 @@ function tierListThumbnailUrl(tierList) {
}
function openTierList(tierList) {
router.push(editorPath(tierList.topicId || tierList.gameId, tierList.id))
router.push(editorPath(tierList.topicId, tierList.id))
}
async function loadResults() {

View File

@@ -20,7 +20,7 @@ const auth = useAuthStore()
const toast = useToast()
const globalRightRailOpen = inject('rightRailOpen', ref(true))
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
const templateId = computed(() => route.params.topicId || route.params.gameId)
const templateId = computed(() => route.params.topicId)
const tierListId = computed(() => route.params.tierListId)
const previewMode = computed(() => route.query.preview === '1')
const templateName = ref('')
@@ -673,7 +673,7 @@ function buildPayload(existingId) {
const finalTitle = effectiveTitle.value
return {
id: existingId || undefined,
gameId: templateId.value,
topicId: templateId.value,
title: finalTitle,
thumbnailSrc: thumbnailSrc.value || '',
description: (description.value || '').trim(),
@@ -842,7 +842,7 @@ async function requestTemplate(type) {
await api.requestTierListTemplate({
type,
sourceTierListId: sourceId,
gameId: templateId.value,
topicId: templateId.value,
requestTitle: templateRequestDraftTitle.value.trim(),
requestDescription: templateRequestDraftDescription.value.trim(),
thumbnailSrc: saved.tierList?.thumbnailSrc || thumbnailSrc.value || '',