릴리스: v1.2.15 공통 3단 셸 구조 고정
This commit is contained in:
@@ -22,7 +22,6 @@ provide('localRightRailTarget', '#local-right-rail-root')
|
||||
const isAdmin = computed(() => !!auth.user?.isAdmin)
|
||||
const isPreviewMode = computed(() => route.query.preview === '1')
|
||||
const usesLocalRightRail = computed(() => ['editEditor', 'newEditor', 'admin'].includes(String(route.name || '')))
|
||||
const usesShellRightRail = computed(() => ['editEditor', 'newEditor'].includes(String(route.name || '')))
|
||||
const avatarUrl = computed(() => (auth.user?.avatarSrc ? toApiUrl(auth.user.avatarSrc) : ''))
|
||||
const accountName = computed(() => {
|
||||
const nickname = (auth.user?.nickname || '').trim()
|
||||
@@ -202,7 +201,6 @@ async function logout() {
|
||||
:class="{
|
||||
'appShell--preview': isPreviewMode,
|
||||
'appShell--rightClosed': !rightRailOpen,
|
||||
'appShell--localRail': usesLocalRightRail,
|
||||
}"
|
||||
>
|
||||
<template v-if="isPreviewMode">
|
||||
@@ -294,16 +292,8 @@ async function logout() {
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside
|
||||
v-if="!usesLocalRightRail || usesShellRightRail"
|
||||
class="rightRail"
|
||||
:class="{ 'rightRail--closed': !rightRailOpen }"
|
||||
:aria-hidden="!rightRailOpen"
|
||||
>
|
||||
<template v-if="usesShellRightRail">
|
||||
<div id="local-right-rail-root" class="localRightRailRoot"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<aside class="rightRail" :class="{ 'rightRail--closed': !rightRailOpen }" :aria-hidden="!rightRailOpen">
|
||||
<template v-if="!usesLocalRightRail">
|
||||
<div class="rightRail__top">
|
||||
<button class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="상태">
|
||||
<img :src="iconGridView" alt="" />
|
||||
@@ -340,6 +330,7 @@ async function logout() {
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
<div id="local-right-rail-root" class="localRightRailRoot"></div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -371,14 +362,6 @@ async function logout() {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.appShell--localRail {
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.appShell--localRail.appShell--rightClosed {
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.leftRail,
|
||||
.rightRail {
|
||||
min-height: 100vh;
|
||||
@@ -900,14 +883,6 @@ async function logout() {
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.appShell--localRail {
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.appShell--localRail.appShell--rightClosed {
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.rightRail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import Sortable from 'sortablejs'
|
||||
import { api } from '../lib/api'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
@@ -8,6 +8,8 @@ import { useToast } from '../composables/useToast'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
const globalRightRailOpen = inject('rightRailOpen', ref(true))
|
||||
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
|
||||
const isAdmin = computed(() => !!auth.user?.isAdmin)
|
||||
|
||||
const activeTab = ref('games')
|
||||
@@ -1340,165 +1342,167 @@ async function saveFeaturedOrder() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="adminSidebar">
|
||||
<section class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Mode</div>
|
||||
<div class="adminSidebar__tabs">
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'games' }" @click="setTab('games')">게임 관리</button>
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'items' }" @click="setTab('items')">아이템 관리</button>
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'tierlists' }" @click="setTab('tierlists')">티어표 관리</button>
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'users' }" @click="setTab('users')">회원 관리</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab === 'games'" class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Game Flow</div>
|
||||
<div class="modeTabs modeTabs--stack">
|
||||
<button class="modeTab" :class="{ 'modeTab--active': gameMode === 'existing' }" @click="setGameMode('existing')">
|
||||
등록된 게임 선택
|
||||
</button>
|
||||
<button class="modeTab" :class="{ 'modeTab--active': gameMode === 'new' }" @click="setGameMode('new')">
|
||||
새 게임 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="gameMode === 'existing'" class="adminSidebar__group">
|
||||
<div class="adminSidebar__groupTitle">선택할 게임</div>
|
||||
<select v-model="selectedGameId" class="select" @change="loadGame">
|
||||
<option value="">게임을 선택해주세요</option>
|
||||
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }} ({{ game.id }})</option>
|
||||
</select>
|
||||
<button class="btn btn--ghost" @click="refreshGames">게임 목록 새로고침</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="adminSidebar__group">
|
||||
<div class="adminSidebar__groupTitle">새 게임 만들기</div>
|
||||
<input v-model="newGameId" class="input" placeholder="game id (영문/숫자)" />
|
||||
<input v-model="newGameName" class="input" placeholder="게임 이름" />
|
||||
<button class="btn btn--primary" @click="createGame">게임 생성</button>
|
||||
</div>
|
||||
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">전체 게임</span>
|
||||
<strong class="sidebarStat__value">{{ games.length }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">상단 고정</span>
|
||||
<strong class="sidebarStat__value">{{ featuredGameIds.length }}/50</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'items'" class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Filters</div>
|
||||
<div class="adminSidebar__group">
|
||||
<input v-model="customItemQuery" class="input" placeholder="파일명, 라벨, 업로더 검색" @keydown.enter.prevent="submitCustomItemSearch" />
|
||||
<button class="btn btn--ghost" @click="submitCustomItemSearch">검색</button>
|
||||
</div>
|
||||
<div class="adminSidebar__group">
|
||||
<select :value="customItemLimit" class="select" @change="changeCustomItemLimit(Number($event.target.value))">
|
||||
<option :value="50">50개씩 보기</option>
|
||||
<option :value="200">200개씩 보기</option>
|
||||
</select>
|
||||
<select v-model="customItemTargetGameId" class="select">
|
||||
<option value="">가져올 게임 선택</option>
|
||||
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
|
||||
</select>
|
||||
<label class="checkRow checkRow--compact">
|
||||
<input v-model="customItemOrphanOnly" type="checkbox" @change="toggleCustomItemOrphanOnly" />
|
||||
<span>미사용 커스텀 이미지만 보기</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshCustomItems">새로고침</button>
|
||||
<button class="btn btn--danger" :disabled="!customItems.length" @click="removeUnusedCustomItems">미사용 이미지 일괄 삭제</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">현재 페이지</span>
|
||||
<strong class="sidebarStat__value">{{ customItemPage }}/{{ customItemPageCount }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">검색 결과</span>
|
||||
<strong class="sidebarStat__value">{{ customItemTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'tierlists'" class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Tierlists</div>
|
||||
<div class="modeTabs modeTabs--stack">
|
||||
<button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'requests' }" @click="setTierlistsMode('requests')">
|
||||
템플릿 요청 관리
|
||||
</button>
|
||||
<button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'lists' }" @click="setTierlistsMode('lists')">
|
||||
전체 티어표 관리
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="tierlistsMode === 'requests'">
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshTemplateRequests">요청 새로고침</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">대기 요청</span>
|
||||
<strong class="sidebarStat__value">{{ templateRequests.length }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="adminSidebar__group">
|
||||
<input
|
||||
v-model="adminTierListQuery"
|
||||
class="input"
|
||||
placeholder="제목, 작성자, 게임 이름 검색"
|
||||
@keydown.enter.prevent="submitAdminTierListSearch"
|
||||
/>
|
||||
<button class="btn btn--ghost" @click="submitAdminTierListSearch">검색</button>
|
||||
<select :value="adminTierListLimit" class="select" @change="changeAdminTierListLimit(Number($event.target.value))">
|
||||
<option :value="50">50개씩 보기</option>
|
||||
<option :value="200">200개씩 보기</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshAdminTierLists">새로고침</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">현재 페이지</span>
|
||||
<strong class="sidebarStat__value">{{ adminTierListPage }}/{{ adminTierListPageCount }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">검색 결과</span>
|
||||
<strong class="sidebarStat__value">{{ adminTierListTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section v-else class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Users</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshUsers">회원 새로고침</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">가입 회원</span>
|
||||
<strong class="sidebarStat__value">{{ users.length }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">관리자 수</span>
|
||||
<strong class="sidebarStat__value">{{ users.filter((user) => user.isAdmin).length }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Teleport :to="localRightRailTarget">
|
||||
<aside v-show="globalRightRailOpen" class="adminSidebar">
|
||||
<section class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Mode</div>
|
||||
<div class="adminSidebar__tabs">
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'games' }" @click="setTab('games')">게임 관리</button>
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'items' }" @click="setTab('items')">아이템 관리</button>
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'tierlists' }" @click="setTab('tierlists')">티어표 관리</button>
|
||||
<button class="tab" :class="{ 'tab--active': activeTab === 'users' }" @click="setTab('users')">회원 관리</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab === 'games'" class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Game Flow</div>
|
||||
<div class="modeTabs modeTabs--stack">
|
||||
<button class="modeTab" :class="{ 'modeTab--active': gameMode === 'existing' }" @click="setGameMode('existing')">
|
||||
등록된 게임 선택
|
||||
</button>
|
||||
<button class="modeTab" :class="{ 'modeTab--active': gameMode === 'new' }" @click="setGameMode('new')">
|
||||
새 게임 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="gameMode === 'existing'" class="adminSidebar__group">
|
||||
<div class="adminSidebar__groupTitle">선택할 게임</div>
|
||||
<select v-model="selectedGameId" class="select" @change="loadGame">
|
||||
<option value="">게임을 선택해주세요</option>
|
||||
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }} ({{ game.id }})</option>
|
||||
</select>
|
||||
<button class="btn btn--ghost" @click="refreshGames">게임 목록 새로고침</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="adminSidebar__group">
|
||||
<div class="adminSidebar__groupTitle">새 게임 만들기</div>
|
||||
<input v-model="newGameId" class="input" placeholder="game id (영문/숫자)" />
|
||||
<input v-model="newGameName" class="input" placeholder="게임 이름" />
|
||||
<button class="btn btn--primary" @click="createGame">게임 생성</button>
|
||||
</div>
|
||||
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">전체 게임</span>
|
||||
<strong class="sidebarStat__value">{{ games.length }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">상단 고정</span>
|
||||
<strong class="sidebarStat__value">{{ featuredGameIds.length }}/50</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'items'" class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Filters</div>
|
||||
<div class="adminSidebar__group">
|
||||
<input v-model="customItemQuery" class="input" placeholder="파일명, 라벨, 업로더 검색" @keydown.enter.prevent="submitCustomItemSearch" />
|
||||
<button class="btn btn--ghost" @click="submitCustomItemSearch">검색</button>
|
||||
</div>
|
||||
<div class="adminSidebar__group">
|
||||
<select :value="customItemLimit" class="select" @change="changeCustomItemLimit(Number($event.target.value))">
|
||||
<option :value="50">50개씩 보기</option>
|
||||
<option :value="200">200개씩 보기</option>
|
||||
</select>
|
||||
<select v-model="customItemTargetGameId" class="select">
|
||||
<option value="">가져올 게임 선택</option>
|
||||
<option v-for="game in games" :key="game.id" :value="game.id">{{ game.name }}</option>
|
||||
</select>
|
||||
<label class="checkRow checkRow--compact">
|
||||
<input v-model="customItemOrphanOnly" type="checkbox" @change="toggleCustomItemOrphanOnly" />
|
||||
<span>미사용 커스텀 이미지만 보기</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshCustomItems">새로고침</button>
|
||||
<button class="btn btn--danger" :disabled="!customItems.length" @click="removeUnusedCustomItems">미사용 이미지 일괄 삭제</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">현재 페이지</span>
|
||||
<strong class="sidebarStat__value">{{ customItemPage }}/{{ customItemPageCount }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">검색 결과</span>
|
||||
<strong class="sidebarStat__value">{{ customItemTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'tierlists'" class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Tierlists</div>
|
||||
<div class="modeTabs modeTabs--stack">
|
||||
<button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'requests' }" @click="setTierlistsMode('requests')">
|
||||
템플릿 요청 관리
|
||||
</button>
|
||||
<button class="modeTab" :class="{ 'modeTab--active': tierlistsMode === 'lists' }" @click="setTierlistsMode('lists')">
|
||||
전체 티어표 관리
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="tierlistsMode === 'requests'">
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshTemplateRequests">요청 새로고침</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">대기 요청</span>
|
||||
<strong class="sidebarStat__value">{{ templateRequests.length }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="adminSidebar__group">
|
||||
<input
|
||||
v-model="adminTierListQuery"
|
||||
class="input"
|
||||
placeholder="제목, 작성자, 게임 이름 검색"
|
||||
@keydown.enter.prevent="submitAdminTierListSearch"
|
||||
/>
|
||||
<button class="btn btn--ghost" @click="submitAdminTierListSearch">검색</button>
|
||||
<select :value="adminTierListLimit" class="select" @change="changeAdminTierListLimit(Number($event.target.value))">
|
||||
<option :value="50">50개씩 보기</option>
|
||||
<option :value="200">200개씩 보기</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshAdminTierLists">새로고침</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">현재 페이지</span>
|
||||
<strong class="sidebarStat__value">{{ adminTierListPage }}/{{ adminTierListPageCount }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">검색 결과</span>
|
||||
<strong class="sidebarStat__value">{{ adminTierListTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section v-else class="adminSidebar__panel">
|
||||
<div class="adminSidebar__label">Users</div>
|
||||
<div class="adminSidebar__actions">
|
||||
<button class="btn btn--ghost" @click="refreshUsers">회원 새로고침</button>
|
||||
</div>
|
||||
<div class="adminSidebar__stats">
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">가입 회원</span>
|
||||
<strong class="sidebarStat__value">{{ users.length }}</strong>
|
||||
</div>
|
||||
<div class="sidebarStat">
|
||||
<span class="sidebarStat__label">관리자 수</span>
|
||||
<strong class="sidebarStat__value">{{ users.filter((user) => user.isAdmin).length }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -1508,7 +1512,7 @@ async function saveFeaturedOrder() {
|
||||
}
|
||||
.adminWorkspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
@@ -1573,9 +1577,6 @@ async function saveFeaturedOrder() {
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.adminSidebar {
|
||||
position: sticky;
|
||||
top: 14px;
|
||||
align-self: start;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -2523,15 +2524,11 @@ async function saveFeaturedOrder() {
|
||||
margin-top: 0;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.adminWorkspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.adminHero__stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.adminSidebar {
|
||||
position: static;
|
||||
order: -1;
|
||||
display: none;
|
||||
}
|
||||
.featuredOrderPanel,
|
||||
.section--topGrid,
|
||||
|
||||
Reference in New Issue
Block a user