StationPlayer 포터블 패키지 — 웹서버 없이 로컬 단독 실행
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -34,3 +34,4 @@ npm-debug.log*
|
||||
# OS/에디터
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
dist-portable/
|
||||
|
||||
@@ -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}'
|
||||
@@ -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('이 창을 닫으면 서버가 종료됩니다.');
|
||||
});
|
||||
Reference in New Issue
Block a user