fix. 코드 리팩토링, 워닝 수정
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
tag: string
|
||||
text: string
|
||||
text?: string
|
||||
imageSrc?: any
|
||||
imageClass?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const sanitizedContent = computed(() => {
|
||||
return props.text?.replace(/\n/g, '<br/>') || ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -27,6 +31,6 @@ const props = defineProps<Props>()
|
||||
:class="`${props.imageClass} hidden sm:block`"
|
||||
/>
|
||||
</template>
|
||||
<span v-else v-html="text?.replace(/\n/g, '<br/>') || ''" />
|
||||
<span v-else-if="text" v-dompurify-html="sanitizedContent" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRegistry } from '#layers/composables/useTemplateRegistry'
|
||||
import { templateRegistry } from '#layers/registry'
|
||||
import type { PageDataTemplate } from '#layers/types/api/pageData'
|
||||
|
||||
const props = defineProps<{ templates: PageDataTemplate[] }>()
|
||||
const registry = useTemplateRegistry() as Record<string, { component: any }>
|
||||
const registry = templateRegistry as Record<string, { component: any }>
|
||||
|
||||
const isShowTemplate = (template: PageDataTemplate) => {
|
||||
return template?.components && Object.keys(template.components).length > 0
|
||||
|
||||
@@ -1,53 +1,76 @@
|
||||
import { getHeader } from 'h3'
|
||||
import { useRuntimeConfig, useRequestEvent } from 'nuxt/app'
|
||||
|
||||
export const useGetGameAlias = () => {
|
||||
/**
|
||||
* 게임 별칭을 추출하는 유틸리티 함수
|
||||
* @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) {
|
||||
try {
|
||||
const event = useRequestEvent()
|
||||
|
||||
if (event) {
|
||||
// 미들웨어에서 설정한 gameAlias가 있다면 우선 사용
|
||||
if (event.context.gameAlias) {
|
||||
return event.context.gameAlias
|
||||
}
|
||||
|
||||
const host = getHeader(event, 'host') || ''
|
||||
const isGameAliasExtractable = host.includes(baseDomain)
|
||||
|
||||
if (isGameAliasExtractable) {
|
||||
const subdomain = host.split('.')[0]
|
||||
|
||||
if (subdomain && subdomain !== 'www') {
|
||||
return subdomain
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('useGetGameAlias server error: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 클라이언트 사이드에서 실행되는 경우
|
||||
if (import.meta.client) {
|
||||
try {
|
||||
const host = window.location.host
|
||||
const isGameAliasExtractable = host.includes(baseDomain)
|
||||
|
||||
if (isGameAliasExtractable) {
|
||||
const subdomain = host.split('.')[0]
|
||||
|
||||
if (subdomain && subdomain !== 'www') {
|
||||
return subdomain
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('useGetGameAlias client error: ', error)
|
||||
}
|
||||
return getGameAliasFromClient(baseDomain)
|
||||
}
|
||||
return ''
|
||||
|
||||
return getGameAliasFromServer(baseDomain)
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import { templateRegistry } from '#layers/registry'
|
||||
export const useTemplateRegistry = () => templateRegistry
|
||||
@@ -35,6 +35,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
const cleanData = JSON.parse(JSON.stringify(response.value))
|
||||
|
||||
store.setPageData(cleanData)
|
||||
} else {
|
||||
store.clearPageData()
|
||||
|
||||
45
layers/plugins/dompurify-html.ts
Normal file
45
layers/plugins/dompurify-html.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html'
|
||||
|
||||
export default defineNuxtPlugin(nuxtApp => {
|
||||
nuxtApp.vueApp.use(VueDOMPurifyHTML, {
|
||||
default: {
|
||||
ALLOWED_TAGS: [
|
||||
'br',
|
||||
'div',
|
||||
'b',
|
||||
'strong',
|
||||
'i',
|
||||
'em',
|
||||
'u',
|
||||
'a',
|
||||
'p',
|
||||
'ul',
|
||||
'li',
|
||||
'span',
|
||||
'img',
|
||||
'pre',
|
||||
'iframe',
|
||||
'input',
|
||||
'dl',
|
||||
'dt',
|
||||
'dd',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
'style',
|
||||
'id',
|
||||
'href',
|
||||
'src',
|
||||
'target',
|
||||
'alt',
|
||||
'class',
|
||||
'width',
|
||||
'height',
|
||||
'frameborder',
|
||||
'allowfullscreen',
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const callerInfoStore = useCallerInfoStore()
|
||||
|
||||
const callerId = `${runtimeConfig.public.stoveGameId}`
|
||||
const callerDetail = `${useCookie('sgs_da_uuid').value || ''}`
|
||||
|
||||
callerInfoStore.setCallerId(callerId)
|
||||
callerInfoStore.setCallerDetail(callerDetail)
|
||||
})
|
||||
@@ -1,33 +0,0 @@
|
||||
export const useCallerInfoStore = defineStore('callerInfoStore', () => {
|
||||
const callerId = ref<string>('')
|
||||
const callerDetail = ref<string>('')
|
||||
|
||||
const setCallerId = (id: string): void => {
|
||||
callerId.value = id
|
||||
}
|
||||
|
||||
const setCallerDetail = (detail: string): void => {
|
||||
callerDetail.value = detail
|
||||
}
|
||||
|
||||
// 초기화
|
||||
const resetCallerInfo = (): void => {
|
||||
callerId.value = ''
|
||||
callerDetail.value = ''
|
||||
}
|
||||
|
||||
// 현재 정보 반환
|
||||
const getCallerInfo = (): { callerId: string; callerDetail: string } => ({
|
||||
callerId: callerId.value,
|
||||
callerDetail: callerDetail.value,
|
||||
})
|
||||
|
||||
return {
|
||||
callerId,
|
||||
callerDetail,
|
||||
setCallerId,
|
||||
setCallerDetail,
|
||||
resetCallerInfo,
|
||||
getCallerInfo,
|
||||
}
|
||||
})
|
||||
45
layers/types/api/common.ts
Normal file
45
layers/types/api/common.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 공통 API 타입 정의
|
||||
*/
|
||||
|
||||
// HTTP 메소드 타입
|
||||
export type HttpMethod =
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'PATCH'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'DELETE'
|
||||
| 'CONNECT'
|
||||
| 'OPTIONS'
|
||||
| 'TRACE'
|
||||
|
||||
// 공통 Fetch 옵션 타입
|
||||
export interface FetchOptions {
|
||||
query?: object | null
|
||||
headers?: object | null
|
||||
body?: object | null
|
||||
key?: string | null
|
||||
loading?: { localId?: string } | boolean
|
||||
}
|
||||
|
||||
// Fetch 요청 옵션 타입
|
||||
export interface FetchRequestOptions {
|
||||
method: HttpMethod
|
||||
headers: Record<string, string>
|
||||
query?: object
|
||||
body?: object
|
||||
key?: string
|
||||
}
|
||||
|
||||
export interface FetchErrorObject {
|
||||
data?: unknown
|
||||
statusCode?: number
|
||||
statusMessage?: string
|
||||
}
|
||||
|
||||
// 요청 객체 타입 (IP 조회용)
|
||||
export interface RequestObject {
|
||||
headers: Record<string, string>
|
||||
socket: { remoteAddress?: string }
|
||||
}
|
||||
@@ -1,156 +1,151 @@
|
||||
import { ref } from 'vue'
|
||||
import type {
|
||||
HttpMethod,
|
||||
FetchOptions,
|
||||
FetchRequestOptions,
|
||||
FetchErrorObject,
|
||||
RequestObject,
|
||||
} from '#layers/types/api/common'
|
||||
|
||||
/**
|
||||
* 공통 API 호출
|
||||
*
|
||||
* @param {string} method - 요청 메소드 (GET, POST 등)
|
||||
* @param {string} url - API URL
|
||||
* @param {object} query - 요청 쿼리 파라미터
|
||||
* @param {object} headers - 요청 헤더
|
||||
* @param {object} body - 요청 본문
|
||||
* @param {string} key - 캐시 키
|
||||
* @param {object} loading - 로딩 설정
|
||||
* 로딩 상태를 시작하는 헬퍼 함수
|
||||
*/
|
||||
export const commonFetch = async (
|
||||
method:
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'PATCH'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'DELETE'
|
||||
| 'CONNECT'
|
||||
| 'OPTIONS'
|
||||
| 'TRACE' = 'GET', // Required
|
||||
url: string, // Required
|
||||
{
|
||||
query, // Optional
|
||||
headers, // Optional
|
||||
body, // Optional
|
||||
key, // Optional
|
||||
loading = false, // Optional
|
||||
}: {
|
||||
query?: object | null
|
||||
headers?: object | null
|
||||
body?: object | null
|
||||
key?: string | null
|
||||
loading?: { localId?: string } | boolean
|
||||
} = {}
|
||||
const startLoading = (
|
||||
loadingStore: ReturnType<typeof useLoadingStore>,
|
||||
loading: FetchOptions['loading']
|
||||
) => {
|
||||
let result = null
|
||||
const currCallerId = ref('')
|
||||
const currCallerDetail = ref('')
|
||||
if (!loadingStore) return
|
||||
|
||||
// 로딩 스토어 가져오기 (클라이언트에서만)
|
||||
let loadingStore: ReturnType<typeof useLoadingStore> | null = null
|
||||
if (import.meta.client) {
|
||||
try {
|
||||
loadingStore = useLoadingStore()
|
||||
// 로딩 시작
|
||||
loadingStore.setLoading(true)
|
||||
if (loading && loadingStore) {
|
||||
if (typeof loading === 'object' && loading.localId) {
|
||||
loadingStore.startLocalLoading(loading.localId)
|
||||
} else {
|
||||
loadingStore.startFullLoading()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Warning] Loading store not available:', e)
|
||||
}
|
||||
loadingStore.setLoading(true)
|
||||
|
||||
if (typeof loading === 'object' && loading.localId) {
|
||||
loadingStore.startLocalLoading(loading.localId)
|
||||
} else if (typeof loading === 'boolean') {
|
||||
loadingStore.startFullLoading()
|
||||
}
|
||||
|
||||
try {
|
||||
const { callerId, callerDetail } = storeToRefs(useCallerInfoStore())
|
||||
currCallerId.value = `${callerId.value}`
|
||||
currCallerDetail.value = `${callerDetail.value}`
|
||||
} catch (e) {
|
||||
// SSR: pinia store 생성 전이므로 빈 값('') 세팅
|
||||
}
|
||||
|
||||
try {
|
||||
const options: {
|
||||
method:
|
||||
| 'GET'
|
||||
| 'HEAD'
|
||||
| 'PATCH'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'DELETE'
|
||||
| 'CONNECT'
|
||||
| 'OPTIONS'
|
||||
| 'TRACE'
|
||||
headers: Record<string, string>
|
||||
query?: object
|
||||
body?: object
|
||||
key?: string
|
||||
} = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
}
|
||||
|
||||
if (query) {
|
||||
options.query = query
|
||||
}
|
||||
// 플랫폼 환경 API 호출 시 Caller-Id, Caller-Detail 헤더 추가
|
||||
if (url.includes('.onstove.com') || url.includes('.gate8.com')) {
|
||||
const callerInfo = {
|
||||
'Caller-Id': `${currCallerId.value}`,
|
||||
'Caller-Detail': `${currCallerDetail.value}`,
|
||||
}
|
||||
options.headers = { ...options.headers, ...callerInfo }
|
||||
}
|
||||
if (headers) {
|
||||
options.headers = { ...options.headers, ...headers }
|
||||
}
|
||||
if (body) {
|
||||
options.body = body
|
||||
}
|
||||
if (key) {
|
||||
options.key = key
|
||||
}
|
||||
|
||||
result = await $fetch(url, options)
|
||||
} catch (e: unknown) {
|
||||
console.error('[Exception] apiUtil.commonFetch: ', e)
|
||||
const error = e as {
|
||||
data?: unknown
|
||||
statusCode?: number
|
||||
statusMessage?: string
|
||||
}
|
||||
result = error.data || {
|
||||
code: error.statusCode,
|
||||
message: error.statusMessage,
|
||||
}
|
||||
} finally {
|
||||
// 로딩 종료
|
||||
if (loadingStore) {
|
||||
loadingStore.setLoading(false)
|
||||
if (loading) {
|
||||
if (typeof loading === 'object' && loading.localId) {
|
||||
loadingStore.stopLocalLoading(loading.localId)
|
||||
} else {
|
||||
loadingStore.stopFullLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 IP 조회
|
||||
*
|
||||
* @param {object} request - 요청 객체
|
||||
* 로딩 상태를 종료하는 헬퍼 함수
|
||||
*/
|
||||
export const getTrueClientIp = (request: {
|
||||
headers: Record<string, string>
|
||||
socket: { remoteAddress?: string }
|
||||
}) => {
|
||||
const requestHeaders = request.headers
|
||||
const targetHeaders = [
|
||||
const stopLoading = (
|
||||
loadingStore: ReturnType<typeof useLoadingStore>,
|
||||
loading: FetchOptions['loading']
|
||||
) => {
|
||||
if (!loadingStore) return
|
||||
|
||||
loadingStore.setLoading(false)
|
||||
|
||||
if (typeof loading === 'object' && loading.localId) {
|
||||
loadingStore.stopLocalLoading(loading.localId)
|
||||
} else if (typeof loading === 'boolean') {
|
||||
loadingStore.stopFullLoading()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 옵션을 구성하는 헬퍼 함수
|
||||
*/
|
||||
const buildRequestOptions = (
|
||||
method: HttpMethod,
|
||||
url: string,
|
||||
options: FetchOptions
|
||||
): FetchRequestOptions => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const callerId = runtimeConfig.public.stoveGameId || ''
|
||||
const callerDetail = useCookie('sgs_da_uuid').value || ''
|
||||
|
||||
const requestOptions: FetchRequestOptions = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
}
|
||||
|
||||
// 쿼리 파라미터 추가
|
||||
if (options.query) {
|
||||
requestOptions.query = options.query
|
||||
}
|
||||
|
||||
// 플랫폼 환경 API 호출 시 Caller 헤더 추가
|
||||
if (url.includes('.onstove.com') || url.includes('.gate8.com')) {
|
||||
requestOptions.headers = {
|
||||
...requestOptions.headers,
|
||||
'Caller-Id': callerId as string,
|
||||
'Caller-Detail': callerDetail as string,
|
||||
}
|
||||
}
|
||||
|
||||
// 사용자 정의 헤더 추가
|
||||
if (options.headers) {
|
||||
requestOptions.headers = {
|
||||
...requestOptions.headers,
|
||||
...(options.headers as Record<string, string>),
|
||||
}
|
||||
}
|
||||
|
||||
// 본문 데이터 추가
|
||||
if (options.body) {
|
||||
requestOptions.body = options.body
|
||||
}
|
||||
|
||||
// 캐시 키 추가
|
||||
if (options.key) {
|
||||
requestOptions.key = options.key
|
||||
}
|
||||
|
||||
return requestOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 API 호출 함수 (리팩토링된 버전)
|
||||
*
|
||||
* @param method - HTTP 메소드
|
||||
* @param url - API URL
|
||||
* @param options - 요청 옵션
|
||||
* @returns API 응답 데이터
|
||||
*/
|
||||
export const commonFetch = async (
|
||||
method: HttpMethod = 'GET',
|
||||
url: string,
|
||||
options: FetchOptions = {}
|
||||
): Promise<any> => {
|
||||
let loadingStore = null
|
||||
|
||||
if (import.meta.client) {
|
||||
loadingStore = useLoadingStore()
|
||||
}
|
||||
|
||||
startLoading(loadingStore, options.loading)
|
||||
|
||||
try {
|
||||
const requestOptions = buildRequestOptions(method, url, options)
|
||||
return await $fetch(url, requestOptions)
|
||||
} catch (error) {
|
||||
console.error('[Exception] apiUtil.commonFetch:', error)
|
||||
const fetchError = error as FetchErrorObject
|
||||
|
||||
return (
|
||||
fetchError.data || {
|
||||
code: fetchError.statusCode,
|
||||
message: fetchError.statusMessage,
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
stopLoading(loadingStore, options.loading)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 IP 조회 함수 (리팩토링된 버전)
|
||||
*
|
||||
* @param request - 요청 객체
|
||||
* @returns 클라이언트 IP 주소
|
||||
*/
|
||||
export const getTrueClientIp = (request: RequestObject): string => {
|
||||
const { headers, socket } = request
|
||||
|
||||
// IP 헤더 우선순위 목록
|
||||
const ipHeaders = [
|
||||
'True-Client-IP',
|
||||
'X-Real-IP',
|
||||
'X-Forwarded-For',
|
||||
@@ -159,22 +154,21 @@ export const getTrueClientIp = (request: {
|
||||
'HTTP_CLIENT_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
]
|
||||
for (const targetHeader of targetHeaders) {
|
||||
let ip =
|
||||
requestHeaders[targetHeader] || requestHeaders[targetHeader.toLowerCase()]
|
||||
if (ip !== undefined && ip != null && ip !== '') {
|
||||
if (ip.includes(',')) {
|
||||
ip = ip.split(',')[0]
|
||||
}
|
||||
return ip
|
||||
|
||||
// 헤더에서 IP 추출
|
||||
for (const header of ipHeaders) {
|
||||
const ip = headers[header] || headers[header.toLowerCase()]
|
||||
|
||||
if (ip?.trim()) {
|
||||
// 여러 IP가 있는 경우 첫 번째 IP 반환
|
||||
return ip.includes(',') ? ip.split(',')[0].trim() : ip.trim()
|
||||
}
|
||||
}
|
||||
if (
|
||||
request.socket.remoteAddress !== undefined &&
|
||||
request.socket.remoteAddress != null &&
|
||||
request.socket.remoteAddress !== ''
|
||||
) {
|
||||
return request.socket.remoteAddress
|
||||
|
||||
// 소켓에서 직접 IP 추출
|
||||
if (socket.remoteAddress?.trim()) {
|
||||
return socket.remoteAddress.trim()
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user