fix. 코드 리팩토링, 워닝 수정

This commit is contained in:
clkim
2025-09-16 18:24:35 +09:00
parent 84b8607bec
commit d6301350ae
10 changed files with 317 additions and 250 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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)
}

View File

@@ -1,2 +0,0 @@
import { templateRegistry } from '#layers/registry'
export const useTemplateRegistry = () => templateRegistry

View File

@@ -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()

View 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',
],
},
})
})

View File

@@ -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)
})

View File

@@ -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,
}
})

View 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 }
}

View File

@@ -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 ''
}