diff --git a/docs/todo.md b/docs/todo.md
index 21a0c80..492af18 100644
--- a/docs/todo.md
+++ b/docs/todo.md
@@ -8,3 +8,4 @@
- 업로드 파일은 MIME 타입뿐 아니라 파일 시그니처 기반 검증까지 확장한다.
- production에서 SESSION_SECRET 누락 시 서버가 부팅되지 않도록 강제한다.
- helmet 기반 보안 헤더와 업로드 정적 응답 헤더를 정리한다.
+- 책 아이콘 기반 사용법 모달은 구조를 먼저 붙였으므로, 실제 16:9 스크린샷 자산과 단계별 문구를 운영 톤에 맞게 채운다.
diff --git a/docs/update.md b/docs/update.md
index 226c1fd..da32809 100644
--- a/docs/update.md
+++ b/docs/update.md
@@ -1,5 +1,9 @@
# 업데이트 로그
+## 2026-04-01 v1.3.27
+- 오른쪽 사이드 하단에 책 아이콘 진입점을 추가하고, 중앙 대형 사용법 모달을 열어 좌측 기능 리스트와 우측 16:9 설명 영역, 좌우 이동, 하단 페이지네이션까지 포함한 기본 가이드 흐름을 붙임.
+- 사용법 모달의 스크린샷 영역은 우선 16:9 플레이스홀더와 설명 텍스트만 배치해, 실제 이미지 자산은 나중에 채워 넣을 수 있게 구조를 먼저 준비함.
+
## 2026-04-01 v1.3.26
- 오른쪽 사이드는 실제 광고 슬롯 기준을 300x600 세로 비율로 잡고, 데스크톱 우측 레일 폭도 325px로 조정해 300px 광고가 내부 패딩과 보더를 제외한 실폭 안에 자연스럽게 들어가도록 보정함.
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 6291b7b..ed59d3c 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -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')
@@ -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() {
+
+
+
+
+
+
+
+
+
+
+
STEP {{ guideStepIndex + 1 }}
+
{{ currentGuideStep.title }}
+
{{ currentGuideStep.summary }}
+
{{ currentGuideStep.description }}
+
+
+
+
+
+
+
+
+
@@ -924,9 +1047,30 @@ function submitGlobalSearch() {
.rightRail__bottom {
display: flex;
align-items: flex-end;
+ justify-content: flex-end;
+ gap: 10px;
padding-top: 12px;
}
+.guideDockButton {
+ width: 42px;
+ height: 42px;
+ border-radius: 14px;
+ border: 1px 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: 12px 14px;
@@ -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: 32px 20px;
+ background: rgba(0, 0, 0, 0.62);
+ backdrop-filter: blur(10px);
+}
+
+.guideModal__dialog {
+ width: min(1180px, calc(100vw - 40px));
+ min-height: min(760px, calc(100dvh - 64px));
+ display: grid;
+ grid-template-columns: 260px minmax(0, 1fr);
+ border-radius: 28px;
+ overflow: hidden;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: linear-gradient(180deg, rgba(34, 34, 34, 0.98), rgba(18, 18, 18, 0.98));
+ box-shadow: 0 28px 90px rgba(0, 0, 0, 0.42);
+}
+
+.guideModal__sidebar {
+ display: grid;
+ align-content: start;
+ gap: 18px;
+ padding: 28px 22px;
+ background: rgba(255, 255, 255, 0.03);
+ border-right: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.guideModal__eyebrow {
+ font-size: 11px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.guideModal__title {
+ font-size: 28px;
+ font-weight: 900;
+ line-height: 1.1;
+ letter-spacing: -0.04em;
+}
+
+.guideModal__list {
+ display: grid;
+ gap: 8px;
+}
+
+.guideModal__listItem {
+ display: grid;
+ grid-template-columns: 26px minmax(0, 1fr);
+ gap: 10px;
+ align-items: center;
+ padding: 12px 14px;
+ border-radius: 16px;
+ border: 1px 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: 12px;
+ font-weight: 900;
+ color: rgba(255, 255, 255, 0.54);
+}
+
+.guideModal__listLabel {
+ min-width: 0;
+ font-size: 14px;
+ font-weight: 700;
+}
+
+.guideModal__main {
+ min-width: 0;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ padding: 24px 28px 28px;
+}
+
+.guideModal__close {
+ justify-self: end;
+ border: 0;
+ background: transparent;
+ color: rgba(255, 255, 255, 0.56);
+ cursor: pointer;
+ font-size: 13px;
+}
+
+.guideModal__content {
+ min-width: 0;
+ display: grid;
+ grid-template-columns: 52px minmax(0, 1fr) 52px;
+ gap: 16px;
+ align-items: center;
+}
+
+.guideModal__body {
+ min-width: 0;
+ display: grid;
+ gap: 18px;
+}
+
+.guideModal__media {
+ width: 100%;
+}
+
+.guideModal__mediaPlaceholder {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ border-radius: 24px;
+ border: 1px 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: 8px;
+ text-align: center;
+}
+
+.guideModal__mediaBadge {
+ font-size: 11px;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: rgba(255, 255, 255, 0.38);
+}
+
+.guideModal__mediaTitle {
+ font-size: 24px;
+ font-weight: 900;
+}
+
+.guideModal__mediaHint {
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.48);
+}
+
+.guideModal__text {
+ display: grid;
+ gap: 8px;
+}
+
+.guideModal__stepLabel {
+ font-size: 11px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: rgba(255, 255, 255, 0.42);
+}
+
+.guideModal__stepTitle {
+ font-size: 28px;
+ font-weight: 900;
+ letter-spacing: -0.04em;
+}
+
+.guideModal__stepSummary {
+ font-size: 16px;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.86);
+}
+
+.guideModal__stepDescription {
+ margin: 0;
+ max-width: 720px;
+ line-height: 1.7;
+ color: rgba(255, 255, 255, 0.62);
+}
+
+.guideModal__footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+}
+
+.guideModal__pagination {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.guideModal__dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ border: 0;
+ background: rgba(255, 255, 255, 0.18);
+ cursor: pointer;
+}
+
+.guideModal__dot--active {
+ width: 26px;
+ background: rgba(77, 127, 233, 0.9);
+}
+
+.guideModal__next {
+ padding: 12px 18px;
+ border-radius: 14px;
+ border: 1px solid rgba(77, 127, 233, 0.96);
+ background: rgba(77, 127, 233, 0.88);
+ color: #fff;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.guideModal__arrow {
+ width: 52px;
+ height: 52px;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.04);
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 28px;
+ 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: 1200px) {
+ .guideModal__dialog {
+ grid-template-columns: 1fr;
+ min-height: auto;
+ }
+
+ .guideModal__sidebar {
+ border-right: 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ }
+
+ .guideModal__content {
+ grid-template-columns: 40px minmax(0, 1fr) 40px;
+ }
+
.appShell {
grid-template-columns: var(--left-rail-width, 248px) minmax(0, 1fr);
}
@@ -1104,6 +1496,45 @@ function submitGlobalSearch() {
}
}
+@media (max-width: 860px) {
+ .guideModal {
+ padding: 20px 12px;
+ }
+
+ .guideModal__dialog {
+ width: min(100%, calc(100vw - 24px));
+ }
+
+ .guideModal__main {
+ padding: 20px 18px 18px;
+ }
+
+ .guideModal__content {
+ grid-template-columns: 1fr;
+ }
+
+ .guideModal__arrow {
+ display: none;
+ }
+
+ .guideModal__footer {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .guideModal__next {
+ width: 100%;
+ }
+
+ .guideDockButton {
+ display: none;
+ }
+
+ .guideModal__list {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
@media (max-width: 860px) {
.appShell {
grid-template-columns: 1fr;
diff --git a/frontend/src/assets/icons/menu_book.svg b/frontend/src/assets/icons/menu_book.svg
new file mode 100644
index 0000000..1dda8a4
--- /dev/null
+++ b/frontend/src/assets/icons/menu_book.svg
@@ -0,0 +1 @@
+
\ No newline at end of file