Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fe4eff7b7 |
@@ -154,7 +154,7 @@ function mapTemplateRequestRow(row) {
|
|||||||
requesterName: getUserDisplayName(row),
|
requesterName: getUserDisplayName(row),
|
||||||
requesterAccountName: getUserAccountName(row),
|
requesterAccountName: getUserAccountName(row),
|
||||||
requesterAvatarSrc: row.requester_avatar_src || '',
|
requesterAvatarSrc: row.requester_avatar_src || '',
|
||||||
sourceTierListId: row.source_tierlist_id,
|
sourceTierListId: row.source_tierlist_id || '',
|
||||||
sourceGameId: row.source_game_id,
|
sourceGameId: row.source_game_id,
|
||||||
sourceGameName: row.source_game_name || '',
|
sourceGameName: row.source_game_name || '',
|
||||||
sourceTierListTitle: row.title_snapshot || '',
|
sourceTierListTitle: row.title_snapshot || '',
|
||||||
@@ -164,6 +164,9 @@ function mapTemplateRequestRow(row) {
|
|||||||
targetGameName: row.target_game_name || '',
|
targetGameName: row.target_game_name || '',
|
||||||
status: row.status,
|
status: row.status,
|
||||||
items: parseJson(row.items_json, []),
|
items: parseJson(row.items_json, []),
|
||||||
|
snapshotGroups: parseJson(row.groups_json, []),
|
||||||
|
snapshotItems: parseJson(row.board_items_json, []),
|
||||||
|
snapshotShowCharacterNames: !!row.show_character_names_snapshot,
|
||||||
createdAt: Number(row.created_at),
|
createdAt: Number(row.created_at),
|
||||||
updatedAt: Number(row.updated_at),
|
updatedAt: Number(row.updated_at),
|
||||||
}
|
}
|
||||||
@@ -389,6 +392,23 @@ async function ensureSchema() {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
const templateRequestSourceTierListColumns = await query("SHOW COLUMNS FROM template_requests LIKE 'source_tierlist_id'")
|
||||||
|
if (templateRequestSourceTierListColumns[0]?.Null !== 'YES') {
|
||||||
|
await query('ALTER TABLE template_requests MODIFY source_tierlist_id VARCHAR(64) NULL')
|
||||||
|
}
|
||||||
|
const templateRequestGroupsColumns = await query("SHOW COLUMNS FROM template_requests LIKE 'groups_json'")
|
||||||
|
if (!templateRequestGroupsColumns.length) {
|
||||||
|
await query("ALTER TABLE template_requests ADD COLUMN groups_json LONGTEXT NOT NULL AFTER items_json")
|
||||||
|
}
|
||||||
|
const templateRequestBoardItemsColumns = await query("SHOW COLUMNS FROM template_requests LIKE 'board_items_json'")
|
||||||
|
if (!templateRequestBoardItemsColumns.length) {
|
||||||
|
await query("ALTER TABLE template_requests ADD COLUMN board_items_json LONGTEXT NOT NULL AFTER groups_json")
|
||||||
|
}
|
||||||
|
const templateRequestShowNamesColumns = await query("SHOW COLUMNS FROM template_requests LIKE 'show_character_names_snapshot'")
|
||||||
|
if (!templateRequestShowNamesColumns.length) {
|
||||||
|
await query("ALTER TABLE template_requests ADD COLUMN show_character_names_snapshot TINYINT(1) NOT NULL DEFAULT 0 AFTER board_items_json")
|
||||||
|
}
|
||||||
|
|
||||||
const tierListThumbnailColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'thumbnail_src'")
|
const tierListThumbnailColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'thumbnail_src'")
|
||||||
if (!tierListThumbnailColumns.length) {
|
if (!tierListThumbnailColumns.length) {
|
||||||
await query("ALTER TABLE tierlists ADD COLUMN thumbnail_src VARCHAR(255) NOT NULL DEFAULT '' AFTER title")
|
await query("ALTER TABLE tierlists ADD COLUMN thumbnail_src VARCHAR(255) NOT NULL DEFAULT '' AFTER title")
|
||||||
@@ -754,7 +774,7 @@ async function listUnusedImageAssets({ limit = 100, minAgeHours = 24 } = {}) {
|
|||||||
query("SELECT src FROM game_items WHERE src <> ''"),
|
query("SELECT src FROM game_items WHERE src <> ''"),
|
||||||
query("SELECT src FROM custom_items WHERE src <> ''"),
|
query("SELECT src FROM custom_items WHERE src <> ''"),
|
||||||
query("SELECT thumbnail_src, pool_json FROM tierlists"),
|
query("SELECT thumbnail_src, pool_json FROM tierlists"),
|
||||||
query("SELECT thumbnail_src_snapshot, items_json FROM template_requests"),
|
query("SELECT thumbnail_src_snapshot, items_json, board_items_json FROM template_requests"),
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const row of userRows) if (row.avatar_src) referencedSrcs.add(row.avatar_src)
|
for (const row of userRows) if (row.avatar_src) referencedSrcs.add(row.avatar_src)
|
||||||
@@ -770,6 +790,7 @@ async function listUnusedImageAssets({ limit = 100, minAgeHours = 24 } = {}) {
|
|||||||
for (const row of templateRequestRows) {
|
for (const row of templateRequestRows) {
|
||||||
if (row.thumbnail_src_snapshot) referencedSrcs.add(row.thumbnail_src_snapshot)
|
if (row.thumbnail_src_snapshot) referencedSrcs.add(row.thumbnail_src_snapshot)
|
||||||
collectUploadSrcsFromItems(parseJson(row.items_json, []), referencedSrcs)
|
collectUploadSrcsFromItems(parseJson(row.items_json, []), referencedSrcs)
|
||||||
|
collectUploadSrcsFromItems(parseJson(row.board_items_json, []), referencedSrcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return assets.filter((asset) => !referencedSrcs.has(asset.src))
|
return assets.filter((asset) => !referencedSrcs.has(asset.src))
|
||||||
@@ -806,7 +827,7 @@ async function listReferencedUploadUsage() {
|
|||||||
query("SELECT src FROM game_items WHERE src <> ''"),
|
query("SELECT src FROM game_items WHERE src <> ''"),
|
||||||
query("SELECT src FROM custom_items WHERE src <> ''"),
|
query("SELECT src FROM custom_items WHERE src <> ''"),
|
||||||
query("SELECT id, thumbnail_src, pool_json FROM tierlists"),
|
query("SELECT id, thumbnail_src, pool_json FROM tierlists"),
|
||||||
query("SELECT id, thumbnail_src_snapshot, items_json FROM template_requests"),
|
query("SELECT id, thumbnail_src_snapshot, items_json, board_items_json FROM template_requests"),
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const row of userRows) addUsage(row.avatar_src, 'avatar')
|
for (const row of userRows) addUsage(row.avatar_src, 'avatar')
|
||||||
@@ -822,6 +843,7 @@ async function listReferencedUploadUsage() {
|
|||||||
for (const row of templateRequestRows) {
|
for (const row of templateRequestRows) {
|
||||||
addUsage(row.thumbnail_src_snapshot, 'template-thumbnail')
|
addUsage(row.thumbnail_src_snapshot, 'template-thumbnail')
|
||||||
for (const item of parseJson(row.items_json, [])) addUsage(item?.src, 'template-item')
|
for (const item of parseJson(row.items_json, [])) addUsage(item?.src, 'template-item')
|
||||||
|
for (const item of parseJson(row.board_items_json, [])) addUsage(item?.src, 'template-board-item')
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(usageMap.entries())
|
return Array.from(usageMap.entries())
|
||||||
@@ -874,7 +896,7 @@ async function replaceUploadSourceReferences({ fromSrc, toSrc }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestRows = await query('SELECT id, thumbnail_src_snapshot, items_json FROM template_requests')
|
const requestRows = await query('SELECT id, thumbnail_src_snapshot, items_json, board_items_json FROM template_requests')
|
||||||
for (const row of requestRows) {
|
for (const row of requestRows) {
|
||||||
let nextThumbnail = row.thumbnail_src_snapshot
|
let nextThumbnail = row.thumbnail_src_snapshot
|
||||||
let changed = false
|
let changed = false
|
||||||
@@ -884,12 +906,14 @@ async function replaceUploadSourceReferences({ fromSrc, toSrc }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const replacedItems = replaceItemSrc(parseJson(row.items_json, []), fromSrc, toSrc)
|
const replacedItems = replaceItemSrc(parseJson(row.items_json, []), fromSrc, toSrc)
|
||||||
if (replacedItems.changed) changed = true
|
const replacedBoardItems = replaceItemSrc(parseJson(row.board_items_json, []), fromSrc, toSrc)
|
||||||
|
if (replacedItems.changed || replacedBoardItems.changed) changed = true
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
await query('UPDATE template_requests SET thumbnail_src_snapshot = ?, items_json = ?, updated_at = ? WHERE id = ?', [
|
await query('UPDATE template_requests SET thumbnail_src_snapshot = ?, items_json = ?, board_items_json = ?, updated_at = ? WHERE id = ?', [
|
||||||
nextThumbnail || '',
|
nextThumbnail || '',
|
||||||
serializeJson(replacedItems.items),
|
serializeJson(replacedItems.items),
|
||||||
|
serializeJson(replacedBoardItems.items),
|
||||||
now(),
|
now(),
|
||||||
row.id,
|
row.id,
|
||||||
])
|
])
|
||||||
@@ -1636,19 +1660,24 @@ async function createTemplateRequest({
|
|||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
requesterId,
|
requesterId,
|
||||||
sourceTierListId,
|
sourceTierListId = '',
|
||||||
sourceGameId,
|
sourceGameId,
|
||||||
targetGameId = '',
|
targetGameId = '',
|
||||||
title,
|
title,
|
||||||
description = '',
|
description = '',
|
||||||
thumbnailSrc = '',
|
thumbnailSrc = '',
|
||||||
items = [],
|
items = [],
|
||||||
|
groups = [],
|
||||||
|
boardItems = [],
|
||||||
|
showCharacterNames = false,
|
||||||
}) {
|
}) {
|
||||||
const existing = await findPendingTemplateRequestByTierList({ sourceTierListId, type })
|
if (sourceTierListId) {
|
||||||
if (existing) {
|
const existing = await findPendingTemplateRequestByTierList({ sourceTierListId, type })
|
||||||
const err = new Error('template_request_exists')
|
if (existing) {
|
||||||
err.code = 'TEMPLATE_REQUEST_EXISTS'
|
const err = new Error('template_request_exists')
|
||||||
throw err
|
err.code = 'TEMPLATE_REQUEST_EXISTS'
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdAt = now()
|
const createdAt = now()
|
||||||
@@ -1666,22 +1695,28 @@ async function createTemplateRequest({
|
|||||||
description_snapshot,
|
description_snapshot,
|
||||||
thumbnail_src_snapshot,
|
thumbnail_src_snapshot,
|
||||||
items_json,
|
items_json,
|
||||||
|
groups_json,
|
||||||
|
board_items_json,
|
||||||
|
show_character_names_snapshot,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
requesterId,
|
requesterId,
|
||||||
sourceTierListId,
|
sourceTierListId || null,
|
||||||
sourceGameId,
|
sourceGameId,
|
||||||
targetGameId,
|
targetGameId,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
thumbnailSrc,
|
thumbnailSrc,
|
||||||
serializeJson(items),
|
serializeJson(items),
|
||||||
|
serializeJson(groups),
|
||||||
|
serializeJson(boardItems),
|
||||||
|
showCharacterNames ? 1 : 0,
|
||||||
createdAt,
|
createdAt,
|
||||||
createdAt,
|
createdAt,
|
||||||
]
|
]
|
||||||
@@ -1704,6 +1739,9 @@ async function findTemplateRequestById(id) {
|
|||||||
tr.description_snapshot,
|
tr.description_snapshot,
|
||||||
tr.thumbnail_src_snapshot,
|
tr.thumbnail_src_snapshot,
|
||||||
tr.items_json,
|
tr.items_json,
|
||||||
|
tr.groups_json,
|
||||||
|
tr.board_items_json,
|
||||||
|
tr.show_character_names_snapshot,
|
||||||
tr.created_at,
|
tr.created_at,
|
||||||
tr.updated_at,
|
tr.updated_at,
|
||||||
u.nickname,
|
u.nickname,
|
||||||
@@ -1739,6 +1777,9 @@ async function listAdminTemplateRequests({ status = 'pending' } = {}) {
|
|||||||
tr.description_snapshot,
|
tr.description_snapshot,
|
||||||
tr.thumbnail_src_snapshot,
|
tr.thumbnail_src_snapshot,
|
||||||
tr.items_json,
|
tr.items_json,
|
||||||
|
tr.groups_json,
|
||||||
|
tr.board_items_json,
|
||||||
|
tr.show_character_names_snapshot,
|
||||||
tr.created_at,
|
tr.created_at,
|
||||||
tr.updated_at,
|
tr.updated_at,
|
||||||
u.nickname,
|
u.nickname,
|
||||||
|
|||||||
@@ -58,6 +58,33 @@ function getCustomTemplateItems(tierList) {
|
|||||||
const upload = createMemoryUpload(multer, { fileSize: 6 * 1024 * 1024 })
|
const upload = createMemoryUpload(multer, { fileSize: 6 * 1024 * 1024 })
|
||||||
const thumbnailUpload = createMemoryUpload(multer, { fileSize: 8 * 1024 * 1024 })
|
const thumbnailUpload = createMemoryUpload(multer, { fileSize: 8 * 1024 * 1024 })
|
||||||
|
|
||||||
|
const templateRequestSchema = z.object({
|
||||||
|
type: z.enum(['create', 'update']),
|
||||||
|
sourceTierListId: z.string().max(64).optional().default(''),
|
||||||
|
gameId: z.string().min(1).max(120),
|
||||||
|
requestTitle: z.string().trim().min(1).max(120),
|
||||||
|
requestDescription: z.string().trim().min(1).max(1000),
|
||||||
|
thumbnailSrc: z.string().max(255).optional().default(''),
|
||||||
|
isPublic: z.boolean().optional().default(false),
|
||||||
|
showCharacterNames: z.boolean().optional().default(false),
|
||||||
|
saveToMyTierList: z.boolean().optional().default(true),
|
||||||
|
groups: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
name: z.string().min(1).max(16),
|
||||||
|
itemIds: z.array(z.string()),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
boardItems: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
src: z.string().min(1),
|
||||||
|
label: z.string().min(1).max(60),
|
||||||
|
origin: z.enum(['game', 'custom']).default('game'),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
const tierListUpsertSchema = z.object({
|
const tierListUpsertSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
gameId: z.string().min(1),
|
gameId: z.string().min(1),
|
||||||
@@ -194,42 +221,64 @@ router.post('/thumbnail', requireAuth, thumbnailUpload.single('thumbnail'), asyn
|
|||||||
res.json({ thumbnailSrc: optimized.src })
|
res.json({ thumbnailSrc: optimized.src })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/:id/template-request', requireAuth, async (req, res) => {
|
router.post('/template-request', requireAuth, async (req, res) => {
|
||||||
const schema = z.object({
|
const parsed = templateRequestSchema.safeParse(req.body)
|
||||||
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' })
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
const tierList = await findTierListById(req.params.id, req.session.userId)
|
const payload = parsed.data
|
||||||
if (!tierList) return res.status(404).json({ error: 'not_found' })
|
const normalizedBoardItems = payload.boardItems.map(normalizePoolItem)
|
||||||
if (tierList.authorId !== req.session.userId) return res.status(403).json({ error: 'forbidden' })
|
const customItems = normalizedBoardItems.filter((item) => item?.origin === 'custom')
|
||||||
|
|
||||||
const customItems = getCustomTemplateItems(tierList)
|
|
||||||
if (!customItems.length) return res.status(400).json({ error: 'custom_items_required' })
|
if (!customItems.length) return res.status(400).json({ error: 'custom_items_required' })
|
||||||
|
|
||||||
if (parsed.data.type === 'create') {
|
if (payload.type === 'create') {
|
||||||
if (tierList.gameId !== FREEFORM_GAME_ID) return res.status(400).json({ error: 'freeform_required' })
|
if (payload.gameId !== FREEFORM_GAME_ID) return res.status(400).json({ error: 'freeform_required' })
|
||||||
} else {
|
} else if (payload.gameId === FREEFORM_GAME_ID) {
|
||||||
if (tierList.gameId === FREEFORM_GAME_ID) return res.status(400).json({ error: 'game_template_required' })
|
return res.status(400).json({ error: 'game_template_required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceTierList = null
|
||||||
|
if (payload.sourceTierListId) {
|
||||||
|
sourceTierList = await findTierListById(payload.sourceTierListId, req.session.userId)
|
||||||
|
if (!sourceTierList) return res.status(404).json({ error: 'not_found' })
|
||||||
|
if (sourceTierList.authorId !== req.session.userId) return res.status(403).json({ error: 'forbidden' })
|
||||||
|
}
|
||||||
|
|
||||||
|
let savedTierList = null
|
||||||
|
if (payload.saveToMyTierList) {
|
||||||
|
savedTierList = await saveTierList({
|
||||||
|
id: sourceTierList?.id || undefined,
|
||||||
|
authorId: req.session.userId,
|
||||||
|
gameId: payload.gameId,
|
||||||
|
title: payload.requestTitle,
|
||||||
|
thumbnailSrc: payload.thumbnailSrc || '',
|
||||||
|
description: payload.requestDescription || '',
|
||||||
|
isPublic: !!payload.isPublic,
|
||||||
|
showCharacterNames: !!payload.showCharacterNames,
|
||||||
|
sourceTierListId: sourceTierList?.sourceTierListId || '',
|
||||||
|
sourceSnapshotTitle: sourceTierList?.sourceSnapshotTitle || '',
|
||||||
|
sourceSnapshotAuthor: sourceTierList?.sourceSnapshotAuthor || '',
|
||||||
|
groups: payload.groups,
|
||||||
|
pool: normalizedBoardItems,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = await createTemplateRequest({
|
const request = await createTemplateRequest({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
type: parsed.data.type,
|
type: payload.type,
|
||||||
requesterId: req.session.userId,
|
requesterId: req.session.userId,
|
||||||
sourceTierListId: tierList.id,
|
sourceTierListId: savedTierList?.id || sourceTierList?.id || '',
|
||||||
sourceGameId: tierList.gameId,
|
sourceGameId: payload.gameId,
|
||||||
targetGameId: parsed.data.type === 'update' ? tierList.gameId : '',
|
targetGameId: payload.type === 'update' ? payload.gameId : '',
|
||||||
title: parsed.data.requestTitle,
|
title: payload.requestTitle,
|
||||||
description: parsed.data.requestDescription,
|
description: payload.requestDescription,
|
||||||
thumbnailSrc: tierList.thumbnailSrc || '',
|
thumbnailSrc: payload.thumbnailSrc || '',
|
||||||
items: customItems,
|
items: customItems,
|
||||||
|
groups: payload.groups,
|
||||||
|
boardItems: normalizedBoardItems,
|
||||||
|
showCharacterNames: !!payload.showCharacterNames,
|
||||||
})
|
})
|
||||||
return res.json({ request })
|
return res.json({ request, savedTierList: savedTierList ? normalizeTierList(savedTierList) : null })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e?.code === 'TEMPLATE_REQUEST_EXISTS') {
|
if (e?.code === 'TEMPLATE_REQUEST_EXISTS') {
|
||||||
return res.status(409).json({ error: 'template_request_exists' })
|
return res.status(409).json({ error: 'template_request_exists' })
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# 할 일 및 이슈
|
# 할 일 및 이슈
|
||||||
|
|
||||||
## 즉시 확인 필요
|
## 즉시 확인 필요
|
||||||
- 티어표 형식 추가 필요. 최근 게임들은 S, A, B,C 같은 랭크 뿐만 아니라 가로 열도 나누어진형태의 티어표를 원함 (공격, 방어, 지원 등 각 파트별 랭크를 보고싶어함)
|
- 최근 게임들은 S, A, B,C 같은 랭크 뿐만 아니라 가로 열도 나누어진형태의 티어표를 원함 (공격, 방어, 지원 등 각 파트별 랭크를 보고싶어함)
|
||||||
|
- 티어표에서 티어 추가(세로라인)를 행 추가 등의 단어로 변경. 열 추가 버튼도 표시. 열을 추가할경우 열에도 이름을 입력 가능. (1열만 있을 경우에는 삭제 불가, 열 제목 사용 안함)
|
||||||
|
- 드래그 아이콘의 위치를 이동시키거나, 제외하는 등(기능은 가능해야함) 적절한 판단을 통해 행 라벨의 너비가 큰편인데 조금 줄일 필요 있음. (실제로는 5~6자 이상 쓰는경우가 거의 없기 때문)
|
||||||
|
- 이미지 저장시 라벨 텍스트가 중앙 정렬이 아닌 살짝 상단으로 치우쳐진 상태라 이쁘지 않은데... 확인 필요함.
|
||||||
- 레거시 파일 정리 스크립트는 준비됐으므로, 운영 단계에서는 cron 등으로 주기 실행할지와 삭제 전 보관 기간을 함께 정한다.
|
- 레거시 파일 정리 스크립트는 준비됐으므로, 운영 단계에서는 cron 등으로 주기 실행할지와 삭제 전 보관 기간을 함께 정한다.
|
||||||
- 관리자 기본 아이템 다중 업로드는 현재 파일명 기반 자동 라벨만 지원하므로, 필요하면 업로드 후 일괄 라벨 수정/정렬 UX를 추가 검토한다.
|
- 관리자 기본 아이템 다중 업로드는 현재 파일명 기반 자동 라벨만 지원하므로, 필요하면 업로드 후 일괄 라벨 수정/정렬 UX를 추가 검토한다.
|
||||||
- 사용자 커스텀 아이템 승격은 현재 수동 복제 방식이므로, 필요하면 중복 감지나 “비슷한 항목 추천” 같은 보조 UX를 검토한다.
|
- 사용자 커스텀 아이템 승격은 현재 수동 복제 방식이므로, 필요하면 중복 감지나 “비슷한 항목 추천” 같은 보조 UX를 검토한다.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-01 v1.3.13
|
||||||
|
- 템플릿 등록/업데이트 요청 모달은 이제 현재 티어표 제목·설명을 기본값으로 가져오고, 비어 있더라도 모달 안에서 바로 작성해 요청할 수 있도록 흐름을 단순화함.
|
||||||
|
- 템플릿 요청 시 `내 티어 리스트에도 저장` 토글을 추가해, 요청 스냅샷만 관리자에게 전달할지 아니면 현재 양식도 내 티어표로 함께 저장할지 분리함.
|
||||||
|
- 관리자 템플릿 요청 관리는 더 이상 원본 티어표 링크에 의존하지 않고, 요청 시점의 그룹/아이템/이름표시 상태를 그대로 담은 스냅샷 미리보기를 직접 열어 확인할 수 있게 확장함.
|
||||||
|
|
||||||
## 2026-04-01 v1.3.12
|
## 2026-04-01 v1.3.12
|
||||||
- 관리자 회원 관리 상단에 정렬 방향 선택을 추가해, 최근 활동순·가입순·작성 티어표순을 각각 오름차순/내림차순으로 다시 볼 수 있게 확장함.
|
- 관리자 회원 관리 상단에 정렬 방향 선택을 추가해, 최근 활동순·가입순·작성 티어표순을 각각 오름차순/내림차순으로 다시 볼 수 있게 확장함.
|
||||||
- 회원 정보 수정, 새 게임 생성, 비밀번호 초기화 모달은 Settings 톤 입력 스타일을 유지하면서 각 입력칸에 글자 수 피드백을 함께 보여주도록 정리함.
|
- 회원 정보 수정, 새 게임 생성, 비밀번호 초기화 모달은 Settings 톤 입력 스타일을 유지하면서 각 입력칸에 글자 수 피드백을 함께 보여주도록 정리함.
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const api = {
|
|||||||
unfavoriteTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}/favorite`, { method: 'DELETE' }),
|
unfavoriteTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}/favorite`, { method: 'DELETE' }),
|
||||||
deleteTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
deleteTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}`, { method: 'DELETE' }),
|
||||||
duplicateTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}/duplicate`, { method: 'POST' }),
|
duplicateTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}/duplicate`, { method: 'POST' }),
|
||||||
requestTierListTemplate: (id, payload) => request(`/api/tierlists/${encodeURIComponent(id)}/template-request`, { method: 'POST', body: payload }),
|
requestTierListTemplate: (payload) => request('/api/tierlists/template-request', { method: 'POST', body: payload }),
|
||||||
saveTierList: (payload) => request('/api/tierlists', { method: 'POST', body: payload }),
|
saveTierList: (payload) => request('/api/tierlists', { method: 'POST', body: payload }),
|
||||||
uploadTierListThumbnail: async (file) => {
|
uploadTierListThumbnail: async (file) => {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
|
|||||||
@@ -1065,6 +1065,38 @@ function openAdminTierList(tierList) {
|
|||||||
previewModalOpen.value = true
|
previewModalOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function previewRequestItemsById(preview) {
|
||||||
|
const items = Array.isArray(preview?.snapshotItems) ? preview.snapshotItems : []
|
||||||
|
return items.reduce((acc, item) => {
|
||||||
|
if (item?.id) acc[item.id] = item
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewRequestGroupItems(preview, group) {
|
||||||
|
const itemsById = previewRequestItemsById(preview)
|
||||||
|
return (group?.itemIds || []).map((itemId) => itemsById[itemId]).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewRequestPoolItems(preview) {
|
||||||
|
const groupedIds = new Set((preview?.snapshotGroups || []).flatMap((group) => group.itemIds || []))
|
||||||
|
return (preview?.snapshotItems || []).filter((item) => !groupedIds.has(item.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTemplateRequestPreview(request) {
|
||||||
|
previewTierList.value = {
|
||||||
|
id: request.id,
|
||||||
|
title: request.sourceTierListTitle || '템플릿 요청 미리보기',
|
||||||
|
description: request.sourceDescription || '',
|
||||||
|
thumbnailSrc: request.thumbnailSrc || '',
|
||||||
|
requestPreview: true,
|
||||||
|
snapshotGroups: request.snapshotGroups || [],
|
||||||
|
snapshotItems: request.snapshotItems || [],
|
||||||
|
snapshotShowCharacterNames: !!request.snapshotShowCharacterNames,
|
||||||
|
}
|
||||||
|
previewModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function closePreviewModal() {
|
function closePreviewModal() {
|
||||||
previewModalOpen.value = false
|
previewModalOpen.value = false
|
||||||
previewTierList.value = null
|
previewTierList.value = null
|
||||||
@@ -1470,13 +1502,14 @@ async function saveFeaturedOrder() {
|
|||||||
<div class="templateRequestCard__head">
|
<div class="templateRequestCard__head">
|
||||||
<div>
|
<div>
|
||||||
<div class="templateRequestCard__title">{{ request.sourceTierListTitle }}</div>
|
<div class="templateRequestCard__title">{{ request.sourceTierListTitle }}</div>
|
||||||
|
<div v-if="request.sourceDescription" class="templateRequestCard__desc">{{ request.sourceDescription }}</div>
|
||||||
<div class="templateRequestCard__meta">
|
<div class="templateRequestCard__meta">
|
||||||
{{ templateRequestTypeLabel(request) }} · {{ request.requesterName }} · {{ fmt(request.createdAt) }}
|
{{ templateRequestTypeLabel(request) }} · {{ request.requesterName }} · {{ fmt(request.createdAt) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="templateRequestCard__meta">{{ templateRequestTargetLabel(request) }}</div>
|
<div class="templateRequestCard__meta">{{ templateRequestTargetLabel(request) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn--ghost btn--small" @click="openAdminTierList({ id: request.sourceTierListId, gameId: request.sourceGameId })">
|
<button class="btn btn--ghost btn--small" @click="openTemplateRequestPreview(request)">
|
||||||
원본 보기
|
요청 미리보기
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1886,8 +1919,45 @@ async function saveFeaturedOrder() {
|
|||||||
<div class="modalCard__title">{{ previewTierList?.title || '티어표 미리보기' }}</div>
|
<div class="modalCard__title">{{ previewTierList?.title || '티어표 미리보기' }}</div>
|
||||||
<button class="btn btn--ghost btn--small" @click="closePreviewModal">닫기</button>
|
<button class="btn btn--ghost btn--small" @click="closePreviewModal">닫기</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="previewTierList?.requestPreview" class="requestPreview">
|
||||||
|
<img
|
||||||
|
v-if="previewTierList.thumbnailSrc"
|
||||||
|
class="requestPreview__thumb"
|
||||||
|
:src="toApiUrl(previewTierList.thumbnailSrc)"
|
||||||
|
:alt="previewTierList.title"
|
||||||
|
/>
|
||||||
|
<div v-if="previewTierList.description" class="requestPreview__desc">{{ previewTierList.description }}</div>
|
||||||
|
<div class="requestPreview__rows">
|
||||||
|
<div v-for="group in previewTierList.snapshotGroups" :key="group.id" class="requestPreview__row">
|
||||||
|
<div class="requestPreview__rowLabel">{{ group.name }}</div>
|
||||||
|
<div class="requestPreview__rowItems">
|
||||||
|
<div
|
||||||
|
v-for="item in previewRequestGroupItems(previewTierList, group)"
|
||||||
|
:key="item.id"
|
||||||
|
class="requestPreview__item"
|
||||||
|
>
|
||||||
|
<img class="requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
|
||||||
|
<div v-if="previewTierList.snapshotShowCharacterNames" class="requestPreview__itemLabel">{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="previewRequestPoolItems(previewTierList).length" class="requestPreview__pool">
|
||||||
|
<div class="requestPreview__poolLabel">남은 아이템</div>
|
||||||
|
<div class="requestPreview__rowItems">
|
||||||
|
<div
|
||||||
|
v-for="item in previewRequestPoolItems(previewTierList)"
|
||||||
|
:key="item.id"
|
||||||
|
class="requestPreview__item requestPreview__item--muted"
|
||||||
|
>
|
||||||
|
<img class="requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
|
||||||
|
<div v-if="previewTierList.snapshotShowCharacterNames" class="requestPreview__itemLabel">{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<iframe
|
<iframe
|
||||||
v-if="previewTierList"
|
v-else-if="previewTierList"
|
||||||
class="previewFrame"
|
class="previewFrame"
|
||||||
:src="previewTierListUrl(previewTierList)"
|
:src="previewTierListUrl(previewTierList)"
|
||||||
title="티어표 미리보기"
|
title="티어표 미리보기"
|
||||||
@@ -3224,6 +3294,12 @@ async function saveFeaturedOrder() {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
.templateRequestCard__desc {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.74);
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
.templateRequestCard__meta {
|
.templateRequestCard__meta {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -3272,6 +3348,76 @@ async function saveFeaturedOrder() {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.requestPreview {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.requestPreview__thumb {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 240px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
.requestPreview__desc {
|
||||||
|
color: rgba(255, 255, 255, 0.74);
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.requestPreview__rows,
|
||||||
|
.requestPreview__pool {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.requestPreview__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 88px minmax(0, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.requestPreview__rowLabel,
|
||||||
|
.requestPreview__poolLabel {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
.requestPreview__rowItems {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.requestPreview__item {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
.requestPreview__item--muted {
|
||||||
|
opacity: 0.52;
|
||||||
|
filter: grayscale(0.2) brightness(0.78);
|
||||||
|
}
|
||||||
|
.requestPreview__itemThumb {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.requestPreview__itemLabel {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0.7));
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
.tierAdminList {
|
.tierAdminList {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const isTemplateRequestModalOpen = ref(false)
|
|||||||
const isTemplateUpdateModalOpen = ref(false)
|
const isTemplateUpdateModalOpen = ref(false)
|
||||||
const templateRequestDraftTitle = ref('')
|
const templateRequestDraftTitle = ref('')
|
||||||
const templateRequestDraftDescription = ref('')
|
const templateRequestDraftDescription = ref('')
|
||||||
|
const templateRequestSaveToMyTierList = ref(true)
|
||||||
const isDeleteModalOpen = ref(false)
|
const isDeleteModalOpen = ref(false)
|
||||||
const isGroupDeleteModalOpen = ref(false)
|
const isGroupDeleteModalOpen = ref(false)
|
||||||
const pendingRemoveGroupId = ref('')
|
const pendingRemoveGroupId = ref('')
|
||||||
@@ -119,14 +120,7 @@ const canRequestTemplateCreate = computed(
|
|||||||
const canRequestTemplateUpdate = computed(
|
const canRequestTemplateUpdate = computed(
|
||||||
() => canEdit.value && !isNewTierList.value && gameId.value !== 'freeform' && customItems.value.length > 0
|
() => canEdit.value && !isNewTierList.value && gameId.value !== 'freeform' && customItems.value.length > 0
|
||||||
)
|
)
|
||||||
const templateRequestChecks = computed(() => [
|
const canSubmitTemplateCreateRequest = computed(() => !!templateRequestDraftTitle.value.trim() && !!templateRequestDraftDescription.value.trim())
|
||||||
{
|
|
||||||
id: 'title',
|
|
||||||
label: '티어표 이름(게임 이름)을 직접 입력했는지',
|
|
||||||
passed: !!(title.value || '').trim() && (title.value || '').trim() !== (gameName.value || '').trim(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
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 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 || '선택한 게임')))
|
||||||
|
|
||||||
@@ -547,8 +541,9 @@ function closeSaveModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetTemplateRequestDrafts() {
|
function resetTemplateRequestDrafts() {
|
||||||
templateRequestDraftTitle.value = ''
|
templateRequestDraftTitle.value = (title.value || '').trim()
|
||||||
templateRequestDraftDescription.value = ''
|
templateRequestDraftDescription.value = (description.value || '').trim()
|
||||||
|
templateRequestSaveToMyTierList.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTemplateRequestModal() {
|
function openTemplateRequestModal() {
|
||||||
@@ -624,27 +619,54 @@ async function toggleFavorite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestTemplate(type) {
|
async function requestTemplate(type) {
|
||||||
if (isNewTierList.value) {
|
|
||||||
toast.error('요청 전에 먼저 티어표를 저장해주세요.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isRequestingTemplate.value = true
|
isRequestingTemplate.value = true
|
||||||
const persisted = await persistTierList({ showModal: false })
|
await uploadPendingCustomItems()
|
||||||
await api.requestTierListTemplate(persisted.savedTierListId, {
|
const uploadedThumbnailSrc = await uploadPendingThumbnail()
|
||||||
|
const response = await api.requestTierListTemplate({
|
||||||
type,
|
type,
|
||||||
|
sourceTierListId: tierListId.value !== 'new' ? tierListId.value : '',
|
||||||
|
gameId: gameId.value,
|
||||||
requestTitle: templateRequestDraftTitle.value.trim(),
|
requestTitle: templateRequestDraftTitle.value.trim(),
|
||||||
requestDescription: templateRequestDraftDescription.value.trim(),
|
requestDescription: templateRequestDraftDescription.value.trim(),
|
||||||
|
thumbnailSrc: uploadedThumbnailSrc || thumbnailSrc.value || '',
|
||||||
|
isPublic: !!isPublic.value,
|
||||||
|
showCharacterNames: !!showCharacterNames.value,
|
||||||
|
saveToMyTierList: !!templateRequestSaveToMyTierList.value,
|
||||||
|
groups: groups.value.map((group) => ({
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
itemIds: [...group.itemIds],
|
||||||
|
})),
|
||||||
|
boardItems: Object.values(itemsById.value),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const savedTierList = response?.savedTierList
|
||||||
|
if (savedTierList) {
|
||||||
|
title.value = savedTierList.title || title.value
|
||||||
|
description.value = savedTierList.description || ''
|
||||||
|
updatedAt.value = Number(savedTierList.updatedAt || Date.now())
|
||||||
|
authorName.value = savedTierList.authorName || effectiveAuthorName.value
|
||||||
|
authorAccountName.value = savedTierList.authorAccountName || authorAccountName.value
|
||||||
|
favoriteCount.value = Number(savedTierList.favoriteCount || favoriteCount.value || 0)
|
||||||
|
isFavorited.value = !!savedTierList.isFavorited
|
||||||
|
if (tierListId.value === 'new' && savedTierList.id) {
|
||||||
|
await router.replace(`/editor/${gameId.value}/${savedTierList.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'create') closeTemplateRequestModal()
|
if (type === 'create') closeTemplateRequestModal()
|
||||||
if (type === 'update') closeTemplateUpdateModal()
|
if (type === 'update') closeTemplateUpdateModal()
|
||||||
toast.success(type === 'create' ? '템플릿 등록 요청을 보냈어요.' : '템플릿 업데이트 요청을 보냈어요.')
|
toast.success(
|
||||||
|
type === 'create'
|
||||||
|
? templateRequestSaveToMyTierList.value
|
||||||
|
? '템플릿 등록 요청과 내 티어표 저장을 함께 완료했어요.'
|
||||||
|
: '템플릿 등록 요청을 보냈어요.'
|
||||||
|
: templateRequestSaveToMyTierList.value
|
||||||
|
? '템플릿 업데이트 요청과 내 티어표 저장을 함께 완료했어요.'
|
||||||
|
: '템플릿 업데이트 요청을 보냈어요.'
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e?.status === 400 && e?.data?.error === 'title_required') {
|
|
||||||
toast.error('템플릿 등록 요청 전에는 티어표 이름을 직접 입력해주세요.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e?.status === 409) {
|
if (e?.status === 409) {
|
||||||
toast.error('이미 처리 대기 중인 같은 요청이 있어요.')
|
toast.error('이미 처리 대기 중인 같은 요청이 있어요.')
|
||||||
return
|
return
|
||||||
@@ -773,18 +795,7 @@ onUnmounted(() => {
|
|||||||
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateRequestTitle">
|
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateRequestTitle">
|
||||||
<div id="templateRequestTitle" class="modalCard__title">템플릿 등록 요청</div>
|
<div id="templateRequestTitle" class="modalCard__title">템플릿 등록 요청</div>
|
||||||
<div class="modalCard__desc">
|
<div class="modalCard__desc">
|
||||||
여러 사용자가 비슷한 주제로 요청할 수 있으니, 관리자에게 전달되기 전에 아래 조건을 먼저 확인해주세요.
|
티어표에 적어둔 제목과 설명이 있다면 그대로 가져와 두었어요. 비어 있다면 여기서 바로 작성해도 괜찮아요.
|
||||||
</div>
|
|
||||||
<div class="requestChecklist">
|
|
||||||
<div
|
|
||||||
v-for="check in templateRequestChecks"
|
|
||||||
:key="check.id"
|
|
||||||
class="requestChecklist__item"
|
|
||||||
:class="{ 'requestChecklist__item--passed': check.passed }"
|
|
||||||
>
|
|
||||||
<span class="requestChecklist__label">{{ check.label }}</span>
|
|
||||||
<span class="requestChecklist__icon">{{ check.passed ? '완료' : '확인 필요' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="requestChecklist__hint">
|
<div class="requestChecklist__hint">
|
||||||
제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 수 있어요.
|
제목과 설명을 함께 적어두면 관리자가 어떤 신규 템플릿인지 훨씬 빠르게 파악할 수 있어요.
|
||||||
@@ -792,15 +803,23 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="templateRequestDraft">
|
<div class="templateRequestDraft">
|
||||||
<label class="templateRequestDraft__field">
|
<label class="templateRequestDraft__field">
|
||||||
<span class="templateRequestDraft__label">요청 제목</span>
|
<span class="templateRequestDraft__label">티어표 제목</span>
|
||||||
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 등록 요청" />
|
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="120" placeholder="예: 템플릿 등록 요청" />
|
||||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/80자</span>
|
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/120자</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="templateRequestDraft__field">
|
<label class="templateRequestDraft__field">
|
||||||
<span class="templateRequestDraft__label">요청 설명</span>
|
<span class="templateRequestDraft__label">티어표 설명</span>
|
||||||
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
|
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="1000" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
|
||||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/240자</span>
|
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/1000자</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="toggleSwitch">
|
||||||
|
<input v-model="templateRequestSaveToMyTierList" type="checkbox" />
|
||||||
|
<span class="toggleSwitch__label">내 티어 리스트에도 저장</span>
|
||||||
|
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
|
||||||
|
</label>
|
||||||
|
<div class="templateRequestDraft__note">
|
||||||
|
저장을 끄면 요청 시점 스냅샷만 관리자에게 전달되고, 내 티어 리스트에는 별도로 남기지 않아요.
|
||||||
|
</div>
|
||||||
</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>
|
||||||
@@ -815,7 +834,7 @@ onUnmounted(() => {
|
|||||||
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateUpdateTitle">
|
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateUpdateTitle">
|
||||||
<div id="templateUpdateTitle" class="modalCard__title">템플릿 요청하기</div>
|
<div id="templateUpdateTitle" class="modalCard__title">템플릿 요청하기</div>
|
||||||
<div class="modalCard__desc">
|
<div class="modalCard__desc">
|
||||||
{{ templateRequestTargetLabel }}에 직접 추가한 아이템을 포함해 달라고 관리자에게 요청을 보냅니다.
|
{{ templateRequestTargetLabel }}에 직접 추가한 아이템을 포함해 달라고 관리자에게 요청합니다. 현재 티어표 제목과 설명은 그대로 가져와 두었어요.
|
||||||
</div>
|
</div>
|
||||||
<div class="modalCard__note">
|
<div class="modalCard__note">
|
||||||
모두가 사용하는 기본 템플릿이니 개인적인 항목이 아닌 공통된 항목만 추가한 뒤 신청해주세요.
|
모두가 사용하는 기본 템플릿이니 개인적인 항목이 아닌 공통된 항목만 추가한 뒤 신청해주세요.
|
||||||
@@ -823,15 +842,23 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="templateRequestDraft">
|
<div class="templateRequestDraft">
|
||||||
<label class="templateRequestDraft__field">
|
<label class="templateRequestDraft__field">
|
||||||
<span class="templateRequestDraft__label">요청 제목</span>
|
<span class="templateRequestDraft__label">티어표 제목</span>
|
||||||
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 업데이트 요청" />
|
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="120" placeholder="예: 템플릿 업데이트 요청" />
|
||||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/80자</span>
|
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/120자</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="templateRequestDraft__field">
|
<label class="templateRequestDraft__field">
|
||||||
<span class="templateRequestDraft__label">요청 설명</span>
|
<span class="templateRequestDraft__label">티어표 설명</span>
|
||||||
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
|
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="1000" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
|
||||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/240자</span>
|
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/1000자</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="toggleSwitch">
|
||||||
|
<input v-model="templateRequestSaveToMyTierList" type="checkbox" />
|
||||||
|
<span class="toggleSwitch__label">내 티어 리스트에도 저장</span>
|
||||||
|
<span class="toggleSwitch__track"><span class="toggleSwitch__thumb"></span></span>
|
||||||
|
</label>
|
||||||
|
<div class="templateRequestDraft__note">
|
||||||
|
저장을 끄면 관리자 확인용 요청 스냅샷만 남고, 현재 작업 중인 티어표는 따로 저장하지 않아요.
|
||||||
|
</div>
|
||||||
</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>
|
||||||
@@ -1449,6 +1476,11 @@ onUnmounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(255, 255, 255, 0.46);
|
color: rgba(255, 255, 255, 0.46);
|
||||||
}
|
}
|
||||||
|
.templateRequestDraft__note {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
.templateRequestDraft__input {
|
.templateRequestDraft__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 14px 0;
|
padding: 14px 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user