Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cdd627658 |
@@ -184,6 +184,8 @@ router.post('/thumbnail', requireAuth, thumbnailUpload.single('thumbnail'), asyn
|
|||||||
router.post('/:id/template-request', requireAuth, async (req, res) => {
|
router.post('/:id/template-request', requireAuth, async (req, res) => {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
type: z.enum(['create', 'update']),
|
type: z.enum(['create', 'update']),
|
||||||
|
requestTitle: z.string().trim().min(1).max(80),
|
||||||
|
requestDescription: z.string().trim().min(1).max(240),
|
||||||
})
|
})
|
||||||
const parsed = schema.safeParse(req.body)
|
const parsed = schema.safeParse(req.body)
|
||||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
@@ -197,9 +199,6 @@ router.post('/:id/template-request', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
if (parsed.data.type === 'create') {
|
if (parsed.data.type === 'create') {
|
||||||
if (tierList.gameId !== FREEFORM_GAME_ID) return res.status(400).json({ error: 'freeform_required' })
|
if (tierList.gameId !== FREEFORM_GAME_ID) return res.status(400).json({ error: 'freeform_required' })
|
||||||
if (!(tierList.title || '').trim() || (tierList.title || '').trim() === FREEFORM_DEFAULT_TITLE) {
|
|
||||||
return res.status(400).json({ error: 'title_required' })
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (tierList.gameId === FREEFORM_GAME_ID) return res.status(400).json({ error: 'game_template_required' })
|
if (tierList.gameId === FREEFORM_GAME_ID) return res.status(400).json({ error: 'game_template_required' })
|
||||||
}
|
}
|
||||||
@@ -212,8 +211,8 @@ router.post('/:id/template-request', requireAuth, async (req, res) => {
|
|||||||
sourceTierListId: tierList.id,
|
sourceTierListId: tierList.id,
|
||||||
sourceGameId: tierList.gameId,
|
sourceGameId: tierList.gameId,
|
||||||
targetGameId: parsed.data.type === 'update' ? tierList.gameId : '',
|
targetGameId: parsed.data.type === 'update' ? tierList.gameId : '',
|
||||||
title: tierList.title,
|
title: parsed.data.requestTitle,
|
||||||
description: tierList.description || '',
|
description: parsed.data.requestDescription,
|
||||||
thumbnailSrc: tierList.thumbnailSrc || '',
|
thumbnailSrc: tierList.thumbnailSrc || '',
|
||||||
items: customItems,
|
items: customItems,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-03-31 v1.2.60
|
||||||
|
- 관리자 티어표 관리 카드에서 사용자가 입력한 설명을 제목 아래에 함께 노출해 요청 의도를 더 빨리 파악할 수 있게 함.
|
||||||
|
- 템플릿 등록/업데이트 요청은 이제 에디터 모달에서 제목과 설명을 별도로 입력받고, 예시 문구와 함께 전송하도록 정리함.
|
||||||
|
|
||||||
## 2026-03-31 v1.2.59
|
## 2026-03-31 v1.2.59
|
||||||
- 관리자 아이템 상세 모달의 게임 선택을 전용 상태로 분리해 기본 선택값이 비어 있도록 바꾸고, 썸네일 아래에 배치해 정보/액션과 시각적으로 분리함.
|
- 관리자 아이템 상세 모달의 게임 선택을 전용 상태로 분리해 기본 선택값이 비어 있도록 바꾸고, 썸네일 아래에 배치해 정보/액션과 시각적으로 분리함.
|
||||||
- 커스텀 아이템이 실제로 사용 중인 게임 목록을 백엔드에서 함께 내려주고, 템플릿 요청 생성 폼에는 게임 ID와 게임 이름 라벨을 추가해 구분을 명확히 함.
|
- 커스텀 아이템이 실제로 사용 중인 게임 목록을 백엔드에서 함께 내려주고, 템플릿 요청 생성 폼에는 게임 ID와 게임 이름 라벨을 추가해 구분을 명확히 함.
|
||||||
|
|||||||
@@ -1421,14 +1421,14 @@ async function saveFeaturedOrder() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="request.type === 'create'" class="templateRequestCard__form">
|
<div v-if="request.type === 'create'" class="templateRequestCard__form">
|
||||||
<label class="templateRequestField">
|
|
||||||
<span class="templateRequestField__label">게임 ID</span>
|
|
||||||
<input v-model="request.draftGameId" class="input" placeholder="새 게임 ID" />
|
|
||||||
</label>
|
|
||||||
<label class="templateRequestField">
|
<label class="templateRequestField">
|
||||||
<span class="templateRequestField__label">게임 이름</span>
|
<span class="templateRequestField__label">게임 이름</span>
|
||||||
<input v-model="request.draftGameName" class="input" placeholder="새 게임 이름" />
|
<input v-model="request.draftGameName" class="input" placeholder="새 게임 이름" />
|
||||||
</label>
|
</label>
|
||||||
|
<label class="templateRequestField">
|
||||||
|
<span class="templateRequestField__label">게임 ID</span>
|
||||||
|
<input v-model="request.draftGameId" class="input" placeholder="새 게임 ID" />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="templateRequestCard__actions">
|
<div class="templateRequestCard__actions">
|
||||||
@@ -1461,6 +1461,7 @@ async function saveFeaturedOrder() {
|
|||||||
<div class="tierAdminCard__head">
|
<div class="tierAdminCard__head">
|
||||||
<div>
|
<div>
|
||||||
<div class="tierAdminCard__title">{{ tierList.title }}</div>
|
<div class="tierAdminCard__title">{{ tierList.title }}</div>
|
||||||
|
<div v-if="tierList.description" class="tierAdminCard__desc">{{ tierList.description }}</div>
|
||||||
<div class="tierAdminCard__meta">
|
<div class="tierAdminCard__meta">
|
||||||
{{ tierList.gameName || tierList.gameId }} · {{ tierListAuthorDisplayName(tierList) }} · {{ tierListVisibilityLabel(tierList) }}
|
{{ tierList.gameName || tierList.gameId }} · {{ tierListAuthorDisplayName(tierList) }} · {{ tierListVisibilityLabel(tierList) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -2950,6 +2951,15 @@ async function saveFeaturedOrder() {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
.tierAdminCard__desc {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.74);
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.tierAdminCard__meta {
|
.tierAdminCard__meta {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
opacity: 0.74;
|
opacity: 0.74;
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const isExporting = ref(false)
|
|||||||
const isSaveModalOpen = ref(false)
|
const isSaveModalOpen = ref(false)
|
||||||
const isTemplateRequestModalOpen = ref(false)
|
const isTemplateRequestModalOpen = ref(false)
|
||||||
const isTemplateUpdateModalOpen = ref(false)
|
const isTemplateUpdateModalOpen = ref(false)
|
||||||
|
const templateRequestDraftTitle = ref('')
|
||||||
|
const templateRequestDraftDescription = ref('')
|
||||||
const isDeleteModalOpen = ref(false)
|
const isDeleteModalOpen = ref(false)
|
||||||
const ownerId = ref('')
|
const ownerId = ref('')
|
||||||
const authorName = ref('')
|
const authorName = ref('')
|
||||||
@@ -112,7 +114,8 @@ const templateRequestChecks = computed(() => [
|
|||||||
passed: !!(title.value || '').trim() && (title.value || '').trim() !== (gameName.value || '').trim(),
|
passed: !!(title.value || '').trim() && (title.value || '').trim() !== (gameName.value || '').trim(),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
const canSubmitTemplateCreateRequest = computed(() => templateRequestChecks.value.every((item) => item.passed))
|
const canSubmitTemplateCreateRequest = computed(() => templateRequestChecks.value.every((item) => item.passed) && !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
|
||||||
|
const canSubmitTemplateUpdateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
|
||||||
const templateRequestTargetLabel = computed(() => (gameId.value === 'freeform' ? '새로운 템플릿' : (gameName.value || gameId.value || '선택한 게임')))
|
const templateRequestTargetLabel = computed(() => (gameId.value === 'freeform' ? '새로운 템플릿' : (gameName.value || gameId.value || '선택한 게임')))
|
||||||
|
|
||||||
watch(error, (message) => {
|
watch(error, (message) => {
|
||||||
@@ -510,20 +513,29 @@ function closeSaveModal() {
|
|||||||
isSaveModalOpen.value = false
|
isSaveModalOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetTemplateRequestDrafts() {
|
||||||
|
templateRequestDraftTitle.value = ''
|
||||||
|
templateRequestDraftDescription.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
function openTemplateRequestModal() {
|
function openTemplateRequestModal() {
|
||||||
|
resetTemplateRequestDrafts()
|
||||||
isTemplateRequestModalOpen.value = true
|
isTemplateRequestModalOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTemplateRequestModal() {
|
function closeTemplateRequestModal() {
|
||||||
isTemplateRequestModalOpen.value = false
|
isTemplateRequestModalOpen.value = false
|
||||||
|
resetTemplateRequestDrafts()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTemplateUpdateModal() {
|
function openTemplateUpdateModal() {
|
||||||
|
resetTemplateRequestDrafts()
|
||||||
isTemplateUpdateModalOpen.value = true
|
isTemplateUpdateModalOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTemplateUpdateModal() {
|
function closeTemplateUpdateModal() {
|
||||||
isTemplateUpdateModalOpen.value = false
|
isTemplateUpdateModalOpen.value = false
|
||||||
|
resetTemplateRequestDrafts()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDeleteModal() {
|
function openDeleteModal() {
|
||||||
@@ -574,7 +586,11 @@ async function requestTemplate(type) {
|
|||||||
try {
|
try {
|
||||||
isRequestingTemplate.value = true
|
isRequestingTemplate.value = true
|
||||||
const persisted = await persistTierList({ showModal: false })
|
const persisted = await persistTierList({ showModal: false })
|
||||||
await api.requestTierListTemplate(persisted.savedTierListId, { type })
|
await api.requestTierListTemplate(persisted.savedTierListId, {
|
||||||
|
type,
|
||||||
|
requestTitle: templateRequestDraftTitle.value.trim(),
|
||||||
|
requestDescription: templateRequestDraftDescription.value.trim(),
|
||||||
|
})
|
||||||
if (type === 'create') closeTemplateRequestModal()
|
if (type === 'create') closeTemplateRequestModal()
|
||||||
if (type === 'update') closeTemplateUpdateModal()
|
if (type === 'update') closeTemplateUpdateModal()
|
||||||
toast.success(type === 'create' ? '템플릿 등록 요청을 보냈어요.' : '템플릿 업데이트 요청을 보냈어요.')
|
toast.success(type === 'create' ? '템플릿 등록 요청을 보냈어요.' : '템플릿 업데이트 요청을 보냈어요.')
|
||||||
@@ -722,7 +738,18 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="requestChecklist__hint">
|
<div class="requestChecklist__hint">
|
||||||
제목만 명확하게 적어두면 관리자가 어떤 게임 템플릿 요청인지 빠르게 파악할 수 있어요. 여러 사용자가 비슷한 주제로 요청할 수 있으니 게임 이름을 구체적으로 적어주세요.
|
제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 수 있어요.
|
||||||
|
예시: 제목 `템플릿 등록 요청`, 설명 `여름 이벤트 한정 캐릭터 중심으로 새 게임 템플릿이 필요합니다.`
|
||||||
|
</div>
|
||||||
|
<div class="templateRequestDraft">
|
||||||
|
<label class="templateRequestDraft__field">
|
||||||
|
<span class="templateRequestDraft__label">요청 제목</span>
|
||||||
|
<input v-model="templateRequestDraftTitle" class="input" maxlength="80" placeholder="예: 템플릿 등록 요청" />
|
||||||
|
</label>
|
||||||
|
<label class="templateRequestDraft__field">
|
||||||
|
<span class="templateRequestDraft__label">요청 설명</span>
|
||||||
|
<textarea v-model="templateRequestDraftDescription" class="textarea templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="modalCard__actions">
|
<div class="modalCard__actions">
|
||||||
<button class="btn btn--ghost" @click="closeTemplateRequestModal">취소</button>
|
<button class="btn btn--ghost" @click="closeTemplateRequestModal">취소</button>
|
||||||
@@ -741,10 +768,21 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modalCard__note">
|
<div class="modalCard__note">
|
||||||
모두가 사용하는 기본 템플릿이니 개인적인 항목이 아닌 공통된 항목만 추가한 뒤 신청해주세요.
|
모두가 사용하는 기본 템플릿이니 개인적인 항목이 아닌 공통된 항목만 추가한 뒤 신청해주세요.
|
||||||
|
예시: 제목 `템플릿 업데이트 요청`, 설명 `여름 이벤트 한정 캐릭터 추가`
|
||||||
|
</div>
|
||||||
|
<div class="templateRequestDraft">
|
||||||
|
<label class="templateRequestDraft__field">
|
||||||
|
<span class="templateRequestDraft__label">요청 제목</span>
|
||||||
|
<input v-model="templateRequestDraftTitle" class="input" maxlength="80" placeholder="예: 템플릿 업데이트 요청" />
|
||||||
|
</label>
|
||||||
|
<label class="templateRequestDraft__field">
|
||||||
|
<span class="templateRequestDraft__label">요청 설명</span>
|
||||||
|
<textarea v-model="templateRequestDraftDescription" class="textarea templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="modalCard__actions">
|
<div class="modalCard__actions">
|
||||||
<button class="btn btn--ghost" @click="closeTemplateUpdateModal">요청 취소</button>
|
<button class="btn btn--ghost" @click="closeTemplateUpdateModal">요청 취소</button>
|
||||||
<button class="btn btn--save" :disabled="isRequestingTemplate" @click="requestTemplate('update')">
|
<button class="btn btn--save" :disabled="!canSubmitTemplateUpdateRequest || isRequestingTemplate" @click="requestTemplate('update')">
|
||||||
{{ isRequestingTemplate ? '요청중...' : '예, 요청할게요' }}
|
{{ isRequestingTemplate ? '요청중...' : '예, 요청할게요' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1253,6 +1291,23 @@ onUnmounted(() => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
opacity: 0.78;
|
opacity: 0.78;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.templateRequestDraft {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.templateRequestDraft__field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.templateRequestDraft__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.64);
|
||||||
|
}
|
||||||
|
.templateRequestDraft__textarea {
|
||||||
|
min-height: 92px;
|
||||||
|
resize: vertical;
|
||||||
}
|
}
|
||||||
.boardTools {
|
.boardTools {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user