# 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 초기 푸시 |
| 이전 | | 김재석 | 2026-05-30 |
|---|---|---|---|
| 다음 | | 김재석 | 2026-05-29 |