@@ -11,6 +11,7 @@ import iconFavorite from './assets/icons/favorite.svg'
import iconLists from './assets/icons/lists.svg'
import iconSearch from './assets/icons/search.svg'
import iconSettings from './assets/icons/settings.svg'
import iconMenuBook from './assets/icons/menu_book.svg'
import RightRailAd from './components/RightRailAd.vue'
import SvgIcon from './components/SvgIcon.vue'
@@ -24,6 +25,8 @@ const rightRailOpen = ref(true)
const searchQuery = ref ( '' )
const searchPlaceholder = computed ( ( ) => ( route . name === 'home' ? '게임 템플릿 검색' : '전체 티어표 검색' ) )
const isCollapsedSearchOpen = ref ( false )
const isGuideModalOpen = ref ( false )
const guideStepIndex = ref ( 0 )
const viewportWidth = ref ( typeof window !== 'undefined' ? window . innerWidth : 1440 )
provide ( 'rightRailOpen' , rightRailOpen )
provide ( 'localRightRailTarget' , '#local-right-rail-root' )
@@ -44,7 +47,7 @@ const accountName = computed(() => {
const accountEmail = computed ( ( ) => ( auth . user ? . email || '' ) . trim ( ) || '로그인 후 개인 메뉴를 사용할 수 있어요.' )
const shellStyle = computed ( ( ) => ( {
'--left-rail-width' : leftRailCollapsed . value ? '76px' : '248px' ,
'--right-rail-width' : ! isRightRailOverlay . value && rightRailOpen . value ? '320 px' : '0px' ,
'--right-rail-width' : ! isRightRailOverlay . value && rightRailOpen . value ? '325 px' : '0px' ,
} ) )
const leftNavItems = computed ( ( ) => {
const items = [
@@ -56,6 +59,35 @@ const leftNavItems = computed(() => {
return items . filter ( ( item ) => ! item . requiresAuth || auth . user )
} )
const showRightRailAction = computed ( ( ) => false )
const guideSteps = [
{
id : 'select-game' ,
title : '게임 또는 양식 선택' ,
summary : '사용할 게임 템플릿을 고르거나 커스텀 티어표를 시작합니다.' ,
description : '홈 화면에서 게임 템플릿을 고르거나 커스텀 티어표 만들기로 시작할 수 있어요. 게임 허브에서는 기존 공개 티어표도 살펴본 뒤 같은 흐름으로 새 보드를 만들 수 있습니다.' ,
} ,
{
id : 'arrange-board' ,
title : '행과 열 구성' ,
summary : '필요한 랭크와 열을 만들고 이름을 정리합니다.' ,
description : '기본 행을 수정하거나 행·열을 추가해서 원하는 구조를 먼저 잡아보세요. 공격, 방어, 지원처럼 가로 열을 나누고 각 행 이름을 짧게 정리하면 실제 배치가 훨씬 빨라집니다.' ,
} ,
{
id : 'drop-items' ,
title : '아이템 배치' ,
summary : '프리셋 아이템과 커스텀 이미지를 드래그로 배치합니다.' ,
description : '오른쪽 아이템 풀에서 이미지를 드래그해서 원하는 칸에 배치합니다. 직접 올린 커스텀 이미지도 같은 방식으로 다룰 수 있고, 이름 표시 옵션으로 결과 톤도 함께 맞출 수 있어요.' ,
} ,
{
id : 'save-share' ,
title : '저장과 공유' ,
summary : '저장, 이미지 다운로드, 템플릿 요청까지 마무리합니다.' ,
description : '완성한 보드는 내 티어표로 저장하거나 PNG 이미지로 내려받을 수 있습니다. 공통 템플릿으로 쓰면 좋겠다면 템플릿 요청을 보내 관리자에게 추가를 제안할 수도 있어요.' ,
} ,
]
const currentGuideStep = computed ( ( ) => guideSteps [ guideStepIndex . value ] || guideSteps [ 0 ] )
const isGuidePrevDisabled = computed ( ( ) => guideStepIndex . value <= 0 )
const isGuideNextDisabled = computed ( ( ) => guideStepIndex . value >= guideSteps . length - 1 )
const showGameHubViewToggle = computed ( ( ) => route . name === 'gameHub' )
const gameHubViewMode = computed ( ( ) => ( route . query . view === 'list' ? 'list' : 'grid' ) )
const leftBottomPrimaryAction = computed ( ( ) => {
@@ -185,6 +217,10 @@ onMounted(async () => {
} )
function handleGlobalKeydown ( event ) {
if ( event . key === 'Escape' && isGuideModalOpen . value ) {
closeGuideModal ( )
return
}
if ( event . key === 'Escape' && isCollapsedSearchOpen . value ) {
closeCollapsedSearch ( )
}
@@ -202,6 +238,7 @@ watch(
( ) => {
searchQuery . value = typeof route . query . q === 'string' ? route . query . q : ''
isCollapsedSearchOpen . value = false
isGuideModalOpen . value = false
}
)
@@ -262,6 +299,29 @@ function closeCollapsedSearch() {
isCollapsedSearchOpen . value = false
}
function openGuideModal ( stepIndex = 0 ) {
guideStepIndex . value = Math . min ( Math . max ( Number ( stepIndex ) || 0 , 0 ) , guideSteps . length - 1 )
isGuideModalOpen . value = true
}
function closeGuideModal ( ) {
isGuideModalOpen . value = false
}
function selectGuideStep ( index ) {
guideStepIndex . value = Math . min ( Math . max ( index , 0 ) , guideSteps . length - 1 )
}
function showPrevGuideStep ( ) {
if ( isGuidePrevDisabled . value ) return
guideStepIndex . value -= 1
}
function showNextGuideStep ( ) {
if ( isGuideNextDisabled . value ) return
guideStepIndex . value += 1
}
function handleLeftRailSearch ( ) {
if ( leftRailCollapsed . value && ! isMobileLayout . value ) {
openCollapsedSearch ( )
@@ -392,6 +452,66 @@ function submitGlobalSearch() {
< / form >
< / div >
< div v-if = "isGuideModalOpen" class="guideModal" role="dialog" aria-modal="true" aria-label="티어 메이커 사용법" @click.self="closeGuideModal" >
< div class = "guideModal__dialog" >
< div class = "guideModal__sidebar" >
< div class = "guideModal__eyebrow" > Guide < / div >
< div class = "guideModal__title" > 티어표 제작 흐름 < / div >
< div class = "guideModal__list" >
< button
v-for = "(step, index) in guideSteps"
:key = "step.id"
class = "guideModal__listItem"
: class = "{ 'guideModal__listItem--active': index === guideStepIndex }"
type = "button"
@click ="selectGuideStep(index)"
>
< span class = "guideModal__listIndex" > { { index + 1 } } < / span >
< span class = "guideModal__listLabel" > { { step . title } } < / span >
< / button >
< / div >
< / div >
< div class = "guideModal__main" >
< button class = "guideModal__close" type = "button" aria -label = " 사용법 닫기 " @click ="closeGuideModal" > 닫기 < / button >
< div class = "guideModal__content" >
< button class = "guideModal__arrow" type = "button" aria -label = " 이전 단계 " :disabled = "isGuidePrevDisabled" @click ="showPrevGuideStep" > ‹ < / button >
< div class = "guideModal__body" >
< div class = "guideModal__media" >
< div class = "guideModal__mediaPlaceholder" >
< div class = "guideModal__mediaBadge" > 16 : 9 < / div >
< div class = "guideModal__mediaTitle" > { { currentGuideStep . title } } < / div >
< div class = "guideModal__mediaHint" > 스크린샷 준비 중 < / div >
< / div >
< / div >
< div class = "guideModal__text" >
< div class = "guideModal__stepLabel" > STEP { { guideStepIndex + 1 } } < / div >
< div class = "guideModal__stepTitle" > { { currentGuideStep . title } } < / div >
< div class = "guideModal__stepSummary" > { { currentGuideStep . summary } } < / div >
< p class = "guideModal__stepDescription" > { { currentGuideStep . description } } < / p >
< / div >
< div class = "guideModal__footer" >
< div class = "guideModal__pagination" >
< button
v-for = "(step, index) in guideSteps"
: key = "step.id + '-dot'"
class = "guideModal__dot"
: class = "{ 'guideModal__dot--active': index === guideStepIndex }"
type = "button"
:aria-label = "step.title"
@click ="selectGuideStep(index)"
> < / button >
< / div >
< button class = "guideModal__next" type = "button" @click ="isGuideNextDisabled ? closeGuideModal() : showNextGuideStep()" >
{{ isGuideNextDisabled ? ' 닫기 ' : ' 다음 ' }}
< / button >
< / div >
< / div >
< button class = "guideModal__arrow" type = "button" aria -label = " 다음 단계 " :disabled = "isGuideNextDisabled" @click ="showNextGuideStep" > › < / button >
< / div >
< / div >
< / div >
< / div >
< button v-if = "rightRailOpen && isRightRailOverlay" class="rightRailBackdrop" type="button" aria-label="오른쪽 패널 닫기" @click="toggleRightRail" > < / button >
< aside class = "rightRail" : class = "{ 'rightRail--closed': !rightRailOpen, 'rightRail--overlay': isRightRailOverlay }" :aria-hidden = "!rightRailOpen" >
@@ -415,6 +535,9 @@ function submitGlobalSearch() {
< / button >
< / section >
< / template >
< button class = "guideDockButton" type = "button" aria -label = " 사용법 열기 " @click ="openGuideModal()" >
< SvgIcon :src = "iconMenuBook" :size = "22" / >
< / button >
< / div >
< / div >
< / aside >
@@ -436,7 +559,7 @@ function submitGlobalSearch() {
. appShell {
min - height : 100 dvh ;
display : grid ;
grid - template - columns : var ( -- left - rail - width , 248 px ) minmax ( 0 , 1 fr ) var ( -- right - rail - width , 320 px ) ;
grid - template - columns : var ( -- left - rail - width , 248 px ) minmax ( 0 , 1 fr ) var ( -- right - rail - width , 325 px ) ;
background : rgba ( 14 , 14 , 14 , 0.96 ) ;
color : rgba ( 255 , 255 , 255 , 0.92 ) ;
transition : grid - template - columns 220 ms ease ;
@@ -924,9 +1047,30 @@ function submitGlobalSearch() {
. rightRail _ _bottom {
display : flex ;
align - items : flex - end ;
justify - content : flex - end ;
gap : 10 px ;
padding - top : 12 px ;
}
. guideDockButton {
width : 42 px ;
height : 42 px ;
border - radius : 14 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
background : rgba ( 255 , 255 , 255 , 0.04 ) ;
color : rgba ( 255 , 255 , 255 , 0.78 ) ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
cursor : pointer ;
flex : 0 0 auto ;
}
. guideDockButton : hover {
background : rgba ( 255 , 255 , 255 , 0.08 ) ;
color : rgba ( 255 , 255 , 255 , 0.96 ) ;
}
. rightRailAction _ _button {
width : 100 % ;
padding : 12 px 14 px ;
@@ -942,6 +1086,240 @@ function submitGlobalSearch() {
display : none ;
}
. guideModal {
position : fixed ;
inset : 0 ;
z - index : 36 ;
display : flex ;
align - items : center ;
justify - content : center ;
padding : 32 px 20 px ;
background : rgba ( 0 , 0 , 0 , 0.62 ) ;
backdrop - filter : blur ( 10 px ) ;
}
. guideModal _ _dialog {
width : min ( 1180 px , calc ( 100 vw - 40 px ) ) ;
min - height : min ( 760 px , calc ( 100 dvh - 64 px ) ) ;
display : grid ;
grid - template - columns : 260 px minmax ( 0 , 1 fr ) ;
border - radius : 28 px ;
overflow : hidden ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
background : linear - gradient ( 180 deg , rgba ( 34 , 34 , 34 , 0.98 ) , rgba ( 18 , 18 , 18 , 0.98 ) ) ;
box - shadow : 0 28 px 90 px rgba ( 0 , 0 , 0 , 0.42 ) ;
}
. guideModal _ _sidebar {
display : grid ;
align - content : start ;
gap : 18 px ;
padding : 28 px 22 px ;
background : rgba ( 255 , 255 , 255 , 0.03 ) ;
border - right : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
}
. guideModal _ _eyebrow {
font - size : 11 px ;
letter - spacing : 0.14 em ;
text - transform : uppercase ;
color : rgba ( 255 , 255 , 255 , 0.4 ) ;
}
. guideModal _ _title {
font - size : 28 px ;
font - weight : 900 ;
line - height : 1.1 ;
letter - spacing : - 0.04 em ;
}
. guideModal _ _list {
display : grid ;
gap : 8 px ;
}
. guideModal _ _listItem {
display : grid ;
grid - template - columns : 26 px minmax ( 0 , 1 fr ) ;
gap : 10 px ;
align - items : center ;
padding : 12 px 14 px ;
border - radius : 16 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
background : rgba ( 255 , 255 , 255 , 0.02 ) ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
cursor : pointer ;
text - align : left ;
}
. guideModal _ _listItem -- active {
border - color : rgba ( 77 , 127 , 233 , 0.5 ) ;
background : rgba ( 77 , 127 , 233 , 0.14 ) ;
color : rgba ( 255 , 255 , 255 , 0.96 ) ;
}
. guideModal _ _listIndex {
font - size : 12 px ;
font - weight : 900 ;
color : rgba ( 255 , 255 , 255 , 0.54 ) ;
}
. guideModal _ _listLabel {
min - width : 0 ;
font - size : 14 px ;
font - weight : 700 ;
}
. guideModal _ _main {
min - width : 0 ;
display : grid ;
grid - template - rows : auto minmax ( 0 , 1 fr ) ;
padding : 24 px 28 px 28 px ;
}
. guideModal _ _close {
justify - self : end ;
border : 0 ;
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.56 ) ;
cursor : pointer ;
font - size : 13 px ;
}
. guideModal _ _content {
min - width : 0 ;
display : grid ;
grid - template - columns : 52 px minmax ( 0 , 1 fr ) 52 px ;
gap : 16 px ;
align - items : center ;
}
. guideModal _ _body {
min - width : 0 ;
display : grid ;
gap : 18 px ;
}
. guideModal _ _media {
width : 100 % ;
}
. guideModal _ _mediaPlaceholder {
position : relative ;
width : 100 % ;
aspect - ratio : 16 / 9 ;
border - radius : 24 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
background : radial - gradient ( circle at top , rgba ( 77 , 127 , 233 , 0.18 ) , rgba ( 255 , 255 , 255 , 0.02 ) 52 % ) , rgba ( 255 , 255 , 255 , 0.03 ) ;
display : grid ;
align - content : center ;
justify - items : center ;
gap : 8 px ;
text - align : center ;
}
. guideModal _ _mediaBadge {
font - size : 11 px ;
letter - spacing : 0.16 em ;
text - transform : uppercase ;
color : rgba ( 255 , 255 , 255 , 0.38 ) ;
}
. guideModal _ _mediaTitle {
font - size : 24 px ;
font - weight : 900 ;
}
. guideModal _ _mediaHint {
font - size : 13 px ;
color : rgba ( 255 , 255 , 255 , 0.48 ) ;
}
. guideModal _ _text {
display : grid ;
gap : 8 px ;
}
. guideModal _ _stepLabel {
font - size : 11 px ;
letter - spacing : 0.14 em ;
text - transform : uppercase ;
color : rgba ( 255 , 255 , 255 , 0.42 ) ;
}
. guideModal _ _stepTitle {
font - size : 28 px ;
font - weight : 900 ;
letter - spacing : - 0.04 em ;
}
. guideModal _ _stepSummary {
font - size : 16 px ;
font - weight : 700 ;
color : rgba ( 255 , 255 , 255 , 0.86 ) ;
}
. guideModal _ _stepDescription {
margin : 0 ;
max - width : 720 px ;
line - height : 1.7 ;
color : rgba ( 255 , 255 , 255 , 0.62 ) ;
}
. guideModal _ _footer {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 18 px ;
}
. guideModal _ _pagination {
display : flex ;
align - items : center ;
gap : 8 px ;
}
. guideModal _ _dot {
width : 10 px ;
height : 10 px ;
border - radius : 999 px ;
border : 0 ;
background : rgba ( 255 , 255 , 255 , 0.18 ) ;
cursor : pointer ;
}
. guideModal _ _dot -- active {
width : 26 px ;
background : rgba ( 77 , 127 , 233 , 0.9 ) ;
}
. guideModal _ _next {
padding : 12 px 18 px ;
border - radius : 14 px ;
border : 1 px solid rgba ( 77 , 127 , 233 , 0.96 ) ;
background : rgba ( 77 , 127 , 233 , 0.88 ) ;
color : # fff ;
font - weight : 800 ;
cursor : pointer ;
}
. guideModal _ _arrow {
width : 52 px ;
height : 52 px ;
border - radius : 999 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
background : rgba ( 255 , 255 , 255 , 0.04 ) ;
color : rgba ( 255 , 255 , 255 , 0.92 ) ;
font - size : 28 px ;
line - height : 1 ;
cursor : pointer ;
}
. guideModal _ _arrow : disabled {
opacity : 0.28 ;
cursor : default ;
}
. collapsedSearchModal {
position : fixed ;
inset : 0 ;
@@ -1071,6 +1449,20 @@ function submitGlobalSearch() {
}
@ media ( max - width : 1200 px ) {
. guideModal _ _dialog {
grid - template - columns : 1 fr ;
min - height : auto ;
}
. guideModal _ _sidebar {
border - right : 0 ;
border - bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
}
. guideModal _ _content {
grid - template - columns : 40 px minmax ( 0 , 1 fr ) 40 px ;
}
. appShell {
grid - template - columns : var ( -- left - rail - width , 248 px ) minmax ( 0 , 1 fr ) ;
}
@@ -1104,6 +1496,45 @@ function submitGlobalSearch() {
}
}
@ media ( max - width : 860 px ) {
. guideModal {
padding : 20 px 12 px ;
}
. guideModal _ _dialog {
width : min ( 100 % , calc ( 100 vw - 24 px ) ) ;
}
. guideModal _ _main {
padding : 20 px 18 px 18 px ;
}
. guideModal _ _content {
grid - template - columns : 1 fr ;
}
. guideModal _ _arrow {
display : none ;
}
. guideModal _ _footer {
flex - direction : column ;
align - items : stretch ;
}
. guideModal _ _next {
width : 100 % ;
}
. guideDockButton {
display : none ;
}
. guideModal _ _list {
grid - template - columns : 1 fr 1 fr ;
}
}
@ media ( max - width : 860 px ) {
. appShell {
grid - template - columns : 1 fr ;