릴리스: v1.2.9 관리자 대시보드 디테일 정리

This commit is contained in:
2026-03-30 16:40:09 +09:00
parent d724a64451
commit a232f5b5c9
5 changed files with 171 additions and 55 deletions

View File

@@ -1,5 +1,10 @@
# 의사결정 이력
## 2026-03-30 v1.2.9
- 관리자 화면은 기능보다 먼저 정보 계층이 읽혀야 하므로, 현재 탭에 맞는 요약 통계를 헤더에서 먼저 보여주는 편이 운영 판단에 더 유리하다고 정리했다.
- 게임/아이템/티어표/회원 카드는 기능이 다른 대신 같은 제품 안에 있으므로, 배경층·반경·패딩은 하나의 대시보드 문법으로 맞춰 시안 톤을 더 강하게 유지하기로 결정했다.
- 우측 운영 패널은 단순 필터 모음보다 “현재 상태를 짧게 읽고 바로 액션하는 패널”에 가까워야 하므로, 입력과 통계 카드를 더 단단한 카드형 레이어로 정리하는 편이 맞다고 판단했다.
## 2026-03-30 v1.2.8
- 에디터는 “보드 편집”과 “옵션 편집”의 역할이 다르므로, 보드 옆에는 드래그용 아이템 풀을 두고 제목/설명/썸네일/저장 같은 설정은 최우측 사이드바에만 남기는 편이 맞다고 판단했다.
- 커스텀 아이템 이름 정리는 배치 중에 계속 보는 정보보다 저장 전 정리용 정보에 가까우므로, 아이템 풀 아래보다 우측 편집 패널 내부가 더 적합하다고 정리했다.

View File

@@ -40,6 +40,7 @@
- 관리자 화면
- 공통 우측 패널 대신 전용 로컬 운영 패널을 사용한다.
- 우측 로컬 패널에는 `게임/아이템/티어표/회원 관리` 탭, 검색, 필터, 새로고침, 빠른 작업 제어를 배치하고, 중앙 영역에는 실제 관리 대상 목록과 상세만 렌더링한다.
- 상단 헤더에는 현재 탭 기준 요약 통계 카드를 배치해 운영 상태를 먼저 읽고, 각 관리 카드는 공통 대시보드 카드 문법(두꺼운 반경, 얕은 레이어 배경, 강조된 액션 버튼)을 공유한다.
## DB 스키마
- `users`

View File

@@ -7,6 +7,7 @@
- 공통 셸과 에디터에는 일부 실제 SVG 아이콘을 연결했지만, 아직 즐겨찾기/설정/관리자 등 나머지 내비 아이콘은 임시 선형 SVG이므로 추가 에셋 교체가 남아 있다.
- 공통 우측 패널의 토글과 독립 컬럼 구조는 반영되었지만, 현재는 안내 카드 중심이므로 실제 화면별 기능 컨트롤을 이 패널로 옮기는 작업이 남아 있다.
- 티어표 편집 화면과 관리자 화면 모두 로컬 우측 패널 구조로 옮겼지만, 아직 세부 카드 밀도와 아이콘/모션 디테일은 피그마 시안 수준으로 더 다듬을 필요가 있다.
- 관리자 화면은 헤더 요약 통계와 카드 계층까지 정리됐지만, 아직 표준 SVG 아이콘 교체와 더 세밀한 상태 색상/선택 상태 표현은 남아 있다.
- 머티리얼 아이콘 SVG는 아직 임시 문자/배지 스타일로 대체된 부분이 있으므로, 최종 아이콘 에셋을 받아 반영하는 작업이 필요하다.
- 미사용 커스텀 이미지 일괄 삭제는 현재 "참조가 없는 항목" 기준이며, 보관 기간 정책 같은 운영 규칙은 아직 없다.
- 업로드 이미지는 현재 원본 파일을 그대로 저장하므로, 운영 부담이 커지면 서버 저장 전 리사이즈/압축(예: 긴 변 제한, WebP 변환) 도입이 필요하다.

View File

@@ -1,5 +1,10 @@
# 업데이트 로그
## 2026-03-30 v1.2.9
- **관리자 대시보드 헤더 보강**: 관리자 화면 상단에 현재 탭 기준 요약 통계 카드를 추가해, 게임/아이템/티어표/회원 상태를 즉시 읽을 수 있게 정리
- **운영 패널 질감 정리**: 우측 `320px` 운영 패널의 탭, 입력, 통계 카드, 버튼 라운드/배경/호버 상태를 공통 셸 톤에 맞춰 더 두꺼운 대시보드 카드 문법으로 통일
- **관리 카드 밀도 개선**: 게임 상세, 커스텀 아이템, 템플릿 요청, 전체 티어표, 회원 카드의 배경층·패딩·반경을 함께 다듬어 시안에 가까운 평평한 관리용 레이아웃으로 보정
## 2026-03-30 v1.2.8
- **실제 SVG 아이콘 연결 시작**: 사용자가 추가한 `grid_view`, `lists`, `dock_to_left`, `dock_to_right` 아이콘을 공통 셸 내비와 우측 패널 토글에 연결해 문자 기반 아이콘을 일부 실제 에셋으로 교체
- **에디터 3열 구조 복구**: 티어표 편집 화면을 `보드 / 아이템 풀 / 우측 편집 사이드바` 구조로 재배치해, 아이템 풀은 보드 옆에서 바로 드래그 가능하고 편집 옵션은 최우측 패널에만 남도록 수정

View File

@@ -95,6 +95,45 @@ const activeTabDescription = computed(() => {
}
return '계정 정보, 권한, 비밀번호와 최근 활동을 함께 확인합니다.'
})
const adminOverviewStats = computed(() => {
const publishedTierLists = adminTierLists.value.filter((tierList) => tierList.isPublic).length
const pendingRequests = templateRequests.value.length
const orphanItems = customItems.value.filter((item) => item.usageCount === 0).length
const adminCount = users.value.filter((user) => user.isAdmin || user.draftIsAdmin).length
if (activeTab.value === 'games') {
return [
{ label: '전체 게임', value: `${games.value.length}` },
{ label: '상단 고정', value: `${featuredGameIds.value.length}/50` },
{ label: '선택 상태', value: hasSelectedGame.value ? '활성' : '대기' },
]
}
if (activeTab.value === 'items') {
return [
{ label: '검색 결과', value: `${customItemTotal.value}` },
{ label: '미사용', value: `${orphanItems}` },
{ label: '대상 게임', value: customItemTargetGameId.value ? '선택됨' : '미선택' },
]
}
if (activeTab.value === 'tierlists') {
return tierlistsMode.value === 'requests'
? [
{ label: '대기 요청', value: `${pendingRequests}` },
{ label: '생성 요청', value: `${templateRequests.value.filter((request) => request.type === 'create').length}` },
{ label: '업데이트 요청', value: `${templateRequests.value.filter((request) => request.type === 'update').length}` },
]
: [
{ label: '검색 결과', value: `${adminTierListTotal.value}` },
{ label: '공개 티어표', value: `${publishedTierLists}` },
{ label: '현재 페이지', value: `${adminTierListPage.value}/${adminTierListPageCount.value}` },
]
}
return [
{ label: '가입 회원', value: `${users.value.length}` },
{ label: '관리자', value: `${adminCount}` },
{ label: '활동 계정', value: `${users.value.filter((user) => user.tierListCount > 0).length}` },
]
})
onMounted(async () => {
await auth.refresh()
@@ -897,6 +936,12 @@ async function saveFeaturedOrder() {
<div class="adminHero__eyebrow">Admin Workspace</div>
<h2 class="adminHero__title">{{ activeTabTitle }}</h2>
<p class="adminHero__desc">{{ activeTabDescription }}</p>
<div class="adminHero__stats">
<article v-for="stat in adminOverviewStats" :key="stat.label" class="adminHeroStat">
<span class="adminHeroStat__label">{{ stat.label }}</span>
<strong class="adminHeroStat__value">{{ stat.value }}</strong>
</article>
</div>
</header>
<template v-if="activeTab === 'games'">
@@ -1474,11 +1519,14 @@ async function saveFeaturedOrder() {
}
.adminHero {
display: grid;
gap: 8px;
padding: 18px 20px;
border-radius: 18px;
gap: 10px;
padding: 22px 24px;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)),
rgba(255, 255, 255, 0.02);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.22);
}
.adminHero__eyebrow {
font-size: 11px;
@@ -1498,6 +1546,32 @@ async function saveFeaturedOrder() {
color: rgba(255, 255, 255, 0.66);
line-height: 1.6;
}
.adminHero__stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 6px;
}
.adminHeroStat {
display: grid;
gap: 6px;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(7, 7, 7, 0.18);
}
.adminHeroStat__label {
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.46);
}
.adminHeroStat__value {
font-size: 22px;
line-height: 1;
font-weight: 900;
letter-spacing: -0.04em;
}
.adminSidebar {
position: sticky;
top: 14px;
@@ -1508,10 +1582,13 @@ async function saveFeaturedOrder() {
.adminSidebar__panel {
display: grid;
gap: 12px;
padding: 14px;
border-radius: 18px;
padding: 16px;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(17, 17, 17, 0.9);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.015)),
rgba(13, 13, 13, 0.94);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
}
.adminSidebar__label {
font-size: 11px;
@@ -1534,8 +1611,8 @@ async function saveFeaturedOrder() {
.sidebarStat {
display: grid;
gap: 4px;
padding: 10px 12px;
border-radius: 12px;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
}
@@ -1589,18 +1666,29 @@ async function saveFeaturedOrder() {
}
.tab,
.modeTab {
padding: 10px 14px;
border-radius: 10px;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
font-weight: 800;
transition:
border-color 0.16s ease,
background 0.16s ease,
transform 0.16s ease;
}
.tab:hover,
.modeTab:hover {
border-color: rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.07);
transform: translateY(-1px);
}
.tab--active,
.modeTab--active {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.18);
background: rgba(96, 165, 250, 0.14);
border-color: rgba(96, 165, 250, 0.28);
color: rgba(239, 246, 255, 0.98);
}
.adminSidebar__tabs .tab,
.modeTabs--stack .modeTab {
@@ -1609,9 +1697,12 @@ async function saveFeaturedOrder() {
}
.panel {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(48, 48, 48, 0.78);
border-radius: 18px;
padding: 16px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.018)),
rgba(34, 34, 34, 0.84);
border-radius: 24px;
padding: 18px;
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.18);
}
.panel--empty {
min-height: 240px;
@@ -1644,9 +1735,9 @@ async function saveFeaturedOrder() {
.featuredOrderPanel__list,
.featuredOrderPanel__picker {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 14px;
background: rgba(255, 255, 255, 0.025);
border-radius: 18px;
padding: 16px;
}
.featuredList,
.featuredPickerList {
@@ -1661,10 +1752,10 @@ async function saveFeaturedOrder() {
gap: 12px;
justify-content: space-between;
align-items: center;
padding: 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.18);
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.22);
}
.featuredCard__meta {
display: flex;
@@ -1706,9 +1797,9 @@ async function saveFeaturedOrder() {
gap: 10px;
justify-content: space-between;
align-items: center;
padding: 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.16);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
@@ -1738,9 +1829,9 @@ async function saveFeaturedOrder() {
}
.adminCard {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 14px;
background: rgba(255, 255, 255, 0.025);
border-radius: 18px;
padding: 16px;
min-width: 0;
}
.sectionHeader {
@@ -1783,8 +1874,8 @@ async function saveFeaturedOrder() {
.input {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border-radius: 12px;
padding: 11px 13px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
color: rgba(255, 255, 255, 0.92);
@@ -1826,8 +1917,8 @@ async function saveFeaturedOrder() {
.btn {
font-size: 12px;
margin-top: 12px;
padding: 10px 12px;
border-radius: 12px;
padding: 11px 13px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.92);
@@ -1835,6 +1926,15 @@ async function saveFeaturedOrder() {
font-weight: 800;
text-align: center;
text-decoration: none;
transition:
background 0.16s ease,
border-color 0.16s ease,
transform 0.16s ease;
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.1);
}
.btn--small {
margin-top: 0;
@@ -1853,6 +1953,7 @@ async function saveFeaturedOrder() {
}
.btn--primary {
background: rgba(96, 165, 250, 0.2);
border-color: rgba(96, 165, 250, 0.26);
}
.btn--danger {
background: rgba(239, 68, 68, 0.14);
@@ -1938,8 +2039,8 @@ async function saveFeaturedOrder() {
flex-wrap: wrap;
}
.itemPreviewCard {
padding: 10px;
border-radius: 14px;
padding: 12px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
}
@@ -1977,9 +2078,9 @@ async function saveFeaturedOrder() {
}
.thumbCard {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.04);
padding: 10px;
padding: 12px;
min-width: 0;
}
.thumb {
@@ -2012,18 +2113,18 @@ async function saveFeaturedOrder() {
.customItemGrid {
margin-top: 14px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.customItemCard {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
overflow: hidden;
display: flex;
gap: 12px;
align-items: flex-start;
padding: 12px;
padding: 14px;
min-width: 0;
}
.customItemCard__image {
@@ -2081,9 +2182,9 @@ async function saveFeaturedOrder() {
}
.userCard {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
padding: 14px;
padding: 16px;
}
.userCard__head {
display: flex;
@@ -2134,8 +2235,8 @@ async function saveFeaturedOrder() {
.userStat {
display: grid;
gap: 4px;
padding: 10px 12px;
border-radius: 12px;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
}
@@ -2176,8 +2277,8 @@ async function saveFeaturedOrder() {
.templateRequestCard {
display: grid;
gap: 14px;
padding: 16px;
border-radius: 18px;
padding: 18px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03);
}
@@ -2242,9 +2343,9 @@ async function saveFeaturedOrder() {
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
gap: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 18px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.03);
padding: 14px;
padding: 16px;
}
.tierAdminCard__preview {
cursor: pointer;
@@ -2304,8 +2405,8 @@ async function saveFeaturedOrder() {
.tierAdminSection {
display: grid;
gap: 10px;
padding: 12px;
border-radius: 14px;
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.14);
}
@@ -2326,8 +2427,8 @@ async function saveFeaturedOrder() {
display: grid;
gap: 8px;
justify-items: center;
padding: 10px;
border-radius: 12px;
padding: 12px 10px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.92);
@@ -2363,8 +2464,8 @@ async function saveFeaturedOrder() {
width: min(560px, 100%);
display: grid;
gap: 14px;
padding: 18px;
border-radius: 20px;
padding: 20px;
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(11, 18, 32, 0.96);
}
@@ -2425,6 +2526,9 @@ async function saveFeaturedOrder() {
.adminWorkspace {
grid-template-columns: 1fr;
}
.adminHero__stats {
grid-template-columns: 1fr;
}
.adminSidebar {
position: static;
order: -1;