feat: 관리자 수동 이미지 대체 기능 추가
This commit is contained in:
@@ -46,6 +46,11 @@ const customItemLimit = ref(50)
|
||||
const customItemTotal = ref(0)
|
||||
const customItemFilter = ref('library')
|
||||
const customItemModalTargetTemplateId = ref('')
|
||||
const customItemReplacementQuery = ref('')
|
||||
const customItemReplacementItems = ref([])
|
||||
const customItemReplacementLoading = ref(false)
|
||||
const customItemReplacementTargetId = ref('')
|
||||
const customItemReplacementBusy = ref(false)
|
||||
|
||||
const adminTierLists = ref([])
|
||||
const adminTierListQuery = ref('')
|
||||
@@ -227,6 +232,9 @@ const filteredTemplatePickerTemplates = computed(() => {
|
||||
})
|
||||
})
|
||||
const customItemTargetTemplate = computed(() => templates.value.find((template) => template.id === customItemModalTargetTemplateId.value) || null)
|
||||
const customItemReplacementTarget = computed(
|
||||
() => customItemReplacementItems.value.find((item) => item.id === customItemReplacementTargetId.value) || null
|
||||
)
|
||||
const importModalItemCount = computed(() => importModalItems.value.length)
|
||||
const activeTabTitle = computed(() => {
|
||||
if (activeTab.value === 'featured') return '목록 관리'
|
||||
@@ -647,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 replacementCandidateCount = computed(() => customItemReplacementItems.value.length)
|
||||
|
||||
const imageStatsPeriodLabel = computed(() => (imageStatsMonth.value ? `${imageStatsMonth.value} 기준` : '전체 기간'))
|
||||
const imageStatsYearOptions = computed(() => {
|
||||
@@ -1041,6 +1050,8 @@ const {
|
||||
removeUnusedCustomItems,
|
||||
saveCustomItemModalLabel,
|
||||
promoteCustomItem,
|
||||
refreshReplacementCandidates,
|
||||
replaceCustomItem,
|
||||
} = useAdminCustomItems({
|
||||
api,
|
||||
toast,
|
||||
@@ -1057,6 +1068,11 @@ const {
|
||||
customItemModalDraftLabel,
|
||||
customItemModalLabelSaving,
|
||||
customItemModalTargetTemplateId,
|
||||
customItemReplacementQuery,
|
||||
customItemReplacementItems,
|
||||
customItemReplacementLoading,
|
||||
customItemReplacementTargetId,
|
||||
customItemReplacementBusy,
|
||||
templates,
|
||||
selectedTemplateId,
|
||||
refreshCustomItems,
|
||||
@@ -2079,6 +2095,49 @@ function openUserProfile(user) {
|
||||
<button class="btn btn--ghost" type="button" @click="openTemplatePickerModal('custom-item-target')">템플릿 선택</button>
|
||||
<button class="btn btn--ghost btn--small customItemModal__createTemplateButton" type="button" @click="openTemplateCreateModal">새 템플릿 만들기</button>
|
||||
</div>
|
||||
<div class="customItemModal__pickerHead">
|
||||
<div class="customItemModal__pickerEyebrow">IMAGE REPLACEMENT</div>
|
||||
<div class="customItemModal__pickerTitle">대체할 이미지 선택</div>
|
||||
</div>
|
||||
<div class="adminSelectionCard">
|
||||
<div class="adminSelectionCard__label">선택한 대체 이미지</div>
|
||||
<div class="adminSelectionCard__title">{{ customItemReplacementTarget?.label || '아직 선택하지 않음' }}</div>
|
||||
<div class="adminSelectionCard__meta">{{ customItemReplacementTarget?.sourceLabel || '검색 후 대체할 이미지를 골라 주세요.' }}</div>
|
||||
</div>
|
||||
<div class="customItemModal__pickerActions">
|
||||
<input
|
||||
v-model="customItemReplacementQuery"
|
||||
class="input"
|
||||
type="text"
|
||||
maxlength="120"
|
||||
placeholder="대체할 이미지 이름 또는 파일명 검색"
|
||||
@keydown.enter.prevent="refreshReplacementCandidates"
|
||||
/>
|
||||
<button class="btn btn--ghost btn--small" type="button" @click="refreshReplacementCandidates">
|
||||
{{ customItemReplacementLoading ? '검색중...' : '대체 이미지 검색' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="customItemModal__replacementList">
|
||||
<button
|
||||
v-for="item in customItemReplacementItems"
|
||||
:key="item.id"
|
||||
class="adminTemplatePicker__item"
|
||||
:class="{ 'adminTemplatePicker__item--active': customItemReplacementTargetId === item.id }"
|
||||
type="button"
|
||||
@click="customItemReplacementTargetId = item.id"
|
||||
>
|
||||
<span class="customItemModal__replacementRow">
|
||||
<img class="customItemModal__replacementThumb" :src="toApiUrl(item.src)" :alt="item.label" />
|
||||
<span class="customItemModal__replacementCopy">
|
||||
<span class="adminTemplatePicker__name">{{ item.label }}</span>
|
||||
<span class="adminTemplatePicker__meta">{{ item.sourceLabel }} · {{ item.ownerName || '알 수 없음' }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<div v-if="!customItemReplacementLoading && !replacementCandidateCount" class="hint hint--tight">
|
||||
대체 후보가 없어요. 검색어를 바꾸거나 먼저 관리자 이미지를 등록해주세요.
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="customItemModal__body">
|
||||
<button class="customItemModal__close" type="button" @click="closeCustomItemModal">닫기</button>
|
||||
@@ -2119,6 +2178,9 @@ function openUserProfile(user) {
|
||||
<button class="btn btn--ghost customItemModal__action" :disabled="!customItemModalTargetTemplateId || modalTargetCustomItem.isPromoting" @click="promoteCustomItem(modalTargetCustomItem)">
|
||||
{{ modalTargetCustomItem.isPromoting ? '추가중...' : '기본 템플릿에 추가' }}
|
||||
</button>
|
||||
<button class="btn btn--primary customItemModal__action" :disabled="!customItemReplacementTargetId || customItemReplacementBusy" @click="replaceCustomItem(modalTargetCustomItem)">
|
||||
{{ customItemReplacementBusy ? '대체중...' : '선택한 이미지로 대체' }}
|
||||
</button>
|
||||
<button v-if="modalTargetCustomItem.canDelete" class="btn btn--danger customItemModal__action" :disabled="modalTargetCustomItem.sourceType === 'user' && (modalTargetCustomItem.usageCount > 0 || visibleLinkedTemplates.length > 0)" @click="openCustomItemDeleteModal(modalTargetCustomItem)">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3611,6 +3673,29 @@ function openUserProfile(user) {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.adminUiScope .customItemModal__replacementList {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.adminUiScope .customItemModal__replacementRow {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.adminUiScope .customItemModal__replacementThumb {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-surface-soft);
|
||||
}
|
||||
.adminUiScope .customItemModal__replacementCopy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
.adminUiScope .customItemModal__createTemplateButton {
|
||||
justify-self: start;
|
||||
}
|
||||
@@ -3736,7 +3821,7 @@ function openUserProfile(user) {
|
||||
}
|
||||
.adminUiScope .customItemModal__actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user