Compare commits

...

11 Commits

15 changed files with 571 additions and 50 deletions

8
.cursorrules Normal file
View File

@@ -0,0 +1,8 @@
# 커밋 메시지 (Generate Commit Message 포함)
커밋 메시지는 **반드시 한글**로 작성한다.
- 형식: `feat: 내용`, `refactor: 내용`, `test: 내용` 등
- 내용은 한글로, 제목 한 줄, 변경 의도가 드러나게 간결하게
예: `feat: 회원 가입 폼 유효성 검사 추가`, `refactor: 결제 모듈 상태 관리 로직 분리`

View File

@@ -4,7 +4,7 @@ BASE_DOMAIN='.onstove.com'
# URLS ############################################################################## # URLS ##############################################################################
STATIC_URL='https://static-pubcomm.onstove.com' STATIC_URL='https://static-pubcomm.onstove.com'
ASSETS_URL='https://static-pubcomm.onstove.com/live/template/brand' ASSETS_URL='https://static-pubcomm.onstove.com/live/template/brand'
DATA_RESOURCE_URL='https://static-pubcomm.gate8.com/live/STOVE_PUBTEMPLATE' DATA_RESOURCE_URL='https://static-pubcomm.onstove.com/live/STOVE_PUBTEMPLATE'
# STOVE ############################################################################# # STOVE #############################################################################
# STOVE - API Url # STOVE - API Url

View File

@@ -4,8 +4,8 @@ BASE_DOMAIN='.gate8.com'
# URLS ############################################################################## # URLS ##############################################################################
STATIC_URL='https://static-pubcomm.gate8.com' STATIC_URL='https://static-pubcomm.gate8.com'
ASSETS_URL='https://static-pubcomm.gate8.com/sandbox/template/brand' ASSETS_URL='https://static-pubcomm.gate8.com/sandbox/template/brand'
DATA_RESOURCE_URL='https://static-pubcomm.gate8.com/sandbox/test'
DATA_RESOURCE_URL='https://static-pubcomm.gate8.com/sandbox/STOVE_PUBTEMPLATE' DATA_RESOURCE_URL='https://static-pubcomm.gate8.com/sandbox/STOVE_PUBTEMPLATE'
# STOVE ############################################################################# # STOVE #############################################################################
# STOVE - API Url # STOVE - API Url
STOVE_API_URL=https://api.gate8.com STOVE_API_URL=https://api.gate8.com

View File

@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute()
const requestURL = useRequestURL()
const { locale } = useI18n() const { locale } = useI18n()
const gameDataStore = useGameDataStore() const gameDataStore = useGameDataStore()
const modalStore = useModalStore() const modalStore = useModalStore()
@@ -91,7 +93,99 @@ const setupGameHead = () => {
} }
} }
// Schema.org 구조화된 데이터 설정
const setupStructuredData = () => {
if (!gameName.value) return
try {
// SEO용 URL은 설정 도메인이 아닌 실제 요청 호스트 기준 (SSR/CSR 모두 useRequestURL)
const baseUrl = requestURL.origin
const currentLocale = locale.value ?? defaultLangCode.value ?? 'ko'
// Organization Schema
const organizationSchema: Record<string, any> = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: gameName.value,
url: baseUrl,
}
// 로고 이미지 추가 (favicon 중 PNG 우선)
if (faviconJson.value) {
const logoUrl = faviconJson.value[2] || faviconJson.value[0]
if (logoUrl) {
organizationSchema.logo = formatPathHost(logoUrl)
}
}
// SNS 링크 추가
const snsData = gameDataStore.snsJson
if (snsData) {
const sameAs: string[] = []
if (snsData.youtube?.use_yn === 1 && snsData.youtube?.url) {
sameAs.push(snsData.youtube.url)
}
if (snsData.twitter?.use_yn === 1 && snsData.twitter?.url) {
sameAs.push(snsData.twitter.url)
}
if (snsData.facebook?.use_yn === 1 && snsData.facebook?.url) {
sameAs.push(snsData.facebook.url)
}
if (snsData.discord?.use_yn === 1 && snsData.discord?.url) {
sameAs.push(snsData.discord.url)
}
if (snsData.instagram?.use_yn === 1 && snsData.instagram?.url) {
sameAs.push(snsData.instagram.url)
}
if (snsData.tiktok?.use_yn === 1 && snsData.tiktok?.url) {
sameAs.push(snsData.tiktok.url)
}
if (sameAs.length > 0) {
organizationSchema.sameAs = sameAs
}
}
// WebSite Schema - 사이트 정보
const websiteSchema: Record<string, any> = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: gameName.value,
url: baseUrl,
inLanguage: currentLocale,
}
// 설명 추가 (있는 경우)
if (gameMetaTag.value?.page_desc) {
websiteSchema.description = gameMetaTag.value.page_desc
}
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify(organizationSchema),
},
{
type: 'application/ld+json',
innerHTML: JSON.stringify(websiteSchema),
},
],
link: [
{
rel: 'canonical',
href: `${baseUrl}${route.path}`,
},
],
})
} catch (error) {
// eslint-disable-next-line no-console
console.error('[setupStructuredData] Failed to setup schema:', error)
}
}
setupGameHead() setupGameHead()
setupStructuredData()
let rafId: number | null = null let rafId: number | null = null
onMounted(() => { onMounted(() => {

View File

@@ -61,6 +61,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// SEO: Prevent search engine indexing of error pages
useSeoMeta({
robots: 'noindex, nofollow',
})
interface ErrorProps { interface ErrorProps {
error?: { error?: {
statusCode?: number statusCode?: number

View File

@@ -119,6 +119,11 @@ import { globalDateFormat } from '@seed-next/date'
import AtomsIconsPlayRoundFill from '#layers/components/atoms/icons/PlayRoundFill.vue' import AtomsIconsPlayRoundFill from '#layers/components/atoms/icons/PlayRoundFill.vue'
import type { PlatformTransformType } from '#layers/types/api/gameData' import type { PlatformTransformType } from '#layers/types/api/gameData'
// SEO: Prevent search engine indexing of inspection pages
useSeoMeta({
robots: 'noindex, nofollow',
})
const config = useRuntimeConfig() const config = useRuntimeConfig()
const stoveApiUrl = config.public.stoveApiUrl as string const stoveApiUrl = config.public.stoveApiUrl as string

View File

@@ -36,7 +36,10 @@ export const useGetInspectionDataExternal = () => {
'GET', 'GET',
apiUrl apiUrl
)) as ResGetInspectionData )) as ResGetInspectionData
console.log('🚀 ~ getInspectionDataExternal ~ response:', response)
if (import.meta.dev) {
console.log('🚀 ~ getInspectionDataExternal ~ response:', response)
}
// FIXME: 테스트용 데이터 --------------------------------------------------- // FIXME: 테스트용 데이터 ---------------------------------------------------
/* if (['local', 'local-gate8', 'dev'].includes(`${runtimeConfig.public.runType}`)) { /* if (['local', 'local-gate8', 'dev'].includes(`${runtimeConfig.public.runType}`)) {

View File

@@ -0,0 +1,221 @@
/**
* Schema.org 구조화된 데이터 유틸리티
*
* Schema.org 마크업을 쉽게 추가하고 관리하기 위한 composable
*
* @example
* ```ts
* const { addSchema, addVideoSchema, addImageGallerySchema } = useSchema()
*
* // 커스텀 스키마 추가
* addSchema({
* '@context': 'https://schema.org',
* '@type': 'Article',
* headline: 'My Article'
* })
*
* // 비디오 스키마 추가
* addVideoSchema({
* name: 'Video Title',
* description: 'Video Description',
* thumbnailUrl: 'https://example.com/thumb.jpg',
* uploadDate: '2025-01-01',
* contentUrl: 'https://youtube.com/watch?v=xxx'
* })
* ```
*/
export const useSchema = () => {
const gameDataStore = useGameDataStore()
const { gameName } = storeToRefs(gameDataStore)
/**
* 커스텀 Schema.org JSON-LD를 head에 추가
*/
const addSchema = (schema: Record<string, any>) => {
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify(schema),
},
],
})
}
/**
* VideoObject Schema 생성 및 추가
*/
const addVideoSchema = (options: {
name: string
description?: string
thumbnailUrl: string
uploadDate: string
contentUrl: string
embedUrl?: string
}) => {
const schema = {
'@context': 'https://schema.org',
'@type': 'VideoObject',
name: options.name,
description: options.description || options.name,
thumbnailUrl: options.thumbnailUrl,
uploadDate: options.uploadDate,
contentUrl: options.contentUrl,
embedUrl: options.embedUrl || options.contentUrl,
publisher: {
'@type': 'Organization',
name: gameName.value,
},
}
addSchema(schema)
}
/**
* ImageGallery Schema 생성 및 추가
*/
const addImageGallerySchema = (
images: Array<{
contentUrl: string
caption?: string
}>
) => {
const schema = {
'@context': 'https://schema.org',
'@type': 'ImageGallery',
image: images.map(img => ({
'@type': 'ImageObject',
contentUrl: img.contentUrl,
caption: img.caption || '',
})),
}
addSchema(schema)
}
/**
* SoftwareApplication Schema 생성 및 추가
*/
const addSoftwareApplicationSchema = (options: {
name: string
operatingSystem: string
description?: string
price?: string
priceCurrency?: string
}) => {
const schema: Record<string, any> = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: options.name,
operatingSystem: options.operatingSystem,
applicationCategory: 'GameApplication',
}
if (options.description) {
schema.description = options.description
}
if (options.price !== undefined) {
schema.offers = {
'@type': 'Offer',
price: options.price,
priceCurrency: options.priceCurrency || 'USD',
}
}
addSchema(schema)
}
/**
* Event Schema 생성 및 추가
*/
const addEventSchema = (options: {
name: string
startDate: string
endDate?: string
eventStatus?:
| 'EventScheduled'
| 'EventCancelled'
| 'EventMovedOnline'
| 'EventPostponed'
| 'EventRescheduled'
description?: string
image?: string
location?: string
}) => {
const schema: Record<string, any> = {
'@context': 'https://schema.org',
'@type': 'Event',
name: options.name,
startDate: options.startDate,
eventStatus: `https://schema.org/${options.eventStatus || 'EventScheduled'}`,
}
if (options.endDate) {
schema.endDate = options.endDate
}
if (options.description) {
schema.description = options.description
}
if (options.image) {
schema.image = options.image
}
if (options.location) {
schema.location = {
'@type': 'VirtualLocation',
url: options.location,
}
}
addSchema(schema)
}
/**
* Article Schema 생성 및 추가
*/
const addArticleSchema = (options: {
headline: string
datePublished: string
dateModified?: string
author?: string
description?: string
image?: string
}) => {
const schema: Record<string, any> = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: options.headline,
datePublished: options.datePublished,
author: {
'@type': 'Organization',
name: options.author || gameName.value,
},
}
if (options.dateModified) {
schema.dateModified = options.dateModified
}
if (options.description) {
schema.description = options.description
}
if (options.image) {
schema.image = options.image
}
addSchema(schema)
}
return {
addSchema,
addVideoSchema,
addImageGallerySchema,
addSoftwareApplicationSchema,
addEventSchema,
addArticleSchema,
}
}

View File

@@ -1,4 +1,70 @@
<script setup lang="ts"></script> <script setup lang="ts">
const route = useRoute()
const { locale } = useI18n()
const gameDataStore = useGameDataStore()
const { gameName } = storeToRefs(gameDataStore)
const requestURL = useRequestURL()
// BreadcrumbList Schema 생성
const breadcrumbSchema = computed(() => {
if (!gameName.value) return null
const baseUrl = requestURL.origin
console.log("🚀 ~ baseUrl:", baseUrl)
const pathSegments = route.path.split('/').filter(Boolean)
const currentLocale = pathSegments[0] || locale.value
const itemListElement: Array<{
'@type': string
position: number
name: string
item: string
}> = [
{
'@type': 'ListItem',
position: 1,
name: gameName.value,
item: `${baseUrl}/${currentLocale}`,
},
]
// 로케일 이후의 경로 세그먼트들을 breadcrumb에 추가
pathSegments.slice(1).forEach((segment, index) => {
const path = `/${pathSegments.slice(0, index + 2).join('/')}`
const displayName = segment
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase())
itemListElement.push({
'@type': 'ListItem',
position: index + 2,
name: displayName,
item: `${baseUrl}${path}`,
})
})
console.log("🚀 ~ itemListElement:", itemListElement)
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement,
}
})
// Schema를 head에 추가
watchEffect(() => {
if (breadcrumbSchema.value) {
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify(breadcrumbSchema.value),
},
],
})
}
})
</script>
<template> <template>
<LayoutsHeader /> <LayoutsHeader />

View File

@@ -1,4 +1,68 @@
<script setup lang="ts"></script> <script setup lang="ts">
const route = useRoute()
const { locale } = useI18n()
const gameDataStore = useGameDataStore()
const { gameName } = storeToRefs(gameDataStore)
const runtimeConfig = useRuntimeConfig()
// BreadcrumbList Schema 생성
const breadcrumbSchema = computed(() => {
if (!gameName.value || !runtimeConfig.public.baseDomain) return null
const baseUrl = `https://${runtimeConfig.public.baseDomain}`
const pathSegments = route.path.split('/').filter(Boolean)
const currentLocale = pathSegments[0] || locale.value
const itemListElement: Array<{
'@type': string
position: number
name: string
item: string
}> = [
{
'@type': 'ListItem',
position: 1,
name: gameName.value,
item: `${baseUrl}/${currentLocale}`,
},
]
// 로케일 이후의 경로 세그먼트들을 breadcrumb에 추가
pathSegments.slice(1).forEach((segment, index) => {
const path = `/${pathSegments.slice(0, index + 2).join('/')}`
const displayName = segment
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase())
itemListElement.push({
'@type': 'ListItem',
position: index + 2,
name: displayName,
item: `${baseUrl}${path}`,
})
})
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement,
}
})
// Schema를 head에 추가
watchEffect(() => {
if (breadcrumbSchema.value) {
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify(breadcrumbSchema.value),
},
],
})
}
})
</script>
<template> <template>
<LayoutsHeader /> <LayoutsHeader />

View File

@@ -1,3 +1,4 @@
import { LRUCache } from 'lru-cache'
import { usePageDataStore } from '#layers/stores/usePageDataStore' import { usePageDataStore } from '#layers/stores/usePageDataStore'
import { useLoadingStore } from '#layers/stores/useLoadingStore' import { useLoadingStore } from '#layers/stores/useLoadingStore'
import { commonFetch } from '#layers/utils/apiUtil' import { commonFetch } from '#layers/utils/apiUtil'
@@ -5,11 +6,24 @@ import { getGameDomain, getPathAfterLanguage } from '#layers/utils/urlUtil'
import { DEFAULT_LOCALE_CODE } from '@/i18n.config' import { DEFAULT_LOCALE_CODE } from '@/i18n.config'
import type { PageDataResponse } from '#layers/types/api/pageData' import type { PageDataResponse } from '#layers/types/api/pageData'
/** 페이지 데이터 API 응답 LRU */
const pageDataResponseCache = new LRUCache<string, PageDataResponse>({
max: 100,
ttl: 1000 * 60,
})
export default defineNuxtRouteMiddleware(async (to, _from) => { export default defineNuxtRouteMiddleware(async (to, _from) => {
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
// error 페이지는 실행X ----- // error 페이지는 실행X -----
if (to.path.includes('/error')) return if (to.path.includes('/error')) {
return abortNavigation(
createError({
statusCode: 404,
statusMessage: 'error page',
})
)
}
// inspection 페이지는 실행X ----- // inspection 페이지는 실행X -----
if (to.path.includes('/inspection')) return if (to.path.includes('/inspection')) return
@@ -64,11 +78,22 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
} }
} }
pageDataResponse = (await commonFetch('GET', apiUrl, { const cacheKey = `${apiUrl}:${JSON.stringify(queryParams)}`
query: queryParams, const cached = pageDataResponseCache.get(cacheKey)
})) as PageDataResponse | null if (cached) {
pageDataResponse = cached
} else {
pageDataResponse = (await commonFetch('GET', apiUrl, {
query: queryParams,
})) as PageDataResponse | null
if (pageDataResponse?.code === 0 && 'value' in pageDataResponse) {
pageDataResponseCache.set(cacheKey, pageDataResponse)
}
}
console.log('🚀 ~ pageData.global response:', pageDataResponse) if (import.meta.dev) {
console.log('🚀 ~ pageData.global response:', pageDataResponse)
}
} catch (error) { } catch (error) {
pageDataStore.clearPageData() pageDataStore.clearPageData()
console.error(error) console.error(error)
@@ -76,7 +101,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
createError({ createError({
statusCode: error.statusCode, statusCode: error.statusCode,
statusMessage: error.message, statusMessage: error.message,
fatal: false, // 즉시 에러 페이지로 fatal: true,
data: { reason: 'post-not-found' }, data: { reason: 'post-not-found' },
}) })
) )
@@ -98,7 +123,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
return abortNavigation( return abortNavigation(
createError({ createError({
statusCode: 404, statusCode: 404,
fatal: false, // 즉시 에러 페이지로 fatal: true,
statusMessage: pageDataResponse?.message, statusMessage: pageDataResponse?.message,
data: { reason: 'post-not-found' }, data: { reason: 'post-not-found' },
}) })
@@ -112,14 +137,14 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
createError({ createError({
statusCode: 404, statusCode: 404,
statusMessage: pageDataResponse?.message, statusMessage: pageDataResponse?.message,
fatal: false, // 즉시 에러 페이지로 fatal: true,
data: { data: {
reason: 'invalid-lang-code', reason: 'invalid-lang-code',
}, },
}) })
) )
} }
return navigateTo(`/${currentLangCode}/home`) return navigateTo(`/${currentLangCode}/home`, { external: true })
} }
// [TODO] // [TODO]

View File

@@ -61,11 +61,16 @@ function setCacheHeaders(
event.node.res.setHeader('Cache-Control', cacheControl) event.node.res.setHeader('Cache-Control', cacheControl)
} }
const cache = new LRUCache<string, WebInspectionData>({ const inspectionDataCache = new LRUCache<string, WebInspectionData>({
max: 100, // 캐시에 저장할 최대 항목 수 max: 100, // 캐시에 저장할 최대 항목 수
ttl: 1000 * 30, // 30초 동안 캐시 유지 ttl: 1000 * 30, // 30초 동안 캐시 유지
}) })
const gameDataResponseCache = new LRUCache<string, GameDataResponse>({
max: 100,
ttl: 1000 * 60, // 60초
})
/** /**
* Locale Middleware 역할 함수 * Locale Middleware 역할 함수
* URL의 언어 코드를 최종 언어로 변경하거나 추가 * URL의 언어 코드를 최종 언어로 변경하거나 추가
@@ -152,14 +157,23 @@ export default defineEventHandler(async event => {
game_domain: gameDomain || '', game_domain: gameDomain || '',
lang_code: getPathLocale(event?.node.req.url), lang_code: getPathLocale(event?.node.req.url),
} }
const response = await $fetch<GameDataResponse>(gameApiUrl, { const gameDataCacheKey = `${gameApiUrl}:${JSON.stringify(queryParams)}`
query: queryParams, const cachedGameData = gameDataResponseCache.get(gameDataCacheKey)
}) if (cachedGameData) {
gameDataResponse = cachedGameData
} else {
const response = await $fetch<GameDataResponse>(gameApiUrl, {
query: queryParams,
})
gameDataResponse = response
if (gameDataResponse?.code === 0 && 'value' in gameDataResponse) {
gameDataResponseCache.set(gameDataCacheKey, gameDataResponse)
}
}
gameDataResponse = response gameDataLangCodes = gameDataResponse?.value?.lang_codes || null
gameDataLangCodes = response?.value?.lang_codes || null gameDataDefaultLangCode = gameDataResponse?.value?.default_lang_code || null
gameDataDefaultLangCode = response?.value?.default_lang_code || null gameDataIntro = gameDataResponse?.value?.intro?.page_url || ''
gameDataIntro = response?.value?.intro?.page_url || ''
event.context.gameDomain = gameDomain event.context.gameDomain = gameDomain
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@@ -171,7 +185,10 @@ export default defineEventHandler(async event => {
const gameDataValue = gameDataResponse.value const gameDataValue = gameDataResponse.value
event.context.gameData = gameDataValue event.context.gameData = gameDataValue
event.context.googleAnalyticsId = gameDataValue?.ga_code event.context.googleAnalyticsId = gameDataValue?.ga_code
console.log('🚀 ~ gameData response:', event.context.gameData)
if (import.meta.dev) {
console.log('🚀 ~ gameData response:', event.context.gameData)
}
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------
// [Inspection Middleware] // [Inspection Middleware]
@@ -201,7 +218,7 @@ export default defineEventHandler(async event => {
let inspectionData let inspectionData
// 3. 캐시된 데이터가 없거나 만료되었을 때만 API 호출 // 3. 캐시된 데이터가 없거나 만료되었을 때만 API 호출
const cachedData = cache.get(cacheKey) const cachedData = inspectionDataCache.get(cacheKey)
if (cachedData) { if (cachedData) {
inspectionData = cachedData inspectionData = cachedData
} else { } else {
@@ -214,7 +231,7 @@ export default defineEventHandler(async event => {
}) })
inspectionData = response?.value?.inspection as WebInspectionData inspectionData = response?.value?.inspection as WebInspectionData
if (inspectionData) { if (inspectionData) {
cache.set(cacheKey, inspectionData) // 캐시에 저장 inspectionDataCache.set(cacheKey, inspectionData) // 캐시에 저장
} }
} }

View File

@@ -19,7 +19,7 @@ export default defineNitroPlugin(nitroApp => {
// 헬스체크 경로 체크 함수 추가 // 헬스체크 경로 체크 함수 추가
const isHealthCheck = (path: string): boolean => { const isHealthCheck = (path: string): boolean => {
return path === '/health' return path === '/health' || path === '/api/healthz'
} }
nitroApp.hooks.hook('request', event => { nitroApp.hooks.hook('request', event => {
@@ -33,22 +33,23 @@ export default defineNitroPlugin(nitroApp => {
const method = event.method || '' const method = event.method || ''
const headers = JSON.stringify(event.node.req.headers, null, 2) const headers = JSON.stringify(event.node.req.headers, null, 2)
const requestId = generateRequestId() const requestId = generateRequestId()
const domain = event.node.req.headers.host || 'unknown'
if (process.env.NODE_ENV !== 'development') { // if (process.env.NODE_ENV !== 'development') {
console.log( console.log(
`Request Info {"requestId":"${requestId}", "type":"request","method":"${method}","url":"${event.path}","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}", "headers" : "${headers}" }` `Request Info {"requestId":"${requestId}", "type":"request","method":"${method}","domain":"${domain}","url":"${event.path}","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}", "headers" : "${headers}" }`
) )
// 요청 완료 후 응답 상태 코드 로깅 // 요청 완료 후 응답 상태 코드 로깅
event.node.res.on('finish', () => { event.node.res.on('finish', () => {
console.log( console.log(
`Response Info {"requestId":"${requestId}","type":"response","method":"${method}","url":"${event.path}","statusCode":${event.node.res.statusCode},"responseTime":"${Date.now() - startTime}ms","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}","statusMessage":"${event.node.res.statusMessage}","responseHeader": ${JSON.stringify(event.node.res.getHeaders(), null, 2)}}` `Response Info {"requestId":"${requestId}","type":"response","method":"${method}","domain":"${domain}","url":"${event.path}","statusCode":${event.node.res.statusCode},"responseTime":"${Date.now() - startTime}ms","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}","statusMessage":"${event.node.res.statusMessage}","responseHeader": ${JSON.stringify(event.node.res.getHeaders(), null, 2)}}`
) )
console.log( console.log(
'===========================================================================================================================================================================================================================================================' '==========================================================================================================================================================================================================================================================='
) )
}) })
} // }
}) })
nitroApp.hooks.hook('error', error => { nitroApp.hooks.hook('error', error => {

View File

@@ -9,34 +9,40 @@ type RobotsConfig = {
} }
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const host = // Nuxt runtimeConfig 사용
(getHeader(event, 'host') || getRequestHost(event)).toString() || '' const runtimeConfig = useRuntimeConfig()
const baseDomain = process.env.BASE_DOMAIN || '.onstove.com' const baseDomain = runtimeConfig.public.baseDomain
const staticUrl = runtimeConfig.public.staticUrl
const runType = runtimeConfig.public.runType
// 프록시 뒤에 있는 경우 X-Forwarded-Host 우선 사용
const forwardedHost = getHeader(event, 'x-forwarded-host')
const host = (
forwardedHost ||
getHeader(event, 'host') ||
getRequestHost(event)
).toString() || ''
const isGameAliasExtractable = host.includes(baseDomain) const isGameAliasExtractable = host.includes(baseDomain)
let gameAlias = '' let gameAlias = ''
if (isGameAliasExtractable) { if (isGameAliasExtractable) {
gameAlias = host.split('.')[0] gameAlias = host.split('.')[0].replace(/-dev$/, '')
} }
// if (gameAlias && gameAlias !== "www") {
// event.context.gameAlias = gameAlias;
// }
// }
// robots 설정을 직접 가져오기 (미들웨어 context 사용)
let config: RobotsConfig let config: RobotsConfig
try { try {
// robots 설정 추출 // gameAlias가 있을 때만 sitemap 포함
const sitemapUrl = gameAlias
? [`${staticUrl}/${runType}/template/${gameAlias}/sitemap.xml`]
: undefined
config = { config = {
userAgent: '*', userAgent: '*',
allow: ['/'], allow: ['/'],
disallow: ['/error', '/inspection/', '/inspection/*', '/html/*'], disallow: ['/error', '/inspection/*', '/html/*'],
sitemap: [ sitemap: sitemapUrl,
`https://static-pubcomm.gate8.com/local/template/${gameAlias}/sitemap.xml`, host: gameAlias ? `${gameAlias}.onstove.com` : undefined,
],
host: `${gameAlias}.onstove.com`,
cache: { sMaxAge: 300, staleWhileRevalidate: 600 }, cache: { sMaxAge: 300, staleWhileRevalidate: 600 },
} }
} catch (error) { } catch (error) {
@@ -46,7 +52,7 @@ export default defineEventHandler(async event => {
config = { config = {
userAgent: '*', userAgent: '*',
allow: ['/'], allow: ['/'],
disallow: ['/error', '/inspection/', '/inspection/*', '/html/*'], disallow: ['/error', '/inspection/*', '/html/*'],
cache: { sMaxAge: 300, staleWhileRevalidate: 600 }, cache: { sMaxAge: 300, staleWhileRevalidate: 600 },
} }
} }
@@ -80,8 +86,14 @@ export default defineEventHandler(async event => {
: config.sitemap : config.sitemap
? [config.sitemap] ? [config.sitemap]
: [] : []
for (const sm of sitemaps) lines.push(`Sitemap: ${sm}`)
if (config.host) lines.push(`Host: ${config.host}`) for (const sm of sitemaps) {
lines.push(`Sitemap: ${sm}`)
}
if (config.host) {
lines.push(`Host: ${config.host}`)
}
// 마지막 개행 // 마지막 개행
return lines.join('\n').trim() + '\n' return lines.join('\n').trim() + '\n'

2
pnpm-lock.yaml generated
View File

@@ -54,7 +54,7 @@ importers:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(magicast@0.3.5) version: 4.0.0(magicast@0.3.5)
pinia: pinia:
specifier: 3.0.3 specifier: ^3.0.3
version: 3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) version: 3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
vue: vue:
specifier: ^3.5.0 specifier: ^3.5.0