bibimbap/.atp/work-session/20261014-091658/implementation/design.md

117 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: design
agent: design-advisor
agent_version: 1
report_invocation_id: inv-002
generated_at: 2026-10-14T09:29:21+09:00
concerns_checked: true
concerns:
- Arbitrary WebGL templates may contain no statically recoverable intended dimensions; automatic inference cannot promise Unity scene layout adaptation. Author override is required for these cases.
- New database columns must precede deployment. Existing shared local containers are running; use isolated test DB/runtime, and do not apply shared/production DDL without orchestrator gate.
references:
requirements: user clarification — every uploaded game, not the example ZIP
research: ../research/research.md
adrs: []
---
# 설계: 업로드 빌드별 WebGL 플레이 화면
## 목표 / 비목표
빌드마다 기준 폭·높이를 안전하게 읽고 등록·수정 시 영속화하여, 상세 화면이 해당 게임의 정확한 비율로 양축 contain한다. 작성자가 감지 결과를 교정할 수 있고, 기존 게임도 읽기 시 감지 또는 명시된 기본값으로 작동한다. 확대·전체화면 변경에도 iframe 및 게임 인스턴스를 유지한다.
Unity 내부 카메라/Canvas Scaler/앵커를 개조하거나 임의 템플릿의 의도 해상도를 완벽히 추론하는 작업은 범위 밖이다. ZIP 원본과 업로드 HTML은 변경하지 않는다. 특정 게임 이름/390×844를 제품 기본값으로 사용하지 않는다.
## 개요
현재 업로드는 ZIP 추출→index 찾기→Unity 파일 검사→저장→webglPath 응답이다. 추가할 정적 분석기는 검증된 추출 디렉터리의 index와 인접 메타만 읽는다. 메타는 정수와 제한된 출처 enum으로만 반환하며 HTML/JS 실행, 스크립트 fetch, 임의 HTML 주입을 하지 않는다. 게임별 dimensions는 DB 게임 row에 저장된다. 부모는 dimensions로 iframe 박스만 조정하므로 별도 asset-origin 구성에서도 작동한다.
JS가 없는 초기 렌더에서도 서버 제공 비율 기반 CSS contain이 작동해야 한다. JS는 가용 크기 재계산과 확대/전체화면 상태만 담당한다. 디자인은 기존 골드/다크 테마, shell, 버튼 스타일을 유지한다. Impeccable adapt skill/context를 읽었으며 narrow refinement로 판정했다. PRODUCT.md 신규 생성/리디자인은 필요하지 않다.
## 플로우
1. ZIP 업로드 후 `ZipSecurity.assertUnityBuild` 통과, 이동 전에 `WebglDisplayMetadata.detect(indexFile)` 실행. 검사 오류/알 수 없는 템플릿은 업로드 실패 사유가 아니다. `displayWidth`, `displayHeight`는 둘 다 integer 또는 둘 다 null, `displaySource`는 enum이다.
2. 등록 폼에 “화면 크기”와 자동/직접 지정 선택을 제공한다. 자동 기본값으로 업로드 결과를 안내하고, 직접 지정이면 폭/높이 두 number 입력을 활성화한다. UX에 HTML parser/출처 enum/DB 등 구현 세부를 노출하지 않는다. 예: “빌드에서 960 × 600 화면을 확인했습니다.” / “화면 크기를 확인하지 못했습니다. 직접 지정하거나 기본 가로 화면으로 등록할 수 있습니다.”
3. 폼은 업로드가 끝난 뒤 최종 `/game/new` 또는 `/game/{id}/edit` 저장 요청을 보낸다. 자동이면 서버가 등록 대상의 저장된 index를 다시 분석하여 dims/source를 확정한다. 직접 지정이면 서버 범위 검증 후 source=manual로 저장한다. 업로드 응답을 클라이언트가 변경해도 server source 판정은 바뀌지 않는다.
4. 수정: 기존 manual 값은 직접 지정 선택에 로드한다. 기존 자동 값은 자동 선택 및 감지 결과 표시. 새 ZIP 선택 시 과거 자동 결과를 초기화하고 새 응답 반영; 직접 지정 값은 사용자 의도이므로 유지하며 필요 시 수정 가능하게 한다. mode 누락을 포함한 구 클라이언트 수정 요청은 webglPath 동일 시 기존 metadata 보존; path 교체 시 재감지한다.
5. 상세 GET: 유효 저장 dims 우선. null인 기존 row는 업로드 저장 index를 읽기 전용 감지한다(백필/write 없음). 감지 실패/파일 없음은 source=unknown, 16:9 fallback. nullable row를 예시 게임 비율로 일괄 변경하지 않는다. 작성자는 기존 게임 수정 화면에서 직접 지정 또는 자동 재저장 가능하다.
6. 플레이 영역은 정확한 ratio 유지, 검은 여백 중앙 정렬. “크게 보기”는 같은 wrapper를 viewport overlay로 전환하고 Esc/닫기로 복귀; “전체화면”은 같은 wrapper의 Fullscreen API 사용. API unsupported/rejected면 크게 보기 기능으로 안전하게 전환하고 간단한 상태 안내. 같은 iframe DOM과 src 유지, reload/reparent/transform-scale 금지.
## 감지 계약 / 우선순위
- 정수 1..16384만 허용; width/height 둘 모두 필요; 극단적인 종횡비 1:32..32:1 밖은 unknown 처리. 정수형 숫자와 숫자 문자열은 명시 검증 후 지원 가능. NaN, 무한대, fraction, 음수, 불완전 pair는 거부/unknown. 300×150은 기본 canvas 버퍼로 간주하여 자동 근거에서 제외한다(작성자 manual 지정은 가능).
- 순서: **manual > 인접 `build-info.json`의 top-level `width/height` > 명확한 Unity canvas의 CSS pixel 크기 > 명시 canvas width/height 속성 > unknown fallback**. build-info가 손상되면 다음 신호로 진행한다. metadata는 기술상 선언값이며 실제 scene 의도 검증과 구분한다.
- `build-info.json`은 선택된 index와 같은 디렉터리만 읽는다. 다른 빌드의 metadata를 재귀 탐색해서 혼합하지 않는다. 정적 index 최대 1MiB, JSON 최대 64KiB, 제한 넘으면 해당 신호 skip; bounded read로 메모리 공격 방지. 읽을 때 normalize/real-path boundary, regular-file 및 symlink 방어를 기존 업로드 경계 규칙과 정합한다.
- 기존 jsoup 의존성 활용. canvas는 `id=unity-canvas` 우선, 없으면 canvas가 정확히 1개인 경우만 허용. 복수 ambiguous canvas는 unknown. pixel CSS는 대상 canvas 또는 명확한 `#unity-container`의 inline style/style block의 width/height pair, 혹은 Unity 기본 템플릿의 canvas 변수에 대한 literal `style.width = "960px"`, `style.height = "600px"` pair만 support한다. 이때 해당 변수가 Unity canvas query와 명확히 연결되고 재할당/충돌없는 경우만 인정한다. 문자열/comments/다른 객체의 style을 주워오지 않는다. 안전한 제한 matcher로 충분하지 않으면 그 JS 형식은 지원 제외하고 explicit attrs를 사용한다.
- percent/vw/vh/calc/min/max/동적 JS 실행값/외부 stylesheet는 intended dimensions 증거로 사용하지 않는다. canvas 속성은 초기 buffer일 수 있어 낮은 confidence로 안내하고 manual override를 제공한다. 충돌한 저우선 신호는 고우선 신호를 덮어쓰지 않는다. parser exceptions은 unknown, 원래 업로드 성공을 유지한다.
- helper는 `detect(Path indexFile /* 검증된 빌드 진입점 */)` 최소 인자. 저장 경로에서 detect를 호출하는 계층은 configured game-storage root 하위만 허용하며 HTTP fetch/사용자 입력 absolute path는 사용하지 않는다. 정적 유틸이면 기존 constructor wiring을 최소화할 수 있다.
## 데이터 모델
`games`: nullable `display_width integer`, `display_height integer`, `display_source varchar(16)` 추가. null pair는 기존/unknown. 두 값은 모두 null 또는 모두 유효 정수; source는 `build-info | css | canvas | manual | unknown`로 제한한다. ratio는 계산값으로 별도 저장하지 않는다. GameData에 Integer displayWidth/displayHeight와 String displaySource 접근자 추가. GamesMapper의 모든 GameData 조회와 INSERT/UPDATE에 필드 포함.
DB check는 pair validity 및 허용 범위/source를 보호한다. 새 스키마와 마이그레이션의 정의가 일치해야 한다. additive nullable migration은 기존 row를 삭제/수정하지 않는다. 마이그레이션 파일은 `ADD COLUMN IF NOT EXISTS` 사용, 제약명 존재 조회로 재실행 가능하게 한다. 앱 자동 DDL 및 공유 DB 자동 적용은 금지. rollback은 우선 앱 이전 버전으로 되돌리고 nullable 컬럼은 남겨도 역호환; 실제 DROP은 별도 승인 절차다.
## 외부 계약
`POST /api/game-files/webgl-zip`: 기존 필드 그대로 + optional nullable `displayWidth`, `displayHeight`, `displaySource`. unknown metadata는 정상 HTTP 200이며 게임 upload security 검사 변경 없음.
`POST /game/new`, `POST /game/{id}/edit`: optional `displayMode=auto|custom`, `displayWidth`, `displayHeight`. unknown mode 및 custom 불완전/범위밖 입력은 HTTP 400, 한국어 field error. 기존 CSRF/auth/ownership 분기 우선순서 유지. auto는 dimension params 무시하고 저장 index 정적 감지; unknown은 null/unknown 저장하며 detail fallback. legacy create(mode 미지정)는 auto. legacy edit는 앞선 보존 규칙 적용. 외부 URL은 분석하지 않는다.
SSR addGameModel은 `webglDisplayWidth/Height` 또는 동등 numeric model attribute 및 known/source를 제공한다. register editModel에도 dimension/source 추가. JSP data attrs에는 검증한 숫자만 출력한다. HTML/script 문자열 삽입 없음. iframe sandbox/allow/asset-origin 계약 유지.
## 플레이어 크기 계약
`scale = min(availableWidth / referenceWidth, availableHeight / referenceHeight)`; width=referenceWidth*scale, height=referenceHeight*scale. 비율 오차는 CSS 반올림 1px 이내. normal height budget은 `min(70dvh, 720px)`(vh fallback), actual host width는 ResizeObserver 측정한다. 큰 세로 게임이 기존 520px/16:9에 갇히지 않도록 frame stage 자체를 높이 budget으로 제공한다. CSS fallback은 stage grid center + iframe aspect ratio와 max width/height를 이용한다.
expanded/fullscreen은 safe-area 및 toolbar를 뺀 stage 실제 content box로 양축 contain. overlay 또는 fullscreen 부모에서 width/height budget을 결정하며 JS는 DOM 측정값만 사용한다. `ResizeObserver`, `window.resize`, `visualViewport.resize`(지원시), fullscreenchange로 갱신; RAF coalescing, observer 재귀 없는 변경 방식. overlay에서 body scroll 이전 값 보존/복원, Esc 및 닫기, focus 이전 요소 복귀, 버튼 aria-expanded/pressed 정확 갱신. Fullscreen promise 실패를 처리. 터치 대상 44px, focus-visible 유지, 툴바 모바일 wrapping, resize 애니메이션 및 canvas CSS transform 금지. 해당 통합은 조작 단추 2개만 사용하며 플레이어에 수동 비율 dropdown을 반복 노출하지 않는다.
## 파일 영향 맵 / 소유권
모든 기존 경로는 직접 읽기/rg로 확인했다. `신규` 표기는 구현 단계 생성할 경로다. 구현자 하나가 backend 집합, 하나가 frontend 집합을 소유할 수 있으며 같은 파일 동시 편집 금지. 테스트 작성도 구현자의 책임이다.
| 변경 | 경로 | 책임/owner |
|---|---|---|
| 신규 | src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java | 정적 감지 value/utility; backend |
| 수정 | src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java | ZIP 감지 및 응답; backend |
| 수정 | src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java | 저장/edit/SSR 및 경계검증; backend |
| 수정 | src/main/java/com/pandoli365/bibimbap/data/GameData.java | nullable metadata; backend |
| 수정 | src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java | 전 조회/INSERT/UPDATE; backend |
| 수정 | db/schema.sql | fresh schema; backend |
| 신규 | db/migrations/20261014-game-display-metadata.sql | 기존 row 호환 additive DDL; backend |
| 신규 | src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java | parser fixtures; backend |
| 수정 | src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java | upload response / malformed tolerated regression; backend |
| 신규 | src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java | create/edit/SSR metadata/legacy/400/CSRF/ownership; backend |
| 조건부 | src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java 및 기존 GameController 직접 호출 테스트 | constructor/signature 변경 시 fixture 전수 갱신; backend |
| 수정 | src/main/webapp/WEB-INF/views/game-register.jsp | 자동/custom 등록·수정 UI 및 upload chaining; frontend |
| 수정 | src/main/webapp/WEB-INF/views/game-detail.jsp | numeric attrs + controls, old fixed aspect CSS 교체; frontend |
| 신규 | src/main/webapp/js/webgl-player.js | contain/overlay/fullscreen stable iframe; frontend |
| 신규 | src/main/webapp/css/webgl-player.css | minimal scoped stage/toolbar/safe-area; frontend |
| 신규 | src/test/js/webgl-player.test.cjs | meaningful layout/resize/state regression (Node native test or repository convention); frontend |
| 신규 | docs/contracts/webgl-display-metadata.md | extraction/precedence/API/fallback declaration; docs owner |
| 신규 | docs/usage/webgl-display.md | author override, existing game edit, migration/use guide; docs owner |
| 신규 | docs/changes/2026-10-14-webgl-display.md | behavior/validation record; docs owner |
| 수정 | docs/contracts/index.md, docs/usage/index.md, docs/changes/index.md | indexes; docs owner |
## 롤아웃 / 마이그레이션
건강한 bibimbap-db(5433)와 bibimbap-app(8080)을 발견했으나 접근/mutation 없음. 별도 disposable DB/schema와 별도 포트 앱에 fresh schema 적용 후 migration 재실행/기존 row contract를 검증한다. credential 값 출력 없이 container 내부 환경을 이용한다. shared local/production 마이그레이션 및 배포가 필요하면 파일+검증 근거를 제공한 뒤 orchestrator가 최종 게이트 처리한다. 승인 전에도 isolated runtime에서 새 JSP와 실제 ZIP 전체 흐름을 검증할 수 있다. migration 선적용→app deployment 순서 문서화. 이 설계와 구현 자체는 사용자 승인 범위이며 재승인 불필요.
## 검증 포인트
AC self-audit: 기대값은 metadata/DOM/DB 행위 불변식에 고정하고 work-session 산출물 개수에 의존하지 않는다. 표현은 literal grep 대신 typed parser/HTTP/DB/DOM assertions; 파일 집합만 아래 명시 목록 전수 확인한다.
- AC-1 parser fixture 집합 전수: portrait, landscape, square, nested index+adjacent info, HTML attrs, Unity literal CSS, conflicting precedence, no metadata, 300×150 default, malformed JSON, invalid pair/range, multiple ambiguous canvas, malicious script/nonliteral, oversize inputs. 각 named fixture 검증 및 product에서 JS 실행 0건; metadata 없는 정상 ZIP HTTP 200.
- AC-2 실제 uploaded game: 제공 ZIP은 390×844 반환; 다른 이름의 1280×720 및 800×800 ZIP도 자신의 값 반환. filename/게임명 상수 없이 동일 계약.
- AC-3 등록→DB→상세/수정 round trip; auto/custom/legacy path 전수 분기, manual 우선, 새 ZIP metadata 업데이트, 구 edit omission 보존, unknown nullable & fallback. 두 숫자 invalid는 400, CSRF/auth/owner unchanged.
- AC-4 isolated PostgreSQL mapper 계약: migration 기존 row 유지/NULL fallback, 재실행 가능; 모든 GameData SELECT 및 INSERT/UPDATE dimensions/source 정합. fresh schema와 migrated schema 동일. L2 remote/seed 미지정만 skip 가능; 알려진 healthy DB에서 가능한 isolated 검증을 우선한다.
- AC-5 stage viewport 집합 전수: 320×844, 1440×900, 844×390, 640×300에서 portrait/landscape/square 모두 iframe 양축 boundary 이내/ratio 오차 ≤1px. 현재 16:9/max520 구현에서 portrait 회귀는 실패하고 수정 후 통과함을 확보한다.
- AC-6 resize/overlay/fullscreen 진입·해제/실패 fallback에서 exact same iframe DOM, src, load count, 게임 state 유지; Esc/닫기/focus/scroll 복원. asset-origin 다른 origin에서도 parent canvas DOM access 없이 지원.
- AC-7 실제 ZIP runtime: 준비 후 CSS client size와 drawing buffer를 별도로 기록; 정상/expanded/fullscreen(지원 브라우저) resize, 상하단 UI 표시, 버튼 좌표/클릭, 모바일 터치 입력 점검. 원본 ZIP 및 served uploaded HTML hash 미변경.
- AC-8 full `./mvnw -o test` 및 WAR package 통과; isolated 최신 앱에서 detail/register/edit 변경 JSP 전수 HTTP 200(인증 포함). runtime stacktrace 및 console error scan. 새 constructor 의존이 생기면 contextLoads mocks 갱신. screenshot은 desktop/mobile 묶음 한 번+필요 fix 한 번; Impeccable detect changed UI 대상 한 번 실행.
- AC-9 파일 영향맵의 신규 산출물은 전수 존재하되 conditional 경로는 조건 성립시에만 변경; docs 계약/사용/변경 3개와 해당 index 링크 모두 확인. verification-advisor는 테스트를 작성하지 않고 실행만 수행.
## 실행 계약 / 자가 검증
update_plan은 tools에 없어 bind를 생성하지 않았고 parent에 고지했다. child 0건, 테스트 실행 0건, 제품/ZIP/DB 변경 0건; 이 design.md만 작성했다. frontmatter/근거 경로/concerns 검토 완료. shared DB migration gate는 구현 blocker가 아니며 isolated validation 가능하다.