@operato/twin-kernel 0.0.6 → 0.2.0
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/contract.d.ts +209 -1
- package/dist/contract.js +4 -0
- package/dist/counterfactual.d.ts +2 -0
- package/dist/counterfactual.js +7 -3
- package/dist/domain-definition.d.ts +84 -1
- package/dist/domain-definition.js +11 -0
- package/dist/duration-estimator.d.ts +26 -2
- package/dist/epcis.d.ts +185 -6
- package/dist/epcis.js +175 -12
- package/dist/flow-engine.d.ts +171 -3
- package/dist/flow-engine.js +522 -21
- package/dist/forecast.d.ts +8 -0
- package/dist/forecast.js +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/iso-duration.d.ts +5 -0
- package/dist/iso-duration.js +43 -0
- package/dist/kernel.js +8 -2
- package/dist/mes-kernel.d.ts +2 -2
- package/dist/mes-kernel.js +18 -9
- package/dist/state-projector.d.ts +65 -2
- package/dist/state-projector.js +241 -17
- package/dist/task-fold.d.ts +56 -0
- package/dist/task-fold.js +60 -0
- package/dist/twin-observer.d.ts +2 -0
- package/dist/twin-observer.js +9 -3
- package/dist-cjs/index.cjs +993 -159
- package/package.json +1 -1
package/dist/epcis.d.ts
CHANGED
|
@@ -11,9 +11,33 @@ export declare const DISP: {
|
|
|
11
11
|
};
|
|
12
12
|
export type EpcisEventType = 'ObjectEvent' | 'AggregationEvent' | 'TransactionEvent' | 'TransformationEvent';
|
|
13
13
|
export type EpcisAction = 'ADD' | 'OBSERVE' | 'DELETE';
|
|
14
|
+
/**
|
|
15
|
+
* 수량 요소 — **클래스 식별자 + 얼마나**. 표준이 세 경우를 규정한다(EPCIS 2.0 §7.3.3.1).
|
|
16
|
+
*
|
|
17
|
+
* | 품목 성격 | quantity | uom | 뜻 |
|
|
18
|
+
* |---|---|---|---|
|
|
19
|
+
* | 고정 계량 | 양의 **정수** | 없음 | 그 클래스의 **개수** |
|
|
20
|
+
* | 가변 계량 | 양수(정수 아니어도) | 있음 | quantity=크기, uom=물리 단위 |
|
|
21
|
+
* | 아무 것 | **없음** | 없음 | 수량을 **모른다**(미지정) |
|
|
22
|
+
*
|
|
23
|
+
* 즉 **`uom` 이 없으면 개수, 있으면 물리량**이다. 개수 단위(each 등)를 `uom` 에 넣는 것은 표준이
|
|
24
|
+
* 의도한 방식이 아니다 — 개수는 uom 없이 정수로 쓴다.
|
|
25
|
+
*
|
|
26
|
+
* 수량을 모를 때 0 으로 꾸미지 않도록 표준이 **"둘 다 생략"** 자리를 따로 두었다.
|
|
27
|
+
*/
|
|
14
28
|
export interface QuantityElement {
|
|
29
|
+
/** 클래스 식별자 — `urn:epc:idpat:…`(조건) 또는 `urn:epc:class:lgtin:…`(로트 클래스). */
|
|
15
30
|
epcClass: string;
|
|
16
|
-
|
|
31
|
+
/**
|
|
32
|
+
* 얼마나. **모르면 생략한다**(0 이 아니다).
|
|
33
|
+
* uom 없으면 양의 정수(개수), uom 있으면 양수(물리량, 소수 허용).
|
|
34
|
+
*/
|
|
35
|
+
quantity?: number;
|
|
36
|
+
/**
|
|
37
|
+
* 측정 단위 — UN/CEFACT 권고 20 "Common Code" 의 2~3자 코드.
|
|
38
|
+
* 표준은 그중 **길이·면적·부피·질량**만 허용하고 폐기(X)·비권장(D) 코드를 제외한다.
|
|
39
|
+
* 예: MTR(미터) 허용 / F17(피트당 파운드힘) 불허. `quantity` 가 없으면 이 값도 없어야 한다.
|
|
40
|
+
*/
|
|
17
41
|
uom?: string;
|
|
18
42
|
}
|
|
19
43
|
export interface BizTransactionElement {
|
|
@@ -23,17 +47,110 @@ export interface BizTransactionElement {
|
|
|
23
47
|
export interface LocationRef {
|
|
24
48
|
id: string;
|
|
25
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* 정정 선언 — 이미 캡처된 이벤트를 취소·수정한다(EPCIS 2.0 `errorDeclaration`).
|
|
52
|
+
*
|
|
53
|
+
* 실 시스템은 잘못 보낸 이벤트를 정정한다. 받을 자리가 없으면 정정이 **또 하나의 사실**로 쌓여
|
|
54
|
+
* 재고·이력이 조용히 틀어진다(목 연동에서는 드러나지 않고 실 시스템에서 바로 터진다).
|
|
55
|
+
*/
|
|
56
|
+
export interface ErrorDeclaration {
|
|
57
|
+
/** 정정을 선언한 시각(ISO). 표준 필수. */
|
|
58
|
+
declarationTime: string;
|
|
59
|
+
/** 정정 이유 — CBV 어휘 URN(did_not_occur / incorrect_data). 도메인이 넓게 쓸 수 있어 열린 문자열. */
|
|
60
|
+
reason?: string;
|
|
61
|
+
/** 이 정정이 가리키는 원본 이벤트들의 `eventID`. */
|
|
62
|
+
correctiveEventIDs?: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 이전 당사자 — 소유·보관이 누구에게서 누구로 옮겨지는가(EPCIS 2.0 §7.3.5).
|
|
66
|
+
* `type` 은 이전의 종류를 가리키는 표준 어휘(예: "owning party"), `source`/`destination` 은 그 당사자다.
|
|
67
|
+
* 3PL·위탁재고·수탁처럼 **물건은 그대로인데 권리가 옮겨지는** 일을 이것으로 표현한다.
|
|
68
|
+
*/
|
|
69
|
+
export interface SourceElement {
|
|
70
|
+
type: string;
|
|
71
|
+
source: string;
|
|
72
|
+
}
|
|
73
|
+
export interface DestinationElement {
|
|
74
|
+
type: string;
|
|
75
|
+
destination: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 지속 상태 — 이벤트 **이후에도 유효한** 업무 조건(EPCIS 2.0 §7.3.4).
|
|
79
|
+
*
|
|
80
|
+
* `disposition` 은 그 순간의 상태이고, 이쪽은 **명시적으로 해제될 때까지 유지**된다. 그래서 회수 대상처럼
|
|
81
|
+
* 매 이벤트에 다시 말하지 않아도 되는 성질을 담는다. set 과 unset 은 서로 독립이다.
|
|
82
|
+
*/
|
|
83
|
+
export interface PersistentDisposition {
|
|
84
|
+
/** 이 이벤트 이후 **설정**되는 조건 URI 들(해제될 때까지 유지). */
|
|
85
|
+
set?: string[];
|
|
86
|
+
/** 이 이벤트 이후 **해제**되는 조건 URI 들. */
|
|
87
|
+
unset?: string[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 센서 관측 묶음 — 콜드체인·충격·습도 같은 물리 측정(EPCIS 2.0 §7.4).
|
|
91
|
+
*
|
|
92
|
+
* 하나의 `sensorMetadata`(모든 보고에 공통으로 적용되는 메타)와 **하나 이상의** `sensorReport`
|
|
93
|
+
* (개별 관측)로 이루어진다. 안의 속성은 인라인이고 어휘가 넓어, 커널은 **구조만 통과**시키고
|
|
94
|
+
* 값의 해석은 도메인에 맡긴다(없는 어휘를 발명하지 않는다).
|
|
95
|
+
*/
|
|
96
|
+
export interface SensorElement {
|
|
97
|
+
sensorMetadata?: Record<string, unknown>;
|
|
98
|
+
sensorReport: Record<string, unknown>[];
|
|
99
|
+
}
|
|
26
100
|
/** EPCIS 2.0 공통 헤더(모든 이벤트). action 은 여기 없음 — TransformationEvent 는 action 이 없다. */
|
|
27
101
|
interface EpcisHeader {
|
|
28
102
|
'@context': string;
|
|
29
103
|
type: EpcisEventType;
|
|
104
|
+
/**
|
|
105
|
+
* 이벤트 자신의 정체성(EPCIS 2.0 `eventID`, 대문자 D).
|
|
106
|
+
*
|
|
107
|
+
* **봉투(`CanonicalEnvelope.eventId`)와 다른 것이다** — 봉투 id 는 우리 전송 단위의 id 이고, 이쪽은
|
|
108
|
+
* EPCIS 이벤트 자체의 id 다. 정정 이벤트가 원본을 `correctiveEventIDs` 로 가리켜야 하므로 이벤트
|
|
109
|
+
* 자신의 id 가 필요하다. 이름이 비슷해 혼동하기 쉬우니 봉투 id 를 그대로 재사용하지 말 것.
|
|
110
|
+
*/
|
|
111
|
+
eventID?: string;
|
|
112
|
+
/** 발생 시각(ISO). */
|
|
30
113
|
eventTime: string;
|
|
31
114
|
eventTimeZoneOffset: string;
|
|
115
|
+
/**
|
|
116
|
+
* **기록** 시각(ISO) — 발생 시각과 별개다.
|
|
117
|
+
*
|
|
118
|
+
* 이 둘이 갈라지는 것이 실 연동의 정상이다(현장에서 일어난 뒤 늦게 도착). 소비처가 순서를 판정할 때
|
|
119
|
+
* `eventTime` 만 보면 늦게 도착한 옛 이벤트가 최신 상태를 덮는다 — 그 판정의 재료가 이 값이다.
|
|
120
|
+
*/
|
|
121
|
+
recordTime?: string;
|
|
32
122
|
bizStep: string;
|
|
33
123
|
disposition?: string;
|
|
34
124
|
readPoint?: LocationRef;
|
|
35
125
|
bizLocation?: LocationRef;
|
|
36
126
|
bizTransactionList?: BizTransactionElement[];
|
|
127
|
+
/** 정정 선언(있으면 이 이벤트는 앞선 이벤트를 정정하는 것이다). */
|
|
128
|
+
errorDeclaration?: ErrorDeclaration;
|
|
129
|
+
/**
|
|
130
|
+
* 개체·로트 마스터데이터 — **값이 같은 범위가 작은** 서술 속성(EPCIS 2.0 §7.3.8).
|
|
131
|
+
*
|
|
132
|
+
* 품목 마스터데이터(무게·치수)는 같은 GTIN 전부에 같은 값이지만, 이쪽은 **한 로트** 또는
|
|
133
|
+
* **한 개체**마다 다를 수 있다(대표 예: 유통기한). 그래서 물건이 처음 생겨나는 이벤트에 한 번 붙인다.
|
|
134
|
+
*
|
|
135
|
+
* 안에 들어가는 속성 이름은 **표준이 정의하지 않는다** — "이름 붙은 속성들의 집합(값은 임의 타입)"
|
|
136
|
+
* 이고 구체적 요소는 EPCIS 위에 얹히는 상위 문서가 정한다(§7.3.8). 그래서 커널은 자리만 열고 어휘를
|
|
137
|
+
* 발명하지 않으며, 받은 것을 **잃지 않고 통과**시킨다.
|
|
138
|
+
*
|
|
139
|
+
* **배치 규칙(§7.3.8)**: 개체가 존재하기 시작할 때 정의되므로 `ObjectEvent`(action=ADD)와
|
|
140
|
+
* `TransformationEvent` 에만 실을 수 있다. 변환에서는 **출력**에 적용된다(입력 아님).
|
|
141
|
+
* 그리고 객체의 생애 동안 **정적**인 것만 담는다 — 변하는 것은 이벤트의 몫이다.
|
|
142
|
+
*/
|
|
143
|
+
ilmd?: Record<string, unknown>;
|
|
144
|
+
/** 이전 출발 당사자들(소유·보관 이전). */
|
|
145
|
+
sourceList?: SourceElement[];
|
|
146
|
+
/** 이전 도착 당사자들. */
|
|
147
|
+
destinationList?: DestinationElement[];
|
|
148
|
+
/** 이벤트 이후에도 유지되는 업무 조건(해제될 때까지). */
|
|
149
|
+
persistentDisposition?: PersistentDisposition;
|
|
150
|
+
/** 센서 관측 묶음(콜드체인 등). */
|
|
151
|
+
sensorElementList?: SensorElement[];
|
|
152
|
+
/** 인증 상세를 가리키는 URL(규제 산업). */
|
|
153
|
+
certificationInfo?: string;
|
|
37
154
|
}
|
|
38
155
|
/** action 을 갖는 이벤트(Object/Aggregation/Transaction). */
|
|
39
156
|
interface EpcisActionEvent extends EpcisHeader {
|
|
@@ -69,10 +186,72 @@ export interface EpcisTransformationEvent extends EpcisHeader {
|
|
|
69
186
|
export type EpcisEvent = EpcisObjectEvent | EpcisAggregationEvent | EpcisTransactionEvent | EpcisTransformationEvent;
|
|
70
187
|
/** SSCC (물류단위: 팔레트/화물/트레일러) — 결정적 카운터 기반. */
|
|
71
188
|
export declare function ssccUri(companyPrefix: string, serial: number): string;
|
|
72
|
-
/** 비직렬 수량용 SGTIN 패턴(epcClass) — quantityList 에 사용. */
|
|
189
|
+
/** 비직렬 수량용 SGTIN 패턴(epcClass) — 로트를 따지지 않는 품목 클래스. quantityList 에 사용. */
|
|
73
190
|
export declare function sgtinClass(companyPrefix: string, itemRef: string): string;
|
|
191
|
+
/**
|
|
192
|
+
* 우리가 방출하는 개체·로트 마스터데이터의 속성 이름 — **한 곳에 모은다.**
|
|
193
|
+
*
|
|
194
|
+
* 표준은 이 이름들을 정의하지 않는다(§7.3.8: 상위 문서 소관). 그래서 값을 실으려면 이름을 정해야 하는데,
|
|
195
|
+
* 흩뿌리면 곧 방언이 된다. GS1 CBV 마스터데이터 이름공간(`urn:epcglobal:cbv:mda:`)의 정식 명칭을
|
|
196
|
+
* 확인하면 **이 상수만 바꾸면 되도록** 여기 모아 둔다.
|
|
197
|
+
*
|
|
198
|
+
* ⚠ 아래 이름은 **아직 정본 확인 전**이다(CBV 마스터데이터 문서 필요). 받는 경로는 이름과 무관하게
|
|
199
|
+
* 동작하므로(받은 것을 잃지 않고 통과) 이 상수는 우리가 **생산할 때**만 쓰인다.
|
|
200
|
+
*/
|
|
201
|
+
export declare const ILMD_ATTR: {
|
|
202
|
+
/** 유통기한·만료(로트 단위). */
|
|
203
|
+
readonly expiry: "cbvmda:itemExpirationDate";
|
|
204
|
+
/** 로트·배치 번호(직렬 개체에 로트를 붙일 때). */
|
|
205
|
+
readonly lot: "cbvmda:lotNumber";
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* 품번 + 로트 클래스(LGTIN) — 낱개 일련번호가 없고 **로트로 관리**하는 자재의 표준 식별자.
|
|
209
|
+
*
|
|
210
|
+
* 원자재·화학·식품이 이 경우다. 로트가 식별자의 한 마디로 들어가므로 "로트별 재고" 가 별도 필드 없이
|
|
211
|
+
* epcClass 별 집계가 된다. 개체가 아니라 클래스이므로 `epcList` 가 아니라 `quantityList` 에 쓴다.
|
|
212
|
+
*
|
|
213
|
+
* 문법(EPC Tag Data Standard 2.1.0 §6.4.1):
|
|
214
|
+
* urn:epc:class:lgtin:CompanyPrefix.ItemRefAndIndicator.Lot
|
|
215
|
+
* 예) urn:epc:class:lgtin:4012345.012345.998877
|
|
216
|
+
* 두 숫자 마디의 자릿수 합은 13(점 제외), Lot 은 GS3A3Component(URI 이스케이프 허용).
|
|
217
|
+
*/
|
|
218
|
+
export declare function lgtinClass(companyPrefix: string, itemRefAndIndicator: string, lot: string): string;
|
|
219
|
+
/** 식별자를 뜯어 본 결과 — 소비처가 문자열을 자르지 않게 한다(자르면 규칙이 갈라진다). */
|
|
220
|
+
export interface ParsedEpc {
|
|
221
|
+
/** 표준 스킴. 모르면 'unknown'(원문을 그대로 남긴다 — 추측하지 않는다). */
|
|
222
|
+
scheme: 'sgtin' | 'lgtin' | 'idpat' | 'sscc' | 'gdti' | 'grai' | 'giai' | 'sgln' | 'unknown';
|
|
223
|
+
/** 개체 단위인가(epcList 에 들어갈 것인가). 클래스 식별자는 false. */
|
|
224
|
+
instance: boolean;
|
|
225
|
+
/** 품목 클래스 키 — `CompanyPrefix.ItemRef`(sgtin/lgtin/idpat 에서). */
|
|
226
|
+
gtinKey?: string;
|
|
227
|
+
/** 로트 번호(LGTIN 에만 있다). */
|
|
228
|
+
lot?: string;
|
|
229
|
+
/** 일련번호(SGTIN·SSCC 등 개체 식별자에만). */
|
|
230
|
+
serial?: string;
|
|
231
|
+
/** 원문 — 해석에 실패해도 잃지 않는다. */
|
|
232
|
+
uri: string;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* EPC/클래스 식별자 파서 — **표준 지식이라 커널이 소유한다.**
|
|
236
|
+
*
|
|
237
|
+
* 소비처가 `uri.split(':').pop()` 으로 자르면 규칙이 갈라진다(실제로 화면이 LGTIN 의 마지막 마디인
|
|
238
|
+
* 로트를 SKU 이름으로 표시할 위험이 있었다). 뜯는 일은 여기 한 곳에서 한다.
|
|
239
|
+
*/
|
|
240
|
+
export declare function parseEpc(uri: string): ParsedEpc;
|
|
74
241
|
/** 거래문서 식별자 = GDTI (PO/SO/WO/어포인트먼트 등). */
|
|
75
242
|
export declare function gdtiUri(companyPrefix: string, docType: string, serial: number): string;
|
|
243
|
+
/** 모든 빌더가 공통으로 받는 표준 헤더 옵션(선택) — 방출부가 필요할 때 채운다. */
|
|
244
|
+
export interface EpcisHeaderOptions {
|
|
245
|
+
eventID?: string;
|
|
246
|
+
recordTime?: string;
|
|
247
|
+
errorDeclaration?: ErrorDeclaration;
|
|
248
|
+
ilmd?: Record<string, unknown>;
|
|
249
|
+
sourceList?: SourceElement[];
|
|
250
|
+
destinationList?: DestinationElement[];
|
|
251
|
+
persistentDisposition?: PersistentDisposition;
|
|
252
|
+
sensorElementList?: SensorElement[];
|
|
253
|
+
certificationInfo?: string;
|
|
254
|
+
}
|
|
76
255
|
export declare function objectEvent(p: {
|
|
77
256
|
eventTime: string;
|
|
78
257
|
action: EpcisAction;
|
|
@@ -83,7 +262,7 @@ export declare function objectEvent(p: {
|
|
|
83
262
|
readPoint?: string;
|
|
84
263
|
bizLocation?: string;
|
|
85
264
|
bizTransactionList?: BizTransactionElement[];
|
|
86
|
-
}): EpcisObjectEvent;
|
|
265
|
+
} & EpcisHeaderOptions): EpcisObjectEvent;
|
|
87
266
|
export declare function aggregationEvent(p: {
|
|
88
267
|
eventTime: string;
|
|
89
268
|
action: EpcisAction;
|
|
@@ -94,7 +273,7 @@ export declare function aggregationEvent(p: {
|
|
|
94
273
|
childQuantityList?: QuantityElement[];
|
|
95
274
|
readPoint?: string;
|
|
96
275
|
bizLocation?: string;
|
|
97
|
-
}): EpcisAggregationEvent;
|
|
276
|
+
} & EpcisHeaderOptions): EpcisAggregationEvent;
|
|
98
277
|
export declare function transactionEvent(p: {
|
|
99
278
|
eventTime: string;
|
|
100
279
|
action: EpcisAction;
|
|
@@ -106,7 +285,7 @@ export declare function transactionEvent(p: {
|
|
|
106
285
|
quantityList?: QuantityElement[];
|
|
107
286
|
readPoint?: string;
|
|
108
287
|
bizLocation?: string;
|
|
109
|
-
}): EpcisTransactionEvent;
|
|
288
|
+
} & EpcisHeaderOptions): EpcisTransactionEvent;
|
|
110
289
|
/** 변환(제조) — 입력 EPC/수량 소비 → 출력 EPC/수량 생산. action 없음. */
|
|
111
290
|
export declare function transformationEvent(p: {
|
|
112
291
|
eventTime: string;
|
|
@@ -120,6 +299,6 @@ export declare function transformationEvent(p: {
|
|
|
120
299
|
readPoint?: string;
|
|
121
300
|
bizLocation?: string;
|
|
122
301
|
bizTransactionList?: BizTransactionElement[];
|
|
123
|
-
}): EpcisTransformationEvent;
|
|
302
|
+
} & EpcisHeaderOptions): EpcisTransformationEvent;
|
|
124
303
|
export declare function validateEpcisEvent(e: EpcisEvent): string[];
|
|
125
304
|
export {};
|
package/dist/epcis.js
CHANGED
|
@@ -23,23 +23,118 @@ export const DISP = {
|
|
|
23
23
|
export function ssccUri(companyPrefix, serial) {
|
|
24
24
|
return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, '0')}`;
|
|
25
25
|
}
|
|
26
|
-
/** 비직렬 수량용 SGTIN 패턴(epcClass) — quantityList 에 사용. */
|
|
26
|
+
/** 비직렬 수량용 SGTIN 패턴(epcClass) — 로트를 따지지 않는 품목 클래스. quantityList 에 사용. */
|
|
27
27
|
export function sgtinClass(companyPrefix, itemRef) {
|
|
28
28
|
return `urn:epc:idpat:sgtin:${companyPrefix}.${itemRef}.*`;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 우리가 방출하는 개체·로트 마스터데이터의 속성 이름 — **한 곳에 모은다.**
|
|
32
|
+
*
|
|
33
|
+
* 표준은 이 이름들을 정의하지 않는다(§7.3.8: 상위 문서 소관). 그래서 값을 실으려면 이름을 정해야 하는데,
|
|
34
|
+
* 흩뿌리면 곧 방언이 된다. GS1 CBV 마스터데이터 이름공간(`urn:epcglobal:cbv:mda:`)의 정식 명칭을
|
|
35
|
+
* 확인하면 **이 상수만 바꾸면 되도록** 여기 모아 둔다.
|
|
36
|
+
*
|
|
37
|
+
* ⚠ 아래 이름은 **아직 정본 확인 전**이다(CBV 마스터데이터 문서 필요). 받는 경로는 이름과 무관하게
|
|
38
|
+
* 동작하므로(받은 것을 잃지 않고 통과) 이 상수는 우리가 **생산할 때**만 쓰인다.
|
|
39
|
+
*/
|
|
40
|
+
export const ILMD_ATTR = {
|
|
41
|
+
/** 유통기한·만료(로트 단위). */
|
|
42
|
+
expiry: 'cbvmda:itemExpirationDate',
|
|
43
|
+
/** 로트·배치 번호(직렬 개체에 로트를 붙일 때). */
|
|
44
|
+
lot: 'cbvmda:lotNumber'
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* 품번 + 로트 클래스(LGTIN) — 낱개 일련번호가 없고 **로트로 관리**하는 자재의 표준 식별자.
|
|
48
|
+
*
|
|
49
|
+
* 원자재·화학·식품이 이 경우다. 로트가 식별자의 한 마디로 들어가므로 "로트별 재고" 가 별도 필드 없이
|
|
50
|
+
* epcClass 별 집계가 된다. 개체가 아니라 클래스이므로 `epcList` 가 아니라 `quantityList` 에 쓴다.
|
|
51
|
+
*
|
|
52
|
+
* 문법(EPC Tag Data Standard 2.1.0 §6.4.1):
|
|
53
|
+
* urn:epc:class:lgtin:CompanyPrefix.ItemRefAndIndicator.Lot
|
|
54
|
+
* 예) urn:epc:class:lgtin:4012345.012345.998877
|
|
55
|
+
* 두 숫자 마디의 자릿수 합은 13(점 제외), Lot 은 GS3A3Component(URI 이스케이프 허용).
|
|
56
|
+
*/
|
|
57
|
+
export function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
|
|
58
|
+
return `urn:epc:class:lgtin:${companyPrefix}.${itemRefAndIndicator}.${encodeURIComponent(lot)}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* EPC/클래스 식별자 파서 — **표준 지식이라 커널이 소유한다.**
|
|
62
|
+
*
|
|
63
|
+
* 소비처가 `uri.split(':').pop()` 으로 자르면 규칙이 갈라진다(실제로 화면이 LGTIN 의 마지막 마디인
|
|
64
|
+
* 로트를 SKU 이름으로 표시할 위험이 있었다). 뜯는 일은 여기 한 곳에서 한다.
|
|
65
|
+
*/
|
|
66
|
+
export function parseEpc(uri) {
|
|
67
|
+
const raw = String(uri ?? '');
|
|
68
|
+
const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
|
|
69
|
+
if (cls) {
|
|
70
|
+
const seg = cls[1].split('.');
|
|
71
|
+
/* LGTIN 은 숫자 두 마디 + 로트. 로트에 점이 들어갈 수 있으므로 앞 둘만 떼고 나머지를 로트로. */
|
|
72
|
+
const lot = seg.slice(2).join('.');
|
|
73
|
+
return {
|
|
74
|
+
scheme: 'lgtin',
|
|
75
|
+
instance: false,
|
|
76
|
+
gtinKey: seg.slice(0, 2).join('.'),
|
|
77
|
+
lot: lot ? decodeURIComponent(lot) : undefined,
|
|
78
|
+
uri: raw
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const pat = raw.match(/^urn:epc:idpat:sgtin:(.+)$/);
|
|
82
|
+
if (pat) {
|
|
83
|
+
const seg = pat[1].split('.');
|
|
84
|
+
return { scheme: 'idpat', instance: false, gtinKey: seg.slice(0, 2).join('.'), uri: raw };
|
|
85
|
+
}
|
|
86
|
+
const id = raw.match(/^urn:epc:id:([a-z]+):(.+)$/);
|
|
87
|
+
if (id) {
|
|
88
|
+
const scheme = id[1];
|
|
89
|
+
const seg = id[2].split('.');
|
|
90
|
+
const known = ['sgtin', 'sscc', 'gdti', 'grai', 'giai', 'sgln'].includes(scheme);
|
|
91
|
+
return {
|
|
92
|
+
scheme: known ? scheme : 'unknown',
|
|
93
|
+
instance: true,
|
|
94
|
+
...(scheme === 'sgtin' ? { gtinKey: seg.slice(0, 2).join('.'), serial: seg[2] } : { serial: seg.slice(1).join('.') }),
|
|
95
|
+
uri: raw
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { scheme: 'unknown', instance: false, uri: raw };
|
|
99
|
+
}
|
|
30
100
|
/** 거래문서 식별자 = GDTI (PO/SO/WO/어포인트먼트 등). */
|
|
31
101
|
export function gdtiUri(companyPrefix, docType, serial) {
|
|
32
102
|
return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
|
|
33
103
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
104
|
+
function header(type, eventTime, bizStep, opts) {
|
|
105
|
+
const h = {
|
|
106
|
+
'@context': EPCIS_CONTEXT,
|
|
107
|
+
type,
|
|
108
|
+
eventTime,
|
|
109
|
+
eventTimeZoneOffset: UTC_OFFSET,
|
|
110
|
+
bizStep
|
|
111
|
+
};
|
|
112
|
+
/* 값이 없으면 키를 만들지 않는다 — 표준 이벤트에 빈 필드를 싣지 않기 위해. */
|
|
113
|
+
if (opts?.eventID)
|
|
114
|
+
h.eventID = opts.eventID;
|
|
115
|
+
if (opts?.recordTime)
|
|
116
|
+
h.recordTime = opts.recordTime;
|
|
117
|
+
if (opts?.errorDeclaration)
|
|
118
|
+
h.errorDeclaration = opts.errorDeclaration;
|
|
119
|
+
if (opts?.ilmd)
|
|
120
|
+
h.ilmd = opts.ilmd;
|
|
121
|
+
if (opts?.sourceList)
|
|
122
|
+
h.sourceList = opts.sourceList;
|
|
123
|
+
if (opts?.destinationList)
|
|
124
|
+
h.destinationList = opts.destinationList;
|
|
125
|
+
if (opts?.persistentDisposition)
|
|
126
|
+
h.persistentDisposition = opts.persistentDisposition;
|
|
127
|
+
if (opts?.sensorElementList)
|
|
128
|
+
h.sensorElementList = opts.sensorElementList;
|
|
129
|
+
if (opts?.certificationInfo)
|
|
130
|
+
h.certificationInfo = opts.certificationInfo;
|
|
131
|
+
return h;
|
|
37
132
|
}
|
|
38
|
-
function common(type, eventTime, action, bizStep) {
|
|
39
|
-
return { ...header(type, eventTime, bizStep), action };
|
|
133
|
+
function common(type, eventTime, action, bizStep, opts) {
|
|
134
|
+
return { ...header(type, eventTime, bizStep, opts), action };
|
|
40
135
|
}
|
|
41
136
|
export function objectEvent(p) {
|
|
42
|
-
const e = { ...common('ObjectEvent', p.eventTime, p.action, p.bizStep), epcList: p.epcList };
|
|
137
|
+
const e = { ...common('ObjectEvent', p.eventTime, p.action, p.bizStep, p), epcList: p.epcList };
|
|
43
138
|
if (p.disposition)
|
|
44
139
|
e.disposition = p.disposition;
|
|
45
140
|
if (p.quantityList)
|
|
@@ -53,7 +148,7 @@ export function objectEvent(p) {
|
|
|
53
148
|
return e;
|
|
54
149
|
}
|
|
55
150
|
export function aggregationEvent(p) {
|
|
56
|
-
const e = { ...common('AggregationEvent', p.eventTime, p.action, p.bizStep), parentID: p.parentID };
|
|
151
|
+
const e = { ...common('AggregationEvent', p.eventTime, p.action, p.bizStep, p), parentID: p.parentID };
|
|
57
152
|
if (p.disposition)
|
|
58
153
|
e.disposition = p.disposition;
|
|
59
154
|
if (p.childEPCs)
|
|
@@ -68,7 +163,7 @@ export function aggregationEvent(p) {
|
|
|
68
163
|
}
|
|
69
164
|
export function transactionEvent(p) {
|
|
70
165
|
const e = {
|
|
71
|
-
...common('TransactionEvent', p.eventTime, p.action, p.bizStep),
|
|
166
|
+
...common('TransactionEvent', p.eventTime, p.action, p.bizStep, p),
|
|
72
167
|
bizTransactionList: p.bizTransactionList
|
|
73
168
|
};
|
|
74
169
|
if (p.disposition)
|
|
@@ -87,7 +182,7 @@ export function transactionEvent(p) {
|
|
|
87
182
|
}
|
|
88
183
|
/** 변환(제조) — 입력 EPC/수량 소비 → 출력 EPC/수량 생산. action 없음. */
|
|
89
184
|
export function transformationEvent(p) {
|
|
90
|
-
const e = header('TransformationEvent', p.eventTime, p.bizStep);
|
|
185
|
+
const e = header('TransformationEvent', p.eventTime, p.bizStep, p);
|
|
91
186
|
if (p.disposition)
|
|
92
187
|
e.disposition = p.disposition;
|
|
93
188
|
if (p.inputEPCList)
|
|
@@ -126,6 +221,58 @@ export function validateEpcisEvent(e) {
|
|
|
126
221
|
v.push('eventTimeZoneOffset 형식 오류');
|
|
127
222
|
if (typeof e.bizStep !== 'string' || !e.bizStep)
|
|
128
223
|
v.push('bizStep 누락');
|
|
224
|
+
/* 선택 헤더 — 있으면 형식을 지킨다(없는 것은 위반이 아니다). */
|
|
225
|
+
if (e.eventID !== undefined && (typeof e.eventID !== 'string' || !e.eventID))
|
|
226
|
+
v.push('eventID 가 빈 문자열');
|
|
227
|
+
if (e.recordTime !== undefined && (typeof e.recordTime !== 'string' || !ISO_RE.test(e.recordTime))) {
|
|
228
|
+
v.push('recordTime ISO8601 아님');
|
|
229
|
+
}
|
|
230
|
+
/* 개체·로트 마스터데이터는 **생겨나는 순간에만** 실린다(§7.3.8) — ObjectEvent(ADD) · Transformation.
|
|
231
|
+
* 아무 이벤트에나 허용하면 "생애 동안 정적" 이라는 성질이 깨지고 이벤트마다 값이 갈라진다. */
|
|
232
|
+
if (e.ilmd !== undefined) {
|
|
233
|
+
const allowed = (e.type === 'ObjectEvent' && e.action === 'ADD') || e.type === 'TransformationEvent';
|
|
234
|
+
if (!allowed)
|
|
235
|
+
v.push('ilmd 는 ObjectEvent(action=ADD) 또는 TransformationEvent 에만 실을 수 있다');
|
|
236
|
+
}
|
|
237
|
+
/* 이전 당사자 — 종류와 당사자가 둘 다 있어야 뜻이 된다(한쪽만 있으면 누가/무엇이 빈다). */
|
|
238
|
+
for (const sd of e.sourceList ?? []) {
|
|
239
|
+
if (!sd?.type || !sd?.source)
|
|
240
|
+
v.push('sourceList 항목에 type 또는 source 누락');
|
|
241
|
+
}
|
|
242
|
+
for (const sd of e.destinationList ?? []) {
|
|
243
|
+
if (!sd?.type || !sd?.destination)
|
|
244
|
+
v.push('destinationList 항목에 type 또는 destination 누락');
|
|
245
|
+
}
|
|
246
|
+
/* 지속 상태 — 같은 조건을 동시에 설정하고 해제하면 뜻이 모순된다. */
|
|
247
|
+
if (e.persistentDisposition !== undefined) {
|
|
248
|
+
const set = e.persistentDisposition.set ?? [];
|
|
249
|
+
const unset = e.persistentDisposition.unset ?? [];
|
|
250
|
+
if (!set.length && !unset.length)
|
|
251
|
+
v.push('persistentDisposition 이 set·unset 둘 다 비어있음');
|
|
252
|
+
const both = set.filter(x => unset.includes(x));
|
|
253
|
+
if (both.length)
|
|
254
|
+
v.push(`persistentDisposition 이 같은 값을 set·unset 동시 지정: ${both.join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
/* 센서 — 관측 보고가 하나 이상 있어야 한다(표준: SHALL contain one or more sensorReport). */
|
|
257
|
+
for (const se of e.sensorElementList ?? []) {
|
|
258
|
+
if (!Array.isArray(se?.sensorReport) || se.sensorReport.length === 0) {
|
|
259
|
+
v.push('sensorElement 에 sensorReport 가 하나도 없음');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (e.errorDeclaration !== undefined) {
|
|
263
|
+
const d = e.errorDeclaration;
|
|
264
|
+
/* 정정인데 언제 정정했는지 모르면 정정 순서를 판정할 수 없다 → 표준 필수. */
|
|
265
|
+
if (typeof d?.declarationTime !== 'string' || !ISO_RE.test(d.declarationTime)) {
|
|
266
|
+
v.push('errorDeclaration.declarationTime ISO8601 아님/누락');
|
|
267
|
+
}
|
|
268
|
+
if (d?.correctiveEventIDs !== undefined) {
|
|
269
|
+
if (!Array.isArray(d.correctiveEventIDs))
|
|
270
|
+
v.push('errorDeclaration.correctiveEventIDs 배열 아님');
|
|
271
|
+
else if (d.correctiveEventIDs.some(x => typeof x !== 'string' || !x)) {
|
|
272
|
+
v.push('errorDeclaration.correctiveEventIDs 에 빈 값');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
129
276
|
if (e.type === 'ObjectEvent') {
|
|
130
277
|
if (!ACTIONS.includes(e.action))
|
|
131
278
|
v.push(`action 부정: ${e.action}`);
|
|
@@ -163,10 +310,26 @@ export function validateEpcisEvent(e) {
|
|
|
163
310
|
];
|
|
164
311
|
for (const list of qtyLists)
|
|
165
312
|
for (const q of list ?? []) {
|
|
313
|
+
/* 클래스 식별자만 — 개체(urn:epc:id:)는 epcList 의 몫이다. */
|
|
166
314
|
if (!q.epcClass?.startsWith('urn:epc:idpat:') && !q.epcClass?.startsWith('urn:epc:class:'))
|
|
167
315
|
v.push(`quantity epcClass 부정: ${q.epcClass}`);
|
|
168
|
-
|
|
169
|
-
|
|
316
|
+
/* 세 경우(EPCIS 2.0 §7.3.3.1) — 느슨하게 통과시키면 "모름" 과 "0" 이 섞인다. */
|
|
317
|
+
const hasQty = q.quantity !== undefined && q.quantity !== null;
|
|
318
|
+
if (!hasQty) {
|
|
319
|
+
/* 수량 미지정 = "모른다". 그때 단위만 남아 있으면 뜻이 없다 → 표준은 생략을 요구한다. */
|
|
320
|
+
if (q.uom !== undefined)
|
|
321
|
+
v.push('quantity 없으면 uom 도 없어야 한다(수량 미지정)');
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (typeof q.quantity !== 'number' || !Number.isFinite(q.quantity) || q.quantity <= 0) {
|
|
325
|
+
v.push('quantity 는 양수여야 한다(모르면 생략)');
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
/* uom 없으면 개수 → 정수여야 한다. uom 있으면 물리량 → 소수 허용. */
|
|
329
|
+
if (q.uom === undefined && !Number.isInteger(q.quantity))
|
|
330
|
+
v.push('uom 없는 quantity 는 정수(개수)여야 한다');
|
|
331
|
+
if (q.uom !== undefined && !/^[A-Z0-9]{2,3}$/.test(q.uom))
|
|
332
|
+
v.push(`uom 형식 부정(UN/CEFACT 권고 20 의 2~3자 코드): ${q.uom}`);
|
|
170
333
|
}
|
|
171
334
|
return v;
|
|
172
335
|
}
|