feat: 템플릿 태그와 병합 가져오기 및 에디터 제거 추가

This commit is contained in:
2026-04-06 11:48:22 +09:00
parent 2d5506e35a
commit fe79c91e82
9 changed files with 482 additions and 60 deletions

View File

@@ -14,6 +14,7 @@ export function useAdminCustomItems({
customItemModalHistoryActive,
modalTargetCustomItem,
customItemModalDraftLabel,
customItemModalDraftTags,
customItemModalLabelSaving,
customItemModalTargetTemplateId,
customItemReplacementQuery,
@@ -88,6 +89,7 @@ export function useAdminCustomItems({
function openCustomItemModal(item) {
modalTargetCustomItem.value = item || null
customItemModalDraftLabel.value = item?.label || ''
customItemModalDraftTags.value = Array.isArray(item?.tags) ? item.tags.join(', ') : ''
customItemModalTargetTemplateId.value = ''
customItemReplacementQuery.value = ''
customItemReplacementItems.value = []
@@ -102,6 +104,7 @@ export function useAdminCustomItems({
customItemDeleteModalOpen.value = false
modalTargetCustomItem.value = null
customItemModalDraftLabel.value = ''
customItemModalDraftTags.value = ''
customItemModalLabelSaving.value = false
customItemModalTargetTemplateId.value = ''
customItemReplacementQuery.value = ''
@@ -198,17 +201,34 @@ export function useAdminCustomItems({
async function saveCustomItemModalLabel() {
const item = modalTargetCustomItem.value
const nextLabel = customItemModalDraftLabel.value.trim().slice(0, 60)
if (!item || !nextLabel || nextLabel === item.label || customItemModalLabelSaving.value) return
const nextTags = Array.from(
new Set(
String(customItemModalDraftTags.value || '')
.split(',')
.map((tag) => tag.trim().replace(/^#/, '').slice(0, 40))
.filter(Boolean)
)
).slice(0, 30)
if (
!item ||
!nextLabel ||
(nextLabel === item.label && JSON.stringify(nextTags) === JSON.stringify(Array.isArray(item.tags) ? item.tags : [])) ||
customItemModalLabelSaving.value
) {
return
}
try {
customItemModalLabelSaving.value = true
const data = await api.updateAdminCustomItemLabel(item.id, { label: nextLabel, sourceType: item.sourceType })
const data = await api.updateAdminCustomItemLabel(item.id, { label: nextLabel, tags: nextTags, sourceType: item.sourceType })
item.label = data.item?.label || nextLabel
item.tags = Array.isArray(data.item?.tags) ? data.item.tags : nextTags
customItemModalDraftLabel.value = item.label
customItems.value = customItems.value.map((entry) => (entry.id === item.id ? { ...entry, label: item.label } : entry))
toast.success('아이템 이름을 변경했어요.')
customItemModalDraftTags.value = item.tags.join(', ')
customItems.value = customItems.value.map((entry) => (entry.id === item.id ? { ...entry, label: item.label, tags: item.tags } : entry))
toast.success('아이템 메타를 변경했어요.')
} catch (e) {
error.value = '아이템 이름 변경에 실패했어요.'
error.value = '아이템 메타 변경에 실패했어요.'
} finally {
customItemModalLabelSaving.value = false
}

View File

@@ -31,6 +31,17 @@ export function useAdminTemplateManager({
success,
error,
}) {
function parseTagsText(value) {
return Array.from(
new Set(
String(value || '')
.split(',')
.map((tag) => tag.trim().replace(/^#/, '').slice(0, 40))
.filter(Boolean)
)
).slice(0, 30)
}
function normalizeDraftSrc(src) {
if (typeof src !== 'string') return ''
const raw = src.trim()
@@ -108,6 +119,32 @@ export function useAdminTemplateManager({
}
}
function mergeLibraryItemsIntoDrafts(items, sourceLabel = '') {
const existingTemplateSrcs = new Set((selectedTemplate.value?.items || []).map((item) => normalizeDraftSrc(item?.src)).filter(Boolean))
const existingDraftSrcs = new Set(uploadItemDrafts.value.map((draft) => normalizeDraftSrc(draft?.src)).filter(Boolean))
const existingKeys = new Set(uploadItemDrafts.value.map((draft) => `${draft.kind}:${draft.itemSourceType || ''}:${draft.itemId || draft.file?.name || ''}`))
const nextDrafts = (items || [])
.filter((item) => item?.id && item?.src)
.map((item) => ({
kind: 'library',
itemId: item.id,
itemSourceType: item.sourceType || 'template',
previewUrl: toApiUrl(item.src),
label: item.label || '',
tagsText: Array.isArray(item.tags) ? item.tags.join(', ') : '',
sourceName: sourceLabel ? `${sourceLabel} · ${item.label || item.id}` : (item.label || item.id),
src: item.src,
}))
.filter((draft) => !existingTemplateSrcs.has(normalizeDraftSrc(draft.src)))
.filter((draft) => !existingDraftSrcs.has(normalizeDraftSrc(draft.src)))
.filter((draft) => !existingKeys.has(`${draft.kind}:${draft.itemSourceType}:${draft.itemId}`))
if (nextDrafts.length) {
uploadItemDrafts.value = [...uploadItemDrafts.value, ...nextDrafts]
}
return nextDrafts.length
}
function removeUploadDraft(targetDraft) {
const targetKey = `${targetDraft.kind}:${targetDraft.requestId || ''}:${targetDraft.itemId || targetDraft.file?.name || ''}:${targetDraft.previewUrl || ''}`
uploadItemDrafts.value = uploadItemDrafts.value.filter((draft) => {
@@ -139,6 +176,7 @@ export function useAdminTemplateManager({
items: (data.items || []).map((item) => ({
...item,
draftLabel: item.label,
draftTags: Array.isArray(item.tags) ? item.tags.join(', ') : '',
})),
}
savedTemplateItemOrderIds.value = (data.items || []).map((item) => item.id)
@@ -293,6 +331,7 @@ export function useAdminTemplateManager({
const fileDrafts = uploadItemDrafts.value.filter((entry) => entry.kind === 'file')
const requestDrafts = uploadItemDrafts.value.filter((entry) => entry.kind === 'request')
const libraryDrafts = uploadItemDrafts.value.filter((entry) => entry.kind === 'library')
const totalUploadBytes = fileDrafts.reduce((sum, entry) => sum + Number(entry.file?.size || 0), 0)
let uploadCount = 0
@@ -352,6 +391,28 @@ export function useAdminTemplateManager({
}
}
if (libraryDrafts.length) {
for (const draft of libraryDrafts) {
const promoted = await api.promoteAdminTemplateItem(draft.itemId, {
topicId: selectedTemplateId.value,
})
const createdItem = promoted?.item || null
if (createdItem?.id) {
const nextTags = parseTagsText(draft.tagsText)
const needsMetaUpdate =
(draft.label || '').trim() !== (createdItem.label || '').trim() ||
JSON.stringify(nextTags) !== JSON.stringify(Array.isArray(createdItem.tags) ? createdItem.tags : [])
if (needsMetaUpdate) {
await api.updateAdminTemplateItem(selectedTemplateId.value, createdItem.id, {
label: (draft.label || createdItem.label || '').trim(),
tags: nextTags,
})
}
uploadCount += 1
}
}
}
resetUploadState()
await loadTemplate()
success.value = `템플릿 기본 아이템 ${uploadCount}개 추가를 완료했어요.`
@@ -397,6 +458,7 @@ export function useAdminTemplateManager({
items: (data.items || []).map((item) => ({
...item,
draftLabel: item.label,
draftTags: Array.isArray(item.tags) ? item.tags.join(', ') : '',
})),
}
savedTemplateItemOrderIds.value = (data.items || []).map((item) => item.id)
@@ -412,6 +474,7 @@ export function useAdminTemplateManager({
destroyTemplateItemSortable,
syncTemplateItemSortable,
mergeRequestItemsIntoDrafts,
mergeLibraryItemsIntoDrafts,
removeUploadDraft,
loadTemplate,
createTemplate,