@things-factory/station 10.1.33

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 (185) hide show
  1. package/README.md +124 -0
  2. package/dist-client/index.d.ts +11 -0
  3. package/dist-client/index.js +13 -0
  4. package/dist-client/index.js.map +1 -0
  5. package/dist-client/station/scene/index.d.ts +11 -0
  6. package/dist-client/station/scene/index.js +12 -0
  7. package/dist-client/station/scene/index.js.map +1 -0
  8. package/dist-client/station/scene/station-context-binding.d.ts +25 -0
  9. package/dist-client/station/scene/station-context-binding.js +39 -0
  10. package/dist-client/station/scene/station-context-binding.js.map +1 -0
  11. package/dist-client/station/scene/station-context-scene.d.ts +28 -0
  12. package/dist-client/station/scene/station-context-scene.js +138 -0
  13. package/dist-client/station/scene/station-context-scene.js.map +1 -0
  14. package/dist-client/station/scene/station-counts-scene.d.ts +6 -0
  15. package/dist-client/station/scene/station-counts-scene.js +51 -0
  16. package/dist-client/station/scene/station-counts-scene.js.map +1 -0
  17. package/dist-client/station/scene/station-field-scene.d.ts +6 -0
  18. package/dist-client/station/scene/station-field-scene.js +89 -0
  19. package/dist-client/station/scene/station-field-scene.js.map +1 -0
  20. package/dist-client/station/scene/station-icons.d.ts +82 -0
  21. package/dist-client/station/scene/station-icons.js +131 -0
  22. package/dist-client/station/scene/station-icons.js.map +1 -0
  23. package/dist-client/station/scene/station-numpad-scene.d.ts +9 -0
  24. package/dist-client/station/scene/station-numpad-scene.js +144 -0
  25. package/dist-client/station/scene/station-numpad-scene.js.map +1 -0
  26. package/dist-client/station/scene/station-scene-style.d.ts +51 -0
  27. package/dist-client/station/scene/station-scene-style.js +108 -0
  28. package/dist-client/station/scene/station-scene-style.js.map +1 -0
  29. package/dist-client/station/scene/station-scene.d.ts +295 -0
  30. package/dist-client/station/scene/station-scene.js +68 -0
  31. package/dist-client/station/scene/station-scene.js.map +1 -0
  32. package/dist-client/station/scene/station-who-scene.d.ts +6 -0
  33. package/dist-client/station/scene/station-who-scene.js +48 -0
  34. package/dist-client/station/scene/station-who-scene.js.map +1 -0
  35. package/dist-client/station/scene/station-work-panel-scene.d.ts +28 -0
  36. package/dist-client/station/scene/station-work-panel-scene.js +37 -0
  37. package/dist-client/station/scene/station-work-panel-scene.js.map +1 -0
  38. package/dist-client/station/station-board-binding.d.ts +16 -0
  39. package/dist-client/station/station-board-binding.js +20 -0
  40. package/dist-client/station/station-board-binding.js.map +1 -0
  41. package/dist-client/station/station-context.d.ts +178 -0
  42. package/dist-client/station/station-context.js +30 -0
  43. package/dist-client/station/station-context.js.map +1 -0
  44. package/dist-client/station/station-palette.d.ts +34 -0
  45. package/dist-client/station/station-palette.js +119 -0
  46. package/dist-client/station/station-palette.js.map +1 -0
  47. package/dist-client/station/station-runtime.d.ts +47 -0
  48. package/dist-client/station/station-runtime.js +47 -0
  49. package/dist-client/station/station-runtime.js.map +1 -0
  50. package/dist-client/station/station-scene-context.d.ts +68 -0
  51. package/dist-client/station/station-scene-context.js +62 -0
  52. package/dist-client/station/station-scene-context.js.map +1 -0
  53. package/dist-client/station/station-screen-flow.d.ts +16 -0
  54. package/dist-client/station/station-screen-flow.js +58 -0
  55. package/dist-client/station/station-screen-flow.js.map +1 -0
  56. package/dist-client/station/station-tile-registry.d.ts +89 -0
  57. package/dist-client/station/station-tile-registry.js +86 -0
  58. package/dist-client/station/station-tile-registry.js.map +1 -0
  59. package/dist-client/station/station-work-panel-view.d.ts +23 -0
  60. package/dist-client/station/station-work-panel-view.js +125 -0
  61. package/dist-client/station/station-work-panel-view.js.map +1 -0
  62. package/dist-client/station/station-work-runtime.d.ts +47 -0
  63. package/dist-client/station/station-work-runtime.js +161 -0
  64. package/dist-client/station/station-work-runtime.js.map +1 -0
  65. package/dist-client/station/station-work-scenarios.d.ts +41 -0
  66. package/dist-client/station/station-work-scenarios.js +59 -0
  67. package/dist-client/station/station-work-scenarios.js.map +1 -0
  68. package/dist-client/station/task-counts.d.ts +32 -0
  69. package/dist-client/station/task-counts.js +26 -0
  70. package/dist-client/station/task-counts.js.map +1 -0
  71. package/dist-client/tsconfig.tsbuildinfo +1 -0
  72. package/dist-server/activities/activity-station-screen-control-withdrawal.d.ts +62 -0
  73. package/dist-server/activities/activity-station-screen-control-withdrawal.js +147 -0
  74. package/dist-server/activities/activity-station-screen-control-withdrawal.js.map +1 -0
  75. package/dist-server/activities/activity-station-screen-release.d.ts +81 -0
  76. package/dist-server/activities/activity-station-screen-release.js +147 -0
  77. package/dist-server/activities/activity-station-screen-release.js.map +1 -0
  78. package/dist-server/activities/index.d.ts +49 -0
  79. package/dist-server/activities/index.js +21 -0
  80. package/dist-server/activities/index.js.map +1 -0
  81. package/dist-server/index.d.ts +3 -0
  82. package/dist-server/index.js +8 -0
  83. package/dist-server/index.js.map +1 -0
  84. package/dist-server/service/index.d.ts +23 -0
  85. package/dist-server/service/index.js +41 -0
  86. package/dist-server/service/index.js.map +1 -0
  87. package/dist-server/service/station/deletion-changes.d.ts +14 -0
  88. package/dist-server/service/station/deletion-changes.js +25 -0
  89. package/dist-server/service/station/deletion-changes.js.map +1 -0
  90. package/dist-server/service/station/index.d.ts +13 -0
  91. package/dist-server/service/station/index.js +25 -0
  92. package/dist-server/service/station/index.js.map +1 -0
  93. package/dist-server/service/station/reversibility.d.ts +21 -0
  94. package/dist-server/service/station/reversibility.js +24 -0
  95. package/dist-server/service/station/reversibility.js.map +1 -0
  96. package/dist-server/service/station/screen-model-digest.d.ts +60 -0
  97. package/dist-server/service/station/screen-model-digest.js +97 -0
  98. package/dist-server/service/station/screen-model-digest.js.map +1 -0
  99. package/dist-server/service/station/staged-material.d.ts +62 -0
  100. package/dist-server/service/station/staged-material.js +44 -0
  101. package/dist-server/service/station/staged-material.js.map +1 -0
  102. package/dist-server/service/station/station-argument-sources.d.ts +31 -0
  103. package/dist-server/service/station/station-argument-sources.js +66 -0
  104. package/dist-server/service/station/station-argument-sources.js.map +1 -0
  105. package/dist-server/service/station/station-assignment-choice.d.ts +81 -0
  106. package/dist-server/service/station/station-assignment-choice.js +105 -0
  107. package/dist-server/service/station/station-assignment-choice.js.map +1 -0
  108. package/dist-server/service/station/station-assignment-resolver.d.ts +104 -0
  109. package/dist-server/service/station/station-assignment-resolver.js +426 -0
  110. package/dist-server/service/station/station-assignment-resolver.js.map +1 -0
  111. package/dist-server/service/station/station-assignment-service.d.ts +33 -0
  112. package/dist-server/service/station/station-assignment-service.js +64 -0
  113. package/dist-server/service/station/station-assignment-service.js.map +1 -0
  114. package/dist-server/service/station/station-axes.d.ts +9 -0
  115. package/dist-server/service/station/station-axes.js +16 -0
  116. package/dist-server/service/station/station-axes.js.map +1 -0
  117. package/dist-server/service/station/station-flow.d.ts +156 -0
  118. package/dist-server/service/station/station-flow.js +255 -0
  119. package/dist-server/service/station/station-flow.js.map +1 -0
  120. package/dist-server/service/station/station-generate.d.ts +76 -0
  121. package/dist-server/service/station/station-generate.js +178 -0
  122. package/dist-server/service/station/station-generate.js.map +1 -0
  123. package/dist-server/service/station/station-product.d.ts +123 -0
  124. package/dist-server/service/station/station-product.js +77 -0
  125. package/dist-server/service/station/station-product.js.map +1 -0
  126. package/dist-server/service/station/station-runtime-binding.d.ts +6 -0
  127. package/dist-server/service/station/station-runtime-binding.js +46 -0
  128. package/dist-server/service/station/station-runtime-binding.js.map +1 -0
  129. package/dist-server/service/station/station-screen-assignment.d.ts +94 -0
  130. package/dist-server/service/station/station-screen-assignment.js +154 -0
  131. package/dist-server/service/station/station-screen-assignment.js.map +1 -0
  132. package/dist-server/service/station/station-screen-control-withdrawal-resolver.d.ts +37 -0
  133. package/dist-server/service/station/station-screen-control-withdrawal-resolver.js +130 -0
  134. package/dist-server/service/station/station-screen-control-withdrawal-resolver.js.map +1 -0
  135. package/dist-server/service/station/station-screen-control-withdrawal.d.ts +104 -0
  136. package/dist-server/service/station/station-screen-control-withdrawal.js +128 -0
  137. package/dist-server/service/station/station-screen-control-withdrawal.js.map +1 -0
  138. package/dist-server/service/station/station-screen-draft-resolver.d.ts +47 -0
  139. package/dist-server/service/station/station-screen-draft-resolver.js +153 -0
  140. package/dist-server/service/station/station-screen-draft-resolver.js.map +1 -0
  141. package/dist-server/service/station/station-screen-layout.d.ts +119 -0
  142. package/dist-server/service/station/station-screen-layout.js +202 -0
  143. package/dist-server/service/station/station-screen-layout.js.map +1 -0
  144. package/dist-server/service/station/station-screen-list-resolver.d.ts +69 -0
  145. package/dist-server/service/station/station-screen-list-resolver.js +215 -0
  146. package/dist-server/service/station/station-screen-list-resolver.js.map +1 -0
  147. package/dist-server/service/station/station-screen-publish-subscriber.d.ts +22 -0
  148. package/dist-server/service/station/station-screen-publish-subscriber.js +90 -0
  149. package/dist-server/service/station/station-screen-publish-subscriber.js.map +1 -0
  150. package/dist-server/service/station/station-screen-release-gate.d.ts +66 -0
  151. package/dist-server/service/station/station-screen-release-gate.js +67 -0
  152. package/dist-server/service/station/station-screen-release-gate.js.map +1 -0
  153. package/dist-server/service/station/station-screen-release-record.d.ts +62 -0
  154. package/dist-server/service/station/station-screen-release-record.js +139 -0
  155. package/dist-server/service/station/station-screen-release-record.js.map +1 -0
  156. package/dist-server/service/station/station-screen-release-signature.d.ts +23 -0
  157. package/dist-server/service/station/station-screen-release-signature.js +32 -0
  158. package/dist-server/service/station/station-screen-release-signature.js.map +1 -0
  159. package/dist-server/service/station/station-screen-scorecard.d.ts +15 -0
  160. package/dist-server/service/station/station-screen-scorecard.js +35 -0
  161. package/dist-server/service/station/station-screen-scorecard.js.map +1 -0
  162. package/dist-server/service/station/station-screen-spec-resolver.d.ts +36 -0
  163. package/dist-server/service/station/station-screen-spec-resolver.js +202 -0
  164. package/dist-server/service/station/station-screen-spec-resolver.js.map +1 -0
  165. package/dist-server/service/station/station-screen-spec.d.ts +104 -0
  166. package/dist-server/service/station/station-screen-spec.js +114 -0
  167. package/dist-server/service/station/station-screen-spec.js.map +1 -0
  168. package/dist-server/service/station/station-screen-thumbnail.d.ts +11 -0
  169. package/dist-server/service/station/station-screen-thumbnail.js +36 -0
  170. package/dist-server/service/station/station-screen-thumbnail.js.map +1 -0
  171. package/dist-server/service/station/station-service-resolver.d.ts +27 -0
  172. package/dist-server/service/station/station-service-resolver.js +95 -0
  173. package/dist-server/service/station/station-service-resolver.js.map +1 -0
  174. package/dist-server/tsconfig.tsbuildinfo +1 -0
  175. package/package.json +44 -0
  176. package/shared/station-axes.d.ts +4 -0
  177. package/shared/station-axes.js +155 -0
  178. package/shared/station-screen-flow-check.d.ts +12 -0
  179. package/shared/station-screen-flow-check.js +74 -0
  180. package/things-scene.config.js +15 -0
  181. package/translations/en.json +42 -0
  182. package/translations/ja.json +42 -0
  183. package/translations/ko.json +42 -0
  184. package/translations/ms.json +42 -0
  185. package/translations/zh.json +42 -0
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.screenModel = screenModel;
4
+ exports.partsOfFlow = partsOfFlow;
5
+ exports.draftScreen = draftScreen;
6
+ exports.axesOfParts = axesOfParts;
7
+ const station_flow_1 = require("./station-flow");
8
+ const station_runtime_binding_1 = require("./station-runtime-binding");
9
+ /** 작업대 화면의 판 크기. 벤치 태블릿의 가로 기준이다. */
10
+ const BOARD = { width: 1280, height: 720 };
11
+ const PAD = 24;
12
+ const GAP = 16;
13
+ /** 부품마다의 높이. 읽는 거리에서 정해진다 — 한 걸음 떨어져 보는 화면이다. */
14
+ const HEIGHT = {
15
+ field: 96,
16
+ fieldBig: 132,
17
+ who: 120,
18
+ order: 132,
19
+ counts: 132,
20
+ numpad: 260,
21
+ action: 88,
22
+ staged: 200,
23
+ directive: 220,
24
+ maintenance: 120,
25
+ context: 0
26
+ };
27
+ /** 한 줄에 여럿이 서는 것들. 나머지는 한 줄에 하나다. */
28
+ const SIDE_BY_SIDE = new Set(['numpad', 'action']);
29
+ /**
30
+ * 부품에서 모델을 만든다.
31
+ *
32
+ * `context` 는 그려지지 않지만 **모델에는 들어간다** — 갱신 주기를 든 부품이라, 없으면 그 화면이
33
+ * 무엇을 얼마나 자주 읽을지 아무 데도 안 적힌다. 자리는 판 밖에 둔다.
34
+ */
35
+ function screenModel(parts) {
36
+ const components = [];
37
+ const inner = BOARD.width - PAD * 2;
38
+ let top = PAD;
39
+ let index = 0;
40
+ /* 나란히 설 것들을 묶어 가며 내려간다. */
41
+ for (let at = 0; at < parts.length; at++) {
42
+ const part = parts[at];
43
+ if (part.part === 'context') {
44
+ components.push({
45
+ type: 'station-context',
46
+ id: `part-${index++}`,
47
+ /* 판 아래에 둔다 — 그려지지 않지만 저작면에서는 집어야 한다. */
48
+ left: PAD,
49
+ top: BOARD.height + GAP,
50
+ width: 240,
51
+ height: 60,
52
+ refreshSeconds: part.refreshSeconds ?? 10
53
+ });
54
+ continue;
55
+ }
56
+ /* 같은 종류가 이어지고 그것이 나란히 서는 것이면 한 줄에 담는다. */
57
+ const row = [part];
58
+ if (SIDE_BY_SIDE.has(part.part)) {
59
+ while (at + 1 < parts.length && parts[at + 1].part === part.part) {
60
+ row.push(parts[++at]);
61
+ }
62
+ }
63
+ const width = Math.floor((inner - GAP * (row.length - 1)) / row.length);
64
+ const height = heightOf(row[0]);
65
+ row.forEach((one, column) => {
66
+ components.push({
67
+ ...propsOf(one),
68
+ id: `part-${index++}`,
69
+ left: PAD + column * (width + GAP),
70
+ top,
71
+ width,
72
+ height
73
+ });
74
+ });
75
+ top += height + GAP;
76
+ }
77
+ return JSON.stringify({ width: BOARD.width, height: Math.max(BOARD.height, top + PAD), components });
78
+ }
79
+ function heightOf(part) {
80
+ if (part.part === 'field') {
81
+ return part.big ? HEIGHT.fieldBig : HEIGHT.field;
82
+ }
83
+ return HEIGHT[part.part] ?? HEIGHT.field;
84
+ }
85
+ /**
86
+ * 부품 선언 → 씬 컴포넌트의 속성.
87
+ *
88
+ * `equipment` 를 넣지 않는다. 비우면 그 화면이 걸린 작업대를 따르고, 적으면 그 설비에 못 박힌다 —
89
+ * 데모 화면은 복제해 고치는 첫 장이라 못 박으면 안 된다.
90
+ */
91
+ function propsOf(part) {
92
+ switch (part.part) {
93
+ case 'field':
94
+ return {
95
+ type: 'station-field',
96
+ field: part.field,
97
+ label: part.label ?? '',
98
+ /* 큰 값은 한 걸음 떨어져 읽는 자리다. */
99
+ valueSize: part.big ? 42 : 26
100
+ };
101
+ case 'who':
102
+ return { type: 'station-who', label: part.label ?? '', prompt: part.prompt ?? '' };
103
+ case 'numpad':
104
+ return { type: 'station-numpad', label: part.label ?? '', counts: part.counts ?? 'good', ...(part.disabled ? { disabled: true } : {}) };
105
+ case 'action':
106
+ return {
107
+ type: 'station-action',
108
+ label: part.label ?? '',
109
+ action: part.action,
110
+ ...(part.disabled ? { disabled: true } : {}),
111
+ ...(part.reasonCode ? { reasonCode: part.reasonCode } : {})
112
+ };
113
+ case 'staged':
114
+ return { type: 'station-staged', label: part.label ?? '', removable: part.removable ?? false };
115
+ default:
116
+ /* order · counts · directive · maintenance — 이름표 하나뿐이다. */
117
+ return { type: `station-${part.part}`, label: part.label ?? '' };
118
+ }
119
+ }
120
+ /**
121
+ * 흐름 → 그려질 부품.
122
+ *
123
+ * ── 단계와 부품은 하나가 아니다 ────────────────────────────────────────────
124
+ * 「값 넣기」 한 단계가 숫자패드 하나를 낳고, 「보기」 한 단계가 읽는 축 수만큼 값을 낳고, 「설정」은
125
+ * 하나도 안 낳는다(그려지지 않지만 모델에는 남는다 — 갱신 주기를 든 부품이라).
126
+ *
127
+ * ── 이름표가 없으면 축 이름을 쓴다 ─────────────────────────────────────────
128
+ * 빈 이름표로 두지 않는다. 작업대에서 맨숫자는 가장 자주 오독되는 것이고, 그 오독은 수량을 잘못
129
+ * 적는 것으로 이어진다.
130
+ */
131
+ function partsOfFlow(flow) {
132
+ const parts = [];
133
+ for (const step of flow.steps) {
134
+ parts.push(...partsOfStep(step, flow));
135
+ }
136
+ return parts;
137
+ }
138
+ function partsOfStep(step, flow) {
139
+ switch (step.kind) {
140
+ case 'scan':
141
+ return [{ part: 'who', label: step.label, prompt: '' }];
142
+ case 'enter':
143
+ /*
144
+ * 무엇을 받는 자리인지는 이 단계를 **쓰는 쪽**이 안다(`{ from: 'input' }`). 여기서는 모르므로
145
+ * 기본값을 두고, 저작면이 고른 뒤에 바뀐다.
146
+ */
147
+ return [{ part: 'numpad', label: step.label, counts: (0, station_runtime_binding_1.entryCounts)(flow, step.id) ?? 'good', disabled: !(0, station_runtime_binding_1.entryCounts)(flow, step.id) }];
148
+ case 'run':
149
+ return [{ part: 'action', label: step.label, action: (0, station_runtime_binding_1.runAction)(step) ?? 'start', disabled: !(0, station_runtime_binding_1.runAction)(step) }];
150
+ case 'settings':
151
+ /* 갱신 주기를 여기서 떨어뜨리면, 계산해 둔 값이 판까지 안 가고 전부 기본값으로 선다. */
152
+ return [{ part: 'context', ...(step.refreshSeconds ? { refreshSeconds: step.refreshSeconds } : {}) }];
153
+ case 'legacy':
154
+ return step.legacyPart ? [step.legacyPart] : [];
155
+ default:
156
+ /* 보기 — 읽는 축마다 값 하나. 축이 없으면 그릴 것도 없다. */
157
+ return (step.reads ?? []).map(axis => ({ part: 'field', field: axis, label: step.label ?? axis }));
158
+ }
159
+ }
160
+ /**
161
+ * 흐름 하나가 낳는 **모든 것**을 한 번에 — 도안 · 읽는 축 · 저작자가 알아야 할 것.
162
+ *
163
+ * ── 왜 셋을 묶었나 ────────────────────────────────────────────────────────
164
+ * 저작면의 미리보기와 저장이 같은 계산을 봐야 한다. 미리보기가 도안만 받고 저장이 축을 따로 세면,
165
+ * 저작자가 본 화면과 배정 검증이 검사하는 축이 갈릴 수 있다 — 그리고 그 차이는 배정하는 날에야
166
+ * 드러난다.
167
+ *
168
+ * ── 빈 흐름은 오류가 아니다 ───────────────────────────────────────────────
169
+ * 저작면은 고칠 때마다 묻고, 그중에는 마지막 단계를 지운 편집도 있다. 그때 던지면 화면이 「그리지
170
+ * 못했습니다」를 말하는데 사실은 그릴 것이 없는 것뿐이다.
171
+ */
172
+ function draftScreen(flow) {
173
+ const safe = { steps: Array.isArray(flow?.steps) ? flow.steps : [] };
174
+ return {
175
+ model: screenModel(partsOfFlow(safe)),
176
+ requires: (0, station_flow_1.axesRead)(safe),
177
+ problems: [...(0, station_flow_1.checkFlow)(safe), ...safe.steps.filter(step => (step.kind === 'run' && !(0, station_runtime_binding_1.runAction)(step)) || (step.kind === 'enter' && !(0, station_runtime_binding_1.entryCounts)(safe, step.id))).map(step => ({
178
+ level: 'error', stepId: step.id, code: 'text.station-runtime-not-connected',
179
+ params: { step: step.label ?? step.id },
180
+ says: `「${step.label ?? step.id}」의 실행 연결이 없습니다. 다른 업무로 대신 실행하지 않습니다.`
181
+ }))]
182
+ };
183
+ }
184
+ /**
185
+ * 이 부품들이 읽는 컨텍스트 축.
186
+ *
187
+ * `StationScreenSpec.requires` 는 **파생값이다.** 손으로 적으면 부품을 고칠 때 어긋나고, 그러면
188
+ * 배정 검증이 이미 사실이 아닌 것을 검사한다.
189
+ *
190
+ * 흐름 쪽에는 같은 일을 하는 `axesRead` 가 따로 있다 — 들어오는 어휘가 다르기 때문이다(시드는
191
+ * 부품으로, 저작면은 단계로 들어온다). 규칙이 갈리지 않게 둘 다 여기 계산기 옆에 산다.
192
+ */
193
+ function axesOfParts(parts) {
194
+ const seen = new Set();
195
+ for (const part of parts) {
196
+ if (part.part === 'field') {
197
+ seen.add(part.field);
198
+ }
199
+ }
200
+ return [...seen].sort();
201
+ }
202
+ //# sourceMappingURL=station-screen-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"station-screen-layout.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-layout.ts"],"names":[],"mappings":"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","sourcesContent":["import { axesRead, checkFlow } from './station-flow'\nimport type { StationFlow, StationProblem, StationStep } from './station-flow'\nimport { entryCounts, runAction } from './station-runtime-binding'\n\n/**\n * 부품 목록 → things-scene 모델. **자리는 여기서 계산한다.**\n *\n * ── 왜 아무도 좌표를 안 드나 ───────────────────────────────────────────────\n * 부품 하나를 더할 때마다 나머지를 손으로 밀어야 하고, 그렇게 만든 판은 2px 씩 어긋난다. 폼에서\n * 눈에 걸리는 것이 대개 그 어긋남이다.\n *\n * ── 계산기는 **한 벌이다** ─────────────────────────────────────────────────\n * 시드도 저작면도 여기를 지난다. 두 벌을 두면 시드로 세운 화면과 사람이 만든 화면이 다르게 생기고,\n * 그때 어느 쪽이 맞는지 아무도 못 정한다. 그래서 이 파일은 시드가 아니라 **station 어휘**에 산다.\n *\n * ── 규칙은 하나다: 세로로 쌓고, 짝지을 것만 나란히 ─────────────────────────\n * 숫자패드와 단추는 여럿이 나란히 놓인다 — 작업자가 손을 옮기는 거리가 짧아야 한다. 나머지는\n * 한 줄에 하나다.\n */\n\n/**\n * 화면에 놓이는 부품 하나 — **그려지는 것만.**\n *\n * 흐름의 단계(`StationStep`)와 다르다. 단계는 「사람이 무엇을 하나」이고 여기는 「무엇이 그려지나」다.\n * 한 단계가 부품 둘을 낳기도 하고(값 넣기 → 숫자패드), 하나도 안 낳기도 한다(설정).\n */\nexport type StationScreenPart =\n /** 컨텍스트 축 하나를 값으로. `stationBindingNames()` 가 이름의 유일한 원본이다. */\n | { part: 'field'; field: string; label?: string; big?: boolean }\n | { part: 'who'; label?: string; prompt?: string }\n | { part: 'order'; label?: string }\n | { part: 'counts'; label?: string }\n | { part: 'numpad'; label?: string; counts?: 'good' | 'scrap'; disabled?: boolean }\n | { part: 'action'; label?: string; action: 'start' | 'finish' | 'pause' | 'resume'; reasonCode?: string; disabled?: boolean }\n | { part: 'staged'; label?: string; removable?: boolean }\n | { part: 'directive'; label?: string }\n | { part: 'maintenance'; label?: string }\n | { part: 'context'; refreshSeconds?: number }\n\n/** 작업대 화면의 판 크기. 벤치 태블릿의 가로 기준이다. */\nconst BOARD = { width: 1280, height: 720 }\n\nconst PAD = 24\nconst GAP = 16\n\n/** 부품마다의 높이. 읽는 거리에서 정해진다 — 한 걸음 떨어져 보는 화면이다. */\nconst HEIGHT: Record<string, number> = {\n field: 96,\n fieldBig: 132,\n who: 120,\n order: 132,\n counts: 132,\n numpad: 260,\n action: 88,\n staged: 200,\n directive: 220,\n maintenance: 120,\n context: 0\n}\n\n/** 한 줄에 여럿이 서는 것들. 나머지는 한 줄에 하나다. */\nconst SIDE_BY_SIDE = new Set(['numpad', 'action'])\n\ninterface Placed {\n type: string\n id: string\n left: number\n top: number\n width: number\n height: number\n [more: string]: unknown\n}\n\n/**\n * 부품에서 모델을 만든다.\n *\n * `context` 는 그려지지 않지만 **모델에는 들어간다** — 갱신 주기를 든 부품이라, 없으면 그 화면이\n * 무엇을 얼마나 자주 읽을지 아무 데도 안 적힌다. 자리는 판 밖에 둔다.\n */\nexport function screenModel(parts: StationScreenPart[]): string {\n const components: Placed[] = []\n const inner = BOARD.width - PAD * 2\n\n let top = PAD\n let index = 0\n\n /* 나란히 설 것들을 묶어 가며 내려간다. */\n for (let at = 0; at < parts.length; at++) {\n const part = parts[at]\n\n if (part.part === 'context') {\n components.push({\n type: 'station-context',\n id: `part-${index++}`,\n /* 판 아래에 둔다 — 그려지지 않지만 저작면에서는 집어야 한다. */\n left: PAD,\n top: BOARD.height + GAP,\n width: 240,\n height: 60,\n refreshSeconds: part.refreshSeconds ?? 10\n })\n continue\n }\n\n /* 같은 종류가 이어지고 그것이 나란히 서는 것이면 한 줄에 담는다. */\n const row: StationScreenPart[] = [part]\n\n if (SIDE_BY_SIDE.has(part.part)) {\n while (at + 1 < parts.length && parts[at + 1].part === part.part) {\n row.push(parts[++at])\n }\n }\n\n const width = Math.floor((inner - GAP * (row.length - 1)) / row.length)\n const height = heightOf(row[0])\n\n row.forEach((one, column) => {\n components.push({\n ...(propsOf(one) as { type: string }),\n id: `part-${index++}`,\n left: PAD + column * (width + GAP),\n top,\n width,\n height\n })\n })\n\n top += height + GAP\n }\n\n return JSON.stringify({ width: BOARD.width, height: Math.max(BOARD.height, top + PAD), components })\n}\n\nfunction heightOf(part: StationScreenPart): number {\n if (part.part === 'field') {\n return part.big ? HEIGHT.fieldBig : HEIGHT.field\n }\n\n return HEIGHT[part.part] ?? HEIGHT.field\n}\n\n/**\n * 부품 선언 → 씬 컴포넌트의 속성.\n *\n * `equipment` 를 넣지 않는다. 비우면 그 화면이 걸린 작업대를 따르고, 적으면 그 설비에 못 박힌다 —\n * 데모 화면은 복제해 고치는 첫 장이라 못 박으면 안 된다.\n */\nfunction propsOf(part: StationScreenPart): Record<string, unknown> {\n switch (part.part) {\n case 'field':\n return {\n type: 'station-field',\n field: part.field,\n label: part.label ?? '',\n /* 큰 값은 한 걸음 떨어져 읽는 자리다. */\n valueSize: part.big ? 42 : 26\n }\n\n case 'who':\n return { type: 'station-who', label: part.label ?? '', prompt: part.prompt ?? '' }\n\n case 'numpad':\n return { type: 'station-numpad', label: part.label ?? '', counts: part.counts ?? 'good', ...(part.disabled ? { disabled: true } : {}) }\n\n case 'action':\n return {\n type: 'station-action',\n label: part.label ?? '',\n action: part.action,\n ...(part.disabled ? { disabled: true } : {}),\n ...(part.reasonCode ? { reasonCode: part.reasonCode } : {})\n }\n\n case 'staged':\n return { type: 'station-staged', label: part.label ?? '', removable: part.removable ?? false }\n\n default:\n /* order · counts · directive · maintenance — 이름표 하나뿐이다. */\n return { type: `station-${part.part}`, label: (part as { label?: string }).label ?? '' }\n }\n}\n\n/**\n * 흐름 → 그려질 부품.\n *\n * ── 단계와 부품은 하나가 아니다 ────────────────────────────────────────────\n * 「값 넣기」 한 단계가 숫자패드 하나를 낳고, 「보기」 한 단계가 읽는 축 수만큼 값을 낳고, 「설정」은\n * 하나도 안 낳는다(그려지지 않지만 모델에는 남는다 — 갱신 주기를 든 부품이라).\n *\n * ── 이름표가 없으면 축 이름을 쓴다 ─────────────────────────────────────────\n * 빈 이름표로 두지 않는다. 작업대에서 맨숫자는 가장 자주 오독되는 것이고, 그 오독은 수량을 잘못\n * 적는 것으로 이어진다.\n */\nexport function partsOfFlow(flow: StationFlow): StationScreenPart[] {\n const parts: StationScreenPart[] = []\n\n for (const step of flow.steps) {\n parts.push(...partsOfStep(step, flow))\n }\n\n return parts\n}\n\nfunction partsOfStep(step: StationStep, flow: StationFlow): StationScreenPart[] {\n switch (step.kind) {\n case 'scan':\n return [{ part: 'who', label: step.label, prompt: '' }]\n\n case 'enter':\n /*\n * 무엇을 받는 자리인지는 이 단계를 **쓰는 쪽**이 안다(`{ from: 'input' }`). 여기서는 모르므로\n * 기본값을 두고, 저작면이 고른 뒤에 바뀐다.\n */\n return [{ part: 'numpad', label: step.label, counts: entryCounts(flow, step.id) ?? 'good', disabled: !entryCounts(flow, step.id) }]\n\n case 'run':\n return [{ part: 'action', label: step.label, action: runAction(step) ?? 'start', disabled: !runAction(step) }]\n\n case 'settings':\n /* 갱신 주기를 여기서 떨어뜨리면, 계산해 둔 값이 판까지 안 가고 전부 기본값으로 선다. */\n return [{ part: 'context', ...(step.refreshSeconds ? { refreshSeconds: step.refreshSeconds } : {}) }]\n\n case 'legacy':\n return step.legacyPart ? [step.legacyPart as StationScreenPart] : []\n\n default:\n /* 보기 — 읽는 축마다 값 하나. 축이 없으면 그릴 것도 없다. */\n return (step.reads ?? []).map(axis => ({ part: 'field', field: axis, label: step.label ?? axis }))\n }\n}\n\n/**\n * 이 실행 단계가 어떤 단추인가.\n *\n * 단계의 `status` 출처가 `{ from: 'step' }` 이라는 것은 「단계의 뜻이 정한다」는 말이고, 그 뜻이\n * 여기서 단추 모양으로도 나온다. 아직 흐름이 그것을 따로 적지 않으므로 이름표에서 읽는다 —\n * 흐름에 칸이 생기면 그때 그것을 본다.\n */\n\n/** 저장하지 않고 흐름이 무엇을 그릴지 — 저작면의 미리보기가 이것을 받는다. */\nexport interface StationScreenDraftResult {\n model: string\n requires: string[]\n problems: StationProblem[]\n}\n\n/**\n * 흐름 하나가 낳는 **모든 것**을 한 번에 — 도안 · 읽는 축 · 저작자가 알아야 할 것.\n *\n * ── 왜 셋을 묶었나 ────────────────────────────────────────────────────────\n * 저작면의 미리보기와 저장이 같은 계산을 봐야 한다. 미리보기가 도안만 받고 저장이 축을 따로 세면,\n * 저작자가 본 화면과 배정 검증이 검사하는 축이 갈릴 수 있다 — 그리고 그 차이는 배정하는 날에야\n * 드러난다.\n *\n * ── 빈 흐름은 오류가 아니다 ───────────────────────────────────────────────\n * 저작면은 고칠 때마다 묻고, 그중에는 마지막 단계를 지운 편집도 있다. 그때 던지면 화면이 「그리지\n * 못했습니다」를 말하는데 사실은 그릴 것이 없는 것뿐이다.\n */\nexport function draftScreen(flow: StationFlow | null | undefined): StationScreenDraftResult {\n const safe: StationFlow = { steps: Array.isArray(flow?.steps) ? flow!.steps : [] }\n\n return {\n model: screenModel(partsOfFlow(safe)),\n requires: axesRead(safe),\n problems: [...checkFlow(safe), ...safe.steps.filter(step =>\n (step.kind === 'run' && !runAction(step)) || (step.kind === 'enter' && !entryCounts(safe, step.id))\n ).map(step => ({\n level: 'error' as const, stepId: step.id, code: 'text.station-runtime-not-connected',\n params: { step: step.label ?? step.id },\n says: `「${step.label ?? step.id}」의 실행 연결이 없습니다. 다른 업무로 대신 실행하지 않습니다.`\n }))]\n }\n}\n\n/**\n * 이 부품들이 읽는 컨텍스트 축.\n *\n * `StationScreenSpec.requires` 는 **파생값이다.** 손으로 적으면 부품을 고칠 때 어긋나고, 그러면\n * 배정 검증이 이미 사실이 아닌 것을 검사한다.\n *\n * 흐름 쪽에는 같은 일을 하는 `axesRead` 가 따로 있다 — 들어오는 어휘가 다르기 때문이다(시드는\n * 부품으로, 저작면은 단계로 들어온다). 규칙이 갈리지 않게 둘 다 여기 계산기 옆에 산다.\n */\nexport function axesOfParts(parts: StationScreenPart[]): string[] {\n const seen = new Set<string>()\n\n for (const part of parts) {\n if (part.part === 'field') {\n seen.add(part.field)\n }\n }\n\n return [...seen].sort()\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import { ListParam } from '@things-factory/shell';
2
+ /**
3
+ * One board, seen as a station screen.
4
+ *
5
+ * ## Why this is a view and not a table
6
+ *
7
+ * The drawing is a `Board`; what makes it a *station* screen is the `StationScreenSpec` beside it.
8
+ * This type joins the two, plus the one fact neither of them holds — how many stations are pointed
9
+ * at it — so that a card can be chosen from rather than merely looked at.
10
+ */
11
+ export declare class StationScreenCard {
12
+ boardId: string;
13
+ name: string;
14
+ description?: string;
15
+ /**
16
+ * The picture, as a path — **not the picture itself.**
17
+ *
18
+ * `Board.thumbnail` is a base64 column, and a page of them is megabytes in one response. The
19
+ * Station owns the card's image route. Asking board-service directly would require a separate
20
+ * Board privilege and make a Station manager see empty cards. The path is built here because
21
+ * whether it carries a `domain/{subdomain}` segment depends on how this deployment resolves
22
+ * tenants, and the server is the side that knows.
23
+ */
24
+ thumbnailPath: string;
25
+ /**
26
+ * `'draft'` or `'released'` — the state of the **working copy**, not of what the floor sees.
27
+ *
28
+ * A board reading `'draft'` may still have an older released version in front of operators. What
29
+ * is actually on the floor lives in `BoardHistory`, which board-service registers but does not
30
+ * export by name, so this card cannot say it yet. It says what it can and stays quiet about the
31
+ * rest rather than implying the two are the same.
32
+ */
33
+ state?: string;
34
+ updatedAt?: Date;
35
+ /**
36
+ * How many stations are pointed at this board.
37
+ *
38
+ * The reason a card is worth more than a row. Zero means nobody sees it — a drawing somebody made
39
+ * and never hung. Twelve means editing it changes twelve benches at once. Name and picture let
40
+ * you pick one; this is what lets you decide.
41
+ */
42
+ assignmentCount: number;
43
+ /** Which station context axes the drawing reads, as declared by the spec. */
44
+ requires?: string[];
45
+ target?: Record<string, unknown>;
46
+ /** Whether releasing this screen has to go through approval. */
47
+ controlled?: boolean;
48
+ }
49
+ export declare class StationScreenCardList {
50
+ items: StationScreenCard[];
51
+ total: number;
52
+ }
53
+ export declare class StationScreenListResolver {
54
+ /**
55
+ * Every board that has been declared a station screen.
56
+ *
57
+ * ## What makes a board a station screen
58
+ *
59
+ * Having a `StationScreenSpec`. Not "an assignment points at it" — a screen being drawn but not
60
+ * yet hung is exactly the state this list exists to show. The spec is the declaration; the
61
+ * assignment is a consequence.
62
+ *
63
+ * ## Paged, and the pictures stay out of it
64
+ *
65
+ * `Board.thumbnail` and `Board.model` are the two big columns and neither belongs in a listing.
66
+ * The projection leaves them out and the card fetches the image over HTTP instead.
67
+ */
68
+ stationScreens(params: ListParam, context: any): Promise<StationScreenCardList>;
69
+ }
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StationScreenListResolver = exports.StationScreenCardList = exports.StationScreenCard = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const board_service_1 = require("@things-factory/board-service");
7
+ const shell_1 = require("@things-factory/shell");
8
+ const station_screen_assignment_1 = require("./station-screen-assignment");
9
+ const station_screen_spec_1 = require("./station-screen-spec");
10
+ /**
11
+ * One board, seen as a station screen.
12
+ *
13
+ * ## Why this is a view and not a table
14
+ *
15
+ * The drawing is a `Board`; what makes it a *station* screen is the `StationScreenSpec` beside it.
16
+ * This type joins the two, plus the one fact neither of them holds — how many stations are pointed
17
+ * at it — so that a card can be chosen from rather than merely looked at.
18
+ */
19
+ let StationScreenCard = class StationScreenCard {
20
+ };
21
+ exports.StationScreenCard = StationScreenCard;
22
+ tslib_1.__decorate([
23
+ (0, type_graphql_1.Field)(),
24
+ tslib_1.__metadata("design:type", String)
25
+ ], StationScreenCard.prototype, "boardId", void 0);
26
+ tslib_1.__decorate([
27
+ (0, type_graphql_1.Field)(),
28
+ tslib_1.__metadata("design:type", String)
29
+ ], StationScreenCard.prototype, "name", void 0);
30
+ tslib_1.__decorate([
31
+ (0, type_graphql_1.Field)({ nullable: true }),
32
+ tslib_1.__metadata("design:type", String)
33
+ ], StationScreenCard.prototype, "description", void 0);
34
+ tslib_1.__decorate([
35
+ (0, type_graphql_1.Field)({ description: 'path to the thumbnail image — not the image' }),
36
+ tslib_1.__metadata("design:type", String)
37
+ ], StationScreenCard.prototype, "thumbnailPath", void 0);
38
+ tslib_1.__decorate([
39
+ (0, type_graphql_1.Field)({ nullable: true }),
40
+ tslib_1.__metadata("design:type", String)
41
+ ], StationScreenCard.prototype, "state", void 0);
42
+ tslib_1.__decorate([
43
+ (0, type_graphql_1.Field)({ nullable: true }),
44
+ tslib_1.__metadata("design:type", Date
45
+ /**
46
+ * How many stations are pointed at this board.
47
+ *
48
+ * The reason a card is worth more than a row. Zero means nobody sees it — a drawing somebody made
49
+ * and never hung. Twelve means editing it changes twelve benches at once. Name and picture let
50
+ * you pick one; this is what lets you decide.
51
+ */
52
+ )
53
+ ], StationScreenCard.prototype, "updatedAt", void 0);
54
+ tslib_1.__decorate([
55
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'how many station assignments point at this board' }),
56
+ tslib_1.__metadata("design:type", Number)
57
+ ], StationScreenCard.prototype, "assignmentCount", void 0);
58
+ tslib_1.__decorate([
59
+ (0, type_graphql_1.Field)(type => [String], { nullable: true }),
60
+ tslib_1.__metadata("design:type", Array)
61
+ ], StationScreenCard.prototype, "requires", void 0);
62
+ tslib_1.__decorate([
63
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true }),
64
+ tslib_1.__metadata("design:type", Object)
65
+ ], StationScreenCard.prototype, "target", void 0);
66
+ tslib_1.__decorate([
67
+ (0, type_graphql_1.Field)({ nullable: true }),
68
+ tslib_1.__metadata("design:type", Boolean)
69
+ ], StationScreenCard.prototype, "controlled", void 0);
70
+ exports.StationScreenCard = StationScreenCard = tslib_1.__decorate([
71
+ (0, type_graphql_1.ObjectType)({ description: 'a board seen as a station screen' })
72
+ ], StationScreenCard);
73
+ let StationScreenCardList = class StationScreenCardList {
74
+ };
75
+ exports.StationScreenCardList = StationScreenCardList;
76
+ tslib_1.__decorate([
77
+ (0, type_graphql_1.Field)(type => [StationScreenCard]),
78
+ tslib_1.__metadata("design:type", Array)
79
+ ], StationScreenCardList.prototype, "items", void 0);
80
+ tslib_1.__decorate([
81
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int),
82
+ tslib_1.__metadata("design:type", Number)
83
+ ], StationScreenCardList.prototype, "total", void 0);
84
+ exports.StationScreenCardList = StationScreenCardList = tslib_1.__decorate([
85
+ (0, type_graphql_1.ObjectType)({ description: 'a page of station screens' })
86
+ ], StationScreenCardList);
87
+ let StationScreenListResolver = class StationScreenListResolver {
88
+ /**
89
+ * Every board that has been declared a station screen.
90
+ *
91
+ * ## What makes a board a station screen
92
+ *
93
+ * Having a `StationScreenSpec`. Not "an assignment points at it" — a screen being drawn but not
94
+ * yet hung is exactly the state this list exists to show. The spec is the declaration; the
95
+ * assignment is a consequence.
96
+ *
97
+ * ## Paged, and the pictures stay out of it
98
+ *
99
+ * `Board.thumbnail` and `Board.model` are the two big columns and neither belongs in a listing.
100
+ * The projection leaves them out and the card fetches the image over HTTP instead.
101
+ */
102
+ async stationScreens(params, context) {
103
+ const { domain } = context.state;
104
+ const { pagination, sortings = [], search } = params;
105
+ const page = pagination?.page ?? 1;
106
+ const limit = pagination?.limit ?? 30;
107
+ /*
108
+ * The query is built here rather than with `getQueryBuilderFromListParams`.
109
+ *
110
+ * That helper sorts and filters against **one repository's own columns**, and almost nothing a
111
+ * person wants to sort this list by is a column of `StationScreenSpec`: the name and the state
112
+ * belong to the board, and "how many stations show it" is counted, not stored. Declaring
113
+ * `assignmentCount` as a sorter with the helper sent it straight into SQL —
114
+ * `no such column: spec.assignmentCount` [seen 2026-09-09].
115
+ *
116
+ * So the board is joined and the count is a subquery, which puts every sortable value in the
117
+ * database where ordering and paging can actually agree with each other. Sorting a page after
118
+ * fetching it would order the page, not the list.
119
+ */
120
+ const counted = (sub) => sub.select('COUNT(1)').from(station_screen_assignment_1.StationScreenAssignment, 'assignment').where('assignment.boardId = spec.boardId');
121
+ const query = (0, shell_1.getRepository)(station_screen_spec_1.StationScreenSpec)
122
+ .createQueryBuilder('spec')
123
+ .innerJoin(board_service_1.Board, 'board', 'board.id = spec.boardId')
124
+ .where('spec.domain = :domain', { domain: domain.id })
125
+ .select('spec.boardId', 'boardId')
126
+ .addSelect('spec.requires', 'requires')
127
+ .addSelect('spec.target', 'target')
128
+ .addSelect('spec.controlled', 'controlled')
129
+ .addSelect('board.name', 'name')
130
+ .addSelect('board.description', 'description')
131
+ .addSelect('board.state', 'state')
132
+ .addSelect('board.updatedAt', 'updatedAt')
133
+ .addSelect('board.thumbnailUpdatedAt', 'thumbnailUpdatedAt')
134
+ .addSelect(counted, 'assignmentCount');
135
+ if (search?.trim()) {
136
+ query.andWhere('(board.name LIKE :term OR board.description LIKE :term)', { term: `%${search.trim()}%` });
137
+ }
138
+ /*
139
+ * Only these can be ordered by, and the list says so rather than passing whatever arrives
140
+ * through to the database. An unknown name coming from the client is a 500 otherwise.
141
+ */
142
+ const ORDERABLE = {
143
+ name: 'board.name',
144
+ state: 'board.state',
145
+ updatedAt: 'board.updatedAt',
146
+ assignmentCount: 'assignmentCount'
147
+ };
148
+ const asked = sortings.filter((sorting) => ORDERABLE[sorting.name]);
149
+ if (asked.length) {
150
+ asked.forEach((sorting, at) => {
151
+ const direction = sorting.desc ? 'DESC' : 'ASC';
152
+ at === 0 ? query.orderBy(ORDERABLE[sorting.name], direction) : query.addOrderBy(ORDERABLE[sorting.name], direction);
153
+ });
154
+ }
155
+ else {
156
+ /* Nobody has hung it anywhere — the most common unfinished state, so it comes first. */
157
+ query.orderBy('assignmentCount', 'ASC').addOrderBy('board.name', 'ASC');
158
+ }
159
+ const total = await query.getCount();
160
+ const rows = await query
161
+ .offset((page - 1) * limit)
162
+ .limit(limit)
163
+ .getRawMany();
164
+ /**
165
+ * Raw rows carry whatever the driver returns, and sqlite returns a datetime as the string it
166
+ * stored — `2026-09-09 05:25:16`, which the `DateTime` scalar refuses [seen 2026-09-09]. TypeORM
167
+ * writes those in UTC, so that is how they are read back; a driver that already hands over a
168
+ * `Date` passes straight through.
169
+ */
170
+ const asDate = (value) => {
171
+ if (!value)
172
+ return undefined;
173
+ if (value instanceof Date)
174
+ return value;
175
+ const text = String(value);
176
+ return new Date(text.includes('T') || text.endsWith('Z') ? text : `${text.replace(' ', 'T')}Z`);
177
+ };
178
+ const contextPath = (0, shell_1.getContextPath)(domain);
179
+ return {
180
+ total,
181
+ items: rows.map(row => ({
182
+ boardId: row.boardId,
183
+ name: row.name,
184
+ description: row.description,
185
+ thumbnailPath: `/${contextPath}station-screen-thumbnail/${row.boardId}${row.thumbnailUpdatedAt || row.updatedAt ? `?v=${new Date(row.thumbnailUpdatedAt ?? row.updatedAt).getTime()}` : ''}`,
186
+ state: row.state,
187
+ updatedAt: asDate(row.updatedAt),
188
+ assignmentCount: Number(row.assignmentCount ?? 0),
189
+ /*
190
+ * Raw rows skip the column transforms, so the stored shapes come back as they are stored:
191
+ * `simple-array` as a comma-joined string and `simple-json` as text. Converted here rather
192
+ * than handed to the client as-is.
193
+ */
194
+ requires: row.requires ? String(row.requires).split(',').filter(Boolean) : undefined,
195
+ target: row.target ? JSON.parse(String(row.target)) : undefined,
196
+ controlled: Boolean(row.controlled)
197
+ }))
198
+ };
199
+ }
200
+ };
201
+ exports.StationScreenListResolver = StationScreenListResolver;
202
+ tslib_1.__decorate([
203
+ (0, type_graphql_1.Directive)('@privilege(category: "station", privilege: "query", domainOwnerGranted: true)'),
204
+ (0, type_graphql_1.Query)(returns => StationScreenCardList, { description: 'a page of boards declared as station screens' }),
205
+ tslib_1.__param(0, (0, type_graphql_1.Args)(type => shell_1.ListParam)),
206
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
207
+ tslib_1.__metadata("design:type", Function),
208
+ tslib_1.__metadata("design:paramtypes", [shell_1.ListParam, Object]),
209
+ tslib_1.__metadata("design:returntype", Promise)
210
+ ], StationScreenListResolver.prototype, "stationScreens", null);
211
+ exports.StationScreenListResolver = StationScreenListResolver = tslib_1.__decorate([
212
+ (0, type_graphql_1.Resolver)(StationScreenCard),
213
+ (0, type_graphql_1.UseMiddleware)(shell_1.SpeakRefusals)
214
+ ], StationScreenListResolver);
215
+ //# sourceMappingURL=station-screen-list-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"station-screen-list-resolver.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-list-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Args, Ctx, Directive, Field, Int, ObjectType, Query, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { Board } from '@things-factory/board-service'\nimport { ListParam, ScalarObject, getContextPath, getRepository, SpeakRefusals } from '@things-factory/shell'\n\nimport { StationScreenAssignment } from './station-screen-assignment'\nimport { StationScreenSpec } from './station-screen-spec'\n\n/**\n * One board, seen as a station screen.\n *\n * ## Why this is a view and not a table\n *\n * The drawing is a `Board`; what makes it a *station* screen is the `StationScreenSpec` beside it.\n * This type joins the two, plus the one fact neither of them holds — how many stations are pointed\n * at it — so that a card can be chosen from rather than merely looked at.\n */\n@ObjectType({ description: 'a board seen as a station screen' })\nexport class StationScreenCard {\n @Field()\n boardId: string\n\n @Field()\n name: string\n\n @Field({ nullable: true })\n description?: string\n\n /**\n * The picture, as a path — **not the picture itself.**\n *\n * `Board.thumbnail` is a base64 column, and a page of them is megabytes in one response. The\n * Station owns the card's image route. Asking board-service directly would require a separate\n * Board privilege and make a Station manager see empty cards. The path is built here because\n * whether it carries a `domain/{subdomain}` segment depends on how this deployment resolves\n * tenants, and the server is the side that knows.\n */\n @Field({ description: 'path to the thumbnail image — not the image' })\n thumbnailPath: string\n\n /**\n * `'draft'` or `'released'` — the state of the **working copy**, not of what the floor sees.\n *\n * A board reading `'draft'` may still have an older released version in front of operators. What\n * is actually on the floor lives in `BoardHistory`, which board-service registers but does not\n * export by name, so this card cannot say it yet. It says what it can and stays quiet about the\n * rest rather than implying the two are the same.\n */\n @Field({ nullable: true })\n state?: string\n\n @Field({ nullable: true })\n updatedAt?: Date\n\n /**\n * How many stations are pointed at this board.\n *\n * The reason a card is worth more than a row. Zero means nobody sees it — a drawing somebody made\n * and never hung. Twelve means editing it changes twelve benches at once. Name and picture let\n * you pick one; this is what lets you decide.\n */\n @Field(type => Int, { description: 'how many station assignments point at this board' })\n assignmentCount: number\n\n /** Which station context axes the drawing reads, as declared by the spec. */\n @Field(type => [String], { nullable: true })\n requires?: string[]\n\n @Field(type => ScalarObject, { nullable: true })\n target?: Record<string, unknown>\n\n /** Whether releasing this screen has to go through approval. */\n @Field({ nullable: true })\n controlled?: boolean\n}\n\n@ObjectType({ description: 'a page of station screens' })\nexport class StationScreenCardList {\n @Field(type => [StationScreenCard])\n items: StationScreenCard[]\n\n @Field(type => Int)\n total: number\n}\n\n@Resolver(StationScreenCard)\n@UseMiddleware(SpeakRefusals)\nexport class StationScreenListResolver {\n /**\n * Every board that has been declared a station screen.\n *\n * ## What makes a board a station screen\n *\n * Having a `StationScreenSpec`. Not \"an assignment points at it\" — a screen being drawn but not\n * yet hung is exactly the state this list exists to show. The spec is the declaration; the\n * assignment is a consequence.\n *\n * ## Paged, and the pictures stay out of it\n *\n * `Board.thumbnail` and `Board.model` are the two big columns and neither belongs in a listing.\n * The projection leaves them out and the card fetches the image over HTTP instead.\n */\n @Directive('@privilege(category: \"station\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => StationScreenCardList, { description: 'a page of boards declared as station screens' })\n async stationScreens(@Args(type => ListParam) params: ListParam, @Ctx() context: any): Promise<StationScreenCardList> {\n const { domain } = context.state\n const { pagination, sortings = [], search } = params as any\n const page = pagination?.page ?? 1\n const limit = pagination?.limit ?? 30\n\n /*\n * The query is built here rather than with `getQueryBuilderFromListParams`.\n *\n * That helper sorts and filters against **one repository's own columns**, and almost nothing a\n * person wants to sort this list by is a column of `StationScreenSpec`: the name and the state\n * belong to the board, and \"how many stations show it\" is counted, not stored. Declaring\n * `assignmentCount` as a sorter with the helper sent it straight into SQL —\n * `no such column: spec.assignmentCount` [seen 2026-09-09].\n *\n * So the board is joined and the count is a subquery, which puts every sortable value in the\n * database where ordering and paging can actually agree with each other. Sorting a page after\n * fetching it would order the page, not the list.\n */\n const counted = (sub: any) => sub.select('COUNT(1)').from(StationScreenAssignment, 'assignment').where('assignment.boardId = spec.boardId')\n\n const query = getRepository(StationScreenSpec)\n .createQueryBuilder('spec')\n .innerJoin(Board, 'board', 'board.id = spec.boardId')\n .where('spec.domain = :domain', { domain: domain.id })\n .select('spec.boardId', 'boardId')\n .addSelect('spec.requires', 'requires')\n .addSelect('spec.target', 'target')\n .addSelect('spec.controlled', 'controlled')\n .addSelect('board.name', 'name')\n .addSelect('board.description', 'description')\n .addSelect('board.state', 'state')\n .addSelect('board.updatedAt', 'updatedAt')\n .addSelect('board.thumbnailUpdatedAt', 'thumbnailUpdatedAt')\n .addSelect(counted, 'assignmentCount')\n\n if (search?.trim()) {\n query.andWhere('(board.name LIKE :term OR board.description LIKE :term)', { term: `%${search.trim()}%` })\n }\n\n /*\n * Only these can be ordered by, and the list says so rather than passing whatever arrives\n * through to the database. An unknown name coming from the client is a 500 otherwise.\n */\n const ORDERABLE: Record<string, string> = {\n name: 'board.name',\n state: 'board.state',\n updatedAt: 'board.updatedAt',\n assignmentCount: 'assignmentCount'\n }\n\n const asked = sortings.filter((sorting: any) => ORDERABLE[sorting.name])\n\n if (asked.length) {\n asked.forEach((sorting: any, at: number) => {\n const direction = sorting.desc ? 'DESC' : 'ASC'\n at === 0 ? query.orderBy(ORDERABLE[sorting.name], direction) : query.addOrderBy(ORDERABLE[sorting.name], direction)\n })\n } else {\n /* Nobody has hung it anywhere — the most common unfinished state, so it comes first. */\n query.orderBy('assignmentCount', 'ASC').addOrderBy('board.name', 'ASC')\n }\n\n const total = await query.getCount()\n const rows = await query\n .offset((page - 1) * limit)\n .limit(limit)\n .getRawMany()\n\n /**\n * Raw rows carry whatever the driver returns, and sqlite returns a datetime as the string it\n * stored — `2026-09-09 05:25:16`, which the `DateTime` scalar refuses [seen 2026-09-09]. TypeORM\n * writes those in UTC, so that is how they are read back; a driver that already hands over a\n * `Date` passes straight through.\n */\n const asDate = (value: unknown): Date | undefined => {\n if (!value) return undefined\n if (value instanceof Date) return value\n\n const text = String(value)\n\n return new Date(text.includes('T') || text.endsWith('Z') ? text : `${text.replace(' ', 'T')}Z`)\n }\n\n const contextPath = getContextPath(domain)\n\n return {\n total,\n items: rows.map(row => ({\n boardId: row.boardId,\n name: row.name,\n description: row.description,\n thumbnailPath: `/${contextPath}station-screen-thumbnail/${row.boardId}${row.thumbnailUpdatedAt || row.updatedAt ? `?v=${new Date(row.thumbnailUpdatedAt ?? row.updatedAt).getTime()}` : ''}`,\n state: row.state,\n updatedAt: asDate(row.updatedAt),\n assignmentCount: Number(row.assignmentCount ?? 0),\n /*\n * Raw rows skip the column transforms, so the stored shapes come back as they are stored:\n * `simple-array` as a comma-joined string and `simple-json` as text. Converted here rather\n * than handed to the client as-is.\n */\n requires: row.requires ? String(row.requires).split(',').filter(Boolean) : undefined,\n target: row.target ? JSON.parse(String(row.target)) : undefined,\n controlled: Boolean(row.controlled)\n })) as StationScreenCard[]\n }\n }\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { EntitySubscriberInterface, InsertEvent, UpdateEvent, EntityManager } from 'typeorm';
2
+ import { Board } from '@things-factory/board-service';
3
+ import { type ReleaseRefusal } from './station-screen-release-gate';
4
+ /** Enforce at persistence, including the common Board release API. Drafts remain editable. */
5
+ export declare class StationScreenPublishSubscriber implements EntitySubscriberInterface<Board> {
6
+ listenTo(): typeof Board;
7
+ beforeInsert(event: InsertEvent<Board>): Promise<void>;
8
+ beforeUpdate(event: UpdateEvent<Board>): Promise<void>;
9
+ }
10
+ /**
11
+ * 통제 문서로 켠 화면은 **결재받은 그 도안일 때만** 나간다.
12
+ *
13
+ * ── 왜 여기인가 ────────────────────────────────────────────────────────────
14
+ * 위의 흐름 검사와 같은 이유다. board 는 우리 spec 을 모르고, `releaseBoard` 는 board-service 의
15
+ * 공용 문이다. 저작면에서만 막으면 그 문으로 그냥 나간다.
16
+ *
17
+ * ── 왜 boardId 하나로 찾나 ─────────────────────────────────────────────────
18
+ * 서명 줄의 색인은 `(domain, boardId)` 지만 `boardId` 는 uuid 라 그 자체로 한 화면을 가리킨다.
19
+ * 구독자가 받는 이벤트에 도메인이 실려 있다는 보장은 없어서, 있을지 없을지 모르는 것으로 범위를
20
+ * 좁히는 대신 확실한 것 하나로 찾는다. 발행 한 번에 한 번 도는 질의다.
21
+ */
22
+ export declare function whyNotReleaseBoard(manager: EntityManager, boardId: string | undefined, model: unknown): Promise<ReleaseRefusal | undefined>;