@fmsim/figure-server 0.0.0-stage → 3.0.0-beta.1
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 +69 -2
- package/dist/app.d.ts +3 -0
- package/dist/app.js +35 -0
- package/dist/app.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/figure-router.d.ts +3 -0
- package/dist/routes/figure-router.js +149 -0
- package/dist/routes/figure-router.js.map +1 -0
- package/dist/storage/figure-storage.d.ts +15 -0
- package/dist/storage/figure-storage.js +132 -0
- package/dist/storage/figure-storage.js.map +1 -0
- package/dist/types/figure.d.ts +37 -0
- package/dist/types/figure.js +3 -0
- package/dist/types/figure.js.map +1 -0
- package/package.json +33 -4
- package/src/app.ts +36 -0
- package/src/index.ts +23 -0
- package/src/routes/figure-router.ts +168 -0
- package/src/storage/figure-storage.ts +144 -0
- package/src/types/figure.ts +40 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fmsim/figure-server
|
|
2
2
|
|
|
3
|
-
|
|
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
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
|
package/dist/app.js.map
ADDED
|
@@ -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"]}
|
package/dist/index.d.ts
ADDED
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,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 @@
|
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "3.0.0-beta.1",
|
|
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": "f862081dfd1570ca17701496dd6b8ba116ee1471"
|
|
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 @@
|
|
|
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"}
|