17 KiB
| phase | agent | agent_version | report_invocation_id | generated_at | concerns_checked | concerns | references | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| design | design-advisor | 1 | inv-002 | 2026-10-14T09:29:21+09:00 | true |
|
|
설계: 업로드 빌드별 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 신규 생성/리디자인은 필요하지 않다.
플로우
- ZIP 업로드 후
ZipSecurity.assertUnityBuild통과, 이동 전에WebglDisplayMetadata.detect(indexFile)실행. 검사 오류/알 수 없는 템플릿은 업로드 실패 사유가 아니다.displayWidth,displayHeight는 둘 다 integer 또는 둘 다 null,displaySource는 enum이다. - 등록 폼에 “화면 크기”와 자동/직접 지정 선택을 제공한다. 자동 기본값으로 업로드 결과를 안내하고, 직접 지정이면 폭/높이 두 number 입력을 활성화한다. UX에 HTML parser/출처 enum/DB 등 구현 세부를 노출하지 않는다. 예: “빌드에서 960 × 600 화면을 확인했습니다.” / “화면 크기를 확인하지 못했습니다. 직접 지정하거나 기본 가로 화면으로 등록할 수 있습니다.”
- 폼은 업로드가 끝난 뒤 최종
/game/new또는/game/{id}/edit저장 요청을 보낸다. 자동이면 서버가 등록 대상의 저장된 index를 다시 분석하여 dims/source를 확정한다. 직접 지정이면 서버 범위 검증 후 source=manual로 저장한다. 업로드 응답을 클라이언트가 변경해도 server source 판정은 바뀌지 않는다. - 수정: 기존 manual 값은 직접 지정 선택에 로드한다. 기존 자동 값은 자동 선택 및 감지 결과 표시. 새 ZIP 선택 시 과거 자동 결과를 초기화하고 새 응답 반영; 직접 지정 값은 사용자 의도이므로 유지하며 필요 시 수정 가능하게 한다. mode 누락을 포함한 구 클라이언트 수정 요청은 webglPath 동일 시 기존 metadata 보존; path 교체 시 재감지한다.
- 상세 GET: 유효 저장 dims 우선. null인 기존 row는 업로드 저장 index를 읽기 전용 감지한다(백필/write 없음). 감지 실패/파일 없음은 source=unknown, 16:9 fallback. nullable row를 예시 게임 비율로 일괄 변경하지 않는다. 작성자는 기존 게임 수정 화면에서 직접 지정 또는 자동 재저장 가능하다.
- 플레이 영역은 정확한 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-levelwidth/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 변수에 대한 literalstyle.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=referenceWidthscale, height=referenceHeightscale. 비율 오차는 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 가능하다.