From f6a9b62dc3af9ffd8416b6925dae1518226ffbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EC=84=B1=EC=9D=BC?= Date: Wed, 15 Jul 2026 16:26:28 +0900 Subject: [PATCH] =?UTF-8?q?StationPlayer=20=ED=8F=AC=ED=84=B0=EB=B8=94=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=E2=80=94=20=EC=9B=B9=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=97=86=EC=9D=B4=20=EB=A1=9C=EC=BB=AC=20=EB=8B=A8?= =?UTF-8?q?=EB=8F=85=20=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server/src/demo.ts: 경량 서버 엔트리 — 정적 클라이언트 + 필수 API 2종만 (/api/elevation DEM 프록시, /api/tile 지도 타일 프록시). 영상은 브라우저 폴더 선택(로컬 파일)이라 업로드/HLS/DB(네이티브 모듈) 불필요 - scripts/build-portable.sh: esbuild 단일 번들(--minify, 소스 비공개) + client 복사 + 호스트 node.exe 동봉 + StationPlayer.bat/README(CRLF) 생성 → dist-portable/StationPlayer-Portable (약 103MB, 더블클릭 실행) - .gitignore: dist-portable/ 제외 Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + scripts/build-portable.sh | 70 +++++++++++++++++++++++++++++++++++++++ server/src/demo.ts | 53 +++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 scripts/build-portable.sh create mode 100644 server/src/demo.ts diff --git a/.gitignore b/.gitignore index fed1682..c07ce35 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ npm-debug.log* # OS/에디터 .DS_Store Thumbs.db +dist-portable/ diff --git a/scripts/build-portable.sh b/scripts/build-portable.sh new file mode 100644 index 0000000..9a5fc2b --- /dev/null +++ b/scripts/build-portable.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# StationPlayer 포터블(로컬 단독 실행) 패키지 빌드 +# 산출물: dist-portable/StationPlayer-Portable/ +# node.exe — Windows 포터블 런타임 (호스트 설치본 복사) +# server.cjs — 경량 서버 단일 번들 (demo.ts) +# client/ — 클라이언트 정적 빌드 +# StationPlayer.bat — 더블클릭 실행 (서버 시작 + 브라우저 오픈) +# README.txt +# 사용: source ~/.nvm/nvm.sh && nvm use 20 && bash scripts/build-portable.sh +set -euo pipefail +cd "$(dirname "$0")/.." + +OUT="dist-portable/StationPlayer-Portable" +NODE_EXE="/mnt/c/Program Files/nodejs/node.exe" + +echo "[1/5] 클라이언트 빌드" +npm run build -w client + +echo "[2/5] 경량 서버 번들 (esbuild)" +rm -rf "$OUT" +mkdir -p "$OUT" +# --minify: 고객 배포용 — 식별자·공백 압축(소스 비공개 수준을 클라이언트와 동일하게) +npx --yes esbuild server/src/demo.ts \ + --bundle --minify --platform=node --target=node18 --format=cjs \ + --outfile="$OUT/server.cjs" --log-level=warning + +echo "[3/5] 클라이언트 복사" +cp -r client/dist "$OUT/client" + +echo "[4/5] node.exe 복사" +if [ -f "$NODE_EXE" ]; then + cp "$NODE_EXE" "$OUT/node.exe" +else + echo " 경고: $NODE_EXE 없음 — node.exe 를 수동으로 넣어주세요 (nodejs.org 'Windows Binary')" +fi + +echo "[5/5] 실행 스크립트/README 생성" +python3 - "$OUT" <<'EOF' +import sys, os +out = sys.argv[1] +bat = "\r\n".join([ + "@echo off", + "chcp 65001 > nul", + "cd /d %~dp0", + "echo StationPlayer 서버 시작 - http://localhost:54000 (이 창을 닫으면 종료)", + 'start "" http://localhost:54000', + "node.exe server.cjs", + "pause", + "", +]) +open(os.path.join(out, "StationPlayer.bat"), "w", encoding="utf-8").write(bat) +readme = "\r\n".join([ + "StationPlayer 포터블 — 스테이션 기반 동영상 플레이어 (로컬 단독 실행)", + "", + "실행 방법:", + " 1) 이 폴더 전체를 아무 위치(USB/바탕화면 등)에 복사", + " 2) StationPlayer.bat 더블클릭", + " 3) 브라우저가 열리면 [폴더 선택]으로 영상 데이터 폴더 지정", + "", + "요구사항: Windows 10/11 + 크롬/엣지 브라우저 (설치 불필요, Node.js 포함됨)", + "포트: 54000 (사용 중이면 StationPlayer.bat 의 PORT 수정)", + "종료: 검은 콘솔 창 닫기", + "", +]) +open(os.path.join(out, "README.txt"), "w", encoding="utf-8-sig").write(readme) +print(" StationPlayer.bat / README.txt 생성") +EOF + +echo "완료: $OUT" +du -sh "$OUT" 2>/dev/null | awk '{print "총 크기: " $1}' diff --git a/server/src/demo.ts b/server/src/demo.ts new file mode 100644 index 0000000..c962393 --- /dev/null +++ b/server/src/demo.ts @@ -0,0 +1,53 @@ +/** + * 포터블(로컬 단독 실행) 경량 서버 — Node 만으로 실행되는 단일 번들용 엔트리. + * + * 용도: 드론 도로영상 플레이어를 웹서버 설치 없이 아무 PC 에서 더블클릭으로 실행. + * - 정적 클라이언트(client/) 서빙 + SPA 폴백 + * - /api/elevation (DEM 프록시), /api/tile (지도 타일 프록시) — 클라이언트 필수 API + * - 영상은 브라우저 폴더 선택(로컬 파일)로 재생하므로 업로드/HLS/DB(네이티브 모듈) 불필요 + * + * 빌드: scripts/build-portable.sh → dist-portable/GhiVideo-Portable/ + * (esbuild 단일 번들 server.cjs + client/ + node.exe + GhiVideo.bat) + */ +import express from 'express'; +import cors from 'cors'; +import path from 'path'; +import elevationRouter from './routes/elevation'; +import tileRouter from './routes/tile'; + +const app = express(); +app.use(cors({ origin: true })); +app.use(express.json()); + +app.use('/api/elevation', elevationRouter); +app.use('/api/tile', tileRouter); +app.get('/api/health', (_req, res) => { + res.json({ status: 'ok', mode: 'portable', timestamp: new Date().toISOString() }); +}); + +// 정적 클라이언트 — 번들(server.cjs) 옆의 client/ 폴더. +const clientDistPath = path.join(__dirname, 'client'); +const cspHeader = + "default-src 'self'; " + + "script-src 'self'; " + + "style-src 'self' 'unsafe-inline'; " + + "font-src 'self' data:; " + + "img-src 'self' blob: data:; " + + "media-src 'self' blob:; " + + "connect-src 'self'; " + + "worker-src 'self' blob:;"; +app.use((req, res, next) => { + if (!req.path.startsWith('/api')) res.setHeader('Content-Security-Policy', cspHeader); + next(); +}); +app.use(express.static(clientDistPath)); +app.get('*', (req, res, next) => { + if (req.path.startsWith('/api')) return next(); + res.sendFile(path.join(clientDistPath, 'index.html')); +}); + +const port = Number(process.env.PORT || 54000); +app.listen(port, () => { + console.log(`GhiVideo 포터블 서버 실행 중 — http://localhost:${port}`); + console.log('이 창을 닫으면 서버가 종료됩니다.'); +});