Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6828b868bc | |||
| 397461b7c0 |
@@ -1,5 +1,13 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-04-01 v1.3.47
|
||||||
|
- 관리자 `사용자 템플릿 요청`도 결국 검수용 카드이므로, 요청 전용 카드 문법을 따로 두기보다 `전체 티어표 관리`와 같은 카드 구조를 재사용하는 편이 더 직관적이라고 정리했다.
|
||||||
|
- 새 템플릿 생성 요청의 기본 게임 ID는 사람이 읽기 어려운 난수보다 요청 단위에서 유일한 임시값을 먼저 채워두고, 승인 전에 관리자가 수정하는 흐름이 더 현실적이라고 판단했다.
|
||||||
|
|
||||||
|
## 2026-04-01 v1.3.46
|
||||||
|
- 관리자 전체 티어표 카드에서는 좌측 영역 전체를 버튼처럼 만드는 것보다, 실제 썸네일 이미지만 미리보기 진입점으로 읽히게 두는 편이 카드 정보 구조가 덜 흔들린다고 정리했다.
|
||||||
|
- 템플릿 요청 미리보기는 일반 티어표 보기와 다른 요약 레이아웃을 새로 두기보다, 같은 내부 프레임 문법 안에서 보드 자체를 먼저 보여주는 편이 사용자가 더 자연스럽게 이해한다고 판단했다.
|
||||||
|
|
||||||
## 2026-04-01 v1.3.45
|
## 2026-04-01 v1.3.45
|
||||||
- 템플릿 요청에서 `내 티어리스트에도 저장`은 별도 부가 기능이 아니라 실제 저장본 생성 경로를 타므로, 새 저장본 ID는 호출자에 기대지 말고 저장 함수 내부에서 항상 보장하는 편이 더 안전하다고 정리했다.
|
- 템플릿 요청에서 `내 티어리스트에도 저장`은 별도 부가 기능이 아니라 실제 저장본 생성 경로를 타므로, 새 저장본 ID는 호출자에 기대지 말고 저장 함수 내부에서 항상 보장하는 편이 더 안전하다고 정리했다.
|
||||||
- 개발 단계의 내부 조치 문구인 `백엔드 재시작` 같은 표현은 사용자 토스트에 직접 노출하지 않고, 운영형 재시도 안내로 낮추는 편이 맞다고 판단했다.
|
- 개발 단계의 내부 조치 문구인 `백엔드 재시작` 같은 표현은 사용자 토스트에 직접 노출하지 않고, 운영형 재시도 안내로 낮추는 편이 맞다고 판단했다.
|
||||||
|
|||||||
@@ -21,3 +21,5 @@
|
|||||||
|
|
||||||
- 관리자 아이템 라이브러리 이름 변경은 템플릿·사용자 업로드·보관 자산까지 모두 가능하므로, 이후에는 일괄 이름 정리나 중복 이름 감지 보조 기능까지 검토한다.
|
- 관리자 아이템 라이브러리 이름 변경은 템플릿·사용자 업로드·보관 자산까지 모두 가능하므로, 이후에는 일괄 이름 정리나 중복 이름 감지 보조 기능까지 검토한다.
|
||||||
- 관리자 템플릿 요청 미리보기는 실제 완성본 모달과 더 가까운 체감이 되도록, 이후에도 보드 여백·행/열 헤더·남은 아이템 밀도를 한 번 더 비교 QA한다.
|
- 관리자 템플릿 요청 미리보기는 실제 완성본 모달과 더 가까운 체감이 되도록, 이후에도 보드 여백·행/열 헤더·남은 아이템 밀도를 한 번 더 비교 QA한다.
|
||||||
|
- 관리자 템플릿 요청 미리보기는 일반 완성본 보기와 거의 같은 구조로 맞췄으므로, 이후 실제 데이터로 row/column 정렬감과 비어 있는 셀 높이를 한 번 더 비교 QA한다.
|
||||||
|
- 관리자 템플릿 요청 카드와 전체 티어표 카드가 같은 문법으로 맞춰졌으므로, 이후에는 라이트모드까지 포함해 두 카드의 썸네일 높이·입력창 밀도·아이템 그리드를 한 번 더 비교 QA한다.
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-04-01 v1.3.47
|
||||||
|
- 관리자 `사용자 템플릿 요청` 카드는 별도 요청 전용 레이아웃 대신 `전체 티어표 관리`와 같은 카드 문법으로 맞추고, 왼쪽 썸네일 클릭으로 같은 미리보기 모달이 열리도록 정리함.
|
||||||
|
- 새 템플릿 요청에는 썸네일 아래에 `게임 이름 / 게임 ID` 입력을 두고, 초기 `게임 ID`는 `new-template` 대신 요청 ID 기반의 임시 고유값으로 채워 나중에 수정하기 쉽게 바꿈.
|
||||||
|
- 요청 카드 오른쪽에는 제목, 설명, 요청 메타, 추가 아이템 목록, 승인/반려 버튼을 같은 정보 계층으로 배치해 전체 티어표 관리와 읽는 흐름을 통일함.
|
||||||
|
|
||||||
|
## 2026-04-01 v1.3.46
|
||||||
|
- 관리자 `전체 티어표 관리`의 썸네일 영역은 카드 좌측 전체가 눌리는 버튼처럼 보이지 않도록 이미지 영역만 상단에 붙여 클릭 진입점으로 유지하고, 카드 본문과의 시각적 분리를 다시 다듬음.
|
||||||
|
- `템플릿 요청 관리` 미리보기는 별도 썸네일 요약형이 아니라, 제목·설명·행/열 보드·남은 아이템이 하나의 내부 프레임 안에서 이어지는 실제 티어표 완성본형 레이아웃으로 다시 정리함.
|
||||||
|
|
||||||
## 2026-04-01 v1.3.45
|
## 2026-04-01 v1.3.45
|
||||||
- 템플릿 요청에서 `내 티어리스트에도 저장`이 켜져 있을 때 발생하던 500 오류는 새 저장본 생성 시 `tierlists.id`에 `undefined`가 들어가던 문제였고, 이제 `saveTierList()`가 생성 시 자동으로 `nanoid()`를 부여하도록 고쳐 저장 분기 자체를 안정화함.
|
- 템플릿 요청에서 `내 티어리스트에도 저장`이 켜져 있을 때 발생하던 500 오류는 새 저장본 생성 시 `tierlists.id`에 `undefined`가 들어가던 문제였고, 이제 `saveTierList()`가 생성 시 자동으로 `nanoid()`를 부여하도록 고쳐 저장 분기 자체를 안정화함.
|
||||||
- 사용자에게 노출되던 `백엔드를 재시작해주세요` 문구는 제거하고, 저장 분기 실패 시에도 일반적인 재시도 안내만 보이도록 조정함.
|
- 사용자에게 노출되던 `백엔드를 재시작해주세요` 문구는 제거하고, 저장 분기 실패 시에도 일반적인 재시도 안내만 보이도록 조정함.
|
||||||
|
|||||||
@@ -654,11 +654,7 @@ async function refreshTemplateRequests() {
|
|||||||
...request,
|
...request,
|
||||||
draftGameId:
|
draftGameId:
|
||||||
request.type === 'create'
|
request.type === 'create'
|
||||||
? (request.sourceTierListTitle || '')
|
? ('tmpl-' + String(request.id || 'request').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 12).toLowerCase())
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '')
|
|
||||||
.slice(0, 80) || 'new-template'
|
|
||||||
: request.targetGameId || request.sourceGameId || '',
|
: request.targetGameId || request.sourceGameId || '',
|
||||||
draftGameName:
|
draftGameName:
|
||||||
request.type === 'create'
|
request.type === 'create'
|
||||||
@@ -1768,45 +1764,64 @@ async function saveFeaturedOrder() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!templateRequests.length" class="hint">현재 처리 대기 중인 템플릿 요청이 없어요.</div>
|
<div v-if="!templateRequests.length" class="hint">현재 처리 대기 중인 템플릿 요청이 없어요.</div>
|
||||||
<div v-else class="templateRequestList">
|
<div v-else class="templateRequestList">
|
||||||
<article v-for="request in templateRequests" :key="request.id" class="templateRequestCard">
|
<article v-for="request in templateRequests" :key="request.id" class="tierAdminCard templateRequestCard templateRequestCard--aligned">
|
||||||
<div class="templateRequestCard__head">
|
<div class="templateRequestCard__side">
|
||||||
<div>
|
<button class="tierAdminCard__preview templateRequestCard__preview" type="button" @click="openTemplateRequestPreview(request)">
|
||||||
<div class="templateRequestCard__title">{{ request.sourceTierListTitle }}</div>
|
<img v-if="request.thumbnailSrc" class="tierAdminCard__thumb" :src="toApiUrl(request.thumbnailSrc)" :alt="request.sourceTierListTitle" />
|
||||||
<div v-if="request.sourceDescription" class="templateRequestCard__desc">{{ request.sourceDescription }}</div>
|
<div v-else class="tierAdminCard__thumb tierAdminCard__thumb--empty"></div>
|
||||||
<div class="templateRequestCard__meta">
|
</button>
|
||||||
{{ templateRequestTypeLabel(request) }} · {{ request.requesterName }} · {{ fmt(request.createdAt) }}
|
|
||||||
|
<div class="templateRequestCard__thumbMeta">
|
||||||
|
<template v-if="request.type === 'create'">
|
||||||
|
<label class="templateRequestField">
|
||||||
|
<span class="templateRequestField__label">게임 이름</span>
|
||||||
|
<input v-model="request.draftGameName" class="input" placeholder="새 게임 이름" />
|
||||||
|
</label>
|
||||||
|
<label class="templateRequestField">
|
||||||
|
<span class="templateRequestField__label">게임 ID</span>
|
||||||
|
<input v-model="request.draftGameId" class="input" placeholder="임시 게임 ID" />
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="templateRequestCard__thumbLabel">게임 이름</div>
|
||||||
|
<div class="templateRequestCard__thumbValue">{{ request.draftGameName || request.sourceGameName || '-' }}</div>
|
||||||
|
<div class="templateRequestCard__thumbLabel">게임 ID</div>
|
||||||
|
<div class="templateRequestCard__thumbValue">{{ request.draftGameId || request.sourceGameId || '-' }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tierAdminCard__body">
|
||||||
|
<div class="tierAdminCard__head">
|
||||||
|
<div>
|
||||||
|
<div class="tierAdminCard__title">{{ request.sourceTierListTitle }}</div>
|
||||||
|
<div v-if="request.sourceDescription" class="tierAdminCard__desc">{{ request.sourceDescription }}</div>
|
||||||
|
<div class="tierAdminCard__meta">
|
||||||
|
{{ templateRequestTypeLabel(request) }} · {{ request.requesterName }} · {{ fmt(request.createdAt) }}
|
||||||
|
</div>
|
||||||
|
<div class="tierAdminCard__meta">{{ templateRequestTargetLabel(request) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="templateRequestCard__meta">{{ templateRequestTargetLabel(request) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn--ghost btn--small" @click="openTemplateRequestPreview(request)">
|
|
||||||
요청 미리보기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="request.items?.length" class="templateRequestItems">
|
<div class="tierAdminCard__stats">
|
||||||
<div v-for="item in request.items" :key="item.id" class="templateRequestItem">
|
<span class="pill">추가 아이템 {{ request.items?.length || 0 }}개</span>
|
||||||
<img class="templateRequestItem__thumb" :src="toApiUrl(item.src)" :alt="item.label" />
|
<span class="pill">{{ request.type === 'create' ? '새 템플릿' : '기존 템플릿 업데이트' }}</span>
|
||||||
<div class="templateRequestItem__label">{{ item.label }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="request.type === 'create'" class="templateRequestCard__form">
|
<div v-if="request.items?.length" class="tierAdminItemList templateRequestCard__items">
|
||||||
<label class="templateRequestField">
|
<button v-for="item in request.items" :key="item.id" class="tierAdminItem" type="button">
|
||||||
<span class="templateRequestField__label">게임 이름</span>
|
<img class="tierAdminItem__thumb" :src="toApiUrl(item.src)" :alt="item.label" />
|
||||||
<input v-model="request.draftGameName" class="input" placeholder="새 게임 이름" />
|
<div class="tierAdminItem__title">{{ item.label }}</div>
|
||||||
</label>
|
</button>
|
||||||
<label class="templateRequestField">
|
</div>
|
||||||
<span class="templateRequestField__label">게임 ID</span>
|
|
||||||
<input v-model="request.draftGameId" class="input" placeholder="새 게임 ID" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="templateRequestCard__actions">
|
<div class="templateRequestCard__actions">
|
||||||
<button class="btn btn--primary" :disabled="request.isHandling" @click="approveTemplateRequest(request)">
|
<button class="btn btn--primary" :disabled="request.isHandling" @click="approveTemplateRequest(request)">
|
||||||
{{ request.isHandling ? '처리중...' : '승인' }}
|
{{ request.isHandling ? '처리중...' : '승인' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn--danger" :disabled="request.isHandling" @click="rejectTemplateRequest(request)">반려 후 숨김</button>
|
<button class="btn btn--danger" :disabled="request.isHandling" @click="rejectTemplateRequest(request)">반려 후 숨김</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -2226,8 +2241,9 @@ async function saveFeaturedOrder() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="previewTierList?.requestPreview" class="requestPreview">
|
<div v-if="previewTierList?.requestPreview" class="requestPreview">
|
||||||
<div class="requestPreview__hero">
|
<div class="requestPreview__frame">
|
||||||
<div class="requestPreview__heroBody">
|
<div class="requestPreview__header">
|
||||||
|
<div class="requestPreview__title">{{ previewTierList.title || '티어표 미리보기' }}</div>
|
||||||
<div v-if="previewTierList.description" class="requestPreview__desc">{{ previewTierList.description }}</div>
|
<div v-if="previewTierList.description" class="requestPreview__desc">{{ previewTierList.description }}</div>
|
||||||
<div class="requestPreview__meta">
|
<div class="requestPreview__meta">
|
||||||
{{ previewRequestHasColumns(previewTierList) ? (previewRequestColumns(previewTierList).length + '열 구성') : '단일 열 구성' }} ·
|
{{ previewRequestHasColumns(previewTierList) ? (previewRequestColumns(previewTierList).length + '열 구성') : '단일 열 구성' }} ·
|
||||||
@@ -2235,8 +2251,7 @@ async function saveFeaturedOrder() {
|
|||||||
{{ previewTierList.snapshotItems?.length || 0 }}개 아이템
|
{{ previewTierList.snapshotItems?.length || 0 }}개 아이템
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="requestPreview__board requestPreview__board--full">
|
||||||
<div class="requestPreview__board requestPreview__board--full">
|
|
||||||
<div v-if="previewRequestHasColumns(previewTierList)" class="requestPreview__boardHead">
|
<div v-if="previewRequestHasColumns(previewTierList)" class="requestPreview__boardHead">
|
||||||
<div class="requestPreview__rowLabel requestPreview__rowLabel--head">행</div>
|
<div class="requestPreview__rowLabel requestPreview__rowLabel--head">행</div>
|
||||||
<div class="requestPreview__columnLabels" :style="previewRequestGridStyle(previewTierList)">
|
<div class="requestPreview__columnLabels" :style="previewRequestGridStyle(previewTierList)">
|
||||||
@@ -2273,16 +2288,17 @@ async function saveFeaturedOrder() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="previewRequestPoolItems(previewTierList).length" class="requestPreview__pool">
|
<div v-if="previewRequestPoolItems(previewTierList).length" class="requestPreview__pool">
|
||||||
<div class="requestPreview__poolLabel">남은 아이템</div>
|
<div class="requestPreview__poolLabel">남은 아이템</div>
|
||||||
<div class="requestPreview__rowItems requestPreview__rowItems--pool">
|
<div class="requestPreview__rowItems requestPreview__rowItems--pool">
|
||||||
<div
|
<div
|
||||||
v-for="item in previewRequestPoolItems(previewTierList)"
|
v-for="item in previewRequestPoolItems(previewTierList)"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="requestPreview__item requestPreview__item--muted"
|
class="requestPreview__item requestPreview__item--muted"
|
||||||
>
|
>
|
||||||
<img class="requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
|
<img class="requestPreview__itemThumb" :src="toApiUrl(item.src)" :alt="item.label" />
|
||||||
<div v-if="previewTierList.snapshotShowCharacterNames" class="requestPreview__itemLabel">{{ item.label }}</div>
|
<div v-if="previewTierList.snapshotShowCharacterNames" class="requestPreview__itemLabel">{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3825,72 +3841,35 @@ async function saveFeaturedOrder() {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
.templateRequestCard {
|
.templateRequestCard {
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 18px;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
background: var(--theme-pill-bg);
|
|
||||||
}
|
}
|
||||||
.templateRequestCard__head {
|
.templateRequestCard--aligned {
|
||||||
display: flex;
|
align-items: start;
|
||||||
|
}
|
||||||
|
.templateRequestCard__side {
|
||||||
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
justify-content: space-between;
|
align-self: start;
|
||||||
align-items: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
.templateRequestCard__title {
|
.templateRequestCard__preview {
|
||||||
font-weight: 900;
|
align-self: start;
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
.templateRequestCard__desc {
|
.templateRequestCard__thumbMeta {
|
||||||
margin-top: 6px;
|
|
||||||
color: var(--theme-text-muted);
|
|
||||||
line-height: 1.55;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
.templateRequestCard__meta {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.72;
|
|
||||||
}
|
|
||||||
.templateRequestItems {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.templateRequestItem {
|
.templateRequestCard__thumbLabel {
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.templateRequestItem__thumb {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
background: var(--theme-surface-soft);
|
|
||||||
}
|
|
||||||
.templateRequestItem__label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.templateRequestCard__form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.templateRequestField {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.templateRequestField__label {
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--theme-text-faint);
|
color: var(--theme-text-faint);
|
||||||
}
|
}
|
||||||
|
.templateRequestCard__thumbValue {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--theme-text);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.templateRequestCard__items {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
}
|
||||||
.templateRequestCard__actions {
|
.templateRequestCard__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -4028,6 +4007,9 @@ async function saveFeaturedOrder() {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
align-self: start;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.tierAdminCard__thumb {
|
.tierAdminCard__thumb {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -4237,6 +4219,10 @@ async function saveFeaturedOrder() {
|
|||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
.requestPreview__summary,
|
.requestPreview__summary,
|
||||||
|
.requestPreview__frame {
|
||||||
|
padding: 18px;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
.requestPreview__boardHead,
|
.requestPreview__boardHead,
|
||||||
.requestPreview__row {
|
.requestPreview__row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user