@operato/twin-kernel 0.11.26 → 0.11.28
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/dist/flow-engine.d.ts +10 -1
- package/dist/flow-engine.js +19 -4
- package/dist/observed-reducer.d.ts +7 -0
- package/dist/observed-reducer.js +12 -0
- package/dist/yms-kernel.js +9 -4
- package/dist-cjs/index.cjs +823 -761
- package/package.json +2 -2
package/dist-cjs/index.cjs
CHANGED
|
@@ -868,6 +868,7 @@ var ALLOCATION_POLICY = {
|
|
|
868
868
|
/** 먼저 만료되는 것부터(First Expired, First Out). */
|
|
869
869
|
fefo: "fefo"
|
|
870
870
|
};
|
|
871
|
+
var BASIS_UNKNOWN = ["BEFORE_BASELINE"];
|
|
871
872
|
var CMD = {
|
|
872
873
|
orderHold: "order.hold",
|
|
873
874
|
orderResume: "order.resume",
|
|
@@ -1044,7 +1045,26 @@ var BIZSTEP = {
|
|
|
1044
1045
|
packing: "urn:epcglobal:cbv:bizstep:packing",
|
|
1045
1046
|
staging_outbound: "urn:epcglobal:cbv:bizstep:staging_outbound",
|
|
1046
1047
|
shipping: "urn:epcglobal:cbv:bizstep:shipping",
|
|
1047
|
-
|
|
1048
|
+
/**
|
|
1049
|
+
* **Replenishment** — moving goods to where they are picked from. CBV has no `replenishing`; this contract
|
|
1050
|
+
* wrote one until 2026-09-24, and the closed-list test found it. CBV `stocking` is the step that means it:
|
|
1051
|
+
* "…within a location to make an object available to the customer or for order fulfilment within a DC"
|
|
1052
|
+
* (CBV ontology, `BizStep-stocking`). Ruled by the chief architect; no reader branches on the value, and
|
|
1053
|
+
* the journal keeps what it already wrote (ADR-0038).
|
|
1054
|
+
*/
|
|
1055
|
+
stocking: "urn:epcglobal:cbv:bizstep:stocking",
|
|
1056
|
+
/**
|
|
1057
|
+
* **Counted, and the book set to the count** — CBV `cycle_counting`. Primary source: the CBV ontology
|
|
1058
|
+
* (gs1/EPCIS `Ontology/CBV.ttl`, `cbv:BizStep-cycle_counting`, also served at ref.gs1.org/cbv): "Process of
|
|
1059
|
+
* counting objects within a location in order to obtain an accurate inventory for business needs other
|
|
1060
|
+
* than accounting purposes (e.g., replenishment and allocation)."
|
|
1061
|
+
*
|
|
1062
|
+
* **Only for an event somebody actually counted** (chief architect, 2026-09-24). A person correcting the
|
|
1063
|
+
* book without counting is `CBV_BIZSTEP.other` with the reason carried as a value — `epcis.ts` says why:
|
|
1064
|
+
* writing that as `cycle_counting` records a count that never happened. A count for the accounts is a
|
|
1065
|
+
* different CBV step (`stock_taking`) and is not this one.
|
|
1066
|
+
*/
|
|
1067
|
+
cycle_counting: "urn:epcglobal:cbv:bizstep:cycle_counting"
|
|
1048
1068
|
};
|
|
1049
1069
|
var BTT = {
|
|
1050
1070
|
/** Purchase Order — the document an inbound receipt is made against. */
|
|
@@ -1092,634 +1112,212 @@ var WMS_TYPES = [
|
|
|
1092
1112
|
{ key: "packer", role: "equipment", label: "twin.type.packer", standardClass: { epcis: "object", isa95: "Equipment" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
1093
1113
|
];
|
|
1094
1114
|
|
|
1095
|
-
// ../ops-contract/dist/
|
|
1096
|
-
var
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
//
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
};
|
|
1108
|
-
var YMS_LOCATION_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
|
|
1109
|
-
var YMS_LOCATION_LEVEL = {
|
|
1110
|
-
gate: "Other",
|
|
1111
|
-
"yard-slot": "StorageUnit",
|
|
1112
|
-
"dock-door": "StorageUnit",
|
|
1113
|
-
staging: "StorageZone"
|
|
1114
|
-
};
|
|
1115
|
-
var YMS_TYPES = [
|
|
1116
|
-
...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, level: YMS_LOCATION_LEVEL[k], capabilities: ["storable"] })),
|
|
1117
|
-
{ key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
1118
|
-
];
|
|
1119
|
-
|
|
1120
|
-
// ../ops-contract/dist/mes-profile.js
|
|
1121
|
-
var MES_BIZSTEP = {
|
|
1122
|
-
receiving: "urn:epcglobal:cbv:bizstep:receiving",
|
|
1123
|
-
// 원자재 수령
|
|
1124
|
-
producing: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
1125
|
-
// 생산(제품 최초 생성)
|
|
1126
|
-
storing: "urn:epcglobal:cbv:bizstep:storing"
|
|
1127
|
-
// 완제품 저장
|
|
1128
|
-
};
|
|
1129
|
-
var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
1130
|
-
function sgtinUri(companyPrefix, itemRef, serial) {
|
|
1131
|
-
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
1132
|
-
}
|
|
1133
|
-
var MES_LOCATION_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
1134
|
-
var MES_LOCATION_CLS = {
|
|
1135
|
-
"raw-store": { epcis: "bizLocation" },
|
|
1136
|
-
"cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1137
|
-
"weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1138
|
-
"paint-booth": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1139
|
-
"assembly-line": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1140
|
-
"fg-store": { epcis: "bizLocation" }
|
|
1141
|
-
};
|
|
1142
|
-
var MES_LOCATION_LEVEL = {
|
|
1143
|
-
"raw-store": "StorageZone",
|
|
1144
|
-
"cut-station": "WorkCell",
|
|
1145
|
-
"weld-station": "WorkCell",
|
|
1146
|
-
"paint-booth": "WorkCell",
|
|
1147
|
-
"assembly-line": "ProductionLine",
|
|
1148
|
-
"fg-store": "StorageZone"
|
|
1149
|
-
};
|
|
1150
|
-
var MES_TYPES = [
|
|
1151
|
-
...MES_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: MES_LOCATION_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, level: MES_LOCATION_LEVEL[k], capabilities: ["storable"] })),
|
|
1152
|
-
{ key: "cutter", role: "equipment", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1153
|
-
{ key: "welder", role: "equipment", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1154
|
-
{ key: "painter", role: "equipment", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1155
|
-
{ key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
1156
|
-
];
|
|
1157
|
-
|
|
1158
|
-
// ../ops-contract/dist/ems-profile.js
|
|
1159
|
-
var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
|
|
1160
|
-
var METER_DIRECTION = ["import", "export", "bidirectional"];
|
|
1161
|
-
var EMS_PROPERTY = {
|
|
1162
|
-
/** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
|
|
1163
|
-
contractKW: "contract.kW",
|
|
1164
|
-
/*
|
|
1165
|
-
* ── 계량 지점의 방향 (2026-08-25) ──────────────────────────────────────────
|
|
1166
|
-
* **요금은 계통 접속점의 순 유입에 매겨진다.** 그러니 그 지점의 계량기가 무엇을 재는지 모르면
|
|
1167
|
-
* 요금을 사실대로 말할 수 없다 — 내보낸 양을 쓴 양으로 세면 계약 대비 판단이 반대로 뒤집힌다.
|
|
1115
|
+
// ../ops-contract/dist/epcis.js
|
|
1116
|
+
var EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
|
|
1117
|
+
var UTC_OFFSET = "+00:00";
|
|
1118
|
+
var DISP = {
|
|
1119
|
+
in_progress: "urn:epcglobal:cbv:disp:in_progress",
|
|
1120
|
+
sellable: "urn:epcglobal:cbv:disp:sellable_accessible",
|
|
1121
|
+
reserved: "urn:epcglobal:cbv:disp:reserved",
|
|
1122
|
+
in_transit: "urn:epcglobal:cbv:disp:in_transit",
|
|
1123
|
+
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other",
|
|
1124
|
+
// 불량/scrap
|
|
1125
|
+
/**
|
|
1126
|
+
* **기한이 지났다** — CBV `expired`.
|
|
1168
1127
|
*
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1128
|
+
* ── 왜 `non_sellable` 로 계산하지 않나 (2026-08-24) ─────────────────────────────
|
|
1129
|
+
* 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
|
|
1130
|
+
* 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
|
|
1131
|
+
* 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
|
|
1171
1132
|
*
|
|
1172
|
-
*
|
|
1173
|
-
* 계량 지점으로 보내는 것은 여전히 하지 않는다. 이 축은 **접속점 계량기**가 무엇을 재는지 말하는
|
|
1174
|
-
* 자리다.
|
|
1133
|
+
* 그리고 표준에 **정확한 낱말이 있다** — 합치는 것은 있는 낱말을 버리는 것이다.
|
|
1175
1134
|
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1135
|
+
* ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
|
|
1136
|
+
* `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
|
|
1137
|
+
* 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
|
|
1178
1138
|
*
|
|
1179
|
-
*
|
|
1180
|
-
|
|
1181
|
-
meterDirection: "meter.direction",
|
|
1182
|
-
/*
|
|
1183
|
-
* ── 부하 계수 — 시뮬레이션이 전기를 만들 수 있게 (2026-08-14) ───────────────
|
|
1184
|
-
* 「이 설비가 돌면 몇 kW 인가」는 **현장이 아는 값**이다. 그래서 타입 기본값을 두지 않는다:
|
|
1185
|
-
* 선언하지 않은 설비는 부하를 만들지 않는다. 기본값을 두면 아무도 그 수가 짐작인 줄 모른 채
|
|
1186
|
-
* 요금 판정이 그 위에 선다.
|
|
1187
|
-
*/
|
|
1188
|
-
/** 가동 중 소비(kW). */
|
|
1189
|
-
ratedKW: "power.ratedKW",
|
|
1190
|
-
/** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
|
|
1191
|
-
standbyKW: "power.standbyKW",
|
|
1192
|
-
/*
|
|
1193
|
-
* ── 요금 단가 — 수를 금액으로 바꾸는 선언 (2026-08-17) ─────────────────────
|
|
1139
|
+
* 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
|
|
1140
|
+
* 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
|
|
1194
1141
|
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1142
|
+
* ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
|
|
1143
|
+
* 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
|
|
1144
|
+
* 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
|
|
1145
|
+
* 그 표에 있다.
|
|
1198
1146
|
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1147
|
+
* **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
|
|
1148
|
+
* 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
|
|
1149
|
+
* CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
|
|
1202
1150
|
*
|
|
1203
|
-
*
|
|
1151
|
+
* 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
|
|
1152
|
+
* 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
|
|
1204
1153
|
*/
|
|
1205
|
-
|
|
1206
|
-
demandChargePerKW: "tariff.demandChargePerKW",
|
|
1154
|
+
expired: "urn:epcglobal:cbv:disp:expired",
|
|
1207
1155
|
/**
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
* 않고 **계산된 결과를 받는다.** 이 값이 있으면 기본요금이 조건부 파생이 아니라 사실이 된다.
|
|
1213
|
-
*
|
|
1214
|
-
* 계약전력(`contract.kW`)을 이 자리에 넣지 말 것. 넘었을 때의 뜻이 다르다 — 계약 초과는 약정
|
|
1215
|
-
* 위반이고, 이쪽은 다음 주기의 기본요금이 오른다는 뜻이다.
|
|
1216
|
-
*/
|
|
1217
|
-
billingDemandKW: "tariff.billingDemandKW",
|
|
1218
|
-
/** 사용량 단가 — 1kWh 당. */
|
|
1219
|
-
energyChargePerKWh: "tariff.energyChargePerKWh",
|
|
1220
|
-
/** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
|
|
1221
|
-
currency: "tariff.currency",
|
|
1222
|
-
/*
|
|
1223
|
-
* ── 축전지의 용량과 방전 정책 (2026-08-17) ─────────────────────────────────
|
|
1224
|
-
*
|
|
1225
|
-
* 「배터리로 피크를 깎는다」를 시뮬이 보이려면 방전을 만들어야 하고, 그것은 **선언에서** 나와야 한다.
|
|
1226
|
-
* 임계·최대율·예비를 우리가 정하면 그 수가 어디서 왔는지 아무도 설명할 수 없다.
|
|
1227
|
-
*
|
|
1228
|
-
* ── 선언한다는 것은 「그 자동화가 있다」는 주장이다 ─────────────────────────
|
|
1229
|
-
* 이 정책을 모델에 적으면 **라이브가 아닌 모든 구동이 그 규칙으로 돈다.** 현장에 BESS 제어기가 없는데
|
|
1230
|
-
* 적으면 파생 부하가 실제보다 낙관적으로 나온다(피크가 깎인 것으로 보인다). 그러니 「도입하면?」을
|
|
1231
|
-
* 묻는 것이라면 선언이 아니라 **what-if 가 덮어쓸 일**이다.
|
|
1232
|
-
*
|
|
1233
|
-
* 용량이 없으면 방전하지 않는다 — 용량을 모르면 「얼마나 버티나」를 답할 수 없고, 버티는 시간을
|
|
1234
|
-
* 모른 채 깎으면 무한히 깎을 수 있다고 주장하는 셈이다.
|
|
1156
|
+
* **폐기됐다 — 남은 것이 없다.** CBV `destroyed` (CBV ontology, `Disp-destroyed`), with the step
|
|
1157
|
+
* `CBV_BIZSTEP.destroying`. Ruled 2026-09-24, shape from plant: the disposition belongs to the quantity
|
|
1158
|
+
* **observed**, which is what is left. A partial scrap leaves the rest `in_progress`; only the last of it —
|
|
1159
|
+
* nothing left — is `destroyed`. Putting it on a remainder would say sound goods were thrown away.
|
|
1235
1160
|
*/
|
|
1236
|
-
|
|
1237
|
-
capacityKWh: "storage.capacityKWh",
|
|
1238
|
-
/** 이 순수요를 넘으면 방전한다(kW) — 피크 억제 임계. */
|
|
1239
|
-
dischargeAboveKW: "dispatch.dischargeAboveKW",
|
|
1240
|
-
/** 최대 방전율(kW) — 인버터가 낼 수 있는 한계. */
|
|
1241
|
-
maxDischargeKW: "dispatch.maxDischargeKW",
|
|
1242
|
-
/** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
|
|
1243
|
-
reserveSoc: "dispatch.reserveSoc",
|
|
1161
|
+
destroyed: "urn:epcglobal:cbv:disp:destroyed",
|
|
1244
1162
|
/**
|
|
1245
|
-
*
|
|
1163
|
+
* **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
|
|
1246
1164
|
*
|
|
1247
|
-
*
|
|
1248
|
-
* 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
|
|
1249
|
-
* 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
|
|
1165
|
+
* 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
|
|
1250
1166
|
*
|
|
1251
|
-
*
|
|
1252
|
-
|
|
1253
|
-
initialSoc: "storage.initialSoc",
|
|
1254
|
-
/*
|
|
1255
|
-
* ── 발전 — 태양광이 서 있기만 하던 자리 (2026-08-18) ────────────────────────
|
|
1167
|
+
* conformant Outcome of a successful/passed inspection in an inspecting or repairing step
|
|
1168
|
+
* non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
|
|
1256
1169
|
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1170
|
+
* ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
|
|
1171
|
+
* 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
|
|
1172
|
+
* 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
|
|
1260
1173
|
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
1174
|
+
* 처분을 쓰면 두 가지가 공짜로 성립한다.
|
|
1175
|
+
* ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
|
|
1176
|
+
* ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
|
|
1264
1177
|
*
|
|
1265
|
-
*
|
|
1178
|
+
* 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
|
|
1179
|
+
* 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
|
|
1180
|
+
* 그때 열되, **판정 자체는 여기서 끝난다.**
|
|
1266
1181
|
*/
|
|
1182
|
+
conformant: "urn:epcglobal:cbv:disp:conformant",
|
|
1183
|
+
non_conformant: "urn:epcglobal:cbv:disp:non_conformant"
|
|
1184
|
+
};
|
|
1185
|
+
var HATIOLAB_VOCABULARY = "https://hatiolab.com/voc/";
|
|
1186
|
+
var PRIVATE_BIZSTEP = {
|
|
1187
|
+
/** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
|
|
1188
|
+
consuming: `${HATIOLAB_VOCABULARY}bizstep/consuming`
|
|
1189
|
+
};
|
|
1190
|
+
var CBV_BIZSTEP = {
|
|
1191
|
+
/* 소비(`consuming`)는 CBV 에 없어 `PRIVATE_BIZSTEP` 으로 갔다(2026-09-24). */
|
|
1267
1192
|
/**
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
* 설비마다 선언하거나, 설비별 값을 모르는 현장은 **자리(수전)에 합계로** 선언한다.
|
|
1193
|
+
* **폐기 — 물건이 끝났다.** CBV `destroying`: "Process of terminating an object. For an instance-level
|
|
1194
|
+
* identifier, the object should not be the subject of subsequent events" (CBV ontology, `BizStep-destroying`).
|
|
1195
|
+
* Ruled 2026-09-24 (chief architect) — plant wrote scrapping as `other`; the warehouse's disposal door uses the
|
|
1196
|
+
* same step. Not a count, and not a correction: the goods are gone.
|
|
1274
1197
|
*/
|
|
1275
|
-
|
|
1198
|
+
destroying: "urn:epcglobal:cbv:bizstep:destroying",
|
|
1199
|
+
/** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
|
|
1200
|
+
commissioning: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
1276
1201
|
/**
|
|
1277
|
-
*
|
|
1202
|
+
* **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
|
|
1203
|
+
* potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
|
|
1204
|
+
* 이어진다(즉 검사는 물건을 소비하지 않는다).
|
|
1278
1205
|
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1206
|
+
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
1207
|
+
* 공정 중 검사가 그 모양이다.
|
|
1281
1208
|
*/
|
|
1282
|
-
|
|
1209
|
+
inspecting: "urn:epcglobal:cbv:bizstep:inspecting",
|
|
1283
1210
|
/**
|
|
1284
|
-
*
|
|
1211
|
+
* **어느 단계로도 이름 붙지 않는 활동** — 원문: 「A business step not identified by any other」.
|
|
1285
1212
|
*
|
|
1286
|
-
*
|
|
1213
|
+
* 이것은 근사가 아니라 **표준이 준 낱말**이다. 그 구별이 중요하다: 재고 조정처럼 한 낱말이 두 일을
|
|
1214
|
+
* 하는 원천(세어 보고 맞춘 것 · 사람이 정정한 것)을 `cycle_counting` 으로 옮기면 **일어나지 않은
|
|
1215
|
+
* 계수를 기록**하게 된다. 그때 쓰는 것이 이 값이고, 무슨 일이었는지는 값으로 함께 나른다.
|
|
1216
|
+
*
|
|
1217
|
+
* 원문 확인: CBV Standard Release 2.0(Ratified Jun 2022) §7.1.
|
|
1287
1218
|
*/
|
|
1288
|
-
|
|
1219
|
+
other: "urn:epcglobal:cbv:bizstep:other"
|
|
1289
1220
|
};
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
[EMS_PROPERTY.standbyKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while idle, in kW." },
|
|
1299
|
-
[EMS_PROPERTY.demandChargePerKW]: { dataType: "xs:double", note: "demand charge per kW of billing-period peak, in the declared currency." },
|
|
1300
|
-
[EMS_PROPERTY.billingDemandKW]: {
|
|
1301
|
-
uom: "kW",
|
|
1302
|
-
dataType: "xs:double",
|
|
1303
|
-
note: "billing demand in kW \u2014 the basis the demand charge is actually billed on (often a rolling 12-month peak). Not the contracted power."
|
|
1304
|
-
},
|
|
1305
|
-
[EMS_PROPERTY.energyChargePerKWh]: { dataType: "xs:double", note: "energy charge per kWh, in the declared currency." },
|
|
1306
|
-
[EMS_PROPERTY.currency]: { dataType: "xs:string", note: "ISO 4217 currency code, e.g. USD or KRW." },
|
|
1307
|
-
[EMS_PROPERTY.capacityKWh]: { uom: "kWh", dataType: "xs:double", note: "usable energy of the storage, in kWh." },
|
|
1308
|
-
[EMS_PROPERTY.dischargeAboveKW]: { uom: "kW", dataType: "xs:double", note: "net demand above which the storage discharges, in kW." },
|
|
1309
|
-
[EMS_PROPERTY.maxDischargeKW]: { uom: "kW", dataType: "xs:double", note: "inverter limit on discharge rate, in kW." },
|
|
1310
|
-
/* 퍼센트다 — 0.8 은 0.8% 이고 80% 가 아니다. 이 한 줄이 없어서 배터리가 조용히 비어 있었다. */
|
|
1311
|
-
[EMS_PROPERTY.reserveSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "reserve state of charge as a percentage 0-100 (20 means 20%), never discharged below." },
|
|
1312
|
-
[EMS_PROPERTY.initialSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "starting state of charge as a percentage 0-100 (80 means 80%, not 0.8)." },
|
|
1313
|
-
[EMS_PROPERTY.genRatedKW]: {
|
|
1314
|
-
uom: "kW",
|
|
1315
|
-
dataType: "xs:double",
|
|
1316
|
-
note: "rated AC generation output, in kW \u2014 the denominator of capacity factor. Declare per equipment, or on the incoming location as a site total."
|
|
1317
|
-
},
|
|
1318
|
-
[EMS_PROPERTY.genRatedKWdc]: {
|
|
1319
|
-
uom: "kW",
|
|
1320
|
-
dataType: "xs:double",
|
|
1321
|
-
note: "rated DC generation capacity, in kW (panel rating for PV). Larger than the AC rating by design; not the capacity-factor denominator."
|
|
1322
|
-
},
|
|
1323
|
-
[EMS_PROPERTY.genDailyProfile]: {
|
|
1324
|
-
dataType: "xs:string",
|
|
1325
|
-
note: "daily shape as 24 comma-separated fractions of rated output (0-1), one per hour of local time \u2014 the site declares its own curve; we do not invent one."
|
|
1326
|
-
}
|
|
1221
|
+
function sgtinClass(companyPrefix, itemRef) {
|
|
1222
|
+
return `urn:epc:idpat:sgtin:${companyPrefix}.${itemRef}.*`;
|
|
1223
|
+
}
|
|
1224
|
+
var ILMD_ATTR = {
|
|
1225
|
+
/** 유통기한·만료(로트 단위). */
|
|
1226
|
+
expiry: "cbvmda:itemExpirationDate",
|
|
1227
|
+
/** 로트·배치 번호(직렬 개체에 로트를 붙일 때). */
|
|
1228
|
+
lot: "cbvmda:lotNumber"
|
|
1327
1229
|
};
|
|
1328
|
-
function
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
const
|
|
1333
|
-
if (
|
|
1230
|
+
function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
|
|
1231
|
+
return `urn:epc:class:lgtin:${companyPrefix}.${itemRefAndIndicator}.${encodeURIComponent(lot)}`;
|
|
1232
|
+
}
|
|
1233
|
+
function parseEpc(uri) {
|
|
1234
|
+
const raw = String(uri ?? "");
|
|
1235
|
+
if (/(?:\/obj\/|:obj:)[^/:\s]+$/.test(raw))
|
|
1236
|
+
return { scheme: "unknown", instance: true, uri: raw };
|
|
1237
|
+
const inHouseClass = raw.match(/(?:\/class\/|:class:)([^/:\s]+)$/);
|
|
1238
|
+
if (inHouseClass) {
|
|
1239
|
+
const at = inHouseClass[1].indexOf(LOT_SEPARATOR);
|
|
1240
|
+
const split = at > 0 ? decodedPair(inHouseClass[1].slice(0, at), inHouseClass[1].slice(at + LOT_SEPARATOR.length)) : void 0;
|
|
1241
|
+
return split ? { scheme: "unknown", instance: false, item: split[0], lot: split[1], uri: raw } : { scheme: "unknown", instance: false, uri: raw };
|
|
1242
|
+
}
|
|
1243
|
+
const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
|
|
1244
|
+
if (cls) {
|
|
1245
|
+
const seg = cls[1].split(".");
|
|
1246
|
+
const lot = seg.slice(2).join(".");
|
|
1247
|
+
return {
|
|
1248
|
+
scheme: "lgtin",
|
|
1249
|
+
instance: false,
|
|
1250
|
+
gtinKey: seg.slice(0, 2).join("."),
|
|
1251
|
+
lot: lot ? decodeURIComponent(lot) : void 0,
|
|
1252
|
+
uri: raw
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
const pat = raw.match(/^urn:epc:idpat:sgtin:(.+)$/);
|
|
1256
|
+
if (pat) {
|
|
1257
|
+
const seg = pat[1].split(".");
|
|
1258
|
+
return { scheme: "idpat", instance: false, gtinKey: seg.slice(0, 2).join("."), uri: raw };
|
|
1259
|
+
}
|
|
1260
|
+
const id = raw.match(/^urn:epc:id:([a-z]+):(.+)$/);
|
|
1261
|
+
if (id) {
|
|
1262
|
+
const scheme = id[1];
|
|
1263
|
+
const seg = id[2].split(".");
|
|
1264
|
+
const known = ["sgtin", "sscc", "gdti", "grai", "giai", "sgln"].includes(scheme);
|
|
1265
|
+
return {
|
|
1266
|
+
scheme: known ? scheme : "unknown",
|
|
1267
|
+
instance: true,
|
|
1268
|
+
...scheme === "sgtin" ? { gtinKey: seg.slice(0, 2).join("."), serial: seg[2] } : { serial: seg.slice(1).join(".") },
|
|
1269
|
+
uri: raw
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
return { scheme: "unknown", instance: false, uri: raw };
|
|
1273
|
+
}
|
|
1274
|
+
function decodedPair(item, lot) {
|
|
1275
|
+
if (!lot)
|
|
1334
1276
|
return void 0;
|
|
1335
|
-
|
|
1336
|
-
|
|
1277
|
+
try {
|
|
1278
|
+
return [decodeURIComponent(item), decodeURIComponent(lot)];
|
|
1279
|
+
} catch {
|
|
1337
1280
|
return void 0;
|
|
1338
|
-
|
|
1281
|
+
}
|
|
1339
1282
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
standardClass: { iec61850: "MMTR", iso50001: "EnergyInput" },
|
|
1351
|
-
identity: { scheme: "kernel:id" },
|
|
1352
|
-
capabilities: ["metered"]
|
|
1353
|
-
},
|
|
1354
|
-
{
|
|
1355
|
-
key: "feeder",
|
|
1356
|
-
role: "location",
|
|
1357
|
-
/* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
|
|
1358
|
-
level: "Other",
|
|
1359
|
-
label: "twin.type.feeder",
|
|
1360
|
-
/* 분기 회로 — 부하 분해의 단위. 계약전력의 하위 배분이 여기서 정해진다. */
|
|
1361
|
-
standardClass: { iec61850: "Feeder", iso50001: "EnergyUse" },
|
|
1362
|
-
identity: { scheme: "kernel:id" },
|
|
1363
|
-
capabilities: ["metered"]
|
|
1364
|
-
},
|
|
1365
|
-
{
|
|
1366
|
-
key: "submeter-zone",
|
|
1367
|
-
role: "location",
|
|
1368
|
-
/* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
|
|
1369
|
-
level: "Other",
|
|
1370
|
-
label: "twin.type.submeter-zone",
|
|
1371
|
-
/*
|
|
1372
|
-
* 구역 계량 — 여러 부하를 한 계량기로 묶어 재는 자리(공조·조명처럼 개별 계량이 없는 것들).
|
|
1373
|
-
*
|
|
1374
|
-
* ISO 50001 의 **SEU**(유의 에너지 사용처)가 대개 이 알갱이다. 개별 설비까지 재지 못하는 현장이
|
|
1375
|
-
* 많고, 그것을 「모른다」로 두는 대신 **묶음으로 아는 것**이 정직하다.
|
|
1376
|
-
*/
|
|
1377
|
-
standardClass: { iec61850: "MMXU", iso50001: "SEU" },
|
|
1378
|
-
identity: { scheme: "kernel:id" },
|
|
1379
|
-
capabilities: ["metered"]
|
|
1380
|
-
},
|
|
1381
|
-
/* ── 설비: 계량 지점과 에너지 자원 ──────────────────────────────────────── */
|
|
1382
|
-
{
|
|
1383
|
-
key: "meter",
|
|
1384
|
-
role: "equipment",
|
|
1385
|
-
label: "twin.type.meter",
|
|
1386
|
-
/* 계량기 — 측정 논리 노드(MMXU=측정단위, MMTR=적산). 자산으로도 하나다(ISO 55000). */
|
|
1387
|
-
standardClass: { iec61850: "MMXU", iso55000: "Asset", iso50001: "MeasurementPoint" },
|
|
1388
|
-
identity: { scheme: "kernel:id" },
|
|
1389
|
-
capabilities: ["metered", "operable"]
|
|
1390
|
-
},
|
|
1391
|
-
{
|
|
1392
|
-
key: "breaker",
|
|
1393
|
-
role: "equipment",
|
|
1394
|
-
label: "twin.type.breaker",
|
|
1395
|
-
/*
|
|
1396
|
-
* 차단기 — IEC 61850 `XCBR`. **우리는 이것을 조작하지 않는다**(안전 계통은 범위 밖: ems.md §1).
|
|
1397
|
-
* 상태를 읽어 계통 구성을 알 뿐이다.
|
|
1398
|
-
*
|
|
1399
|
-
* 능력은 `switching` 이다 — `operable` 이 아니다(2026-08-14). `operable` 이던 동안 저작면이 차단기에
|
|
1400
|
-
* **제어 컴포넌트**를 권했다: 우리가 하지 않기로 한 조작을 화면이 부추긴 것이다. 「위치를 읽는다」와
|
|
1401
|
-
* 「명령을 받는다」는 다른 능력이고, 이제 그 둘이 갈려 있다(capability.ts 주석).
|
|
1402
|
-
*/
|
|
1403
|
-
standardClass: { iec61850: "XCBR", iso55000: "Asset" },
|
|
1404
|
-
identity: { scheme: "kernel:id" },
|
|
1405
|
-
capabilities: ["switching"]
|
|
1406
|
-
},
|
|
1407
|
-
{
|
|
1408
|
-
key: "pv-array",
|
|
1409
|
-
role: "equipment",
|
|
1410
|
-
label: "twin.type.pv-array",
|
|
1411
|
-
/* 태양광 어레이 — IEC 61850-7-420(분산자원)의 `DPVA`. 발전과 계량은 다른 능력이다. */
|
|
1412
|
-
standardClass: { iec61850: "DPVA", iso55000: "Asset", iso50001: "RenewableSupply" },
|
|
1413
|
-
identity: { scheme: "kernel:id" },
|
|
1414
|
-
capabilities: ["energyGenerating", "metered", "operable"]
|
|
1415
|
-
},
|
|
1416
|
-
{
|
|
1417
|
-
key: "battery",
|
|
1418
|
-
role: "equipment",
|
|
1419
|
-
label: "twin.type.battery",
|
|
1420
|
-
/* 축전지(ESS) — `ZBAT`. 충전·방전을 나눠 재고, 저장은 보관(`storable`)이 아니다(물건이 아니다). */
|
|
1421
|
-
standardClass: { iec61850: "ZBAT", iso55000: "Asset" },
|
|
1422
|
-
identity: { scheme: "kernel:id" },
|
|
1423
|
-
capabilities: ["energyStoring", "metered", "operable"]
|
|
1424
|
-
},
|
|
1425
|
-
{
|
|
1426
|
-
key: "utility",
|
|
1427
|
-
role: "equipment",
|
|
1428
|
-
label: "twin.type.utility",
|
|
1429
|
-
/*
|
|
1430
|
-
* 공통 설비 — 공조·컴프레서·칠러·조명·폐수처리처럼 **어느 공정에도 귀속되지 않는** 소비처.
|
|
1431
|
-
*
|
|
1432
|
-
* ── 왜 따로 있나 (2026-08-14) ────────────────────────────────────────────
|
|
1433
|
-
* 물류·생산 트윈에서 이런 것들은 **설비가 아니다**(공정에 매핑되지 않으므로 자원 축에 없다).
|
|
1434
|
-
* 그런데 에너지에서는 소비의 절반을 차지하고 감축 후보 1순위다 — 담을 자리가 반드시 있어야 한다.
|
|
1435
|
-
*
|
|
1436
|
-
* 처음에는 `curtailable-load` 하나로 받으려 했다. 그런데 그 이름은 **「줄일 수 있다」고 주장**한다:
|
|
1437
|
-
* 폐수처리·방폭 환기·서버실 냉방은 공통이지만 줄일 수 없고, 그것을 감축 가능으로 두면 트윈이
|
|
1438
|
-
* 「이걸 줄이면 됩니다」라는 거짓 제안을 한다. 공통성과 감축 가능성은 **다른 축**이다.
|
|
1439
|
-
*
|
|
1440
|
-
* 표준: IEC 61850 에 「공통 설비」라는 논리 노드는 없다(설비 종류마다 다른 노드다) — 비운다.
|
|
1441
|
-
* ISO 50001 의 SEU 는 이 부류를 가장 많이 가리킨다(유의 에너지 사용처).
|
|
1442
|
-
*/
|
|
1443
|
-
standardClass: { iso50001: "SEU", iso55000: "Asset" },
|
|
1444
|
-
identity: { scheme: "kernel:id" },
|
|
1445
|
-
capabilities: ["metered", "operable"]
|
|
1446
|
-
},
|
|
1447
|
-
{
|
|
1448
|
-
key: "curtailable-load",
|
|
1449
|
-
role: "equipment",
|
|
1450
|
-
label: "twin.type.curtailable-load",
|
|
1451
|
-
/*
|
|
1452
|
-
* 감축 가능 부하 — 줄일 수 있는 소비처(공조·충전기·비상시 미가동 라인).
|
|
1453
|
-
*
|
|
1454
|
-
* 표준에 이 이름은 없다: IEC 61850 은 설비를 종류로 부르고 「감축 가능」은 **운영 정책**이다.
|
|
1455
|
-
* 그래서 `iec61850` 칸을 비우고 ISO 50001 의 SEU 로만 대응한다 — 억지로 논리 노드를 적으면
|
|
1456
|
-
* 적합성 표가 거짓을 말한다.
|
|
1457
|
-
*/
|
|
1458
|
-
standardClass: { iso50001: "SEU" },
|
|
1459
|
-
identity: { scheme: "kernel:id" },
|
|
1460
|
-
capabilities: ["curtailable", "metered", "operable"]
|
|
1283
|
+
function itemClassOf(uri) {
|
|
1284
|
+
if (!uri)
|
|
1285
|
+
return void 0;
|
|
1286
|
+
const parsed = parseEpc(uri);
|
|
1287
|
+
if (parsed.scheme === "lgtin" && parsed.gtinKey)
|
|
1288
|
+
return `urn:epc:idpat:sgtin:${parsed.gtinKey}.*`;
|
|
1289
|
+
if (parsed.item !== void 0) {
|
|
1290
|
+
const marker = /^(.*(?:\/class\/|:class:))[^/:\s]+$/.exec(parsed.uri);
|
|
1291
|
+
if (marker)
|
|
1292
|
+
return `${marker[1]}${parsed.item.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c])}`;
|
|
1461
1293
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
if (!self)
|
|
1294
|
+
return parsed.uri;
|
|
1295
|
+
}
|
|
1296
|
+
function lotClassOfItem(itemClass, lot) {
|
|
1297
|
+
if (!itemClass)
|
|
1467
1298
|
return void 0;
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1299
|
+
const idpat = /^urn:epc:idpat:sgtin:(\d+)\.(\d+)\.\*$/.exec(itemClass);
|
|
1300
|
+
if (idpat)
|
|
1301
|
+
return lotClassUri({ namespace: "", item: "", lot, gtin: `${idpat[1]}.${idpat[2]}` });
|
|
1302
|
+
const parsed = parseEpc(itemClass);
|
|
1303
|
+
if (parsed.instance || parsed.lot !== void 0)
|
|
1472
1304
|
return void 0;
|
|
1473
|
-
|
|
1305
|
+
const url = /^(https?:\/\/.+)\/class\/([^/:\s]+)$/.exec(itemClass);
|
|
1306
|
+
const urn = /^(urn:.+):class:([^/:\s]+)$/.exec(itemClass);
|
|
1307
|
+
const at = url ?? urn;
|
|
1308
|
+
if (!at)
|
|
1309
|
+
return void 0;
|
|
1310
|
+
let item;
|
|
1311
|
+
try {
|
|
1312
|
+
item = decodeURIComponent(at[2]);
|
|
1313
|
+
} catch {
|
|
1314
|
+
return void 0;
|
|
1315
|
+
}
|
|
1316
|
+
return lotClassUri({ namespace: at[1], item, lot });
|
|
1474
1317
|
}
|
|
1475
|
-
function
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
// ../ops-contract/dist/domain-catalog.js
|
|
1480
|
-
var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
|
|
1481
|
-
var DOMAIN_CATALOG = {
|
|
1482
|
-
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
1483
|
-
wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
|
|
1484
|
-
yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
|
|
1485
|
-
mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) },
|
|
1486
|
-
ems: { system: "ems", label: "twin.system.ems", types: EMS_TYPES, locationTypes: locationKeys(EMS_TYPES) }
|
|
1487
|
-
};
|
|
1488
|
-
|
|
1489
|
-
// ../ops-contract/dist/domain-definition.js
|
|
1490
|
-
var OP_PARAM = {
|
|
1491
|
-
/** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
|
|
1492
|
-
yield: "yield",
|
|
1493
|
-
/** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
|
|
1494
|
-
* 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
|
|
1495
|
-
setupDuration: "setupDuration"
|
|
1496
|
-
};
|
|
1497
|
-
function procedureViolations(route) {
|
|
1498
|
-
const elements = route.procedure ?? [];
|
|
1499
|
-
if (!elements.length)
|
|
1500
|
-
return [];
|
|
1501
|
-
const errors = [];
|
|
1502
|
-
const byKey = new Map(elements.map((e) => [e.key, e]));
|
|
1503
|
-
const hasChild = new Set(elements.map((e) => e.parent).filter((k) => !!k));
|
|
1504
|
-
const stepSet = new Set(route.steps ?? []);
|
|
1505
|
-
const seenStep = /* @__PURE__ */ new Map();
|
|
1506
|
-
for (const n of elements) {
|
|
1507
|
-
if (n.parent && !byKey.has(n.parent))
|
|
1508
|
-
errors.push(`${n.key}: \uC0C1\uC704 '${n.parent}' \uAC00 \uC774 \uC808\uCC28\uC5D0 \uC5C6\uB2E4`);
|
|
1509
|
-
if (n.step === void 0)
|
|
1510
|
-
continue;
|
|
1511
|
-
if (hasChild.has(n.key))
|
|
1512
|
-
errors.push(`${n.key}: \uC544\uB798 \uC694\uC18C\uAC00 \uC788\uB294\uB370 \uB2E8\uACC4\uB97C \uB4E0\uB2E4 \u2014 \uC2E4\uD589\uB418\uB294 \uAC83\uC740 \uC78E\uBFD0\uC774\uB2E4`);
|
|
1513
|
-
if (!stepSet.has(n.step))
|
|
1514
|
-
errors.push(`${n.key}: \uBAA8\uB974\uB294 \uB2E8\uACC4 '${n.step}' \u2014 steps \uC5D0 \uC5C6\uB2E4`);
|
|
1515
|
-
const already = seenStep.get(n.step);
|
|
1516
|
-
if (already)
|
|
1517
|
-
errors.push(`\uB2E8\uACC4 '${n.step}' \uAC00 '${already}' \uC640 '${n.key}' \uB458\uC5D0 \uC2E4\uB838\uB2E4 \u2014 \uC808\uCC28 \uC5B4\uB514\uC778\uC9C0 \uB9D0\uD560 \uC218 \uC5C6\uB2E4`);
|
|
1518
|
-
else
|
|
1519
|
-
seenStep.set(n.step, n.key);
|
|
1520
|
-
}
|
|
1521
|
-
return errors;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
// ../ops-contract/dist/energy-ingest.js
|
|
1525
|
-
function resolveSubject(kind, localId, opts) {
|
|
1526
|
-
const declared = opts.identityOf?.(kind, localId);
|
|
1527
|
-
if (declared)
|
|
1528
|
-
return { subject: declared, basis: "declared" };
|
|
1529
|
-
return { subject: opts.scopeId ? `${opts.scopeId}/${localId}` : localId, basis: "twin-local" };
|
|
1530
|
-
}
|
|
1531
|
-
function periodFactId(tenantId, kind, subject, from, to) {
|
|
1532
|
-
return [tenantId, kind, subject, from, to].join("|");
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// ../ops-contract/dist/epcis.js
|
|
1536
|
-
var EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
|
|
1537
|
-
var UTC_OFFSET = "+00:00";
|
|
1538
|
-
var DISP = {
|
|
1539
|
-
in_progress: "urn:epcglobal:cbv:disp:in_progress",
|
|
1540
|
-
sellable: "urn:epcglobal:cbv:disp:sellable_accessible",
|
|
1541
|
-
reserved: "urn:epcglobal:cbv:disp:reserved",
|
|
1542
|
-
in_transit: "urn:epcglobal:cbv:disp:in_transit",
|
|
1543
|
-
non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other",
|
|
1544
|
-
// 불량/scrap
|
|
1545
|
-
/**
|
|
1546
|
-
* **기한이 지났다** — CBV `expired`.
|
|
1547
|
-
*
|
|
1548
|
-
* ── 왜 `non_sellable` 로 계산하지 않나 (2026-08-24) ─────────────────────────────
|
|
1549
|
-
* 커널의 `non_sellable` 은 CBV 의 `non_sellable_other`, 즉 **「그 밖의 이유」**다. 기한 지남을 거기
|
|
1550
|
-
* 넣으면 「기한이 지나 못 판다」와 「깨져서 못 판다」가 같은 값이 되고, 화면은 회수·폐기의 사유를
|
|
1551
|
-
* 구별할 수 없다. 식품에서 그 둘은 다른 조치다.
|
|
1552
|
-
*
|
|
1553
|
-
* 그리고 표준에 **정확한 낱말이 있다** — 합치는 것은 있는 낱말을 버리는 것이다.
|
|
1554
|
-
*
|
|
1555
|
-
* ── 기한 날짜와 다른 축이다 ────────────────────────────────────────────────
|
|
1556
|
-
* `ItemState.expiry` 는 **날짜**이고 이것은 **상태**다. 날짜가 있으면 「지났나」는 파생이지만, 원본이
|
|
1557
|
-
* 「기한 지남」을 상태로 선언하는 시스템이 있다 — 그때 이 값은 관측이다.
|
|
1558
|
-
*
|
|
1559
|
-
* 둘이 어긋나면(날짜는 남았는데 상태가 지남, 또는 그 반대) **어느 쪽이 맞다고 정하지 않는다** —
|
|
1560
|
-
* 아직 그 판정을 세울 근거가 없다. 어긋남의 구분을 없애지 않는 것이 지금의 규율이다.
|
|
1561
|
-
*
|
|
1562
|
-
* ── 원문으로 확인했다 (2026-08-24) ────────────────────────────────────────
|
|
1563
|
-
* 1차 출처: **CBV Standard Release 2.0, Ratified Jun 2022** §7.2.3 처분 값 표(38개). 이 객체의
|
|
1564
|
-
* 다른 값들(`in_progress`·`sellable_accessible`·`reserved`·`in_transit`·`non_sellable_other`)도
|
|
1565
|
-
* 그 표에 있다.
|
|
1566
|
-
*
|
|
1567
|
-
* **`non_sellable_expired` 를 쓰지 않는 이유**: 그 값은 CBV 1.0 의 것이고 표준이 **폐기**했다 —
|
|
1568
|
-
* 「deprecated in favour of new disposition values expired, damaged, disposed, … introduced in
|
|
1569
|
-
* CBV 1.1」. 폐기된 값을 쓰면 새 소비처가 읽지 못한다.
|
|
1570
|
-
*
|
|
1571
|
-
* 참고: GS1 어휘 등록처(`ref.gs1.org/cbv/…`)로는 확인할 수 없었다 — **없는 값에도 같은 응답**을
|
|
1572
|
-
* 준다(지어낸 값의 JSON-LD 가 실재 값과 바이트까지 같았다). 그 경로를 근거로 삼지 말 것.
|
|
1573
|
-
*/
|
|
1574
|
-
expired: "urn:epcglobal:cbv:disp:expired",
|
|
1575
|
-
/**
|
|
1576
|
-
* **검사에 합격했다 / 불합격했다** — CBV `conformant` / `non_conformant`.
|
|
1577
|
-
*
|
|
1578
|
-
* 1차 출처(CBV 2.0 §7.2.3) 정의 그대로다.
|
|
1579
|
-
*
|
|
1580
|
-
* conformant Outcome of a successful/passed inspection in an inspecting or repairing step
|
|
1581
|
-
* non_conformant Outcome of an unsuccessful/failed inspection in an inspecting or repairing step
|
|
1582
|
-
*
|
|
1583
|
-
* ── 왜 시험 결과 축을 자원에 더하지 않고 이것을 쓰나 (2026-08-24) ────────────
|
|
1584
|
-
* 로트의 검사 판정을 담을 자리를 찾다가 `ItemState.testResults` 를 더하려 했다. 그런데 표준은 그
|
|
1585
|
-
* 사실을 **이미 처분으로 말한다**: `bizStep: inspecting` 사건에 이 처분이 붙는다.
|
|
1586
|
-
*
|
|
1587
|
-
* 처분을 쓰면 두 가지가 공짜로 성립한다.
|
|
1588
|
-
* ① **상태 ⊆ 이벤트** — 처분은 이미 사건에서 온다. 상태에만 있는 축을 만들지 않는다
|
|
1589
|
-
* ② **운영에 곧 닿는다** — 「이 자재를 쓸 수 있나」가 처분으로 답해진다(판정을 따로 읽지 않는다)
|
|
1590
|
-
*
|
|
1591
|
-
* 시험의 **자세한 내용**(어느 명세로, 무엇을 재어)은 다른 물음이고, 표준은 그것을 `TestResult` 로
|
|
1592
|
-
* 두며 결과가 대상을 가리킨다(`TestableObjectID`) — 대상이 결과를 들지 않는다. 그 축이 필요해지면
|
|
1593
|
-
* 그때 열되, **판정 자체는 여기서 끝난다.**
|
|
1594
|
-
*/
|
|
1595
|
-
conformant: "urn:epcglobal:cbv:disp:conformant",
|
|
1596
|
-
non_conformant: "urn:epcglobal:cbv:disp:non_conformant"
|
|
1597
|
-
};
|
|
1598
|
-
var CBV_BIZSTEP = {
|
|
1599
|
-
/** 공정에 자재가 들어갔다 — ISA-95 `MaterialUse: Consumed`. */
|
|
1600
|
-
consuming: "urn:epcglobal:cbv:bizstep:consuming",
|
|
1601
|
-
/** 새 물품이 생겨 계보가 시작된다 — ISA-95 `MaterialUse: Produced`. */
|
|
1602
|
-
commissioning: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
1603
|
-
/**
|
|
1604
|
-
* **검사** — CBV `inspecting`. 1차 출처(CBV 2.0) 정의: 「Process of reviewing objects to address
|
|
1605
|
-
* potential physical or documentation defects」이고, 「표본과 달리 검사된 대상은 그대로 남는다」고
|
|
1606
|
-
* 이어진다(즉 검사는 물건을 소비하지 않는다).
|
|
1607
|
-
*
|
|
1608
|
-
* 이 단계에 `DISP.conformant`/`DISP.non_conformant` 가 붙어 판정이 처분으로 남는다 — 입고검수·
|
|
1609
|
-
* 공정 중 검사가 그 모양이다.
|
|
1610
|
-
*/
|
|
1611
|
-
inspecting: "urn:epcglobal:cbv:bizstep:inspecting",
|
|
1612
|
-
/**
|
|
1613
|
-
* **어느 단계로도 이름 붙지 않는 활동** — 원문: 「A business step not identified by any other」.
|
|
1614
|
-
*
|
|
1615
|
-
* 이것은 근사가 아니라 **표준이 준 낱말**이다. 그 구별이 중요하다: 재고 조정처럼 한 낱말이 두 일을
|
|
1616
|
-
* 하는 원천(세어 보고 맞춘 것 · 사람이 정정한 것)을 `cycle_counting` 으로 옮기면 **일어나지 않은
|
|
1617
|
-
* 계수를 기록**하게 된다. 그때 쓰는 것이 이 값이고, 무슨 일이었는지는 값으로 함께 나른다.
|
|
1618
|
-
*
|
|
1619
|
-
* 원문 확인: CBV Standard Release 2.0(Ratified Jun 2022) §7.1.
|
|
1620
|
-
*/
|
|
1621
|
-
other: "urn:epcglobal:cbv:bizstep:other"
|
|
1622
|
-
};
|
|
1623
|
-
function sgtinClass(companyPrefix, itemRef) {
|
|
1624
|
-
return `urn:epc:idpat:sgtin:${companyPrefix}.${itemRef}.*`;
|
|
1625
|
-
}
|
|
1626
|
-
var ILMD_ATTR = {
|
|
1627
|
-
/** 유통기한·만료(로트 단위). */
|
|
1628
|
-
expiry: "cbvmda:itemExpirationDate",
|
|
1629
|
-
/** 로트·배치 번호(직렬 개체에 로트를 붙일 때). */
|
|
1630
|
-
lot: "cbvmda:lotNumber"
|
|
1631
|
-
};
|
|
1632
|
-
function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
|
|
1633
|
-
return `urn:epc:class:lgtin:${companyPrefix}.${itemRefAndIndicator}.${encodeURIComponent(lot)}`;
|
|
1634
|
-
}
|
|
1635
|
-
function parseEpc(uri) {
|
|
1636
|
-
const raw = String(uri ?? "");
|
|
1637
|
-
if (/(?:\/obj\/|:obj:)[^/:\s]+$/.test(raw))
|
|
1638
|
-
return { scheme: "unknown", instance: true, uri: raw };
|
|
1639
|
-
const inHouseClass = raw.match(/(?:\/class\/|:class:)([^/:\s]+)$/);
|
|
1640
|
-
if (inHouseClass) {
|
|
1641
|
-
const at = inHouseClass[1].indexOf(LOT_SEPARATOR);
|
|
1642
|
-
const split = at > 0 ? decodedPair(inHouseClass[1].slice(0, at), inHouseClass[1].slice(at + LOT_SEPARATOR.length)) : void 0;
|
|
1643
|
-
return split ? { scheme: "unknown", instance: false, item: split[0], lot: split[1], uri: raw } : { scheme: "unknown", instance: false, uri: raw };
|
|
1644
|
-
}
|
|
1645
|
-
const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
|
|
1646
|
-
if (cls) {
|
|
1647
|
-
const seg = cls[1].split(".");
|
|
1648
|
-
const lot = seg.slice(2).join(".");
|
|
1649
|
-
return {
|
|
1650
|
-
scheme: "lgtin",
|
|
1651
|
-
instance: false,
|
|
1652
|
-
gtinKey: seg.slice(0, 2).join("."),
|
|
1653
|
-
lot: lot ? decodeURIComponent(lot) : void 0,
|
|
1654
|
-
uri: raw
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
const pat = raw.match(/^urn:epc:idpat:sgtin:(.+)$/);
|
|
1658
|
-
if (pat) {
|
|
1659
|
-
const seg = pat[1].split(".");
|
|
1660
|
-
return { scheme: "idpat", instance: false, gtinKey: seg.slice(0, 2).join("."), uri: raw };
|
|
1661
|
-
}
|
|
1662
|
-
const id = raw.match(/^urn:epc:id:([a-z]+):(.+)$/);
|
|
1663
|
-
if (id) {
|
|
1664
|
-
const scheme = id[1];
|
|
1665
|
-
const seg = id[2].split(".");
|
|
1666
|
-
const known = ["sgtin", "sscc", "gdti", "grai", "giai", "sgln"].includes(scheme);
|
|
1667
|
-
return {
|
|
1668
|
-
scheme: known ? scheme : "unknown",
|
|
1669
|
-
instance: true,
|
|
1670
|
-
...scheme === "sgtin" ? { gtinKey: seg.slice(0, 2).join("."), serial: seg[2] } : { serial: seg.slice(1).join(".") },
|
|
1671
|
-
uri: raw
|
|
1672
|
-
};
|
|
1673
|
-
}
|
|
1674
|
-
return { scheme: "unknown", instance: false, uri: raw };
|
|
1675
|
-
}
|
|
1676
|
-
function decodedPair(item, lot) {
|
|
1677
|
-
if (!lot)
|
|
1678
|
-
return void 0;
|
|
1679
|
-
try {
|
|
1680
|
-
return [decodeURIComponent(item), decodeURIComponent(lot)];
|
|
1681
|
-
} catch {
|
|
1682
|
-
return void 0;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
function itemClassOf(uri) {
|
|
1686
|
-
if (!uri)
|
|
1687
|
-
return void 0;
|
|
1688
|
-
const parsed = parseEpc(uri);
|
|
1689
|
-
if (parsed.scheme === "lgtin" && parsed.gtinKey)
|
|
1690
|
-
return `urn:epc:idpat:sgtin:${parsed.gtinKey}.*`;
|
|
1691
|
-
if (parsed.item !== void 0) {
|
|
1692
|
-
const marker = /^(.*(?:\/class\/|:class:))[^/:\s]+$/.exec(parsed.uri);
|
|
1693
|
-
if (marker)
|
|
1694
|
-
return `${marker[1]}${parsed.item.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c])}`;
|
|
1695
|
-
}
|
|
1696
|
-
return parsed.uri;
|
|
1697
|
-
}
|
|
1698
|
-
function lotClassOfItem(itemClass, lot) {
|
|
1699
|
-
if (!itemClass)
|
|
1700
|
-
return void 0;
|
|
1701
|
-
const idpat = /^urn:epc:idpat:sgtin:(\d+)\.(\d+)\.\*$/.exec(itemClass);
|
|
1702
|
-
if (idpat)
|
|
1703
|
-
return lotClassUri({ namespace: "", item: "", lot, gtin: `${idpat[1]}.${idpat[2]}` });
|
|
1704
|
-
const parsed = parseEpc(itemClass);
|
|
1705
|
-
if (parsed.instance || parsed.lot !== void 0)
|
|
1706
|
-
return void 0;
|
|
1707
|
-
const url = /^(https?:\/\/.+)\/class\/([^/:\s]+)$/.exec(itemClass);
|
|
1708
|
-
const urn = /^(urn:.+):class:([^/:\s]+)$/.exec(itemClass);
|
|
1709
|
-
const at = url ?? urn;
|
|
1710
|
-
if (!at)
|
|
1711
|
-
return void 0;
|
|
1712
|
-
let item;
|
|
1713
|
-
try {
|
|
1714
|
-
item = decodeURIComponent(at[2]);
|
|
1715
|
-
} catch {
|
|
1716
|
-
return void 0;
|
|
1717
|
-
}
|
|
1718
|
-
return lotClassUri({ namespace: at[1], item, lot });
|
|
1719
|
-
}
|
|
1720
|
-
function sameItemClass(a, b) {
|
|
1721
|
-
const x = itemClassOf(a);
|
|
1722
|
-
return x !== void 0 && x === itemClassOf(b);
|
|
1318
|
+
function sameItemClass(a, b) {
|
|
1319
|
+
const x = itemClassOf(a);
|
|
1320
|
+
return x !== void 0 && x === itemClassOf(b);
|
|
1723
1321
|
}
|
|
1724
1322
|
function gdtiUri(companyPrefix, docType, serial) {
|
|
1725
1323
|
return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
|
|
@@ -1743,170 +1341,612 @@ function lotClassUri(input) {
|
|
|
1743
1341
|
throw new Error(`lotClassUri: ${wrong}`);
|
|
1744
1342
|
return uri;
|
|
1745
1343
|
}
|
|
1746
|
-
if (!item)
|
|
1747
|
-
throw new Error(`lotClassUri: \uD488\uBC88\uC774 \uBE44\uC5C8\uB2E4 (lot=${lot})`);
|
|
1748
|
-
const enc = (v) => v.replace(LOT_COMPONENT_BREAKING, (c) => PERCENT_ENCODED[c]);
|
|
1749
|
-
return joinUnderNamespace(input.namespace, "class", `${enc(item)}${LOT_SEPARATOR}${enc(lot)}`);
|
|
1750
|
-
}
|
|
1751
|
-
var LOT_SEPARATOR = ";lot=";
|
|
1752
|
-
var LOT_COMPONENT_BREAKING = /[%/:?# ;]/g;
|
|
1753
|
-
function bizTransactionUri(namespace, transId) {
|
|
1754
|
-
return underNamespace(namespace, "bt", transId);
|
|
1755
|
-
}
|
|
1756
|
-
var STRUCTURE_BREAKING = /[%/:?# ]/g;
|
|
1757
|
-
var PERCENT_ENCODED = {
|
|
1758
|
-
"%": "%25",
|
|
1759
|
-
"/": "%2F",
|
|
1760
|
-
":": "%3A",
|
|
1761
|
-
"?": "%3F",
|
|
1762
|
-
"#": "%23",
|
|
1763
|
-
" ": "%20",
|
|
1764
|
-
/* 로트 클래스의 가르는 글자 — `lotClassUri` 만 쓴다(§`LOT_COMPONENT_BREAKING`). 표는 한 벌이다. */
|
|
1765
|
-
";": "%3B"
|
|
1766
|
-
};
|
|
1767
|
-
function underNamespace(namespace, marker, id) {
|
|
1768
|
-
const raw = String(id);
|
|
1769
|
-
if (!raw)
|
|
1770
|
-
return void 0;
|
|
1771
|
-
return joinUnderNamespace(namespace, marker, raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]));
|
|
1772
|
-
}
|
|
1773
|
-
function joinUnderNamespace(namespace, marker, v) {
|
|
1774
|
-
const ns = namespace?.trim();
|
|
1775
|
-
if (!ns)
|
|
1344
|
+
if (!item)
|
|
1345
|
+
throw new Error(`lotClassUri: \uD488\uBC88\uC774 \uBE44\uC5C8\uB2E4 (lot=${lot})`);
|
|
1346
|
+
const enc = (v) => v.replace(LOT_COMPONENT_BREAKING, (c) => PERCENT_ENCODED[c]);
|
|
1347
|
+
return joinUnderNamespace(input.namespace, "class", `${enc(item)}${LOT_SEPARATOR}${enc(lot)}`);
|
|
1348
|
+
}
|
|
1349
|
+
var LOT_SEPARATOR = ";lot=";
|
|
1350
|
+
var LOT_COMPONENT_BREAKING = /[%/:?# ;]/g;
|
|
1351
|
+
function bizTransactionUri(namespace, transId) {
|
|
1352
|
+
return underNamespace(namespace, "bt", transId);
|
|
1353
|
+
}
|
|
1354
|
+
var STRUCTURE_BREAKING = /[%/:?# ]/g;
|
|
1355
|
+
var PERCENT_ENCODED = {
|
|
1356
|
+
"%": "%25",
|
|
1357
|
+
"/": "%2F",
|
|
1358
|
+
":": "%3A",
|
|
1359
|
+
"?": "%3F",
|
|
1360
|
+
"#": "%23",
|
|
1361
|
+
" ": "%20",
|
|
1362
|
+
/* 로트 클래스의 가르는 글자 — `lotClassUri` 만 쓴다(§`LOT_COMPONENT_BREAKING`). 표는 한 벌이다. */
|
|
1363
|
+
";": "%3B"
|
|
1364
|
+
};
|
|
1365
|
+
function underNamespace(namespace, marker, id) {
|
|
1366
|
+
const raw = String(id);
|
|
1367
|
+
if (!raw)
|
|
1368
|
+
return void 0;
|
|
1369
|
+
return joinUnderNamespace(namespace, marker, raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]));
|
|
1370
|
+
}
|
|
1371
|
+
function joinUnderNamespace(namespace, marker, v) {
|
|
1372
|
+
const ns = namespace?.trim();
|
|
1373
|
+
if (!ns)
|
|
1374
|
+
return void 0;
|
|
1375
|
+
if (/^https?:\/\/[^/\s]+/.test(ns))
|
|
1376
|
+
return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
|
|
1377
|
+
if (/^urn:epc(global)?:/.test(ns))
|
|
1378
|
+
return void 0;
|
|
1379
|
+
if (/^urn:[^:\s]+/.test(ns))
|
|
1380
|
+
return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
|
|
1381
|
+
return void 0;
|
|
1382
|
+
}
|
|
1383
|
+
function header(type, eventTime, bizStep, opts) {
|
|
1384
|
+
const h = {
|
|
1385
|
+
"@context": EPCIS_CONTEXT,
|
|
1386
|
+
type,
|
|
1387
|
+
eventTime,
|
|
1388
|
+
eventTimeZoneOffset: UTC_OFFSET,
|
|
1389
|
+
bizStep
|
|
1390
|
+
};
|
|
1391
|
+
if (opts?.eventID)
|
|
1392
|
+
h.eventID = opts.eventID;
|
|
1393
|
+
if (opts?.recordTime)
|
|
1394
|
+
h.recordTime = opts.recordTime;
|
|
1395
|
+
if (opts?.errorDeclaration)
|
|
1396
|
+
h.errorDeclaration = opts.errorDeclaration;
|
|
1397
|
+
if (opts?.bizTransactionList?.length)
|
|
1398
|
+
h.bizTransactionList = opts.bizTransactionList;
|
|
1399
|
+
if (opts?.ilmd)
|
|
1400
|
+
h.ilmd = opts.ilmd;
|
|
1401
|
+
if (opts?.sourceList)
|
|
1402
|
+
h.sourceList = opts.sourceList;
|
|
1403
|
+
if (opts?.destinationList)
|
|
1404
|
+
h.destinationList = opts.destinationList;
|
|
1405
|
+
if (opts?.persistentDisposition)
|
|
1406
|
+
h.persistentDisposition = opts.persistentDisposition;
|
|
1407
|
+
if (opts?.sensorElementList)
|
|
1408
|
+
h.sensorElementList = opts.sensorElementList;
|
|
1409
|
+
if (opts?.certificationInfo)
|
|
1410
|
+
h.certificationInfo = opts.certificationInfo;
|
|
1411
|
+
return h;
|
|
1412
|
+
}
|
|
1413
|
+
function common(type, eventTime, action, bizStep, opts) {
|
|
1414
|
+
return { ...header(type, eventTime, bizStep, opts), action };
|
|
1415
|
+
}
|
|
1416
|
+
function objectEvent(p) {
|
|
1417
|
+
const e = { ...common("ObjectEvent", p.eventTime, p.action, p.bizStep, p), epcList: p.epcList };
|
|
1418
|
+
if (p.disposition)
|
|
1419
|
+
e.disposition = p.disposition;
|
|
1420
|
+
if (p.quantityList)
|
|
1421
|
+
e.quantityList = p.quantityList;
|
|
1422
|
+
if (p.readPoint)
|
|
1423
|
+
e.readPoint = { id: p.readPoint };
|
|
1424
|
+
if (p.bizLocation)
|
|
1425
|
+
e.bizLocation = { id: p.bizLocation };
|
|
1426
|
+
if (p.bizTransactionList)
|
|
1427
|
+
e.bizTransactionList = p.bizTransactionList;
|
|
1428
|
+
return e;
|
|
1429
|
+
}
|
|
1430
|
+
function aggregationEvent(p) {
|
|
1431
|
+
const e = { ...common("AggregationEvent", p.eventTime, p.action, p.bizStep, p), parentID: p.parentID };
|
|
1432
|
+
if (p.disposition)
|
|
1433
|
+
e.disposition = p.disposition;
|
|
1434
|
+
if (p.childEPCs)
|
|
1435
|
+
e.childEPCs = p.childEPCs;
|
|
1436
|
+
if (p.childQuantityList)
|
|
1437
|
+
e.childQuantityList = p.childQuantityList;
|
|
1438
|
+
if (p.readPoint)
|
|
1439
|
+
e.readPoint = { id: p.readPoint };
|
|
1440
|
+
if (p.bizLocation)
|
|
1441
|
+
e.bizLocation = { id: p.bizLocation };
|
|
1442
|
+
return e;
|
|
1443
|
+
}
|
|
1444
|
+
function transactionEvent(p) {
|
|
1445
|
+
const e = {
|
|
1446
|
+
...common("TransactionEvent", p.eventTime, p.action, p.bizStep, p),
|
|
1447
|
+
bizTransactionList: p.bizTransactionList
|
|
1448
|
+
};
|
|
1449
|
+
if (p.disposition)
|
|
1450
|
+
e.disposition = p.disposition;
|
|
1451
|
+
if (p.parentID)
|
|
1452
|
+
e.parentID = p.parentID;
|
|
1453
|
+
if (p.epcList)
|
|
1454
|
+
e.epcList = p.epcList;
|
|
1455
|
+
if (p.quantityList)
|
|
1456
|
+
e.quantityList = p.quantityList;
|
|
1457
|
+
if (p.readPoint)
|
|
1458
|
+
e.readPoint = { id: p.readPoint };
|
|
1459
|
+
if (p.bizLocation)
|
|
1460
|
+
e.bizLocation = { id: p.bizLocation };
|
|
1461
|
+
return e;
|
|
1462
|
+
}
|
|
1463
|
+
function transformationEvent(p) {
|
|
1464
|
+
const e = header("TransformationEvent", p.eventTime, p.bizStep, p);
|
|
1465
|
+
if (p.disposition)
|
|
1466
|
+
e.disposition = p.disposition;
|
|
1467
|
+
if (p.inputEPCList)
|
|
1468
|
+
e.inputEPCList = p.inputEPCList;
|
|
1469
|
+
if (p.inputQuantityList)
|
|
1470
|
+
e.inputQuantityList = p.inputQuantityList;
|
|
1471
|
+
if (p.outputEPCList)
|
|
1472
|
+
e.outputEPCList = p.outputEPCList;
|
|
1473
|
+
if (p.outputQuantityList)
|
|
1474
|
+
e.outputQuantityList = p.outputQuantityList;
|
|
1475
|
+
if (p.transformationID)
|
|
1476
|
+
e.transformationID = p.transformationID;
|
|
1477
|
+
if (p.readPoint)
|
|
1478
|
+
e.readPoint = { id: p.readPoint };
|
|
1479
|
+
if (p.bizLocation)
|
|
1480
|
+
e.bizLocation = { id: p.bizLocation };
|
|
1481
|
+
if (p.bizTransactionList)
|
|
1482
|
+
e.bizTransactionList = p.bizTransactionList;
|
|
1483
|
+
return e;
|
|
1484
|
+
}
|
|
1485
|
+
var GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
|
|
1486
|
+
function gs1KeyDigitViolation(uri) {
|
|
1487
|
+
if (!uri)
|
|
1488
|
+
return void 0;
|
|
1489
|
+
const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
|
|
1490
|
+
if (!m)
|
|
1491
|
+
return void 0;
|
|
1492
|
+
const want = GS1_KEY_DIGITS[m[1]];
|
|
1493
|
+
if (!want)
|
|
1494
|
+
return void 0;
|
|
1495
|
+
const parts = m[2].split(".");
|
|
1496
|
+
if (parts.length < 2)
|
|
1497
|
+
return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uAC00 '.' \uB85C \uAC08\uB824\uC57C \uD55C\uB2E4`;
|
|
1498
|
+
const [prefix, ref] = parts;
|
|
1499
|
+
if (ref === "*")
|
|
1500
|
+
return void 0;
|
|
1501
|
+
if (!/^\d+$/.test(prefix) || !/^\d+$/.test(ref)) {
|
|
1502
|
+
return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uB294 \uC22B\uC790\uB2E4`;
|
|
1503
|
+
}
|
|
1504
|
+
const got = prefix.length + ref.length;
|
|
1505
|
+
if (got === want)
|
|
1506
|
+
return void 0;
|
|
1507
|
+
return `${m[1]} \uC790\uB9AC \uC218 \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4(${prefix.length}) + \uCC38\uC870(${ref.length}) = ${got} \uC774\uC9C0\uB9CC ${want} \uC5EC\uC57C \uD55C\uB2E4(GS1 TDS). \uD504\uB9AC\uD53D\uC2A4\uB97C \uBC14\uAFB8\uBA74 \uCC38\uC870 \uC790\uB9AC \uC218\uB97C \uD568\uAED8 \uB9DE\uCDB0\uC57C \uD55C\uB2E4.`;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// ../ops-contract/dist/yms-profile.js
|
|
1511
|
+
var YARD_BIZSTEP = {
|
|
1512
|
+
arriving: "urn:epcglobal:cbv:bizstep:arriving",
|
|
1513
|
+
// 게이트-인
|
|
1514
|
+
loading: "urn:epcglobal:cbv:bizstep:loading",
|
|
1515
|
+
// 도크 작업(상차: 트레일러←화물)
|
|
1516
|
+
unloading: "urn:epcglobal:cbv:bizstep:unloading",
|
|
1517
|
+
// 도크 작업(하차: 트레일러→화물)
|
|
1518
|
+
departing: "urn:epcglobal:cbv:bizstep:departing"
|
|
1519
|
+
// 게이트-아웃
|
|
1520
|
+
};
|
|
1521
|
+
var YARD_PRIVATE_BIZSTEP = {
|
|
1522
|
+
staging: `${HATIOLAB_VOCABULARY}bizstep/staging`
|
|
1523
|
+
// 야드 슬롯 주차·대기
|
|
1524
|
+
};
|
|
1525
|
+
var YMS_LOCATION_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
|
|
1526
|
+
var YMS_LOCATION_LEVEL = {
|
|
1527
|
+
gate: "Other",
|
|
1528
|
+
"yard-slot": "StorageUnit",
|
|
1529
|
+
"dock-door": "StorageUnit",
|
|
1530
|
+
staging: "StorageZone"
|
|
1531
|
+
};
|
|
1532
|
+
var YMS_TYPES = [
|
|
1533
|
+
...YMS_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, level: YMS_LOCATION_LEVEL[k], capabilities: ["storable"] })),
|
|
1534
|
+
{ key: "hostler", role: "equipment", label: "twin.type.hostler", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
1535
|
+
];
|
|
1536
|
+
|
|
1537
|
+
// ../ops-contract/dist/mes-profile.js
|
|
1538
|
+
var MES_BIZSTEP = {
|
|
1539
|
+
receiving: "urn:epcglobal:cbv:bizstep:receiving",
|
|
1540
|
+
// 원자재 수령
|
|
1541
|
+
producing: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
1542
|
+
// 생산(제품 최초 생성)
|
|
1543
|
+
storing: "urn:epcglobal:cbv:bizstep:storing"
|
|
1544
|
+
// 완제품 저장
|
|
1545
|
+
};
|
|
1546
|
+
var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
1547
|
+
function sgtinUri(companyPrefix, itemRef, serial) {
|
|
1548
|
+
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
1549
|
+
}
|
|
1550
|
+
var MES_LOCATION_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
1551
|
+
var MES_LOCATION_CLS = {
|
|
1552
|
+
"raw-store": { epcis: "bizLocation" },
|
|
1553
|
+
"cut-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1554
|
+
"weld-station": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1555
|
+
"paint-booth": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1556
|
+
"assembly-line": { isa95: "WorkCenter", epcis: "bizLocation" },
|
|
1557
|
+
"fg-store": { epcis: "bizLocation" }
|
|
1558
|
+
};
|
|
1559
|
+
var MES_LOCATION_LEVEL = {
|
|
1560
|
+
"raw-store": "StorageZone",
|
|
1561
|
+
"cut-station": "WorkCell",
|
|
1562
|
+
"weld-station": "WorkCell",
|
|
1563
|
+
"paint-booth": "WorkCell",
|
|
1564
|
+
"assembly-line": "ProductionLine",
|
|
1565
|
+
"fg-store": "StorageZone"
|
|
1566
|
+
};
|
|
1567
|
+
var MES_TYPES = [
|
|
1568
|
+
...MES_LOCATION_TYPES.map((k) => ({ key: k, role: "location", label: `twin.type.${k}`, standardClass: MES_LOCATION_CLS[k] ?? {}, identity: { scheme: "gs1:SGLN" }, level: MES_LOCATION_LEVEL[k], capabilities: ["storable"] })),
|
|
1569
|
+
{ key: "cutter", role: "equipment", label: "twin.type.cutter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1570
|
+
{ key: "welder", role: "equipment", label: "twin.type.welder", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1571
|
+
{ key: "painter", role: "equipment", label: "twin.type.painter", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
1572
|
+
{ key: "assembler", role: "equipment", label: "twin.type.assembler", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
1573
|
+
];
|
|
1574
|
+
|
|
1575
|
+
// ../ops-contract/dist/ems-profile.js
|
|
1576
|
+
var EMS_LOCATION_TYPES = ["incoming", "feeder", "submeter-zone"];
|
|
1577
|
+
var METER_DIRECTION = ["import", "export", "bidirectional"];
|
|
1578
|
+
var EMS_PROPERTY = {
|
|
1579
|
+
/** 계약전력(kW) — 수전·분기 자리에 선언한다. 없으면 계약 대비 판정을 하지 않는다. */
|
|
1580
|
+
contractKW: "contract.kW",
|
|
1581
|
+
/*
|
|
1582
|
+
* ── 계량 지점의 방향 (2026-08-25) ──────────────────────────────────────────
|
|
1583
|
+
* **요금은 계통 접속점의 순 유입에 매겨진다.** 그러니 그 지점의 계량기가 무엇을 재는지 모르면
|
|
1584
|
+
* 요금을 사실대로 말할 수 없다 — 내보낸 양을 쓴 양으로 세면 계약 대비 판단이 반대로 뒤집힌다.
|
|
1585
|
+
*
|
|
1586
|
+
* 이 축이 없는 동안 계량 지점의 kW 를 전부 부하로 셌다. 상계 거래를 하는 현장, 축전지를 붙인 현장,
|
|
1587
|
+
* 열병합이 있는 현장 — 접속점 계량기가 양쪽을 재는 곳은 늘 있다.
|
|
1588
|
+
*
|
|
1589
|
+
* **발전기를 계량기로 만드는 것과 다른 일이다.** 발전은 설비의 사실이고(`energyGenerating`), 그것을
|
|
1590
|
+
* 계량 지점으로 보내는 것은 여전히 하지 않는다. 이 축은 **접속점 계량기**가 무엇을 재는지 말하는
|
|
1591
|
+
* 자리다.
|
|
1592
|
+
*
|
|
1593
|
+
* 방향은 **관측이 아니라 선언**이다. 표본마다 바뀌는 값이 아니고, 그 계량기가 무엇을 재도록
|
|
1594
|
+
* 설치되었는지는 현장이 안다. 그래서 표본(`EnergyRecord`)에 넣지 않고 자원 속성으로 둔다.
|
|
1595
|
+
*
|
|
1596
|
+
* 값은 계량기 자신의 낱말을 쓴다(적산 레지스터가 그렇게 갈려 있다). §`METER_DIRECTION`.
|
|
1597
|
+
*/
|
|
1598
|
+
meterDirection: "meter.direction",
|
|
1599
|
+
/*
|
|
1600
|
+
* ── 부하 계수 — 시뮬레이션이 전기를 만들 수 있게 (2026-08-14) ───────────────
|
|
1601
|
+
* 「이 설비가 돌면 몇 kW 인가」는 **현장이 아는 값**이다. 그래서 타입 기본값을 두지 않는다:
|
|
1602
|
+
* 선언하지 않은 설비는 부하를 만들지 않는다. 기본값을 두면 아무도 그 수가 짐작인 줄 모른 채
|
|
1603
|
+
* 요금 판정이 그 위에 선다.
|
|
1604
|
+
*/
|
|
1605
|
+
/** 가동 중 소비(kW). */
|
|
1606
|
+
ratedKW: "power.ratedKW",
|
|
1607
|
+
/** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
|
|
1608
|
+
standbyKW: "power.standbyKW",
|
|
1609
|
+
/*
|
|
1610
|
+
* ── 요금 단가 — 수를 금액으로 바꾸는 선언 (2026-08-17) ─────────────────────
|
|
1611
|
+
*
|
|
1612
|
+
* 그동안 트윈은 「최대수요 328kW」·「전력량 1,547kWh」까지 말하고 멈췄다. 그런데 피크를 깎는 일이
|
|
1613
|
+
* 돈이 되는 이유는 **요금이 둘로 나뉘기** 때문이다: 사용량(kWh)에 붙는 요금과 **최대수요(kW)에 붙는
|
|
1614
|
+
* 기본요금**. 단가가 없으면 그 절반을 말할 수 없고, 그래서 「피크를 깎아 얼마를 아끼나」에 답하지 못했다.
|
|
1615
|
+
*
|
|
1616
|
+
* 단가는 **현장의 계약**이다(같은 나라 안에서도 사업자·요금제마다 다르다). 그래서 기본값을 두지
|
|
1617
|
+
* 않는다 — 선언하지 않으면 금액을 계산하지 않는다. 짐작한 단가로 낸 금액은 숫자가 있다는 것만으로
|
|
1618
|
+
* 사람을 결정으로 밀어붙인다.
|
|
1619
|
+
*
|
|
1620
|
+
* 계약전력(`contract.kW`)과 같은 자리(수전)에 선언한다.
|
|
1621
|
+
*/
|
|
1622
|
+
/** 기본요금 단가 — 최대수요 1kW 당(청구 주기 기준). */
|
|
1623
|
+
demandChargePerKW: "tariff.demandChargePerKW",
|
|
1624
|
+
/**
|
|
1625
|
+
* **요금적용전력**(kW) — 기본요금이 실제로 매겨지는 기준. 계약전력과 다른 값이다.
|
|
1626
|
+
*
|
|
1627
|
+
* 기본요금이 걸리는 수는 이 주기에 잰 최대가 아닌 경우가 많다 — 지난 몇 달의 최고를 끌고 가거나,
|
|
1628
|
+
* 약정 용량으로 매기거나, 사업자가 따로 정한다. 그 규칙은 나라와 계약마다 다르므로 커널이 계산하지
|
|
1629
|
+
* 않고 **계산된 결과를 받는다.** 이 값이 있으면 기본요금이 조건부 파생이 아니라 사실이 된다.
|
|
1630
|
+
*
|
|
1631
|
+
* 계약전력(`contract.kW`)을 이 자리에 넣지 말 것. 넘었을 때의 뜻이 다르다 — 계약 초과는 약정
|
|
1632
|
+
* 위반이고, 이쪽은 다음 주기의 기본요금이 오른다는 뜻이다.
|
|
1633
|
+
*/
|
|
1634
|
+
billingDemandKW: "tariff.billingDemandKW",
|
|
1635
|
+
/** 사용량 단가 — 1kWh 당. */
|
|
1636
|
+
energyChargePerKWh: "tariff.energyChargePerKWh",
|
|
1637
|
+
/** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
|
|
1638
|
+
currency: "tariff.currency",
|
|
1639
|
+
/*
|
|
1640
|
+
* ── 축전지의 용량과 방전 정책 (2026-08-17) ─────────────────────────────────
|
|
1641
|
+
*
|
|
1642
|
+
* 「배터리로 피크를 깎는다」를 시뮬이 보이려면 방전을 만들어야 하고, 그것은 **선언에서** 나와야 한다.
|
|
1643
|
+
* 임계·최대율·예비를 우리가 정하면 그 수가 어디서 왔는지 아무도 설명할 수 없다.
|
|
1644
|
+
*
|
|
1645
|
+
* ── 선언한다는 것은 「그 자동화가 있다」는 주장이다 ─────────────────────────
|
|
1646
|
+
* 이 정책을 모델에 적으면 **라이브가 아닌 모든 구동이 그 규칙으로 돈다.** 현장에 BESS 제어기가 없는데
|
|
1647
|
+
* 적으면 파생 부하가 실제보다 낙관적으로 나온다(피크가 깎인 것으로 보인다). 그러니 「도입하면?」을
|
|
1648
|
+
* 묻는 것이라면 선언이 아니라 **what-if 가 덮어쓸 일**이다.
|
|
1649
|
+
*
|
|
1650
|
+
* 용량이 없으면 방전하지 않는다 — 용량을 모르면 「얼마나 버티나」를 답할 수 없고, 버티는 시간을
|
|
1651
|
+
* 모른 채 깎으면 무한히 깎을 수 있다고 주장하는 셈이다.
|
|
1652
|
+
*/
|
|
1653
|
+
/** 축전지 용량(kWh) — SOC 를 에너지로 바꾸는 값. 없으면 방전을 만들지 않는다. */
|
|
1654
|
+
capacityKWh: "storage.capacityKWh",
|
|
1655
|
+
/** 이 순수요를 넘으면 방전한다(kW) — 피크 억제 임계. */
|
|
1656
|
+
dischargeAboveKW: "dispatch.dischargeAboveKW",
|
|
1657
|
+
/** 최대 방전율(kW) — 인버터가 낼 수 있는 한계. */
|
|
1658
|
+
maxDischargeKW: "dispatch.maxDischargeKW",
|
|
1659
|
+
/** 예비 SOC(%) — 이 아래로는 쓰지 않는다(비상 대비). 없으면 0 으로 본다. */
|
|
1660
|
+
reserveSoc: "dispatch.reserveSoc",
|
|
1661
|
+
/**
|
|
1662
|
+
* 시뮬레이션이 **출발할 때의 SOC(%)** — 씨앗값이다.
|
|
1663
|
+
*
|
|
1664
|
+
* 계측이 SOC 를 알려 주는 트윈에는 필요 없다(잰 값이 진실이다). 시뮬 트윈에는 알려 줄 것이 없어서
|
|
1665
|
+
* 「SOC 를 모르면 방전하지 않는다」는 규율에 걸려 **배터리가 아무 일도 하지 못했다** — 정책을 선언해도
|
|
1666
|
+
* 피크가 한 톨도 깎이지 않았다(실화면에서 그렇게 났다).
|
|
1667
|
+
*
|
|
1668
|
+
* 이것은 상태의 씨앗이지 계측이 아니다. 그래서 계측이 들어오는 순간 그것이 이긴다.
|
|
1669
|
+
*/
|
|
1670
|
+
initialSoc: "storage.initialSoc",
|
|
1671
|
+
/*
|
|
1672
|
+
* ── 발전 — 태양광이 서 있기만 하던 자리 (2026-08-18) ────────────────────────
|
|
1673
|
+
*
|
|
1674
|
+
* 계통도에 태양광을 그려 놓고 시뮬에서는 한 톨도 만들지 못했다. 미러는 원천이 발전량을 보내 주지만,
|
|
1675
|
+
* 시뮬 트윈에는 그것을 만들 근거가 없었다 — 그래서 「태양광을 늘리면 피크가 얼마나 내려가나」를 물을 수
|
|
1676
|
+
* 없었다(what-if 의 값이 절반만 성립했다).
|
|
1677
|
+
*
|
|
1678
|
+
* **형상을 우리가 지어내지 않는다.** 위도·계절·날씨로 곡선을 만들면 그 수가 어디서 왔는지 아무도
|
|
1679
|
+
* 설명할 수 없고, 흐린 날 현장의 실적과 어긋난다. 그래서 현장이 **하루 형상을 적는다**: 24개 비율
|
|
1680
|
+
* (0~1)이면 그것이 그 현장의 곡선이다(측정한 형상을 그대로 붙일 수 있다).
|
|
1681
|
+
*
|
|
1682
|
+
* 정격만 있고 형상이 없으면 발전하지 않는다 — 하루 종일 정격으로 발전하는 태양광은 없다.
|
|
1683
|
+
*/
|
|
1684
|
+
/**
|
|
1685
|
+
* 발전 정격(kW) — **교류 쪽 최대 출력**. 계통으로 나가는 값이다.
|
|
1686
|
+
*
|
|
1687
|
+
* 이용률의 분모가 이 값이다(만든 양 ÷ 정격 × 시간). **직류 쪽 정격(§`genRatedKWdc`)과 다르고 더 작다** —
|
|
1688
|
+
* 두 값을 한 이름에 담으면 이용률이 그 차이만큼 틀리고, 틀린 이유가 아무 데도 표시되지 않는다.
|
|
1689
|
+
*
|
|
1690
|
+
* 설비마다 선언하거나, 설비별 값을 모르는 현장은 **자리(수전)에 합계로** 선언한다.
|
|
1691
|
+
*/
|
|
1692
|
+
genRatedKW: "generation.ratedKW",
|
|
1693
|
+
/**
|
|
1694
|
+
* 발전 정격(kW) — **직류 쪽**. 변환기(인버터)의 입력 쪽 정격이고, 태양광이면 패널 정격의 합이다.
|
|
1695
|
+
*
|
|
1696
|
+
* 이용률의 분모로 쓰지 않는다. 이 값이 교류 정격보다 큰 것은 설계이고(변환기를 그렇게 고른다),
|
|
1697
|
+
* 그 차이가 「맑은 정오에도 교류 출력이 더 오르지 않는 이유」를 설명한다.
|
|
1698
|
+
*/
|
|
1699
|
+
genRatedKWdc: "generation.ratedKWdc",
|
|
1700
|
+
/**
|
|
1701
|
+
* 하루 형상 — 쉼표로 나눈 **24개 비율**(0~1). 시각(현장 시간대)의 정격 대비 출력이다.
|
|
1702
|
+
*
|
|
1703
|
+
* 예: `0,0,0,0,0,0,0.05,0.2,0.45,0.7,0.9,1,1,0.95,0.8,0.6,0.35,0.12,0.02,0,0,0,0,0`
|
|
1704
|
+
*/
|
|
1705
|
+
genDailyProfile: "generation.dailyProfile"
|
|
1706
|
+
};
|
|
1707
|
+
var EMS_PROPERTY_SPEC = {
|
|
1708
|
+
[EMS_PROPERTY.contractKW]: { uom: "kW", dataType: "xs:double", note: "contracted power at the metering point, in kW." },
|
|
1709
|
+
/* 값이 셋 중 하나여야 한다 — 그 밖의 낱말은 받지 않는다(뜻이 통할 것 같은 말도 받지 않는다). */
|
|
1710
|
+
[EMS_PROPERTY.meterDirection]: {
|
|
1711
|
+
dataType: "xs:string",
|
|
1712
|
+
note: 'what this metering point measures: "import" (drawn from the grid), "export" (generation sent out), or "bidirectional" (one meter for both, sign carries the direction). Left undeclared, the point counts as load \u2014 leaving it out never makes demand look smaller than it is.'
|
|
1713
|
+
},
|
|
1714
|
+
[EMS_PROPERTY.ratedKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while running, in kW." },
|
|
1715
|
+
[EMS_PROPERTY.standbyKW]: { uom: "kW", dataType: "xs:double", note: "power drawn while idle, in kW." },
|
|
1716
|
+
[EMS_PROPERTY.demandChargePerKW]: { dataType: "xs:double", note: "demand charge per kW of billing-period peak, in the declared currency." },
|
|
1717
|
+
[EMS_PROPERTY.billingDemandKW]: {
|
|
1718
|
+
uom: "kW",
|
|
1719
|
+
dataType: "xs:double",
|
|
1720
|
+
note: "billing demand in kW \u2014 the basis the demand charge is actually billed on (often a rolling 12-month peak). Not the contracted power."
|
|
1721
|
+
},
|
|
1722
|
+
[EMS_PROPERTY.energyChargePerKWh]: { dataType: "xs:double", note: "energy charge per kWh, in the declared currency." },
|
|
1723
|
+
[EMS_PROPERTY.currency]: { dataType: "xs:string", note: "ISO 4217 currency code, e.g. USD or KRW." },
|
|
1724
|
+
[EMS_PROPERTY.capacityKWh]: { uom: "kWh", dataType: "xs:double", note: "usable energy of the storage, in kWh." },
|
|
1725
|
+
[EMS_PROPERTY.dischargeAboveKW]: { uom: "kW", dataType: "xs:double", note: "net demand above which the storage discharges, in kW." },
|
|
1726
|
+
[EMS_PROPERTY.maxDischargeKW]: { uom: "kW", dataType: "xs:double", note: "inverter limit on discharge rate, in kW." },
|
|
1727
|
+
/* 퍼센트다 — 0.8 은 0.8% 이고 80% 가 아니다. 이 한 줄이 없어서 배터리가 조용히 비어 있었다. */
|
|
1728
|
+
[EMS_PROPERTY.reserveSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "reserve state of charge as a percentage 0-100 (20 means 20%), never discharged below." },
|
|
1729
|
+
[EMS_PROPERTY.initialSoc]: { uom: "%", dataType: "xs:double", range: [0, 100], note: "starting state of charge as a percentage 0-100 (80 means 80%, not 0.8)." },
|
|
1730
|
+
[EMS_PROPERTY.genRatedKW]: {
|
|
1731
|
+
uom: "kW",
|
|
1732
|
+
dataType: "xs:double",
|
|
1733
|
+
note: "rated AC generation output, in kW \u2014 the denominator of capacity factor. Declare per equipment, or on the incoming location as a site total."
|
|
1734
|
+
},
|
|
1735
|
+
[EMS_PROPERTY.genRatedKWdc]: {
|
|
1736
|
+
uom: "kW",
|
|
1737
|
+
dataType: "xs:double",
|
|
1738
|
+
note: "rated DC generation capacity, in kW (panel rating for PV). Larger than the AC rating by design; not the capacity-factor denominator."
|
|
1739
|
+
},
|
|
1740
|
+
[EMS_PROPERTY.genDailyProfile]: {
|
|
1741
|
+
dataType: "xs:string",
|
|
1742
|
+
note: "daily shape as 24 comma-separated fractions of rated output (0-1), one per hour of local time \u2014 the site declares its own curve; we do not invent one."
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
function generationFractionAt(profile, hourOfDay) {
|
|
1746
|
+
const parts = String(profile ?? "").split(",").map((v2) => v2.trim()).filter((v2) => v2 !== "");
|
|
1747
|
+
if (parts.length !== 24)
|
|
1748
|
+
return void 0;
|
|
1749
|
+
const h = Math.floor(hourOfDay);
|
|
1750
|
+
if (!Number.isFinite(h) || h < 0 || h > 23)
|
|
1751
|
+
return void 0;
|
|
1752
|
+
const v = Number(parts[h]);
|
|
1753
|
+
if (!Number.isFinite(v) || v < 0 || v > 1)
|
|
1754
|
+
return void 0;
|
|
1755
|
+
return v;
|
|
1756
|
+
}
|
|
1757
|
+
var EMS_TYPES = [
|
|
1758
|
+
/* ── 자리: 전기적 구간 ─────────────────────────────────────────────────── */
|
|
1759
|
+
{
|
|
1760
|
+
key: "incoming",
|
|
1761
|
+
role: "location",
|
|
1762
|
+
/* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
|
|
1763
|
+
level: "Other",
|
|
1764
|
+
label: "twin.type.incoming",
|
|
1765
|
+
/* 수전 지점 — 계약전력이 걸리는 자리이고, 요금의 근거가 되는 수요는 여기서 잰다.
|
|
1766
|
+
ISO 50001 의 「에너지 유입」 경계이기도 하다(조직의 에너지 검토가 여기서 시작한다). */
|
|
1767
|
+
standardClass: { iec61850: "MMTR", iso50001: "EnergyInput" },
|
|
1768
|
+
identity: { scheme: "kernel:id" },
|
|
1769
|
+
capabilities: ["metered"]
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
key: "feeder",
|
|
1773
|
+
role: "location",
|
|
1774
|
+
/* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
|
|
1775
|
+
level: "Other",
|
|
1776
|
+
label: "twin.type.feeder",
|
|
1777
|
+
/* 분기 회로 — 부하 분해의 단위. 계약전력의 하위 배분이 여기서 정해진다. */
|
|
1778
|
+
standardClass: { iec61850: "Feeder", iso50001: "EnergyUse" },
|
|
1779
|
+
identity: { scheme: "kernel:id" },
|
|
1780
|
+
capabilities: ["metered"]
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
key: "submeter-zone",
|
|
1784
|
+
role: "location",
|
|
1785
|
+
/* 전기 계통의 구간은 **설비 계층의 단이 아니다** — 표준의 탈출구(`Other`)를 쓴다(아래 주석). */
|
|
1786
|
+
level: "Other",
|
|
1787
|
+
label: "twin.type.submeter-zone",
|
|
1788
|
+
/*
|
|
1789
|
+
* 구역 계량 — 여러 부하를 한 계량기로 묶어 재는 자리(공조·조명처럼 개별 계량이 없는 것들).
|
|
1790
|
+
*
|
|
1791
|
+
* ISO 50001 의 **SEU**(유의 에너지 사용처)가 대개 이 알갱이다. 개별 설비까지 재지 못하는 현장이
|
|
1792
|
+
* 많고, 그것을 「모른다」로 두는 대신 **묶음으로 아는 것**이 정직하다.
|
|
1793
|
+
*/
|
|
1794
|
+
standardClass: { iec61850: "MMXU", iso50001: "SEU" },
|
|
1795
|
+
identity: { scheme: "kernel:id" },
|
|
1796
|
+
capabilities: ["metered"]
|
|
1797
|
+
},
|
|
1798
|
+
/* ── 설비: 계량 지점과 에너지 자원 ──────────────────────────────────────── */
|
|
1799
|
+
{
|
|
1800
|
+
key: "meter",
|
|
1801
|
+
role: "equipment",
|
|
1802
|
+
label: "twin.type.meter",
|
|
1803
|
+
/* 계량기 — 측정 논리 노드(MMXU=측정단위, MMTR=적산). 자산으로도 하나다(ISO 55000). */
|
|
1804
|
+
standardClass: { iec61850: "MMXU", iso55000: "Asset", iso50001: "MeasurementPoint" },
|
|
1805
|
+
identity: { scheme: "kernel:id" },
|
|
1806
|
+
capabilities: ["metered", "operable"]
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
key: "breaker",
|
|
1810
|
+
role: "equipment",
|
|
1811
|
+
label: "twin.type.breaker",
|
|
1812
|
+
/*
|
|
1813
|
+
* 차단기 — IEC 61850 `XCBR`. **우리는 이것을 조작하지 않는다**(안전 계통은 범위 밖: ems.md §1).
|
|
1814
|
+
* 상태를 읽어 계통 구성을 알 뿐이다.
|
|
1815
|
+
*
|
|
1816
|
+
* 능력은 `switching` 이다 — `operable` 이 아니다(2026-08-14). `operable` 이던 동안 저작면이 차단기에
|
|
1817
|
+
* **제어 컴포넌트**를 권했다: 우리가 하지 않기로 한 조작을 화면이 부추긴 것이다. 「위치를 읽는다」와
|
|
1818
|
+
* 「명령을 받는다」는 다른 능력이고, 이제 그 둘이 갈려 있다(capability.ts 주석).
|
|
1819
|
+
*/
|
|
1820
|
+
standardClass: { iec61850: "XCBR", iso55000: "Asset" },
|
|
1821
|
+
identity: { scheme: "kernel:id" },
|
|
1822
|
+
capabilities: ["switching"]
|
|
1823
|
+
},
|
|
1824
|
+
{
|
|
1825
|
+
key: "pv-array",
|
|
1826
|
+
role: "equipment",
|
|
1827
|
+
label: "twin.type.pv-array",
|
|
1828
|
+
/* 태양광 어레이 — IEC 61850-7-420(분산자원)의 `DPVA`. 발전과 계량은 다른 능력이다. */
|
|
1829
|
+
standardClass: { iec61850: "DPVA", iso55000: "Asset", iso50001: "RenewableSupply" },
|
|
1830
|
+
identity: { scheme: "kernel:id" },
|
|
1831
|
+
capabilities: ["energyGenerating", "metered", "operable"]
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
key: "battery",
|
|
1835
|
+
role: "equipment",
|
|
1836
|
+
label: "twin.type.battery",
|
|
1837
|
+
/* 축전지(ESS) — `ZBAT`. 충전·방전을 나눠 재고, 저장은 보관(`storable`)이 아니다(물건이 아니다). */
|
|
1838
|
+
standardClass: { iec61850: "ZBAT", iso55000: "Asset" },
|
|
1839
|
+
identity: { scheme: "kernel:id" },
|
|
1840
|
+
capabilities: ["energyStoring", "metered", "operable"]
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
key: "utility",
|
|
1844
|
+
role: "equipment",
|
|
1845
|
+
label: "twin.type.utility",
|
|
1846
|
+
/*
|
|
1847
|
+
* 공통 설비 — 공조·컴프레서·칠러·조명·폐수처리처럼 **어느 공정에도 귀속되지 않는** 소비처.
|
|
1848
|
+
*
|
|
1849
|
+
* ── 왜 따로 있나 (2026-08-14) ────────────────────────────────────────────
|
|
1850
|
+
* 물류·생산 트윈에서 이런 것들은 **설비가 아니다**(공정에 매핑되지 않으므로 자원 축에 없다).
|
|
1851
|
+
* 그런데 에너지에서는 소비의 절반을 차지하고 감축 후보 1순위다 — 담을 자리가 반드시 있어야 한다.
|
|
1852
|
+
*
|
|
1853
|
+
* 처음에는 `curtailable-load` 하나로 받으려 했다. 그런데 그 이름은 **「줄일 수 있다」고 주장**한다:
|
|
1854
|
+
* 폐수처리·방폭 환기·서버실 냉방은 공통이지만 줄일 수 없고, 그것을 감축 가능으로 두면 트윈이
|
|
1855
|
+
* 「이걸 줄이면 됩니다」라는 거짓 제안을 한다. 공통성과 감축 가능성은 **다른 축**이다.
|
|
1856
|
+
*
|
|
1857
|
+
* 표준: IEC 61850 에 「공통 설비」라는 논리 노드는 없다(설비 종류마다 다른 노드다) — 비운다.
|
|
1858
|
+
* ISO 50001 의 SEU 는 이 부류를 가장 많이 가리킨다(유의 에너지 사용처).
|
|
1859
|
+
*/
|
|
1860
|
+
standardClass: { iso50001: "SEU", iso55000: "Asset" },
|
|
1861
|
+
identity: { scheme: "kernel:id" },
|
|
1862
|
+
capabilities: ["metered", "operable"]
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
key: "curtailable-load",
|
|
1866
|
+
role: "equipment",
|
|
1867
|
+
label: "twin.type.curtailable-load",
|
|
1868
|
+
/*
|
|
1869
|
+
* 감축 가능 부하 — 줄일 수 있는 소비처(공조·충전기·비상시 미가동 라인).
|
|
1870
|
+
*
|
|
1871
|
+
* 표준에 이 이름은 없다: IEC 61850 은 설비를 종류로 부르고 「감축 가능」은 **운영 정책**이다.
|
|
1872
|
+
* 그래서 `iec61850` 칸을 비우고 ISO 50001 의 SEU 로만 대응한다 — 억지로 논리 노드를 적으면
|
|
1873
|
+
* 적합성 표가 거짓을 말한다.
|
|
1874
|
+
*/
|
|
1875
|
+
standardClass: { iso50001: "SEU" },
|
|
1876
|
+
identity: { scheme: "kernel:id" },
|
|
1877
|
+
capabilities: ["curtailable", "metered", "operable"]
|
|
1878
|
+
}
|
|
1879
|
+
];
|
|
1880
|
+
function electricalUpstreamOf(locations, id) {
|
|
1881
|
+
const byId2 = new Map((locations ?? []).filter((l) => l?.id).map((l) => [String(l.id), l]));
|
|
1882
|
+
const self = byId2.get(String(id));
|
|
1883
|
+
if (!self)
|
|
1776
1884
|
return void 0;
|
|
1777
|
-
if (
|
|
1778
|
-
return
|
|
1779
|
-
|
|
1885
|
+
if (self.upstreamId)
|
|
1886
|
+
return String(self.upstreamId);
|
|
1887
|
+
const parent = self.parentId ? byId2.get(String(self.parentId)) : void 0;
|
|
1888
|
+
if (!parent)
|
|
1780
1889
|
return void 0;
|
|
1781
|
-
|
|
1782
|
-
return `${ns.replace(/:+$/, "")}:${marker}:${v}`;
|
|
1783
|
-
return void 0;
|
|
1784
|
-
}
|
|
1785
|
-
function header(type, eventTime, bizStep, opts) {
|
|
1786
|
-
const h = {
|
|
1787
|
-
"@context": EPCIS_CONTEXT,
|
|
1788
|
-
type,
|
|
1789
|
-
eventTime,
|
|
1790
|
-
eventTimeZoneOffset: UTC_OFFSET,
|
|
1791
|
-
bizStep
|
|
1792
|
-
};
|
|
1793
|
-
if (opts?.eventID)
|
|
1794
|
-
h.eventID = opts.eventID;
|
|
1795
|
-
if (opts?.recordTime)
|
|
1796
|
-
h.recordTime = opts.recordTime;
|
|
1797
|
-
if (opts?.errorDeclaration)
|
|
1798
|
-
h.errorDeclaration = opts.errorDeclaration;
|
|
1799
|
-
if (opts?.bizTransactionList?.length)
|
|
1800
|
-
h.bizTransactionList = opts.bizTransactionList;
|
|
1801
|
-
if (opts?.ilmd)
|
|
1802
|
-
h.ilmd = opts.ilmd;
|
|
1803
|
-
if (opts?.sourceList)
|
|
1804
|
-
h.sourceList = opts.sourceList;
|
|
1805
|
-
if (opts?.destinationList)
|
|
1806
|
-
h.destinationList = opts.destinationList;
|
|
1807
|
-
if (opts?.persistentDisposition)
|
|
1808
|
-
h.persistentDisposition = opts.persistentDisposition;
|
|
1809
|
-
if (opts?.sensorElementList)
|
|
1810
|
-
h.sensorElementList = opts.sensorElementList;
|
|
1811
|
-
if (opts?.certificationInfo)
|
|
1812
|
-
h.certificationInfo = opts.certificationInfo;
|
|
1813
|
-
return h;
|
|
1814
|
-
}
|
|
1815
|
-
function common(type, eventTime, action, bizStep, opts) {
|
|
1816
|
-
return { ...header(type, eventTime, bizStep, opts), action };
|
|
1817
|
-
}
|
|
1818
|
-
function objectEvent(p) {
|
|
1819
|
-
const e = { ...common("ObjectEvent", p.eventTime, p.action, p.bizStep, p), epcList: p.epcList };
|
|
1820
|
-
if (p.disposition)
|
|
1821
|
-
e.disposition = p.disposition;
|
|
1822
|
-
if (p.quantityList)
|
|
1823
|
-
e.quantityList = p.quantityList;
|
|
1824
|
-
if (p.readPoint)
|
|
1825
|
-
e.readPoint = { id: p.readPoint };
|
|
1826
|
-
if (p.bizLocation)
|
|
1827
|
-
e.bizLocation = { id: p.bizLocation };
|
|
1828
|
-
if (p.bizTransactionList)
|
|
1829
|
-
e.bizTransactionList = p.bizTransactionList;
|
|
1830
|
-
return e;
|
|
1890
|
+
return isElectricalLocationType(String(parent.type ?? "")) ? String(parent.id) : void 0;
|
|
1831
1891
|
}
|
|
1832
|
-
function
|
|
1833
|
-
|
|
1834
|
-
if (p.disposition)
|
|
1835
|
-
e.disposition = p.disposition;
|
|
1836
|
-
if (p.childEPCs)
|
|
1837
|
-
e.childEPCs = p.childEPCs;
|
|
1838
|
-
if (p.childQuantityList)
|
|
1839
|
-
e.childQuantityList = p.childQuantityList;
|
|
1840
|
-
if (p.readPoint)
|
|
1841
|
-
e.readPoint = { id: p.readPoint };
|
|
1842
|
-
if (p.bizLocation)
|
|
1843
|
-
e.bizLocation = { id: p.bizLocation };
|
|
1844
|
-
return e;
|
|
1892
|
+
function isElectricalLocationType(type) {
|
|
1893
|
+
return EMS_LOCATION_TYPES.includes(type);
|
|
1845
1894
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1895
|
+
|
|
1896
|
+
// ../ops-contract/dist/domain-catalog.js
|
|
1897
|
+
var locationKeys = (types) => types.filter((t) => t.role === "location").map((t) => t.key);
|
|
1898
|
+
var DOMAIN_CATALOG = {
|
|
1899
|
+
// label 은 언어 중립 i18n 키(twin.system.<code>) — 사람 언어는 표현계층이 렌더(L2).
|
|
1900
|
+
wms: { system: "wms", label: "twin.system.wms", types: WMS_TYPES, locationTypes: locationKeys(WMS_TYPES) },
|
|
1901
|
+
yms: { system: "yms", label: "twin.system.yms", types: YMS_TYPES, locationTypes: locationKeys(YMS_TYPES) },
|
|
1902
|
+
mes: { system: "mes", label: "twin.system.mes", types: MES_TYPES, locationTypes: locationKeys(MES_TYPES) },
|
|
1903
|
+
ems: { system: "ems", label: "twin.system.ems", types: EMS_TYPES, locationTypes: locationKeys(EMS_TYPES) }
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
// ../ops-contract/dist/domain-definition.js
|
|
1907
|
+
var OP_PARAM = {
|
|
1908
|
+
/** 양품률(0..1, 무차원). 없으면 커널 기본값 — 기본값을 쓴 사실은 `specCoverage()` 가 밝힌다. */
|
|
1909
|
+
yield: "yield",
|
|
1910
|
+
/** 셋업·체인지오버 소요(ISO 8601 기간 문자열). ISA-95 는 셋업을 별도 세그먼트로도 표현하지만,
|
|
1911
|
+
* 현재 커널은 작업에 붙는 셋업으로 다루므로 모수로 받는다. */
|
|
1912
|
+
setupDuration: "setupDuration"
|
|
1913
|
+
};
|
|
1914
|
+
function procedureViolations(route) {
|
|
1915
|
+
const elements = route.procedure ?? [];
|
|
1916
|
+
if (!elements.length)
|
|
1917
|
+
return [];
|
|
1918
|
+
const errors = [];
|
|
1919
|
+
const byKey = new Map(elements.map((e) => [e.key, e]));
|
|
1920
|
+
const hasChild = new Set(elements.map((e) => e.parent).filter((k) => !!k));
|
|
1921
|
+
const stepSet = new Set(route.steps ?? []);
|
|
1922
|
+
const seenStep = /* @__PURE__ */ new Map();
|
|
1923
|
+
for (const n of elements) {
|
|
1924
|
+
if (n.parent && !byKey.has(n.parent))
|
|
1925
|
+
errors.push(`${n.key}: \uC0C1\uC704 '${n.parent}' \uAC00 \uC774 \uC808\uCC28\uC5D0 \uC5C6\uB2E4`);
|
|
1926
|
+
if (n.step === void 0)
|
|
1927
|
+
continue;
|
|
1928
|
+
if (hasChild.has(n.key))
|
|
1929
|
+
errors.push(`${n.key}: \uC544\uB798 \uC694\uC18C\uAC00 \uC788\uB294\uB370 \uB2E8\uACC4\uB97C \uB4E0\uB2E4 \u2014 \uC2E4\uD589\uB418\uB294 \uAC83\uC740 \uC78E\uBFD0\uC774\uB2E4`);
|
|
1930
|
+
if (!stepSet.has(n.step))
|
|
1931
|
+
errors.push(`${n.key}: \uBAA8\uB974\uB294 \uB2E8\uACC4 '${n.step}' \u2014 steps \uC5D0 \uC5C6\uB2E4`);
|
|
1932
|
+
const already = seenStep.get(n.step);
|
|
1933
|
+
if (already)
|
|
1934
|
+
errors.push(`\uB2E8\uACC4 '${n.step}' \uAC00 '${already}' \uC640 '${n.key}' \uB458\uC5D0 \uC2E4\uB838\uB2E4 \u2014 \uC808\uCC28 \uC5B4\uB514\uC778\uC9C0 \uB9D0\uD560 \uC218 \uC5C6\uB2E4`);
|
|
1935
|
+
else
|
|
1936
|
+
seenStep.set(n.step, n.key);
|
|
1937
|
+
}
|
|
1938
|
+
return errors;
|
|
1864
1939
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
e.inputQuantityList = p.inputQuantityList;
|
|
1873
|
-
if (p.outputEPCList)
|
|
1874
|
-
e.outputEPCList = p.outputEPCList;
|
|
1875
|
-
if (p.outputQuantityList)
|
|
1876
|
-
e.outputQuantityList = p.outputQuantityList;
|
|
1877
|
-
if (p.transformationID)
|
|
1878
|
-
e.transformationID = p.transformationID;
|
|
1879
|
-
if (p.readPoint)
|
|
1880
|
-
e.readPoint = { id: p.readPoint };
|
|
1881
|
-
if (p.bizLocation)
|
|
1882
|
-
e.bizLocation = { id: p.bizLocation };
|
|
1883
|
-
if (p.bizTransactionList)
|
|
1884
|
-
e.bizTransactionList = p.bizTransactionList;
|
|
1885
|
-
return e;
|
|
1940
|
+
|
|
1941
|
+
// ../ops-contract/dist/energy-ingest.js
|
|
1942
|
+
function resolveSubject(kind, localId, opts) {
|
|
1943
|
+
const declared = opts.identityOf?.(kind, localId);
|
|
1944
|
+
if (declared)
|
|
1945
|
+
return { subject: declared, basis: "declared" };
|
|
1946
|
+
return { subject: opts.scopeId ? `${opts.scopeId}/${localId}` : localId, basis: "twin-local" };
|
|
1886
1947
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
if (!uri)
|
|
1890
|
-
return void 0;
|
|
1891
|
-
const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
|
|
1892
|
-
if (!m)
|
|
1893
|
-
return void 0;
|
|
1894
|
-
const want = GS1_KEY_DIGITS[m[1]];
|
|
1895
|
-
if (!want)
|
|
1896
|
-
return void 0;
|
|
1897
|
-
const parts = m[2].split(".");
|
|
1898
|
-
if (parts.length < 2)
|
|
1899
|
-
return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uAC00 '.' \uB85C \uAC08\uB824\uC57C \uD55C\uB2E4`;
|
|
1900
|
-
const [prefix, ref] = parts;
|
|
1901
|
-
if (ref === "*")
|
|
1902
|
-
return void 0;
|
|
1903
|
-
if (!/^\d+$/.test(prefix) || !/^\d+$/.test(ref)) {
|
|
1904
|
-
return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uB294 \uC22B\uC790\uB2E4`;
|
|
1905
|
-
}
|
|
1906
|
-
const got = prefix.length + ref.length;
|
|
1907
|
-
if (got === want)
|
|
1908
|
-
return void 0;
|
|
1909
|
-
return `${m[1]} \uC790\uB9AC \uC218 \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4(${prefix.length}) + \uCC38\uC870(${ref.length}) = ${got} \uC774\uC9C0\uB9CC ${want} \uC5EC\uC57C \uD55C\uB2E4(GS1 TDS). \uD504\uB9AC\uD53D\uC2A4\uB97C \uBC14\uAFB8\uBA74 \uCC38\uC870 \uC790\uB9AC \uC218\uB97C \uD568\uAED8 \uB9DE\uCDB0\uC57C \uD55C\uB2E4.`;
|
|
1948
|
+
function periodFactId(tenantId, kind, subject, from, to) {
|
|
1949
|
+
return [tenantId, kind, subject, from, to].join("|");
|
|
1910
1950
|
}
|
|
1911
1951
|
|
|
1912
1952
|
// ../ops-contract/dist/order-fold.js
|
|
@@ -2260,8 +2300,12 @@ var SPECS = {
|
|
|
2260
2300
|
*/
|
|
2261
2301
|
recipeKey: "string",
|
|
2262
2302
|
recipeBasis: "object",
|
|
2263
|
-
materialBasis: "object[]"
|
|
2303
|
+
materialBasis: "object[]",
|
|
2304
|
+
/* 근거 칸이 빈 까닭 — 닫힌 낱말이다(§`BASIS_UNKNOWN`). 투입이 근거 장치보다 먼저였던 오더도 이행은 사실이다. */
|
|
2305
|
+
basisUnknown: "string"
|
|
2264
2306
|
},
|
|
2307
|
+
/* 상태·종류는 도메인이 소유한다 — 닫지 않는다. 근거가 빈 까닭은 커널이 읽으므로 닫는다. */
|
|
2308
|
+
enums: { basisUnknown: BASIS_UNKNOWN },
|
|
2265
2309
|
/*
|
|
2266
2310
|
* 확보분의 안쪽을 본다 — 열쇠와 수 둘이다(§`AllocatedQuantity`). 안 보면 틀린 이름이 조용히
|
|
2267
2311
|
* 지나가고(`epcs`·`quantity` 따위), 미러는 잡힌 수를 0 으로 읽는다.
|
|
@@ -2283,7 +2327,6 @@ var SPECS = {
|
|
|
2283
2327
|
positive: ["qty"]
|
|
2284
2328
|
}
|
|
2285
2329
|
}
|
|
2286
|
-
/* 상태·종류는 도메인이 소유한다 — 닫지 않는다. */
|
|
2287
2330
|
},
|
|
2288
2331
|
quality: {
|
|
2289
2332
|
eventType: OP_EVENT.quality,
|
|
@@ -3865,6 +3908,17 @@ var ObservedReducer = class {
|
|
|
3865
3908
|
this.corrections = (cp?.corrections ?? []).map((c) => ({ ...c }));
|
|
3866
3909
|
this.unhandled = new Map((cp?.unhandled ?? []).map(({ eventType, ...v }) => [eventType, { ...v }]));
|
|
3867
3910
|
}
|
|
3911
|
+
/**
|
|
3912
|
+
* **이어받은 접힌 쪽을 앉힌다**(ADR-0092) — 이어가기 씨앗으로 뜨는 미러가 부른다(§`FlowEngine.hydrateContinuity`).
|
|
3913
|
+
*
|
|
3914
|
+
* 이미 접힌 것이 있으면 **건드리지 않는다.** 재개점(체크포인트)이 먼저 되세웠다면 그것이 정본이고, 같은 값을 또 더하면 세는
|
|
3915
|
+
* 수가 두 번 는다. 비어 있을 때만 이어받는다. 이어받았으면 참을 돌려준다.
|
|
3916
|
+
*/
|
|
3917
|
+
adoptFoldedOrders(folded) {
|
|
3918
|
+
if (foldedOrderCount(this.foldedOrders)) return false;
|
|
3919
|
+
this.foldedOrders = structuredClone(folded);
|
|
3920
|
+
return true;
|
|
3921
|
+
}
|
|
3868
3922
|
/**
|
|
3869
3923
|
* **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
|
|
3870
3924
|
*
|
|
@@ -4839,6 +4893,14 @@ var FlowEngine = class {
|
|
|
4839
4893
|
hydrateContinuity(snap) {
|
|
4840
4894
|
for (const id of snap?.acked ?? []) this._acked.add(id);
|
|
4841
4895
|
for (const e of snap?.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
|
|
4896
|
+
if (snap?.foldedOrders && foldedOrderCount(snap.foldedOrders)) {
|
|
4897
|
+
if (this.observeMode) {
|
|
4898
|
+
if (!this.observer) this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
4899
|
+
if (this.observer.adoptFoldedOrders(snap.foldedOrders)) this.observedDirty = true;
|
|
4900
|
+
} else if (!foldedOrderCount(this.foldedOrders)) {
|
|
4901
|
+
this.foldedOrders = structuredClone(snap.foldedOrders);
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4842
4904
|
}
|
|
4843
4905
|
/**
|
|
4844
4906
|
* 씨앗의 확보분 중 **이 트윈에 실제로 있는 것만** 남긴다 — 없는 것은 세고 버린다.
|
|
@@ -6976,7 +7038,7 @@ var FlowEngine = class {
|
|
|
6976
7038
|
this.emit(objectEvent({
|
|
6977
7039
|
eventTime: this.now(),
|
|
6978
7040
|
action: "OBSERVE",
|
|
6979
|
-
bizStep:
|
|
7041
|
+
bizStep: PRIVATE_BIZSTEP.consuming,
|
|
6980
7042
|
disposition: DISP.in_progress,
|
|
6981
7043
|
epcList: [],
|
|
6982
7044
|
quantityList: [{ epcClass: it.gtin ?? it.epc, quantity: left, uom: it.uom }],
|
|
@@ -6990,7 +7052,7 @@ var FlowEngine = class {
|
|
|
6990
7052
|
}
|
|
6991
7053
|
if (!whole.length) return;
|
|
6992
7054
|
if (this.adoptConsumed(t, whole)) {
|
|
6993
|
-
this.observeDisposition(whole, DISP.in_progress,
|
|
7055
|
+
this.observeDisposition(whole, DISP.in_progress, PRIVATE_BIZSTEP.consuming);
|
|
6994
7056
|
return;
|
|
6995
7057
|
}
|
|
6996
7058
|
const at = this.items.get(whole[0])?.location ?? t.toNode;
|
|
@@ -7005,7 +7067,7 @@ var FlowEngine = class {
|
|
|
7005
7067
|
objectEvent({
|
|
7006
7068
|
eventTime: this.now(),
|
|
7007
7069
|
action: "DELETE",
|
|
7008
|
-
bizStep:
|
|
7070
|
+
bizStep: PRIVATE_BIZSTEP.consuming,
|
|
7009
7071
|
disposition: DISP.in_progress,
|
|
7010
7072
|
epcList: whole.slice(),
|
|
7011
7073
|
readPoint: at,
|
|
@@ -8065,8 +8127,8 @@ var YmsKernel = class extends FlowEngine {
|
|
|
8065
8127
|
return;
|
|
8066
8128
|
}
|
|
8067
8129
|
if (to.type === "yard-slot") {
|
|
8068
|
-
trailer.disposition
|
|
8069
|
-
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep:
|
|
8130
|
+
delete trailer.disposition;
|
|
8131
|
+
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: YARD_PRIVATE_BIZSTEP.staging, epcList: [trailer.epc], readPoint: to.id, bizLocation: to.id }));
|
|
8070
8132
|
return;
|
|
8071
8133
|
}
|
|
8072
8134
|
if (to.type === "dock-door") {
|
|
@@ -8080,7 +8142,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
8080
8142
|
this.aggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.loading, readPoint: to.id, consume: { readPoint: staging.id, disposition: DISP.in_transit } });
|
|
8081
8143
|
}
|
|
8082
8144
|
this.trailerCargo.delete(trailer.epc);
|
|
8083
|
-
trailer.disposition
|
|
8145
|
+
delete trailer.disposition;
|
|
8084
8146
|
} else if (cargo.length && staging) {
|
|
8085
8147
|
this.disaggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.unloading, readPoint: to.id, materialize: { location: staging.id, disposition: DISP.sellable } });
|
|
8086
8148
|
this.trailerCargo.delete(trailer.epc);
|