템플릿 slug 구조와 빈 DB 초기화를 정리
This commit is contained in:
@@ -110,6 +110,9 @@ const success = ref('')
|
||||
const newTemplateId = ref('')
|
||||
const newTemplateName = ref('')
|
||||
const newTemplateIsPublic = ref(false)
|
||||
const templateMetaDraftName = ref('')
|
||||
const templateMetaDraftSlug = ref('')
|
||||
const templateMetaSaving = ref(false)
|
||||
const templateVisibilitySaving = ref(false)
|
||||
|
||||
const uploadFiles = ref([])
|
||||
@@ -177,6 +180,17 @@ function normalizeAdminSrc(src) {
|
||||
}
|
||||
|
||||
const hasSelectedTemplate = computed(() => !!selectedTemplate.value?.template?.id)
|
||||
const canSaveTemplateMeta = computed(() => {
|
||||
const template = selectedTemplate.value?.template
|
||||
if (!template?.id) return false
|
||||
const nextName = templateMetaDraftName.value.trim()
|
||||
const nextSlug = templateMetaDraftSlug.value.trim()
|
||||
return (
|
||||
!!nextName &&
|
||||
!!nextSlug &&
|
||||
(nextName !== (template.name || '') || nextSlug !== (template.slug || template.id || ''))
|
||||
)
|
||||
})
|
||||
const canApplyThumbnail = computed(() => !!thumbFile.value && !!selectedTemplateId.value)
|
||||
const canAddItem = computed(() => uploadItemDrafts.value.length > 0 && uploadItemDrafts.value.every((item) => !!item.label.trim()) && !!selectedTemplateId.value)
|
||||
const stagedRequestDraftCount = computed(() => uploadItemDrafts.value.filter((item) => item.kind === 'request').length)
|
||||
@@ -203,7 +217,7 @@ const filteredTemplatePickerTemplates = computed(() => {
|
||||
const query = templatePickerQuery.value.trim().toLowerCase()
|
||||
const list = templates.value.filter((template) => {
|
||||
if (!query) return true
|
||||
const haystack = `${template.name || ''} ${template.id || ''}`.toLowerCase()
|
||||
const haystack = `${template.name || ''} ${template.slug || ''} ${template.id || ''}`.toLowerCase()
|
||||
return haystack.includes(query)
|
||||
})
|
||||
|
||||
@@ -462,6 +476,8 @@ watch(
|
||||
watch(
|
||||
() => selectedTemplate.value?.template?.id || '',
|
||||
async (templateId) => {
|
||||
templateMetaDraftName.value = selectedTemplate.value?.template?.name || ''
|
||||
templateMetaDraftSlug.value = selectedTemplate.value?.template?.slug || selectedTemplate.value?.template?.id || ''
|
||||
await refreshSelectedTemplateTierListStats(templateId)
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -893,7 +909,7 @@ async function refreshTemplateRequests() {
|
||||
draftTopicId:
|
||||
request.type === 'create'
|
||||
? ('tmpl-' + String(request.id || 'request').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 12).toLowerCase())
|
||||
: request.targetTopicId || request.sourceTopicId || '',
|
||||
: request.targetTopicSlug || request.sourceTopicSlug || request.targetTopicId || request.sourceTopicId || '',
|
||||
draftTopicName:
|
||||
request.type === 'create'
|
||||
? `${request.sourceTierListTitle || request.sourceTopicName || '새 템플릿'}`
|
||||
@@ -1212,6 +1228,44 @@ async function saveTemplateVisibility() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplateMeta() {
|
||||
if (!selectedTemplate.value?.template?.id || templateMetaSaving.value || !canSaveTemplateMeta.value) return
|
||||
|
||||
try {
|
||||
templateMetaSaving.value = true
|
||||
const data = await api.updateAdminTemplate(selectedTemplate.value.template.id, {
|
||||
name: templateMetaDraftName.value.trim(),
|
||||
slug: templateMetaDraftSlug.value.trim().toLowerCase(),
|
||||
isPublic: !!selectedTemplate.value.template.isPublic,
|
||||
})
|
||||
const nextTemplate = data.template || {}
|
||||
selectedTemplate.value = {
|
||||
...selectedTemplate.value,
|
||||
template: {
|
||||
...selectedTemplate.value.template,
|
||||
...nextTemplate,
|
||||
},
|
||||
}
|
||||
templateMetaDraftName.value = nextTemplate.name || selectedTemplate.value.template.name || ''
|
||||
templateMetaDraftSlug.value = nextTemplate.slug || selectedTemplate.value.template.slug || selectedTemplate.value.template.id || ''
|
||||
await refreshTemplates()
|
||||
success.value = '템플릿 이름과 slug를 저장했어요.'
|
||||
} catch (e) {
|
||||
const errorCode = e?.data?.error || ''
|
||||
if (errorCode === 'topic_slug_taken') {
|
||||
error.value = '이미 사용 중인 템플릿 slug입니다.'
|
||||
return
|
||||
}
|
||||
if (errorCode === 'topic_slug_invalid') {
|
||||
error.value = 'slug는 영문 소문자, 숫자, 하이픈만 사용할 수 있어요.'
|
||||
return
|
||||
}
|
||||
error.value = '템플릿 이름/slug를 저장하지 못했어요.'
|
||||
} finally {
|
||||
templateMetaSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleSelectedTemplateVisibility(nextValue) {
|
||||
if (!selectedTemplate.value?.template?.id || templateVisibilitySaving.value) return
|
||||
const previous = !!selectedTemplate.value.template.isPublic
|
||||
@@ -1603,8 +1657,9 @@ function closePreviewModal() {
|
||||
}
|
||||
|
||||
function previewTierListUrl(tierList) {
|
||||
if (!tierList?.topicId || !tierList?.id) return ''
|
||||
return editorPath(tierList.topicId, tierList.id, { preview: true })
|
||||
const topicRef = tierList?.topicSlug || tierList?.topicId || ''
|
||||
if (!topicRef || !tierList?.id) return ''
|
||||
return editorPath(topicRef, tierList.id, { preview: true })
|
||||
}
|
||||
|
||||
function openTierListImportModal(tierList, items) {
|
||||
@@ -1619,9 +1674,10 @@ function openTierListImportModal(tierList, items) {
|
||||
importModalItems.value = nextItems
|
||||
importModalMode.value = 'existing'
|
||||
importModalTargetTemplateId.value = ''
|
||||
importModalNewTemplateId.value = tierList.topicId === 'freeform' ? '' : `${tierList.topicId}-copy`
|
||||
const baseSlug = tierList.topicSlug || tierList.topicId || ''
|
||||
importModalNewTemplateId.value = baseSlug === 'freeform' ? '' : `${baseSlug}-copy`
|
||||
importModalNewTemplateName.value =
|
||||
tierList.topicId === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.topicName || tierList.topicId} 파생 템플릿`
|
||||
baseSlug === 'freeform' ? `${tierList.title} 템플릿` : `${tierList.topicName || baseSlug} 파생 템플릿`
|
||||
importModalOpen.value = true
|
||||
}
|
||||
|
||||
@@ -1655,15 +1711,15 @@ async function confirmTierListImport() {
|
||||
if (selectedTemplateId.value === importModalTargetTemplateId.value) await loadTemplate()
|
||||
success.value = `${data.items?.length || 0}개의 아이템을 기존 템플릿에 추가했어요.`
|
||||
} else {
|
||||
const nextTopicId = (importModalNewTemplateId.value || '').trim()
|
||||
const nextTopicId = (importModalNewTemplateId.value || '').trim().toLowerCase()
|
||||
const nextTopicName = (importModalNewTemplateName.value || '').trim()
|
||||
if (!nextTopicId || !nextTopicName) {
|
||||
error.value = '새 템플릿 ID와 이름을 모두 입력해주세요.'
|
||||
error.value = '새 템플릿 slug와 이름을 모두 입력해주세요.'
|
||||
return
|
||||
}
|
||||
|
||||
const data = await api.createAdminTemplateFromTierList(tierList.id, {
|
||||
topicId: nextTopicId,
|
||||
slug: nextTopicId,
|
||||
name: nextTopicName,
|
||||
itemIds,
|
||||
})
|
||||
@@ -1684,11 +1740,11 @@ function templateRequestTypeLabel(request) {
|
||||
function templateRequestTargetLabel(request) {
|
||||
if (request.type === 'create') {
|
||||
if (request.targetTopicName || request.targetTopicId) {
|
||||
return `연결된 템플릿 · ${request.targetTopicName || request.targetTopicId}`
|
||||
return `연결된 템플릿 · ${request.targetTopicName || request.targetTopicSlug || request.targetTopicId}`
|
||||
}
|
||||
return '연결된 템플릿 없음'
|
||||
}
|
||||
return request.targetTopicName || request.targetTopicId || request.sourceTopicName
|
||||
return request.targetTopicName || request.targetTopicSlug || request.targetTopicId || request.sourceTopicName
|
||||
}
|
||||
|
||||
const displayThumbnailUrl = computed(() => {
|
||||
@@ -1768,6 +1824,11 @@ function openUserProfile(user) {
|
||||
:has-selected-template="hasSelectedTemplate"
|
||||
:selected-template="selectedTemplate"
|
||||
:display-thumbnail-url="displayThumbnailUrl"
|
||||
v-model:template-meta-draft-name="templateMetaDraftName"
|
||||
v-model:template-meta-draft-slug="templateMetaDraftSlug"
|
||||
:template-meta-saving="templateMetaSaving"
|
||||
:can-save-template-meta="canSaveTemplateMeta"
|
||||
:save-template-meta="saveTemplateMeta"
|
||||
:can-apply-thumbnail="canApplyThumbnail"
|
||||
:template-visibility-saving="templateVisibilitySaving"
|
||||
:thumb-file-input-ref="setThumbFileInputRef"
|
||||
@@ -1868,9 +1929,9 @@ function openUserProfile(user) {
|
||||
/>
|
||||
|
||||
<div v-if="templateCreateModalOpen" class="modalOverlay" @click.self="closeTemplateCreateModal">
|
||||
<div class="modalCard" role="dialog" aria-modal="true">
|
||||
<div class="modalCard__title">새 템플릿 만들기</div>
|
||||
<div class="modalCard__desc">템플릿 이름과 고유 ID를 입력한 뒤 생성하면 바로 아래 상세 관리 화면으로 이어집니다.</div>
|
||||
<div class="modalCard" role="dialog" aria-modal="true">
|
||||
<div class="modalCard__title">새 템플릿 만들기</div>
|
||||
<div class="modalCard__desc">템플릿 이름과 공개 주소용 slug를 입력하면, 내부 ID는 서버가 자동으로 생성합니다.</div>
|
||||
<div class="modalCard__form">
|
||||
<label class="field">
|
||||
<span class="field__label">템플릿 이름</span>
|
||||
@@ -1878,15 +1939,15 @@ function openUserProfile(user) {
|
||||
<span class="field__hint">{{ newTemplateName.length }}/60자</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field__label">템플릿 ID</span>
|
||||
<span class="field__label">템플릿 slug</span>
|
||||
<input
|
||||
v-model="newTemplateId"
|
||||
class="field__input"
|
||||
maxlength="120"
|
||||
placeholder="topic id (영문/숫자)"
|
||||
placeholder="예: idol-rhythm"
|
||||
@keydown.enter.prevent="createTemplate"
|
||||
/>
|
||||
<span class="field__hint">영문, 숫자, 하이픈 조합 권장 · {{ newTemplateId.length }}/120자</span>
|
||||
<span class="field__hint">영문 소문자, 숫자, 하이픈만 사용 · {{ newTemplateId.length }}/120자</span>
|
||||
</label>
|
||||
<label class="toggleSwitch">
|
||||
<input v-model="newTemplateIsPublic" type="checkbox" />
|
||||
@@ -1988,7 +2049,7 @@ function openUserProfile(user) {
|
||||
</div>
|
||||
|
||||
<div v-else class="modalCard__form">
|
||||
<input v-model="importModalNewTemplateId" class="input" placeholder="새 템플릿 ID" />
|
||||
<input v-model="importModalNewTemplateId" class="input" placeholder="새 템플릿 slug" />
|
||||
<input v-model="importModalNewTemplateName" class="input" placeholder="새 템플릿 이름" />
|
||||
</div>
|
||||
|
||||
@@ -2012,7 +2073,7 @@ function openUserProfile(user) {
|
||||
<div class="adminSelectionCard">
|
||||
<div class="adminSelectionCard__label">선택한 템플릿</div>
|
||||
<div class="adminSelectionCard__title">{{ customItemTargetTemplate?.name || '아직 선택하지 않음' }}</div>
|
||||
<div class="adminSelectionCard__meta">{{ customItemTargetTemplate?.id || '템플릿을 골라 주세요.' }}</div>
|
||||
<div class="adminSelectionCard__meta">{{ customItemTargetTemplate?.slug || customItemTargetTemplate?.id || '템플릿을 골라 주세요.' }}</div>
|
||||
</div>
|
||||
<div class="customItemModal__pickerActions">
|
||||
<button class="btn btn--ghost" type="button" @click="openTemplatePickerModal('custom-item-target')">템플릿 선택</button>
|
||||
@@ -2078,7 +2139,7 @@ function openUserProfile(user) {
|
||||
<button class="btn btn--ghost btn--small" @click="closeTemplatePickerModal">닫기</button>
|
||||
</div>
|
||||
<div class="modalCard__form">
|
||||
<input v-model="templatePickerQuery" class="input" placeholder="템플릿 이름 또는 ID 검색" />
|
||||
<input v-model="templatePickerQuery" class="input" placeholder="템플릿 이름 또는 slug 검색" />
|
||||
<select v-model="templatePickerSort" class="select">
|
||||
<option value="recent">최신순</option>
|
||||
<option value="oldest">오래된순</option>
|
||||
@@ -2110,7 +2171,7 @@ function openUserProfile(user) {
|
||||
@click="chooseTemplateFromPicker(template.id)"
|
||||
>
|
||||
<span class="adminTemplatePicker__name">{{ template.name }}</span>
|
||||
<span class="adminTemplatePicker__meta">{{ template.id }}</span>
|
||||
<span class="adminTemplatePicker__meta">{{ template.slug || template.id }}</span>
|
||||
<span v-if="templatePickerMode === 'custom-item-target' && linkedCustomItemTemplateIds.has(template.id)" class="adminTemplatePicker__state">이미 추가됨</span>
|
||||
</button>
|
||||
<div v-if="!filteredTemplatePickerTemplates.length" class="hint hint--tight">검색 결과가 없어요.</div>
|
||||
@@ -2276,7 +2337,7 @@ function openUserProfile(user) {
|
||||
<div v-if="selectedTemplate?.template" class="adminSelectionCard">
|
||||
<div class="adminSelectionCard__label">선택한 템플릿</div>
|
||||
<div class="adminSelectionCard__title">{{ selectedTemplate.template.name }}</div>
|
||||
<div class="adminSelectionCard__meta">{{ selectedTemplate.template.id }}</div>
|
||||
<div class="adminSelectionCard__meta">{{ selectedTemplate.template.slug || selectedTemplate.template.id }}</div>
|
||||
</div>
|
||||
<div v-if="selectedTemplateId && !hasSelectedTemplate && !isTemplateLoading" class="hint hint--tight">선택된 템플릿 ID: {{ selectedTemplateId }}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user