feat: dompurify 플러그인설치, 푸터 추가

This commit is contained in:
“hyeonggkim”
2025-09-10 09:55:28 +09:00
parent fcd4dc799b
commit 6f267879cf
5 changed files with 698 additions and 292 deletions

176
flow-diagram.md Normal file
View File

@@ -0,0 +1,176 @@
# 전체 로직 흐름도
## 1. 애플리케이션 초기화 및 요청 처리
```mermaid
graph TD
A[사용자 요청] --> B[Nuxt 서버 시작]
B --> C[환경 설정 로드]
C --> D[미들웨어 체인 실행]
D --> E[게임 데이터 로드]
E --> F[페이지 데이터 로드]
F --> G[컴포넌트 렌더링]
G --> H[최종 HTML 응답]
```
## 2. 서버 사이드 미들웨어 체인
```mermaid
graph TD
A[요청 수신] --> B{정적 자산?}
B -->|Yes| C[스킵]
B -->|No| D[gameAlias.ts 미들웨어]
D --> E[호스트에서 게임 별칭 추출]
E --> F[게임 데이터 API 호출]
F --> G[event.context에 저장]
G --> H[라우트 처리]
```
## 3. 데이터 흐름 및 상태 관리
```mermaid
graph LR
A[게임 데이터 API] --> B[GameDataStore]
C[페이지 데이터 API] --> D[PageDataStore]
B --> E[게임 기본 정보]
B --> F[지원 언어 목록]
B --> G[SEO 메타 정보]
D --> H[템플릿 배열]
D --> I[페이지별 컴포넌트]
D --> J[페이지 SEO 정보]
```
## 4. 국제화(i18n) 처리 흐름
```mermaid
graph TD
A[URL 요청] --> B[언어 코드 추출]
B --> C{지원 언어?}
C -->|No| D[404 에러]
C -->|Yes| E[동적 언어 설정]
E --> F[prefix 전략 적용]
F --> G[페이지 라우팅]
G --> H[템플릿 렌더링]
```
## 5. 페이지 렌더링 흐름
```mermaid
graph TD
A[app.vue] --> B[SSR gameData 로드]
B --> C[스토어에 저장]
C --> D[SEO 메타 태그 설정]
D --> E[NuxtLayout 렌더링]
E --> F[페이지 컴포넌트]
F --> G[pageData 로드]
G --> H[Section 컴포넌트]
H --> I[템플릿 렌더링]
```
## 6. 템플릿 시스템 구조
```mermaid
graph TD
A[Section 컴포넌트] --> B[templates 배열 순회]
B --> C[templateRegistry 조회]
C --> D{템플릿 코드 매칭}
D -->|GR_VISUAL_01| E[GrVisual01]
D -->|GR_VISUAL_02| F[GrVisual02]
D -->|GR_VISUAL_03| G[GrVisual03]
E --> H[동적 컴포넌트 렌더링]
F --> H
G --> H
H --> I[TemplatesBackground]
I --> J[최종 UI 출력]
```
## 7. 전체 요청-응답 사이클
```mermaid
sequenceDiagram
participant U as 사용자
participant S as 서버
participant M as 미들웨어
participant A as API
participant C as 컴포넌트
U->>S: 요청 (game.onstove.com/ko/about/story)
S->>M: gameAlias.ts 실행
M->>M: 게임 별칭 추출 (game)
M->>A: 게임 데이터 API 호출
A-->>M: 게임 정보 응답
M->>M: event.context에 저장
S->>C: 페이지 컴포넌트 로드
C->>A: 페이지 데이터 API 호출
A-->>C: 템플릿 데이터 응답
C->>C: 템플릿 렌더링
C-->>S: HTML 생성
S-->>U: 최종 응답
```
## 8. API 통신 구조
```mermaid
graph LR
A[게임 데이터 API] --> B[/pub-comm/v1.0/template/game]
C[페이지 데이터 API] --> D[/pub-comm/v1.0/template/page]
B --> E[game_alias]
B --> F[lang_code]
D --> G[game_alias]
D --> H[lang_code]
D --> I[page_url]
E --> J[게임 기본 정보]
F --> J
G --> K[페이지별 템플릿]
H --> K
I --> K
```
## 9. 상태 관리 구조
```mermaid
graph TD
A[Pinia 스토어] --> B[useGameDataStore]
A --> C[usePageDataStore]
B --> D[gameData]
B --> E[setGameData]
B --> F[clearGameData]
C --> G[pageData]
C --> H[setPageData]
C --> I[clearPageData]
D --> J[게임 기본 정보]
D --> K[지원 언어 목록]
D --> L[SEO 메타 정보]
G --> M[템플릿 배열]
G --> N[페이지 컴포넌트]
G --> O[페이지 SEO 정보]
```
## 10. 컴포넌트 계층 구조
```mermaid
graph TD
A[app.vue] --> B[NuxtLayout]
B --> C[LayoutDefaultHeader]
B --> D[NuxtPage]
B --> E[LayoutDefaultFooter]
D --> F[페이지 컴포넌트]
F --> G[Section]
G --> H[TemplatesBackground]
G --> I[동적 템플릿 컴포넌트]
I --> J[GrVisual01]
I --> K[GrVisual02]
I --> L[GrVisual03]
A --> M[MoleculesLoadingFull]
A --> N[MoleculesLoadingLocal]
```

View File

@@ -1,12 +1,208 @@
<template>
<footer class="border-t p-4 bg-black">
<div class="flex items-center justify-between">
<div class="flex items-center">
<a href="/" class="text-white">로고</a>
<footer id="footer" ref="footerRef" class="bg-black">
<div class="inner relative max-w-7xl mx-auto px-10 py-8 text-[12px] text-gray-400 md:px-4 md:py-7 md:text-[12px]">
<div class="menu-area">
<ul class="flex items-center flex-wrap gap-1.5 md:gap-0">
<li v-for="(footerMenuItem, index) in footerLinks" :key="index"
class="footer-menu-item">
<NuxtLink
:to="footerMenuItem.link"
:target="footerMenuItem.target"
:class="[
footerMenuItem.active === 'y' && 'text-orange-600',
'hover:text-gray-600 transition-colors'
]"
>
{{ footerMenuItem.title }}
</NuxtLink>
</li>
</ul>
</div>
<div class="flex items-center">
<AtomsLanguageSwitcher />
<div class="rating-area">
<img :src="footerData.dev_ci_url" alt="게임등급" class="rating-image" />
<dl class="rating-info">
<dt class="rating-label">게임명</dt>
<dd class="rating-value">{{ footerData.game_rating_info.title }}</dd>
<dt class="rating-label">상호</dt>
<dd class="rating-value">{{ footerData.game_rating_info.company_name }}</dd>
<dt class="rating-label">이용등급</dt>
<dd class="rating-value">{{ footerData.game_rating_info.reg_no }}</dd>
<dt class="rating-label">등급분류번호</dt>
<dd class="rating-value">{{ footerData.game_rating_info.rating_grade }}</dd>
<dt class="rating-label">제작년월일</dt>
<dd class="rating-value">{{ footerData.game_rating_info.prod_date }}</dd>
<dt class="rating-label">신고(등록)번호</dt>
<dd class="rating-value">{{ footerData.game_rating_info.rating_class_no }}</dd>
</dl>
</div>
<div class="address-area mt-6">
<address class="not-italic text-gray-500">
<div class="row my-1.5">
<span v-dompurify-html="footerData.footer_info" class="[&_a]:cursor-pointer [&_a]:text-blue-500 [&_a]:underline"></span>
</div>
</address>
</div>
<div class="copyright-area mt-5 text-gray-500 md:mt-4">
<span>&copy; Smilegate. All rights reserved</span>
</div>
<div class="logo-area flex mt-3 md:mt-2.5">
<a
:href="locale === 'ja' ? 'https://www.smilegate.com/jp' : 'https://www.smilegate.com/en'"
target="_blank"
class="smilegate w-22 h-4.5 md:w-14 md:h-3"
>
<img
:src="footerData.dev_ci_url"
alt="스마일게이트 로고"
class="w-full h-full object-contain"
/>
</a>
<a href="https://www.nx3games.com" target="_blank" class="nx3 w-14 h-3.5 ml-2.5 md:w-9 md:h-2.5 md:ml-4">
<img
:src="footerData.dev_ci_url"
alt="NX3 로고"
class="w-full h-full object-contain"
/>
</a>
</div>
<div class="language-area absolute bottom-7 right-10 text-white md:bottom-5.5 md:right-4">
<!-- <SelectLanguage /> -->
<!-- <AtomsLanguageSwitcher /> -->
</div>
</div>
</footer>
</template>
<script setup lang="ts">
const { locale } = useI18n();
interface FooterMenuType {
id: string;
title: string;
link: string;
target: string;
active: string;
highlight?: string;
}
const footerLinks = ref<FooterMenuType[]>([
{
"id": "company",
"title": "회사소개",
"link": "#",
"target": "_blank",
"active": "n"
},
{
"id": "terms",
"title": "이용약관",
"link": "https://common.game.onstove.com/terms/index?gameType=SG&termsType=1&langCode=@m{Terms_Lang_Code}",
"target": "_blank",
"active": "n"
},
{
"id": "privacy",
"title": "개인정보처리방침",
"link": "https://clause.onstove.com/stove/terms?category=privacy",
"target": "_blank",
"active": "y"
},
{
"id": "operation",
"title": "운영정책",
"link": "https://common.game.onstove.com/terms/index?gameType=CZN&termsType=3&langCode=@m{Terms_Lang_Code}",
"target": "_blank",
"active": "n"
},
{
"id": "fund",
"title": "청소년보호정책",
"link": "https://common.game.onstove.com/terms/index?gameType=CZN&termsType=6&langCode=ja",
"target": "_blank",
"active": "n"
},
{
"id": "customerService",
"title": "게임 이용 등급",
"link": "https://cs.onstove.com/@m{Terms_Lang_Code}/service/STOVE_CHAOSZERO",
"target": "_blank",
"active": "n"
}
] as FooterMenuType[])
const footerData = ref({
"dev_ci_url": "https://cdn.testgame.com/ci/ko_logo.png",
"use_dev_ci_url": true,
"fund_display_yn": true,
"use_game_rating": true,
"fund_display_url": "https://testgame.com/law/fund-ko",
"game_rating_info": {
"title": "테스트 게임",
"reg_no": "R-2024-7890",
"prod_date": "2024-05-01",
"rating_type": "15",
"company_name": "테스트엔터테인먼트",
"content_info": "1,2,3,",
"rating_grade": "15세 이용가",
"rating_class_no": "2024-123456"
},
"footer_info": "(주)스마일게이트홀딩스 메가포트지점 대표: 성준호<br>주소: 경기도 성남시 분당구 분당로 55, 7층 (서현동 분당 퍼스트타워)<br>통신판매업 신고번호: 제2023-성남분당A-0145호<br>사업자등록번호: 813-85-02492<br>E-mail: <a href='mailto:help@smilegate.com'>help@smilegate.com</a><br>TEL: 1670-0399",
})
</script>
<style scoped>
.footer-menu-item {
@apply text-sm md:text-xs md:tracking-tight relative;
}
.footer-menu-item:not(:first-child)::before {
content: '';
@apply inline-block bg-gray-400 h-2 w-px mx-1.5 mt-1 align-top;
}
@media (min-width: 768px) {
.footer-menu-item:nth-of-type(5)::before {
display: none;
}
}
@media (max-width: 411px) {
.footer-menu-item {
@apply th:text-xs;
}
}
@media (max-width: 321px) {
.footer-menu-item {
@apply th:text-[14px];
}
}
/* Rating Area Styles */
.rating-area {
@apply flex mt-2.5 md:flex-col md:mt-1.5;
}
.rating-image {
@apply w-41 h-14 md:w-34 md:h-12 md:order-1;
}
.rating-info {
@apply grid grid-cols-[110px_auto_110px_auto] w-full max-w-[490px] ml-5
border-t border-l border-gray-600 tracking-tight
md:grid-cols-[66px_auto_84px_auto] md:max-w-[358px] md:m-0 md:mb-2.5;
}
.rating-label {
@apply p-1.5 px-4 border-r border-b border-gray-600 bg-[#1a1a1a] md:p-0.5 md:px-1.5;
}
.rating-value {
@apply p-1.5 px-4 border-r border-b border-gray-600 text-gray-500 md:p-0.5 md:px-1.5;
}
</style>

View File

@@ -0,0 +1,16 @@
import DOMPurify from 'dompurify'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive('dompurify-html', {
mounted(el: HTMLElement, binding: any) {
if (binding.value) {
el.innerHTML = DOMPurify.sanitize(binding.value)
}
},
updated(el: HTMLElement, binding: any) {
if (binding.value) {
el.innerHTML = DOMPurify.sanitize(binding.value)
}
}
})
})

View File

@@ -18,6 +18,7 @@
"@pinia/nuxt": "^0.6.1",
"@vueuse/core": "^13.6.0",
"@vueuse/nuxt": "^13.6.0",
"dompurify": "^3.2.6",
"h3": "^1.15.4",
"nuxt": "^4.0.3",
"pinia": "^2.3.1",
@@ -25,6 +26,7 @@
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",
"@types/dompurify": "^3.2.0",
"@types/node": "^24.3.0",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",

588
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff