Compare commits

...

1 Commits

Author SHA1 Message Date
bdc7ee42e2 admin: polish template item actions 2026-04-06 12:26:22 +09:00
4 changed files with 67 additions and 13 deletions

View File

@@ -1,5 +1,9 @@
# 의사결정 이력
## 2026-04-06 v1.4.90
- `templateSettingsCard__actions`는 카드 안에서 버튼이 줄바꿈될 수 있어야 하지만, 공통 버튼 스타일의 높이 100% 규칙까지 그대로 받으면 줄바꿈된 행이 비정상적으로 늘어날 수 있으므로 이 영역의 버튼만 높이를 자동으로 되돌리는 편이 맞다고 정리했다.
- 또 템플릿 기본 아이템 삭제는 “기존 저장 티어표에는 영향 없음”이라는 정책 설명이 중요하므로, 브라우저 기본 확인창보다 관리자 공통 모달로 통일해 같은 톤과 문구 체계 안에서 보여주는 쪽이 더 낫다고 판단했다.
## 2026-04-06 v1.4.89
- 템플릿 화면에서 이름/slug 저장과 아이템 태그 일괄 추가는 성격이 다르므로, 기존처럼 하나의 `메타` 개념으로 묶기보다 `이름/주소 저장``공통 태그 추가`를 분리해 보여주는 편이 운영자가 이해하기 쉽다고 정리했다.
-`templateSettingsCard`는 버튼 문구가 비교적 길고 썸네일/폼/토글이 함께 들어가는 카드라서, 좁은 폭에서 각 블록의 최소 너비를 풀어 주지 않으면 카드 밖으로 밀려나기 쉬우므로 입력 필드와 액션 버튼 모두 카드 내부에서 줄어들고 줄바꿈되게 하는 쪽이 맞다고 판단했다.

View File

@@ -1,5 +1,10 @@
# 업데이트 로그
## 2026-04-06 v1.4.90
- `templateSettingsCard__actions` 내부 버튼은 좁은 화면에서 과하게 세로로 늘어나지 않도록 버튼 높이를 자동으로 풀고, 카드 너비 안에서 자연스럽게 줄바꿈되도록 다시 보정했다.
- 템플릿 기본 아이템 삭제 확인은 브라우저 기본 `confirm` 대신 관리자 공통 모달로 바꿨다. 저장된 다른 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외된다는 안내도 모달 안에서 함께 보여준다.
- 확인: `npm run build`
## 2026-04-06 v1.4.89
- 템플릿 관리의 `템플릿 메타 저장` 버튼은 실제 역할에 맞춰 `이름/주소 저장`으로 바꿨다. 이제 이 버튼은 템플릿 이름과 slug 저장만 담당한다.
- 대신 현재 템플릿의 기본 아이템 전체에 같은 태그를 한 번에 추가하는 `기본 아이템 공통 태그` 기능을 추가했다. 배지형 입력으로 태그를 넣고 적용하면, 같은 태그는 중복 없이 각 아이템에 합쳐 저장된다.

View File

@@ -257,7 +257,7 @@ function setThumbFileElement(el) {
<div v-for="item in props.selectedTemplate.items" :key="item.id" class="thumbCard" :data-template-item-id="item.id">
<div class="thumbCard__media">
<img class="thumb thumb--template" :src="toApiUrl(item.src)" :alt="item.label" draggable="false" />
<button class="thumbCard__deleteBtn" type="button" data-no-drag @click="props.removeTemplateItem(item.id)">X</button>
<button class="thumbCard__deleteBtn" type="button" data-no-drag @click="props.removeTemplateItem(item)">X</button>
</div>
<input
v-model="item.draftLabel"

View File

@@ -92,6 +92,7 @@ const userDeleteModalOpen = ref(false)
const userRoleModalOpen = ref(false)
const customItemModalOpen = ref(false)
const customItemDeleteModalOpen = ref(false)
const templateItemDeleteModalOpen = ref(false)
const customItemModalHistoryActive = ref(false)
const modalTargetUser = ref(null)
const modalPasswordDraft = ref('')
@@ -100,6 +101,8 @@ const modalUserDraftEmail = ref('')
const modalUserDraftNickname = ref('')
const modalUserDraftIsAdmin = ref(false)
const modalTargetCustomItem = ref(null)
const modalTargetTemplateItem = ref(null)
const modalTargetTemplateItemUsage = ref({ totalCount: 0, publicCount: 0, privateCount: 0 })
const customItemModalDraftLabel = ref('')
const customItemModalDraftTags = ref([])
const customItemModalLabelSaving = ref(false)
@@ -373,6 +376,7 @@ const isAnyModalOpen = computed(
templatePickerModalOpen.value ||
customItemModalOpen.value ||
customItemDeleteModalOpen.value ||
templateItemDeleteModalOpen.value ||
adminTierListManageModalOpen.value ||
imageResetModalOpen.value ||
previewModalOpen.value
@@ -1420,12 +1424,26 @@ async function toggleSelectedTemplateVisibility(nextValue) {
}
}
async function removeTemplateItem(itemId) {
function closeTemplateItemDeleteModal() {
templateItemDeleteModalOpen.value = false
modalTargetTemplateItem.value = null
modalTargetTemplateItemUsage.value = { totalCount: 0, publicCount: 0, privateCount: 0 }
}
function templateItemDeleteImpactText() {
const usage = modalTargetTemplateItemUsage.value || { totalCount: 0, publicCount: 0, privateCount: 0 }
if (usage.totalCount) {
return `이 아이템은 이미 저장된 티어표 ${usage.totalCount}개(공개 ${usage.publicCount}개, 비공개 ${usage.privateCount}개)에서 사용 중이에요. 기존 저장 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외됩니다.`
}
return '이 기본 아이템을 삭제할까요? 기존 저장 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외됩니다.'
}
async function removeTemplateItem(item) {
resetMessages()
if (!selectedTemplateId.value) return
if (!selectedTemplateId.value || !item?.id) return
try {
const usageRes = await fetch(
toApiUrl(`/api/admin/templates/${encodeURIComponent(selectedTemplateId.value)}/items/${encodeURIComponent(itemId)}/usage`),
toApiUrl(`/api/admin/templates/${encodeURIComponent(selectedTemplateId.value)}/items/${encodeURIComponent(item.id)}/usage`),
{
credentials: 'include',
}
@@ -1433,16 +1451,22 @@ async function removeTemplateItem(itemId) {
if (!usageRes.ok) throw new Error('usage_failed')
const usageData = await usageRes.json()
const usage = usageData?.usage || { totalCount: 0, publicCount: 0, privateCount: 0 }
const impactMessage = usage.totalCount
? `이 아이템은 이미 저장된 티어표 ${usage.totalCount}개(공개 ${usage.publicCount}개, 비공개 ${usage.privateCount}개)에서 사용 중이에요.\n기존 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외됩니다.\n정말 삭제할까요?`
: '이 기본 아이템을 삭제할까요?\n기존 저장 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외됩니다.'
const ok = window.confirm(impactMessage)
if (!ok) return
modalTargetTemplateItem.value = item
modalTargetTemplateItemUsage.value = usageData?.usage || { totalCount: 0, publicCount: 0, privateCount: 0 }
templateItemDeleteModalOpen.value = true
} catch (e) {
error.value = '템플릿 기본 아이템 삭제에 실패했어요.'
}
}
async function confirmTemplateItemDelete() {
resetMessages()
if (!selectedTemplateId.value || !modalTargetTemplateItem.value?.id) return
try {
const previousScrollY = window.scrollY
const res = await fetch(
toApiUrl(`/api/admin/templates/${encodeURIComponent(selectedTemplateId.value)}/items/${encodeURIComponent(itemId)}`),
toApiUrl(`/api/admin/templates/${encodeURIComponent(selectedTemplateId.value)}/items/${encodeURIComponent(modalTargetTemplateItem.value.id)}`),
{
method: 'DELETE',
credentials: 'include',
@@ -1450,6 +1474,7 @@ async function removeTemplateItem(itemId) {
)
if (!res.ok) throw new Error('failed')
closeTemplateItemDeleteModal()
await loadTemplate()
await nextTick()
window.scrollTo({ top: previousScrollY, behavior: 'auto' })
@@ -2649,6 +2674,20 @@ function openUserProfile(user) {
</div>
</div>
<div v-if="templateItemDeleteModalOpen" class="modalOverlay" @click.self="closeTemplateItemDeleteModal">
<div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__title">기본 아이템 삭제</div>
<div class="modalCard__desc">
<strong>{{ modalTargetTemplateItem?.label || '선택한 아이템' }}</strong>
{{ ' ' }}{{ templateItemDeleteImpactText() }}
</div>
<div class="modalCard__actions">
<button class="btn btn--ghost" @click="closeTemplateItemDeleteModal">취소</button>
<button class="btn btn--danger" @click="confirmTemplateItemDelete">삭제</button>
</div>
</div>
</div>
<div v-if="adminTierListManageModalOpen" class="modalOverlay" @click.self="closeAdminTierListManageModal">
<div class="modalCard" role="dialog" aria-modal="true">
<div class="modalCard__title">티어표 관리</div>
@@ -3683,6 +3722,12 @@ function openUserProfile(user) {
}
.adminUiScope .templateSettingsCard__actions > .btn {
flex: 0 0 auto;
height: auto;
max-width: 100%;
white-space: normal;
}
.adminUiScope .templateSettingsCard__actions > a.btn {
height: auto;
max-width: 100%;
white-space: normal;
}
@@ -3958,8 +4003,8 @@ function openUserProfile(user) {
}
.adminUiScope .thumbCard__deleteBtn {
position: absolute;
top: 8px;
right: 8px;
top: -24px;
right: -24px;
width: 28px;
height: 28px;
display: grid;