Files
web-page-backend/docs/superpowers/specs/2026-05-18-nas-windows-distributed-architecture-design.md
gahusb 90f6af6ab3 docs(arch): NAS↔Windows 분산 아키텍처 통합 design spec
박재오 7결정 + Obsidian 3개 문서(7결정 통합/API 부하/역할 분담)를
실행 가능한 형태로 정리.

12개 SP 분할 (Track A Quick Win 2건 + Track B Infrastructure 10건),
의존성 그래프, 시간대 조건부 우선순위(평일 비휴장일만 트레이딩 HIGH),
Windows Render Worker 통합 패턴 (인스타·음악·영상 셋이 같은 구조),
Redis 큐 컨벤션, SMB direct write + NAS internal webhook,
X-WebAI-Key / X-Internal-Key 분리, 3-layer 차단(IP 화이트리스트 +
Tailscale + 헤더), Suno+영상 API 키 Windows 이전 명세.

첫 plan 대상: Track A (SP-A1 web-ai 캐시 TTL + SP-A2 NAS stock
TTLCache, ~40분 작업, V2 재시작 시 NAS 인바운드 70% 감소).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:24:37 +09:00

585 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NAS ↔ Windows 분산 아키텍처 — Design Spec
**Date:** 2026-05-18
**Author:** CEO (with Claude)
**Scope:** `web-backend` + `web-ai` + 신규 `web-ai-services` (Windows WSL2 컨테이너 모음)
---
## 1. 배경 & 목적
NAS Synology J4025 (Celeron 2C/2.0GHz, 18GB)에서 11개 docker 컨테이너가 CPU를 과점유. 진단 결과 가장 큰 원인은 **외부 인바운드 API 호출 빈도** (web-ai signal_v1/v2가 분당 12회 NAS stock 호출) + **insta-lab Playwright Chromium의 동시 launch 비용**이었다.
박재오 통찰: *"NAS = 24/7 표출·게이트웨이 / Windows = 트레이딩 메인 + 트리거 기반 컴퓨팅"*. 박재오가 이미 7건의 의사결정을 마쳤고 1주 셋업 가이드도 정리되어 있다 (`Obsidian Vault/raw/2026-05-18-Windows-NAS-아키텍처-7결정-통합.md`).
본 spec은 그 위에 **실행 단위 분할(SP) + 의존성 그래프 + 통합 패턴 + 데이터 플로우**를 정리해서 실제 구현 plan으로 진입 가능한 형태로 만든다.
### 박재오 7결정 (수용된 결정 사항)
1. **d+b 조합** — Windows 작업 감지 큐 정지 + 트레이딩 우선순위 High
2. **insta-lab Playwright 1순위** 이전 (NAS → Windows)
3. **트리거 B(비동기) + C(예약)** — 즉시 응답 X, task_id 발급 + 폴링
4. **외부 영상 생성 도구** (Runway·Sora·Veo·Pika·Kling·Luma)
5. **Redis NAS 컨테이너** — 24/7 안정 큐
6. **옵션 4 하이브리드** — 트레이딩 Native Python / 신규 WSL2 Docker Engine
7. **NSSM** — Windows Service 도구 (자동 시작·우선순위)
---
## 2. 전체 아키텍처
```
[사용자 브라우저]
↓ HTTPS
[NAS Synology J4025] ─── 24/7 안정 · 표출 · 게이트웨이 · 상태(state)
├─ frontend (nginx :8080) React SPA
├─ redis (:6379) ⭐ NEW 24/7 큐 + 캐시
├─ stock (:18500) +TTLCache 메타 + KIS data + WebAI gateway
├─ insta-lab (:18700) 분할 후 카피 생성 + DB + Redis push
├─ music-lab (:18600) 분할 후 메타 + Redis push (Suno/MusicGen 미실행)
├─ video-lab (:18XXX) ⭐ NEW 영상 게이트웨이 + Redis push
├─ agent-office (:18900) 텔레그램 라우팅 + scheduler
├─ lotto / realestate-lab / personal / packs-lab / travel-proxy
└─ deployer (:19010)
↓ Redis BLPOP / 직접 HTTP webhook
[Windows AI Server 192.168.45.59] ─── 트레이딩 최우선 · 트리거 컴퓨팅
├─ 🔵 Native Python (NSSM HIGH priority)
│ ├─ signal_v2 (:8001) ⭐ 트레이딩 절대 우선
│ ├─ Ollama qwen3:14b (:11435)
│ └─ MusicGen (:8765)
└─ 🟢 WSL2 + Docker Engine (NORMAL priority)
├─ insta-render (:18710) ⭐ NEW Playwright Chromium pool
├─ music-render (:18711) ⭐ NEW Suno API + MusicGen orchestration
├─ video-render (:18712) ⭐ NEW 외부 영상 API gateway (6 provider)
└─ task-watcher 박재오 작업 감지 + 시간대 분기
```
### 핵심 원칙
1. **NAS = state(DB) + view(nginx 미디어 서빙)**, **Windows = stateless compute**
2. **트레이딩 절대 우선** — 시간대 조건부 (아래 §3 참조)
3. **무거운 작업 시간대 분리** — 데드존 23:3004:30 + 주말·휴장일 = 골든타임
---
## 3. 시간대별 우선순위 모드
| 모드 | 조건 | signal_v2 | task-watcher 정책 |
|------|------|-----------|------------------|
| 🔴 트레이딩 | 평일 비휴장일 07:0016:30 | NSSM HIGH, polling 활성 | 박재오 활동 감지 시 `queue:paused` SET |
| 🟡 일반 | 평일 16:3023:30 (NXT) | NSSM HIGH 유지 (5분 폴링 가벼움) | 박재오 활동 감지 시 SET |
| 🟢 자유 | 주말·휴장일 + 평일 23:3004:30 | 자동 idle (휴장일 polling 미실행) | `queue:paused` DEL 유지 — 큐 항상 활성 |
### 구현 위치
- **signal_v2의 휴장일 인식**: `web-ai` CHECK_POINT #7 `holidays.json` 자동 동기화 항목. 휴장일·주말에 polling 자체 미실행.
- **휴장일 단일 소스**: `web-backend/stock/app/holidays.json` 정본. NAS stock이 `GET /api/stock/holidays`로 노출. signal_v2 + task-watcher가 매일 00:00 갱신.
- **task-watcher 시간대 분기**: `current_mode()` 함수가 30초 폴링마다 모드 판정 → `queue:paused` 토글.
---
## 4. Sub-project 카탈로그 (12개)
| SP | 명칭 | 트랙 | 위치 | 소요 |
|----|------|------|------|------|
| **SP-A1** | web-ai 캐시 TTL 증가 | A | `web-ai/signal_v2/stock_client.py` | 10분 |
| **SP-A2** | NAS stock TTLCache | A | `web-backend/stock/app/*` | 30분 |
| **SP-1** | NAS Redis 컨테이너 | B (Base) | `web-backend/docker-compose.yml` | 30분 |
| **SP-2** | Windows WSL2 + Docker Engine | B (Base) | (Windows AI) | 2h |
| **SP-3** | insta-render Windows 서비스 | B | `web-ai-services/insta-render/` (신규) | 4h |
| **SP-4** | NAS insta-lab 분할 | B | `web-backend/insta-lab` | 2h |
| **SP-5** | music-render Windows 서비스 | B | `web-ai-services/music-render/` (신규) | 3h |
| **SP-6** | NAS music-lab 분할 | B | `web-backend/music-lab` | 2h |
| **SP-7** | video-render Windows 서비스 | B | `web-ai-services/video-render/` (신규) | 3h |
| **SP-8** | NAS video-lab 신설 | B | `web-backend/video-lab/` (신규 컨테이너) | 2h |
| **SP-9** | NSSM 자동 시작 + 우선순위 | B | (Windows) | 1h |
| **SP-10** | task-watcher (시간대 + 활동 감지) | B | `web-ai-services/task-watcher/` (신규) | 2h |
**총 작업시간**: ~22.5h (1주 일정에 부합)
### 의존성 그래프
```
A 트랙 (병행, ~40분)
SP-A1 ─╮
├── V2 재시작 시 효과
SP-A2 ─╯
B 트랙 Base (Day 1~2)
SP-1 (Redis) ─┐
├── 인스타·음악·영상 3 트랙 모두 의존
SP-2 (WSL2) ──┘
인스타 트랙 (Day 3~4)
SP-3 (insta-render) ──→ SP-4 (NAS insta-lab 분할)
음악 트랙 (Day 4~5)
SP-5 (music-render) ──→ SP-6 (NAS music-lab 분할)
영상 트랙 (Day 5~6)
SP-7 (video-render) ──→ SP-8 (NAS video-lab 신설)
인프라 마무리 (Day 6~7)
SP-9 (NSSM) ──→ SP-10 (task-watcher)
```
### Critical Path
`SP-1 ∥ SP-2``SP-3``SP-4``SP-9``SP-10` (최단 약 11.5h)
병렬화: SP-1(NAS)·SP-2(Windows)는 다른 머신이라 동시 진행. 인스타·음악·영상 트랙은 패턴이 같아 한 번 정착 후 빠르게 복제.
---
## 5. 통합 패턴 — "Windows Render Worker"
인스타·음악·영상 3 트랙이 **완전히 같은 패턴**. 한 번만 정의하고 3번 재사용한다.
### 시퀀스
```
사용자 ─POST /api/{kind}/generate ...──→ NAS {kind}-lab
├─ DB.create_task() → task_id
├─ Redis RPUSH queue:{kind}-render {task_id, params, ...}
└─ 200 {task_id} ─→ 사용자
[Windows {kind}-render]
│ (queue:paused 체크 후 BLPOP queue:{kind}-render)
├─ POST /api/internal/{kind}/update
│ {status: "processing", progress: 30} ─→ NAS DB update
├─ 무거운 작업 (Playwright / Suno / Runway 등)
│ 결과 파일 → /mnt/nas/data/{kind}/{id}/{file} (SMB direct write)
└─ POST /api/internal/{kind}/update
{status: "succeeded", progress: 100,
result_path: "/media/{kind}/{id}/{file}"} ─→ NAS DB update
사용자 ─GET /api/{kind}/tasks/{task_id}──→ NAS {kind}-lab
└─ DB.get_task() → {status, progress, result_path}
─→ 사용자 (폴링)
```
### 4가지 미세 개선 (반영됨)
1. **결과물 저장**: SMB direct write (`/mnt/nas/data/`) — 별도 HTTP upload 단계 제거
2. **NAS 알림**: Windows → NAS internal webhook (`POST /api/internal/{kind}/update`) — NAS polling 부담 0
3. **사용자 응답**: 폴링 유지 (YAGNI, 미래 SSE 검토)
4. **인증 키 분리**: `X-WebAI-Key`(read, web-ai→NAS) vs `X-Internal-Key`(write, Windows→NAS)
---
## 6. Redis 키 컨벤션
| 키 | 종류 | TTL | 용도 |
|----|------|-----|------|
| `queue:insta-render` | list | (없음) | 인스타 카드 렌더 작업 큐 |
| `queue:music-render` | list | (없음) | 음악 생성 작업 큐 |
| `queue:video-render` | list | (없음) | 영상 생성 작업 큐 |
| `queue:paused` | string `"1"` | 600s | task-watcher가 set/del. worker가 BLPOP 전 확인 |
| (옵션) `cache:stock:*` | string (json) | 120~600s | NAS stock Redis 캐시 (SP-A2와 별개 옵션) |
### 큐 payload 표준 (JSON)
```json
{
"task_id": "uuid-...",
"kind": "insta|music|video",
"params": { ... },
"submitted_at": "2026-05-18T08:30:00+09:00"
}
```
Worker는 `BLPOP queue:{kind}-render` (1초 timeout) → `queue:paused` 체크 → 처리.
---
## 7. NAS 볼륨 레이아웃 + nginx 서빙
### 실 파일 시스템
```
/volume1/docker/webpage/data/
├── insta/{slate_id}/01.png ~ 10.png
├── music/{track_id}/{file}.mp3
└── video/{video_id}/{file}.mp4
```
### WSL2 마운트
```bash
# WSL2 /etc/fstab
//gahusb.synology.me/docker/webpage/data /mnt/nas cifs username=...,vers=3.0,uid=1000,_netdev 0 0
```
### nginx 서빙
```
https://gahusb.synology.me/media/insta/{id}/01.png
/music/{id}/...
/video/{id}/...
```
→ nginx `location /media/` 블록은 `/volume1/docker/webpage/data/`를 alias로 서빙 (기존 패턴).
---
## 8. NAS internal webhook 명세
### Endpoint
`POST /api/internal/{kind}/update` (kind ∈ `insta`|`music`|`video`)
### 인증 — 3-layer 차단
1. **nginx IP 화이트리스트** (Layer 1·2):
```nginx
location /api/internal/ {
allow 192.168.45.0/24; # LAN 화이트리스트
allow 100.64.0.0/10; # Tailscale CGNAT 대역
deny all;
...
}
```
2. **`X-Internal-Key` 헤더 검증** (Layer 3): `verify_internal_key` dependency
### Payload
```json
{
"task_id": "uuid-...",
"status": "processing|succeeded|failed",
"progress": 0-100,
"result_path": "/media/insta/123/01.png", // succeeded일 때만, nginx 경로
"error": "exception message" // failed일 때만
}
```
### NAS 측 처리
1. `tasks` 테이블 row update (status, progress, result_path, error)
2. (옵션) Redis PUBLISH `task:{id}` — 미래 SSE 통합 시 활용
3. 200 응답 (또는 401 if invalid key)
### 인증 키 정책
| 키 | 방향 | 권한 | 위치 |
|----|------|------|------|
| `X-WebAI-Key` | web-ai → NAS | read-only (`GET /api/webai/*`) | NAS `.env` + web-ai `.env` |
| `X-Internal-Key` | Windows worker → NAS | write-only (`POST /api/internal/*`) | NAS `.env` + Windows `.env` |
분리 사유: Principle of Least Privilege, 독립 로테이션, 감사 로그 명확성.
### 인증 helper (NAS 공통 모듈, `web-backend/_shared/auth.py` 또는 각 컨테이너 복제)
```python
from fastapi import Header, HTTPException
import os
async def verify_internal_key(x_internal_key: str = Header(...)):
expected = os.getenv("INTERNAL_API_KEY")
if not expected or x_internal_key != expected:
raise HTTPException(401, "Invalid X-Internal-Key")
# 라우터 사용
@app.post("/api/internal/insta/update", dependencies=[Depends(verify_internal_key)])
async def insta_update(payload: InternalUpdate): ...
```
기존 `verify_webai_key` 패턴(메모리 `reference_webai_auth_pattern.md`)을 복제.
---
## 9. Suno + 외부 영상 API 키 이전
NAS `.env`에서 다음 키들을 **제거** → Windows `.env`로 이전:
| 키 | NAS 이전 | Windows 이후 |
|-----|---------|-------------|
| `SUNO_API_KEY` | music-lab | music-render |
| `RUNWAY_API_KEY` | (없음) | video-render |
| `OPENAI_API_KEY` (Sora) | (있을 수도) | video-render |
| `GEMINI_API_KEY` (Veo) | (없음) | video-render |
| `PIKA_API_KEY` / `KLING_API_KEY` / `LUMA_API_KEY` | (없음) | video-render |
→ NAS music-lab + video-lab은 외부 API 호출 코드를 가지지 않음. Redis push만.
---
## 10. SP 상세 명세
### SP-A1 — web-ai 캐시 TTL 증가 (10분)
**파일**: `web-ai/signal_v2/stock_client.py`
변경:
```python
# 변경 전
PORTFOLIO_TTL = 60
NEWS_TTL = 300
SCREENER_TTL = 60
# 변경 후
PORTFOLIO_TTL = 180 # 3분
NEWS_TTL = 600 # 10분
SCREENER_TTL = 300 # 5분
```
**효과**: 분당 12 → 3~4 호출 (~70% 감소), 캐시 hit ratio 0~50% → 66~80%
### SP-A2 — NAS stock TTLCache (30분)
**파일**: `web-backend/stock/app/*` (webai endpoint 위치 확인 후)
```python
from cachetools import TTLCache
_PORTFOLIO_CACHE = TTLCache(maxsize=1, ttl=120)
_NEWS_CACHE = TTLCache(maxsize=10, ttl=600)
_SCREENER_CACHE = TTLCache(maxsize=10, ttl=180)
@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
async def portfolio():
if "result" in _PORTFOLIO_CACHE:
return _PORTFOLIO_CACHE["result"]
result = await compute_portfolio()
_PORTFOLIO_CACHE["result"] = result
return result
```
3 endpoint 적용: `/api/webai/portfolio` · `/api/webai/news-sentiment` · `/api/stock/screener/run`. `cachetools` 의존성 requirements.txt 확인.
**효과**: V1+V2 동시 호출도 NAS에서 1회 계산. KIS·LLM 재호출 방지.
### SP-1 — NAS Redis 컨테이너 (30분)
**파일**: `web-backend/docker-compose.yml`에 추가
```yaml
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- ${RUNTIME_PATH}/redis-data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 60s
timeout: 5s
retries: 3
```
검증: `docker exec redis redis-cli PING` → `PONG`
### SP-2 — Windows WSL2 + Docker Engine + Tailscale (2h)
박재오 Windows AI Server에서 (관리자 PowerShell):
```powershell
wsl --install -d Ubuntu-22.04
# 재부팅 후
wsl -d Ubuntu-22.04
```
WSL2 안:
```bash
# Docker Engine
sudo apt update && sudo apt install -y ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
# Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# NAS SMB mount
sudo mkdir -p /mnt/nas
echo "//gahusb.synology.me/docker/webpage/data /mnt/nas cifs username=...,vers=3.0,uid=1000,_netdev 0 0" | sudo tee -a /etc/fstab
sudo mount -a
```
검증: `docker ps`, `tailscale status`, `ls /mnt/nas`
### SP-3 — insta-render Windows 서비스 (4h)
**디렉토리**: `C:\Users\jaeoh\Desktop\workspace\web-ai-services\insta-render\`
```
insta-render/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env
├── main.py
├── worker.py
└── card_renderer.py # 기존 NAS insta-lab/app/card_renderer.py 이식
```
핵심 로직:
- `worker.py`: Redis BLPOP `queue:insta-render` (paused 체크)
- `card_renderer.py`: Browser pool (`init_browser`/`shutdown_browser`) + `render_slate`
- `main.py`: 시작 시 browser init + worker async task spawn
- 완료 시 `/mnt/nas/data/insta/{slate_id}/` 저장 + NAS webhook `POST /api/internal/insta/update`
### SP-4 — NAS insta-lab 분할 (2h)
**파일**: `web-backend/insta-lab/app/main.py` + `app/card_renderer.py`
변경:
```python
# 변경 전 — NAS에서 직접 렌더
async def _bg_render(task_id: str, slate_id: int):
async with RENDER_SEMAPHORE:
await card_renderer.render_slate(slate_id, ...)
# 변경 후 — Redis 큐에 push만
import redis.asyncio as aioredis
redis_client = aioredis.from_url(os.getenv("REDIS_URL", "redis://redis:6379"))
async def _bg_render(task_id: str, slate_id: int):
payload = {"task_id": task_id, "kind": "insta",
"params": {"slate_id": slate_id, "theme": "hedgy75"},
"submitted_at": datetime.now(KST).isoformat()}
await redis_client.rpush("queue:insta-render", json.dumps(payload))
```
추가: `POST /api/internal/insta/update` endpoint (Windows webhook 수신).
삭제: `card_renderer.py` Playwright 코드 (Browser pool, Semaphore 등), `requirements.txt`에서 `playwright` 제거, Dockerfile에서 Chromium install 제거.
### SP-5 — music-render Windows 서비스 (3h)
**디렉토리**: `web-ai-services/music-render/`
- Suno API client (외부 SaaS, polling 1~5분)
- MusicGen local call (Windows localhost:8765)
- Redis BLPOP `queue:music-render`
- 결과 mp3 → `/mnt/nas/data/music/{track_id}/{file}.mp3`
- NAS webhook `POST /api/internal/music/update`
`SUNO_API_KEY` Windows `.env`에 단독 보관.
### SP-6 — NAS music-lab 분할 (2h)
Suno 호출 코드 + MusicGen 호출 코드 삭제. `_bg_generate` 함수를 Redis push로 변경. `POST /api/internal/music/update` endpoint 추가.
### SP-7 — video-render Windows 서비스 (3h)
**디렉토리**: `web-ai-services/video-render/`
6 provider gateway (Runway·Sora·Veo·Pika·Kling·Luma) — provider 선택은 payload에서. 각 외부 API 호출 + 결과 mp4 다운로드 → `/mnt/nas/data/video/{id}/`. NAS webhook.
### SP-8 — NAS video-lab 신설 (2h)
새 docker 컨테이너. `web-backend/video-lab/`:
- `app/main.py`: 2 endpoint
- `POST /api/video/generate` → Redis push `queue:video-render` + task_id 반환
- `GET /api/video/tasks/{id}` → DB 조회
- `app/db.py`: video_tasks 테이블 (sqlite)
- `POST /api/internal/video/update` (Windows webhook)
- Dockerfile, requirements, docker-compose.yml entry
매우 가벼움 (NAS CPU 부담 미미).
### SP-9 — NSSM 자동 시작 + 우선순위 (1h)
Windows AI에서 NSSM 다운로드 후:
```powershell
# 트레이딩 (Native, HIGH)
nssm install signal_v2 "C:\Python312\python.exe" "-m uvicorn main:app --host 0.0.0.0 --port 8001"
nssm set signal_v2 AppDirectory "C:\Users\jaeoh\Desktop\workspace\web-ai\signal_v2"
nssm set signal_v2 Priority HIGH_PRIORITY_CLASS
nssm set signal_v2 AppStartup AUTO
# WSL2 Docker (NORMAL)
nssm install wsl_docker "wsl" "-d Ubuntu-22.04 -- sudo service docker start && cd /workspace/web-ai-services && docker compose up -d"
nssm set wsl_docker Priority NORMAL_PRIORITY_CLASS
nssm set wsl_docker AppStartup AUTO
nssm start signal_v2
nssm start wsl_docker
```
### SP-10 — task-watcher (2h)
**디렉토리**: `web-ai-services/task-watcher/`
WSL2 Docker 컨테이너. 30초마다:
1. `current_mode()` 판정 (시간대 + holidays.json 체크 + KST 시각)
2. `is_user_active()` 판정 (마우스/키보드 idle < 5분 또는 게임 process 감지)
3. 모드 + 활동 → `queue:paused` 토글
- `mode == "free"``DEL queue:paused`
- `mode != "free" and active``SET queue:paused 1 EX 600`
- `mode != "free" and idle``DEL queue:paused`
---
## 11. 데이터 플로우 검증 — 인스타 사례 end-to-end
```
1. 사용자 클릭 "카드 생성"
POST /api/insta/slates/123/render
↓ NAS insta-lab
2. NAS insta-lab
- db.create_task("slate_render", {slate_id: 123}) → task_id="t-abc"
- redis.rpush("queue:insta-render", {task_id: "t-abc", kind: "insta", params: {slate_id: 123, theme: "hedgy75"}})
- 응답 {task_id: "t-abc"}
↓ 즉시 사용자
3. Windows insta-render worker
- redis.blpop("queue:insta-render", 1)
- paused 체크 → 통과
- webhook(processing, 10%) → NAS DB update
- Playwright 카드 10장 렌더 → /mnt/nas/data/insta/123/01.png..10.png
- webhook(processing, 90%) 진행률 보고
- webhook(succeeded, 100, result_path="/media/insta/123/01.png") → NAS DB update
4. 사용자 폴링
GET /api/insta/tasks/t-abc → {status: "succeeded", result_path: "/media/insta/123/01.png"}
브라우저에서 <img src="/media/insta/123/01.png" /> 렌더
```
---
## 12. Out of Scope
- V1/V2 재시작 결정 (사용자 보류, 두 process 정지 유지)
- NAS 하드웨어 업그레이드 (#12 보류)
- 컨테이너 리소스 제한 cpus 0.5 (#11 박재오 진행 금지)
- SSE/WS push 모델 (YAGNI, 폴링 유지)
- Grafana 모니터링 (NAS 자산 활용 옵션, 향후)
## 13. 위험 요소
| 위험 | 완화 |
|------|------|
| Windows 재부팅 시 worker 중단 | NSSM AppStartup AUTO + WSL2 자동 시작 (SP-9) |
| Windows ↔ NAS 네트워크 단절 | task가 큐에 남음, NAS 측 timeout 처리 (예: 30분 timeout → failed) |
| 박재오 게임·작업 중 worker 충돌 | task-watcher queue:paused (SP-10) + NORMAL priority |
| Suno API rate limit | music-render 내부에서 retry + 큐 직렬 처리 |
| SMB 마운트 실패 | WSL2 부팅 시 `mount -a`, 실패 시 alarm (로그) |
| Redis 다운 | docker restart unless-stopped + healthcheck. 다운 시 모든 worker idle (NAS는 응답 계속) |
| 키 노출 | 3-layer 차단 (IP 화이트리스트 + nginx + X-Internal-Key) |
## 14. 첫 plan 작성 대상
**옵션 A — Track A만 (사용자 선택 확정)**:
- SP-A1: web-ai 캐시 TTL 증가 (10분)
- SP-A2: NAS stock TTLCache (30분)
이 plan은 즉시 NAS CPU 70% 감소 효과 (V2 재시작 시). Track B는 별도 spec/plan으로 차후 진행.
차후 plan 작성 순서 권장:
1. **Plan-A (이번)** — SP-A1 + SP-A2
2. **Plan-B-Base** — SP-1 + SP-2
3. **Plan-B-Insta** — SP-3 + SP-4 (1순위 패턴 정착)
4. **Plan-B-Music** — SP-5 + SP-6
5. **Plan-B-Video** — SP-7 + SP-8
6. **Plan-B-Infra** — SP-9 + SP-10
## 15. 참고
- 박재오 7결정 통합: `Obsidian Vault/raw/2026-05-18-Windows-NAS-아키텍처-7결정-통합.md`
- API 부하 해결: `Obsidian Vault/raw/2026-05-18-NAS-Window-AI-API-부하-해결방안.md`
- 역할 분담 최적화: `Obsidian Vault/raw/2026-05-18-NAS-Windows-역할-분담-최적화.md`
- web-backend CHECK_POINT.md (즉시·중기·장기 + 7결정 매핑)
- web-ai CHECK_POINT.md (Phase 진행도)
- 기존 인증 패턴: 메모리 `reference_webai_auth_pattern.md`