From 91e16ba4153a3015cdae73a28e8052afb7ea41bd Mon Sep 17 00:00:00 2001 From: zenn Date: Wed, 1 Apr 2026 14:52:50 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4:=20v1.3.27=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=B2=95=20=EB=AA=A8=EB=8B=AC=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/todo.md | 1 + docs/update.md | 4 + frontend/src/App.vue | 431 ++++++++++++++++++++++++ frontend/src/assets/icons/menu_book.svg | 1 + 4 files changed, 437 insertions(+) create mode 100644 frontend/src/assets/icons/menu_book.svg 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() { + + @@ -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