@fmsim/fmsim 2.0.0-beta.9 → 2.0.0-beta.91

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.
@@ -0,0 +1,658 @@
1
+ "use strict";
2
+ /**
3
+ * Playback 목업 데이터 생성기
4
+ *
5
+ * 대규모 팩토리 레이아웃 시뮬레이션:
6
+ * - AGV 20대, OHT 10대 (VEHICLE 시나리오)
7
+ * - Port 200개 (PORT 시나리오)
8
+ * - Zone 50개 (ZONE 시나리오)
9
+ * - Stocker 10기 (STOCKER 시나리오)
10
+ *
11
+ * 캐리어 이동 시뮬레이션:
12
+ * Vehicle이 Port에서 캐리어를 픽업 → 이동 → 다른 Port에 전달
13
+ * 상태 전이: IDLE → MOVING → LOADING → MOVING → UNLOADING → IDLE
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MOCK_ENTITIES = void 0;
17
+ exports.generatePlaybackData = generatePlaybackData;
18
+ exports.generateCarrierLocationHistory = generateCarrierLocationHistory;
19
+ // ── 엔티티 정의 ──
20
+ const AGVS = Array.from({ length: 20 }, (_, i) => `AGV${String(i + 1).padStart(2, '0')}`);
21
+ const OHTS = Array.from({ length: 10 }, (_, i) => `OHT${String(i + 1).padStart(2, '0')}`);
22
+ const VEHICLES = [...AGVS, ...OHTS];
23
+ const PORTS = Array.from({ length: 200 }, (_, i) => {
24
+ const zone = String(Math.floor(i / 10) + 1).padStart(2, '0');
25
+ const port = String((i % 10) + 1).padStart(2, '0');
26
+ return `ZONE${zone}_PORT${port}`;
27
+ });
28
+ const ZONES = Array.from({ length: 50 }, (_, i) => `ZONE${String(i + 1).padStart(2, '0')}`);
29
+ const STOCKERS = Array.from({ length: 10 }, (_, i) => `STK${String(i + 1).padStart(2, '0')}`);
30
+ const EQUIPMENTS = Array.from({ length: 20 }, (_, i) => `EQP${String(i + 1).padStart(2, '0')}`);
31
+ const CONVEYORS = Array.from({ length: 30 }, (_, i) => `CNV${String(i + 1).padStart(2, '0')}`);
32
+ const OHTLINES = ['OHTLINE01', 'OHTLINE02', 'OHTLINE03'];
33
+ const AGVLINES = ['AGVLINE01', 'AGVLINE02', 'AGVLINE03', 'AGVLINE04', 'AGVLINE05', 'AGVLINE06'];
34
+ const LINES = [...OHTLINES, ...AGVLINES];
35
+ // ── 노드 경로 (Vehicle 이동 경로) ──
36
+ const NODE_IDS = Array.from({ length: 100 }, (_, i) => String(i + 1).padStart(4, '0'));
37
+ // 노드별 소속 라인 (보드 모델의 NodeMachine과 정확히 일치해야 함)
38
+ const NODE_MACHINE_MAP = [
39
+ [0, 6, 'OHTLINE01'], // 0001-0006
40
+ [6, 12, 'OHTLINE02'], // 0007-0012
41
+ [12, 18, 'OHTLINE03'], // 0013-0018
42
+ [18, 30, 'OHTLINE01'], // 0019-0030
43
+ [30, 36, 'AGVLINE01'], // 0031-0036
44
+ [36, 42, 'AGVLINE02'], // 0037-0042
45
+ [42, 48, 'AGVLINE03'], // 0043-0048
46
+ [48, 60, 'AGVLINE01'], // 0049-0060
47
+ [60, 66, 'AGVLINE04'], // 0061-0066
48
+ [66, 72, 'AGVLINE05'], // 0067-0072
49
+ [72, 78, 'AGVLINE06'], // 0073-0078
50
+ [78, 100, 'AGVLINE04'], // 0079-0100
51
+ ];
52
+ const NODE_MACHINES = {};
53
+ NODE_IDS.forEach((id, i) => {
54
+ const entry = NODE_MACHINE_MAP.find(([from, to]) => i >= from && i < to);
55
+ if (entry)
56
+ NODE_MACHINES[id] = entry[2];
57
+ });
58
+ // 차량별 이동 가능 노드 범위
59
+ const AGV_NODES = NODE_IDS.filter(id => { var _a; return (_a = NODE_MACHINES[id]) === null || _a === void 0 ? void 0 : _a.startsWith('AGVLINE'); });
60
+ const OHT_NODES = NODE_IDS.filter(id => { var _a; return (_a = NODE_MACHINES[id]) === null || _a === void 0 ? void 0 : _a.startsWith('OHTLINE'); });
61
+ // 포트별 연결 노드 — 포트를 차량 유형별 노드 범위에 매핑
62
+ // ZONE01-05(포트 0-49): OHT 영역, ZONE06-20(포트 50-199): AGV 영역
63
+ const PORT_NODES = {};
64
+ PORTS.forEach((port, i) => {
65
+ if (i < 50) {
66
+ PORT_NODES[port] = OHT_NODES[i % OHT_NODES.length];
67
+ }
68
+ else {
69
+ PORT_NODES[port] = AGV_NODES[(i - 50) % AGV_NODES.length];
70
+ }
71
+ });
72
+ // 차량 유형별 포트 범위
73
+ const OHT_PORTS = PORTS.slice(0, 50);
74
+ const AGV_PORTS = PORTS.slice(50);
75
+ // ── 상태 값 ──
76
+ const CARRIER_STATUSES = ['FULL', 'EMPTY', 'EMPTYEMPTY'];
77
+ const PORT_STATUSES = ['2001', '2002', '2005', '2006']; // DOWN, EMPTY, FULL, NOTINSERVICE
78
+ const MACHINE_STATUSES = ['1001', '1002', '1005', '1006', '1007', '1008']; // UP, OUTOFSERVICE, DOWN, NOTAUTO, ALARM, FULL
79
+ const VEHICLE_STATUSES = ['4001', '4006', '4007', '4008']; // ASSIGNED, NOTASSIGNED, INSTALLED, CHARGING
80
+ const UNIT_STATUSES = ['2002', '2003', '2005']; // EMPTY, EMPTY_RESERVED, FULL
81
+ // ── 유틸리티 ──
82
+ function pick(arr) {
83
+ return arr[Math.floor(Math.random() * arr.length)];
84
+ }
85
+ function randInt(min, max) {
86
+ return Math.floor(Math.random() * (max - min + 1)) + min;
87
+ }
88
+ function formatTime(date) {
89
+ const pad = (n) => String(n).padStart(2, '0');
90
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
91
+ }
92
+ // ── 시뮬레이션 엔진 ──
93
+ class FactorySimulator {
94
+ constructor() {
95
+ this.vehicles = [];
96
+ this.ports = [];
97
+ this.zones = [];
98
+ this.stockers = [];
99
+ this.equipments = [];
100
+ this.conveyors = [];
101
+ this.lines = [];
102
+ this.carrierSeq = 1;
103
+ this.initVehicles();
104
+ this.initPorts();
105
+ this.initZones();
106
+ this.initStockers();
107
+ this.equipments = EQUIPMENTS.map(name => ({ name, status: pick(MACHINE_STATUSES) }));
108
+ this.conveyors = CONVEYORS.map(name => ({ name, status: pick(MACHINE_STATUSES) }));
109
+ this.lines = LINES.map(name => ({ name, status: pick(MACHINE_STATUSES) }));
110
+ }
111
+ initVehicles() {
112
+ this.vehicles = VEHICLES.map(name => {
113
+ const nodes = name.startsWith('OHT') ? OHT_NODES : AGV_NODES;
114
+ return {
115
+ name,
116
+ phase: 'IDLE',
117
+ nodeId: pick(nodes),
118
+ targetNodeId: pick(nodes),
119
+ carrierName: null,
120
+ carrierStatus: null,
121
+ emptyType: 'EMPTY',
122
+ batteryRate: name.startsWith('AGV') ? randInt(30, 100) : null,
123
+ phaseTimer: randInt(1, 5),
124
+ sourcePort: null,
125
+ destPort: null,
126
+ };
127
+ });
128
+ }
129
+ initPorts() {
130
+ this.ports = PORTS.map(name => ({
131
+ name,
132
+ status: pick(PORT_STATUSES),
133
+ carrierName: Math.random() > 0.3 ? this.newCarrierName() : null,
134
+ carrierStatus: null,
135
+ emptyType: null,
136
+ }));
137
+ // 캐리어가 있는 포트의 상태 설정
138
+ this.ports.forEach(p => {
139
+ if (p.carrierName) {
140
+ p.emptyType = 'FULL';
141
+ p.carrierStatus = pick(CARRIER_STATUSES);
142
+ }
143
+ else {
144
+ p.emptyType = 'EMPTY';
145
+ }
146
+ });
147
+ }
148
+ initZones() {
149
+ this.zones = ZONES.map(name => {
150
+ const max = randInt(20, 100);
151
+ return {
152
+ name,
153
+ status: pick(MACHINE_STATUSES),
154
+ currentCapacity: randInt(0, max),
155
+ maxCapacity: max,
156
+ highWaterMark: String(Math.round(max * 0.8)),
157
+ lowWaterMark: String(Math.round(max * 0.6)),
158
+ };
159
+ });
160
+ }
161
+ initStockers() {
162
+ this.stockers = STOCKERS.map(name => {
163
+ const max = randInt(50, 200);
164
+ const cur = randInt(0, max);
165
+ return {
166
+ name,
167
+ status: pick(MACHINE_STATUSES),
168
+ currentCapacity: cur,
169
+ maxCapacity: max,
170
+ highWaterMark: Math.round(max * 0.8),
171
+ lowWaterMark: Math.round(max * 0.2),
172
+ };
173
+ });
174
+ }
175
+ newCarrierName() {
176
+ return `PLATE-${String(this.carrierSeq++).padStart(4, '0')}`;
177
+ }
178
+ // ── 1초 진행 ──
179
+ tick() {
180
+ const vehicleChanges = [];
181
+ const portChanges = [];
182
+ for (const v of this.vehicles) {
183
+ const changed = this.tickVehicle(v, portChanges);
184
+ if (changed)
185
+ vehicleChanges.push(changed);
186
+ }
187
+ // 인접 포트 간 캐리어 이동 (컨베이어 흐름 시뮬레이션)
188
+ this.tickConveyorFlow(portChanges);
189
+ const zoneChanges = this.tickZones();
190
+ const stockerChanges = this.tickStockers();
191
+ const equipmentChanges = this.tickMachines(this.equipments);
192
+ const conveyorChanges = this.tickMachines(this.conveyors);
193
+ const lineChanges = this.tickMachines(this.lines);
194
+ return { vehicleChanges, portChanges, zoneChanges, stockerChanges, equipmentChanges, conveyorChanges, lineChanges };
195
+ }
196
+ tickMachines(machines) {
197
+ const changes = [];
198
+ for (const m of machines) {
199
+ if (Math.random() > 0.05)
200
+ continue; // 5% 확률로 상태 변경
201
+ m.status = pick(MACHINE_STATUSES);
202
+ changes.push({ NAME: m.name, STATUS: m.status });
203
+ }
204
+ return changes;
205
+ }
206
+ tickVehicle(v, portChanges) {
207
+ v.phaseTimer--;
208
+ // AGV 배터리 감소
209
+ if (v.batteryRate != null && v.phase !== 'IDLE') {
210
+ v.batteryRate = Math.max(0, v.batteryRate - (Math.random() > 0.8 ? 1 : 0));
211
+ }
212
+ if (v.phaseTimer > 0)
213
+ return null;
214
+ // Phase 전이
215
+ switch (v.phase) {
216
+ case 'IDLE': {
217
+ // 차량 유형별 포트 범위에서 캐리어가 있는 포트를 찾아 픽업하러 출발
218
+ const myPorts = v.name.startsWith('OHT') ? OHT_PORTS : AGV_PORTS;
219
+ const sourcePort = this.ports.find(p => myPorts.includes(p.name) && p.carrierName && (p.status === '2002' || p.status === '2003'));
220
+ if (!sourcePort) {
221
+ v.phaseTimer = randInt(2, 5);
222
+ return null;
223
+ }
224
+ v.phase = 'MOVING_TO_PICKUP';
225
+ v.sourcePort = sourcePort.name;
226
+ v.targetNodeId = PORT_NODES[sourcePort.name] || pick(v.name.startsWith('OHT') ? OHT_NODES : AGV_NODES);
227
+ v.phaseTimer = randInt(3, 10); // 이동 시간
228
+ return this.vehicleRecord(v, '4001');
229
+ }
230
+ case 'MOVING_TO_PICKUP': {
231
+ // 이동 중 노드 변경 (경로 시뮬레이션)
232
+ v.nodeId = v.targetNodeId;
233
+ v.phase = 'LOADING';
234
+ v.phaseTimer = randInt(2, 4); // 로딩 시간
235
+ return this.vehicleRecord(v, '4001');
236
+ }
237
+ case 'LOADING': {
238
+ // 포트에서 캐리어 픽업
239
+ const port = this.ports.find(p => p.name === v.sourcePort);
240
+ if (port && port.carrierName) {
241
+ v.carrierName = port.carrierName;
242
+ v.carrierStatus = port.carrierStatus;
243
+ v.emptyType = 'FULL';
244
+ // 포트에서 캐리어 제거
245
+ port.carrierName = null;
246
+ port.carrierStatus = null;
247
+ port.emptyType = 'EMPTY';
248
+ portChanges.push(this.portRecord(port));
249
+ }
250
+ // 차량 유형별 포트 범위에서 드롭할 빈 포트 선택
251
+ const dropPorts = v.name.startsWith('OHT') ? OHT_PORTS : AGV_PORTS;
252
+ const destPort = this.ports.find(p => dropPorts.includes(p.name) && !p.carrierName && (p.status === '2002' || p.status === '2003') && p.name !== v.sourcePort);
253
+ v.destPort = (destPort === null || destPort === void 0 ? void 0 : destPort.name) || pick(dropPorts);
254
+ v.targetNodeId = PORT_NODES[v.destPort] || pick(v.name.startsWith('OHT') ? OHT_NODES : AGV_NODES);
255
+ v.phase = 'MOVING_TO_DROP';
256
+ v.phaseTimer = randInt(3, 12); // 이동 시간
257
+ return this.vehicleRecord(v, '4001');
258
+ }
259
+ case 'MOVING_TO_DROP': {
260
+ // 이동 중 노드 이동
261
+ v.nodeId = v.targetNodeId;
262
+ v.phase = 'UNLOADING';
263
+ v.phaseTimer = randInt(2, 4); // 언로딩 시간
264
+ return this.vehicleRecord(v, '4001');
265
+ }
266
+ case 'UNLOADING': {
267
+ // 포트에 캐리어 전달
268
+ const port = this.ports.find(p => p.name === v.destPort);
269
+ if (port && !port.carrierName && v.carrierName) {
270
+ port.carrierName = v.carrierName;
271
+ port.carrierStatus = v.carrierStatus;
272
+ port.emptyType = 'FULL';
273
+ portChanges.push(this.portRecord(port));
274
+ }
275
+ v.carrierName = null;
276
+ v.carrierStatus = null;
277
+ v.emptyType = 'EMPTY';
278
+ v.sourcePort = null;
279
+ v.destPort = null;
280
+ v.phase = 'IDLE';
281
+ v.phaseTimer = randInt(1, 5); // 대기 시간
282
+ // AGV 충전 (낮은 배터리)
283
+ if (v.batteryRate != null && v.batteryRate < 20) {
284
+ v.phaseTimer = randInt(5, 15);
285
+ return this.vehicleRecord(v, '4008');
286
+ }
287
+ return this.vehicleRecord(v, '4006');
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ // 이동 중 매 초 노드 이동 시뮬레이션
293
+ tickVehicleMoving(v) {
294
+ if (v.phase !== 'MOVING_TO_PICKUP' && v.phase !== 'MOVING_TO_DROP')
295
+ return null;
296
+ if (v.phaseTimer <= 0)
297
+ return null;
298
+ // 차량 유형별 노드 범위 내에서 순차 이동
299
+ const nodes = v.name.startsWith('OHT') ? OHT_NODES : AGV_NODES;
300
+ const currentIdx = nodes.indexOf(v.nodeId);
301
+ const targetIdx = nodes.indexOf(v.targetNodeId);
302
+ if (currentIdx >= 0 && targetIdx >= 0 && currentIdx !== targetIdx) {
303
+ const step = currentIdx < targetIdx ? 1 : -1;
304
+ v.nodeId = nodes[currentIdx + step] || v.nodeId;
305
+ return this.vehicleRecord(v, '4001');
306
+ }
307
+ return null;
308
+ }
309
+ vehicleRecord(v, status) {
310
+ return Object.assign({ NAME: v.name, STATUS: status, EMPTYTYPE: v.emptyType, NODEMACHINE: NODE_MACHINES[v.nodeId] || v.name, CARRIERNAME: v.carrierName, CARRIERSTATUS: v.carrierStatus, SUBNAME: `V${String(randInt(1, 999)).padStart(3, '0')}`, NODEID: v.nodeId }, (v.batteryRate != null ? { BATTERYRATE: v.batteryRate } : {}));
311
+ }
312
+ portRecord(p) {
313
+ return {
314
+ NAME: p.name,
315
+ STATUS: p.status,
316
+ EMPTYTYPE: p.emptyType,
317
+ CARRIERNAME: p.carrierName,
318
+ CARRIERSTATUS: p.carrierStatus,
319
+ };
320
+ }
321
+ /**
322
+ * 인접 포트 간 캐리어 이동 시뮬레이션 (컨베이어 흐름).
323
+ * 같은 ZONE 내 포트끼리 한 칸씩 이동 — 매 틱 30% 확률.
324
+ */
325
+ tickConveyorFlow(portChanges) {
326
+ var _a;
327
+ // ZONE별 10개 포트 그룹
328
+ for (let z = 0; z < 20; z++) {
329
+ if (Math.random() > 0.3)
330
+ continue;
331
+ const start = z * 10;
332
+ const zonePorts = this.ports.slice(start, start + 10);
333
+ // 캐리어가 있는 포트 → 인접 빈 포트로 이동
334
+ for (let i = 0; i < zonePorts.length; i++) {
335
+ const src = zonePorts[i];
336
+ if (!src.carrierName)
337
+ continue;
338
+ // 인접 포트 (i-1 또는 i+1) 중 빈 것
339
+ const neighbors = [i - 1, i + 1].filter(j => j >= 0 && j < zonePorts.length);
340
+ const dest = zonePorts[(_a = neighbors.find(j => !zonePorts[j].carrierName)) !== null && _a !== void 0 ? _a : -1];
341
+ if (!dest)
342
+ continue;
343
+ // 이동
344
+ dest.carrierName = src.carrierName;
345
+ dest.carrierStatus = src.carrierStatus;
346
+ dest.emptyType = 'FULL';
347
+ src.carrierName = null;
348
+ src.carrierStatus = null;
349
+ src.emptyType = 'EMPTY';
350
+ portChanges.push(this.portRecord(src));
351
+ portChanges.push(this.portRecord(dest));
352
+ break; // ZONE당 1회 이동
353
+ }
354
+ }
355
+ }
356
+ tickZones() {
357
+ const changes = [];
358
+ for (const z of this.zones) {
359
+ if (Math.random() > 0.15)
360
+ continue;
361
+ const delta = randInt(-3, 3);
362
+ const newCap = Math.max(0, Math.min(z.maxCapacity, z.currentCapacity + delta));
363
+ if (newCap === z.currentCapacity)
364
+ continue;
365
+ z.currentCapacity = newCap;
366
+ changes.push({
367
+ NAME: z.name,
368
+ STATUS: z.status,
369
+ CURRENTCAPACITY: z.currentCapacity,
370
+ MAXCAPACITY: z.maxCapacity,
371
+ HIGHWATERMARK: z.highWaterMark,
372
+ LOWWATERMARK: z.lowWaterMark,
373
+ FULLUP: z.currentCapacity >= z.maxCapacity ? 'T' : 'F',
374
+ });
375
+ }
376
+ return changes;
377
+ }
378
+ tickStockers() {
379
+ const changes = [];
380
+ for (const s of this.stockers) {
381
+ if (Math.random() > 0.4)
382
+ continue;
383
+ const delta = randInt(-5, 5);
384
+ const newCap = Math.max(0, Math.min(s.maxCapacity, s.currentCapacity + delta));
385
+ const prevStatus = s.status;
386
+ if (Math.random() < 0.05)
387
+ s.status = pick(MACHINE_STATUSES);
388
+ if (newCap === s.currentCapacity && s.status === prevStatus)
389
+ continue;
390
+ s.currentCapacity = newCap;
391
+ changes.push({
392
+ NAME: s.name,
393
+ STATUS: s.status,
394
+ CURRENTCAPACITY: s.currentCapacity,
395
+ MAXCAPACITY: s.maxCapacity,
396
+ HIGHWATERMARK: s.highWaterMark,
397
+ LOWWATERMARK: s.lowWaterMark,
398
+ FULLUP: s.currentCapacity >= s.maxCapacity ? 'T' : 'F',
399
+ });
400
+ }
401
+ return changes;
402
+ }
403
+ // ── Snapshot 생성 ──
404
+ getVehicleSnapshot() {
405
+ return this.vehicles.map(v => {
406
+ const status = v.phase === 'IDLE' ? '4006' // NOTASSIGNED
407
+ : v.phase === 'LOADING' ? '4001' // ASSIGNED
408
+ : v.phase === 'UNLOADING' ? '4001' // ASSIGNED
409
+ : '4001'; // ASSIGNED (moving)
410
+ return this.vehicleRecord(v, status);
411
+ });
412
+ }
413
+ getPortSnapshot() {
414
+ return this.ports.map(p => this.portRecord(p));
415
+ }
416
+ getZoneSnapshot() {
417
+ return this.zones.map(z => ({
418
+ NAME: z.name,
419
+ STATUS: z.status,
420
+ CURRENTCAPACITY: z.currentCapacity,
421
+ MAXCAPACITY: z.maxCapacity,
422
+ HIGHWATERMARK: z.highWaterMark,
423
+ LOWWATERMARK: z.lowWaterMark,
424
+ FULLUP: z.currentCapacity >= z.maxCapacity ? 'T' : 'F',
425
+ }));
426
+ }
427
+ getStockerSnapshot() {
428
+ return this.stockers.map(s => ({
429
+ NAME: s.name,
430
+ STATUS: s.status,
431
+ CURRENTCAPACITY: s.currentCapacity,
432
+ MAXCAPACITY: s.maxCapacity,
433
+ HIGHWATERMARK: s.highWaterMark,
434
+ LOWWATERMARK: s.lowWaterMark,
435
+ FULLUP: s.currentCapacity >= s.maxCapacity ? 'T' : 'F',
436
+ }));
437
+ }
438
+ }
439
+ /**
440
+ * Mock playback 데이터 생성.
441
+ * - `endTime` 제공: 범위 기반 (startTime → endTime 까지 1초 간격 diff)
442
+ * - `count` 제공: 프레임 수 기반 (snapshot 포함 총 count 개 프레임)
443
+ * - 둘 다 없으면 빈 배열
444
+ * - 둘 다 있으면 `endTime` 우선
445
+ */
446
+ function generatePlaybackData(startTime, opts) {
447
+ const start = new Date(startTime.replace(' ', 'T'));
448
+ // 총 생성할 프레임 수 결정 (첫 프레임은 snapshot, 나머지는 diff)
449
+ let totalFrames;
450
+ if (opts.endTime) {
451
+ const end = new Date(opts.endTime.replace(' ', 'T'));
452
+ const totalSeconds = Math.floor((end.getTime() - start.getTime()) / 1000);
453
+ if (totalSeconds < 0 || totalSeconds > 7200)
454
+ return [];
455
+ totalFrames = totalSeconds + 1;
456
+ }
457
+ else if (opts.count && opts.count > 0) {
458
+ totalFrames = Math.min(opts.count, 7201);
459
+ }
460
+ else {
461
+ return [];
462
+ }
463
+ const sim = new FactorySimulator();
464
+ const t0 = formatTime(start);
465
+ const vehicleFrames = [
466
+ { type: 'snapshot', time: t0, data: sim.getVehicleSnapshot() }
467
+ ];
468
+ const portFrames = [
469
+ { type: 'snapshot', time: t0, data: sim.getPortSnapshot() }
470
+ ];
471
+ const zoneFrames = [
472
+ { type: 'snapshot', time: t0, data: sim.getZoneSnapshot() }
473
+ ];
474
+ const stockerFrames = [
475
+ { type: 'snapshot', time: t0, data: sim.getStockerSnapshot() }
476
+ ];
477
+ const equipmentFrames = [
478
+ { type: 'snapshot', time: t0, data: sim.equipments.map(m => ({ NAME: m.name, STATUS: m.status })) }
479
+ ];
480
+ const conveyorFrames = [
481
+ { type: 'snapshot', time: t0, data: sim.conveyors.map(m => ({ NAME: m.name, STATUS: m.status })) }
482
+ ];
483
+ const lineFrames = [
484
+ { type: 'snapshot', time: t0, data: sim.lines.map(m => ({ NAME: m.name, STATUS: m.status })) }
485
+ ];
486
+ // diff 프레임 (snapshot 외 추가 프레임이 있으면)
487
+ for (let s = 1; s < totalFrames; s++) {
488
+ const t = formatTime(new Date(start.getTime() + s * 1000));
489
+ // 이동 중인 차량의 노드 변경 (매 초)
490
+ const movingChanges = [];
491
+ for (const v of sim.vehicles) {
492
+ const mc = sim.tickVehicleMoving(v);
493
+ if (mc)
494
+ movingChanges.push(mc);
495
+ }
496
+ // 상태 전이 처리
497
+ const { vehicleChanges, portChanges, zoneChanges, stockerChanges, equipmentChanges, conveyorChanges, lineChanges } = sim.tick();
498
+ // 이동 변경 + 상태 전이 병합 (같은 vehicle은 상태 전이가 우선)
499
+ const vehicleChangeMap = new Map();
500
+ for (const c of movingChanges)
501
+ vehicleChangeMap.set(c.NAME, c);
502
+ for (const c of vehicleChanges)
503
+ vehicleChangeMap.set(c.NAME, c);
504
+ vehicleFrames.push({ type: 'diff', time: t, data: [...vehicleChangeMap.values()] });
505
+ portFrames.push({ type: 'diff', time: t, data: portChanges });
506
+ zoneFrames.push({ type: 'diff', time: t, data: zoneChanges });
507
+ stockerFrames.push({ type: 'diff', time: t, data: stockerChanges });
508
+ equipmentFrames.push({ type: 'diff', time: t, data: equipmentChanges });
509
+ conveyorFrames.push({ type: 'diff', time: t, data: conveyorChanges });
510
+ lineFrames.push({ type: 'diff', time: t, data: lineChanges });
511
+ }
512
+ return [
513
+ { scenarioName: 'MCS_LV_VEHICLE', snapshots: vehicleFrames },
514
+ { scenarioName: 'MCS_LV_PORT', snapshots: portFrames },
515
+ { scenarioName: 'MCS_LV_ZONE', snapshots: zoneFrames },
516
+ { scenarioName: 'MCS_LV_STOCKER', snapshots: stockerFrames },
517
+ { scenarioName: 'MCS_LV_EQUIPMENT', snapshots: equipmentFrames },
518
+ { scenarioName: 'MCS_LV_CONVEYOR', snapshots: conveyorFrames },
519
+ { scenarioName: 'MCS_LV_LINE', snapshots: lineFrames },
520
+ ];
521
+ }
522
+ // ── 엔티티 목록 (보드 작화용) ──
523
+ exports.MOCK_ENTITIES = {
524
+ vehicles: { agvs: AGVS, ohts: OHTS },
525
+ ports: PORTS,
526
+ zones: ZONES,
527
+ stockers: STOCKERS,
528
+ equipments: EQUIPMENTS,
529
+ conveyors: CONVEYORS,
530
+ lines: LINES,
531
+ };
532
+ /** 문자열 해시 (carrierName → seed). djb2 변형. */
533
+ function hashString(s) {
534
+ let h = 5381;
535
+ for (let i = 0; i < s.length; i++)
536
+ h = ((h << 5) + h + s.charCodeAt(i)) | 0;
537
+ return Math.abs(h) || 1;
538
+ }
539
+ /** Mulberry32 PRNG — 가벼운 deterministic 난수. */
540
+ function seededRng(seed) {
541
+ let s = seed >>> 0;
542
+ return () => {
543
+ s = (s + 0x6d2b79f5) >>> 0;
544
+ let t = s;
545
+ t = Math.imul(t ^ (t >>> 15), t | 1);
546
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
547
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
548
+ };
549
+ }
550
+ /** "YYYY-MM-DDTHH:mm:ss" — 정확히 초 단위 (ms 없음, T 구분자). */
551
+ function formatTimeSec(date) {
552
+ const pad = (n) => String(n).padStart(2, '0');
553
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
554
+ }
555
+ /**
556
+ * 위치 풀 — playback mock 이 사용하는 동일 식별자만 사용.
557
+ * 영구추적 fallback resolver 가 scene.findFirst 로 매칭하므로 보드 컴포넌트
558
+ * id 와 정확히 일치해야 함.
559
+ *
560
+ * 머신/유닛 매핑:
561
+ * PORT (예: ZONE01_PORT01) — machine = "ZONE01", unit = "ZONE01_PORT01"
562
+ * PORT 는 ZONE 의 sub-unit. fallback 은 unit 우선 lookup, miss 면 machine.
563
+ * STOCKER / EQUIPMENT / CONVEYOR — machine == unit (자체가 단일 단위)
564
+ */
565
+ const CARRIER_LOCATION_POOL = (() => {
566
+ const pool = [];
567
+ for (const port of PORTS) {
568
+ const zone = port.split('_')[0];
569
+ pool.push({ machine: zone, unit: port });
570
+ }
571
+ for (const m of STOCKERS)
572
+ pool.push({ machine: m, unit: m });
573
+ for (const m of EQUIPMENTS)
574
+ pool.push({ machine: m, unit: m });
575
+ for (const m of CONVEYORS)
576
+ pool.push({ machine: m, unit: m });
577
+ return pool;
578
+ })();
579
+ /**
580
+ * 단일 carrier 의 시간 구간 내 위치 이력 mock.
581
+ *
582
+ * 동작:
583
+ * - **매 초마다 한 record** 제공 (1시간 = 3601개, 2시간 = 7201개).
584
+ * - carrierName 기반 deterministic — 같은 인자엔 같은 결과 (재현성).
585
+ * - 위치는 playback mock 의 PORTS / STOCKERS / EQUIPMENTS / CONVEYORS 에서만 선택
586
+ * → 보드 컴포넌트 id 와 항상 일치.
587
+ * - carrier 가 한 위치에 5–30초 머문 후 다음 위치로 전환.
588
+ * - 위치 전환 시 15% 확률로 NA (Not Available) 진입. NA 도 동일하게 5–30초 지속.
589
+ * - 시간 포맷: "YYYY-MM-DDTHH:mm:ss" — 정확히 초 단위, T 구분자.
590
+ *
591
+ * 호출자가 max 2시간 범위 (== /playback 과 동일) 안에서 사용한다고 가정.
592
+ */
593
+ /**
594
+ * 테스트 모드 — machineName / unitName 을 각각 독립적으로 풀에서 random 선택.
595
+ * 풀: STK01~STK10 (총 10개 값).
596
+ * 정상 시뮬레이션으로 복원하려면 false 로 토글.
597
+ */
598
+ const TEST_FIXED_LOCATION = true;
599
+ /** 테스트 모드 값 풀 — STK01~STK10 (10개). */
600
+ const TEST_VALUE_POOL = Array.from({ length: 10 }, (_, i) => `STK${String(i + 1).padStart(2, '0')}`);
601
+ function generateCarrierLocationHistory(startTime, endTime, carrierName) {
602
+ const start = new Date(startTime.replace(' ', 'T'));
603
+ const end = new Date(endTime.replace(' ', 'T'));
604
+ const durationMs = end.getTime() - start.getTime();
605
+ if (durationMs <= 0 || durationMs > 7200000)
606
+ return [];
607
+ const startSec = Math.floor(start.getTime() / 1000) * 1000;
608
+ const totalSec = Math.floor(durationMs / 1000);
609
+ const result = [];
610
+ if (TEST_FIXED_LOCATION) {
611
+ // 테스트 모드 — machineName / unitName 을 풀에서 각각 독립적으로 random 선택,
612
+ // 5-30초마다 동시에 갱신. carrierName 기반 deterministic. NA 없음.
613
+ const rng = seededRng(hashString(carrierName));
614
+ const rngInt = (min, max) => Math.floor(rng() * (max - min + 1)) + min;
615
+ const pickValue = () => TEST_VALUE_POOL[Math.floor(rng() * TEST_VALUE_POOL.length)];
616
+ let machineName = pickValue();
617
+ let unitName = pickValue();
618
+ let nextChangeAt = rngInt(5, 30);
619
+ for (let s = 0; s <= totalSec; s++) {
620
+ if (s === nextChangeAt) {
621
+ machineName = pickValue();
622
+ unitName = pickValue();
623
+ nextChangeAt = s + rngInt(5, 30);
624
+ }
625
+ result.push({
626
+ time: formatTimeSec(new Date(startSec + s * 1000)),
627
+ machineName,
628
+ unitName
629
+ });
630
+ }
631
+ return result;
632
+ }
633
+ // ── 정상 시뮬레이션 (carrier 가 시간에 따라 여러 위치를 옮겨다님) ──
634
+ const rng = seededRng(hashString(carrierName));
635
+ const rngInt = (min, max) => Math.floor(rng() * (max - min + 1)) + min;
636
+ const NA_LOC = { machine: 'NA', unit: 'NA' };
637
+ const pickLocation = () => {
638
+ // 15% 확률로 NA 구간 진입, 나머지는 풀에서 random
639
+ if (rng() < 0.15)
640
+ return NA_LOC;
641
+ return CARRIER_LOCATION_POOL[Math.floor(rng() * CARRIER_LOCATION_POOL.length)];
642
+ };
643
+ let cur = pickLocation();
644
+ let nextChangeAt = rngInt(5, 30); // 다음 위치 전환 시점 (초)
645
+ for (let s = 0; s <= totalSec; s++) {
646
+ if (s === nextChangeAt) {
647
+ cur = pickLocation();
648
+ nextChangeAt = s + rngInt(5, 30);
649
+ }
650
+ result.push({
651
+ time: formatTimeSec(new Date(startSec + s * 1000)),
652
+ machineName: cur.machine,
653
+ unitName: cur.unit
654
+ });
655
+ }
656
+ return result;
657
+ }
658
+ //# sourceMappingURL=mock-data.js.map