Replace project with current server release and Windows setup

Import the 2026-10-09 source release, Fastify/SQLite backend, restored source
assets, and analytics-enabled server build. Replace files from the previous
release while retaining the existing repository history.

Make builds portable across Windows and Linux, configure local startup,
normalize Windows static asset paths, and fix test fixture cleanup and file
URL handling. Exclude local environment settings, databases, dependencies,
validation output, and TypeScript build cache from version control.

Validation: npm ci, static and server builds, 106 arrival tests, and actual
local HTTP checks passed. The full suite passed 467 of 470 tests; three tests
still require QA fixtures omitted from the supplied release, documented in
README-서버실행.md.
This commit is contained in:
2026-10-09 16:43:35 +09:00
parent 71c8d3b7ff
commit e358e24436
642 changed files with 35202 additions and 17642 deletions
+139
View File
@@ -0,0 +1,139 @@
import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import { test } from 'node:test';
import vm from 'node:vm';
import ts from 'typescript';
import * as THREE from 'three';
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
function load(relative) {
const path=new URL(relative,import.meta.url); const exports={};
const source=readFileSync(path,'utf8');
vm.runInNewContext(ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText,{exports,require(spec){if(spec==='three')return THREE;if(spec.includes('RectArea'))return {RectAreaLightUniformsLib};return load(new URL(spec+'.ts',path).href);}});
return exports;
}
function fixture(){
const {MeshGlassRenderer}=load('../src/rendering/MeshGlassRenderer.ts');
const calls=[];let target=null; const renderer={autoClear:true,extensions:{has:()=>false},debug:{},getContext:()=>({isContextLost:()=>false}),getRenderTarget:()=>target,setRenderTarget(t){target=t;calls.push(['target',t]);},setScissorTest(){},clearDepth(){},render(scene,camera){camera.updateMatrixWorld();scene.updateMatrixWorld();calls.push(['render',scene,target]);scene.traverse(o=>{if(o.isMesh)o.onBeforeRender(renderer,scene,camera,o.geometry,o.material,null);});}};
const source=new THREE.Mesh(new THREE.PlaneGeometry(1,1,128,128),new THREE.ShaderMaterial({uniforms:{uTexture:{value:new THREE.Texture()},uDepthMap:{value:new THREE.Texture()},uDepthScale:{value:.25},uDepthBias:{value:0},uIsVideo:{value:false}}}));
const b=new MeshGlassRenderer(renderer,source);return {b,renderer,calls,source};
}
test('B is a standalone default renderer without A output targets or shaders',()=>{
assert.ok(existsSync(new URL('../src/rendering/MeshGlassRenderer.ts',import.meta.url)),'standalone B module must exist');
const {b,calls}=fixture(); b.sync({width:1,height:1.32,grid:20,curvature:1},.6,new THREE.Vector3(.6,.8,2.6),new THREE.Color());
const camera=new THREE.PerspectiveCamera(),frame=new THREE.Scene();b.render(camera,frame,true);
assert.equal('targetA' in b,false);assert.equal('targetB' in b,false);
assert.equal(calls.filter(c=>c[0]==='render').length,2);assert.equal(calls.filter(c=>c[0]==='render')[0][2],null);
b.dispose();
});
test('B shares replacements and photo transforms without first rendering A',()=>{
const {b,source}=fixture(),shape={width:1,height:1.32,grid:20,curvature:1};
b.sync(shape,.6,new THREE.Vector3(),new THREE.Color());const geometry=b.glass.geometry;
source.position.set(.1,-.2,.22);source.scale.set(.8,1.1,1);const texture=new THREE.Texture();source.material.uniforms.uTexture.value=texture;
b.sync(shape,.6,new THREE.Vector3(),new THREE.Color());assert.equal(b.glass.geometry,geometry);
const shader={uniforms:{},vertexShader:THREE.ShaderLib.physical.vertexShader,fragmentShader:THREE.ShaderLib.physical.fragmentShader};b.material.onBeforeCompile(shader,{});
assert.equal(shader.uniforms.uBPhoto.value,texture);assert.deepEqual(b.scene.children.find(x=>x.geometry===source.geometry).position.toArray(),source.position.toArray());
b.dispose();
});
test('B excludes caller output from owned transmission targets and disposes once',()=>{
const {b,renderer,source}=fixture();b.sync({width:1,height:1,grid:20,curvature:1},.6,new THREE.Vector3(),new THREE.Color());
const output=new THREE.WebGLRenderTarget(20,20);let outputDisposed=0,sourceDisposed=0,glassDisposed=0;
output.addEventListener('dispose',()=>outputDisposed++);source.geometry.addEventListener('dispose',()=>sourceDisposed++);b.glass.geometry.addEventListener('dispose',()=>glassDisposed++);
b.render(new THREE.PerspectiveCamera(),new THREE.Scene(),true,output);b.dispose();b.dispose();
assert.equal(outputDisposed,0);assert.equal(sourceDisposed,0);assert.equal(glassDisposed,1);output.dispose();
});
test('normal flow selects B, A is an explicit lazy option, and failure never silently selects A',()=>{
const main=readFileSync(new URL('../src/main.ts',import.meta.url),'utf8'),html=readFileSync(new URL('../index.html',import.meta.url),'utf8');
assert.match(html,/value="b" selected/);assert.match(main,/import\('\.\/rendering\/OriginalGlassRenderer'\)/);
assert.match(main,/meshRenderer\.render/);assert.match(main,/showMeshFailure/);
assert.match(main,/meshRenderer\.restore/);assert.match(main,/originalRenderer\?\.invalidate/);
});
test('a compiled B failure stays explicit until retry or context restoration',()=>{
const {b,renderer}=fixture();let first=true;const render=renderer.render.bind(renderer);
renderer.render=(...args)=>{if(first){first=false;renderer.debug.onShaderError?.({},null,null,null);}render(...args);};
const camera=new THREE.PerspectiveCamera(),frame=new THREE.Scene();
assert.throws(()=>b.render(camera,frame,true),/shader|failed/i);
assert.throws(()=>b.render(camera,frame,true),/shader|failed/i,'Three caches failed programs; absence of a second callback is not success');
b.restore();assert.doesNotThrow(()=>b.render(camera,frame,true));b.dispose();
});
test('fresh Glass Only still releases the hidden transmission target without drawing the photo',()=>{
const {b,renderer,source}=fixture(),internal=new THREE.WebGLRenderTarget(1,1);let frees=0;
internal.addEventListener('dispose',()=>frees++);
const original=renderer.render.bind(renderer),setTarget=renderer.setRenderTarget.bind(renderer);
renderer.render=(scene,camera)=>{
if(scene===b.scene){const output=renderer.getRenderTarget();setTarget(internal);for(const object of scene.children)if(object.visible&&object.geometry&&object!==b.glass){assert.equal(object.material.colorWrite,false);assert.equal(object.material.depthWrite,false);object.onBeforeRender(renderer,scene,camera,object.geometry,object.material,null);}setTarget(output);}
original(scene,camera);
};
b.render(new THREE.PerspectiveCamera(),new THREE.Scene(),true,null,false);b.dispose();
assert.equal(frees,1);assert.equal(source.material.colorWrite,true);assert.equal(source.material.depthWrite,true,'shared photo material state restores');
});
test('sliding B moves only the actual pane and keeps world-space exits tied to the translated geometry',()=>{
const {b,source}=fixture(),shape={width:1,height:1.32,grid:20,curvature:1};
b.setSlide(-.5);b.sync(shape,.6,new THREE.Vector3(),new THREE.Color());
assert.equal(b.glass.position.x,-.5);assert.equal(source.position.x,0);
const geometry=b.glass.geometry;
const shader={uniforms:{},fragmentShader:THREE.ShaderLib.physical.fragmentShader};b.material.onBeforeCompile(shader,{});
assert.equal(shader.uniforms.uBPaneCenterX.value,-.5);
assert.match(shader.fragmentShader,/side\*uBPaneHalfSize\.x\+uBPaneCenterX-entry\.x/);
assert.ok(b.material.clippingPlanes.length===1);assert.equal(b.material.clippingPlanes[0].constant,.55);
b.setSlide(-1);b.sync(shape,.6,new THREE.Vector3(),new THREE.Color());
assert.equal(b.glass.geometry,geometry,'opening does not rebuild patterned glass');
assert.equal(shader.uniforms.uBPaneCenterX.value,-1);
assert.equal(b.scene.children.find(x=>x.geometry===source.geometry).position.x,0);
b.restore();assert.equal(b.glass.position.x,-1);b.dispose();
});
// A CPU depth-buffer oracle around the real production render calls. Geometry
// intersections are real Three triangles; this does not execute WebGL shaders.
test('B preserves pane depth into the frame pass while foreground and open-door reveal still draw',()=>{
const {b,renderer,source}=fixture();
const {WindowReveal}=load('../src/objects/WindowReveal.ts');
source.position.z=.2;source.scale.set(1.2,1.584,1);
const frame=new THREE.Scene(),wood=new THREE.MeshBasicMaterial({side:THREE.DoubleSide});
const reveal=new WindowReveal(wood);reveal.update(1,1.32,1.2,1.584,.6,.2);
frame.add(...reveal.meshes);reveal.meshes.forEach(mesh=>mesh.name='reveal');
const front=new THREE.Mesh(new THREE.BoxGeometry(.12,1.6,.16),wood);front.position.set(-.55,0,.66);front.name='foreground';frame.add(front);
const originalRender=renderer.render.bind(renderer);let samples=[],depth=[],winner=[];
renderer.clearDepth=()=>{depth.fill(Infinity);};
renderer.render=(scene,camera)=>{
originalRender(scene,camera);
if(renderer.autoClear){depth=samples.map(()=>Infinity);winner=samples.map(()=>null);}
for(let i=0;i<samples.length;i++){
const ray=new THREE.Raycaster(camera.position,samples[i].clone().sub(camera.position).normalize());
const hits=ray.intersectObjects(scene.children,true).filter(hit=>hit.object.visible&&hit.object.material.depthTest!==false);
for(const hit of hits){if(hit.object===b.glass&&hit.point.x < -b.material.clippingPlanes[0].constant)continue;
if(hit.distance<=depth[i]){if(hit.object.material.colorWrite)winner[i]=hit.object;if(hit.object.material.depthWrite)depth[i]=hit.distance;}
}
}
};
let protectedRear=0,visibleFront=0,visibleOpenReveal=0;
for(const opening of [0,.5,1])for(const eye of [[0,0,3],[.6,-.2,1.1],[-.6,-.2,1.1],[0,.5,1.05]]){
b.setSlide(-opening);b.sync({width:1,height:1.32,grid:20,curvature:1},.6,new THREE.Vector3(),new THREE.Color());
const camera=new THREE.PerspectiveCamera();camera.position.set(...eye);camera.lookAt(0,0,.6);camera.updateMatrixWorld();
samples=[];for(const y of [-.6,-.3,0,.3,.6])for(const x of [-.56,-.49,-.4,-.2,0,.2,.4,.49])samples.push(new THREE.Vector3(x,y,.6));
b.render(camera,frame,true);
for(let i=0;i<samples.length;i++){
const ray=new THREE.Raycaster(camera.position,samples[i].clone().sub(camera.position).normalize());
const all=ray.intersectObjects([...b.scene.children,...frame.children],true).filter(hit=>hit.object.visible&&!(hit.object===b.glass&&hit.point.x < -b.material.clippingPlanes[0].constant));
const nearest=all[0]?.object??null;
assert.equal(winner[i]?.uuid,nearest?.uuid,`opening ${opening}, eye ${eye}, target ${samples[i].toArray()}: rear frame must not overwrite a nearer pane`);
if(nearest===b.glass&&all.some(hit=>reveal.meshes.includes(hit.object)))protectedRear++;
if(nearest===front)visibleFront++;
if(opening>0&&reveal.meshes.includes(nearest))visibleOpenReveal++;
}
}
assert.ok(protectedRear>0,'regression rays actually see rear reveal behind glass');
assert.ok(visibleFront>0,'near-side wood still occludes the pane');
assert.ok(visibleOpenReveal>0,'slid-open areas retain direct reveal coverage');
console.log('B/frame depth oracle:',JSON.stringify({protectedRear,visibleFront,visibleOpenReveal}));
b.dispose();reveal.dispose();front.geometry.dispose();wood.dispose();source.geometry.dispose();source.material.dispose();
});
test('B keeps caller render state after a failing frame pass without clearing the pane depth',()=>{
const {b,renderer}=fixture(),frame=new THREE.Scene();let clears=0;
renderer.clearDepth=()=>{clears++;};const render=renderer.render.bind(renderer);
renderer.render=(scene,camera)=>{if(scene===frame)throw new Error('frame failure');render(scene,camera);};
assert.throws(()=>b.render(new THREE.PerspectiveCamera(),frame,true),/frame failure/);
assert.equal(renderer.autoClear,true);assert.equal(clears,0,'frame failure must not discard the current pane depth');
b.dispose();
});