diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 085e8d4..79fbe17 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -822,6 +822,7 @@ router.post('/custom-items/:itemId/replace', requireAdmin, async (req, res) => { const sourceItem = await findLibraryItemForReplacement(req.params.itemId) if (!sourceItem?.src) return res.status(404).json({ error: 'source_not_found' }) + if (sourceItem.sourceType !== 'user') return res.status(400).json({ error: 'user_item_required' }) const targetItem = await findLibraryItemForReplacement(parsed.data.targetItemId, parsed.data.targetSourceType) if (!targetItem?.src) return res.status(404).json({ error: 'target_not_found' }) @@ -835,6 +836,10 @@ router.post('/custom-items/:itemId/replace', requireAdmin, async (req, res) => { toLabel: targetItem.label || '', }) + const sourceCustomItems = await findCustomItemsByIds([sourceItem.id]) + await deleteCustomItems([sourceItem.id]) + await removeCustomItemFiles(sourceCustomItems) + res.json({ ok: true, updatedRows: result.updatedRows || 0, diff --git a/docs/history.md b/docs/history.md index 2893e24..91b1c43 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-04-06 v1.4.79 +- 관리자 대체 모달은 열자마자 현재 라이브러리 결과를 그대로 쏟아내면 “같은 보드 안의 비슷한 항목을 고르는 화면”처럼 읽히기 쉬우므로, 검색 전에는 후보를 비워 두고 운영자가 의도적으로 찾은 뒤 고르는 방식이 더 분명하다고 판단했다. +- 또 사용자 업로드 A를 F로 대체했을 때 관리자 목록에 F가 두 장 보이면 “참조 이동”보다 “복제 생성”처럼 느껴지므로, 사용자 업로드 대체는 참조를 옮긴 뒤 원본 레코드 자체를 정리해 결과적으로 목표 이미지 한 장만 남는 쪽이 운영 기대와 더 잘 맞는다고 정리했다. + ## 2026-04-06 v1.4.78 - 사용자 업로드 이미지의 “같은 캐릭터인데 파일만 다른 경우”는 자동 판별하려 들수록 오탐 위험이 커지므로, 관리자 모달에서 대상 이미지를 직접 검색·선택하는 수동 치환 흐름으로 시작하는 편이 가장 안전하다고 판단했다. - 이때 `src`만 바꾸고 기존 라벨을 남기면 운영자가 통합한 뒤에도 표기가 제각각 남을 수 있으므로, 치환 대상의 `라벨`을 기준으로 사용자 업로드 행과 저장된 티어표/요청 스냅샷 내부 라벨까지 함께 맞춰 주는 편이 운영 목적에 더 부합한다고 정리했다. diff --git a/docs/update.md b/docs/update.md index 3b4dd9c..4ef8648 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 로그 +## 2026-04-06 v1.4.79 +- 관리자 이미지 대체 모달은 처음 열었을 때 기존 목록을 자동으로 보여주지 않고, 검색을 실행한 뒤에만 대체 후보를 표시하도록 바꿨다. 같은 티어표/같은 문맥의 항목이 처음부터 섞여 보여 혼란스럽던 점을 줄이기 위한 조정이다. +- 사용자 업로드 아이템을 다른 이미지로 대체할 때는 이제 원본 항목을 그대로 `대상 이미지와 라벨`로 덮어써 중복 항목을 남기지 않고, 참조를 옮긴 뒤 원본 사용자 아이템 레코드와 파일을 함께 정리해 관리자 라이브러리에 동일 이미지가 두 번 보이지 않도록 맞췄다. +- 이미지 대체 기능은 현재 사용자 업로드 아이템에만 노출되도록 제한해, 템플릿 기본 아이템이나 보관 자산까지 같은 방식으로 다뤄 생길 수 있는 오해를 줄였다. +- 확인: `node --check backend/src/routes/admin.js`, `npm run build` + ## 2026-04-06 v1.4.78 - 관리자 아이템 관리 모달에 `선택한 이미지로 대체` 기능을 추가했다. 운영자는 대체할 원본 아이템을 연 뒤, 모달 안에서 다른 라이브러리 이미지를 검색·선택해 수동으로 치환할 수 있다. - 이 치환은 단순히 `src`만 바꾸는 것이 아니라, 선택한 대상 이미지의 `라벨`도 함께 따라가도록 처리해 사용자 업로드 아이템과 티어표 저장 JSON 안의 표기가 같은 이름으로 정리되게 맞췄다. diff --git a/frontend/src/composables/useAdminCustomItems.js b/frontend/src/composables/useAdminCustomItems.js index caf70aa..67815fc 100644 --- a/frontend/src/composables/useAdminCustomItems.js +++ b/frontend/src/composables/useAdminCustomItems.js @@ -89,13 +89,12 @@ export function useAdminCustomItems({ modalTargetCustomItem.value = item || null customItemModalDraftLabel.value = item?.label || '' customItemModalTargetTemplateId.value = '' - customItemReplacementQuery.value = item?.label || '' + customItemReplacementQuery.value = '' customItemReplacementItems.value = [] customItemReplacementTargetId.value = '' customItemReplacementBusy.value = false customItemModalOpen.value = true pushCustomItemModalHistoryState() - void refreshReplacementCandidates() } function closeCustomItemModal({ fromPopState = false } = {}) { diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 0d6390b..1ceb722 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -655,6 +655,7 @@ const visibleLinkedTemplates = computed(() => (modalTargetCustomItem.value?.linkedTemplates || []).filter((template) => template?.id && template.id !== 'freeform') ) const linkedCustomItemTemplateIds = computed(() => new Set(visibleLinkedTemplates.value.map((template) => template.id).filter(Boolean))) +const canReplaceModalTarget = computed(() => modalTargetCustomItem.value?.sourceType === 'user') const replacementCandidateCount = computed(() => customItemReplacementItems.value.length) const imageStatsPeriodLabel = computed(() => (imageStatsMonth.value ? `${imageStatsMonth.value} 기준` : '전체 기간')) @@ -2095,48 +2096,53 @@ function openUserProfile(user) { -
-
IMAGE REPLACEMENT
-
대체할 이미지 선택
-
-
-
선택한 대체 이미지
-
{{ customItemReplacementTarget?.label || '아직 선택하지 않음' }}
-
{{ customItemReplacementTarget?.sourceLabel || '검색 후 대체할 이미지를 골라 주세요.' }}
-
-
- - -
-
- -
- 대체 후보가 없어요. 검색어를 바꾸거나 먼저 관리자 이미지를 등록해주세요. + +
+ 이미지 대체는 현재 사용자 업로드 아이템에서만 지원합니다.
@@ -2178,7 +2184,7 @@ function openUserProfile(user) { -