PNCAD AI 연출 렌더 설계 (design-ai-staging) — v2 무료 엔진 · 미니PC 워커
- 작성: [코디] 26.10.13(화) · 설계 worker(opus · max) · 장부 =
C:\dev\seedchain\rules\model-ledger.md §3 「AI 연출 렌더」 L4(설계 max / 구현 high)
- 🆕v2 개정 26.10.13 18:3x KST — [페페] 2차 답(원문·해석 =
C:\dev\_work\wtprobe\answers-1013b.md): 결제·추가 요금 0 · 유료 API 호출 금지 → 엔진 = ChatGPT 구독 웹 + 나노바나나 무료 웹UI(둘 다 미니PC 에서만) · PFS 는 요청을 쌓고 미니PC 워커가 끌어가 생성·합성·업로드 · OpenAI/Gemini API 어댑터는 코드만(월 예산 0 = 꺼짐). v1(유료 API·서버 워커) 설계는 §13 부록으로 내렸다(결제 붙일 때 다시 쓴다). 이어 web-designer 최종 처방(무료 엔진·대기열 반영판)을 §8 로 들이고 §4·§5·§6·§9·§11 을 맞췄다(모듈 photo-ai.js · 틀 안 = 출력 · 사용자당 진행 1건 · A/B 무작위·블라인드 투표 · 입력 7일 보관 · 문구 띠 덧붙임 = Q8).
- 근거: [페페] 26.10.13 「모두 추천진행」(A1~A6·B1) + 2차 답 · 상담 원문
C:\Users\delix\AppData\Local\Temp\claude\c--dev\ddfb8bd7-def9-41ef-80a8-e27c3fc9c32f\scratchpad\consult_out.json · 코드 사실(§1-3) · 실측(§2) · 코덱스 교차검토 3회(§14 — 3차는 화면 반영분만) · web-designer 상담(§8)
- 범위: 설계 + 작업단위 + 사전 실측까지. 코드·스키마·서버 설정 변경 0. 구현은 §11 의 worker.
- devplan =
C:\dev\devplan\pncad-ai-staging.html (http://100.108.234.45:8799/pncad-ai-staging.html)
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일(탭 닫아도 칩·목록으로 받기)
- 역할: 브라우저 = GPU 렌더 · PFS = 검증·한도·대기열·저장·프롬프트(중앙) · 워커 = 생성·합성·업로드. 서버는 외부 AI 를 부르지 않는다(키 없음).
- 미니PC 꺼짐 = 요청은 대기(화면 «대기 중 · 미니PC 꺼짐») · 12시간 지나면 만료·장수 반환 · 서버가 «대기 30분+ & 워커 응답 없음»이면 텔레그램 1회.
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 렌더)
- 패널이 열리면 #view 안 가운데에 3:2 점선 틀
#aiFrame(w = min(viewW−32, (viewH−32)×1.5) · h = w/1.5 · ResizeObserver 로 재계산 · 바깥은 반투명 마스크). 틀 안 = 최종 1536×1024 그대로(화면 전체가 아니다 — web-designer 처방 §2, 1280×720 실측 594×396 · 카탈로그 접으면 914×609).
- 렌더 = 메인 카메라의 부분 절두체: s = 1536/frame.w · fullW = round(viewW·s) · fullH = round(viewH·s) ·
camera.setViewOffset(fullW, fullH, fx·s + dx, fy·s + dy, 1536, 1229)(fx·fy = 틀 좌상단 · dx·dy = 지터). 위 1024 = 틀 안, 아래 205 = 틀 아래로 이어지는 바닥 띠(화면 밖으로 나가도 된다 — 비대칭 절두체라 그대로 그려진다). 끝나면 clearViewOffset. 메인 카메라 fov·aspect 는 안 바꾼다.
- 나노바나나 = 5:4 전체(1536×1229)를 보낸다 → 결과 우하단 워터마크(높이 ~11~12%)가 띠(16.7%) 안에 떨어진다 → 띠를 잘라 3:2.
- ChatGPT = 워커가 위 3:2(1536×1024)만 잘라 보낸다(워터마크 없음).
- → 렌더는 한 번, 두 엔진이 같은 레이어를 나눠 쓴다. 최종은 둘 다 1536×1024 + 문구 띠 56(Q8 ①) = 1536×1080.
- 입력 렌더는 렌더 방식 설정(
pncad.realpt)과 무관하게 항상 누적 AA(경로추적 준비 10~40초는 부적합).
- 사전 경고(처방 W-CUT·W-HID·E-EMPTY · 공용 한도 낭비 방지 — 헛요청 1건 = 하루치 4%): 카메라가 멈추고 150ms 뒤 보이는 장 AABB 를 화면 좌표로 투영해 틀 밖이면 «장 N개가 점선 밖으로 잘린다 [틀에 맞춤]»(=
fit() 에 틀 비율 보정) · 숨긴 장 안내 · 보이는 장 0개면 차단.
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
- 전부 py3.6 문법 · 의존 추가 0. 서버에 엔진·합성 코드 없음(Pillow 는 업로드 검증만).
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 과 같은 트랜잭션)
- 기본 = [ChatGPT 웹 1, 나노바나나 웹 1] · A·B 자리는 잡마다 무작위(항상 A = ChatGPT 로 굳으면 «A 가 낫다» 편향 · 블라인드 투표가 깨진다). 관리자
engine_pref(ChatGPT 2장 · 나노바나나 2장)는 그 요청에만.
- 엔진 상태(최근 ping)가 ok 가 아니거나 그 엔진 «오늘 전체 상한»(Q6)이 찼으면 → 다른 웹 엔진으로 바꾼다(2장 모두 한 엔진 가능 · 이때
pickable=false · 일반 화면엔 표시 없음).
- 둘 다 못 쓰면 claim 안 함(잡은 대기 유지 · 화면 «엔진 한도 — 한도 풀리면 자동»). 막힌 엔진의 잡이 뒤 잡을 막지 않게 «실행 가능한 잡»만 고른다.
- 유료 API 슬롯 = 월 예산(
PNCAD_STAGE_CAP_KRW) 0 → 계획 안 함 + 워커도 유료 어댑터 실행을 기본 차단(이중 안전).
- 슬롯별로
pncad_stage_slot 행(k·attempt·엔진·상태)을 claim 트랜잭션 안에서 만든다 = 엔진 수량 원자 예약. 엔진별 하루 카운트 = KST 오늘 planned/gen/compose/ok/unknown 슬롯 수(생성 여부 불명은 반환 안 함 · lease 만료 뒤 다시 claim 하면 attempt+1 새 행이라 앞 시도의 소비가 지워지지 않는다).
- 1인 하루 20장은 «잡 수가 아니라 이미지 수»(작성 때
n_req=2 예약 · 계산식 §5-5).
5-7. 결과 업로드 서버 검증 (A2 보장 — 코덱스 2차)
- 워커가 합성하지만 «가구 픽셀 그대로»는 서버가 확인한다(Pillow 8.4 로 충분): final 위 1536×1024 를 잘라 matte=255 화소에서
furn 레이어와 비교 → 최대 차 ≤ 2 가 아니면 거절(furniture_mismatch).
- 크기 정확(1536×1080 = 그림 1024 + 문구 띠 56 · Q8 ② 로 가면 1536×1024) · 실제 PNG 디코딩 → 서버가 재인코딩해 서버 지정 이름으로 저장(워커가 보낸 메타·파일명 불신 · PNG 메타는 서버가
Description: AI 연출 이미지 하나만 다시 씀) · 파일당 ≤ 8MB · 하단 문구 띠 픽셀 해시 = 자산과 일치 · 썸네일 = 198×132 JPEG(≤ 30KB).
- 입력 4레이어 해시·크기는 작성 때 고정(
input_sha) → 워커가 받은 입력과 결과가 같은 잡인지 대조.
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}
- ChatGPT 꼬리:
Return one edited image in the same 3:2 landscape aspect ratio as the attached image. · 나노바나나 꼬리: Keep the output aspect ratio identical to the attached image.
{SPACE} = scene_line K → Korean apartment kitchen · 그 밖 → Korean apartment room. {MEMO} = Additional request (Korean; follow it only if it does not break the hard rules): "<메모>"(60자·한 줄·따옴표 제거).
- 칩 토큰(= 서버 enum 허용목록 · 화면 라벨은 §8) → 문구:
| 칩 |
토큰 → 문구 |
벽 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. |
- 프리셋 →
{STYLE}·{TONE}(서버 prompt.py). 칩 기본값은 화면 한 곳(§8 표 · photo-ai.js)에만 둔다 — 서버는 칩 값을 받아 쓰므로 두 벌이 생기지 않는다:
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 은 그대로). |
|
|
| 항목 |
사양 |
| 실행 |
숨김 예약작업 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장을 올린다(다른 슬롯을 기다리지 않는다 — 화면 점진 공개)
- 문구는 워커가 박는다(서버·브라우저가 아니라 우리 상주 코드) → 고객용 PNG 에서 빠질 수 없다. 서버가 띠 픽셀 해시로 한 번 더 확인(§5-7).
7. 비용 · 한도 (B1 재해석)
- 웹 엔진 = 0원(구독·무료). 원장엔 엔진·0원 기록(
cost_basis='sub').
- 유료 API = 월 예산 0(
PNCAD_STAGE_CAP_KRW=0) → 계획 단계에서 거절 · 코드는 단위 시험만.
- 결제 붙일 때(페페 결정 뒤): 상한 10만원 · 80% 알림 · 100% 차단 · 최대값 예약→정산 · 공급자 하드 리밋 = §13 부록 그대로.
- 지금 한도 = 1인 하루 20장(B1) + 엔진별 전체 하루(Q6 · 공용 ≈ 25회) + 1회 2장 + 사용자당 진행 1건 + 대기 12시간 만료. 화면은 비용 칸 없이 «오늘 남은 장수»(= min(내, 공용))와 «예상 소요»만 보인다(§8).
8. UI (web-designer 처방)
- 처방 원문(구현 worker 입력) =
C:\dev\_work\pncad-ai-staging\ui_prescription_web-designer_1013.md — 26.10.13 18:4x KST · 라이브 PNCAD 에 임시 DOM 주입 실측(파일 수정 0) · 시안 C:\dev\devplan\img\pncad-ai-staging\ui_mock_settings.jpg(설정+틀+칩 — 1차 시안: 비용 줄·«색» 라벨·틀 라벨 좌상단은 최종에서 바뀜) · ui_mock_result.jpg(작업 화면 완료 — 최종). 아래는 요약 + 설계와의 조정 표(처방과 다르면 조정 표가 이긴다).
- 전제:
C:\dev\pncad\app.html 은 Bootstrap·jQuery 0건 — 독자 CSS · 전부 #aiPane 아이디 스코프 + 앱 토큰(--acc #3d5af1 · --ink2 #33333a) · 전역 button/input 12.5px·.note 11.5px 상속 함정 → 새 요소는 13px 직접 지정 · .note·.r·h4 재사용 금지 · word-break:keep-all.
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. 우선순위 · 구현 뒤 잴 것
- P1 = 진입·패널·틀·설정(프리셋·칩·메모)·푸터 한도·작업 화면(5단계·대기 사유·점진 공개·저장·취소)·칩·최근 연출·오류 표·접근성·W-CUT/W-HID/E-EMPTY. P2 = 투표·나란히 보기·실패 슬롯만 다시·관리자 상태 두 줄·엔진 비교 요약. P3 = 프리셋 실제 샘플 썸네일·100% 확대·삭제·탭 제목.
- U6 검증(처방 §11): ①
#aiPane 안 13px 미만 0 ② 버튼 라벨 1줄(최장 «같은 설정으로 다시 만들기 · 2장» 184px) ③ [×] ↔ QA 토글 교차 0(elementFromPoint) ④ 1280×720 · 1366×657 · 1536×864 · 1920×1080 에서 본문 스크롤량·#view·틀 크기 ⑤ 1단계 중 패널·카탈로그 토글 불가 ⑥ Tab 순서·Esc 표 ⑦ 대기(꺼짐)·진행·부분·만료 4상태에서 칩·최근·작업 화면 문구 일치 ⑧ 탭 숨김 → 폴링 정지 → 복귀 즉시 갱신.
- 회귀 위험(처방 원문 «회귀 위험» 절): #view 폭 변화 →
resize()·U.ro · lock()/guardKey 가 패널 키를 삼킴(예외 추가) · 광원 덮어쓰기 원복 · pncad.fold 불리언 전용 · #hint·#sideTab 숨김 원복 · 폴링·타이머 누수 · 예약 차감·반환 뒤 usage 재조회.
- 사람 눈: 실제 가구 장면의 틀·마스크 농도 · 스와치·6번째 프리셋·«단색» · 문구 띠 실물(카톡·견적서) · 1366×657 스크롤 체감 · NVDA 낭독 순서.
9. DB 스키마 변경 — migrations/patch64_pncad_stage.sql (계획만 · 실행 금지)
-
🆕[코디] 26.10.14 번호 63→64 개명(pfs c12de112 · 내용 불변 · 테스트 DB 테이블은 이미 적용) — 다른 세션 patch63_list_type_count_index.sql 이 테스트·운영에 먼저 적용됨. 아래 patch63 표기는 이력.
-
🆕[코디] 26.10.13 번호 = patch63(U1 실측 — patch62 는 patch62_mat_orderpaper_item_receive_date 가 이미 점유) · U1 이 칸 3개 추가: job.kind(new|again|retry — 재시도 자식은 부모 슬롯 엔진을 이어받아 계획) · job.ks(이 잡이 만드는 슬롯 키 AB|A|B — 재시도 자식 = 실패한 k 만) · job.claim_at(현재 시도 claim 시각 = 최대 처리시간 20분 판정). job.lease_until 은 queued 일 때 «재시도 가능 시각»(실패 재큐 백오프)으로도 쓴다. 정본 SQL = C:\dev\_wt\pfs-ai-stage-u1\migrations\patch63_pncad_stage.sql(U2 브랜치도 그대로 · 아래 블록은 원 설계 + 3칸 표시).
- 🆕[코디] 26.10.13 U2 는 칸을 더 안 늘렸다 — 기존 TEXT 칸을 쓴다:
slot.flags.by·slot.flags.lease(claim 때 임대 증명 = 토큰 이름 + lease_token sha256 앞 32 · 늦은 회수 대조) · job.flags.claim(claim_key sha256 앞 32 · claim 응답 유실 재전송 멱등). 원문 저장 0 · 화면·관리자 응답에 안 나간다.
새 테이블 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;
- 되돌리기 = 네 테이블 DROP(새 테이블 · 다른 기능 영향 0 · DROP 도 승인).
- 검증 =
SHOW CREATE TABLE 4개 대조 + mock 워커 스모크(테스트 RDS).
- (선택) 결제를 계속 안 붙일 거면
pncad_stage_month 는 빼도 된다 — 그 경우 유료 켤 때 patch 1개 추가.
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 |
- 병렬: U0 ∥ U1 ∥ U6(모의) → U2 → U3 ∥ U4 → (v3 병합) U5 → U7 → U8 → U9. 예상 합 ≈ 24시간.
- 공통 브리프: pfs
git add <파일>만(-A·디렉터리 금지) · git commit -- <파일> · 동시 작업이면 별도 worktree(C:\dev\pfs_wt_stage) · pncad worktree(C:\dev\_wt\pncad-stage) · CRLF 파일 Edit 뒤 CRLF 복원 · Playwright 끝에 close · 시크릿 파일 내용 출력 금지(grep -c) · 테스트 배포 확인(gh run watch) 생략 금지 · pfs 수정 = 사이트맵 재빌드까지 · CDP 는 cdplease · 상주·예약작업은 winledger 장부.
- U-L 로컬 모델 PoC(Q7 ③ 이면): 집PC RTX 4070(
gpubox.py job 으로 깨워 쓰고 끔 · C: 여유 200GB 하한) ComfyUI + Qwen-Image-Edit(또는 FLUX Kontext dev, 12GB 라 fp8/GGUF) · 같은 프로브 입력 + 가구 마스크 인페인팅 3장 → 품질·소요(깨우기 포함)·정합 비교표 → 워커 엔진 local_gpu 로 붙일지 판단 — L4 · worker-opus-xhigh(원격 PC·모델 수십 GB) · 0원.
- 나중(결제 승인 뒤) U10 유료 엔진 켜기: 실행 위치(가: 같은 미니PC 워커 — 키 단일출처 유지 · 나: EC2 systemd 워커 — Q1 ①, 미니PC 꺼져도 동작) · 월 상한·하드 리밋 · §13 부록 — L4/L5.
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. 출처
- OpenAI
gpt-image-2 https://developers.openai.com/api/docs/models/gpt-image-2 · gpt-image-2.5-sunburst https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst · 가격 https://developers.openai.com/api/docs/pricing · 이미지 가이드 https://developers.openai.com/api/docs/guides/image-generation · 지출 한도 https://developers.openai.com/api/docs/guides/spend-limits
- Gemini 가격 https://ai.google.dev/gemini-api/docs/pricing · 이미지 생성 https://ai.google.dev/gemini-api/docs/image-generation · GCP spend cap https://docs.cloud.google.com/billing/docs/how-to/budgets-spend-caps
- 실측 로그
C:\Users\delix\AppData\Local\Temp\claude\C--dev\ddfb8bd7-def9-41ef-80a8-e27c3fc9c32f\scratchpad\probe_out\result_openai.json · result_gemini.json
- 페페 2차 답
C:\dev\_work\wtprobe\answers-1013b.md