From 2cdd62765891413767b9d2cc1936095cdf8e0ecd Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 31 Mar 2026 15:15:18 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4:=20v1.2.60=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=9A=94=EC=B2=AD=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=20=EC=84=A4=EB=AA=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/tierlists.js | 9 ++-- docs/update.md | 4 ++ frontend/src/views/AdminView.vue | 18 ++++++-- frontend/src/views/TierEditorView.vue | 63 +++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/backend/src/routes/tierlists.js b/backend/src/routes/tierlists.js index d555e6a..7c2ceff 100644 --- a/backend/src/routes/tierlists.js +++ b/backend/src/routes/tierlists.js @@ -184,6 +184,8 @@ router.post('/thumbnail', requireAuth, thumbnailUpload.single('thumbnail'), asyn router.post('/:id/template-request', requireAuth, async (req, res) => { const schema = z.object({ 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) 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 (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 { 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, sourceGameId: tierList.gameId, targetGameId: parsed.data.type === 'update' ? tierList.gameId : '', - title: tierList.title, - description: tierList.description || '', + title: parsed.data.requestTitle, + description: parsed.data.requestDescription, thumbnailSrc: tierList.thumbnailSrc || '', items: customItems, }) diff --git a/docs/update.md b/docs/update.md index 2a476c4..0a48b06 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,9 @@ # 업데이트 로그 +## 2026-03-31 v1.2.60 +- 관리자 티어표 관리 카드에서 사용자가 입력한 설명을 제목 아래에 함께 노출해 요청 의도를 더 빨리 파악할 수 있게 함. +- 템플릿 등록/업데이트 요청은 이제 에디터 모달에서 제목과 설명을 별도로 입력받고, 예시 문구와 함께 전송하도록 정리함. + ## 2026-03-31 v1.2.59 - 관리자 아이템 상세 모달의 게임 선택을 전용 상태로 분리해 기본 선택값이 비어 있도록 바꾸고, 썸네일 아래에 배치해 정보/액션과 시각적으로 분리함. - 커스텀 아이템이 실제로 사용 중인 게임 목록을 백엔드에서 함께 내려주고, 템플릿 요청 생성 폼에는 게임 ID와 게임 이름 라벨을 추가해 구분을 명확히 함. diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 46cc143..eb1789a 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -1421,14 +1421,14 @@ async function saveFeaturedOrder() {
- +
@@ -1461,6 +1461,7 @@ async function saveFeaturedOrder() {
{{ tierList.title }}
+
{{ tierList.description }}
{{ tierList.gameName || tierList.gameId }} · {{ tierListAuthorDisplayName(tierList) }} · {{ tierListVisibilityLabel(tierList) }}
@@ -2950,6 +2951,15 @@ async function saveFeaturedOrder() { font-size: 18px; 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 { margin-top: 4px; opacity: 0.74; diff --git a/frontend/src/views/TierEditorView.vue b/frontend/src/views/TierEditorView.vue index 67fca66..e557476 100644 --- a/frontend/src/views/TierEditorView.vue +++ b/frontend/src/views/TierEditorView.vue @@ -43,6 +43,8 @@ const isExporting = ref(false) const isSaveModalOpen = ref(false) const isTemplateRequestModalOpen = ref(false) const isTemplateUpdateModalOpen = ref(false) +const templateRequestDraftTitle = ref('') +const templateRequestDraftDescription = ref('') const isDeleteModalOpen = ref(false) const ownerId = ref('') const authorName = ref('') @@ -112,7 +114,8 @@ const templateRequestChecks = computed(() => [ 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 || '선택한 게임'))) watch(error, (message) => { @@ -510,20 +513,29 @@ function closeSaveModal() { isSaveModalOpen.value = false } +function resetTemplateRequestDrafts() { + templateRequestDraftTitle.value = '' + templateRequestDraftDescription.value = '' +} + function openTemplateRequestModal() { + resetTemplateRequestDrafts() isTemplateRequestModalOpen.value = true } function closeTemplateRequestModal() { isTemplateRequestModalOpen.value = false + resetTemplateRequestDrafts() } function openTemplateUpdateModal() { + resetTemplateRequestDrafts() isTemplateUpdateModalOpen.value = true } function closeTemplateUpdateModal() { isTemplateUpdateModalOpen.value = false + resetTemplateRequestDrafts() } function openDeleteModal() { @@ -574,7 +586,11 @@ async function requestTemplate(type) { try { isRequestingTemplate.value = true 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 === 'update') closeTemplateUpdateModal() toast.success(type === 'create' ? '템플릿 등록 요청을 보냈어요.' : '템플릿 업데이트 요청을 보냈어요.') @@ -722,7 +738,18 @@ onUnmounted(() => {
- 제목만 명확하게 적어두면 관리자가 어떤 게임 템플릿 요청인지 빠르게 파악할 수 있어요. 여러 사용자가 비슷한 주제로 요청할 수 있으니 게임 이름을 구체적으로 적어주세요. + 제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 수 있어요. + 예시: 제목 `템플릿 등록 요청`, 설명 `여름 이벤트 한정 캐릭터 중심으로 새 게임 템플릿이 필요합니다.` +
+
+ +