@things-factory/ops-master 10.0.18

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.
Files changed (181) hide show
  1. package/.dockerignore +31 -0
  2. package/dist-client/index.d.ts +1 -0
  3. package/dist-client/index.js +2 -0
  4. package/dist-client/index.js.map +1 -0
  5. package/dist-client/pages/master-axes.d.ts +33 -0
  6. package/dist-client/pages/master-axes.js +176 -0
  7. package/dist-client/pages/master-axes.js.map +1 -0
  8. package/dist-client/pages/master-list.d.ts +68 -0
  9. package/dist-client/pages/master-list.js +267 -0
  10. package/dist-client/pages/master-list.js.map +1 -0
  11. package/dist-client/route.d.ts +1 -0
  12. package/dist-client/route.js +8 -0
  13. package/dist-client/route.js.map +1 -0
  14. package/dist-client/tsconfig.tsbuildinfo +1 -0
  15. package/dist-server/index.d.ts +1 -0
  16. package/dist-server/index.js +5 -0
  17. package/dist-server/index.js.map +1 -0
  18. package/dist-server/service/asset/asset-lifecycle.d.ts +83 -0
  19. package/dist-server/service/asset/asset-lifecycle.js +99 -0
  20. package/dist-server/service/asset/asset-lifecycle.js.map +1 -0
  21. package/dist-server/service/asset/asset-master-resolver.d.ts +44 -0
  22. package/dist-server/service/asset/asset-master-resolver.js +140 -0
  23. package/dist-server/service/asset/asset-master-resolver.js.map +1 -0
  24. package/dist-server/service/asset/asset-master.d.ts +21 -0
  25. package/dist-server/service/asset/asset-master.js +25 -0
  26. package/dist-server/service/asset/asset-master.js.map +1 -0
  27. package/dist-server/service/asset/asset-query.d.ts +8 -0
  28. package/dist-server/service/asset/asset-query.js +50 -0
  29. package/dist-server/service/asset/asset-query.js.map +1 -0
  30. package/dist-server/service/asset/index.d.ts +6 -0
  31. package/dist-server/service/asset/index.js +10 -0
  32. package/dist-server/service/asset/index.js.map +1 -0
  33. package/dist-server/service/asset/physical-asset-class.d.ts +34 -0
  34. package/dist-server/service/asset/physical-asset-class.js +104 -0
  35. package/dist-server/service/asset/physical-asset-class.js.map +1 -0
  36. package/dist-server/service/asset/physical-asset.d.ts +53 -0
  37. package/dist-server/service/asset/physical-asset.js +123 -0
  38. package/dist-server/service/asset/physical-asset.js.map +1 -0
  39. package/dist-server/service/equipment/equipment-class.d.ts +34 -0
  40. package/dist-server/service/equipment/equipment-class.js +104 -0
  41. package/dist-server/service/equipment/equipment-class.js.map +1 -0
  42. package/dist-server/service/equipment/equipment-master-resolver.d.ts +60 -0
  43. package/dist-server/service/equipment/equipment-master-resolver.js +200 -0
  44. package/dist-server/service/equipment/equipment-master-resolver.js.map +1 -0
  45. package/dist-server/service/equipment/equipment-master.d.ts +29 -0
  46. package/dist-server/service/equipment/equipment-master.js +36 -0
  47. package/dist-server/service/equipment/equipment-master.js.map +1 -0
  48. package/dist-server/service/equipment/equipment-query.d.ts +8 -0
  49. package/dist-server/service/equipment/equipment-query.js +50 -0
  50. package/dist-server/service/equipment/equipment-query.js.map +1 -0
  51. package/dist-server/service/equipment/equipment.d.ts +45 -0
  52. package/dist-server/service/equipment/equipment.js +133 -0
  53. package/dist-server/service/equipment/equipment.js.map +1 -0
  54. package/dist-server/service/equipment/index.d.ts +8 -0
  55. package/dist-server/service/equipment/index.js +12 -0
  56. package/dist-server/service/equipment/index.js.map +1 -0
  57. package/dist-server/service/equipment/state-period-rules.d.ts +76 -0
  58. package/dist-server/service/equipment/state-period-rules.js +88 -0
  59. package/dist-server/service/equipment/state-period-rules.js.map +1 -0
  60. package/dist-server/service/equipment/stop-reason-query.d.ts +11 -0
  61. package/dist-server/service/equipment/stop-reason-query.js +39 -0
  62. package/dist-server/service/equipment/stop-reason-query.js.map +1 -0
  63. package/dist-server/service/equipment/stop-reason.d.ts +58 -0
  64. package/dist-server/service/equipment/stop-reason.js +114 -0
  65. package/dist-server/service/equipment/stop-reason.js.map +1 -0
  66. package/dist-server/service/index.d.ts +46 -0
  67. package/dist-server/service/index.js +80 -0
  68. package/dist-server/service/index.js.map +1 -0
  69. package/dist-server/service/location/index.d.ts +5 -0
  70. package/dist-server/service/location/index.js +9 -0
  71. package/dist-server/service/location/index.js.map +1 -0
  72. package/dist-server/service/location/location-master-resolver.d.ts +28 -0
  73. package/dist-server/service/location/location-master-resolver.js +80 -0
  74. package/dist-server/service/location/location-master-resolver.js.map +1 -0
  75. package/dist-server/service/location/location-master.d.ts +24 -0
  76. package/dist-server/service/location/location-master.js +27 -0
  77. package/dist-server/service/location/location-master.js.map +1 -0
  78. package/dist-server/service/location/location-query.d.ts +7 -0
  79. package/dist-server/service/location/location-query.js +41 -0
  80. package/dist-server/service/location/location-query.js.map +1 -0
  81. package/dist-server/service/location/operational-location.d.ts +52 -0
  82. package/dist-server/service/location/operational-location.js +132 -0
  83. package/dist-server/service/location/operational-location.js.map +1 -0
  84. package/dist-server/service/master-axis.d.ts +29 -0
  85. package/dist-server/service/master-axis.js +39 -0
  86. package/dist-server/service/master-axis.js.map +1 -0
  87. package/dist-server/service/master-crud.d.ts +44 -0
  88. package/dist-server/service/master-crud.js +73 -0
  89. package/dist-server/service/master-crud.js.map +1 -0
  90. package/dist-server/service/master-list.d.ts +12 -0
  91. package/dist-server/service/master-list.js +24 -0
  92. package/dist-server/service/master-list.js.map +1 -0
  93. package/dist-server/service/material/index.d.ts +6 -0
  94. package/dist-server/service/material/index.js +10 -0
  95. package/dist-server/service/material/index.js.map +1 -0
  96. package/dist-server/service/material/material-class.d.ts +34 -0
  97. package/dist-server/service/material/material-class.js +104 -0
  98. package/dist-server/service/material/material-class.js.map +1 -0
  99. package/dist-server/service/material/material-definition.d.ts +42 -0
  100. package/dist-server/service/material/material-definition.js +112 -0
  101. package/dist-server/service/material/material-definition.js.map +1 -0
  102. package/dist-server/service/material/material-master-resolver.d.ts +44 -0
  103. package/dist-server/service/material/material-master-resolver.js +140 -0
  104. package/dist-server/service/material/material-master-resolver.js.map +1 -0
  105. package/dist-server/service/material/material-master.d.ts +9 -0
  106. package/dist-server/service/material/material-master.js +24 -0
  107. package/dist-server/service/material/material-master.js.map +1 -0
  108. package/dist-server/service/material/material-query.d.ts +8 -0
  109. package/dist-server/service/material/material-query.js +50 -0
  110. package/dist-server/service/material/material-query.js.map +1 -0
  111. package/dist-server/service/operation/index.d.ts +5 -0
  112. package/dist-server/service/operation/index.js +9 -0
  113. package/dist-server/service/operation/index.js.map +1 -0
  114. package/dist-server/service/operation/operation-definition.d.ts +52 -0
  115. package/dist-server/service/operation/operation-definition.js +142 -0
  116. package/dist-server/service/operation/operation-definition.js.map +1 -0
  117. package/dist-server/service/operation/operation-master-resolver.d.ts +28 -0
  118. package/dist-server/service/operation/operation-master-resolver.js +80 -0
  119. package/dist-server/service/operation/operation-master-resolver.js.map +1 -0
  120. package/dist-server/service/operation/operation-master.d.ts +21 -0
  121. package/dist-server/service/operation/operation-master.js +28 -0
  122. package/dist-server/service/operation/operation-master.js.map +1 -0
  123. package/dist-server/service/operation/operation-query.d.ts +7 -0
  124. package/dist-server/service/operation/operation-query.js +41 -0
  125. package/dist-server/service/operation/operation-query.js.map +1 -0
  126. package/dist-server/service/personnel/index.d.ts +6 -0
  127. package/dist-server/service/personnel/index.js +10 -0
  128. package/dist-server/service/personnel/index.js.map +1 -0
  129. package/dist-server/service/personnel/person.d.ts +51 -0
  130. package/dist-server/service/personnel/person.js +119 -0
  131. package/dist-server/service/personnel/person.js.map +1 -0
  132. package/dist-server/service/personnel/personnel-class.d.ts +54 -0
  133. package/dist-server/service/personnel/personnel-class.js +114 -0
  134. package/dist-server/service/personnel/personnel-class.js.map +1 -0
  135. package/dist-server/service/personnel/personnel-master-resolver.d.ts +44 -0
  136. package/dist-server/service/personnel/personnel-master-resolver.js +140 -0
  137. package/dist-server/service/personnel/personnel-master-resolver.js.map +1 -0
  138. package/dist-server/service/personnel/personnel-master.d.ts +39 -0
  139. package/dist-server/service/personnel/personnel-master.js +34 -0
  140. package/dist-server/service/personnel/personnel-master.js.map +1 -0
  141. package/dist-server/service/personnel/personnel-query.d.ts +21 -0
  142. package/dist-server/service/personnel/personnel-query.js +57 -0
  143. package/dist-server/service/personnel/personnel-query.js.map +1 -0
  144. package/dist-server/service/personnel/qualification.d.ts +97 -0
  145. package/dist-server/service/personnel/qualification.js +122 -0
  146. package/dist-server/service/personnel/qualification.js.map +1 -0
  147. package/dist-server/service/resource/resource-class.d.ts +25 -0
  148. package/dist-server/service/resource/resource-class.js +29 -0
  149. package/dist-server/service/resource/resource-class.js.map +1 -0
  150. package/dist-server/service/status/axis-stamp.d.ts +16 -0
  151. package/dist-server/service/status/axis-stamp.js +22 -0
  152. package/dist-server/service/status/axis-stamp.js.map +1 -0
  153. package/dist-server/service/status/index.d.ts +3 -0
  154. package/dist-server/service/status/index.js +7 -0
  155. package/dist-server/service/status/index.js.map +1 -0
  156. package/dist-server/service/status/master-status.d.ts +45 -0
  157. package/dist-server/service/status/master-status.js +129 -0
  158. package/dist-server/service/status/master-status.js.map +1 -0
  159. package/dist-server/service/test-spec/index.d.ts +5 -0
  160. package/dist-server/service/test-spec/index.js +9 -0
  161. package/dist-server/service/test-spec/index.js.map +1 -0
  162. package/dist-server/service/test-spec/test-spec-master-resolver.d.ts +28 -0
  163. package/dist-server/service/test-spec/test-spec-master-resolver.js +80 -0
  164. package/dist-server/service/test-spec/test-spec-master-resolver.js.map +1 -0
  165. package/dist-server/service/test-spec/test-spec-master.d.ts +6 -0
  166. package/dist-server/service/test-spec/test-spec-master.js +18 -0
  167. package/dist-server/service/test-spec/test-spec-master.js.map +1 -0
  168. package/dist-server/service/test-spec/test-spec-query.d.ts +7 -0
  169. package/dist-server/service/test-spec/test-spec-query.js +41 -0
  170. package/dist-server/service/test-spec/test-spec-query.js.map +1 -0
  171. package/dist-server/service/test-spec/test-specification.d.ts +41 -0
  172. package/dist-server/service/test-spec/test-specification.js +106 -0
  173. package/dist-server/service/test-spec/test-specification.js.map +1 -0
  174. package/dist-server/tsconfig.tsbuildinfo +1 -0
  175. package/package.json +50 -0
  176. package/things-factory.config.js +12 -0
  177. package/translations/en.json +53 -0
  178. package/translations/ja.json +1 -0
  179. package/translations/ko.json +53 -0
  180. package/translations/ms.json +1 -0
  181. package/translations/zh.json +1 -0
@@ -0,0 +1,97 @@
1
+ import type { ResourceClassDef, TestResult } from '@operato/ops-contract';
2
+ /**
3
+ * 이 사람이 이 공정을 할 자격이 있나 — **ISA-95 `PersonnelSpecification` 을 실제로 거는 자리.**
4
+ *
5
+ * 공정은 등급으로 요구하고(`OperationDefinition.personnelSpecification`), 등급은 검사를 요구하고
6
+ * (`PersonnelClass.testSpecificationIds`), 사람은 결과를 든다(`Person.testResults`). 세 자리가 다
7
+ * 있었는데 그것을 잇는 판단이 없어서, 마스터에 적힌 자격이 아무 일도 하지 않고 있었다.
8
+ *
9
+ * ── 기록이 없을 때의 판단이 커널과 반대다 ──────────────────────────────────
10
+ * 계약의 `meetsTests` 는 결과가 선언되지 않은 검사를 **제약으로 보지 않는다**(`contract.ts` §meetsTests).
11
+ * 트윈에서는 그것이 맞다 — 모델 데이터가 성긴데 기록이 없다고 다 막으면 라인이 굶는다.
12
+ *
13
+ * 실행 시스템에서는 반대다. 「이 사람의 용접 자격 기록이 없다」는 통과 사유가 아니라 막을 사유다.
14
+ * 그래서 등급 상속(`classClosure`) · 필수 검사 수집(`requiredTestsFor`) · 만료 판정(`testPassedAt`) 은
15
+ * 계약 것을 그대로 쓰고, **없을 때의 판단만 여기서 뒤집는다.** 커널 함수는 고치지 않는다.
16
+ *
17
+ * ── 왜 이유를 종류별로 돌려주나 ────────────────────────────────────────────
18
+ * 막기만 하고 무엇이 모자란지 말하지 않는 화면은 현장에서 우회 대상이 된다. 자격이 없는 것과 만료된
19
+ * 것은 할 일이 다르다 — 앞은 사람을 바꾸고, 뒤는 갱신하면 된다.
20
+ */
21
+ /** 무엇이 모자란가. 종류마다 현장이 할 일이 다르다. */
22
+ export type QualificationLack =
23
+ /** 그 등급에 속하지 않는다 — 다른 사람을 부르는 수밖에 없다. */
24
+ 'not-in-class'
25
+ /** 등급이 요구하는 검사의 기록이 없다 — 받았는지 확인하고 기록한다. */
26
+ | 'test-missing'
27
+ /** 불합격 기록이다. */
28
+ | 'test-failed'
29
+ /** 합격했으나 유효기간이 지났다 — 갱신하면 된다. */
30
+ | 'test-expired'
31
+ /** 아직 현장에 없거나(입사 전) 이미 없다(퇴사). */
32
+ | 'not-effective'
33
+ /**
34
+ * 그 이름이 인원 마스터에 없다 — 자격을 따질 수가 없다.
35
+ *
36
+ * 「자격 없음」으로 내면 안 된다. 자격이 없는 것과 그 사람이 누구인지 모르는 것은 할 일이 다르다
37
+ * (앞은 사람을 바꾸고, 뒤는 등록하거나 이름을 고친다).
38
+ */
39
+ | 'not-registered';
40
+ export interface QualificationGap {
41
+ /** 누가 — `Person.name`. */
42
+ personName: string;
43
+ /** 어느 등급을 요구했나 — `PersonnelClass.name`. */
44
+ classId: string;
45
+ lack: QualificationLack;
46
+ /** 검사 때문이면 어느 검사인가 — `TestSpecification.id`. */
47
+ specId?: string;
48
+ /** 만료된 것이면 언제 만료됐나(ISO). */
49
+ expiredAt?: string;
50
+ }
51
+ /** 등급별로 몇 명이 필요한데 몇 명이 자격을 갖췄나. */
52
+ export interface QualificationShortfall {
53
+ classId: string;
54
+ required: number;
55
+ qualified: number;
56
+ }
57
+ export interface QualificationVerdict {
58
+ qualified: boolean;
59
+ gaps: QualificationGap[];
60
+ shortfalls: QualificationShortfall[];
61
+ }
62
+ /** 판단에 필요한 사람의 사실만. 엔티티를 통째로 받지 않는다 — 그러면 테스트가 DB 를 끌고 온다. */
63
+ export interface QualifiablePerson {
64
+ name: string;
65
+ personnelClassIds?: string[];
66
+ testResults?: TestResult[];
67
+ effectiveStart?: Date | string;
68
+ effectiveEnd?: Date | string;
69
+ }
70
+ /** ISA-95 `PersonnelSpecification` — 등급 하나와 그 등급으로 몇 명이 필요한가. */
71
+ export interface PersonnelSpecificationEntry {
72
+ personnelClassId: string;
73
+ quantity?: number;
74
+ }
75
+ /**
76
+ * 선언된 필요 인원을 읽는다.
77
+ *
78
+ * `OperationDefinition.personnelSpecification` 은 JSON 칸이라 모양이 보장되지 않는다. 등급 이름이 없는
79
+ * 줄은 **버리지 않고 걸러 낸다** — 그런 줄로 요구를 만들면 아무도 만족시킬 수 없는 공정이 된다.
80
+ * 수량이 없으면 1명으로 읽는다(ISA-95 `Quantity` 는 선택 항목이고, 요구했다는 것 자체가 최소 한 명이다).
81
+ */
82
+ export declare function personnelSpecificationOf(raw: readonly Record<string, unknown>[] | undefined): PersonnelSpecificationEntry[];
83
+ /**
84
+ * 이 사람이 이 등급의 자격을 갖췄나. 모자라면 **무엇이** 모자란지 돌려준다.
85
+ *
86
+ * 상속을 탄다 — 「6축 용접 자격」을 가진 사람은 「용접 자격」을 요구하는 공정에 선다. 그 상속은
87
+ * 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.
88
+ */
89
+ export declare function qualificationGapsFor(person: QualifiablePerson, classId: string, classDefs: readonly ResourceClassDef[], at: string): QualificationGap[];
90
+ /**
91
+ * 이 사람들로 이 공정을 돌릴 수 있나.
92
+ *
93
+ * 등급마다 수량을 센다. 한 사람이 여러 등급을 만족시킬 수 있으므로 **등급별로 따로 센다** — 「용접
94
+ * 1명 · 지게차 1명」을 두 자격을 가진 한 사람으로 채울 수 있는지는 현장이 정할 일이지 여기서 정할
95
+ * 일이 아니다. 여기서는 각 요구가 채워졌는지만 본다.
96
+ */
97
+ export declare function judgePersonnel(spec: readonly PersonnelSpecificationEntry[], persons: readonly QualifiablePerson[], classDefs: readonly ResourceClassDef[], at: string): QualificationVerdict;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.personnelSpecificationOf = personnelSpecificationOf;
4
+ exports.qualificationGapsFor = qualificationGapsFor;
5
+ exports.judgePersonnel = judgePersonnel;
6
+ const ops_contract_1 = require("@operato/ops-contract");
7
+ /**
8
+ * 선언된 필요 인원을 읽는다.
9
+ *
10
+ * `OperationDefinition.personnelSpecification` 은 JSON 칸이라 모양이 보장되지 않는다. 등급 이름이 없는
11
+ * 줄은 **버리지 않고 걸러 낸다** — 그런 줄로 요구를 만들면 아무도 만족시킬 수 없는 공정이 된다.
12
+ * 수량이 없으면 1명으로 읽는다(ISA-95 `Quantity` 는 선택 항목이고, 요구했다는 것 자체가 최소 한 명이다).
13
+ */
14
+ function personnelSpecificationOf(raw) {
15
+ if (!raw?.length) {
16
+ return [];
17
+ }
18
+ const entries = [];
19
+ for (const row of raw) {
20
+ const classId = row?.personnelClassId ?? row?.personnelClassID ?? row?.classId;
21
+ if (typeof classId !== 'string' || !classId) {
22
+ continue;
23
+ }
24
+ const quantity = Number(row?.quantity);
25
+ entries.push({ personnelClassId: classId, quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1 });
26
+ }
27
+ return entries;
28
+ }
29
+ /**
30
+ * 이 사람이 이 등급의 자격을 갖췄나. 모자라면 **무엇이** 모자란지 돌려준다.
31
+ *
32
+ * 상속을 탄다 — 「6축 용접 자격」을 가진 사람은 「용접 자격」을 요구하는 공정에 선다. 그 상속은
33
+ * 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.
34
+ */
35
+ function qualificationGapsFor(person, classId, classDefs, at) {
36
+ const period = {
37
+ effectiveStart: isoOf(person.effectiveStart),
38
+ effectiveEnd: isoOf(person.effectiveEnd)
39
+ };
40
+ /* 유효기간 밖이면 자격을 볼 것도 없다 — 퇴사자의 용접 자격은 유효해도 배정 대상이 아니다. */
41
+ if ((0, ops_contract_1.effectivityAt)(period, at) !== undefined) {
42
+ return [{ personName: person.name, classId, lack: 'not-effective' }];
43
+ }
44
+ const closure = (0, ops_contract_1.classClosure)(person.personnelClassIds, classDefs, at);
45
+ if (!closure.has(classId)) {
46
+ return [{ personName: person.name, classId, lack: 'not-in-class' }];
47
+ }
48
+ const required = (0, ops_contract_1.requiredTestsFor)([classId], classDefs, at);
49
+ if (!required.length) {
50
+ return [];
51
+ }
52
+ const bySpec = new Map((person.testResults ?? []).map(result => [result.specId, result]));
53
+ const gaps = [];
54
+ for (const specId of required) {
55
+ const result = bySpec.get(specId);
56
+ /*
57
+ * 여기가 커널과 갈리는 한 줄이다. 계약은 없는 결과를 제약으로 보지 않고, 실행 시스템은 막는다.
58
+ * 기록이 없다는 것은 「자격이 있다」가 아니라 「모른다」이고, 모르는 채로 작업을 보내지 않는다.
59
+ */
60
+ if (!result) {
61
+ gaps.push({ personName: person.name, classId, lack: 'test-missing', specId });
62
+ continue;
63
+ }
64
+ if (result.result === 'fail') {
65
+ gaps.push({ personName: person.name, classId, lack: 'test-failed', specId });
66
+ continue;
67
+ }
68
+ /* 판정이 비어 있는 것도 통과가 아니다 — 「재기만 하고 판정하지 않았다」는 합격이 아니다. */
69
+ if (result.result !== 'pass') {
70
+ gaps.push({ personName: person.name, classId, lack: 'test-missing', specId });
71
+ continue;
72
+ }
73
+ if (!(0, ops_contract_1.testPassedAt)(result, at)) {
74
+ gaps.push({ personName: person.name, classId, lack: 'test-expired', specId, expiredAt: result.expiresAt });
75
+ }
76
+ }
77
+ return gaps;
78
+ }
79
+ /**
80
+ * 이 사람들로 이 공정을 돌릴 수 있나.
81
+ *
82
+ * 등급마다 수량을 센다. 한 사람이 여러 등급을 만족시킬 수 있으므로 **등급별로 따로 센다** — 「용접
83
+ * 1명 · 지게차 1명」을 두 자격을 가진 한 사람으로 채울 수 있는지는 현장이 정할 일이지 여기서 정할
84
+ * 일이 아니다. 여기서는 각 요구가 채워졌는지만 본다.
85
+ */
86
+ function judgePersonnel(spec, persons, classDefs, at) {
87
+ if (!spec.length) {
88
+ return { qualified: true, gaps: [], shortfalls: [] };
89
+ }
90
+ const gaps = [];
91
+ const shortfalls = [];
92
+ for (const { personnelClassId, quantity } of spec) {
93
+ const required = quantity ?? 1;
94
+ let count = 0;
95
+ for (const person of persons) {
96
+ const found = qualificationGapsFor(person, personnelClassId, classDefs, at);
97
+ if (!found.length) {
98
+ count += 1;
99
+ continue;
100
+ }
101
+ /*
102
+ * 그 등급에 아예 속하지 않는 사람은 이유로 내지 않는다. 실적에 이름이 다섯이면 그 중 넷은
103
+ * 다른 일을 하는 사람이고, 그들까지 「용접 자격 없음」으로 늘어놓으면 진짜 이유가 묻힌다.
104
+ */
105
+ if (found.some(gap => gap.lack === 'not-in-class')) {
106
+ continue;
107
+ }
108
+ gaps.push(...found);
109
+ }
110
+ if (count < required) {
111
+ shortfalls.push({ classId: personnelClassId, required, qualified: count });
112
+ }
113
+ }
114
+ return { qualified: shortfalls.length === 0, gaps, shortfalls };
115
+ }
116
+ function isoOf(at) {
117
+ if (!at) {
118
+ return undefined;
119
+ }
120
+ return typeof at === 'string' ? at : at.toISOString();
121
+ }
122
+ //# sourceMappingURL=qualification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qualification.js","sourceRoot":"","sources":["../../../server/service/personnel/qualification.ts"],"names":[],"mappings":"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","sourcesContent":["import { classClosure, effectivityAt, requiredTestsFor, testPassedAt } from '@operato/ops-contract'\nimport type { ResourceClassDef, TestResult } from '@operato/ops-contract'\n\n/**\n * 이 사람이 이 공정을 할 자격이 있나 — **ISA-95 `PersonnelSpecification` 을 실제로 거는 자리.**\n *\n * 공정은 등급으로 요구하고(`OperationDefinition.personnelSpecification`), 등급은 검사를 요구하고\n * (`PersonnelClass.testSpecificationIds`), 사람은 결과를 든다(`Person.testResults`). 세 자리가 다\n * 있었는데 그것을 잇는 판단이 없어서, 마스터에 적힌 자격이 아무 일도 하지 않고 있었다.\n *\n * ── 기록이 없을 때의 판단이 커널과 반대다 ──────────────────────────────────\n * 계약의 `meetsTests` 는 결과가 선언되지 않은 검사를 **제약으로 보지 않는다**(`contract.ts` §meetsTests).\n * 트윈에서는 그것이 맞다 — 모델 데이터가 성긴데 기록이 없다고 다 막으면 라인이 굶는다.\n *\n * 실행 시스템에서는 반대다. 「이 사람의 용접 자격 기록이 없다」는 통과 사유가 아니라 막을 사유다.\n * 그래서 등급 상속(`classClosure`) · 필수 검사 수집(`requiredTestsFor`) · 만료 판정(`testPassedAt`) 은\n * 계약 것을 그대로 쓰고, **없을 때의 판단만 여기서 뒤집는다.** 커널 함수는 고치지 않는다.\n *\n * ── 왜 이유를 종류별로 돌려주나 ────────────────────────────────────────────\n * 막기만 하고 무엇이 모자란지 말하지 않는 화면은 현장에서 우회 대상이 된다. 자격이 없는 것과 만료된\n * 것은 할 일이 다르다 — 앞은 사람을 바꾸고, 뒤는 갱신하면 된다.\n */\n\n/** 무엇이 모자란가. 종류마다 현장이 할 일이 다르다. */\nexport type QualificationLack =\n /** 그 등급에 속하지 않는다 — 다른 사람을 부르는 수밖에 없다. */\n | 'not-in-class'\n /** 등급이 요구하는 검사의 기록이 없다 — 받았는지 확인하고 기록한다. */\n | 'test-missing'\n /** 불합격 기록이다. */\n | 'test-failed'\n /** 합격했으나 유효기간이 지났다 — 갱신하면 된다. */\n | 'test-expired'\n /** 아직 현장에 없거나(입사 전) 이미 없다(퇴사). */\n | 'not-effective'\n /**\n * 그 이름이 인원 마스터에 없다 — 자격을 따질 수가 없다.\n *\n * 「자격 없음」으로 내면 안 된다. 자격이 없는 것과 그 사람이 누구인지 모르는 것은 할 일이 다르다\n * (앞은 사람을 바꾸고, 뒤는 등록하거나 이름을 고친다).\n */\n | 'not-registered'\n\nexport interface QualificationGap {\n /** 누가 — `Person.name`. */\n personName: string\n /** 어느 등급을 요구했나 — `PersonnelClass.name`. */\n classId: string\n lack: QualificationLack\n /** 검사 때문이면 어느 검사인가 — `TestSpecification.id`. */\n specId?: string\n /** 만료된 것이면 언제 만료됐나(ISO). */\n expiredAt?: string\n}\n\n/** 등급별로 몇 명이 필요한데 몇 명이 자격을 갖췄나. */\nexport interface QualificationShortfall {\n classId: string\n required: number\n qualified: number\n}\n\nexport interface QualificationVerdict {\n qualified: boolean\n gaps: QualificationGap[]\n shortfalls: QualificationShortfall[]\n}\n\n/** 판단에 필요한 사람의 사실만. 엔티티를 통째로 받지 않는다 — 그러면 테스트가 DB 를 끌고 온다. */\nexport interface QualifiablePerson {\n name: string\n personnelClassIds?: string[]\n testResults?: TestResult[]\n effectiveStart?: Date | string\n effectiveEnd?: Date | string\n}\n\n/** ISA-95 `PersonnelSpecification` — 등급 하나와 그 등급으로 몇 명이 필요한가. */\nexport interface PersonnelSpecificationEntry {\n personnelClassId: string\n quantity?: number\n}\n\n/**\n * 선언된 필요 인원을 읽는다.\n *\n * `OperationDefinition.personnelSpecification` 은 JSON 칸이라 모양이 보장되지 않는다. 등급 이름이 없는\n * 줄은 **버리지 않고 걸러 낸다** — 그런 줄로 요구를 만들면 아무도 만족시킬 수 없는 공정이 된다.\n * 수량이 없으면 1명으로 읽는다(ISA-95 `Quantity` 는 선택 항목이고, 요구했다는 것 자체가 최소 한 명이다).\n */\nexport function personnelSpecificationOf(raw: readonly Record<string, unknown>[] | undefined): PersonnelSpecificationEntry[] {\n if (!raw?.length) {\n return []\n }\n\n const entries: PersonnelSpecificationEntry[] = []\n\n for (const row of raw) {\n const classId = row?.personnelClassId ?? row?.personnelClassID ?? row?.classId\n if (typeof classId !== 'string' || !classId) {\n continue\n }\n\n const quantity = Number(row?.quantity)\n entries.push({ personnelClassId: classId, quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1 })\n }\n\n return entries\n}\n\n/**\n * 이 사람이 이 등급의 자격을 갖췄나. 모자라면 **무엇이** 모자란지 돌려준다.\n *\n * 상속을 탄다 — 「6축 용접 자격」을 가진 사람은 「용접 자격」을 요구하는 공정에 선다. 그 상속은\n * 계약의 `classClosure` 가 닫으므로 여기서 다시 만들지 않는다.\n */\nexport function qualificationGapsFor(\n person: QualifiablePerson,\n classId: string,\n classDefs: readonly ResourceClassDef[],\n at: string\n): QualificationGap[] {\n const period = {\n effectiveStart: isoOf(person.effectiveStart),\n effectiveEnd: isoOf(person.effectiveEnd)\n }\n\n /* 유효기간 밖이면 자격을 볼 것도 없다 — 퇴사자의 용접 자격은 유효해도 배정 대상이 아니다. */\n if (effectivityAt(period, at) !== undefined) {\n return [{ personName: person.name, classId, lack: 'not-effective' }]\n }\n\n const closure = classClosure(person.personnelClassIds, classDefs, at)\n if (!closure.has(classId)) {\n return [{ personName: person.name, classId, lack: 'not-in-class' }]\n }\n\n const required = requiredTestsFor([classId], classDefs, at)\n if (!required.length) {\n return []\n }\n\n const bySpec = new Map((person.testResults ?? []).map(result => [result.specId, result]))\n const gaps: QualificationGap[] = []\n\n for (const specId of required) {\n const result = bySpec.get(specId)\n\n /*\n * 여기가 커널과 갈리는 한 줄이다. 계약은 없는 결과를 제약으로 보지 않고, 실행 시스템은 막는다.\n * 기록이 없다는 것은 「자격이 있다」가 아니라 「모른다」이고, 모르는 채로 작업을 보내지 않는다.\n */\n if (!result) {\n gaps.push({ personName: person.name, classId, lack: 'test-missing', specId })\n continue\n }\n\n if (result.result === 'fail') {\n gaps.push({ personName: person.name, classId, lack: 'test-failed', specId })\n continue\n }\n\n /* 판정이 비어 있는 것도 통과가 아니다 — 「재기만 하고 판정하지 않았다」는 합격이 아니다. */\n if (result.result !== 'pass') {\n gaps.push({ personName: person.name, classId, lack: 'test-missing', specId })\n continue\n }\n\n if (!testPassedAt(result, at)) {\n gaps.push({ personName: person.name, classId, lack: 'test-expired', specId, expiredAt: result.expiresAt })\n }\n }\n\n return gaps\n}\n\n/**\n * 이 사람들로 이 공정을 돌릴 수 있나.\n *\n * 등급마다 수량을 센다. 한 사람이 여러 등급을 만족시킬 수 있으므로 **등급별로 따로 센다** — 「용접\n * 1명 · 지게차 1명」을 두 자격을 가진 한 사람으로 채울 수 있는지는 현장이 정할 일이지 여기서 정할\n * 일이 아니다. 여기서는 각 요구가 채워졌는지만 본다.\n */\nexport function judgePersonnel(\n spec: readonly PersonnelSpecificationEntry[],\n persons: readonly QualifiablePerson[],\n classDefs: readonly ResourceClassDef[],\n at: string\n): QualificationVerdict {\n if (!spec.length) {\n return { qualified: true, gaps: [], shortfalls: [] }\n }\n\n const gaps: QualificationGap[] = []\n const shortfalls: QualificationShortfall[] = []\n\n for (const { personnelClassId, quantity } of spec) {\n const required = quantity ?? 1\n let count = 0\n\n for (const person of persons) {\n const found = qualificationGapsFor(person, personnelClassId, classDefs, at)\n\n if (!found.length) {\n count += 1\n continue\n }\n\n /*\n * 그 등급에 아예 속하지 않는 사람은 이유로 내지 않는다. 실적에 이름이 다섯이면 그 중 넷은\n * 다른 일을 하는 사람이고, 그들까지 「용접 자격 없음」으로 늘어놓으면 진짜 이유가 묻힌다.\n */\n if (found.some(gap => gap.lack === 'not-in-class')) {\n continue\n }\n\n gaps.push(...found)\n }\n\n if (count < required) {\n shortfalls.push({ classId: personnelClassId, required, qualified: count })\n }\n }\n\n return { qualified: shortfalls.length === 0, gaps, shortfalls }\n}\n\nfunction isoOf(at: Date | string | undefined): string | undefined {\n if (!at) {\n return undefined\n }\n\n return typeof at === 'string' ? at : at.toISOString()\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import type { ResourceClassDef } from '@operato/ops-contract';
2
+ /**
3
+ * 등급을 트윈이 받는 모양으로 옮긴다 — **자원 셋이 같은 함수를 쓴다.**
4
+ *
5
+ * ISA-95 는 `PersonnelClass` · `EquipmentClass` · `PhysicalAssetClass` 를 따로 정의하지만 구조는 같다.
6
+ * 계약 층도 그것을 `ResourceClassDef` 하나로 갖는다. 그래서 옮기는 함수도 하나다 — 셋으로 나누면 한쪽만
7
+ * 고쳐지는 날이 온다.
8
+ *
9
+ * 표(엔티티)는 셋으로 나눈다. 등급은 자원 하나에만 속하고, 한 표에 담으면 인원 등급을 설비에 붙이는
10
+ * 일을 막을 방법이 없다.
11
+ */
12
+ /** 등급 엔티티가 공통으로 갖는 칸. 세 표가 같은 모양이다. */
13
+ export interface ResourceClassFields {
14
+ name: string;
15
+ description?: string;
16
+ baseIds?: string[];
17
+ testSpecificationIds?: string[];
18
+ properties?: Record<string, unknown>[];
19
+ effectiveStart?: Date;
20
+ effectiveEnd?: Date;
21
+ }
22
+ /** 빈 값은 칸을 만들지 않는다 — 빈 칸은 「비어 있다」와 「그런 것이 없다」를 섞는다. */
23
+ export declare function put<T>(target: Record<string, unknown>, key: string, value: T | undefined | null): void;
24
+ export declare function iso(at: Date | undefined): string | undefined;
25
+ export declare function toResourceClassDef(klass: ResourceClassFields): ResourceClassDef;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.put = put;
4
+ exports.iso = iso;
5
+ exports.toResourceClassDef = toResourceClassDef;
6
+ /** 빈 값은 칸을 만들지 않는다 — 빈 칸은 「비어 있다」와 「그런 것이 없다」를 섞는다. */
7
+ function put(target, key, value) {
8
+ if (value === undefined || value === null) {
9
+ return;
10
+ }
11
+ if (Array.isArray(value) && value.length === 0) {
12
+ return;
13
+ }
14
+ target[key] = value;
15
+ }
16
+ function iso(at) {
17
+ return at ? new Date(at).toISOString() : undefined;
18
+ }
19
+ function toResourceClassDef(klass) {
20
+ const out = { id: klass.name };
21
+ put(out, 'description', klass.description);
22
+ put(out, 'baseIds', klass.baseIds);
23
+ put(out, 'testSpecificationIds', klass.testSpecificationIds);
24
+ put(out, 'properties', klass.properties);
25
+ put(out, 'effectiveStart', iso(klass.effectiveStart));
26
+ put(out, 'effectiveEnd', iso(klass.effectiveEnd));
27
+ return out;
28
+ }
29
+ //# sourceMappingURL=resource-class.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-class.js","sourceRoot":"","sources":["../../../server/service/resource/resource-class.ts"],"names":[],"mappings":";;AAyBA,kBAQC;AAED,kBAEC;AAED,gDAWC;AA1BD,uDAAuD;AACvD,SAAgB,GAAG,CAAI,MAA+B,EAAE,GAAW,EAAE,KAA2B;IAC9F,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAM;IACR,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAM;IACR,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AACrB,CAAC;AAED,SAAgB,GAAG,CAAC,EAAoB;IACtC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACpD,CAAC;AAED,SAAgB,kBAAkB,CAAC,KAA0B;IAC3D,MAAM,GAAG,GAA4B,EAAE,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,CAAA;IAEvD,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IAC1C,GAAG,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IAClC,GAAG,CAAC,GAAG,EAAE,sBAAsB,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAAA;IAC5D,GAAG,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;IACxC,GAAG,CAAC,GAAG,EAAE,gBAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAA;IACrD,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAA;IAEjD,OAAO,GAAkC,CAAA;AAC3C,CAAC","sourcesContent":["import type { ResourceClassDef } from '@operato/ops-contract'\n\n/**\n * 등급을 트윈이 받는 모양으로 옮긴다 — **자원 셋이 같은 함수를 쓴다.**\n *\n * ISA-95 는 `PersonnelClass` · `EquipmentClass` · `PhysicalAssetClass` 를 따로 정의하지만 구조는 같다.\n * 계약 층도 그것을 `ResourceClassDef` 하나로 갖는다. 그래서 옮기는 함수도 하나다 — 셋으로 나누면 한쪽만\n * 고쳐지는 날이 온다.\n *\n * 표(엔티티)는 셋으로 나눈다. 등급은 자원 하나에만 속하고, 한 표에 담으면 인원 등급을 설비에 붙이는\n * 일을 막을 방법이 없다.\n */\n\n/** 등급 엔티티가 공통으로 갖는 칸. 세 표가 같은 모양이다. */\nexport interface ResourceClassFields {\n name: string\n description?: string\n baseIds?: string[]\n testSpecificationIds?: string[]\n properties?: Record<string, unknown>[]\n effectiveStart?: Date\n effectiveEnd?: Date\n}\n\n/** 빈 값은 칸을 만들지 않는다 — 빈 칸은 「비어 있다」와 「그런 것이 없다」를 섞는다. */\nexport function put<T>(target: Record<string, unknown>, key: string, value: T | undefined | null): void {\n if (value === undefined || value === null) {\n return\n }\n if (Array.isArray(value) && value.length === 0) {\n return\n }\n target[key] = value\n}\n\nexport function iso(at: Date | undefined): string | undefined {\n return at ? new Date(at).toISOString() : undefined\n}\n\nexport function toResourceClassDef(klass: ResourceClassFields): ResourceClassDef {\n const out: Record<string, unknown> = { id: klass.name }\n\n put(out, 'description', klass.description)\n put(out, 'baseIds', klass.baseIds)\n put(out, 'testSpecificationIds', klass.testSpecificationIds)\n put(out, 'properties', klass.properties)\n put(out, 'effectiveStart', iso(klass.effectiveStart))\n put(out, 'effectiveEnd', iso(klass.effectiveEnd))\n\n return out as unknown as ResourceClassDef\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 축의 상태를 한 값으로 — **크기로 비교하지 말고 같은지만 본다.**
3
+ *
4
+ * 시각을 크기로 비교하면 안 된다. 시계가 되감기면(백업 복원 · 시각 보정) 바뀐 것을 놓치고, 원천이
5
+ * 내림차순으로 주면 커서가 뒤로 처진다.
6
+ *
7
+ * 시각만으로는 같은 초에 두 번 바뀐 것을 놓치고, 개수만으로는 내용이 고쳐진 것을 놓친다. 둘을 함께 두면
8
+ * 어느 쪽이 바뀌어도 표시값은 달라진다.
9
+ *
10
+ * **뜻을 읽는 것이 아니다.** 모양에 기대지 말고 같은지만 볼 것.
11
+ *
12
+ * ── 왜 따로 두나 ────────────────────────────────────────────────────────────
13
+ * 데코레이터가 붙은 모듈은 소스를 그대로 실행하는 테스트에서 실행되지 않는다. 순수한 부분을 갈라 두면
14
+ * 그 부분은 테스트가 볼 수 있다 — 이 저장소에서 계약과 엔진을 나눈 것과 같은 규칙이다.
15
+ */
16
+ export declare function axisStamp(count: number, changedAt: Date | undefined): string;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.axisStamp = axisStamp;
4
+ /**
5
+ * 축의 상태를 한 값으로 — **크기로 비교하지 말고 같은지만 본다.**
6
+ *
7
+ * 시각을 크기로 비교하면 안 된다. 시계가 되감기면(백업 복원 · 시각 보정) 바뀐 것을 놓치고, 원천이
8
+ * 내림차순으로 주면 커서가 뒤로 처진다.
9
+ *
10
+ * 시각만으로는 같은 초에 두 번 바뀐 것을 놓치고, 개수만으로는 내용이 고쳐진 것을 놓친다. 둘을 함께 두면
11
+ * 어느 쪽이 바뀌어도 표시값은 달라진다.
12
+ *
13
+ * **뜻을 읽는 것이 아니다.** 모양에 기대지 말고 같은지만 볼 것.
14
+ *
15
+ * ── 왜 따로 두나 ────────────────────────────────────────────────────────────
16
+ * 데코레이터가 붙은 모듈은 소스를 그대로 실행하는 테스트에서 실행되지 않는다. 순수한 부분을 갈라 두면
17
+ * 그 부분은 테스트가 볼 수 있다 — 이 저장소에서 계약과 엔진을 나눈 것과 같은 규칙이다.
18
+ */
19
+ function axisStamp(count, changedAt) {
20
+ return `${count}:${changedAt ? changedAt.toISOString() : '-'}`;
21
+ }
22
+ //# sourceMappingURL=axis-stamp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"axis-stamp.js","sourceRoot":"","sources":["../../../server/service/status/axis-stamp.ts"],"names":[],"mappings":";;AAeA,8BAEC;AAjBD;;;;;;;;;;;;;;GAcG;AACH,SAAgB,SAAS,CAAC,KAAa,EAAE,SAA2B;IAClE,OAAO,GAAG,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;AAChE,CAAC","sourcesContent":["/**\n * 축의 상태를 한 값으로 — **크기로 비교하지 말고 같은지만 본다.**\n *\n * 시각을 크기로 비교하면 안 된다. 시계가 되감기면(백업 복원 · 시각 보정) 바뀐 것을 놓치고, 원천이\n * 내림차순으로 주면 커서가 뒤로 처진다.\n *\n * 시각만으로는 같은 초에 두 번 바뀐 것을 놓치고, 개수만으로는 내용이 고쳐진 것을 놓친다. 둘을 함께 두면\n * 어느 쪽이 바뀌어도 표시값은 달라진다.\n *\n * **뜻을 읽는 것이 아니다.** 모양에 기대지 말고 같은지만 볼 것.\n *\n * ── 왜 따로 두나 ────────────────────────────────────────────────────────────\n * 데코레이터가 붙은 모듈은 소스를 그대로 실행하는 테스트에서 실행되지 않는다. 순수한 부분을 갈라 두면\n * 그 부분은 테스트가 볼 수 있다 — 이 저장소에서 계약과 엔진을 나눈 것과 같은 규칙이다.\n */\nexport function axisStamp(count: number, changedAt: Date | undefined): string {\n return `${count}:${changedAt ? changedAt.toISOString() : '-'}`\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { OpsMasterStatusQuery } from './master-status';
2
+ export declare const entities: any[];
3
+ export declare const resolvers: (typeof OpsMasterStatusQuery)[];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvers = exports.entities = void 0;
4
+ const master_status_1 = require("./master-status");
5
+ exports.entities = [];
6
+ exports.resolvers = [master_status_1.OpsMasterStatusQuery];
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/status/index.ts"],"names":[],"mappings":";;;AAAA,mDAAsD;AAEzC,QAAA,QAAQ,GAAG,EAAE,CAAA;AACb,QAAA,SAAS,GAAG,CAAC,oCAAoB,CAAC,CAAA","sourcesContent":["import { OpsMasterStatusQuery } from './master-status'\n\nexport const entities = []\nexport const resolvers = [OpsMasterStatusQuery]\n"]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 축이 언제 바뀌었나 — **당길지 말지를 싸게 판단하는 자리.**
3
+ *
4
+ * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
5
+ * 트윈은 마스터를 주기적으로 당긴다. 그런데 인원 마스터가 수천 건이면 바뀐 것이 없어도 매번 그만큼을
6
+ * 옮기게 된다. 이 질의는 **축마다 마지막으로 바뀐 시각과 건수만** 내므로, 트윈이 자기가 마지막으로 본
7
+ * 값과 비교해 바뀐 축만 당길 수 있다.
8
+ *
9
+ * ── 왜 지우지 않는가 ────────────────────────────────────────────────────────
10
+ * 건수가 줄지 않는 것을 전제로 한다. 마스터에서 무엇을 「없앨」 때 행을 지우지 않고 유효 기간으로
11
+ * 끝내기 때문이다(§`effectiveEnd`). 지우면 그 개체를 가리키는 지난 기록이 가리킬 것을 잃는다.
12
+ */
13
+ export declare class MasterAxisStatus {
14
+ axis: string;
15
+ count: number;
16
+ /** 마지막으로 바뀐 시각. 축이 비어 있으면 없다 — 「비어 있다」와 「바뀐 적 없다」를 섞지 않는다. */
17
+ changedAt?: Date;
18
+ /**
19
+ * 이 축의 상태를 대표하는 값 하나 — **크기로 비교하지 말고 같은지만 본다.**
20
+ *
21
+ * 시각을 크기로 비교하면 시계가 되감길 때(백업 복원 · 시각 보정) 바뀐 것을 놓친다. 개수를 함께 담아
22
+ * 두므로 같은 초에 두 번 바뀌어도 표시가 달라진다. 읽는 쪽은 자기가 마지막으로 본 표시와 **다르면**
23
+ * 당기면 된다.
24
+ */
25
+ stamp: string;
26
+ }
27
+ export declare class OpsMasterStatus {
28
+ axes: MasterAxisStatus[];
29
+ /**
30
+ * 우리가 맞춘 계약 판 — 읽는 쪽이 자기 판과 비교한다.
31
+ *
32
+ * 앞선 판이면 당기지 않고 말하는 것이 맞다. 반쯤 읽은 마스터로 모델을 세우면 없던 것이 「없다」로
33
+ * 읽히고, 그것은 오류보고없이 틀린다.
34
+ */
35
+ contractVersion: string;
36
+ }
37
+ export declare class OpsMasterStatusQuery {
38
+ /**
39
+ * 모든 축의 변경 상태 — 트윈이 주기적으로 부르는 자리.
40
+ *
41
+ * 한 번에 낸다. 축마다 따로 물으면 일곱 번 오가고, 그 사이에 마스터가 바뀌면 축들이 서로 다른 시점을
42
+ * 보게 된다.
43
+ */
44
+ opsMasterStatus(context: ResolverContext): Promise<OpsMasterStatus>;
45
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpsMasterStatusQuery = exports.OpsMasterStatus = exports.MasterAxisStatus = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const shell_1 = require("@things-factory/shell");
7
+ const physical_asset_1 = require("../asset/physical-asset");
8
+ const physical_asset_class_1 = require("../asset/physical-asset-class");
9
+ const equipment_1 = require("../equipment/equipment");
10
+ const equipment_class_1 = require("../equipment/equipment-class");
11
+ const operational_location_1 = require("../location/operational-location");
12
+ const material_class_1 = require("../material/material-class");
13
+ const material_definition_1 = require("../material/material-definition");
14
+ const operation_definition_1 = require("../operation/operation-definition");
15
+ const person_1 = require("../personnel/person");
16
+ const personnel_class_1 = require("../personnel/personnel-class");
17
+ const test_specification_1 = require("../test-spec/test-specification");
18
+ const ops_contract_1 = require("@operato/ops-contract");
19
+ const axis_stamp_1 = require("./axis-stamp");
20
+ /**
21
+ * 축이 언제 바뀌었나 — **당길지 말지를 싸게 판단하는 자리.**
22
+ *
23
+ * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
24
+ * 트윈은 마스터를 주기적으로 당긴다. 그런데 인원 마스터가 수천 건이면 바뀐 것이 없어도 매번 그만큼을
25
+ * 옮기게 된다. 이 질의는 **축마다 마지막으로 바뀐 시각과 건수만** 내므로, 트윈이 자기가 마지막으로 본
26
+ * 값과 비교해 바뀐 축만 당길 수 있다.
27
+ *
28
+ * ── 왜 지우지 않는가 ────────────────────────────────────────────────────────
29
+ * 건수가 줄지 않는 것을 전제로 한다. 마스터에서 무엇을 「없앨」 때 행을 지우지 않고 유효 기간으로
30
+ * 끝내기 때문이다(§`effectiveEnd`). 지우면 그 개체를 가리키는 지난 기록이 가리킬 것을 잃는다.
31
+ */
32
+ let MasterAxisStatus = class MasterAxisStatus {
33
+ };
34
+ exports.MasterAxisStatus = MasterAxisStatus;
35
+ tslib_1.__decorate([
36
+ (0, type_graphql_1.Field)({ description: 'axis name, matching the pull query' }),
37
+ tslib_1.__metadata("design:type", String)
38
+ ], MasterAxisStatus.prototype, "axis", void 0);
39
+ tslib_1.__decorate([
40
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'number of rows in this axis' }),
41
+ tslib_1.__metadata("design:type", Number)
42
+ ], MasterAxisStatus.prototype, "count", void 0);
43
+ tslib_1.__decorate([
44
+ (0, type_graphql_1.Field)({ nullable: true, description: 'last change time, absent when the axis is empty' }),
45
+ tslib_1.__metadata("design:type", Date
46
+ /**
47
+ * 이 축의 상태를 대표하는 값 하나 — **크기로 비교하지 말고 같은지만 본다.**
48
+ *
49
+ * 시각을 크기로 비교하면 시계가 되감길 때(백업 복원 · 시각 보정) 바뀐 것을 놓친다. 개수를 함께 담아
50
+ * 두므로 같은 초에 두 번 바뀌어도 표시가 달라진다. 읽는 쪽은 자기가 마지막으로 본 표시와 **다르면**
51
+ * 당기면 된다.
52
+ */
53
+ )
54
+ ], MasterAxisStatus.prototype, "changedAt", void 0);
55
+ tslib_1.__decorate([
56
+ (0, type_graphql_1.Field)({ description: 'opaque value — compare for equality, never for order' }),
57
+ tslib_1.__metadata("design:type", String)
58
+ ], MasterAxisStatus.prototype, "stamp", void 0);
59
+ exports.MasterAxisStatus = MasterAxisStatus = tslib_1.__decorate([
60
+ (0, type_graphql_1.ObjectType)({ description: 'When one master axis last changed' })
61
+ ], MasterAxisStatus);
62
+ let OpsMasterStatus = class OpsMasterStatus {
63
+ };
64
+ exports.OpsMasterStatus = OpsMasterStatus;
65
+ tslib_1.__decorate([
66
+ (0, type_graphql_1.Field)(type => [MasterAxisStatus]),
67
+ tslib_1.__metadata("design:type", Array)
68
+ ], OpsMasterStatus.prototype, "axes", void 0);
69
+ tslib_1.__decorate([
70
+ (0, type_graphql_1.Field)({ description: 'version of @operato/ops-contract this master was written against' }),
71
+ tslib_1.__metadata("design:type", String)
72
+ ], OpsMasterStatus.prototype, "contractVersion", void 0);
73
+ exports.OpsMasterStatus = OpsMasterStatus = tslib_1.__decorate([
74
+ (0, type_graphql_1.ObjectType)({ description: 'Change status of every master axis' })
75
+ ], OpsMasterStatus);
76
+ /** 축 하나의 건수와 마지막 변경 시각. 두 값을 한 질의로 읽는다. */
77
+ async function axisStatus(axis, repository, domainId) {
78
+ const row = await repository
79
+ .createQueryBuilder('row')
80
+ .select('COUNT(1)', 'count')
81
+ .addSelect('MAX(row.updatedAt)', 'changedAt')
82
+ .where('row.domain = :domainId', { domainId })
83
+ .getRawOne();
84
+ const count = Number(row?.count ?? 0);
85
+ const changedAt = row?.changedAt ? new Date(row.changedAt) : undefined;
86
+ return {
87
+ axis,
88
+ count,
89
+ stamp: (0, axis_stamp_1.axisStamp)(count, changedAt),
90
+ ...(changedAt ? { changedAt } : {})
91
+ };
92
+ }
93
+ let OpsMasterStatusQuery = class OpsMasterStatusQuery {
94
+ /**
95
+ * 모든 축의 변경 상태 — 트윈이 주기적으로 부르는 자리.
96
+ *
97
+ * 한 번에 낸다. 축마다 따로 물으면 일곱 번 오가고, 그 사이에 마스터가 바뀌면 축들이 서로 다른 시점을
98
+ * 보게 된다.
99
+ */
100
+ async opsMasterStatus(context) {
101
+ const { domain } = context.state;
102
+ const axes = await Promise.all([
103
+ axisStatus('personnelClasses', (0, shell_1.getRepository)(personnel_class_1.PersonnelClass), domain.id),
104
+ axisStatus('persons', (0, shell_1.getRepository)(person_1.Person), domain.id),
105
+ axisStatus('equipmentClasses', (0, shell_1.getRepository)(equipment_class_1.EquipmentClass), domain.id),
106
+ axisStatus('equipment', (0, shell_1.getRepository)(equipment_1.Equipment), domain.id),
107
+ axisStatus('assetClasses', (0, shell_1.getRepository)(physical_asset_class_1.PhysicalAssetClass), domain.id),
108
+ axisStatus('assets', (0, shell_1.getRepository)(physical_asset_1.PhysicalAsset), domain.id),
109
+ axisStatus('locations', (0, shell_1.getRepository)(operational_location_1.OperationalLocation), domain.id),
110
+ axisStatus('materialClasses', (0, shell_1.getRepository)(material_class_1.MaterialClass), domain.id),
111
+ axisStatus('materialDefinitions', (0, shell_1.getRepository)(material_definition_1.MaterialDefinition), domain.id),
112
+ axisStatus('operations', (0, shell_1.getRepository)(operation_definition_1.OperationDefinition), domain.id),
113
+ axisStatus('testSpecifications', (0, shell_1.getRepository)(test_specification_1.TestSpecification), domain.id)
114
+ ]);
115
+ return { axes, contractVersion: ops_contract_1.CONTRACT_VERSION };
116
+ }
117
+ };
118
+ exports.OpsMasterStatusQuery = OpsMasterStatusQuery;
119
+ tslib_1.__decorate([
120
+ (0, type_graphql_1.Query)(returns => OpsMasterStatus, { description: 'Change status of every master axis' }),
121
+ tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
122
+ tslib_1.__metadata("design:type", Function),
123
+ tslib_1.__metadata("design:paramtypes", [Object]),
124
+ tslib_1.__metadata("design:returntype", Promise)
125
+ ], OpsMasterStatusQuery.prototype, "opsMasterStatus", null);
126
+ exports.OpsMasterStatusQuery = OpsMasterStatusQuery = tslib_1.__decorate([
127
+ (0, type_graphql_1.Resolver)(OpsMasterStatus)
128
+ ], OpsMasterStatusQuery);
129
+ //# sourceMappingURL=master-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"master-status.js","sourceRoot":"","sources":["../../../server/service/status/master-status.ts"],"names":[],"mappings":"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","sourcesContent":["import { Ctx, Field, Int, ObjectType, Query, Resolver } from 'type-graphql'\nimport type { ObjectLiteral, Repository } from 'typeorm'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { PhysicalAsset } from '../asset/physical-asset'\nimport { PhysicalAssetClass } from '../asset/physical-asset-class'\nimport { Equipment } from '../equipment/equipment'\nimport { EquipmentClass } from '../equipment/equipment-class'\nimport { OperationalLocation } from '../location/operational-location'\nimport { MaterialClass } from '../material/material-class'\nimport { MaterialDefinition } from '../material/material-definition'\nimport { OperationDefinition } from '../operation/operation-definition'\nimport { Person } from '../personnel/person'\nimport { PersonnelClass } from '../personnel/personnel-class'\nimport { TestSpecification } from '../test-spec/test-specification'\nimport { CONTRACT_VERSION } from '@operato/ops-contract'\n\nimport { axisStamp } from './axis-stamp'\n\n/**\n * 축이 언제 바뀌었나 — **당길지 말지를 싸게 판단하는 자리.**\n *\n * ── 왜 필요한가 ─────────────────────────────────────────────────────────────\n * 트윈은 마스터를 주기적으로 당긴다. 그런데 인원 마스터가 수천 건이면 바뀐 것이 없어도 매번 그만큼을\n * 옮기게 된다. 이 질의는 **축마다 마지막으로 바뀐 시각과 건수만** 내므로, 트윈이 자기가 마지막으로 본\n * 값과 비교해 바뀐 축만 당길 수 있다.\n *\n * ── 왜 지우지 않는가 ────────────────────────────────────────────────────────\n * 건수가 줄지 않는 것을 전제로 한다. 마스터에서 무엇을 「없앨」 때 행을 지우지 않고 유효 기간으로\n * 끝내기 때문이다(§`effectiveEnd`). 지우면 그 개체를 가리키는 지난 기록이 가리킬 것을 잃는다.\n */\n@ObjectType({ description: 'When one master axis last changed' })\nexport class MasterAxisStatus {\n @Field({ description: 'axis name, matching the pull query' })\n axis: string\n\n @Field(type => Int, { description: 'number of rows in this axis' })\n count: number\n\n /** 마지막으로 바뀐 시각. 축이 비어 있으면 없다 — 「비어 있다」와 「바뀐 적 없다」를 섞지 않는다. */\n @Field({ nullable: true, description: 'last change time, absent when the axis is empty' })\n changedAt?: Date\n\n /**\n * 이 축의 상태를 대표하는 값 하나 — **크기로 비교하지 말고 같은지만 본다.**\n *\n * 시각을 크기로 비교하면 시계가 되감길 때(백업 복원 · 시각 보정) 바뀐 것을 놓친다. 개수를 함께 담아\n * 두므로 같은 초에 두 번 바뀌어도 표시가 달라진다. 읽는 쪽은 자기가 마지막으로 본 표시와 **다르면**\n * 당기면 된다.\n */\n @Field({ description: 'opaque value — compare for equality, never for order' })\n stamp: string\n}\n\n@ObjectType({ description: 'Change status of every master axis' })\nexport class OpsMasterStatus {\n @Field(type => [MasterAxisStatus])\n axes: MasterAxisStatus[]\n\n /**\n * 우리가 맞춘 계약 판 — 읽는 쪽이 자기 판과 비교한다.\n *\n * 앞선 판이면 당기지 않고 말하는 것이 맞다. 반쯤 읽은 마스터로 모델을 세우면 없던 것이 「없다」로\n * 읽히고, 그것은 오류보고없이 틀린다.\n */\n @Field({ description: 'version of @operato/ops-contract this master was written against' })\n contractVersion: string\n}\n\n/** 축 하나의 건수와 마지막 변경 시각. 두 값을 한 질의로 읽는다. */\nasync function axisStatus<T extends ObjectLiteral>(\n axis: string,\n repository: Repository<T>,\n domainId: string\n): Promise<MasterAxisStatus> {\n const row = await repository\n .createQueryBuilder('row')\n .select('COUNT(1)', 'count')\n .addSelect('MAX(row.updatedAt)', 'changedAt')\n .where('row.domain = :domainId', { domainId })\n .getRawOne<{ count: string | number; changedAt: Date | string | null }>()\n\n const count = Number(row?.count ?? 0)\n const changedAt = row?.changedAt ? new Date(row.changedAt) : undefined\n\n return {\n axis,\n count,\n stamp: axisStamp(count, changedAt),\n ...(changedAt ? { changedAt } : {})\n }\n}\n\n\n@Resolver(OpsMasterStatus)\nexport class OpsMasterStatusQuery {\n /**\n * 모든 축의 변경 상태 — 트윈이 주기적으로 부르는 자리.\n *\n * 한 번에 낸다. 축마다 따로 물으면 일곱 번 오가고, 그 사이에 마스터가 바뀌면 축들이 서로 다른 시점을\n * 보게 된다.\n */\n @Query(returns => OpsMasterStatus, { description: 'Change status of every master axis' })\n async opsMasterStatus(@Ctx() context: ResolverContext): Promise<OpsMasterStatus> {\n const { domain } = context.state\n\n const axes = await Promise.all([\n axisStatus('personnelClasses', getRepository(PersonnelClass), domain.id),\n axisStatus('persons', getRepository(Person), domain.id),\n axisStatus('equipmentClasses', getRepository(EquipmentClass), domain.id),\n axisStatus('equipment', getRepository(Equipment), domain.id),\n axisStatus('assetClasses', getRepository(PhysicalAssetClass), domain.id),\n axisStatus('assets', getRepository(PhysicalAsset), domain.id),\n axisStatus('locations', getRepository(OperationalLocation), domain.id),\n axisStatus('materialClasses', getRepository(MaterialClass), domain.id),\n axisStatus('materialDefinitions', getRepository(MaterialDefinition), domain.id),\n axisStatus('operations', getRepository(OperationDefinition), domain.id),\n axisStatus('testSpecifications', getRepository(TestSpecification), domain.id)\n ])\n\n return { axes, contractVersion: CONTRACT_VERSION }\n }\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import { TestSpecification } from './test-specification';
2
+ import { TestSpecificationQuery } from './test-spec-query';
3
+ import { TestSpecificationResolver } from './test-spec-master-resolver';
4
+ export declare const entities: (typeof TestSpecification)[];
5
+ export declare const resolvers: (typeof TestSpecificationResolver | typeof TestSpecificationQuery)[];
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvers = exports.entities = void 0;
4
+ const test_specification_1 = require("./test-specification");
5
+ const test_spec_query_1 = require("./test-spec-query");
6
+ const test_spec_master_resolver_1 = require("./test-spec-master-resolver");
7
+ exports.entities = [test_specification_1.TestSpecification];
8
+ exports.resolvers = [test_spec_query_1.TestSpecificationQuery, test_spec_master_resolver_1.TestSpecificationResolver];
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/test-spec/index.ts"],"names":[],"mappings":";;;AAAA,6DAAwD;AACxD,uDAA0D;AAC1D,2EAAuE;AAE1D,QAAA,QAAQ,GAAG,CAAC,sCAAiB,CAAC,CAAA;AAC9B,QAAA,SAAS,GAAG,CAAC,wCAAsB,EAAE,qDAAyB,CAAC,CAAA","sourcesContent":["import { TestSpecification } from './test-specification'\nimport { TestSpecificationQuery } from './test-spec-query'\nimport { TestSpecificationResolver } from './test-spec-master-resolver'\n\nexport const entities = [TestSpecification]\nexport const resolvers = [TestSpecificationQuery, TestSpecificationResolver]\n"]}
@@ -0,0 +1,28 @@
1
+ import { ListParam } from '@things-factory/shell';
2
+ import { MasterPatch } from '../master-crud';
3
+ import { TestSpecification } from './test-specification';
4
+ /**
5
+ * 검사 명세 축을 채우고 고친다.
6
+ *
7
+ * ── 왜 마스터는 그리드에서 고칠 수 있나 ────────────────────────────────────
8
+ * 업무 기록과 달리 마스터는 **선언**이다. 「이 사람이 용접 자격을 갖는다」는 일어난 일이 아니라 지금의
9
+ * 규정이고, 규정은 바뀐다. 없앨 때 행을 지우지 않고 유효 기간의 끝을 채우는 것이 그래서다 —
10
+ * 지우면 그것을 가리키는 지난 기록이 가리킬 것을 잃는다.
11
+ *
12
+ * 규칙은 `master-crud.ts` 한 곳에 있다. 여기는 축의 이름만 붙인다.
13
+ */
14
+ export declare class TestSpecificationList {
15
+ items: TestSpecification[];
16
+ total: number;
17
+ }
18
+ export declare class TestSpecificationResolver {
19
+ testSpecifications(params: ListParam, context: ResolverContext): Promise<TestSpecificationList>;
20
+ /**
21
+ * 그리드가 보낸 변경을 한 번에 적용한다.
22
+ *
23
+ * 패치를 `ScalarObject` 로 받는다 — 축마다 입력 타입을 따로 만들면 칸을 하나 더할 때마다 두 곳을
24
+ * 고쳐야 하고, 한쪽을 빠뜨리면 그 칸이 오류 보고 없이 저장되지 않는다. 돌려주는 것은 타입이 있는
25
+ * 엔티티라 무엇이 저장됐는지 스키마로 확인할 수 있다.
26
+ */
27
+ updateMultipleTestSpecification(patches: MasterPatch[], context: ResolverContext): Promise<TestSpecification[]>;
28
+ }