Compare commits

...

3 Commits

Author SHA1 Message Date
이정수 e67996e5f3 docs(webgl): 화면 대응과 로컬 확인 결과 상세 기록
구현 결정, 검증 증거, 로컬 DB 적용과 게임 등록, 사용자 긍정 의견 및 남은 확인을 기록한다. 스크린샷 15장과 갤러리를 보존하고 링크와 이미지 해시를 검증했다.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-14 17:20:43 +09:00
이정수 90af59939c docs(atp): WebGL 해상도 대응 검증 세션 종료 기록
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-14 10:02:57 +09:00
이정수 ddd9abfe41 feat(webgl): 업로드 빌드별 화면 비율 대응 및 확대 지원
게임별 기준 크기를 정적 감지·저장하고 작성자 직접 지정과 기존 게임 fallback을 제공한다.
비율을 유지한 양축 크기 조정 및 동일 iframe의 확대·전체화면을 지원한다.

검증: Java 377 실패 0, Node 11/11, 격리 PostgreSQL L2 통과.
JSP 5경로 200, 빌드 3종 화면 12조합, 실제 Unity trusted touch 메뉴 전환 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-14 10:02:28 +09:00
91 changed files with 7059 additions and 14 deletions

View File

@ -0,0 +1,4 @@
# Preserve verbatim test/runtime stdout as evidence.
artifacts/*.log -whitespace
artifacts/**/*.log -whitespace
artifacts/runtime-*.txt -whitespace

View File

@ -0,0 +1,3 @@
# Local input copies and transient browser fixtures are reproducible.
*.zip
runtime-login.html

View File

@ -0,0 +1,13 @@
## 검증 포인트
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는 테스트를 작성하지 않고 실행만 수행.

View File

@ -0,0 +1,16 @@
TAP version 13
# Subtest: SSR player ships the stored ratio and responsive stage without the old hard limit
ok 1 - SSR player ships the stored ratio and responsive stage without the old hard limit
---
duration_ms: 0.93375
type: 'test'
...
1..1
# tests 1
# suites 0
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 44.434208

View File

@ -0,0 +1,53 @@
---
phase: documentation
agent: documentation-advisor
agent_version: 1
report_invocation_id: inv-005
generated_at: 2026-10-14T09:54:53.525775+09:00
concerns_checked: true
concerns:
- Existing usage index setup-checklist.md and faq.md links have no target; unrelated to feature.
- Trusted touchscreen and named Unity button outcome remain unverified.
- Baseline failure evidence is SSR markup regression; previous browser geometry was not replayed.
---
# Final documentation and scoped UI audit
Implementation integrity verdict: PASS for the changed upload sizing/player surfaces within the evidence below. No new UI bug was observed. This is a bounded source/screenshot audit, not a full WCAG certification or performance benchmark.
## Documentation
- Corrected docs/usage/webgl-display.md: current WAR has no Main manifest. Replaced java -jar with the successful Java 21 Maven spring-boot:run command, parameterizing isolated DB port. Added explicit macOS Homebrew Java 21 environment for the test launcher that failed under default Java.
- Updated docs/changes/2026-10-14-webgl-display.md with actual 377 Java (0 failures/errors, ordinary opt-in L2 skip 1), Node 11/11, package pass, separately enabled actual L2 1/1, authenticated JSP 5-path HTTP 200, 3-build × 4-viewport boundary/ratio pass, ready Unity/CSS-buffer/state/fullscreen/hash evidence and explicit incomplete input verification.
- Read contract, usage, change documents, design and implementation reports, verification, document-category-classification, and documentation-advisor definition. documentation-guidelines.md is absent; category and advisor checklists were applied instead.
- AC9: all 21 unconditional paths from the complete design file impact map exist (including three index files). Conditional constructor-fixture changes were not triggered per implementation report. contracts/index.md, usage/index.md, changes/index.md contain their required links. All relative links in the three feature docs resolve, and all three new feature index links resolve. Full category-index scan also found pre-existing missing usage/setup-checklist.md and usage/faq.md targets (each linked twice). These unrelated index defects were reported without expanding edits. Cross-links connect contract, usage, change and verification strategy.
## Single final UI detect
Impeccable had already been announced and context loaded by root. Read SKILL.md plus audit/detection instructions; did not rerun context or edit UI. Ran exactly one final detect on game-detail.jsp, game-register.jsp, webgl-player.css and webgl-player.js. JSON: final-impeccable-detect.json. Exit 2 reflects two warnings, not a clean automated result:
1. game-detail.jsp line 454 side-tab accent border: existing creator-note block outside changed player. Incumbent design preserved; no new drift.
2. game-register.jsp line 462 hidden preview img without src: verified false positive. Source sets object-URL src before unhiding, removes src only while hiding, and handles image errors with the fallback. No visible broken-image box.
Inspected existing runtime-desktop.png and runtime-mobile.png together. Both show the complete portrait game, top information and bottom navigation inside the player, centered in available space. Outer controls wrap without clipping and have visible focus styling. Registration evidence covers auto/custom metadata and replacement behavior; there is no registration screenshot, so its audit is source/evidence based.
| Dimension | Scoped finding |
|---|---|
| Accessibility | Player buttons have names, state attributes, 44px minima, focus-visible; size form has legend, radio labels, associated numeric labels and live status. Runtime recorded Esc/focus/scroll restoration. Contrast and iframe-internal keyboard traversal not independently certified. |
| Performance | Resize/visualViewport/observer events coalesce through RAF; parent only sizes iframe and does not access Unity DOM. No transforms or layout animation introduced. No timing benchmark claimed. |
| Responsive | Exact runtime 12/12 boundary and ratio checks, max error 0.006665px. Screenshot pair shows top/bottom game UI. Trusted mobile input remains open. |
| Theming | New outer controls and form use incumbent text/accent/border/background tokens; intentional dark WebGL stage retained. No new visual world. |
| Integrity | Same iframe/src/Unity instance and zero additional loads observed. Detector findings verified as pre-existing/out of scope or false positive. |
No P0/P1/P2 defect identified in the changed surfaces from available evidence. No redesign or policy/settings/runtime/DB mutation performed.
## Unused variables/imports
Focused manual inspection covered changed Java/JSP/JS files and the new mapper/parser/controller tests. A supplemental Python symbol-reference scan checked each explicit Java import against its file body; no unused-import candidate was found. Reviewed player and added form variable declarations against their uses: all are referenced. Parser locals are used in bounded reading and dimension selection; model fields are exposed through accessors/MyBatis. This is a focused source audit, not an automated linter/LSP result; no lint capability was invoked or claimed.
## Actual baseline markup regression
At root's explicit request, copied current test/JS/CSS into a temporary tree outside repository, substituted ONLY game-detail.jsp with git show HEAD:src/main/webapp/WEB-INF/views/game-detail.jsp, and ran node --test --test-name-pattern='SSR player ships' src/test/js/webgl-player.test.cjs. Prechange test failed exit 1 due absent stored-ratio attributes; current identical named test passed exit 0. Logs: final-prechange-ssr-regression.log and final-current-ssr-regression.log. Temporary tree removed. This proves baseline SSR markup fails the changed contract; it does not claim old browser geometry or old full runtime execution. No new tests/source edits.
## Limits and self verification
Named Unity button outcome and trusted touchscreen input remain unverified: existing pointer delivery is trusted mouse only. Separate asset-origin support is Node harness evidence, not separate browser runtime. No complete-acceptance claim. No additional full test/build rerun needed for doc edits; baseline named test was explicitly requested. update_plan unavailable, root informed; ATP_POOL_BIND not fabricated. Child count 0, commit/push 0. Artifacts present; required frontmatter checked; concerns deliberately reviewed. Backport exclusion N/A.

View File

@ -0,0 +1,22 @@
[
{
"antipattern": "side-tab",
"name": "Side-tab accent border",
"description": "Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.",
"severity": "warning",
"category": "slop",
"file": "/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/game-detail.jsp",
"line": 454,
"snippet": "border-left: 3px solid var(--accent)"
},
{
"antipattern": "broken-image",
"name": "Broken or placeholder image",
"description": "<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.",
"severity": "warning",
"category": "quality",
"file": "/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp/WEB-INF/views/game-register.jsp",
"line": 462,
"snippet": "<img id=\"preview-thumb\" alt=\"\" hidden />"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
import fs from 'node:fs';
const tabs=await(await fetch('http://127.0.0.1:18087/json')).json();
const ws=new WebSocket(tabs.find(x=>x.type==='page').webSocketDebuggerUrl);await new Promise(r=>ws.onopen=r);let id=0;const pending=new Map();ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result)}};
const send=(method,params={})=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params}))});
const ev=async(expression)=>{const r=await send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw r.exceptionDetails;return r.result.value};
const action=process.argv[2];
if(action==='init'){await send('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:true});await send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:1});await send('Page.navigate',{url:'http://127.0.0.1:18086/'});}
if(action==='eval') console.log(JSON.stringify(await ev(process.argv[3]),null,2));
if(action==='shot'){const r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(process.argv[3],Buffer.from(r.data,'base64'));}
if(action==='touch'){const x=+process.argv[3],y=+process.argv[4];await send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{x,y,id:1,radiusX:1,radiusY:1,force:1}]});await new Promise(r=>setTimeout(r,100));await send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});}
if(action==='resize'){await send('Emulation.setDeviceMetricsOverride',{width:+process.argv[3],height:+process.argv[4],deviceScaleFactor:1,mobile:true});}
if(action==='expanded'){await ev('document.querySelector("[data-player-expand]").click()');await send('Emulation.setDeviceMetricsOverride',{width:844,height:390,deviceScaleFactor:1,mobile:false});await new Promise(r=>setTimeout(r,1000));console.log(JSON.stringify(await ev('(()=>{let f=document.querySelector("iframe"),c=f.contentDocument.querySelector("canvas");return {view:[innerWidth,innerHeight],frame:f.getBoundingClientRect().toJSON(),canvas:c.getBoundingClientRect().toJSON(),buffer:[c.width,c.height],sameFrame:f===window.__originalFrame,sameUnity:f.contentWindow.unityInstance===window.__originalUnity}})()'),null,2));let r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(process.argv[3],Buffer.from(r.data,'base64'));}
if(action==='expandedtap'){await send('Emulation.setDeviceMetricsOverride',{width:844,height:390,deviceScaleFactor:1,mobile:false});await new Promise(r=>setTimeout(r,400));await ev('document.querySelector("iframe").contentWindow.__events=[]');await send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{x:466,y:345,id:2,radiusX:1,radiusY:1,force:1}]});await new Promise(r=>setTimeout(r,100));await send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});await new Promise(r=>setTimeout(r,400));console.log(JSON.stringify(await ev('document.querySelector("iframe").contentWindow.__events'),null,2));let r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(process.argv[3],Buffer.from(r.data,'base64'));}
if(action==='portrait'){await ev('document.querySelector("[data-player-expand]").click()');await send('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:false});await new Promise(r=>setTimeout(r,500));let geo=await ev('(()=>{let f=document.querySelector("iframe"),c=f.contentDocument.querySelector("canvas");return {view:[innerWidth,innerHeight],frame:f.getBoundingClientRect().toJSON(),canvas:c.getBoundingClientRect().toJSON(),buffer:[c.width,c.height],sameFrame:f===window.__originalFrame,sameUnity:f.contentWindow.unityInstance===window.__originalUnity}})()');let coords=(portion)=>({x:geo.frame.x+geo.canvas.x+geo.canvas.width*portion,y:geo.frame.y+geo.canvas.y+geo.canvas.height*.96});let tap=async(p)=>{await send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{...p,id:3,radiusX:1,radiusY:1,force:1}]});await new Promise(r=>setTimeout(r,100));await send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});await new Promise(r=>setTimeout(r,300));};await tap(coords(.1));let r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(process.argv[3]+'/portrait-home-before.png',Buffer.from(r.data,'base64'));await ev('document.querySelector("iframe").contentWindow.__events=[]');await tap(coords(.3));r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(process.argv[3]+'/portrait-shop-after.png',Buffer.from(r.data,'base64'));let events=await ev('document.querySelector("iframe").contentWindow.__events');let result={geo,coordinates:coords(.3),events};fs.writeFileSync(process.argv[3]+'/portrait-input.json',JSON.stringify(result,null,2));console.log(JSON.stringify(result,null,2));}
ws.close();

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,59 @@
{
"geo": {
"view": [
390,
844
],
"frame": {
"x": 58.453125,
"y": 76,
"width": 273.078125,
"height": 591,
"top": 76,
"right": 331.53125,
"bottom": 667,
"left": 58.453125
},
"canvas": {
"x": 0,
"y": 0,
"width": 273,
"height": 591,
"top": 0,
"right": 273,
"bottom": 591,
"left": 0
},
"buffer": [
273,
591
],
"sameFrame": true,
"sameUnity": true
},
"coordinates": {
"x": 140.35312499999998,
"y": 643.36
},
"events": [
{
"type": "pointerdown",
"trusted": true,
"pointerType": "touch",
"x": 81.89999389648438,
"y": 567.3599853515625
},
{
"type": "touchstart",
"trusted": true,
"x": 81.89999389648438,
"y": 567.3599853515625
},
{
"type": "touchend",
"trusted": true,
"x": 81.89999389648438,
"y": 567.3599853515625
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -0,0 +1,14 @@
from http.server import HTTPServer,SimpleHTTPRequestHandler
from pathlib import Path
root=Path('/Users/wemadeplay/workspace/stz/bibimbap')
game=Path('/tmp/bibimbap-inv004-uploads/game/d4bf0e5b-5582-49f7-862d-be47e99092b2/GoraniCoin-WebGLTest')
class H(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path=='/':
b=b'''<!doctype html><meta charset="utf-8"><link rel="stylesheet" href="/css/webgl-player.css"><style>body{margin:0;padding:16px;box-sizing:border-box}</style><div class="webgl-player" data-webgl-player data-display-width="390" data-display-height="844" style="--game-ratio:0.4620853"><div class="webgl-player__toolbar"><button class="webgl-player__button" data-player-expand aria-expanded="false">Expand</button><button class="webgl-player__button" data-player-fullscreen aria-pressed="false">Fullscreen</button><span class="webgl-player__status" data-player-status role="status"></span></div><div class="webgl-player__stage" data-player-stage id="webgl-stage"><iframe src="/game/index.html" sandbox="allow-scripts allow-same-origin allow-pointer-lock" allow="fullscreen; autoplay; xr-spatial-tracking"></iframe></div></div><script src="/js/webgl-player.js"></script>'''
self.send_response(200);self.send_header('Content-Type','text/html');self.end_headers();self.wfile.write(b);return
super().do_GET()
def translate_path(self,p):
if p.startswith('/game/'):return str(game/p[6:])
return str(root/'src/main/webapp'/p.lstrip('/'))
HTTPServer(('127.0.0.1',18086),H).serve_forever()

View File

@ -0,0 +1,6 @@
{
"zip_sha256": "02e5272e4086f8f0e2e3a4b41ca4313d5f8862897a1655421f3f1b4315942461",
"html_hashes": {
"GoraniCoin-WebGLTest/index.html": "8a9f8aaff7e949b49f4cab1a87c5b6d414db4031b5fb3e268f5102bc40cbcc74"
}
}

View File

@ -0,0 +1,85 @@
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.pandoli365:bibimbap >-----------------------
[INFO] Building bibimbap 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] >>> spring-boot:3.5.16:run (default-cli) > test-compile @ bibimbap >>>
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ bibimbap ---
[INFO] Copying 7 resources from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.14.1:compile (default-compile) @ bibimbap ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ bibimbap ---
[INFO] skip non existing resourceDirectory /Users/wemadeplay/workspace/stz/bibimbap/src/test/resources
[INFO]
[INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ bibimbap ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] <<< spring-boot:3.5.16:run (default-cli) < test-compile @ bibimbap <<<
[INFO]
[INFO]
[INFO] --- spring-boot:3.5.16:run (default-cli) @ bibimbap ---
[INFO] Attaching agents: []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.5.16)
2026-10-14T09:43:05.496+09:00 INFO 43913 --- [bibimbap] [ main] c.p.bibimbap.BibimbapApplication : Starting BibimbapApplication using Java 21.0.11 with PID 43913 (/Users/wemadeplay/workspace/stz/bibimbap/target/classes started by wemadeplay in /Users/wemadeplay/workspace/stz/bibimbap)
2026-10-14T09:43:05.497+09:00 INFO 43913 --- [bibimbap] [ main] c.p.bibimbap.BibimbapApplication : The following 1 profile is active: "dev"
2026-10-14T09:43:05.744+09:00 DEBUG 43913 --- [bibimbap] [ main] org.apache.ibatis.logging.LogFactory : Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2026-10-14T09:43:05.891+09:00 INFO 43913 --- [bibimbap] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 18081 (http)
2026-10-14T09:43:05.898+09:00 INFO 43913 --- [bibimbap] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-10-14T09:43:05.898+09:00 INFO 43913 --- [bibimbap] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.55]
2026-10-14T09:43:05.963+09:00 INFO 43913 --- [bibimbap] [ main] org.apache.jasper.servlet.TldScanner : At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
2026-10-14T09:43:05.965+09:00 INFO 43913 --- [bibimbap] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2026-10-14T09:43:05.965+09:00 INFO 43913 --- [bibimbap] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 455 ms
2026-10-14T09:43:06.020+09:00 DEBUG 43913 --- [bibimbap] [ main] org.apache.ibatis.logging.LogFactory : Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2026-10-14T09:43:06.255+09:00 INFO 43913 --- [bibimbap] [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page template: index
2026-10-14T09:43:06.313+09:00 WARN 43913 --- [bibimbap] [ main] o.s.w.s.resource.ResourceHandlerUtils : Appended trailing slash to static resource location: file:///tmp/bibimbap-inv004-uploads/profile/
2026-10-14T09:43:06.372+09:00 INFO 43913 --- [bibimbap] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 18081 (http) with context path '/'
2026-10-14T09:43:06.376+09:00 INFO 43913 --- [bibimbap] [ main] c.p.bibimbap.BibimbapApplication : Started BibimbapApplication in 1.039 seconds (process running for 1.157)
2026-10-14T09:43:06.382+09:00 INFO 43913 --- [bibimbap] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-10-14T09:43:06.425+09:00 INFO 43913 --- [bibimbap] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@6c96403e
2026-10-14T09:43:06.425+09:00 INFO 43913 --- [bibimbap] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-10-14T09:43:06.435+09:00 INFO 43913 --- [bibimbap] [ main] c.p.bibimbap.config.BadgeCatalogSeeder : [BADGE] 배지 카탈로그 시드 완료: 2 종
2026-10-14T09:43:06.440+09:00 INFO 43913 --- [bibimbap] [ main] c.p.b.config.PermissionCatalogVerifier : [RBAC] 권한 카탈로그 시드 완료: 4 키
2026-10-14T09:43:07.462+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-10-14T09:43:07.462+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2026-10-14T09:43:07.465+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 3 ms
2026-10-14T09:44:25.321+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-3] c.p.b.c.api.GameUploadController : WebGL zip upload request received. fileName=independent-original.zip, size=39008123, contentType=application/zip
2026-10-14T09:44:25.948+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-3] c.p.b.c.api.GameUploadController : WebGL zip upload completed. gameUuid=d4bf0e5b-5582-49f7-862d-be47e99092b2, webglPath=/game/d4bf0e5b-5582-49f7-862d-be47e99092b2/GoraniCoin-WebGLTest/index.html, entryCount=8, extractedBytes=145049041
2026-10-14T09:46:04.257+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-4] c.p.b.c.api.GameUploadController : WebGL zip upload request received. fileName=independent-landscape.zip, size=1054, contentType=application/zip
2026-10-14T09:46:04.267+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-4] c.p.b.c.api.GameUploadController : WebGL zip upload completed. gameUuid=1f84a200-dd40-45b2-9171-544cd222a89d, webglPath=/game/1f84a200-dd40-45b2-9171-544cd222a89d/index.html, entryCount=6, extractedBytes=380
2026-10-14T09:46:22.030+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-7] c.p.b.c.api.GameUploadController : WebGL zip upload request received. fileName=independent-landscape.zip, size=1054, contentType=application/zip
2026-10-14T09:46:22.034+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-7] c.p.b.c.api.GameUploadController : WebGL zip upload completed. gameUuid=274566a9-8825-45f7-b255-b7de3cb148c8, webglPath=/game/274566a9-8825-45f7-b255-b7de3cb148c8/index.html, entryCount=6, extractedBytes=380
2026-10-14T09:46:23.406+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-9] c.p.b.c.api.GameUploadController : WebGL zip upload request received. fileName=independent-square.zip, size=1052, contentType=application/zip
2026-10-14T09:46:23.411+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-9] c.p.b.c.api.GameUploadController : WebGL zip upload completed. gameUuid=81081344-47f1-4fc7-b7ce-2cd6519bf6bd, webglPath=/game/81081344-47f1-4fc7-b7ce-2cd6519bf6bd/index.html, entryCount=6, extractedBytes=378
2026-10-14T09:48:19.361+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-4] c.p.b.c.api.GameUploadController : WebGL zip upload request received. fileName=replacement.zip, size=1054, contentType=application/zip
2026-10-14T09:48:19.370+09:00 INFO 43913 --- [bibimbap] [.1-18081-exec-4] c.p.b.c.api.GameUploadController : WebGL zip upload completed. gameUuid=98252972-544b-4109-878b-406b452876ad, webglPath=/game/98252972-544b-4109-878b-406b452876ad/index.html, entryCount=6, extractedBytes=380
2026-10-14T09:49:34.942+09:00 INFO 43913 --- [bibimbap] [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
2026-10-14T09:49:34.945+09:00 INFO 43913 --- [bibimbap] [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
2026-10-14T09:49:34.949+09:00 INFO 43913 --- [bibimbap] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2026-10-14T09:49:34.951+09:00 INFO 43913 --- [bibimbap] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 06:30 min
[INFO] Finished at: 2026-10-14T09:49:35+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.5.16:run (default-cli) on project bibimbap: Process terminated with exit code: 143 -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

View File

@ -0,0 +1 @@
target/bibimbap-0.0.1-SNAPSHOT.war? ?? Manifest ??? ????.

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View File

@ -0,0 +1,178 @@
[
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"ready\":true,\"iframe\":{\"rect\":{\"x\":112.8828125,\"y\":718.6171875,\"width\":259.2265625,\"height\":561,\"top\":718.6171875,\"right\":372.109375,\"bottom\":1279.6171875,\"left\":112.8828125},\"src\":\"http://127.0.0.1:18081/game/d4bf0e5b-5582-49f7-862d-be47e99092b2/GoraniCoin-WebGLTest/index.html\",\"attrs\":[[\"title\",\"independent-portrait WebGL\"],[\"src\",\"/game/d4bf0e5b-5582-49f7-862d-be47e99092b2/GoraniCoin-WebGLTest/index.html\"],[\"sandbox\",\"allow-scripts allow-same-origin allow-pointer-lock\"],[\"allow\",\"fullscreen; autoplay; xr-spatial-tracking\"],[\"loading\",\"eager\"],[\"referrerpolicy\",\"same-origin\"],[\"style\",\"width: 259.23px; height: 561px;\"]],\"parent\":\"<div class=\\\"game-webgl__frame-wrap webgl-player__stage\\\" id=\\\"webgl-stage\\\" data-player-stage=\\\"\\\">\\n <iframe title=\\\"independent-portrait WebGL\\\" src=\\\"/game/d4bf0e5b-5582-49f7-862d-be47e99092b2/GoraniCoin-WebGLTest/index.html\\\" sandbox=\\\"allow-scripts allow-same-origin allow-pointer-lock\\\" allow=\\\"fullscreen; autoplay; xr-spatial-tracking\\\" loading=\\\"eager\\\" referrerpolicy=\\\"same-origin\\\" style=\\\"width: 259.23px; height: 561px;\\\"></iframe>\\n </div>\"},\"canvas\":{\"css\":[259,561],\"buffer\":[518,1122]},\"buttons\":[{\"text\":\"\\n \\n \\n \\n \\n \\n \\n \\n \\n \",\"id\":\"theme-toggle\"},{\"text\":\"취소\",\"id\":\"site-modal-cancel\"},{\"text\":\"확인\",\"id\":\"site-modal-confirm\"},{\"text\":\"삭제\",\"id\":\"game-delete-button\"},{\"text\":\"\\n \\n \\n \\n 0\\n \",\"id\":\"game-like-btn\"}]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"viewport\":[500,801],\"frame\":[259.2265625,561],\"stage\":[447,560.6953125],\"inside\":true,\"ratioError\":0.003295319905191718,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"css\":[259,561],\"buffer\":[518,1122]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"viewport\":[1440,801],\"frame\":[259.2265625,561],\"stage\":[858,560.6953125],\"inside\":true,\"ratioError\":0.003295319905191718,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"css\":[259,561],\"buffer\":[518,1122]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"viewport\":[844,390],\"frame\":[126.1484375,273],\"stage\":[791,273],\"inside\":true,\"ratioError\":0.0008515995260722775,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"css\":[126,273],\"buffer\":[252,546]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"viewport\":[640,300],\"frame\":[97.03125,210],\"stage\":[587,210],\"inside\":true,\"ratioError\":0.006664691943129242,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"css\":[97,210],\"buffer\":[194,420]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"portrait-exact\",\"viewport\":[320,844],\"frame\":[267,577.8125],\"stage\":[267,590.796875],\"inside\":true,\"ratioError\":0.0013329383886002688,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"canvasCSS\":[267,578],\"buffer\":[534,1156]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"portrait-exact\",\"viewport\":[1440,900],\"frame\":[291.109375,630],\"stage\":[858,630],\"inside\":true,\"ratioError\":0.004369075829401936,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"canvasCSS\":[291,630],\"buffer\":[582,1260]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"portrait-exact\",\"viewport\":[844,390],\"frame\":[126.1484375,273],\"stage\":[791,273],\"inside\":true,\"ratioError\":0.0008515995260722775,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"canvasCSS\":[126,273],\"buffer\":[252,546]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"portrait-exact\",\"viewport\":[640,300],\"frame\":[97.03125,210],\"stage\":[587,210],\"inside\":true,\"ratioError\":0.006664691943129242,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"canvasCSS\":[97,210],\"buffer\":[194,420]}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"fullscreen\":true,\"sameFrame\":true,\"sameUnity\":true,\"loads\":0,\"canvas\":{\"css\":[388,840],\"buffer\":[776,1680]}}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"mode\":\"custom\",\"width\":\"400\",\"height\":\"900\",\"status\":\"빌드에서 1280 × 720 화면을 확인했습니다. 크기가 다르면 직접 지정해 주세요.\",\"uploadPath\":\"/game/1f84a200-dd40-45b2-9171-544cd222a89d/index.html\"}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"landscape\",\"viewport\":[320,844],\"frame\":[267,150.1875],\"stage\":[267,590.796875],\"inside\":true,\"ratioError\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"landscape\",\"viewport\":[1440,900],\"frame\":[858,482.625],\"stage\":[858,630],\"inside\":true,\"ratioError\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"landscape\",\"viewport\":[844,390],\"frame\":[485.328125,273],\"stage\":[791,273],\"inside\":true,\"ratioError\":0.0052083333333143855}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"landscape\",\"viewport\":[640,300],\"frame\":[373.328125,210],\"stage\":[587,210],\"inside\":true,\"ratioError\":0.0052083333333143855}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"square\",\"viewport\":[320,844],\"frame\":[267,267],\"stage\":[267,590.796875],\"inside\":true,\"ratioError\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"square\",\"viewport\":[1440,900],\"frame\":[630,630],\"stage\":[858,630],\"inside\":true,\"ratioError\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"square\",\"viewport\":[844,390],\"frame\":[273,273],\"stage\":[791,273],\"inside\":true,\"ratioError\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"square\",\"viewport\":[640,300],\"frame\":[210,210],\"stage\":[587,210],\"inside\":true,\"ratioError\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"touch\":1,\"events\":[{\"type\":\"pointerdown\",\"trusted\":true,\"client\":[136.5,295.5],\"pointerType\":\"mouse\"},{\"type\":\"mousedown\",\"trusted\":true,\"client\":[136,295]},{\"type\":\"click\",\"trusted\":true,\"client\":[136,295],\"pointerType\":\"mouse\"}],\"ready\":true,\"canvasCSS\":[273,591],\"buffer\":[273,591],\"sameUnity\":true,\"loads\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"fullscreen\":true,\"overflow\":\"hidden\",\"closeButton\":false,\"status\":\"\",\"sameFrame\":true,\"sameUnity\":true,\"loads\":0}\n```"
}
]
},
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"kind\":\"rejection-fallback\",\"fullscreen\":false,\"overflow\":\"hidden\",\"closeButton\":true,\"status\":\"전체화면을 사용할 수 없어 크게 보기로 전환했습니다.\",\"sameFrame\":true,\"sameUnity\":true,\"loads\":0}\n```"
}
]
}
]

View File

@ -0,0 +1,5 @@
127.0.0.1 - - [14/Oct/2026 09:44:24] "GET /independent-original.zip HTTP/1.1" 200 -
127.0.0.1 - - [14/Oct/2026 09:46:04] "GET /independent-landscape.zip HTTP/1.1" 200 -
127.0.0.1 - - [14/Oct/2026 09:46:22] "GET /independent-landscape.zip HTTP/1.1" 304 -
127.0.0.1 - - [14/Oct/2026 09:46:23] "GET /independent-square.zip HTTP/1.1" 200 -
127.0.0.1 - - [14/Oct/2026 09:48:19] "GET /independent-landscape.zip HTTP/1.1" 304 -

View File

@ -0,0 +1,8 @@
from http.server import HTTPServer,SimpleHTTPRequestHandler
import os
os.chdir('.atp/work-session/20261014-091658/artifacts')
class Handler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin','http://127.0.0.1:18081')
super().end_headers()
HTTPServer(('127.0.0.1',18082),Handler).serve_forever()

View File

@ -0,0 +1,11 @@
{
"originalZipUnchanged": true,
"originalZipSHA256": "02e5272e4086f8f0e2e3a4b41ca4313d5f8862897a1655421f3f1b4315942461",
"storedHtml": [
{
"path": "/tmp/bibimbap-inv004-uploads/game/d4bf0e5b-5582-49f7-862d-be47e99092b2/GoraniCoin-WebGLTest/index.html",
"sha256": "8a9f8aaff7e949b49f4cab1a87c5b6d414db4031b5fb3e268f5102bc40cbcc74",
"unchanged": true
}
]
}

View File

@ -0,0 +1,8 @@
{
"content": [
{
"type": "text",
"text": "Script ran on page and returned:\n```json\n{\"http\":[{\"path\":\"/game/new\",\"status\":200},{\"path\":\"/game/1\",\"status\":200},{\"path\":\"/game/1/edit\",\"status\":200},{\"path\":\"/game/2\",\"status\":200},{\"path\":\"/game/3\",\"status\":200}],\"servedHtmlSHA256\":\"8a9f8aaff7e949b49f4cab1a87c5b6d414db4031b5fb3e268f5102bc40cbcc74\"}\n```"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,9 @@
{
"content": [
{
"type": "text",
"text": "Error: Cannot read properties of null (reading 'parentElement')"
}
],
"isError": true
}

View File

@ -0,0 +1,7 @@
id | name | display_width | display_height | display_source | webgl_path
----+-----------------------+---------------+----------------+----------------+----------------------------------------------------------------------------
1 | independent-portrait | 390 | 844 | build-info | /game/d4bf0e5b-5582-49f7-862d-be47e99092b2/GoraniCoin-WebGLTest/index.html
2 | independent-landscape | 1280 | 720 | build-info | /game/274566a9-8825-45f7-b255-b7de3cb148c8/index.html
3 | independent-square | 800 | 800 | build-info | /game/81081344-47f1-4fc7-b7ce-2cd6519bf6bd/index.html
(3 rows)

View File

@ -0,0 +1,5 @@
id | name | display_width | display_height | display_source
----+----------------------+---------------+----------------+----------------
1 | independent-portrait | 400 | 900 | manual
(1 row)

View File

@ -0,0 +1,205 @@
CREATE SCHEMA
CREATE SCHEMA
SET
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
ALTER TABLE
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
ALTER TABLE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
ALTER TABLE
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
CREATE INDEX
CREATE INDEX
ALTER TABLE
UPDATE 0
COMMENT
ALTER TABLE
COMMENT
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
DO
DO
CREATE INDEX
CREATE INDEX
COMMENT
COMMENT
CREATE VIEW
COMMENT
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
ALTER TABLE
ALTER TABLE
ALTER TABLE
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE VIEW
COMMENT
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
ALTER TABLE
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
DO
CREATE INDEX
CREATE INDEX
COMMENT
COMMENT
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
CREATE INDEX
DO
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
CREATE INDEX
CREATE INDEX
CREATE SEQUENCE
CREATE TABLE
ALTER SEQUENCE
DO
DO
CREATE INDEX
CREATE INDEX

View File

@ -0,0 +1,5 @@
id | name | display_width | display_height | display_source
----+--------------------+---------------+----------------+----------------
3 | independent-square | 1280 | 720 | build-info
(1 row)

View File

@ -0,0 +1,56 @@
{
"TEST-com.pandoli365.bibimbap.mapper.WebglDisplayDatabaseContractTest.xml": {
"tests": "1",
"failures": "0",
"errors": "0",
"cases": [
"actualMapperInsertUpdateAndEveryGameSelectRoundTripDisplayMetadata"
]
},
"TEST-com.pandoli365.bibimbap.util.WebglDisplayMetadataTest.xml": {
"tests": "6",
"failures": "0",
"errors": "0",
"cases": [
"portraitLandscapeAndSquareUseTheirOwnCanvasSizes",
"oversizeHtmlAndManifestAndSymlinksAreBounded",
"dynamicScriptAmbiguityAndBrowserDefaultsDoNotInventDimensions",
"nestedEntryOnlyReadsItsAdjacentManifest",
"manifestThenPixelCssThenAttributesHavePriority",
"invalidManifestFallsThroughAndBoundsAreEnforced"
]
},
"TEST-com.pandoli365.bibimbap.controller.api.GameDisplayMetadataTest.xml": {
"tests": "7",
"failures": "0",
"errors": "0",
"cases": [
"legacyCreateDetectsAndPathReplacementRedetectsWhileUnknownRemainsNullable",
"unknownAndLegacyFallbackDoNotBackfillAndCannotReadOutsideStorage",
"invalidCustomAndUnknownModeAre400AndDoNotWrite",
"customAndLegacyEditPreserveAuthorChoiceButAutoReplacesIt",
"autoCreateRedetectsServerFileAndRoundTripsIntoEditAndDetail",
"explicitCustomCanUseBrowserDefaultDimensionsAndDetailPrefersIt",
"csrfAuthenticationAndOwnershipPrecedeMetadataErrors"
]
},
"TEST-com.pandoli365.bibimbap.controller.api.GameUploadControllerSecurityTest.xml": {
"tests": "12",
"failures": "0",
"errors": "0",
"cases": [
"root_returns403_whenCsrfInvalid",
"webglZip_allowsReplace_whenOwner",
"webglZip_returns403_whenReplaceUuidNotOwned",
"webglZip_returns400_whenNotZipExtension",
"thumbnail_returns403_whenCsrfInvalid",
"webglZip_returns403_whenJamModeWithoutPermission",
"webglZip_returns403_whenCsrfInvalid",
"webglZip_returns413_whenOriginalTooLarge",
"webglZip_passesGate_whenAuthenticatedNonJam",
"webglZip_returns400_whenBadMagic",
"uploadDetectsEachBuildAndMalformedMetadataStillSucceeds",
"webglZip_returns401_whenNotAuthenticated"
]
}
}

View File

@ -0,0 +1,489 @@
bash: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): Bad file descriptor
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.pandoli365:bibimbap >-----------------------
[INFO] Building bibimbap 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ bibimbap ---
[INFO] Copying 7 resources from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.14.1:compile (default-compile) @ bibimbap ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 122 source files with javac [debug release 21] to target/classes
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ bibimbap ---
[INFO] skip non existing resourceDirectory /Users/wemadeplay/workspace/stz/bibimbap/src/test/resources
[INFO]
[INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ bibimbap ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 35 source files with javac [debug release 21] to target/test-classes
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[61,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[61,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[61,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[61,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[61,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[65,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[65,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[65,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[65,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[65,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[68,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[68,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[68,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[68,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[68,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[71,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[71,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[71,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[71,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[71,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[74,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[74,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[74,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[74,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[74,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[77,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[77,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[77,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[77,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[77,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[80,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[80,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[80,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[80,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[80,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[83,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[83,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[83,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[83,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[83,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[86,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[86,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[86,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[86,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[86,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[89,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[89,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[89,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[89,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[89,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[92,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[92,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[92,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[92,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[92,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[95,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[95,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[95,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[95,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[95,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[98,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[98,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[98,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[98,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[98,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[101,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[101,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[101,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[101,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[101,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[104,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[104,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[104,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[104,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[104,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[107,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[107,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[107,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[107,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[107,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[110,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[110,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[110,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[110,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[110,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[113,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[113,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[113,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[113,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[113,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[116,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[116,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[116,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[116,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[116,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[119,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[119,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[119,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[119,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[WARNING] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java:[119,6] org.springframework.boot.test.mock.mockito.MockBean in org.springframework.boot.test.mock.mockito has been deprecated and marked for removal
[INFO] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/controller/api/GameLikeControllerTest.java: /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/controller/api/GameLikeControllerTest.java uses or overrides a deprecated API.
[INFO] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/controller/api/GameLikeControllerTest.java: Recompile with -Xlint:deprecation for details.
[INFO] /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java: /Users/wemadeplay/workspace/stz/bibimbap/src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java has additional uses or overrides of a deprecated API that is marked for removal.
[INFO]
[INFO] --- surefire:3.5.5:test (default-test) @ bibimbap ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.pandoli365.bibimbap.util.WebglDisplayMetadataTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.214 s -- in com.pandoli365.bibimbap.util.WebglDisplayMetadataTest
[INFO] Running com.pandoli365.bibimbap.util.TagSanitizerTest
[INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.020 s -- in com.pandoli365.bibimbap.util.TagSanitizerTest
[INFO] Running com.pandoli365.bibimbap.security.PermissionGateTest
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
WARNING: A Java agent has been loaded dynamically (/Users/wemadeplay/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.658 s -- in com.pandoli365.bibimbap.security.PermissionGateTest
[INFO] Running com.pandoli365.bibimbap.security.SsrfSafeFetcherTest
[INFO] Tests run: 20, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.189 s -- in com.pandoli365.bibimbap.security.SsrfSafeFetcherTest
[INFO] Running com.pandoli365.bibimbap.security.ZipSecurityTest
[INFO] Tests run: 20, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.960 s -- in com.pandoli365.bibimbap.security.ZipSecurityTest
[INFO] Running com.pandoli365.bibimbap.security.JamRoleGateTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.039 s -- in com.pandoli365.bibimbap.security.JamRoleGateTest
[INFO] Running com.pandoli365.bibimbap.mapper.WebglDisplayDatabaseContractTest
[WARNING] Tests run: 1, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0 s -- in com.pandoli365.bibimbap.mapper.WebglDisplayDatabaseContractTest
[INFO] Running com.pandoli365.bibimbap.controller.AdminConsoleControllerTest
[INFO] Tests run: 13, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.054 s -- in com.pandoli365.bibimbap.controller.AdminConsoleControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.PostControllerTest
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.110 s -- in com.pandoli365.bibimbap.controller.PostControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.JamAwardAdminControllerTest
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.043 s -- in com.pandoli365.bibimbap.controller.JamAwardAdminControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.JamAwardControllerTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.027 s -- in com.pandoli365.bibimbap.controller.JamAwardControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.JamControllerTest
[INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.063 s -- in com.pandoli365.bibimbap.controller.JamControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.BadgeManageControllerTest
[INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.027 s -- in com.pandoli365.bibimbap.controller.BadgeManageControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.JamScoringControllerTest
[INFO] Tests run: 19, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.068 s -- in com.pandoli365.bibimbap.controller.JamScoringControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.SearchControllerTest
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.034 s -- in com.pandoli365.bibimbap.controller.SearchControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.api.GameCommentControllerTest
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.026 s -- in com.pandoli365.bibimbap.controller.api.GameCommentControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.api.GameLikeControllerTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.074 s -- in com.pandoli365.bibimbap.controller.api.GameLikeControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.api.GameReviewControllerTest
09:42:31.897 [main] WARN com.pandoli365.bibimbap.controller.api.GameReviewController -- [BADGE] ?? ?? ? ?? userId=7 reviewId=50
java.lang.RuntimeException: boom
at com.pandoli365.bibimbap.badge.ReputationService.record(ReputationService.java:22)
at com.pandoli365.bibimbap.controller.api.GameReviewController.createReview(GameReviewController.java:222)
at com.pandoli365.bibimbap.controller.api.GameReviewControllerTest.createReview(GameReviewControllerTest.java:525)
at com.pandoli365.bibimbap.controller.api.GameReviewControllerTest.createReviewSucceedsEvenWhenReputationHookThrows(GameReviewControllerTest.java:447)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:775)
at org.junit.platform.commons.support.ReflectionSupport.invokeMethod(ReflectionSupport.java:479)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:161)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:152)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:91)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:112)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:94)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:93)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:87)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:216)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:212)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:156)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:146)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:144)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:100)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:160)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:146)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:144)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:100)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:160)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:146)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:144)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:143)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:100)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:201)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:170)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:94)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:59)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:142)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:58)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:103)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$1(InterceptingLauncher.java:39)
at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25)
at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:38)
at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)
at org.apache.maven.surefire.junitplatform.LauncherAdapter.executeWithoutCancellationToken(LauncherAdapter.java:60)
at org.apache.maven.surefire.junitplatform.LauncherAdapter.execute(LauncherAdapter.java:52)
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:203)
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:168)
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:136)
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385)
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507)
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495)
[INFO] Tests run: 24, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.058 s -- in com.pandoli365.bibimbap.controller.api.GameReviewControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.api.GameUploadControllerSecurityTest
09:42:31.933 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=701, contentType=application/zip
09:42:31.942 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload completed. gameUuid=01c16184-c912-4e6a-a91e-d1866c38221f, webglPath=/game/01c16184-c912-4e6a-a91e-d1866c38221f/index.html, entryCount=5, extractedBytes=56
09:42:31.948 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=701, contentType=application/zip
09:42:31.950 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.txt, size=701, contentType=text/plain
09:42:31.956 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=701, contentType=application/zip
09:42:31.960 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=537919488, contentType=application/zip
09:42:31.962 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=701, contentType=application/zip
09:42:31.972 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload completed. gameUuid=69b411a6-a08b-481c-b31d-4b29f26d275f, webglPath=/game/69b411a6-a08b-481c-b31d-4b29f26d275f/index.html, entryCount=5, extractedBytes=56
09:42:31.978 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=9, contentType=application/zip
09:42:31.980 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=929, contentType=application/zip
09:42:31.989 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload completed. gameUuid=d5cc7729-aed7-4bd2-8edf-4644efc5d21f, webglPath=/game/d5cc7729-aed7-4bd2-8edf-4644efc5d21f/nested/index.html, entryCount=6, extractedBytes=95
09:42:31.989 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=930, contentType=application/zip
09:42:31.994 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload completed. gameUuid=e1b3bdf4-3c52-4fde-88cf-4d132b7bf169, webglPath=/game/e1b3bdf4-3c52-4fde-88cf-4d132b7bf169/nested/index.html, entryCount=6, extractedBytes=96
09:42:31.994 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=924, contentType=application/zip
09:42:32.001 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload completed. gameUuid=02b8dc25-68f6-4192-b57d-c3f0d88044f6, webglPath=/game/02b8dc25-68f6-4192-b57d-c3f0d88044f6/nested/index.html, entryCount=6, extractedBytes=95
09:42:32.001 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=888, contentType=application/zip
09:42:32.010 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload completed. gameUuid=81843fa5-c951-42dc-9994-8745df568909, webglPath=/game/81843fa5-c951-42dc-9994-8745df568909/nested/index.html, entryCount=6, extractedBytes=52
09:42:32.028 [main] INFO com.pandoli365.bibimbap.controller.api.GameUploadController -- WebGL zip upload request received. fileName=build.zip, size=701, contentType=application/zip
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.118 s -- in com.pandoli365.bibimbap.controller.api.GameUploadControllerSecurityTest
[INFO] Running com.pandoli365.bibimbap.controller.api.GameDisplayMetadataTest
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.042 s -- in com.pandoli365.bibimbap.controller.api.GameDisplayMetadataTest
[INFO] Running com.pandoli365.bibimbap.controller.api.UserControllerCsrfTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.485 s -- in com.pandoli365.bibimbap.controller.api.UserControllerCsrfTest
[INFO] Running com.pandoli365.bibimbap.controller.TagControllerTest
[INFO] Tests run: 19, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.023 s -- in com.pandoli365.bibimbap.controller.TagControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.WebMvcControllerTest
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.022 s -- in com.pandoli365.bibimbap.controller.WebMvcControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.JamJudgeAdminControllerTest
[INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.007 s -- in com.pandoli365.bibimbap.controller.JamJudgeAdminControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.JamVoteControllerTest
[INFO] Tests run: 23, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.024 s -- in com.pandoli365.bibimbap.controller.JamVoteControllerTest
[INFO] Running com.pandoli365.bibimbap.controller.JamAdminControllerTest
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.035 s -- in com.pandoli365.bibimbap.controller.JamAdminControllerTest
[INFO] Running com.pandoli365.bibimbap.BibimbapApplicationTests
09:42:32.705 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.pandoli365.bibimbap.BibimbapApplicationTests]: BibimbapApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
09:42:32.763 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.pandoli365.bibimbap.BibimbapApplication for test class com.pandoli365.bibimbap.BibimbapApplicationTests
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.5.16)
2026-10-14T09:42:32.908+09:00 INFO 41713 --- [bibimbap] [ main] c.p.bibimbap.BibimbapApplicationTests : Starting BibimbapApplicationTests using Java 21.0.11 with PID 41713 (started by wemadeplay in /Users/wemadeplay/workspace/stz/bibimbap)
2026-10-14T09:42:32.909+09:00 INFO 41713 --- [bibimbap] [ main] c.p.bibimbap.BibimbapApplicationTests : The following 1 profile is active: "dev"
2026-10-14T09:42:33.541+09:00 INFO 41713 --- [bibimbap] [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page template: index
2026-10-14T09:42:33.606+09:00 WARN 41713 --- [bibimbap] [ main] o.s.w.s.resource.ResourceHandlerUtils : Appended trailing slash to static resource location: file:///Users/wemadeplay/workspace/stz/bibimbap/src/main/resources/static/game/profile/
2026-10-14T09:42:33.656+09:00 INFO 41713 --- [bibimbap] [ main] c.p.bibimbap.BibimbapApplicationTests : Started BibimbapApplicationTests in 0.845 seconds (process running for 17.757)
2026-10-14T09:42:33.658+09:00 INFO 41713 --- [bibimbap] [ main] c.p.bibimbap.config.BadgeCatalogSeeder : [BADGE] ?? ???? ?? ??: 2 ?
2026-10-14T09:42:33.658+09:00 INFO 41713 --- [bibimbap] [ main] c.p.b.config.PermissionCatalogVerifier : [RBAC] ?? ???? ?? ??: 4 ?
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.994 s -- in com.pandoli365.bibimbap.BibimbapApplicationTests
[INFO] Running com.pandoli365.bibimbap.service.PostMarkdownServiceTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 s -- in com.pandoli365.bibimbap.service.PostMarkdownServiceTest
[INFO] Running com.pandoli365.bibimbap.service.BadgeServiceTest
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in com.pandoli365.bibimbap.service.BadgeServiceTest
[INFO] Running com.pandoli365.bibimbap.service.UnityFeedPollerTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.020 s -- in com.pandoli365.bibimbap.service.UnityFeedPollerTest
[INFO] Running com.pandoli365.bibimbap.service.FeedParserTest
[Fatal Error] :1:1: ??????? ???? ???? ????.
[Fatal Error] :2:10: DOCTYPE? "http://apache.org/xml/features/disallow-doctype-decl" ??? true? ??? ?? ???? ????.
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.031 s -- in com.pandoli365.bibimbap.service.FeedParserTest
[INFO] Running com.pandoli365.bibimbap.jam.JamAwardServiceTest
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 s -- in com.pandoli365.bibimbap.jam.JamAwardServiceTest
[INFO] Running com.pandoli365.bibimbap.jam.JamLifecycleTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 s -- in com.pandoli365.bibimbap.jam.JamLifecycleTest
[INFO] Running com.pandoli365.bibimbap.jam.JamEvalWindowTest
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 s -- in com.pandoli365.bibimbap.jam.JamEvalWindowTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 377, Failures: 0, Errors: 0, Skipped: 1
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 21.623 s
[INFO] Finished at: 2026-10-14T09:42:33+09:00
[INFO] ------------------------------------------------------------------------
TAP version 13
# Subtest: every uploaded build fits both axes in every normal and expanded viewport
ok 1 - every uploaded build fits both axes in every normal and expanded viewport
---
duration_ms: 0.692625
type: 'test'
...
# Subtest: portrait iframe uses build aspect rather than the former fixed landscape rectangle
ok 2 - portrait iframe uses build aspect rather than the former fixed landscape rectangle
---
duration_ms: 0.063958
type: 'test'
...
# Subtest: SSR player ships the stored ratio and responsive stage without the old hard limit
ok 3 - SSR player ships the stored ratio and responsive stage without the old hard limit
---
duration_ms: 2.61775
type: 'test'
...
# Subtest: resize and overlay preserve same iframe, game state and source; close restores scroll/focus
ok 4 - resize and overlay preserve same iframe, game state and source; close restores scroll/focus
---
duration_ms: 0.482375
type: 'test'
...
# Subtest: successful fullscreen enters and exits without reload and restores focus
ok 5 - successful fullscreen enters and exits without reload and restores focus
---
duration_ms: 3.01275
type: 'test'
...
# Subtest: unsupported and rejected fullscreen use dismissible overlay
ok 6 - unsupported and rejected fullscreen use dismissible overlay
---
duration_ms: 0.517167
type: 'test'
...
# Subtest: native fullscreen exit from overlay returns to overlay; Tab remains within player
ok 7 - native fullscreen exit from overlay returns to overlay; Tab remains within player
---
duration_ms: 0.333167
type: 'test'
...
# Subtest: invalid reference metadata retains safe fallback rather than overflowing
ok 8 - invalid reference metadata retains safe fallback rather than overflowing
---
duration_ms: 0.819833
type: 'test'
...
# Subtest: ZIP selection discovers each build before save; automatic metadata resets on replacement
ok 9 - ZIP selection discovers each build before save; automatic metadata resets on replacement
---
duration_ms: 3.199
type: 'test'
...
# Subtest: custom dimensions survive new ZIP and final save chains server upload path
ok 10 - custom dimensions survive new ZIP and final save chains server upload path
---
duration_ms: 1.52025
type: 'test'
...
# Subtest: automatic save sends mode and upload path and lets server resolve dimensions
ok 11 - automatic save sends mode and upload path and lets server resolve dimensions
---
duration_ms: 0.855791
type: 'test'
...
1..11
# tests 11
# suites 0
# pass 11
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 59.209958
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.pandoli365:bibimbap >-----------------------
[INFO] Building bibimbap 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ bibimbap ---
[INFO] Copying 7 resources from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.14.1:compile (default-compile) @ bibimbap ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ bibimbap ---
[INFO] skip non existing resourceDirectory /Users/wemadeplay/workspace/stz/bibimbap/src/test/resources
[INFO]
[INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ bibimbap ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- surefire:3.5.5:test (default-test) @ bibimbap ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- war:3.4.0:war (default-war) @ bibimbap ---
[INFO] Packaging webapp
[INFO] Assembling webapp [bibimbap] in [/Users/wemadeplay/workspace/stz/bibimbap/target/bibimbap-0.0.1-SNAPSHOT]
[INFO] Processing war project
[INFO] Copying webapp resources [/Users/wemadeplay/workspace/stz/bibimbap/src/main/webapp]
[INFO] Building war: /Users/wemadeplay/workspace/stz/bibimbap/target/bibimbap-0.0.1-SNAPSHOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.073 s
[INFO] Finished at: 2026-10-14T09:42:35+09:00
[INFO] ------------------------------------------------------------------------
L1 WebGL display: PASS (Java regression, Node player regression, WAR package)
NOTICE: column "display_width" of relation "games" already exists, skipping
NOTICE: column "display_height" of relation "games" already exists, skipping
NOTICE: column "display_source" of relation "games" already exists, skipping
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.pandoli365:bibimbap >-----------------------
[INFO] Building bibimbap 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ bibimbap ---
[INFO] Copying 7 resources from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.14.1:compile (default-compile) @ bibimbap ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ bibimbap ---
[INFO] skip non existing resourceDirectory /Users/wemadeplay/workspace/stz/bibimbap/src/test/resources
[INFO]
[INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ bibimbap ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- surefire:3.5.5:test (default-test) @ bibimbap ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.pandoli365.bibimbap.mapper.WebglDisplayDatabaseContractTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.228 s -- in com.pandoli365.bibimbap.mapper.WebglDisplayDatabaseContractTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.457 s
[INFO] Finished at: 2026-10-14T09:42:41+09:00
[INFO] ------------------------------------------------------------------------
L2 WebGL display: PASS (legacy row, idempotency, checks, fresh equivalence, actual mapper roundtrip)

View File

@ -0,0 +1,4 @@
bash: warning: setlocale: LC_ALL: cannot change locale (C.UTF-8): Bad file descriptor
The operation couldn’t be completed. Unable to locate a Java Runtime.
Please visit http://www.java.com for information on installing Java.

View File

@ -0,0 +1,22 @@
---
phase: documentation
agent: documentation-advisor
agent_version: 1
report_invocation_id: inv-005
generated_at: 2026-10-14T09:54:53.525775+09:00
concerns_checked: true
concerns:
- Existing usage index setup-checklist.md and faq.md links have no target; unrelated to feature.
- Trusted touchscreen and named Unity button outcome remain unverified.
- Baseline failure evidence is SSR markup regression; previous browser geometry was not replayed.
---
# 문서화 보고
| 경로 | 카테고리 | 유형 | 링크 추가한 index | 교차 링크 |
|---|---|---|---|---|
| docs/usage/webgl-display.md | usage | 실행 명령/Java 환경 교정 | docs/usage/index.md 기존 링크 확인 | contracts/webgl-display-metadata.md |
| docs/changes/2026-10-14-webgl-display.md | changes | 실제 검증 결과/한계 기록 | docs/changes/index.md 기존 링크 확인 | usage/webgl-display.md, 세션 검증/감사 |
실행 계약은 usage, 이번 구현 결과는 changes, 메타 계약은 contracts에 유지했다. 문서 스코프 대조 완료. 상세 감사: [final-doc-ui-audit.md](artifacts/final-doc-ui-audit.md). 미완료: trusted touch/named Unity 버튼 결과, 이전 브라우저 geometry. 구현 제품/테스트 코드와 DB/런타임/정책은 변경하지 않았다.
self_verification: checklist_passed=true; backport=N/A. documentation-guidelines.md는 없어 category/advisor 체크리스트를 적용했다.

View File

@ -0,0 +1,116 @@
---
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 가능하다.

View File

@ -0,0 +1,12 @@
{"recorded_at": "2026-10-14T09:31:17.298843+09:00", "logical_task": "backend", "report_invocation_id": "inv-003-w01", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_1of4_f610aa01", "attempt": 1, "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0, "event": "accepted", "provenance": "environment", "source_ref": "collaboration.spawn_agent response /root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_1of4_f610aa01"}
{"recorded_at": "2026-10-14T09:31:17.298843+09:00", "logical_task": "backend", "report_invocation_id": "inv-003-w01", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_1of4_f610aa01", "attempt": 1, "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0, "event": "ownership_active", "provenance": "advisor", "source_ref": "implementation/ownership.md#backend", "scope": ["backend"], "rationale": "Explicit allocated file write authority"}
{"recorded_at": "2026-10-14T09:31:17.298843+09:00", "logical_task": "frontend", "report_invocation_id": "inv-003-w02", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_2of4_f610aa02", "attempt": 1, "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0, "event": "accepted", "provenance": "environment", "source_ref": "collaboration.spawn_agent response /root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_2of4_f610aa02"}
{"recorded_at": "2026-10-14T09:31:17.298843+09:00", "logical_task": "frontend", "report_invocation_id": "inv-003-w02", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_2of4_f610aa02", "attempt": 1, "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0, "event": "ownership_active", "provenance": "advisor", "source_ref": "implementation/ownership.md#frontend", "scope": ["frontend"], "rationale": "Explicit allocated file write authority"}
{"recorded_at": "2026-10-14T09:35:46.542933+09:00", "logical_task": "backend", "report_invocation_id": "inv-003-w01", "environment_invocation_id": "01a137d1-c16a-7cc1-b011-f03b6435a0e3", "attempt": 1, "event": "completed", "provenance": "environment", "source_ref": "ATP_POOL_TERMINAL_DELTA wgworkers01 index1 sha88f4812ab710268d1ffef930eb4c40dba4708e83a676391753d832d8bbcd9688", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0}
{"recorded_at": "2026-10-14T09:35:46.542933+09:00", "logical_task": "migration", "report_invocation_id": "inv-003-w03", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_3of4_f610aa03", "attempt": 1, "event": "accepted", "provenance": "environment", "source_ref": "collaboration.spawn_agent response", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0}
{"recorded_at": "2026-10-14T09:35:46.542933+09:00", "logical_task": "migration", "report_invocation_id": "inv-003-w03", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_3of4_f610aa03", "attempt": 1, "event": "ownership_active", "provenance": "advisor", "source_ref": "implementation/ownership.md#migration", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0, "scope": ["migration"], "rationale": "Explicit allocated write authority"}
{"recorded_at": "2026-10-14T09:36:18.956153+09:00", "logical_task": "frontend", "report_invocation_id": "inv-003-w02", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_2of4_f610aa02", "attempt": 1, "event": "completed", "provenance": "environment", "source_ref": "collaboration FINAL_ANSWER wgworkers01 2/4 f610aa02", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0}
{"recorded_at": "2026-10-14T09:36:18.956153+09:00", "logical_task": "docs-verification", "report_invocation_id": "inv-003-w04", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_4of4_f610aa04", "attempt": 1, "event": "accepted", "provenance": "environment", "source_ref": "collaboration.spawn_agent response wgworkers01 4/4", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0}
{"recorded_at": "2026-10-14T09:36:18.956153+09:00", "logical_task": "docs-verification", "report_invocation_id": "inv-003-w04", "environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_4of4_f610aa04", "attempt": 1, "event": "ownership_active", "provenance": "advisor", "source_ref": "implementation/ownership.md#docs-verification", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0, "scope": ["docs-verification"], "rationale": "Explicit allocated write authority"}
{"recorded_at": "2026-10-14T09:36:43.485758+09:00", "logical_task": "migration", "report_invocation_id": "inv-003-w03", "environment_invocation_id": "01a137d5-fbdb-7f31-a4aa-87ad3782bff0", "attempt": 1, "event": "completed", "provenance": "environment", "source_ref": "ATP_POOL_TERMINAL_DELTA wgworkers01 3/4 sha11a533967ee89942af4dc9ec55b7c6a20e8ace2b9d838080053bf979c7e272b2", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0}
{"recorded_at": "2026-10-14T09:40:31.246818+09:00", "logical_task": "docs-verification", "report_invocation_id": "inv-003-w04", "environment_invocation_id": "01a137d6-5eab-7600-a956-13344e6c6f2f", "attempt": 1, "event": "completed", "provenance": "environment", "source_ref": "ATP_POOL_TERMINAL_DELTA wgworkers01 4/4 shaa871dd0ad901598b785b5386f2c299c786a32bba7c2634d76d0b9aa1aeb7a924", "clean_retry_limit": 0, "clean_retry_limit_source": "parent no automatic retry authorization", "retries_spawned": 0}

View File

@ -0,0 +1,169 @@
[
{
"scope_id": "backend",
"scope_kind": "directory",
"scope_paths": [
"src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java",
"src/main/java/com/pandoli365/bibimbap/data/GameData.java",
"src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java",
"src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w01",
"owner_environment_invocation_id": "01a137d1-c16a-7cc1-b011-f03b6435a0e3",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": null,
"namespace": null,
"namespace_key": null,
"schema_files": []
},
"generated_paths": [
"src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java",
"src/main/java/com/pandoli365/bibimbap/data/GameData.java",
"src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java",
"src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
},
{
"scope_id": "frontend",
"scope_kind": "directory",
"scope_paths": [
"src/main/webapp/WEB-INF/views/game-register.jsp",
"src/main/webapp/WEB-INF/views/game-detail.jsp",
"src/main/webapp/js/webgl-player.js",
"src/main/webapp/css/webgl-player.css",
"src/test/js/webgl-player.test.cjs"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w02",
"owner_environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_2of4_f610aa02",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": null,
"namespace": null,
"namespace_key": null,
"schema_files": []
},
"generated_paths": [
"src/main/webapp/WEB-INF/views/game-register.jsp",
"src/main/webapp/WEB-INF/views/game-detail.jsp",
"src/main/webapp/js/webgl-player.js",
"src/main/webapp/css/webgl-player.css",
"src/test/js/webgl-player.test.cjs"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
},
{
"scope_id": "migration",
"scope_kind": "migration_namespace",
"scope_paths": [
"db/schema.sql",
"db/migrations/20261014-game-display-metadata.sql"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w03",
"owner_environment_invocation_id": "01a137d5-fbdb-7f31-a4aa-87ad3782bff0",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": "db/migrations",
"namespace": "20261014-game-display-metadata",
"namespace_key": "db/migrations::20261014-game-display-metadata",
"schema_files": [
"db/schema.sql"
]
},
"generated_paths": [
"db/schema.sql",
"db/migrations/20261014-game-display-metadata.sql"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
},
{
"scope_id": "docs-verification",
"scope_kind": "directory",
"scope_paths": [
"docs/contracts/webgl-display-metadata.md",
"docs/usage/webgl-display.md",
"docs/changes/2026-10-14-webgl-display.md",
"docs/contracts/index.md",
"docs/usage/index.md",
"docs/changes/index.md",
"docs/development/verification-strategies.md",
"scripts/verify-webgl-display.sh",
"src/test/java/com/pandoli365/bibimbap/mapper/WebglDisplayDatabaseContractTest.java"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w04",
"owner_environment_invocation_id": "01a137d6-5eab-7600-a956-13344e6c6f2f",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": null,
"namespace": null,
"namespace_key": null,
"schema_files": []
},
"generated_paths": [
"docs/contracts/webgl-display-metadata.md",
"docs/usage/webgl-display.md",
"docs/changes/2026-10-14-webgl-display.md",
"docs/contracts/index.md",
"docs/usage/index.md",
"docs/changes/index.md",
"docs/development/verification-strategies.md",
"scripts/verify-webgl-display.sh",
"src/test/java/com/pandoli365/bibimbap/mapper/WebglDisplayDatabaseContractTest.java"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
}
]

View File

@ -0,0 +1,185 @@
---
phase: implementation
agent: implementation-advisor
agent_version: 2
generated_at: 2026-10-14T09:40:31.246818+09:00
concerns: []
concerns_checked: true
lifecycle_ledger: "./lifecycle-events.jsonl"
---
# 파일 소유권 맵
```json
{
"ownership_records": [
{
"scope_id": "backend",
"scope_kind": "directory",
"scope_paths": [
"src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java",
"src/main/java/com/pandoli365/bibimbap/data/GameData.java",
"src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java",
"src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/BibimbapApplicationTests.java"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w01",
"owner_environment_invocation_id": "01a137d1-c16a-7cc1-b011-f03b6435a0e3",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": null,
"namespace": null,
"namespace_key": null,
"schema_files": []
},
"generated_paths": [
"src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java",
"src/main/java/com/pandoli365/bibimbap/data/GameData.java",
"src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java",
"src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
},
{
"scope_id": "frontend",
"scope_kind": "directory",
"scope_paths": [
"src/main/webapp/WEB-INF/views/game-register.jsp",
"src/main/webapp/WEB-INF/views/game-detail.jsp",
"src/main/webapp/js/webgl-player.js",
"src/main/webapp/css/webgl-player.css",
"src/test/js/webgl-player.test.cjs"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w02",
"owner_environment_invocation_id": "/root/atp_pool_wgimpl01_1of1_a6814cde/atp_pool_wgworkers01_2of4_f610aa02",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": null,
"namespace": null,
"namespace_key": null,
"schema_files": []
},
"generated_paths": [
"src/main/webapp/WEB-INF/views/game-register.jsp",
"src/main/webapp/WEB-INF/views/game-detail.jsp",
"src/main/webapp/js/webgl-player.js",
"src/main/webapp/css/webgl-player.css",
"src/test/js/webgl-player.test.cjs"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
},
{
"scope_id": "migration",
"scope_kind": "migration_namespace",
"scope_paths": [
"db/schema.sql",
"db/migrations/20261014-game-display-metadata.sql"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w03",
"owner_environment_invocation_id": "01a137d5-fbdb-7f31-a4aa-87ad3782bff0",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": "db/migrations",
"namespace": "20261014-game-display-metadata",
"namespace_key": "db/migrations::20261014-game-display-metadata",
"schema_files": [
"db/schema.sql"
]
},
"generated_paths": [
"db/schema.sql",
"db/migrations/20261014-game-display-metadata.sql"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
},
{
"scope_id": "docs-verification",
"scope_kind": "directory",
"scope_paths": [
"docs/contracts/webgl-display-metadata.md",
"docs/usage/webgl-display.md",
"docs/changes/2026-10-14-webgl-display.md",
"docs/contracts/index.md",
"docs/usage/index.md",
"docs/changes/index.md",
"docs/development/verification-strategies.md",
"scripts/verify-webgl-display.sh",
"src/test/java/com/pandoli365/bibimbap/mapper/WebglDisplayDatabaseContractTest.java"
],
"state": "released",
"owner_report_invocation_id": "inv-003-w04",
"owner_environment_invocation_id": "01a137d6-5eab-7600-a956-13344e6c6f2f",
"revoked_from_report_invocation_id": null,
"revoked_from_environment_invocation_id": null,
"handoff_to_report_invocation_id": null,
"handoff_to_environment_invocation_id": null,
"handoff_at": null,
"dependencies": [],
"reservation": {
"directory": null,
"namespace": null,
"namespace_key": null,
"schema_files": []
},
"generated_paths": [
"docs/contracts/webgl-display-metadata.md",
"docs/usage/webgl-display.md",
"docs/changes/2026-10-14-webgl-display.md",
"docs/contracts/index.md",
"docs/usage/index.md",
"docs/changes/index.md",
"docs/development/verification-strategies.md",
"scripts/verify-webgl-display.sh",
"src/test/java/com/pandoli365/bibimbap/mapper/WebglDisplayDatabaseContractTest.java"
],
"pause": {
"reason": null,
"source_ref": null,
"caused_by_report_invocation_id": null,
"caused_by_environment_invocation_id": null
}
}
]
}
```

View File

@ -0,0 +1,68 @@
---
phase: implementation
agent: implementation-advisor
agent_version: 2
generated_at: 2026-10-14T09:40:31.246818+09:00
concerns_checked: true
concerns:
- "Automated unused diagnostics unavailable: no LSP/linter tool; verification must assess separate unused gate."
workers_spawned: 4
planned_workers: 4
actual_workers: 4
---
# 구현 보고
## 변경 목록
| 파일 | worker | 결과 요약 |
|---|---|---|
| src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java | inv-003-w01 | backend 구현 |
| src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java | inv-003-w01 | backend 구현 |
| src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java | inv-003-w01 | backend 구현 |
| src/main/java/com/pandoli365/bibimbap/data/GameData.java | inv-003-w01 | backend 구현 |
| src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java | inv-003-w01 | backend 구현 |
| src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java | inv-003-w01 | backend 구현 |
| src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java | inv-003-w01 | backend 구현 |
| src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java | inv-003-w01 | backend 구현 |
| src/main/webapp/WEB-INF/views/game-register.jsp | inv-003-w02 | frontend 구현 |
| src/main/webapp/WEB-INF/views/game-detail.jsp | inv-003-w02 | frontend 구현 |
| src/main/webapp/js/webgl-player.js | inv-003-w02 | frontend 구현 |
| src/main/webapp/css/webgl-player.css | inv-003-w02 | frontend 구현 |
| src/test/js/webgl-player.test.cjs | inv-003-w02 | frontend 구현 |
| db/schema.sql | inv-003-w03 | migration 구현 |
| db/migrations/20261014-game-display-metadata.sql | inv-003-w03 | migration 구현 |
| docs/contracts/webgl-display-metadata.md | inv-003-w04 | docs-verification 구현 |
| docs/usage/webgl-display.md | inv-003-w04 | docs-verification 구현 |
| docs/changes/2026-10-14-webgl-display.md | inv-003-w04 | docs-verification 구현 |
| docs/contracts/index.md | inv-003-w04 | docs-verification 구현 |
| docs/usage/index.md | inv-003-w04 | docs-verification 구현 |
| docs/changes/index.md | inv-003-w04 | docs-verification 구현 |
| docs/development/verification-strategies.md | inv-003-w04 | docs-verification 구현 |
| scripts/verify-webgl-display.sh | inv-003-w04 | docs-verification 구현 |
| src/test/java/com/pandoli365/bibimbap/mapper/WebglDisplayDatabaseContractTest.java | inv-003-w04 | docs-verification 구현 |
## Lifecycle ledger
- [lifecycle-events.jsonl](./lifecycle-events.jsonl)
- [ownership.md](./ownership.md)
- [worker-invocations.json](./worker-invocations.json) — report schema v2 append-ready worker payloads; unavailable token telemetry null.
## Wait/wakeup ledger
- [wait-wakeup-events.jsonl](./wait-wakeup-events.jsonl): managed bounded pool 4 requested/accepted/terminal/collected; max2 concurrent; 0 list/interrupt/recovery.
## Bash 단계 (advisor 직접)
- Read-only git diff/status and source contract inspection, artifact ledger/report serialization. No tests/build/runtime/DB/commit/push executed.
- update_plan unavailable for advisor/workers; bind not fabricated.
## 설계와의 차이
- Existing JSP uses separate edit attributes, so approved editDisplayWidth/Height/Source naming replaces hypothetical editModel object.
- Uploaded JavaScript style-variable assignments conservatively omitted; literal CSS, adjacent build-info, canvas attrs supported, manual override covers unknown templates.
- Added actual isolated MyBatis mapper contract test to requested docs/harness worker scope before writing.
- Coherent backend/frontend groups allocated explicitly; disjoint files, schema-only migration worker, fourth code-writer for docs/harness.
## Verification 을 위한 힌트
- Run `bash scripts/verify-webgl-display.sh all` (full offline Maven tests, Node tests, WAR package, disposable PostgreSQL migration/mapper contract).
- L2 requires local postgres:16 image and Docker daemon; override WEBGL_CONTRACT_POSTGRES_IMAGE to local compatible image, no auto pull.
- Runtime/auth/JSP/actual ZIP commands: docs/usage/webgl-display.md. Root has Chrome tools for isolated browser smoke.
- Test matrix: WebglDisplayMetadataTest, GameDisplayMetadataTest, GameUploadControllerSecurityTest, WebglDisplayDatabaseContractTest, src/test/js/webgl-player.test.cjs.
- No compile/test success claimed; unused automated gate unperformed because tools unavailable.
- Original ZIP/served uploaded HTML untouched.

View File

@ -0,0 +1,2 @@
{"event": "await_capability_checked", "recorded_at": "2026-10-14T09:30:42.577553+09:00", "owner_report_invocation_id": "inv-003", "formal_adapter_enabled": false, "host_managed_subagent_orchestration": "supported", "selected_mode": "host_managed_subagent_orchestration", "capability_profile_ref": "atp:codex-team \u00a77"}
{"event": "measurement", "recorded_at": "2026-10-14T09:40:31.246818+09:00", "owner_report_invocation_id": "inv-003", "requested_agents": 4, "spawn_calls": 4, "accepted_spawns": 4, "capacity_denials": 0, "maximum_observed_running": 2, "nonterminal_updates": null, "terminal_deliveries": 4, "collected_results": 4, "generic_list_calls": 0, "interrupt_calls": 0, "semantic_recovery_actions": 0, "pool_wait_calls": null, "pending_refills": 2, "final_pending": 0, "final_running": 0}

View File

@ -0,0 +1,162 @@
[
{
"id": "inv-003-w01",
"layer": "worker",
"name": "code-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:31:17.298843+09:00",
"ended_at": "2026-10-14T09:35:46.542933+09:00",
"input_digest": "Exclusive backend file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Static bounded metadata, upload/server/SSR/mapper persistence and regression tests",
"artifacts": [
"src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java",
"src/main/java/com/pandoli365/bibimbap/data/GameData.java",
"src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java",
"src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "large",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
},
{
"id": "inv-003-w02",
"layer": "worker",
"name": "code-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:31:17.298843+09:00",
"ended_at": "2026-10-14T09:36:18.956153+09:00",
"input_digest": "Exclusive frontend file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Generic contain player, author controls, overlay/fullscreen stable iframe and 11 Node tests",
"artifacts": [
"src/main/webapp/WEB-INF/views/game-register.jsp",
"src/main/webapp/WEB-INF/views/game-detail.jsp",
"src/main/webapp/js/webgl-player.js",
"src/main/webapp/css/webgl-player.css",
"src/test/js/webgl-player.test.cjs"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "large",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
},
{
"id": "inv-003-w03",
"layer": "worker",
"name": "migration-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:35:46.542933+09:00",
"ended_at": "2026-10-14T09:36:43.485758+09:00",
"input_digest": "Exclusive migration file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Nullable additive idempotent schema/migration; no DB apply",
"artifacts": [
"db/schema.sql",
"db/migrations/20261014-game-display-metadata.sql"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "medium",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
},
{
"id": "inv-003-w04",
"layer": "worker",
"name": "code-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:36:18.956153+09:00",
"ended_at": "2026-10-14T09:40:31.246818+09:00",
"input_digest": "Exclusive docs-verification file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Contract/usage/change docs, integrated L1/L2 script and isolated actual MyBatis roundtrip test",
"artifacts": [
"docs/contracts/webgl-display-metadata.md",
"docs/usage/webgl-display.md",
"docs/changes/2026-10-14-webgl-display.md",
"docs/contracts/index.md",
"docs/usage/index.md",
"docs/changes/index.md",
"docs/development/verification-strategies.md",
"scripts/verify-webgl-display.sh",
"src/test/java/com/pandoli365/bibimbap/mapper/WebglDisplayDatabaseContractTest.java"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "medium",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
}
]

View File

@ -0,0 +1,425 @@
---
schema_version: 2
session_id: 20261014-091658
started_at: 2026-10-14T09:17:15.328427+09:00
ended_at: 2026-10-14T10:02:57.818935+09:00
user_request: |
Downloads/GoraniCoin-WebGLTest.zip Unity WebGL 빌드의 실행 화면 해상도 대응 기능 추가.
---
# Summary
업로드 게임별 기준 크기 감지/저장, 작성자 보정, 비율 유지 플레이어 및 확대/전체화면 구현 완료. 사용자 추가 발화로 예시 전용에서 일반 업로드 대응 범위를 명확히 했다. 전체 테스트/DB 계약 및 독립 runtime/보완 trusted touch 검증 통과.
# Invocations
- id: inv-001
layer: advisor
name: research-advisor
parent_invocation_id: null
started_at: 2026-10-14T09:17:15.328427+09:00
ended_at: 2026-10-14T09:20:31.663736+09:00
termination: completed
input_digest: ZIP HTML 및 서비스 플레이어의 크기 정책 조사, 수정 가능 범위와 계획 근거 수집. 코드 변경 금지.
output_digest: 원본 390:844 유지 및 양축 contain 플레이어 권장, 내부 UI 변경에는 Unity 소스 필요 가능성
artifacts: [research/research.md]
model_choice: {tier: large, resolved_model: inherit, rationale: 빌드와 호스트 경계의 원인 조사}
attempt: 1
# Advisor Invocation Decision Log
- advisor: research-advisor
decision: call
rationale: 빌드물과 호스트 화면의 크기 정책을 근거로 구분해야 함
checked_at: 2026-10-14T09:17:15.328427+09:00
- advisor: graphify-lookup-advisor
decision: skip
rationale: ZIP은 그래프 외부이며 JSP/CSS 플레이어는 프로젝트 문서에 그래프 미지원으로 명시
checked_at: 2026-10-14T09:17:15.328427+09:00
# Decisions
- selected_mode: host_managed_subagent_orchestration
request_intent: general_task
rationale: exact ATP hook marker 존재, Codex 배포 profile 적용
- 구현은 조사 결과 기반 계획 제시 및 사용자 동의 뒤 진행 (task SKILL §5.0).
# Open Items
- 기존 사용자 잔여: .atp/work-session/20260724-095414/, .atp/work-session/20260826-093449/, ~/ (작업 제외).
- update_plan 도구가 현재 도구 목록에 없음. child에 bind 수행 가능 여부 명시 요청, 미수행을 성공으로 기록하지 않음.
# Phase Status
- research: completed (terminal delta 및 FINAL_ANSWER token 일치, artifact 검증 완료)
- implementation: authorized_build_aware_general_support
- verified_by_me: research artifact 존재/근거 검토; 제품 코드 변경 0
- needs_user_verification: 아직 구현 전. 브라우저 실행 검증은 구현 단계에 수행.
- user_signals: {positive: [], negative: []}
## Phase 2 — 업로드 게임별 자동 대응
# Decisions
- user: 예시 전용이 아닌 사용자가 업로드한 게임 빌드에 맞춘 추천 구현 진행 승인.
- scope: 업로드 빌드 기준 크기 추출 및 영속화, 판별 불가 시 등록자 지정, 플레이어 비율 유지 자동 맞춤 및 전체화면.
# Invocations
- id: inv-002
layer: advisor
name: design-advisor
parent_invocation_id: null
started_at: 2026-10-14T09:26:02.640678+09:00
ended_at: 2026-10-14T09:29:51.485835+09:00
termination: completed
input_digest: 일반 업로드 빌드의 크기 메타 추출/저장과 플레이어 자동 맞춤 계약 설계
output_digest: 업로드별 메타 감지/저장 및 작성자 override, 양축 contain/fullscreen 계약 확정
artifacts: [implementation/design.md]
attempt: 1
model_choice: {tier: large, resolved_model: inherit, rationale: 다중 빌드 형식과 영속화 및 UI 계약 설계}
# Advisor Invocation Decision Log
- advisor: design-advisor
decision: call
rationale: 사용자가 업로드별 자동 대응 범위를 명시하여 데이터/업로드/플레이어 계약 필요
- advisor: requirements-advisor
decision: skip
rationale: 사용자 추가 발화로 범용 빌드 대응 및 진행 권한 명확, 설계에 미확정 입력 검토 포함
# Invocations
- id: inv-003
layer: advisor
name: implementation-advisor
parent_invocation_id: null
started_at: 2026-10-14T09:29:51.485835+09:00
ended_at: 2026-10-14T09:41:14.055959+09:00
termination: completed
input_digest: 승인된 업로드별 메타/플레이어 설계 구현, nested bounded workers 및 회귀테스트 작성
output_digest: 24파일 업로드별 크기 감지/저장/플레이어 및 검증 harness 구현 완료
artifacts: [implementation/report.md, implementation/ownership.md]
planned_workers: 4
actual_workers: 4
attempt: 1
model_choice: {tier: large, resolved_model: inherit, rationale: backend/frontend/migration 계약 통합}
# Advisor Invocation Decision Log
- advisor: implementation-advisor
decision: call
rationale: 승인된 다중 파일 구현과 worker ownership 분할 필요
# Decisions
- 작성자 지정 > 인접 build-info > 정적 CSS > canvas 속성 > 기본 비율. ZIP/HTML 원본 보존, 별도 origin 지원.
- 공유 DB 마이그레이션 미적용; additive 파일 작성 및 격리 환경 검증 승인 범위로 진행.
# Worker Invocations
```json
[
{
"id": "inv-003-w01",
"layer": "worker",
"name": "code-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:31:17.298843+09:00",
"ended_at": "2026-10-14T09:35:46.542933+09:00",
"input_digest": "Exclusive backend file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Static bounded metadata, upload/server/SSR/mapper persistence and regression tests",
"artifacts": [
"src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java",
"src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java",
"src/main/java/com/pandoli365/bibimbap/data/GameData.java",
"src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java",
"src/test/java/com/pandoli365/bibimbap/util/WebglDisplayMetadataTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameUploadControllerSecurityTest.java",
"src/test/java/com/pandoli365/bibimbap/controller/api/GameDisplayMetadataTest.java"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "large",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
},
{
"id": "inv-003-w02",
"layer": "worker",
"name": "code-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:31:17.298843+09:00",
"ended_at": "2026-10-14T09:36:18.956153+09:00",
"input_digest": "Exclusive frontend file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Generic contain player, author controls, overlay/fullscreen stable iframe and 11 Node tests",
"artifacts": [
"src/main/webapp/WEB-INF/views/game-register.jsp",
"src/main/webapp/WEB-INF/views/game-detail.jsp",
"src/main/webapp/js/webgl-player.js",
"src/main/webapp/css/webgl-player.css",
"src/test/js/webgl-player.test.cjs"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "large",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
},
{
"id": "inv-003-w03",
"layer": "worker",
"name": "migration-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:35:46.542933+09:00",
"ended_at": "2026-10-14T09:36:43.485758+09:00",
"input_digest": "Exclusive migration file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Nullable additive idempotent schema/migration; no DB apply",
"artifacts": [
"db/schema.sql",
"db/migrations/20261014-game-display-metadata.sql"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "medium",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
},
{
"id": "inv-003-w04",
"layer": "worker",
"name": "code-writer",
"agent_version": 1,
"parent_invocation_id": "inv-003",
"started_at": "2026-10-14T09:36:18.956153+09:00",
"ended_at": "2026-10-14T09:40:31.246818+09:00",
"input_digest": "Exclusive docs-verification file allocation; approved per-upload general implementation; no tests execution or DB/runtime mutation",
"output_digest": "Contract/usage/change docs, integrated L1/L2 script and isolated actual MyBatis roundtrip test",
"artifacts": [
"docs/contracts/webgl-display-metadata.md",
"docs/usage/webgl-display.md",
"docs/changes/2026-10-14-webgl-display.md",
"docs/contracts/index.md",
"docs/usage/index.md",
"docs/changes/index.md",
"docs/development/verification-strategies.md",
"scripts/verify-webgl-display.sh",
"src/test/java/com/pandoli365/bibimbap/mapper/WebglDisplayDatabaseContractTest.java"
],
"concerns": [],
"model_choice": {
"phase": "implementation",
"dispatch_size": "parallel",
"tier": "medium",
"effort": null,
"resolved_model": "inherit",
"capped": false,
"capped_from": null,
"escalation_reason": null,
"fallback_reason": null,
"rationale": "Coherent owned file group; schema isolated; default inherited model"
},
"token_usage": {
"input": null,
"output": null
},
"attempt": 1,
"termination": "completed",
"retry_of": null,
"lifecycle_fallback_reason": null
}
]
```
# Invocations
- id: inv-004
layer: advisor
name: verification-advisor
parent_invocation_id: null
started_at: 2026-10-14T09:41:14.055959+09:00
ended_at: 2026-10-14T09:52:22.735705+09:00
termination: completed
input_digest: AC + 실행 명령만 사용하여 통합 L1/L2 및 실제 ZIP/JSP/browser 검증
output_digest: L1/L2/runtime 통과, 전체 AC는 문서 실행 오류/회귀 전 실패/터치/UI audit 미확인으로 fail
artifacts: [verification.md]
attempt: 1
model_choice: {tier: large, resolved_model: inherit, rationale: 런타임/DB/Unity 실화면 독립 검증}
# Advisor Invocation Decision Log
- advisor: verification-advisor
decision: call
rationale: 코드 변경 및 통합/런타임 검증 의무
- advisor: documentation-advisor
decision: skip
rationale: 구현 docs-worker가 계약/사용/변경 문서와 인덱스 작성 완료
# Decisions
- graph_refresh: skip:no-graphify (설치 cache에서 atp-graphify add-on 확인되지 않음)
- 제품 파일 git diff/status 교차 확인: tracked 12 파일 및 신규 구현/테스트/문서 파일 존재. 사용자 잔여 제외.
# User Signals
user_signals:
positive: []
negative:
- quote_or_paraphrase: 단순히 내가 준 예시에만 맞추는게 아니라 사용자가 업로드한 게임에 맞춰서야
about: 초기 수동 비율 선택 중심 제안의 범용 업로드 자동 대응 범위 보정
structural: false
# Open Items
- docs/usage/webgl-display.md WAR 직접 실행 명령은 실제 WAR Main manifest 부재로 실패; runtime working command 확인 후 문서 수정 예정.
# Invocations
- id: inv-005
layer: advisor
name: documentation-advisor
parent_invocation_id: null
started_at: 2026-10-14T09:52:22.735705+09:00
ended_at: 2026-10-14T09:55:42.931969+09:00
termination: completed
input_digest: 검증된 실행 명령으로 문서 수정, 영향맵/index/Impeccable UI audit 완료
output_digest: docs 실행명령 교정, mapped21files/index/UI audit 및 실제 HEAD JSP 회귀 실패/현재 성공 확인
artifacts: [documentation.md, artifacts/final-doc-ui-audit.md]
attempt: 1
model_choice: {tier: large, resolved_model: inherit, rationale: runtime 증거 기반 문서 교정 및 UI audit}
# Advisor Invocation Decision Log
- advisor: documentation-advisor
decision: call
rationale: 독립 실행 검증이 운영 가이드 오류 및 최종 문서/UI audit 미확인 발견
# Regression
- surfaced_at_stage: verification
source_stage: docs
defect: executable manifest 없는 WAR의 java -jar 실행을 가이드에서 제안
full_set_recheck: true
downstream_rerun: [documentation, supplemental-verification]
# Invocations
- id: inv-006
layer: advisor
name: verification-advisor-supplemental
parent_invocation_id: null
started_at: 2026-10-14T09:55:42.931969+09:00
ended_at: 2026-10-14T10:00:24.919475+09:00
termination: completed
input_digest: 문서/회귀/audit 증거 재평가 및 실제 Unity 버튼/모바일 터치 추가 검증
output_digest: trusted CDP touch Shop/Open Upgrades 일반/확대/resize 통과, 문서/회귀/audit 미확인 해소. 실제 기기 스모크만 수동 확인.
artifacts: [supplemental-verification.md]
attempt: 1
model_choice: {tier: large, resolved_model: inherit, rationale: 미확인 입력 동작과 종료 AC 보완 검증}
# Advisor Invocation Decision Log
- advisor: verification-advisor-supplemental
decision: call
rationale: 전체 AC fail의 남은 회귀/audit/입력 확인을 보완, passing 전체 suite 반복 불필요
# Final Summary
- 사용자 업로드별 크기 감지·저장/작성자 override/기존 게임 fallback/비율 유지 확대·전체화면 완료.
- L1/L2/runtime 및 보완 trusted touch 입력 검사 통과. 공유 DB/앱 적용 및 push 요청 없음. 로컬 feat/v2 커밋 준비.
# verified_by_me
- L1: Java377 실패0 오류0 (일반 opt-in L2 1skip), Node11/11, WAR package pass (독립 advisor 실행 로그 취합).
- L2: disposable PostgreSQL migration/idempotency/constraints/fresh schema/mapper7 SELECT 계약 pass1/1.
- Runtime: JSP5paths HTTP200, 빌드3종 viewport12/12 geometry, 원본/서빙 hash 보존, 동일 iframe/Unity instance resize/확대/fullscreen.
- Input: CDP trusted touch Shop/Open Upgrades 메뉴 일반/확대/가로 resize 전환 pass.
- Regression: 실제 HEAD JSP로 동일 SSR 테스트 fail1, 현재 JSP pass1. 이전 browser geometry 재실행 아님.
- UI/docs audit:21file impact map + feature docs/index links pass, final detect warn2(existing/falsepositive).
- 로그 스캔: server clean; browser errors0/warn4(기존 Unity/autoplay/sandbox).
- graph_refresh: skip:no-graphify (옵트인 add-on 미설치).
- project_gate: runtime JSP smoke 의무 수행 pass; 별도 배포 gate skip:no-project-gate.
# needs_user_verification
- 실제 터치 기기에서 게임 상세 → 크게 보기/전체화면 → 방향 전환 → Shop/Upgrades 입력 스모크 1회. 자동 trusted browser touch는 통과.
- 배포 전에 db/migrations/20261014-game-display-metadata.sql 선적용 필요. 공유/운영 DB 및 배포는 이번 요청에 미포함.
# Open Items — Final disposition
- 기존 사용자 잔여 .atp/work-session/20260724-095414/, .atp/work-session/20260826-093449/, ~/ 작업 제외.
- 원본 ZIP 복사 artifacts/*.zip 및 임시 로그인 HTML은 로컬 재현 자료로 session artifacts/.gitignore 제외.
- 기존 usage index setup-checklist.md/faq.md 링크 미존재는 이번 작업 밖 (audit 보고서에 기재).
- 각 pool terminal 모두 수신/취합. update_plan 부재 사실 보존, 자동 복구/interrupt/list 없음.
# Invocations
- id: inv-007
layer: advisor
name: retrospective-advisor
parent_invocation_id: null
started_at: 2026-10-14T10:00:24.919475+09:00
ended_at: 2026-10-14T10:02:08.396635+09:00
termination: completed
input_digest: 최신 report 전체 phase/사용자 범위 보정/실행 검증 결과로 회고, report sink only
output_digest: 범용 업로드 scope 보정 및 trusted touch 검증 교훈, 최신 증거 docs sync 권고
artifacts: [report.md#Retrospective]
attempt: 1
model_choice: {tier: medium, resolved_model: inherit, rationale: 완료된 세션 증거 기반 회고}
# Advisor Invocation Decision Log
- advisor: retrospective-advisor
decision: call
rationale: 전체 결과 수렴 및 L1/L2 통과 뒤 필수 회고
# Retrospective
```yaml
Retrospective:
signals:
positive: []
negative:
- quote_or_paraphrase: "단순히 내가 준 예시에만 맞추는게 아니라 사용자가 업로드한 게임에 맞춰서야"
about: "초기 제안의 예시 중심 범위를 일반 사용자 업로드 자동 대응으로 교정"
structural: false
what_went_well:
- "범위 교정 뒤 게임명·파일명 상수 없이 서로 다른 비율 업로드와 unknown/manual/legacy 계약을 구현·검증했다. 이는 실행 관찰이며 사용자 긍정 발화로 분류하지 않는다."
- "독립 검증이 잘못된 WAR 실행 명령과 입력 증거 공백을 드러냈고, 문서 교정 및 보완 검증으로 해소했다. L1/L2/runtime와 trusted CDP touch의 실제 Shop/Upgrades 전환이 통과했다."
- "자동 브라우저 touch와 물리 기기 검증을 구분하여 실제 기기 스모크만 needs_user_verification으로 유지했다."
what_to_improve:
- "제공 ZIP은 재현 fixture로 취급하고 일반 업로드 범위를 초기 제안에서 명시한다. 범용 업로드 계약은 이미 제품 문서에 있어 별도 중복 규칙은 제안하지 않는다."
- "docs/changes/2026-10-14-webgl-display.md 마지막 문단의 named Unity 버튼/trusted touch 미검증 표현은 최신 보완 검증보다 오래됐다. root가 supplemental-verification.md 기준으로 정확히 동기화해야 한다."
- "운영 명령의 실행 확인은 기존 검증 규약과 이번 수정으로 다루므로 중복 교훈 추가를 제안하지 않는다."
memory_candidates:
- name: trusted-touch-evidence-boundary
type: feedback
description: "터치 검증은 이벤트 종류·신뢰 여부와 실제 게임 동작을 함께 관찰한다."
body_draft: |
Why: touch emulation을 켠 상태의 일반 click도 pointerType=mouse일 수 있어 터치 입력 좌표 검증이 되지 않는다.
How to apply: 모바일 입력 확인 시 pointerType=touch와 isTrusted=true 이벤트를 기록하고, 크기 변경 전후 명명된 게임 버튼의 실제 화면 전환을 확인한다. CDP trusted touch 통과와 물리 터치 기기 스모크 완료 여부를 각각 보고한다.
rationale_for_saving: "기존 레지스트리는 클릭·터치 점검만 요구하며 mouse 이벤트 오인 방지의 구체 증거 기준은 없다."
signal_source: observation
docs_sync_target: docs/development/verification-strategies.md
memory_optional: false
protocol_feedback: []
applied_changes: []
```
Memory는 disabled/unset이므로 전역 memory를 읽거나 변경하지 않았다. 위 후보는 docs 전용 권고이며 직접 적용하지 않았다. 운영 명령 및 일반 업로드 범위의 중복 후보는 제외했다. 최신 증거 동기화는 교훈 후보와 별개의 필수 문서 정합 수정이다.
# Retrospective disposition
- accepted: trusted browser touch/실제 메뉴 결과/물리기기 smoke 구분 규칙을 docs/development/verification-strategies.md에 추가.
- applied_changes: docs/changes/2026-10-14-webgl-display.md 최신 보완 touch 증거 동기화; 전역 memory 미변경.
- Final staging: raw stdout/psql evidence whitespace를 session .gitattributes로 보존; 제품 파일 whitespace check pass.
# Session Closure
- implementation_commit: ddd9abf
- local_branch: feat/v2
- requested_mutations: 구현/문서/회귀테스트/격리 검증 완료, 로컬 커밋 완료.
- push_or_shared_deployment: 미요청; 배포 전 migration 필요.
- terminal_barrier: root advisors7 + nested workers4 모두 completed 수신 및 결과 취합.
- pending_children: 0
- running_children: 0
- repo_status: 이번 작업의 코드/문서/세션 증거 커밋됨. 이전 사용자 untracked3 항목만 제외.
- end_status: implemented_and_verified; physical-device-smoke needs_user_verification.

View File

@ -0,0 +1,90 @@
---
phase: research
agent: research-advisor
agent_version: 4
report_invocation_id: inv-001
concerns_checked: true
source_confidence: mixed
workers_spawned: 0
concerns:
- Unity 씬 소스 미포함으로 실제 UI anchor/Canvas Scaler 설정 확인 불가
- 브라우저 실행 전이므로 화면 잘림 및 입력 이상은 아직 런타임 확인하지 않음
- 분리 asset origin 사용 시 부모의 iframe DOM 직접 접근 불가
---
# WebGL 해상도 대응 조사
## 결론
원인 후보는 플랫폼의 고정 가로 플레이 영역과 빌드 HTML의 세로 영역 계산이다. “Unity가 390×844 해상도만 렌더한다”는 가정은 자료와 맞지 않는다. 이 ZIP은 이미 CSS 크기에 맞춰 Unity 렌더 타깃을 동기화하는 설정이며, 게임 내부 UI가 새 크기에 맞춰 배치되는지는 별개의 문제다.
기본 정책은 **원본 390:844 비율을 유지하면서 가능한 영역 안에 전체 화면을 표시(contain)**하는 것을 추천한다. README가 PC에서도 세로 화면 유지한다고 명시하므로 화면을 늘리거나 자르는 cover/fill 정책은 이 게임의 안전한 기본값이 아니다. 여백 없이 모든 화면을 채우려면 Unity 소스에서 다양한 비율의 UI와 카메라를 검증·수정하여 재빌드해야 한다.
## ZIP 근거 (원본 변경 없음)
ZIP: `/Users/wemadeplay/Downloads/GoraniCoin-WebGLTest.zip`; zipfile로 읽기만 수행.
- `GoraniCoin-WebGLTest/build-info.json:5`: Unity `6000.0.74f1`.
- `build-info.json:8,13–14`: `Assets/Scenes/UITestScene_Final.unity`, 기준 `390×844`.
- `README.txt:3`: PC에서도 세로 화면 유지. `README.txt:5`: Toss 결제 및 호스트 safe-area 호출 비활성화. 실제 프로젝트 소스는 ZIP에 없다.
- `index.html:9–15`: html/body 100%, overflow hidden; container 폭 `min(100vw, calc(100svh * 390 / 844))`, 높이 `100svh`; canvas CSS 폭/높이 100%.
- `index.html:27`: canvas 초기 HTML 버퍼 속성 390×844. 이 속성만으로 실행 중 렌더 타깃이 고정이라고 판단하면 안 된다.
- `index.html:46–61`: config에 `devicePixelRatio: Math.min(window.devicePixelRatio || 1, 2)` 설정. `matchWebGLToCanvasSize`는 미설정.
- `Build/GoraniCoin-WebGLTest.framework.js:7575–7580`: `matchWebGLToCanvasSize`가 undefined면 true를 반환. `:7592–7594`: preferred DPR은 match false면 1, 그 외 Module DPR/window DPR 사용.
- `index.html:69–71`: `window.unityInstance`, 준비 상태 `data-unity-ready`를 제공하여 브라우저 검사에 활용 가능.
**HTML의 비율 결함:** 컨테이너 폭이 viewport 폭 때문에 제한되더라도 높이는 viewport 전체 높이를 유지한다. 예를 들어 iframe 320×844라면 canvas CSS가 320×844가 되어 원래 390:844보다 좁다. Unity는 이 비율에서 새 렌더링을 할 가능성이 높으며, UI가 잘리는지 여부는 내부 레이아웃에 달렸다. 적절한 contain 계산은 `scale=min(availableWidth/390, availableHeight/844)`, 폭 `390*scale`, 높이 `844*scale`이다. 이미지 CSS stretch와 Unity UI 재배치를 혼동하지 않아야 한다.
## 플랫폼 근거
- `src/main/webapp/WEB-INF/views/game-detail.jsp:365–384`: frame wrap 폭 100%, `aspect-ratio:16/9`, `max-height:min(70vh,520px)`, overflow hidden; iframe absolute inset 0, 폭/높이 100%. 비율과 최대 높이가 동시에 걸려 실제 frame 비율이 16:9보다 넓어질 수도 있다.
- `game-detail.jsp:1259–1264`: 업로드 빌드 URL iframe에 그대로 연결. sandbox scripts/same-origin/pointer-lock 허용; fullscreen/autoplay 권한 허용. 전체화면 제어 UI는 없음.
- 이 빌드가 800×450 iframe에 들어가면 자체 portrait 계산에 의해 canvas 약 208×450이 되어 큰 좌우 여백과 작은 세로 화면이 생긴다. 모바일 상세 페이지의 가로 iframe은 더 낮아질 수 있다. 이는 계산상 예상이며 아직 실화면 재현은 아니다.
- `src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java:198–225`: ZIP 압축 해제, index 탐색, Unity 파일 검증, 디렉터리 이동, 빌드 index URL 반환. 별도 템플릿 정규화나 responsive wrapper는 없다.
- `src/main/java/com/pandoli365/bibimbap/controller/api/GameAssetController.java:57–68`: CSP 헤더 설정 뒤 파일 바이트 그대로 복사. CSP는 self/inline script와 self/inline style을 허용한다. CSP 변경 자체는 해상도 지원에 필요하지 않다.
- `src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:348–357`: `app.webgl.asset-origin`이 있으면 iframe을 별도 origin에 연결. 따라서 “부모 페이지 JS가 모든 게임의 canvas를 직접 고친다”는 구현은 일반화 불가. 호스트는 iframe 박스 크기는 변경 가능하나 내부 문서는 same-origin에서만 직접 접근 가능하다.
- `src/main/java/com/pandoli365/bibimbap/data/GameData.java` 및 game-register 입력에 게임 해상도/비율 메타 필드는 발견되지 않았다.
- `docs/graph/index.md` 알려진 한계와 `docs/development/verification-strategies.md:260–262`: JSP/CSS 정적 프론트 코드는 직접 탐색 대상. graph lookup skip은 타당.
- contracts index는 아직 비어 있고 해상도 계약 문서는 없음. usage index 일부 링크는 파일이 존재하지 않아 해당 문서에 의존하지 않음.
## 외부 근거 — Unity 공식 문서 확인
[Unity 6.0 Web Canvas size](https://docs.unity3d.com/6000.0/Documentation/Manual/webgl-canvas-size.html): CSS 표시 크기와 GPU 렌더 타깃을 구분하며 기본 자동 동기화를 설명한다. `matchWebGLToCanvasSize=false`는 자동 동기화를 끄고 버퍼를 수동 관리할 때 사용하는 설정이다. 이 게임의 config와 framework 근거가 기본 동기화와 일치한다.
[Unity uGUI 2.0 Canvas Scaler](https://docs.unity3d.com/Packages/com.unity.ugui@2.0/manual/script-CanvasScaler.html): Scale With Screen Size/Reference Resolution/Screen Match Mode가 UI 크기와 서로 다른 비율의 레이아웃을 결정한다. HTML에서 렌더 해상도를 바꾸는 것만으로 게임 내부 레이아웃 적응을 보장할 수 없다. 현재 게임이 uGUI인지 UI Toolkit인지도 ZIP만으로 확정하지 않는다.
[MDN postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage): 다른 origin의 iframe과 통신하려면 협력하는 child 측 프로토콜이 필요하며 sender origin/source와 message 값을 검증해야 한다. 부모에서 iframe src만 넣는 현행 구조로는 임의 ZIP의 내부 비율 자동 감지/수정 계약이 없다.
## 권장 구현 계획 (구현하지 않음)
1. 빌드 원본과 플레이어를 로컬 HTTP로 실행해 기준 390×844 화면, 창 축소, 모바일 세로·가로에서 재현한다. 준비 상태/실제 canvas client size와 width·height를 기록한다.
2. bibimbap 플레이 영역에 사용자 조절 가능한 비율(현재 가로/세로/원본 또는 사용자 지정)과 확대/전체화면 기능을 도입한다. 390:844 같은 정확 비율을 유지하면서 부모 가용 폭과 높이 양쪽에 맞게 계산한다. 앱의 iframe 전체를 390:844로 맞추면 이 ZIP의 자체 CSS를 수정하지 않고도 양쪽 계산이 일치하게 만들 수 있다. “세로”를 일반 9:16으로만 맞추면 이 게임 원본과 다르므로 원본 정확 비율 옵션이 필요하다.
3. 최소 변경안은 상세 JSP/CSS/전용 JS와 플레이어 테스트를 수정하고, 기존 게임은 현재 정책으로 fallback하면서 이번 게임은 사용자가 원본 비율을 선택할 수 있도록 한다. 화면 비율 추론을 hardcode하거나 특정 ZIP 이름을 앱에 박지 않는다.
4. 모든 게임에 자동 대응이 필요하면 작성자 해상도/비율 메타 입력 또는 협력하는 표준 빌드 메타/resize message 계약을 추가한다. 메타 저장은 GameData/Mapper/API/등록 JSP/DB migration 및 계약 테스트로 영향이 커진다. 업로드 HTML 임의 주입/정규식 개조는 범용 보장과 원본 보존 측면에서 우선안으로 추천하지 않는다.
5. 이 게임 HTML 자체도 개선하려면 원본 ZIP을 보존하고 복사본 index 컨테이너만 양축 contain 방식으로 보정한다. 재빌드 없이 가능한 웹 wrapper 수정이지만, 내부 UI 재설계와는 다르다. matchWebGLToCanvasSize=false나 고정 버퍼는 픽셀 기준 UI 문제가 실제 확인됐을 때 개발자와 함께 검토한다.
## 영향 파일 후보
최소 플랫폼안: `src/main/webapp/WEB-INF/views/game-detail.jsp`, 신규 전용 WebGL player JS/CSS, 동작 회귀 테스트, `docs/changes/...`, 해상도 사용 계약 문서. 공용 `.player` CSS(`bibimbap.css:343`)는 현재 상세 페이지 실제 iframe class와 다르므로 그 줄만 바꾸어도 해결되지 않는다.
자동 메타 영속화안: 위 + `GameData.java`, `GamesMapper.java`, `GameController.java`, 등록 폼, upload metadata parser(채택 시), DB migration, 컨트롤러/매퍼 테스트. 이는 이번 조사에서 확정한 구현 요구사항이 아니라 별도 범위 선택이다.
## 검증 기준
- 원본 390:844와 좁은 320×844, desktop 1440×900, 가로 844×390, 작은 높이 창에서 플레이 영역 양축이 가용 공간을 넘지 않음.
- contain 모드 canvas/iframe 비율 390:844, 화면 잘림 없이 중앙 정렬. 여백은 의도대로 남음. ResizeObserver/viewport resize 후 갱신되고 Unity instance가 재생성되지 않아 플레이 상태가 유지됨.
- Unity 준비 후 CSS 크기와 drawing buffer 크기를 각각 기록; DPR 1/2에서 실제 동기화를 확인. initial width/height attribute만 검사하지 않음.
- 게임 로딩/상하단 UI/버튼 클릭/터치/포인터 위치/다시 축소/전체화면 진입과 해제를 실제 빌드로 확인. 화면만 보기 좋은데 입력이 틀어지는 회귀를 방지.
- 기존 가로 게임과 별도 asset-origin 구성에서 iframe 재생이 유지됨. cross-origin 접근 실패를 정상 지원 여부와 구분.
- `docs/development/verification-strategies.md`에 따라 수정 전 실패→수정 후 통과하는 회귀 포함, JSP 변경은 WAR 빌드 외 HTTP 200 런타임 스모크가 의무.
## 미해결
- 사용자가 원하는 대응 범위: 원본 콘텐츠를 전체 표시하며 확대하는 플레이어인가, 게임 UI가 가로·세로 전체 화면에서 재배치되는 것인가? 후자는 Unity 소스 또는 담당 개발자의 재빌드가 필요할 수 있다.
- 각 게임 비율의 자동 감지/저장까지 이번 범위인지 여부.
- 실제 Unity UI 컴포넌트와 픽셀 단위 레이아웃, 카메라 viewport, safe-area 대응 설정.
- 현행 플레이어와 ZIP의 런타임 실화면은 아직 미실행.
## 실행 계약
Parent instruction에 따라 nested children 0건. update_plan은 callable tools에 없어 bind 호출을 만들지 않았으며 parent에 명시 고지했다. 제품 코드/원본 ZIP/credential 미변경. 파일 산출은 이 research.md 1건만이다.

View File

@ -0,0 +1,45 @@
---
phase: verification
agent: verification-advisor-supplemental
agent_version: 1
report_invocation_id: inv-006
generated_at: 2026-10-14T10:01:00+09:00
concerns_checked: true
concerns:
- Physical touchscreen device smoke remains unperformed; trusted CDP touch is browser automation.
- Baseline evidence proves SSR contract regression, not an old-browser geometry replay.
- Cross-origin behavior remains Node harness coverage, not independent runtime origin coverage.
- update_plan unavailable; binding tool action not fabricated.
---
# Supplemental independent verification
Read only prior verification.md, final-doc-ui-audit.md, final-prechange-ssr-regression.log, final-current-ssr-regression.log and operational docs/usage/webgl-display.md. No implementation/design/diff inspection, product/test/source edits, installs, DB or application changes. Current player JS/CSS were executed by HTTP without inspecting their contents. Root supplied fixture DOM contract. The exact unmodified previously uploaded GoraniCoin build was served from its retained temporary directory.
## Previously open concerns reassessed
The erroneous java-jar WAR instruction is resolved: current operational guide explicitly explains absent Main manifest and documents the actual successful Java21 Maven spring-boot:run command, plus macOS Java environment setup. No operational-guide failure remains observed.
The supplied real HEAD baseline log fails the named SSR stored-ratio/responsive-stage regression (1 failure); the identical current named regression passes (1 pass). This resolves absent independent baseline execution evidence for the SSR contract. It does not retrospectively prove an old browser geometry matrix; prior current 12/12 geometry evidence remains valid.
Final documentation advisor report records all 21 unconditional impact-map paths present, three index links valid, all feature-doc links resolved, scoped Impeccable detector findings reviewed as incumbent/out-of-scope and false-positive. Therefore earlier AC8 audit and AC9 root evidence gaps are resolved by supplied scoped audit evidence. Unrelated pre-existing usage index links remain disclosed there.
## Observed Unity menu and trusted touch verification
An isolated own Chrome headless instance on loopback18087 and HTTP fixture18086 avoided the unavailable shared Chrome MCP profile. Node22 built-in WebSocket issued Chrome DevTools Protocol Input.dispatchTouchEvent touchStart/touchEnd. Unity document event instrumentation recorded trusted pointerdown(pointerType touch), touchstart and touchend, all isTrusted=true. These are real browser-dispatched trusted events, not synthetic JavaScript TouchEvents; they are still automated emulation and do not represent hardware touchscreen execution.
1. Normal initial viewport500×757: screenshot visibly showed Home and named Shop button. Viewport touch(202,586) produced iframe-local(74.453125,510), and before/after screenshot confirmed Home→SHOP with hardware list and Open Upgrades.
2. Expanded after resize to exact844×390: same iframe and same Unity instance. iframe rect(345.75,60,152.484375,330), canvas CSS152×330, drawing buffer152×330. Screenshot visibly showed SHOP and Open Upgrades. Trusted touch at viewport(466,345), iframe-local(120.25,285), changed to UPGRADES with Fan Tune1 and INSTALL UI. Both screenshots inspected.
3. Return to normal and exact390×844: same iframe and same Unity instance; iframe rect(58.453125,76,273.078125,591), canvas CSS273×591, drawing buffer273×591. Home navigation was exercised before the screenshot. Trusted touch on named Shop at viewport(140.353125,643.36), iframe-local(81.8999939,567.3599854), changed Home→SHOP. Screenshots inspected. Top currency/progress and bottom navigation visible.
Coordinates match the current scaled canvas and iframe offsets in both portrait normal and landscape expanded modes. No mine modal obstructed these observations; mining progress changed during the run without preventing the named actions. Resize/return retained game currency/progression, DOM iframe and Unity instance. No observed product input mapping defect.
Evidence: artifacts/input-supplemental/normal-before.png, normal-shop-after.png, expanded-landscape-before.png, expanded-landscape-upgrades-after.png, portrait-home-before.png, portrait-shop-after.png, portrait-input.json. Harness server.py/cdp.mjs are session-only artifacts. Expanded event and geometry values above were captured from CDP output and screenshots, not inferred from source.
## Integrity and outcome
Original ZIP SHA256 remains02e5272e4086f8f0e2e3a4b41ca4313d5f8862897a1655421f3f1b4315942461. Retained uploaded entry HTML and HTTP-served HTML remain8a9f8aaff7e949b49f4cab1a87c5b6d414db4031b5fb3e268f5102bc40cbcc74. Original ZIP/uploaded HTML were not modified.
AC7 named Unity action and trusted automated mobile-touch coordinates now PASS in normal portrait and expanded resized landscape. Earlier 377 Java/11 Node/WAR/actual L2/JSP/12-geometry/fullscreen-state checks stand; doc-only changes do not require full suite replay. No observed defect or rollback signal exists. All automated/runtime observations pass within the explicitly stated baseline and cross-origin evidence bounds. Overall: needs_user_verification for real physical touchscreen smoke, not fail due to an observed product defect. Complete physical-device acceptance is not claimed.
Owned fixture server and own isolated Chrome terminated after collection; shared8080/5433 and shared browser untouched. Temporary upload and original ZIP retained. No children; update_plan unavailable disclosed to root. ATP_POOL_RESULT wginput1 1/1 14ce996a.

View File

@ -0,0 +1,106 @@
---
phase: verification
agent: verification-advisor
agent_version: 1
generated_at: 2026-10-14T09:51:00+09:00
concerns_checked: true
concerns:
- "Documented java -jar WAR command fails: no Main manifest attribute; isolated runtime passed via spring-boot:run."
- "Actual trusted touchscreen input and specific Unity button outcome remain unverified: Chrome click generated trusted mouse events even in touch emulation."
- "Prechange regression failure was not independently executed; temporary browser fixture after cleanup was unavailable; current regression passed."
- "AC9 impact map/docs index and Impeccable detect require root verification; not supplied to independent advisor."
- "update_plan unavailable; ATP_POOL_BIND tool action not fabricated."
---
# Independent verification
Implementation code, git diff, design.md and implementation reports were not read. No repository product/test edits were made. Tests executed against the user-authorized isolated environment; shared app8080 and DB5433 were untouched.
## Acceptance criteria (provided verbatim)
## 검증 포인트
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는 테스트를 작성하지 않고 실행만 수행.
## Executed strategies
| Command | Exit | Result |
|---|---:|---|
| `bash scripts/verify-webgl-display.sh all` | 1 | Environment startup failure: default Java launcher could not locate runtime |
| `JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home PATH=/opt/homebrew/opt/openjdk@21/bin:$PATH bash scripts/verify-webgl-display.sh all` | 0 | PASS |
Full Maven tests:377, failures0, errors0, skipped1 (opt-in L2 contract excluded from ordinary test run). Node11/11 PASS. WAR package PASS. Disposable PostgreSQL L2 enabled and actual mapper test1/1 PASS; runner confirmed legacy row preserved, migration twice/idempotency, invalid CHECK rejection, fresh/migrated equivalence, INSERT/UPDATE/all7 SELECT roundtrip. L2 was not skipped.
## Runtime evidence
Separate `postgres:16` container `bibimbap-inv004-postgres` exposed loopback55003; fresh schema executed through psql from db/schema.sql (source not inspected). Temporary account created through registration UI in isolated DB only. Latest packaged artifacts launched with Java21 using working command:
```bash
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home PATH=/opt/homebrew/opt/openjdk@21/bin:$PATH ./mvnw -o spring-boot:run '-Dspring-boot.run.arguments=--server.address=127.0.0.1 --server.port=18081 --spring.config.import= --spring.datasource.driver-class-name=org.postgresql.Driver --spring.datasource.url=jdbc:postgresql://127.0.0.1:55003/webgl_contract?currentSchema=dev --spring.datasource.username=postgres --spring.datasource.password= --app.upload.game-storage-path=/tmp/bibimbap-inv004-uploads'
```
The documentation's `java -jar target/bibimbap-0.0.1-SNAPSHOT.war` failed with no Main manifest attribute. This is a reproducible operational-guide issue, not a JSP/runtime failure.
Actual ZIP browser upload used unmodified byte copy renamed independent-original.zip, since Chrome upload_file denied every workspace path. A private loopback18082 CORS file server supplied bytes to a DataTransfer/File in the actual form. Upload HTTP200; UI detected390x844. Automatic register persisted390x844/build-info and edit/detail reflected it. Custom400x900/manual persisted, reopening edit selected custom and both values. Uploading1280x720 while custom left400x900 unchanged. Automatic resave re-detected original390x844. Synthetic valid Unity ZIPs named independent-landscape and independent-square returned1280x720 and800x800, saved in DB. Updating square with new landscape ZIP persisted1280x720. An isolated legacy row with null metadata rendered fallback16:9.
Authenticated fetch requests `/game/new`, `/game/1`, `/game/1/edit`, `/game/2`, `/game/3` all HTTP200, exercising actual JSP compilation. Server logs contain no ERROR/Exception/stacktrace; warning only appended static upload location slash.
Every portrait/landscape/square at320x844,1440x900,844x390,640x300 passed boundary and ratio checks (12/12), max error0.006665px. Initial resize_page window had OS minimum size limits, so exact matrix used viewport emulation and recorded actual innerWidth/innerHeight. Portrait actual Unity instance was ready. CSS versus drawing buffers recorded independently: desktop1440x900 normal291x630/582x1260; fullscreen388x840/776x1680; mobile touch emulation273x591/273x591. Actual original game top information and bottom menus visible in desktop/mobile screenshots.
During normal resize, expand, fullscreen success/exit, rejection fallback and Esc: same iframe DOM, same Unity instance,0 additional iframe loads. iframe source unchanged. Fullscreen returned focus to fullscreen trigger; overflow restored. Forced requestFullscreen rejection correctly displayed dismissible expanded fallback. Touch emulation maxTouchPoints1, but actual Chrome click generated trusted pointerdown(pointerType mouse), mousedown and click at canvas136.5x295.5. This proves mouse delivery, not trusted touchscreen delivery or a named Unity button outcome.
Original ZIP SHA256 stayed02e5272e4086f8f0e2e3a4b41ca4313d5f8862897a1655421f3f1b4315942461. Original/stored/HTTP served entry HTML all SHA2568a9f8aaff7e949b49f4cab1a87c5b6d414db4031b5fb3e268f5102bc40cbcc74. No source HTML mutation.
Browser console errors:0. Observed warnings: iframe sandbox allow-scripts+same-origin, Unity WebGL INVALID_ENUM, DontDestroyOnLoad root warning, AudioContext autoplay warning. No lint tool was invoked; lint absence is not runtime failure.
## Acceptance matching
| AC | Verdict and evidence |
|---|---|
| AC1 | PASS through6 grouped parser cases plus12 upload security cases; fixture case names in verification-cases.json. Dynamic/nonliteral data rejected/unknown, malformed metadata upload succeeds. Independent JS execution instrumentation not separately added. |
| AC2 | PASS runtime actual390x844 and distinct-name1280x720,800x800 uploads |
| AC3 | PASS7 Java contracts plus runtime auto/custom/edit/new ZIP/legacy fallback; HTTP invalid/auth/CSRF branches executed by Java contracts |
| AC4 | PASS actual isolated L2, no skip |
| AC5 | PASS current12/12 viewport matrix; prechange failure evidence NOT independently executed |
| AC6 | PASS same-origin runtime identity/load/Unity and fullscreen rejection; cross-origin no-DOM dependency covered by Node harness, not separate runtime asset origin |
| AC7 | PARTIAL actual ready/CSS-buffer/state/hash/UI imagery and trusted mouse delivery pass; named Unity button outcome and trusted mobile touch NOT verified |
| AC8 | PASS automated full/package/JSP/errors/screenshots; Impeccable detect not performed by independent advisor |
| AC9 | NOT VERIFIED independent artifact existence/impact map/docs indexes not supplied; root check required |
## Overall
overall: fail
rollback_signal: none
Automated suite and observed runtime behavior PASS. Complete acceptance is not claimed because prechange failure, trusted touch/button outcome and root-owned audit requirements remain unverified. No observed product runtime failure warrants rollback. Operational guide's boot command is reproducibly wrong for current WAR.
## Artifacts and cleanup
- artifacts/verification-integrated-java21.log — full377+11 tests, package, L2 evidence
- artifacts/verification-integrated.log — initial Java environment failure
- artifacts/verification-cases.json — named grouped Java test cases
- artifacts/runtime-dom-evidence.json — exact viewport and Unity state/input evidence
- artifacts/runtime-http-evidence.json — authenticated JSP status and served hash
- artifacts/runtime-hashes.json and original-hashes.json — ZIP and HTML preservation
- artifacts/runtime-roundtrip-auto.txt, runtime-roundtrip-manual.txt, runtime-zip-replacement.txt — actual isolated DB rows
- artifacts/runtime-app-maven.log and runtime-app.log — running application and manifest failure
- artifacts/runtime-schema.log — actual fresh schema execution
- artifacts/runtime-desktop.png, runtime-mobile.png — screenshot pair
Owned app18081 process43913, loopback file server18082, disposable container bibimbap-inv004-postgres are removed after collection. Temporary upload bytes at /tmp/bibimbap-inv004-uploads and session fixture ZIPs intentionally retained for root collection; no shared volume deletion. Browser tab2 retained, pointed at isolated runtime page. Session HTTP cookie artifact removed rather than delivered.
## Bounded regression attempt
After cleanup the runtime page no longer had its iframe. First DOM fixture failed due absent iframe. An independent blank iframe/stage fixture evaluation remained pending and was terminated without result. No baseline failure evidence is claimed. runtime-prechange-geometry.json records the first unavailable attempt, not a passing baseline proof.

View File

@ -0,0 +1,3 @@
{"event": "await_capability_checked", "at": "2026-10-14T09:17:15.328427+09:00", "formal_adapter_enabled": false, "host_managed_subagent_orchestration": "supported", "selected_mode": "host_managed_subagent_orchestration", "profile_ref": "codex-team/SKILL.md §7"}
{"event": "measurement", "at": "2026-10-14T09:20:31.663736+09:00", "requested_agents": 1, "spawn_calls": 1, "accepted_spawns": 1, "terminal_deliveries": 1, "collected_results": 1, "nonterminal_updates": 2, "pool_wait_calls": 3, "list_calls": 0, "interrupt_calls": 0, "recovery_actions": 0, "pending": 0, "running": 0, "bind_tool_available": false}
{"event": "measurement", "at": "2026-10-14T10:02:57.818935+09:00", "requested_agents": 7, "spawn_calls": 7, "terminal_deliveries": 7, "collected_results": 7, "pending": 0, "running": 0, "root_maximum_observed_running": 1, "nested_workers_requested": 4, "nested_workers_terminal": 4, "list_calls": 0, "interrupt_calls": 0, "recovery_actions": 0, "nonterminal_updates": null, "pool_wait_calls": null}

View File

@ -0,0 +1,47 @@
-- 게임별 WebGL 기준 화면 크기. 앱 배포 전에 대상 스키마에 적용한다.
-- search_path는 실행자가 대상(dev/live/격리 테스트) 스키마로 명시한다.
-- 앱 자동 적용 없음. 기존 행 변경/백필 없음. 재실행 가능.
-- 롤백: 이전 앱 버전으로 복귀해도 nullable 컬럼은 유지 가능하다.
-- 컬럼/제약 삭제는 별도 승인 후 수행한다.
BEGIN;
ALTER TABLE "games"
ADD COLUMN IF NOT EXISTS "display_width" integer,
ADD COLUMN IF NOT EXISTS "display_height" integer,
ADD COLUMN IF NOT EXISTS "display_source" character varying(16);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'games'::regclass
AND conname = 'games_display_dimensions_check'
) THEN
ALTER TABLE "games"
ADD CONSTRAINT "games_display_dimensions_check" CHECK (
("display_width" IS NULL AND "display_height" IS NULL)
OR (
"display_width" IS NOT NULL AND "display_height" IS NOT NULL
AND "display_width" BETWEEN 1 AND 16384
AND "display_height" BETWEEN 1 AND 16384
AND "display_width"::bigint <= "display_height"::bigint * 32
AND "display_height"::bigint <= "display_width"::bigint * 32
)
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'games'::regclass
AND conname = 'games_display_source_check'
) THEN
ALTER TABLE "games"
ADD CONSTRAINT "games_display_source_check" CHECK (
"display_source" IS NULL
OR "display_source" IN ('build-info', 'css', 'canvas', 'manual', 'unknown')
);
END IF;
END
$$;
COMMIT;

View File

@ -102,6 +102,46 @@ CREATE TABLE IF NOT EXISTS "games" (
);
ALTER SEQUENCE "games_id_seq" OWNED BY "games"."id";
-- 게임별 WebGL 기준 화면 크기 (20261014-game-display-metadata.sql과 동일)
-- Nullable 추가만 수행하며 기존 게임의 해상도는 추정/일괄 갱신하지 않는다.
ALTER TABLE "games"
ADD COLUMN IF NOT EXISTS "display_width" integer,
ADD COLUMN IF NOT EXISTS "display_height" integer,
ADD COLUMN IF NOT EXISTS "display_source" character varying(16);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'games'::regclass
AND conname = 'games_display_dimensions_check'
) THEN
ALTER TABLE "games"
ADD CONSTRAINT "games_display_dimensions_check" CHECK (
("display_width" IS NULL AND "display_height" IS NULL)
OR (
"display_width" IS NOT NULL AND "display_height" IS NOT NULL
AND "display_width" BETWEEN 1 AND 16384
AND "display_height" BETWEEN 1 AND 16384
AND "display_width"::bigint <= "display_height"::bigint * 32
AND "display_height"::bigint <= "display_width"::bigint * 32
)
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'games'::regclass
AND conname = 'games_display_source_check'
) THEN
ALTER TABLE "games"
ADD CONSTRAINT "games_display_source_check" CHECK (
"display_source" IS NULL
OR "display_source" IN ('build-info', 'css', 'canvas', 'manual', 'unknown')
);
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "idx_games_visible_order"
ON "games" ("is_visible", "is_delete", "sort_order", "created_at" DESC, "id" DESC);

View File

@ -0,0 +1,25 @@
---
kind: changes
title: 업로드 빌드별 WebGL 플레이 화면
owner: bibimbap
stability: stable
last_reviewed: 2026-10-14
---
# 업로드 빌드별 WebGL 플레이 화면
기존 상세 실행 영역은 16:9와 최대 높이 520px로 고정되어 세로 게임을 좁은 가로 영역에 넣었다. 이제 업로드한 빌드마다 선언된 기준 폭·높이를 감지·저장하고, 상세 플레이어가 가용 폭·높이 양쪽에 비율을 유지하여 맞춘다. 특정 예시 게임에 따른 크기 상수는 없다.
등록·수정에 자동/직접 지정 크기 설정이 추가되었다. 미확인 빌드는 자동 등록을 계속 허용하며 16:9 fallback을 사용한다. 기존 null 메타 게임은 파일에서 읽기 전용 감지하고 수정 시 교정할 수 있다. 크게 보기와 전체화면은 기존 iframe을 유지해 게임 재시작을 피한다. 전체화면 실패는 크게 보기로 전환한다. Unity 내부 레이아웃 재배치는 빌드 개발자의 영역이다.
추가 DB 마이그레이션은 `20261014-game-display-metadata.sql`이다. 배포 전 nullable 세 컬럼과 CHECK를 적용한다. 기존 행 백필·원본 ZIP/HTML 수정·앱 자동 DDL은 없다. 정적 감지는 안전한 선언 신호만 사용하며 JS style 대입은 지원하지 않는다. 전체 계약은 [화면 메타](../contracts/webgl-display-metadata.md), 운영 절차는 [등록·실행 가이드](../usage/webgl-display.md)에 있다.
2026-10-14 격리 검증에서 Java 21을 명시한 `bash scripts/verify-webgl-display.sh all`은 성공했다. Java 377건은 실패·오류 0, 일반 실행에서 opt-in L2 1건 skip이었다. Node 11/11과 WAR package가 통과했고, disposable PostgreSQL에서 별도로 활성화한 실제 MyBatis L2는 1/1 통과하여 생략하지 않았다. 등록·상세·수정 JSP 경로 5개가 인증 후 HTTP 200이었다.
실제 390×844 ZIP과 별도 이름의 1280×720·800×800 빌드에 대해 320×844, 1440×900, 844×390, 640×300 viewport 12/12가 stage 경계와 1px 이내 비율 조건을 통과했다. 실제 Unity instance는 준비 상태였으며 CSS client 크기와 drawing buffer를 구분해 기록했다(데스크톱 일반 291×630/582×1260, 전체화면 388×840/776×1680, 모바일 emulation 273×591/273×591). Resize·크게 보기·전체화면/복귀·API 거절 fallback 동안 같은 iframe DOM/src와 Unity instance를 유지하고 추가 load는 0이었다. 원본 ZIP 및 저장·HTTP 제공 entry HTML의 SHA-256은 변하지 않았다. 브라우저 console error와 앱 stacktrace는 없었다.
최종 정적 UI detect에서 기존 소개 패널 accent border와 숨겨진 thumbnail preview만 지적되었다. 후자는 src를 먼저 설정한 뒤 표시하므로 broken-image 오탐이다. 변경된 플레이어·크기 설정에서 확인된 새 UI 결함은 없었다. 실제 HEAD 이전 JSP를 임시 복사본에 대입한 동일 SSR regression은 실패(exit 1), 현재 JSP는 통과(exit 0)했다. 이는 markup 회귀 증거이며 이전 브라우저 geometry 실행 증거는 아니다.
보완 검증에서는 CDP touch 이벤트의 pointerType=touch, isTrusted=true와 실제 Unity 메뉴 전환을 함께 확인했다. 세로 일반 화면의 Shop, 가로 확대·resize 후 Open Upgrades가 입력 좌표에 맞게 전환되었고 같은 iframe/Unity 상태를 유지했다. 실제 물리 터치 기기의 스모크는 별도 사용자 확인 항목이다. [보완 검증 보고서](../../.atp/work-session/20261014-091658/supplemental-verification.md), [최초 검증 보고서](../../.atp/work-session/20261014-091658/verification.md), [최종 문서·UI 감사](../../.atp/work-session/20261014-091658/artifacts/final-doc-ui-audit.md)에 근거와 한계를 남겼다. 재현 절차는 [등록·실행 가이드](../usage/webgl-display.md)에 있다. 공유·운영 DB 적용과 배포는 검증 harness에 포함하지 않는다.
후속 로컬 확인에서 개발 DB `dev`에 마이그레이션을 적용하고 앱을 재시작한 뒤 게임 9/10/11을 등록했다. 스크린샷 15장과 사용자 “괜찮은거 같네” 의견을 포함한 현재 상태는 [상세 작업 기록](../work-log/2026-10-14-webgl-display-session.md)에 정리했다. 운영 환경 배포와는 별개다.

View File

@ -4,6 +4,8 @@
## 목록
- [2026-10-14-webgl-display.md](./2026-10-14-webgl-display.md) — 업로드 빌드별 크기 감지·영속화, 자동/직접 지정, 비율 유지와 확대/전체화면. 검증 명령·범위와 배포 전 nullable DDL 절차 등록.
- [2026-06-18-w3-2-comments-reviews.md](./2026-06-18-w3-2-comments-reviews.md) — W3-2 댓글/리뷰 분리 구현. game_comments 서버 영속화 전환 + game_reviews 도메인 신설. 신규 API 9개(댓글 C1~C4, 리뷰 R1~R5), DDL 2종, 권한(작성자/운영자), cascade 확장. L1 31테스트 PASS. L3 스모크·DDL 적용은 needs_user_verification. 좋아요는 범위밖.
- [2026-06-22-w3-2-comments-reviews-enhancement.md](./2026-06-22-w3-2-comments-reviews-enhancement.md) — W3-2 고도화(코어 위에 얹음). 다축 평점(game_review_axes 6축·육각형 SVG 레이더) + A1~A3 일관성(commentView 통일·작성자 마스킹 QG-2 해결·edited/updatedAt) + B1~B3 목록규모(페이지네이션·본문10자·TextNormalizer) + C1~C6 UX + game_review_stats 집계뷰(C3 재분류 — W2-3 동결 무관 확정). 신규 4파일+변경 13. L1 43/43 GREEN. DDL·L3 스모크 needs_user_verification.
- [2026-06-29-w3-w4-features.md](./2026-06-29-w3-w4-features.md) — W3(잔여)+W4 5기능 구현(W3-1·W3-4·W3-3·W3-5·W4) + 후속(20260629-142115) 배지 표시 배선 완료·보안 하드닝 b1/b2. 태그+검색·메인허브·포스팅보드·업로드보안·배지/평판 구현(5 커밋 35f1dc3~305cc73, 최종 L1 347/347 GREEN). 후속: 게임카드 creator 배지 칩·프로필 myBadges 배선(3d10449, L1 352/352) + SsrfSafeFetcher @PostConstruct DNS ttl=30(b1 best-effort) + 업로드 저장루트 static 밖 이전(b2, 9041bb7, L1 353/353). DB DDL 8종 사용자 직접 적용 완료. L3 스모크 및 b2 자산 수동 이전은 배포 후 과제 이월 — 상세: [maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md).

View File

@ -4,4 +4,4 @@
## 목록
_(아직 문서 없음)_
- [webgl-display-metadata.md](./webgl-display-metadata.md) — 게임별 빌드 크기 감지·저장·API·플레이어 fallback 계약.

View File

@ -0,0 +1,47 @@
---
kind: contracts
title: 게임별 WebGL 화면 크기 계약
owner: bibimbap
stability: living
last_reviewed: 2026-10-14
---
# 게임별 WebGL 화면 크기
게임의 기준 폭·높이를 등록 정보에 저장하고 플레이어 iframe을 같은 비율로 가용 폭·높이 안에 표시한다. 게임명·ZIP 파일명에 따른 특별 처리는 없다. 이 값은 빌드에 선언된 화면 크기이며 Unity 내부 UI 재배치를 보장하지 않는다.
## 감지와 검증
작성자의 직접 지정이 우선한다. 자동 감지는 선택된 `index.html`과 같은 디렉터리의 `build-info.json` top-level `width`/`height` → 명확한 canvas 또는 그 조상 `#unity-container`의 정수 pixel CSS → canvas의 명시 `width`/`height` 속성 순이다.
Canvas는 `#unity-canvas`가 하나이면 선택하고, 해당 ID가 없으면 canvas가 정확히 하나인 경우 선택한다. CSS는 inline 또는 `style` 블록의 정확한 단일 ID selector만 지원한다. `@` 규칙이 포함된 style 블록, 상대 단위·동적 표현식·외부 CSS·JavaScript style 대입은 감지하지 않는다. JS 실행이나 외부 요청은 하지 않는다. CSS cascade 전체를 계산하는 브라우저가 아니므로 감지된 선언값이 실제 게임과 다르면 직접 지정한다.
폭·높이는 둘 다 정수 1~16384, 비율은 1:32~32:1이다. 숫자 문자열도 허용하되 분수·부호·공백은 허용하지 않는다. 자동 감지의 300×150은 브라우저 기본값으로 취급해 제외하며 직접 지정은 허용한다. HTML 읽기 상한은 1MiB, JSON은 64KiB다. 손상된 선택 메타는 다음 신호로 넘어가고, 알 수 없는 크기는 정상 업로드를 유지한다. 저장 경로는 업로드 저장루트 내부의 regular file만 분석한다.
```json
{"width": 1280, "height": 720}
```
메타 파일은 진입점 옆에 선택적으로 넣는다. 하위 또는 다른 빌드의 파일을 재귀 탐색해서 섞지 않는다.
## API와 저장
| 경계 | 필드 / 동작 |
| --- | --- |
| `POST /api/game-files/webgl-zip` | 기존 응답에 `displayWidth`, `displayHeight`, `displaySource` 추가. 크기 미확인은 null/null/unknown이며 HTTP 200 |
| `POST /game/new`, `POST /game/{id}/edit` | 선택 필드 `displayMode=auto\|custom`, `displayWidth`, `displayHeight` |
| 자동 저장 | 클라이언트 크기 값을 신뢰하지 않고 서버 저장 진입점을 재감지 |
| 직접 지정 저장 | 두 숫자 검증 후 source=manual. 불완전 pair/범위 밖/알 수 없는 mode는 HTTP 400 |
| 구 클라이언트 등록 | mode 누락은 auto |
| 구 클라이언트 수정 | mode 누락과 같은 경로는 기존 메타 유지, 경로 교체는 재감지 |
| 상세 / 수정 읽기 | 유효 저장값 우선, 기존 null 값은 파일에서 읽기 전용 감지. 조회 중 DB 백필 없음 |
기존 인증·CSRF·소유권 검증이 크기 필드 검증보다 먼저 적용된다. Source enum은 `build-info`, `css`, `canvas`, `manual`, `unknown`이다. DB 컬럼은 nullable `games.display_width integer`, `display_height integer`, `display_source varchar(16)`이며 pair/범위/비율/source CHECK가 있다. Source는 legacy 호환을 위해 null도 허용한다. Ratio는 별도 저장하지 않는다. `GameData`를 반환하는 모든 7개 mapper 조회와 INSERT/UPDATE가 세 필드를 전달한다.
## 표시와 fallback
상세 SSR은 검증한 `webglDisplayWidth/Height`, source/known을 전달한다. 미확인 빌드는 16:9 화면으로 표시하며 작성자가 직접 교정할 수 있다. 특정 세로 게임 크기로 기존 행을 일괄 변경하지 않는다.
`scale = min(availableWidth / referenceWidth, availableHeight / referenceHeight)`로 iframe 두 축을 맞춘다. 일반 화면 높이 budget은 `min(70dvh, 720px)`이며 vh fallback이 있다. 여백은 중앙 정렬한다. 확대/전체화면은 safe area와 툴바를 뺀 stage의 실제 크기를 사용한다. ResizeObserver·window/visualViewport resize·fullscreenchange를 RAF로 묶어 반영한다. iframe DOM과 src를 유지하여 게임을 다시 로드하지 않는다. 별도 asset origin에서도 부모는 iframe 박스만 조정하며 canvas DOM 접근을 요구하지 않는다.
전체화면 API가 없거나 거절되면 크게 보기로 전환하고 안내한다. 닫기/Esc·스크롤·포커스 복귀를 지원한다. [사용 절차](../usage/webgl-display.md), [검증 등록](../development/verification-strategies.md)을 함께 참고한다.

View File

@ -111,6 +111,35 @@ strategies:
note: "L1 + L2 + 로그 스캔 통합. ATP task 세션 종료 전 의무."
```
## WebGL 빌드별 화면 대응 (2026-10-14)
이 scope의 구체 명령은 아래 등록값을 사용한다. 상세 계약은 `docs/contracts/webgl-display-metadata.md`, 격리 런타임 절차는 `docs/usage/webgl-display.md`다. 코드 검증과 JSP 런타임 컴파일 검증을 구분한다.
```yaml
strategies:
- id: webgl-display-l1
cmd: bash scripts/verify-webgl-display.sh l1
scope: [src/main/java/com/pandoli365/bibimbap/util/WebglDisplayMetadata.java, src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java, src/main/java/com/pandoli365/bibimbap/controller/api/GameUploadController.java, src/main/webapp/js/webgl-player.js, src/main/webapp/css/webgl-player.css, src/main/webapp/WEB-INF/views/game-detail.jsp, src/main/webapp/WEB-INF/views/game-register.jsp]
timeout_s: 600
failure_severity: blocker
note: Java full regression + Node contain/resize/state regression + WAR package. Runtime JSP compilation is separate.
- id: contract-webgl-display-postgres
cmd: bash scripts/verify-webgl-display.sh l2
scope: [db/schema.sql, db/migrations/20261014-game-display-metadata.sql, src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java, src/main/java/com/pandoli365/bibimbap/data/GameData.java]
timeout_s: 240
failure_severity: blocker
preconditions: [Docker daemon available, local postgres:16 image, Java 21 and cached Maven dependencies]
note: Disposable loopback PostgreSQL only; legacy null row + rerun migration + CHECK rejection + fresh equivalence + actual mapper INSERT/UPDATE/all seven SELECT typed roundtrip. No shared DB mutation. Missing prerequisites must be reported explicitly; failed contract cannot be called skip.
- id: verify-webgl-display
cmd: bash scripts/verify-webgl-display.sh all
scope: webgl-display
timeout_s: 900
failure_severity: blocker
note: Integrated L1 + L2; temporary database removed by EXIT trap.
```
L3 필수 확인: 새 WAR를 기존 앱과 겹치지 않는 포트·disposable DB·임시 업로드 경로에서 기동하고, 인증 후 변경된 등록/상세/수정 JSP 모두 HTTP 200 및 JSP stacktrace 부재를 확인한다. 실제 제공 ZIP과 다른 비율 ZIP으로 등록→DB→상세→수정 roundtrip, viewport 320×844/1440×900/844×390/640×300의 양축 containment·1px 비율 오차, 확대/전체화면/resize 중 같은 iframe DOM/src/load 횟수·게임 상태 유지, CSS canvas 크기와 drawing buffer 크기, 클릭·터치 입력을 점검한다. 전체화면 거절 fallback·Esc·포커스·스크롤 복귀와 서버/브라우저 오류 스캔도 기록한다. 사용자 실제 기기 스모크가 남으면 환경·확인 항목을 별도로 명시한다.
## 필드 정의
| 필드 | 의미 |
@ -280,3 +309,9 @@ graphify(`/graphify src/ docs/`)는 `src/main/webapp/WEB-INF/views/*.jsp`, `src/
§299 는 `.jspf` 정적 include 의 `pageEncoding` 을 다루지만, **DefaultServlet 이 서빙하는 순수 `.html`** 도 동일 mojibake 함정이 있다. Tomcat 은 정적 `.html` 에 `Content-Type: text/html`(charset 없음)만 붙이므로, 파일에 `<meta charset="utf-8">` 가 없으면 브라우저가 Latin-1 로 추정해 UTF-8 한글이 깨진다. 앱 정적경로에 두는 프리뷰·정적 페이지는 `<head>` 에 `<meta charset="utf-8">` 를 반드시 포함한다.
> 근거: 세션 20260701-083240 — 카드 프리뷰 정적 html 을 `/css/` 서빙했으나 meta charset 누락으로 전체 한글 mojibake, 사용자 스샷으로 표면화 → meta 추가로 즉시 해소. 변경 상세: [changes/2026-06-30-ui-multiaxis-ratio-visibility-fix.md](../changes/2026-06-30-ui-multiaxis-ratio-visibility-fix.md).
### WebGL 터치 좌표 검증
모바일 emulation에서 click만 실행하면 pointerType=mouse일 수 있으므로 터치 검증으로 집계하지 않는다. CDP touch 입력에서 pointerType=touch와 isTrusted=true를 확인하고, 비율 유지·확대·resize 후 이름이 있는 게임 버튼의 화면 전환까지 검증한다. 실제 기기 스모크는 자동 browser touch와 구분하여 기록한다.
> 근거: 20261014 WebGL 세션 보완 검증 — Shop/Open Upgrades 입력과 메뉴 전환 확인.

View File

@ -13,6 +13,8 @@ last_reviewed: 2026-05-07
## 목록
- [webgl-display.md](./webgl-display.md) — WebGL 게임별 화면 크기 등록·직접 지정·확대/전체화면과 격리 검증·배포 순서.
- [local-setup.md](./local-setup.md) — 로컬 개발환경 온보딩 (경로 A Docker / 경로 B 직접 실행, DB 스키마 초기화, 트러블슈팅) — 전 과정 실행 검증됨
- [setup-checklist.md](./setup-checklist.md) — 이식 후 30분 내 완료 설정 체크리스트 (복사 제외 재확인 → CLAUDE.md 병합 → 검증 명령 교체 → 카테고리 조정 → graphify 도입 여부)
- [faq.md](./faq.md) — 문제 해결 / FAQ + 이식자 실수 카탈로그 (M1~M8)

View File

@ -0,0 +1,66 @@
---
kind: usage
title: WebGL 게임 화면 크기 등록과 실행
owner: bibimbap
stability: living
last_reviewed: 2026-10-14
---
# WebGL 화면 크기 등록과 실행
게임 등록·수정의 “화면 크기”에서 기본 선택인 “빌드에 맞추기”를 사용한다. ZIP 업로드 후 확인한 폭·높이를 안내한다. 세로·가로·정사각형 게임마다 자기 비율로 플레이 영역에 맞춰 표시된다.
크기를 확인하지 못하거나 실제 게임 화면과 다른 경우 “직접 지정”에서 게임이 기준으로 삼는 폭·높이를 입력한다. 두 값은 1~16384 정수이며 비율은 1:32~32:1이다. 미확인 상태로 자동 등록하면 기본 가로 화면(16:9)을 사용한다. 기존 게임도 수정 화면에서 다시 자동 저장하거나 직접 지정할 수 있다. 직접 지정 중 새 ZIP을 선택해도 입력한 값은 유지되므로 새 게임의 크기와 일치하는지 확인한다.
상세 화면의 “크게 보기”는 게임을 유지하면서 넓은 영역을 제공한다. “전체화면”을 사용할 수 없는 브라우저는 크게 보기로 전환한다. 닫기/Esc로 돌아오며 창 크기·화면 방향이 바뀌어도 게임의 비율을 유지한다. Unity 게임 자체가 브라우저 크기 변경을 처리하지 않는다면 내부 Canvas Scaler·앵커·카메라 설정을 개발자가 고쳐 다시 빌드해야 한다.
템플릿에 의도 크기가 없으면 진입 `index.html`과 같은 폴더에 선택적인 `build-info.json`을 넣을 수 있다. 예: `{"width":800,"height":800}`. 임의 JS로 계산한 크기를 서버가 실행하여 추론하지는 않는다. 전체 [메타 계약](../contracts/webgl-display-metadata.md)을 참고한다.
## 배포 순서
1. 대상 스키마를 지정하여 `db/migrations/20261014-game-display-metadata.sql`을 먼저 적용한다. 운영·공유 DB 변경은 해당 환경의 승인 절차를 따른다.
2. 새 WAR를 배포한다. 앱 시작 시 자동 DDL은 없다.
3. 기존 게임과 새 게임을 상세·등록·수정 화면에서 확인한다.
마이그레이션은 nullable 컬럼을 추가하고 기존 행을 백필하지 않으며 재실행 가능하다. `db/schema.sql`의 신규 DB 정의도 동일하다. 앱을 이전 버전으로 되돌릴 때 이 컬럼은 남겨도 된다. 컬럼 DROP은 별도 변경으로 다룬다. 다음 명령은 **대상 DB를 운영자가 명확히 지정한 뒤** 실행한다.
```bash
PGOPTIONS='-c search_path=dev' psql -X -v ON_ERROR_STOP=1 \
-f db/migrations/20261014-game-display-metadata.sql
```
## 자동 검증
Java 21, Node, 캐시된 Maven 의존성, Docker와 로컬 `postgres:16` 이미지가 필요하다. Maven offline cache가 없으면 의존성을 준비한 후 실행한다. 스크립트는 기존 DB/앱/볼륨을 사용하지 않고 임시 PostgreSQL을 loopback 임의 포트에 띄우며 종료 시 제거한다. 이미지를 자동 pull하지 않는다.
```bash
bash scripts/verify-webgl-display.sh all
# 개별 실행
bash scripts/verify-webgl-display.sh l1
bash scripts/verify-webgl-display.sh l2
```
macOS에서 기본 Java launcher가 런타임을 찾지 못하면 설치된 Java 21을 명시한다. 검증 환경의 Homebrew 경로 예시는 다음과 같다.
```bash
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
PATH=/opt/homebrew/opt/openjdk@21/bin:$PATH \
bash scripts/verify-webgl-display.sh all
```
L1은 Java 전체 단위·회귀, Node 플레이어 테스트, WAR 패키징이다. L2는 기존 null 행 보존, 마이그레이션 두 번 적용, CHECK 거부, fresh/migrated display 스키마 동등성 및 실제 MyBatis INSERT/UPDATE/7개 SELECT roundtrip이다. 일반 Maven test에서 격리 DB 계약 테스트는 환경변수 부재로 skip된다. harness가 만든 DB에서만 활성화하며 JDBC URL은 loopback의 `webgl_contract` DB로 제한한다.
## 격리 런타임 확인
WAR package만으로 JSP 컴파일 성공을 판단하지 않는다. 새 disposable PostgreSQL을 별도로 만들고 `db/schema.sql`을 적용한 후, 기존 앱과 겹치지 않는 포트(예: 18081)에 Maven `spring-boot:run`으로 앱을 기동한다. 현재 WAR에는 Main manifest가 없으므로 `java -jar`로 실행할 수 없다. 임시 DB 포트는 `<isolated-port>`에 지정한다. 다음은 검증에서 성공한 Java 21 실행 명령을 일반화한 예시다. macOS 외 환경에서는 설치된 Java 21 경로로 바꾼다.
```bash
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
PATH=/opt/homebrew/opt/openjdk@21/bin:$PATH \
./mvnw -o spring-boot:run \
'-Dspring-boot.run.arguments=--server.address=127.0.0.1 --server.port=18081 --spring.config.import= --spring.datasource.driver-class-name=org.postgresql.Driver --spring.datasource.url=jdbc:postgresql://127.0.0.1:<isolated-port>/webgl_contract?currentSchema=dev --spring.datasource.username=postgres --spring.datasource.password= --app.upload.game-storage-path=/tmp/bibimbap-webgl-smoke-uploads'
```
격리 DB의 임시 계정으로 로그인한 뒤 등록→업로드→저장→상세→수정 흐름을 확인한다. 실제 받은 ZIP과 별도 가로·정사각형 빌드를 각각 등록해 자기 크기가 적용되는지 본다. 직접 지정·새 ZIP 교체·기존 크기 미확인 게임도 확인한다. 등록 `/game/new`, 상세 `/game/{id}`, 수정 `/game/{id}/edit`의 응답 200과 서버의 JSP/stacktrace 오류 부재를 확인한다. 기존 원본 ZIP과 저장된 HTML의 SHA-256을 비교하여 빌드가 변하지 않았음을 남긴다.
320×844, 1440×900, 844×390, 640×300 viewport에서 세로·가로·정사각형 게임 모두 iframe이 stage 경계 안에 있고 비율 오차가 1px 이내인지 확인한다. 일반·크게 보기·전체화면 진입/복귀·API 거절 fallback·resize를 확인하며 iframe DOM/src/load 횟수와 게임 상태가 유지되는지 기록한다. 실제 Unity canvas는 CSS client 크기와 drawing buffer 크기를 각각 확인하고 상하단 UI·클릭 좌표·모바일 터치 입력을 점검한다. 콘솔 오류, focus 복귀, Esc, body 스크롤 복원도 확인한다. 확인 후 임시 앱/DB/업로드만 정리한다.

View File

@ -0,0 +1,185 @@
---
kind: work-log
title: WebGL 화면 대응 구현과 로컬 확인 기록
owner: bibimbap
status: implemented-local-review
last_reviewed: 2026-10-14
source_session: 20261014-091658
implementation_commit: ddd9abf
verification_closure_commit: 90af599
---
# WebGL 화면 대응 구현과 로컬 확인 기록
사용자가 업로드한 Unity WebGL 빌드마다 기준 화면 크기를 감지·저장하고, 실행 화면이 그 비율을 유지하며 가용 공간에 맞춰 표시되도록 구현했다. 등록자가 크기를 직접 교정할 수 있고, 확대·전체화면·창 크기 변경에도 실행 중인 게임을 유지한다. 특정 게임의 이름이나 해상도에 맞춘 특별 분기는 없다.
구현과 격리 검증 이후 로컬 개발 DB에 마이그레이션을 적용하고 앱을 재시작했다. 실제 고라니 게임과 가로·정사각형 검증용 빌드가 로컬 서비스에 등록되어 있다. 사용자는 브라우저에서 확인할 수 있는 상태에서 **“일단 여기까지 상세히 기록하자. 괜찮은거 같네”**라고 평가했다. 현재 단계의 긍정적 검토 의견으로 기록하며, 실제 모바일 기기의 모든 동작을 확인했다는 의미로 확대하지 않는다.
## 요청과 범위 확정
최초 입력은 `/Users/wemadeplay/Downloads/GoraniCoin-WebGLTest.zip`이었다. 다른 개발자가 만든 실행용 빌드이며 Unity 원본 프로젝트는 포함하지 않았다. 사용자는 고정된 실행 영역과 화면 크기가 달라질 때 제대로 표시되지 않는 문제를 해결하려고 했다.
첫 제안은 원래 비율을 유지하는 플레이어 크기 조정이었다. 사용자가 **“단순히 내가 준 예시에만 맞추는게 아니라 사용자가 업로드한 게임에 맞춰서야”**라고 범위를 명확히 했다. 이에 따라 수동 비율 선택만 제공하는 수준에서 업로드별 크기 감지, 등록·수정 시 저장, 상세 화면 자동 적용까지 범위를 확정했다.
Unity 내부의 UI 앵커·Canvas Scaler·카메라 배치는 수정하지 않았다. 현재 기능은 빌드가 선언한 기준 비율로 브라우저 실행 영역을 맞춘다. 모든 비율에서 게임 내부 UI가 재배치되어야 하는 경우에는 해당 개발자의 원본 수정과 재빌드가 필요할 수 있다.
## 기존 화면에서 확인한 문제
| 대상 | 기존 동작 | 영향 |
| --- | --- | --- |
| 서비스 상세 플레이어 | 16:9, 최대 높이 `min(70vh, 520px)` | 세로 게임이 낮은 가로 영역 안에서 작게 표시됨 |
| 제공 빌드 | 기준 390×844, PC에서도 세로 유지 | 서비스의 고정 가로 영역과 비율이 다름 |
| 제공 빌드 HTML | 폭은 줄이지만 높이는 viewport 높이를 유지 | 폭이 먼저 제한되면 원래 비율이 깨질 수 있음 |
| Unity 렌더 타깃 | canvas CSS 크기와 자동 동기화가 이미 활성화됨 | 초기 width/height만 보고 렌더 해상도가 고정이라고 판단하면 안 됨 |
제공 빌드는 Unity `6000.0.74f1`이었다. 캔버스 표시 크기, GPU drawing buffer, 게임 내부 UI 배치를 구분하여 조사했다. 세부 근거는 [조사 보고서](../../.atp/work-session/20261014-091658/research/research.md)에 있다.
## 크기 감지와 등록 저장
작성자가 직접 지정한 값이 우선한다. 자동 감지는 선택된 `index.html`과 같은 디렉터리의 `build-info.json`에 있는 `width`·`height`, 명확한 canvas 또는 `#unity-container`의 정수 pixel CSS, 명시 canvas 속성 순으로 처리한다.
업로드된 JavaScript를 실행하거나 외부 리소스를 요청하여 크기를 추론하지 않는다. 상대 단위·동적 표현식·외부 CSS·JavaScript style 대입 등 정적으로 해석하기 어려운 템플릿은 감지 대상에서 제외한다. 메타가 없거나 잘못되었어도 정상 빌드 업로드는 계속 허용한다.
| 검증 항목 | 계약 |
| --- | --- |
| 폭과 높이 | 둘 모두 정수, 각각 1~16384 |
| 종횡비 | 1:32~32:1 |
| 기본 canvas 300×150 | 자동 감지 근거에서는 제외, 직접 지정은 허용 |
| 파일 읽기 상한 | HTML 1MiB, JSON 64KiB |
| 분석 경로 | 업로드 저장 루트 내부의 파일만 사용 |
| 감지 실패 | 미확인 상태로 등록 가능. 상세는 기본 16:9 사용 |
자동 감지값은 빌드의 선언값이다. 실제 게임 의도와 다르면 등록·수정 화면의 직접 지정으로 교정한다. 상세 감지 계약은 [화면 크기 계약](../contracts/webgl-display-metadata.md)에 있다.
ZIP 업로드 응답에 `displayWidth`, `displayHeight`, `displaySource`를 추가했다. 등록·수정 화면에서 “빌드에 맞추기” 또는 “직접 지정”을 선택한다. 자동 저장은 브라우저가 보낸 크기 대신 서버에 저장된 빌드에서 다시 감지한다.
직접 지정한 값은 새 ZIP을 선택해도 유지하며, 자동 모드에서는 새 빌드 결과를 반영한다. 이전 클라이언트가 크기 파라미터를 보내지 않는 수정 요청은 같은 빌드 경로라면 기존 메타를 보존하고, 경로를 교체하면 다시 감지한다.
기존 게임의 크기 값이 null이면 상세·수정 조회에서 파일을 읽어 감지한다. 조회 중 DB 백필은 하지 않는다. 작성자가 수정 화면에서 자동 저장하거나 직접 지정할 수 있다. 기존 인증·CSRF·소유권 검증을 유지했다.
## 플레이어 비율과 상태 유지
크기 계산은 `scale = min(가용 폭 / 기준 폭, 가용 높이 / 기준 높이)`를 사용한다. 양축이 공간을 넘지 않게 iframe 크기를 정하고 중앙에 배치한다. 일반 높이 한도는 `min(70dvh, 720px)`이며 vh fallback이 있다.
“크게 보기”와 “전체화면”은 같은 iframe과 src를 유지한다. 창 크기 변경·방향 전환·확대·복귀 때문에 Unity 인스턴스를 다시 만들지 않는다. 전체화면 API가 없거나 거절되면 크게 보기로 전환한다. 닫기·Esc·스크롤 및 포커스 복귀도 지원한다.
부모는 iframe 박스만 조정하므로 별도 asset origin을 사용할 때도 내부 canvas DOM 접근이 필요하지 않다. 이 계약은 Node 테스트로 확인했으며, 다른 origin의 실제 서비스 조합은 별도 실행하지 않았다. 실제 Unity 실행과 입력 검증은 같은 origin의 격리 환경에서 수행했다.
## 저장 구조와 변경 파일
`games`에 nullable `display_width`, `display_height`, `display_source`를 추가했다. 폭·높이 pair, 범위, 종횡비, source CHECK를 두었다. 신규 DB 정의와 기존 DB용 마이그레이션을 함께 작성했다. 기존 행 삭제·백필 없이 적용하며 재실행 가능하다.
| 영역 | 주요 파일 |
| --- | --- |
| 정적 감지 | `WebglDisplayMetadata.java` |
| 업로드·저장·상세 모델 | `GameUploadController.java`, `GameController.java` |
| 데이터와 SQL | `GameData.java`, `GamesMapper.java` |
| 화면과 입력 | `game-register.jsp`, `game-detail.jsp`, `webgl-player.js`, `webgl-player.css` |
| DB | `db/schema.sql`, `db/migrations/20261014-game-display-metadata.sql` |
| Java 검증 | `WebglDisplayMetadataTest`, `GameDisplayMetadataTest`, `GameUploadControllerSecurityTest`, `WebglDisplayDatabaseContractTest` |
| 플레이어 검증 | `src/test/js/webgl-player.test.cjs` |
| 통합 실행 | `scripts/verify-webgl-display.sh` |
구현은 `feat/v2`의 `ddd9abf`, 최초 검증 세션 종료 기록은 `90af599`에 커밋했다. 원격 push와 운영 환경 배포는 수행하지 않았다.
## 검증 결과와 증거 범위
| 검증 | 결과 |
| --- | --- |
| Java 전체 테스트 | 377건, 실패·오류 0. 일반 실행에서 opt-in L2 1건 skip |
| Node 플레이어 테스트 | 11/11 통과 |
| WAR package | 통과 |
| PostgreSQL L2 | 임시 DB에서 별도 활성화하여 1/1 통과. 반복 마이그레이션, 기존 행 보존, 제약, 신규·기존 스키마 정합, 실제 저장·수정과 7개 mapper 조회 확인 |
| JSP 실행 | 인증 후 등록·상세·수정 경로 5개 HTTP 200 |
| 비율과 경계 | 세로 390×844, 가로 1280×720, 정사각형 800×800 각각 4 viewport, 총 12/12 통과 |
| viewport | 320×844, 1440×900, 844×390, 640×300 |
| 상태 유지 | resize·확대·전체화면/복귀·API 거절 fallback에서 동일 iframe/src/Unity 유지, 추가 load 0 |
| 입력 | trusted CDP touch로 세로 Shop, 가로 확대·resize 후 Open Upgrades 전환 확인 |
| 무결성 | 원본 ZIP 및 저장·HTTP 제공 HTML SHA-256 보존 |
| 회귀 | 실제 이전 JSP를 임시 복사본에 적용하면 동일 SSR 테스트 실패, 현재 JSP에서는 통과 |
| UI와 문서 | 영향맵 21개 파일과 기능 링크 확인. detect 2건은 기존 스타일 및 숨겨진 preview 오탐 |
현재 12개 화면 조합의 경계·비율 검증은 모두 통과했다. 수정 전 실패 증거는 실제 이전 JSP의 SSR 계약에 대한 것이며, 이전 브라우저에서 12개 geometry 조합을 재실행한 증거는 아니다.
Unity 준비 후 CSS client 크기와 drawing buffer를 별도로 검사했다. 데스크톱 일반은 291×630 / 582×1260, 전체화면은 388×840 / 776×1680이었다. 표시 크기와 렌더 버퍼를 혼동하지 않았다.
터치 검증은 `pointerType=touch`, `isTrusted=true`와 이름이 있는 게임 버튼의 실제 화면 전환을 함께 확인했다. 최초 모바일 emulation에서 trusted mouse click만 나온 결과는 터치 통과로 집계하지 않고, 별도 Chrome CDP 검증으로 보완했다. 자동 브라우저 터치는 확인했으며 실제 물리 기기 스모크는 남아 있다.
브라우저 console error와 앱 stacktrace는 없었다. Unity 경고, autoplay 및 iframe sandbox 경고는 검증 보고서에 별도로 기록했다.
가이드의 `java -jar` 예시는 현재 WAR의 Main manifest 부재로 실패하여, 실제 성공한 Java 21의 Maven `spring-boot:run` 명령으로 수정했다. 기본 Java launcher가 런타임을 찾지 못하는 macOS 환경의 경로 예시도 추가했다.
## 로컬 적용과 테스트 게임 등록
최초 구현 검증은 임시 PostgreSQL과 별도 포트 앱에서 수행했다. 이후 사용자가 **“테스트용 게임 로컬에 등록해줘 내가 브라우저에서 직접 보게”**라고 요청하여 현재 로컬 개발 서비스에 적용했다.
| 항목 | 현재 로컬 상태 |
| --- | --- |
| 앱 | `bibimbap-app`, `http://localhost:8080` |
| DB | `bibimbap-db`, 호스트 포트 5433, database `bibimbap`, schema `dev` |
| DB 변경 | `20261014-game-display-metadata.sql` 적용 완료. 세 display 컬럼 확인 |
| 앱 반영 | `docker compose restart app` 완료 |
| 등록 방식 | 전용 로컬 테스트 계정의 정상 가입·로그인, CSRF를 거친 ZIP 업로드와 게임 등록 API |
| 공개 접근 | 상세·플레이어 JS/CSS·빌드 진입 HTML HTTP 200, 세 게임 공개 상태 |
| 저장 | 일반 업로드 저장소와 Docker uploads 볼륨. 격리 검증용 임시 DB에만 남긴 등록이 아님 |
| ID | 게임 | 기준 크기 | 주소 | 내용 |
| --- | --- | --- | --- | --- |
| 9 | 고라니 코인 · 화면 대응 테스트 | 390×844 | [실제 게임](http://localhost:8080/game/9) | 받은 Unity 빌드 그대로 업로드 |
| 10 | 가로형 · 화면 비율 검증 | 1280×720 | [가로형](http://localhost:8080/game/10) | 콘텐츠 없는 검증 빌드, 흰 캔버스 |
| 11 | 정사각형 · 화면 비율 검증 | 800×800 | [정사각형](http://localhost:8080/game/11) | 콘텐츠 없는 검증 빌드, 흰 캔버스 |
세 게임 모두 `display_source=build-info`이고 공개 상태다. 기록 직전에 비로그인 요청으로 상세 HTTP 200과 기준 크기 data 속성을 다시 확인했다. 고라니 게임을 기본 브라우저로 열었다. 비밀번호와 세션 쿠키는 기록하지 않는다.
ID와 URL은 현재 개발 DB의 시점 정보이며 DB 초기화나 삭제 후에는 달라질 수 있다. 등록 메타와 업로드 경로는 [local-games.json](./assets/2026-10-14-webgl-display/local-games.json)에 보존했다.
## 스크린샷 15장 보존
사용자 요청으로 `/tmp/bibimbap`에 PNG 15장, `index.html` 갤러리와 `README.txt`를 모았다. 임시 디렉터리 정리 이후에도 확인할 수 있도록 이 기록과 함께 복사본을 보존했다.
[보존한 스크린샷 갤러리](./assets/2026-10-14-webgl-display/index.html) · [이미지 목록](./assets/2026-10-14-webgl-display/README.txt)
| 번호 | 화면 | 이미지 크기 |
| --- | --- | --- |
| 01 | 서비스 상세 데스크톱, 실제 세로 게임 | 1440×900 |
| 02 | 서비스 상세 모바일, 실제 세로 게임 | 320×844 |
| 03~04 | 일반 화면 Home / Shop | 각각 500×757 |
| 05~06 | 가로 확대 Shop / Upgrades | 각각 844×390 |
| 07~08 | 모바일 Home / Shop | 각각 390×844 |
| 09~10 | 추가 촬영 실제 게임 데스크톱 / 모바일 | 1440×900 / 390×844 |
| 11~12 | 가로형 검증 빌드 데스크톱 / 모바일 | 1440×900 / 390×844 |
| 13~14 | 정사각형 검증 빌드 데스크톱 / 모바일 | 1440×900 / 390×844 |
| 15 | 추가 촬영 실제 게임 전체화면 | 1440×900 |
01~08은 앞선 실제 서비스와 입력 검증 화면이다. 09~15는 현재 제품 플레이어 JS/CSS를 실행한 별도 로컬 확인 화면이다. 가로·정사각형의 흰 영역은 검증용 캔버스이며 완성된 게임 콘텐츠가 아니다.
## 사용자 판단과 남은 확인
사용자 의견은 **“괜찮은거 같네”**이며, 이 상태를 상세히 남기고 작업을 여기서 정리하기로 했다. 자동화와 로컬 확인에서 새 동작의 결함은 관찰하지 않았다.
추가 구현을 시작한 상태는 아니다. 이후 확인할 항목은 실제 터치 기기의 크게 보기·전체화면·방향 전환·게임 버튼 스모크와, 필요 시 서로 다른 asset origin의 실제 서비스 실행이다. 운영 환경에 적용할 때는 해당 DB 마이그레이션을 먼저 실행하고 앱을 반영해야 한다. 로컬 `dev` 적용 완료와 운영 배포 완료를 구분한다.
모든 템플릿의 의도 크기를 완벽히 추론하는 기능은 제공하지 않는다. 정적 크기 선언이 없거나 실제 의도와 다르면 작성자가 직접 지정한다. Unity 내부 화면 배치는 빌드 개발자와 별도로 다룬다.
## 다음 확인 순서
1. [고라니 게임](http://localhost:8080/game/9)에서 일반 화면, 크게 보기, 전체화면, 복귀를 확인한다.
2. 창 크기나 방향을 바꾸고 게임 상태 및 Shop / Upgrades 입력이 유지되는지 확인한다.
3. [가로형](http://localhost:8080/game/10)·[정사각형](http://localhost:8080/game/11)의 캔버스로 비율과 여백을 비교한다.
4. 다른 실제 빌드 등록 시 자동 안내와 직접 지정 저장을 확인한다.
5. 실제 모바일 기기 또는 별도 origin 배포가 필요하면 해당 환경 스모크를 수행한다.
검증 명령과 실행 방법은 [사용 가이드](../usage/webgl-display.md), API·저장 규칙은 [계약 문서](../contracts/webgl-display-metadata.md)를 따른다.
## 관련 기록
- [구현 변경 이력](../changes/2026-10-14-webgl-display.md)
- [최초 조사](../../.atp/work-session/20261014-091658/research/research.md)
- [설계](../../.atp/work-session/20261014-091658/implementation/design.md)
- [구현 보고](../../.atp/work-session/20261014-091658/implementation/report.md)
- [최초 독립 검증](../../.atp/work-session/20261014-091658/verification.md)
- [입력 보완 검증](../../.atp/work-session/20261014-091658/supplemental-verification.md)
- [최종 문서와 UI 감사](../../.atp/work-session/20261014-091658/artifacts/final-doc-ui-audit.md)
- [최초 세션 보고서](../../.atp/work-session/20261014-091658/report.md)
최초 세션 보고서는 구현·격리 검증 완료 시점의 기록이다. 후속 스크린샷 수집, 로컬 DB 적용, 실제 서비스 등록과 사용자 긍정 의견은 이 기록의 현재 상태를 따른다. 과거 검증 보고서의 최초 미확인 항목은 보완 검증과 함께 읽는다.

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@ -0,0 +1,22 @@
bibimbap 실행 화면 스크린샷 15장
index.html을 열면 전체 화면 목록을 볼 수 있습니다.
01–08은 이전 검증 스크린샷, 09–15는 이번에 추가 촬영한 제품 플레이어 확인 화면입니다.
가로·정사각형은 게임 콘텐츠 없는 검증용 빌드이며 흰 영역은 해당 캔버스입니다.
실제 제공 게임은 세로형입니다.
01-service-desktop-portrait.png — 서비스 상세 · 데스크톱 · 실제 게임 (1440×900)
02-service-mobile-portrait.png — 서비스 상세 · 모바일 · 실제 게임 (320×844)
03-game-home-normal.png — Home · 일반 화면 (500×757)
04-game-shop-normal.png — Shop · 일반 화면 (500×757)
05-game-shop-expanded-landscape.png — Shop · 가로 확대 (844×390)
06-game-upgrades-expanded-landscape.png — Upgrades · 가로 확대 (844×390)
07-game-home-mobile.png — Home · 모바일 (390×844)
08-game-shop-mobile.png — Shop · 모바일 (390×844)
09-actual-game-portrait-desktop.png — 세로 실제 게임 · 데스크톱 (추가 촬영) (1440×900)
10-actual-game-portrait-mobile.png — 세로 실제 게임 · 모바일 (추가 촬영) (390×844)
11-test-build-landscape-desktop.png — 가로 검증용 빌드 · 데스크톱 (1440×900)
12-test-build-landscape-mobile.png — 가로 검증용 빌드 · 모바일 (390×844)
13-test-build-square-desktop.png — 정사각형 검증용 빌드 · 데스크톱 (1440×900)
14-test-build-square-mobile.png — 정사각형 검증용 빌드 · 모바일 (390×844)
15-actual-game-portrait-fullscreen-desktop.png — 세로 실제 게임 · 전체화면 (추가 촬영) (1440×900)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
[
{
"name": "고라니 코인 · 화면 대응 테스트",
"url": "http://localhost:8080/game/9",
"gameId": 9,
"width": 390,
"height": 844,
"webglPath": "/game/a4602641-5fa6-4cc1-9047-a8d7786bb1f9/GoraniCoin-WebGLTest/index.html",
"publicStatus": 200
},
{
"name": "가로형 · 화면 비율 검증 (1280×720)",
"url": "http://localhost:8080/game/10",
"gameId": 10,
"width": 1280,
"height": 720,
"webglPath": "/game/1e2ddd48-4cf1-4b0e-a222-f4bf3f79d8df/index.html",
"publicStatus": 200
},
{
"name": "정사각형 · 화면 비율 검증 (800×800)",
"url": "http://localhost:8080/game/11",
"gameId": 11,
"width": 800,
"height": 800,
"webglPath": "/game/01e8b5db-00d8-4d0c-b0c6-b302dec6ba5d/index.html",
"publicStatus": 200
}
]

View File

@ -4,6 +4,7 @@
## 목록
- [2026-10-14-webgl-display-session.md](./2026-10-14-webgl-display-session.md) — 업로드별 WebGL 크기 감지·저장, 비율 유지 확대·전체화면 구현과 검증, 로컬 게임9/10/11 등록 및 스크린샷15장 보존. 사용자 “괜찮은거 같네” 의견과 남은 확인 항목 포함. `status: implemented-local-review`.
- [2026-06-17-jam-platform-roadmap.md](./2026-06-17-jam-platform-roadmap.md) — bibimbap 플랫폼 고도화 목적별 워크스트림 로드맵. 4개 W(W1 거버넌스/RBAC · W2 게임잼 · W3 사이트플랫폼 · W4 유저배지)+운영으로 재구조. 게임잼은 W2 하나. 의존성(빌드순서) 유지 + 잼일정 분리가능 여부 명시. S→W 매핑 + 150635 대비 교정 5건 포함. `status: active`, source_session: 20260617-150635, restructured_by: 20260617-162536.
- [2026-06-17-w3-feature-skeletons.md](./2026-06-17-w3-feature-skeletons.md) — W3(사이트 플랫폼) 5개 서브기능 골자 카탈로그. 기능당 목적·핵심동작·결합/의존·미결질문·후속진입점(윤곽 깊이, 미결 해소 안 함). 코드 현황 대조(tag/review 테이블 신규·Interceptor 미구현·`/game/**` 핸들러 미등록). 결합표 + 착수순서 제언(W3-2 1순위) + QG-1~3. 당장 구현 아님. `status: confirmed`(20260617-174635 세션서 5기능+착수순서 항목별 합의), source_session: 20260617-172407. ⚠️ **코드 현황 #2·#3 stale (2026-06-23 정정)** — w2-w4-skeletons 참조.
- [2026-06-23-w2-w4-full-design-summary.md](./2026-06-23-w2-w4-full-design-summary.md) — **W2+W3(잔여)+W4 풀설계 통합 요약/설계 인덱스**. 11기능(W2-1~6·W3-1/3-3/3-4/3-5·W4) 풀설계(W1-design 깊이, 오픈질문 0) 진입 인덱스 + 크로스-W 결정 매트릭스(정석 확정) + 교차정합 감사 verdict(HIGH 1 해소·동결 단일권위 유지) + 골자 stale 정정 3건 + 착수순서. 설계 본문은 `.atp/work-session/20260623-104307/implementation/`. 설계 전용(코드 0줄). `status: active`, source_session: 20260623-104307, resumed_from: 20260622-180054.

84
scripts/verify-webgl-display.sh Executable file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
mode="${1:-all}"
case "$mode" in all|l1|l2) ;; *) echo 'Usage: bash scripts/verify-webgl-display.sh [all|l1|l2]' >&2; exit 2 ;; esac
if [[ "$mode" != l2 ]]; then
./mvnw -o test
node --test src/test/js/webgl-player.test.cjs
./mvnw -o -DskipTests package
echo 'L1 WebGL display: PASS (Java regression, Node player regression, WAR package)'
fi
[[ "$mode" != l1 ]] || exit 0
# No existing database, credentials, volume, or application container is used.
image="${WEBGL_CONTRACT_POSTGRES_IMAGE:-postgres:16}"
docker image inspect "$image" >/dev/null
container="bibimbap-webgl-contract-$$-${RANDOM}"
cleanup() { docker rm -f "$container" >/dev/null 2>&1 || true; }
docker run --pull=never -d --name "$container" --label bibimbap.webgl-contract=true \
-e POSTGRES_HOST_AUTH_METHOD=trust -e POSTGRES_DB=webgl_contract \
-p 127.0.0.1::5432 "$image" >/dev/null
trap cleanup EXIT
ready=false
for ((attempt=0; attempt<60; attempt++)); do
if docker exec "$container" pg_isready -h 127.0.0.1 -U postgres -d webgl_contract >/dev/null 2>&1; then ready=true; break; fi
sleep 1
done
[[ "$ready" == true ]] || { echo 'Disposable PostgreSQL did not become ready' >&2; exit 1; }
sql() { docker exec -i "$container" psql -X -v ON_ERROR_STOP=1 -U postgres -d "$1" "${@:2}"; }
sql webgl_contract -q < db/schema.sql
sql webgl_contract -q <<'SQL'
SET search_path TO dev;
ALTER TABLE games DROP COLUMN display_width, DROP COLUMN display_height, DROP COLUMN display_source;
INSERT INTO users(id, display_name) VALUES (9000001, 'Legacy contract author');
INSERT INTO games(id,user_id,name) VALUES (9000001,9000001,'Legacy contract game');
SQL
for repeat in 1 2; do
{ echo 'SET search_path TO dev;'; cat db/migrations/20261014-game-display-metadata.sql; } | sql webgl_contract -q
done
sql webgl_contract -q <<'SQL'
SET search_path TO dev;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM games WHERE id=9000001 AND name='Legacy contract game'
AND display_width IS NULL AND display_height IS NULL AND display_source IS NULL) THEN
RAISE EXCEPTION 'Migration changed existing row or metadata nullability';
END IF;
UPDATE games SET display_width=800,display_height=800,display_source='build-info' WHERE id=9000001;
BEGIN
UPDATE games SET display_height=NULL WHERE id=9000001;
RAISE EXCEPTION 'Partial pair unexpectedly accepted';
EXCEPTION WHEN check_violation THEN NULL; END;
BEGIN
UPDATE games SET display_width=0 WHERE id=9000001;
RAISE EXCEPTION 'Zero width unexpectedly accepted';
EXCEPTION WHEN check_violation THEN NULL; END;
BEGIN
UPDATE games SET display_width=16385 WHERE id=9000001;
RAISE EXCEPTION 'Oversize width unexpectedly accepted';
EXCEPTION WHEN check_violation THEN NULL; END;
BEGIN
UPDATE games SET display_width=1,display_height=100 WHERE id=9000001;
RAISE EXCEPTION 'Extreme ratio unexpectedly accepted';
EXCEPTION WHEN check_violation THEN NULL; END;
BEGIN
UPDATE games SET display_source='unexpected' WHERE id=9000001;
RAISE EXCEPTION 'Unknown source enum unexpectedly accepted';
EXCEPTION WHEN check_violation THEN NULL; END;
UPDATE games SET display_width=NULL,display_height=NULL,display_source='unknown' WHERE id=9000001;
END $$;
CREATE DATABASE webgl_fresh;
SQL
sql webgl_fresh -q < db/schema.sql
signature="SELECT 'column:'||column_name||':'||data_type||':'||is_nullable||':'||COALESCE(character_maximum_length::text,'') FROM information_schema.columns WHERE table_schema='dev' AND table_name='games' AND column_name LIKE 'display_%' UNION ALL SELECT 'constraint:'||conname||':'||pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid='dev.games'::regclass AND conname LIKE 'games_display_%' ORDER BY 1"
migrated="$(sql webgl_contract -At -c "$signature")"
fresh="$(sql webgl_fresh -At -c "$signature")"
[[ "$migrated" == "$fresh" ]] || { echo 'Fresh/migrated display schema mismatch' >&2; exit 1; }
port="$(docker port "$container" 5432/tcp)"
port="${port##*:}"
WEBGL_CONTRACT_JDBC_URL="jdbc:postgresql://127.0.0.1:${port}/webgl_contract?currentSchema=dev" \
./mvnw -o -Dtest=WebglDisplayDatabaseContractTest test
echo 'L2 WebGL display: PASS (legacy row, idempotency, checks, fresh equivalence, actual mapper roundtrip)'

View File

@ -1,6 +1,7 @@
package com.pandoli365.bibimbap.controller.api;
import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.util.WebglDisplayMetadata;
import com.pandoli365.bibimbap.data.GameLikeData;
import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
import com.pandoli365.bibimbap.mapper.GameLikesMapper;
@ -27,6 +28,10 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
import java.util.Objects;
@Controller
public class GameController {
@ -44,6 +49,9 @@ public class GameController {
@Value("${app.webgl.asset-origin:}")
private String webglAssetOrigin;
@Value("${app.upload.game-storage-path:src/main/resources/static}")
private String uploadStoragePath;
public GameController(GamesMapper gamesMapper,
GameCommentsMapper gameCommentsMapper,
GameReviewsMapper gameReviewsMapper,
@ -113,6 +121,12 @@ public class GameController {
game.setVisible(isChecked(visible));
game.setSortOrder(gamesMapper.nextSortOrder());
try {
applyDisplayMetadata(game, request, null);
} catch (IllegalArgumentException e) {
return response(HttpStatus.BAD_REQUEST, e.getMessage());
}
gamesMapper.addGame(game);
if (game.getId() == null) {
return response(HttpStatus.INTERNAL_SERVER_ERROR, "게임 등록 결과를 확인하지 못했습니다.");
@ -173,6 +187,10 @@ public class GameController {
model.addAttribute("editThumbnailUrl", trimToEmpty(game.getThumbnailUrl()));
model.addAttribute("editVisible", game.getVisible() == null || game.getVisible());
model.addAttribute("editGameUuid", gameUuidFromPath(trimToEmpty(game.getWebglPath())));
WebglDisplayMetadata display = resolvedDisplay(game);
model.addAttribute("editDisplayWidth", display.width());
model.addAttribute("editDisplayHeight", display.height());
model.addAttribute("editDisplaySource", display.source());
return "game-register";
}
@ -225,6 +243,16 @@ public class GameController {
}
String oldWebglPath = existing.getWebglPath();
GameData replacement = new GameData();
replacement.setWebglPath(normalizedWebglPath);
try {
applyDisplayMetadata(replacement, request, existing);
} catch (IllegalArgumentException e) {
return response(HttpStatus.BAD_REQUEST, e.getMessage());
}
existing.setDisplayWidth(replacement.getDisplayWidth());
existing.setDisplayHeight(replacement.getDisplayHeight());
existing.setDisplaySource(replacement.getDisplaySource());
existing.setName(normalizedName);
existing.setCreatorNote(normalizedCreatorNote);
existing.setGitUrl(safeExternalUrl(gitUrl));
@ -337,6 +365,11 @@ public class GameController {
model.addAttribute("gitUrl", safeExternalUrl(game.getGitUrl()));
model.addAttribute("webglUrl", webglPath);
model.addAttribute("webglFrameSrc", webglFrameSrc(webglPath));
WebglDisplayMetadata display = resolvedDisplay(game);
model.addAttribute("webglDisplayWidth", display.known() ? display.width() : 16);
model.addAttribute("webglDisplayHeight", display.known() ? display.height() : 9);
model.addAttribute("webglDisplaySource", display.source());
model.addAttribute("webglDisplayKnown", display.known());
model.addAttribute("webglDeployPath", webglPath);
model.addAttribute("owner", currentUserId != null && currentUserId.equals(game.getUserId()));
model.addAttribute("currentUserId", currentUserId);
@ -357,6 +390,54 @@ public class GameController {
return origin + (normalizedPath.startsWith("/") ? normalizedPath : "/" + normalizedPath);
}
private void applyDisplayMetadata(GameData game, HttpServletRequest request, GameData existing) {
String mode = request.getParameter("displayMode");
if (mode == null && existing != null && Objects.equals(game.getWebglPath(), existing.getWebglPath())) {
game.setDisplayWidth(existing.getDisplayWidth());
game.setDisplayHeight(existing.getDisplayHeight());
game.setDisplaySource(existing.getDisplaySource());
return;
}
WebglDisplayMetadata display;
if ("custom".equals(mode)) {
Integer width = WebglDisplayMetadata.integer(request.getParameter("displayWidth"));
Integer height = WebglDisplayMetadata.integer(request.getParameter("displayHeight"));
if (!WebglDisplayMetadata.valid(width, height)) {
throw new IllegalArgumentException("화면 폭과 높이는 1~16384 정수이며 비율은 1:32~32:1이어야 합니다.");
}
display = new WebglDisplayMetadata(width, height, "manual");
} else if (mode == null || "auto".equals(mode)) {
display = detectStoredDisplay(game.getWebglPath());
} else {
throw new IllegalArgumentException("화면 크기 설정을 확인해 주세요.");
}
game.setDisplayWidth(display.width());
game.setDisplayHeight(display.height());
game.setDisplaySource(display.source());
}
private WebglDisplayMetadata resolvedDisplay(GameData game) {
if (WebglDisplayMetadata.valid(game.getDisplayWidth(), game.getDisplayHeight())) {
return new WebglDisplayMetadata(game.getDisplayWidth(), game.getDisplayHeight(), game.getDisplaySource());
}
return detectStoredDisplay(game.getWebglPath());
}
private WebglDisplayMetadata detectStoredDisplay(String webglPath) {
if (uploadStoragePath == null || !isGameAssetPath(webglPath) || !webglPath.endsWith("/index.html")) {
return WebglDisplayMetadata.unknown();
}
try {
Path root = Path.of(uploadStoragePath).toAbsolutePath().normalize().resolve("game").normalize();
Path index = root.resolve(webglPath.substring("/game/".length())).normalize();
if (!index.startsWith(root) || !Files.exists(root) || !Files.exists(index)
|| !index.toRealPath().startsWith(root.toRealPath())) return WebglDisplayMetadata.unknown();
return WebglDisplayMetadata.detect(index);
} catch (IOException | RuntimeException e) {
return WebglDisplayMetadata.unknown();
}
}
private String safeExternalUrl(String value) {
String text = trimToNull(value);
if (text == null) {

View File

@ -1,6 +1,7 @@
package com.pandoli365.bibimbap.controller.api;
import com.pandoli365.bibimbap.data.GameAssetOwner;
import com.pandoli365.bibimbap.util.WebglDisplayMetadata;
import com.pandoli365.bibimbap.data.GameUploadAudit;
import com.pandoli365.bibimbap.mapper.GameUploadAuditMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
@ -206,6 +207,7 @@ public class GameUploadController {
.body(Map.of("message", "zip 안에서 index.html을 찾지 못했습니다."));
}
ZipSecurity.assertUnityBuild(tmpDir);
WebglDisplayMetadata display = WebglDisplayMetadata.detect(indexFile);
String relIndex = tmpDir.relativize(indexFile).toString().replace('\\', '/');
@ -230,6 +232,9 @@ public class GameUploadController {
response.put("gameUuid", newUuid);
response.put("webglPath", webglPath);
response.put("deployPath", deployPath);
response.put("displayWidth", display.width());
response.put("displayHeight", display.height());
response.put("displaySource", display.source());
response.put("entryCount", extractResult.entryCount());
response.put("extractedBytes", extractResult.extractedBytes());
log.info("WebGL zip upload completed. gameUuid={}, webglPath={}, entryCount={}, extractedBytes={}",

View File

@ -11,6 +11,9 @@ public class GameData {
private String creatorNote;
private String gitUrl;
private String webglPath;
private Integer displayWidth;
private Integer displayHeight;
private String displaySource;
private String thumbnailUrl;
private Integer likeCount;
private Boolean visible;
@ -81,6 +84,13 @@ public class GameData {
return thumbnailUrl;
}
public Integer getDisplayWidth() { return displayWidth; }
public void setDisplayWidth(Integer displayWidth) { this.displayWidth = displayWidth; }
public Integer getDisplayHeight() { return displayHeight; }
public void setDisplayHeight(Integer displayHeight) { this.displayHeight = displayHeight; }
public String getDisplaySource() { return displaySource; }
public void setDisplaySource(String displaySource) { this.displaySource = displaySource; }
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}

View File

@ -24,6 +24,9 @@ public interface GamesMapper {
g.creator_note AS creatorNote,
g.git_url AS gitUrl,
g.webgl_path AS webglPath,
g.display_width AS displayWidth,
g.display_height AS displayHeight,
g.display_source AS displaySource,
g.thumbnail_url AS thumbnailUrl,
g.like_count AS likeCount,
g.is_visible AS visible,
@ -47,6 +50,9 @@ public interface GamesMapper {
g.creator_note AS creatorNote,
g.git_url AS gitUrl,
g.webgl_path AS webglPath,
g.display_width AS displayWidth,
g.display_height AS displayHeight,
g.display_source AS displaySource,
g.thumbnail_url AS thumbnailUrl,
g.like_count AS likeCount,
g.is_visible AS visible,
@ -71,6 +77,9 @@ public interface GamesMapper {
g.creator_note AS creatorNote,
g.git_url AS gitUrl,
g.webgl_path AS webglPath,
g.display_width AS displayWidth,
g.display_height AS displayHeight,
g.display_source AS displaySource,
g.thumbnail_url AS thumbnailUrl,
g.like_count AS likeCount,
g.is_visible AS visible,
@ -100,6 +109,9 @@ public interface GamesMapper {
g.creator_note AS creatorNote,
g.git_url AS gitUrl,
g.webgl_path AS webglPath,
g.display_width AS displayWidth,
g.display_height AS displayHeight,
g.display_source AS displaySource,
g.thumbnail_url AS thumbnailUrl,
g.like_count AS likeCount,
g.is_visible AS visible,
@ -122,6 +134,9 @@ public interface GamesMapper {
creator_note,
git_url,
webgl_path,
display_width,
display_height,
display_source,
thumbnail_url,
is_visible,
sort_order
@ -131,6 +146,9 @@ public interface GamesMapper {
#{creatorNote},
#{gitUrl},
#{webglPath},
#{displayWidth},
#{displayHeight},
#{displaySource},
#{thumbnailUrl},
#{visible},
#{sortOrder}
@ -154,6 +172,9 @@ public interface GamesMapper {
creator_note = #{creatorNote},
git_url = #{gitUrl},
webgl_path = #{webglPath},
display_width = #{displayWidth},
display_height = #{displayHeight},
display_source = #{displaySource},
thumbnail_url = #{thumbnailUrl},
is_visible = #{visible},
sort_order = #{sortOrder},
@ -204,6 +225,7 @@ public interface GamesMapper {
SELECT g.id, g.user_id AS userId, g.name, u.display_name AS creator,
g.creator_note AS creatorNote, g.git_url AS gitUrl, g.webgl_path AS webglPath,
g.thumbnail_url AS thumbnailUrl, g.like_count AS likeCount, g.is_visible AS visible,
g.display_width AS displayWidth, g.display_height AS displayHeight, g.display_source AS displaySource,
g.sort_order AS sortOrder, g.created_at AS createdAt, g.updated_at AS updatedAt,
g.view_count AS viewCount,
grs.avg_rating AS "avgRating",
@ -271,6 +293,9 @@ public interface GamesMapper {
g.creator_note AS creatorNote,
g.git_url AS gitUrl,
g.webgl_path AS webglPath,
g.display_width AS displayWidth,
g.display_height AS displayHeight,
g.display_source AS displaySource,
g.thumbnail_url AS thumbnailUrl,
g.like_count AS likeCount,
g.is_visible AS visible,
@ -308,6 +333,9 @@ public interface GamesMapper {
g.creator_note AS creatorNote,
g.git_url AS gitUrl,
g.webgl_path AS webglPath,
g.display_width AS displayWidth,
g.display_height AS displayHeight,
g.display_source AS displaySource,
g.thumbnail_url AS thumbnailUrl,
g.like_count AS likeCount,
g.is_visible AS visible,

View File

@ -0,0 +1,131 @@
package com.pandoli365.bibimbap.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Declared build dimensions, obtained without executing uploaded code. */
public record WebglDisplayMetadata(Integer width, Integer height, String source) {
private static final int HTML_LIMIT = 1024 * 1024;
private static final int JSON_LIMIT = 64 * 1024;
private static final ObjectMapper JSON = new ObjectMapper();
private static final Pattern PIXELS = Pattern.compile("(?i)^([0-9]+)px(?:\\s*!important)?$");
public static WebglDisplayMetadata unknown() {
return new WebglDisplayMetadata(null, null, "unknown");
}
public static boolean valid(Integer width, Integer height) {
return width != null && height != null && width >= 1 && height >= 1
&& width <= 16384 && height <= 16384
&& (long) width <= (long) height * 32 && (long) height <= (long) width * 32;
}
public boolean known() { return valid(width, height); }
public static WebglDisplayMetadata detect(Path indexFile) {
if (indexFile == null) return unknown();
try {
Path index = indexFile.toAbsolutePath().normalize();
if (!Files.isRegularFile(index, LinkOption.NOFOLLOW_LINKS)) return unknown();
index = index.toRealPath();
Path parent = index.getParent();
if (parent == null) return unknown();
String html = boundedRead(index, HTML_LIMIT);
Path info = parent.resolve("build-info.json");
try {
if (Files.isRegularFile(info, LinkOption.NOFOLLOW_LINKS) && info.toRealPath().getParent().equals(parent)) {
String text = boundedRead(info, JSON_LIMIT);
if (text != null) {
JsonNode node = JSON.readTree(text);
WebglDisplayMetadata metadata = pair(integer(node == null ? null : node.get("width")),
integer(node == null ? null : node.get("height")), "build-info");
if (metadata.known()) return metadata;
}
}
} catch (IOException | RuntimeException ignored) {
// An optional manifest must not make a valid build fail to upload.
}
if (html == null) return unknown();
Document document = Jsoup.parse(html);
Element canvas = document.selectFirst("canvas#unity-canvas");
if (document.select("canvas#unity-canvas").size() > 1) return unknown();
if (canvas == null && document.select("canvas").size() == 1) canvas = document.selectFirst("canvas");
if (canvas == null) return unknown();
WebglDisplayMetadata css = cssPair(canvas.attr("style"));
if (css.known()) return css;
Element container = canvas.parent();
while (container != null && !"unity-container".equals(container.id())) container = container.parent();
if (container != null) {
css = cssPair(container.attr("style"));
if (css.known()) return css;
}
// Only exact selectors are accepted; media queries and dynamic expressions are not evidence.
String selector = canvas.id().isBlank() ? "" : "#" + canvas.id();
for (Element style : document.select("style")) {
String text = style.data().replaceAll("(?s)/\\*.*?\\*/", "");
if (text.contains("@")) continue;
Matcher rule = Pattern.compile("([^{}]+)\\{([^{}]*)}").matcher(text);
while (rule.find()) {
String target = rule.group(1).trim();
if ((!selector.isEmpty() && selector.equals(target))
|| (container != null && "#unity-container".equals(target))) {
css = cssPair(rule.group(2));
if (css.known()) return css;
}
}
}
return pair(integer(canvas.attr("width")), integer(canvas.attr("height")), "canvas");
} catch (IOException | RuntimeException ignored) {
return unknown();
}
}
private static String boundedRead(Path path, int limit) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > limit) return null;
try (var input = Files.newInputStream(path)) {
byte[] bytes = input.readNBytes(limit + 1);
return bytes.length > limit ? null : new String(bytes, StandardCharsets.UTF_8);
}
}
private static WebglDisplayMetadata cssPair(String style) {
Integer width = null;
Integer height = null;
for (String declaration : style.split(";")) {
String[] parts = declaration.split(":", 2);
if (parts.length != 2) continue;
String key = parts[0].trim().toLowerCase(java.util.Locale.ROOT);
Matcher value = PIXELS.matcher(parts[1].trim());
Integer pixels = value.matches() ? integer(value.group(1)) : null;
if ("width".equals(key)) width = pixels;
if ("height".equals(key)) height = pixels;
}
return pair(width, height, "css");
}
private static Integer integer(JsonNode node) {
if (node == null || !(node.isIntegralNumber() || node.isTextual())) return null;
return integer(node.asText());
}
public static Integer integer(String text) {
if (text == null || !text.matches("[0-9]{1,5}")) return null;
try { return Integer.valueOf(text); } catch (NumberFormatException ignored) { return null; }
}
private static WebglDisplayMetadata pair(Integer width, Integer height, String source) {
return valid(width, height) && !(width == 300 && height == 150)
? new WebglDisplayMetadata(width, height, source) : unknown();
}
}

View File

@ -19,14 +19,22 @@
webglFrameSrc = ctx + webglFrameSrc;
}
String webglFrameSrcValue = HtmlUtils.htmlEscape(webglFrameSrc);
int displayWidth = request.getAttribute("webglDisplayWidth") instanceof Number ? ((Number) request.getAttribute("webglDisplayWidth")).intValue() : 16;
int displayHeight = request.getAttribute("webglDisplayHeight") instanceof Number ? ((Number) request.getAttribute("webglDisplayHeight")).intValue() : 9;
if (displayWidth < 1 || displayWidth > 16384 || displayHeight < 1 || displayHeight > 16384 || (double) displayWidth / displayHeight < 1.0 / 32 || (double) displayWidth / displayHeight > 32) {
displayWidth = 16;
displayHeight = 9;
}
%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
<title><%= gameNameValue %> — bibimbap</title>
<link rel="stylesheet" href="<%= ctx %>/css/webgl-player.css">
<script src="<%= ctx %>/js/webgl-player.js" defer></script>
<style>
html {
color-scheme: light;
@ -365,8 +373,6 @@
.game-webgl__frame-wrap {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
max-height: min(70vh, 520px);
background: var(--webgl-bg);
border-radius: 15px;
overflow: hidden;
@ -375,13 +381,6 @@
html:not([data-theme="dark"]) .game-webgl__frame-wrap {
border-color: rgba(0, 0, 0, 0.12);
}
.game-webgl__frame-wrap iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: none;
}
/* 제작자 한마디 ~ 덧글: 공통 패널 */
.game-community {
margin-top: 2rem;
@ -1254,8 +1253,13 @@
<h2 id="webgl-heading">플레이</h2>
<span class="game-webgl__badge">WebGL</span>
</div>
<div class="game-webgl__shell">
<div class="game-webgl__frame-wrap">
<div class="game-webgl__shell webgl-player" data-webgl-player data-display-width="<%= displayWidth %>" data-display-height="<%= displayHeight %>" style="--game-ratio: <%= (double) displayWidth / displayHeight %>" aria-label="게임 플레이어">
<div class="webgl-player__toolbar">
<button type="button" class="webgl-player__button" data-player-expand aria-expanded="false" aria-controls="webgl-stage">크게 보기</button>
<button type="button" class="webgl-player__button" data-player-fullscreen aria-pressed="false" aria-controls="webgl-stage">전체화면</button>
<span class="webgl-player__status" data-player-status role="status" aria-live="polite"></span>
</div>
<div class="game-webgl__frame-wrap webgl-player__stage" id="webgl-stage" data-player-stage>
<iframe title="<%= gameNameValue %> WebGL"
src="<%= webglFrameSrcValue %>"
sandbox="allow-scripts allow-same-origin allow-pointer-lock"

View File

@ -17,6 +17,9 @@
String editThumbnailUrl = HtmlUtils.htmlEscape(request.getAttribute("editThumbnailUrl") == null ? "" : String.valueOf(request.getAttribute("editThumbnailUrl")));
String editGameUuid = HtmlUtils.htmlEscape(request.getAttribute("editGameUuid") == null ? "" : String.valueOf(request.getAttribute("editGameUuid")));
boolean editVisible = !editMode || Boolean.TRUE.equals(request.getAttribute("editVisible"));
String editDisplayWidth = request.getAttribute("editDisplayWidth") instanceof Number ? String.valueOf(((Number) request.getAttribute("editDisplayWidth")).intValue()) : "";
String editDisplayHeight = request.getAttribute("editDisplayHeight") instanceof Number ? String.valueOf(((Number) request.getAttribute("editDisplayHeight")).intValue()) : "";
boolean customDisplay = "manual".equals(request.getAttribute("editDisplaySource"));
%>
<html lang="ko">
<head>
@ -141,6 +144,14 @@
height: 3rem;
padding: 0 0.875rem;
}
.game-display { margin: 0; padding: 0; border: 0; min-width: 0; }
.game-display legend { margin-bottom: .5rem; }
.game-display__options { display: flex; flex-wrap: wrap; gap: .5rem 1rem; }
.game-display__options label { display: inline-flex; align-items: center; gap: .375rem; min-height: 44px; cursor: pointer; }
.game-display__options input { accent-color: var(--accent); }
.game-display__hint { margin: .375rem 0 .75rem; font-size: .8125rem; line-height: 1.5; color: var(--text-muted); }
.game-display__sizes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .875rem; }
.game-display__sizes[hidden] { display: none; }
.game-file {
min-height: 3rem;
padding: 0.625rem 0.75rem;
@ -397,6 +408,26 @@
</label>
<input class="game-file__input" type="file" id="game-thumbnail-image" name="thumbnailImage" accept="image/png,image/jpeg,image/webp,image/gif" />
</div>
<div class="game-field game-field--full">
<fieldset class="game-display" id="game-display" data-width="<%= editDisplayWidth %>" data-height="<%= editDisplayHeight %>">
<legend class="game-field__label">화면 크기</legend>
<div class="game-display__options">
<label><input type="radio" name="displayMode" value="auto" <%= customDisplay ? "" : "checked" %>> 빌드에 맞추기</label>
<label><input type="radio" name="displayMode" value="custom" <%= customDisplay ? "checked" : "" %>> 직접 지정</label>
</div>
<p class="game-display__hint" id="game-display-hint" role="status" aria-live="polite"><%= !editDisplayWidth.isBlank() && !editDisplayHeight.isBlank() ? "현재 화면 크기: " + editDisplayWidth + " × " + editDisplayHeight : "ZIP을 선택하면 게임의 화면 크기를 확인합니다." %></p>
<div class="game-display__sizes" id="game-display-sizes" <%= customDisplay ? "" : "hidden" %>>
<div class="game-field">
<label class="game-field__label" for="game-display-width">폭 (px)</label>
<input class="game-field__input" type="number" id="game-display-width" name="displayWidth" min="1" max="16384" step="1" value="<%= editDisplayWidth %>" <%= customDisplay ? "required" : "disabled" %>>
</div>
<div class="game-field">
<label class="game-field__label" for="game-display-height">높이 (px)</label>
<input class="game-field__input" type="number" id="game-display-height" name="displayHeight" min="1" max="16384" step="1" value="<%= editDisplayHeight %>" <%= customDisplay ? "required" : "disabled" %>>
</div>
</div>
</fieldset>
</div>
<div class="game-field game-field--full">
<label class="game-field__label" for="game-creator-note">소개</label>
<textarea class="game-field__textarea" id="game-creator-note" name="creatorNote" maxlength="600"><%= editCreatorNote %></textarea>
@ -466,6 +497,13 @@
var previewThumb = document.getElementById('preview-thumb');
var previewFallback = document.getElementById('preview-fallback');
var thumbnailPreviewUrl = null;
var displayField = document.getElementById('game-display');
var displayHint = document.getElementById('game-display-hint');
var displaySizes = document.getElementById('game-display-sizes');
var displayWidthInput = document.getElementById('game-display-width');
var displayHeightInput = document.getElementById('game-display-height');
var webglUploadPromise = null;
var uploadGeneration = 0;
var submitText = submitLabel ? submitLabel.textContent : '';
function valueOr(input, fallback) {
@ -473,6 +511,29 @@
return value || fallback;
}
function displayMode() { return form.querySelector('input[name="displayMode"]:checked').value; }
function updateDisplayMode() {
var custom = displayMode() === 'custom';
displaySizes.hidden = !custom;
[displayWidthInput, displayHeightInput].forEach(function (input) { input.disabled = !custom; input.required = custom; input.setCustomValidity(''); });
}
function applyDisplayMetadata(data) {
var width = Number(data.displayWidth), height = Number(data.displayHeight);
var known = Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0;
displayField.dataset.width = known ? width : '';
displayField.dataset.height = known ? height : '';
displayHint.textContent = known ? '빌드에서 ' + width + ' × ' + height + ' 화면을 확인했습니다. 크기가 다르면 직접 지정해 주세요.' : '화면 크기를 확인하지 못했습니다. 직접 지정하거나 기본 가로 화면으로 등록할 수 있습니다.';
if (known && displayMode() === 'auto') {
if (!displayWidthInput.value && !displayHeightInput.value) { displayWidthInput.value = width; displayHeightInput.value = height; }
}
}
function acceptWebglResult(data) {
uploadedGameUuid.value = data.gameUuid;
uploadedWebglPath.value = data.webglPath;
applyDisplayMetadata(data);
return data;
}
function updatePreview() {
previewName.textContent = valueOr(nameInput, '게임 이름');
previewNote.textContent = valueOr(noteInput, '소개');
@ -518,6 +579,8 @@
function setSubmitting(submitting) {
if (!submitBtn) return;
submitBtn.disabled = submitting;
webglInput.disabled = submitting;
thumbnailInput.disabled = submitting;
if (submitLabel) {
submitLabel.textContent = submitting ? (editMode ? '수정 중...' : '등록 중...') : submitText;
}
@ -565,6 +628,11 @@
body.set('gameUuid', valueOr(uploadedGameUuid, ''));
body.set('webglPath', valueOr(uploadedWebglPath, ''));
body.set('thumbnailUrl', valueOr(uploadedThumbnailUrl, ''));
body.set('displayMode', displayMode());
if (displayMode() === 'custom') {
body.set('displayWidth', displayWidthInput.value);
body.set('displayHeight', displayHeightInput.value);
}
if (visibleInput && visibleInput.checked) {
body.set('visible', visibleInput.value || 'true');
}
@ -597,9 +665,21 @@
if (nameInput) nameInput.addEventListener('input', updatePreview);
if (noteInput) noteInput.addEventListener('input', updatePreview);
form.querySelectorAll('input[name="displayMode"]').forEach(function (input) { input.addEventListener('change', updateDisplayMode); });
[displayWidthInput, displayHeightInput].forEach(function (input) {
input.addEventListener('input', function () {
var ratio = Number(displayWidthInput.value) / Number(displayHeightInput.value);
displayWidthInput.setCustomValidity(displayMode() === 'custom' && Number.isFinite(ratio) && (ratio < 1 / 32 || ratio > 32) ? '폭과 높이의 비율은 1:32부터 32:1까지 지정할 수 있습니다.' : '');
});
});
if (webglInput) {
webglInput.addEventListener('change', function () {
var file = webglInput.files && webglInput.files[0];
var generation = ++uploadGeneration;
webglUploadPromise = null;
displayField.dataset.width = '';
displayField.dataset.height = '';
displayHint.textContent = file ? '게임을 업로드하고 화면 크기를 확인하는 중입니다…' : 'ZIP을 선택하면 게임의 화면 크기를 확인합니다.';
webglFileName.textContent = file ? file.name : '선택된 파일 없음';
if (uploadedWebglPath) {
uploadedWebglPath.value = '';
@ -607,6 +687,15 @@
if (uploadedGameUuid) {
uploadedGameUuid.value = '';
}
if (file) {
webglUploadPromise = uploadFile(ctx + '/api/game-files/webgl-zip', file).then(function (data) {
if (generation === uploadGeneration) return acceptWebglResult(data);
throw new Error('선택한 ZIP이 변경되었습니다. 다시 등록해 주세요.');
});
webglUploadPromise.catch(function (error) {
if (generation === uploadGeneration) displayHint.textContent = '업로드하지 못했습니다. ZIP을 다시 선택해 주세요. ' + error.message;
});
}
});
}
if (thumbnailInput) {
@ -646,6 +735,7 @@
form.reportValidity();
return;
}
if (submitBtn.disabled) return;
var webglFile = webglInput && webglInput.files ? webglInput.files[0] : null;
var thumbnailFile = thumbnailInput && thumbnailInput.files ? thumbnailInput.files[0] : null;
@ -655,7 +745,7 @@
}
setSubmitting(true);
(webglFile ? uploadFile(ctx + '/api/game-files/webgl-zip', webglFile) : Promise.resolve({
(webglFile ? (webglUploadPromise || uploadFile(ctx + '/api/game-files/webgl-zip', webglFile).then(acceptWebglResult)) : Promise.resolve({
gameUuid: uploadedGameUuid ? uploadedGameUuid.value : '',
webglPath: uploadedWebglPath ? uploadedWebglPath.value : ''
})).then(function (webglResult) {
@ -708,6 +798,7 @@
}
updatePreview();
updateDisplayMode();
})();
</script>
</body>

View File

@ -0,0 +1,67 @@
.webgl-player__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .5rem;
padding: .5rem;
color: var(--text);
background: var(--card-bg, var(--surface, #1e1e1e));
border-radius: 15px 15px 0 0;
}
.webgl-player__button {
min-width: 44px;
min-height: 44px;
padding: .5rem .875rem;
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
background: var(--card-bg, var(--surface, #1e1e1e));
font: inherit;
font-weight: 700;
cursor: pointer;
}
.webgl-player__button:hover { border-color: var(--accent); }
.webgl-player__button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.webgl-player__status { font-size: .8125rem; color: var(--text-muted); }
.webgl-player .webgl-player__stage {
box-sizing: border-box;
display: grid;
place-items: center;
height: min(70vh, 720px);
height: min(70dvh, 720px);
container-type: size;
border: 0;
border-radius: 0 0 15px 15px;
}
.webgl-player__stage iframe {
display: block;
border: 0;
width: min(100%, calc(min(70vh, 720px) * var(--game-ratio)));
height: auto;
aspect-ratio: var(--game-ratio);
max-width: 100%;
max-height: 100%;
}
@supports (width: 1cqw) {
.webgl-player__stage iframe { width: min(100cqw, calc(100cqh * var(--game-ratio))); }
}
.webgl-player.is-expanded,
.webgl-player:fullscreen {
position: fixed;
inset: 0;
z-index: 10000;
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
height: 100dvh;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
border-radius: 0;
background: var(--webgl-bg, #080808);
}
.webgl-player.is-expanded .webgl-player__stage,
.webgl-player:fullscreen .webgl-player__stage { flex: 1; min-height: 0; height: auto; border-radius: 0; }
.webgl-player.is-expanded .webgl-player__toolbar,
.webgl-player:fullscreen .webgl-player__toolbar { border-radius: 0; flex-shrink: 0; }
.webgl-player::backdrop { background: #080808; }

View File

@ -0,0 +1,96 @@
(function (root, factory) {
var api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
else {
root.BibimbapWebglPlayer = api;
root.document.querySelectorAll('[data-webgl-player]').forEach(function (element) { api.mount(element, root); });
}
})(typeof window === 'undefined' ? globalThis : window, function () {
'use strict';
function contain(width, height, referenceWidth, referenceHeight) {
if (![width, height, referenceWidth, referenceHeight].every(Number.isFinite) || width <= 0 || height <= 0 || referenceWidth <= 0 || referenceHeight <= 0) return { width: 0, height: 0 };
var scale = Math.min(width / referenceWidth, height / referenceHeight);
return { width: referenceWidth * scale, height: referenceHeight * scale };
}
function mount(element, win) {
var doc = win.document;
var stage = element.querySelector('[data-player-stage]');
var frame = stage.querySelector('iframe');
var expand = element.querySelector('[data-player-expand]');
var fullscreen = element.querySelector('[data-player-fullscreen]');
var status = element.querySelector('[data-player-status]');
var referenceWidth = Number(element.dataset.displayWidth);
var referenceHeight = Number(element.dataset.displayHeight);
if (!Number.isInteger(referenceWidth) || !Number.isInteger(referenceHeight) || referenceWidth < 1 || referenceHeight < 1 || referenceWidth > 16384 || referenceHeight > 16384 || referenceWidth / referenceHeight < 1 / 32 || referenceWidth / referenceHeight > 32) { referenceWidth = 16; referenceHeight = 9; }
var expanded = false, captured = false, previousFocus, previousOverflow, pending = false;
function layout() {
pending = false;
var size = contain(stage.clientWidth, stage.clientHeight, referenceWidth, referenceHeight);
if (size.width && size.height) { frame.style.width = size.width + 'px'; frame.style.height = size.height + 'px'; }
}
function schedule() {
if (pending) return;
pending = true;
win.requestAnimationFrame(layout);
}
function sync() {
var isFullscreen = doc.fullscreenElement === element;
var active = expanded || isFullscreen;
if (active && !captured) {
previousFocus = doc.activeElement;
previousOverflow = doc.body.style.overflow;
captured = true;
doc.body.style.overflow = 'hidden';
}
if (!active && captured) {
doc.body.style.overflow = previousOverflow;
captured = false;
if (previousFocus && previousFocus.isConnected && previousFocus.focus) previousFocus.focus();
}
element.classList.toggle('is-expanded', expanded);
expand.textContent = expanded ? '닫기' : '크게 보기';
expand.setAttribute('aria-expanded', String(expanded));
fullscreen.setAttribute('aria-pressed', String(isFullscreen));
fullscreen.textContent = isFullscreen ? '전체화면 종료' : '전체화면';
if (active) { element.setAttribute('role', 'dialog'); element.setAttribute('aria-modal', 'true'); }
else { element.removeAttribute('role'); element.removeAttribute('aria-modal'); status.textContent = ''; }
schedule();
}
function setExpanded(value) { expanded = value; sync(); if (value) expand.focus(); }
function fallback() {
setExpanded(true);
status.textContent = '전체화면을 사용할 수 없어 크게 보기로 전환했습니다.';
}
expand.addEventListener('click', function () {
if (doc.fullscreenElement === element) { expanded = false; Promise.resolve(doc.exitFullscreen()).catch(fallback); }
else setExpanded(!expanded);
});
fullscreen.addEventListener('click', function () {
if (doc.fullscreenElement === element) { Promise.resolve(doc.exitFullscreen()).catch(fallback); return; }
if (!element.requestFullscreen) { fallback(); return; }
try { Promise.resolve(element.requestFullscreen()).then(function () { sync(); expand.focus(); }).catch(fallback); }
catch (_) { fallback(); }
});
doc.addEventListener('fullscreenchange', sync);
doc.addEventListener('keydown', function (event) {
if (!expanded && doc.fullscreenElement !== element) return;
if (event.key === 'Escape') {
expanded = false;
if (doc.fullscreenElement === element) Promise.resolve(doc.exitFullscreen()).catch(fallback);
else sync();
}
// Keep keyboard navigation in the enlarged player, including its game frame.
if (event.key === 'Tab') {
var first = expand, last = frame;
if (event.shiftKey && doc.activeElement === first) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && doc.activeElement === last) { event.preventDefault(); first.focus(); }
}
});
if (win.ResizeObserver) new win.ResizeObserver(schedule).observe(stage);
win.addEventListener('resize', schedule);
if (win.visualViewport) win.visualViewport.addEventListener('resize', schedule);
sync();
return { layout: layout, setExpanded: setExpanded };
}
return { contain: contain, mount: mount };
});

View File

@ -0,0 +1,146 @@
package com.pandoli365.bibimbap.controller.api;
import com.pandoli365.bibimbap.badge.ReputationService;
import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.mapper.*;
import com.pandoli365.bibimbap.service.GameAssetCleanupService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.ui.ExtendedModelMap;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class GameDisplayMetadataTest {
@TempDir Path storage;
GamesMapper mapper;
GameController controller;
MockHttpSession session;
MockHttpServletRequest request;
@BeforeEach void setup() {
mapper = mock(GamesMapper.class);
controller = new GameController(mapper,mock(GameCommentsMapper.class),mock(GameReviewsMapper.class),
mock(GameViewsMapper.class),mock(GameLikesMapper.class),mock(GameAssetCleanupService.class),mock(ReputationService.class));
ReflectionTestUtils.setField(controller,"uploadStoragePath",storage.toString());
session = new MockHttpSession();
session.setAttribute("userId",1L);
session.setAttribute("csrfToken","token");
request = new MockHttpServletRequest();
request.setSession(session);
request.addHeader("X-CSRF-Token","token");
doAnswer(invocation -> { GameData game = invocation.getArgument(0); game.setId(7L); return 1; })
.when(mapper).addGame(any());
}
private GameData existing() {
GameData game = new GameData();
game.setId(7L);game.setUserId(1L);game.setName("test");game.setWebglPath("/game/build/index.html");
when(mapper.getGame(7L)).thenReturn(game);
return game;
}
private void build(int width, int height) throws Exception {
Path directory = Files.createDirectories(storage.resolve("game/build"));
Files.writeString(directory.resolve("index.html"),"<canvas width='"+width+"' height='"+height+"'></canvas>");
}
@Test void autoCreateRedetectsServerFileAndRoundTripsIntoEditAndDetail() throws Exception {
build(1280,720);
request.setParameter("displayMode","auto");request.setParameter("displayWidth","390");
assertThat(controller.createGame("game",null,null,"/game/build/index.html",null,"on",request,session)
.getStatusCode().value()).isEqualTo(200);
var captor = org.mockito.ArgumentCaptor.forClass(GameData.class);
verify(mapper).addGame(captor.capture());
GameData game = captor.getValue();
assertThat(game.getDisplayWidth()).isEqualTo(1280);assertThat(game.getDisplayHeight()).isEqualTo(720);
assertThat(game.getDisplaySource()).isEqualTo("canvas");
when(mapper.getGame(7L)).thenReturn(game);
ExtendedModelMap detail = new ExtendedModelMap();
controller.gameDetail(7L,detail,session);
assertThat(detail).containsEntry("webglDisplayWidth",1280).containsEntry("webglDisplayHeight",720);
ExtendedModelMap edit = new ExtendedModelMap();controller.gameEditView(7L,edit,session);
assertThat(edit).containsEntry("editDisplayWidth",1280).containsEntry("editDisplaySource","canvas");
}
@Test void customAndLegacyEditPreserveAuthorChoiceButAutoReplacesIt() throws Exception {
build(800,800);
GameData game = existing();
request.setParameter("displayMode","custom");request.setParameter("displayWidth","390");request.setParameter("displayHeight","844");
assertThat(controller.updateGame(7L,"game",null,null,game.getWebglPath(),null,"on",request,session).getStatusCode().value()).isEqualTo(200);
assertThat(game.getDisplayWidth()).isEqualTo(390);assertThat(game.getDisplaySource()).isEqualTo("manual");
request.removeParameter("displayMode");
controller.updateGame(7L,"game",null,null,game.getWebglPath(),null,"on",request,session);
assertThat(game.getDisplayWidth()).isEqualTo(390);
request.setParameter("displayMode","auto");
controller.updateGame(7L,"game",null,null,game.getWebglPath(),null,"on",request,session);
assertThat(game.getDisplayWidth()).isEqualTo(800);assertThat(game.getDisplaySource()).isEqualTo("canvas");
}
@Test void invalidCustomAndUnknownModeAre400AndDoNotWrite() {
for (String[] values : new String[][]{{"custom","0","800"},{"custom","1.2","800"},
{"custom","800",""},{"custom","16385","800"},{"custom","1","800"},{"bogus","800","800"}}) {
request.setParameter("displayMode",values[0]);request.setParameter("displayWidth",values[1]);request.setParameter("displayHeight",values[2]);
assertThat(controller.createGame("game",null,null,"/game/build/index.html",null,"on",request,session).getStatusCode().value()).isEqualTo(400);
}
verify(mapper,never()).addGame(any());
}
@Test void legacyCreateDetectsAndPathReplacementRedetectsWhileUnknownRemainsNullable() throws Exception {
build(960,600);
assertThat(controller.createGame("game",null,null,"/game/build/index.html",null,null,request,session)
.getStatusCode().value()).isEqualTo(200);
var captor = org.mockito.ArgumentCaptor.forClass(GameData.class);
verify(mapper).addGame(captor.capture());
assertThat(captor.getValue().getDisplayWidth()).isEqualTo(960);
GameData game = existing();game.setDisplayWidth(390);game.setDisplayHeight(844);game.setDisplaySource("manual");
Path replacement = Files.createDirectories(storage.resolve("game/other"));
Files.writeString(replacement.resolve("index.html"),"<canvas width=800 height=800></canvas>");
controller.updateGame(7L,"game",null,null,"/game/other/index.html",null,null,request,session);
assertThat(game.getDisplayWidth()).isEqualTo(800);assertThat(game.getDisplaySource()).isEqualTo("canvas");
request.setParameter("displayMode","auto");
controller.updateGame(7L,"game",null,null,"/game/missing/index.html",null,null,request,session);
assertThat(game.getDisplayWidth()).isNull();assertThat(game.getDisplayHeight()).isNull();
assertThat(game.getDisplaySource()).isEqualTo("unknown");
}
@Test void explicitCustomCanUseBrowserDefaultDimensionsAndDetailPrefersIt() throws Exception {
build(800,800);
GameData game = existing();
request.setParameter("displayMode","custom");request.setParameter("displayWidth","300");request.setParameter("displayHeight","150");
assertThat(controller.updateGame(7L,"game",null,null,game.getWebglPath(),null,null,request,session)
.getStatusCode().value()).isEqualTo(200);
ExtendedModelMap detail = new ExtendedModelMap();controller.gameDetail(7L,detail,session);
assertThat(detail).containsEntry("webglDisplayWidth",300).containsEntry("webglDisplayHeight",150)
.containsEntry("webglDisplaySource","manual");
}
@Test void unknownAndLegacyFallbackDoNotBackfillAndCannotReadOutsideStorage() throws Exception {
GameData game = existing();
ExtendedModelMap model = new ExtendedModelMap();controller.gameDetail(7L,model,session);
assertThat(model).containsEntry("webglDisplayWidth",16).containsEntry("webglDisplayHeight",9);
verify(mapper,never()).updateGame(any());
Path outside = Files.createDirectories(storage.resolve("outside"));
Files.writeString(outside.resolve("index.html"),"<canvas width=800 height=800></canvas>");
Files.createDirectories(storage.resolve("game"));
Files.createSymbolicLink(storage.resolve("game/build"),outside);
model = new ExtendedModelMap();controller.gameDetail(7L,model,session);
assertThat(model).containsEntry("webglDisplayKnown",false);
}
@Test void csrfAuthenticationAndOwnershipPrecedeMetadataErrors() {
request.setParameter("displayMode","bad");
request.removeHeader("X-CSRF-Token");
assertThat(controller.createGame("game",null,null,"/game/build/index.html",null,null,request,session).getStatusCode().value()).isEqualTo(403);
request.addHeader("X-CSRF-Token","token");session.removeAttribute("userId");
assertThat(controller.createGame("game",null,null,"/game/build/index.html",null,null,request,session).getStatusCode().value()).isEqualTo(401);
session.setAttribute("userId",2L);existing();
assertThat(controller.updateGame(7L,"game",null,null,"/game/build/index.html",null,null,request,session).getStatusCode().value()).isEqualTo(403);
}
}

View File

@ -111,6 +111,36 @@ class GameUploadControllerSecurityTest {
zip.closeEntry();
}
private byte[] displayUnityZip(String html, String manifest) throws IOException {
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ZipOutputStream zip = new ZipOutputStream(bytes)) {
putEntry(zip, "nested/index.html", html);
if (manifest != null) putEntry(zip,"nested/build-info.json",manifest);
putEntry(zip,"nested/Build/x.loader.js","// loader");
putEntry(zip,"nested/Build/x.framework.js","framework");
putEntry(zip,"nested/Build/x.data","data");
putEntry(zip,"nested/Build/x.wasm","wasm");
zip.finish();
return bytes.toByteArray();
}
}
@Test
void uploadDetectsEachBuildAndMalformedMetadataStillSucceeds() throws IOException {
MockHttpSession session = authedSession();
when(gate.isAuthenticated(any())).thenReturn(true);
for (int[] size : new int[][]{{390,844},{1280,720},{800,800}}) {
byte[] archive = displayUnityZip("<canvas id='unity-canvas' width='" + size[0] + "' height='" + size[1] + "'></canvas>", "malformed");
var response = controller.uploadWebglZip(zipFile(archive),null,null,validCsrfRequest(session),session);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).containsEntry("displayWidth",size[0])
.containsEntry("displayHeight",size[1]).containsEntry("displaySource","canvas");
}
var unknown = controller.uploadWebglZip(zipFile(displayUnityZip("<canvas></canvas>","malformed")),
null,null,validCsrfRequest(session),session);
assertThat(unknown.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(unknown.getBody()).containsEntry("displayWidth",null).containsEntry("displaySource","unknown");
}
private MockMultipartFile zipFile(byte[] bytes) {
return new MockMultipartFile("file", "build.zip", "application/zip", bytes);
}
@ -178,6 +208,8 @@ class GameUploadControllerSecurityTest {
assertThat(body).isNotNull();
assertThat(body.get("gameUuid")).isNotNull();
assertThat((String) body.get("webglPath")).startsWith("/game/");
assertThat(body).containsEntry("displayWidth", null).containsEntry("displayHeight", null)
.containsEntry("displaySource", "unknown");
ArgumentCaptor<GameUploadAudit> captor = ArgumentCaptor.forClass(GameUploadAudit.class);
verify(auditMapper).insertAudit(captor.capture());

View File

@ -0,0 +1,86 @@
package com.pandoli365.bibimbap.mapper;
import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.SearchCriteria;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/** Runs only against the disposable database created by verify-webgl-display.sh. */
@EnabledIfEnvironmentVariable(named = "WEBGL_CONTRACT_JDBC_URL", matches = ".+")
class WebglDisplayDatabaseContractTest {
@Test
void actualMapperInsertUpdateAndEveryGameSelectRoundTripDisplayMetadata() throws Exception {
String url = System.getenv("WEBGL_CONTRACT_JDBC_URL");
assertTrue(url.matches("jdbc:postgresql://127\\.0\\.0\\.1:[0-9]+/webgl_contract\\?currentSchema=dev"),
"Contract test accepts only the isolated loopback webgl_contract database");
var datasource = new UnpooledDataSource("org.postgresql.Driver", url, "postgres", "");
var configuration = new Configuration(new Environment("webgl-contract", new JdbcTransactionFactory(), datasource));
configuration.addMapper(GamesMapper.class);
try (var session = new SqlSessionFactoryBuilder().build(configuration).openSession()) {
long userId;
try (var statement = session.getConnection().createStatement();
var result = statement.executeQuery("INSERT INTO users(display_name) VALUES ('WebGL contract author') RETURNING id")) {
assertTrue(result.next());
userId = result.getLong(1);
}
GamesMapper mapper = session.getMapper(GamesMapper.class);
GameData game = new GameData();
game.setUserId(userId);
game.setName("WebGL contract game");
game.setWebglPath("/game/contract/index.html");
game.setVisible(true);
game.setSortOrder(1);
game.setDisplayWidth(390);
game.setDisplayHeight(844);
game.setDisplaySource("canvas");
assertEquals(1, mapper.addGame(game));
assertNotNull(game.getId());
checkAllSelects(mapper, game);
game.setDisplayWidth(1280);
game.setDisplayHeight(720);
game.setDisplaySource("manual");
assertEquals(1, mapper.updateGame(game));
checkAllSelects(mapper, game);
game.setDisplayWidth(null);
game.setDisplayHeight(null);
game.setDisplaySource("unknown");
assertEquals(1, mapper.updateGame(game));
checkAllSelects(mapper, game);
session.rollback();
}
}
private static void checkAllSelects(GamesMapper mapper, GameData expected) {
check(mapper.getGame(expected.getId()), expected);
var criteria = new SearchCriteria();
criteria.setKeyword("WebGL contract game");
criteria.setSort("latest");
List<List<GameData>> results = List.of(
mapper.getVisibleGames(), mapper.searchVisibleGames("WebGL contract game"),
mapper.getGamesByUserId(expected.getUserId()), mapper.searchGamesAdvanced(criteria),
mapper.listVisibleKeyset(null, null, null, 100),
mapper.searchVisibleKeyset("WebGL contract game", null, null, null, 100));
for (List<GameData> result : results) {
GameData actual = result.stream().filter(row -> row.getId().equals(expected.getId())).findFirst().orElseThrow();
check(actual, expected);
}
}
private static void check(GameData actual, GameData expected) {
assertNotNull(actual);
assertEquals(expected.getDisplayWidth(), actual.getDisplayWidth());
assertEquals(expected.getDisplayHeight(), actual.getDisplayHeight());
assertEquals(expected.getDisplaySource(), actual.getDisplaySource());
}
}

View File

@ -0,0 +1,77 @@
package com.pandoli365.bibimbap.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class WebglDisplayMetadataTest {
@TempDir Path root;
private WebglDisplayMetadata detect(String html) throws Exception {
Path index = root.resolve("index.html");
Files.writeString(index, html);
return WebglDisplayMetadata.detect(index);
}
@Test void portraitLandscapeAndSquareUseTheirOwnCanvasSizes() throws Exception {
for (int[] size : new int[][]{{390,844},{1280,720},{800,800}}) {
assertThat(detect("<canvas id='unity-canvas' width='" + size[0] + "' height='" + size[1] + "'></canvas>"))
.isEqualTo(new WebglDisplayMetadata(size[0], size[1], "canvas"));
}
}
@Test void manifestThenPixelCssThenAttributesHavePriority() throws Exception {
String html = "<style>#unity-canvas{width:960px;height:600px}</style><canvas id='unity-canvas' width='1280' height='720'></canvas>";
assertThat(detect(html)).isEqualTo(new WebglDisplayMetadata(960,600,"css"));
Files.writeString(root.resolve("build-info.json"), "{\"width\":\"800\",\"height\":800}");
assertThat(detect(html)).isEqualTo(new WebglDisplayMetadata(800,800,"build-info"));
Files.writeString(root.resolve("build-info.json"), "malformed");
assertThat(detect(html)).isEqualTo(new WebglDisplayMetadata(960,600,"css"));
}
@Test void nestedEntryOnlyReadsItsAdjacentManifest() throws Exception {
Files.writeString(root.resolve("build-info.json"), "{\"width\":900,\"height\":900}");
Path nested = Files.createDirectory(root.resolve("nested"));
Files.writeString(nested.resolve("index.html"), "<canvas width=640 height=480></canvas>");
Files.writeString(nested.resolve("build-info.json"), "{\"width\":1200,\"height\":800}");
assertThat(WebglDisplayMetadata.detect(nested.resolve("index.html")))
.isEqualTo(new WebglDisplayMetadata(1200,800,"build-info"));
}
@Test void dynamicScriptAmbiguityAndBrowserDefaultsDoNotInventDimensions() throws Exception {
for (String html : new String[]{"<body>no metadata</body>","<canvas></canvas>",
"<canvas width=300 height=150></canvas>", "<canvas width=800 height=600></canvas><canvas width=500 height=500></canvas>",
"<canvas></canvas><script>throw new Error('never run');const fake = 'style.width=900px;style.height=500px';</script>",
"<canvas style='width:100%;height:100vh'></canvas>"}) {
assertThat(detect(html)).isEqualTo(WebglDisplayMetadata.unknown());
}
}
@Test void invalidManifestFallsThroughAndBoundsAreEnforced() throws Exception {
for (String json : new String[]{"{\"width\":1.2,\"height\":800}","{\"width\":20000,\"height\":800}",
"{\"width\":800}","{\"width\":1,\"height\":16384}","{\"width\":-1,\"height\":800}"}) {
Files.writeString(root.resolve("build-info.json"),json);
assertThat(detect("<canvas width=640 height=480></canvas>"))
.isEqualTo(new WebglDisplayMetadata(640,480,"canvas"));
}
assertThat(WebglDisplayMetadata.valid(16384,512)).isTrue();
assertThat(WebglDisplayMetadata.valid(16384,511)).isFalse();
}
@Test void oversizeHtmlAndManifestAndSymlinksAreBounded() throws Exception {
assertThat(detect(" ".repeat(1024 * 1024 + 1))).isEqualTo(WebglDisplayMetadata.unknown());
Files.writeString(root.resolve("build-info.json")," ".repeat(64 * 1024 + 1));
assertThat(detect("<canvas width=800 height=600></canvas>"))
.isEqualTo(new WebglDisplayMetadata(800,600,"canvas"));
Files.delete(root.resolve("build-info.json"));
Path other = root.resolve("private.json");
Files.writeString(other,"{\"width\":900,\"height\":900}");
Files.createSymbolicLink(root.resolve("build-info.json"),other);
assertThat(detect("<canvas width=800 height=600></canvas>"))
.isEqualTo(new WebglDisplayMetadata(800,600,"canvas"));
Files.createSymbolicLink(root.resolve("alias.html"),root.resolve("index.html"));
assertThat(WebglDisplayMetadata.detect(root.resolve("alias.html"))).isEqualTo(WebglDisplayMetadata.unknown());
}
}

View File

@ -0,0 +1,194 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const { contain, mount } = require('../../main/webapp/js/webgl-player.js');
const viewports = [[320, 844], [1440, 900], [844, 390], [640, 300]];
const builds = [[390, 844], [1280, 720], [800, 800]];
test('every uploaded build fits both axes in every normal and expanded viewport', () => {
for (const [vw, vh] of viewports) for (const [rw, rh] of builds) {
for (const available of [[vw - 32, Math.min(vh * .7, 720)], [vw, vh - 64]]) {
const fitted = contain(...available, rw, rh);
assert.ok(fitted.width <= available[0] + 1e-8);
assert.ok(fitted.height <= available[1] + 1e-8);
assert.ok(Math.abs(fitted.width - fitted.height * rw / rh) < 1);
assert.ok(Math.abs(fitted.width - available[0]) < 1 || Math.abs(fitted.height - available[1]) < 1);
}
}
});
test('portrait iframe uses build aspect rather than the former fixed landscape rectangle', () => {
const fitted = contain(1000, 630, 390, 844);
assert.equal(fitted.height, 630);
assert.ok(fitted.width < 300);
assert.ok(Math.abs(fitted.width / fitted.height - 390 / 844) < 1e-8);
// Baseline width 1000, height min(1000 * 9/16, 520) violates this ratio.
assert.ok(Math.abs(1000 / 520 - 390 / 844) > 1);
});
test('SSR player ships the stored ratio and responsive stage without the old hard limit', () => {
const jsp = readFileSync(path.join(__dirname, '../../main/webapp/WEB-INF/views/game-detail.jsp'), 'utf8');
const css = readFileSync(path.join(__dirname, '../../main/webapp/css/webgl-player.css'), 'utf8');
assert.match(jsp, /data-display-width="<%= displayWidth %>"/);
assert.match(jsp, /data-display-height="<%= displayHeight %>"/);
assert.doesNotMatch(jsp, /max-height:\s*min\(70vh,\s*520px\)/);
assert.match(css, /aspect-ratio:\s*var\(--game-ratio\)/);
assert.match(css, /height:\s*min\(70dvh,\s*720px\)/);
});
function harness(width = 390, height = 844) {
let doc;
function node() {
const events = {}, attrs = {};
return { events, attrs, style: {}, isConnected: true, textContent: '',
addEventListener(name, callback) { (events[name] ||= []).push(callback); },
dispatch(name, event = {}) { (events[name] || []).forEach(callback => callback(event)); },
setAttribute(key, value) { attrs[key] = value; }, removeAttribute(key) { delete attrs[key]; },
focus() { doc.activeElement = this; }
};
}
doc = node(); doc.body = node(); doc.body.style.overflow = 'scroll';
const prior = node(); doc.activeElement = prior;
const iframe = node(); iframe.src = 'https://assets.example/game/index.html'; iframe.gameState = { score: 123 }; iframe.loadCount = 1;
// Parent player must work across origins: reading game/canvas DOM is forbidden.
Object.defineProperty(iframe, 'contentDocument', { get() { throw new Error('Cross origin access'); } });
Object.defineProperty(iframe, 'contentWindow', { get() { throw new Error('Cross origin access'); } });
const stage = node(); stage.clientWidth = 1000; stage.clientHeight = 630;
stage.querySelector = selector => selector === 'iframe' ? iframe : null;
const expand = node(), fullscreen = node(), status = node(), element = node();
element.dataset = { displayWidth: String(width), displayHeight: String(height) };
const classes = new Set(); element.classList = { toggle(key, value) { if (value) classes.add(key); else classes.delete(key); }, contains(key) { return classes.has(key); } };
const lookup = { '[data-player-stage]': stage, '[data-player-expand]': expand, '[data-player-fullscreen]': fullscreen, '[data-player-status]': status };
element.querySelector = selector => lookup[selector];
const win = node(); win.document = doc; win.visualViewport = node();
const raf = []; win.requestAnimationFrame = callback => raf.push(callback);
let observer;
win.ResizeObserver = class { constructor(callback) { observer = callback; } observe(target) { assert.equal(target, stage); } };
const flush = () => { while (raf.length) raf.shift()(); };
doc.exitFullscreen = () => { doc.fullscreenElement = null; doc.dispatch('fullscreenchange'); return Promise.resolve(); };
const controller = mount(element, win); flush();
return { doc, prior, iframe, stage, expand, fullscreen, status, element, win, flush, controller, resize: () => observer() };
}
test('resize and overlay preserve same iframe, game state and source; close restores scroll/focus', () => {
const h = harness(), frame = h.iframe, state = frame.gameState, source = frame.src;
assert.equal(parseFloat(frame.style.height), 630);
h.expand.dispatch('click'); h.flush();
assert.equal(h.doc.body.style.overflow, 'hidden');
assert.equal(h.element.attrs['aria-modal'], 'true');
assert.equal(h.expand.attrs['aria-expanded'], 'true');
h.stage.clientWidth = 300; h.stage.clientHeight = 240;
h.resize(); h.win.dispatch('resize'); h.win.visualViewport.dispatch('resize'); h.flush();
assert.ok(parseFloat(frame.style.width) < 112);
h.doc.dispatch('keydown', { key: 'Escape' }); h.flush();
assert.equal(h.doc.activeElement, h.prior);
assert.equal(h.doc.body.style.overflow, 'scroll');
assert.equal(h.element.attrs['aria-modal'], undefined);
assert.equal(h.expand.attrs['aria-expanded'], 'false');
assert.equal(h.stage.querySelector('iframe'), frame);
assert.equal(frame.src, source); assert.equal(frame.loadCount, 1); assert.equal(frame.gameState, state);
h.expand.dispatch('click'); h.expand.dispatch('click'); h.flush();
assert.equal(h.doc.body.style.overflow, 'scroll');
});
test('successful fullscreen enters and exits without reload and restores focus', async () => {
const h = harness(1280, 720), frame = h.iframe;
h.element.requestFullscreen = () => { h.doc.fullscreenElement = h.element; h.doc.dispatch('fullscreenchange'); return Promise.resolve(); };
h.fullscreen.dispatch('click'); await new Promise(resolve => setImmediate(resolve)); h.flush();
assert.equal(h.fullscreen.attrs['aria-pressed'], 'true');
assert.equal(h.doc.body.style.overflow, 'hidden');
h.fullscreen.dispatch('click'); await new Promise(resolve => setImmediate(resolve)); h.flush();
assert.equal(h.fullscreen.attrs['aria-pressed'], 'false');
assert.equal(h.doc.body.style.overflow, 'scroll'); assert.equal(h.doc.activeElement, h.prior);
assert.equal(h.stage.querySelector('iframe'), frame); assert.equal(frame.loadCount, 1);
});
test('unsupported and rejected fullscreen use dismissible overlay', async () => {
for (const rejection of [false, true]) {
const h = harness(800, 800);
if (rejection) h.element.requestFullscreen = () => Promise.reject(new Error('Denied'));
h.fullscreen.dispatch('click'); await new Promise(resolve => setImmediate(resolve)); h.flush();
assert.equal(h.element.classList.contains('is-expanded'), true);
assert.equal(h.fullscreen.attrs['aria-pressed'], 'false');
assert.match(h.status.textContent, /크게 보기/);
h.expand.dispatch('click'); h.flush();
assert.equal(h.element.classList.contains('is-expanded'), false);
assert.equal(h.doc.body.style.overflow, 'scroll'); assert.equal(h.doc.activeElement, h.prior);
assert.equal(h.iframe.loadCount, 1);
}
});
test('native fullscreen exit from overlay returns to overlay; Tab remains within player', async () => {
const h = harness(); h.expand.dispatch('click'); h.flush();
h.element.requestFullscreen = () => { h.doc.fullscreenElement = h.element; h.doc.dispatch('fullscreenchange'); return Promise.resolve(); };
h.fullscreen.dispatch('click'); await new Promise(resolve => setImmediate(resolve));
await h.doc.exitFullscreen(); h.flush();
assert.equal(h.expand.attrs['aria-expanded'], 'true'); assert.equal(h.doc.body.style.overflow, 'hidden');
h.expand.focus(); let prevented = false;
h.doc.dispatch('keydown', { key: 'Tab', shiftKey: true, preventDefault() { prevented = true; } });
assert.equal(prevented, true); assert.equal(h.doc.activeElement, h.iframe);
h.expand.dispatch('click'); h.flush(); assert.equal(h.doc.activeElement, h.prior);
});
test('invalid reference metadata retains safe fallback rather than overflowing', () => {
const h = harness(-5, 0);
assert.ok(Math.abs(parseFloat(h.iframe.style.width) / parseFloat(h.iframe.style.height) - 16 / 9) < 1e-8);
assert.deepEqual(contain(0, 1, 16, 9), { width: 0, height: 0 });
});
function registration() {
const events = {}, nodes = {};
function node(id) {
return nodes[id] ||= { value: '', textContent: '', dataset: {}, style: {}, files: [],
addEventListener(name, fn) { (events[id + ':' + name] ||= []).push(fn); },
dispatch(name, event = {}) { (events[id + ':' + name] || []).forEach(fn => fn(event)); },
setCustomValidity(message) { this.validationMessage = message; }, removeAttribute() {} };
}
const form = node('game-register-form'); form.action = '/game/new';
form.getAttribute = () => 'false'; form.checkValidity = () => true;
const auto = node('auto'), custom = node('custom'); auto.value = 'auto'; custom.value = 'custom';
let selected = auto;
form.querySelector = selector => selector.includes('displayMode') ? selected : null;
form.querySelectorAll = () => [auto, custom];
const fetched = [], responses = [{ displayWidth: 1280, displayHeight: 720, displaySource: 'canvas' }, { displayWidth: 800, displayHeight: 800, displaySource: 'css' }, { displayWidth: null, displayHeight: null, displaySource: 'unknown' }];
let uploadIndex = 0;
const context = { document: { getElementById: node }, window: { BibimbapModal: { alert() {} } },
FormData: class { append() {} }, URLSearchParams, URL, Number, Promise, Error, Object,
fetch(url, options) {
fetched.push({ url, body: options.body });
const data = url.includes('webgl-zip') ? { gameUuid: 'uuid-' + uploadIndex, webglPath: '/uploads/' + uploadIndex++ + '/index.html', ...responses.shift() } : { gameId: 7 };
return Promise.resolve({ ok: true, json: () => Promise.resolve(data) });
}
};
node('game-name').value = 'uploaded game';
const jsp = readFileSync(path.join(__dirname, '../../main/webapp/WEB-INF/views/game-register.jsp'), 'utf8');
const script = jsp.match(/<script>\s*([\s\S]*?)<\/script>/)[1].replace(/<%= ctx %>/g, '');
vm.runInNewContext(script, context);
return { node, form, fetched, select(mode) { selected = mode === 'custom' ? custom : auto; selected.dispatch('change'); },
choose(name) { node('game-webgl-zip').files = [{ name }]; node('game-webgl-zip').dispatch('change'); },
flush: () => new Promise(resolve => setImmediate(resolve)) };
}
test('ZIP selection discovers each build before save; automatic metadata resets on replacement', async () => {
const r = registration(); r.choose('landscape.zip'); await r.flush();
assert.equal(r.node('game-display').dataset.width, 1280);
assert.match(r.node('game-display-hint').textContent, /1280 × 720/);
r.choose('square.zip');
assert.equal(r.node('game-display').dataset.width, '');
await r.flush(); assert.equal(r.node('game-display').dataset.width, 800);
r.choose('unknown.zip'); await r.flush();
assert.equal(r.node('game-display').dataset.width, '');
assert.match(r.node('game-display-hint').textContent, /直接|직접 지정/);
});
test('custom dimensions survive new ZIP and final save chains server upload path', async () => {
const r = registration(); r.select('custom');
r.node('game-display-width').value = '960'; r.node('game-display-height').value = '600';
r.choose('another-build.zip'); await r.flush();
assert.equal(r.node('game-display-width').value, '960'); assert.equal(r.node('game-display-height').value, '600');
r.form.dispatch('submit', { preventDefault() {} }); await r.flush();
assert.equal(r.fetched.filter(call => call.url.includes('webgl-zip')).length, 1);
const saved = r.fetched.find(call => call.url === '/game/new').body;
assert.equal(saved.get('displayMode'), 'custom'); assert.equal(saved.get('displayWidth'), '960');
assert.equal(saved.get('displayHeight'), '600'); assert.equal(saved.get('webglPath'), '/uploads/0/index.html');
assert.equal(saved.get('gameUuid'), 'uuid-0');
});
test('automatic save sends mode and upload path and lets server resolve dimensions', async () => {
const r = registration(); r.choose('build.zip'); await r.flush();
r.form.dispatch('submit', { preventDefault() {} }); await r.flush();
const saved = r.fetched.find(call => call.url === '/game/new').body;
assert.equal(saved.get('displayMode'), 'auto'); assert.equal(saved.has('displayWidth'), false);
assert.equal(saved.has('displayHeight'), false); assert.equal(saved.get('webglPath'), '/uploads/0/index.html');
});