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
+40 -10
View File
@@ -1,15 +1,20 @@
import * as THREE from 'three';
import { DepthProcessor } from './DepthProcessor';
import type { DepthProvider } from '../types/depth';
import type { AiDepthModel } from '../constants/depth';
import type { DepthProvider, AiDepthRun } from '../types/depth';
type WorkerReply =
| { type: 'status'; id: number; message: string }
| { type: 'result'; id: number; width: number; height: number; values: ArrayBuffer; normalizedValues: ArrayBuffer }
| { type: 'result'; id: number; width: number; height: number; values: ArrayBuffer; normalizedValues: ArrayBuffer; run: AiDepthRun }
| { type: 'fallback'; id: number; preparationMs: number; inferenceMs: number }
| { type: 'error'; id: number; message: string };
type PendingRequest = {
id: number;
started: number;
image: HTMLImageElement;
fallbackAttempted: boolean;
preparationMs: number;
inferenceMs: number;
resolve: (texture: THREE.Texture) => void;
reject: (error: Error) => void;
};
@@ -21,12 +26,12 @@ export class AiDepthProvider implements DepthProvider {
constructor(private readonly onStatus: (message: string) => void) {}
async getDepthMap(image: HTMLImageElement, model: AiDepthModel = 'small'): Promise<THREE.Texture> {
async getDepthMap(image: HTMLImageElement): Promise<THREE.Texture> {
if (this.pending) throw new Error('AI depth 추론이 이미 진행 중입니다.');
const id = ++this.requestId;
return new Promise<THREE.Texture>((resolve, reject) => {
this.pending = { id, resolve, reject };
void this.captureAndSend(image, id, model).catch((error: unknown) => {
this.pending = { id, started: performance.now(), image, fallbackAttempted: false, preparationMs: 0, inferenceMs: 0, resolve, reject };
void this.captureAndSend(image, id).catch((error: unknown) => {
this.fail(id, error instanceof Error ? error : new Error('AI depth 생성에 실패했습니다.'));
});
});
@@ -38,10 +43,10 @@ export class AiDepthProvider implements DepthProvider {
this.requestId += 1;
this.worker?.terminate();
this.worker = null;
pending?.reject(new Error('AI depth 요청이 취소되었습니다.'));
pending?.reject(new DOMException('AI depth 요청이 취소되었습니다.', 'AbortError'));
}
private async captureAndSend(image: HTMLImageElement, id: number, model: AiDepthModel): Promise<void> {
private async captureAndSend(image: HTMLImageElement, id: number, forceWasm = false): Promise<void> {
const width = image.naturalWidth || image.width;
const height = image.naturalHeight || image.height;
if (!width || !height) throw new Error('사진 크기를 확인할 수 없습니다.');
@@ -55,9 +60,9 @@ export class AiDepthProvider implements DepthProvider {
bitmap.close();
return;
}
const worker = this.ensureWorker();
try {
worker.postMessage({ type: 'infer', id, image: bitmap, model }, [bitmap]);
const worker = this.ensureWorker();
worker.postMessage({ type: 'infer', id, image: bitmap, ...(forceWasm ? { forceWasm: true } : {}) }, [bitmap]);
} catch (error) {
bitmap.close();
throw error;
@@ -68,6 +73,7 @@ export class AiDepthProvider implements DepthProvider {
if (this.worker) return this.worker;
const worker = new Worker(new URL('./aiDepth.worker.ts', import.meta.url), { type: 'module' });
worker.onmessage = (event: MessageEvent<WorkerReply>) => {
if (this.worker !== worker) return;
const message = event.data;
if (this.pending?.id !== message.id) return;
if (message.type === 'status') {
@@ -75,23 +81,47 @@ export class AiDepthProvider implements DepthProvider {
} else if (message.type === 'result') {
const pending = this.pending;
this.pending = null;
// Both result buffers are transferred into this realm. Release the
// inference runtime before the caller starts the full-screen GL scene.
// A manual new inference uses cached files in a fresh worker.
this.resetWorker(worker);
try {
const texture = DepthProcessor.fromNormalizedDepth(new Uint8Array(message.values), message.width, message.height);
// Keep unquantized normalized values for inspection; geometry still uses the existing byte texture.
texture.userData.normalizedDepth = new Float32Array(message.normalizedValues);
// Main-thread wall time includes capture, worker messaging, preparation and inference.
texture.userData.aiDepthRun = { ...message.run, preparationMs: pending.preparationMs + message.run.preparationMs, inferenceMs: pending.inferenceMs + message.run.inferenceMs, totalMs: performance.now() - pending.started } satisfies AiDepthRun;
pending.resolve(texture);
} catch (error) {
pending.reject(error instanceof Error ? error : new Error('AI depth 결과를 읽지 못했습니다.'));
}
} else if (message.type === 'fallback') {
const pending = this.pending;
this.resetWorker(worker);
if (pending.fallbackAttempted) {
this.fail(message.id, new Error('이 기기에서 AI 깊이를 계산하지 못했습니다. 다시 시도하거나 평면 사진으로 볼 수 있어요.'));
return;
}
pending.fallbackAttempted = true;
pending.preparationMs += message.preparationMs;
pending.inferenceMs += message.inferenceMs;
this.onStatus('다른 길을 찾고 있어요. 조금만 더 기다려 주세요…');
void this.captureAndSend(pending.image, pending.id, true).catch((error: unknown) => {
this.fail(pending.id, error instanceof Error ? error : new Error('AI depth 생성에 실패했습니다.'));
});
} else {
// A failed runtime can keep a rejected session chain; manual retry must be fresh too.
this.resetWorker(worker);
this.fail(message.id, new Error(message.message));
}
};
worker.onerror = (event) => {
if (this.worker !== worker) return;
this.resetWorker(worker);
this.fail(this.pending?.id ?? -1, new Error(event.message || 'AI depth worker 오류가 발생했습니다.'));
};
worker.onmessageerror = () => {
if (this.worker !== worker) return;
this.resetWorker(worker);
this.fail(this.pending?.id ?? -1, new Error('AI depth worker 결과를 읽지 못했습니다.'));
};