Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 876c13d99b |
@@ -1,5 +1,10 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-03-30 v1.2.22
|
||||
- 왼쪽 레일은 홈/목록/에디터 어디서든 “사라지는 패널”보다 “축소된 내비 레일”로 읽히는 편이 구조적으로 더 일관되므로, 완전 숨김 대신 아이콘 중심 축소 상태를 유지하기로 했다.
|
||||
- 좌우 패널 토글은 상태마다 다른 아이콘이 바뀌기보다 방향만 고정하는 편이 덜 혼란스러우므로, 우측은 `dock_to_left`, 좌측은 `dock_to_right` 하나로 통일하기로 정리했다.
|
||||
- 좌측 검색도 임시 선형 SVG보다 실제 에셋을 쓰는 편이 전체 레일 완성도가 높으므로, 사용자가 추가한 `search.svg`를 우선 적용하기로 했다.
|
||||
|
||||
## 2026-03-30 v1.2.21
|
||||
- 티어표 목록 카드는 페이지마다 다른 메타 구성을 두기보다, `제목+좋아요 / 작성자+최종 수정일` 두 줄 문법으로 통일하는 편이 시안과 사용성 모두에 더 맞다고 정리했다.
|
||||
- `내 즐겨찾기` 화면에서 “즐겨찾기한 날짜”는 컬렉션 내부 정보일 뿐 카드 핵심 정보는 아니므로, 정렬은 유지하되 카드에는 마지막 수정일만 보여주는 편이 더 읽기 쉽다고 판단했다.
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
## 공통 레이아웃
|
||||
- 앱 셸 파일: `frontend/src/App.vue`
|
||||
- 역할: 좌측 내비게이션, 중앙 워크스페이스, 우측 컨텍스트 패널로 구성된 공통 앱 셸 렌더링, 로그인 상태 반영, 최근 즐겨찾기 바로가기와 전역 검색 입력, 관리자 메뉴 노출 제어, 실제 SVG 에셋과 선형 SVG 아이콘이 혼합된 레일 UI, 전역 우측 상단 토스트 렌더링
|
||||
- 세부: 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 세 컬럼 모두 상단에 높이 `56px`의 헤더 블록을 유지한다. 중앙 헤더에는 고정 브랜드 `Tier Maker by zenn`이 표시되고, 우측 패널 토글은 닫혀 있을 때 중앙 헤더, 열려 있을 때 우측 헤더에 아이콘만 표시된다.
|
||||
- 세부: 좌측 패널은 `248px` 기준 폭을 사용하되 축소 시 아이콘 중심의 좁은 레일로 전환하고, 우측 패널은 `320px` 기준 폭을 사용한다. 세 컬럼 모두 상단에 높이 `56px`의 헤더 블록을 유지한다. 중앙 헤더에는 고정 브랜드 `Tier Maker by zenn`이 표시되고, 우측 패널 토글은 닫혀 있을 때 중앙 헤더, 열려 있을 때 우측 헤더에 아이콘만 표시된다.
|
||||
|
||||
## 백엔드 진입점
|
||||
- 서버 엔트리: `backend/index.js`
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- 프런트 파비콘은 운영 정적 파일 차단 영향을 줄이기 위해 `index.html`의 인라인 데이터 URL로 제공한다.
|
||||
- 프런트 앱 셸은 `좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널` 3단 구조로 재정의되었고, preview 모드에서는 이 셸을 숨기고 콘텐츠만 렌더링한다.
|
||||
- 좌측 패널은 `248px`, 우측 패널은 `320px` 기준 폭을 사용하며, 우측 패널은 상단 토글 버튼으로 접고 펼칠 수 있다.
|
||||
- 좌측 패널은 필요 시 축소형 레일로 접을 수 있으며, 접힌 상태에서는 아이콘 중심 내비게이션과 축약된 바로가기만 유지한다.
|
||||
- 이 3단 셸 구조는 홈, 게임 허브, 에디터, 관리자 등 일반 페이지 전반의 공통 뼈대로 유지하고, 페이지별 차이는 중앙/우측에 어떤 콘텐츠를 넣는지만 달라지도록 관리한다.
|
||||
- 비로그인 상태의 로그인 유도는 좌측 하단 버튼으로만 노출하고, 좌측 상단 사용자 카드 영역에는 별도 게스트 안내 카드를 렌더링하지 않는다.
|
||||
- 공통 셸의 좌측 내비, 우측 패널, 빠른 점프 버튼은 간단한 선형 SVG 아이콘과 두꺼운 카드형 버튼 문법을 공유한다.
|
||||
@@ -27,6 +28,7 @@
|
||||
## 화면 구조
|
||||
- 좌측 패널
|
||||
- 사용자 요약, 전체 공개 티어표 검색 입력, 주요 라우트 내비게이션, 최근 즐겨찾기 티어표 바로가기, 관리자 진입 버튼을 배치한다.
|
||||
- 상단 토글 버튼은 항상 고정되어 있고, 패널을 축소하면 텍스트를 숨기고 아이콘 중심 레일로 전환한다.
|
||||
- `Settings`는 별도 메뉴 항목으로만 진입하며, 사용자 카드 자체는 정보 표시 용도로만 사용한다.
|
||||
- 사용자 아바타는 원형 보더 스타일을 유지하고, `Favorites` 영역은 최근 즐겨찾기 티어표 최대 10개를 메인 메뉴보다 작은 밀도의 바로가기 목록으로 보여준 뒤 하단 `즐겨찾기 더 보기` 링크로 전체 즐겨찾기 화면에 연결한다.
|
||||
- 중앙 워크스페이스
|
||||
@@ -38,6 +40,7 @@
|
||||
- 현재 화면 문맥에 맞는 설명, 빠른 액션, 계정 상태 같은 보조 정보를 배치한다.
|
||||
- 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다.
|
||||
- 공통 토글 버튼은 패널이 닫혀 있을 때 중앙 헤더, 열려 있을 때 우측 헤더에 각각 아이콘만 표시하는 방식으로 동작한다.
|
||||
- 오른쪽 패널 토글은 열기/닫기 모두 `dock_to_left`, 왼쪽 패널 토글은 `dock_to_right` 아이콘으로 통일한다.
|
||||
- 홈 화면 기준 우측 패널은 임시 정보 카드 여러 개보다 핵심 CTA 하나만 남겨, 시안처럼 단순한 보조 레일 역할을 우선 유지한다.
|
||||
- 티어표 편집 화면
|
||||
- 공통 우측 패널 대신 전용 로컬 편집 패널을 사용한다.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- 에디터 우측 패널 외곽 래퍼는 제거했으므로, 다음 단계는 공통 오른쪽 컬럼 안에서 입력/버튼/구분선 간격을 시안처럼 더 정교하게 다듬는 작업이다.
|
||||
- 공통 56px 셸 헤더는 반영했으므로, 다음 단계는 좌/중앙/우 헤더 안에 실제 아이콘/상태 요소를 시안 순서에 맞게 하나씩 채워 넣는 작업이다.
|
||||
- 좌측 레일은 최근 즐겨찾기와 전역 검색까지 붙었으므로, 다음 단계는 검색 자동완성이나 즐겨찾기 썸네일 품질 같은 디테일을 더 다듬는 작업이다.
|
||||
- 좌측 레일 축소형은 반영했으므로, 다음 단계는 축소 상태에서 관리자/로그인 진입점과 hover 툴팁 같은 보조 UX를 더 다듬는 작업이다.
|
||||
- 카드 목록은 4열 기준과 메타 줄 구성까지 통일했으므로, 다음 단계는 필터 상태 배지나 hover·selection 강조 같은 상호작용 디테일을 더 다듬는 작업이다.
|
||||
- 검색 결과 화면은 좌측 전역 검색 입력만 쓰도록 정리됐으므로, 다음 단계는 결과 필터/정렬 여부를 검토하는 식으로 확장하면 된다.
|
||||
- 공통 3단 셸 구조는 고정했지만, 관리자/에디터 우측 패널 내부에 아직 바디에 남아 있는 제어 요소를 더 옮겨야 한다.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 로그
|
||||
|
||||
## 2026-03-30 v1.2.22
|
||||
- **왼쪽 사이드 축소/확대 추가**: 좌측 레일을 완전히 숨기지 않고 축소형 내비로 접었다 펼 수 있게 바꾸고, 접힌 상태에서는 아이콘 중심으로만 보이도록 레이아웃을 정리
|
||||
- **좌우 패널 토글 아이콘 통일**: 오른쪽 패널 열기/닫기는 모두 `dock_to_left`, 왼쪽 패널 토글은 `dock_to_right` 아이콘만 사용하도록 통일
|
||||
- **전역 검색 아이콘 교체**: 좌측 전역 검색 입력에 사용자가 추가한 `search.svg`를 실제 아이콘으로 연결
|
||||
|
||||
## 2026-03-30 v1.2.21
|
||||
- **티어표 카드 문법 통일**: 게임 허브, 검색 결과, 내 티어표, 즐겨찾기 목록의 카드 레이아웃을 `상단 썸네일 / 제목+좋아요 / 작성자+최종 수정일` 2줄 메타 구조로 통일하고, 데스크톱 기준 한 줄 4개 카드가 보이도록 재배치
|
||||
- **즐겨찾기 화면 날짜 기준 단순화**: `내 즐겨찾기` 화면은 더 이상 즐겨찾기한 시각을 표시하지 않고, 정렬 기준과 무관하게 덱의 마지막 수정일만 카드에 노출하도록 정리
|
||||
|
||||
@@ -10,6 +10,7 @@ import iconDockToRight from './assets/icons/dock_to_right.svg'
|
||||
import iconGridView from './assets/icons/grid_view.svg'
|
||||
import iconLists from './assets/icons/lists.svg'
|
||||
import iconMore from './assets/icons/more.svg'
|
||||
import iconSearch from './assets/icons/search.svg'
|
||||
import iconSettings from './assets/icons/settings.svg'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -17,6 +18,7 @@ const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const { toasts, dismissToast } = useToast()
|
||||
|
||||
const leftRailCollapsed = ref(false)
|
||||
const rightRailOpen = ref(true)
|
||||
const searchQuery = ref('')
|
||||
const favoriteShortcuts = ref([])
|
||||
@@ -35,6 +37,10 @@ const accountName = computed(() => {
|
||||
return 'Guest'
|
||||
})
|
||||
const accountEmail = computed(() => (auth.user?.email || '').trim() || '로그인 후 개인 메뉴를 사용할 수 있어요.')
|
||||
const shellStyle = computed(() => ({
|
||||
'--left-rail-width': leftRailCollapsed.value ? '76px' : '248px',
|
||||
'--right-rail-width': rightRailOpen.value ? '320px' : '0px',
|
||||
}))
|
||||
const leftNavItems = computed(() => {
|
||||
const items = [
|
||||
{ key: 'home', label: 'Games', path: '/', iconSrc: iconGridView },
|
||||
@@ -139,16 +145,12 @@ const routeMeta = computed(() => {
|
||||
action: () => router.push('/'),
|
||||
}
|
||||
})
|
||||
function railGlyph(type) {
|
||||
if (type === 'menu') return 'M4 6.5h16M4 12h16M4 17.5h16'
|
||||
if (type === 'search') return 'M10.2 6.2a4 4 0 1 1 0 8 4 4 0 0 1 0-8z M13.6 13.6l3.2 3.2'
|
||||
if (type === 'link') return 'M8 12h8 M12 8l4 4-4 4'
|
||||
return 'M4 12h16'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.refresh()
|
||||
if (typeof window !== 'undefined') {
|
||||
const leftSaved = window.localStorage.getItem('tier-maker:left-rail-collapsed')
|
||||
if (leftSaved === '1') leftRailCollapsed.value = true
|
||||
const saved = window.localStorage.getItem('tier-maker:right-rail-open')
|
||||
if (saved === '0') rightRailOpen.value = false
|
||||
}
|
||||
@@ -168,6 +170,13 @@ function isRouteActive(path) {
|
||||
return route.path.startsWith(path)
|
||||
}
|
||||
|
||||
function toggleLeftRail() {
|
||||
leftRailCollapsed.value = !leftRailCollapsed.value
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem('tier-maker:left-rail-collapsed', leftRailCollapsed.value ? '1' : '0')
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRightRail() {
|
||||
rightRailOpen.value = !rightRailOpen.value
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -218,8 +227,10 @@ watch(
|
||||
class="appShell"
|
||||
:class="{
|
||||
'appShell--preview': isPreviewMode,
|
||||
'appShell--leftCollapsed': leftRailCollapsed,
|
||||
'appShell--rightClosed': !rightRailOpen,
|
||||
}"
|
||||
:style="shellStyle"
|
||||
>
|
||||
<template v-if="isPreviewMode">
|
||||
<main class="appMain appMain--preview">
|
||||
@@ -229,8 +240,8 @@ watch(
|
||||
<template v-else>
|
||||
<aside class="leftRail">
|
||||
<div class="leftRail__top railHeader">
|
||||
<button class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="메뉴">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path :d="railGlyph('menu')" /></svg>
|
||||
<button class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="왼쪽 패널 토글" @click="toggleLeftRail">
|
||||
<img :src="iconDockToRight" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -248,9 +259,9 @@ watch(
|
||||
|
||||
<form class="searchStub" @submit.prevent="submitGlobalSearch">
|
||||
<span class="searchStub__icon">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path :d="railGlyph('search')" /></svg>
|
||||
<img :src="iconSearch" alt="" />
|
||||
</span>
|
||||
<input v-model="searchQuery" class="searchStub__input" type="search" placeholder="전체 티어표 검색" />
|
||||
<input v-model="searchQuery" class="searchStub__input" type="search" :placeholder="leftRailCollapsed ? '' : '전체 티어표 검색'" />
|
||||
</form>
|
||||
|
||||
<nav class="leftNav">
|
||||
@@ -265,7 +276,7 @@ watch(
|
||||
<img v-if="item.iconSrc" :src="item.iconSrc" alt="" />
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true"><path :d="item.icon" /></svg>
|
||||
</span>
|
||||
<span>{{ item.label }}</span>
|
||||
<span class="leftNav__label">{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
@@ -323,7 +334,7 @@ watch(
|
||||
<aside class="rightRail" :class="{ 'rightRail--closed': !rightRailOpen }" :aria-hidden="!rightRailOpen">
|
||||
<div class="rightRail__top railHeader">
|
||||
<button v-if="rightRailOpen" class="ghostIcon ghostIcon--iconOnly" type="button" aria-label="패널 닫기" @click="toggleRightRail">
|
||||
<img :src="iconDockToRight" alt="" />
|
||||
<img :src="iconDockToLeft" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="rightRail__body">
|
||||
@@ -355,7 +366,7 @@ watch(
|
||||
.appShell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 248px minmax(0, 1fr) 320px;
|
||||
grid-template-columns: var(--left-rail-width, 248px) minmax(0, 1fr) var(--right-rail-width, 320px);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.04), transparent 28%),
|
||||
linear-gradient(180deg, #1a1a1a 0%, #121212 100%);
|
||||
@@ -376,6 +387,7 @@ watch(
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rightRail {
|
||||
@@ -388,10 +400,6 @@ watch(
|
||||
border-color 220ms ease;
|
||||
}
|
||||
|
||||
.appShell--rightClosed {
|
||||
grid-template-columns: 248px minmax(0, 1fr) 0px;
|
||||
}
|
||||
|
||||
.appShell--rightClosed .rightRail {
|
||||
opacity: 0;
|
||||
transform: translateX(18px);
|
||||
@@ -417,6 +425,10 @@ watch(
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.leftRail__top {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.rightRail__top {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -458,7 +470,9 @@ watch(
|
||||
}
|
||||
|
||||
.ghostIcon img,
|
||||
.leftNav__glyph img {
|
||||
.leftNav__glyph img,
|
||||
.searchStub__icon img,
|
||||
.favoriteMoreLink__icon img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
@@ -475,6 +489,7 @@ watch(
|
||||
position: relative;
|
||||
margin-bottom: 14px;
|
||||
min-height: 58px;
|
||||
transition: margin 220ms ease;
|
||||
}
|
||||
|
||||
.appUserCard__button,
|
||||
@@ -491,6 +506,7 @@ watch(
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
box-sizing: border-box;
|
||||
transition: padding 220ms ease, justify-content 220ms ease;
|
||||
}
|
||||
|
||||
.appUserCard__avatar {
|
||||
@@ -514,6 +530,7 @@ watch(
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
.appUserCard__name {
|
||||
@@ -541,6 +558,7 @@ watch(
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
margin-bottom: 14px;
|
||||
box-sizing: border-box;
|
||||
transition: padding 220ms ease, justify-content 220ms ease;
|
||||
}
|
||||
|
||||
.searchStub__input {
|
||||
@@ -551,6 +569,7 @@ watch(
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
outline: none;
|
||||
font: inherit;
|
||||
transition: opacity 180ms ease, width 180ms ease;
|
||||
}
|
||||
|
||||
.searchStub__input::placeholder {
|
||||
@@ -581,6 +600,11 @@ watch(
|
||||
transition: background 180ms ease, color 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.leftNav__label {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.leftNav__item--active,
|
||||
.leftNav__item.router-link-active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
@@ -601,6 +625,7 @@ watch(
|
||||
margin-top: 22px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
transition: margin 220ms ease;
|
||||
}
|
||||
|
||||
.leftRail__sectionTitle {
|
||||
@@ -687,6 +712,66 @@ watch(
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .leftRail__body {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .appUserCard {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .appUserCard__button,
|
||||
.appShell--leftCollapsed .appUserCard__guest {
|
||||
justify-content: center;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .appUserCard__meta,
|
||||
.appShell--leftCollapsed .searchStub__input,
|
||||
.appShell--leftCollapsed .leftNav__label,
|
||||
.appShell--leftCollapsed .leftRail__sectionTitle,
|
||||
.appShell--leftCollapsed .favoriteShortcut__label,
|
||||
.appShell--leftCollapsed .favoriteMoreLink span:not(.favoriteMoreLink__icon),
|
||||
.appShell--leftCollapsed .favoriteEmpty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .searchStub {
|
||||
justify-content: center;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .leftNav {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .leftNav__item {
|
||||
justify-content: center;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .leftRail__section {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .favoriteShortcut {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .favoriteMoreLink {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.appShell--leftCollapsed .leftRail__bottom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.leftRail__bottom {
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
|
||||
1
frontend/src/assets/icons/search.svg
Normal file
1
frontend/src/assets/icons/search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>
|
||||
|
After Width: | Height: | Size: 375 B |
Reference in New Issue
Block a user