From a0d01aa41ea228f77677d3bf45043bd1012ea697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Wed, 28 Oct 2026 09:51:24 +0900 Subject: [PATCH 1/7] feat: add review highlight controls (#6) --- web/index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/index.html b/web/index.html index ed57ed7..a775d24 100644 --- a/web/index.html +++ b/web/index.html @@ -26,6 +26,12 @@ +
+ + +

빨간색 1초 · 원래 색 1초 반복
오류 확정이 아닌 점검 필요 표시입니다.

+ +

높이와 형상

×1×10
×1×8
From 3a1069f08ea43316a36dae3e90b3f7710a9eddff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Wed, 28 Oct 2026 09:51:25 +0900 Subject: [PATCH 2/7] feat: add review highlight controls (#6) --- web/app.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/web/app.js b/web/app.js index d7d49aa..b8174e3 100644 --- a/web/app.js +++ b/web/app.js @@ -35,6 +35,8 @@ function drawProfile(f){ export function selectFacility(f){ if(!f)return;app.selected=f;app.renderer.set({selected:f.pickId});$('#view-name').textContent=f.id; let html=`${labels[f.type]}

${esc(f.id)}

`; + const reviewReasons=app.network.issues.filter(i=>['length_mismatch','adverse_slope','missing_coordinate','unplaced_link','coincident_endpoints'].includes(i.code)&&i.ids.includes(f.id)); + if(reviewReasons.length)html+=section('점검 필요 · 오류 확정 아님',`
    ${reviewReasons.map(i=>`
  • ${esc(i.message)}
  • `).join('')}
`); const placed=isPlaced(f),hidden=!app.renderer.state.layers[layerOf(f)]; html+=``; if(!placed)html+=`

${f.xy===null?'좌표가 없어 위치를 배치하지 않았습니다.':f.id==='UHR1-01411'?'양 끝의 평면 좌표가 같아 관로 형상을 생략했습니다.':'연결 노드 좌표 또는 단면이 없어 형상을 배치하지 않았습니다.'}

`; @@ -85,7 +87,15 @@ function updateScales(){ const height=Number($('#height-scale').value),size=Number($('#size-scale').value);app.renderer.set({height,size,section:$('#section-mode').checked}); $('#height-output').value=`×${height}`;$('#size-output').value=`×${size}`;$('#height-badge').textContent=`높이 ×${height}`;$('#size-badge').textContent=`단면·맨홀 폭 ×${size}`;$('#section-badge').hidden=!$('#section-mode').checked; } +function syncAuditHighlight(){ + const enabled=$('#audit-highlight').checked,reduced=matchMedia('(prefers-reduced-motion: reduce)').matches; + const steady=$('#audit-steady').checked||reduced; + $('#audit-steady').disabled=!enabled||reduced; + $('#audit-highlight-note').textContent=reduced?'동작 줄이기 설정에 따라 고정 강조합니다. 오류 확정이 아닌 점검 필요 표시입니다.':steady?'고정 빨간색으로 강조합니다. 오류 확정이 아닌 점검 필요 표시입니다.':'빨간색 1초 · 원래 색 1초 반복. 오류 확정이 아닌 점검 필요 표시입니다.'; + app.renderer.setAuditHighlight(enabled,steady); +} function reset(){ + $('#audit-highlight').checked=false;$('#audit-steady').checked=false;syncAuditHighlight(); $('#height-scale').value=3;$('#size-scale').value=3;$('#section-mode').checked=false;$$('[data-layer]').forEach(b=>{b.checked=true;app.renderer.setLayer(b.dataset.layer,true);});updateScales();app.renderer.set({selected:0});app.selected=null;$('#search').value='';updateSearch();clearDetails();$('#view-name').textContent='전체 관망';app.renderer.setView('iso');app.renderer.fit();toast('표시·배율·시점을 초기화했습니다.'); } function frame(renderer,error){ @@ -109,6 +119,12 @@ async function main(){ app.network=await response.json();const n=app.network,s=n.summary; app.facilities=[...n.nodes,...n.links].map((f,i)=>({...f,pickId:i+1})); app.renderer=new NetworkRenderer($('#scene'),n,app.facilities,selectFacility,frame); + const unplaced=app.facilities.filter(f=>!isPlaced(f)); + $('#audit-highlight-count').textContent=`표시 가능한 점검 관로 ${app.renderer.auditPickIds.size}개`; + $('#audit-unplaced').textContent=`위치 표시 불가 ${unplaced.length}개 · 사유 보기 ↗`; + $('#audit-unplaced').onclick=()=>openAudit('placement'); + $('#audit-highlight').onchange=syncAuditHighlight;$('#audit-steady').onchange=syncAuditHighlight; + matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change',syncAuditHighlight);syncAuditHighlight(); $('#node-count').textContent=s.counts.junction;$('#pipe-count').textContent=s.counts.conduit; for(const [layer,count] of Object.entries({junction:s.counts.junction,conduit:s.counts.conduit,outfall:s.counts.outfall,other:s.counts.storage+s.counts.pump+s.counts.weir}))$(`#layer-${layer}-count`).textContent=count; $('#circle-count').textContent=s.shapes.CIRCULAR;$('#rect-count').textContent=s.shapes.RECT_CLOSED;$('#source-name').textContent=n.meta.sourceName; From 3ed6b9def2a2c1abab8ecc70f5ea39dd20ccd146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Wed, 28 Oct 2026 09:51:25 +0900 Subject: [PATCH 3/7] feat: add review highlight controls (#6) --- web/renderer.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/web/renderer.js b/web/renderer.js index 932f7c5..1250943 100644 --- a/web/renderer.js +++ b/web/renderer.js @@ -1,14 +1,14 @@ import {vec,multiply,perspective,lookAt,project} from './math.js'; import {buildGeometry,layerOf,physicalPoint,conduitPath} from './geometry.js'; const vertex=`precision highp float; -attribute vec3 aBase;attribute vec3 aOffset;attribute vec3 aNormal;attribute vec3 aColor;attribute float aId;attribute float aKind;attribute float aSlice; +attribute vec3 aBase;attribute vec3 aOffset;attribute vec3 aNormal;attribute vec3 aColor;attribute float aId;attribute float aKind;attribute float aSlice;attribute float aAudit; uniform mat4 uMatrix;uniform float uHeight;uniform float uSize;uniform float uDpr;uniform float uPointSize; -varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset; +varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset;varying float vAudit; void main(){vec3 p=aBase+aOffset*uSize;p.z*=uHeight;gl_Position=uMatrix*vec4(p,1.0);gl_PointSize=uPointSize*uDpr; -vNormal=normalize(aNormal/vec3(1.0,1.0,uHeight));vColor=aColor;vId=aId;vKind=aKind;vSlice=aSlice;vOffset=aOffset;}`; +vNormal=normalize(aNormal/vec3(1.0,1.0,uHeight));vColor=aColor;vId=aId;vKind=aKind;vSlice=aSlice;vOffset=aOffset;vAudit=aAudit;}`; const fragment=`precision highp float; -uniform vec3 uGlowColor;uniform float uGlow;uniform float uSelected;uniform float uPicking;uniform float uPoints;uniform float uSection;uniform vec2 uCameraDir; -varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset; +uniform vec3 uGlowColor;uniform float uGlow;uniform float uSelected;uniform float uPicking;uniform float uPoints;uniform float uSection;uniform vec2 uCameraDir;uniform float uAuditPulse; +varying vec3 vNormal;varying vec3 vColor;varying float vId;varying float vKind;varying float vSlice;varying vec3 vOffset;varying float vAudit; void main(){ if(uPoints>.5&&length(gl_PointCoord-vec2(.5))>.5)discard; if(uSection>.5){if(vKind>.5&&vKind<1.5&&dot(vOffset.xy,uCameraDir)>0.0)discard;if(vKind>1.5&&vKind<2.5&&vSlice>.03)discard;} @@ -17,6 +17,7 @@ void main(){ vec3 color=vColor;if(vKind<3.5&&uPoints<.5)color*=light; if(abs(vId-uSelected)<.1&&uSelected>.5)color=mix(vec3(.97,.41,.14),vec3(1.0,.78,.3),.32*max(n.z,0.0)); if(uGlow>0.0&&abs(vId-uSelected)>.1)color=mix(color,min(vec3(1.0),uGlowColor*1.25+vec3(.08)),uGlow); + if(uAuditPulse>.5&&vAudit>.5&&abs(vId-uSelected)>.1)color=vec3(1.0,.10,.22); gl_FragColor=vec4(color,1.0); }`; export class NetworkRenderer { @@ -29,14 +30,30 @@ export class NetworkRenderer { const gl=this.gl; const shader=(type,source)=>{const s=gl.createShader(type);gl.shaderSource(s,source);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(s));return s;}; const p=gl.createProgram();gl.attachShader(p,shader(gl.VERTEX_SHADER,vertex));gl.attachShader(p,shader(gl.FRAGMENT_SHADER,fragment));gl.linkProgram(p);if(!gl.getProgramParameter(p,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(p));this.program=p; - this.attributes=Object.fromEntries(['Base','Offset','Normal','Color','Id','Kind','Slice'].map(n=>[n,gl.getAttribLocation(p,'a'+n)])); - this.uniforms=Object.fromEntries(['Matrix','Height','Size','Dpr','PointSize','Selected','Picking','Points','Section','CameraDir','Glow','GlowColor'].map(n=>[n,gl.getUniformLocation(p,'u'+n)])); + this.attributes=Object.fromEntries(['Base','Offset','Normal','Color','Id','Kind','Slice','Audit'].map(n=>[n,gl.getAttribLocation(p,'a'+n)])); + this.uniforms=Object.fromEntries(['Matrix','Height','Size','Dpr','PointSize','Selected','Picking','Points','Section','CameraDir','Glow','GlowColor','AuditPulse'].map(n=>[n,gl.getUniformLocation(p,'u'+n)])); + const reviewIds=new Set(network.issues.filter(i=>['length_mismatch','adverse_slope'].includes(i.code)).flatMap(i=>i.ids)); + this.auditPickIds=new Set(facilities.filter(f=>f.type==='conduit'&&f.renderable&&reviewIds.has(f.id)).map(f=>f.pickId)); + this.auditEnabled=false;this.auditSteady=false;this.auditPhase=0;this.auditTimer=null; this.batches={};const geometry=buildGeometry(network,facilities); for(const [layer,groups] of Object.entries(geometry)){this.batches[layer]={};for(const [kind,batch] of Object.entries(groups)){ - const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(batch.data),gl.STATIC_DRAW);this.batches[layer][kind]={buffer,count:batch.count}; + const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(batch.data),gl.STATIC_DRAW); + const auditBuffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,auditBuffer);gl.bufferData(gl.ARRAY_BUFFER,Float32Array.from({length:batch.count},(_,i)=>this.auditPickIds.has(batch.data[i*15+12])?1:0),gl.STATIC_DRAW); + this.batches[layer][kind]={buffer,auditBuffer,count:batch.count}; }} this.picking={fbo:gl.createFramebuffer(),texture:gl.createTexture(),depth:gl.createRenderbuffer()}; this.interactions();this.observer=new ResizeObserver(()=>this.resize());this.observer.observe(canvas);this.resize();this.fit(); + document.addEventListener('visibilitychange',()=>this.setAuditHighlight(this.auditEnabled,this.auditSteady)); + } + setAuditHighlight(enabled,steady=false){ + clearTimeout(this.auditTimer);this.auditTimer=null; + this.auditEnabled=enabled;this.auditSteady=steady;this.auditPhase=enabled?1:0; + if(enabled&&!steady&&!document.hidden){ + const started=performance.now(); + const tick=()=>{const elapsed=performance.now()-started;this.auditPhase=Math.floor(elapsed/1000)%2===0?1:0;this.request();this.auditTimer=setTimeout(tick,1000-elapsed%1000);}; + this.auditTimer=setTimeout(tick,1000); + } + this.request(); } visible(f){return this.state.layers[layerOf(f)]&&(f.xy!=null||f.renderable);} set(patch){const oldHeight=this.state.height;Object.assign(this.state,patch);if(patch.height||patch.size){if(this.autoFit)this.fit();else if(patch.height)this.camera.target[2]*=patch.height/oldHeight;}this.request();} @@ -85,6 +102,7 @@ export class NetworkRenderer { this.matrix=multiply(perspective(this.width/this.height,Math.max(.03,c.distance/15000),Math.max(30000,c.distance*5)),lookAt(eye,c.target)); gl.bindFramebuffer(gl.FRAMEBUFFER,picking?this.picking.fbo:null);gl.viewport(0,0,this.canvas.width,this.canvas.height);gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);gl.disable(gl.BLEND);gl.disable(gl.DITHER);gl.disable(gl.CULL_FACE);gl.useProgram(this.program); gl.uniformMatrix4fv(u.Matrix,false,this.matrix);gl.uniform1f(u.Height,this.state.height);gl.uniform1f(u.Size,this.state.size);gl.uniform1f(u.Dpr,this.dpr);gl.uniform1f(u.PointSize,picking?9:5);gl.uniform1f(u.Selected,this.state.selected);gl.uniform1f(u.Picking,picking?1:0);gl.uniform1f(u.Section,this.state.section?1:0);gl.uniform2f(u.CameraDir,Math.cos(c.azimuth),Math.sin(c.azimuth)); + gl.uniform1f(u.AuditPulse,picking?0:this.auditPhase); for(const [layer,groups] of Object.entries(this.batches)){ if(layer==='grid'&&picking)continue;if(layer!=='grid'&&!this.state.layers[layer])continue; gl.uniform1f(u.Glow,picking?0:(this.glowForLayer?.(layer)||0)); @@ -92,6 +110,7 @@ export class NetworkRenderer { for(const [kind,b] of Object.entries(groups)){ if(!b.count)continue;gl.bindBuffer(gl.ARRAY_BUFFER,b.buffer); let offset=0;for(const [name,size] of [['Base',3],['Offset',3],['Normal',3],['Color',3],['Id',1],['Kind',1],['Slice',1]]){const a=this.attributes[name];gl.enableVertexAttribArray(a);gl.vertexAttribPointer(a,size,gl.FLOAT,false,60,offset*4);offset+=size;} + gl.bindBuffer(gl.ARRAY_BUFFER,b.auditBuffer);gl.enableVertexAttribArray(this.attributes.Audit);gl.vertexAttribPointer(this.attributes.Audit,1,gl.FLOAT,false,4,0); gl.uniform1f(u.Points,kind==='points'?1:0);gl.drawArrays(kind==='mesh'?gl.TRIANGLES:kind==='lines'?gl.LINES:gl.POINTS,0,b.count); } } From 49e8ccbcfff6a5f31a18732387fef4a4ce857c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Wed, 28 Oct 2026 09:51:26 +0900 Subject: [PATCH 4/7] feat: add review highlight controls (#6) --- web/styles.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/styles.css b/web/styles.css index 0177867..f056b67 100644 --- a/web/styles.css +++ b/web/styles.css @@ -12,3 +12,11 @@ .legend-panel{background:#18282be6;border-color:#3b514b}.legend-title{color:#c1d6c9}.legend-panel>div{color:#a9c5b6}.legend-panel>div span{color:#91a79b}.legend-panel small{color:#8aa296}.line-key.circular{background:#64bdb9}.line-key.rectangular{background:#d7b47a} .axes>span{color:#839d92}.view-toolbar{background:#152126ed;border-color:#3a4a4d;box-shadow:0 8px 30px #0003}.view-toolbar button{color:#a4bcb0}.view-toolbar button:hover{background:#2d403e}.view-toolbar button[aria-pressed=true]{background:#a7e3cb;color:#1b3a2c}.view-toolbar i{background:#354446} .viewer-footer>span{color:#8aa69b}.viewer .text-button{color:#a5b8b0}.viewer .text-button:hover{color:#a7e3cb}.datum-note{color:#839d92} +.audit-highlight-controls .toggle-label{align-items:center;gap:12px} +.audit-highlight-controls .toggle-label small{display:block;margin-top:5px} +.audit-highlight-controls .audit-steady-label{display:flex;align-items:center;gap:7px;font-size:12px;margin:12px 0;color:#52695d} +.audit-highlight-controls input{accent-color:#e83650} +.audit-highlight-controls .fine-print{line-height:1.65} +.audit-highlight-controls .text-button{font-size:12px;text-align:left;white-space:normal} +.review-reasons{padding-left:18px;color:#a6384b;font-size:12px;line-height:1.7} +.review-reasons li+li{margin-top:6px} From 324e5e0c77510b38916276003364bd3219c1ee94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Wed, 28 Oct 2026 09:51:27 +0900 Subject: [PATCH 5/7] feat: add review highlight controls (#6) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f8c9c1..29e164f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hwacheon-network-viewer-validation", "private": true, - "scripts": { "test:ui": "node tests/viewer.cjs", "test:runner": "node tests/launch-failure.cjs" }, + "scripts": { "test:ui": "node tests/viewer.cjs", "test:runner": "node tests/launch-failure.cjs", "test:audit": "node tests/audit-highlight.cjs" }, "devDependencies": { "playwright": "1.62.1" } } From 9332b1a95c9289d1e14917ed92e0cbd41c5467f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Wed, 28 Oct 2026 09:51:27 +0900 Subject: [PATCH 6/7] test: verify review highlight controls (#6) --- tests/audit-highlight.cjs | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/audit-highlight.cjs diff --git a/tests/audit-highlight.cjs b/tests/audit-highlight.cjs new file mode 100644 index 0000000..9b98d3f --- /dev/null +++ b/tests/audit-highlight.cjs @@ -0,0 +1,60 @@ +const {chromium}=require('playwright'); +const assert=require('node:assert/strict'); +const fs=require('node:fs/promises'); +const path=require('node:path'); +const out=path.resolve(__dirname,'../test-results'); +(async()=>{ + const browser=await chromium.launch({headless:true,...(process.env.PLAYWRIGHT_CHANNEL?{channel:process.env.PLAYWRIGHT_CHANNEL}:{})}); + const page=await browser.newPage({viewport:{width:1440,height:1000}}); + const errors=[];page.on('pageerror',e=>errors.push(e.message)); + const report={status:'failed'}; + await fs.mkdir(out,{recursive:true}); + const state=()=>page.evaluate(async()=>{ + const {app}=await import('./app.js');const r=app.renderer; + r.draw();const gl=r.gl,b=new Uint8Array(r.canvas.width*r.canvas.height*4); + gl.readPixels(0,0,r.canvas.width,r.canvas.height,gl.RGBA,gl.UNSIGNED_BYTE,b); + let red=0,hash=0;for(let i=0;i220&&b[i+1]<80&&b[i+2]<120)red++;hash=(hash+b[i]*3+b[i+1]*5+b[i+2]*7)>>>0;} + return {phase:r.auditPhase,enabled:r.auditEnabled,timer:r.auditTimer!==null,red,hash, + targetCount:r.auditPickIds.size,targets:r.facilities.filter(f=>r.auditPickIds.has(f.pickId)).map(f=>f.id)}; + }); + try{ + await page.clock.install({time:new Date('2026-10-28T00:00:00Z')}); + await page.goto(process.env.VIEWER_URL||'http://127.0.0.1:8081/',{waitUntil:'networkidle'}); + await page.waitForFunction(()=>document.querySelector('#loading').hidden); + await page.clock.pauseAt(new Date('2026-10-28T00:01:00Z')); + const before=await state();assert.equal(before.enabled,false); + const data=await (await page.request.get(new URL('data/network.json',page.url()).href)).json(); + const ids=new Set(data.issues.filter(i=>['length_mismatch','adverse_slope'].includes(i.code)).flatMap(i=>i.ids)); + const expected=data.links.filter(l=>l.type==='conduit'&&l.renderable&&ids.has(l.id)).map(l=>l.id).sort(); + assert.deepEqual(before.targets.sort(),expected); + await page.locator('#audit-highlight').check({force:true}); + const on=await state();assert.equal(on.phase,1);assert.ok(on.red>before.red+20); + await page.screenshot({path:path.join(out,'audit-red.png'),fullPage:true}); + await page.clock.runFor(1000); + const normal=await state();assert.equal(normal.red,before.red);assert.equal(normal.hash,before.hash); + await page.clock.runFor(1000);assert.equal((await state()).phase,1); + await page.locator('#audit-highlight').uncheck({force:true});const stopped=await state(); + assert.equal(stopped.timer,false);assert.equal(stopped.hash,before.hash); + await page.locator('#audit-highlight').check({force:true});await page.locator('#audit-steady').check({force:true}); + const fixed=await state();assert.equal(fixed.timer,false);assert.equal(fixed.phase,1); + await page.clock.runFor(1150);assert.equal((await state()).phase,1); + await page.locator('#show-conduit').uncheck({force:true});assert.equal((await state()).red,0);await page.locator('#show-conduit').check({force:true}); + const target=expected[0];await page.locator('#search').fill(target);await page.locator('#search-results button').first().click({force:true}); + assert.ok((await page.locator('#details').textContent()).includes('점검 필요 · 오류 확정 아님')); + await page.locator('#audit-unplaced').click({force:true}); + for(const id of ['OUT_PUMP','C_OUT_PUMP','UHR1-01411'])assert.ok((await page.locator('#audit-list').textContent()).includes(id)); + await page.locator('#audit-dialog .close-dialog').click({force:true}); + await page.locator('#reset').click({force:true});assert.equal((await state()).enabled,false); + await page.emulateMedia({reducedMotion:'reduce'});await page.locator('#audit-highlight').check({force:true}); + const reduced=await state();assert.equal(reduced.timer,false);assert.equal(reduced.phase,1); + assert.equal(await page.locator('#audit-steady').isDisabled(),true); + await page.clock.resume();await page.waitForTimeout(150); + await page.locator('#audit-highlight').scrollIntoViewIfNeeded(); + await page.screenshot({path:path.join(out,'audit-fixed.png'),fullPage:true}); + assert.deepEqual(errors,[]);Object.assign(report,{status:'passed',target_conduits:expected.length, + red_pixels_on:on.red,red_pixels_off:normal.red,original_colors_restored:true, + checks:['target IDs','red pixels','1 second phases','exact original pixel restoration','timer cancellation', + 'steady mode','layer visibility','selection reasons','3 unplaced facilities','reset','reduced motion','no page errors']}); + }catch(e){report.error=e.stack;process.exitCode=1;await page.screenshot({path:path.join(out,'audit-failure.png'),fullPage:true}).catch(()=>{});} + finally{await fs.writeFile(path.join(out,'audit-highlight-validation.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report,null,2));await browser.close();} +})().catch(e=>{console.error(e);process.exitCode=1;}); From 56b1f2cb9ffb7c70f8b0c750f7db674ac1736a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Wed, 28 Oct 2026 09:51:28 +0900 Subject: [PATCH 7/7] test: verify review highlight controls (#6) --- validation/issue-6-result.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 validation/issue-6-result.json diff --git a/validation/issue-6-result.json b/validation/issue-6-result.json new file mode 100644 index 0000000..019390e --- /dev/null +++ b/validation/issue-6-result.json @@ -0,0 +1,21 @@ +{ + "status": "passed", + "target_conduits": 207, + "red_pixels_on": 4457, + "red_pixels_off": 0, + "original_colors_restored": true, + "checks": [ + "target IDs", + "red pixels", + "1 second phases", + "exact original pixel restoration", + "timer cancellation", + "steady mode", + "layer visibility", + "selection reasons", + "3 unplaced facilities", + "reset", + "reduced motion", + "no page errors" + ] +} \ No newline at end of file