86.8 KB · 수정 2026-10-14 00:32
목차

PNCAD AI 연출 렌더 설계 (design-ai-staging) — v2 무료 엔진 · 미니PC 워커


0. 페페 결정

0-1. 받은 답 (26.10.13 18:3x) → 설계 반영

Q 답 반영
Q1 실행 위치 ① 별도 워커 uwsgi 스레드 안 씀. 지금 워커 = 미니PC 상주(무료 웹 엔진이 미니PC 에서만 돈다). 서버 systemd 워커는 유료 API 를 켤 때(§13)
Q2 OpenAI 키 ② 기존 키를 C:\dev\.secrets\.env 로 U0 에서 «추가만»(값 출력 0). 사본(C:\dev\SARA\.env·C:\dev\tools\agent-dialogue-hub\.env) 삭제는 승인 뒤
Q3 · Q4 결제·하드 리밋 추후 보류 · 추가 요금 0 유료 API 호출 금지 · API 어댑터 = 코드만 · 서버 월 예산 0 → 계획 단계에서 API 슬롯을 안 만든다

0-2. 새로 필요한 결정 (선택지 + 추천)

# 질문 선택지 코디 추천
Q5 웹 엔진 «학습 사용» 설정 — 고객 가구 렌더가 ChatGPT·Gemini 소비자 웹에 올라간다 ① ChatGPT «모두를 위한 모델 개선» 끔 + Gemini «앱 활동» 끔 ② ChatGPT 만 끔(Gemini 대화 기록 유지) ③ 둘 다 그대로 ① — 단 Gemini «앱 활동»을 끄면 delix0731 계정의 Gemini 대화 기록이 72시간만 남는다(페페 개인 사용 영향). 싫으면 ②
Q6 엔진별 «전체» 하루 상한(구독 한도·계정 위험 관리) ChatGPT 20장 · 나노바나나 30장(전 사용자 합, KST 하루) / 다른 값 제안값. ChatGPT Plus ≈ 24시간 90~100장을 카드 굽기(whymycat·ghostdex)와 같은 계정으로 나눠 쓴다
Q7 웹 자동화 계정 위험 대응(코덱스 2차: «무인 상주 서비스의 안정 기반으로 보기 어렵다» — OpenAI 약관은 자동 출력 추출 금지, Google 은 반복 위반 시 제품·구글 계정 제한 가능) ① 그대로 — delix0731 로 ChatGPT·나노바나나 무인 자동화(상한·회로 차단만) ② 나노바나나만 별도 무료 구글 계정(페페가 만들고 1회 로그인 — Gmail 등 주 계정 보호) ③ 집PC RTX 4070 로컬 모델 PoC 병행(Qwen-Image-Edit 등 · 0원 · 약관 무관 · 데이터 외부 전송 0 · 마스크 인페인팅으로 형상 보존 강함 · 대신 집PC 깨우기·초기 구축) ④ 반자동 — 사람이 웹에서 생성·저장, 워커는 합성·업로드만 ②+③ — 당장은 웹 자동화로 시작하되 구글 쪽은 계정을 격리하고, 무인 운영의 본선을 로컬 모델로 옮길지 PoC(U-L)로 판단
Q8 하단 고정 문구(A1) 위치 — 요청서는 «하단에 박힌다» 까지만 정했다 ① 그림 아래 56px 어두운 띠를 덧붙임(그림 1536×1024 무가림 · 최종 PNG 1536×1080 · 띠 #1f1f24 · 흰 글자 28px) ② 그림 안 하단을 덮음(1536×1024 유지 · 하단 약 5.5% 가림 → 3D 틀에 «문구 자리» 빗금 표시 필요) ① — 가구·배경을 하나도 안 덮어 «가구 100% 보존» 주장과 충돌 0(web-designer 권고)

이의 있을 때만 바꾸는 기본값(코디 판단): ① 1회 2장 = ChatGPT 1 + 나노바나나 1(엔진 비교 내장 · 한쪽이 한도·로그인 풀림이면 다른 쪽 2장) ② 대기 요청은 12시간 지나면 만료(장수 반환) ③ 웹 엔진은 전용 CDP 크롬(빈 포트 · delix0731 프로필 «복사» = 로그인 그대로 · 사람 클릭 0)에서 돈다 — 9334(페페 수동)와 분리 ④ B2 기본 엔진은 실사용 결과(1+1)와 블라인드 투표(A·B 엔진을 요청마다 무작위 배정 · 일반 사용자 화면엔 엔진명 없음 · §8)를 보고 나중에 ⑤ 1인 하루 20장(B1) 유지 — web-designer 는 공용 50장(≈25회) 대비 10~12장 시작을 제안(3명이면 하루치 소진) · env 한 줄이라 쓰는 양을 보고 조정 ⑥ 사용자당 진행(대기 포함) 1건.


1. 목표 · 확정 결정 · 코드 사실

1-1. 목표

몸통·도어만 있는 PNCAD 장면을 «방 안에 놓인 실사 사진»처럼 만든다. 벽·바닥·창·소품만 AI 가 그리고, 가구는 PNCAD 렌더 픽셀을 그대로 다시 덮는다(형상·치수·색 100% 보존). 고객 제출용. 추가 비용 0.

1-2. 확정 결정

항목 결정
A1 고객 제출 허용 + 하단 고정 문구 「배경·소품은 AI로 연출한 이미지로, 실제 현장과 다를 수 있습니다」
A2 가구 픽셀 보존 합성 — 가구 + 회색 껍데기 렌더 → AI 는 껍데기만 → 가구 원본 픽셀 재합성으로 보장(엔진 마스크·지시 불신) · 가구 앞 가림 소품 금지
A3 → v2 ~~API 2종~~ → ChatGPT 구독 웹 + 나노바나나 무료 웹UI(미니PC 워커 큐) · API 어댑터 = 코드만·꺼짐
A4 프리셋 5~6 + 칩(벽 · 바닥 · 창 · 낮/저녁 · 소품) + 한 줄 메모 · 창 칩 = 3D 광원 방향도
A5 · A6 Claude 미사용(템플릿 프롬프트) · 그림자 v3 와 병행(photo.js 그림자 구간 불가침, 의존만)
B1 → v2 1인 하루 20장 · 1회 2장 유지. 월 10만원 상한·80% 알림·100% 차단은 유료 엔진을 켤 때 적용(지금 비용 0)
B2 기본 엔진 = 실사용 결과 보고 페페
기본값 비동기 + 진행·취소 · PNG · 서버 기록 30일 · 테스트(8001) 먼저 · 운영 = 페페 채팅 승인

1-3. 코드 사실(직접 확인)

사실 근거
🔴 PFS 는 Django 가 아니라 Flask 1.0.2 + flask-restplus · uwsgi processes=5(스레드 꺼짐) · EC2 Python 3.6.9 · requests 2.21 · Pillow 8.4(numpy 없음) · 배포 = systemctl restart C:\dev\pfs\requirements.txt · C:\dev\pfs\service\uwsgi\pfs-live.ini · C:\dev\pfs\scripts\deploy-*.sh
nginx client_max_body_size 20m · uwsgi_read_timeout 기본 60초 → 요청 안에서 생성 대기 불가 = 비동기 필수 pfs rules L1486
PNCAD 서빙 /production/pncad/<asset>(@login_required) · API /api/pncad 네임스페이스 · DAO 의 ensure_table()(DDL 오류 삼킴) C:\dev\pfs\web\pfs\default\controller\default_controller.py L84-94 · C:\dev\pfs\api\config.py L66-132 · C:\dev\pfs\api\pfs\pncad\dao\pncad_scene_dao.py L52
/api 무세션 401 · 외부 계정 = api_gate fail-closed(새 엔드포인트 자동 차단) · 상주 데몬 = _AGENT_PATHS+공유 토큰 X-PFS-Agent-Token · PC별 폐기 가능한 이름 토큰(TREAD: env PFS_TREAD_TOKENS = 「이름:sha256」, 정해진 경로만) C:\dev\pfs\web\__init__.py L178-220
선례 = 미니PC 상주 워커 C:\dev\tools\worklog-ai\worker.py — 1분 주기 숨김 예약작업(\Cody\WorklogAI-prod/test · wscript → pythonw) · 파일 락 lease · todo/result 두 API · 키는 미니PC .secrets 에만 · 10분 연속 실패 시 텔레그램 하루 1회 C:\dev\tools\worklog-ai\worker.py 머리 · C:\dev\tools\winledger\WINDOWS.md W114·W115
ChatGPT 웹 엔진 = C:\dev\tools\imagegen\chatgptimg.py(CDP 크롬 탭 · 26.8.29 「앱을 안 건드리는 새 경로」) — gptapp.py image 는 ChatGPT «앱»을 포트 9350 으로 모는 옛 경로라 앱이 디버그 포트 없이 떠 있으면 실패(페페 앱 죽이기 금지) · 워터마크 없음 · 참조 첨부 시 «편집 파이프라인» ≈ 5분(타임아웃 900초) · 한도·거절은 말로 온다(문구 판정) · 참조 첨부 시 «캐릭터 참조» 문구를 자동으로 붙인다(우리 용도엔 틀림 → 편집 모드 필요) C:\dev\tools\imagegen\chatgptimg.py L1-35 · L241-372
나노바나나 웹 엔진 = C:\dev\tools\imagegen\imagegen.py(gemini.google.com · CDP 9334 상수) — 우하단 가시 워터마크(높이 ~11~12% 띠) · 원본 바이트 =s2048 로 받음 · 참조 = 붙여넣기 주입 · 「Generate an image:」 머리를 붙인다(편집 모드 필요) C:\dev\tools\imagegen\imagegen.py L1-30 · L94-224
CDP 포트 = C:\dev\tools\portgate\PORTS.md 단일 장부 · 런처 2종(.bat+_hidden.vbs) · 프로필 복사로 로그인 상속(사람 클릭 0) · 쓰기 전 cdplease.py acquire/release · 9334 = 페페 수동·나노바나나 · 9343/9344 = 카드 굽기(ChatGPT) C:\dev\tools\cdp-profiles\PORTS.md
⚠ 포트 9350 충돌 — gptapp.py 앱 CDP(9350) ↔ portgate 9350 = claude-console 크롬(26.10.13 claim) C:\dev\tools\gptapp\gptapp.py L26 · portgate L112 (보고만)
PNCAD 렌더러 WebGLRenderer({antialias, alpha:true}) · 실사 스틸 photo.js still() = 메인 렌더러 · Halton 지터 누적 · 바닥 P.floor(y=-1) · 입면·소프트웨어 렌더러 거부 C:\dev\pncad\app.html L1343-1344 · C:\dev\pncad\photo.js L535-596
벽 = SCENE.walls 를 x 한 줄로 700mm 간격 나열(펼친 입면 · 실제 방 모서리 없음) C:\dev\pncad\app.html L1337-1338 · L1871-1878
렌더 UI = #vbar 실사7·▾(#rqp)·렌더 시작 · 진행/완료 #rbar · 렌더 중 #top·#cat·#side·#vbar inert C:\dev\pncad\photo.js L720-1121

2. 사전 실측 · 엔진 사실

2-1. API 실측 (26.10.13 17:5x KST — «유료 0» 답 «전»에 한 2회 · 키 값 기록 0 · 이후 유료 호출 0)

엔진 결과
OpenAI gpt-image-2 /v1/images/edits(키 = C:\dev\SARA\.env 사본) ✅ 200 · 14.6초 · 입력 1,685 토큰(이미지 1,536) · 출력 158 → $0.0178(≈26원) · 조직 인증 요구 없음 · 출력 1536×1024 그대로. 모델 목록에 gpt-image-2.5-sunburst·-flare(26.9.8) 있음
Gemini gemini-nano-banana-2.1 generateContent(키 = .secrets) ❌ 429 무료 등급 이미지 한도 0 = 결제 미연결 · 과금 0

프로브 관찰(입력 = GS 안산 성포 84A 경로추적 캡처의 3D 뷰): OpenAI 가 오크 마루·흰 벽·왼쪽 창·화분·액자를 그렸고 벽 조각 틈(700mm)도 한 벽으로 메웠다. 장 위치·원근은 유지(가구 영역 평균 변화 19/255 vs 껍데기 48/255). 그래도 가구를 «깨끗하게» 다시 그렸다 → 재합성 필수 실증. 색 임계 마스크로 재합성하니 테두리 얼룩 → 마스크는 렌더 패스로(§4). 게시본 C:\dev\devplan\img\pncad-ai-staging\probe_*.jpg · 원본 C:\Users\delix\AppData\Local\Temp\claude\C--dev\ddfb8bd7-def9-41ef-80a8-e27c3fc9c32f\scratchpad\probe_out\.

2-2. 웹 엔진 사실(코드·기존 기록 — 이번엔 미실측, U0 에서 잰다)

항목 ChatGPT 웹(chatgptimg.py) 나노바나나 웹(imagegen.py)
비용 0원(ChatGPT Plus 구독) 0원(무료)
하루 한도 계정 단위 롤링 24시간 ≈ 90~100장(카드 굽기와 공유 · 평소 소비 25~30장) ≈ 20~100장(기록 편차)
소요 참조 첨부(편집) ≈ 5분(타임아웃 900초) · 참조 없이 ≈ 50~60초 참조 첨부 ≈ 수십 초~2분(U0 실측)
출력 크기 고정 크기 중 하나(가로면 1536×1024 예상 — U0) 입력 비율 유지 예상 · 원본 최대 2048(U0)
워터마크 없음 우하단 반짝이(높이 ~11~12% 띠)
기하 보존 미실측 — API 와 달리 픽셀 단위 보장 없음(U0 · §6 정합 보정) 미실측(U0)
실패 신호 한도·거절 = 텍스트 답 · 로그인 풀림 = /auth/ 로그인 = accounts.google.com
약관 소비자 약관상 자동 추출 금지 → 계정 제한 위험(§12) 같음

2-3. 유료 API 단가(결제 붙일 때 참고 — 26.10.13 공식)

모델 1536×1024 1장 2장 요청(편집 입력 포함)
gpt-image-2 low / medium / high $0.005 / $0.041 / $0.165 ≈$0.024 / $0.096 / $0.344
gemini-nano-banana-2.1 1K / 2K $0.0336 / $0.0504 ≈$0.08 / $0.11
- OpenAI 프로젝트 «Enforce a hard limit»(초과 시 429) · GCP «Spend cap»(Gemini API 지원) — 둘 다 반영 지연. 원장 환율 1,650원/$.

3. 아키텍처 v2

[브라우저 PNCAD]                         [PFS Flask(EC2)]                         [미니PC 워커 · 1분 주기 숨김 예약작업]           [전용 CDP 크롬]
① «AI 연출» 패널(#side 자리) · #view 안 점선 3:2 틀에 구도 · 프리셋·칩·메모 (3D 에 껍데기·창 광원 미리보기)
② photo.stageLayers(): 5:4 (위 3:2 = 틀 안 + 아래 띠) · 회색 껍데기 · 창 칩 광원 — 이 단계만 3D 잠금(3~10초)
   → base · furn(검은 배경) · matte(α) · shadow(α)
③ POST /api/pncad/stage ─────────────► 검증 · 1인 하루 20장 예약 · 잡 queued(외부 호출 0)
   ◄── 202 {job_no, 대기 순번, 미니PC 상태}
                                          ◄─ POST agent/ping (매 회차)
                                          ◄─ POST agent/claim ── lease + 슬롯 계획(엔진별 하루 상한 확인) + 프롬프트
                                          ◄─ GET  agent/<job>/input/*  (4 PNG)
                                                                       엔진별 생성(ChatGPT 탭 ∥ 나노바나나 탭) ─────────► 이미지
                                                                       정합 보정 → 가구 재합성 → 띠 크롭 → 하단 문구
                                          ◄─ POST agent/<job>/heartbeat (cancel 확인)
                                          ◄─ POST agent/<job>/result (final·raw·thumb · 슬롯별 · 멱등)
④ 폴링 GET /api/pncad/stage/<job> ◄──── 상태·대기 사유·슬롯별 단계·결과(슬롯마다 끝나는 즉시 — 점진 공개)
⑤ 작업 화면: A안·B안·원본(3D) · PNG 저장 · 더 나은 안 투표 · 같은 설정으로 다시 · 최근 연출 30일(탭 닫아도 칩·목록으로 받기)

4. 클라이언트 — 렌더 레이어

4-1. 모듈

파일 할 일
C:\dev\pncad\photo.js (그림자 v3 병합 «뒤» · U5) export async function stageLayers(opts) — 틀·껍데기·광원 프리셋·4패스. still() 과 같은 누적 코어(v3 가 바꾸는 지터·광원 흔들기 포함) · export function stagePreview(opts\|null) — 패널이 열린 동안 3D 에 껍데기·창/시간대 광원(P.aiSun 별도 필드)·gWall 숨김을 걸고 null 이면 원복 · still() 의 W·H·setViewOffset 은 옵션 인자로만 분기(기존 «렌더 시작» 불변)
C:\dev\pncad\photo-ai.js (신규 · U6 — 이름은 web-designer 처방을 따름 · 옛 안 stage.js) 패널(#aiPane)·틀(#aiFrame)·칩(#aiChip)·요청·업로드·폴링·작업 화면·최근 연출·큰 미리보기(#aiPrev) 상태기계
C:\dev\pncad\app.html (U6) #bAi(+.vsep)·#aiChip·#aiPane·#aiFrame·#aiPrev 마크업 · #aiPane 아이디 스코프 CSS(13px 직접 지정) · 클릭 배선 1줄 = phLoad().then(m => import('./photo-ai.js').then(a => a.toggle(m, phCtx()))) (기존 ['bReal','bRealQ','bRender'] 루프 옆 · photo.js press() 는 안 고친다 — 처방의 «press 분기» 대신)
C:\dev\pncad\tools\{sync_to_pfs,_deploy_hash,_verify_deploy}.py (U6) FILES 3곳에 photo-ai.js(빠지면 pfs 404)
- photo.js 를 만지는 줄(stageLayers·stagePreview·still 옵션)은 전부 U5 로 몬다 — 그림자 v3 가 photo.js 를 고치는 중이라 U6 은 photo-ai.js·app.html 만 건드리고 ?stagefake=1(가짜 레이어·미리보기 없음)로 먼저 돈다. 페이지 로드 때 칩 복원(GET /stage/recent?pending=1)은 photo.js 를 안 부르고 photo-ai.js 만 가볍게 불러 한다.

4-2. 틀 = #view 안 점선 3:2(= 출력 그대로) + 아래 띠(5:4 렌더)

4-3. 회색 껍데기 · 4패스

요소 사양
뒷벽 평면 z = −2mm · x = 장면 최소 −6000 ~ 최대 +6000 · y 0 ~ 천장 H(SCENE.walls 최대 h, 없으면 2400) · #c9c9c9 · 그림자 받음·안 드리움
바닥 P.floor(y=-1) 재사용, 연출 중만 #a3a3a3 → 끝나면 복귀
천장 임시 카메라 y < H 일 때만 #dcdcdc
숨김 gWall(700mm 틈 벽 조각) · 치수·라벨·선택선(ctx.hide)
배경 #d4d4d4
옆벽 v1 없음 — 벽이 펼친 일자 배치라 방 모서리가 없다. 방 끝은 AI 가 그린다(처방의 «옆벽» 은 이 줄로 조정)
미리보기 패널이 열린 동안 3D 에도 같은 껍데기·바닥색·광원·gWall 숨김(stagePreview) — 틀 안 구도 = 결과 구도. #bWallV(벽 3) 토글과 무관. 패널을 닫으면 원복
패스 껍데기 가구 배경 톤매핑·AO 산출
P1 BEAUTY 회색 실사 재질 #d4d4d4 ACES · AO base.png — AI 입력
P2 FURN 검정 basic 실사 재질 검정 ACES · AO(P1 과 같게) furn.png = α·가구색(프리멀티)
P3 MATTE 검정 basic 흰색 basic 검정 NoToneMapping · AO 끔 matte.png = 가구 α
P4 SHADOW ShadowMaterial(검정, 1) colorWrite:false · depthWrite:false(그림자만 드리움) 투명 NoToneMapping · AO 끔 shadow.png = 그림자 α
- 네 패스 = 같은 지터 순서·같은 표본 수 → 가장자리 α 일치 → 프리멀티 합성 헤일로 0.
- 자체 점검: α=255 화소 base − furn ≤ 3 · matte 평균이 0·1 이 아님 → 실패면 업로드 안 함.
- 표본 = v3 스틸 표본(지금 high 16). 예상 = 현 스틸 × 2.3. 패스별 진행률을 패널로.
- 실패·취소·창 크기 변경 = still() 과 같은 finally 원복(viewOffset·픽셀비·톤매핑·재질 WeakMap·바닥색·gWall·껍데기 제거·P.busy).
- 업로드 ≈ base 2.5~3.5MB · furn 1~2MB · matte·shadow 0.1~0.3MB → 합 ≤ 7MB < 20MB.

4-4. 광원 프리셋(창·시간대 칩 → 3D · 패널이 열린 동안 + 연출 렌더)

«왼쪽·오른쪽» = 화면(이미지) 기준(처방 §3) — 3D 를 돌려도 그림자는 늘 그 쪽에서 온다. 방향 = a·R + b·Y + c·C 를 정규화(R = 카메라 오른쪽 수평 단위벡터 · Y = 위 · C = 표적→카메라 수평 단위벡터). 카메라가 멈추고 150ms 뒤 다시 계산. | 칩 | (a, b, c) | |---|---| | 창 왼쪽 | (−0.85, 0.80, 0.55) — 그림자 오른쪽 뒤 | | 창 오른쪽 | (0.85, 0.80, 0.55) | | 창 없음 | (0.15, 1.0, 0.45) — 짧은 그림자 | | 저녁 | 색 #ffd4a8 · 세기 ×0.75 · env ×0.85 · b 0.80 → 0.55(해를 낮춰 그림자를 길게 · 처방 제안 — U5 에서 육안 조정) | - 프롬프트의 창 위치와 같은 쪽(이미지 기준). 덮어쓰기는 P.aiSun 별도 필드 → 패널을 닫으면 원복(«렌더 시작»·실사 일반 화면에 안 남는다). fitShadow() 가 방향 인자를 받아야 한다 → U5 는 v3 병합 뒤.

4-5. 그림자 v3 와의 경계(A6)

구간 이 기능 순서
fitShadow · 그림자 맵 · bias · 스틸 지터 루프 · GTAO 수치 안 고친다 v3 담당
stageLayers · 누적 코어 공용화 · fitShadow(dir) · aoOverlay(camera) 인자 U5 v3 가 pncad master 에 병합된 뒤
서버 · 워커 · UI · 합성 지금 병행 독립
실사용 샘플(U8) v3 반영 뒤 가구 그림자·AO 가 결과에 그대로 박힌다

5. 서버 (PFS) — 대기열 · 저장 · 한도 · 프롬프트

5-1. 파일 (C:\dev\pfs)

api/pfs/pncad/controller/pncad_stage_controller.py   # 사용자 API + 에이전트 API(/api/pncad/stage…)
api/pfs/pncad/dao/pncad_stage_dao.py                 # SQL 전부(트랜잭션)
api/pfs/pncad/stage/conf.py      # env(켜기·한도·만료·예산) — 키 없음
api/pfs/pncad/stage/quota.py     # 1인 하루 · 엔진별 하루 · 동시 · 대기열 상한
api/pfs/pncad/stage/plan.py      # claim 때 슬롯 계획(엔진 상태·하루 상한·API 예산 0)
api/pfs/pncad/stage/prompt.py    # PROMPT_VER · 프리셋 6 · 칩 문구 · 엔진별 문안
api/pfs/pncad/stage/budget.py    # 유료 슬롯 예약·정산(지금 예산 0 → 항상 거절) — 단위 시험만
api/pfs/pncad/stage/auth.py      # 에이전트 이름 토큰(PFS_STAGE_TOKENS) · lease_token
api/pfs/pncad/stage/storage.py   # 경로 · 업로드 검증 · 30일 정리
migrations/patch63_pncad_stage.sql                   # 🆕U1 실측: patch62 = 자재 입고일이 이미 점유 → 63
tests_cody/test_pncad_stage_{quota,plan,prompt,storage,db,auth,controller}.py

5-2. 사용자 API (/api/pncad)

응답 필드는 web-designer 처방 §11 을 이 문서 경로·이름으로 옮긴 것(처방과 다르면 이 표가 정본). | 메서드 · 경로 | 하는 일 | 권한·검사 | |---|---|---| | GET /stage/usage | 패널 열 때 · 상태 변화 뒤 · 관리자 접힘 30초 → {enabled, admin, dayLimit, dayLeft, bind:'user'\|'pool', worker:'online'\|'offline'\|'engine_down', queueLen, etaSec, resetDayAt, activeJobId, monthPct:null, estWon:null, engines?} · dayLeft = min(내 남은, 공용 남은) · 공용 남은 = Σ엔진(오늘 상한 − 오늘 슬롯) − 2×(아직 안 잡힌 대기 건) · 2 미만 = 0 · engines(엔진별 left·cap·state)는 관리자만 | 로그인 | | POST /stage | multipart base·furn·matte·shadow + meta(idem_key(= Idempotency-Key)·scene_no?·scene_name·scene_line·preset·opts{wall,floor,window,time,props}·memo·engine_pref(관리자만 · 나머지는 무시)) → 검증 → 사용자당 진행 1건(넘으면 409 busy) · 1인 하루(day_limit·day_short) · 공용 잔량(pool_limit) → 잡 queued(expire_at = +12시간) → 202 {job_no, status:'queued', position, expireAt} · 같은 키+같은 입력 = 기존 잡 200 · 같은 키+다른 입력 = 409 | 로그인 자사 계정 · Origin/Referer 같은 호스트 · PNG 서명·Pillow 열기·크기 정확히 1536×1229 · 파일당 ≤ 8MB · 합 ≤ 18MB · enum 허용목록(§5-6) · 메모 ≤ 60자(줄바꿈·제어문자 제거) · 엔진·장수는 서버가 정한다 | | GET /stage/<job_no> | {status: queued\|running\|done\|partial\|failed\|expired\|canceled, wait: queue\|offline\|engine\|null, position, etaSec, expireAt, slots:[{k:'A', st: wait\|gen\|compose\|done\|fail, thumb, w, h, sec, eng?, why?}, {k:'B', …}], pickable, pick, baseThumb, site, refund:{imgs}, err:{code}, createdAt, finishedAt, seen} — eng·why 는 관리자만 · 폴링 3초(작업 화면)·15초(칩만) | 본인 · 총관리자 | | POST /stage/<job_no>/cancel | 대기 = 즉시 canceled · 장수 2 반환 · 그리기 시작 뒤 = cancel_req → 워커가 다음 heartbeat 에서 멈춤(시작한 장은 장수에서 빠질 수 있다) · 이미 끝났으면 결과 그대로(경쟁) | 본인 | | POST /stage/<job_no>/again {idem_key} | 같은 입력 4장으로 새 잡(parent_job · n_req 2) — 입력은 부모 행 잠금(FOR UPDATE) 아래 새 잡 폴더로 복사(정리와 직렬화 · 부모 inputs_purged ≠ '0' 면 410) · 1·2단계 생략(3D 잠금 없음) · 생성 순서·한도·진행 1건·멱등 = POST /stage 와 같은 경로 | 본인 | | POST /stage/<job_no>/retry {k, idem_key} | partial·failed 잡의 실패 슬롯만 새 자식 잡으로(n_req = 실패 장수 · k 고정 · 입력 복사 · 자기 ymd·expire_at) — 부모 잡은 그대로(성공분·pick·seen 불변 · 환급 계산 안 흔들림) · 자식 조회 때 부모의 성공 슬롯을 inherited:true 로 붙여 보여 준다 · 입력 7일 넘으면 410 — P2 | 본인 | | POST /stage/<job_no>/pick {pick: A\|B\|tie} | 블라인드 투표 — 두 안이 다른 엔진이고 둘 다 완료일 때만(pickable) · 바꿀 수 있다 | 본인 | | POST /stage/<job_no>/seen | 안 본 완료 표시 해제(칩 · «새 결과») | 본인 | | GET /stage/<job_no>/img/<k>?kind=final\|thumb\|raw · img/base?kind=jpg\|thumb | 바이트(make_response — send_file(BytesIO) 금지) · 저장 이름 {작업명}_AI연출{A\|B}_{YYYY-MM-DD_HHmm}.png(엔진명 안 넣음) | 본인 · 총관리자(raw = 관리자만) | | GET /stage/recent?limit=20&before=<job_no> · ?pending=1 | 본인 최근 30일(썸네일·상태·현장명 · 진행 건 맨 위) · pending=1 = 진행 중 + 안 본 완료만(페이지 로드 때 칩 복원) | 본인(관리자 = 전체 + 사용자명) | - 오류 코드(UI 문구 = 처방 §7): day_limit · day_short · pool_limit · busy · queue_full · engine · safety · expired · off · auth. 응답이 HTML(로그인 페이지)이면 클라가 E-AUTH. - 🆕[코디] 26.10.13 U2 구현 확정(위 표와 다른 점) — ① 응답 = 평문 JSON(날짜 YYYY-MM-DD HH:MM:SS KST) · 오류 = HTTP + {code} ② Origin/Referer 불일치 = 400 origin(403 은 화면이 «로그인 만료»로 읽는다) · 남의 잡 = 403 forbidden · 없음 404 ③ 410 = {code:'expired', reason:'inputs_gone'}(again·retry 입력 7일) ④ 슬롯 st: unknown(결과 불명) = wait + hold:true(화면에 «확인 대기» 상태가 없어 fail 로 내면 [다시 만들기]가 409) · 끝난 잡의 todo = fail ⑤ 잡·목록에 own(관리자가 남의 잡을 볼 때 false) ⑥ usage 에 ready · 설정 문제(띠 해시·Pillow·폴더)면 enabled:false(관리자만 offReason) ⑦ 총관리자 판정 = DB(세션 값 불신) · 관리자 recent = 전 사용자(?mine=1 본인) · pending=1 은 늘 본인 ⑧ 다음 쪽(before)엔 진행 건 제외(첫 쪽 맨 위에 이미 나옴) ⑨ img final = Content-Disposition 저장 이름(엔진명 0). - 잡을 만드는 3경로(POST /stage·again·retry) 공통 순서(코덱스 3차): ① 사용자 잠금 GET_LOCK('pncad_stage_u<user_no>', 5)(못 잡으면 429 busy) ② idem_key 로 기존 잡 조회 — 있으면 그대로 반환(busy·한도 검사보다 먼저 · 세 경로 모두 idem_key 필수) ③ 사용자당 진행 1건 ④ 1인 하루·공용 잔량 ⑤ INSERT(+입력 저장·복사) ⑥ 커밋 ⑦ RELEASE_LOCK(finally · 연결이 끊겨도 풀린다). 공용 잔량은 사용자 사이 경쟁이 남지만(드묾) claim 의 원자 예약이 최종 방어선. - 대기 사유 wait: offline = 마지막 ping 3분+(워커가 1분 주기 작업이라 60초가 아니라 3분) · engine = 두 웹 엔진 모두 못 씀(한도·로그인·고장 — 상한이 찬 뒤 남은 드문 대기 건도 여기로, 자정 KST 에 풀리면 자동) · 그 밖 queue. etaSec = (앞 건수 + 1) × 최근 완료 20건 평균(없으면 120초).

5-3. 에이전트 API (/api/pncad/stage/agent/ — 워커 전용)

메서드 · 경로 하는 일
POST agent/ping {worker_id, version, target, engines:{chatgpt_web:{state,left?}, nanobanana_web:{…}}} 워커 상태 기록(pncad_stage_worker) · 응답 = 대기 건수. 엔진 state = ok / limit(까지 시각) / login / broken
POST agent/claim {worker_id, engines_ok:[…]} 한 트랜잭션: 실행 가능한 queued(만료 전 · 계획 엔진이 가용) 중 «사용자별 순환 → 오래된 순» 1건을 조건부 UPDATE 로 잡고 + 엔진별 하루 수량을 슬롯 행으로 원자 예약 → lease_token(무작위) + attempt(증가) + lease 15분 + 슬롯 계획 + 엔진별 프롬프트 + 입력 URL·해시. 없으면 204
GET agent/<job>/input/<name> 입력 PNG(현재 임대·attempt 일치 시)
POST agent/<job>/heartbeat {lease_token, attempt, slots:[{k, st: gen\|compose, need_fallback?}]} 유효한 임대만 연장(만료된 임대는 부활 금지 → 409, 워커는 그 잡을 버린다) · 슬롯 단계(gen·compose)를 기록 = 화면 «그리는 중·합치는 중» · pncad_stage_worker.last_ping 도 갱신 · 응답 {cancel, replan} — 실패 슬롯의 대체 엔진은 서버가 하루 수량을 다시 원자 예약해 승인
POST agent/<job>/result multipart final.png·raw.jpg·thumb.jpg + meta{lease_token, attempt, k, engine, model, ms, flags, sha256} 슬롯 1장씩, 끝나는 즉시(점진 공개 — 먼저 끝난 안부터 화면에 뜬다) · 현재 임대(lease_token + attempt)만 받는다 — 옛 시도의 늦은 결과는 409(최신을 못 덮는다) · 슬롯 확정 = 조건부 UPDATE(status IN ('planned','gen','compose') → 영향 1행일 때만 같은 트랜잭션에서 n_ok+1) · 같은 해시 재전송 = 기존 성공(증가 없음) · 다른 해시 = 409 · 임시 파일 검증 뒤 확정 · 서버 검증(§5-7) · 마지막 슬롯이 끝나면 잡 done/partial · 정산(웹 = 0원)
POST agent/<job>/fail {lease_token, attempt, reason, slot_states, retryable} 끝난 슬롯은 보존하고 빠진 슬롯만 다시 대기 · 시도 < 3 & 재시도 가능 = queued · 아니면 failed. 생성 여부 불명 슬롯은 엔진 수량을 섣불리 돌려주지 않는다
- 인증 = 별도 이름 토큰 분기(TREAD 방식 — _AGENT_PATHS·공유 토큰에 넣지 않는다): 서버 env PFS_STAGE_TOKENS = 「minipc:sha256」(해시만) · Authorization: Bearer · 6개 경로 + HTTP 메서드만 정확히 허용 · 핸들러가 다시 확인 · 토큰 이름을 호출마다 기록(감사) · 이름별 폐기·회전. 토큰 원문 = C:\dev\.secrets\.env PFS_STAGE_AGENT_TOKEN.
- 임대 식별 = 토큰 이름 + lease_token + attempt + 유효기간 — 넷 다 맞아야 input·heartbeat·result·fail 이 먹는다 → 늦게 깨어난 옛 워커·중복 워커의 쓰기를 막는다. 잡 최대 처리시간 20분.
- 🆕[코디] 26.10.13 U2 확정 계약(= U3 입력 · 정본 코드 C:\dev\_wt\pfs-ai-stage-u2\api\pfs\pncad\controller\pncad_stage_controller.py 커밋 c39e7fce)
- 인증 Authorization: Bearer <PFS_STAGE_AGENT_TOKEN> · 토큰 이름 = worker_id · 서버 env PFS_STAGE_TOKENS="minipc:<sha256>"(발급 = python C:\dev\pfs\tools\tread_token_issue.py minipc — 같은 꼴) · 에이전트 6경로는 swagger 에서 숨김.
- ping {target:'test'|'prod', version, engines} → {ok, queued, enabled, ready, reason, serverTime} · ready=false(reason band_unset·pil_missing·dir_unwritable)면 claim 이 204 · 409 target = 다른 서버(멈춤).
- claim {engines_ok, claim_key(필수 · [A-Za-z0-9_.:-]{1,40} · 요청마다 새 값 · 응답 유실 재전송엔 같은 값)} → 204 {job_no, attempt, lease_token, lease_until, slots[{k, engine, prompt, st}], input_sha, scene_line, prompt_ver, inputs{base,furn,matte,shadow: URL}, resumed} — 같은 키 재전송 = 같은 임대(resumed:true) · 다른 키(재기동·중복 프로세스)는 남의 임대를 못 받는다. resumed 인데 로컬 기록 없는 gen/compose 슬롯 = fail(slot_states unknown).
- input = GET + 헤더 X-Stage-Lease·X-Stage-Attempt → PNG + X-Input-Sha256 · 묶음 해시 = sha256(「이름:파일sha256\n」 base→furn→matte→shadow) = claim 의 input_sha · 409 임대 아님 · 410 정리됨. 입력은 워커가 시도 폴더에 보관(늦은 회수 때 다시 못 받는다).
- heartbeat → {cancel, replan[{k, engine|null, prompt?}], lease_until, replan_deferred} · 409 = 그 잡 즉시 포기.
- result multipart final(PNG 1536×1080 — matte=255 화소 = furn ±2 · 아래 56px = 문구 띠 «자산 PNG 를 그대로 붙임») · raw(JPEG 선택) · meta{lease_token, attempt, k, engine, model, ms, flags, input_sha(필수), sha256?(= final 바이트), recover?} · thumb 는 서버가 final 로 만든다(보내도 무시) · 같은 바이트 재전송 = 200 exists(재큐 뒤에도) → 200 받을 때까지 final 바이트 보관 · 409 lease·conflict·input_mismatch·superseded·closed · 422 furniture_mismatch·size·band_mismatch·band_unset·invalid·raw_* · 410 expired · 400 invalid·sha_mismatch · 413 · 500 input_corrupt. flags 의 by·lease·fallback_from·recovered·claim 키는 서버가 버린다.
- fail {lease_token, attempt, reason, slot_states[{k, st: fail|unknown}], retryable} · reason 첫 낱말 safety = 화면 «안전 정책» · 그 밖 = engine · 재전송 409 lease = 이미 반영.
- 늦은 회수(U1 미결 ①) = 임대가 끝난 시도의 슬롯을 로컬 기록(받은 raw·대화 주소)으로 회수했으면 result 를 recover:true + 그 시도의 원래 lease_token·attempt 로 보낸다(→ 로컬 기록에 lease_token 저장 필수). 서버: 임대가 아직 유효하면 일반 결과 · 만료면 그 잡 임대를 즉시 회수(정리 1분 주기 기다리지 않음) → 슬롯이 unknown · 임대 증명(슬롯 flags.by·flags.lease) 일치 · k 미성공 · 잡 취소/만료 아님 · 입력 남음 · 관리자 [다시 생성] 자식 없음일 때만 ok(잡 partial→done · seen 초기화). «회수 불가 확정»(unknown→fail)은 없다 = 관리자 [다시 생성]만.
- claim 응답 유실 멱등(U1 미결 ②) = 위 claim_key.
- 문구 띠 해시 env PNCAD_STAGE_BAND_SHA = 띠 영역 RGB 원시 바이트(1536×56×3)의 sha256(PNG 파일 바이트 아님 · storage.verify_final) → U4 가 assets/disclaimer_w1536.png 로 계산해 G2 에 넣는다.

5-4. 슬롯 계획 (claim 때 서버 plan.py · claim 과 같은 트랜잭션)

  1. 기본 = [ChatGPT 웹 1, 나노바나나 웹 1] · A·B 자리는 잡마다 무작위(항상 A = ChatGPT 로 굳으면 «A 가 낫다» 편향 · 블라인드 투표가 깨진다). 관리자 engine_pref(ChatGPT 2장 · 나노바나나 2장)는 그 요청에만.
  2. 엔진 상태(최근 ping)가 ok 가 아니거나 그 엔진 «오늘 전체 상한»(Q6)이 찼으면 → 다른 웹 엔진으로 바꾼다(2장 모두 한 엔진 가능 · 이때 pickable=false · 일반 화면엔 표시 없음).
  3. 둘 다 못 쓰면 claim 안 함(잡은 대기 유지 · 화면 «엔진 한도 — 한도 풀리면 자동»). 막힌 엔진의 잡이 뒤 잡을 막지 않게 «실행 가능한 잡»만 고른다.
  4. 유료 API 슬롯 = 월 예산(PNCAD_STAGE_CAP_KRW) 0 → 계획 안 함 + 워커도 유료 어댑터 실행을 기본 차단(이중 안전).
  5. 슬롯별로 pncad_stage_slot 행(k·attempt·엔진·상태)을 claim 트랜잭션 안에서 만든다 = 엔진 수량 원자 예약. 엔진별 하루 카운트 = KST 오늘 planned/gen/compose/ok/unknown 슬롯 수(생성 여부 불명은 반환 안 함 · lease 만료 뒤 다시 claim 하면 attempt+1 새 행이라 앞 시도의 소비가 지워지지 않는다).
  6. 1인 하루 20장은 «잡 수가 아니라 이미지 수»(작성 때 n_req=2 예약 · 계산식 §5-5).

5-7. 결과 업로드 서버 검증 (A2 보장 — 코덱스 2차)

5-5. 한도 · 만료 · 정리(서버가 요청 때 게으르게 · 1분에 1회 이하)

항목 규칙
1인 하루 20장(B1) — 오늘 사용 = Σ 오늘 내 잡(queued·running = n_req · done·partial·failed = n_ok · canceled = 그리기 시작한 슬롯 수 · expired = 0) → 부분 성공은 완성된 장만 차감 · 남은 < 2 = day_short(1장) / day_limit(0장) · 응답 refund.imgs 로 화면이 «돌려줬다» 문구를 붙인다
엔진별 전체 하루 ChatGPT 20 · 나노바나나 30(Q6) — claim 때 슬롯 행으로 원자 예약 · 작성 때는 «공용 남은 ≥ 2» 만 확인(§5-2 — 경쟁으로 넘친 드문 건은 대기 → 자정 KST 뒤 처리 또는 12시간 만료)
동시 사용자당 진행(대기 포함) 1건(busy — 공용 한도가 작아 쌓아 두기·더블클릭 중복을 막는다) · 전체 대기열 ≤ 20(넘으면 429 queue_full)
워커 상태 마지막 ping 3분+ = offline(화면 «미니PC 꺼짐») · 두 엔진 다 limit/login/broken = engine_down(화면 «AI 연결 점검 중»)
lease 만료 running + lease 지남 → 끝난 슬롯은 보존 · 제출 전 슬롯만 queued 로(시도 +1, 3회째면 failed) · 제출했는데 결과 불명 슬롯 = unknown(자동 재생성 안 함 — 워커가 로컬 진행기록·대화 주소로 회수 시도 → 안 되면 화면 «확인 대기», 관리자가 [다시 생성]) · 대기 12시간 만료는 queued 에만
대기 만료 queued 12시간(expire_at 저장 · 화면이 «26.10.14(수) 06:41 KST 까지» 로 보여 준다) → expired · 장수 반환
워커 무응답 알림 대기 30분+ & 마지막 ping 10분+ → 텔레그램 1회(common/utils/telegram_notify.send_async)
보관 · 정리 결과(final·thumb·raw) 30일 · 입력 4장 PNG 7일(같은 입력 «다시 만들기»·실패 슬롯 재시도용) · base 는 JPEG+썸네일로 30일(«원본(3D)» 보기) · 30일 뒤 잡 폴더 삭제(DB 행은 남김) · 정리 = 3단계 표식(코덱스 3차): 조건부 UPDATE '0' → 'P'(status 가 queued·running 이 아니고 기한 지난 행만 — again·retry 의 부모 행 잠금과 직렬화) → 파일 삭제 → '1' · 죽어서 'P' 로 남으면 다음 회차가 이어 지운다(삭제는 멱등) · 워커 ping(1분마다)에서도 제한량씩 처리(정리 커서) — 요청이 없어도 돈다 · 디스크 최대치(공용 상한을 매일 꽉 채움) ≈ 입력 1.2GB + 결과 ≈ 4GB → U7 에서 df -h 확인, 모자라면 결과 보관 14일
대기열 공정성 실행 가능한 잡 중 «사용자별 순환 → 오래된 순» · 엔진별 재시도 시각·백오프 · 시도 3회 넘은 잡 격리(failed)
저장 위치 PNCAD_STAGE_DIR = /home/ubuntu/pfs-data/pncad-stage/<env>(리포 밖 · 배포 git clean 무관) · 로컬 C:\dev\_work\pncad-stage-data\

5-6. 프롬프트 (서버 중앙 · PROMPT_VER st2 · A5 = Claude 미사용)

공통 문안(영문 — 엔진 이해도 최상, 한 줄로 평탄화해 보낸다):

Edit the attached image. It is a 3D render of built-in furniture standing in an empty room whose floor and walls are flat gray placeholders.
Repaint ONLY the gray placeholder floor, walls{and ceiling} into a finished {SPACE}: {STYLE}.
Floor: {FLOOR}. Walls: {WALL}. Light: {WINDOW} {TIME}. Decor: {PROPS}
Hard rules: keep every piece of furniture exactly unchanged (same position, size, shape, color, material, perspective — do not restyle, clean up or redraw it);
keep the exact same framing, aspect ratio, camera viewpoint, lens perspective and the floor-wall line;
do not place any object in front of, on top of, inside, or overlapping the furniture, keep a clear gap around it;
natural neutral white balance; no people, no text, no logos, no watermark, no border.
{MEMO}
칩 토큰 → 문구
벽 wall color(단색) → smooth matte painted walls in {TONE} · paper(벽지) → walls covered in subtle textured wallpaper in {TONE} · tile → walls finished with large-format matte ceramic tiles
바닥 floor lightwood → light natural oak wood flooring · darkwood → dark walnut wood flooring · tile → large-format matte porcelain floor tiles in light gray · marble → polished light marble floor
창 window(이미지 기준) left·right → a large window on the left (right) side of the image with soft daylight coming from the left (right) · none → no windows in view; soft ambient ceiling lighting
시간대 time day → bright daytime · eve → early evening with warm indoor lights on
소품 props none → No decorative objects at all. · few → Only one or two small decorative items, placed away from the furniture. · normal → A few tasteful decorative items (a plant, framed art, a rug), all placed away from the furniture.
preset {STYLE} {TONE}
modern 모던 a modern, clean interior with neutral tones warm light gray
natural 내추럴 우드 a warm natural interior with light wood and linen textures warm off-white
nordic 북유럽 a bright Scandinavian interior, white and pale wood, simple and airy white
hotel 호텔 an elegant hotel-like interior with soft indirect lighting warm beige
minimal 미니멀 a minimal, uncluttered interior with smooth surfaces white
classic 클래식 a calm classic interior with dark wood and subtle wall moldings ivory
- 칩을 바꾼 «사용자 지정»도 preset = 마지막으로 고른 프리셋(STYLE·TONE 은 그대로).

6. 미니PC 워커 — C:\dev\tools\pncad-stage-worker\

6-1. 형태(선례 C:\dev\tools\worklog-ai 와 같은 틀)

항목 사양
실행 숨김 예약작업 1분 주기 \Cody\PncadStage-test · \Cody\PncadStage-prod → wscript run_<target>.vbs → pythonw worker.py --target test\|prod · import nowin
등록 C:\dev\tools\winledger\WINDOWS.md claim(permanent task) · 전용 CDP 포트 portgate.py check/claim(빈 번호, 예 9354) · 런처 2종 C:\dev\tools\cdp-profiles\launch_pncad_stage.bat+_hidden.vbs · 프로필 = delix0731 복사(로그인 상속)
한 회차 파일 락(run/<target>.lock — 같은 PC 중복만 막는다, 진짜 배타는 서버 claim·임대) → ping → claim 반복(최대 10분 · 1건씩) → 없으면 종료. 앞 회차가 돌고 있으면 그냥 끝
하트비트 생성 대기와 독립 스레드(60초마다) — ChatGPT 편집 5분 동안에도 임대 유지 · 409 받으면 그 잡 즉시 포기
진행 기록·재개 슬롯마다 로컬 기록 run/jobs/<job>-<attempt>/slot<i>.json(제출 시각·대화 주소·받은 raw 경로) → 회차가 죽어도 다음 회차가 받은 raw 는 그대로 업로드, 제출만 된 건 대화 주소에서 회수(grab) 시도 → 실패면 unknown 보고(자동 재생성 안 함)
회로 차단 엔진별: 한도 문구 → limit · 로그인·CAPTCHA·계정 경고 → 즉시 정지(login/blocked, 자동 재시도 안 함) + 텔레그램 · 셀렉터 실패 2회 → broken. 사람 복구 뒤 worker.py --reset-engine <이름>
유료 차단 유료 어댑터는 PAID_ENABLED=False 상수가 기본(서버 계획과 별개로 워커도 막는다)
크롬 cdp.ensure(port) 를 워커가 소유 · cdplease.py acquire/release · 탭 2개(ChatGPT · Gemini) · 끝나면 탭 about:blank(램)
엔진 engines/chatgpt_web.py(= chatgptimg.generate 재사용) · engines/nanobanana_web.py(= imagegen.generate 재사용) · engines/openai_api.py·gemini_api.py(코드만 · 서버가 계획 안 하면 안 돈다) · engines/mock.py(test 전용)
공용 도구 수정(하위호환) imagegen.py = CDP 포트 env 덮기(기본 9334 그대로) + mode='edit'(「Generate an image:」 대신 편집 머리) · chatgptimg.py = ref_mode='edit'(캐릭터 참조 문구 대신 편집 문구) — 카드 굽기 기본 동작 불변, 자체검사 chatgptimg.py --demo
속도 엔진당 탭 1 · 제출 간 ≥ 20초 · 두 엔진은 병렬
알림 python C:\dev\tools\notify.py done …(10분 연속 실패·복구 1회 — worklog-ai 와 같은 규칙)
키 웹 엔진 = 없음. API 어댑터 = C:\dev\.secrets\.env(Q2 로 옮긴 OpenAI 키 · Gemini 키) — 서버 사본 0
PFS 토큰 C:\dev\.secrets\.env PFS_STAGE_AGENT_TOKEN(값 출력 금지)

6-2. 합성 (compose.py · Python 3.12 · Pillow · numpy · opencv-python-headless)

1 raw → RGB. 비율 확인: ChatGPT 3:2 · 나노바나나 5:4 (±2% 밖이면 슬롯 실패 «비율»)
2 크기 맞춤: ChatGPT → 1536×1024 · 나노바나나 → 1536×1229 (LANCZOS)
3 정합 보정: 가장자리 지도(그라디언트) ECC(affine · 배율 ±4% · 이동 ±3%) — 가구 윤곽 띠 + 바닥·벽 경계 위주 마스크
   **변형은 AI 배경에만** 적용(가구 원본 픽셀·좌표·α 는 절대 안 움직인다 — 좌표 규약 = 렌더 틀 1536×1229 기준)
   수렴 실패·잔차 큼 → flags.align_low (결과는 내되 화면에 «정합 낮음» 표시)
   잔상 검사: 가구 윤곽 바깥 2~12px 고리에서 AI 배경의 가장자리 밀도가 base 보다 크게 높으면 flags.ghost(AI 가 그린 가구 테두리 잔상 의심)
4 합성: out = furn + (1−α)·(AI정합 × (1 − 0.45·shadow))       # 프리멀티 · 헤일로 0
5 나노바나나: 아래 205px 띠 잘라 1536×1024 (워터마크 제거)
6 하단 문구 띠 «덧붙임»(Q8 ①): 1536×56 · 바탕 #1f1f24 · 흰 글자 28px · 왼쪽 여백 24 · 세로 가운데 = `assets/disclaimer_w1536.png`(OFL 한글 폰트로 미리 구움) → 최종 1536×1080
  PNG 메타 = `Description: AI 연출 이미지` 하나만(엔진명·job 번호 금지 — 고객 전달 파일 · 엔진·job 은 DB 에만)
7 썸네일 198×132 JPEG(목록·작업 화면 공용 · ≤ 30KB) · raw 는 JPEG q90 으로 올림(관리자 검수용)
8 검수 플래그: align 잔차(px) · furn_mad(가구 영역에서 AI 가 바꾼 정도) · 워터마크 잔존 검사(우하단)
9 슬롯이 끝나는 즉시 그 1장을 올린다(다른 슬롯을 기다리지 않는다 — 화면 점진 공개)

7. 비용 · 한도 (B1 재해석)


8. UI (web-designer 처방)

8-1. 배치 · 흐름

요소 처방 요지
진입 #bAi «AI 연출» #vbar «렌더 시작» 뒤 구분선 .vsep 다음 · «렌더 시작»과 같은 모양 · 토글 · 단축키 없음(공용 한도 · Ctrl+8 = 탭 전환) · 막혀도 aria-disabled 안 씀(사유는 패널 배너 · 최근 연출 열람 보장) · 실사가 꺼져 있으면 켠다
패널 #aiPane #side(334px) 자리를 통째로 대신(드로어 = 3D 48% 가림 · 모달 = 설정 중 3D 0% → 부적합) · 1280×720 #view 626×642 불변 · innerWidth < 1500 이면 카탈로그 «임시» 접기(946×642 · setCat(false) 만 — foldSet·catToggle 금지) · 헤더 오른쪽 64px 비움(QA 토글) · 모달 아님(포커스 트랩 없음)
틀 #aiFrame #view 안 가운데 3:2 점선 + 바깥 마스크 .35 · 라벨 틀 안 좌하단(좌상단은 #pickMode 와 겹침 실측) «이미지 범위 · 3:2 · 1536×1024 — 3D 를 돌려 이 안에 맞춰라» · 출력 = 틀 안 그대로(§4-2)
3D 잠금 1단계(입력 장면 3~10초)만 — #rbar progress 재사용 · 취소 = 무료 · 2단계(전송)부터 해제 · 패널·탭을 닫아도 서버·워커가 계속
진행 5단계 입력 장면(내 PC) → 전송 → 대기(서버 · 사유 3종 queue/offline/engine) → 그리기(미니PC · 슬롯별) → 합성·올리기
작업 화면 진행·결과 한 화면(data-view=job) · 단계 목록(완성 0장일 때) → 1장 완료부터 한 줄 요약 · 썸네일 3칸 A안·B안·원본(3D) · 먼저 끝난 안부터 열람(자동으로 3D 를 덮지 않음) · 큰 미리보기 #aiPrev(방향키 = 비교 · Esc = 3D 로) · A안 PNG 저장·B안 PNG 저장 · https 면 [선택한 안 복사](카톡용) · 투표 «더 나은 안은? [A안][B안][비슷함]»(다른 엔진·둘 다 완료일 때만) · 끝나면 CTA «같은 설정으로 다시 만들기 · 2장»(같은 입력 재사용)
칩 #aiChip 패널을 닫았을 때·페이지 로드 때(서버 값으로만 복원) #vbar 에 상태 칩 — 대기·꺼짐·점검·그리는 중·완료·부분·만료·실패·안 본 여러 건
최근 연출 P1(탭 닫아도 처리되니 «돌아와서 받기»의 정식 경로) · 패널 헤더 [최근 연출 12 · 진행 중 / · 새 1] · 52px 행(썸네일 60×40 · 날짜 · 모던 · 2장 · {현장명} · 상태 칩 색+글자) · 20건 + 더 보기 · 썸네일 전용 URL 만
폴링 작업 화면 3초 · 칩만 15초 · 탭 숨김 중지 · 복귀 즉시 1회 · 오류 백오프 3→6→12→30초 · 끝난 상태에서 정지 · 폴링은 한 개만
엔진명 일반 사용자 화면·파일명·PNG 메타 어디에도 없음(A·B 무작위 + 블라인드 투표) · 관리자만 미리보기 태그·메타줄·접힘(엔진 선택 «이 요청에만» · 엔진별 남은 장수·로그인 상태)
한도 표시 푸터 오늘 남은 장수 14장 / 20장(= min(내, 공용) · 공용이 막으면 8장 (전체 공용)) · 예상 소요 약 1~3분 / 앞에 2건 · 약 5~7분 · 비용 칸 없음(0원 — «무료»도 안 쓴다 · estWon·monthPct 는 서버가 값을 줄 때만 = 유료 복귀 대비)
문구 기능명 «AI 연출» 하나 · 금지어 «실사·사진·완성 이미지·시공 전/후·실제 모습» · 어투 PNCAD 현행 ~다/~라 · 오류·대기 26종 표(E-DAY·E-POOL·WAIT-OFF·WAIT-ENG·E-EXPIRED·E-PART·E-BUSY…) = 처방 §7 그대로 · 만료 시각 26.10.14(수) 06:41 KST 형식

8-2. 프리셋 · 칩(화면 = 칩 기본값의 단일 출처 · 서버 토큰 = §5-6)

프리셋 스와치(벽/바닥) 벽 바닥 창 시간대 소품
모던 #cfd2d6 / #5d554e 단색 어두운 마루 왼쪽 낮 적게
내추럴 우드 #efe6d6 / #c99a62 벽지 밝은 마루 왼쪽 낮 보통
북유럽 #f6f6f3 / #dfc7a1 단색 밝은 마루 왼쪽 낮 보통
호텔 #d9cdbb / #e8e4de 벽지 대리석 없음 저녁 적게
미니멀 #f2f2f2 / #d8d2c8 단색 밝은 마루 왼쪽 낮 없음
클래식(6번째) #e3d3b8 / #6a4630 벽지 어두운 마루 왼쪽 저녁 보통
- 카드 3열×2행 94×58(repeat(3,minmax(0,1fr)) · padding 3px 2px — «내추럴 우드» 여유 0 실측) · 칩 button[role=radio] 28px · 벽 라벨 «색» → «단색» · 칩을 바꾸면 «사용자 지정 · [처음 값으로]» · 프리셋을 고르면 3D 광원 즉시 갱신 · 메모 maxlength=60 · Enter 무동작(IME 조합 무시) · 안내 «이름·연락처는 쓰지 마라 — 외부 AI 로 전송된다» · select·datalist·드롭리스트 0(전부 라디오 칩) · 저장 = localStorage pncad.ai(메모 제외) · 접힘은 pncad.fold 의 ai.admin 하나.

8-3. 조정 표 (처방 ↔ 설계)

처방 설계 결정 이유
새 모듈 photo-ai.js · photo.js press() 에 bAi 분기 이름 채택 · press 분기 대신 app.html 배선 1줄(§4-1) 그림자 v3 가 photo.js 를 고치는 중 — photo.js 줄은 U5 로만
출력 = 틀 안 1536×1024 채택 + 아래 205px 띠를 붙인 5:4 렌더(§4-2) 나노바나나 워터마크를 띠에서 잘라낸다
입력 장면 = 바닥·뒤벽·옆벽 옆벽 v1 없음(§4-3) 펼친 일자 벽 배치라 모서리가 없다
API GET quota · POST jobs · DELETE jobs/{id} … §5-2 경로가 정본(/stage/usage · POST /stage · /stage/<job>/cancel · again · retry · pick · seen · recent?pending=1) — 응답 필드는 처방 그대로 PFS 기존 /api/pncad 네임스페이스 관례
썸네일 webp JPEG 198×132(§5-7) 서버 Pillow 8.4 의 webp 지원 미확인 · JPEG 도 10KB 안팎
하트비트 60초 끊김 = 꺼짐 3분(§5-2) 워커 = 1분 주기 예약작업(잡 없으면 바로 끝난다)
사용자당 진행 1건 채택(옛 설계 2건 → 1건) 공용 한도가 작다 · 더블클릭 중복
«B안만 다시 만들기 · 1장»(다시 대기열) 화면 흐름 그대로 · 서버는 새 자식 잡(부모 성공분을 inherited 로 같이 보여 줌) 코덱스 3차 — 옛 잡 날짜·만료·환급이 꼬이지 않게
다시 만들기 = 같은 입력 재사용 채택 → 입력 4장 7일 보관(옛 설계 = 완료 즉시 삭제) §5-5
A·B 엔진 무작위 + 블라인드 투표 채택(§5-4 · 스키마 pick·seen·slot k) B2 를 표 수로 결정
문구 띠 = 아래 56px 덧붙임 Q8 기본값 ①(페페 확인) 가구 무가림
1인 하루 10~12장 시작 제안 B1 20장 유지(기본값 ⑤ · env 조정) 페페 결정 사항
W-CUT·W-HID·E-EMPTY 를 P1 로 채택(§4-2) 헛요청 1건 = 공용 하루치 4%
탭 제목 «(완료)» · 엔진 비교 요약(관리자) · 100% 확대 · 삭제 버튼 P3 · 엔진 비교 요약만 P2 v1 범위 관리

8-4. 우선순위 · 구현 뒤 잴 것


9. DB 스키마 변경 — migrations/patch64_pncad_stage.sql (계획만 · 실행 금지)

새 테이블 4개 · 기존 ALTER 0. 적용 = 위험작업 → 테스트 RDS = 총괄 승인(G1) 뒤 SSM · 운영 RDS = 페페 채팅 승인(운영배포 직전). ensure_table() 안 씀(DDL 오류를 삼킨다) → 테이블이 없으면 기능은 «준비 안 됨»으로 닫힌다.

CREATE TABLE pncad_stage_job (
  job_no        INT AUTO_INCREMENT PRIMARY KEY,
  idem_key      VARCHAR(40)  NOT NULL,
  input_sha     CHAR(64)     NOT NULL,
  user_no       INT          NOT NULL,
  user_name     VARCHAR(100) NULL,
  scene_no      INT          NULL,                          -- pncad_scene 참조(FK 없음)
  scene_name    VARCHAR(200) NULL,                          -- 요청 시점 작업명(현장명) — 파일명·최근 목록
  scene_line    VARCHAR(4)   NULL,                          -- K|F
  parent_job    INT          NULL,                          -- «같은 설정으로 다시»(again)의 원본 잡
  kind          VARCHAR(6)   NOT NULL DEFAULT 'new',        -- 🆕U1 new|again|retry
  ks            VARCHAR(2)   NOT NULL DEFAULT 'AB',         -- 🆕U1 만들 슬롯 키 AB|A|B
  ymd           DATE         NOT NULL,                      -- KST(1인 하루 한도)
  n_req         TINYINT      NOT NULL DEFAULT 2,
  n_ok          TINYINT      NOT NULL DEFAULT 0,
  preset        VARCHAR(30)  NULL,
  opts          TEXT         NULL,                          -- 칩 JSON
  memo          VARCHAR(200) NULL,                          -- 60자 이하
  engine_pref   VARCHAR(16)  NULL,                          -- 관리자만: chatgpt|nano (NULL = 기본 1+1)
  prompt_ver    VARCHAR(12)  NOT NULL,
  status        VARCHAR(12)  NOT NULL DEFAULT 'queued',     -- queued|running|done|partial|failed|canceled|expired
  phase         VARCHAR(16)  NULL,
  expire_at     DATETIME     NOT NULL,                      -- queued 만료(작성 + 12시간)
  cancel_req    CHAR(1)      NOT NULL DEFAULT '0',
  worker_id     VARCHAR(40)  NULL,
  lease_token   CHAR(32)     NULL,
  lease_until   DATETIME     NULL,                          -- running = 임대 기한 · queued = 재시도 가능 시각
  claim_at      DATETIME     NULL,                          -- 🆕U1 현재 시도 claim 시각(최대 처리시간)
  attempts      TINYINT      NOT NULL DEFAULT 0,
  cost_krw      INT          NOT NULL DEFAULT 0,
  pick          VARCHAR(4)   NULL,                          -- 블라인드 투표 A|B|tie
  seen          CHAR(1)      NOT NULL DEFAULT '0',          -- 결과를 봤나(칩·«새 결과»)
  flags         TEXT         NULL,
  error         VARCHAR(500) NULL,
  inputs_purged CHAR(1)      NOT NULL DEFAULT '0',          -- 입력 4장 7일 뒤 삭제 · 0|P(지우는 중)|1
  files_purged  CHAR(1)      NOT NULL DEFAULT '0',          -- 결과 30일 뒤 삭제 · 0|P|1
  reg_date      DATETIME     DEFAULT CURRENT_TIMESTAMP,
  start_date    DATETIME     NULL,
  end_date      DATETIME     NULL,
  UNIQUE KEY uk_user_idem (user_no, idem_key),
  KEY idx_status (status, job_no),
  KEY idx_user_day (user_no, ymd),
  KEY idx_user_status (user_no, status, seen),
  KEY idx_reg (reg_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE pncad_stage_slot (                             -- 결과 1장의 claim 1회 = 1행(lease 만료 뒤 재claim = attempt+1 새 행)
  slot_no       INT AUTO_INCREMENT PRIMARY KEY,
  job_no        INT          NOT NULL,
  k             CHAR(1)      NOT NULL,                      -- A|B (엔진은 잡마다 무작위 배정)
  attempt       TINYINT      NOT NULL,                      -- = 그 claim 때의 job.attempts
  engine        VARCHAR(20)  NOT NULL,                      -- chatgpt_web|nanobanana_web|openai_api|gemini_api|mock
  model         VARCHAR(64)  NULL,
  ymd           DATE         NOT NULL,                      -- KST(엔진별 하루 한도)
  status        VARCHAR(10)  NOT NULL DEFAULT 'planned',    -- planned|gen|compose|ok|fail|unknown
  ms            INT          NULL,
  final_sha     CHAR(64)     NULL,                          -- 결과 멱등(같은 해시 재전송 = 기존 성공)
  cost_usd      DECIMAL(10,5) NULL,
  cost_krw      INT          NOT NULL DEFAULT 0,
  flags         TEXT         NULL,                          -- align 잔차·furn_mad·ghost 등
  error         VARCHAR(500) NULL,
  done_at       DATETIME     NULL,
  UNIQUE KEY uk_job_k_att (job_no, k, attempt),
  KEY idx_engine_day (engine, ymd, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE pncad_stage_worker (                           -- 미니PC 워커 상태(ping)
  worker_id     VARCHAR(40)  NOT NULL PRIMARY KEY,
  target        VARCHAR(8)   NOT NULL,                      -- test|prod
  version       VARCHAR(20)  NULL,
  engines       TEXT         NULL,                          -- 엔진별 state JSON
  last_ping     DATETIME     NOT NULL,
  alert_at      DATETIME     NULL                           -- 무응답 알림 1회 기록
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE pncad_stage_month (                            -- 유료 엔진 원장(지금 상한 0 · 결제 붙일 때 사용)
  ym            CHAR(7)      NOT NULL PRIMARY KEY,
  cap_krw       INT          NOT NULL,
  spent_krw     INT          NOT NULL DEFAULT 0,
  reserved_krw  INT          NOT NULL DEFAULT 0,
  alert80       VARCHAR(8)   NOT NULL DEFAULT 'none',
  alert100      VARCHAR(8)   NOT NULL DEFAULT 'none',
  modify_date   DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

10. 서버 외 설정 변경(게이트)

# 변경 위치 승인
G1 patch63 테이블 4개(🆕번호 62→63) 테스트 RDS → 운영 RDS 테스트 = 총괄 승인 · 운영 = 페페 채팅
G2 PFS_STAGE_TOKENS(해시) · PNCAD_STAGE_ENABLED 등 env /home/ubuntu/pfs-test/.env.production → 운영 /home/ubuntu/pfs/.env.production 테스트 = 총괄 · 운영 = 페페 채팅
G3 미니PC 예약작업 2개 · 전용 CDP 포트·런처·프로필 복사 · C:\dev\.secrets\.env 2줄(OpenAI 키 이관 · 워커 토큰) 미니PC 총괄(winledger·portgate 장부 등록)
G4 계정 설정(Q5) · (Q7 ② 면) 나노바나나용 별도 무료 구글 계정 신설·1회 로그인 ChatGPT·Gemini 계정 페페
G5 운영배포 deploy-live 페페 채팅
- EC2 systemd 서비스·서버 키 배치 = 지금 없음(v1 G2 소멸).

11. 작업단위 (U0~U9)

U 이름 파일(전체 경로) 장부 · worker 검증 의존·게이트
U0 웹 엔진 프로브 + 키 이관 C:\dev\_work\pncad-ai-staging\u0\(프로브 스크립트·결과) · C:\dev\.secrets\.env(OpenAI 키 «추가만» · 값 출력 0) L3 · worker-opus-high 기존 프로브 입력(+5:4 띠 만든 판)으로 ChatGPT 웹 1장 · 나노바나나 웹 1장(무료 · 9334 는 cdplease 로 잠깐) → 출력 크기·비율·소요·워터마크 위치·기하 보존(가구 윤곽 정합 오차 px) 표 · 편집 문구 2종 비교 즉시(무료) · Q2
U1 서버 원장·대기열 코어 C:\dev\pfs\migrations\patch63_pncad_stage.sql(🆕62→63) · C:\dev\pfs\api\pfs\pncad\dao\pncad_stage_dao.py · C:\dev\pfs\api\pfs\pncad\stage\{conf,quota,plan,prompt,budget,storage}.py · C:\dev\pfs\common\database\mysql_util.py(영향 행 수 메서드 «추가만») · C:\dev\pfs\tests_cody\test_pncad_stage_{quota,plan,prompt}.py L4 · worker-opus-xhigh 가짜 트랜잭션 단위: 1인 20장(사용 계산식 — 부분·취소·만료 반환)·공용 잔량(dayLeft·bind)·엔진별 하루·사용자당 진행 1건(생성 3경로 동시 호출 — 사용자 잠금으로 초과 0)·멱등 조회가 busy 보다 먼저·동시 20스레드 경쟁(초과 0)·claim 1회성·A/B 무작위 배정·lease 만료 재claim = attempt+1 새 행(앞 시도 소비 유지)·늦은 옛 결과 409·n_ok 1회 증가·재시도 = 자식 잡(부모 불변)·정리 3단계 ↔ again 직렬화·12시간 만료·KST 경계·API 예산 0 거절 · py3.6 문법 검사 · 코덱스 리뷰 즉시 · DB 적용 = G1
U2 서버 API(사용자 + 에이전트) C:\dev\pfs\api\pfs\pncad\controller\pncad_stage_controller.py · C:\dev\pfs\api\pfs\pncad\stage\auth.py · C:\dev\pfs\web\__init__.py(에이전트 접두 1곳 · TREAD 방식) · C:\dev\pfs\api\config.py(네임스페이스 1줄) · C:\dev\pfs\tests_cody\test_pncad_stage_{auth,controller}.py L4 · worker-opus-xhigh(권한 경로) 컨트롤러 전역 request/session 가짜 단위(py3.9 venv 에서 Flask() 생성 금지 — rules) · 토큰 없음/틀림/다른 접두 401 · lease_token 불일치 409 · 업로드 위변조(크기·형식·잡 번호·가구 픽셀 불일치) 거절 · 소유권 · Origin · 외부 계정 403 · 일반 사용자 응답에 엔진명 0(eng·why·engines = 관리자만) · again(입력 7일)·retry·pick(pickable 일 때만)·seen·recent pending=1
🆕✅ 26.10.13 구현 · worktree C:\dev\_wt\pfs-ai-stage-u2(ai-stage-u2-1013 · U1 위) 로컬 커밋 c39e7fce — 컨트롤러(16경로) · stage\auth.py · web 문 1줄 · config 1줄 · DAO(claim_key 멱등 · 슬롯 임대 증명 · recover() 늦은 회수 · ready 경쟁 수정 등) · 검증 = 실제 앱(web.create_app) + Flask test client + 임시 MariaDB 41 + auth 32 + U1 79 = 151 통과(시험 venv C:\dev\_work\pncad-ai-staging\u2\venv38 = py3.8 + pfs 판 + Werkzeug 0.15.6) · pfs venv db 45/45 · 변이 12종 검출 · photo-ai.js 를 실서버에 붙인 E2E 15/15(C:\dev\_work\pncad-ai-staging\u2\e2e_real_flask.py + real_server.py 127.0.0.1:13398) · 코덱스 1 · vff 반박(P1 1·P2 2·P3 7 반영) · U3 계약 = §5-3 🆕
U1
U3 미니PC 워커 C:\dev\tools\pncad-stage-worker\{worker.py,client.py,engines\*.py,run_test.vbs,run_prod.vbs,install_task.py,README.md} · C:\dev\tools\imagegen\{imagegen.py,chatgptimg.py}(하위호환 인자만) · C:\dev\tools\cdp-profiles\launch_pncad_stage.bat·_hidden.vbs · 장부 C:\dev\tools\portgate\PORTS.md·C:\dev\tools\winledger\WINDOWS.md L4 · worker-opus-xhigh(공유 계정·상주·공용 도구) mock 엔진 + 가짜 PFS(로컬 http)로 ping·claim·lease·heartbeat(슬롯 gen/compose)·취소·슬롯별 즉시 업로드(A 먼저 끝나면 A 만)·result 재전송 멱등·fail 재큐 · 엔진 상태 전이(limit/login/broken) · 회귀 chatgptimg.py --demo · 카드 굽기 기본값 불변 확인 · 예약작업 숨김(창 0) · 코덱스 리뷰 U2 계약 · G3
U4 합성·정합 C:\dev\tools\pncad-stage-worker\{compose.py,align.py,assets\disclaimer_w1536.png,make_disclaimer.py} · …\tests\test_compose.py L3 · worker-opus-high 골든: α=255 화소 = 가구 원본 그대로 · 0<α<1 헤일로 0 · 인공 이동/배율(±3%) 넣은 AI 판 정합 복원 ≤ 1px · 띠 크롭 후 워터마크 0 · 최종 1536×1080 · 문구 띠 픽셀 해시 · PNG 메타 = Description 하나(엔진명 0) · 썸네일 198×132 · U0 실제 산출로 육안 U0 결과(엔진별 크기·정합 성향) · Q8
U5 클라 렌더 레이어 C:\dev\pncad\photo.js(stageLayers · stagePreview · 누적 코어 공용 · still() 옵션 인자 · fitShadow(dir)·aoOverlay(camera)) · C:\dev\pncad\tools\_e2e_stage_layers.py L3 · worker-opus-high E2E local(PNCAD_LOCAL_* env): 1536×1229 4장 · α 범위 · α=255 base≈furn(±3) · 위 1024 = 점선 틀 안(틀 모서리 표식 대조) · 창 칩 = 이미지 기준(카메라 90° 돌려도 그림자 쪽 유지) · stagePreview(null) 원복(카메라·톤매핑·재질·바닥색·gWall·광원) · «렌더 시작» 결과 불변 · pageerror 0 · 회귀 _e2e_photo.py·_e2e_3dui.py 🔴 그림자 v3 가 pncad master 에 병합된 뒤
U6 클라 UI C:\dev\pncad\photo-ai.js(신규) · C:\dev\pncad\app.html(마크업·CSS·배선 1줄) · C:\dev\pncad\tools\{sync_to_pfs,_deploy_hash,_verify_deploy}.py(FILES 3곳) · C:\dev\pncad\tools\_e2e_stage.py L3 · worker-opus-high · 처방 원문 C:\dev\_work\pncad-ai-staging\ui_prescription_web-designer_1013.md + §8 조정 표 · 착수 전 web-designer 재확인(훅) E2E local(API route 모의): §8-4 의 8항목 + 대기(queue·offline·engine)·부분·취소(대기·그리기)·만료·1인/공용 한도·busy·완료·again · 칩·최근 목록 복원(새로고침) · W-CUT 투영 경고 · 키보드·aria-live · 네이티브 select/datalist 0 · pageerror 0 §5-2 계약 고정 · ?stagefake=1(가짜 레이어)로 U5 전 병렬 · photo.js 수정 0
U7 테스트 통합 pfs master · 테스트 RDS · 테스트 env · 미니PC 워커 --target test L4 · worker-opus-xhigh G1·G2·G3 → sync_to_pfs → pfs master push → gh run watch <id> --exit-status → _deploy_hash.py test → _e2e_stage.py test(mock) → 실제 웹 엔진 1회(1+1) → 사이트맵 cd C:\dev\pfs && python tools/sitemap/build_sitemap.py --dump U1~U6
U8 실사용 샘플 → B2 C:\dev\_work\pncad-ai-staging\samples\ · devplan 샘플 절 L3 · worker-opus-high 실제 작업본 3개(주방 K · 일반 F · 장 많은 장면) × (ChatGPT 1 + 나노 1) × 프리셋 2 = 12장(무료) · 정합·furn_mad·광원 일치·소요 표 → 페페 기본 엔진 결정 U7 · 그림자 v3 반영 뒤
U9 운영 반영 운영 RDS · 운영 env · deploy-live 선별 포워드 · master push · 워커 --target prod 작업 활성 L4 · worker-opus-xhigh 페페 채팅 «운영배포» 뒤만 · 해시 대조 · 운영 1회(페페 확인) · 사이트맵 재빌드 U8 · G1·G2 운영 · G5

12. 위험 · 함정

# 위험 대응
R1 🔴 소비자 약관 — 웹 자동화 금지 → 계정 제한. OpenAI 약관은 자동·프로그램 방식 출력 추출 금지 · Google 은 반복 위반 시 제품 또는 구글 계정 제한 가능(코덱스 2차). delix0731 ChatGPT 계정 = 코덱스 상의(gptapp ask 인증)·카드 굽기와 같은 계정 · delix0731 구글 = 페페 주 계정(Gmail). 프로필 복사는 장애 격리가 아니다 엔진별 하루 상한(Q6) · 제출 간 20초+ · 탭 1 · 인증·CAPTCHA·경고 → 즉시 정지(자동 반복 금지) · Q7(나노바나나 별도 계정 · 로컬 모델 PoC · 반자동) · 카드 굽기가 이미 같은 계정을 자동화 중(증분 위험) · 페페 1회 고지(총괄)
R2 학습 사용(소비자 웹) Q5 — 설정 끄기
R3 미니PC 의존(꺼짐·재부팅·절전) 대기열 · 12시간 만료 · 무응답 알림 · 화면 상태 표시
R4 웹 결과의 기하 어긋남(픽셀 보존 보장 없음) 정합 보정(ECC) · align_low 표시 · U0 실측으로 임계 · 심하면 그 엔진 제외
R5 웹 UI 변경으로 셀렉터 깨짐 broken 상태 + 텔레그램 · 다른 엔진으로 자동 전환
R6 구독 한도 경합(카드 굽기와 같은 계정) 엔진별 하루 상한 · 한도 문구 감지 → limit
R7 조명 이질감 창 칩 = 3D 태양 방향 · 중립 화이트밸런스
R8 소품이 가구 테두리에 걸침 문안 금지 + 재합성(가구가 위) + furn_mad + «같은 설정으로 다시 만들기»
R9 오픈 선반 속은 빈 채(가구 픽셀 보존) 한계 명시
R10 벽 여러 개 작업본 = 펼친 일자 방으로 연출 패널 안내(한 벽씩·다른 벽 장 숨김 — 숨긴 장은 렌더 제외)
R11 높은 각도 화면은 사진 구도와 다름 3:2 틀 미리보기 · «눈높이 구도» 칩은 U8 뒤
R12 그림자 v3 와 photo.js 충돌 U5 를 v3 병합 뒤로
R13 공용 도구(imagegen·chatgptimg) 수정이 카드 굽기를 깨뜨림 인자 추가만(기본값 불변) · --demo 회귀
R14 9334 공유(페페 수동) 전용 포트·프로필 복사
R15 나노바나나 워터마크 5:4 띠에 떨어뜨려 잘라냄(U0 로 위치 확인)
R16 AI 기본법 표시 범위 하단 문구 + PNG 메타. 법 적용 범위 확인은 운영 전 1회
R17 http 테스트 주소에서 crypto.randomUUID 없음 idem_key = crypto.getRandomValues
R18 메모 개인정보 안내 문구 · 30일 뒤 파일 삭제(DB 메모는 남음 — 넣지 말 것)
R19 공용 하루 ≈ 25회가 작다 — 헛요청(틀 밖 잘림·숨긴 장·빈 장면)이 남의 몫을 깎는다 W-CUT·W-HID·E-EMPTY 사전 경고(§4-2) · 사용자당 진행 1건 · 클릭 즉시 비활성 + idem_key
R20 폴링·칩 타이머 누수(패널 열고 닫기·작업본 전환) 폴링 1개만 · 끝난 상태에서 정지 · 탭 숨김 정지 · U6 검증 ⑧
R21 서버 디스크(입력 7일 + 결과 30일) 최대치 ≈ 5GB 계산 · U7 df -h · 모자라면 결과 14일

보고만(범위 밖): ① C:\dev\pfs\web\__init__.py L69 — 운영 PFS_SECRET_KEY 누락 시 개발용 기본키로 기동(코덱스 1차 지적) ② 포트 9350 충돌(gptapp.py 앱 CDP ↔ claude-console 크롬) ③ OpenAI 키 사본 2곳(C:\dev\SARA\.env·C:\dev\tools\agent-dialogue-hub\.env) = 단일출처 위반(Q2 이관 뒤 삭제는 승인).


13. 부록 — 유료 API 엔진 (결제 붙일 때)

13-1. 실행 위치(그때 결정)

가) 같은 미니PC 워커가 API 도 호출 — 키 .secrets 단일 출처 유지 · 서버 변경 0 · 대신 미니PC 의존. 나) EC2 systemd 워커(Q1 ①) — 미니PC 꺼져도 동작 · 서버에 키 사본(.env.production) · py3.6 이면 SDK·numpy 없음(요청·Pillow) 또는 새 Python 설치.

13-2. 어댑터·원장(v1 설계 요약 — 코드는 U3 에 «꺼진 채» 들어간다)

항목 OpenAI Gemini
호출 POST /v1/images/edits · gpt-image-2 · n=2 · size=1536x1024 · 마스크(가구 α=255 · 껍데기 투명) · input_fidelity 금지 POST v1beta/models/gemini-nano-banana-2.1:generateContent · responseModalities:['IMAGE'] · imageConfig{aspectRatio, imageSize} · 2회
비용 usage 범주별(입력 텍스트·이미지 · 출력 이미지) usageMetadata 범주별(사고 토큰 포함)
오류 본문 있는 4xx/5xx = 미과금 · 전송 후 타임아웃 = unknown(예약액 확정·재호출 금지) 같음
- 원장: 월 행 INSERT IGNORE → FOR UPDATE 로 직렬화 · «허용 설정의 최대 원가» 예약(gpt-image-2 medium 2장 200원 · high 710원 · 나노 2K 290원) · usage 정산 1회(settled 조건부) · 80% 알림 outbox · 100% 차단 · 단가판·환율 잡 고정 · 공급자 하드 리밋 2차.

13-3. 칩 문구 · 프리셋

웹·API 엔진 공용이라 §5-6 으로 옮겼다(칩 토큰·문구·STYLE·TONE) · 칩 기본값 = §8. v1 의 6번째 «대리석 클래식»은 처방의 «클래식»(벽지·어두운 마루·저녁)으로 바뀌었다.


14. 코덱스 교차검토

14-1. 1차(v1 유료 API 설계 · 26.10.13 17:2x · 원문 …\scratchpad\codex_review.txt) → 반영

지적 반영
월 행 멱등 생성·잠금 순서 · 한 트랜잭션 · rollback · 영향 행 수 §13-2 · §11 U1
멱등키+입력 해시 · 전송 후 자동 재호출 금지 · 이중 정산 방지 §5-2 · §13-2
평균 대신 최대 예약 · 단가·환율 고정 §13-2
일 장수 = 완료+예약 원자 · 호출별 기록 · KST 고정 §5-5 · pncad_stage_slot
uwsgi 스레드 유실 → 독립 워커 + DB 큐 · lease · heartbeat §6 미니PC 워커 · §5-3
env·600·로그 가림 · 소유권·Origin·허용목록·크기 제한 · 클라 값 불신 §5-2 · §5-3
알림 outbox · ensure_table 재사용 금지 §5-5 · §9
공급자 하드 리밋은 알림과 별개 §2-3

14-2. 2차(v2 미니PC pull 구조 · 26.10.13 18:3x · 원문 …\scratchpad\codex_review2.txt) → 반영

결론 = «pull 방향은 적절. 핵심 구멍은 생성 후 장애의 중복과 웹 자동화의 계정 의존성». 유료 0원은 서버 거절 + 워커 차단 둘 다. | 지적 | 반영 | |---|---| | 파일 락은 같은 PC 만 막는다 → claim·엔진 수량 예약을 한 DB 트랜잭션으로 · attempt·임대 식별자 | §5-3 claim · §5-4 · §6-1 | | 모든 에이전트 호출에 토큰 이름+attempt+임대 식별자+유효기간 · 만료 임대 부활 금지 · 하트비트 독립 · 최대 처리시간 | §5-3 · §6-1 하트비트 | | 생성 뒤 업로드 전 장애 → 슬롯별 로컬 진행기록·대화 주소·다운로드 파일로 재개 · 불확실하면 재생성 말고 확인 대기 | §5-5 lease 만료 · §6-1 진행 기록 | | 업로드 위변조 → 임대·소유권 검사 · 서버 지정 이름 · 크기·픽셀 제한 · 디코딩·재인코딩 · 원본 픽셀 보장은 서버가 마스크 영역 검증 | §5-7 서버 검증(matte=255 화소 = furn) | | _AGENT_PATHS 금지 → 별도 인증 분기 · 6경로+메서드 명시 · 핸들러 재검사 · 이름별 폐기·감사 | §5-3 인증 | | 결과 멱등 → 잡+슬롯 유일 · 같은 해시 = 기존 성공 · 다른 해시 = 충돌 · 완료 덮기 금지 | §5-3 result | | 1인 20장 = 이미지 수 · 엔진 수량 원자 예약 · 대체 엔진은 서버 승인 · 불명 실패는 수량 반환 보류 · 워커도 유료 차단 | §5-4 · §6-1 유료 차단 | | 대기열 굶주림 → 실행 가능 잡만 · 사용자별 순환 · 엔진별 백오프 · 독성 잡 격리 · 끝난 1장 보존·빠진 1장만 재시도 | §5-4 · §5-5 공정성 · §5-3 fail | | 게으른 정리는 요청이 없으면 안 돈다 → ping 에서도 제한량 처리 · 12시간 만료는 queued 만 | §5-5 | | 정합은 생성 배경에만 · 잔상 검사 · 띠 크롭은 원본 보존·범위 검증 | §6-2 3·5·8 | | 같은 구글 로그인 의존 = 장애 전파 · 프로필 복사는 격리 아님 → 엔진별 회로 차단 · 인증 오류·CAPTCHA 자동 반복 중단 · 수동 복구 경로 | §6-1 회로 차단 · Q7 | | (대안) 반자동(사람이 웹 생성, 워커는 합성) 또는 보유 장비 로컬 모델 | Q7 ③④ · U-L |

14-3. 3차(화면 처방 반영분만 · 26.10.13 19:xx · --cd C:\dev\seedchain\projects\pncad · 원문 …\scratchpad\codex_review3.txt) → 반영

지적 반영
진행 1건 경쟁 — stage·again·retry 가 동시에 검사하면 다 통과 §5-2 «잡을 만드는 3경로 공통 순서» — 사용자 잠금(GET_LOCK) 안에서 검사·예약 확정
재시도가 옛 잡의 ymd·expire_at 에 묶임 · 성공 1 + 재시도 1 집계 불명 · 만료 시 성공분까지 환급 재시도 = 새 자식 잡(자기 ymd·expire_at·n_req) · 부모 불변(§5-2)
again·retry 요청 키 없음 · 기존 요청 조회가 busy 보다 늦음 세 경로 idem_key 필수 · 조회를 맨 앞으로(§5-2)
final_sha 만으로 옛 시도의 늦은 결과를 못 막음 · n_ok 중복 증가 현재 임대만 수락 · 조건부 UPDATE 영향 1행일 때만 n_ok+1(§5-3) · 슬롯 유일키 = job·k·attempt(§9)
정리 ↔ again 입력 복사 경쟁 · 삭제 도중 장애 3단계 표식 0 → P → 1 · 부모 행 잠금과 직렬화 · P 재개(§5-5)
재시도 뒤 pick 무효화·seen 초기화 · 늦은 seen 끝난 잡의 결과는 바뀌지 않는다(재시도 = 새 잡) → pick·seen 은 잡마다 고정

15. 출처