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
+129
View File
@@ -0,0 +1,129 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { test } from 'node:test';
import vm from 'node:vm';
import ts from 'typescript';
import * as THREE from 'three';
const main=await readFile(new URL('../src/main.ts',import.meta.url),'utf8');
const compile=source=>ts.transpileModule(source,{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ES2022}}).outputText;
const helper=await readFile(new URL('../src/rendering/RenderDemand.ts',import.meta.url),'utf8');
const {RenderDemand}=await import(`data:text/javascript;base64,${Buffer.from(compile(helper)).toString('base64')}`);
const {loadTypescript}=await import('./support/load-typescript.mjs');
const {CameraController}=await loadTypescript(new URL('../src/input/CameraController.ts',import.meta.url));
const slideSource=await readFile(new URL('../src/input/WindowSlideController.ts',import.meta.url),'utf8');
const {WindowSlideController}=await import(`data:text/javascript;base64,${Buffer.from(compile(slideSource)).toString('base64')}`);
const {SceneHints}=await import(`data:text/javascript;base64,${Buffer.from(compile(await readFile(new URL('../src/ui/SceneHints.ts',import.meta.url),'utf8'))).toString('base64')}`);
function harness(){
const camera=new THREE.PerspectiveCamera(40,1,.1,100),petDepth=new THREE.Mesh(new THREE.PlaneGeometry(),new THREE.MeshBasicMaterial());
camera.position.z=3;
const cameraController=new CameraController(camera),renderDemand=new RenderDemand(),doorMotion=new WindowSlideController();
const uniforms={uViewOffset:{value:new THREE.Vector2()},uPetWorldToLocal:{value:new THREE.Matrix4()},uContentViewProjection:{value:new THREE.Matrix4()},uContentInverseViewProjection:{value:new THREE.Matrix4()},uContentCameraPosition:{value:new THREE.Vector3()},uTime:{value:0},uSourceZ:{value:0},uRegionAvailable:{value:0},uRegionStrength:{value:0},uStrength:{value:1}};
let submissions=0,now=0,signatureRevision=0;
const context={firstGuideUI:{recenter(){}},gyroRecenter:{hidden:true},exportUI:{setAvailable(){}},canCaptureScene:()=>false,canAdjustScenePhoto:()=>false,sceneMotion:{hidden:false},gyroNotice:{hidden:true},mobileTilt:true,hintElement:{dataset:{},textContent:''},sceneHints:new SceneHints(),comparisonRequested:false,THREE,last:0,raf:0,gyro:{setPaused(){}},experience:{isBlocked:()=>false,isGuideOpen:()=>false,isPhotoAdjusting:()=>false,stopPhotoAdjust(){}},pointer:{isInteracting:false},doorMotion,slideEnabled:()=>true,doorReducedMotion:null,syncDoorPresentation(){},renderLost:false,aiDepthBusy:false,document:{hidden:false,querySelector:()=>({hidden:true})},comparisonMetrics:{reset(){}},meshFailure:false,uniforms,cameraController,sensitivity:.55,camera,petDepth,housePhotoDepth:{sync(){}},houseSurround:{isReady:true},blenderFrame:{isWood:true},mode:'final',active:{},emptyTex:{},depthDebug:{update:()=>''},depthUniforms:{uDepthScale:{value:.2},uDepthBias:{value:0}},wrap:{clientWidth:1000},maxDepthValue:1,debugOutput:{},comparison:null,comparisonPanes:null,renderSelected(){submissions++;},requestAnimationFrame:()=>1,renderDemand,activeIsVideo:false,selectedRenderer:'b',effectInput:{value:'mosaic'},mosaicStyleInput:{value:'2'},comparisonTiming:{textContent:''},currentRenderSignature:()=>[...camera.matrixWorld.elements,...camera.projectionMatrix.elements,...petDepth.matrixWorld.elements,doorMotion.value,signatureRevision].join('|')};
context.renderer={domElement:{width:1000,height:1000}};
context.blenderFrame.isReady=true;
context.cancelDoorInteraction=()=>doorMotion.cancel();
const hintStart=main.indexOf('function syncSceneHints('),hintEnd=main.indexOf('const imageInput =',hintStart);
vm.runInNewContext(compile(main.slice(hintStart,hintEnd)),context);
const begin=main.indexOf('function currentRenderSignature()');
const end=main.indexOf('\nraf = requestAnimationFrame(frame);',begin);
assert.ok(begin>=0&&end>begin,'extract the actual production frame function');
vm.runInNewContext(compile(main.slice(begin,end))+'\nglobalThis.runFrame=frame;',context);
const visibility=main.match(/document\.addEventListener\('visibilitychange',(\(\)=>\{.*?\}),\{signal:/);
assert.ok(visibility,'exercise the real visibility listener without a hidden RAF');
vm.runInNewContext('globalThis.visibility='+visibility[1],context);
return {context,cameraController,doorMotion,renderDemand,get submissions(){return submissions;},advance(frames=1){for(let i=0;i<frames;i++){now+=1000/60;context.runFrame(now);}},change(){context.depthUniforms.uDepthScale.value+=.001;signatureRevision++;}};
}
test('actual main frame submits no GPU render calls during ten seconds of static B idle',()=>{
const h=harness();h.advance();const warmup=h.submissions;h.advance(600);
assert.equal(h.submissions-warmup,0,'idle must bypass renderSelected, not merely throttle JS');
});
test('actual main resumes drag, settles exactly, then resumes zoom, asset, depth and recovery',()=>{
const h=harness();h.advance();h.cameraController.add(.5,-.25);h.advance(15);
assert.ok(h.submissions>1,'camera smoothing renders intermediate frames');
h.advance(240);const settled=h.submissions;h.advance(600);assert.equal(h.submissions,settled);
let expected=settled;
for(const reason of ['pinch','asset ready','depth complete','theme','resize','renderer retry']){
h.change();h.advance();assert.equal(h.submissions,++expected,reason);h.advance();assert.equal(h.submissions,expected);
}
h.context.renderLost=true;h.advance(10);assert.equal(h.submissions,expected);
h.context.renderLost=false;h.advance();assert.equal(h.submissions,++expected,'restored canvas receives a new frame');
});
test('actual frame preserves mechanical movement and its exact final position',()=>{
const h=harness();h.advance();h.doorMotion.moveTo(1,1000/60);h.advance(10);
assert.ok(h.doorMotion.value>0&&h.doorMotion.value<1);assert.ok(h.submissions>1);
h.advance(20);assert.equal(h.doorMotion.value,1);const settled=h.submissions;h.advance(120);assert.equal(h.submissions,settled);
});
test('hidden and AI-busy paths submit nothing and redraw once when resumed',()=>{
const h=harness();h.advance();let expected=h.submissions;
for(const flag of ['hidden','aiDepthBusy']){
const owner=flag==='hidden'?h.context.document:h.context;
owner[flag]=true;h.advance(60);assert.equal(h.submissions,expected);
owner[flag]=false;h.advance();assert.equal(h.submissions,++expected);h.advance();assert.equal(h.submissions,expected);
}
});
test('actual frame and signature follow decoded video frames, then stop on paused video',()=>{
const h=harness(),texture=new THREE.Texture();texture.image={currentTime:0,requestVideoFrameCallback(){}};
h.context.active=texture;h.context.activeIsVideo=true;h.context.depthUniforms.uTexture={value:texture};
h.advance();const initial=h.submissions;h.advance(60);assert.equal(h.submissions,initial);
texture.needsUpdate=true;h.advance();assert.equal(h.submissions,initial+1);
h.advance(60);assert.equal(h.submissions,initial+1,'no new decoded frame means no repeated GPU draw');
delete texture.image.requestVideoFrameCallback;
h.advance();const fallback=h.submissions;texture.image.currentTime=.04;h.advance();assert.equal(h.submissions,fallback+1);
texture.needsUpdate=true;h.advance();assert.equal(h.submissions,fallback+1,'fallback texture upload cannot invalidate itself');
});
test('static comparison is also idle and its cadence label clearly says paused',()=>{
const h=harness();h.context.comparison={};h.advance();const first=h.submissions;h.advance(60);
assert.equal(h.submissions,first);assert.match(h.context.comparisonTiming.textContent,/정지 화면/);
});
test('tab restoration redraws even when the browser delivered zero hidden RAF callbacks',()=>{
const h=harness();h.advance(2);const first=h.submissions;
h.context.document.hidden=true;h.context.visibility();
h.context.document.hidden=false;h.context.visibility();
h.advance();assert.equal(h.submissions,first+1);h.advance();assert.equal(h.submissions,first+1);
});
test('passive hint fade and repeat change DOM without waking idle WebGL',()=>{
const h=harness();h.advance();const initial=h.submissions;
h.advance(310);assert.equal(h.context.hintElement.dataset.phase,'in');
h.advance(50);assert.equal(h.context.hintElement.dataset.phase,'hold');
h.advance(170);assert.equal(h.context.hintElement.dataset.phase,'out');
h.advance(40);assert.equal(h.context.hintElement.dataset.phase,'hidden');
h.advance(1090);assert.notEqual(h.context.hintElement.dataset.phase,'hidden');
assert.equal(h.submissions,initial,'teaching cycles never invalidate the GL image');
});
test('mobile unavailable notice hides while settings block the scene',()=>{
const h=harness();h.context.gyro.state='unavailable';h.advance();
assert.equal(h.context.gyroNotice.hidden,false);
h.context.experience.isBlocked=()=>true;h.advance();assert.equal(h.context.gyroNotice.hidden,true);
h.context.experience.isBlocked=()=>false;h.advance();assert.equal(h.context.gyroNotice.hidden,false,'fallback stays discoverable after closing controls');
});
test('desktop never exposes the tilt row, including after a scene unblocks',()=>{
const h=harness();h.context.mobileTilt=false;h.context.gyro.state='unavailable';h.advance();
assert.equal(h.context.sceneMotion.hidden,true);assert.equal(h.context.gyroNotice.hidden,true);
h.context.experience.isBlocked=()=>true;h.advance();h.context.experience.isBlocked=()=>false;h.advance();
assert.equal(h.context.sceneMotion.hidden,true);
h.context.mobileTilt=true;h.advance();assert.equal(h.context.sceneMotion.hidden,false);
});
test('mobile recenter is shown only with live tilt and no blocking dialog',()=>{
const h=harness();h.context.gyro.state='active';h.advance();assert.equal(h.context.gyroRecenter.hidden,false);
h.context.mobileTilt=false;h.advance();assert.equal(h.context.gyroRecenter.hidden,true);
h.context.mobileTilt=true;h.context.gyro.state='off';h.advance();assert.equal(h.context.gyroRecenter.hidden,true);
h.context.gyro.state='active';h.context.experience.isBlocked=()=>true;h.advance();assert.equal(h.context.gyroRecenter.hidden,true);
});
test('spotlight leaves mobile targets visible while pausing normal scene input',()=>{
const h=harness();h.context.mobileTilt=true;h.context.gyro.state='paused';h.context.experience.isBlocked=()=>true;h.context.experience.isGuideOpen=()=>true;h.advance();
assert.equal(h.context.sceneMotion.hidden,false);assert.equal(h.context.gyroRecenter.hidden,false);
h.context.renderLost=true;h.advance();assert.equal(h.context.sceneMotion.hidden,true);
});