# 나노바나나 워커 ↔ 백엔드 RenderJob 계약 (v1) > 대상: `tools/nanobanana/worker.py`(Python 워커 사이드카) ↔ Spring Boot 백엔드(M5-1). > 근거: PLANNING §6(나노바나나 파이프라인 v1.2) · §8(아키텍처) · IMPLEMENTATION_BACKLOG S-4/M5-1~M5-3. > 이 문서는 **큐 메시지 형식**과 **완료 이벤트 형식**의 단일 출처다. 백엔드 M5 계약과 정합해야 한다. --- ## 1. 파이프라인 위치 ``` Spring Boot(M5-1) Redis Python 워커(S-4 / M5-2·M5-3) 오브젝트 스토리지 / WebSocket ───────────────── ─────── ───────────────────────── ────────────────────────── RenderJob 발행 ──LPUSH────▶ list: kintex:renderjobs ──BLPOP──▶ worker.process_job() │ scene → build_booth_prompt │ → render_shot (생성형) │ 또는 S6 → render_wiring_overlay_raster (래스터) │ → 이미지 + 메타데이터 ├──put──▶ object storage (이미지 + .meta.json 사이드카) └──PUBLISH──▶ channel: kintex:renderjob:events │ Spring 구독 ◀──완료 이벤트────────────────────────────────────────────────────────────┘ └─ WebSocket/STOMP 로 프론트에 진행·완료 푸시 ``` - 워커는 **상태 기계의 소비자**일 뿐, RenderJob 원본(상태·쿼터·캐시)의 소유는 백엔드(PostgreSQL RenderJob 테이블)다. - 워커는 결과 이미지·메타데이터를 오브젝트 스토리지에 적재하고 **완료 이벤트만** 발행한다. DB 상태 전이는 백엔드가 이벤트를 받아 수행한다. --- ## 2. 큐 메시지 — RenderJob (백엔드 → 워커) - 큐: Redis list `kintex:renderjobs` (env `NANOBANANA_QUEUE`). - 인입: `LPUSH` / 소비: `BLPOP`(워커, timeout 폴링). - 페이로드: UTF-8 JSON 1건. | 필드 | 타입 | 필수 | 설명 | |---|---|---|---| | `job_id` | string | ✔ | RenderJob PK(백엔드 발급). 이벤트·산출물 키의 상관 ID. | | `event_id` | string | | 행사 ID. 스토리지 경로·쿼터 스코프. | | `booth_id` | string | | 부스 ID. 스토리지 경로·상관. | | `shot_preset` | string | ✔ | `S1`~`S7` (PLANNING §6-3). `S6`은 래스터 합성 경로. | | `scene` | object | ✔* | PLANNING §6-2 scene 스키마(hall·booth·design·lighting·wiring). 생성형 샷 필수. | | `seed` | int | | 컷 간 일관성(B-12). 생략 시 참조체인 일관성 폴백. | | `reference_image` | string | | 참조 이미지 경로/URI(빈 부스 실측·간이 렌더). 구조 보존. 목 모드는 무시. | | `layers` | string[] | | 활성 레이어(`furniture`·`lighting`·`electrical`·`network`). 생략 시 샷별 기본값. | | `wiring` | object | ✔** | S6 전용. `scene.wiring` 미제공 시 사용. `{power,network,plumbing}[]`. | | `hall_dims_m` | [number,number] | | S6 좌표계 기준(m). 생략 시 booth.size_m → hall.dims_m 폴백. | | `options` | object | | `px_per_m`(S6), `kinds`(S6 렌더 레이어) 등 렌더 옵션. | `*` 생성형 샷(S1~S5,S7)은 `scene` 필수. `**` S6은 `scene.wiring` 또는 최상위 `wiring` 중 하나 필수. 예: ```json { "job_id": "rj_01H...", "event_id": "evt_2026_kes", "booth_id": "A-102", "shot_preset": "S2", "seed": 12345, "reference_image": "s3://kintex/refs/hall7_empty.jpg", "scene": { "hall": {"id": "제1전시장 7홀", "dims_m": [126, 90], "ceiling_m": 12}, "booth": {"id": "A-102", "size_m": [6, 3], "type": "independent"}, "design": {"signage": {"text": "주식회사 가디아"}, "brand_color": "#0052A5"}, "lighting": {"mode": "night", "color_temp_k": 4000}, "render_hints": {"style": "tech"} } } ``` --- ## 3. 완료 이벤트 (워커 → 백엔드) - 채널: Redis pub/sub `kintex:renderjob:events` (env `NANOBANANA_EVENT_CHANNEL`). - 발행: `PUBLISH`. 백엔드가 구독해 WebSocket/STOMP로 릴레이(§8). - 페이로드: UTF-8 JSON 1건. | 필드 | 타입 | 설명 | |---|---|---| | `type` | string | `renderjob.completed` \| `renderjob.failed`. | | `job_id` | string | 상관 ID. | | `event_id` / `booth_id` / `shot_preset` | string | 에코백. | | `status` | string | `DONE`(생성/합성 성공) \| `FAILED`. | | `image_ref` | object | 산출물 참조(§4). 실패 시 `null`. | | `meta` | object | 이미지 메타데이터(§5). 실패 시 부분. | | `error` | object | `{code, message}` — 실패 시. 스택트레이스·키 **미포함**. | | `emitted_at` | string(ISO-8601 UTC) | 이벤트 발행 시각. | - 에러 코드(친화적 분류, PLANNING §6-5): `AUTH`(키 무효) · `QUOTA`(429·쿼터) · `SAFETY` · `BAD_REQUEST` · `RENDER_ERROR`. - **쿼터는 성공(`DONE`) 시에만 차감** — 백엔드가 완료 이벤트 수신 시 처리(실패는 소모 안 함). --- ## 4. `image_ref` — 산출물 참조 ```json { "backend": "local" | "s3" | "gcs", "key": "evt_2026_kes/A-102/rj_01H..._S2.png", "uri": "file:///.../output/visualizations/evt_2026_kes/A-102/rj_01H..._S2.png", "sidecar_key": "evt_2026_kes/A-102/rj_01H..._S2.png.meta.json", "content_type": "image/png" } ``` - 사이드카 `.meta.json`은 이미지와 **항상 함께** 적재된다(결정적, B-03). - 기본 백엔드는 로컬 파일시스템(`NANOBANANA_OUTPUT_DIR`, degraded 오브젝트 스토리지). 운영 시 S3/GCS 어댑터로 교체. --- ## 5. `meta` — 이미지 메타데이터 (§6-6 워터마크·고지 필수) `client.build_metadata` 산출 + 워커 증강: | 필드 | 설명 | |---|---| | `generated_at` | 생성 시각(ISO-8601 UTC). | | `schema_hash` | scene 스키마 SHA-256(정렬 직렬화). **동일 해시 캐시 키**(§6-4). | | `model_version` | 모델명(`gemini-3.1-flash-image-preview` 등) 또는 `mock`. | | `shot_preset` | 샷 프리셋. | | `seed` | 시드(있으면). | | `render_path` | `generative` \| `backend_raster_composite`(S6). | | `watermark_required` | 항상 `true`. | | `watermark_text` | "AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있음". | | `notice` | "계약·심사 서류 사용 금지(도면만 유효)". | | `live` | `true`(실 Gemini 호출) \| `false`(목/degraded). | | `degraded` | `true`면 플레이스홀더(G1 미승인·목 모드). | --- ## 6. G1 게이트 — 실 Gemini 호출 조건 (PLANNING R12) | 조건 | 결과 | |---|---| | `NANOBANANA_LIVE=1` **그리고** `GEMINI_API_KEY` 존재 | **실 호출**(생성형 샷 → `NanoBananaClient.render_shot`). | | 그 외(기본) | **목/degraded 모드** — 플레이스홀더 이미지 + 정상 메타데이터. 파이프라인 구조는 성립. | - 목 모드는 키·네트워크 없이 동작한다. `GEMINI_API_KEY`는 **불필요**하며 어디에도 로그·기록하지 않는다. - **S6(래스터 합성)은 G1과 무관** — 항상 로컬 PIL 결정적 합성(생성형 아님). 목/live 모두 동일 경로. - 소유자 승인(G1) 확정 후 `NANOBANANA_LIVE=1` + 서버 env `GEMINI_API_KEY` 설정으로 무코드변경 전환. --- ## 7. 환경변수 | 변수 | 기본값 | 용도 | |---|---|---| | `NANOBANANA_LIVE` | (미설정=목) | `1`일 때만 실 Gemini 호출 시도(+키 필요). | | `GEMINI_API_KEY` | — | 실 호출 시에만. 코드·로그·이벤트·응답 기록 금지. | | `NANOBANANA_MODEL` | `gemini-3.1-flash-image-preview` | 모델 오버라이드(`client.MODEL_NAME`). | | `REDIS_URL` | `redis://localhost:6379/0` | 큐/이벤트 연결(지연 연결). | | `NANOBANANA_QUEUE` | `kintex:renderjobs` | RenderJob 큐 리스트 키. | | `NANOBANANA_EVENT_CHANNEL` | `kintex:renderjob:events` | 완료 이벤트 pub/sub 채널. | | `NANOBANANA_OUTPUT_DIR` | `output/visualizations` | 로컬 오브젝트 스토리지 루트. | --- ## 8. 운영 불변식 1. **지연 연결**: Redis 미기동이어도 `import`·`process_job(job)` 직접 호출은 성립(스모크·단위 테스트용). 2. **결정적 사이드카**: 모든 산출물에 `.meta.json` 동반(§5). 3. **성공 시에만 쿼터 차감**: 실패 이벤트는 소모 신호 아님. 4. **비밀 미노출**: 이벤트·로그·에러에 키·IP·스택트레이스 금지(§6-5, GUARDiA 보안 제약). 5. **S6 결정성**: 배선 오버레이는 생성 모델 미개입(좌표 정합 목적).