웰컴스 이미지 변환 개발 기록

2026.05.30

# wweb SaaS 쇼핑몰 빌더 — 서버 관리 가이드


> 마지막 업데이트: 2026-05-30 (관리자 /admin 분리, 대시보드 프로필, max_shops 제한, 백업 자동화, 커스텀 도메인 SSL, GitLab 푸시)


---


# # 1. 시스템 아키텍처


## # 1.1 전체 구성도


```mermaid

graph TB

    Client[방문자 / 관리자]

    DNS[DNS<br/>*.wweb.wellcoms.co.kr<br/>→ 125.242.108.20]

    Caddy[Caddy<br/>리버스프록시 + TLS<br/>php_fastcgi + 정적 서빙]


    subgraph Wweb["wweb 플랫폼"]

        API[FastAPI<br/>wweb_api:8000<br/>110MB]

        PHP[PHP-FPM 8.3<br/>wweb-php:9000<br/>62MB]

        DB[(MariaDB 11<br/>wweb_db:3306<br/>432MB)]

        S3[(MinIO S3<br/>s3.wellcoms.co.kr<br/>177MB)]

        Static[정적 HTML 캐시<br/>/srv/docker/wweb/static/]

    end


    subgraph Tenants["테넌트 (디렉토리 기반)"]

        T1["mytest<br/>/var/www/tenants/mytest/"]

        T2["demo1<br/>/var/www/tenants/demo1/"]

        T3["wweb_main<br/>/var/www/tenants/wweb_main/"]

    end


    Client -->|HTTPS| DNS

    DNS -->|443| Caddy


    Caddy -->|/api/*, /builder/*, /dashboard| API

    Caddy -->|정적 index.html 있음| Static

    Caddy -->|PHP 요청<br/>php_fastcgi| PHP


    PHP --> T1

    PHP --> T2

    PHP --> T3


    T1 -->|wp-config.php<br/>DB 연결| DB

    T2 -->|wp-config.php<br/>DB 연결| DB

    T3 -->|wp-config.php<br/>DB 연결| DB


    API -->|이미지 업로드| S3

    API -->|테넌트/인증| DB

    API -->|컨테이너/디렉토리 관리| DockerSock[Docker Socket]


    style Caddy fill:#2ecc71,color:#fff

    style API fill:#3498db,color:#fff

    style PHP fill:#e67e22,color:#fff

    style DB fill:#e74c3c,color:#fff

    style S3 fill:#9b59b6,color:#fff

    style Static fill:#1abc9c,color:#fff

```


## # 1.2 핵심 아키텍처: 단일 PHP-FPM 컨테이너 멀티테넌시


**이전 (Apache mod_php):** 테넌트 1개 = Docker 컨테이너 1개 (각각 Apache + PHP 독립 로드)

**현재 (PHP-FPM):** 모든 테넌트가 단일 PHP-FPM 컨테이너를 공유


```

Caddy (호스트명 라우팅)

  ├── mytest.wweb.wellcoms.co.kr → php_fastcgi wweb-php:9000

  │     └── SCRIPT_FILENAME: /var/www/tenants/mytest/index.php

  ├── demo1.wweb.wellcoms.co.kr → php_fastcgi wweb-php:9000

  │     └── SCRIPT_FILENAME: /var/www/tenants/demo1/index.php

  └── wweb.wellcoms.co.kr → php_fastcgi wweb-php:9000

        └── SCRIPT_FILENAME: /var/www/tenants/wweb_main/index.php

```


**핵심 규칙:**

- `index.php`는 **실제 파일**이어야 함 (심볼릭 링크 금지 — Caddy `php_fastcgi`가 resolve 시 core 경로로 전달하여 ABSPATH 불일치 발생)

- 나머지 WP 파일(wp-settings.php 등)은 심볼릭 링크로 core 공유

- WP 코어는 `/var/www/core/`에 마운트 (읽기 전용)

- 플러그인/테마는 `/var/www/shared/`에 마운트 (읽기 전용)


## # 1.3 요청 흐름


```mermaid

sequenceDiagram

    participant V as 방문자

    participant C as Caddy

    participant S as 정적 HTML

    participant FPM as PHP-FPM

    participant DB as MariaDB

    participant S3 as MinIO S3


    Note over V,S3: 홈페이지 접속 (정적 캐싱)


    V->>C: GET https://mytest.wweb.wellcoms.co.kr/

    C->>S: index.html 존재 확인

    S-->>C: index.html 반환

    C-->>V: 정적 HTML 응답 (PHP 0, 메모리 0)


    Note over V,S3: 상품 상세 페이지 (PHP-FPM 동적)


    V->>C: GET /m_product_info/?vid=5

    C->>S: index.html 없음

    C->>FPM: php_fastcgi (SCRIPT_FILENAME=/var/www/tenants/mytest/index.php)

    FPM->>DB: wp-config.php → wweb_mytest 스키마

    FPM-->>C: PHP 8.3 렌더링 결과

    C-->>V: 동적 페이지 응답


    Note over V,S3: 이미지 로드


    V->>S3: GET https://s3.wellcoms.co.kr/wweb/mytest/uploads/aaa.png

    S3-->>V: 이미지 직접 서빙 (Caddy/PHP 거치지 않음)

```


## # 1.4 테넌트 생성 흐름


```mermaid

sequenceDiagram

    participant U as 사용자

    participant API as FastAPI

    participant DB as MariaDB

    participant SH as create-tenant.sh

    participant Caddy as Caddy


    U->>API: POST /api/tenants {slug, plan_id}

    API->>DB: 중복 slug + 1계정1쇼핑몰 검증

    API->>DB: tenants 행 생성

    API->>DB: wweb_{slug} 스키마 생성 + WP 초기화

    API->>SH: create-tenant.sh {slug}

    Note over SH: 1. /var/www/tenants/{slug}/ 디렉토리 생성<br/>2. index.php 실제 파일 생성 (ABSPATH 명시)<br/>3. wp-config.php 생성 (DB 접속 정보)<br/>4. 나머지 WP 파일 → /var/www/core/ 심볼릭 링크

    API->>Caddy: Caddyfile 테넌트 블록 추가 (php_fastcgi)

    API->>Caddy: caddy reload

    API-->>U: 생성 완료

```


> **Apache 방식과의 차이:** `docker run`으로 컨테이너를 생성하지 않음. 디렉토리 + wp-config.php + Caddy 라우트만 추가하면 즉시 서비스 가능.


## # 1.5 이미지 업로드 흐름


```mermaid

sequenceDiagram

    participant U as 관리자

    participant API as FastAPI

    participant S3 as MinIO S3


    Note over U,S3: S3 이미지 업로드 (빌더/대시보드)


    U->>API: POST /api/wp/{slug}/upload (multipart)

    API->>S3: put_object (wweb/{slug}/uploads/{YYYY/MM}/{timestamp}.{ext})

    S3-->>API: 업로드 완료

    API-->>U: {"url": "https://s3.wellcoms.co.kr/wweb/{slug}/uploads/..."}

```


---


# # 2. 하드웨어 스펙 및 리소스 현황


## # 2.1 서버 사양


| 항목 | 사양 |

|------|------|

| CPU | Intel Xeon E3-1231 v3 (4코어 / 8스레드, 3.40GHz) |

| RAM | 31GB DDR3 |

| 디스크 | NVMe 232GB (LVM) |

| 네트워크 | 1Gbps (enp0s25) |

| OS | Ubuntu Linux |

| Swap | 8GB (5.7GB 사용 중) |


## # 2.2 현재 리소스 사용량 (2026-05-30 실측)


| 리소스 | 사용량 | 한계 | 여유 |

|--------|--------|------|------|

| RAM | 16GB / 31GB | - | 14GB 가용 |

| 디스크 | 190GB / 232GB | 87% | 30GB 남음 |

| 실행 컨테이너 | 75개 | - | - |

| Swap | 5.7GB / 8GB | 71% | 2.3GB 남음 |


## # 2.3 wweb 전용 리소스 사용량


| 컨테이너 | 메모리 | 역할 |

|----------|--------|------|

| `wweb-php` | **62MB** | PHP-FPM 8.3 (모든 테넌트 공유) |

| `wweb_api` | 110MB | FastAPI (인증, 프록시, S3 업로드) |

| `6ebc5dcd73aa_wweb_db` | 432MB | MariaDB (전체 테넌트 DB) |

| `minio` | 177MB | S3 이미지 스토리지 |

| `minio-console` | 50MB | MinIO 관리 콘솔 |

| `caddy` | 25MB | 리버스프록시 + TLS + php_fastcgi |

| **wweb 전체 합계** | **~856MB** | |


## # 2.4 PHP-FPM vs Apache 비교 (실측)


```mermaid

graph LR

    subgraph "이전: Apache mod_php"

        A1["tenant_a_wp<br/>48MB"]

        A2["tenant_b_wp<br/>48MB"]

        A3["wweb_wp<br/>146MB"]

        A4["...N개×75MB"]

    end


    subgraph "현재: PHP-FPM"

        B1["wweb-php<br/>62MB<br/>(모든 테넌트 공유)"]

    end


    style A1 fill:#e74c3c,color:#fff

    style A2 fill:#e74c3c,color:#fff

    style A3 fill:#e74c3c,color:#fff

    style A4 fill:#e74c3c,color:#fff

    style B1 fill:#2ecc71,color:#fff

```


| 시나리오 | Apache (기존) | PHP-FPM (현재) | 절약 |

|----------|--------------|----------------|------|

| 3 테넌트 | 48+48+146 = **242MB** | **62MB** | 180MB (74%) |

| 10 테넌트 | 10×75 = **750MB** | **~100MB** | 650MB (87%) |

| 100 테넌트 | 100×75 = **7,500MB** (불가) | **~300MB** idle / **~500MB** peak | 7,000MB (93%) |


## # 2.5 테넌트당 리소스 사용량


| 리소스 | 테넌트 1개당 | 비고 |

|--------|-------------|------|

| RAM (FPM 공유) | **~5MB** | idle 시. active 시 FPM 워커 1개당 ~15MB |

| 디스크 (WP 파일) | **~5MB** | 코어/플러그인/테마는 RO 공유, wp-config + index.php만 개별 |

| DB 스키마 | **5~130MB** | 상품 수에 따라 차이 큼 |

| S3 이미지 | **무제한** | 로컬 디스크 사용 안 함 |

| Caddy 설정 | **~500바이트** | php_fastcgi 블록 1개 추가 |


---


# # 3. 용량 계획 (Capacity Planning)


## # 3.1 병목별 최대 테넌트 수


| 병목 | 한계 테넌트 수 | 계산 근거 |

|------|---------------|-----------|

| **RAM** | **~700명** | 가용 14GB - 다른 서비스 10GB = 4GB / 5MB ≈ 800 (여유 포함 700) |

| **CPU** | **~400명** | 8코어, PHP 50ms/요청, FPM max_children=50 → 동시 50요청 |

| **디스크** | **~1,000명+** | S3 전환으로 이미지 디스크 제로. DB 130MB×1000 = 130GB (주의) |

| **네트워크** | **~500명+** | 1Gbps, HTML 100KB → 동시 250명 로드. 정적 캐싱 시 더 많음 |


## # 3.2 확장 로드맵


```mermaid

timeline

    title wweb 확장 로드맵

    section Phase 1 (완료)

        PHP-FPM 전환 : 단일 컨테이너 멀티테넌시

        : S3 + 정적 캐싱 + 빌더 CSS 수정

        : ~700명 수용

    section Phase 2 (서버 분리)

        wweb 전용 서버 : DB/S3은 기존 유지

        : ~1,000명 수용

    section Phase 3 (고급)

        16코어 CPU : DB 튜닝

        : ~1,500명 수용

    section Phase 4 (클라우드)

        Kubernetes : 오토스케일링

        : 무한 확장

```


## # 3.3 동시 접속자 기준


| 총 테넌트 | 평균 동시 접속 | 피크 동시 접속 | FPM 워커 | CPU 부하 |

|-----------|---------------|---------------|----------|----------|

| 10명 | 5명 | 50명 | 10 | ~10% |

| 100명 | 50명 | 500명 | 50 (max) | ~40% |

| 300명 | 150명 | 1,500명 | 50+큐 | ~70% |

| 700명 | 350명 | 3,500명 | 큐 대기 | CPU 포화 |


> FPM `pm.max_children=50` 설정. 300명 이상 시 100~200으로 증설 필요.


---


# # 4. 컨테이너 관리


## # 4.1 컨테이너 구성


```mermaid

graph TB

    subgraph NetworkWeb["web 네트워크 (Caddy 통신)"]

        Caddy

        API[wweb_api]

        MinIO

        MinioConsole[minio-console]

    end


    subgraph NetworkInternal["wweb_wweb-internal (DB + PHP 통신)"]

        API

        PHP[wweb-php]

        DB[(MariaDB)]

    end


    subgraph Volumes["PHP-FPM 볼륨 마운트"]

        Core["/srv/docker/wweb/wp-core<br/>→ /var/www/core (ro)"]

        Plugins["/srv/docker/wweb/master/plugins<br/>→ /var/www/shared/plugins (ro)"]

        Themes["/srv/docker/wweb/master/themes<br/>→ /var/www/shared/themes (ro)"]

        Tenants["/srv/docker/wweb/tenants<br/>→ /var/www/tenants (rw)"]

    end


    PHP ---|ro| Core

    PHP ---|ro| Plugins

    PHP ---|ro| Themes

    PHP ---|rw| Tenants

    API ---|rw| DockerSock["/var/run/docker.sock"]

    API ---|rw| CaddyFile["Caddyfile"]

    API ---|rw| StaticDir["static/"]

    API ---|rw| TenantsDir["tenants/"]

```


## # 4.2 PHP-FPM 컨테이너 설정


```bash

# 현재 실행 중인 컨테이너 재생성

docker stop wweb-php && docker rm wweb-php && \

docker run -d \

  --name wweb-php \

  --restart unless-stopped \

  --network wweb_wweb-internal \

  -v /srv/docker/wweb/wp-core:/var/www/core:ro \

  -v /srv/docker/wweb/tenants:/var/www/tenants \

  -v /srv/docker/wweb/master/plugins:/var/www/shared/plugins:ro \

  -v /srv/docker/wweb/master/themes:/var/www/shared/themes:ro \

  -v /srv/docker/wweb/static:/srv/docker/wweb/static \

  wweb_php_img

```


> **주의:** 이미지 리빌드 시에만 재생성 필요. php-fpm-pool.conf, php-custom.ini 변경 시 `docker build` 필요 (볼륨 마운트가 아닌 COPY 베이킹).


## # 4.3 API 컨테이너 재생성


```bash

# 코드 변경 후 반드시 rebuild 필요 (COPY 방식)

docker build -t wweb_api_img /srv/docker/wweb/backend/ && \

docker stop wweb_api && docker rm wweb_api && \

docker run -d \

  --name wweb_api \

  --restart unless-stopped \

  --network wweb_wweb-internal \

  -e SECRET_KEY=7f2fa3a7297e60375f2cbbdf2f07e410c3862afa3dc30aff98b651502cd9de39 \

  -e TENANTS_BASE_PATH=/srv/docker/wweb/tenants \

  -e DB_HOST=6ebc5dcd73aa_wweb_db \

  -e DB_ROOT_USER=root \

  -e DB_ROOT_PASSWORD=ab9f6db6d8f8b138fd48693d9244337d \

  -e DB_PORT=3306 \

  -v /var/run/docker.sock:/var/run/docker.sock \

  -v /srv/docker/caddy/Caddyfile:/srv/docker/caddy/Caddyfile \

  -v /srv/docker/wweb/static:/srv/docker/wweb/static \

  -v /srv/docker/wweb/tenants:/srv/docker/wweb/tenants \

  wweb_api_img && \

sleep 2 && docker network connect web wweb_api

```


> **주의:** `SECRET_KEY` 변경 시 기존 JWT 토큰이 모두 무효화됩니다. 반드시 고정값 사용.


## # 4.4 PHP-FPM 이미지 리빌드


```bash

# php-fpm-pool.conf, php-custom.ini, Dockerfile 변경 시

docker build -t wweb_php_img -f /srv/docker/wweb/docker/Dockerfile.php-fpm /srv/docker/wweb/docker/ && \

docker stop wweb-php && docker rm wweb-php && \

# 4.2의 docker run 명령으로 재생성

```


---


# # 5. 서비스별 설정


## # 5.1 PHP-FPM 풀 설정


```ini

# /srv/docker/wweb/docker/php-fpm-pool.conf

[www]

user = www-data

group = www-data

listen = 9000

pm = dynamic

pm.max_children = 50

pm.start_servers = 5

pm.min_spare_servers = 3

pm.max_spare_servers = 10

pm.max_requests = 500

request_terminate_timeout = 30

php_admin_value[memory_limit] = 512M

php_admin_value[upload_max_filesize] = 128M

php_admin_value[post_max_size] = 128M

php_admin_value[max_execution_time] = 60

```


| 설정 | 값 | 비고 |

|------|-----|------|

| `pm.max_children` | 50 | 동시 처리 최대 50개 PHP 요청 |

| `memory_limit` | 512M | 8MB+ 콘텐츠 저장 시 wp_update_post() 메모리 사용 대응 |

| `pm.max_requests` | 500 | 메모리 누수 방지 (500 요청 후 워커 재생성) |

| `request_terminate_timeout` | 30 | 30초 이상 요청 강제 종료 |


## # 5.2 PHP 설정


```ini

# /srv/docker/wweb/docker/php-custom.ini

upload_max_filesize = 128M

post_max_size = 128M

max_execution_time = 60

memory_limit = 512M

```


## # 5.3 Caddy php_fastcgi 설정


```caddyfile

# 테넌트별 Caddy 블록 예시

mytest.wweb.wellcoms.co.kr {

    encode zstd gzip


    # 정적 캐시 (홈페이지만)

    @static file /index.html

    handle @static {

        root * /srv/docker/wweb/static/mytest

        rewrite /index.html

        file_server

    }


    # 정적 파일 (CSS/JS/이미지) — Caddy 직접 서빙

    @staticFiles path *.css *.js *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ttf *.ico

    handle @staticFiles {

        root * /var/www/tenants/mytest

        file_server

    }


    # 나머지 — PHP-FPM 처리

    handle {

        root * /var/www/tenants/mytest

        php_fastcgi wweb-php:9000

    }

}

```


## # 5.4 MinIO S3 설정


| 항목 | 값 |

|------|-----|

| 컨테이너 | `minio` |

| 엔드포인트 (내부) | `http://minio:9000` |

| 공개 URL | `https://s3.wellcoms.co.kr` |

| 콘솔 | `https://minio.wellcoms.co.kr` |

| 버킷 | `wweb` (공개 읽기) |

| 자격증명 | `lordly` / `rlawoehf@123A` |

| 이미지 경로 | `wweb/{slug}/uploads/{YYYY/MM}/{timestamp}.{ext}` |


## # 5.5 정적 HTML 캐싱


| 항목 | 설명 |

|------|------|

| 캐시 위치 | `/srv/docker/wweb/static/{slug}/index.html` |

| 배포 API | `POST /api/wp/{slug}/pages/{page_id}/publish-static` |

| 적용 범위 | **홈페이지만** (상품상세/장바구니/결제는 PHP-FPM 동적) |

| 캐시 무효화 | 빌더에서 "배포" 시 자동 재생성 |


---


# # 6. 운영 정보


## # 6.1 DB 구조


```mermaid

erDiagram

    SAAS["wweb_wp (SaaS 메타 DB)"] {

        int users PK "사용자 (id, email, name, phone, role, password_hash)"

        int plans PK "요금제 (id, name, price, max_pages, max_products, max_shops, custom_domain)"

        int tenants PK "테넌트 (id, user_id, slug, plan_id, status, domain, subscription_started_at, subscription_expires_at)"

    }


    TENANT_DB["tenant_{slug}<br/>(테넌트별 DB)"] {

        int wp_posts PK "WordPress 페이지/글"

        int wp_postmeta PK "페이지 메타 (빌더 CSS: _wweb_css)"

        int mb_commerce_product PK "상품"

        text mb_options "PG 결제 설정 등"

    }


    S3_STORAGE["MinIO S3<br/>(이미지 스토리지)"] {

        text wweb_slug_uploads "wweb/{slug}/uploads/*"

    }


    users ||--o{ tenants : "1:N (max_shops 제한)"

    plans ||--o{ tenants : "요금제 할당"

    tenants ||--o{ TENANT_DB : "1테넌트 1DB"

    TENANT_DB }o--|| S3_STORAGE : "이미지 URL 참조"

```


## # 6.2 DB 크기 (현재)


| 스키마 | 크기 | 비고 |

|--------|------|------|

| `wweb_mytest` | 131.6MB | 테스트 상품/페이지 다수 (8.4MB 콘텐츠 포함) |

| `wweb_demo1` | 4.6MB | 기본 상태 |

| `wweb_wp` | 5.6MB | SaaS 메인 WordPress |

| `wweb_saas` | 0.2MB | 사용자/테넌트/플랜 |


## # 6.3 현재 테넌트 목록


| slug | user_id | 상태 | 요금제 | PHP-FPM | 정적 캐싱 | S3 |

|------|---------|------|--------|:---:|:---:|:---:|

| mytest | 3 | active | 엔터프라이즈 (plan=3) | ✅ | ✅ | ✅ |

| demo1 | - | active | - | ✅ | ✅ | - |

| e2etest | - | active | - | ✅ | - | - |

| wweb_main | - | active | - | ✅ | - | - |


## # 6.4 SaaS 사용자


| 이메일 | user_id | 역할 | 비고 |

|--------|---------|------|------|

| lordlykim@gmail.com | 3 | admin | 관리자, PW: rlawoehf1!A |

| test@test.com | 1 | user | 테스트 계정 |

| test@wweb.kr | 2 | user | 테스트 계정 |

| s3test@test.com | 4 | user | S3 테스트 계정 |


## # 6.5 요금제 현황


| id | 요금제명 | 월 요금 | max_shops | max_pages | max_products | custom_domain |

|----|----------|---------|-----------|-----------|--------------|---------------|

| 1 | 무료 | 0원 | 1 | 무제한 | 무제한 | 불가 |

| 2 | 프로 | 29,000원 | 1 | 무제한 | 무제한 | 가능 |

| 3 | 엔터프라이즈 | 99,000원 | 1 | 무제한 | 무제한 | 가능 |


> **주의:** 전 요금제 max_shops=1 통일 (2026-05-30 기준)


## # 6.6 빌더 CSS 저장 메커니즘


빌더는 GrapesJS를 사용하며, HTML과 CSS를 분리 저장합니다.


**저장 흐름:**

1. `collectAllComponentStyles(editor)` — 모든 컴포넌트를 순회하며 style 속성을 CSS 규칙으로 변환

2. `editor.getCss({ keepUnusedStyles: true })` — GrapesJS CSS 매니저에서 등록된 규칙 가져오기

3. 두 소스를 병합하고 ID별 중복 제거

4. `post_content` (HTML)과 `_wweb_css` (CSS) 분리 저장


**WP 렌더링:** `wweb-head-css` 플러그인이 `_wweb_css` 메타를 `<style>` 태그로 `<head>`에 출력.


## # 6.7 캐시 버스터


- 현재: `?v=20260529z`

- 변경 시 빌더 HTML의 `app.js` 쿼리 업데이트 필요


---


# # 7. 관리자 /admin 분리 아키텍처


## # 7.1 개요


관리자 대시보드는 일반 대시보드(`/dashboard`)와 완전히 분리된 별도 페이지(`/admin`).


| 항목 | 일반 대시보드 | 관리자 대시보드 |

|------|-------------|---------------|

| URL | `/dashboard` | `/admin` |

| 토큰 | `wweb_token` | `wweb_admin_token` |

| JS 파일 | `dashboard/app.js` | `admin/app.js` |

| CSS 파일 | `dashboard/style.css` | `admin/style.css` |

| HTML | `dashboard/index.html` | `admin/index.html` |

| 접근 | 모든 인증 사용자 | role=admin만 |

| 기능 | 내정보, 쇼핑몰관리, 주문, 통계 | 개요/사용자/테넌트/요금제/리소스/시스템 |


## # 7.2 관리자 6탭 구성


| 탭 | API | 기능 |

|----|-----|------|

| 개요 | `/api/admin/stats` | 전체 사용자/테넌트/매출 통계 |

| 사용자 | `/api/admin/users`, `/api/admin/users/{id}` | 목록/상세/역할변경/삭제 |

| 테넌트 | `/api/admin/tenants` | 목록/요금제변경/정지/활성화/삭제 |

| 요금제 | `/api/admin/plans` | 목록/수정 (max_shobs 등) |

| 리소스 | `/api/admin/resources` | CPU/RAM/디스크 사용량 |

| 시스템 | `/api/admin/containers` | Docker 컨테이너 목록 |


## # 7.3 Caddyfile /admin 라우팅


WordPress가 `/admin` → `/wp-admin/`으로 리다이렉트하므로, Caddy에서 API로 우선 라우팅:


```caddyfile

# wweb.wellcoms.co.kr 서버 블록 내

handle /admin {

    reverse_proxy wweb_api:8000

}

handle /admin/* {

    reverse_proxy wweb_api:8000

}

# 일반 WordPress 라우팅은 그 아래에 위치

```


## # 7.4 관리자 로딩 스피너


모든 탭 전환 시 회전 애니메이션 + "불러오는 중..." 표시:


```css

/* admin/style.css */

@keyframes aspin { to { transform: rotate(360deg); } }

.loading-spinner { animation: aspin 1s linear infinite; }

```


```javascript

// admin/app.js — 모든 탭 로드 함수에서 호출

function showLoading() {

    document.getElementById('tab-content').innerHTML =

        '<div style="text-align:center;padding:60px"><div class="loading-spinner" ' +

        'style="width:40px;height:40px;border:4px solid #eee;border-top:4px solid #3498db;' +

        'border-radius:50%;margin:0 auto 16px"></div>불러오는 중...</div>';

}

```


---


# # 8. 대시보드 프로필 (내 정보)


## # 8.1 프로필 탭 구성


대시보드 "내 정보" 섹션에서 사용자 정보 + 구독 현황을 표시.


**표시 항목:**

- 기본 정보: 이름, 이메일, 전화번호, 가입일

- 구독 현황: 요금제명, 가격, 도메인, 구독시작일, 만료일

- 요금제 제한: 상품 수, 페이지 수, 커스텀 도메인 가능 여부


## # 8.2 API 확장


`/api/auth/me` 응답에 tenants 조인으로 plan 정보 포함:


```python

# auth.py — get_user() 확장

def get_user(user_id: int):

    conn = get_saas_conn()

    cursor = conn.cursor()

    cursor.execute("""

        SELECT u.*, t.slug AS tenant_slug, t.domain,

               t.subscription_started_at, t.subscription_expires_at,

               p.name AS plan_name, p.price AS plan_price,

               p.max_pages, p.max_products, p.custom_domain

        FROM users u

        LEFT JOIN tenants t ON t.user_id = u.id AND t.status = 'active'

        LEFT JOIN plans p ON p.id = t.plan_id

        WHERE u.id = %s

    """, (user_id,))

```


## # 8.3 DB 컬럼 추가


```sql

-- tenants 테이블에 구독 관련 컬럼 추가됨

ALTER TABLE tenants ADD COLUMN subscription_started_at DATETIME;

ALTER TABLE tenants ADD COLUMN subscription_expires_at DATETIME;

```


---


# # 9. 쇼핑몰 생성 제한 (max_shops)


## # 9.1 개요


사용자가 생성할 수 있는 쇼핑몰 수를 `plans.max_shops` 값으로 제한. 하드코딩이 아닌 DB 기반.


## # 9.2 API 응답


`/api/my-shops` 응답에 `max_shops` 필드 포함:


```json

{

    "shops": [

        {"id": 1, "slug": "mytest", "plan_name": "엔터프라이즈", ...}

    ],

    "max_shops": 1

}

```


```python

# auth.py — get_user_tenants()에 max_shops 조인

def get_user_tenants(user_id: int):

    cursor.execute("""

        SELECT t.*, p.name AS plan_name, p.max_shops

        FROM tenants t

        LEFT JOIN plans p ON p.id = t.plan_id

        WHERE t.user_id = %s AND t.status != 'deleted'

    """, (user_id,))

```


## # 9.3 프론트엔드 제한 로직


```javascript

// dashboard/app.js — loadShops()

if (shops.length >= maxShops) {

    section.style.opacity = '0.5';

    section.style.pointerEvents = 'none';

    // "생성 한도 도달" 안내 메시지 표시

}

```


## # 9.4 현재 설정


전 요금제 `max_shops=1` 통일. 관리자가 `/admin` 요금제 탭에서 변경 가능.


---


# # 10. 백업 자동화


## # 10.1 백업 스크립트


`/srv/docker/wweb/docker/backup.sh` — 매일 03:00 자동 실행


**백업 대상:**

1. MariaDB 전체 덤프 (`mysqldump --all-databases`)

2. 정적 HTML 캐시 (`/srv/docker/wweb/static/`)

3. 테넌트 설정 (`/srv/docker/wweb/tenants/`)


**저장 위치:** `/srv/docker/wweb/backups/`


## # 10.2 크론 설정


```bash

# crontab -l

0 3 * * * /srv/docker/wweb/docker/backup.sh >> /srv/docker/wweb/backups/backup.log 2>&1

```


## # 10.3 백업 파일명 규칙


```

backups/

├── wweb_db_20260530.sql.gz      # DB 덤프

├── wweb_static_20260530.tar.gz   # 정적 HTML

└── backup.log                    # 실행 로그

```


---


# # 11. 장애 대응


## # 11.1 장애 시나리오별 대응


```mermaid

graph TD

    Problem[장애 발생] --> Check{어떤 서비스?}


    Check -->|사이트 안 열림| SiteCheck

    Check -->|이미지 안 나옴| ImageCheck

    Check -->|빌더 안 됨| BuilderCheck

    Check -->|로그인 안 됨| AuthCheck

    Check -->|502 에러| FPMCheck


    SiteCheck["Caddy + PHP-FPM 확인"]

    SiteCheck -->|wweb-php 다운| RestartPHP["docker restart wweb-php"]

    SiteCheck -->|컨테이너 정상| DBCheck["DB 연결 확인"]


    ImageCheck["S3 접근 확인"]

    ImageCheck -->|404| S3Restart["docker restart minio"]

    ImageCheck -->|200| CDNCheck["Caddy s3 라우트 확인"]


    BuilderCheck["API 컨테이너 확인"]

    BuilderCheck -->|다운| APIRestart["docker restart wweb_api"]


    AuthCheck["JWT 토큰 확인"]

    AuthCheck -->|키 변경됨| KeyFix["동일 SECRET_KEY로 재생성"]


    FPMCheck["PHP-FPM 로그 확인"]

    FPMCheck --> OOM["memory_limit 초과?<br/>docker logs wweb-php | grep Fatal"]

    OOM --> IncreaseMem["pool.conf memory_limit 증설<br/>이미지 리빌드"]


    style Problem fill:#e74c3c,color:#fff

    style RestartPHP fill:#2ecc71,color:#fff

    style APIRestart fill:#2ecc71,color:#fff

    style KeyFix fill:#2ecc71,color:#fff

    style IncreaseMem fill:#2ecc71,color:#fff

```


## # 11.2 주요 명령어


```bash

# PHP-FPM 재시작 (모든 테넌트에 영향)

docker restart wweb-php


# API 컨테이너 재시작

docker restart wweb_api


# Caddy 설정 리로드

docker exec caddy caddy reload --config /etc/caddy/Caddyfile


# DB 연결 확인

docker exec 6ebc5dcd73aa_wweb_db mariadb -u root -p'ab9f6db6d8f8b138fd48693d9244337d' -e "SELECT 1"


# PHP-FPM 에러 로그 확인

docker logs wweb-php --tail 50 2>&1 | grep -i "fatal\|error\|warning"


# MinIO 상태 확인

docker exec minio mc admin info local


# 정적 캐시 수동 삭제 (다음 요청 시 PHP-FPM fallback)

rm -f /srv/docker/wweb/static/{slug}/index.html


# 빌더 API 플러그인 업데이트 (공유 볼륨이므로 즉시 반영)

# 파일 수정 후 별도 배포 불필요 (마운트된 디렉토리에 있음)

# 단, wweb-builder-api.php는 테넌트 디렉토리에 심볼릭 링크로 연결됨


# 컨테이너 로그 실시간 확인

docker logs -f wweb-php

docker logs -f wweb_api


# FPM 프로세스 상태 확인

docker exec wweb-php ps aux | grep php-fpm


# PHP 설정 확인

docker exec wweb-php php -i | grep memory_limit

```


## # 11.3 알려진 이슈


| 이슈 | 원인 | 해결 |

|------|------|------|

| 빌더 저장 시 500 에러 | 8MB+ 콘텐츠 + wp_update_post() → 256MB OOM | memory_limit 512M로 증설 완료 |

| theme-function.php Warning | `$container_padding` null 접근 | 기본값 삼항연산자로 수정 완료 |

| 빨간 글자 → 검은 글자 | GrapesJS `getCss()`가 모든 컴포넌트 스타일 미반환 | `collectAllComponentStyles()` 추가로 수정 완료 |

| index.php 심볼릭 링크 불가 | Caddy `php_fastcgi`가 symlink resolve 시 ABSPATH 불일치 | 실제 파일로 생성 + ABSPATH 명시 |

| `esc is not defined` | dashboard/app.js IIFE에 `esc()` 헬퍼 누락 | IIFE 내부에 `esc()` 함수 추가 완료 |

| `loadOrdersDropdown` TypeError | `shops.forEach` → API 응답이 `{shops:[...]}` 형태 | `d.shops.forEach`로 수정 완료 |


---


# # 12. 정기 유지보수


## # 12.1 주간 점검


| 항목 | 명령어 | 기준 |

|------|--------|------|

| 디스크 사용률 | `df -h /` | 90% 미만 |

| 메모리 사용률 | `free -h` | Swap 80% 미만 |

| 컨테이너 상태 | `docker ps -a` | 모든 컨테이너 Up |

| Docker 쓰레기 | `docker system df` | Reclaimable 10GB 미만 |

| PHP-FPM 에러 | `docker logs wweb-php --since 7d \| grep -c Fatal` | 0건 |

| DB 크기 | `docker exec ... mariadb -e "SELECT table_schema..."` | 급증 확인 |


## # 12.2 정리 명령어


```bash

# Dangling 이미지 정리

docker image prune -f


# 빌드 캐시 정리

docker builder prune -f


# 미사용 볼륨 정리 (주의: 백업 후 실행)

docker volume prune -f


# 전체 정리 (한 번에)

docker system prune -f

```


## # 12.3 백업


| 대상 | 방법 | 주기 |

|------|------|------|

| MariaDB | `mysqldump` → gzip | 매일 03:00 (자동) |

| 정적 HTML | tar.gz | 매일 03:00 (자동) |

| 테넌트 설정 | tar.gz | 매일 03:00 (자동) |

| MinIO S3 | NAS 스토리지에 복제 | 실시간 (NAS 마운트) |

| Caddy 설정 | `/srv/docker/caddy/Caddyfile` | 변경 시 |


```bash

# DB 백업 스크립트

docker exec 6ebc5dcd73aa_wweb_db mysqldump -u root -p'ab9f6db6d8f8b138fd48693d9244337d' \

  --all-databases --single-transaction | gzip > /srv/docker/backup/wweb_db_$(date +%Y%m%d).sql.gz

```


---


# # 13. 서버 분리 아키텍처 (wweb 전용 서버)


## # 13.1 분리 개요


wweb 서비스를 전용 서버로 분리. DB와 S3는 현재 서버에 유지.


```mermaid

graph TB

    subgraph CurrentServer["현재 서버 (192.168.0.2)<br/>Xeon E3-1231 v3 / 31GB / 232GB NVMe"]

        DB[(MariaDB<br/>wweb DB 포함<br/>432MB)]

        S3[(MinIO S3<br/>s3.wellcoms.co.kr<br/>177MB)]

        Mail[Mailcow 메일]

        GitLab[GitLab]

        Others[n8n, Immich, Seafile 등]

        CaddyOld[Caddy<br/>기존 서비스 프록시]

    end


    subgraph NewServer["신규 wweb 전용 서버<br/>12세대 i7 / 32~64GB DDR4"]

        CaddyNew[Caddy<br/>wweb 전용 프록시]

        API[FastAPI<br/>wweb_api]

        PHP[PHP-FPM<br/>모든 테넌트 공유]

        Static[정적 HTML 캐시]

    end


    Internet((인터넷 1Gbps))


    Internet --> CaddyOld

    Internet --> CaddyNew


    CaddyNew --> API

    CaddyNew --> PHP

    CaddyNew --> Static


    API -.->|LAN 1Gbps<br/>DB 쿼리| DB

    PHP -.->|LAN 1Gbps<br/>DB 쿼리| DB

    API -.->|S3 업로드| S3

    Internet -.->|이미지 서빙| S3


    style DB fill:#e74c3c,color:#fff

    style S3 fill:#9b59b6,color:#fff

    style CaddyNew fill:#2ecc71,color:#fff

    style API fill:#3498db,color:#fff

    style PHP fill:#e67e22,color:#fff

```


## # 13.2 DB 원격 접속 설정


**현재 서버 (DB) — 1회성 설정:**


```bash

# 1. MariaDB 원격 접속 허용

# bind-address = 0.0.0.0 으로 변경 후 restart


# 2. wweb용 DB 사용자에 원격 접근 권한 부여

docker exec 6ebc5dcd73aa_wweb_db mariadb -u root -p'PASSWORD' -e "

  CREATE USER IF NOT EXISTS 'wweb_remote'@'%' IDENTIFIED BY 'STRONG_PASSWORD';

  GRANT ALL PRIVILEGES ON wweb_%.* TO 'wweb_remote'@'%';

  FLUSH PRIVILEGES;

"


# 3. 방화벽: 3306 포트 신규 서버 IP만 허용

ufw allow from 192.168.0.{신규서버IP} to any port 3306

```


**신규 서버 — wp-config.php:**


```php

// 변경: DB_HOST = 192.168.0.2 (현재 서버 LAN IP)

define('DB_HOST', '192.168.0.2:3306');

```


## # 13.3 DB + S3 같은 서버 — 병목 분석


| 리소스 | MariaDB | MinIO (S3) | 간섭 |

|--------|---------|------------|:----:|

| CPU | 쿼리 처리 (정수 연산) | 거의 안 씀 | 없음 |

| RAM | Buffer Pool (데이터 캐시) | 객체 메타데이터만 | 없음 |

| 디스크 | 랜덤 I/O (작은 읽기/쓰기) | 순차 I/O (큰 파일) | 없음 |

| 네트워크 | LAN (작은 패킷, 쿼리) | 인터넷 (큰 패킷, 이미지) | 없음 |


> **결론: 700명까지 현재 서버에 DB + S3 같이 있어도 병목 없음.**


## # 13.4 DB 성장에 따른 튜닝 필요 시점


| 테넌트 수 | DB RAM | DB CPU | 필요 조치 |

|-----------|--------|--------|-----------|

| ~100명 | ~1.5GB | ~10% | `innodb_buffer_pool_size` 1GB 증가 |

| ~300명 | ~3GB | ~25% | buffer pool 2GB, `max_connections` 300 |

| ~500명 | ~5GB | ~40% | buffer pool 4GB, slow query 분석 |

| ~700명 | ~7GB | ~55% | DB CPU 병목 시작, 슬레이브 DB 고려 |


---


# # 14. 시나리오별 정리


| 구성 | 안전 한계 | 최대 한계 | 비용 |

|------|----------|----------|------|

| 현재 서버 + PHP-FPM | **~300명** | ~400명 | 최소 |

| 신규 i7-12700 + 32GB + PHP-FPM | **~700명** | ~1,000명 | 서버 1대 |

| i7-13700 + 32GB + PHP-FPM | **~1,000명** | ~1,500명 | CPU 업그레이드 |

| i7-13700 + 32GB + PHP-FPM + DB 분리 | **~1,500명** | ~2,000명 | DB 전용 추가 |


```

현재 상태: 단일 서버 + PHP-FPM → 안정 ~300명 수용 가능

신규 서버 분리 시: 안정 ~700명 수용 가능

64GB RAM은 의미 없음 (CPU가 먼저 한계)

추가 인터넷 회선은 700명까지 불필요

```


---


# # 15. 핵심 파일 경로


| 파일 | 경로 | 설명 |

|------|------|------|

| FastAPI 메인 | `/srv/docker/wweb/backend/app/main.py` | API 엔드포인트, S3 업로드, 정적 publish, /admin 라우트 |

| 인증 모듈 | `/srv/docker/wweb/backend/app/auth.py` | JWT, 구독 정보, max_shops 조인 |

| Caddy 관리 | `/srv/docker/wweb/backend/app/caddy.py` | 라우트 추가/삭제 (php_fastcgi) |

| Docker 제어 | `/srv/docker/wweb/backend/app/docker_ctrl.py` | 테넌트 디렉토리 + wp-config 생성 |

| WP 초기화 | `/srv/docker/wweb/backend/app/wp_init.py` | wweb-php 컨테이너에서 wp-cli 실행 |

| 피드 생성 | `/srv/docker/wweb/backend/app/feed.py` | 네이버 EP/쇼핑 XML, 사이트맵 |

| 관리자 HTML | `/srv/docker/wweb/backend/app/admin/index.html` | 관리자 6탭 UI |

| 관리자 JS | `/srv/docker/wweb/backend/app/admin/app.js` | 관리자 전용 로직 (showLoading) |

| 관리자 CSS | `/srv/docker/wweb/backend/app/admin/style.css` | 관리자 전용 스타일 |

| 빌더 JS | `/srv/docker/wweb/backend/app/builder/app.js` | GrapesJS 에디터 (getFullCss 포함) |

| 대시보드 HTML | `/srv/docker/wweb/backend/app/dashboard/index.html` | 대시보드 UI |

| 대시보드 JS | `/srv/docker/wweb/backend/app/dashboard/app.js` | 프로필/max_shops/주문 로직 |

| WP API 플러그인 | `/srv/docker/wweb/master/plugins/wweb-builder-api/` | 상품/페이지/PG/주문 API |

| Head CSS 플러그인 | `/srv/docker/wweb/master/plugins/wweb-head-css/` | _wweb_css → <style> 출력 |

| PHP-FPM Dockerfile | `/srv/docker/wweb/docker/Dockerfile.php-fpm` | PHP 8.3 + 확장 + dpkg 최적화 |

| PHP-FPM Pool 설정 | `/srv/docker/wweb/docker/php-fpm-pool.conf` | FPM 워커 풀 + memory_limit=512M |

| 백업 스크립트 | `/srv/docker/wweb/docker/backup.sh` | DB+정적+테넌트 자동 백업 |

| Caddy 설정 | `/srv/docker/caddy/Caddyfile` | 전체 리버스프록시 + /admin 라우트 |

| WP 코어 | `/srv/docker/wweb/wp-core/` | WordPress 파일 (RO 마운트) |

| 테넌트 디렉토리 | `/srv/docker/wweb/tenants/{slug}/` | wp-config.php + index.php |

| 정적 캐시 | `/srv/docker/wweb/static/{slug}/` | 홈페이지 HTML |

| DB 데이터 | Docker volume (6ebc5dcd73aa_wweb_db) | MariaDB 데이터 |

| API Dockerfile | `/srv/docker/wweb/backend/Dockerfile` | FastAPI 컨테이너 |

| 상품비교 문서 | `/srv/docker/wweb/docs/상품비교.md` | WWeb vs 아임웨비 기능 비교 |

| 프로젝트 README | `/srv/docker/wweb/README.md` | 프로젝트 전체 문서 |

| GitLab 저장소 | `https://gitlab.wellcoms.co.kr/jskim/wweb` | 소스코드 저장소 |


---


# # 16. 작업 이력


| 날짜 | 작업 내용 |

|------|----------|

| 2026-05-28 | PHP-FPM 전환, 빌더 CSS 저장 버그 수정, 주문 API + 대시보드 주문 관리 UI |

| 2026-05-28 | 대시보드 통계 고도화 (7일 차트 + 상품별 TOP5) |

| 2026-05-29 | 관리자 API 14개 엔드포인트, 백업 자동화 (backup.sh + 크론) |

| 2026-05-29 | 커스텀 도메인 SSL 자동화 E2E 검증 |

| 2026-05-30 | 관리자 `/admin` 별도 페이지 분리 (HTML/JS/CSS/main.py 라우트/Caddyfile) |

| 2026-05-30 | 일반 대시보드에서 관리자 섹션/JS 완전 제거 |

| 2026-05-30 | 관리자 로딩 스피너 (showLoading + CSS animation) |

| 2026-05-30 | 대시보드 "내 정보" 프로필 탭 (구독/요금제 포함) |

| 2026-05-30 | DB subscription_started_at/expires_at 컬럼 추가 |

| 2026-05-30 | 쇼핑몰 생성 제한 (max_shops DB 기반, 프론트엔드 비활성화) |

| 2026-05-30 | esc() 누락 수정, loadOrdersDropdown 버그 수정 |

| 2026-05-30 | 전 요금제 max_shops=1 통일 |

| 2026-05-30 | 상품비교.md 작성 (WWeb vs 아임웨비 10카테고리) |

| 2026-05-30 | README.md 작성 + GitLab jskim/wweb 초기 푸시 |


▣ 마크 다운(Markdown) 문서(Mermaid 포함) 지원합니다.
글보기
제목웰컴스 이미지 변환 개발 기록2026-05-30 00:59
카테고리 기술노트
작성자 Level 10

# wweb SaaS 쇼핑몰 빌더 — 서버 관리 가이드


> 마지막 업데이트: 2026-05-30 (관리자 /admin 분리, 대시보드 프로필, max_shops 제한, 백업 자동화, 커스텀 도메인 SSL, GitLab 푸시)


---


## 1. 시스템 아키텍처


### 1.1 전체 구성도


```mermaid

graph TB

    Client[방문자 / 관리자]

    DNS[DNS<br/>*.wweb.wellcoms.co.kr<br/>→ 125.242.108.20]

    Caddy[Caddy<br/>리버스프록시 + TLS<br/>php_fastcgi + 정적 서빙]


    subgraph Wweb["wweb 플랫폼"]

        API[FastAPI<br/>wweb_api:8000<br/>110MB]

        PHP[PHP-FPM 8.3<br/>wweb-php:9000<br/>62MB]

        DB[(MariaDB 11<br/>wweb_db:3306<br/>432MB)]

        S3[(MinIO S3<br/>s3.wellcoms.co.kr<br/>177MB)]

        Static[정적 HTML 캐시<br/>/srv/docker/wweb/static/]

    end


    subgraph Tenants["테넌트 (디렉토리 기반)"]

        T1["mytest<br/>/var/www/tenants/mytest/"]

        T2["demo1<br/>/var/www/tenants/demo1/"]

        T3["wweb_main<br/>/var/www/tenants/wweb_main/"]

    end


    Client -->|HTTPS| DNS

    DNS -->|443| Caddy


    Caddy -->|/api/*, /builder/*, /dashboard| API

    Caddy -->|정적 index.html 있음| Static

    Caddy -->|PHP 요청<br/>php_fastcgi| PHP


    PHP --> T1

    PHP --> T2

    PHP --> T3


    T1 -->|wp-config.php<br/>DB 연결| DB

    T2 -->|wp-config.php<br/>DB 연결| DB

    T3 -->|wp-config.php<br/>DB 연결| DB


    API -->|이미지 업로드| S3

    API -->|테넌트/인증| DB

    API -->|컨테이너/디렉토리 관리| DockerSock[Docker Socket]


    style Caddy fill:#2ecc71,color:#fff

    style API fill:#3498db,color:#fff

    style PHP fill:#e67e22,color:#fff

    style DB fill:#e74c3c,color:#fff

    style S3 fill:#9b59b6,color:#fff

    style Static fill:#1abc9c,color:#fff

```


### 1.2 핵심 아키텍처: 단일 PHP-FPM 컨테이너 멀티테넌시


**이전 (Apache mod_php):** 테넌트 1개 = Docker 컨테이너 1개 (각각 Apache + PHP 독립 로드)

**현재 (PHP-FPM):** 모든 테넌트가 단일 PHP-FPM 컨테이너를 공유


```

Caddy (호스트명 라우팅)

  ├── mytest.wweb.wellcoms.co.kr → php_fastcgi wweb-php:9000

  │     └── SCRIPT_FILENAME: /var/www/tenants/mytest/index.php

  ├── demo1.wweb.wellcoms.co.kr → php_fastcgi wweb-php:9000

  │     └── SCRIPT_FILENAME: /var/www/tenants/demo1/index.php

  └── wweb.wellcoms.co.kr → php_fastcgi wweb-php:9000

        └── SCRIPT_FILENAME: /var/www/tenants/wweb_main/index.php

```


**핵심 규칙:**

- `index.php`는 **실제 파일**이어야 함 (심볼릭 링크 금지 — Caddy `php_fastcgi`가 resolve 시 core 경로로 전달하여 ABSPATH 불일치 발생)

- 나머지 WP 파일(wp-settings.php 등)은 심볼릭 링크로 core 공유

- WP 코어는 `/var/www/core/`에 마운트 (읽기 전용)

- 플러그인/테마는 `/var/www/shared/`에 마운트 (읽기 전용)


### 1.3 요청 흐름


```mermaid

sequenceDiagram

    participant V as 방문자

    participant C as Caddy

    participant S as 정적 HTML

    participant FPM as PHP-FPM

    participant DB as MariaDB

    participant S3 as MinIO S3


    Note over V,S3: 홈페이지 접속 (정적 캐싱)


    V->>C: GET https://mytest.wweb.wellcoms.co.kr/

    C->>S: index.html 존재 확인

    S-->>C: index.html 반환

    C-->>V: 정적 HTML 응답 (PHP 0, 메모리 0)


    Note over V,S3: 상품 상세 페이지 (PHP-FPM 동적)


    V->>C: GET /m_product_info/?vid=5

    C->>S: index.html 없음

    C->>FPM: php_fastcgi (SCRIPT_FILENAME=/var/www/tenants/mytest/index.php)

    FPM->>DB: wp-config.php → wweb_mytest 스키마

    FPM-->>C: PHP 8.3 렌더링 결과

    C-->>V: 동적 페이지 응답


    Note over V,S3: 이미지 로드


    V->>S3: GET https://s3.wellcoms.co.kr/wweb/mytest/uploads/aaa.png

    S3-->>V: 이미지 직접 서빙 (Caddy/PHP 거치지 않음)

```


### 1.4 테넌트 생성 흐름


```mermaid

sequenceDiagram

    participant U as 사용자

    participant API as FastAPI

    participant DB as MariaDB

    participant SH as create-tenant.sh

    participant Caddy as Caddy


    U->>API: POST /api/tenants {slug, plan_id}

    API->>DB: 중복 slug + 1계정1쇼핑몰 검증

    API->>DB: tenants 행 생성

    API->>DB: wweb_{slug} 스키마 생성 + WP 초기화

    API->>SH: create-tenant.sh {slug}

    Note over SH: 1. /var/www/tenants/{slug}/ 디렉토리 생성<br/>2. index.php 실제 파일 생성 (ABSPATH 명시)<br/>3. wp-config.php 생성 (DB 접속 정보)<br/>4. 나머지 WP 파일 → /var/www/core/ 심볼릭 링크

    API->>Caddy: Caddyfile 테넌트 블록 추가 (php_fastcgi)

    API->>Caddy: caddy reload

    API-->>U: 생성 완료

```


> **Apache 방식과의 차이:** `docker run`으로 컨테이너를 생성하지 않음. 디렉토리 + wp-config.php + Caddy 라우트만 추가하면 즉시 서비스 가능.


### 1.5 이미지 업로드 흐름


```mermaid

sequenceDiagram

    participant U as 관리자

    participant API as FastAPI

    participant S3 as MinIO S3


    Note over U,S3: S3 이미지 업로드 (빌더/대시보드)


    U->>API: POST /api/wp/{slug}/upload (multipart)

    API->>S3: put_object (wweb/{slug}/uploads/{YYYY/MM}/{timestamp}.{ext})

    S3-->>API: 업로드 완료

    API-->>U: {"url": "https://s3.wellcoms.co.kr/wweb/{slug}/uploads/..."}

```


---


## 2. 하드웨어 스펙 및 리소스 현황


### 2.1 서버 사양


| 항목 | 사양 |

|------|------|

| CPU | Intel Xeon E3-1231 v3 (4코어 / 8스레드, 3.40GHz) |

| RAM | 31GB DDR3 |

| 디스크 | NVMe 232GB (LVM) |

| 네트워크 | 1Gbps (enp0s25) |

| OS | Ubuntu Linux |

| Swap | 8GB (5.7GB 사용 중) |


### 2.2 현재 리소스 사용량 (2026-05-30 실측)


| 리소스 | 사용량 | 한계 | 여유 |

|--------|--------|------|------|

| RAM | 16GB / 31GB | - | 14GB 가용 |

| 디스크 | 190GB / 232GB | 87% | 30GB 남음 |

| 실행 컨테이너 | 75개 | - | - |

| Swap | 5.7GB / 8GB | 71% | 2.3GB 남음 |


### 2.3 wweb 전용 리소스 사용량


| 컨테이너 | 메모리 | 역할 |

|----------|--------|------|

| `wweb-php` | **62MB** | PHP-FPM 8.3 (모든 테넌트 공유) |

| `wweb_api` | 110MB | FastAPI (인증, 프록시, S3 업로드) |

| `6ebc5dcd73aa_wweb_db` | 432MB | MariaDB (전체 테넌트 DB) |

| `minio` | 177MB | S3 이미지 스토리지 |

| `minio-console` | 50MB | MinIO 관리 콘솔 |

| `caddy` | 25MB | 리버스프록시 + TLS + php_fastcgi |

| **wweb 전체 합계** | **~856MB** | |


### 2.4 PHP-FPM vs Apache 비교 (실측)


```mermaid

graph LR

    subgraph "이전: Apache mod_php"

        A1["tenant_a_wp<br/>48MB"]

        A2["tenant_b_wp<br/>48MB"]

        A3["wweb_wp<br/>146MB"]

        A4["...N개×75MB"]

    end


    subgraph "현재: PHP-FPM"

        B1["wweb-php<br/>62MB<br/>(모든 테넌트 공유)"]

    end


    style A1 fill:#e74c3c,color:#fff

    style A2 fill:#e74c3c,color:#fff

    style A3 fill:#e74c3c,color:#fff

    style A4 fill:#e74c3c,color:#fff

    style B1 fill:#2ecc71,color:#fff

```


| 시나리오 | Apache (기존) | PHP-FPM (현재) | 절약 |

|----------|--------------|----------------|------|

| 3 테넌트 | 48+48+146 = **242MB** | **62MB** | 180MB (74%) |

| 10 테넌트 | 10×75 = **750MB** | **~100MB** | 650MB (87%) |

| 100 테넌트 | 100×75 = **7,500MB** (불가) | **~300MB** idle / **~500MB** peak | 7,000MB (93%) |


### 2.5 테넌트당 리소스 사용량


| 리소스 | 테넌트 1개당 | 비고 |

|--------|-------------|------|

| RAM (FPM 공유) | **~5MB** | idle 시. active 시 FPM 워커 1개당 ~15MB |

| 디스크 (WP 파일) | **~5MB** | 코어/플러그인/테마는 RO 공유, wp-config + index.php만 개별 |

| DB 스키마 | **5~130MB** | 상품 수에 따라 차이 큼 |

| S3 이미지 | **무제한** | 로컬 디스크 사용 안 함 |

| Caddy 설정 | **~500바이트** | php_fastcgi 블록 1개 추가 |


---


## 3. 용량 계획 (Capacity Planning)


### 3.1 병목별 최대 테넌트 수


| 병목 | 한계 테넌트 수 | 계산 근거 |

|------|---------------|-----------|

| **RAM** | **~700명** | 가용 14GB - 다른 서비스 10GB = 4GB / 5MB ≈ 800 (여유 포함 700) |

| **CPU** | **~400명** | 8코어, PHP 50ms/요청, FPM max_children=50 → 동시 50요청 |

| **디스크** | **~1,000명+** | S3 전환으로 이미지 디스크 제로. DB 130MB×1000 = 130GB (주의) |

| **네트워크** | **~500명+** | 1Gbps, HTML 100KB → 동시 250명 로드. 정적 캐싱 시 더 많음 |


### 3.2 확장 로드맵


```mermaid

timeline

    title wweb 확장 로드맵

    section Phase 1 (완료)

        PHP-FPM 전환 : 단일 컨테이너 멀티테넌시

        : S3 + 정적 캐싱 + 빌더 CSS 수정

        : ~700명 수용

    section Phase 2 (서버 분리)

        wweb 전용 서버 : DB/S3은 기존 유지

        : ~1,000명 수용

    section Phase 3 (고급)

        16코어 CPU : DB 튜닝

        : ~1,500명 수용

    section Phase 4 (클라우드)

        Kubernetes : 오토스케일링

        : 무한 확장

```


### 3.3 동시 접속자 기준


| 총 테넌트 | 평균 동시 접속 | 피크 동시 접속 | FPM 워커 | CPU 부하 |

|-----------|---------------|---------------|----------|----------|

| 10명 | 5명 | 50명 | 10 | ~10% |

| 100명 | 50명 | 500명 | 50 (max) | ~40% |

| 300명 | 150명 | 1,500명 | 50+큐 | ~70% |

| 700명 | 350명 | 3,500명 | 큐 대기 | CPU 포화 |


> FPM `pm.max_children=50` 설정. 300명 이상 시 100~200으로 증설 필요.


---


## 4. 컨테이너 관리


### 4.1 컨테이너 구성


```mermaid

graph TB

    subgraph NetworkWeb["web 네트워크 (Caddy 통신)"]

        Caddy

        API[wweb_api]

        MinIO

        MinioConsole[minio-console]

    end


    subgraph NetworkInternal["wweb_wweb-internal (DB + PHP 통신)"]

        API

        PHP[wweb-php]

        DB[(MariaDB)]

    end


    subgraph Volumes["PHP-FPM 볼륨 마운트"]

        Core["/srv/docker/wweb/wp-core<br/>→ /var/www/core (ro)"]

        Plugins["/srv/docker/wweb/master/plugins<br/>→ /var/www/shared/plugins (ro)"]

        Themes["/srv/docker/wweb/master/themes<br/>→ /var/www/shared/themes (ro)"]

        Tenants["/srv/docker/wweb/tenants<br/>→ /var/www/tenants (rw)"]

    end


    PHP ---|ro| Core

    PHP ---|ro| Plugins

    PHP ---|ro| Themes

    PHP ---|rw| Tenants

    API ---|rw| DockerSock["/var/run/docker.sock"]

    API ---|rw| CaddyFile["Caddyfile"]

    API ---|rw| StaticDir["static/"]

    API ---|rw| TenantsDir["tenants/"]

```


### 4.2 PHP-FPM 컨테이너 설정


```bash

# 현재 실행 중인 컨테이너 재생성

docker stop wweb-php && docker rm wweb-php && \

docker run -d \

  --name wweb-php \

  --restart unless-stopped \

  --network wweb_wweb-internal \

  -v /srv/docker/wweb/wp-core:/var/www/core:ro \

  -v /srv/docker/wweb/tenants:/var/www/tenants \

  -v /srv/docker/wweb/master/plugins:/var/www/shared/plugins:ro \

  -v /srv/docker/wweb/master/themes:/var/www/shared/themes:ro \

  -v /srv/docker/wweb/static:/srv/docker/wweb/static \

  wweb_php_img

```


> **주의:** 이미지 리빌드 시에만 재생성 필요. php-fpm-pool.conf, php-custom.ini 변경 시 `docker build` 필요 (볼륨 마운트가 아닌 COPY 베이킹).


### 4.3 API 컨테이너 재생성


```bash

# 코드 변경 후 반드시 rebuild 필요 (COPY 방식)

docker build -t wweb_api_img /srv/docker/wweb/backend/ && \

docker stop wweb_api && docker rm wweb_api && \

docker run -d \

  --name wweb_api \

  --restart unless-stopped \

  --network wweb_wweb-internal \

  -e SECRET_KEY=7f2fa3a7297e60375f2cbbdf2f07e410c3862afa3dc30aff98b651502cd9de39 \

  -e TENANTS_BASE_PATH=/srv/docker/wweb/tenants \

  -e DB_HOST=6ebc5dcd73aa_wweb_db \

  -e DB_ROOT_USER=root \

  -e DB_ROOT_PASSWORD=ab9f6db6d8f8b138fd48693d9244337d \

  -e DB_PORT=3306 \

  -v /var/run/docker.sock:/var/run/docker.sock \

  -v /srv/docker/caddy/Caddyfile:/srv/docker/caddy/Caddyfile \

  -v /srv/docker/wweb/static:/srv/docker/wweb/static \

  -v /srv/docker/wweb/tenants:/srv/docker/wweb/tenants \

  wweb_api_img && \

sleep 2 && docker network connect web wweb_api

```


> **주의:** `SECRET_KEY` 변경 시 기존 JWT 토큰이 모두 무효화됩니다. 반드시 고정값 사용.


### 4.4 PHP-FPM 이미지 리빌드


```bash

# php-fpm-pool.conf, php-custom.ini, Dockerfile 변경 시

docker build -t wweb_php_img -f /srv/docker/wweb/docker/Dockerfile.php-fpm /srv/docker/wweb/docker/ && \

docker stop wweb-php && docker rm wweb-php && \

# 4.2의 docker run 명령으로 재생성

```


---


## 5. 서비스별 설정


### 5.1 PHP-FPM 풀 설정


```ini

# /srv/docker/wweb/docker/php-fpm-pool.conf

[www]

user = www-data

group = www-data

listen = 9000

pm = dynamic

pm.max_children = 50

pm.start_servers = 5

pm.min_spare_servers = 3

pm.max_spare_servers = 10

pm.max_requests = 500

request_terminate_timeout = 30

php_admin_value[memory_limit] = 512M

php_admin_value[upload_max_filesize] = 128M

php_admin_value[post_max_size] = 128M

php_admin_value[max_execution_time] = 60

```


| 설정 | 값 | 비고 |

|------|-----|------|

| `pm.max_children` | 50 | 동시 처리 최대 50개 PHP 요청 |

| `memory_limit` | 512M | 8MB+ 콘텐츠 저장 시 wp_update_post() 메모리 사용 대응 |

| `pm.max_requests` | 500 | 메모리 누수 방지 (500 요청 후 워커 재생성) |

| `request_terminate_timeout` | 30 | 30초 이상 요청 강제 종료 |


### 5.2 PHP 설정


```ini

# /srv/docker/wweb/docker/php-custom.ini

upload_max_filesize = 128M

post_max_size = 128M

max_execution_time = 60

memory_limit = 512M

```


### 5.3 Caddy php_fastcgi 설정


```caddyfile

# 테넌트별 Caddy 블록 예시

mytest.wweb.wellcoms.co.kr {

    encode zstd gzip


    # 정적 캐시 (홈페이지만)

    @static file /index.html

    handle @static {

        root * /srv/docker/wweb/static/mytest

        rewrite /index.html

        file_server

    }


    # 정적 파일 (CSS/JS/이미지) — Caddy 직접 서빙

    @staticFiles path *.css *.js *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ttf *.ico

    handle @staticFiles {

        root * /var/www/tenants/mytest

        file_server

    }


    # 나머지 — PHP-FPM 처리

    handle {

        root * /var/www/tenants/mytest

        php_fastcgi wweb-php:9000

    }

}

```


### 5.4 MinIO S3 설정


| 항목 | 값 |

|------|-----|

| 컨테이너 | `minio` |

| 엔드포인트 (내부) | `http://minio:9000` |

| 공개 URL | `https://s3.wellcoms.co.kr` |

| 콘솔 | `https://minio.wellcoms.co.kr` |

| 버킷 | `wweb` (공개 읽기) |

| 자격증명 | `lordly` / `rlawoehf@123A` |

| 이미지 경로 | `wweb/{slug}/uploads/{YYYY/MM}/{timestamp}.{ext}` |


### 5.5 정적 HTML 캐싱


| 항목 | 설명 |

|------|------|

| 캐시 위치 | `/srv/docker/wweb/static/{slug}/index.html` |

| 배포 API | `POST /api/wp/{slug}/pages/{page_id}/publish-static` |

| 적용 범위 | **홈페이지만** (상품상세/장바구니/결제는 PHP-FPM 동적) |

| 캐시 무효화 | 빌더에서 "배포" 시 자동 재생성 |


---


## 6. 운영 정보


### 6.1 DB 구조


```mermaid

erDiagram

    SAAS["wweb_wp (SaaS 메타 DB)"] {

        int users PK "사용자 (id, email, name, phone, role, password_hash)"

        int plans PK "요금제 (id, name, price, max_pages, max_products, max_shops, custom_domain)"

        int tenants PK "테넌트 (id, user_id, slug, plan_id, status, domain, subscription_started_at, subscription_expires_at)"

    }


    TENANT_DB["tenant_{slug}<br/>(테넌트별 DB)"] {

        int wp_posts PK "WordPress 페이지/글"

        int wp_postmeta PK "페이지 메타 (빌더 CSS: _wweb_css)"

        int mb_commerce_product PK "상품"

        text mb_options "PG 결제 설정 등"

    }


    S3_STORAGE["MinIO S3<br/>(이미지 스토리지)"] {

        text wweb_slug_uploads "wweb/{slug}/uploads/*"

    }


    users ||--o{ tenants : "1:N (max_shops 제한)"

    plans ||--o{ tenants : "요금제 할당"

    tenants ||--o{ TENANT_DB : "1테넌트 1DB"

    TENANT_DB }o--|| S3_STORAGE : "이미지 URL 참조"

```


### 6.2 DB 크기 (현재)


| 스키마 | 크기 | 비고 |

|--------|------|------|

| `wweb_mytest` | 131.6MB | 테스트 상품/페이지 다수 (8.4MB 콘텐츠 포함) |

| `wweb_demo1` | 4.6MB | 기본 상태 |

| `wweb_wp` | 5.6MB | SaaS 메인 WordPress |

| `wweb_saas` | 0.2MB | 사용자/테넌트/플랜 |


### 6.3 현재 테넌트 목록


| slug | user_id | 상태 | 요금제 | PHP-FPM | 정적 캐싱 | S3 |

|------|---------|------|--------|:---:|:---:|:---:|

| mytest | 3 | active | 엔터프라이즈 (plan=3) | ✅ | ✅ | ✅ |

| demo1 | - | active | - | ✅ | ✅ | - |

| e2etest | - | active | - | ✅ | - | - |

| wweb_main | - | active | - | ✅ | - | - |


### 6.4 SaaS 사용자


| 이메일 | user_id | 역할 | 비고 |

|--------|---------|------|------|

| lordlykim@gmail.com | 3 | admin | 관리자, PW: rlawoehf1!A |

| test@test.com | 1 | user | 테스트 계정 |

| test@wweb.kr | 2 | user | 테스트 계정 |

| s3test@test.com | 4 | user | S3 테스트 계정 |


### 6.5 요금제 현황


| id | 요금제명 | 월 요금 | max_shops | max_pages | max_products | custom_domain |

|----|----------|---------|-----------|-----------|--------------|---------------|

| 1 | 무료 | 0원 | 1 | 무제한 | 무제한 | 불가 |

| 2 | 프로 | 29,000원 | 1 | 무제한 | 무제한 | 가능 |

| 3 | 엔터프라이즈 | 99,000원 | 1 | 무제한 | 무제한 | 가능 |


> **주의:** 전 요금제 max_shops=1 통일 (2026-05-30 기준)


### 6.6 빌더 CSS 저장 메커니즘


빌더는 GrapesJS를 사용하며, HTML과 CSS를 분리 저장합니다.


**저장 흐름:**

1. `collectAllComponentStyles(editor)` — 모든 컴포넌트를 순회하며 style 속성을 CSS 규칙으로 변환

2. `editor.getCss({ keepUnusedStyles: true })` — GrapesJS CSS 매니저에서 등록된 규칙 가져오기

3. 두 소스를 병합하고 ID별 중복 제거

4. `post_content` (HTML)과 `_wweb_css` (CSS) 분리 저장


**WP 렌더링:** `wweb-head-css` 플러그인이 `_wweb_css` 메타를 `<style>` 태그로 `<head>`에 출력.


### 6.7 캐시 버스터


- 현재: `?v=20260529z`

- 변경 시 빌더 HTML의 `app.js` 쿼리 업데이트 필요


---


## 7. 관리자 /admin 분리 아키텍처


### 7.1 개요


관리자 대시보드는 일반 대시보드(`/dashboard`)와 완전히 분리된 별도 페이지(`/admin`).


| 항목 | 일반 대시보드 | 관리자 대시보드 |

|------|-------------|---------------|

| URL | `/dashboard` | `/admin` |

| 토큰 | `wweb_token` | `wweb_admin_token` |

| JS 파일 | `dashboard/app.js` | `admin/app.js` |

| CSS 파일 | `dashboard/style.css` | `admin/style.css` |

| HTML | `dashboard/index.html` | `admin/index.html` |

| 접근 | 모든 인증 사용자 | role=admin만 |

| 기능 | 내정보, 쇼핑몰관리, 주문, 통계 | 개요/사용자/테넌트/요금제/리소스/시스템 |


### 7.2 관리자 6탭 구성


| 탭 | API | 기능 |

|----|-----|------|

| 개요 | `/api/admin/stats` | 전체 사용자/테넌트/매출 통계 |

| 사용자 | `/api/admin/users`, `/api/admin/users/{id}` | 목록/상세/역할변경/삭제 |

| 테넌트 | `/api/admin/tenants` | 목록/요금제변경/정지/활성화/삭제 |

| 요금제 | `/api/admin/plans` | 목록/수정 (max_shobs 등) |

| 리소스 | `/api/admin/resources` | CPU/RAM/디스크 사용량 |

| 시스템 | `/api/admin/containers` | Docker 컨테이너 목록 |


### 7.3 Caddyfile /admin 라우팅


WordPress가 `/admin` → `/wp-admin/`으로 리다이렉트하므로, Caddy에서 API로 우선 라우팅:


```caddyfile

# wweb.wellcoms.co.kr 서버 블록 내

handle /admin {

    reverse_proxy wweb_api:8000

}

handle /admin/* {

    reverse_proxy wweb_api:8000

}

# 일반 WordPress 라우팅은 그 아래에 위치

```


### 7.4 관리자 로딩 스피너


모든 탭 전환 시 회전 애니메이션 + "불러오는 중..." 표시:


```css

/* admin/style.css */

@keyframes aspin { to { transform: rotate(360deg); } }

.loading-spinner { animation: aspin 1s linear infinite; }

```


```javascript

// admin/app.js — 모든 탭 로드 함수에서 호출

function showLoading() {

    document.getElementById('tab-content').innerHTML =

        '<div style="text-align:center;padding:60px"><div class="loading-spinner" ' +

        'style="width:40px;height:40px;border:4px solid #eee;border-top:4px solid #3498db;' +

        'border-radius:50%;margin:0 auto 16px"></div>불러오는 중...</div>';

}

```


---


## 8. 대시보드 프로필 (내 정보)


### 8.1 프로필 탭 구성


대시보드 "내 정보" 섹션에서 사용자 정보 + 구독 현황을 표시.


**표시 항목:**

- 기본 정보: 이름, 이메일, 전화번호, 가입일

- 구독 현황: 요금제명, 가격, 도메인, 구독시작일, 만료일

- 요금제 제한: 상품 수, 페이지 수, 커스텀 도메인 가능 여부


### 8.2 API 확장


`/api/auth/me` 응답에 tenants 조인으로 plan 정보 포함:


```python

# auth.py — get_user() 확장

def get_user(user_id: int):

    conn = get_saas_conn()

    cursor = conn.cursor()

    cursor.execute("""

        SELECT u.*, t.slug AS tenant_slug, t.domain,

               t.subscription_started_at, t.subscription_expires_at,

               p.name AS plan_name, p.price AS plan_price,

               p.max_pages, p.max_products, p.custom_domain

        FROM users u

        LEFT JOIN tenants t ON t.user_id = u.id AND t.status = 'active'

        LEFT JOIN plans p ON p.id = t.plan_id

        WHERE u.id = %s

    """, (user_id,))

```


### 8.3 DB 컬럼 추가


```sql

-- tenants 테이블에 구독 관련 컬럼 추가됨

ALTER TABLE tenants ADD COLUMN subscription_started_at DATETIME;

ALTER TABLE tenants ADD COLUMN subscription_expires_at DATETIME;

```


---


## 9. 쇼핑몰 생성 제한 (max_shops)


### 9.1 개요


사용자가 생성할 수 있는 쇼핑몰 수를 `plans.max_shops` 값으로 제한. 하드코딩이 아닌 DB 기반.


### 9.2 API 응답


`/api/my-shops` 응답에 `max_shops` 필드 포함:


```json

{

    "shops": [

        {"id": 1, "slug": "mytest", "plan_name": "엔터프라이즈", ...}

    ],

    "max_shops": 1

}

```


```python

# auth.py — get_user_tenants()에 max_shops 조인

def get_user_tenants(user_id: int):

    cursor.execute("""

        SELECT t.*, p.name AS plan_name, p.max_shops

        FROM tenants t

        LEFT JOIN plans p ON p.id = t.plan_id

        WHERE t.user_id = %s AND t.status != 'deleted'

    """, (user_id,))

```


### 9.3 프론트엔드 제한 로직


```javascript

// dashboard/app.js — loadShops()

if (shops.length >= maxShops) {

    section.style.opacity = '0.5';

    section.style.pointerEvents = 'none';

    // "생성 한도 도달" 안내 메시지 표시

}

```


### 9.4 현재 설정


전 요금제 `max_shops=1` 통일. 관리자가 `/admin` 요금제 탭에서 변경 가능.


---


## 10. 백업 자동화


### 10.1 백업 스크립트


`/srv/docker/wweb/docker/backup.sh` — 매일 03:00 자동 실행


**백업 대상:**

1. MariaDB 전체 덤프 (`mysqldump --all-databases`)

2. 정적 HTML 캐시 (`/srv/docker/wweb/static/`)

3. 테넌트 설정 (`/srv/docker/wweb/tenants/`)


**저장 위치:** `/srv/docker/wweb/backups/`


### 10.2 크론 설정


```bash

# crontab -l

0 3 * * * /srv/docker/wweb/docker/backup.sh >> /srv/docker/wweb/backups/backup.log 2>&1

```


### 10.3 백업 파일명 규칙


```

backups/

├── wweb_db_20260530.sql.gz      # DB 덤프

├── wweb_static_20260530.tar.gz   # 정적 HTML

└── backup.log                    # 실행 로그

```


---


## 11. 장애 대응


### 11.1 장애 시나리오별 대응


```mermaid

graph TD

    Problem[장애 발생] --> Check{어떤 서비스?}


    Check -->|사이트 안 열림| SiteCheck

    Check -->|이미지 안 나옴| ImageCheck

    Check -->|빌더 안 됨| BuilderCheck

    Check -->|로그인 안 됨| AuthCheck

    Check -->|502 에러| FPMCheck


    SiteCheck["Caddy + PHP-FPM 확인"]

    SiteCheck -->|wweb-php 다운| RestartPHP["docker restart wweb-php"]

    SiteCheck -->|컨테이너 정상| DBCheck["DB 연결 확인"]


    ImageCheck["S3 접근 확인"]

    ImageCheck -->|404| S3Restart["docker restart minio"]

    ImageCheck -->|200| CDNCheck["Caddy s3 라우트 확인"]


    BuilderCheck["API 컨테이너 확인"]

    BuilderCheck -->|다운| APIRestart["docker restart wweb_api"]


    AuthCheck["JWT 토큰 확인"]

    AuthCheck -->|키 변경됨| KeyFix["동일 SECRET_KEY로 재생성"]


    FPMCheck["PHP-FPM 로그 확인"]

    FPMCheck --> OOM["memory_limit 초과?<br/>docker logs wweb-php | grep Fatal"]

    OOM --> IncreaseMem["pool.conf memory_limit 증설<br/>이미지 리빌드"]


    style Problem fill:#e74c3c,color:#fff

    style RestartPHP fill:#2ecc71,color:#fff

    style APIRestart fill:#2ecc71,color:#fff

    style KeyFix fill:#2ecc71,color:#fff

    style IncreaseMem fill:#2ecc71,color:#fff

```


### 11.2 주요 명령어


```bash

# PHP-FPM 재시작 (모든 테넌트에 영향)

docker restart wweb-php


# API 컨테이너 재시작

docker restart wweb_api


# Caddy 설정 리로드

docker exec caddy caddy reload --config /etc/caddy/Caddyfile


# DB 연결 확인

docker exec 6ebc5dcd73aa_wweb_db mariadb -u root -p'ab9f6db6d8f8b138fd48693d9244337d' -e "SELECT 1"


# PHP-FPM 에러 로그 확인

docker logs wweb-php --tail 50 2>&1 | grep -i "fatal\|error\|warning"


# MinIO 상태 확인

docker exec minio mc admin info local


# 정적 캐시 수동 삭제 (다음 요청 시 PHP-FPM fallback)

rm -f /srv/docker/wweb/static/{slug}/index.html


# 빌더 API 플러그인 업데이트 (공유 볼륨이므로 즉시 반영)

# 파일 수정 후 별도 배포 불필요 (마운트된 디렉토리에 있음)

# 단, wweb-builder-api.php는 테넌트 디렉토리에 심볼릭 링크로 연결됨


# 컨테이너 로그 실시간 확인

docker logs -f wweb-php

docker logs -f wweb_api


# FPM 프로세스 상태 확인

docker exec wweb-php ps aux | grep php-fpm


# PHP 설정 확인

docker exec wweb-php php -i | grep memory_limit

```


### 11.3 알려진 이슈


| 이슈 | 원인 | 해결 |

|------|------|------|

| 빌더 저장 시 500 에러 | 8MB+ 콘텐츠 + wp_update_post() → 256MB OOM | memory_limit 512M로 증설 완료 |

| theme-function.php Warning | `$container_padding` null 접근 | 기본값 삼항연산자로 수정 완료 |

| 빨간 글자 → 검은 글자 | GrapesJS `getCss()`가 모든 컴포넌트 스타일 미반환 | `collectAllComponentStyles()` 추가로 수정 완료 |

| index.php 심볼릭 링크 불가 | Caddy `php_fastcgi`가 symlink resolve 시 ABSPATH 불일치 | 실제 파일로 생성 + ABSPATH 명시 |

| `esc is not defined` | dashboard/app.js IIFE에 `esc()` 헬퍼 누락 | IIFE 내부에 `esc()` 함수 추가 완료 |

| `loadOrdersDropdown` TypeError | `shops.forEach` → API 응답이 `{shops:[...]}` 형태 | `d.shops.forEach`로 수정 완료 |


---


## 12. 정기 유지보수


### 12.1 주간 점검


| 항목 | 명령어 | 기준 |

|------|--------|------|

| 디스크 사용률 | `df -h /` | 90% 미만 |

| 메모리 사용률 | `free -h` | Swap 80% 미만 |

| 컨테이너 상태 | `docker ps -a` | 모든 컨테이너 Up |

| Docker 쓰레기 | `docker system df` | Reclaimable 10GB 미만 |

| PHP-FPM 에러 | `docker logs wweb-php --since 7d \| grep -c Fatal` | 0건 |

| DB 크기 | `docker exec ... mariadb -e "SELECT table_schema..."` | 급증 확인 |


### 12.2 정리 명령어


```bash

# Dangling 이미지 정리

docker image prune -f


# 빌드 캐시 정리

docker builder prune -f


# 미사용 볼륨 정리 (주의: 백업 후 실행)

docker volume prune -f


# 전체 정리 (한 번에)

docker system prune -f

```


### 12.3 백업


| 대상 | 방법 | 주기 |

|------|------|------|

| MariaDB | `mysqldump` → gzip | 매일 03:00 (자동) |

| 정적 HTML | tar.gz | 매일 03:00 (자동) |

| 테넌트 설정 | tar.gz | 매일 03:00 (자동) |

| MinIO S3 | NAS 스토리지에 복제 | 실시간 (NAS 마운트) |

| Caddy 설정 | `/srv/docker/caddy/Caddyfile` | 변경 시 |


```bash

# DB 백업 스크립트

docker exec 6ebc5dcd73aa_wweb_db mysqldump -u root -p'ab9f6db6d8f8b138fd48693d9244337d' \

  --all-databases --single-transaction | gzip > /srv/docker/backup/wweb_db_$(date +%Y%m%d).sql.gz

```


---


## 13. 서버 분리 아키텍처 (wweb 전용 서버)


### 13.1 분리 개요


wweb 서비스를 전용 서버로 분리. DB와 S3는 현재 서버에 유지.


```mermaid

graph TB

    subgraph CurrentServer["현재 서버 (192.168.0.2)<br/>Xeon E3-1231 v3 / 31GB / 232GB NVMe"]

        DB[(MariaDB<br/>wweb DB 포함<br/>432MB)]

        S3[(MinIO S3<br/>s3.wellcoms.co.kr<br/>177MB)]

        Mail[Mailcow 메일]

        GitLab[GitLab]

        Others[n8n, Immich, Seafile 등]

        CaddyOld[Caddy<br/>기존 서비스 프록시]

    end


    subgraph NewServer["신규 wweb 전용 서버<br/>12세대 i7 / 32~64GB DDR4"]

        CaddyNew[Caddy<br/>wweb 전용 프록시]

        API[FastAPI<br/>wweb_api]

        PHP[PHP-FPM<br/>모든 테넌트 공유]

        Static[정적 HTML 캐시]

    end


    Internet((인터넷 1Gbps))


    Internet --> CaddyOld

    Internet --> CaddyNew


    CaddyNew --> API

    CaddyNew --> PHP

    CaddyNew --> Static


    API -.->|LAN 1Gbps<br/>DB 쿼리| DB

    PHP -.->|LAN 1Gbps<br/>DB 쿼리| DB

    API -.->|S3 업로드| S3

    Internet -.->|이미지 서빙| S3


    style DB fill:#e74c3c,color:#fff

    style S3 fill:#9b59b6,color:#fff

    style CaddyNew fill:#2ecc71,color:#fff

    style API fill:#3498db,color:#fff

    style PHP fill:#e67e22,color:#fff

```


### 13.2 DB 원격 접속 설정


**현재 서버 (DB) — 1회성 설정:**


```bash

# 1. MariaDB 원격 접속 허용

# bind-address = 0.0.0.0 으로 변경 후 restart


# 2. wweb용 DB 사용자에 원격 접근 권한 부여

docker exec 6ebc5dcd73aa_wweb_db mariadb -u root -p'PASSWORD' -e "

  CREATE USER IF NOT EXISTS 'wweb_remote'@'%' IDENTIFIED BY 'STRONG_PASSWORD';

  GRANT ALL PRIVILEGES ON wweb_%.* TO 'wweb_remote'@'%';

  FLUSH PRIVILEGES;

"


# 3. 방화벽: 3306 포트 신규 서버 IP만 허용

ufw allow from 192.168.0.{신규서버IP} to any port 3306

```


**신규 서버 — wp-config.php:**


```php

// 변경: DB_HOST = 192.168.0.2 (현재 서버 LAN IP)

define('DB_HOST', '192.168.0.2:3306');

```


### 13.3 DB + S3 같은 서버 — 병목 분석


| 리소스 | MariaDB | MinIO (S3) | 간섭 |

|--------|---------|------------|:----:|

| CPU | 쿼리 처리 (정수 연산) | 거의 안 씀 | 없음 |

| RAM | Buffer Pool (데이터 캐시) | 객체 메타데이터만 | 없음 |

| 디스크 | 랜덤 I/O (작은 읽기/쓰기) | 순차 I/O (큰 파일) | 없음 |

| 네트워크 | LAN (작은 패킷, 쿼리) | 인터넷 (큰 패킷, 이미지) | 없음 |


> **결론: 700명까지 현재 서버에 DB + S3 같이 있어도 병목 없음.**


### 13.4 DB 성장에 따른 튜닝 필요 시점


| 테넌트 수 | DB RAM | DB CPU | 필요 조치 |

|-----------|--------|--------|-----------|

| ~100명 | ~1.5GB | ~10% | `innodb_buffer_pool_size` 1GB 증가 |

| ~300명 | ~3GB | ~25% | buffer pool 2GB, `max_connections` 300 |

| ~500명 | ~5GB | ~40% | buffer pool 4GB, slow query 분석 |

| ~700명 | ~7GB | ~55% | DB CPU 병목 시작, 슬레이브 DB 고려 |


---


## 14. 시나리오별 정리


| 구성 | 안전 한계 | 최대 한계 | 비용 |

|------|----------|----------|------|

| 현재 서버 + PHP-FPM | **~300명** | ~400명 | 최소 |

| 신규 i7-12700 + 32GB + PHP-FPM | **~700명** | ~1,000명 | 서버 1대 |

| i7-13700 + 32GB + PHP-FPM | **~1,000명** | ~1,500명 | CPU 업그레이드 |

| i7-13700 + 32GB + PHP-FPM + DB 분리 | **~1,500명** | ~2,000명 | DB 전용 추가 |


```

현재 상태: 단일 서버 + PHP-FPM → 안정 ~300명 수용 가능

신규 서버 분리 시: 안정 ~700명 수용 가능

64GB RAM은 의미 없음 (CPU가 먼저 한계)

추가 인터넷 회선은 700명까지 불필요

```


---


## 15. 핵심 파일 경로


| 파일 | 경로 | 설명 |

|------|------|------|

| FastAPI 메인 | `/srv/docker/wweb/backend/app/main.py` | API 엔드포인트, S3 업로드, 정적 publish, /admin 라우트 |

| 인증 모듈 | `/srv/docker/wweb/backend/app/auth.py` | JWT, 구독 정보, max_shops 조인 |

| Caddy 관리 | `/srv/docker/wweb/backend/app/caddy.py` | 라우트 추가/삭제 (php_fastcgi) |

| Docker 제어 | `/srv/docker/wweb/backend/app/docker_ctrl.py` | 테넌트 디렉토리 + wp-config 생성 |

| WP 초기화 | `/srv/docker/wweb/backend/app/wp_init.py` | wweb-php 컨테이너에서 wp-cli 실행 |

| 피드 생성 | `/srv/docker/wweb/backend/app/feed.py` | 네이버 EP/쇼핑 XML, 사이트맵 |

| 관리자 HTML | `/srv/docker/wweb/backend/app/admin/index.html` | 관리자 6탭 UI |

| 관리자 JS | `/srv/docker/wweb/backend/app/admin/app.js` | 관리자 전용 로직 (showLoading) |

| 관리자 CSS | `/srv/docker/wweb/backend/app/admin/style.css` | 관리자 전용 스타일 |

| 빌더 JS | `/srv/docker/wweb/backend/app/builder/app.js` | GrapesJS 에디터 (getFullCss 포함) |

| 대시보드 HTML | `/srv/docker/wweb/backend/app/dashboard/index.html` | 대시보드 UI |

| 대시보드 JS | `/srv/docker/wweb/backend/app/dashboard/app.js` | 프로필/max_shops/주문 로직 |

| WP API 플러그인 | `/srv/docker/wweb/master/plugins/wweb-builder-api/` | 상품/페이지/PG/주문 API |

| Head CSS 플러그인 | `/srv/docker/wweb/master/plugins/wweb-head-css/` | _wweb_css → <style> 출력 |

| PHP-FPM Dockerfile | `/srv/docker/wweb/docker/Dockerfile.php-fpm` | PHP 8.3 + 확장 + dpkg 최적화 |

| PHP-FPM Pool 설정 | `/srv/docker/wweb/docker/php-fpm-pool.conf` | FPM 워커 풀 + memory_limit=512M |

| 백업 스크립트 | `/srv/docker/wweb/docker/backup.sh` | DB+정적+테넌트 자동 백업 |

| Caddy 설정 | `/srv/docker/caddy/Caddyfile` | 전체 리버스프록시 + /admin 라우트 |

| WP 코어 | `/srv/docker/wweb/wp-core/` | WordPress 파일 (RO 마운트) |

| 테넌트 디렉토리 | `/srv/docker/wweb/tenants/{slug}/` | wp-config.php + index.php |

| 정적 캐시 | `/srv/docker/wweb/static/{slug}/` | 홈페이지 HTML |

| DB 데이터 | Docker volume (6ebc5dcd73aa_wweb_db) | MariaDB 데이터 |

| API Dockerfile | `/srv/docker/wweb/backend/Dockerfile` | FastAPI 컨테이너 |

| 상품비교 문서 | `/srv/docker/wweb/docs/상품비교.md` | WWeb vs 아임웨비 기능 비교 |

| 프로젝트 README | `/srv/docker/wweb/README.md` | 프로젝트 전체 문서 |

| GitLab 저장소 | `https://gitlab.wellcoms.co.kr/jskim/wweb` | 소스코드 저장소 |


---


## 16. 작업 이력


| 날짜 | 작업 내용 |

|------|----------|

| 2026-05-28 | PHP-FPM 전환, 빌더 CSS 저장 버그 수정, 주문 API + 대시보드 주문 관리 UI |

| 2026-05-28 | 대시보드 통계 고도화 (7일 차트 + 상품별 TOP5) |

| 2026-05-29 | 관리자 API 14개 엔드포인트, 백업 자동화 (backup.sh + 크론) |

| 2026-05-29 | 커스텀 도메인 SSL 자동화 E2E 검증 |

| 2026-05-30 | 관리자 `/admin` 별도 페이지 분리 (HTML/JS/CSS/main.py 라우트/Caddyfile) |

| 2026-05-30 | 일반 대시보드에서 관리자 섹션/JS 완전 제거 |

| 2026-05-30 | 관리자 로딩 스피너 (showLoading + CSS animation) |

| 2026-05-30 | 대시보드 "내 정보" 프로필 탭 (구독/요금제 포함) |

| 2026-05-30 | DB subscription_started_at/expires_at 컬럼 추가 |

| 2026-05-30 | 쇼핑몰 생성 제한 (max_shops DB 기반, 프론트엔드 비활성화) |

| 2026-05-30 | esc() 누락 수정, loadOrdersDropdown 버그 수정 |

| 2026-05-30 | 전 요금제 max_shops=1 통일 |

| 2026-05-30 | 상품비교.md 작성 (WWeb vs 아임웨비 10카테고리) |

| 2026-05-30 | README.md 작성 + GitLab jskim/wweb 초기 푸시 |


댓글
자동등록방지
(자동등록방지 숫자를 입력해 주세요)