[feat] deploy,solution/frontend: 운영에서 dev 서버를 걷어낸다 — 정적 번들을 nginx 가 서빙
킹서버에 `vite dev` 가 떠 있었다. 요청마다 트랜스파일하고, 컨테이너 기동이 npm install 네트워크에 의존하고, /src 원본과 소스맵이 그대로 나간다. 발행물을 파는 사이트의 진입점이 dev 서버일 이유가 없다. - nginx/Dockerfile 신규: @o2o/frontend 를 굽는 build 스테이지 + 번들을 담은 nginx. 발행 사이트는 이미지에 안 넣는다 — 사이트가 늘 때마다 이미지를 다시 굽지 않으려고 site-out 볼륨에서 읽는다. - vite.config.ts: build.assetsDir='builder-assets'. 발행본과 같은 오리진이라 `/assets/` 를 서로 뺏는다 — 안 가르면 빌더 JS·CSS 가 404 인데 화면은 떠서 원인이 안 보인다. - site.conf: `/` 를 /srv/app 정적으로. index.html 은 no-cache — 번들 해시가 여기 박혀 있어 캐시되면 재배포해도 옛 번들 주소를 계속 부른다. `/fonts/` 는 발행본 먼저 보고 없으면 빌더로 떨어뜨린다(둘이 같은 경로를 각자 쓴다). - compose: solution-frontend(dev 서버)를 profile dev 로 내리고, 굽기만 하는 solution-prerender 를 기본으로 올린다. VITE_* 는 번들에 구워지므로 build args 다 — 주소를 바꾸면 재빌드해야 한다. docker compose config 통과
This commit is contained in:
parent
9fe7f3f7f5
commit
fceadd68ce
@ -30,8 +30,9 @@ usage() {
|
|||||||
환경변수
|
환경변수
|
||||||
DEPLOY_BRANCH 기본 main. 다른 브랜치를 배포할 때만 쓴다
|
DEPLOY_BRANCH 기본 main. 다른 브랜치를 배포할 때만 쓴다
|
||||||
|
|
||||||
서비스: solution-backend · solution-worker · solution-frontend · solution-site
|
서비스: solution-backend · solution-worker · solution-prerender · solution-site
|
||||||
admin-backend · admin-frontend (프로필 admin, 기본 기동에서 빠져 있다)
|
admin-backend · admin-frontend (프로필 admin, 기본 기동에서 빠져 있다)
|
||||||
|
solution-frontend (프로필 dev, 로컬 HMR 전용 — 운영에서 띄우지 않는다)
|
||||||
o2o-web4ai-solution-backend 처럼 컨테이너명으로 적어도 된다
|
o2o-web4ai-solution-backend 처럼 컨테이너명으로 적어도 된다
|
||||||
|
|
||||||
★ solution-backend·solution-worker·admin-backend 는 이미지가 한 벌이다. 하나를 빌드하면 나머지도 새 이미지로
|
★ solution-backend·solution-worker·admin-backend 는 이미지가 한 벌이다. 하나를 빌드하면 나머지도 새 이미지로
|
||||||
|
|||||||
@ -123,10 +123,14 @@ services:
|
|||||||
driver: json-file
|
driver: json-file
|
||||||
options: { max-size: "10m", max-file: "5" }
|
options: { max-size: "10m", max-file: "5" }
|
||||||
|
|
||||||
# 사장님 앱(:3000) + 발행 사이트 프리렌더·정적서버(:3001, `/s/*` 프록시)
|
# 발행 사이트를 굽는다. **굽기만 한다** — 서빙은 solution-site(nginx)가 맡는다.
|
||||||
solution-frontend:
|
#
|
||||||
|
# ★ 사장님 앱 dev 서버는 여기서 빠졌다. 운영에 `vite dev` 를 띄우면 요청마다 트랜스파일하고
|
||||||
|
# 기동이 npm install 네트워크에 의존하고 /src 원본이 그대로 나간다. 번들은 solution-site
|
||||||
|
# 이미지가 굽는다(nginx/Dockerfile). 로컬에서 HMR 이 필요하면 `--profile dev`.
|
||||||
|
solution-prerender:
|
||||||
image: node:24-alpine
|
image: node:24-alpine
|
||||||
container_name: o2o-web4ai-solution-frontend
|
container_name: o2o-web4ai-solution-prerender
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
@ -136,19 +140,12 @@ services:
|
|||||||
# ★ `-d node_modules` 로 판단하면 안 된다. 익명 볼륨은 빈 디렉토리로 이미 존재해서
|
# ★ `-d node_modules` 로 판단하면 안 된다. 익명 볼륨은 빈 디렉토리로 이미 존재해서
|
||||||
# 설치를 건너뛰고 `vite: not found`(exit 127)로 죽는다.
|
# 설치를 건너뛰고 `vite: not found`(exit 127)로 죽는다.
|
||||||
[ -x node_modules/.bin/vite ] || npm install
|
[ -x node_modules/.bin/vite ] || npm install
|
||||||
node solution/site/scripts/serve-sites.mjs &
|
exec node solution/site/scripts/watch-payloads.mjs
|
||||||
node solution/site/scripts/watch-payloads.mjs &
|
|
||||||
exec npm run dev -w @o2o/frontend
|
|
||||||
environment:
|
environment:
|
||||||
PORT: 3001
|
|
||||||
INDEXNOW_KEY: ${INDEXNOW_KEY:-}
|
INDEXNOW_KEY: ${INDEXNOW_KEY:-}
|
||||||
# ★ 발행 호스트를 프론트 .env 에 따로 적지 않는다 — 루트 .env 의 SITE_PUBLIC_HOST 를
|
# ★ 발행 호스트를 프론트 .env 에 따로 적지 않는다 — 루트 .env 의 SITE_PUBLIC_HOST 를
|
||||||
# 그대로 흘려보낸다. 두 곳에 적으면 canonical 과 화면 주소가 조용히 갈라진다.
|
# 그대로 흘려보낸다. 두 곳에 적으면 canonical 과 화면 주소가 조용히 갈라진다.
|
||||||
VITE_PUBLISH_HOST: ${SITE_PUBLIC_HOST:-w4ai.o2o.kr}
|
VITE_PUBLISH_HOST: ${SITE_PUBLIC_HOST:-w4ai.o2o.kr}
|
||||||
# ★ 이 둘은 **브라우저가** 부르는 주소다. 컨테이너 안에서 보는 주소가 아니라
|
|
||||||
# 화면을 연 사람이 닿을 수 있는 주소여야 한다 — localhost 는 서버에 올리는 순간 틀린다.
|
|
||||||
VITE_API_BASE_URL: ${PUBLIC_API_BASE_URL:-http://localhost:9800}
|
|
||||||
VITE_SITE_PREVIEW_URL: ${PUBLIC_WEB_BASE_URL:-http://localhost:3000}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./package.json:/app/package.json
|
- ./package.json:/app/package.json
|
||||||
- ./package-lock.json:/app/package-lock.json
|
- ./package-lock.json:/app/package-lock.json
|
||||||
@ -163,6 +160,45 @@ services:
|
|||||||
- /app/admin/frontend/node_modules
|
- /app/admin/frontend/node_modules
|
||||||
# ★ 산출물은 named volume. 호스트 경로면 재배포로 코드를 갈아엎는 순간 전 사이트가 404 다.
|
# ★ 산출물은 named volume. 호스트 경로면 재배포로 코드를 갈아엎는 순간 전 사이트가 404 다.
|
||||||
- site-out:/app/solution/site/out
|
- site-out:/app/solution/site/out
|
||||||
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options: { max-size: "10m", max-file: "5" }
|
||||||
|
|
||||||
|
# 사장님 앱 dev 서버 + 발행본 정적서버(:3001). **로컬 전용**이다 — `--profile dev`.
|
||||||
|
# 운영에서 이게 뜨면 안 된다(위 solution-prerender 주석).
|
||||||
|
solution-frontend:
|
||||||
|
image: node:24-alpine
|
||||||
|
container_name: o2o-web4ai-solution-frontend
|
||||||
|
profiles: ["dev"]
|
||||||
|
working_dir: /app
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
cd /app
|
||||||
|
[ -x node_modules/.bin/vite ] || npm install
|
||||||
|
node solution/site/scripts/serve-sites.mjs &
|
||||||
|
exec npm run dev -w @o2o/frontend
|
||||||
|
environment:
|
||||||
|
PORT: 3001
|
||||||
|
INDEXNOW_KEY: ${INDEXNOW_KEY:-}
|
||||||
|
VITE_PUBLISH_HOST: ${SITE_PUBLIC_HOST:-w4ai.o2o.kr}
|
||||||
|
# ★ 이 둘은 **브라우저가** 부르는 주소다. 컨테이너 안에서 보는 주소가 아니라
|
||||||
|
# 화면을 연 사람이 닿을 수 있는 주소여야 한다 — localhost 는 서버에 올리는 순간 틀린다.
|
||||||
|
VITE_API_BASE_URL: ${PUBLIC_API_BASE_URL:-http://localhost:9800}
|
||||||
|
VITE_SITE_PREVIEW_URL: ${PUBLIC_WEB_BASE_URL:-http://localhost:3000}
|
||||||
|
volumes:
|
||||||
|
- ./package.json:/app/package.json
|
||||||
|
- ./package-lock.json:/app/package-lock.json
|
||||||
|
- ./tsconfig.base.json:/app/tsconfig.base.json
|
||||||
|
- ./solution:/app/solution
|
||||||
|
- ./admin:/app/admin
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/solution/site/node_modules
|
||||||
|
- /app/solution/frontend/node_modules
|
||||||
|
- /app/admin/frontend/node_modules
|
||||||
|
- site-out:/app/solution/site/out
|
||||||
ports:
|
ports:
|
||||||
- "${WEB_BIND:-0.0.0.0}:${WEB_PORT:-3000}:3000"
|
- "${WEB_BIND:-0.0.0.0}:${WEB_PORT:-3000}:3000"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@ -202,16 +238,23 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
# ★ 운영에서는 내부망에만 연다. 0.0.0.0 으로 열면 앱을 가른 의미가 없다.
|
# ★ 운영에서는 내부망에만 연다. 0.0.0.0 으로 열면 앱을 가른 의미가 없다.
|
||||||
- "${ADMIN_BIND:-127.0.0.1}:${ADMIN_PORT:-3002}:3002"
|
- "${ADMIN_BIND:-127.0.0.1}:${ADMIN_PORT:-3002}:3002"
|
||||||
depends_on:
|
|
||||||
- solution-frontend
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options: { max-size: "10m", max-file: "5" }
|
options: { max-size: "10m", max-file: "5" }
|
||||||
|
|
||||||
# 발행 사이트 정적 서빙. 굽는 쪽(web)과 분리 — 볼륨 하나를 사이에 두고 서로를 모른다.
|
# 공개 진입점 하나. 사장님 앱(구운 번들) · 발행 사이트(볼륨) · API 프록시를 한 오리진으로 준다.
|
||||||
solution-site:
|
solution-site:
|
||||||
image: nginx:alpine
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: nginx/Dockerfile
|
||||||
|
args:
|
||||||
|
# ★ VITE_* 는 **번들에 구워진다.** .env 를 고쳐도 재빌드 전엔 안 바뀐다
|
||||||
|
# → 주소를 바꿨으면 `./deploy.sh solution-site`.
|
||||||
|
VITE_API_BASE_URL: ${PUBLIC_API_BASE_URL:-http://localhost:9800}
|
||||||
|
VITE_PUBLISH_HOST: ${SITE_PUBLIC_HOST:-w4ai.o2o.kr}
|
||||||
|
VITE_SITE_PREVIEW_URL: ${PUBLIC_WEB_BASE_URL:-http://localhost:3000}
|
||||||
|
image: o2o-web4ai-solution-site
|
||||||
container_name: o2o-web4ai-solution-site
|
container_name: o2o-web4ai-solution-site
|
||||||
volumes:
|
volumes:
|
||||||
- site-out:/srv/sites:ro
|
- site-out:/srv/sites:ro
|
||||||
@ -223,7 +266,7 @@ services:
|
|||||||
# 앞단 프록시를 세울 거면 여기서 포트만 옮기고 프록시가 이쪽을 가리키게 한다.
|
# 앞단 프록시를 세울 거면 여기서 포트만 옮기고 프록시가 이쪽을 가리키게 한다.
|
||||||
- "${SITE_HTTP_BIND:-0.0.0.0}:${SITE_HTTP_PORT:-80}:80"
|
- "${SITE_HTTP_BIND:-0.0.0.0}:${SITE_HTTP_PORT:-80}:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- solution-frontend
|
- solution-prerender
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
|
|||||||
36
nginx/Dockerfile
Normal file
36
nginx/Dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 공개 진입점 이미지 — 사장님 앱 정적 번들을 구워 넣고, nginx 가 그것과 발행 사이트를 함께 준다.
|
||||||
|
#
|
||||||
|
# ★ 왜 dev 서버를 안 띄우나
|
||||||
|
# 운영에 `vite dev` 를 두면 요청마다 트랜스파일하고, 기동이 `npm install` 네트워크에 의존하고,
|
||||||
|
# `/src/*` 원본과 소스맵이 그대로 나간다. 실측(킹서버): 컨테이너 기동에 npm install 이 걸려
|
||||||
|
# 재기동 시간이 네트워크 상태에 좌우됐다.
|
||||||
|
#
|
||||||
|
# ★ 발행 사이트(`/s/...`)는 이 이미지에 안 들어간다. named volume(site-out)에서 읽는다 —
|
||||||
|
# 사이트가 늘 때마다 이미지를 다시 굽지 않는다.
|
||||||
|
|
||||||
|
FROM node:24-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 의존성 레이어를 소스와 분리한다. 소스만 바뀌면 npm ci 를 건너뛴다.
|
||||||
|
COPY package.json package-lock.json tsconfig.base.json ./
|
||||||
|
COPY solution/shared/package.json solution/shared/
|
||||||
|
COPY solution/frontend/package.json solution/frontend/
|
||||||
|
COPY solution/site/package.json solution/site/
|
||||||
|
COPY admin/frontend/package.json admin/frontend/
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY solution ./solution
|
||||||
|
COPY admin ./admin
|
||||||
|
|
||||||
|
# ★ VITE_* 는 **빌드 시점에 번들로 구워진다.** 런타임 환경변수로는 못 바꾼다 —
|
||||||
|
# 주소를 바꾸면 이 이미지를 다시 빌드해야 한다(`./deploy.sh solution-site`).
|
||||||
|
ARG VITE_API_BASE_URL
|
||||||
|
ARG VITE_PUBLISH_HOST
|
||||||
|
ARG VITE_SITE_PREVIEW_URL
|
||||||
|
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \
|
||||||
|
VITE_PUBLISH_HOST=$VITE_PUBLISH_HOST \
|
||||||
|
VITE_SITE_PREVIEW_URL=$VITE_SITE_PREVIEW_URL
|
||||||
|
RUN npm run build -w @o2o/frontend
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=build /app/solution/frontend/dist /srv/app
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# 공개 진입점 하나. 사장님 앱 · 발행 사이트 · API 가 **같은 오리진**을 쓴다.
|
# 공개 진입점 하나. 사장님 앱 · 발행 사이트 · API 가 **같은 오리진**을 쓴다.
|
||||||
#
|
#
|
||||||
# / → 사장님 앱(solution-frontend:3000)
|
# / → 사장님 앱 (이미지에 구워 넣은 정적 번들, /srv/app)
|
||||||
# /s/<slug> → 발행 사이트 (site-out 볼륨에서 정적)
|
# /s/<slug> → 발행 사이트 (site-out 볼륨에서 정적)
|
||||||
# /assets/ → 발행본 공용 번들 (정적)
|
# /assets/ → 발행본 공용 번들 (정적)
|
||||||
# /robots.txt · /sitemap.xml → 크롤러가 읽는 파일 (정적)
|
# /robots.txt · /sitemap.xml → 크롤러가 읽는 파일 (정적)
|
||||||
@ -13,11 +13,6 @@
|
|||||||
# ★ 산출물은 named volume(site-out)으로 들어온다. 프리렌더가 쓰고 여기서 읽기만 한다 —
|
# ★ 산출물은 named volume(site-out)으로 들어온다. 프리렌더가 쓰고 여기서 읽기만 한다 —
|
||||||
# 호스트 경로가 등장하지 않으므로 재배포로 코드를 갈아엎어도 사이트가 죽지 않는다.
|
# 호스트 경로가 등장하지 않으므로 재배포로 코드를 갈아엎어도 사이트가 죽지 않는다.
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
|
||||||
default upgrade;
|
|
||||||
'' close;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
@ -34,9 +29,8 @@ server {
|
|||||||
client_max_body_size 20m;
|
client_max_body_size 20m;
|
||||||
|
|
||||||
# ★ Docker 내장 DNS. 업스트림을 변수로 두면 nginx 가 **기동할 때** 이름을 풀지 않는다 —
|
# ★ Docker 내장 DNS. 업스트림을 변수로 두면 nginx 가 **기동할 때** 이름을 풀지 않는다 —
|
||||||
# 안 그러면 solution-frontend 가 아직 안 떴을 때 nginx 자체가 죽는다.
|
# 안 그러면 solution-backend 가 아직 안 떴을 때 nginx 자체가 죽는다.
|
||||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||||
set $builder http://solution-frontend:3000;
|
|
||||||
set $api http://solution-backend:9800;
|
set $api http://solution-backend:9800;
|
||||||
|
|
||||||
# 텍스트 산출물은 압축이 크게 먹는다(HTML 55KB → 10KB 안팎).
|
# 텍스트 산출물은 압축이 크게 먹는다(HTML 55KB → 10KB 안팎).
|
||||||
@ -65,9 +59,17 @@ server {
|
|||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ★ 빌더와 발행본이 `/fonts/` 를 **각자** 쓴다(vite.config.ts 주석). 발행본을 먼저 보고
|
||||||
|
# 없으면 빌더 것으로 떨어뜨린다 — 한쪽만 잡으면 다른 쪽 폰트가 조용히 404 다.
|
||||||
location ^~ /fonts/ {
|
location ^~ /fonts/ {
|
||||||
|
root /srv/sites;
|
||||||
add_header Cache-Control "public, max-age=604800";
|
add_header Cache-Control "public, max-age=604800";
|
||||||
access_log off;
|
access_log off;
|
||||||
|
try_files $uri @app_fonts;
|
||||||
|
}
|
||||||
|
location @app_fonts {
|
||||||
|
root /srv/app;
|
||||||
|
access_log off;
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,19 +107,23 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── 사장님 앱 (그 외 전부) ─────────────────────────────────
|
# ── 사장님 앱 (그 외 전부) ─────────────────────────────────
|
||||||
# ★ Vite dev 서버다. Host 헤더를 그대로 넘기므로 vite.config.ts 의 allowedHosts 에
|
# 해시가 박힌 번들. 내용이 바뀌면 이름이 바뀌므로 영구 캐시가 안전하다.
|
||||||
# 발행 호스트가 들어 있어야 한다 — 없으면 전부 403 "Blocked request" 다.
|
# ★ 발행본 `/assets/` 와 겹치지 않게 빌더만 `builder-assets` 로 뺐다
|
||||||
|
# (solution/frontend/vite.config.ts 의 build.assetsDir).
|
||||||
|
location ^~ /builder-assets/ {
|
||||||
|
root /srv/app;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
access_log off;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA 다. 없는 경로는 index.html 로 넘겨 클라이언트 라우터가 받게 한다.
|
||||||
|
# ★ index.html 은 캐시하지 않는다 — 여기에 번들 해시가 박혀 있어서, 캐시되면
|
||||||
|
# 새로 배포해도 브라우저가 옛 번들 주소를 계속 부른다(404 → 흰 화면).
|
||||||
location / {
|
location / {
|
||||||
proxy_pass $builder;
|
root /srv/app;
|
||||||
proxy_http_version 1.1;
|
try_files $uri $uri/index.html /index.html;
|
||||||
proxy_set_header Host $host;
|
add_header Cache-Control "no-cache";
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
|
||||||
# HMR 웹소켓. 없으면 화면은 뜨는데 콘솔이 재연결 실패로 계속 시끄럽다.
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 발행되지 않은 주소. 사장님이 오타를 냈을 때 흰 화면 대신 이유를 보여준다.
|
# 발행되지 않은 주소. 사장님이 오타를 냈을 때 흰 화면 대신 이유를 보여준다.
|
||||||
|
|||||||
@ -18,6 +18,12 @@ export default defineConfig({
|
|||||||
'@o2o/shared': path.resolve(__dirname, '../shared/src'),
|
'@o2o/shared': path.resolve(__dirname, '../shared/src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
// ★ 발행본과 **같은 오리진**을 쓰므로 `/assets/` 를 서로 뺏는다. 빌더 번들만 다른
|
||||||
|
// 디렉토리로 뺀다 — 안 그러면 nginx 의 `/assets/` 규칙이 발행본 것만 주고
|
||||||
|
// 빌더 JS·CSS 가 404 다(화면은 뜨는데 스타일도 동작도 없다).
|
||||||
|
assetsDir: 'builder-assets',
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
// ★ Vite 6 는 모르는 Host 헤더를 403 "Blocked request" 로 막는다(DNS rebinding 방어).
|
// ★ Vite 6 는 모르는 Host 헤더를 403 "Blocked request" 로 막는다(DNS rebinding 방어).
|
||||||
// 운영은 앞단 nginx 가 Host 를 그대로 넘기므로 발행 호스트를 여기 넣어야 화면이 뜬다.
|
// 운영은 앞단 nginx 가 Host 를 그대로 넘기므로 발행 호스트를 여기 넣어야 화면이 뜬다.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user