diff --git a/layers/assets/css/app.css b/layers/assets/css/app.css
index 77e7eea..86458e7 100644
--- a/layers/assets/css/app.css
+++ b/layers/assets/css/app.css
@@ -1,5 +1,7 @@
@import './base/_theme.css';
@import './base/_reset.css';
+@import './components/_swiper-pagination.css';
+@import './components/_button.css';
@tailwind base;
@tailwind components;
diff --git a/layers/assets/css/components/_button.css b/layers/assets/css/components/_button.css
new file mode 100644
index 0000000..669f90b
--- /dev/null
+++ b/layers/assets/css/components/_button.css
@@ -0,0 +1,44 @@
+/* Button Size Classes */
+@layer components {
+ .btn-base {
+ @apply relative inline-flex items-center justify-center font-medium border border-gray-600/30 overflow-hidden;
+ /* 기본 크기: size-medium */
+ --btn-padding: theme('spacing.10');
+ --btn-height: theme('spacing.14');
+ --btn-text: theme('fontSize.base');
+ --btn-radius: theme('borderRadius.lg');
+ @apply px-10 h-14 text-base rounded-lg;
+ }
+
+ .size-extra-small {
+ --btn-padding: theme('spacing.6');
+ --btn-height: theme('spacing.10');
+ --btn-text: theme('fontSize.sm');
+ --btn-radius: theme('borderRadius.DEFAULT');
+ @apply px-6 h-10 text-sm rounded;
+ }
+
+ .size-small {
+ --btn-padding: theme('spacing.10');
+ --btn-height: theme('spacing.12');
+ --btn-text: theme('fontSize.sm');
+ --btn-radius: theme('borderRadius.lg');
+ @apply px-10 h-12 text-sm rounded-lg;
+ }
+
+ .size-medium {
+ --btn-padding: theme('spacing.10');
+ --btn-height: theme('spacing.14');
+ --btn-text: theme('fontSize.base');
+ --btn-radius: theme('borderRadius.lg');
+ @apply px-10 h-14 text-base rounded-lg;
+ }
+
+ .size-large {
+ --btn-padding: theme('spacing.10');
+ --btn-height: theme('spacing.16');
+ --btn-text: theme('fontSize.lg');
+ --btn-radius: theme('borderRadius.lg');
+ @apply px-10 h-16 text-lg rounded-lg;
+ }
+}
diff --git a/layers/assets/css/components/_swiper-pagination.css b/layers/assets/css/components/_swiper-pagination.css
new file mode 100644
index 0000000..af4e031
--- /dev/null
+++ b/layers/assets/css/components/_swiper-pagination.css
@@ -0,0 +1,73 @@
+/* 페이지네이션 버튼 */
+main .slide-pagination.swiper-pagination-bullets {
+ position: absolute;
+ bottom: 48px;
+ left: 0;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 24px;
+ z-index: 5;
+}
+
+.slide-pagination .swiper-pagination-bullet {
+ position: relative;
+ width: 12px;
+ height: 12px;
+ background: var(--primary);
+ border-radius: 50%;
+ opacity: 1;
+}
+
+.slide-pagination .swiper-pagination-bullet:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.slide-pagination .swiper-pagination-bullet-active:after {
+ display: none;
+}
+
+/* 네비게이션 버튼 */
+.slide-prev,
+.slide-next {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 48px;
+ height: 48px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ z-index: 5;
+
+ background-color: var(--primary);
+}
+
+.slide-prev {
+ left: 40px;
+}
+
+.slide-next {
+ right: 40px;
+}
+
+@media (max-width: 1023px) {
+ main .slide-pagination.swiper-pagination-bullets {
+ bottom: 32px;
+ gap: 12px;
+ }
+
+ .slide-pagination .swiper-pagination-bullet {
+ width: 8px;
+ height: 8px;
+ }
+}
diff --git a/layers/components/atoms/Button.vue b/layers/components/atoms/Button.vue
index 1f06333..7c35a21 100644
--- a/layers/components/atoms/Button.vue
+++ b/layers/components/atoms/Button.vue
@@ -1,13 +1,14 @@
diff --git a/layers/components/blocks/modal/YouTube.vue b/layers/components/blocks/modal/YouTube.vue
index 14f0c1b..966b05d 100644
--- a/layers/components/blocks/modal/YouTube.vue
+++ b/layers/components/blocks/modal/YouTube.vue
@@ -72,7 +72,7 @@ const props = withDefaults(defineProps(), {
const emit = defineEmits()
-// YouTube URL을 임베드 가능한 형태로 변환
+// [TODO] YouTube URL을 임베드 가능한 형태로 변환
const embedUrl = computed(() => {
if (!props.youtubeUrl) return ''
diff --git a/layers/components/layouts/Main.vue b/layers/components/layouts/Main.vue
index 82b1117..8c81326 100644
--- a/layers/components/layouts/Main.vue
+++ b/layers/components/layouts/Main.vue
@@ -65,6 +65,7 @@ watchEffect(() => {
diff --git a/layers/composables/useGetGameAlias.ts b/layers/composables/useGetGameAlias.ts
deleted file mode 100644
index 55c6c46..0000000
--- a/layers/composables/useGetGameAlias.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { getHeader } from 'h3'
-import { useRuntimeConfig, useRequestEvent } from 'nuxt/app'
-
-/**
- * 게임 별칭을 추출하는 유틸리티 함수
- * @param host 호스트 문자열
- * @param baseDomain 기본 도메인
- * @returns 게임 별칭 또는 빈 문자열
- */
-const extractGameAliasFromHost = (host: string, baseDomain: string): string => {
- if (!host || !host.includes(baseDomain)) {
- return ''
- }
-
- const subdomain = host.split('.')[0]
- return subdomain && subdomain !== 'www' ? subdomain : ''
-}
-
-/**
- * 서버 사이드에서 게임 별칭을 가져오는 함수
- * @param baseDomain 기본 도메인
- * @returns 게임 별칭 또는 빈 문자열
- */
-const getGameAliasFromServer = (baseDomain: string): string => {
- try {
- const event = useRequestEvent()
-
- if (!event) {
- return ''
- }
-
- // 미들웨어에서 설정한 gameAlias가 있다면 우선 사용
- if (event.context.gameAlias) {
- return event.context.gameAlias
- }
-
- const host = getHeader(event, 'host') || ''
- return extractGameAliasFromHost(host, baseDomain)
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error('useGetGameAlias server error:', error)
- return ''
- }
-}
-
-/**
- * 클라이언트 사이드에서 게임 별칭을 가져오는 함수
- * @param baseDomain 기본 도메인
- * @returns 게임 별칭 또는 빈 문자열
- */
-const getGameAliasFromClient = (baseDomain: string): string => {
- try {
- const host = window.location.host
- return extractGameAliasFromHost(host, baseDomain)
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error('useGetGameAlias client error:', error)
- return ''
- }
-}
-
-/**
- * 게임 별칭을 가져오는 컴포저블 함수
- * 서버와 클라이언트 환경에서 모두 동작
- * @returns 게임 별칭 문자열
- */
-export const useGetGameAlias = (): string => {
- const config = useRuntimeConfig()
- const baseDomain = (config.public.baseDomain || '.onstove.com') as string
-
- if (import.meta.client) {
- return getGameAliasFromClient(baseDomain)
- }
-
- return getGameAliasFromServer(baseDomain)
-}
diff --git a/layers/composables/useGetGameDomain.ts b/layers/composables/useGetGameDomain.ts
new file mode 100644
index 0000000..353352f
--- /dev/null
+++ b/layers/composables/useGetGameDomain.ts
@@ -0,0 +1,35 @@
+import { getHeader, getRequestHost } from 'h3'
+import { useRequestEvent } from 'nuxt/app'
+
+/**
+ * 게임 도메인을 가져오는 컴포저블 함수
+ * 서버와 클라이언트 환경에서 모두 동작
+ * @returns 게임 도메인 문자열
+ */
+export const useGetGameDomain = (): string => {
+ try {
+ if (import.meta.client) {
+ const host = window.location.host || ''
+ return host.split(':')[0]
+ }
+
+ const event = useRequestEvent()
+ if (!event) {
+ return ''
+ }
+
+ // 미들웨어에서 설정한 gameDomain가 있다면 우선 사용
+ if (event.context.gameDomain) {
+ return event.context.gameDomain
+ }
+
+ const host =
+ (getHeader(event, 'host') || getRequestHost(event)).toString() || ''
+ const cleanHost = host.split(':')[0]
+
+ return cleanHost || ''
+ } catch (error) {
+ console.error('useGetGameDomain error:', error)
+ return ''
+ }
+}
diff --git a/layers/middleware/pageData.global.ts b/layers/middleware/pageData.global.ts
index 0fcded0..2bd020e 100644
--- a/layers/middleware/pageData.global.ts
+++ b/layers/middleware/pageData.global.ts
@@ -1,13 +1,13 @@
import { commonFetch } from '#layers/utils/apiUtil'
import { usePageDataStore } from '#layers/stores/usePageDataStore'
-import { useGetGameAlias } from '#layers/composables/useGetGameAlias'
+import { useGetGameDomain } from '@/layers/composables/useGetGameDomain'
import { usePathResolver } from '#layers/composables/usePathResolver'
import type { PageDataResponse } from '#layers/types/api/pageData'
export default defineNuxtRouteMiddleware(async (to, _from) => {
const config = useRuntimeConfig()
const store = usePageDataStore()
- const gameAlias = useGetGameAlias()
+ const gameDomain = useGetGameDomain()
const { getPathAfterLanguage } = usePathResolver()
const stoveApiBaseUrl = config.public.stoveApiUrl
@@ -22,7 +22,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
}
const queryParams: Record = {
- game_alias: gameAlias,
+ game_domain: gameDomain,
lang_code: 'ko',
page_url: pageUrl,
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
diff --git a/layers/server/middleware/gameInfo.ts b/layers/server/middleware/gameInfo.ts
index 013df7f..62cbfd5 100644
--- a/layers/server/middleware/gameInfo.ts
+++ b/layers/server/middleware/gameInfo.ts
@@ -22,14 +22,11 @@ export default defineEventHandler(async event => {
const host =
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
const baseDomain = process.env.BASE_DOMAIN || '.onstove.com'
- const isGameAliasExtractable = host.includes(baseDomain)
+ const isGameDomainExtractable = host.includes(baseDomain)
- if (isGameAliasExtractable) {
- const gameAlias = host.split('.')[0]
-
- if (gameAlias && gameAlias !== 'www') {
- event.context.gameAlias = gameAlias
- }
+ if (isGameDomainExtractable) {
+ const cleanHost = host.split(':')[0]
+ event.context.gameDomain = cleanHost
}
// gameData를 직접 가져와서 context에 저장 (API 호출 없이)
@@ -52,7 +49,7 @@ export default defineEventHandler(async event => {
const langCode = pathSegments[0] || 'ko'
const queryParams: Record = {
- game_alias: event.context.gameAlias || '',
+ game_domain: event.context.gameDomain || '',
lang_code: langCode,
}
@@ -63,7 +60,7 @@ export default defineEventHandler(async event => {
// 타입 단언을 사용하여 response의 타입 오류를 해결
const res = response as { code?: number; value?: unknown }
- if (res?.code === 0 && 'value' in res) {
+ if (res?.code === 0 && res && typeof res === 'object' && 'value' in res) {
event.context.gameData = res.value
}
} catch (error) {
diff --git a/layers/templates/GrVisual01/index.vue b/layers/templates/GrVisual01/index.vue
index 1922597..037c1d8 100644
--- a/layers/templates/GrVisual01/index.vue
+++ b/layers/templates/GrVisual01/index.vue
@@ -1,6 +1,7 @@
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ prev
+
+
+ next
+
+
diff --git a/package.json b/package.json
index ccdf8ef..36de89a 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"h3": "^1.15.4",
"nuxt": "^4.0.3",
"pinia": "^2.3.1",
+ "swiper": "^12.0.1",
"vue": "^3.5.0",
"vue-dompurify-html": "^5.3.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a8b024b..ffd0eda 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
pinia:
specifier: ^2.3.1
version: 2.3.1(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
+ swiper:
+ specifier: ^12.0.1
+ version: 12.0.1
vue:
specifier: ^3.5.0
version: 3.5.21(typescript@5.9.2)
@@ -4171,6 +4174,10 @@ packages:
engines: {node: '>=16'}
hasBin: true
+ swiper@12.0.1:
+ resolution: {integrity: sha512-Fi+gNw/tfc4hsGowQU5tRC/f1HFknkh4Vz8PaDI4JTinLUMTwhZyaovcH/va+iXq98BNUHN5ok0c2lEI82Fsgw==}
+ engines: {node: '>= 4.7.0'}
+
synckit@0.11.11:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -9190,6 +9197,8 @@ snapshots:
picocolors: 1.1.1
sax: 1.4.1
+ swiper@12.0.1: {}
+
synckit@0.11.11:
dependencies:
'@pkgr/core': 0.2.9