@fmsim/figure-server 0.0.0-stage → 3.0.0-beta.2

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/README.md CHANGED
@@ -1,3 +1,70 @@
1
- # Temporary Holding Version
1
+ # @fmsim/figure-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@fmsim/figure-server`는 MCS-LV 3차 고도화를 위한 **독립 Figure & 3D 그래픽 자산 관리 Node.js 마이크로서비스**입니다.
4
+
5
+ ---
6
+
7
+ ## 1. 아키텍처 특징
8
+
9
+ 1. **things-factory 생태계 활용 & Koa2 탈피**:
10
+ - 기존 things-factory의 2D/3D Scene 컴포넌트 모델(`Things Scene`, `Board-Service`) 데이터 규격을 완벽히 수용합니다.
11
+ - 단, 서버 프레임워크는 레거시 Koa2에 종속되지 않고, 고성능 경량 Node.js (Express 기반) RESTful 아키텍처로 독립 구성되었습니다.
12
+ 2. **Application 단 React 지원**:
13
+ - Figure 저작 화면(공정 설비 모델러, Carrier 프로퍼티 모델러) 및 자산 카탈로그 UI는 **React** 기반 컴포넌트로 자유롭게 구현 및 임베딩 가능합니다.
14
+ - 캔버스 영역은 Things Scene 2D 및 Three.js 3D 뷰포트를 React Wrapper 형태로 바인딩합니다.
15
+ 3. **독립 프로세스 & 정적 스트리밍**:
16
+ - 기본 포트 `5050`에서 단독 실행되며, 메인 MCS-LV 관제 서버(`fmsim-restful`)의 가동 여부와 관계없이 3D 바이너리(glTF/GLB) 및 썸네일을 0ms 지연으로 스트리밍 서빙합니다.
17
+
18
+ ---
19
+
20
+ ## 2. 패키지 디렉토리 구조
21
+
22
+ ```text
23
+ packages/figure-server/
24
+ ├── package.json # @fmsim/figure-server
25
+ ├── tsconfig.json # TypeScript 컴파일 설정
26
+ ├── src/
27
+ │ ├── index.ts # 서버 진입점 (포트: 5050, 0.0.0.0)
28
+ │ ├── app.ts # Express 애플리케이션 및 CORS/미들웨어 설정
29
+ │ ├── types/
30
+ │ │ └── figure.ts # 자산 엔티티 및 DTO 스키마
31
+ │ ├── storage/
32
+ │ │ └── figure-storage.ts # 메타데이터 JSON 및 바이너리 로컬 스토리지
33
+ │ └── routes/
34
+ │ └── figure-router.ts # 자산 RESTful CRUD & 스트리밍 라우터
35
+ ├── data/ # 런타임 자산 바이너리 저장소
36
+ │ ├── figures-metadata.json # 메타데이터 카탈로그
37
+ │ └── {componentType}/ # 자산별 glTF/GLB 모델 및 thumbnail.png
38
+ └── dist/ # 빌드 산출물
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 3. 실행 및 빌드
44
+
45
+ ```bash
46
+ # 빌드
47
+ yarn build
48
+
49
+ # 개발 모드 실행 (ts-node)
50
+ yarn dev
51
+
52
+ # 프로덕션 실행
53
+ yarn start
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 4. 핵심 REST API
59
+
60
+ | Method | Endpoint | 설명 |
61
+ |---|---|---|
62
+ | `GET` | `/health` | 서버 헬스체크 |
63
+ | `GET` | `/api/v1/figures` | 자산 목록 페이징 및 조건 검색 (카테고리, 스코프, 사이트) |
64
+ | `GET` | `/api/v1/figures/:componentType` | 자산 단건 상세 조회 |
65
+ | `POST` | `/api/v1/figures` | 자산 신규 등록 |
66
+ | `PUT` | `/api/v1/figures/:componentType` | 자산 수정 |
67
+ | `DELETE` | `/api/v1/figures/:componentType` | 자산 삭제 |
68
+ | `POST` | `/api/v1/figures/:componentType/upload` | 3D 모델(glTF/GLB) 및 썸네일 업로드 |
69
+ | `GET` | `/api/v1/figures/:componentType/model` | 3D 바이너리 스트리밍 |
70
+ | `GET` | `/api/v1/figures/:componentType/thumbnail` | 썸네일 이미지 스트리밍 |
package/dist/app.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Application } from 'express';
2
+ import { FigureStorage } from './storage/figure-storage';
3
+ export declare function createApp(storage?: FigureStorage): Application;
package/dist/app.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createApp = createApp;
4
+ const tslib_1 = require("tslib");
5
+ const express_1 = tslib_1.__importDefault(require("express"));
6
+ const cors_1 = tslib_1.__importDefault(require("cors"));
7
+ const figure_storage_1 = require("./storage/figure-storage");
8
+ const figure_router_1 = require("./routes/figure-router");
9
+ function createApp(storage) {
10
+ const app = (0, express_1.default)();
11
+ const figureStorage = storage || new figure_storage_1.FigureStorage();
12
+ app.use((0, cors_1.default)());
13
+ app.use(express_1.default.json({ limit: '10mb' }));
14
+ app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' }));
15
+ // Health check
16
+ app.get('/health', (_req, res) => {
17
+ res.json({
18
+ status: 'UP',
19
+ service: '@fmsim/figure-server',
20
+ timestamp: new Date().toISOString()
21
+ });
22
+ });
23
+ // REST API v1
24
+ app.use('/api/v1', (0, figure_router_1.createFigureRouter)(figureStorage));
25
+ // Global Error Handler
26
+ app.use((err, _req, res, _next) => {
27
+ console.error('[@fmsim/figure-server Error]:', err);
28
+ res.status(500).json({
29
+ success: false,
30
+ error: err.message || 'Internal Server Error'
31
+ });
32
+ });
33
+ return app;
34
+ }
35
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":";;AAKA,8BA8BC;;AAnCD,8DAAgF;AAChF,wDAAwB;AACxB,6DAAyD;AACzD,0DAA4D;AAE5D,SAAgB,SAAS,CAAC,OAAuB;IAC/C,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IACtB,MAAM,aAAa,GAAG,OAAO,IAAI,IAAI,8BAAa,EAAE,CAAC;IAErD,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,GAAE,CAAC,CAAC;IAChB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAE/D,eAAe;IACf,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QAClD,GAAG,CAAC,IAAI,CAAC;YACP,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,sBAAsB;YAC/B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,cAAc;IACd,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAA,kCAAkB,EAAC,aAAa,CAAC,CAAC,CAAC;IAEtD,uBAAuB;IACvB,GAAG,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,IAAa,EAAE,GAAa,EAAE,KAAmB,EAAE,EAAE;QACtE,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC;QACpD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,GAAG,CAAC,OAAO,IAAI,uBAAuB;SAC9C,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import express, { Application, Request, Response, NextFunction } from 'express';\nimport cors from 'cors';\nimport { FigureStorage } from './storage/figure-storage';\nimport { createFigureRouter } from './routes/figure-router';\n\nexport function createApp(storage?: FigureStorage): Application {\n const app = express();\n const figureStorage = storage || new FigureStorage();\n\n app.use(cors());\n app.use(express.json({ limit: '10mb' }));\n app.use(express.urlencoded({ extended: true, limit: '10mb' }));\n\n // Health check\n app.get('/health', (_req: Request, res: Response) => {\n res.json({\n status: 'UP',\n service: '@fmsim/figure-server',\n timestamp: new Date().toISOString()\n });\n });\n\n // REST API v1\n app.use('/api/v1', createFigureRouter(figureStorage));\n\n // Global Error Handler\n app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {\n console.error('[@fmsim/figure-server Error]:', err);\n res.status(500).json({\n success: false,\n error: err.message || 'Internal Server Error'\n });\n });\n\n return app;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ declare const app: import("express").Application;
2
+ declare const server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
3
+ export { app, server };
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.server = exports.app = void 0;
4
+ const app_1 = require("./app");
5
+ const PORT = parseInt(process.env.FIGURE_SERVER_PORT || process.env.PORT || '5050', 10);
6
+ const HOST = process.env.FIGURE_SERVER_HOST || '0.0.0.0';
7
+ const app = (0, app_1.createApp)();
8
+ exports.app = app;
9
+ const server = app.listen(PORT, HOST, () => {
10
+ console.log(`=======================================================`);
11
+ console.log(`🚀 @fmsim/figure-server is running on http://${HOST}:${PORT}`);
12
+ console.log(`📦 Health Check: http://${HOST}:${PORT}/health`);
13
+ console.log(`📋 API Endpoint: http://${HOST}:${PORT}/api/v1/figures`);
14
+ console.log(`=======================================================`);
15
+ });
16
+ exports.server = server;
17
+ process.on('SIGTERM', () => {
18
+ console.log('SIGTERM signal received: closing HTTP server');
19
+ server.close(() => {
20
+ console.log('HTTP server closed');
21
+ });
22
+ });
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,+BAAkC;AAElC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;AACxF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,SAAS,CAAC;AAEzD,MAAM,GAAG,GAAG,IAAA,eAAS,GAAE,CAAC;AAiBf,kBAAG;AAfZ,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;IACzC,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;IACvE,OAAO,CAAC,GAAG,CAAC,gDAAgD,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;IAC5E,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,IAAI,IAAI,SAAS,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,IAAI,IAAI,iBAAiB,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;AACzE,CAAC,CAAC,CAAC;AASW,wBAAM;AAPpB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACzB,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;IAC5D,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE;QAChB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { createApp } from './app';\n\nconst PORT = parseInt(process.env.FIGURE_SERVER_PORT || process.env.PORT || '5050', 10);\nconst HOST = process.env.FIGURE_SERVER_HOST || '0.0.0.0';\n\nconst app = createApp();\n\nconst server = app.listen(PORT, HOST, () => {\n console.log(`=======================================================`);\n console.log(`🚀 @fmsim/figure-server is running on http://${HOST}:${PORT}`);\n console.log(`📦 Health Check: http://${HOST}:${PORT}/health`);\n console.log(`📋 API Endpoint: http://${HOST}:${PORT}/api/v1/figures`);\n console.log(`=======================================================`);\n});\n\nprocess.on('SIGTERM', () => {\n console.log('SIGTERM signal received: closing HTTP server');\n server.close(() => {\n console.log('HTTP server closed');\n });\n});\n\nexport { app, server };\n"]}
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { FigureStorage } from '../storage/figure-storage';
3
+ export declare function createFigureRouter(storage: FigureStorage): Router;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFigureRouter = createFigureRouter;
4
+ const tslib_1 = require("tslib");
5
+ const express_1 = require("express");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const multer_1 = tslib_1.__importDefault(require("multer"));
9
+ function createFigureRouter(storage) {
10
+ const router = (0, express_1.Router)();
11
+ const upload = (0, multer_1.default)({
12
+ dest: path.resolve(process.cwd(), 'data/temp_uploads'),
13
+ limits: { fileSize: 50 * 1024 * 1024 } // 50MB max
14
+ });
15
+ // 1. 자산 목록 조회
16
+ router.get('/figures', (req, res) => {
17
+ try {
18
+ const { scope, siteCode, category, status, keyword, page, limit } = req.query;
19
+ const result = storage.list({
20
+ scope: scope,
21
+ siteCode: siteCode,
22
+ category: category,
23
+ status: status,
24
+ keyword: keyword,
25
+ page: page ? parseInt(page, 10) : 1,
26
+ limit: limit ? parseInt(limit, 10) : 50
27
+ });
28
+ res.json({ success: true, data: result });
29
+ }
30
+ catch (err) {
31
+ res.status(500).json({ success: false, error: err.message });
32
+ }
33
+ });
34
+ // 2. 자산 단건 상세 조회
35
+ router.get('/figures/:componentType', (req, res) => {
36
+ const componentType = String(req.params.componentType);
37
+ const figure = storage.get(componentType);
38
+ if (!figure) {
39
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
40
+ }
41
+ res.json({ success: true, data: figure });
42
+ });
43
+ // 3. 자산 신규 등록
44
+ router.post('/figures', (req, res) => {
45
+ try {
46
+ const body = req.body;
47
+ if (!body.assetCode || !body.componentType || !body.category) {
48
+ return res.status(400).json({ success: false, error: 'Missing required fields: assetCode, componentType, category' });
49
+ }
50
+ const existing = storage.get(body.componentType);
51
+ if (existing) {
52
+ return res.status(409).json({ success: false, error: `Figure with componentType '${body.componentType}' already exists` });
53
+ }
54
+ const newFigure = {
55
+ id: `fig-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
56
+ assetCode: body.assetCode,
57
+ scope: body.scope || 'GLOBAL',
58
+ siteCode: body.siteCode || null,
59
+ componentType: body.componentType,
60
+ category: body.category,
61
+ name: body.name || body.assetCode,
62
+ description: body.description || '',
63
+ version: body.version || '1.0.0',
64
+ definition2d: body.definition2d || {},
65
+ properties3d: body.properties3d || {},
66
+ status: body.status || 'PUBLISHED',
67
+ createdAt: new Date().toISOString(),
68
+ updatedAt: new Date().toISOString()
69
+ };
70
+ const saved = storage.save(newFigure);
71
+ res.status(201).json({ success: true, data: saved });
72
+ }
73
+ catch (err) {
74
+ res.status(500).json({ success: false, error: err.message });
75
+ }
76
+ });
77
+ // 4. 자산 수정
78
+ router.put('/figures/:componentType', (req, res) => {
79
+ const componentType = String(req.params.componentType);
80
+ const figure = storage.get(componentType);
81
+ if (!figure) {
82
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
83
+ }
84
+ const updates = req.body;
85
+ Object.assign(figure, updates, { updatedAt: new Date().toISOString() });
86
+ const saved = storage.save(figure);
87
+ res.json({ success: true, data: saved });
88
+ });
89
+ // 5. 자산 삭제
90
+ router.delete('/figures/:componentType', (req, res) => {
91
+ const componentType = String(req.params.componentType);
92
+ const deleted = storage.delete(componentType);
93
+ if (!deleted) {
94
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
95
+ }
96
+ res.json({ success: true, message: `Figure '${componentType}' deleted successfully` });
97
+ });
98
+ // 6. 3D 모델 바이너리 스트리밍 서빙
99
+ router.get('/figures/:componentType/model', (req, res) => {
100
+ const componentType = String(req.params.componentType);
101
+ const figure = storage.get(componentType);
102
+ if (!figure || !figure.model3dPath || !fs.existsSync(figure.model3dPath)) {
103
+ return res.status(404).json({ success: false, error: '3D Model file not found' });
104
+ }
105
+ const ext = path.extname(figure.model3dPath).toLowerCase();
106
+ const contentType = ext === '.glb' ? 'model/gltf-binary' : 'model/gltf+json';
107
+ res.setHeader('Content-Type', contentType);
108
+ fs.createReadStream(figure.model3dPath).pipe(res);
109
+ });
110
+ // 7. 썸네일 이미지 스트리밍 서빙
111
+ router.get('/figures/:componentType/thumbnail', (req, res) => {
112
+ const componentType = String(req.params.componentType);
113
+ const figure = storage.get(componentType);
114
+ if (!figure || !figure.thumbnailPath || !fs.existsSync(figure.thumbnailPath)) {
115
+ return res.status(404).json({ success: false, error: 'Thumbnail file not found' });
116
+ }
117
+ res.setHeader('Content-Type', 'image/png');
118
+ fs.createReadStream(figure.thumbnailPath).pipe(res);
119
+ });
120
+ // 8. 3D glTF/GLB 및 썸네일 업로드
121
+ router.post('/figures/:componentType/upload', upload.fields([
122
+ { name: 'model', maxCount: 1 },
123
+ { name: 'thumbnail', maxCount: 1 }
124
+ ]), (req, res) => {
125
+ var _a, _b;
126
+ const componentType = String(req.params.componentType);
127
+ const figure = storage.get(componentType);
128
+ if (!figure) {
129
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
130
+ }
131
+ const files = req.files;
132
+ if ((_a = files === null || files === void 0 ? void 0 : files.model) === null || _a === void 0 ? void 0 : _a[0]) {
133
+ const modelFile = files.model[0];
134
+ const destPath = storage.getAssetPath(componentType, `model${path.extname(modelFile.originalname)}`);
135
+ fs.renameSync(modelFile.path, destPath);
136
+ figure.model3dPath = destPath;
137
+ }
138
+ if ((_b = files === null || files === void 0 ? void 0 : files.thumbnail) === null || _b === void 0 ? void 0 : _b[0]) {
139
+ const thumbFile = files.thumbnail[0];
140
+ const destPath = storage.getAssetPath(componentType, `thumbnail.png`);
141
+ fs.renameSync(thumbFile.path, destPath);
142
+ figure.thumbnailPath = destPath;
143
+ }
144
+ storage.save(figure);
145
+ res.json({ success: true, data: figure });
146
+ });
147
+ return router;
148
+ }
149
+ //# sourceMappingURL=figure-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-router.js","sourceRoot":"","sources":["../../src/routes/figure-router.ts"],"names":[],"mappings":"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","sourcesContent":["import { Router, Request, Response } from 'express';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport multer from 'multer';\nimport { FigureStorage } from '../storage/figure-storage';\nimport { FigureEntity } from '../types/figure';\n\nexport function createFigureRouter(storage: FigureStorage): Router {\n const router = Router();\n\n const upload = multer({\n dest: path.resolve(process.cwd(), 'data/temp_uploads'),\n limits: { fileSize: 50 * 1024 * 1024 } // 50MB max\n });\n\n // 1. 자산 목록 조회\n router.get('/figures', (req: Request, res: Response) => {\n try {\n const { scope, siteCode, category, status, keyword, page, limit } = req.query;\n const result = storage.list({\n scope: scope as any,\n siteCode: siteCode as string,\n category: category as any,\n status: status as any,\n keyword: keyword as string,\n page: page ? parseInt(page as string, 10) : 1,\n limit: limit ? parseInt(limit as string, 10) : 50\n });\n res.json({ success: true, data: result });\n } catch (err: any) {\n res.status(500).json({ success: false, error: err.message });\n }\n });\n\n // 2. 자산 단건 상세 조회\n router.get('/figures/:componentType', (req: Request, res: Response) => {\n const componentType = String(req.params.componentType);\n const figure = storage.get(componentType);\n if (!figure) {\n return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });\n }\n res.json({ success: true, data: figure });\n });\n\n // 3. 자산 신규 등록\n router.post('/figures', (req: Request, res: Response) => {\n try {\n const body = req.body as Partial<FigureEntity>;\n if (!body.assetCode || !body.componentType || !body.category) {\n return res.status(400).json({ success: false, error: 'Missing required fields: assetCode, componentType, category' });\n }\n\n const existing = storage.get(body.componentType);\n if (existing) {\n return res.status(409).json({ success: false, error: `Figure with componentType '${body.componentType}' already exists` });\n }\n\n const newFigure: FigureEntity = {\n id: `fig-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,\n assetCode: body.assetCode,\n scope: body.scope || 'GLOBAL',\n siteCode: body.siteCode || null,\n componentType: body.componentType,\n category: body.category,\n name: body.name || body.assetCode,\n description: body.description || '',\n version: body.version || '1.0.0',\n definition2d: body.definition2d || {},\n properties3d: body.properties3d || {},\n status: body.status || 'PUBLISHED',\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString()\n };\n\n const saved = storage.save(newFigure);\n res.status(201).json({ success: true, data: saved });\n } catch (err: any) {\n res.status(500).json({ success: false, error: err.message });\n }\n });\n\n // 4. 자산 수정\n router.put('/figures/:componentType', (req: Request, res: Response) => {\n const componentType = String(req.params.componentType);\n const figure = storage.get(componentType);\n if (!figure) {\n return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });\n }\n\n const updates = req.body as Partial<FigureEntity>;\n Object.assign(figure, updates, { updatedAt: new Date().toISOString() });\n const saved = storage.save(figure);\n res.json({ success: true, data: saved });\n });\n\n // 5. 자산 삭제\n router.delete('/figures/:componentType', (req: Request, res: Response) => {\n const componentType = String(req.params.componentType);\n const deleted = storage.delete(componentType);\n if (!deleted) {\n return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });\n }\n res.json({ success: true, message: `Figure '${componentType}' deleted successfully` });\n });\n\n // 6. 3D 모델 바이너리 스트리밍 서빙\n router.get('/figures/:componentType/model', (req: Request, res: Response) => {\n const componentType = String(req.params.componentType);\n const figure = storage.get(componentType);\n if (!figure || !figure.model3dPath || !fs.existsSync(figure.model3dPath)) {\n return res.status(404).json({ success: false, error: '3D Model file not found' });\n }\n\n const ext = path.extname(figure.model3dPath).toLowerCase();\n const contentType = ext === '.glb' ? 'model/gltf-binary' : 'model/gltf+json';\n res.setHeader('Content-Type', contentType);\n fs.createReadStream(figure.model3dPath).pipe(res);\n });\n\n // 7. 썸네일 이미지 스트리밍 서빙\n router.get('/figures/:componentType/thumbnail', (req: Request, res: Response) => {\n const componentType = String(req.params.componentType);\n const figure = storage.get(componentType);\n if (!figure || !figure.thumbnailPath || !fs.existsSync(figure.thumbnailPath)) {\n return res.status(404).json({ success: false, error: 'Thumbnail file not found' });\n }\n\n res.setHeader('Content-Type', 'image/png');\n fs.createReadStream(figure.thumbnailPath).pipe(res);\n });\n\n // 8. 3D glTF/GLB 및 썸네일 업로드\n router.post(\n '/figures/:componentType/upload',\n upload.fields([\n { name: 'model', maxCount: 1 },\n { name: 'thumbnail', maxCount: 1 }\n ]),\n (req: any, res: Response) => {\n const componentType = String(req.params.componentType);\n const figure = storage.get(componentType);\n if (!figure) {\n return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });\n }\n\n const files = req.files as { [fieldname: string]: any[] };\n\n if (files?.model?.[0]) {\n const modelFile = files.model[0];\n const destPath = storage.getAssetPath(componentType, `model${path.extname(modelFile.originalname)}`);\n fs.renameSync(modelFile.path, destPath);\n figure.model3dPath = destPath;\n }\n\n if (files?.thumbnail?.[0]) {\n const thumbFile = files.thumbnail[0];\n const destPath = storage.getAssetPath(componentType, `thumbnail.png`);\n fs.renameSync(thumbFile.path, destPath);\n figure.thumbnailPath = destPath;\n }\n\n storage.save(figure);\n res.json({ success: true, data: figure });\n }\n );\n\n return router;\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { FigureEntity, FigureFilterOptions, FigureListResponse } from '../types/figure';
2
+ export declare class FigureStorage {
3
+ private baseDir;
4
+ private metadataFile;
5
+ private figures;
6
+ constructor(storageDir?: string);
7
+ private initStorage;
8
+ private saveMetadata;
9
+ private seedDefaultFigures;
10
+ list(options?: FigureFilterOptions): FigureListResponse;
11
+ get(componentType: string): FigureEntity | undefined;
12
+ save(figure: FigureEntity): FigureEntity;
13
+ delete(componentType: string): boolean;
14
+ getAssetPath(componentType: string, filename: string): string;
15
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FigureStorage = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ class FigureStorage {
8
+ constructor(storageDir) {
9
+ this.figures = new Map();
10
+ this.baseDir = storageDir || path.resolve(process.cwd(), 'data/figures');
11
+ this.metadataFile = path.join(this.baseDir, 'figures-metadata.json');
12
+ this.initStorage();
13
+ }
14
+ initStorage() {
15
+ if (!fs.existsSync(this.baseDir)) {
16
+ fs.mkdirSync(this.baseDir, { recursive: true });
17
+ }
18
+ if (fs.existsSync(this.metadataFile)) {
19
+ try {
20
+ const raw = fs.readFileSync(this.metadataFile, 'utf-8');
21
+ const list = JSON.parse(raw);
22
+ for (const item of list) {
23
+ this.figures.set(item.componentType, item);
24
+ }
25
+ }
26
+ catch (err) {
27
+ console.error('[FigureStorage] Failed to parse metadata file, initializing empty:', err);
28
+ }
29
+ }
30
+ else {
31
+ this.seedDefaultFigures();
32
+ this.saveMetadata();
33
+ }
34
+ }
35
+ saveMetadata() {
36
+ const list = Array.from(this.figures.values());
37
+ fs.writeFileSync(this.metadataFile, JSON.stringify(list, null, 2), 'utf-8');
38
+ }
39
+ seedDefaultFigures() {
40
+ const defaultFigures = [
41
+ {
42
+ id: 'seed-eq-mixer-01',
43
+ assetCode: 'EQ-MIXER-01',
44
+ scope: 'GLOBAL',
45
+ siteCode: null,
46
+ componentType: 'global:equipment:mixer',
47
+ category: 'EQUIPMENT',
48
+ name: 'Mixer (전극 교반기)',
49
+ description: '대형 원통형 교반 탱크 및 모터 구동 3D 설비',
50
+ version: '1.0.0',
51
+ definition2d: { type: 'rect', width: 120, height: 80, fillStyle: '#3b82f6' },
52
+ properties3d: { width: 4500, length: 5000, height: 3800, material: { metalness: 0.6, roughness: 0.3 } },
53
+ status: 'PUBLISHED',
54
+ createdAt: new Date().toISOString(),
55
+ updatedAt: new Date().toISOString()
56
+ },
57
+ {
58
+ id: 'seed-cr-roll-01',
59
+ assetCode: 'CR-ROLL-01',
60
+ scope: 'GLOBAL',
61
+ siteCode: null,
62
+ componentType: 'global:carrier:roll',
63
+ category: 'CARRIER',
64
+ name: 'Electrode Roll (전극 롤)',
65
+ description: '표준 800파이 원통형 양극 전극롤 캐리어',
66
+ version: '1.0.0',
67
+ definition2d: { type: 'ellipse', rx: 30, ry: 30, fillStyle: '#10b981' },
68
+ properties3d: { outerDiameter: 800, innerDiameter: 150, length: 1200, maxPayloadKg: 500 },
69
+ status: 'PUBLISHED',
70
+ createdAt: new Date().toISOString(),
71
+ updatedAt: new Date().toISOString()
72
+ }
73
+ ];
74
+ for (const f of defaultFigures) {
75
+ this.figures.set(f.componentType, f);
76
+ }
77
+ }
78
+ list(options = {}) {
79
+ let all = Array.from(this.figures.values());
80
+ if (options.scope) {
81
+ all = all.filter(f => f.scope === options.scope);
82
+ }
83
+ if (options.siteCode) {
84
+ all = all.filter(f => !f.siteCode || f.siteCode === options.siteCode);
85
+ }
86
+ if (options.category) {
87
+ all = all.filter(f => f.category === options.category);
88
+ }
89
+ if (options.status) {
90
+ all = all.filter(f => f.status === options.status);
91
+ }
92
+ if (options.keyword) {
93
+ const kw = options.keyword.toLowerCase();
94
+ all = all.filter(f => f.name.toLowerCase().includes(kw) ||
95
+ f.assetCode.toLowerCase().includes(kw) ||
96
+ f.componentType.toLowerCase().includes(kw));
97
+ }
98
+ const total = all.length;
99
+ const page = Math.max(1, options.page || 1);
100
+ const limit = Math.max(1, options.limit || 50);
101
+ const totalPages = Math.ceil(total / limit) || 1;
102
+ const startIndex = (page - 1) * limit;
103
+ const items = all.slice(startIndex, startIndex + limit);
104
+ return { items, total, page, limit, totalPages };
105
+ }
106
+ get(componentType) {
107
+ return this.figures.get(componentType);
108
+ }
109
+ save(figure) {
110
+ figure.updatedAt = new Date().toISOString();
111
+ this.figures.set(figure.componentType, figure);
112
+ this.saveMetadata();
113
+ return figure;
114
+ }
115
+ delete(componentType) {
116
+ const deleted = this.figures.delete(componentType);
117
+ if (deleted) {
118
+ this.saveMetadata();
119
+ }
120
+ return deleted;
121
+ }
122
+ getAssetPath(componentType, filename) {
123
+ const safeName = componentType.replace(/[:\/\\?%*|"<>]/g, '_');
124
+ const assetFolder = path.join(this.baseDir, safeName);
125
+ if (!fs.existsSync(assetFolder)) {
126
+ fs.mkdirSync(assetFolder, { recursive: true });
127
+ }
128
+ return path.join(assetFolder, filename);
129
+ }
130
+ }
131
+ exports.FigureStorage = FigureStorage;
132
+ //# sourceMappingURL=figure-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-storage.js","sourceRoot":"","sources":["../../src/storage/figure-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { FigureEntity, FigureFilterOptions, FigureListResponse } from '../types/figure';\n\nexport class FigureStorage {\n private baseDir: string;\n private metadataFile: string;\n private figures: Map<string, FigureEntity> = new Map();\n\n constructor(storageDir?: string) {\n this.baseDir = storageDir || path.resolve(process.cwd(), 'data/figures');\n this.metadataFile = path.join(this.baseDir, 'figures-metadata.json');\n this.initStorage();\n }\n\n private initStorage(): void {\n if (!fs.existsSync(this.baseDir)) {\n fs.mkdirSync(this.baseDir, { recursive: true });\n }\n\n if (fs.existsSync(this.metadataFile)) {\n try {\n const raw = fs.readFileSync(this.metadataFile, 'utf-8');\n const list: FigureEntity[] = JSON.parse(raw);\n for (const item of list) {\n this.figures.set(item.componentType, item);\n }\n } catch (err) {\n console.error('[FigureStorage] Failed to parse metadata file, initializing empty:', err);\n }\n } else {\n this.seedDefaultFigures();\n this.saveMetadata();\n }\n }\n\n private saveMetadata(): void {\n const list = Array.from(this.figures.values());\n fs.writeFileSync(this.metadataFile, JSON.stringify(list, null, 2), 'utf-8');\n }\n\n private seedDefaultFigures(): void {\n const defaultFigures: FigureEntity[] = [\n {\n id: 'seed-eq-mixer-01',\n assetCode: 'EQ-MIXER-01',\n scope: 'GLOBAL',\n siteCode: null,\n componentType: 'global:equipment:mixer',\n category: 'EQUIPMENT',\n name: 'Mixer (전극 교반기)',\n description: '대형 원통형 교반 탱크 및 모터 구동 3D 설비',\n version: '1.0.0',\n definition2d: { type: 'rect', width: 120, height: 80, fillStyle: '#3b82f6' },\n properties3d: { width: 4500, length: 5000, height: 3800, material: { metalness: 0.6, roughness: 0.3 } },\n status: 'PUBLISHED',\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString()\n },\n {\n id: 'seed-cr-roll-01',\n assetCode: 'CR-ROLL-01',\n scope: 'GLOBAL',\n siteCode: null,\n componentType: 'global:carrier:roll',\n category: 'CARRIER',\n name: 'Electrode Roll (전극 롤)',\n description: '표준 800파이 원통형 양극 전극롤 캐리어',\n version: '1.0.0',\n definition2d: { type: 'ellipse', rx: 30, ry: 30, fillStyle: '#10b981' },\n properties3d: { outerDiameter: 800, innerDiameter: 150, length: 1200, maxPayloadKg: 500 },\n status: 'PUBLISHED',\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString()\n }\n ];\n\n for (const f of defaultFigures) {\n this.figures.set(f.componentType, f);\n }\n }\n\n public list(options: FigureFilterOptions = {}): FigureListResponse {\n let all = Array.from(this.figures.values());\n\n if (options.scope) {\n all = all.filter(f => f.scope === options.scope);\n }\n if (options.siteCode) {\n all = all.filter(f => !f.siteCode || f.siteCode === options.siteCode);\n }\n if (options.category) {\n all = all.filter(f => f.category === options.category);\n }\n if (options.status) {\n all = all.filter(f => f.status === options.status);\n }\n if (options.keyword) {\n const kw = options.keyword.toLowerCase();\n all = all.filter(f =>\n f.name.toLowerCase().includes(kw) ||\n f.assetCode.toLowerCase().includes(kw) ||\n f.componentType.toLowerCase().includes(kw)\n );\n }\n\n const total = all.length;\n const page = Math.max(1, options.page || 1);\n const limit = Math.max(1, options.limit || 50);\n const totalPages = Math.ceil(total / limit) || 1;\n const startIndex = (page - 1) * limit;\n const items = all.slice(startIndex, startIndex + limit);\n\n return { items, total, page, limit, totalPages };\n }\n\n public get(componentType: string): FigureEntity | undefined {\n return this.figures.get(componentType);\n }\n\n public save(figure: FigureEntity): FigureEntity {\n figure.updatedAt = new Date().toISOString();\n this.figures.set(figure.componentType, figure);\n this.saveMetadata();\n return figure;\n }\n\n public delete(componentType: string): boolean {\n const deleted = this.figures.delete(componentType);\n if (deleted) {\n this.saveMetadata();\n }\n return deleted;\n }\n\n public getAssetPath(componentType: string, filename: string): string {\n const safeName = componentType.replace(/[:\\/\\\\?%*|\"<>]/g, '_');\n const assetFolder = path.join(this.baseDir, safeName);\n if (!fs.existsSync(assetFolder)) {\n fs.mkdirSync(assetFolder, { recursive: true });\n }\n return path.join(assetFolder, filename);\n }\n}\n"]}
@@ -0,0 +1,37 @@
1
+ export type FigureCategory = 'EQUIPMENT' | 'CARRIER' | 'UTILITY' | 'COMPONENT';
2
+ export type FigureScope = 'GLOBAL' | 'LOCAL';
3
+ export type FigureStatus = 'DRAFT' | 'PUBLISHED' | 'DEPRECATED';
4
+ export interface FigureEntity {
5
+ id: string;
6
+ assetCode: string;
7
+ scope: FigureScope;
8
+ siteCode?: string | null;
9
+ componentType: string;
10
+ category: FigureCategory;
11
+ name: string;
12
+ description?: string;
13
+ version: string;
14
+ definition2d: Record<string, any>;
15
+ properties3d: Record<string, any>;
16
+ thumbnailPath?: string;
17
+ model3dPath?: string;
18
+ status: FigureStatus;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ }
22
+ export interface FigureFilterOptions {
23
+ scope?: FigureScope;
24
+ siteCode?: string;
25
+ category?: FigureCategory;
26
+ status?: FigureStatus;
27
+ keyword?: string;
28
+ page?: number;
29
+ limit?: number;
30
+ }
31
+ export interface FigureListResponse {
32
+ items: FigureEntity[];
33
+ total: number;
34
+ page: number;
35
+ limit: number;
36
+ totalPages: number;
37
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=figure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure.js","sourceRoot":"","sources":["../../src/types/figure.ts"],"names":[],"mappings":"","sourcesContent":["export type FigureCategory = 'EQUIPMENT' | 'CARRIER' | 'UTILITY' | 'COMPONENT';\nexport type FigureScope = 'GLOBAL' | 'LOCAL';\nexport type FigureStatus = 'DRAFT' | 'PUBLISHED' | 'DEPRECATED';\n\nexport interface FigureEntity {\n id: string;\n assetCode: string;\n scope: FigureScope;\n siteCode?: string | null;\n componentType: string;\n category: FigureCategory;\n name: string;\n description?: string;\n version: string;\n definition2d: Record<string, any>;\n properties3d: Record<string, any>;\n thumbnailPath?: string;\n model3dPath?: string;\n status: FigureStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface FigureFilterOptions {\n scope?: FigureScope;\n siteCode?: string;\n category?: FigureCategory;\n status?: FigureStatus;\n keyword?: string;\n page?: number;\n limit?: number;\n}\n\nexport interface FigureListResponse {\n items: FigureEntity[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@fmsim/figure-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "3.0.0-beta.2",
4
+ "description": "독립 Figure & 3D 그래픽 자산 관리 Node.js 마이크로서비스",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "author": "hatiolab.com",
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public",
11
+ "@fmsim:registry": "https://registry.npmjs.org"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "start": "node dist/index.js",
16
+ "dev": "ts-node src/index.ts",
17
+ "clean": "rimraf dist"
18
+ },
19
+ "dependencies": {
20
+ "cors": "^2.8.5",
21
+ "express": "^4.18.2",
22
+ "lodash": "^4.17.21",
23
+ "multer": "^1.4.5-lts.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/cors": "^2.8.13",
27
+ "@types/express": "^5.0.6",
28
+ "@types/multer": "^1.4.7",
29
+ "@types/node": "^18.11.18",
30
+ "rimraf": "^5.0.0",
31
+ "ts-node": "^10.9.1",
32
+ "typescript": "^5.8.2"
33
+ },
34
+ "gitHead": "23b7fedf8bb2b70b1150b963bd0f75d77a26052d"
35
+ }
package/src/app.ts ADDED
@@ -0,0 +1,36 @@
1
+ import express, { Application, Request, Response, NextFunction } from 'express';
2
+ import cors from 'cors';
3
+ import { FigureStorage } from './storage/figure-storage';
4
+ import { createFigureRouter } from './routes/figure-router';
5
+
6
+ export function createApp(storage?: FigureStorage): Application {
7
+ const app = express();
8
+ const figureStorage = storage || new FigureStorage();
9
+
10
+ app.use(cors());
11
+ app.use(express.json({ limit: '10mb' }));
12
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
13
+
14
+ // Health check
15
+ app.get('/health', (_req: Request, res: Response) => {
16
+ res.json({
17
+ status: 'UP',
18
+ service: '@fmsim/figure-server',
19
+ timestamp: new Date().toISOString()
20
+ });
21
+ });
22
+
23
+ // REST API v1
24
+ app.use('/api/v1', createFigureRouter(figureStorage));
25
+
26
+ // Global Error Handler
27
+ app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
28
+ console.error('[@fmsim/figure-server Error]:', err);
29
+ res.status(500).json({
30
+ success: false,
31
+ error: err.message || 'Internal Server Error'
32
+ });
33
+ });
34
+
35
+ return app;
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { createApp } from './app';
2
+
3
+ const PORT = parseInt(process.env.FIGURE_SERVER_PORT || process.env.PORT || '5050', 10);
4
+ const HOST = process.env.FIGURE_SERVER_HOST || '0.0.0.0';
5
+
6
+ const app = createApp();
7
+
8
+ const server = app.listen(PORT, HOST, () => {
9
+ console.log(`=======================================================`);
10
+ console.log(`🚀 @fmsim/figure-server is running on http://${HOST}:${PORT}`);
11
+ console.log(`📦 Health Check: http://${HOST}:${PORT}/health`);
12
+ console.log(`📋 API Endpoint: http://${HOST}:${PORT}/api/v1/figures`);
13
+ console.log(`=======================================================`);
14
+ });
15
+
16
+ process.on('SIGTERM', () => {
17
+ console.log('SIGTERM signal received: closing HTTP server');
18
+ server.close(() => {
19
+ console.log('HTTP server closed');
20
+ });
21
+ });
22
+
23
+ export { app, server };
@@ -0,0 +1,168 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import multer from 'multer';
5
+ import { FigureStorage } from '../storage/figure-storage';
6
+ import { FigureEntity } from '../types/figure';
7
+
8
+ export function createFigureRouter(storage: FigureStorage): Router {
9
+ const router = Router();
10
+
11
+ const upload = multer({
12
+ dest: path.resolve(process.cwd(), 'data/temp_uploads'),
13
+ limits: { fileSize: 50 * 1024 * 1024 } // 50MB max
14
+ });
15
+
16
+ // 1. 자산 목록 조회
17
+ router.get('/figures', (req: Request, res: Response) => {
18
+ try {
19
+ const { scope, siteCode, category, status, keyword, page, limit } = req.query;
20
+ const result = storage.list({
21
+ scope: scope as any,
22
+ siteCode: siteCode as string,
23
+ category: category as any,
24
+ status: status as any,
25
+ keyword: keyword as string,
26
+ page: page ? parseInt(page as string, 10) : 1,
27
+ limit: limit ? parseInt(limit as string, 10) : 50
28
+ });
29
+ res.json({ success: true, data: result });
30
+ } catch (err: any) {
31
+ res.status(500).json({ success: false, error: err.message });
32
+ }
33
+ });
34
+
35
+ // 2. 자산 단건 상세 조회
36
+ router.get('/figures/:componentType', (req: Request, res: Response) => {
37
+ const componentType = String(req.params.componentType);
38
+ const figure = storage.get(componentType);
39
+ if (!figure) {
40
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
41
+ }
42
+ res.json({ success: true, data: figure });
43
+ });
44
+
45
+ // 3. 자산 신규 등록
46
+ router.post('/figures', (req: Request, res: Response) => {
47
+ try {
48
+ const body = req.body as Partial<FigureEntity>;
49
+ if (!body.assetCode || !body.componentType || !body.category) {
50
+ return res.status(400).json({ success: false, error: 'Missing required fields: assetCode, componentType, category' });
51
+ }
52
+
53
+ const existing = storage.get(body.componentType);
54
+ if (existing) {
55
+ return res.status(409).json({ success: false, error: `Figure with componentType '${body.componentType}' already exists` });
56
+ }
57
+
58
+ const newFigure: FigureEntity = {
59
+ id: `fig-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
60
+ assetCode: body.assetCode,
61
+ scope: body.scope || 'GLOBAL',
62
+ siteCode: body.siteCode || null,
63
+ componentType: body.componentType,
64
+ category: body.category,
65
+ name: body.name || body.assetCode,
66
+ description: body.description || '',
67
+ version: body.version || '1.0.0',
68
+ definition2d: body.definition2d || {},
69
+ properties3d: body.properties3d || {},
70
+ status: body.status || 'PUBLISHED',
71
+ createdAt: new Date().toISOString(),
72
+ updatedAt: new Date().toISOString()
73
+ };
74
+
75
+ const saved = storage.save(newFigure);
76
+ res.status(201).json({ success: true, data: saved });
77
+ } catch (err: any) {
78
+ res.status(500).json({ success: false, error: err.message });
79
+ }
80
+ });
81
+
82
+ // 4. 자산 수정
83
+ router.put('/figures/:componentType', (req: Request, res: Response) => {
84
+ const componentType = String(req.params.componentType);
85
+ const figure = storage.get(componentType);
86
+ if (!figure) {
87
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
88
+ }
89
+
90
+ const updates = req.body as Partial<FigureEntity>;
91
+ Object.assign(figure, updates, { updatedAt: new Date().toISOString() });
92
+ const saved = storage.save(figure);
93
+ res.json({ success: true, data: saved });
94
+ });
95
+
96
+ // 5. 자산 삭제
97
+ router.delete('/figures/:componentType', (req: Request, res: Response) => {
98
+ const componentType = String(req.params.componentType);
99
+ const deleted = storage.delete(componentType);
100
+ if (!deleted) {
101
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
102
+ }
103
+ res.json({ success: true, message: `Figure '${componentType}' deleted successfully` });
104
+ });
105
+
106
+ // 6. 3D 모델 바이너리 스트리밍 서빙
107
+ router.get('/figures/:componentType/model', (req: Request, res: Response) => {
108
+ const componentType = String(req.params.componentType);
109
+ const figure = storage.get(componentType);
110
+ if (!figure || !figure.model3dPath || !fs.existsSync(figure.model3dPath)) {
111
+ return res.status(404).json({ success: false, error: '3D Model file not found' });
112
+ }
113
+
114
+ const ext = path.extname(figure.model3dPath).toLowerCase();
115
+ const contentType = ext === '.glb' ? 'model/gltf-binary' : 'model/gltf+json';
116
+ res.setHeader('Content-Type', contentType);
117
+ fs.createReadStream(figure.model3dPath).pipe(res);
118
+ });
119
+
120
+ // 7. 썸네일 이미지 스트리밍 서빙
121
+ router.get('/figures/:componentType/thumbnail', (req: Request, res: Response) => {
122
+ const componentType = String(req.params.componentType);
123
+ const figure = storage.get(componentType);
124
+ if (!figure || !figure.thumbnailPath || !fs.existsSync(figure.thumbnailPath)) {
125
+ return res.status(404).json({ success: false, error: 'Thumbnail file not found' });
126
+ }
127
+
128
+ res.setHeader('Content-Type', 'image/png');
129
+ fs.createReadStream(figure.thumbnailPath).pipe(res);
130
+ });
131
+
132
+ // 8. 3D glTF/GLB 및 썸네일 업로드
133
+ router.post(
134
+ '/figures/:componentType/upload',
135
+ upload.fields([
136
+ { name: 'model', maxCount: 1 },
137
+ { name: 'thumbnail', maxCount: 1 }
138
+ ]),
139
+ (req: any, res: Response) => {
140
+ const componentType = String(req.params.componentType);
141
+ const figure = storage.get(componentType);
142
+ if (!figure) {
143
+ return res.status(404).json({ success: false, error: `Figure '${componentType}' not found` });
144
+ }
145
+
146
+ const files = req.files as { [fieldname: string]: any[] };
147
+
148
+ if (files?.model?.[0]) {
149
+ const modelFile = files.model[0];
150
+ const destPath = storage.getAssetPath(componentType, `model${path.extname(modelFile.originalname)}`);
151
+ fs.renameSync(modelFile.path, destPath);
152
+ figure.model3dPath = destPath;
153
+ }
154
+
155
+ if (files?.thumbnail?.[0]) {
156
+ const thumbFile = files.thumbnail[0];
157
+ const destPath = storage.getAssetPath(componentType, `thumbnail.png`);
158
+ fs.renameSync(thumbFile.path, destPath);
159
+ figure.thumbnailPath = destPath;
160
+ }
161
+
162
+ storage.save(figure);
163
+ res.json({ success: true, data: figure });
164
+ }
165
+ );
166
+
167
+ return router;
168
+ }
@@ -0,0 +1,144 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { FigureEntity, FigureFilterOptions, FigureListResponse } from '../types/figure';
4
+
5
+ export class FigureStorage {
6
+ private baseDir: string;
7
+ private metadataFile: string;
8
+ private figures: Map<string, FigureEntity> = new Map();
9
+
10
+ constructor(storageDir?: string) {
11
+ this.baseDir = storageDir || path.resolve(process.cwd(), 'data/figures');
12
+ this.metadataFile = path.join(this.baseDir, 'figures-metadata.json');
13
+ this.initStorage();
14
+ }
15
+
16
+ private initStorage(): void {
17
+ if (!fs.existsSync(this.baseDir)) {
18
+ fs.mkdirSync(this.baseDir, { recursive: true });
19
+ }
20
+
21
+ if (fs.existsSync(this.metadataFile)) {
22
+ try {
23
+ const raw = fs.readFileSync(this.metadataFile, 'utf-8');
24
+ const list: FigureEntity[] = JSON.parse(raw);
25
+ for (const item of list) {
26
+ this.figures.set(item.componentType, item);
27
+ }
28
+ } catch (err) {
29
+ console.error('[FigureStorage] Failed to parse metadata file, initializing empty:', err);
30
+ }
31
+ } else {
32
+ this.seedDefaultFigures();
33
+ this.saveMetadata();
34
+ }
35
+ }
36
+
37
+ private saveMetadata(): void {
38
+ const list = Array.from(this.figures.values());
39
+ fs.writeFileSync(this.metadataFile, JSON.stringify(list, null, 2), 'utf-8');
40
+ }
41
+
42
+ private seedDefaultFigures(): void {
43
+ const defaultFigures: FigureEntity[] = [
44
+ {
45
+ id: 'seed-eq-mixer-01',
46
+ assetCode: 'EQ-MIXER-01',
47
+ scope: 'GLOBAL',
48
+ siteCode: null,
49
+ componentType: 'global:equipment:mixer',
50
+ category: 'EQUIPMENT',
51
+ name: 'Mixer (전극 교반기)',
52
+ description: '대형 원통형 교반 탱크 및 모터 구동 3D 설비',
53
+ version: '1.0.0',
54
+ definition2d: { type: 'rect', width: 120, height: 80, fillStyle: '#3b82f6' },
55
+ properties3d: { width: 4500, length: 5000, height: 3800, material: { metalness: 0.6, roughness: 0.3 } },
56
+ status: 'PUBLISHED',
57
+ createdAt: new Date().toISOString(),
58
+ updatedAt: new Date().toISOString()
59
+ },
60
+ {
61
+ id: 'seed-cr-roll-01',
62
+ assetCode: 'CR-ROLL-01',
63
+ scope: 'GLOBAL',
64
+ siteCode: null,
65
+ componentType: 'global:carrier:roll',
66
+ category: 'CARRIER',
67
+ name: 'Electrode Roll (전극 롤)',
68
+ description: '표준 800파이 원통형 양극 전극롤 캐리어',
69
+ version: '1.0.0',
70
+ definition2d: { type: 'ellipse', rx: 30, ry: 30, fillStyle: '#10b981' },
71
+ properties3d: { outerDiameter: 800, innerDiameter: 150, length: 1200, maxPayloadKg: 500 },
72
+ status: 'PUBLISHED',
73
+ createdAt: new Date().toISOString(),
74
+ updatedAt: new Date().toISOString()
75
+ }
76
+ ];
77
+
78
+ for (const f of defaultFigures) {
79
+ this.figures.set(f.componentType, f);
80
+ }
81
+ }
82
+
83
+ public list(options: FigureFilterOptions = {}): FigureListResponse {
84
+ let all = Array.from(this.figures.values());
85
+
86
+ if (options.scope) {
87
+ all = all.filter(f => f.scope === options.scope);
88
+ }
89
+ if (options.siteCode) {
90
+ all = all.filter(f => !f.siteCode || f.siteCode === options.siteCode);
91
+ }
92
+ if (options.category) {
93
+ all = all.filter(f => f.category === options.category);
94
+ }
95
+ if (options.status) {
96
+ all = all.filter(f => f.status === options.status);
97
+ }
98
+ if (options.keyword) {
99
+ const kw = options.keyword.toLowerCase();
100
+ all = all.filter(f =>
101
+ f.name.toLowerCase().includes(kw) ||
102
+ f.assetCode.toLowerCase().includes(kw) ||
103
+ f.componentType.toLowerCase().includes(kw)
104
+ );
105
+ }
106
+
107
+ const total = all.length;
108
+ const page = Math.max(1, options.page || 1);
109
+ const limit = Math.max(1, options.limit || 50);
110
+ const totalPages = Math.ceil(total / limit) || 1;
111
+ const startIndex = (page - 1) * limit;
112
+ const items = all.slice(startIndex, startIndex + limit);
113
+
114
+ return { items, total, page, limit, totalPages };
115
+ }
116
+
117
+ public get(componentType: string): FigureEntity | undefined {
118
+ return this.figures.get(componentType);
119
+ }
120
+
121
+ public save(figure: FigureEntity): FigureEntity {
122
+ figure.updatedAt = new Date().toISOString();
123
+ this.figures.set(figure.componentType, figure);
124
+ this.saveMetadata();
125
+ return figure;
126
+ }
127
+
128
+ public delete(componentType: string): boolean {
129
+ const deleted = this.figures.delete(componentType);
130
+ if (deleted) {
131
+ this.saveMetadata();
132
+ }
133
+ return deleted;
134
+ }
135
+
136
+ public getAssetPath(componentType: string, filename: string): string {
137
+ const safeName = componentType.replace(/[:\/\\?%*|"<>]/g, '_');
138
+ const assetFolder = path.join(this.baseDir, safeName);
139
+ if (!fs.existsSync(assetFolder)) {
140
+ fs.mkdirSync(assetFolder, { recursive: true });
141
+ }
142
+ return path.join(assetFolder, filename);
143
+ }
144
+ }
@@ -0,0 +1,40 @@
1
+ export type FigureCategory = 'EQUIPMENT' | 'CARRIER' | 'UTILITY' | 'COMPONENT';
2
+ export type FigureScope = 'GLOBAL' | 'LOCAL';
3
+ export type FigureStatus = 'DRAFT' | 'PUBLISHED' | 'DEPRECATED';
4
+
5
+ export interface FigureEntity {
6
+ id: string;
7
+ assetCode: string;
8
+ scope: FigureScope;
9
+ siteCode?: string | null;
10
+ componentType: string;
11
+ category: FigureCategory;
12
+ name: string;
13
+ description?: string;
14
+ version: string;
15
+ definition2d: Record<string, any>;
16
+ properties3d: Record<string, any>;
17
+ thumbnailPath?: string;
18
+ model3dPath?: string;
19
+ status: FigureStatus;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ }
23
+
24
+ export interface FigureFilterOptions {
25
+ scope?: FigureScope;
26
+ siteCode?: string;
27
+ category?: FigureCategory;
28
+ status?: FigureStatus;
29
+ keyword?: string;
30
+ page?: number;
31
+ limit?: number;
32
+ }
33
+
34
+ export interface FigureListResponse {
35
+ items: FigureEntity[];
36
+ total: number;
37
+ page: number;
38
+ limit: number;
39
+ totalPages: number;
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "declaration": true,
7
+ "composite": false
8
+ },
9
+ "include": ["src/**/*"]
10
+ }
@@ -0,0 +1 @@
1
+ {"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/tslib/tslib.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","../../node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","../../node_modules/@types/send/index.d.ts","../../node_modules/@types/qs/index.d.ts","../../node_modules/@types/range-parser/index.d.ts","../../node_modules/@types/express-serve-static-core/index.d.ts","../../node_modules/@types/http-errors/index.d.ts","../../node_modules/@types/mime/index.d.ts","../../node_modules/@types/serve-static/node_modules/@types/send/index.d.ts","../../node_modules/@types/serve-static/index.d.ts","../../node_modules/@types/connect/index.d.ts","../../node_modules/@types/body-parser/index.d.ts","../../node_modules/@types/express/node_modules/@types/express-serve-static-core/index.d.ts","../../node_modules/@types/express/node_modules/@types/serve-static/index.d.ts","../../node_modules/@types/express/index.d.ts","../../node_modules/@types/cors/index.d.ts","./src/types/figure.ts","./src/storage/figure-storage.ts","../../node_modules/@types/multer/index.d.ts","./src/routes/figure-router.ts","./src/app.ts","./src/index.ts","../../node_modules/@jest/expect-utils/build/index.d.ts","../../node_modules/chalk/index.d.ts","../../node_modules/@sinclair/typebox/typebox.d.ts","../../node_modules/@jest/schemas/build/index.d.ts","../../node_modules/pretty-format/build/index.d.ts","../../node_modules/jest-diff/build/index.d.ts","../../node_modules/jest-matcher-utils/build/index.d.ts","../../node_modules/expect/build/index.d.ts","../../node_modules/@types/jest/index.d.ts"],"fileIdsList":[[54,96],[54,96,166],[54,96,110,143,152],[54,96,110,143],[54,96,107,110,143,144,145,146],[54,96,147,151,153,154,155],[54,96,110,143,148],[54,96,168,171],[54,96,125,156],[54,96,108,125,143],[54,96,110,143,148,150],[54,96,108,125,143,149],[54,96,164,170],[54,96,168],[54,96,165,169],[54,96,167],[54,93,96],[54,95,96],[96],[54,96,101,128],[54,96,97,107,115,125,136],[54,96,97,98,107,115],[49,50,51,54,96],[54,96,99,137],[54,96,100,101,108,116],[54,96,101,125,133],[54,96,102,104,107,115],[54,95,96,103],[54,96,104,105],[54,96,106,107],[54,95,96,107],[54,96,107,108,109,125,136],[54,96,107,108,109,122,125,128],[54,96,104,107,110,115,125,136],[54,96,107,108,110,111,115,125,133,136],[54,96,110,112,125,133,136],[52,53,54,55,56,57,58,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142],[54,96,107,113],[54,96,114,136,141],[54,96,104,107,115,125],[54,96,116],[54,96,117],[54,95,96,118],[54,96,119,135,141],[54,96,120],[54,96,121],[54,96,107,122,123],[54,96,122,124,137,139],[54,96,107,125,126,128],[54,96,127,128],[54,96,125,126],[54,96,128],[54,96,129],[54,96,125,130],[54,96,107,131,132],[54,96,131,132],[54,96,101,115,125,133],[54,96,134],[54,96,115,135],[54,96,110,121,136],[54,96,101,137],[54,96,125,138],[54,96,114,139],[54,96,140],[54,91,96],[54,91,96,107,109,118,125,128,136,139,141],[54,96,125,142],[54,68,72,96,136],[54,68,96,125,136],[54,63,96],[54,65,68,96,133,136],[54,96,115,133],[54,96,143],[54,63,96,143],[54,65,68,96,115,136],[54,60,61,64,67,96,107,125,136],[54,60,66,96],[54,64,68,96,128,136,143],[54,84,96,143],[54,62,63,96,143],[54,68,96],[54,62,63,64,65,66,67,68,69,70,72,73,74,75,76,77,78,79,80,81,82,83,85,86,87,88,89,90,96],[54,68,75,76,96],[54,66,68,76,77,96],[54,67,96],[54,60,63,68,96],[54,68,72,76,77,96],[54,72,96],[54,66,68,71,96,136],[54,60,65,66,68,72,75,96],[54,96,125],[54,63,68,84,96,141,143],[48,54,96,156,157,159,161],[48,54,96,162],[48,54,96,108,117,156,158,159,160],[48,54,96,108,117,158],[48,54,96]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"07f073f19d67f74d732b1adea08e1dc66b1b58d77cb5b43931dee3d798a2fd53","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6a5253138c5432c68a1510c70fe78a644fe2e632111ba778e1978010d6edfec","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cb3140d0e9cee0aea7264fd6a1d297394052a18eb05ca0220d133e6c043fb5","affectsGlobalScope":true,"impliedFormat":1},{"version":"362d474eb9feae178a83ead94d757c21e42d6d7090e4182f0c12e92830a3d25e","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc27badd4bf4a2b0024a0cd32a9bbf0be7073902c5177a58be14242e7d8bf2c7","affectsGlobalScope":true,"impliedFormat":1},{"version":"4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"7180c03fd3cb6e22f911ce9ba0f8a7008b1a6ddbe88ccf16a9c8140ef9ac1686","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"54cb85a47d760da1c13c00add10d26b5118280d44d58e6908d8e89abbd9d7725","impliedFormat":1},{"version":"3e4825171442666d31c845aeb47fcd34b62e14041bb353ae2b874285d78482aa","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"a967bfe3ad4e62243eb604bf956101e4c740f5921277c60debaf325c1320bf88","impliedFormat":1},{"version":"e9775e97ac4877aebf963a0289c81abe76d1ec9a2a7778dbe637e5151f25c5f3","impliedFormat":1},{"version":"471e1da5a78350bc55ef8cef24eb3aca6174143c281b8b214ca2beda51f5e04a","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"db3435f3525cd785bf21ec6769bf8da7e8a776be1a99e2e7efb5f244a2ef5fee","impliedFormat":1},{"version":"c3b170c45fc031db31f782e612adf7314b167e60439d304b49e704010e7bafe5","impliedFormat":1},{"version":"40383ebef22b943d503c6ce2cb2e060282936b952a01bea5f9f493d5fb487cc7","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"3a84b7cb891141824bd00ef8a50b6a44596aded4075da937f180c90e362fe5f6","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"33203609eba548914dc83ddf6cadbc0bcb6e8ef89f6d648ca0908ae887f9fcc5","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"e53a3c2a9f624d90f24bf4588aacd223e7bec1b9d0d479b68d2f4a9e6011147f","impliedFormat":1},{"version":"339dc5265ee5ed92e536a93a04c4ebbc2128f45eeec6ed29f379e0085283542c","impliedFormat":1},{"version":"9f0a92164925aa37d4a5d9dd3e0134cff8177208dba55fd2310cd74beea40ee2","impliedFormat":1},{"version":"8bfdb79bf1a9d435ec48d9372dc93291161f152c0865b81fc0b2694aedb4578d","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"d32275be3546f252e3ad33976caf8c5e842c09cb87d468cb40d5f4cf092d1acc","impliedFormat":1},{"version":"4a0c3504813a3289f7fb1115db13967c8e004aa8e4f8a9021b95285502221bd1","impliedFormat":1},{"version":"b972357e61ef2e072f8a88b9f4f5a70984c417237e6106f6b2390414a09ce523","affectsGlobalScope":true,"impliedFormat":1},{"version":"076cac5898bd833255def0f7c5717b83534212873505c9c958f1926d49f9bec6","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"75eb536b960b85f75e21490beeab53ea616646a995ad203e1af532d67a774fb6","impliedFormat":1},{"version":"36d0976d3dad74078f707af107b5082dbe42ffcadb3442ff140c36c8a33b4887","affectsGlobalScope":true,"impliedFormat":1},{"version":"86e0d632e9ef88593e8724ffb6af05104e13a08f9d8df733a30f9991ac387fff","impliedFormat":1},{"version":"7646ad748a9ca15bf43d4c88f83cc851c67f8ec9c1186295605b59ba6bb36dcb","impliedFormat":1},{"version":"cef8931bc129687165253f0642427c2a72705a4613b3ac461b9fa78c7cdaef32","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"47b62c294beb69daa5879f052e416b02e6518f3e4541ae98adbfb27805dd6711","impliedFormat":1},{"version":"f8375506002c556ec412c7e2a5a9ece401079ee5d9eb2c1372e9f5377fac56c7","impliedFormat":1},{"version":"8edd6482bd72eca772f9df15d05c838dd688cdbd4d62690891fca6578cfda6fe","impliedFormat":1},{"version":"07ba29a1a495b710aea48a4cf19ae12b3cbda2a8e9ac62192af477027a99e8de","impliedFormat":1},{"version":"6dead64c944504250dd2fc9095231f36887cfc1534f1ff57737c19f92d165c91","impliedFormat":1},{"version":"b9a4824bb83f25d6d227394db2ed99985308cf2a3a35f0d6d39aa72b15473982","impliedFormat":1},{"version":"6e9948b1e396106601365283680c319a9103c71a5725e7d03e26fe246df60c4c","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e8e284b3832911aeede987e4d74cf0a00f2b03896b2fd3bf924344cc0f96b3c","impliedFormat":1},{"version":"37d37474a969ab1b91fc332eb6a375885dfd25279624dfa84dea48c9aedf4472","impliedFormat":1},{"version":"1ddd8c1a3ae1f8ab28affd53b13910be4afe0b35f28517b7f14c268e9e42647a","impliedFormat":1},{"version":"f1a79b6047d006548185e55478837dfbcdd234d6fe51532783f5dffd401cfb2b","impliedFormat":1},{"version":"cbc91187014fb1e738ef252751a9f84abf2989ec1c3b1637ec23b5b39cdf3d25","impliedFormat":1},{"version":"e822320b448edce0c7ede9cbeada034c72e1f1c8c8281974817030564c63dcb1","impliedFormat":1},{"version":"9d65568cba17c9db40251023406668695ad698ea4a34542364af3e78edd37811","affectsGlobalScope":true,"impliedFormat":1},{"version":"f23e3d484de54d235bf702072100b541553a1df2550bad691fe84995e15cf7be","impliedFormat":1},{"version":"821c79b046e40d54a447bebd9307e70b86399a89980a87bbc98114411169e274","impliedFormat":1},{"version":"17bc38afc78d40b2f54af216c0cc31a4bd0c6897a5945fa39945dfc43260be2c","impliedFormat":1},{"version":"d201b44ff390c220a94fb0ff6a534fe9fa15b44f8a86d0470009cdde3a3e62ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"d44445141f204d5672c502a39c1124bcf1df225eba05df0d2957f79122be87b5","affectsGlobalScope":true,"impliedFormat":1},{"version":"de905bc5f7e7a81cb420e212b95ab5e3ab840f93e0cfa8ce879f6e7fa465d4a2","impliedFormat":1},{"version":"bc2ff43214898bc6d53cab92fb41b5309efec9cbb59a0650525980aee994de2b","impliedFormat":1},{"version":"bede3143eeddca3b8ec3592b09d7eb02042f9e195251040c5146eac09b173236","impliedFormat":1},{"version":"64a40cf4ec8a7a29db2b4bc35f042e5be8537c4be316e5221f40f30ca8ed7051","impliedFormat":1},{"version":"294c082d609e6523520290db4f1d54114ebc83643fb42abd965be5bcc5d9416b","impliedFormat":1},{"version":"cf7d740e39bd8adbdc7840ee91bef0af489052f6467edfcefb7197921757ec3b","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"b9f0681c4d2cb00a5cfe08a7be9662627b912de562926819ebddfe2ef6a9b5ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"b85151402164ab7cb665e58df5c1a29aa25ea4ed3a367f84a15589e7d7a9c8ca","impliedFormat":1},{"version":"89eb8abe2b5c146fbb8f3bf72f4e91de3541f2fb559ad5fed4ad5bf223a3dedb","impliedFormat":1},{"version":"bc6cb10764a82f3025c0f4822b8ad711c16d1a5c75789be2d188d553b69b2d48","affectsGlobalScope":true,"impliedFormat":1},{"version":"41d510caf7ed692923cb6ef5932dc9cf1ed0f57de8eb518c5bab8358a21af674","impliedFormat":1},{"version":"2751c5a6b9054b61c9b03b3770b2d39b1327564672b63e3485ac03ffeb28b4f6","impliedFormat":1},{"version":"dc058956a93388aab38307b7b3b9b6379e1021e73a244aab6ac9427dc3a252a7","impliedFormat":1},{"version":"f33302cf240672359992c356f2005d395b559e176196d03f31a28cc7b01e69bc","impliedFormat":1},{"version":"3ce25041ff6ae06c08fcaccd5fcd9baf4ca6e80e6cb5a922773a1985672e74c2","affectsGlobalScope":true,"impliedFormat":1},{"version":"652c0de14329a834ff06af6ad44670fac35849654a464fd9ae36edb92a362c12","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b1e178016d3fc554505ae087c249b205b1c50624d482c542be9d4682bab81fc","impliedFormat":1},{"version":"5db7c5bb02ef47aaaec6d262d50c4e9355c80937d649365c343fa5e84569621d","impliedFormat":1},{"version":"cf45d0510b661f1da461479851ff902f188edb111777c37055eff12fa986a23a","impliedFormat":1},{"version":"6831f13f06a15391dfeb2477d48ac58311ab675f85846a05499ee92d6e856933","affectsGlobalScope":true,"impliedFormat":1},{"version":"37bef1064b7d015aeaa7c0716fe23a0b3844abe2c0a3df7144153ca8445fe0da","impliedFormat":1},{"version":"83178a1174286d5f5178c5c75067e36c41b975c26be7b86d99cb18393eb30a41","impliedFormat":1},{"version":"d34aa8df2d0b18fb56b1d772ff9b3c7aea7256cf0d692f969be6e1d27b74d660","impliedFormat":1},{"version":"93a3b8e57c68e348fc4054b245bd7cf4893225f56c991028844b693c2fa8c03c","impliedFormat":1},{"version":"2f5747b1508ccf83fad0c251ba1e5da2f5a30b78b09ffa1cfaf633045160afed","impliedFormat":1},{"version":"7850260a82d3d1d797b4a7d4da1f29383ddc847146a11ae9aa0b7c4c2accf49f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b71c603a539078a5e3a039b20f2b0a0d1708967530cf97dec8850a9ca45baa2b","impliedFormat":1},{"version":"d3f2d715f57df3f04bf7b16dde01dec10366f64fce44503c92b8f78f614c1769","impliedFormat":1},{"version":"cb90077223cc1365fa21ef0911a1f9b8f2f878943523d97350dc557973ca3823","impliedFormat":1},{"version":"18f1541b81b80d806120a3489af683edfb811deb91aeca19735d9bb2613e6311","impliedFormat":1},{"version":"104c67f0da1bdf0d94865419247e20eded83ce7f9911a1aa75fc675c077ca66e","impliedFormat":1},{"version":"cc0d0b339f31ce0ab3b7a5b714d8e578ce698f1e13d7f8c60bfb766baeb1d35c","impliedFormat":1},{"version":"1b3395fe23460dd30674c940494aaf8422dc6afbc516a72c431404fb97473c3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"168d88e14e0d81fe170e0dadd38ae9d217476c11435ea640ddb9b7382bdb6c1f","impliedFormat":1},{"version":"8e04cf0688e0d921111659c2b55851957017148fa7b977b02727477d155b3c47","impliedFormat":1},{"version":"25be1eb939c9c63242c7a45446edb20c40541da967f43f1aa6a00ed53c0552db","impliedFormat":1},{"version":"45a384ab9ffe36e2e8cbe834c74afd78c7e18f769c54446d5ef69950a001c107","signature":"9e9c19c683d9ad1ba17b2275137ff6e787b221313cceced78589b5301f2ea533"},{"version":"2bdb732d0ed7e0a0ffe4cb071bbdf2c428160e94160710e8f798341cf3c93e99","signature":"ab1ee2af89e188f6d5c3120afbdb5123154289b5d08190baea9ee43b35287f04"},{"version":"d57be402cf1a3f1bd1852fc71b31ff54da497f64dcdcf8af9ad32435e3f32c1f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2806fb7ba6bba887c0ce022860fa787204c3ce8923bca026cbeae458f3a8704","signature":"7de0aaa8064562187fa7072d1ccc23ab2d34308acec29c846dfaca6323b7f8f5"},{"version":"77ca43e35a68a57d0f1e5ac18cac8e429e520492f5d5487facbbcebcc8a6fd42","signature":"9852e2d6d846f67fb819405dfa30f8176cc3c9c90df47c6f6eb8423d0a2ec377"},{"version":"e3b69d619aaab87f3954673e8c873a5ab82f609773157679f203930e0e46a461","signature":"57a26e29c48d8847d4f26f8d83ea6525047b1056f984f599f0358a38a7233b6b"},{"version":"cdcc132f207d097d7d3aa75615ab9a2e71d6a478162dde8b67f88ea19f3e54de","impliedFormat":1},{"version":"0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","impliedFormat":1},{"version":"e1028394c1cf96d5d057ecc647e31e457b919092f882ed0c7092152b077fed9d","impliedFormat":1},{"version":"f315e1e65a1f80992f0509e84e4ae2df15ecd9ef73df975f7c98813b71e4c8da","impliedFormat":1},{"version":"5b9586e9b0b6322e5bfbd2c29bd3b8e21ab9d871f82346cb71020e3d84bae73e","impliedFormat":1},{"version":"3e70a7e67c2cb16f8cd49097360c0309fe9d1e3210ff9222e9dac1f8df9d4fb6","impliedFormat":1},{"version":"ab68d2a3e3e8767c3fba8f80de099a1cfc18c0de79e42cb02ae66e22dfe14a66","impliedFormat":1},{"version":"d96cc6598148bf1a98fb2e8dcf01c63a4b3558bdaec6ef35e087fd0562eb40ec","impliedFormat":1},{"version":"f8db4fea512ab759b2223b90ecbbe7dae919c02f8ce95ec03f7fb1cf757cfbeb","affectsGlobalScope":true,"impliedFormat":1}],"root":[158,159,[161,163]],"options":{"allowJs":true,"allowSyntheticDefaultImports":true,"composite":false,"declaration":true,"emitDecoratorMetadata":true,"esModuleInterop":true,"experimentalDecorators":true,"importHelpers":true,"inlineSources":true,"module":1,"noEmitOnError":true,"noImplicitAny":false,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":false,"target":4,"useDefineForClassFields":false},"referencedMap":[[164,1],[167,2],[166,1],[153,3],[152,4],[157,4],[147,5],[156,6],[154,5],[155,7],[148,1],[172,8],[149,1],[160,9],[145,1],[146,1],[144,10],[151,11],[150,12],[59,1],[165,1],[171,13],[169,14],[170,15],[168,16],[48,1],[46,1],[47,1],[8,1],[9,1],[11,1],[10,1],[2,1],[12,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[3,1],[20,1],[21,1],[4,1],[22,1],[26,1],[23,1],[24,1],[25,1],[27,1],[28,1],[29,1],[5,1],[30,1],[31,1],[32,1],[33,1],[6,1],[37,1],[34,1],[35,1],[36,1],[38,1],[7,1],[39,1],[44,1],[45,1],[40,1],[41,1],[42,1],[43,1],[1,1],[93,17],[94,17],[95,18],[54,19],[96,20],[97,21],[98,22],[49,1],[52,23],[50,1],[51,1],[99,24],[100,25],[101,26],[102,27],[103,28],[104,29],[105,29],[106,30],[107,31],[108,32],[109,33],[55,1],[53,1],[110,34],[111,35],[112,36],[143,37],[113,38],[114,39],[115,40],[116,41],[117,42],[118,43],[119,44],[120,45],[121,46],[122,47],[123,47],[124,48],[125,49],[127,50],[126,51],[128,52],[129,53],[130,54],[131,55],[132,56],[133,57],[134,58],[135,59],[136,60],[137,61],[138,62],[139,63],[140,64],[56,1],[57,1],[58,1],[92,65],[141,66],[142,67],[75,68],[82,69],[74,68],[89,70],[66,71],[65,72],[88,73],[83,74],[86,75],[68,76],[67,77],[63,78],[62,73],[85,79],[64,80],[69,81],[70,1],[73,81],[60,1],[91,82],[90,81],[77,83],[78,84],[80,85],[76,86],[79,87],[84,73],[71,88],[72,89],[81,90],[61,91],[87,92],[162,93],[163,94],[161,95],[159,96],[158,97]],"version":"5.8.3"}