@things-factory/weather 9.0.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/assets/images/wind.png +0 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +0 -0
- package/client/ol-components/webgl-canvas-layer.ts +413 -0
- package/client/ol-components/webgl-image-static.ts +174 -0
- package/client/ol-components/wind.ts +120 -0
- package/client/pages/weather-page.ts +318 -0
- package/client/route.ts +7 -0
- package/client/tsconfig.json +11 -0
- package/config/config.development.js +25 -0
- package/config/config.production.js +7 -0
- package/data/geo-json/countries-max-geo.json +1 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +0 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/ol-components/webgl-canvas-layer.d.ts +61 -0
- package/dist-client/ol-components/webgl-canvas-layer.js +364 -0
- package/dist-client/ol-components/webgl-canvas-layer.js.map +1 -0
- package/dist-client/ol-components/webgl-image-static.d.ts +41 -0
- package/dist-client/ol-components/webgl-image-static.js +135 -0
- package/dist-client/ol-components/webgl-image-static.js.map +1 -0
- package/dist-client/ol-components/wind.d.ts +3 -0
- package/dist-client/ol-components/wind.js +104 -0
- package/dist-client/ol-components/wind.js.map +1 -0
- package/dist-client/pages/weather-page.d.ts +18 -0
- package/dist-client/pages/weather-page.js +300 -0
- package/dist-client/pages/weather-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +8 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controllers/open-meteo-adapter.d.ts +4 -0
- package/dist-server/controllers/open-meteo-adapter.js +83 -0
- package/dist-server/controllers/open-meteo-adapter.js.map +1 -0
- package/dist-server/controllers/types.d.ts +55 -0
- package/dist-server/controllers/types.js +3 -0
- package/dist-server/controllers/types.js.map +1 -0
- package/dist-server/controllers/weather-controller.d.ts +12 -0
- package/dist-server/controllers/weather-controller.js +164 -0
- package/dist-server/controllers/weather-controller.js.map +1 -0
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +4 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/routers/geo-router.d.ts +59 -0
- package/dist-server/routers/geo-router.js +277 -0
- package/dist-server/routers/geo-router.js.map +1 -0
- package/dist-server/routers/weather-router.d.ts +1 -0
- package/dist-server/routers/weather-router.js +10 -0
- package/dist-server/routers/weather-router.js.map +1 -0
- package/dist-server/routes.d.ts +1 -0
- package/dist-server/routes.js +11 -0
- package/dist-server/routes.js.map +1 -0
- package/dist-server/services/chrome-renderer.d.ts +13 -0
- package/dist-server/services/chrome-renderer.js +137 -0
- package/dist-server/services/chrome-renderer.js.map +1 -0
- package/dist-server/services/headless-pool.d.ts +4 -0
- package/dist-server/services/headless-pool.js +89 -0
- package/dist-server/services/headless-pool.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/utils/svg-utils.d.ts +35 -0
- package/dist-server/utils/svg-utils.js +89 -0
- package/dist-server/utils/svg-utils.js.map +1 -0
- package/dist-server/utils/tile-utils.d.ts +38 -0
- package/dist-server/utils/tile-utils.js +64 -0
- package/dist-server/utils/tile-utils.js.map +1 -0
- package/package.json +45 -0
- package/server/controllers/open-meteo-adapter.ts +91 -0
- package/server/controllers/types.ts +63 -0
- package/server/controllers/weather-controller.ts +176 -0
- package/server/index.ts +1 -0
- package/server/routers/geo-router.ts +297 -0
- package/server/routers/weather-router.ts +8 -0
- package/server/routes.ts +10 -0
- package/server/services/chrome-renderer.ts +175 -0
- package/server/services/data.json +8438 -0
- package/server/services/headless-pool.ts +97 -0
- package/server/tsconfig.json +10 -0
- package/server/utils/svg-utils.ts +108 -0
- package/server/utils/tile-utils.ts +77 -0
- package/things-factory.config.js +8 -0
- package/translations/en.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/zh.json +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;AAAA,oBAAiB","sourcesContent":["import './routes'\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Koa 기반의 GeoJSON → MBTiles 변환 및 VectorTile(MVT) 서비스 제공 라우터
|
|
4
|
+
*
|
|
5
|
+
* 이 라우터는 GeoJSON 데이터를 MBTiles 형식으로 변환하고, OpenLayers 등의 클라이언트에서
|
|
6
|
+
* Vector Tile(MVT) 형식으로 요청할 수 있도록 API 엔드포인트를 제공합니다.
|
|
7
|
+
*
|
|
8
|
+
* 📌 주요 기능:
|
|
9
|
+
* 1️⃣ `/tiles/regenerate` (POST) → GeoJSON을 다시 MBTiles로 변환
|
|
10
|
+
* 2️⃣ `/tiles/:z/:x/:y.mvt` (GET) → 특정 타일 데이터를 반환
|
|
11
|
+
* 3️⃣ `/tiles/metadata` (GET) → MBTiles의 메타데이터 반환
|
|
12
|
+
*
|
|
13
|
+
* 🚀 서비스 초기화 흐름:
|
|
14
|
+
* 1. 서버 시작 시 `initializeTileService()` 호출
|
|
15
|
+
* - MBTiles 파일이 없으면 `convertGeoJsonToMBTiles()` 실행하여 생성
|
|
16
|
+
* - MBTiles가 존재하면 `loadTileSource()`를 호출하여 데이터 로드
|
|
17
|
+
* 2. 클라이언트가 MVT 타일을 요청하면 `tileSource.getTile(z, x, y, callback)`을 통해 데이터 반환
|
|
18
|
+
*
|
|
19
|
+
* 📌 주요 처리 로직:
|
|
20
|
+
* ✅ **GeoJSON → MBTiles 변환 (`convertGeoJsonToMBTiles`)**
|
|
21
|
+
* - `tippecanoe` CLI를 사용하여 GeoJSON을 MBTiles로 변환
|
|
22
|
+
* - 기존 MBTiles 파일이 있으면 삭제 후 재생성
|
|
23
|
+
* - `spawn`을 사용하여 비동기적으로 실행하며 로그 출력
|
|
24
|
+
*
|
|
25
|
+
* ✅ **MBTiles 파일을 로드 (`loadTileSource`)**
|
|
26
|
+
* - `@mapbox/mbtiles` 라이브러리를 사용하여 MBTiles 데이터베이스를 로드
|
|
27
|
+
* - 타일 요청이 있을 때 해당 타일을 `tileSource.getTile()`을 통해 제공
|
|
28
|
+
*
|
|
29
|
+
* ✅ **타일 요청 처리 (`/tiles/:z/:x/:y.mvt`)**
|
|
30
|
+
* - MBTiles에서 특정 타일을 찾아 바이너리 MVT 데이터로 반환
|
|
31
|
+
* - `Content-Encoding: gzip` 설정으로 압축된 타일 제공
|
|
32
|
+
* - `TMS 좌표 변환` 문제 제거하여 직접 요청 가능하도록 개선
|
|
33
|
+
*
|
|
34
|
+
* ✅ **타일셋 메타데이터 반환 (`/tiles/metadata`)**
|
|
35
|
+
* - MBTiles 파일에서 메타데이터 정보를 가져와 JSON 형태로 반환
|
|
36
|
+
*
|
|
37
|
+
* 📌 종속성:
|
|
38
|
+
* - `koa-router`: Koa 기반 API 라우팅
|
|
39
|
+
* - `fs`, `path`: 파일 시스템 접근 및 경로 처리
|
|
40
|
+
* - `child_process`, `util`: `tippecanoe` 실행을 위한 비동기 프로세스 실행
|
|
41
|
+
* - `@mapbox/mbtiles`: MBTiles 데이터베이스 접근 라이브러리
|
|
42
|
+
* - `@things-factory/env`: 환경 변수 관리
|
|
43
|
+
*
|
|
44
|
+
* 📌 디렉토리 구조:
|
|
45
|
+
* ```
|
|
46
|
+
* /cache/mvt-tiles/ # 생성된 MBTiles 저장
|
|
47
|
+
* /data/geo-json/ # 원본 GeoJSON 파일 저장
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* 📌 실행 방법:
|
|
51
|
+
* 1. GeoJSON 데이터 준비
|
|
52
|
+
* 2. Koa 서버 실행 → 자동으로 MBTiles 변환 후 서비스 시작
|
|
53
|
+
* 3. 클라이언트(OpenLayers 등)에서 VectorTile 요청 가능
|
|
54
|
+
* 4. 필요 시 `/tiles/regenerate` 엔드포인트로 타일 재생성
|
|
55
|
+
*
|
|
56
|
+
* @module geoRouter
|
|
57
|
+
*/
|
|
58
|
+
export declare const geoRouter: any;
|
|
59
|
+
export default geoRouter;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview
|
|
4
|
+
* Koa 기반의 GeoJSON → MBTiles 변환 및 VectorTile(MVT) 서비스 제공 라우터
|
|
5
|
+
*
|
|
6
|
+
* 이 라우터는 GeoJSON 데이터를 MBTiles 형식으로 변환하고, OpenLayers 등의 클라이언트에서
|
|
7
|
+
* Vector Tile(MVT) 형식으로 요청할 수 있도록 API 엔드포인트를 제공합니다.
|
|
8
|
+
*
|
|
9
|
+
* 📌 주요 기능:
|
|
10
|
+
* 1️⃣ `/tiles/regenerate` (POST) → GeoJSON을 다시 MBTiles로 변환
|
|
11
|
+
* 2️⃣ `/tiles/:z/:x/:y.mvt` (GET) → 특정 타일 데이터를 반환
|
|
12
|
+
* 3️⃣ `/tiles/metadata` (GET) → MBTiles의 메타데이터 반환
|
|
13
|
+
*
|
|
14
|
+
* 🚀 서비스 초기화 흐름:
|
|
15
|
+
* 1. 서버 시작 시 `initializeTileService()` 호출
|
|
16
|
+
* - MBTiles 파일이 없으면 `convertGeoJsonToMBTiles()` 실행하여 생성
|
|
17
|
+
* - MBTiles가 존재하면 `loadTileSource()`를 호출하여 데이터 로드
|
|
18
|
+
* 2. 클라이언트가 MVT 타일을 요청하면 `tileSource.getTile(z, x, y, callback)`을 통해 데이터 반환
|
|
19
|
+
*
|
|
20
|
+
* 📌 주요 처리 로직:
|
|
21
|
+
* ✅ **GeoJSON → MBTiles 변환 (`convertGeoJsonToMBTiles`)**
|
|
22
|
+
* - `tippecanoe` CLI를 사용하여 GeoJSON을 MBTiles로 변환
|
|
23
|
+
* - 기존 MBTiles 파일이 있으면 삭제 후 재생성
|
|
24
|
+
* - `spawn`을 사용하여 비동기적으로 실행하며 로그 출력
|
|
25
|
+
*
|
|
26
|
+
* ✅ **MBTiles 파일을 로드 (`loadTileSource`)**
|
|
27
|
+
* - `@mapbox/mbtiles` 라이브러리를 사용하여 MBTiles 데이터베이스를 로드
|
|
28
|
+
* - 타일 요청이 있을 때 해당 타일을 `tileSource.getTile()`을 통해 제공
|
|
29
|
+
*
|
|
30
|
+
* ✅ **타일 요청 처리 (`/tiles/:z/:x/:y.mvt`)**
|
|
31
|
+
* - MBTiles에서 특정 타일을 찾아 바이너리 MVT 데이터로 반환
|
|
32
|
+
* - `Content-Encoding: gzip` 설정으로 압축된 타일 제공
|
|
33
|
+
* - `TMS 좌표 변환` 문제 제거하여 직접 요청 가능하도록 개선
|
|
34
|
+
*
|
|
35
|
+
* ✅ **타일셋 메타데이터 반환 (`/tiles/metadata`)**
|
|
36
|
+
* - MBTiles 파일에서 메타데이터 정보를 가져와 JSON 형태로 반환
|
|
37
|
+
*
|
|
38
|
+
* 📌 종속성:
|
|
39
|
+
* - `koa-router`: Koa 기반 API 라우팅
|
|
40
|
+
* - `fs`, `path`: 파일 시스템 접근 및 경로 처리
|
|
41
|
+
* - `child_process`, `util`: `tippecanoe` 실행을 위한 비동기 프로세스 실행
|
|
42
|
+
* - `@mapbox/mbtiles`: MBTiles 데이터베이스 접근 라이브러리
|
|
43
|
+
* - `@things-factory/env`: 환경 변수 관리
|
|
44
|
+
*
|
|
45
|
+
* 📌 디렉토리 구조:
|
|
46
|
+
* ```
|
|
47
|
+
* /cache/mvt-tiles/ # 생성된 MBTiles 저장
|
|
48
|
+
* /data/geo-json/ # 원본 GeoJSON 파일 저장
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* 📌 실행 방법:
|
|
52
|
+
* 1. GeoJSON 데이터 준비
|
|
53
|
+
* 2. Koa 서버 실행 → 자동으로 MBTiles 변환 후 서비스 시작
|
|
54
|
+
* 3. 클라이언트(OpenLayers 등)에서 VectorTile 요청 가능
|
|
55
|
+
* 4. 필요 시 `/tiles/regenerate` 엔드포인트로 타일 재생성
|
|
56
|
+
*
|
|
57
|
+
* @module geoRouter
|
|
58
|
+
*/
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.geoRouter = void 0;
|
|
61
|
+
const tslib_1 = require("tslib");
|
|
62
|
+
const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
|
|
63
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
64
|
+
const path = tslib_1.__importStar(require("path"));
|
|
65
|
+
const child_process = tslib_1.__importStar(require("child_process"));
|
|
66
|
+
const util = tslib_1.__importStar(require("util"));
|
|
67
|
+
const env_1 = require("@things-factory/env");
|
|
68
|
+
const mbtiles_1 = tslib_1.__importDefault(require("@mapbox/mbtiles"));
|
|
69
|
+
const exec = util.promisify(child_process.exec);
|
|
70
|
+
exports.geoRouter = new koa_router_1.default();
|
|
71
|
+
const TILE_CACHE_DIR = path.resolve(env_1.config.get('directory', './cache'), 'mvt-tiles');
|
|
72
|
+
const GEO_DATA_DIR = path.resolve(__dirname, '..', '..', 'data', 'geo-json');
|
|
73
|
+
if (!fs.existsSync(TILE_CACHE_DIR)) {
|
|
74
|
+
fs.mkdirSync(TILE_CACHE_DIR, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
if (!fs.existsSync(GEO_DATA_DIR)) {
|
|
77
|
+
fs.mkdirSync(GEO_DATA_DIR, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
const COUNTRIES_GEO_JSON = path.resolve(GEO_DATA_DIR, 'countries-max-geo.json');
|
|
80
|
+
const MBTILES_PATH = path.resolve(TILE_CACHE_DIR, 'countries.mbtiles');
|
|
81
|
+
const child_process_1 = require("child_process");
|
|
82
|
+
const convertGeoJsonToMBTiles = async () => {
|
|
83
|
+
// tippecanoe -o ./packages/operato-ecs/cache/mvt-tiles/countries.mbtiles -z10 -Z5 --force --maximum-zoom=10 --minimum-zoom=0 --drop-smallest-as-needed --layer=countries ./packages/weather/data/geo-json/countries-max-geo.json
|
|
84
|
+
try {
|
|
85
|
+
if (!fs.existsSync(COUNTRIES_GEO_JSON)) {
|
|
86
|
+
console.error('GeoJSON 파일을 찾을 수 없습니다:', COUNTRIES_GEO_JSON);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (fs.existsSync(MBTILES_PATH)) {
|
|
90
|
+
fs.unlinkSync(MBTILES_PATH);
|
|
91
|
+
console.log('기존 MBTiles 파일 삭제됨');
|
|
92
|
+
}
|
|
93
|
+
const tippecanoeCmd = [
|
|
94
|
+
'-o',
|
|
95
|
+
MBTILES_PATH,
|
|
96
|
+
'-z10',
|
|
97
|
+
'-Z5',
|
|
98
|
+
'--force',
|
|
99
|
+
'--maximum-zoom=10',
|
|
100
|
+
'--minimum-zoom=0',
|
|
101
|
+
'--drop-smallest-as-needed',
|
|
102
|
+
'--layer=countries',
|
|
103
|
+
COUNTRIES_GEO_JSON
|
|
104
|
+
];
|
|
105
|
+
console.log('Tippecanoe 명령 실행 중...');
|
|
106
|
+
console.log(`tippecanoe ${tippecanoeCmd.join(' ')}`);
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const process = (0, child_process_1.spawn)('tippecanoe', tippecanoeCmd);
|
|
109
|
+
process.stdout.on('data', data => {
|
|
110
|
+
console.log(`[tippecanoe]: ${data.toString()}`);
|
|
111
|
+
});
|
|
112
|
+
process.stderr.on('data', data => {
|
|
113
|
+
console.error(`[tippecanoe ERROR]: ${data.toString()}`);
|
|
114
|
+
});
|
|
115
|
+
process.on('close', code => {
|
|
116
|
+
if (code === 0) {
|
|
117
|
+
console.log('MBTiles 변환 완료 ✅');
|
|
118
|
+
resolve(true);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.error(`Tippecanoe 종료 코드: ${code}`);
|
|
122
|
+
resolve(false);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
process.on('error', err => {
|
|
126
|
+
console.error(`Tippecanoe 실행 오류: ${err}`);
|
|
127
|
+
reject(err);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error('MBTiles 변환 오류:', error);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
// MBTiles 소스
|
|
137
|
+
let tileSource = null;
|
|
138
|
+
// MBTiles 소스 로드
|
|
139
|
+
const loadTileSource = async () => {
|
|
140
|
+
if (tileSource)
|
|
141
|
+
return tileSource;
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
new mbtiles_1.default(`mbtiles://${MBTILES_PATH}`, (err, source) => {
|
|
144
|
+
if (err) {
|
|
145
|
+
console.error('MBTiles 로드 오류:', err);
|
|
146
|
+
return reject(err);
|
|
147
|
+
}
|
|
148
|
+
tileSource = source;
|
|
149
|
+
console.log('MBTiles 소스 로드 완료');
|
|
150
|
+
resolve(source);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
// 타일 서비스 초기화
|
|
155
|
+
const initializeTileService = async () => {
|
|
156
|
+
try {
|
|
157
|
+
// MBTiles 파일이 없으면 생성
|
|
158
|
+
if (!fs.existsSync(MBTILES_PATH)) {
|
|
159
|
+
console.log('MBTiles 파일이 없습니다. 생성을 시작합니다...');
|
|
160
|
+
const result = await convertGeoJsonToMBTiles();
|
|
161
|
+
if (!result)
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
// MBTiles 소스 로드
|
|
165
|
+
await loadTileSource();
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.error('타일 서비스 초기화 오류:', error);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
// API 엔드포인트: 타일 재생성 트리거
|
|
174
|
+
exports.geoRouter.post('/tiles/regenerate', async (ctx) => {
|
|
175
|
+
try {
|
|
176
|
+
console.log('타일 재생성 요청 받음');
|
|
177
|
+
const result = await convertGeoJsonToMBTiles();
|
|
178
|
+
if (result) {
|
|
179
|
+
// 새 타일소스 로드
|
|
180
|
+
await loadTileSource();
|
|
181
|
+
ctx.body = { success: true, message: 'MBTiles 타일 생성 완료' };
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
ctx.status = 500;
|
|
185
|
+
ctx.body = { success: false, message: 'MBTiles 타일 생성 실패' };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error('타일 재생성 오류:', error);
|
|
190
|
+
ctx.status = 500;
|
|
191
|
+
ctx.body = { success: false, message: '서버 오류 발생' };
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
exports.geoRouter.get('/tiles/:z/:x/:y.mvt', async (ctx) => {
|
|
195
|
+
try {
|
|
196
|
+
const z = parseInt(ctx.params.z, 10);
|
|
197
|
+
const x = parseInt(ctx.params.x, 10);
|
|
198
|
+
const y = parseInt(ctx.params.y, 10);
|
|
199
|
+
// 타일 서비스 로드 확인
|
|
200
|
+
if (!tileSource) {
|
|
201
|
+
console.log(`🚀 타일 서비스 로드 중...`);
|
|
202
|
+
const initialized = await initializeTileService();
|
|
203
|
+
if (!initialized) {
|
|
204
|
+
ctx.status = 500;
|
|
205
|
+
ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' };
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
await new Promise((resolve, reject) => {
|
|
210
|
+
tileSource.getTile(z, x, y, (err, data, headers) => {
|
|
211
|
+
if (err) {
|
|
212
|
+
if (err.message.includes('Tile does not exist')) {
|
|
213
|
+
ctx.status = 204;
|
|
214
|
+
console.log(`🚨 타일 없음: z=${z}, x=${x}, y=${y}`);
|
|
215
|
+
return resolve(null);
|
|
216
|
+
}
|
|
217
|
+
console.error(`❌ 타일 로드 오류 (z:${z}, x:${x}, y:${y}):`, err);
|
|
218
|
+
ctx.status = 500;
|
|
219
|
+
ctx.body = { error: '타일을 로드할 수 없습니다.' };
|
|
220
|
+
return resolve(null);
|
|
221
|
+
}
|
|
222
|
+
ctx.set('Content-Type', headers['Content-Type'] || 'application/x-protobuf');
|
|
223
|
+
ctx.set({
|
|
224
|
+
'Access-Control-Allow-Origin': '*',
|
|
225
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate, proxy-revalidate',
|
|
226
|
+
'Content-Encoding': 'gzip'
|
|
227
|
+
});
|
|
228
|
+
ctx.body = data;
|
|
229
|
+
resolve(null);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.error('❌ 타일 서비스 오류:', error);
|
|
235
|
+
ctx.status = 500;
|
|
236
|
+
ctx.body = { error: '서버 오류가 발생했습니다.' };
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
exports.geoRouter.get('/tiles/metadata', async (ctx) => {
|
|
240
|
+
try {
|
|
241
|
+
if (!tileSource) {
|
|
242
|
+
const initialized = await initializeTileService();
|
|
243
|
+
if (!initialized) {
|
|
244
|
+
ctx.status = 500;
|
|
245
|
+
ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' };
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
await new Promise((resolve, reject) => {
|
|
250
|
+
tileSource.getInfo((err, info) => {
|
|
251
|
+
if (err) {
|
|
252
|
+
console.error('메타데이터 로드 오류:', err);
|
|
253
|
+
ctx.status = 500;
|
|
254
|
+
ctx.body = { error: '메타데이터를 로드할 수 없습니다.' };
|
|
255
|
+
return resolve(null);
|
|
256
|
+
}
|
|
257
|
+
ctx.body = info;
|
|
258
|
+
resolve(null);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
console.error('메타데이터 서비스 오류:', error);
|
|
264
|
+
ctx.status = 500;
|
|
265
|
+
ctx.body = { error: '서버 오류가 발생했습니다.' };
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
initializeTileService().then(success => {
|
|
269
|
+
if (success) {
|
|
270
|
+
console.log('🗺️ 타일 서비스가 준비되었습니다.');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.error('❌ 타일 서비스 초기화에 실패했습니다.');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
exports.default = exports.geoRouter;
|
|
277
|
+
//# sourceMappingURL=geo-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geo-router.js","sourceRoot":"","sources":["../../server/routers/geo-router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview\n * Koa 기반의 GeoJSON → MBTiles 변환 및 VectorTile(MVT) 서비스 제공 라우터\n *\n * 이 라우터는 GeoJSON 데이터를 MBTiles 형식으로 변환하고, OpenLayers 등의 클라이언트에서\n * Vector Tile(MVT) 형식으로 요청할 수 있도록 API 엔드포인트를 제공합니다.\n *\n * 📌 주요 기능:\n * 1️⃣ `/tiles/regenerate` (POST) → GeoJSON을 다시 MBTiles로 변환\n * 2️⃣ `/tiles/:z/:x/:y.mvt` (GET) → 특정 타일 데이터를 반환\n * 3️⃣ `/tiles/metadata` (GET) → MBTiles의 메타데이터 반환\n *\n * 🚀 서비스 초기화 흐름:\n * 1. 서버 시작 시 `initializeTileService()` 호출\n * - MBTiles 파일이 없으면 `convertGeoJsonToMBTiles()` 실행하여 생성\n * - MBTiles가 존재하면 `loadTileSource()`를 호출하여 데이터 로드\n * 2. 클라이언트가 MVT 타일을 요청하면 `tileSource.getTile(z, x, y, callback)`을 통해 데이터 반환\n *\n * 📌 주요 처리 로직:\n * ✅ **GeoJSON → MBTiles 변환 (`convertGeoJsonToMBTiles`)**\n * - `tippecanoe` CLI를 사용하여 GeoJSON을 MBTiles로 변환\n * - 기존 MBTiles 파일이 있으면 삭제 후 재생성\n * - `spawn`을 사용하여 비동기적으로 실행하며 로그 출력\n *\n * ✅ **MBTiles 파일을 로드 (`loadTileSource`)**\n * - `@mapbox/mbtiles` 라이브러리를 사용하여 MBTiles 데이터베이스를 로드\n * - 타일 요청이 있을 때 해당 타일을 `tileSource.getTile()`을 통해 제공\n *\n * ✅ **타일 요청 처리 (`/tiles/:z/:x/:y.mvt`)**\n * - MBTiles에서 특정 타일을 찾아 바이너리 MVT 데이터로 반환\n * - `Content-Encoding: gzip` 설정으로 압축된 타일 제공\n * - `TMS 좌표 변환` 문제 제거하여 직접 요청 가능하도록 개선\n *\n * ✅ **타일셋 메타데이터 반환 (`/tiles/metadata`)**\n * - MBTiles 파일에서 메타데이터 정보를 가져와 JSON 형태로 반환\n *\n * 📌 종속성:\n * - `koa-router`: Koa 기반 API 라우팅\n * - `fs`, `path`: 파일 시스템 접근 및 경로 처리\n * - `child_process`, `util`: `tippecanoe` 실행을 위한 비동기 프로세스 실행\n * - `@mapbox/mbtiles`: MBTiles 데이터베이스 접근 라이브러리\n * - `@things-factory/env`: 환경 변수 관리\n *\n * 📌 디렉토리 구조:\n * ```\n * /cache/mvt-tiles/ # 생성된 MBTiles 저장\n * /data/geo-json/ # 원본 GeoJSON 파일 저장\n * ```\n *\n * 📌 실행 방법:\n * 1. GeoJSON 데이터 준비\n * 2. Koa 서버 실행 → 자동으로 MBTiles 변환 후 서비스 시작\n * 3. 클라이언트(OpenLayers 등)에서 VectorTile 요청 가능\n * 4. 필요 시 `/tiles/regenerate` 엔드포인트로 타일 재생성\n *\n * @module geoRouter\n */\n\nimport Router from 'koa-router'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport * as child_process from 'child_process'\nimport * as util from 'util'\nimport { config } from '@things-factory/env'\nimport mbtiles from '@mapbox/mbtiles'\n\nconst exec = util.promisify(child_process.exec)\n\nexport const geoRouter = new Router()\n\nconst TILE_CACHE_DIR = path.resolve(config.get('directory', './cache'), 'mvt-tiles')\nconst GEO_DATA_DIR = path.resolve(__dirname, '..', '..', 'data', 'geo-json')\n\nif (!fs.existsSync(TILE_CACHE_DIR)) {\n fs.mkdirSync(TILE_CACHE_DIR, { recursive: true })\n}\nif (!fs.existsSync(GEO_DATA_DIR)) {\n fs.mkdirSync(GEO_DATA_DIR, { recursive: true })\n}\n\nconst COUNTRIES_GEO_JSON = path.resolve(GEO_DATA_DIR, 'countries-max-geo.json')\nconst MBTILES_PATH = path.resolve(TILE_CACHE_DIR, 'countries.mbtiles')\n\nimport { spawn } from 'child_process'\n\nconst convertGeoJsonToMBTiles = async () => {\n // tippecanoe -o ./packages/operato-ecs/cache/mvt-tiles/countries.mbtiles -z10 -Z5 --force --maximum-zoom=10 --minimum-zoom=0 --drop-smallest-as-needed --layer=countries ./packages/weather/data/geo-json/countries-max-geo.json\n try {\n if (!fs.existsSync(COUNTRIES_GEO_JSON)) {\n console.error('GeoJSON 파일을 찾을 수 없습니다:', COUNTRIES_GEO_JSON)\n return false\n }\n\n if (fs.existsSync(MBTILES_PATH)) {\n fs.unlinkSync(MBTILES_PATH)\n console.log('기존 MBTiles 파일 삭제됨')\n }\n\n const tippecanoeCmd = [\n '-o',\n MBTILES_PATH,\n '-z10',\n '-Z5',\n '--force',\n '--maximum-zoom=10',\n '--minimum-zoom=0',\n '--drop-smallest-as-needed',\n '--layer=countries',\n COUNTRIES_GEO_JSON\n ]\n\n console.log('Tippecanoe 명령 실행 중...')\n console.log(`tippecanoe ${tippecanoeCmd.join(' ')}`)\n\n return new Promise((resolve, reject) => {\n const process = spawn('tippecanoe', tippecanoeCmd)\n\n process.stdout.on('data', data => {\n console.log(`[tippecanoe]: ${data.toString()}`)\n })\n\n process.stderr.on('data', data => {\n console.error(`[tippecanoe ERROR]: ${data.toString()}`)\n })\n\n process.on('close', code => {\n if (code === 0) {\n console.log('MBTiles 변환 완료 ✅')\n resolve(true)\n } else {\n console.error(`Tippecanoe 종료 코드: ${code}`)\n resolve(false)\n }\n })\n\n process.on('error', err => {\n console.error(`Tippecanoe 실행 오류: ${err}`)\n reject(err)\n })\n })\n } catch (error) {\n console.error('MBTiles 변환 오류:', error)\n return false\n }\n}\n\n// MBTiles 소스\nlet tileSource = null\n\n// MBTiles 소스 로드\nconst loadTileSource = async () => {\n if (tileSource) return tileSource\n\n return new Promise((resolve, reject) => {\n new mbtiles(`mbtiles://${MBTILES_PATH}`, (err, source) => {\n if (err) {\n console.error('MBTiles 로드 오류:', err)\n return reject(err)\n }\n\n tileSource = source\n console.log('MBTiles 소스 로드 완료')\n resolve(source)\n })\n })\n}\n\n// 타일 서비스 초기화\nconst initializeTileService = async () => {\n try {\n // MBTiles 파일이 없으면 생성\n if (!fs.existsSync(MBTILES_PATH)) {\n console.log('MBTiles 파일이 없습니다. 생성을 시작합니다...')\n const result = await convertGeoJsonToMBTiles()\n if (!result) return false\n }\n\n // MBTiles 소스 로드\n await loadTileSource()\n return true\n } catch (error) {\n console.error('타일 서비스 초기화 오류:', error)\n return false\n }\n}\n\n// API 엔드포인트: 타일 재생성 트리거\ngeoRouter.post('/tiles/regenerate', async ctx => {\n try {\n console.log('타일 재생성 요청 받음')\n const result = await convertGeoJsonToMBTiles()\n\n if (result) {\n // 새 타일소스 로드\n await loadTileSource()\n ctx.body = { success: true, message: 'MBTiles 타일 생성 완료' }\n } else {\n ctx.status = 500\n ctx.body = { success: false, message: 'MBTiles 타일 생성 실패' }\n }\n } catch (error) {\n console.error('타일 재생성 오류:', error)\n ctx.status = 500\n ctx.body = { success: false, message: '서버 오류 발생' }\n }\n})\n\ngeoRouter.get('/tiles/:z/:x/:y.mvt', async ctx => {\n try {\n const z = parseInt(ctx.params.z, 10)\n const x = parseInt(ctx.params.x, 10)\n const y = parseInt(ctx.params.y, 10)\n\n // 타일 서비스 로드 확인\n if (!tileSource) {\n console.log(`🚀 타일 서비스 로드 중...`)\n const initialized = await initializeTileService()\n if (!initialized) {\n ctx.status = 500\n ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' }\n return\n }\n }\n\n await new Promise((resolve, reject) => {\n tileSource.getTile(z, x, y, (err, data, headers) => {\n if (err) {\n if (err.message.includes('Tile does not exist')) {\n ctx.status = 204\n console.log(`🚨 타일 없음: z=${z}, x=${x}, y=${y}`)\n return resolve(null)\n }\n\n console.error(`❌ 타일 로드 오류 (z:${z}, x:${x}, y:${y}):`, err)\n ctx.status = 500\n ctx.body = { error: '타일을 로드할 수 없습니다.' }\n return resolve(null)\n }\n\n ctx.set('Content-Type', headers['Content-Type'] || 'application/x-protobuf')\n ctx.set({\n 'Access-Control-Allow-Origin': '*',\n 'Cache-Control': 'no-cache, no-store, must-revalidate, proxy-revalidate',\n 'Content-Encoding': 'gzip'\n })\n\n ctx.body = data\n resolve(null)\n })\n })\n } catch (error) {\n console.error('❌ 타일 서비스 오류:', error)\n ctx.status = 500\n ctx.body = { error: '서버 오류가 발생했습니다.' }\n }\n})\n\ngeoRouter.get('/tiles/metadata', async ctx => {\n try {\n if (!tileSource) {\n const initialized = await initializeTileService()\n if (!initialized) {\n ctx.status = 500\n ctx.body = { error: '타일 서비스를 초기화할 수 없습니다.' }\n return\n }\n }\n\n await new Promise((resolve, reject) => {\n tileSource.getInfo((err, info) => {\n if (err) {\n console.error('메타데이터 로드 오류:', err)\n ctx.status = 500\n ctx.body = { error: '메타데이터를 로드할 수 없습니다.' }\n return resolve(null)\n }\n\n ctx.body = info\n resolve(null)\n })\n })\n } catch (error) {\n console.error('메타데이터 서비스 오류:', error)\n ctx.status = 500\n ctx.body = { error: '서버 오류가 발생했습니다.' }\n }\n})\n\ninitializeTileService().then(success => {\n if (success) {\n console.log('🗺️ 타일 서비스가 준비되었습니다.')\n } else {\n console.error('❌ 타일 서비스 초기화에 실패했습니다.')\n }\n})\n\nexport default geoRouter\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const weatherRouter: any;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.weatherRouter = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
|
|
6
|
+
const weather_controller_1 = require("../controllers/weather-controller");
|
|
7
|
+
exports.weatherRouter = new koa_router_1.default();
|
|
8
|
+
// 전체 날씨 지도 이미지 엔드포인트
|
|
9
|
+
exports.weatherRouter.get('/map/:type.png', weather_controller_1.globalWeatherController);
|
|
10
|
+
//# sourceMappingURL=weather-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weather-router.js","sourceRoot":"","sources":["../../server/routers/weather-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAE/B,0EAA2E;AAE9D,QAAA,aAAa,GAAG,IAAI,oBAAM,EAAE,CAAA;AAEzC,qBAAqB;AACrB,qBAAa,CAAC,GAAG,CAAC,gBAAgB,EAAE,4CAAuB,CAAC,CAAA","sourcesContent":["import Router from 'koa-router'\n\nimport { globalWeatherController } from '../controllers/weather-controller'\n\nexport const weatherRouter = new Router()\n\n// 전체 날씨 지도 이미지 엔드포인트\nweatherRouter.get('/map/:type.png', globalWeatherController)\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const headless_pool_js_1 = require("./services/headless-pool.js");
|
|
4
|
+
const weather_router_js_1 = require("./routers/weather-router.js");
|
|
5
|
+
const geo_router_js_1 = require("./routers/geo-router.js");
|
|
6
|
+
process.on('bootstrap-module-global-public-route', (app, globalPublicRouter) => {
|
|
7
|
+
(0, headless_pool_js_1.getBrowserPool)();
|
|
8
|
+
globalPublicRouter.use('/weather', weather_router_js_1.weatherRouter.routes(), weather_router_js_1.weatherRouter.allowedMethods());
|
|
9
|
+
globalPublicRouter.use('/geo', geo_router_js_1.geoRouter.routes(), geo_router_js_1.geoRouter.allowedMethods());
|
|
10
|
+
});
|
|
11
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../server/routes.ts"],"names":[],"mappings":";;AAAA,kEAAyF;AACzF,mEAA2D;AAC3D,2DAAmD;AAEnD,OAAO,CAAC,EAAE,CAAC,sCAA6C,EAAE,CAAC,GAAG,EAAE,kBAAkB,EAAE,EAAE;IACpF,IAAA,iCAAyB,GAAE,CAAA;IAE3B,kBAAkB,CAAC,GAAG,CAAC,UAAU,EAAE,iCAAa,CAAC,MAAM,EAAE,EAAE,iCAAa,CAAC,cAAc,EAAE,CAAC,CAAA;IAC1F,kBAAkB,CAAC,GAAG,CAAC,MAAM,EAAE,yBAAS,CAAC,MAAM,EAAE,EAAE,yBAAS,CAAC,cAAc,EAAE,CAAC,CAAA;AAChF,CAAC,CAAC,CAAA","sourcesContent":["import { getBrowserPool as getHeadlessPoolForWeather } from './services/headless-pool.js'\nimport { weatherRouter } from './routers/weather-router.js'\nimport { geoRouter } from './routers/geo-router.js'\n\nprocess.on('bootstrap-module-global-public-route' as any, (app, globalPublicRouter) => {\n getHeadlessPoolForWeather()\n\n globalPublicRouter.use('/weather', weatherRouter.routes(), weatherRouter.allowedMethods())\n globalPublicRouter.use('/geo', geoRouter.routes(), geoRouter.allowedMethods())\n})\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function renderWeatherMap(type: string, data: {
|
|
2
|
+
lat: number;
|
|
3
|
+
lng: number;
|
|
4
|
+
value: number;
|
|
5
|
+
description: number;
|
|
6
|
+
temperature: number;
|
|
7
|
+
humidity: number;
|
|
8
|
+
cloudCoverage: number;
|
|
9
|
+
solarRadiation: number;
|
|
10
|
+
windSpeed: number;
|
|
11
|
+
windDirection: number;
|
|
12
|
+
precipitation?: number;
|
|
13
|
+
}[], width?: number, height?: number): Promise<Buffer>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderWeatherMap = renderWeatherMap;
|
|
4
|
+
const headless_pool_1 = require("./headless-pool");
|
|
5
|
+
async function renderWeatherMap(type, data, width = 1800, height = 900) {
|
|
6
|
+
console.log(`renderWeatherMap 호출됨: 타입=${type}, 데이터 개수=${data.length}`);
|
|
7
|
+
if (!data || data.length === 0) {
|
|
8
|
+
throw new Error('데이터가 없습니다. PNG 생성을 중단합니다.');
|
|
9
|
+
}
|
|
10
|
+
const browser = await (0, headless_pool_1.getBrowserPool)().acquire();
|
|
11
|
+
try {
|
|
12
|
+
console.log('브라우저 인스턴스 획득');
|
|
13
|
+
const page = await browser.newPage();
|
|
14
|
+
// 모든 콘솔 로그를 캡처하여 Node.js 프로세스에 출력
|
|
15
|
+
page.on('console', msg => {
|
|
16
|
+
console.log(`PAGE LOG [${msg.type()}]: ${msg.text()}`);
|
|
17
|
+
});
|
|
18
|
+
// 오류 이벤트도 추적 (예: Unhandled Promise Rejection)
|
|
19
|
+
page.on('pageerror', err => {
|
|
20
|
+
console.error('BROWSER PAGE ERROR:', err);
|
|
21
|
+
});
|
|
22
|
+
await page.setViewport({ width, height });
|
|
23
|
+
await page.setContent(`
|
|
24
|
+
<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<style>body { margin: 0; }</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<canvas id="weatherCanvas" width="${width}" height="${height}"></canvas>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
`);
|
|
34
|
+
console.log('HTML 콘텐츠 설정 완료');
|
|
35
|
+
await page.evaluate((type, data, width, height) => {
|
|
36
|
+
try {
|
|
37
|
+
console.log('Canvas 렌더링 시작');
|
|
38
|
+
const canvas = document.getElementById('weatherCanvas');
|
|
39
|
+
if (!canvas) {
|
|
40
|
+
throw new Error('캔버스를 찾을 수 없습니다.');
|
|
41
|
+
}
|
|
42
|
+
const ctx = canvas.getContext('2d');
|
|
43
|
+
if (!ctx) {
|
|
44
|
+
throw new Error('CanvasRenderingContext2D를 가져올 수 없습니다.');
|
|
45
|
+
}
|
|
46
|
+
ctx.clearRect(0, 0, width, height);
|
|
47
|
+
function latLngToPixel(lat, lng) {
|
|
48
|
+
if (isNaN(lat) || isNaN(lng))
|
|
49
|
+
return null;
|
|
50
|
+
const x = Math.floor(((lng + 180) / 360) * width);
|
|
51
|
+
const y = Math.floor(((90 - lat) / 180) * height);
|
|
52
|
+
if (x < 0 || x >= width || y < 0 || y >= height)
|
|
53
|
+
return null;
|
|
54
|
+
return { x, y };
|
|
55
|
+
}
|
|
56
|
+
function valueToRGB(type, dataPoint) {
|
|
57
|
+
if (!dataPoint || typeof dataPoint.value !== 'number') {
|
|
58
|
+
console.warn('잘못된 데이터 포인트:', dataPoint);
|
|
59
|
+
return { r: 0, g: 0, b: 0, a: 0 };
|
|
60
|
+
}
|
|
61
|
+
switch (type) {
|
|
62
|
+
case 'temperature':
|
|
63
|
+
return { r: Math.min(255, Math.max(0, (dataPoint.value + 50) * 2.55)), g: 0, b: 0, a: 255 };
|
|
64
|
+
case 'humidity':
|
|
65
|
+
return { r: 0, g: Math.min(255, Math.max(0, dataPoint.humidity * 2.55)), b: 0, a: 255 };
|
|
66
|
+
case 'wind':
|
|
67
|
+
return { r: Math.min(255, Math.max(0, dataPoint.windSpeed * 5)), g: 0, b: 255, a: 255 };
|
|
68
|
+
case 'precipitation':
|
|
69
|
+
return { r: 0, g: 0, b: Math.min(255, Math.max(0, dataPoint.precipitation * 2.55)), a: 255 };
|
|
70
|
+
default:
|
|
71
|
+
return { r: 255, g: 255, b: 255, a: 255 };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const imageData = ctx.createImageData(width, height);
|
|
75
|
+
data.forEach(point => {
|
|
76
|
+
const pixel = latLngToPixel(point.lat, point.lng);
|
|
77
|
+
if (!pixel)
|
|
78
|
+
return;
|
|
79
|
+
const color = valueToRGB(type, point);
|
|
80
|
+
const index = (pixel.y * width + pixel.x) * 4;
|
|
81
|
+
if (index >= 0 && index < imageData.data.length) {
|
|
82
|
+
imageData.data[index] = color.r;
|
|
83
|
+
imageData.data[index + 1] = color.g;
|
|
84
|
+
imageData.data[index + 2] = color.b;
|
|
85
|
+
imageData.data[index + 3] = color.a;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.warn('잘못된 인덱스:', index);
|
|
89
|
+
}
|
|
90
|
+
/* only for test */
|
|
91
|
+
for (let dy = -18; dy <= 18; dy++) {
|
|
92
|
+
for (let dx = -18; dx <= 18; dx++) {
|
|
93
|
+
const fillIndex = ((pixel.y + dy) * width + (pixel.x + dx)) * 4;
|
|
94
|
+
if (fillIndex >= 0 && fillIndex < imageData.data.length) {
|
|
95
|
+
imageData.data[fillIndex] = color.r;
|
|
96
|
+
imageData.data[fillIndex + 1] = color.g;
|
|
97
|
+
imageData.data[fillIndex + 2] = color.b;
|
|
98
|
+
imageData.data[fillIndex + 3] = color.a;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
ctx.putImageData(imageData, 0, 0);
|
|
104
|
+
console.log('Canvas 렌더링 완료');
|
|
105
|
+
window.renderComplete = true;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error('렌더링 오류:', err);
|
|
109
|
+
window.renderComplete = false;
|
|
110
|
+
}
|
|
111
|
+
}, type, data, width, height);
|
|
112
|
+
console.log('렌더링 완료');
|
|
113
|
+
const renderComplete = await page
|
|
114
|
+
.waitForFunction('window.renderComplete === true', { timeout: 10000 })
|
|
115
|
+
.catch(err => {
|
|
116
|
+
console.error('렌더링 완료 신호 감지 실패:', err);
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
if (!renderComplete) {
|
|
120
|
+
throw new Error('렌더링이 완료되지 않았습니다.');
|
|
121
|
+
}
|
|
122
|
+
const pngBuffer = await page.screenshot({
|
|
123
|
+
type: 'png',
|
|
124
|
+
omitBackground: true
|
|
125
|
+
});
|
|
126
|
+
console.log(`PNG 생성 완료: ${pngBuffer.length} 바이트`);
|
|
127
|
+
return pngBuffer;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error('날씨 맵 렌더링 오류:', error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await (0, headless_pool_1.getBrowserPool)().release(browser);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=chrome-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome-renderer.js","sourceRoot":"","sources":["../../server/services/chrome-renderer.ts"],"names":[],"mappings":";;AAEA,4CA4KC;AA9KD,mDAAgD;AAEzC,KAAK,UAAU,gBAAgB,CACpC,IAAY,EACZ,IAYG,EACH,QAAgB,IAAI,EACpB,SAAiB,GAAG;IAEpB,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,YAAY,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IAEtE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAA;IAC9C,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,IAAA,8BAAc,GAAE,CAAC,OAAO,EAAE,CAAA;IAEhD,IAAI,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;QAC3B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;QAEpC,kCAAkC;QAClC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE;YACvB,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACxD,CAAC,CAAC,CAAA;QAEF,8CAA8C;QAC9C,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE;YACzB,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAA;QAC3C,CAAC,CAAC,CAAA;QAEF,MAAM,IAAI,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;QAEzC,MAAM,IAAI,CAAC,UAAU,CAAC;;;;;;;8CAOoB,KAAK,aAAa,MAAM;;;KAGjE,CAAC,CAAA;QAEF,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QAE7B,MAAM,IAAI,CAAC,QAAQ,CACjB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAC5B,IAAI,CAAC;gBACH,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;gBAE5B,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAsB,CAAA;gBAC5E,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;gBACpC,CAAC;gBAED,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;gBACnC,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;gBAC1D,CAAC;gBAED,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;gBAElC,SAAS,aAAa,CAAC,GAAG,EAAE,GAAG;oBAC7B,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC;wBAAE,OAAO,IAAI,CAAA;oBACzC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;oBACjD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC,CAAA;oBACjD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,MAAM;wBAAE,OAAO,IAAI,CAAA;oBAC5D,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;gBACjB,CAAC;gBAED,SAAS,UAAU,CAAC,IAAI,EAAE,SAAS;oBACjC,IAAI,CAAC,SAAS,IAAI,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;wBACtD,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,CAAA;wBACvC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;oBACnC,CAAC;oBACD,QAAQ,IAAI,EAAE,CAAC;wBACb,KAAK,aAAa;4BAChB,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,KAAK,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAA;wBAC7F,KAAK,UAAU;4BACb,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAA;wBACzF,KAAK,MAAM;4BACT,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAA;wBACzF,KAAK,eAAe;4BAClB,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAA;wBAC9F;4BACE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAA;oBAC7C,CAAC;gBACH,CAAC;gBAED,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;gBAEpD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;oBACnB,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;oBACjD,IAAI,CAAC,KAAK;wBAAE,OAAM;oBAElB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;oBACrC,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;oBAE7C,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;wBAChD,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;wBAC/B,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;wBACnC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;wBACnC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;oBACrC,CAAC;yBAAM,CAAC;wBACN,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;oBACjC,CAAC;oBAED,mBAAmB;oBACnB,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;wBAClC,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;4BAClC,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;4BAC/D,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gCACxD,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;gCACnC,SAAS,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;gCACvC,SAAS,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;gCACvC,SAAS,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;4BACzC,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC,CAAC,CAAA;gBAEF,GAAG,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;gBACjC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAC3B;gBAAC,MAAc,CAAC,cAAc,GAAG,IAAI,CAAA;YACxC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,CAC5B;gBAAC,MAAc,CAAC,cAAc,GAAG,KAAK,CAAA;YACzC,CAAC;QACH,CAAC,EACD,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAM,CACP,CAAA;QAED,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAErB,MAAM,cAAc,GAAG,MAAM,IAAI;aAC9B,eAAe,CAAC,gCAAgC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;aACrE,KAAK,CAAC,GAAG,CAAC,EAAE;YACX,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAA;YACtC,OAAO,KAAK,CAAA;QACd,CAAC,CAAC,CAAA;QAEJ,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACrC,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC;YACtC,IAAI,EAAE,KAAK;YACX,cAAc,EAAE,IAAI;SACrB,CAAC,CAAA;QAEF,OAAO,CAAC,GAAG,CAAC,cAAc,SAAS,CAAC,MAAM,MAAM,CAAC,CAAA;QACjD,OAAO,SAAS,CAAA;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAA;QACpC,MAAM,KAAK,CAAA;IACb,CAAC;YAAS,CAAC;QACT,MAAM,IAAA,8BAAc,GAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC;AACH,CAAC","sourcesContent":["import { getBrowserPool } from './headless-pool'\n\nexport async function renderWeatherMap(\n type: string,\n data: {\n lat: number\n lng: number\n value: number\n description: number\n temperature: number\n humidity: number\n cloudCoverage: number\n solarRadiation: number\n windSpeed: number\n windDirection: number\n precipitation?: number\n }[],\n width: number = 1800,\n height: number = 900\n): Promise<Buffer> {\n console.log(`renderWeatherMap 호출됨: 타입=${type}, 데이터 개수=${data.length}`)\n\n if (!data || data.length === 0) {\n throw new Error('데이터가 없습니다. PNG 생성을 중단합니다.')\n }\n\n const browser = await getBrowserPool().acquire()\n\n try {\n console.log('브라우저 인스턴스 획득')\n const page = await browser.newPage()\n\n // 모든 콘솔 로그를 캡처하여 Node.js 프로세스에 출력\n page.on('console', msg => {\n console.log(`PAGE LOG [${msg.type()}]: ${msg.text()}`)\n })\n\n // 오류 이벤트도 추적 (예: Unhandled Promise Rejection)\n page.on('pageerror', err => {\n console.error('BROWSER PAGE ERROR:', err)\n })\n\n await page.setViewport({ width, height })\n\n await page.setContent(`\n <!DOCTYPE html>\n <html>\n <head>\n <style>body { margin: 0; }</style>\n </head>\n <body>\n <canvas id=\"weatherCanvas\" width=\"${width}\" height=\"${height}\"></canvas>\n </body>\n </html>\n `)\n\n console.log('HTML 콘텐츠 설정 완료')\n\n await page.evaluate(\n (type, data, width, height) => {\n try {\n console.log('Canvas 렌더링 시작')\n\n const canvas = document.getElementById('weatherCanvas') as HTMLCanvasElement\n if (!canvas) {\n throw new Error('캔버스를 찾을 수 없습니다.')\n }\n\n const ctx = canvas.getContext('2d')\n if (!ctx) {\n throw new Error('CanvasRenderingContext2D를 가져올 수 없습니다.')\n }\n\n ctx.clearRect(0, 0, width, height)\n\n function latLngToPixel(lat, lng) {\n if (isNaN(lat) || isNaN(lng)) return null\n const x = Math.floor(((lng + 180) / 360) * width)\n const y = Math.floor(((90 - lat) / 180) * height)\n if (x < 0 || x >= width || y < 0 || y >= height) return null\n return { x, y }\n }\n\n function valueToRGB(type, dataPoint) {\n if (!dataPoint || typeof dataPoint.value !== 'number') {\n console.warn('잘못된 데이터 포인트:', dataPoint)\n return { r: 0, g: 0, b: 0, a: 0 }\n }\n switch (type) {\n case 'temperature':\n return { r: Math.min(255, Math.max(0, (dataPoint.value + 50) * 2.55)), g: 0, b: 0, a: 255 }\n case 'humidity':\n return { r: 0, g: Math.min(255, Math.max(0, dataPoint.humidity * 2.55)), b: 0, a: 255 }\n case 'wind':\n return { r: Math.min(255, Math.max(0, dataPoint.windSpeed * 5)), g: 0, b: 255, a: 255 }\n case 'precipitation':\n return { r: 0, g: 0, b: Math.min(255, Math.max(0, dataPoint.precipitation * 2.55)), a: 255 }\n default:\n return { r: 255, g: 255, b: 255, a: 255 }\n }\n }\n\n const imageData = ctx.createImageData(width, height)\n\n data.forEach(point => {\n const pixel = latLngToPixel(point.lat, point.lng)\n if (!pixel) return\n\n const color = valueToRGB(type, point)\n const index = (pixel.y * width + pixel.x) * 4\n\n if (index >= 0 && index < imageData.data.length) {\n imageData.data[index] = color.r\n imageData.data[index + 1] = color.g\n imageData.data[index + 2] = color.b\n imageData.data[index + 3] = color.a\n } else {\n console.warn('잘못된 인덱스:', index)\n }\n\n /* only for test */\n for (let dy = -18; dy <= 18; dy++) {\n for (let dx = -18; dx <= 18; dx++) {\n const fillIndex = ((pixel.y + dy) * width + (pixel.x + dx)) * 4\n if (fillIndex >= 0 && fillIndex < imageData.data.length) {\n imageData.data[fillIndex] = color.r\n imageData.data[fillIndex + 1] = color.g\n imageData.data[fillIndex + 2] = color.b\n imageData.data[fillIndex + 3] = color.a\n }\n }\n }\n })\n\n ctx.putImageData(imageData, 0, 0)\n console.log('Canvas 렌더링 완료')\n ;(window as any).renderComplete = true\n } catch (err) {\n console.error('렌더링 오류:', err)\n ;(window as any).renderComplete = false\n }\n },\n type,\n data,\n width,\n height\n )\n\n console.log('렌더링 완료')\n\n const renderComplete = await page\n .waitForFunction('window.renderComplete === true', { timeout: 10000 })\n .catch(err => {\n console.error('렌더링 완료 신호 감지 실패:', err)\n return false\n })\n\n if (!renderComplete) {\n throw new Error('렌더링이 완료되지 않았습니다.')\n }\n\n const pngBuffer = await page.screenshot({\n type: 'png',\n omitBackground: true\n })\n\n console.log(`PNG 생성 완료: ${pngBuffer.length} 바이트`)\n return pngBuffer\n } catch (error) {\n console.error('날씨 맵 렌더링 오류:', error)\n throw error\n } finally {\n await getBrowserPool().release(browser)\n }\n}\n"]}
|