@things-factory/ops-master 10.0.19 → 10.1.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.
Files changed (247) hide show
  1. package/dist-client/bootstrap.d.ts +18 -0
  2. package/dist-client/bootstrap.js +36 -0
  3. package/dist-client/bootstrap.js.map +1 -0
  4. package/dist-client/index.d.ts +15 -1
  5. package/dist-client/index.js +18 -1
  6. package/dist-client/index.js.map +1 -1
  7. package/dist-client/pages/master/equipment-class.d.ts +12 -0
  8. package/dist-client/pages/master/equipment-class.js +42 -0
  9. package/dist-client/pages/master/equipment-class.js.map +1 -0
  10. package/dist-client/pages/master/equipment.d.ts +12 -0
  11. package/dist-client/pages/master/equipment.js +57 -0
  12. package/dist-client/pages/master/equipment.js.map +1 -0
  13. package/dist-client/pages/master/grist-iso-duration.d.ts +20 -0
  14. package/dist-client/pages/master/grist-iso-duration.js +70 -0
  15. package/dist-client/pages/master/grist-iso-duration.js.map +1 -0
  16. package/dist-client/pages/master/grist-reference.d.ts +91 -0
  17. package/dist-client/pages/master/grist-reference.js +419 -0
  18. package/dist-client/pages/master/grist-reference.js.map +1 -0
  19. package/dist-client/pages/master/import-rows.d.ts +35 -0
  20. package/dist-client/pages/master/import-rows.js +66 -0
  21. package/dist-client/pages/master/import-rows.js.map +1 -0
  22. package/dist-client/pages/master/iso-duration.d.ts +34 -0
  23. package/dist-client/pages/master/iso-duration.js +76 -0
  24. package/dist-client/pages/master/iso-duration.js.map +1 -0
  25. package/dist-client/pages/master/master-options.d.ts +33 -0
  26. package/dist-client/pages/master/master-options.js +47 -0
  27. package/dist-client/pages/master/master-options.js.map +1 -0
  28. package/dist-client/pages/master/master-page-base.d.ts +357 -0
  29. package/dist-client/pages/master/master-page-base.js +626 -0
  30. package/dist-client/pages/master/master-page-base.js.map +1 -0
  31. package/dist-client/pages/master/material-class.d.ts +12 -0
  32. package/dist-client/pages/master/material-class.js +42 -0
  33. package/dist-client/pages/master/material-class.js.map +1 -0
  34. package/dist-client/pages/master/material-definition.d.ts +12 -0
  35. package/dist-client/pages/master/material-definition.js +62 -0
  36. package/dist-client/pages/master/material-definition.js.map +1 -0
  37. package/dist-client/pages/master/operation-definition.d.ts +12 -0
  38. package/dist-client/pages/master/operation-definition.js +49 -0
  39. package/dist-client/pages/master/operation-definition.js.map +1 -0
  40. package/dist-client/pages/master/operational-location.d.ts +84 -0
  41. package/dist-client/pages/master/operational-location.js +426 -0
  42. package/dist-client/pages/master/operational-location.js.map +1 -0
  43. package/dist-client/pages/master/person.d.ts +12 -0
  44. package/dist-client/pages/master/person.js +41 -0
  45. package/dist-client/pages/master/person.js.map +1 -0
  46. package/dist-client/pages/master/personnel-class.d.ts +12 -0
  47. package/dist-client/pages/master/personnel-class.js +43 -0
  48. package/dist-client/pages/master/personnel-class.js.map +1 -0
  49. package/dist-client/pages/master/physical-asset-class.d.ts +12 -0
  50. package/dist-client/pages/master/physical-asset-class.js +40 -0
  51. package/dist-client/pages/master/physical-asset-class.js.map +1 -0
  52. package/dist-client/pages/master/physical-asset.d.ts +12 -0
  53. package/dist-client/pages/master/physical-asset.js +41 -0
  54. package/dist-client/pages/master/physical-asset.js.map +1 -0
  55. package/dist-client/pages/master/recipe-definition.d.ts +136 -0
  56. package/dist-client/pages/master/recipe-definition.js +404 -0
  57. package/dist-client/pages/master/recipe-definition.js.map +1 -0
  58. package/dist-client/pages/master/reference-targets.d.ts +42 -0
  59. package/dist-client/pages/master/reference-targets.js +82 -0
  60. package/dist-client/pages/master/reference-targets.js.map +1 -0
  61. package/dist-client/pages/master/route-definition.d.ts +19 -0
  62. package/dist-client/pages/master/route-definition.js +40 -0
  63. package/dist-client/pages/master/route-definition.js.map +1 -0
  64. package/dist-client/pages/master/stop-reason.d.ts +12 -0
  65. package/dist-client/pages/master/stop-reason.js +48 -0
  66. package/dist-client/pages/master/stop-reason.js.map +1 -0
  67. package/dist-client/pages/master/test-specification.d.ts +93 -0
  68. package/dist-client/pages/master/test-specification.js +449 -0
  69. package/dist-client/pages/master/test-specification.js.map +1 -0
  70. package/dist-client/pages/master-axes.d.ts +20 -29
  71. package/dist-client/pages/master-axes.js +32 -182
  72. package/dist-client/pages/master-axes.js.map +1 -1
  73. package/dist-client/route.d.ts +1 -1
  74. package/dist-client/route.js +76 -3
  75. package/dist-client/route.js.map +1 -1
  76. package/dist-client/tsconfig.tsbuildinfo +1 -1
  77. package/dist-server/service/asset/asset-lifecycle.d.ts +30 -7
  78. package/dist-server/service/asset/asset-lifecycle.js +33 -6
  79. package/dist-server/service/asset/asset-lifecycle.js.map +1 -1
  80. package/dist-server/service/asset/asset-master-resolver.d.ts +9 -0
  81. package/dist-server/service/asset/asset-master-resolver.js +41 -0
  82. package/dist-server/service/asset/asset-master-resolver.js.map +1 -1
  83. package/dist-server/service/asset/asset-master.js +1 -1
  84. package/dist-server/service/asset/asset-master.js.map +1 -1
  85. package/dist-server/service/asset/asset-query.d.ts +9 -1
  86. package/dist-server/service/asset/asset-query.js +28 -6
  87. package/dist-server/service/asset/asset-query.js.map +1 -1
  88. package/dist-server/service/asset/physical-asset-class.js +1 -1
  89. package/dist-server/service/asset/physical-asset-class.js.map +1 -1
  90. package/dist-server/service/asset/physical-asset.d.ts +17 -4
  91. package/dist-server/service/asset/physical-asset.js +6 -6
  92. package/dist-server/service/asset/physical-asset.js.map +1 -1
  93. package/dist-server/service/equipment/equipment-class.js +1 -1
  94. package/dist-server/service/equipment/equipment-class.js.map +1 -1
  95. package/dist-server/service/equipment/equipment-master-resolver.d.ts +9 -0
  96. package/dist-server/service/equipment/equipment-master-resolver.js +58 -0
  97. package/dist-server/service/equipment/equipment-master-resolver.js.map +1 -1
  98. package/dist-server/service/equipment/equipment-master.d.ts +3 -2
  99. package/dist-server/service/equipment/equipment-master.js +21 -4
  100. package/dist-server/service/equipment/equipment-master.js.map +1 -1
  101. package/dist-server/service/equipment/equipment-query.d.ts +9 -1
  102. package/dist-server/service/equipment/equipment-query.js +28 -6
  103. package/dist-server/service/equipment/equipment-query.js.map +1 -1
  104. package/dist-server/service/equipment/equipment.d.ts +27 -1
  105. package/dist-server/service/equipment/equipment.js +8 -3
  106. package/dist-server/service/equipment/equipment.js.map +1 -1
  107. package/dist-server/service/equipment/state-period-rules.d.ts +19 -0
  108. package/dist-server/service/equipment/state-period-rules.js +35 -1
  109. package/dist-server/service/equipment/state-period-rules.js.map +1 -1
  110. package/dist-server/service/index.d.ts +4 -1
  111. package/dist-server/service/index.js +11 -2
  112. package/dist-server/service/index.js.map +1 -1
  113. package/dist-server/service/location/location-master-resolver.d.ts +29 -0
  114. package/dist-server/service/location/location-master-resolver.js +228 -1
  115. package/dist-server/service/location/location-master-resolver.js.map +1 -1
  116. package/dist-server/service/location/location-query.d.ts +9 -1
  117. package/dist-server/service/location/location-query.js +24 -9
  118. package/dist-server/service/location/location-query.js.map +1 -1
  119. package/dist-server/service/location/operational-location.d.ts +37 -0
  120. package/dist-server/service/location/operational-location.js +22 -2
  121. package/dist-server/service/location/operational-location.js.map +1 -1
  122. package/dist-server/service/master-axis.js +2 -2
  123. package/dist-server/service/master-axis.js.map +1 -1
  124. package/dist-server/service/master-conformance-resolver.d.ts +34 -0
  125. package/dist-server/service/master-conformance-resolver.js +143 -0
  126. package/dist-server/service/master-conformance-resolver.js.map +1 -0
  127. package/dist-server/service/master-conformance.d.ts +80 -0
  128. package/dist-server/service/master-conformance.js +266 -0
  129. package/dist-server/service/master-conformance.js.map +1 -0
  130. package/dist-server/service/master-page-query.d.ts +33 -0
  131. package/dist-server/service/master-page-query.js +55 -0
  132. package/dist-server/service/master-page-query.js.map +1 -0
  133. package/dist-server/service/master-page.d.ts +85 -0
  134. package/dist-server/service/master-page.js +104 -0
  135. package/dist-server/service/master-page.js.map +1 -0
  136. package/dist-server/service/material/material-class.js +1 -1
  137. package/dist-server/service/material/material-class.js.map +1 -1
  138. package/dist-server/service/material/material-definition.js +2 -2
  139. package/dist-server/service/material/material-definition.js.map +1 -1
  140. package/dist-server/service/material/material-master-resolver.d.ts +62 -0
  141. package/dist-server/service/material/material-master-resolver.js +213 -0
  142. package/dist-server/service/material/material-master-resolver.js.map +1 -1
  143. package/dist-server/service/material/material-query.d.ts +9 -1
  144. package/dist-server/service/material/material-query.js +28 -6
  145. package/dist-server/service/material/material-query.js.map +1 -1
  146. package/dist-server/service/material/recipe-index.d.ts +23 -0
  147. package/dist-server/service/material/recipe-index.js +44 -0
  148. package/dist-server/service/material/recipe-index.js.map +1 -0
  149. package/dist-server/service/operation/operation-definition.d.ts +1 -1
  150. package/dist-server/service/operation/operation-definition.js +4 -4
  151. package/dist-server/service/operation/operation-definition.js.map +1 -1
  152. package/dist-server/service/operation/operation-master-resolver.d.ts +24 -0
  153. package/dist-server/service/operation/operation-master-resolver.js +107 -0
  154. package/dist-server/service/operation/operation-master-resolver.js.map +1 -1
  155. package/dist-server/service/operation/operation-query.d.ts +10 -2
  156. package/dist-server/service/operation/operation-query.js +25 -10
  157. package/dist-server/service/operation/operation-query.js.map +1 -1
  158. package/dist-server/service/personnel/person.js +3 -3
  159. package/dist-server/service/personnel/person.js.map +1 -1
  160. package/dist-server/service/personnel/personnel-class.d.ts +1 -1
  161. package/dist-server/service/personnel/personnel-class.js +2 -2
  162. package/dist-server/service/personnel/personnel-class.js.map +1 -1
  163. package/dist-server/service/personnel/personnel-master-resolver.d.ts +9 -0
  164. package/dist-server/service/personnel/personnel-master-resolver.js +43 -0
  165. package/dist-server/service/personnel/personnel-master-resolver.js.map +1 -1
  166. package/dist-server/service/personnel/personnel-master.js +1 -1
  167. package/dist-server/service/personnel/personnel-master.js.map +1 -1
  168. package/dist-server/service/personnel/personnel-query.d.ts +9 -1
  169. package/dist-server/service/personnel/personnel-query.js +28 -6
  170. package/dist-server/service/personnel/personnel-query.js.map +1 -1
  171. package/dist-server/service/recipe/recipe-definition.d.ts +3 -3
  172. package/dist-server/service/recipe/recipe-definition.js +3 -3
  173. package/dist-server/service/recipe/recipe-definition.js.map +1 -1
  174. package/dist-server/service/recipe/recipe-master-resolver.d.ts +1 -1
  175. package/dist-server/service/recipe/recipe-master-resolver.js +1 -1
  176. package/dist-server/service/recipe/recipe-master-resolver.js.map +1 -1
  177. package/dist-server/service/recipe/recipe-master.d.ts +1 -1
  178. package/dist-server/service/recipe/recipe-master.js +2 -2
  179. package/dist-server/service/recipe/recipe-master.js.map +1 -1
  180. package/dist-server/service/recipe/recipe-query.d.ts +11 -3
  181. package/dist-server/service/recipe/recipe-query.js +30 -9
  182. package/dist-server/service/recipe/recipe-query.js.map +1 -1
  183. package/dist-server/service/recipe/route-definition.d.ts +6 -6
  184. package/dist-server/service/recipe/route-definition.js +6 -6
  185. package/dist-server/service/recipe/route-definition.js.map +1 -1
  186. package/dist-server/service/resource/class-lineage.d.ts +46 -0
  187. package/dist-server/service/resource/class-lineage.js +80 -0
  188. package/dist-server/service/resource/class-lineage.js.map +1 -0
  189. package/dist-server/service/resource/class-members.d.ts +36 -0
  190. package/dist-server/service/resource/class-members.js +50 -0
  191. package/dist-server/service/resource/class-members.js.map +1 -0
  192. package/dist-server/service/resource/referrers.d.ts +54 -0
  193. package/dist-server/service/resource/referrers.js +99 -0
  194. package/dist-server/service/resource/referrers.js.map +1 -0
  195. package/dist-server/service/test-spec/criteria-check.d.ts +53 -0
  196. package/dist-server/service/test-spec/criteria-check.js +142 -0
  197. package/dist-server/service/test-spec/criteria-check.js.map +1 -0
  198. package/dist-server/service/test-spec/criteria-rows.d.ts +89 -0
  199. package/dist-server/service/test-spec/criteria-rows.js +139 -0
  200. package/dist-server/service/test-spec/criteria-rows.js.map +1 -0
  201. package/dist-server/service/test-spec/test-spec-master-resolver.d.ts +33 -7
  202. package/dist-server/service/test-spec/test-spec-master-resolver.js +137 -2
  203. package/dist-server/service/test-spec/test-spec-master-resolver.js.map +1 -1
  204. package/dist-server/service/test-spec/test-spec-query.d.ts +9 -1
  205. package/dist-server/service/test-spec/test-spec-query.js +23 -9
  206. package/dist-server/service/test-spec/test-spec-query.js.map +1 -1
  207. package/dist-server/service/test-spec/test-specification.js +1 -1
  208. package/dist-server/service/test-spec/test-specification.js.map +1 -1
  209. package/dist-server/tsconfig.tsbuildinfo +1 -1
  210. package/helps/ops-master/equipment-class.ko.md +11 -0
  211. package/helps/ops-master/equipment-class.md +11 -0
  212. package/helps/ops-master/equipment.ko.md +11 -0
  213. package/helps/ops-master/equipment.md +11 -0
  214. package/helps/ops-master/material-class.ko.md +11 -0
  215. package/helps/ops-master/material-class.md +11 -0
  216. package/helps/ops-master/material-definition.ko.md +11 -0
  217. package/helps/ops-master/material-definition.md +11 -0
  218. package/helps/ops-master/operation-definition.ko.md +11 -0
  219. package/helps/ops-master/operation-definition.md +11 -0
  220. package/helps/ops-master/operational-location.ko.md +11 -0
  221. package/helps/ops-master/operational-location.md +11 -0
  222. package/helps/ops-master/person.ko.md +11 -0
  223. package/helps/ops-master/person.md +11 -0
  224. package/helps/ops-master/personnel-class.ko.md +11 -0
  225. package/helps/ops-master/personnel-class.md +11 -0
  226. package/helps/ops-master/physical-asset-class.ko.md +11 -0
  227. package/helps/ops-master/physical-asset-class.md +11 -0
  228. package/helps/ops-master/physical-asset.ko.md +11 -0
  229. package/helps/ops-master/physical-asset.md +11 -0
  230. package/helps/ops-master/recipe-definition.ko.md +11 -0
  231. package/helps/ops-master/recipe-definition.md +11 -0
  232. package/helps/ops-master/route-definition.ko.md +11 -0
  233. package/helps/ops-master/route-definition.md +11 -0
  234. package/helps/ops-master/stop-reason.ko.md +11 -0
  235. package/helps/ops-master/stop-reason.md +11 -0
  236. package/helps/ops-master/test-specification.ko.md +11 -0
  237. package/helps/ops-master/test-specification.md +11 -0
  238. package/package.json +5 -5
  239. package/things-factory.config.js +22 -2
  240. package/translations/en.json +86 -3
  241. package/translations/ja.json +92 -1
  242. package/translations/ko.json +93 -10
  243. package/translations/ms.json +92 -1
  244. package/translations/zh.json +92 -1
  245. package/dist-client/pages/master-list.d.ts +0 -68
  246. package/dist-client/pages/master-list.js +0 -239
  247. package/dist-client/pages/master-list.js.map +0 -1
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ /**
3
+ * 마스터가 **말이 되나** — 축을 가로질러 한 번에 본다.
4
+ *
5
+ * ── 왜 필요한가 ────────────────────────────────────────────────────────────
6
+ * 축마다 화면이 있고 축마다 값이 차 있는데, 그 사이가 이어지는지는 아무도 안 봤다. 설비 등급 다섯이
7
+ * 서 있고 설비 스물둘이 어느 등급에도 안 들어 있던 일이 그랬다 — 두 화면 다 멀쩡해 보였다.
8
+ * 카메라 자산 다섯이 ECU 등급에 들어 있던 것도, 정지 사유 고르기가 빈 목록으로 보이던 것도 같다.
9
+ *
10
+ * 사람이 열두 화면을 오가며 맞춰 보는 것으로는 안 된다. 맞춰 보지 않으면 아무도 모르고, 맞춰 봤는지
11
+ * 아닌지도 남지 않는다.
12
+ *
13
+ * ── 두 가지만 본다 ─────────────────────────────────────────────────────────
14
+ * **비어 있으면 안 되는 칸**과 **가리키는데 없는 것**이다. 둘 다 사실 판정이라 다툼이 없다.
15
+ * 「이 값이 옳은가」는 여기서 안 본다 — 그것은 현장이 정한다.
16
+ *
17
+ * ── 없는 것을 없다고 말한다 ────────────────────────────────────────────────
18
+ * 표가 통째로 비었으면 그것도 낸다. 「빠진 것 0건」과 「아직 아무것도 안 세웠다」는 다르고, 세지
19
+ * 않으면 둘이 같아 보인다.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.checkMaster = checkMaster;
23
+ exports.scannedCounts = scannedCounts;
24
+ exports.summarize = summarize;
25
+ exports.readMasterSnapshot = readMasterSnapshot;
26
+ const nameSet = (rows) => new Set(rows.map(one => one.name).filter(Boolean));
27
+ /** 값이 비었나 — 빈 문자열과 빈 배열도 빈 것으로 본다. */
28
+ function blank(value) {
29
+ if (value === null || value === undefined) {
30
+ return true;
31
+ }
32
+ if (Array.isArray(value)) {
33
+ return value.length === 0;
34
+ }
35
+ return String(value).trim() === '';
36
+ }
37
+ /** 이름 목록으로 만든다 — 한 개짜리 문자열도, 배열도 같게 다룬다. */
38
+ function asList(value) {
39
+ if (blank(value)) {
40
+ return [];
41
+ }
42
+ return (Array.isArray(value) ? value : [value]).map(one => String(one).trim()).filter(Boolean);
43
+ }
44
+ /**
45
+ * 축 하나를 잰다.
46
+ *
47
+ * `required` 는 비어 있으면 안 되는 칸, `refs` 는 「이 칸의 값이 저 이름들 안에 있어야 한다」이다.
48
+ */
49
+ function checkAxis(axis, rows, required, refs) {
50
+ const found = [];
51
+ if (rows.length === 0) {
52
+ /* 표가 통째로 비었다. 「빠진 것 0건」과 다르다. */
53
+ return [{ axis, kind: 'empty', field: axis }];
54
+ }
55
+ for (const row of rows) {
56
+ for (const field of required) {
57
+ if (blank(row[field])) {
58
+ found.push({ axis, subject: row.name, kind: 'missing', field });
59
+ }
60
+ }
61
+ for (const { field, within } of refs) {
62
+ for (const value of asList(row[field])) {
63
+ if (!within.has(value)) {
64
+ found.push({ axis, subject: row.name, kind: 'dangling', field, value });
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+ /** 아무도 안 가리키는 것들 — 세워 놓고 쓰지 않는 선언. */
72
+ function checkUnused(axis, rows, pointedAt, field) {
73
+ return rows
74
+ .filter(one => one.name && !pointedAt.has(one.name))
75
+ .map(one => ({ axis, subject: one.name, kind: 'unused', field }));
76
+ }
77
+ /**
78
+ * 마스터 전체를 잰다.
79
+ *
80
+ * **순서에 뜻이 없다.** 어느 축부터 보든 같은 답이 나와야 하고, 그래야 이 결과를 두 번 재서
81
+ * 견줄 수 있다.
82
+ */
83
+ function checkMaster(snapshot) {
84
+ const locations = nameSet(snapshot.operationalLocations);
85
+ const operations = nameSet(snapshot.operationDefinitions);
86
+ const routes = nameSet(snapshot.routeDefinitions);
87
+ const materials = nameSet(snapshot.materialDefinitions);
88
+ const specs = nameSet(snapshot.testSpecifications);
89
+ const personnelClasses = nameSet(snapshot.personnelClasses);
90
+ const equipmentClasses = nameSet(snapshot.equipmentClasses);
91
+ const assetClasses = nameSet(snapshot.physicalAssetClasses);
92
+ const materialClasses = nameSet(snapshot.materialClasses);
93
+ const stopReasons = nameSet(snapshot.stopReasons);
94
+ const found = [
95
+ ...checkAxis('person', snapshot.persons, ['personnelClassIds', 'homeLocation'], [
96
+ { field: 'personnelClassIds', within: personnelClasses },
97
+ { field: 'homeLocation', within: locations },
98
+ { field: 'testSpecificationIds', within: specs }
99
+ ]),
100
+ ...checkAxis('equipment', snapshot.equipment, ['kind', 'homeLocation', 'equipmentClassIds'], [
101
+ { field: 'equipmentClassIds', within: equipmentClasses },
102
+ { field: 'homeLocation', within: locations },
103
+ { field: 'testSpecificationIds', within: specs }
104
+ ]),
105
+ ...checkAxis('physicalAsset', snapshot.physicalAssets, ['assetClassIds'], [
106
+ { field: 'assetClassIds', within: assetClasses },
107
+ { field: 'homeLocation', within: locations },
108
+ { field: 'testSpecificationIds', within: specs }
109
+ ]),
110
+ ...checkAxis('materialDefinition', snapshot.materialDefinitions, ['materialClassIds'], [
111
+ { field: 'materialClassIds', within: materialClasses },
112
+ { field: 'testSpecificationIds', within: specs }
113
+ ]),
114
+ /*
115
+ * 공정의 `resourceType` 은 설비 **종류**를 가리키는 값이라 이름 집합이 없다. 여기서는
116
+ * 비었는지만 본다 — 없는 것을 있는 척 검사하지 않는다.
117
+ */
118
+ ...checkAxis('operationDefinition', snapshot.operationDefinitions, ['intent', 'locationType', 'duration'], []),
119
+ ...checkAxis('routeDefinition', snapshot.routeDefinitions, ['steps'], [
120
+ { field: 'steps', within: operations }
121
+ ]),
122
+ ...checkAxis('recipeDefinition', snapshot.recipeDefinitions, ['materialDefinitionName', 'route', 'inputs'], [
123
+ { field: 'materialDefinitionName', within: materials },
124
+ { field: 'route', within: routes }
125
+ ]),
126
+ ...checkAxis('operationalLocation', snapshot.operationalLocations, ['type', 'level'], [
127
+ { field: 'parentId', within: locations }
128
+ ]),
129
+ ...checkAxis('stopReason', snapshot.stopReasons, ['status'], [{ field: 'parentName', within: stopReasons }]),
130
+ ...checkAxis('testSpecification', snapshot.testSpecifications, ['criteria'], [])
131
+ ];
132
+ /*
133
+ * 제조법의 투입은 한 줄이 여럿을 가리킨다. 위의 검사는 칸 하나를 보므로 여기서 따로 본다.
134
+ */
135
+ for (const recipe of snapshot.recipeDefinitions) {
136
+ for (const part of recipe.inputs ?? []) {
137
+ if (part?.material && !materials.has(part.material)) {
138
+ found.push({ axis: 'recipeDefinition', subject: recipe.name, kind: 'dangling', field: 'inputs.material', value: part.material });
139
+ }
140
+ if (part?.operation && !operations.has(part.operation)) {
141
+ found.push({ axis: 'recipeDefinition', subject: recipe.name, kind: 'dangling', field: 'inputs.operation', value: part.operation });
142
+ }
143
+ }
144
+ }
145
+ /*
146
+ * ── 세워 놓고 아무도 안 든 등급 ─────────────────────────────────────────
147
+ *
148
+ * **상속을 타고 센다.** 등급은 등급을 상속하고(`baseIds`), 자격 판정도 그 사슬을 닫아서 본다
149
+ * (커널의 `classClosure`). 그러니 「아무도 안 든다」도 닫힌 소속으로 물어야 한다.
150
+ *
151
+ * 직접 소속만 세면 뿌리 등급이 늘 걸린다. 품목의 유형 등급(완제품 · 반제품 · 원재료)이 그렇다 —
152
+ * 품목은 성격 등급에 들고 그 등급이 유형을 상속하므로, 직접 소속은 언제나 0이다. 그것을 결함으로
153
+ * 읽으면 멀쩡한 등급을 지우러 간다.
154
+ *
155
+ * 처음에는 품목만 따로 다뤘고 나머지 셋은 직접 소속으로 셌다. 아키텍트 레인이 설비 등급에도
156
+ * 같은 축이 있다고 알려 주어 넷을 한 규칙으로 모았다 — 축마다 다르게 세면 한쪽만 고쳐지는 날이
157
+ * 온다. [재봤음] 2026-09-04
158
+ */
159
+ const classAxes = [
160
+ ['personnelClass', snapshot.personnelClasses, snapshot.persons, 'personnelClassIds'],
161
+ ['equipmentClass', snapshot.equipmentClasses, snapshot.equipment, 'equipmentClassIds'],
162
+ ['physicalAssetClass', snapshot.physicalAssetClasses, snapshot.physicalAssets, 'assetClassIds'],
163
+ ['materialClass', snapshot.materialClasses, snapshot.materialDefinitions, 'materialClassIds']
164
+ ];
165
+ for (const [axis, classes, members, field] of classAxes) {
166
+ found.push(...checkUnused(axis, classes, closedMembership(classes, members, field), 'members'));
167
+ }
168
+ /* 아무도 안 가리키는 라우팅·공정. 만들어 놓고 안 쓰는 것은 지워도 되는 것인지 사람이 판단한다. */
169
+ const routesUsed = new Set(snapshot.recipeDefinitions.map(one => one.route).filter(Boolean));
170
+ const operationsUsed = new Set([
171
+ ...snapshot.routeDefinitions.flatMap(one => asList(one.steps)),
172
+ ...snapshot.recipeDefinitions.flatMap(one => (one.inputs ?? []).map((part) => part?.operation).filter(Boolean))
173
+ ]);
174
+ found.push(...checkUnused('routeDefinition', snapshot.routeDefinitions, routesUsed, 'usedByRecipes'), ...checkUnused('operationDefinition', snapshot.operationDefinitions, operationsUsed, 'usedInRoutes'));
175
+ return found;
176
+ }
177
+ /**
178
+ * **닫힌 소속** — 소속이 상속을 타고 닿는 등급까지 모은다.
179
+ *
180
+ * 사람이 `welding-robot-6axis` 에 들고 그것이 `welding-equipment` 를 상속하면, 두 등급 다 「누가
181
+ * 들었다」로 센다. 자격 판정이 그 사슬을 닫아서 보므로(커널의 `classClosure`) 여기서도 같게 본다.
182
+ *
183
+ * **고리를 밟으면 멈춘다.** `baseIds` 는 화면에서 손으로 넣을 수 있어 A→B→A 가 생길 수 있다.
184
+ */
185
+ function closedMembership(classes, members, field) {
186
+ const baseOf = new Map(classes.map(one => [one.name, asList(one.baseIds)]));
187
+ const reached = new Set();
188
+ const walk = (name) => {
189
+ if (!name || reached.has(name)) {
190
+ return;
191
+ }
192
+ reached.add(name);
193
+ for (const base of baseOf.get(name) ?? []) {
194
+ walk(base);
195
+ }
196
+ };
197
+ for (const row of members) {
198
+ for (const name of asList(row[field])) {
199
+ walk(name);
200
+ }
201
+ }
202
+ return reached;
203
+ }
204
+ /** 여러 줄이 가리키는 이름들을 모은다. */
205
+ function collect(rows, field) {
206
+ return new Set(rows.flatMap(one => asList(one[field])));
207
+ }
208
+ /**
209
+ * **무엇을 몇 개 봤나.**
210
+ *
211
+ * 어긋남 0건이 「멀쩡하다」인지 「안 돌았다」인지는 이것으로만 갈린다. 본 수를 안 내면 두 답이
212
+ * 같은 글자로 나온다 — 검사기가 스스로를 못 믿게 만드는 가장 흔한 방식이다.
213
+ */
214
+ function scannedCounts(snapshot) {
215
+ return Object.entries(snapshot)
216
+ .map(([axis, rows]) => ({ axis, rows: rows.length }))
217
+ .sort((a, b) => a.axis.localeCompare(b.axis));
218
+ }
219
+ /** 축·종류별로 몇 건인지. 사람이 먼저 볼 것을 고르는 데 쓴다. */
220
+ function summarize(findings) {
221
+ const tally = new Map();
222
+ for (const one of findings) {
223
+ const key = `${one.axis}${one.kind}`;
224
+ tally.set(key, (tally.get(key) ?? 0) + 1);
225
+ }
226
+ return [...tally.entries()]
227
+ .map(([key, count]) => {
228
+ const [axis, kind] = key.split('');
229
+ return { axis, kind, count };
230
+ })
231
+ .sort((a, b) => b.count - a.count || a.axis.localeCompare(b.axis));
232
+ }
233
+ /**
234
+ * 한 도메인의 마스터를 통째로 읽어 잰다.
235
+ *
236
+ * **상한을 두지 않는다.** 정합성은 「전부 봤나」가 곧 답이라, 일부만 보고 「어긋남 0건」이라고 하면
237
+ * 그 답이 거짓이 된다. 그래서 이것은 목록 화면이 부르는 것이 아니라 **사람이 눌러서 도는 검사**다.
238
+ */
239
+ async function readMasterSnapshot(domain, getRepository, entities) {
240
+ const all = async (Entity, select) => (await getRepository(Entity).find({
241
+ where: { domain: { id: domain.id } },
242
+ select: ['id', 'name', ...select]
243
+ }));
244
+ const [persons, equipment, physicalAssets, materialDefinitions, operationDefinitions, routeDefinitions, recipeDefinitions, operationalLocations, stopReasons, testSpecifications, personnelClasses, equipmentClasses, physicalAssetClasses, materialClasses] = await Promise.all([
245
+ all(entities.Person, ['personnelClassIds', 'homeLocation', 'testSpecificationIds']),
246
+ all(entities.Equipment, ['kind', 'homeLocation', 'equipmentClassIds', 'testSpecificationIds']),
247
+ all(entities.PhysicalAsset, ['assetClassIds', 'homeLocation', 'testSpecificationIds']),
248
+ all(entities.MaterialDefinition, ['materialClassIds', 'testSpecificationIds']),
249
+ all(entities.OperationDefinition, ['intent', 'locationType', 'resourceType', 'duration']),
250
+ all(entities.RouteDefinition, ['steps']),
251
+ all(entities.RecipeDefinition, ['materialDefinitionName', 'route', 'inputs']),
252
+ all(entities.OperationalLocation, ['type', 'level', 'parentId']),
253
+ all(entities.StopReason, ['status', 'parentName']),
254
+ all(entities.TestSpecification, ['criteria']),
255
+ all(entities.PersonnelClass, ['baseIds']),
256
+ all(entities.EquipmentClass, ['baseIds']),
257
+ all(entities.PhysicalAssetClass, ['baseIds']),
258
+ all(entities.MaterialClass, ['baseIds'])
259
+ ]);
260
+ return {
261
+ persons, equipment, physicalAssets, materialDefinitions, operationDefinitions,
262
+ routeDefinitions, recipeDefinitions, operationalLocations, stopReasons, testSpecifications,
263
+ personnelClasses, equipmentClasses, physicalAssetClasses, materialClasses
264
+ };
265
+ }
266
+ //# sourceMappingURL=master-conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"master-conformance.js","sourceRoot":"","sources":["../../server/service/master-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * 마스터가 **말이 되나** — 축을 가로질러 한 번에 본다.\n *\n * ── 왜 필요한가 ────────────────────────────────────────────────────────────\n * 축마다 화면이 있고 축마다 값이 차 있는데, 그 사이가 이어지는지는 아무도 안 봤다. 설비 등급 다섯이\n * 서 있고 설비 스물둘이 어느 등급에도 안 들어 있던 일이 그랬다 — 두 화면 다 멀쩡해 보였다.\n * 카메라 자산 다섯이 ECU 등급에 들어 있던 것도, 정지 사유 고르기가 빈 목록으로 보이던 것도 같다.\n *\n * 사람이 열두 화면을 오가며 맞춰 보는 것으로는 안 된다. 맞춰 보지 않으면 아무도 모르고, 맞춰 봤는지\n * 아닌지도 남지 않는다.\n *\n * ── 두 가지만 본다 ─────────────────────────────────────────────────────────\n * **비어 있으면 안 되는 칸**과 **가리키는데 없는 것**이다. 둘 다 사실 판정이라 다툼이 없다.\n * 「이 값이 옳은가」는 여기서 안 본다 — 그것은 현장이 정한다.\n *\n * ── 없는 것을 없다고 말한다 ────────────────────────────────────────────────\n * 표가 통째로 비었으면 그것도 낸다. 「빠진 것 0건」과 「아직 아무것도 안 세웠다」는 다르고, 세지\n * 않으면 둘이 같아 보인다.\n */\n\n/** 한 건의 어긋남. 사실만 담는다 — 무엇을, 어디서, 무엇이. */\nexport interface Finding {\n /** 어느 축인가. `person` · `equipment` · `recipeDefinition` … */\n axis: string\n /** 어느 줄인가. 표가 통째로 빈 경우에는 없다. */\n subject?: string\n /** `missing` = 비어 있으면 안 되는 칸이 비었다 · `dangling` = 가리키는데 그런 것이 없다 · `empty` = 표가 비었다 · `unused` = 아무도 안 가리킨다 */\n kind: 'missing' | 'dangling' | 'empty' | 'unused'\n /** 어느 칸인가(`missing`·`dangling`), 또는 무엇이 비었나(`empty`·`unused`). */\n field: string\n /** 가리키던 값. `dangling` 일 때만. */\n value?: string\n}\n\n/** 한 축이 내놓는 줄들. 이름과 칸 몇 개만 본다. */\nexport type Row = Record<string, any>\n\nexport interface MasterSnapshot {\n persons: Row[]\n equipment: Row[]\n physicalAssets: Row[]\n materialDefinitions: Row[]\n operationDefinitions: Row[]\n routeDefinitions: Row[]\n recipeDefinitions: Row[]\n operationalLocations: Row[]\n stopReasons: Row[]\n testSpecifications: Row[]\n personnelClasses: Row[]\n equipmentClasses: Row[]\n physicalAssetClasses: Row[]\n materialClasses: Row[]\n}\n\nconst nameSet = (rows: Row[]) => new Set(rows.map(one => one.name).filter(Boolean))\n\n/** 값이 비었나 — 빈 문자열과 빈 배열도 빈 것으로 본다. */\nfunction blank(value: any): boolean {\n if (value === null || value === undefined) {\n return true\n }\n if (Array.isArray(value)) {\n return value.length === 0\n }\n\n return String(value).trim() === ''\n}\n\n/** 이름 목록으로 만든다 — 한 개짜리 문자열도, 배열도 같게 다룬다. */\nfunction asList(value: any): string[] {\n if (blank(value)) {\n return []\n }\n\n return (Array.isArray(value) ? value : [value]).map(one => String(one).trim()).filter(Boolean)\n}\n\n/**\n * 축 하나를 잰다.\n *\n * `required` 는 비어 있으면 안 되는 칸, `refs` 는 「이 칸의 값이 저 이름들 안에 있어야 한다」이다.\n */\nfunction checkAxis(\n axis: string,\n rows: Row[],\n required: string[],\n refs: { field: string; within: Set<string> }[]\n): Finding[] {\n const found: Finding[] = []\n\n if (rows.length === 0) {\n /* 표가 통째로 비었다. 「빠진 것 0건」과 다르다. */\n return [{ axis, kind: 'empty', field: axis }]\n }\n\n for (const row of rows) {\n for (const field of required) {\n if (blank(row[field])) {\n found.push({ axis, subject: row.name, kind: 'missing', field })\n }\n }\n\n for (const { field, within } of refs) {\n for (const value of asList(row[field])) {\n if (!within.has(value)) {\n found.push({ axis, subject: row.name, kind: 'dangling', field, value })\n }\n }\n }\n }\n\n return found\n}\n\n/** 아무도 안 가리키는 것들 — 세워 놓고 쓰지 않는 선언. */\nfunction checkUnused(axis: string, rows: Row[], pointedAt: Set<string>, field: string): Finding[] {\n return rows\n .filter(one => one.name && !pointedAt.has(one.name))\n .map(one => ({ axis, subject: one.name, kind: 'unused' as const, field }))\n}\n\n/**\n * 마스터 전체를 잰다.\n *\n * **순서에 뜻이 없다.** 어느 축부터 보든 같은 답이 나와야 하고, 그래야 이 결과를 두 번 재서\n * 견줄 수 있다.\n */\nexport function checkMaster(snapshot: MasterSnapshot): Finding[] {\n const locations = nameSet(snapshot.operationalLocations)\n const operations = nameSet(snapshot.operationDefinitions)\n const routes = nameSet(snapshot.routeDefinitions)\n const materials = nameSet(snapshot.materialDefinitions)\n const specs = nameSet(snapshot.testSpecifications)\n const personnelClasses = nameSet(snapshot.personnelClasses)\n const equipmentClasses = nameSet(snapshot.equipmentClasses)\n const assetClasses = nameSet(snapshot.physicalAssetClasses)\n const materialClasses = nameSet(snapshot.materialClasses)\n const stopReasons = nameSet(snapshot.stopReasons)\n\n const found: Finding[] = [\n ...checkAxis('person', snapshot.persons, ['personnelClassIds', 'homeLocation'], [\n { field: 'personnelClassIds', within: personnelClasses },\n { field: 'homeLocation', within: locations },\n { field: 'testSpecificationIds', within: specs }\n ]),\n\n ...checkAxis('equipment', snapshot.equipment, ['kind', 'homeLocation', 'equipmentClassIds'], [\n { field: 'equipmentClassIds', within: equipmentClasses },\n { field: 'homeLocation', within: locations },\n { field: 'testSpecificationIds', within: specs }\n ]),\n\n ...checkAxis('physicalAsset', snapshot.physicalAssets, ['assetClassIds'], [\n { field: 'assetClassIds', within: assetClasses },\n { field: 'homeLocation', within: locations },\n { field: 'testSpecificationIds', within: specs }\n ]),\n\n ...checkAxis('materialDefinition', snapshot.materialDefinitions, ['materialClassIds'], [\n { field: 'materialClassIds', within: materialClasses },\n { field: 'testSpecificationIds', within: specs }\n ]),\n\n /*\n * 공정의 `resourceType` 은 설비 **종류**를 가리키는 값이라 이름 집합이 없다. 여기서는\n * 비었는지만 본다 — 없는 것을 있는 척 검사하지 않는다.\n */\n ...checkAxis('operationDefinition', snapshot.operationDefinitions, ['intent', 'locationType', 'duration'], []),\n\n ...checkAxis('routeDefinition', snapshot.routeDefinitions, ['steps'], [\n { field: 'steps', within: operations }\n ]),\n\n ...checkAxis('recipeDefinition', snapshot.recipeDefinitions, ['materialDefinitionName', 'route', 'inputs'], [\n { field: 'materialDefinitionName', within: materials },\n { field: 'route', within: routes }\n ]),\n\n ...checkAxis('operationalLocation', snapshot.operationalLocations, ['type', 'level'], [\n { field: 'parentId', within: locations }\n ]),\n\n ...checkAxis('stopReason', snapshot.stopReasons, ['status'], [{ field: 'parentName', within: stopReasons }]),\n\n ...checkAxis('testSpecification', snapshot.testSpecifications, ['criteria'], [])\n ]\n\n /*\n * 제조법의 투입은 한 줄이 여럿을 가리킨다. 위의 검사는 칸 하나를 보므로 여기서 따로 본다.\n */\n for (const recipe of snapshot.recipeDefinitions) {\n for (const part of recipe.inputs ?? []) {\n if (part?.material && !materials.has(part.material)) {\n found.push({ axis: 'recipeDefinition', subject: recipe.name, kind: 'dangling', field: 'inputs.material', value: part.material })\n }\n if (part?.operation && !operations.has(part.operation)) {\n found.push({ axis: 'recipeDefinition', subject: recipe.name, kind: 'dangling', field: 'inputs.operation', value: part.operation })\n }\n }\n }\n\n /*\n * ── 세워 놓고 아무도 안 든 등급 ─────────────────────────────────────────\n *\n * **상속을 타고 센다.** 등급은 등급을 상속하고(`baseIds`), 자격 판정도 그 사슬을 닫아서 본다\n * (커널의 `classClosure`). 그러니 「아무도 안 든다」도 닫힌 소속으로 물어야 한다.\n *\n * 직접 소속만 세면 뿌리 등급이 늘 걸린다. 품목의 유형 등급(완제품 · 반제품 · 원재료)이 그렇다 —\n * 품목은 성격 등급에 들고 그 등급이 유형을 상속하므로, 직접 소속은 언제나 0이다. 그것을 결함으로\n * 읽으면 멀쩡한 등급을 지우러 간다.\n *\n * 처음에는 품목만 따로 다뤘고 나머지 셋은 직접 소속으로 셌다. 아키텍트 레인이 설비 등급에도\n * 같은 축이 있다고 알려 주어 넷을 한 규칙으로 모았다 — 축마다 다르게 세면 한쪽만 고쳐지는 날이\n * 온다. [재봤음] 2026-09-04\n */\n const classAxes: [string, Row[], Row[], string][] = [\n ['personnelClass', snapshot.personnelClasses, snapshot.persons, 'personnelClassIds'],\n ['equipmentClass', snapshot.equipmentClasses, snapshot.equipment, 'equipmentClassIds'],\n ['physicalAssetClass', snapshot.physicalAssetClasses, snapshot.physicalAssets, 'assetClassIds'],\n ['materialClass', snapshot.materialClasses, snapshot.materialDefinitions, 'materialClassIds']\n ]\n\n for (const [axis, classes, members, field] of classAxes) {\n found.push(...checkUnused(axis, classes, closedMembership(classes, members, field), 'members'))\n }\n\n /* 아무도 안 가리키는 라우팅·공정. 만들어 놓고 안 쓰는 것은 지워도 되는 것인지 사람이 판단한다. */\n const routesUsed = new Set(snapshot.recipeDefinitions.map(one => one.route).filter(Boolean))\n const operationsUsed = new Set([\n ...snapshot.routeDefinitions.flatMap(one => asList(one.steps)),\n ...snapshot.recipeDefinitions.flatMap(one => (one.inputs ?? []).map((part: any) => part?.operation).filter(Boolean))\n ])\n\n found.push(\n ...checkUnused('routeDefinition', snapshot.routeDefinitions, routesUsed, 'usedByRecipes'),\n ...checkUnused('operationDefinition', snapshot.operationDefinitions, operationsUsed, 'usedInRoutes')\n )\n\n return found\n}\n\n/**\n * **닫힌 소속** — 소속이 상속을 타고 닿는 등급까지 모은다.\n *\n * 사람이 `welding-robot-6axis` 에 들고 그것이 `welding-equipment` 를 상속하면, 두 등급 다 「누가\n * 들었다」로 센다. 자격 판정이 그 사슬을 닫아서 보므로(커널의 `classClosure`) 여기서도 같게 본다.\n *\n * **고리를 밟으면 멈춘다.** `baseIds` 는 화면에서 손으로 넣을 수 있어 A→B→A 가 생길 수 있다.\n */\nfunction closedMembership(classes: Row[], members: Row[], field: string): Set<string> {\n const baseOf = new Map(classes.map(one => [one.name, asList(one.baseIds)]))\n const reached = new Set<string>()\n\n const walk = (name: string) => {\n if (!name || reached.has(name)) {\n return\n }\n reached.add(name)\n\n for (const base of baseOf.get(name) ?? []) {\n walk(base)\n }\n }\n\n for (const row of members) {\n for (const name of asList(row[field])) {\n walk(name)\n }\n }\n\n return reached\n}\n\n/** 여러 줄이 가리키는 이름들을 모은다. */\nfunction collect(rows: Row[], field: string): Set<string> {\n return new Set(rows.flatMap(one => asList(one[field])))\n}\n\n/**\n * **무엇을 몇 개 봤나.**\n *\n * 어긋남 0건이 「멀쩡하다」인지 「안 돌았다」인지는 이것으로만 갈린다. 본 수를 안 내면 두 답이\n * 같은 글자로 나온다 — 검사기가 스스로를 못 믿게 만드는 가장 흔한 방식이다.\n */\nexport function scannedCounts(snapshot: MasterSnapshot): { axis: string; rows: number }[] {\n return Object.entries(snapshot)\n .map(([axis, rows]) => ({ axis, rows: (rows as Row[]).length }))\n .sort((a, b) => a.axis.localeCompare(b.axis))\n}\n\n/** 축·종류별로 몇 건인지. 사람이 먼저 볼 것을 고르는 데 쓴다. */\nexport function summarize(findings: Finding[]): { axis: string; kind: string; count: number }[] {\n const tally = new Map<string, number>()\n\n for (const one of findings) {\n const key = `${one.axis}\u0000${one.kind}`\n tally.set(key, (tally.get(key) ?? 0) + 1)\n }\n\n return [...tally.entries()]\n .map(([key, count]) => {\n const [axis, kind] = key.split('\u0000')\n\n return { axis, kind, count }\n })\n .sort((a, b) => b.count - a.count || a.axis.localeCompare(b.axis))\n}\n\n/**\n * 한 도메인의 마스터를 통째로 읽어 잰다.\n *\n * **상한을 두지 않는다.** 정합성은 「전부 봤나」가 곧 답이라, 일부만 보고 「어긋남 0건」이라고 하면\n * 그 답이 거짓이 된다. 그래서 이것은 목록 화면이 부르는 것이 아니라 **사람이 눌러서 도는 검사**다.\n */\nexport async function readMasterSnapshot(domain: any, getRepository: any, entities: Record<string, any>): Promise<MasterSnapshot> {\n const all = async (Entity: any, select: string[]) =>\n (await getRepository(Entity).find({\n where: { domain: { id: domain.id } } as any,\n select: ['id', 'name', ...select] as any\n })) as Row[]\n\n const [\n persons, equipment, physicalAssets, materialDefinitions, operationDefinitions,\n routeDefinitions, recipeDefinitions, operationalLocations, stopReasons, testSpecifications,\n personnelClasses, equipmentClasses, physicalAssetClasses, materialClasses\n ] = await Promise.all([\n all(entities.Person, ['personnelClassIds', 'homeLocation', 'testSpecificationIds']),\n all(entities.Equipment, ['kind', 'homeLocation', 'equipmentClassIds', 'testSpecificationIds']),\n all(entities.PhysicalAsset, ['assetClassIds', 'homeLocation', 'testSpecificationIds']),\n all(entities.MaterialDefinition, ['materialClassIds', 'testSpecificationIds']),\n all(entities.OperationDefinition, ['intent', 'locationType', 'resourceType', 'duration']),\n all(entities.RouteDefinition, ['steps']),\n all(entities.RecipeDefinition, ['materialDefinitionName', 'route', 'inputs']),\n all(entities.OperationalLocation, ['type', 'level', 'parentId']),\n all(entities.StopReason, ['status', 'parentName']),\n all(entities.TestSpecification, ['criteria']),\n all(entities.PersonnelClass, ['baseIds']),\n all(entities.EquipmentClass, ['baseIds']),\n all(entities.PhysicalAssetClass, ['baseIds']),\n all(entities.MaterialClass, ['baseIds'])\n ])\n\n return {\n persons, equipment, physicalAssets, materialDefinitions, operationDefinitions,\n routeDefinitions, recipeDefinitions, operationalLocations, stopReasons, testSpecifications,\n personnelClasses, equipmentClasses, physicalAssetClasses, materialClasses\n }\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { Domain } from '@things-factory/shell';
2
+ import type { Page, PageRequest } from './master-page';
3
+ /**
4
+ * 마스터 한 축을 **나눠 읽는다** — 저장소 쪽. 판단은 `master-page` 가 한다.
5
+ *
6
+ * ═══════════════════════════════════════════════════════════════════════════
7
+ * ── 등급은 매 쪽에 통째로 싣는다 ───────────────────────────────────────────
8
+ * 이 축들은 구성원이 자기 **등급을 이름으로 가리킨다.** 등급과 구성원을 따로 나눠 보내면 구성원이
9
+ * 먼저 도착하고 등급이 나중에 오는 순간이 생기고, 그 순간 받는 쪽은 **없는 등급을 가리키는 구성원**을
10
+ * 갖는다. 원래 이 조회들이 「자르지 않는다」고 적어 둔 이유가 그것이다.
11
+ *
12
+ * 그래서 **구성원만 나누고 등급은 매 쪽에 전부 싣는다.** 등급은 수십 단위라 실어도 크지 않고, 그러면
13
+ * 어느 쪽을 먼저 받아도 가리키는 곳이 항상 있다.
14
+ *
15
+ * ── 이름 다음부터 읽는다 ───────────────────────────────────────────────────
16
+ * `skip` 은 규모에서 못 쓴다 — 건너뛴 만큼을 매번 다시 읽는다. 이 축들은 이미 이름으로 정렬해 내므로
17
+ * **마지막 이름 다음부터** 읽으면 건너뛸 것이 없다.
18
+ * ═══════════════════════════════════════════════════════════════════════════
19
+ */
20
+ /**
21
+ * 한 축을 한 쪽만큼 읽는다.
22
+ *
23
+ * 이름 칸을 받는 이유: 축마다 다르다(`name` · `operationKey` …). 여기서 알아보려 하면 축이 늘 때마다
24
+ * 이 함수를 고쳐야 한다.
25
+ */
26
+ export declare function pullPage<T>(entity: any, domain: Domain, request: PageRequest, nameColumn?: keyof T & string): Promise<Page<T>>;
27
+ /**
28
+ * **통째로 달라고 했는데 너무 크면 던진다.**
29
+ *
30
+ * 자르고 넘어가지 않는다 — 받는 쪽은 그것이 전부인 줄 알고 트윈을 세우고, 없는 품목이 화면에서 그냥
31
+ * 존재하지 않게 된다. 오류가 안 난다. 그래서 **틀린 답 대신 오류**를 준다.
32
+ */
33
+ export declare function refuseIfTooLarge(axis: string, request: PageRequest, page: Page<unknown>): void;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pullPage = pullPage;
4
+ exports.refuseIfTooLarge = refuseIfTooLarge;
5
+ const typeorm_1 = require("typeorm");
6
+ const shell_1 = require("@things-factory/shell");
7
+ const master_page_1 = require("./master-page");
8
+ /**
9
+ * 마스터 한 축을 **나눠 읽는다** — 저장소 쪽. 판단은 `master-page` 가 한다.
10
+ *
11
+ * ═══════════════════════════════════════════════════════════════════════════
12
+ * ── 등급은 매 쪽에 통째로 싣는다 ───────────────────────────────────────────
13
+ * 이 축들은 구성원이 자기 **등급을 이름으로 가리킨다.** 등급과 구성원을 따로 나눠 보내면 구성원이
14
+ * 먼저 도착하고 등급이 나중에 오는 순간이 생기고, 그 순간 받는 쪽은 **없는 등급을 가리키는 구성원**을
15
+ * 갖는다. 원래 이 조회들이 「자르지 않는다」고 적어 둔 이유가 그것이다.
16
+ *
17
+ * 그래서 **구성원만 나누고 등급은 매 쪽에 전부 싣는다.** 등급은 수십 단위라 실어도 크지 않고, 그러면
18
+ * 어느 쪽을 먼저 받아도 가리키는 곳이 항상 있다.
19
+ *
20
+ * ── 이름 다음부터 읽는다 ───────────────────────────────────────────────────
21
+ * `skip` 은 규모에서 못 쓴다 — 건너뛴 만큼을 매번 다시 읽는다. 이 축들은 이미 이름으로 정렬해 내므로
22
+ * **마지막 이름 다음부터** 읽으면 건너뛸 것이 없다.
23
+ * ═══════════════════════════════════════════════════════════════════════════
24
+ */
25
+ /**
26
+ * 한 축을 한 쪽만큼 읽는다.
27
+ *
28
+ * 이름 칸을 받는 이유: 축마다 다르다(`name` · `operationKey` …). 여기서 알아보려 하면 축이 늘 때마다
29
+ * 이 함수를 고쳐야 한다.
30
+ */
31
+ async function pullPage(entity, domain, request, nameColumn = 'name') {
32
+ const where = { domain: { id: domain.id } };
33
+ if (request.after) {
34
+ where[nameColumn] = (0, typeorm_1.MoreThan)(request.after);
35
+ }
36
+ const read = (await (0, shell_1.getRepository)(entity).find({
37
+ where,
38
+ order: { [nameColumn]: 'ASC' },
39
+ take: (0, master_page_1.takeFor)(request)
40
+ }));
41
+ return (0, master_page_1.pageOf)(read, request, row => String(row[nameColumn]));
42
+ }
43
+ /**
44
+ * **통째로 달라고 했는데 너무 크면 던진다.**
45
+ *
46
+ * 자르고 넘어가지 않는다 — 받는 쪽은 그것이 전부인 줄 알고 트윈을 세우고, 없는 품목이 화면에서 그냥
47
+ * 존재하지 않게 된다. 오류가 안 난다. 그래서 **틀린 답 대신 오류**를 준다.
48
+ */
49
+ function refuseIfTooLarge(axis, request, page) {
50
+ const problem = (0, master_page_1.tooLargeToPull)(axis, request, page);
51
+ if (problem) {
52
+ throw new Error(problem);
53
+ }
54
+ }
55
+ //# sourceMappingURL=master-page-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"master-page-query.js","sourceRoot":"","sources":["../../server/service/master-page-query.ts"],"names":[],"mappings":";;AA+BA,4BAmBC;AAQD,4CAMC;AAhED,qCAAkC;AAElC,iDAA6D;AAE7D,+CAA+D;AAG/D;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACI,KAAK,UAAU,QAAQ,CAC5B,MAAW,EACX,MAAc,EACd,OAAoB,EACpB,aAA+B,MAAa;IAE5C,MAAM,KAAK,GAA4B,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,CAAA;IAEpE,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,KAAK,CAAC,UAAU,CAAC,GAAG,IAAA,kBAAQ,EAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,IAAA,qBAAa,EAAC,MAAM,CAAC,CAAC,IAAI,CAAC;QAC7C,KAAK;QACL,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE;QAC9B,IAAI,EAAE,IAAA,qBAAO,EAAC,OAAO,CAAC;KACvB,CAAC,CAAQ,CAAA;IAEV,OAAO,IAAA,oBAAM,EAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,MAAM,CAAE,GAAW,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AACvE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,IAAY,EAAE,OAAoB,EAAE,IAAmB;IACtF,MAAM,OAAO,GAAG,IAAA,4BAAc,EAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IAEnD,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAA;IAC1B,CAAC;AACH,CAAC","sourcesContent":["import { MoreThan } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { pageOf, takeFor, tooLargeToPull } from './master-page'\nimport type { Page, PageRequest } from './master-page'\n\n/**\n * 마스터 한 축을 **나눠 읽는다** — 저장소 쪽. 판단은 `master-page` 가 한다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 등급은 매 쪽에 통째로 싣는다 ───────────────────────────────────────────\n * 이 축들은 구성원이 자기 **등급을 이름으로 가리킨다.** 등급과 구성원을 따로 나눠 보내면 구성원이\n * 먼저 도착하고 등급이 나중에 오는 순간이 생기고, 그 순간 받는 쪽은 **없는 등급을 가리키는 구성원**을\n * 갖는다. 원래 이 조회들이 「자르지 않는다」고 적어 둔 이유가 그것이다.\n *\n * 그래서 **구성원만 나누고 등급은 매 쪽에 전부 싣는다.** 등급은 수십 단위라 실어도 크지 않고, 그러면\n * 어느 쪽을 먼저 받아도 가리키는 곳이 항상 있다.\n *\n * ── 이름 다음부터 읽는다 ───────────────────────────────────────────────────\n * `skip` 은 규모에서 못 쓴다 — 건너뛴 만큼을 매번 다시 읽는다. 이 축들은 이미 이름으로 정렬해 내므로\n * **마지막 이름 다음부터** 읽으면 건너뛸 것이 없다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * 한 축을 한 쪽만큼 읽는다.\n *\n * 이름 칸을 받는 이유: 축마다 다르다(`name` · `operationKey` …). 여기서 알아보려 하면 축이 늘 때마다\n * 이 함수를 고쳐야 한다.\n */\nexport async function pullPage<T>(\n entity: any,\n domain: Domain,\n request: PageRequest,\n nameColumn: keyof T & string = 'name' as any\n): Promise<Page<T>> {\n const where: Record<string, unknown> = { domain: { id: domain.id } }\n\n if (request.after) {\n where[nameColumn] = MoreThan(request.after)\n }\n\n const read = (await getRepository(entity).find({\n where,\n order: { [nameColumn]: 'ASC' },\n take: takeFor(request)\n })) as T[]\n\n return pageOf(read, request, row => String((row as any)[nameColumn]))\n}\n\n/**\n * **통째로 달라고 했는데 너무 크면 던진다.**\n *\n * 자르고 넘어가지 않는다 — 받는 쪽은 그것이 전부인 줄 알고 트윈을 세우고, 없는 품목이 화면에서 그냥\n * 존재하지 않게 된다. 오류가 안 난다. 그래서 **틀린 답 대신 오류**를 준다.\n */\nexport function refuseIfTooLarge(axis: string, request: PageRequest, page: Page<unknown>): void {\n const problem = tooLargeToPull(axis, request, page)\n\n if (problem) {\n throw new Error(problem)\n }\n}\n"]}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * **마스터를 나눠 내는 규칙** — 순수 판단.
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════
5
+ * ── 무엇이 문제였나 ────────────────────────────────────────────────────────
6
+ * 트윈이 당겨 가는 마스터 조회 여덟 축이 **전부 전량 조회**였다. 도메인 조건 하나만 걸고 그 축의 모든
7
+ * 행을 읽어 JSON 한 덩어리로 낸다.
8
+ *
9
+ * 재봤다: 품목 100만에서 **2,219ms · 49MB**. 지금 미라텍은 자재 106개라 안 아프지만, 이 제품의 규모
10
+ * 기준은 엔티티 10만 · 품목 100만이다. 그 규모에서 이 조회는 서지 않는다.
11
+ *
12
+ * ── 왜 「조용히 자르기」가 아니라 「거절」인가 ──────────────────────────────
13
+ * 한계를 두고 **말없이 앞부분만 주는 것이 제일 나쁘다.** 받는 쪽은 그것이 전부인 줄 알고 트윈을
14
+ * 세우고, 없는 설비·없는 품목이 화면에서 그냥 **존재하지 않게** 된다. 오류가 안 난다.
15
+ *
16
+ * 오늘 하루 종일 잡은 부류가 그것이라, 여기서는 그렇게 하지 않는다.
17
+ *
18
+ * 나눠 달라고 하면(`limit`) 그만큼 주고 **더 있는지 말한다**(`hasMore`)
19
+ * 통째로 달라고 했는데 크면 **거절하고 나눠 받으라고 말한다**
20
+ * 통째로 달라고 했는데 작으면 그냥 준다 — 대부분의 현장이 여기다
21
+ *
22
+ * 그래서 작은 공장은 지금 그대로 돌고, 큰 공장은 **틀린 답 대신 오류**를 받는다.
23
+ *
24
+ * ── 왜 이름으로 이어 받나 ──────────────────────────────────────────────────
25
+ * `offset` 은 규모에서 못 쓴다 — 뒤로 갈수록 건너뛴 만큼을 매번 다시 읽는다. 이 축들은 이미 이름으로
26
+ * 정렬해 내므로(`order: { name: 'ASC' }`), **마지막 이름 다음부터** 읽으면 건너뛸 것이 없다.
27
+ *
28
+ * 이름이 유일하다는 것에 기댄다. 도메인 안에서 마스터 이름은 유일하다(그 이름으로 실적·사실이 자원을
29
+ * 가리킨다 — 유일하지 않으면 그것부터 깨진다).
30
+ * ═══════════════════════════════════════════════════════════════════════════
31
+ */
32
+ /**
33
+ * 한 번에 낼 수 있는 **최대**. 이보다 크게 달라고 해도 이만큼만 낸다.
34
+ *
35
+ * ⚠ **이것은 안전장치이지 권하는 쪽 크기가 아니다.** 받는 쪽이 이 수에 붙여 두면, 우리가 한도를
36
+ * 내리는 날 그쪽은 **그날부터** 못 읽는다. 권하는 크기는 `SUGGESTED_PAGE` 다.
37
+ *
38
+ * 인티그레이션이 붙이면서 스스로 이 구별을 하고 2,000 을 골랐다(2026-09-05) — 「그 수는 거절하는
39
+ * 자리이지 권하는 크기가 아니다」. 맞는 판단이고, 여기 적혀 있지 않아서 스스로 알아낸 것이라 적는다.
40
+ */
41
+ export declare const MAX_PAGE = 5000;
42
+ /**
43
+ * 권하는 쪽 크기.
44
+ *
45
+ * [재봤음] 품목 100만이 전량 49MB 였으니 2,000이면 한 쪽이 100KB 안팎이다. 한 쪽에 1.4ms 이고
46
+ * 이어받기는 0.1ms 라, 쪽을 더 키워 얻을 것이 없다.
47
+ */
48
+ export declare const SUGGESTED_PAGE = 2000;
49
+ /** `limit` 을 안 주고 나눠 받지도 않을 때, 통째로 내주는 한도. 넘으면 거절한다. */
50
+ export declare const WHOLE_LIMIT = 5000;
51
+ /** 나눠 달라고 했는데 몇 개인지는 안 말했을 때. */
52
+ export declare const DEFAULT_PAGE = 1000;
53
+ export interface PageRequest {
54
+ /** 이 이름 **다음부터**. 앞 페이지의 `next` 를 그대로 준다. */
55
+ after?: string;
56
+ /** 몇 개까지. 없으면 통째로 달라는 뜻이다. */
57
+ limit?: number;
58
+ }
59
+ /**
60
+ * 실제로 몇 개를 읽을 것인가.
61
+ *
62
+ * **하나를 더 읽는다.** 더 있는지 알려면 세거나(`COUNT`) 한 개 더 읽어야 하는데, 세는 것은 같은 조건을
63
+ * 두 번 훑는 것이라 규모에서 두 배가 된다.
64
+ */
65
+ export declare function takeFor(request: PageRequest): number;
66
+ export interface Page<T> {
67
+ rows: T[];
68
+ /** 더 있나. **이 값을 안 보고 쓰면 조용히 잘린 자료를 쓰게 된다.** */
69
+ hasMore: boolean;
70
+ /** 이어 받을 자리 — 다음 요청의 `after`. 더 없으면 없다. */
71
+ next?: string;
72
+ }
73
+ /**
74
+ * 하나 더 읽어 온 결과를 페이지로 만든다.
75
+ *
76
+ * `nameOf` 를 받는 이유: 축마다 이름 칸이 다르다(`name` · `operationKey` …). 그 차이를 여기서 알아보려
77
+ * 하면 축이 늘 때마다 이 함수를 고쳐야 한다.
78
+ */
79
+ export declare function pageOf<T>(read: readonly T[], request: PageRequest, nameOf: (row: T) => string): Page<T>;
80
+ /**
81
+ * **통째로 달라고 했는데 너무 크다** — 낼 수 없는 이유를 사람 말로.
82
+ *
83
+ * 자르지 않는다. 자르면 받는 쪽은 그것이 전부인 줄 안다.
84
+ */
85
+ export declare function tooLargeToPull(axis: string, request: PageRequest, page: Page<unknown>): string | undefined;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * **마스터를 나눠 내는 규칙** — 순수 판단.
4
+ *
5
+ * ═══════════════════════════════════════════════════════════════════════════
6
+ * ── 무엇이 문제였나 ────────────────────────────────────────────────────────
7
+ * 트윈이 당겨 가는 마스터 조회 여덟 축이 **전부 전량 조회**였다. 도메인 조건 하나만 걸고 그 축의 모든
8
+ * 행을 읽어 JSON 한 덩어리로 낸다.
9
+ *
10
+ * 재봤다: 품목 100만에서 **2,219ms · 49MB**. 지금 미라텍은 자재 106개라 안 아프지만, 이 제품의 규모
11
+ * 기준은 엔티티 10만 · 품목 100만이다. 그 규모에서 이 조회는 서지 않는다.
12
+ *
13
+ * ── 왜 「조용히 자르기」가 아니라 「거절」인가 ──────────────────────────────
14
+ * 한계를 두고 **말없이 앞부분만 주는 것이 제일 나쁘다.** 받는 쪽은 그것이 전부인 줄 알고 트윈을
15
+ * 세우고, 없는 설비·없는 품목이 화면에서 그냥 **존재하지 않게** 된다. 오류가 안 난다.
16
+ *
17
+ * 오늘 하루 종일 잡은 부류가 그것이라, 여기서는 그렇게 하지 않는다.
18
+ *
19
+ * 나눠 달라고 하면(`limit`) 그만큼 주고 **더 있는지 말한다**(`hasMore`)
20
+ * 통째로 달라고 했는데 크면 **거절하고 나눠 받으라고 말한다**
21
+ * 통째로 달라고 했는데 작으면 그냥 준다 — 대부분의 현장이 여기다
22
+ *
23
+ * 그래서 작은 공장은 지금 그대로 돌고, 큰 공장은 **틀린 답 대신 오류**를 받는다.
24
+ *
25
+ * ── 왜 이름으로 이어 받나 ──────────────────────────────────────────────────
26
+ * `offset` 은 규모에서 못 쓴다 — 뒤로 갈수록 건너뛴 만큼을 매번 다시 읽는다. 이 축들은 이미 이름으로
27
+ * 정렬해 내므로(`order: { name: 'ASC' }`), **마지막 이름 다음부터** 읽으면 건너뛸 것이 없다.
28
+ *
29
+ * 이름이 유일하다는 것에 기댄다. 도메인 안에서 마스터 이름은 유일하다(그 이름으로 실적·사실이 자원을
30
+ * 가리킨다 — 유일하지 않으면 그것부터 깨진다).
31
+ * ═══════════════════════════════════════════════════════════════════════════
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.DEFAULT_PAGE = exports.WHOLE_LIMIT = exports.SUGGESTED_PAGE = exports.MAX_PAGE = void 0;
35
+ exports.takeFor = takeFor;
36
+ exports.pageOf = pageOf;
37
+ exports.tooLargeToPull = tooLargeToPull;
38
+ /**
39
+ * 한 번에 낼 수 있는 **최대**. 이보다 크게 달라고 해도 이만큼만 낸다.
40
+ *
41
+ * ⚠ **이것은 안전장치이지 권하는 쪽 크기가 아니다.** 받는 쪽이 이 수에 붙여 두면, 우리가 한도를
42
+ * 내리는 날 그쪽은 **그날부터** 못 읽는다. 권하는 크기는 `SUGGESTED_PAGE` 다.
43
+ *
44
+ * 인티그레이션이 붙이면서 스스로 이 구별을 하고 2,000 을 골랐다(2026-09-05) — 「그 수는 거절하는
45
+ * 자리이지 권하는 크기가 아니다」. 맞는 판단이고, 여기 적혀 있지 않아서 스스로 알아낸 것이라 적는다.
46
+ */
47
+ exports.MAX_PAGE = 5_000;
48
+ /**
49
+ * 권하는 쪽 크기.
50
+ *
51
+ * [재봤음] 품목 100만이 전량 49MB 였으니 2,000이면 한 쪽이 100KB 안팎이다. 한 쪽에 1.4ms 이고
52
+ * 이어받기는 0.1ms 라, 쪽을 더 키워 얻을 것이 없다.
53
+ */
54
+ exports.SUGGESTED_PAGE = 2_000;
55
+ /** `limit` 을 안 주고 나눠 받지도 않을 때, 통째로 내주는 한도. 넘으면 거절한다. */
56
+ exports.WHOLE_LIMIT = 5_000;
57
+ /** 나눠 달라고 했는데 몇 개인지는 안 말했을 때. */
58
+ exports.DEFAULT_PAGE = 1_000;
59
+ /**
60
+ * 실제로 몇 개를 읽을 것인가.
61
+ *
62
+ * **하나를 더 읽는다.** 더 있는지 알려면 세거나(`COUNT`) 한 개 더 읽어야 하는데, 세는 것은 같은 조건을
63
+ * 두 번 훑는 것이라 규모에서 두 배가 된다.
64
+ */
65
+ function takeFor(request) {
66
+ if (request.limit === undefined) {
67
+ /* 통째로 달라는 요청이다. 한도를 넘는지 보려면 한도+1 을 읽어 봐야 한다. */
68
+ return exports.WHOLE_LIMIT + 1;
69
+ }
70
+ /* 0 이나 음수를 「전부」로 읽지 않는다 — 그렇게 읽으면 실수 하나가 전량 조회가 된다. */
71
+ const asked = Math.floor(request.limit);
72
+ const bounded = Number.isFinite(asked) ? Math.min(Math.max(asked, 1), exports.MAX_PAGE) : exports.DEFAULT_PAGE;
73
+ return bounded + 1;
74
+ }
75
+ /**
76
+ * 하나 더 읽어 온 결과를 페이지로 만든다.
77
+ *
78
+ * `nameOf` 를 받는 이유: 축마다 이름 칸이 다르다(`name` · `operationKey` …). 그 차이를 여기서 알아보려
79
+ * 하면 축이 늘 때마다 이 함수를 고쳐야 한다.
80
+ */
81
+ function pageOf(read, request, nameOf) {
82
+ const size = takeFor(request) - 1;
83
+ const hasMore = read.length > size;
84
+ const rows = hasMore ? read.slice(0, size) : [...read];
85
+ return {
86
+ rows,
87
+ hasMore,
88
+ ...(hasMore && rows.length ? { next: nameOf(rows[rows.length - 1]) } : {})
89
+ };
90
+ }
91
+ /**
92
+ * **통째로 달라고 했는데 너무 크다** — 낼 수 없는 이유를 사람 말로.
93
+ *
94
+ * 자르지 않는다. 자르면 받는 쪽은 그것이 전부인 줄 안다.
95
+ */
96
+ function tooLargeToPull(axis, request, page) {
97
+ if (request.limit !== undefined || !page.hasMore) {
98
+ return undefined;
99
+ }
100
+ return (`${axis} 축이 한 번에 낼 수 있는 ${exports.WHOLE_LIMIT}건을 넘습니다. ` +
101
+ `limit 과 after 로 나눠 받으십시오 — ${exports.SUGGESTED_PAGE}건쯤이 알맞고(최대 ${exports.MAX_PAGE}건), ` +
102
+ '응답의 hasMore 가 거짓이 될 때까지 next 를 after 로 넘기면 됩니다.');
103
+ }
104
+ //# sourceMappingURL=master-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"master-page.js","sourceRoot":"","sources":["../../server/service/master-page.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;;;AAwCH,0BAWC;AAgBD,wBAUC;AAOD,wCAUC;AA5FD;;;;;;;;GAQG;AACU,QAAA,QAAQ,GAAG,KAAK,CAAA;AAE7B;;;;;GAKG;AACU,QAAA,cAAc,GAAG,KAAK,CAAA;AAEnC,wDAAwD;AAC3C,QAAA,WAAW,GAAG,KAAK,CAAA;AAEhC,iCAAiC;AACpB,QAAA,YAAY,GAAG,KAAK,CAAA;AASjC;;;;;GAKG;AACH,SAAgB,OAAO,CAAC,OAAoB;IAC1C,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,gDAAgD;QAChD,OAAO,mBAAW,GAAG,CAAC,CAAA;IACxB,CAAC;IAED,uDAAuD;IACvD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,gBAAQ,CAAC,CAAC,CAAC,CAAC,oBAAY,CAAA;IAE9F,OAAO,OAAO,GAAG,CAAC,CAAA;AACpB,CAAC;AAUD;;;;;GAKG;AACH,SAAgB,MAAM,CAAI,IAAkB,EAAE,OAAoB,EAAE,MAA0B;IAC5F,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;IAClC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAEtD,OAAO;QACL,IAAI;QACJ,OAAO;QACP,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAgB,cAAc,CAAC,IAAY,EAAE,OAAoB,EAAE,IAAmB;IACpF,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QACjD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,OAAO,CACL,GAAG,IAAI,mBAAmB,mBAAW,WAAW;QAChD,8BAA8B,sBAAc,cAAc,gBAAQ,MAAM;QACxE,iDAAiD,CAClD,CAAA;AACH,CAAC","sourcesContent":["/**\n * **마스터를 나눠 내는 규칙** — 순수 판단.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 무엇이 문제였나 ────────────────────────────────────────────────────────\n * 트윈이 당겨 가는 마스터 조회 여덟 축이 **전부 전량 조회**였다. 도메인 조건 하나만 걸고 그 축의 모든\n * 행을 읽어 JSON 한 덩어리로 낸다.\n *\n * 재봤다: 품목 100만에서 **2,219ms · 49MB**. 지금 미라텍은 자재 106개라 안 아프지만, 이 제품의 규모\n * 기준은 엔티티 10만 · 품목 100만이다. 그 규모에서 이 조회는 서지 않는다.\n *\n * ── 왜 「조용히 자르기」가 아니라 「거절」인가 ──────────────────────────────\n * 한계를 두고 **말없이 앞부분만 주는 것이 제일 나쁘다.** 받는 쪽은 그것이 전부인 줄 알고 트윈을\n * 세우고, 없는 설비·없는 품목이 화면에서 그냥 **존재하지 않게** 된다. 오류가 안 난다.\n *\n * 오늘 하루 종일 잡은 부류가 그것이라, 여기서는 그렇게 하지 않는다.\n *\n * 나눠 달라고 하면(`limit`) 그만큼 주고 **더 있는지 말한다**(`hasMore`)\n * 통째로 달라고 했는데 크면 **거절하고 나눠 받으라고 말한다**\n * 통째로 달라고 했는데 작으면 그냥 준다 — 대부분의 현장이 여기다\n *\n * 그래서 작은 공장은 지금 그대로 돌고, 큰 공장은 **틀린 답 대신 오류**를 받는다.\n *\n * ── 왜 이름으로 이어 받나 ──────────────────────────────────────────────────\n * `offset` 은 규모에서 못 쓴다 — 뒤로 갈수록 건너뛴 만큼을 매번 다시 읽는다. 이 축들은 이미 이름으로\n * 정렬해 내므로(`order: { name: 'ASC' }`), **마지막 이름 다음부터** 읽으면 건너뛸 것이 없다.\n *\n * 이름이 유일하다는 것에 기댄다. 도메인 안에서 마스터 이름은 유일하다(그 이름으로 실적·사실이 자원을\n * 가리킨다 — 유일하지 않으면 그것부터 깨진다).\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * 한 번에 낼 수 있는 **최대**. 이보다 크게 달라고 해도 이만큼만 낸다.\n *\n * ⚠ **이것은 안전장치이지 권하는 쪽 크기가 아니다.** 받는 쪽이 이 수에 붙여 두면, 우리가 한도를\n * 내리는 날 그쪽은 **그날부터** 못 읽는다. 권하는 크기는 `SUGGESTED_PAGE` 다.\n *\n * 인티그레이션이 붙이면서 스스로 이 구별을 하고 2,000 을 골랐다(2026-09-05) — 「그 수는 거절하는\n * 자리이지 권하는 크기가 아니다」. 맞는 판단이고, 여기 적혀 있지 않아서 스스로 알아낸 것이라 적는다.\n */\nexport const MAX_PAGE = 5_000\n\n/**\n * 권하는 쪽 크기.\n *\n * [재봤음] 품목 100만이 전량 49MB 였으니 2,000이면 한 쪽이 100KB 안팎이다. 한 쪽에 1.4ms 이고\n * 이어받기는 0.1ms 라, 쪽을 더 키워 얻을 것이 없다.\n */\nexport const SUGGESTED_PAGE = 2_000\n\n/** `limit` 을 안 주고 나눠 받지도 않을 때, 통째로 내주는 한도. 넘으면 거절한다. */\nexport const WHOLE_LIMIT = 5_000\n\n/** 나눠 달라고 했는데 몇 개인지는 안 말했을 때. */\nexport const DEFAULT_PAGE = 1_000\n\nexport interface PageRequest {\n /** 이 이름 **다음부터**. 앞 페이지의 `next` 를 그대로 준다. */\n after?: string\n /** 몇 개까지. 없으면 통째로 달라는 뜻이다. */\n limit?: number\n}\n\n/**\n * 실제로 몇 개를 읽을 것인가.\n *\n * **하나를 더 읽는다.** 더 있는지 알려면 세거나(`COUNT`) 한 개 더 읽어야 하는데, 세는 것은 같은 조건을\n * 두 번 훑는 것이라 규모에서 두 배가 된다.\n */\nexport function takeFor(request: PageRequest): number {\n if (request.limit === undefined) {\n /* 통째로 달라는 요청이다. 한도를 넘는지 보려면 한도+1 을 읽어 봐야 한다. */\n return WHOLE_LIMIT + 1\n }\n\n /* 0 이나 음수를 「전부」로 읽지 않는다 — 그렇게 읽으면 실수 하나가 전량 조회가 된다. */\n const asked = Math.floor(request.limit)\n const bounded = Number.isFinite(asked) ? Math.min(Math.max(asked, 1), MAX_PAGE) : DEFAULT_PAGE\n\n return bounded + 1\n}\n\nexport interface Page<T> {\n rows: T[]\n /** 더 있나. **이 값을 안 보고 쓰면 조용히 잘린 자료를 쓰게 된다.** */\n hasMore: boolean\n /** 이어 받을 자리 — 다음 요청의 `after`. 더 없으면 없다. */\n next?: string\n}\n\n/**\n * 하나 더 읽어 온 결과를 페이지로 만든다.\n *\n * `nameOf` 를 받는 이유: 축마다 이름 칸이 다르다(`name` · `operationKey` …). 그 차이를 여기서 알아보려\n * 하면 축이 늘 때마다 이 함수를 고쳐야 한다.\n */\nexport function pageOf<T>(read: readonly T[], request: PageRequest, nameOf: (row: T) => string): Page<T> {\n const size = takeFor(request) - 1\n const hasMore = read.length > size\n const rows = hasMore ? read.slice(0, size) : [...read]\n\n return {\n rows,\n hasMore,\n ...(hasMore && rows.length ? { next: nameOf(rows[rows.length - 1]) } : {})\n }\n}\n\n/**\n * **통째로 달라고 했는데 너무 크다** — 낼 수 없는 이유를 사람 말로.\n *\n * 자르지 않는다. 자르면 받는 쪽은 그것이 전부인 줄 안다.\n */\nexport function tooLargeToPull(axis: string, request: PageRequest, page: Page<unknown>): string | undefined {\n if (request.limit !== undefined || !page.hasMore) {\n return undefined\n }\n\n return (\n `${axis} 축이 한 번에 낼 수 있는 ${WHOLE_LIMIT}건을 넘습니다. ` +\n `limit 과 after 로 나눠 받으십시오 — ${SUGGESTED_PAGE}건쯤이 알맞고(최대 ${MAX_PAGE}건), ` +\n '응답의 hasMore 가 거짓이 될 때까지 next 를 after 로 넘기면 됩니다.'\n )\n}\n"]}