Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdc7ee42e2 |
@@ -1,5 +1,9 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-06 v1.4.90
|
||||||
|
- `templateSettingsCard__actions`는 카드 안에서 버튼이 줄바꿈될 수 있어야 하지만, 공통 버튼 스타일의 높이 100% 규칙까지 그대로 받으면 줄바꿈된 행이 비정상적으로 늘어날 수 있으므로 이 영역의 버튼만 높이를 자동으로 되돌리는 편이 맞다고 정리했다.
|
||||||
|
- 또 템플릿 기본 아이템 삭제는 “기존 저장 티어표에는 영향 없음”이라는 정책 설명이 중요하므로, 브라우저 기본 확인창보다 관리자 공통 모달로 통일해 같은 톤과 문구 체계 안에서 보여주는 쪽이 더 낫다고 판단했다.
|
||||||
|
|
||||||
## 2026-04-06 v1.4.89
|
## 2026-04-06 v1.4.89
|
||||||
- 템플릿 화면에서 이름/slug 저장과 아이템 태그 일괄 추가는 성격이 다르므로, 기존처럼 하나의 `메타` 개념으로 묶기보다 `이름/주소 저장`과 `공통 태그 추가`를 분리해 보여주는 편이 운영자가 이해하기 쉽다고 정리했다.
|
- 템플릿 화면에서 이름/slug 저장과 아이템 태그 일괄 추가는 성격이 다르므로, 기존처럼 하나의 `메타` 개념으로 묶기보다 `이름/주소 저장`과 `공통 태그 추가`를 분리해 보여주는 편이 운영자가 이해하기 쉽다고 정리했다.
|
||||||
- 또 `templateSettingsCard`는 버튼 문구가 비교적 길고 썸네일/폼/토글이 함께 들어가는 카드라서, 좁은 폭에서 각 블록의 최소 너비를 풀어 주지 않으면 카드 밖으로 밀려나기 쉬우므로 입력 필드와 액션 버튼 모두 카드 내부에서 줄어들고 줄바꿈되게 하는 쪽이 맞다고 판단했다.
|
- 또 `templateSettingsCard`는 버튼 문구가 비교적 길고 썸네일/폼/토글이 함께 들어가는 카드라서, 좁은 폭에서 각 블록의 최소 너비를 풀어 주지 않으면 카드 밖으로 밀려나기 쉬우므로 입력 필드와 액션 버튼 모두 카드 내부에서 줄어들고 줄바꿈되게 하는 쪽이 맞다고 판단했다.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-06 v1.4.90
|
||||||
|
- `templateSettingsCard__actions` 내부 버튼은 좁은 화면에서 과하게 세로로 늘어나지 않도록 버튼 높이를 자동으로 풀고, 카드 너비 안에서 자연스럽게 줄바꿈되도록 다시 보정했다.
|
||||||
|
- 템플릿 기본 아이템 삭제 확인은 브라우저 기본 `confirm` 대신 관리자 공통 모달로 바꿨다. 저장된 다른 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외된다는 안내도 모달 안에서 함께 보여준다.
|
||||||
|
- 확인: `npm run build`
|
||||||
|
|
||||||
## 2026-04-06 v1.4.89
|
## 2026-04-06 v1.4.89
|
||||||
- 템플릿 관리의 `템플릿 메타 저장` 버튼은 실제 역할에 맞춰 `이름/주소 저장`으로 바꿨다. 이제 이 버튼은 템플릿 이름과 slug 저장만 담당한다.
|
- 템플릿 관리의 `템플릿 메타 저장` 버튼은 실제 역할에 맞춰 `이름/주소 저장`으로 바꿨다. 이제 이 버튼은 템플릿 이름과 slug 저장만 담당한다.
|
||||||
- 대신 현재 템플릿의 기본 아이템 전체에 같은 태그를 한 번에 추가하는 `기본 아이템 공통 태그` 기능을 추가했다. 배지형 입력으로 태그를 넣고 적용하면, 같은 태그는 중복 없이 각 아이템에 합쳐 저장된다.
|
- 대신 현재 템플릿의 기본 아이템 전체에 같은 태그를 한 번에 추가하는 `기본 아이템 공통 태그` 기능을 추가했다. 배지형 입력으로 태그를 넣고 적용하면, 같은 태그는 중복 없이 각 아이템에 합쳐 저장된다.
|
||||||
|
|||||||
@@ -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 v-for="item in props.selectedTemplate.items" :key="item.id" class="thumbCard" :data-template-item-id="item.id">
|
||||||
<div class="thumbCard__media">
|
<div class="thumbCard__media">
|
||||||
<img class="thumb thumb--template" :src="toApiUrl(item.src)" :alt="item.label" draggable="false" />
|
<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>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="item.draftLabel"
|
v-model="item.draftLabel"
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ const userDeleteModalOpen = ref(false)
|
|||||||
const userRoleModalOpen = ref(false)
|
const userRoleModalOpen = ref(false)
|
||||||
const customItemModalOpen = ref(false)
|
const customItemModalOpen = ref(false)
|
||||||
const customItemDeleteModalOpen = ref(false)
|
const customItemDeleteModalOpen = ref(false)
|
||||||
|
const templateItemDeleteModalOpen = ref(false)
|
||||||
const customItemModalHistoryActive = ref(false)
|
const customItemModalHistoryActive = ref(false)
|
||||||
const modalTargetUser = ref(null)
|
const modalTargetUser = ref(null)
|
||||||
const modalPasswordDraft = ref('')
|
const modalPasswordDraft = ref('')
|
||||||
@@ -100,6 +101,8 @@ const modalUserDraftEmail = ref('')
|
|||||||
const modalUserDraftNickname = ref('')
|
const modalUserDraftNickname = ref('')
|
||||||
const modalUserDraftIsAdmin = ref(false)
|
const modalUserDraftIsAdmin = ref(false)
|
||||||
const modalTargetCustomItem = ref(null)
|
const modalTargetCustomItem = ref(null)
|
||||||
|
const modalTargetTemplateItem = ref(null)
|
||||||
|
const modalTargetTemplateItemUsage = ref({ totalCount: 0, publicCount: 0, privateCount: 0 })
|
||||||
const customItemModalDraftLabel = ref('')
|
const customItemModalDraftLabel = ref('')
|
||||||
const customItemModalDraftTags = ref([])
|
const customItemModalDraftTags = ref([])
|
||||||
const customItemModalLabelSaving = ref(false)
|
const customItemModalLabelSaving = ref(false)
|
||||||
@@ -373,6 +376,7 @@ const isAnyModalOpen = computed(
|
|||||||
templatePickerModalOpen.value ||
|
templatePickerModalOpen.value ||
|
||||||
customItemModalOpen.value ||
|
customItemModalOpen.value ||
|
||||||
customItemDeleteModalOpen.value ||
|
customItemDeleteModalOpen.value ||
|
||||||
|
templateItemDeleteModalOpen.value ||
|
||||||
adminTierListManageModalOpen.value ||
|
adminTierListManageModalOpen.value ||
|
||||||
imageResetModalOpen.value ||
|
imageResetModalOpen.value ||
|
||||||
previewModalOpen.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()
|
resetMessages()
|
||||||
if (!selectedTemplateId.value) return
|
if (!selectedTemplateId.value || !item?.id) return
|
||||||
try {
|
try {
|
||||||
const usageRes = await fetch(
|
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',
|
credentials: 'include',
|
||||||
}
|
}
|
||||||
@@ -1433,16 +1451,22 @@ async function removeTemplateItem(itemId) {
|
|||||||
if (!usageRes.ok) throw new Error('usage_failed')
|
if (!usageRes.ok) throw new Error('usage_failed')
|
||||||
|
|
||||||
const usageData = await usageRes.json()
|
const usageData = await usageRes.json()
|
||||||
const usage = usageData?.usage || { totalCount: 0, publicCount: 0, privateCount: 0 }
|
modalTargetTemplateItem.value = item
|
||||||
const impactMessage = usage.totalCount
|
modalTargetTemplateItemUsage.value = usageData?.usage || { totalCount: 0, publicCount: 0, privateCount: 0 }
|
||||||
? `이 아이템은 이미 저장된 티어표 ${usage.totalCount}개(공개 ${usage.publicCount}개, 비공개 ${usage.privateCount}개)에서 사용 중이에요.\n기존 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외됩니다.\n정말 삭제할까요?`
|
templateItemDeleteModalOpen.value = true
|
||||||
: '이 기본 아이템을 삭제할까요?\n기존 저장 티어표에는 영향을 주지 않고, 이후 새로 만드는 티어표에서만 제외됩니다.'
|
} catch (e) {
|
||||||
const ok = window.confirm(impactMessage)
|
error.value = '템플릿 기본 아이템 삭제에 실패했어요.'
|
||||||
if (!ok) return
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmTemplateItemDelete() {
|
||||||
|
resetMessages()
|
||||||
|
if (!selectedTemplateId.value || !modalTargetTemplateItem.value?.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
const previousScrollY = window.scrollY
|
const previousScrollY = window.scrollY
|
||||||
const res = await fetch(
|
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',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@@ -1450,6 +1474,7 @@ async function removeTemplateItem(itemId) {
|
|||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('failed')
|
if (!res.ok) throw new Error('failed')
|
||||||
|
|
||||||
|
closeTemplateItemDeleteModal()
|
||||||
await loadTemplate()
|
await loadTemplate()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
window.scrollTo({ top: previousScrollY, behavior: 'auto' })
|
window.scrollTo({ top: previousScrollY, behavior: 'auto' })
|
||||||
@@ -2649,6 +2674,20 @@ function openUserProfile(user) {
|
|||||||
</div>
|
</div>
|
||||||
</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 v-if="adminTierListManageModalOpen" class="modalOverlay" @click.self="closeAdminTierListManageModal">
|
||||||
<div class="modalCard" role="dialog" aria-modal="true">
|
<div class="modalCard" role="dialog" aria-modal="true">
|
||||||
<div class="modalCard__title">티어표 관리</div>
|
<div class="modalCard__title">티어표 관리</div>
|
||||||
@@ -3683,6 +3722,12 @@ function openUserProfile(user) {
|
|||||||
}
|
}
|
||||||
.adminUiScope .templateSettingsCard__actions > .btn {
|
.adminUiScope .templateSettingsCard__actions > .btn {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.adminUiScope .templateSettingsCard__actions > a.btn {
|
||||||
|
height: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
@@ -3958,8 +4003,8 @@ function openUserProfile(user) {
|
|||||||
}
|
}
|
||||||
.adminUiScope .thumbCard__deleteBtn {
|
.adminUiScope .thumbCard__deleteBtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: -24px;
|
||||||
right: 8px;
|
right: -24px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user