Compare commits

...

3 Commits

6 changed files with 110 additions and 40 deletions

View File

@@ -1,5 +1,17 @@
# 의사결정 이력
## 2026-03-30 v1.2.14
- 에디터 우측 패널은 본문 내부 그리드의 일부가 아니라 공통 셸의 세 번째 컬럼이어야 메인 화면과 같은 구조로 읽히므로, Teleport로 셸 aside에 직접 붙이는 편이 맞다고 정리했다.
- 로컬 우측 패널 화면에서 “메인 안쪽 2단 레이아웃”과 “셸 3단 레이아웃”을 섞으면 계속 혼선이 생기므로, 에디터는 셸 레벨 3단 구조를 우선 기준으로 삼기로 결정했다.
## 2026-03-30 v1.2.13
- 공통 상태를 로컬 우측 패널에 연결할 때는 템플릿의 ref 자동 언래핑을 고려해야 하므로, 템플릿에서는 `.value` 없이 직접 참조하는 편이 안전하다고 다시 정리했다.
- 이번 회귀처럼 편집 화면이 통째로 무너질 수 있는 연결점은 작은 레이아웃 수정이어도 바로 복구 릴리스로 끊는 편이 낫다고 판단했다.
## 2026-03-30 v1.2.12
- 공통 상단의 패널 토글은 로컬 우측 패널 화면에서도 같은 의미로 동작해야 하므로, 에디터의 `editorSidebar`도 같은 상태를 공유해 접고 펴는 편이 일관된다고 판단했다.
- 로컬 우측 패널 화면에 공통 `rightClosed` 그리드 계산이 다시 들어오면 컬럼 수가 꼬일 수 있으므로, 에디터/관리자 화면은 셸 차원에서 별도 예외 컬럼 규칙을 유지하기로 결정했다.
## 2026-03-30 v1.2.11
- 에디터와 관리자처럼 자체 우측 패널이 있는 화면은 공통 `workspaceBody` 카드 배경 안에 다시 넣기보다, 셸 레벨에서 중앙 본문을 투명하게 풀어주는 편이 우측 사이드바 독립성이 더 잘 살아난다고 판단했다.
- 로컬 우측 패널의 핵심은 “본문 안쪽 보조 박스”가 아니라 “진짜 오른쪽 컬럼”처럼 읽히는 것이므로, 에디터에서는 본문 카드보다 패널 분리감을 먼저 확보하기로 결정했다.

View File

@@ -36,7 +36,8 @@
- 공통 토글 버튼으로 패널을 접으면 중앙 워크스페이스가 남는 공간을 확장 사용한다.
- 티어표 편집 화면
- 공통 우측 패널 대신 전용 로컬 편집 패널을 사용한다.
- 공통 `workspaceBody` 카드 컨테이너를 벗기고, 중앙 보드 영역 우측 `320px` 편집 패널이 별도 컬럼처럼 보이도록 배치한다.
- 공통 `workspaceBody` 카드 컨테이너를 벗기고, 중앙 보드 영역은 메인 컬럼에, 우측 `320px` 편집 패널은 공통 셸의 세 번째 컬럼 aside에 배치한다.
- 공통 상단 토글 버튼은 Teleport로 이동한 로컬 편집 패널의 접힘/펼침 상태와도 연결되어, 우측 패널을 숨기면 중앙 보드 영역이 확장된다.
- 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 우측 로컬 패널에 배치한다.
- 보드 바로 옆에는 드래그용 아이템 풀을 별도 패널로 두고, 커스텀 아이템 이름 정리 목록은 우측 편집 패널 내부에서 관리한다.
- 관리자 화면

View File

@@ -9,6 +9,8 @@
- 공통 우측 패널의 토글과 독립 컬럼 구조는 반영되었지만, 현재는 안내 카드 중심이므로 실제 화면별 기능 컨트롤을 이 패널로 옮기는 작업이 남아 있다.
- 티어표 편집 화면과 관리자 화면 모두 로컬 우측 패널 구조로 옮겼지만, 아직 세부 카드 밀도와 아이콘/모션 디테일은 피그마 시안 수준으로 더 다듬을 필요가 있다.
- 에디터/관리자 로컬 우측 패널은 셸 카드에서 분리됐지만, 아직 실제 피그마처럼 패널 토글 전환 모션과 상태 강조가 더 필요하다.
- 에디터 로컬 우측 패널은 공통 토글과 연결됐지만, 아직 완전한 피그마 수준의 패널 애니메이션과 내부 카드 재배치는 더 다듬을 필요가 있다.
- 에디터 우측 패널은 셸의 세 번째 컬럼으로 옮겼지만, 내부 카드 간격과 섹션 구분선은 아직 첨부 시안처럼 더 촘촘하게 정리할 필요가 있다.
- 관리자 화면은 헤더 요약 통계와 카드 계층까지 정리됐지만, 아직 표준 SVG 아이콘 교체와 더 세밀한 상태 색상/선택 상태 표현은 남아 있다.
- 머티리얼 아이콘 SVG는 아직 임시 문자/배지 스타일로 대체된 부분이 있으므로, 최종 아이콘 에셋을 받아 반영하는 작업이 필요하다.
- 미사용 커스텀 이미지 일괄 삭제는 현재 "참조가 없는 항목" 기준이며, 보관 기간 정책 같은 운영 규칙은 아직 없다.

View File

@@ -1,5 +1,16 @@
# 업데이트 로그
## 2026-03-30 v1.2.14
- **에디터 우측 패널 셸 컬럼 이관**: 티어표 편집 화면의 `editorSidebar``workspaceBody` 내부 보조 칼럼이 아니라 공통 셸의 세 번째 컬럼으로 옮겨, 메인 화면과 같은 `왼쪽 사이드 | 메인 | 오른쪽 사이드` 구조를 사용하도록 재배치
- **공통 토글과 실제 aside 연결**: 상단 패널 토글 버튼은 이제 Teleport로 이동한 에디터 우측 aside를 직접 접고 펴며, 본문 내부 2단 레이아웃처럼 보이던 구조를 제거
## 2026-03-30 v1.2.13
- **에디터 우측 패널 회귀 수정**: 공통 패널 상태를 템플릿에서 잘못 참조해 `editorSidebar`가 항상 닫힌 상태로 계산되던 문제를 수정해, 제목/설명/썸네일/저장 패널이 다시 정상 표시되도록 복구
## 2026-03-30 v1.2.12
- **에디터 우측 패널 토글 연결**: 공통 상단의 패널 토글 버튼이 이제 티어표 편집 화면의 `editorSidebar`에도 직접 연결되어, 숨기면 우측 패널이 접히고 중앙 보드 영역이 넓어지도록 수정
- **로컬 우측 패널 컬럼 충돌 방지**: 에디터/관리자처럼 로컬 우측 패널을 쓰는 화면에서는 공통 `rightClosed` 셸 컬럼 계산이 다시 끼어들지 않도록 예외 처리를 추가해 레이아웃이 다시 틀어지지 않게 보정
## 2026-03-30 v1.2.11
- **에디터 로컬 우측 패널 분리 보정**: 에디터/관리자처럼 로컬 우측 패널을 쓰는 화면은 공통 `workspaceBody` 카드 컨테이너를 벗기고, 로컬 패널이 중앙 본문 안쪽이 아니라 독립 컬럼처럼 보이도록 셸 구조를 조정
- **에디터 우측 컬럼 간격 보정**: 티어표 편집 화면의 `editorSidebar`가 본문 내부 보조 박스처럼 눌리지 않도록 간격과 최소 폭을 정리해 우측 사이드바 역할이 더 분명하게 보이도록 수정

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from './stores/auth'
import { toApiUrl } from './lib/runtime'
@@ -16,10 +16,13 @@ const { toasts, dismissToast } = useToast()
const menuOpen = ref(false)
const rightRailOpen = ref(true)
provide('rightRailOpen', rightRailOpen)
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()
@@ -292,46 +295,51 @@ async function logout() {
</main>
<aside
v-if="!usesLocalRightRail"
v-if="!usesLocalRightRail || usesShellRightRail"
class="rightRail"
:class="{ 'rightRail--closed': !rightRailOpen }"
:aria-hidden="!rightRailOpen"
>
<div class="rightRail__top">
<button class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="상태">
<img :src="iconGridView" alt="" />
</button>
</div>
<section class="contextCard">
<div class="contextCard__label">Context</div>
<h2 class="contextCard__title">{{ routeMeta.contextTitle }}</h2>
<p class="contextCard__text">{{ routeMeta.contextText }}</p>
<button class="contextCard__action" type="button" @click="routeMeta.action">
{{ routeMeta.actionLabel }}
</button>
</section>
<section class="contextCard">
<div class="contextCard__label">Account</div>
<div class="contextStat">
<span class="contextStat__name">현재 사용자</span>
<span class="contextStat__value">{{ accountName }}</span>
<template v-if="usesShellRightRail">
<div id="local-right-rail-root" class="localRightRailRoot"></div>
</template>
<template v-else>
<div class="rightRail__top">
<button class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="상태">
<img :src="iconGridView" alt="" />
</button>
</div>
<div class="contextStat">
<span class="contextStat__name">권한</span>
<span class="contextStat__value">{{ isAdmin ? 'Admin' : auth.user ? 'Member' : 'Guest' }}</span>
</div>
</section>
<section class="contextCard contextCard--links">
<div class="contextCard__label">Jump</div>
<button class="contextLink" type="button" @click="$router.push('/')">
<svg viewBox="0 0 24 24" aria-hidden="true"><path :d="railGlyph('link')" /></svg>
<span>게임 목록으로</span>
</button>
<button v-if="auth.user" class="contextLink" type="button" @click="$router.push('/me')">
<svg viewBox="0 0 24 24" aria-hidden="true"><path :d="railGlyph('link')" /></svg>
<span> 티어표 열기</span>
</button>
</section>
<section class="contextCard">
<div class="contextCard__label">Context</div>
<h2 class="contextCard__title">{{ routeMeta.contextTitle }}</h2>
<p class="contextCard__text">{{ routeMeta.contextText }}</p>
<button class="contextCard__action" type="button" @click="routeMeta.action">
{{ routeMeta.actionLabel }}
</button>
</section>
<section class="contextCard">
<div class="contextCard__label">Account</div>
<div class="contextStat">
<span class="contextStat__name">현재 사용자</span>
<span class="contextStat__value">{{ accountName }}</span>
</div>
<div class="contextStat">
<span class="contextStat__name">권한</span>
<span class="contextStat__value">{{ isAdmin ? 'Admin' : auth.user ? 'Member' : 'Guest' }}</span>
</div>
</section>
<section class="contextCard contextCard--links">
<div class="contextCard__label">Jump</div>
<button class="contextLink" type="button" @click="$router.push('/')">
<svg viewBox="0 0 24 24" aria-hidden="true"><path :d="railGlyph('link')" /></svg>
<span>게임 목록으로</span>
</button>
<button v-if="auth.user" class="contextLink" type="button" @click="$router.push('/me')">
<svg viewBox="0 0 24 24" aria-hidden="true"><path :d="railGlyph('link')" /></svg>
<span> 티어표 열기</span>
</button>
</section>
</template>
</aside>
</template>
@@ -367,6 +375,10 @@ async function logout() {
grid-template-columns: 248px minmax(0, 1fr);
}
.appShell--localRail.appShell--rightClosed {
grid-template-columns: 248px minmax(0, 1fr);
}
.leftRail,
.rightRail {
min-height: 100vh;
@@ -738,6 +750,10 @@ async function logout() {
gap: 18px;
}
.localRightRailRoot {
min-height: calc(100vh - 40px);
}
.contextCard {
display: grid;
gap: 12px;
@@ -888,6 +904,10 @@ async function logout() {
grid-template-columns: 220px minmax(0, 1fr);
}
.appShell--localRail.appShell--rightClosed {
grid-template-columns: 220px minmax(0, 1fr);
}
.rightRail {
display: none;
}

View File

@@ -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 { useRoute, useRouter } from 'vue-router'
import Sortable from 'sortablejs'
import * as htmlToImage from 'html-to-image'
@@ -12,6 +12,8 @@ const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const toast = useToast()
const globalRightRailOpen = inject('rightRailOpen', ref(true))
const localRightRailTarget = inject('localRightRailTarget', '#local-right-rail-root')
const gameId = computed(() => route.params.gameId)
const tierListId = computed(() => route.params.tierListId)
const previewMode = computed(() => route.query.preview === '1')
@@ -780,7 +782,10 @@ onUnmounted(() => {
</div>
</div>
<aside class="editorSidebar">
</section>
<Teleport :to="localRightRailTarget">
<aside v-show="globalRightRailOpen" class="editorSidebar">
<div class="editorSidebar__section">
<div class="editorSidebar__label">Title</div>
<input v-model="title" class="editorSidebar__input" placeholder="Title Text" :readonly="!canEdit" />
@@ -864,7 +869,7 @@ onUnmounted(() => {
</button>
</div>
</aside>
</section>
</Teleport>
</template>
</template>
@@ -1039,6 +1044,10 @@ onUnmounted(() => {
grid-template-columns: minmax(0, 1fr) 320px;
gap: 18px;
align-items: start;
transition: grid-template-columns 220ms ease;
}
.layout--railClosed {
grid-template-columns: minmax(0, 1fr) 0;
}
.error {
margin: 10px 0 14px;
@@ -1357,6 +1366,21 @@ onUnmounted(() => {
position: sticky;
top: 14px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
transition:
opacity 220ms ease,
transform 220ms ease,
padding 220ms ease,
border-color 220ms ease,
background 220ms ease;
}
.layout--railClosed .editorSidebar {
opacity: 0;
transform: translateX(18px);
pointer-events: none;
overflow: hidden;
padding-left: 0;
padding-right: 0;
border-color: transparent;
}
.editorSidebar__section {
display: grid;