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
+67
View File
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict';
import {test} from 'node:test';
import {readFile} from 'node:fs/promises';
import {loadTypescript} from './support/load-typescript.mjs';
test('export shader keeps normal path and can produce transparent photo misses',async()=>{
const source=await readFile(new URL('../src/rendering/MeshRefraction.ts',import.meta.url),'utf8');
assert.match(source,/uBPhotoExport/);
assert.match(source,/gl_FragColor\.a\s*=\s*clamp\(bLight\.a/);
});
test('pixel extraction flips WebGL rows, crops alpha bounds, and preserves partial alpha',async()=>{
const {prepareExportPixels}=await loadTypescript(new URL('../src/rendering/ExportPixels.ts',import.meta.url));
const bytes=new Uint8Array(4*3*4);bytes.set([30,40,50,128],(1*4+2)*4);bytes.set([10,20,30,255],(2*4+1)*4);
const result=prepareExportPixels(bytes,4,3,true);
assert.equal(result.width,2);assert.equal(result.height,2);
assert.deepEqual([...result.data.slice(0,4)],[10,20,30,255]);
assert.deepEqual([...result.data.slice(12,16)],[30,40,50,128]);
const full=prepareExportPixels(bytes,4,3,false);assert.equal(full.width,4);assert.equal(full.height,3);
assert.throws(()=>prepareExportPixels(new Uint8Array(16),2,2,true),/사진/);
});
import * as THREE from 'three';
function captureHarness({readError=false,drawError=false,delay=false}={}){
let target=null,disposed=false,resolveRead;const original={name:'original'};
const renderer={autoClear:false,current:original,viewport:new THREE.Vector4(1,2,3,4),scissor:new THREE.Vector4(2,3,4,5),scissorTest:true,color:new THREE.Color(.2,.3,.4),alpha:.7,
getContext:()=>({isContextLost:()=>false}),getDrawingBufferSize:v=>v.set(4,3),getRenderTarget(){return this.current;},getViewport(v){return v.copy(this.viewport);},getScissor(v){return v.copy(this.scissor);},getScissorTest(){return this.scissorTest;},getClearColor(v){return v.copy(this.color);},getClearAlpha(){return this.alpha;},
setRenderTarget(v){this.current=v;},setViewport(v){this.viewport.copy(v);},setScissor(v){this.scissor.copy(v);},setScissorTest(v){this.scissorTest=v;},setClearColor(v,a){this.color.set(v);this.alpha=a;},
async readRenderTargetPixelsAsync(t,x,y,w,h,bytes){assert.equal(this.current,original,'live target restored before readback wait');if(readError)throw Error('read failed');if(delay)await new Promise(r=>resolveRead=r);for(let i=0;i<bytes.length;i+=4)bytes.set([128,128,128,255],i);}};
let output;
const root={createElement:()=>({getContext:()=>({createImageData:(w,h)=>({data:new Uint8ClampedArray(w*h*4)}),putImageData:data=>output=data.data}),toBlob:cb=>cb(new Blob(['png'],{type:'image/png'}))})};
const draw=t=>{target=t;t.addEventListener('dispose',()=>disposed=true);assert.equal(t.texture.colorSpace,THREE.SRGBColorSpace);assert.equal(t.samples,0);renderer.setRenderTarget(t);renderer.setViewport(new THREE.Vector4(0,0,4,3));if(drawError)throw Error('draw failed');};
const restored=()=>{assert.equal(renderer.current,original);assert.deepEqual(renderer.viewport.toArray(),[1,2,3,4]);assert.deepEqual(renderer.scissor.toArray(),[2,3,4,5]);assert.equal(renderer.scissorTest,true);assert.equal(renderer.autoClear,false);assert.equal(renderer.alpha,.7);assert.ok(Math.abs(renderer.color.r-.2)<1e-10);};
return {renderer,root,draw,restored,get output(){return output;},get disposed(){return disposed;},resolve:()=>resolveRead()};
}
test('capture restores renderer and disposes target on success and failures without double color encoding',async()=>{
const {captureScenePNG}=await loadTypescript(new URL('../src/rendering/SceneExport.ts',import.meta.url));
for(const settings of [{},{readError:true},{drawError:true}]){
const h=captureHarness(settings),signal=new AbortController().signal;
if(settings.readError||settings.drawError)await assert.rejects(captureScenePNG(h.renderer,h.root,'scene',signal,h.draw),/failed/);
else {assert.equal((await captureScenePNG(h.renderer,h.root,'photo',signal,h.draw)).type,'image/png');assert.deepEqual([...h.output.slice(0,4)],[128,128,128,255]);}
h.restored();assert.equal(h.disposed,true);
}
});
test('aborting pending readback prevents PNG creation and releases target after readback settles',async()=>{
const {captureScenePNG}=await loadTypescript(new URL('../src/rendering/SceneExport.ts',import.meta.url));
const h=captureHarness({delay:true}),controller=new AbortController();
const promise=captureScenePNG(h.renderer,h.root,'photo',controller.signal,h.draw);h.restored();controller.abort();h.resolve();
await assert.rejects(promise,{name:'AbortError'});assert.equal(h.disposed,true);assert.equal(h.output,undefined);
});
test('isolated B draw omits frame and restores background, alpha flags, camera and slide even on failure',async()=>{
const {MeshGlassRenderer}=await loadTypescript(new URL('../src/rendering/MeshGlassRenderer.ts',import.meta.url));
for(const fail of [false,true]){
let current=null;const calls=[];
const renderer={extensions:{has:()=>false},debug:{onShaderError:null},autoClear:true,getContext:()=>({isContextLost:()=>false}),getRenderTarget:()=>current,setRenderTarget:t=>current=t,setScissorTest(){},render(scene,camera){calls.push({scene,camera});scene.children[0]?.onBeforeRender?.();if(fail)throw Error('forced');}};
const material=new THREE.ShaderMaterial({uniforms:{uTexture:{value:new THREE.Texture()},uDepthMap:{value:new THREE.Texture()},uDepthScale:{value:0},uDepthBias:{value:0}},alphaToCoverage:true});
const source=new THREE.Mesh(new THREE.PlaneGeometry(1,1),material),mesh=new MeshGlassRenderer(renderer,source);
const bg=new THREE.Color(.2,.3,.4);mesh.scene.background=bg;mesh.setSlide(-.3);
const camera=new THREE.PerspectiveCamera();camera.projectionMatrix.elements[8]=.17;const projection=[...camera.projectionMatrix.elements];
const target=new THREE.WebGLRenderTarget(2,2),frame=new THREE.Scene();
const blockerMaterial=new THREE.MeshBasicMaterial();frame.add(new THREE.Mesh(new THREE.PlaneGeometry(1,1),blockerMaterial));
const originalRender=renderer.render;renderer.render=(scene,camera)=>{if(scene===frame)assert.equal(blockerMaterial.colorWrite,false);return originalRender(scene,camera);};
if(fail)assert.throws(()=>mesh.renderForExport(camera,frame,target,true),/forced/);else mesh.renderForExport(camera,frame,target,true);
assert.equal(calls.length,fail?1:2);assert.equal(calls[0].camera,camera);assert.equal(calls[0].scene,frame,'frame depth blocks hidden photo without painting object colors');
if(!fail)assert.equal(calls[1].scene,mesh.scene);
assert.equal(mesh.scene.background,bg);assert.equal(blockerMaterial.colorWrite,true);assert.equal(renderer.autoClear,true);assert.equal(material.alphaToCoverage,true);assert.equal(mesh.glass.position.x,-.3);assert.deepEqual(camera.projectionMatrix.elements,projection);
assert.equal(mesh.internalTargets.has(target),false,'caller-owned export target never becomes an internal target');
assert.equal(mesh.refraction.uniforms.uBPhotoExport.value,false);mesh.dispose();target.dispose();source.geometry.dispose();material.dispose();
}
});