@things-factory/headless-twin 10.0.6 โ 10.0.7
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 +37 -24
- package/dist-server/engine/entity-delta.d.ts +13 -1
- package/dist-server/engine/entity-delta.js +130 -16
- package/dist-server/engine/entity-delta.js.map +1 -1
- package/dist-server/engine/index.d.ts +4 -0
- package/dist-server/engine/index.js +4 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +20 -3
- package/dist-server/engine/kpi-fold.js +21 -4
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +9 -0
- package/dist-server/engine/kpi-query.js +118 -19
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/kpi-target.d.ts +141 -0
- package/dist-server/engine/kpi-target.js +137 -0
- package/dist-server/engine/kpi-target.js.map +1 -0
- package/dist-server/engine/live-attentions.d.ts +4 -2
- package/dist-server/engine/live-attentions.js +5 -1
- package/dist-server/engine/live-attentions.js.map +1 -1
- package/dist-server/engine/measured-estimator.d.ts +50 -0
- package/dist-server/engine/measured-estimator.js +78 -0
- package/dist-server/engine/measured-estimator.js.map +1 -0
- package/dist-server/engine/model-basis.d.ts +23 -0
- package/dist-server/engine/model-basis.js +100 -0
- package/dist-server/engine/model-basis.js.map +1 -0
- package/dist-server/engine/oee-accumulator.d.ts +2 -2
- package/dist-server/engine/oee-accumulator.js +4 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/spec-coverage.d.ts +49 -0
- package/dist-server/engine/spec-coverage.js +60 -0
- package/dist-server/engine/spec-coverage.js.map +1 -0
- package/dist-server/engine/structure-diff.d.ts +25 -0
- package/dist-server/engine/structure-diff.js +63 -0
- package/dist-server/engine/structure-diff.js.map +1 -0
- package/dist-server/engine/travel-estimator.d.ts +57 -0
- package/dist-server/engine/travel-estimator.js +120 -0
- package/dist-server/engine/travel-estimator.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +153 -17
- package/dist-server/engine/twin-engine.js +473 -81
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/warm-start.d.ts +5 -5
- package/dist-server/engine/warm-start.js +4 -4
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +21 -14
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/reference-live.js +2 -1
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +307 -1
- package/dist-server/service/reference/reference-master.js +96 -8
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.js +3 -3
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/reference/template-registry.d.ts +1 -1
- package/dist-server/service/reference/template-registry.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
- package/dist-server/service/twin-event/twin-event-keys.js +3 -3
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +11 -0
- package/dist-server/service/twin-event/twin-event.js +6 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +50 -7
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +1 -1
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +9 -0
- package/dist-server/service/twin-journal/twin-journal-query.js +54 -3
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +4 -3
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-area.js +1 -1
- package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +3 -3
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +11 -0
- package/dist-server/service/twin-space/twin-space.js +5 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-structure/index.d.ts +2 -0
- package/dist-server/service/twin-structure/index.js +6 -0
- package/dist-server/service/twin-structure/index.js.map +1 -0
- package/dist-server/service/twin-structure/twin-structure.d.ts +28 -0
- package/dist-server/service/twin-structure/twin-structure.js +97 -0
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -0
- package/dist-server/service/twin-target/index.d.ts +4 -0
- package/dist-server/service/twin-target/index.js +8 -0
- package/dist-server/service/twin-target/index.js.map +1 -0
- package/dist-server/service/twin-target/twin-target-resolver.d.ts +7 -0
- package/dist-server/service/twin-target/twin-target-resolver.js +143 -0
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -0
- package/dist-server/service/twin-target/twin-target.d.ts +25 -0
- package/dist-server/service/twin-target/twin-target.js +112 -0
- package/dist-server/service/twin-target/twin-target.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/engine/entity-delta.ts +135 -15
- package/server/engine/index.ts +4 -0
- package/server/engine/kpi-fold.ts +40 -8
- package/server/engine/kpi-query.ts +129 -19
- package/server/engine/kpi-target.ts +226 -0
- package/server/engine/live-attentions.ts +12 -2
- package/server/engine/measured-estimator.ts +91 -0
- package/server/engine/model-basis.ts +94 -0
- package/server/engine/oee-accumulator.ts +5 -5
- package/server/engine/spec-coverage.ts +85 -0
- package/server/engine/structure-diff.ts +88 -0
- package/server/engine/travel-estimator.ts +133 -0
- package/server/engine/twin-engine.ts +498 -84
- package/server/engine/warm-start.ts +8 -8
- package/server/service/index.ts +7 -0
- package/server/service/reference/reference-live.ts +2 -1
- package/server/service/reference/reference-master.ts +383 -10
- package/server/service/reference/reference-resolver.ts +3 -3
- package/server/service/reference/template-registry.ts +1 -1
- package/server/service/twin-attention/twin-attention-query.ts +1 -1
- package/server/service/twin-control/twin-control-mutation.ts +1 -1
- package/server/service/twin-event/twin-event-keys.ts +2 -2
- package/server/service/twin-event/twin-event.ts +15 -1
- package/server/service/twin-forecast/twin-forecast-query.ts +56 -8
- package/server/service/twin-instance/twin-instance.ts +1 -1
- package/server/service/twin-journal/twin-journal-query.ts +52 -4
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +4 -3
- package/server/service/twin-space/twin-space-area.ts +1 -1
- package/server/service/twin-space/twin-space-resolver.ts +3 -3
- package/server/service/twin-space/twin-space.ts +14 -0
- package/server/service/twin-structure/index.ts +3 -0
- package/server/service/twin-structure/twin-structure.ts +93 -0
- package/server/service/twin-target/index.ts +5 -0
- package/server/service/twin-target/twin-target-resolver.ts +124 -0
- package/server/service/twin-target/twin-target.ts +97 -0
- package/test/capability-mapping.test.ts +5 -5
- package/test/duration-estimators.test.ts +144 -0
- package/test/entity-delta.test.ts +150 -19
- package/test/ingest-bench.test.ts +9 -9
- package/test/kpi-fold.test.ts +232 -3
- package/test/live-mirror-parity.test.ts +53 -19
- package/test/master-to-twin.test.ts +87 -13
- package/test/model-basis.test.ts +86 -0
- package/test/oee-accumulator.test.ts +9 -9
- package/test/scale-twin-bench.test.ts +22 -22
- package/test/spec-coverage.test.ts +113 -0
- package/test/streamline-e2e.test.ts +10 -10
- package/test/structure-revision-db.test.ts +310 -0
- package/test/twin-event-keys.test.ts +2 -2
- package/test/vocabulary-guard.test.ts +43 -0
- package/test/warm-start.test.ts +9 -9
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ์ฑ๊ณผ ๋ชฉํ โ **์ซ์๋ฅผ ํ๋จ์ผ๋ก ๋ฐ๊พธ๋ ์ธต.**
|
|
3
|
+
*
|
|
4
|
+
* โโ ์ ํ์ํ๊ฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
* ํ๋ฉด๊ณผ AI ๋ "์ฒ๋ฆฌ๋ 345๊ฑด ยท ์์
์๊ฐ 29์ด" ๋ฅผ ์ ํํ ๋งํ๋ค. ๊ทธ๋ฐ๋ฐ **๊ทธ๊ฒ์ด ์ข์์ง ๋์์ง๋ ๋งํ ์
|
|
6
|
+
* ์๋ค.** ํ๋จ ๊ทผ๊ฑฐ๊ฐ ์ ๊ตฌ๊ฐ ๋น๊ต๋ฟ์ด๋ผ "์ด์ ๋ณด๋ค ๋์์ก๋ค" ๊น์ง๊ฐ ํ๊ณ์ด๊ณ , ์ด์ ๋ ๋๋นด์ผ๋ฉด ๊ทธ ๋ง์
|
|
7
|
+
* ์ธ๋ชจ๊ฐ ์๋ค. ๋ชฉํ๊ฐ ๋ถ๋ ์๊ฐ ๊ฐ์ ์ซ์๊ฐ **์ ๋ณด์์ ํ๋จ์ผ๋ก** ๋ฐ๋๋ค.
|
|
8
|
+
*
|
|
9
|
+
* โโ ์ธต์ ์์ง ์๋๋ค โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
10
|
+
* ํด๋(`kpi-fold`)๋ **์ฌ์ค๋ง** ๋ธ๋ค โ ๋ชฉํ๋ฅผ ๋ชจ๋ฅธ๋ค. ๋ชฉํ๋ฅผ ์๋ ์๊ฐ "์ฌ์ค์ ์ธ๋ ๊ท์น" ๊ณผ "์ํ๋
|
|
11
|
+
* ํ์ ํ๋ ๊ท์น" ์ด ํ ํจ์์ ์์ด๊ณ , ๋ชฉํ๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ฌ์ค ๊ณ์ฐ์ ๊ฑด๋๋ฆฌ๊ฒ ๋๋ค. ๊ทธ๋์ ํ์ ์
|
|
12
|
+
* ์ฌ๊ธฐ, **์์ ํจ์**๋ก ๋ฐ๋ก ๋๋ค(ํด๋์ฒ๋ผ ํ
์คํธ๊ฐ ๊ฐ์ธ๋ค).
|
|
13
|
+
*
|
|
14
|
+
* โโ ๋ชฉํ์ ์ง (์ฌ์ฉ์ ๊ฒฐ์ 2026-08-02) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
15
|
+
* **ํธ์์ ์ด์ ๋ชฉํ๋ฅผ ๊ฐ๊ณ , `@things-factory/kpi` ๋ ๊ฒฝ์ ๋ชฉํ๋ฅผ ๊ฐ๋๋ค.** ๊ฒฝ๊ณ:
|
|
16
|
+
*
|
|
17
|
+
* ํธ์(TwinTarget) ์ด ๊ตฌ๊ฐยท์ด ๊ต๋ยท์ด ๊ณต๊ฐ์ **์ฆ์ ํ๋จ**. ํ์ฅ ๊ด๋ฆฌ์๊ฐ ํธ์ ํ๋ฉด์์ ๊ณ ์น๋ค.
|
|
18
|
+
* ๊ฐ์ ์ ๋ ํด๋์์ **๊ทธ๋๊ทธ๋ ๊ณ์ฐ**๋๋ค(์ ์ฅ๋ ๊ฐ์ ์ฝ์ง ์๋๋ค).
|
|
19
|
+
* packages/kpi **๊ธฐ๊ฐ ํ๊ฐ**(์กฐ์งยท๋ถ๊ธฐยท์ ์ยท๊ฐ์ค์นยท์๋ฆผ). ๊ฐ์ ์ ์ฅ๋ผ ์์ด์ผ ํ๋ค.
|
|
20
|
+
*
|
|
21
|
+
* ์ด ๊ฒฝ๊ณ๋ฅผ ์งํค๋ ๊ท์น ํ๋: **ํธ์์ ๋ชฉํ๋ฅผ ๋ณต์ ํ์ง ์๋๋ค.** ๊ฒฝ์ ๋ชฉํ๊ฐ ํ์ํ๋ฉด ํธ์์ ์ฌ์ค์
|
|
22
|
+
* ๋ด๋ณด๋ด ๊ทธ์ชฝ์ด ์๋นํ๊ณ , ๊ทธ์ชฝ ๋ชฉํ๋ฅผ ์ฌ๊ธฐ๋ก ๋ฒ ๊ปด ์ค์ง ์๋๋ค โ ๋ฒ ๋ผ๋ ์๊ฐ ๋ ๊ฐ์ ์ง์ค์ด ๋๋ค.
|
|
23
|
+
*/
|
|
24
|
+
/*
|
|
25
|
+
* โโ ์ด์ ์๊ณ(์ปค๋ attention)์์ ๊ฒฝ๊ณ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
26
|
+
*
|
|
27
|
+
* **attention ์ ๊ฐ์ฒด ํ๋์ ์ง๊ธ ์ํ**๋ค โ ์ต์ปค(์ค๋นยท์๋ฆฌยท์ค๋)์ ํ ์ ์๋ ์กฐ์น๋ฅผ ๊ฐ๋๋ค.
|
|
28
|
+
* **target(์ฌ๊ธฐ)์ ๊ตฌ๊ฐ์ ์ง๊ณ**๋ค โ ์ต์ปค๋ ์กฐ์น๋ ์๊ณ ์ถ์ธ๋ฅผ ํ๋จํ๋ค.
|
|
29
|
+
*
|
|
30
|
+
* ๊ทธ๋์ ์ฐฝ ๋จ์ ์ง๊ณ ํ์ ์ attention ์ผ๋ก ๋ฃ์ง ์๋๋ค: "๊ฐ๋๋ฅ ์ ํ"(์ฐฝ ํ๊ท ) attention ์ ๋ง๋ค๋ฉด
|
|
31
|
+
* `utilization.ratio` ๋ชฉํ์ **๊ฐ์ ์ฌ์ค์ ๋ ์ธต์ด ๋ค๋ฅธ ์๊ณ๋ก ํ์ **ํ๊ฒ ๋๋ค.
|
|
32
|
+
* (design/map/performance.md ยง5.0-4 โ ํ์ฌ ๋ค์ฏ attention ๊ณผ ์ด ์ดํ๋ ๊ฒน์น์ง ์๋๋ค.)
|
|
33
|
+
*/
|
|
34
|
+
import type { DurationStats, KpiResult } from './kpi-fold.js'
|
|
35
|
+
|
|
36
|
+
/** ์ธก์ ๋ ๊ธฐ๋ก์ด ์์ ๋๋ง ๊ฐ์ผ๋ก ์ธ์ ํ๋ค โ ์์ผ๋ฉด ๋ชจ๋ฆ(0 ์ด ์๋๋ค). */
|
|
37
|
+
const measured = (k: KpiResult, v: number): number | undefined => (k.eventsInWindow > 0 ? v : undefined)
|
|
38
|
+
|
|
39
|
+
/** ๋ถ์์๋ ํ๋ณธ์ด ์์ ๋๋ง ๊ฐ์ด๋ค โ `count === 0` ์ `p50Ms: 0` ์ "0 ๋ฐ๋ฆฌ์ด" ๊ฐ ์๋๋ผ "์์" ์ด๋ค. */
|
|
40
|
+
const statOf = (s: DurationStats, pick: (d: DurationStats) => number): number | undefined =>
|
|
41
|
+
s.count > 0 ? pick(s) : undefined
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* ์งํ ์ดํ โ **์ด๋ฆ์ ํ ๊ณณ์์ ์ ํ๋ค.**
|
|
45
|
+
*
|
|
46
|
+
* ๋ชฉํ์ ์ฌ์ค์ด ์๋ก ๋ค๋ฅธ ์ด๋ฆ์ ์ฐ๋ฉด ๊ทธ ์ฌ์ด์ ๋งคํ ํ๊ฐ ์๊ธฐ๊ณ , ๊ทธ ํ๊ฐ ๊ณง ๋ฐฉ์ธ์ด ๋๋ค. ๊ทธ๋์
|
|
47
|
+
* ๋ชฉํ๊ฐ ๊ฐ๋ฆฌํฌ ์ ์๋ ์งํ๋ ์ฌ๊ธฐ ์ด๊ฑฐ๋ ๊ฒ๋ฟ์ด๊ณ , ๊ฐ์ ๊บผ๋ด๋ ๋ฐฉ๋ฒ๋ ์ฌ๊ธฐ ํจ๊ป ์๋ค.
|
|
48
|
+
*
|
|
49
|
+
* **`direction` ์ ์งํ์ ์ฑ์ง์ด์ง ์ฌ์ฉ์์ ์ ํ์ด ์๋๋ค.** ์ฒ๋ฆฌ๋์ ๋์์๋ก ์ข๊ณ ๋๊ธฐ์๊ฐ์ ๋ฎ์์๋ก
|
|
50
|
+
* ์ข๋ค โ ์ด๊ฒ์ ์
๋ ฅ์ผ๋ก ๋ฐ์ผ๋ฉด ๋ฐ๋๋ก ์ ์ ๋ชฉํ๊ฐ ๋ง๋ค์ด์ง๊ณ , ๊ทธ ๋ค ํ์ ์ด ์ ๋ถ ๋ค์งํ๋ค.
|
|
51
|
+
*/
|
|
52
|
+
export const TWIN_METRIC = {
|
|
53
|
+
/* ๊ธฐ๋ก์ด ์๋ ๊ตฌ๊ฐ์ ์ฒ๋ฆฌ๋ 0 ์ด ์๋๋ผ **๋ชจ๋ฆ**์ด๋ค โ `eventsInWindow` ๊ฐ ๊ทธ๊ฒ์ ๊ฐ๋ฅธ๋ค.
|
|
54
|
+
0 ์ผ๋ก ํ์ ํ๋ฉด "์ธก์ ๋ชป ํ ๊ตฌ๊ฐ" ์ด "์ผ์ ์ ํ ๊ตฌ๊ฐ" ์ผ๋ก ๋ณด๊ณ ๋๋ค. */
|
|
55
|
+
'throughput.tasks': { direction: 'higher', unit: 'count', of: (k: KpiResult) => measured(k, k.throughput.tasks) },
|
|
56
|
+
'throughput.orders': { direction: 'higher', unit: 'count', of: (k: KpiResult) => measured(k, k.throughput.orders) },
|
|
57
|
+
/* ์์์๊ฐ ๋ถ์์๋ `count === 0` ์ด๋ฉด **0 ์ด ์๋๋ผ ์๋ค.** ์ด๊ฒ์ 0 ์ผ๋ก ์ฝ์ผ๋ฉด ์๊ฐ ์งํ๋
|
|
58
|
+
"๋ฎ์์๋ก ์ข๋ค" ๋ผ์ **์ผ์ด ์๋ ๊ตฌ๊ฐ์ด ๋ชฉํ๋ฅผ ์๋ฒฝํ ๋ฌ์ฑํ ๊ฒ์ผ๋ก** ๋ณด๊ณ ๋๋ค โ ๋ฏธ๋ฌ์ ๋์น๋
|
|
59
|
+
๊ฒ๋ณด๋ค ๋์๋ค(๊ณ ์น ๊ณณ์ด ์๋๋ฐ ์ํ๊ณ ์๋ค๊ณ ๋งํ๋ค). ํ
์คํธ๊ฐ ์ด๊ฒ์ ์ก์๋ค. */
|
|
60
|
+
'leadTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.leadTime, d => d.p50Ms) },
|
|
61
|
+
'leadTime.p90Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.leadTime, d => d.p90Ms) },
|
|
62
|
+
'workTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.workTime, d => d.p50Ms) },
|
|
63
|
+
'waitTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.waitTime, d => d.p50Ms) },
|
|
64
|
+
/** ์์ ์ ์ โ ๊ฐ๋ณ ์์์ด ์๋๋ผ **์ฐฝ ์ ์ฒด์ ํ๊ท ๋น์จ**(์์๋ณ์ ๊ด์ ์ถ์ ์ผ์ด๋ค). */
|
|
65
|
+
'utilization.ratio': {
|
|
66
|
+
direction: 'higher',
|
|
67
|
+
unit: 'ratio',
|
|
68
|
+
of: (k: KpiResult) =>
|
|
69
|
+
k.utilization.length ? k.utilization.reduce((s, u) => s + u.ratio, 0) / k.utilization.length : undefined
|
|
70
|
+
}
|
|
71
|
+
} as const
|
|
72
|
+
|
|
73
|
+
export type TwinMetricId = keyof typeof TWIN_METRIC
|
|
74
|
+
export const isTwinMetric = (id: string): id is TwinMetricId => id in TWIN_METRIC
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* ์ ์ฅ๋ ๋ชฉํ ํ์์ **์ด ๋ฒ์์ ๊ฑธ๋ฆฐ ๋ชฉํ**๋ฅผ ๊ณ ๋ฅธ๋ค โ ์ข์ ์ ์ธ์ด ๋์ ์ ์ธ์ ๋ฎ๋๋ค.
|
|
78
|
+
*
|
|
79
|
+
* **์ ์์ ํจ์์ธ๊ฐ**: ์ด ๊ท์น(๊ณต๊ฐ ๋ชฉํ ์์ ์ธ์คํด์ค ๋ชฉํ๋ฅผ ์น๋๋ค)์ ์ง๊ธ๊น์ง DB ์ ๊ทผ ๋ค์ ์จ์ด
|
|
80
|
+
* ์์ด์ **ํ๋ฉด์ ๋๋ฌ ๋ด์ผ๋ง** ํ์ธํ ์ ์์๋ค. ๊ทธ๋ฐ๋ฐ ์ด ํ๋ก์ ํธ๋ ๋ก๊ทธ์ธ ๋ค ํ๋ฉด์ ์๋์ผ๋ก
|
|
81
|
+
* ๊ฒ์ฆํ ๋ฐฉ๋ฒ์ด ์์ด, ์ค์ ๋ก ์ฌ๋ฌ ๊ธฐ๋ฅ์ด "์คํ๋ฉด ๋ฏธ๊ฒ์ฆ" ์ผ๋ก ์์๋ค. ๊ท์น์ ๋ฐ์ผ๋ก ๋นผ๋ฉด ํ
์คํธ๊ฐ
|
|
82
|
+
* ๊ฐ์ธ์ง๋ค โ ์กฐํ๋ ํ์ ๊ฐ์ ธ์ค๊ธฐ๋ง ํ๊ณ , **ํ๋จ์ ์ฌ๊ธฐ์** ํ๋ค.
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* ์ถ ๋ชฉํ๊น์ง ๋ด์ ๋ชฉ๋ก โ ์ ์ฒด ๋ชฉํ์ **์ถ์ ํ ๊ฐ**์ ๊ฑธ๋ฆฐ ๋ชฉํ๋ฅผ ํจ๊ป ๋ ๋ค.
|
|
86
|
+
*
|
|
87
|
+
* ์ถ ๋ชฉํ๋ ์ ์ฒด ๋ชฉํ๋ฅผ **๋ฎ์ง ์๋๋ค.** ๋ค๋ฅธ ์ง๋ฌธ์ด๊ธฐ ๋๋ฌธ์ด๋ค: "ํ์ฅ ์ ์ฒด๊ฐ 100๊ฑด" ๊ณผ
|
|
88
|
+
* "์ฉ์ ๊ตฌ์ญ์ด 30๊ฑด" ์ ๋์์ ์ฐธ์ผ ์ ์๋ค. ๋ฎ๊ฒ ๋ง๋ค๋ฉด ์ถ ํ๋์ ๋ชฉํ๋ฅผ ๊ฑฐ๋ ์๊ฐ ์ ์ฒด ํ์ ์ด
|
|
89
|
+
* ์ฌ๋ผ์ ธ ์ฌ์ฉ์๊ฐ "๋ชฉํ๋ฅผ ์ง์ ๋?" ๋ฅผ ๋ฌป๊ฒ ๋๋ค.
|
|
90
|
+
*/
|
|
91
|
+
export interface ScopedTargets {
|
|
92
|
+
/** ์ฐฝ ์ ์ฒด์ ๊ฑธ๋ฆฐ ๋ชฉํ. */
|
|
93
|
+
overall: TwinTargetSpec[]
|
|
94
|
+
/** ์ถ โ (๊ทธ ์ถ์ ๊ฐ โ ๋ชฉํ๋ค). ์์ฒญํ ์ถ๋ง ์ฑ์ด๋ค. */
|
|
95
|
+
byAxisKey: Record<string, TwinTargetSpec[]>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ์ถ์ ๊ฐ ๊ฐ์ ๋ํด ๋ชฉํ ๋๋น ํ์ โ **๊ทธ๋ฃน ํญ๋ชฉ์ ๊ทธ๋๋ก ๋ถ์ผ ํํ.**
|
|
100
|
+
*
|
|
101
|
+
* ๊ทธ๋ฃน์ ์งํ๋ ์ ์ฒด๋ณด๋ค ์ข๋ค(์ฃผ๋ฌธ ์ยท์์ ์ ์ ๋ ์ถ ๋จ์๋ก ๋ป์ด ๋ค๋ฅด๊ฑฐ๋ ์๋ค). ์๋ ์งํ์ ๋ชฉํ๊ฐ
|
|
102
|
+
* ๊ฑธ๋ ค ์์ผ๋ฉด **ํ์ ํ์ง ์๋๋ค** โ ์ฌ๊ธฐ์ 0 ์ผ๋ก ์ฝ์ผ๋ฉด ์๊ฐ ์งํ๊ฐ "์๋ฒฝ ๋ฌ์ฑ" ์ผ๋ก ๋ค์งํ๋ค
|
|
103
|
+
* (์ ์ฒด ํ์ ์์ ์ด๋ฏธ ๊ฒช์ ๊ฒฐํจ์ด๋ค).
|
|
104
|
+
*/
|
|
105
|
+
export function judgeGroupTargets(
|
|
106
|
+
group: { key: string; tasks: number; leadTime: { count: number; p50Ms: number }; workTime: { count: number; p50Ms: number }; waitTime: { count: number; p50Ms: number } },
|
|
107
|
+
targets: readonly TwinTargetSpec[] | undefined
|
|
108
|
+
): TargetVerdict[] {
|
|
109
|
+
const value = (metric: TwinMetricId): number | undefined => {
|
|
110
|
+
switch (metric) {
|
|
111
|
+
case 'throughput.tasks':
|
|
112
|
+
return group.tasks
|
|
113
|
+
case 'leadTime.p50Ms':
|
|
114
|
+
return group.leadTime.count > 0 ? group.leadTime.p50Ms : undefined
|
|
115
|
+
case 'workTime.p50Ms':
|
|
116
|
+
return group.workTime.count > 0 ? group.workTime.p50Ms : undefined
|
|
117
|
+
case 'waitTime.p50Ms':
|
|
118
|
+
return group.waitTime.count > 0 ? group.waitTime.p50Ms : undefined
|
|
119
|
+
default:
|
|
120
|
+
return undefined // ์ถ ๋จ์๋ก ๋ป์ด ์๋ ์งํ(์ฃผ๋ฌธ ์ยท์์ ์ ์ ) โ ์ง์ด๋ด์ง ์๋๋ค
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const out: TargetVerdict[] = []
|
|
124
|
+
for (const t of targets ?? []) {
|
|
125
|
+
const spec = TWIN_METRIC[t.metric]
|
|
126
|
+
const actual = value(t.metric)
|
|
127
|
+
if (!spec || typeof actual !== 'number' || !Number.isFinite(actual) || !Number.isFinite(t.target)) continue
|
|
128
|
+
out.push(verdictOf(t.metric, actual, t.target, spec.direction))
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function mergeTargets(
|
|
134
|
+
rows: readonly TargetRow[],
|
|
135
|
+
scope: { spaceId?: string; instanceIds: readonly string[]; axis?: string }
|
|
136
|
+
): TwinTargetSpec[] {
|
|
137
|
+
return scopedTargets(rows, scope).overall
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** ์ ์ฅ๋ ๋ชฉํ ํ โ ํ๋จ์ ํ์ํ ๊ฒ๋ง(์ํฐํฐ ์ ์ฒด๋ฅผ ์๊ตฌํ์ง ์๋๋ค: ํ
์คํธ๊ฐ ๊ฐ์ธ์ง๋ค). */
|
|
141
|
+
export interface TargetRow {
|
|
142
|
+
metric: string
|
|
143
|
+
target: number
|
|
144
|
+
spaceId?: string | null
|
|
145
|
+
instanceId?: string | null
|
|
146
|
+
axis?: string | null
|
|
147
|
+
axisKey?: string | null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** ์ ์ฒด ๋ชฉํ์ ์ถ ๋ชฉํ๋ฅผ ํจ๊ป ๊ณ ๋ฅธ๋ค โ ์ข์ ์ ์ธ(์ธ์คํด์ค)์ด ๋์ ์ ์ธ(๊ณต๊ฐ)์ ๋ฎ๋ ๊ท์น์ ์์ชฝ ๊ฐ๋ค. */
|
|
151
|
+
export function scopedTargets(
|
|
152
|
+
rows: readonly TargetRow[],
|
|
153
|
+
scope: { spaceId?: string; instanceIds: readonly string[]; axis?: string }
|
|
154
|
+
): ScopedTargets {
|
|
155
|
+
/* ๊ฐ์ ์๋ฆฌ(์งํ ๋๋ ์ถ๊ฐ+์งํ)์ ๊ณต๊ฐยท์ธ์คํด์ค ๋ชฉํ๊ฐ ํจ๊ป ์์ผ๋ฉด ์ธ์คํด์ค๊ฐ ์ด๊ธด๋ค. */
|
|
156
|
+
const pick = new Map<string, { target: number; fromInstance: boolean; axisKey?: string; metric: TwinMetricId }>()
|
|
157
|
+
for (const r of rows) {
|
|
158
|
+
if (!isTwinMetric(r.metric) || typeof r.target !== 'number' || !Number.isFinite(r.target)) continue
|
|
159
|
+
const inSpace = !!scope.spaceId && r.spaceId === scope.spaceId
|
|
160
|
+
const inInstance = !!r.instanceId && scope.instanceIds.includes(r.instanceId)
|
|
161
|
+
if (!inSpace && !inInstance) continue
|
|
162
|
+
if (r.axis) {
|
|
163
|
+
if (!scope.axis || r.axis !== scope.axis || !r.axisKey) continue // ์ง๊ธ ๋ณด๋ ์ถ๋ง
|
|
164
|
+
}
|
|
165
|
+
const slot = `${r.axis ?? ''}\u0000${r.axisKey ?? ''}\u0000${r.metric}`
|
|
166
|
+
const prev = pick.get(slot)
|
|
167
|
+
if (prev?.fromInstance && !inInstance) continue // ์ธ์คํด์ค ์ ์ธ์ ๊ณต๊ฐ ์ ์ธ์ด ๋ฎ์ง ์๋๋ค
|
|
168
|
+
pick.set(slot, { target: r.target, fromInstance: inInstance, axisKey: r.axisKey ?? undefined, metric: r.metric })
|
|
169
|
+
}
|
|
170
|
+
const overall: TwinTargetSpec[] = []
|
|
171
|
+
const byAxisKey: Record<string, TwinTargetSpec[]> = {}
|
|
172
|
+
for (const [slot, v] of pick) {
|
|
173
|
+
const spec = { metric: v.metric, target: v.target }
|
|
174
|
+
if (slot.startsWith('\u0000')) overall.push(spec)
|
|
175
|
+
else (byAxisKey[v.axisKey as string] ??= []).push(spec)
|
|
176
|
+
}
|
|
177
|
+
return { overall, byAxisKey }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** ๋ชฉํ ํ๋ โ ์ด๋ ์งํ๋ฅผ, ์ผ๋ง๋ก. ๋ฐฉํฅ์ ์งํ๊ฐ ์๋ค(ยงTWIN_METRIC). */
|
|
181
|
+
export interface TwinTargetSpec {
|
|
182
|
+
metric: TwinMetricId
|
|
183
|
+
target: number
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface TargetVerdict {
|
|
187
|
+
metric: TwinMetricId
|
|
188
|
+
/** ์ด ๊ตฌ๊ฐ์ ์ค์ ๊ฐ. */
|
|
189
|
+
actual: number
|
|
190
|
+
target: number
|
|
191
|
+
direction: 'higher' | 'lower'
|
|
192
|
+
/** ๋ชฉํ๋ฅผ ๋ง์กฑํ๋. */
|
|
193
|
+
met: boolean
|
|
194
|
+
/**
|
|
195
|
+
* ๋ชฉํ ๋๋น ๋น์จ โ **ํญ์ "1 ์ด์์ด๋ฉด ์ข๋ค"** ๋ก ์ ๊ทํํ๋ค(๋ฐฉํฅ์ด ๋ค๋ฅธ ์งํ๋ฅผ ํ ํ๋ฉด์์ ๋น๊ตํ ์
|
|
196
|
+
* ์๊ฒ). ์ฒ๋ฆฌ๋์ `actual/target`, ์๊ฐ์ `target/actual`.
|
|
197
|
+
*/
|
|
198
|
+
ratio: number
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* ๋ชฉํ ๋๋น ํ์ โ **๋ชฉํ๊ฐ ์๋ ์งํ๋ ํ์ ํ์ง ์๋๋ค.**
|
|
203
|
+
*
|
|
204
|
+
* ๊ฐ์ด ์๋ ์งํ(๊ทธ ๊ตฌ๊ฐ์ ๊ธฐ๋ก์ด ์์ด `undefined`)๋ ํ์ ํ์ง ์๋๋ค. ์๋ ๊ฒ์ 0 ์ผ๋ก ๋์ผ๋ฉด
|
|
205
|
+
* "๋ชฉํ ํฌ๊ฒ ๋ฏธ๋ฌ" ์ด๋ผ๋ **์๋ ์ฌ์ค**์ด ๋ง๋ค์ด์ง๊ณ , ์ฌ์ฉ์๋ ๋ฉ์ถ ์ ์๋ ๋ผ์ธ์ ๊ณ ์น๋ฌ ๊ฐ๋ค.
|
|
206
|
+
* ๋ชฉํ๋ฅผ 0 ์ผ๋ก ์ค ๊ฒฝ์ฐ๋ ๋น์จ์ ๋ด์ง ์๋๋ค(0 ์ผ๋ก ๋๋์ง ์๋๋ค) โ ๋ง์กฑ ์ฌ๋ถ๋ง ๋ตํ๋ค.
|
|
207
|
+
*/
|
|
208
|
+
export function judgeTargets(kpi: KpiResult, targets: readonly TwinTargetSpec[] | undefined): TargetVerdict[] {
|
|
209
|
+
const out: TargetVerdict[] = []
|
|
210
|
+
for (const t of targets ?? []) {
|
|
211
|
+
const spec = TWIN_METRIC[t.metric]
|
|
212
|
+
if (!spec) continue // ๋ชจ๋ฅด๋ ์งํ ์ด๋ฆ โ ์ง์ํด ๋งคํํ์ง ์๋๋ค
|
|
213
|
+
const actual = spec.of(kpi)
|
|
214
|
+
if (typeof actual !== 'number' || !Number.isFinite(actual)) continue // ๊ฐ์ด ์๋ค โ ๊ฐ์ด 0
|
|
215
|
+
if (!Number.isFinite(t.target)) continue
|
|
216
|
+
out.push(verdictOf(t.metric, actual, t.target, spec.direction))
|
|
217
|
+
}
|
|
218
|
+
return out
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** ํ์ ์ฐ์ ํ ๊ณณ โ ์ ์ฒด ํ์ ๊ณผ ์ถ ํ์ ์ด **๊ฐ์ ๊ท์น**์ ์ด๋ค(๋ ๋ฒ์ด๋ฉด ๊ฐ๋ผ์ง๋ค). */
|
|
222
|
+
function verdictOf(metric: TwinMetricId, actual: number, target: number, direction: 'higher' | 'lower'): TargetVerdict {
|
|
223
|
+
const met = direction === 'higher' ? actual >= target : actual <= target
|
|
224
|
+
const ratio = direction === 'higher' ? (target > 0 ? actual / target : Number.NaN) : actual > 0 ? target / actual : Number.NaN
|
|
225
|
+
return { metric, actual, target, direction, met, ratio }
|
|
226
|
+
}
|
|
@@ -6,10 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { deriveAttentions } from '@operato/twin-kernel'
|
|
8
8
|
|
|
9
|
-
export function withLiveAttentions<S extends {
|
|
9
|
+
export function withLiveAttentions<S extends { equipment?: any[]; locations?: any[]; orders?: any[]; tasks?: any[]; attentions?: any[]; nowTime?: string }>(
|
|
10
|
+
state: S
|
|
11
|
+
): S {
|
|
10
12
|
if (!state) return state
|
|
11
13
|
return {
|
|
12
14
|
...state,
|
|
13
|
-
|
|
15
|
+
/*
|
|
16
|
+
* ์
๋ ฅ์ **์๋ฎฌ ์ชฝ๊ณผ ๋๊ฐ์ด** ๋ฃ๋๋ค. ํ์ชฝ๋ง ๋น ์ง๋ฉด ๊ฐ์ ๊ณต์ฅ์ด ํ์ชฝ์์๋ง ๋งํ ๊ฒ์ฒ๋ผ ๋ณด์ด๊ณ ,
|
|
17
|
+
* ๊ทธ ์ด๊ธ๋จ์ ๊ท์น์ด ์๋๋ผ ๋ฐฐ์ ๋๋ฌธ์ด๋ผ ์๋ฌด ๋ฐ์๋ ์ ์กํ๋ค(`tasks` ๋ฅผ ๋น ๋จ๋ ค ์ค์ ๋ก ๊ทธ๋ด ๋ปํ๋ค).
|
|
18
|
+
*/
|
|
19
|
+
attentions: deriveAttentions(
|
|
20
|
+
{ equipment: state.equipment ?? [], locations: state.locations ?? [], orders: state.orders ?? [], tasks: state.tasks ?? [] },
|
|
21
|
+
undefined,
|
|
22
|
+
state.nowTime
|
|
23
|
+
)
|
|
14
24
|
}
|
|
15
25
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ์ค์ธก ์์ ์ถ์ ๊ธฐ โ **์ด๋ ฅ์์ ๋ฐฐ์ด ๊ฐ์ด ์์ยท์ ์ธ๊ฐ์ ์ด๊ธด๋ค.**
|
|
3
|
+
*
|
|
4
|
+
* ๋ฐฐ๊ฒฝ(operato-twin design/plans/simulation-spec.md ยง5-4): ์์์๊ฐ์ ๋ช
์ธ๋ก ๋ฐ์ ์ ์๊ฒ ๋์ง๋ง
|
|
5
|
+
* ๋ช
์ธ๋ ์ ์ธ์ด๋ค โ ๊ทธ ํ์ฅ์์ ์ค์ ๋ก ์ผ๋ง ๊ฑธ๋ฆฌ๋์ง๋ ์ ๋์ด ์๋ค. `gap-analytics` ๋ ์งํ ์์ค์
|
|
6
|
+
* ํธํฅ๋ง ๋ฐฐ์ ๊ณ (์์ธก๊ฐ ๋ณด์ ), **๊ณต์ ๋ณ ์์๋ฅผ ์ถ์ ๊ธฐ๋ก ๋๋จน์ด๋ ๊ฒฝ๋ก๋ ์์๋ค.**
|
|
7
|
+
*
|
|
8
|
+
* ์ฌ๊ธฐ์ ํ๋ ์ผ: KPI ํด๋๊ฐ ์์
์ข
๋ฅ๋ณ๋ก ๋ธ **์์
์๊ฐ(์ฐฉ์โ์๋ฃ) p50** ์ ๊ทธ ์ข
๋ฅ์ ์์๋ก ์ด๋ค.
|
|
9
|
+
* ๋ฆฌ๋ํ์(์์ฑโ์๋ฃ)์ด ์๋๋ผ ์์
์๊ฐ์ ์ฐ๋ ์ด์ : ๋ฆฌ๋ํ์์๋ **๋๊ธฐ๊ฐ ์์ฌ ์๊ณ **, ๋๊ธฐ๋
|
|
10
|
+
* ์๋ฎฌ๋ ์ด์
์ด ์์ ๊ฒฝํฉ์ผ๋ก ์ค์ค๋ก ๋ง๋ค์ด์ผ ํ๋ ๊ฐ์ด๋ค. ๋๊ธฐ๋ฅผ ์์์ ๋ฃ์ผ๋ฉด ์ด์ค ๊ณ์ฐ์ด ๋๋ค.
|
|
11
|
+
*
|
|
12
|
+
* **ํ๊ท ๋ง ๋ฐฐ์ฐ๋ฉด ๋๊ธฐ์ ๋ณ๋ชฉ์ ๊ณผ์ํ๊ฐํ๋ค.** ์์๊ฐ ๋ ์ ํํ ๊ฐ์ผ๋ฉด ์ค์ด ์๊ธฐ์ง ์๋๋ค โ ํ์ฅ์
|
|
13
|
+
* ์ค์ ํ๋ค๋ฆผ์์ ๋์จ๋ค. ๊ทธ๋์ p50 ๋ง์ด ์๋๋ผ **๊ด์ธก๋ ํผ์ง(p10..p90)** ์ ํจ๊ป ๋๊ธฐ๊ณ , ํ๋ณธ์
|
|
14
|
+
* ์ปค๋์ด ์๊ธฐ ๋์๋ก ๋ฝ๋๋ค(fork ๊ฒฐ์ ์ฑ). ์์ชฝ ๊ผฌ๋ฆฌ๋ฅผ ๋ค ๊ด์ธก์์ ๊ฐ์ ธ์ค๋ ์ด์ : p90 ๋ง ์์ผ๋ฉด
|
|
15
|
+
* ์๋์ชฝ์ ์์ชฝ์์ ๊ฑฐ์ธ์ฒ๋ผ ๋ฒ ๊ปด์ผ ํ๋๋ฐ ๊ทธ๊ฑด ๋ฐ๋ช
์ด๋ค.
|
|
16
|
+
*
|
|
17
|
+
* ์ ์ง ๊ท์จ:
|
|
18
|
+
* - ํ๋ณธ์ด ์ ์ผ๋ฉด ์ฐ์ง ์๋๋ค(`minSamples`). ํ๋ ๊ฑด์ p50 ์ ๊ทธ ํ์ฅ์ ๊ฐ์ด ์๋๋ค.
|
|
19
|
+
* - 'unknown' ์ถ์ ์ฐ์ง ์๋๋ค โ ์ข
๋ฅ๋ฅผ ๋ชจ๋ฅด๋ ๊ธฐ๋ก์ ํน์ ์ข
๋ฅ์ ๊ทผ๊ฑฐ๋ก ์ผ์ ์ ์๋ค.
|
|
20
|
+
* - ํผ์ง์ด ์ฑ๋ฆฝํ์ง ์์ผ๋ฉด(p10 โฅ p90, ๋๋ 0) **ํผ์ง ์์ด ํ๊ท ๋ง** ๋๊ธด๋ค. ์๋ ํ๋ค๋ฆผ์ ๋ง๋ค์ง ์๋๋ค.
|
|
21
|
+
* - ๋ฌด์์ ๋ฐฐ์ ๊ณ ๋ฌด์์ ํ๋ณธ ๋ถ์กฑ์ผ๋ก ๋ฒ๋ ธ๋์ง ํจ๊ป ๋ธ๋ค(์กฐ์ฉํ ์ผ๋ถ๋ง ๋ฐ์ํ์ง ์๋๋ค).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** ์ปค๋ `DurationEstimate` ์ ๊ฐ์ ๋ชจ์(์ปค๋ ํ์
์ ์์กดํ์ง ์๊ธฐ ์ํด ๊ตฌ์กฐ๋ก ๋ง์ถ๋ค). */
|
|
25
|
+
export interface MeasuredEstimate {
|
|
26
|
+
meanMs: number
|
|
27
|
+
spread?: { distribution: 'triangular'; minMs: number; maxMs: number; modeMs: number }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MeasuredEstimatorResult {
|
|
31
|
+
estimator?: {
|
|
32
|
+
estimate(ctx: { kind: string; fromNode: string; toNode: string; resourceKind?: string }): MeasuredEstimate | undefined
|
|
33
|
+
}
|
|
34
|
+
/** ์์
์ข
๋ฅ โ ์ค์ธก ์์(ms). ์ถ์ ๊ธฐ๊ฐ ๋ตํ๋ ์ข
๋ฅ๋ค. */
|
|
35
|
+
learned: Record<string, number>
|
|
36
|
+
/** ํผ์ง๊น์ง ๋ฐฐ์ด ์ข
๋ฅ โ ํ๊ท ๋ง ๋ฐฐ์ด ๊ฒ๊ณผ ๊ตฌ๋ณํ๋ค(๋ณ๋ ์๋ ์์๋ ๋๊ธฐ๋ฅผ ๊ณผ์ํ๊ฐํ๋ค). */
|
|
37
|
+
spreads: Record<string, { minMs: number; maxMs: number }>
|
|
38
|
+
/** ํ๋ณธ์ด ๋ชจ์๋ผ ์ฐ์ง ์์ ์ข
๋ฅ์ ๊ทธ ๊ฑด์ โ ๋ฐฐ์ฐ์ง ๋ชปํ ๊ฒ์ ๋ฐํ๋ค. */
|
|
39
|
+
skipped: { kind: string; count: number }[]
|
|
40
|
+
minSamples: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** ๊ธฐ๋ณธ ํ๋ณธ ํํ โ ์ด๋ณด๋ค ์ ์ผ๋ฉด ๊ทธ ์ข
๋ฅ๋ ๋ฐฐ์ฐ์ง ์๋๋ค(์์ ํ๋ณธ์ p50 ์ ํ์ฅ ๊ฐ์ผ๋ก ์น๊ฒฉ ๊ธ์ง). */
|
|
44
|
+
export const DEFAULT_MIN_SAMPLES = 5
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ์์
์ข
๋ฅ๋ณ KPI ๊ทธ๋ฃน์์ ์ถ์ ๊ธฐ๋ฅผ ๋ง๋ ๋ค(์์ โ DBยท์ปค๋ ๋ชจ๋ฆ).
|
|
48
|
+
* `groups` ๋ `computeTwinKpi({ groupBy: 'taskKind' })` ์ `groups.items` ๋ฅผ ๊ทธ๋๋ก ๋๊ธด๋ค.
|
|
49
|
+
*/
|
|
50
|
+
export function buildMeasuredEstimator(
|
|
51
|
+
groups: { key: string; workTime?: { count: number; p10Ms?: number; p50Ms: number; p90Ms?: number } }[] | undefined,
|
|
52
|
+
opts: { minSamples?: number } = {}
|
|
53
|
+
): MeasuredEstimatorResult {
|
|
54
|
+
const minSamples = opts.minSamples ?? DEFAULT_MIN_SAMPLES
|
|
55
|
+
const learned: Record<string, number> = {}
|
|
56
|
+
const spreads: Record<string, { minMs: number; maxMs: number }> = {}
|
|
57
|
+
const estimates: Record<string, MeasuredEstimate> = {}
|
|
58
|
+
const skipped: { kind: string; count: number }[] = []
|
|
59
|
+
for (const g of groups ?? []) {
|
|
60
|
+
if (!g?.key || g.key === 'unknown') continue
|
|
61
|
+
const w = g.workTime
|
|
62
|
+
const count = w?.count ?? 0
|
|
63
|
+
const p50 = w?.p50Ms ?? 0
|
|
64
|
+
if (count < minSamples || !(p50 > 0)) { skipped.push({ kind: g.key, count }); continue }
|
|
65
|
+
const mean = Math.round(p50)
|
|
66
|
+
learned[g.key] = mean
|
|
67
|
+
/* ํผ์ง์ **๊ด์ธก๋ ๋ ๊ผฌ๋ฆฌ**๋ก๋ง ๋ง๋ ๋ค. ํ์ชฝ์ด๋ผ๋ ์๊ฑฐ๋ ๊ตฌ๊ฐ์ด ์ฑ๋ฆฝํ์ง ์์ผ๋ฉด ํ๊ท ๋ง ๋๊ธด๋ค. */
|
|
68
|
+
const lo = w?.p10Ms
|
|
69
|
+
const hi = w?.p90Ms
|
|
70
|
+
if (typeof lo === 'number' && typeof hi === 'number' && lo > 0 && hi > lo) {
|
|
71
|
+
const minMs = Math.round(lo)
|
|
72
|
+
const maxMs = Math.round(hi)
|
|
73
|
+
spreads[g.key] = { minMs, maxMs }
|
|
74
|
+
estimates[g.key] = { meanMs: mean, spread: { distribution: 'triangular', minMs, maxMs, modeMs: mean } }
|
|
75
|
+
} else {
|
|
76
|
+
estimates[g.key] = { meanMs: mean }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!Object.keys(learned).length) return { learned, spreads, skipped, minSamples }
|
|
80
|
+
return {
|
|
81
|
+
learned,
|
|
82
|
+
spreads,
|
|
83
|
+
skipped,
|
|
84
|
+
minSamples,
|
|
85
|
+
estimator: {
|
|
86
|
+
estimate(ctx) {
|
|
87
|
+
return estimates[ctx.kind]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ๋ชจ๋ธ ๊ทผ๊ฑฐ โ **๋ฐฐ์ด ๋ชจ๋ธ์๋ง ๋ณด์ ์ ์ ์ฉํ๊ธฐ ์ํ ๋น๊ต.**
|
|
3
|
+
*
|
|
4
|
+
* ์์ธก ์ถ๋ ฅ ๋ณด์ (P3.2 `twinLearnCalibration`)์ "์ฐ๋ฆฌ ๋ชจ๋ธ์ด ๋ ์ด๋งํผ ์ ๊ฒ ๋ณธ๋ค" ๋ฅผ ๋ฐฐ์ ๊ฒฐ๊ณผ๋ฅผ
|
|
5
|
+
* ๋ฐ์ด ์ค๋ค. ๊ทธ ํ์ต์ **๊ทธ๋์ ๋ชจ๋ธ**์ ์ ์ ๋ก ํ๋ค. ๊ทธ๋ฐ๋ฐ ์์์๊ฐ ์ถ์ ๊ธฐ(์ค์ธกยท๊ฑฐ๋ฆฌ๊ณ์ฐ)์ ๊ณต์
|
|
6
|
+
* ๋ช
์ธ๊ฐ ๋ค์ด์ค๋ฉด์ ๋ชจ๋ธ ์์ฒด๊ฐ ๋ฐ๋ ์ ์๊ฒ ๋๋ค โ ๋ชจ๋ธ์ด ์ด๋ฏธ ๊ทธ ํธํฅ์ ์์๋๋ฐ ๋ณด์ ์ด ๋ ๋ฐ์ด
|
|
7
|
+
* ์ฃผ๋ฉด **์ด์ค ๋ณด์ **์ด ๋์ด ์ ํ๋๊ฐ ๋๋น ์ง๋ค. ํ์ต ์์ ์ accAfter ๋ ๋๋ ์ ํจํ์ง ์๋ค.
|
|
8
|
+
*
|
|
9
|
+
* ๊ทธ๋์ ๋ณด์ ๊ธฐ๋ก์ **๊ทธ๋ ๋ชจ๋ธ์ ๊ทผ๊ฑฐ**๋ฅผ ํจ๊ป ๋จ๊ธฐ๊ณ , ์ ์ฉ ์ ์ ์ง๊ธ ๊ทผ๊ฑฐ์ ๋น๊ตํ๋ค. ๋ฌ๋ผ์ก์ผ๋ฉด
|
|
10
|
+
* ์ ์ฉํ์ง ์๊ณ **๊ทธ ์ฌ์ค์ ์๋ต์ ์ฃ๋๋ค**(์กฐ์ฉํ ์ฌ๋ผ์ง๋ฉด ์ฌ์ฉ์๋ ํ์ต์ด ์ ์ ๋ฃ๋์ง ๋ชจ๋ฅธ๋ค).
|
|
11
|
+
*
|
|
12
|
+
* ๋น๊ต๋ **์๋ ํ์ฉ์ค์ฐจ**๋ก ํ๋ค(์์ํ ์ง๋ฌธ์ด ์๋๋ผ). ์ค์ธก p50 ์ ์ ๊ธฐ๋ก์ด ์์ผ ๋๋ง๋ค 1~2์ด์ฉ
|
|
13
|
+
* ํ๋ค๋ฆฌ๋๋ฐ, ๊ฐ์ ๋ฒํท์ผ๋ก ๋ญ๊ฐ ๋น๊ตํ๋ฉด ๋ฒํท ๊ฒฝ๊ณ์ ๊ฑธ๋ฆฐ ๊ฐ์ด 1์ด ๋๋ฆฌํํธ๋ก๋ ๋ฌ๋ผ์ ธ ๋ฒ๋ฆฐ๋ค โ
|
|
14
|
+
* "์์ ๋๋ฆฌํํธ๋ก ํ์ต์ ๋ฌดํจํํ์ง ์๋๋ค" ๋ ๋ชฉ์ ์ ๊ฒฝ๊ณ์์ ๋ชป ์งํจ๋ค. ๋น์จ๋ก ๋น๊ตํ๋ฉด ์ด๋์๋
|
|
15
|
+
* ๊ฐ์ ๊ท์น์ด ์ฑ๋ฆฝํ๋ค: 10% ์ด๋ด๋ ๊ฐ์ ๋ชจ๋ธ, 30์ดโ47์ด(57%)๋ ๋ค๋ฅธ ๋ชจ๋ธ.
|
|
16
|
+
*
|
|
17
|
+
* ์ฌ๋ฃ๋ ํธ์คํธ๊ฐ ์๋ ๊ฒ๋ค์ด๋ค(์ปค๋ API ์ ์์กดํ์ง ์์ ๋ฐํ ์ํ์ ๋ฌด๊ดํ๊ฒ ๋์ํ๋ค):
|
|
18
|
+
* โ ์ค์ธก์ผ๋ก ๋ฐฐ์ด ์์
์ข
๋ฅ๋ณ ์์ โก ์์ ์ข
๋ฅ๋ณ ์๋ โข ์ ์ธ๋ ๊ณต์ ์์.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** ๊ฐ์ ๋ชจ๋ธ๋ก ๋ณผ ์๋ ํ์ฉ์ค์ฐจ โ ์ด ๋น์จ ์์ ๋ณํ๋ ๋๋ฆฌํํธ, ๋์ผ๋ฉด ๋ค๋ฅธ ๋ชจ๋ธ. */
|
|
22
|
+
export const BASIS_TOLERANCE = 0.1
|
|
23
|
+
|
|
24
|
+
export interface ModelBasis {
|
|
25
|
+
/** ์์
์ข
๋ฅ โ ์ค์ธก ์์(ms). `buildMeasuredEstimator().learned`. */
|
|
26
|
+
measured?: Record<string, number>
|
|
27
|
+
/** ์์ ์ข
๋ฅ โ ์๋(m/s). `buildTravelEstimator().speedsByKind`. */
|
|
28
|
+
speeds?: Record<string, number>
|
|
29
|
+
/** ์ ์ธ๋ ๊ณต์ ์์ โ key + ISO 8601 ์๋ฌธ(๋ฌธ์์ด์ ๊ทธ๋๋ก ๋น๊ตํ๋ค: ์ ์ธ์ด ๋ฐ๋๋ฉด ๋ค๋ฅธ ๋ชจ๋ธ). */
|
|
30
|
+
declared?: { key: string; duration?: string }[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** ๋ป ์๋ ์๋ง ๋จ๊ธด๋ค โ 0ยท์์ยทNaN ์ ๋ชจ๋ธ์ด ์๋๋ค. */
|
|
34
|
+
function clean(map?: Record<string, number>): Record<string, number> {
|
|
35
|
+
const out: Record<string, number> = {}
|
|
36
|
+
for (const [k, v] of Object.entries(map ?? {})) if (Number.isFinite(v) && v > 0) out[k] = v
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function declaredMap(list?: { key: string; duration?: string }[]): Record<string, string> {
|
|
41
|
+
const out: Record<string, string> = {}
|
|
42
|
+
for (const o of list ?? []) if (o?.key && o.duration) out[o.key] = o.duration
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** ๋ ์์น ์ง๋๊ฐ ์ค์ง์ ์ผ๋ก ๋ค๋ฅธ๊ฐ โ ํค ์งํฉ์ด ๋ค๋ฅด๊ฑฐ๋, ๊ฐ์ด ํ์ฉ์ค์ฐจ๋ฅผ ๋์ด ๋ค๋ฅด๋ฉด ๋ค๋ฅด๋ค. */
|
|
47
|
+
function numericChanged(a: Record<string, number>, b: Record<string, number>, tol: number): boolean {
|
|
48
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)])
|
|
49
|
+
for (const k of keys) {
|
|
50
|
+
const x = a[k]
|
|
51
|
+
const y = b[k]
|
|
52
|
+
if (x === undefined || y === undefined) return true // ์๋ก ๋ฐฐ์ด ์ข
๋ฅยท์ฌ๋ผ์ง ์ข
๋ฅ = ๋ค๋ฅธ ๋ชจ๋ธ
|
|
53
|
+
const base = Math.max(Math.abs(x), Math.abs(y))
|
|
54
|
+
if (base > 0 && Math.abs(x - y) / base > tol) return true
|
|
55
|
+
}
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* ๋ชจ๋ธ์ด ๋ฌ๋ผ์ก๋๊ฐ โ ๋ณด์ ์ ์ ์ฉํ๊ธฐ ์ ์ ๊ด๋ฌธ.
|
|
61
|
+
* ํ์ชฝ ๊ทผ๊ฑฐ๋ฅผ ๋ชจ๋ฅด๋ฉด **ํ์ ํ์ง ์๋๋ค**(false) โ ๋ชจ๋ฅด๋ ๊ฒ์ ๊ทผ๊ฑฐ๋ก ํ์ต์ ๋์ง ์๋๋ค.
|
|
62
|
+
*/
|
|
63
|
+
export function modelBasisChanged(before?: ModelBasis, after?: ModelBasis, tol: number = BASIS_TOLERANCE): boolean {
|
|
64
|
+
if (!before || !after) return false
|
|
65
|
+
if (numericChanged(clean(before.measured), clean(after.measured), tol)) return true
|
|
66
|
+
if (numericChanged(clean(before.speeds), clean(after.speeds), tol)) return true
|
|
67
|
+
const da = declaredMap(before.declared)
|
|
68
|
+
const db = declaredMap(after.declared)
|
|
69
|
+
const keys = new Set([...Object.keys(da), ...Object.keys(db)])
|
|
70
|
+
for (const k of keys) if (da[k] !== db[k]) return true
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ์ฌ๋์ด ์ฝ๋ ๊ทผ๊ฑฐ ์์ฝ โ **๋ก๊ทธยท์ง๋จ์ฉ**(๋น๊ต์ ๊ทผ๊ฑฐ๊ฐ ์๋๋ค).
|
|
76
|
+
* ์ง๋ฌธ์ผ๋ก ๊ฐ์์ ํ์ ํ์ง ์๋ ์ด์ ๋ ์ ์ฃผ์ ์ฐธ์กฐ. ๋ ๊ทผ๊ฑฐ๊ฐ ์ ๋ฌ๋ผ์ก๋์ง ๋์ผ๋ก ๋ณด๋ ค๋ฉด ์ด๊ฒ์ ์ฐ๋๋ค.
|
|
77
|
+
*/
|
|
78
|
+
export function describeModelBasis(basis?: ModelBasis): string {
|
|
79
|
+
if (!basis) return 'unknown'
|
|
80
|
+
const seg: string[] = []
|
|
81
|
+
const m = Object.entries(clean(basis.measured))
|
|
82
|
+
.map(([k, v]) => `${k}=${Math.round(v / 1000)}s`)
|
|
83
|
+
.sort()
|
|
84
|
+
if (m.length) seg.push(`measured(${m.join(',')})`)
|
|
85
|
+
const s = Object.entries(clean(basis.speeds))
|
|
86
|
+
.map(([k, v]) => `${k}=${Math.round(v * 10) / 10}m/s`)
|
|
87
|
+
.sort()
|
|
88
|
+
if (s.length) seg.push(`speed(${s.join(',')})`)
|
|
89
|
+
const d = Object.entries(declaredMap(basis.declared))
|
|
90
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
91
|
+
.sort()
|
|
92
|
+
if (d.length) seg.push(`declared(${d.join(',')})`)
|
|
93
|
+
return seg.length ? seg.join(' ') : 'kernel defaults only'
|
|
94
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* OEE ์ด๋ฒคํธ ๋์ ๊ธฐ (์์ยท๊ทผ์ฌ) โ live-๋ชจ๋ OEE ์
๋ ฅ ์์ฐ์ ํ ๊ฒฝ๋ก(face2-inbound-live ยง1.1, option โก).
|
|
3
3
|
*
|
|
4
4
|
* ๋ฌธ์ : OEE ๊ณต์(computeOee)์ sim/live ๊ณต์ ๋๋, ์
๋ ฅ ์นด์ดํฐ(runMs/downMs/โฆ)๋ ๋ฏธ๋ฌ Stateยท์ด๋ฒคํธ์ ์๋ค
|
|
5
|
-
* (
|
|
5
|
+
* (emitEquipment ๋ status/location/motion ๋ง ๋ฐฉ์ถ). โ live ๋ ์นด์ดํฐ๋ฅผ ๋ณ๋ ์์ฐํด์ผ ํ๋ค.
|
|
6
6
|
* ์ด ๋์ ๊ธฐ: equipment.status ์ ์ด ์คํธ๋ฆผ์ผ๋ก ๋ฌด๋ฒ๋ณ ์๊ฐ-in-์ํ๋ฅผ ๋์ ํด ์นด์ดํฐ๋ฅผ ๊ทผ์ฌ ์์ฐ.
|
|
7
7
|
*
|
|
8
8
|
* ์ ํ: downMs(๊ณ ์ฅ ์๊ฐ = down ๊ตฌ๊ฐ ํฉ) ยท quality(good/scrap = quality.output ๋ธํ). ๊ทผ์ฌยทํ๊ณ:
|
|
@@ -79,10 +79,10 @@ export class OeeAccumulator {
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* live State ๋ฅผ OEE ๋ก ๋ณด๊ฐ โ projector ๋ฏธ๋ฌ State(๊ด์ธก)์ ๋ฌด๋ฒ์ ๋์ ๊ธฐ OEE(๊ณ์ฐ)๋ฅผ ์ฑ์ด๋ค.
|
|
82
|
-
* live-๋ชจ๋ publishEntityData ๊ฐ buildEntityDeltas ์ ์ ์ ์ฉ โ
|
|
82
|
+
* live-๋ชจ๋ publishEntityData ๊ฐ buildEntityDeltas ์ ์ ์ ์ฉ โ equipment data(tag) payload ์ oee ํฌํจ
|
|
83
83
|
* (sim ์ ์ปค๋์ด ์ฑ์ โ ์ด๋ก์จ live ๋ ๋์ผ payload ํ์). ์์(์
๋ ฅ ๋ถ๋ณ, ์์ ๋ณต์ฌ).
|
|
84
84
|
*/
|
|
85
|
-
export function withLiveOee<S extends {
|
|
86
|
-
if (!state?.
|
|
87
|
-
return { ...state,
|
|
85
|
+
export function withLiveOee<S extends { equipment?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {
|
|
86
|
+
if (!state?.equipment) return state
|
|
87
|
+
return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }
|
|
88
88
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ์๋ฎฌ ๋ช
์ธ ์๊ธฐ๋ณด๊ณ โ **"์ด ์์ธก์ผ๋ก ๋ฌด์์ ๋งํ ์ ์๋" ๋ฅผ ์์ธก๊ณผ ํจ๊ป ๋ด๋ณด๋ธ๋ค.**
|
|
3
|
+
*
|
|
4
|
+
* ๋ฐฐ๊ฒฝ(operato-twin design/plans/simulation-spec.md): ๊ณต์ ์์ยท์์จ์ด ์ปค๋ ์์ค์ ์์์๋ค. ์ด์
|
|
5
|
+
* ๋ง์คํฐ๊ฐ ๋ช
์ธ๋ฅผ ์ค์ด ์ค๋ฉด ์ปค๋์ด ๊ทธ๊ฒ์ผ๋ก ๊ตด๋ฆฌ๊ณ , `specCoverage()` ๊ฐ ๋ฌด์์ ์ ์ธ๊ฐ์ผ๋ก ๋ฌด์์
|
|
6
|
+
* ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ผ๋์ง ๋ณด๊ณ ํ๋ค. ๊ทธ ๋ณด๊ณ ๋ฅผ **์์ธก ์๋ต์ ๋ถ์ด์ง ์์ผ๋ฉด** ๋ฐ๋ ์ชฝ์ ์์๋ก ๊ตด๋ฆฐ ์ซ์์
|
|
7
|
+
* ํ์ฅ ๋ช
์ธ๋ก ๊ตด๋ฆฐ ์ซ์๋ฅผ ๊ตฌ๋ณํ ์ ์๋ค โ ๋๊ฐ์ด "p50 168" ๋ก ๋ณด์ธ๋ค.
|
|
8
|
+
*
|
|
9
|
+
* ์ธ์ด ์ค๋ฆฝ: ์ฌ๊ธฐ์๋ **์ฝ๋์ ์์น๋ง** ๋ธ๋ค(`qualification` + ์นด์ดํธ). ์ฌ๋์ด ์ฝ๋ ๋ฌธ์ฅ์ ํํ
|
|
10
|
+
* ๊ณ์ธต(ํด๋ผ i18next / AI ๋ต๋ณ)์ด ๋ง๋ ๋ค โ attention ๊ณผ ๊ฐ์ ๊ท์จ.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** ์์ธก ์๊ฒฉ โ ์ ์ธ๋ ๋ช
์ธ๊ฐ ์ผ๋ง๋ ๋ฎ๋์ง์ ๋ฐ๋ผ ์ด ์์ธก์ผ๋ก ์ ๋นํ๊ฒ ๋งํ ์ ์๋ ๋ฒ์๊ฐ ๋ฌ๋ผ์ง๋ค. */
|
|
14
|
+
export type ForecastQualification =
|
|
15
|
+
/** ๋ชจ๋ ์์๊ฐ ์ปค๋ ๊ธฐ๋ณธ ์์ โ **์๋ ๋น๊ต๋ง**("์์ 2๋ ๋ ๋ฃ์ผ๋ฉด ๋์์ง๋"). ์ ๋ ์๊ฐ ์ฃผ์ฅ ๋ถ๊ฐ. */
|
|
16
|
+
| 'relative'
|
|
17
|
+
/** ์ผ๋ถ๋ง ์ ์ธยท์ค์ธก โ ๊ทธ ๊ตฌ๊ฐ์ ์ ์ง, ๋๋จธ์ง๋ ์์. ํผํฉ. */
|
|
18
|
+
| 'partial'
|
|
19
|
+
/** ์์ ์ ๋ถ ์ ์ธ โ ์ ๋ ์์ธก ์๋ ๊ฐ๋ฅ. ๋จ **์ ํ๋๋ ์ด๋ ฅ์ด ํ์ธํ๋ค**(์ ์ธ != ๊ฒ์ฆ). */
|
|
20
|
+
| 'absolute-capable'
|
|
21
|
+
/**
|
|
22
|
+
* ์์ ์ ๋ถ๊ฐ **์ด ํ์ฅ์ ์ด๋ ฅ์์ ์จ ๊ฐ**(์ถ์ ๊ธฐ) โ ๊ฐ์ฅ ๊ฐํ ๊ทผ๊ฑฐ. ์ ์ธ์ ์ฌ๋์ด ์ ์ ๊ฐ์ด๊ณ
|
|
23
|
+
* ์ด๊ฒ์ ์ค์ ๋ก ์ผ์ด๋ ๊ฐ์ด๋ค. ๊ทธ๋๋ "์ ํํ๋ค" ๊ฐ ์๋๋ผ "๊ด์ธก์ ์ ์ง๋๋ค" ๋ ๋ป์ด๋ค.
|
|
24
|
+
*/
|
|
25
|
+
| 'calibrated'
|
|
26
|
+
|
|
27
|
+
export interface SpecCoverageReport {
|
|
28
|
+
operations: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters: string[] }[]
|
|
29
|
+
/** ์ด๋ ฅยท๊ณ์ฐ์์ ์จ ์์(์ถ์ ๊ธฐ) โ ์ ์ธ๊ฐ๋ณด๋ค ๊ฐํ ๊ทผ๊ฑฐ. */
|
|
30
|
+
measuredDurations: number
|
|
31
|
+
declaredDurations: number
|
|
32
|
+
defaultDurations: number
|
|
33
|
+
/** ๋ณ๋(๋ถํฌ)์ ๊ฐ์ง ์์
์ข
๋ฅ ์. **0 ์ด๋ฉด ๋ชจ๋ ์์๊ฐ ๊ณ ์ ๊ฐ**์ด๋ผ ๋๊ธฐยท๋ณ๋ชฉ์ด ๊ณผ์ํ๊ฐ๋๋ค. */
|
|
34
|
+
varyingDurations: number
|
|
35
|
+
qualification: ForecastQualification
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ๋ช
์ธ ์๊ฒฉ ํ์ (์์) โ ์ปค๋ ๋ณด๊ณ ๋ฅผ ๋ฐ์ ๋ถ๋ฅํ๋ค. ์ปค๋์ ๋ชจ๋ฅด๋ฏ๋ก ํ
์คํธ๊ฐ ์ฝ๋ค.
|
|
40
|
+
* ๊ด์ธก๋ ์์
์ด ํ๋๋ ์์ผ๋ฉด(`operations` ๋น ๋ฐฐ์ด) ํ์ ํ ๊ทผ๊ฑฐ๊ฐ ์์ผ๋ฏ๋ก **undefined**(๋ชจ๋ฅธ๋ค).
|
|
41
|
+
*/
|
|
42
|
+
export function classifySpecCoverage(cov: {
|
|
43
|
+
operations?: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters?: string[] }[]
|
|
44
|
+
measuredDurations?: number
|
|
45
|
+
declaredDurations?: number
|
|
46
|
+
defaultDurations?: number
|
|
47
|
+
}): SpecCoverageReport | undefined {
|
|
48
|
+
const ops = cov?.operations
|
|
49
|
+
if (!ops?.length) return undefined
|
|
50
|
+
const operations = ops.map(o => ({
|
|
51
|
+
kind: o.kind,
|
|
52
|
+
duration: o.duration,
|
|
53
|
+
...(o.variability ? { variability: o.variability } : {}),
|
|
54
|
+
parameters: o.parameters ?? []
|
|
55
|
+
}))
|
|
56
|
+
const measuredDurations = cov.measuredDurations ?? operations.filter(o => o.duration === 'measured').length
|
|
57
|
+
const declaredDurations = cov.declaredDurations ?? operations.filter(o => o.duration === 'declared').length
|
|
58
|
+
const defaultDurations = cov.defaultDurations ?? operations.filter(o => o.duration === 'default').length
|
|
59
|
+
const varyingDurations = operations.filter(o => o.variability && o.variability !== 'constant').length
|
|
60
|
+
/* ์ฌ๋ค๋ฆฌ: ์ ๋ถ ์ด๋ ฅ โ calibrated ยท ์์ ์์ โ absolute-capable ยท ์ ์ง๊ฐ ํ๋๋ ์์ โ relative ยท ๋๋จธ์ง โ partial. */
|
|
61
|
+
const grounded = measuredDurations + declaredDurations
|
|
62
|
+
const qualification: ForecastQualification =
|
|
63
|
+
grounded === 0
|
|
64
|
+
? 'relative'
|
|
65
|
+
: defaultDurations > 0
|
|
66
|
+
? 'partial'
|
|
67
|
+
: declaredDurations === 0
|
|
68
|
+
? 'calibrated'
|
|
69
|
+
: 'absolute-capable'
|
|
70
|
+
return { operations, measuredDurations, declaredDurations, defaultDurations, varyingDurations, qualification }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* ์ปค๋์์ ์ฝ๋๋ค โ **์์ง ์ด API ๊ฐ ์๋ ์ปค๋(๋ฐํ ์ด์ )์ด๋ฉด undefined.**
|
|
75
|
+
* ๊ทธ๋ `undefined` ๋ "๋ช
์ธ๊ฐ ์๋ค" ๊ฐ ์๋๋ผ "๋ชจ๋ฅธ๋ค" ๋ค. ์๋น์ฒ๊ฐ ๊ทธ ๋์ ์์ง ์๋๋ก ์ฃผ์์ ๋จ๊ธด๋ค:
|
|
76
|
+
* ์๋ ๊ฒ์ "์ ๋ถ ๊ธฐ๋ณธ๊ฐ" ์ด๋ผ๊ณ ๋จ์ ํ์ง ์๋๋ค(๋จ์ ํ๋ ค๋ฉด ์ปค๋์ด ๋งํด ์ค์ผ ํ๋ค).
|
|
77
|
+
*/
|
|
78
|
+
export function readSpecCoverage(kernel: any): SpecCoverageReport | undefined {
|
|
79
|
+
if (typeof kernel?.specCoverage !== 'function') return undefined
|
|
80
|
+
try {
|
|
81
|
+
return classifySpecCoverage(kernel.specCoverage())
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ๊ตฌ์กฐ ์ด๋ ฅ์ **์ฐจ์ด** โ "2๋ฒ์งธ์ ๋์ฅ ๋ถ์ค๊ฐ ๋ท ๋์๋ค" ๋ฅผ ๋งํ ์ ์๊ฒ.
|
|
3
|
+
*
|
|
4
|
+
* ๋ฆฌ๋น์ ์ ๊ทธ ์์ ๊ณต์ฅ์ ํต์งธ๋ก ๋ด๋๋ค(์ฌ์ํ๋ ค๋ฉด ์ ์ฒด ์์์ด ํ์ํ๋ค). ๊ทธ๋ฐ๋ฐ ์ฌ๋์ด ๋ณด๊ณ ์ถ์
|
|
5
|
+
* ๊ฒ์ ํต์งธ๊ฐ ์๋๋ผ **๋ฌด์์ด ๋ฌ๋ผ์ก๋** ๋ค โ 62KB ์ง๋ฆฌ ๋ณด๋ ๋ ๊ฐ๋ฅผ ๋๋ํ ๋๊ณ ๋์ผ๋ก ๋น๊ตํ ์๋
|
|
6
|
+
* ์๋ค. ๊ทธ ํ ์ค์ ์ฌ๊ธฐ์ ๋ง๋ ๋ค.
|
|
7
|
+
*
|
|
8
|
+
* ์์ ํจ์์ธ ์ด์ : ๊ฐ์ ๋ฌธ์ฅ์ ํ๋ฉด๊ณผ AI ๊ฐ ํจ๊ป ์จ์ผ ํ๋ค. ํ์ชฝ์์๋ง ๊ณ์ฐํ๋ฉด ๋์ด ๋ค๋ฅธ ๋ง์ ํ๋ค.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** ์ข
๋ฅ๋ณ ์ฆ๊ฐ โ "paint-booth +4" ์ฒ๋ผ ์ฝํ๋ค. */
|
|
12
|
+
export interface StructureCountChange {
|
|
13
|
+
key: string
|
|
14
|
+
before: number
|
|
15
|
+
after: number
|
|
16
|
+
delta: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StructureDiff {
|
|
20
|
+
/** ์๋ฆฌ ์ข
๋ฅ๋ณ ์ฆ๊ฐ(0 ์ธ ๊ฒ์ ๋นผ๊ณ ). */
|
|
21
|
+
locationTypes: StructureCountChange[]
|
|
22
|
+
/** ์ค๋น ์ข
๋ฅ๋ณ ์ฆ๊ฐ(0 ์ธ ๊ฒ์ ๋นผ๊ณ ). */
|
|
23
|
+
equipmentKinds: StructureCountChange[]
|
|
24
|
+
locationsAdded: number
|
|
25
|
+
locationsRemoved: number
|
|
26
|
+
equipmentAdded: number
|
|
27
|
+
equipmentRemoved: number
|
|
28
|
+
/** ๊ฐ์๋ ๊ฐ์๋ฐ ๋ด์ฉ์ด ๋ฐ๋ ๊ฒฝ์ฐ(์ฉ๋ ์กฐ์ ยท์๋ฆฌ ์ด๋ ๋ฑ)๊ฐ ์๋๊ฐ. */
|
|
29
|
+
changedInPlace: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const countBy = <T>(rows: readonly T[] | undefined, key: (r: T) => string | undefined): Map<string, number> => {
|
|
33
|
+
const out = new Map<string, number>()
|
|
34
|
+
for (const r of rows ?? []) {
|
|
35
|
+
const k = key(r)
|
|
36
|
+
if (k) out.set(k, (out.get(k) ?? 0) + 1)
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const idsOf = (rows: readonly { id?: string }[] | undefined): Set<string> => new Set((rows ?? []).map(r => r.id).filter((x): x is string => !!x))
|
|
42
|
+
|
|
43
|
+
function changes(before: Map<string, number>, after: Map<string, number>): StructureCountChange[] {
|
|
44
|
+
const keys = [...new Set([...before.keys(), ...after.keys()])].sort()
|
|
45
|
+
return keys
|
|
46
|
+
.map(key => ({ key, before: before.get(key) ?? 0, after: after.get(key) ?? 0, delta: (after.get(key) ?? 0) - (before.get(key) ?? 0) }))
|
|
47
|
+
.filter(c => c.delta !== 0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ๋ ๊ตฌ์กฐ์ ์ฐจ์ด. `before` ๊ฐ ์์ผ๋ฉด **์ต์ด ๋ฆฌ๋น์ **์ด๋ผ ๋น๊ต ๋์์ด ์๋ค โ ์ ๋ถ ์๋ก ์๊ธด ๊ฒ์ผ๋ก ์ผ๋ค.
|
|
52
|
+
*
|
|
53
|
+
* ์์ด๋๋ **์งํฉ์ผ๋ก** ๋น๊ตํ๋ค(์๋ง ์ธ๋ฉด ๋ท์ด ์ฌ๋ผ์ง๊ณ ๋ท์ด ์๊ธด ๊ต์ฒด๋ฅผ "๋ณํ ์์" ์ผ๋ก ๋ตํ๋ค).
|
|
54
|
+
*/
|
|
55
|
+
export function diffStructures(before: any | undefined, after: any): StructureDiff {
|
|
56
|
+
const beforeLocations = before?.locations ?? []
|
|
57
|
+
const beforeEquipment = before?.equipment ?? []
|
|
58
|
+
const afterLocations = after?.locations ?? []
|
|
59
|
+
const afterEquipment = after?.equipment ?? []
|
|
60
|
+
|
|
61
|
+
const beforeLocationIds = idsOf(beforeLocations)
|
|
62
|
+
const afterLocationIds = idsOf(afterLocations)
|
|
63
|
+
const beforeEquipmentIds = idsOf(beforeEquipment)
|
|
64
|
+
const afterEquipmentIds = idsOf(afterEquipment)
|
|
65
|
+
|
|
66
|
+
const added = <T>(a: Set<T>, b: Set<T>) => [...b].filter(x => !a.has(x)).length
|
|
67
|
+
const locationsAdded = added(beforeLocationIds, afterLocationIds)
|
|
68
|
+
const locationsRemoved = added(afterLocationIds, beforeLocationIds)
|
|
69
|
+
const equipmentAdded = added(beforeEquipmentIds, afterEquipmentIds)
|
|
70
|
+
const equipmentRemoved = added(afterEquipmentIds, beforeEquipmentIds)
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* ์์ด๋๋ ๊ทธ๋๋ก์ธ๋ฐ ์์ฑ์ด ๋ฐ๋ ๊ฒฝ์ฐ โ ์ฉ๋์ ๋๋ ธ๊ฑฐ๋ ์ค๋น๋ฅผ ์ฎ๊ฒผ๋ค. ์ง๋ฌธ์ด ๋ฌ๋ผ ์ ๋ฆฌ๋น์ ์ด
|
|
74
|
+
* ์๊ฒผ๋๋ฐ ์ฆ๊ฐ์ด ์ ๋ถ 0 ์ด๋ฉด, ํ๋ฉด์ด "๋ฐ๋ ๊ฒ ์๋ค" ๊ณ ๋ตํ๊ฒ ๋๋ค. ๊ทธ๋ฐ ์นจ๋ฌต์ ๋ง๋๋ค.
|
|
75
|
+
*/
|
|
76
|
+
const changedInPlace =
|
|
77
|
+
!locationsAdded && !locationsRemoved && !equipmentAdded && !equipmentRemoved && !!before
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
locationTypes: changes(countBy(beforeLocations, (n: any) => n.type), countBy(afterLocations, (n: any) => n.type)),
|
|
81
|
+
equipmentKinds: changes(countBy(beforeEquipment, (m: any) => m.kind), countBy(afterEquipment, (m: any) => m.kind)),
|
|
82
|
+
locationsAdded,
|
|
83
|
+
locationsRemoved,
|
|
84
|
+
equipmentAdded,
|
|
85
|
+
equipmentRemoved,
|
|
86
|
+
changedInPlace
|
|
87
|
+
}
|
|
88
|
+
}
|