@things-factory/ops-master 10.0.18 → 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 (269) 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 -169
  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 +12 -2
  111. package/dist-server/service/index.js +21 -1
  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.d.ts +1 -1
  123. package/dist-server/service/master-axis.js +16 -1
  124. package/dist-server/service/master-axis.js.map +1 -1
  125. package/dist-server/service/master-conformance-resolver.d.ts +34 -0
  126. package/dist-server/service/master-conformance-resolver.js +143 -0
  127. package/dist-server/service/master-conformance-resolver.js.map +1 -0
  128. package/dist-server/service/master-conformance.d.ts +80 -0
  129. package/dist-server/service/master-conformance.js +266 -0
  130. package/dist-server/service/master-conformance.js.map +1 -0
  131. package/dist-server/service/master-page-query.d.ts +33 -0
  132. package/dist-server/service/master-page-query.js +55 -0
  133. package/dist-server/service/master-page-query.js.map +1 -0
  134. package/dist-server/service/master-page.d.ts +85 -0
  135. package/dist-server/service/master-page.js +104 -0
  136. package/dist-server/service/master-page.js.map +1 -0
  137. package/dist-server/service/material/material-class.js +1 -1
  138. package/dist-server/service/material/material-class.js.map +1 -1
  139. package/dist-server/service/material/material-definition.js +2 -2
  140. package/dist-server/service/material/material-definition.js.map +1 -1
  141. package/dist-server/service/material/material-master-resolver.d.ts +62 -0
  142. package/dist-server/service/material/material-master-resolver.js +213 -0
  143. package/dist-server/service/material/material-master-resolver.js.map +1 -1
  144. package/dist-server/service/material/material-query.d.ts +9 -1
  145. package/dist-server/service/material/material-query.js +28 -6
  146. package/dist-server/service/material/material-query.js.map +1 -1
  147. package/dist-server/service/material/recipe-index.d.ts +23 -0
  148. package/dist-server/service/material/recipe-index.js +44 -0
  149. package/dist-server/service/material/recipe-index.js.map +1 -0
  150. package/dist-server/service/operation/index.d.ts +2 -1
  151. package/dist-server/service/operation/index.js +2 -1
  152. package/dist-server/service/operation/index.js.map +1 -1
  153. package/dist-server/service/operation/operation-definition.d.ts +1 -1
  154. package/dist-server/service/operation/operation-definition.js +4 -4
  155. package/dist-server/service/operation/operation-definition.js.map +1 -1
  156. package/dist-server/service/operation/operation-master-resolver.d.ts +24 -0
  157. package/dist-server/service/operation/operation-master-resolver.js +107 -0
  158. package/dist-server/service/operation/operation-master-resolver.js.map +1 -1
  159. package/dist-server/service/operation/operation-query.d.ts +10 -2
  160. package/dist-server/service/operation/operation-query.js +25 -10
  161. package/dist-server/service/operation/operation-query.js.map +1 -1
  162. package/dist-server/service/operation/operation-readiness-resolver.d.ts +39 -0
  163. package/dist-server/service/operation/operation-readiness-resolver.js +111 -0
  164. package/dist-server/service/operation/operation-readiness-resolver.js.map +1 -0
  165. package/dist-server/service/operation/operation-readiness.d.ts +34 -0
  166. package/dist-server/service/operation/operation-readiness.js +133 -0
  167. package/dist-server/service/operation/operation-readiness.js.map +1 -0
  168. package/dist-server/service/personnel/person.js +3 -3
  169. package/dist-server/service/personnel/person.js.map +1 -1
  170. package/dist-server/service/personnel/personnel-class-readiness.d.ts +22 -0
  171. package/dist-server/service/personnel/personnel-class-readiness.js +98 -0
  172. package/dist-server/service/personnel/personnel-class-readiness.js.map +1 -0
  173. package/dist-server/service/personnel/personnel-class.d.ts +1 -1
  174. package/dist-server/service/personnel/personnel-class.js +2 -2
  175. package/dist-server/service/personnel/personnel-class.js.map +1 -1
  176. package/dist-server/service/personnel/personnel-master-resolver.d.ts +9 -0
  177. package/dist-server/service/personnel/personnel-master-resolver.js +43 -0
  178. package/dist-server/service/personnel/personnel-master-resolver.js.map +1 -1
  179. package/dist-server/service/personnel/personnel-master.js +1 -1
  180. package/dist-server/service/personnel/personnel-master.js.map +1 -1
  181. package/dist-server/service/personnel/personnel-query.d.ts +9 -1
  182. package/dist-server/service/personnel/personnel-query.js +28 -6
  183. package/dist-server/service/personnel/personnel-query.js.map +1 -1
  184. package/dist-server/service/personnel/qualification.d.ts +7 -0
  185. package/dist-server/service/personnel/qualification.js +25 -0
  186. package/dist-server/service/personnel/qualification.js.map +1 -1
  187. package/dist-server/service/recipe/index.d.ts +9 -0
  188. package/dist-server/service/recipe/index.js +14 -0
  189. package/dist-server/service/recipe/index.js.map +1 -0
  190. package/dist-server/service/recipe/recipe-definition.d.ts +71 -0
  191. package/dist-server/service/recipe/recipe-definition.js +130 -0
  192. package/dist-server/service/recipe/recipe-definition.js.map +1 -0
  193. package/dist-server/service/recipe/recipe-master-resolver.d.ts +28 -0
  194. package/dist-server/service/recipe/recipe-master-resolver.js +124 -0
  195. package/dist-server/service/recipe/recipe-master-resolver.js.map +1 -0
  196. package/dist-server/service/recipe/recipe-master.d.ts +37 -0
  197. package/dist-server/service/recipe/recipe-master.js +47 -0
  198. package/dist-server/service/recipe/recipe-master.js.map +1 -0
  199. package/dist-server/service/recipe/recipe-query.d.ts +21 -0
  200. package/dist-server/service/recipe/recipe-query.js +75 -0
  201. package/dist-server/service/recipe/recipe-query.js.map +1 -0
  202. package/dist-server/service/recipe/route-definition.d.ts +51 -0
  203. package/dist-server/service/recipe/route-definition.js +113 -0
  204. package/dist-server/service/recipe/route-definition.js.map +1 -0
  205. package/dist-server/service/resource/class-lineage.d.ts +46 -0
  206. package/dist-server/service/resource/class-lineage.js +80 -0
  207. package/dist-server/service/resource/class-lineage.js.map +1 -0
  208. package/dist-server/service/resource/class-members.d.ts +36 -0
  209. package/dist-server/service/resource/class-members.js +50 -0
  210. package/dist-server/service/resource/class-members.js.map +1 -0
  211. package/dist-server/service/resource/referrers.d.ts +54 -0
  212. package/dist-server/service/resource/referrers.js +99 -0
  213. package/dist-server/service/resource/referrers.js.map +1 -0
  214. package/dist-server/service/status/master-status.d.ts +0 -7
  215. package/dist-server/service/status/master-status.js +5 -6
  216. package/dist-server/service/status/master-status.js.map +1 -1
  217. package/dist-server/service/test-spec/criteria-check.d.ts +53 -0
  218. package/dist-server/service/test-spec/criteria-check.js +142 -0
  219. package/dist-server/service/test-spec/criteria-check.js.map +1 -0
  220. package/dist-server/service/test-spec/criteria-rows.d.ts +89 -0
  221. package/dist-server/service/test-spec/criteria-rows.js +139 -0
  222. package/dist-server/service/test-spec/criteria-rows.js.map +1 -0
  223. package/dist-server/service/test-spec/test-spec-master-resolver.d.ts +33 -7
  224. package/dist-server/service/test-spec/test-spec-master-resolver.js +137 -2
  225. package/dist-server/service/test-spec/test-spec-master-resolver.js.map +1 -1
  226. package/dist-server/service/test-spec/test-spec-query.d.ts +9 -1
  227. package/dist-server/service/test-spec/test-spec-query.js +23 -9
  228. package/dist-server/service/test-spec/test-spec-query.js.map +1 -1
  229. package/dist-server/service/test-spec/test-specification.js +1 -1
  230. package/dist-server/service/test-spec/test-specification.js.map +1 -1
  231. package/dist-server/tsconfig.tsbuildinfo +1 -1
  232. package/helps/ops-master/equipment-class.ko.md +11 -0
  233. package/helps/ops-master/equipment-class.md +11 -0
  234. package/helps/ops-master/equipment.ko.md +11 -0
  235. package/helps/ops-master/equipment.md +11 -0
  236. package/helps/ops-master/material-class.ko.md +11 -0
  237. package/helps/ops-master/material-class.md +11 -0
  238. package/helps/ops-master/material-definition.ko.md +11 -0
  239. package/helps/ops-master/material-definition.md +11 -0
  240. package/helps/ops-master/operation-definition.ko.md +11 -0
  241. package/helps/ops-master/operation-definition.md +11 -0
  242. package/helps/ops-master/operational-location.ko.md +11 -0
  243. package/helps/ops-master/operational-location.md +11 -0
  244. package/helps/ops-master/person.ko.md +11 -0
  245. package/helps/ops-master/person.md +11 -0
  246. package/helps/ops-master/personnel-class.ko.md +11 -0
  247. package/helps/ops-master/personnel-class.md +11 -0
  248. package/helps/ops-master/physical-asset-class.ko.md +11 -0
  249. package/helps/ops-master/physical-asset-class.md +11 -0
  250. package/helps/ops-master/physical-asset.ko.md +11 -0
  251. package/helps/ops-master/physical-asset.md +11 -0
  252. package/helps/ops-master/recipe-definition.ko.md +11 -0
  253. package/helps/ops-master/recipe-definition.md +11 -0
  254. package/helps/ops-master/route-definition.ko.md +11 -0
  255. package/helps/ops-master/route-definition.md +11 -0
  256. package/helps/ops-master/stop-reason.ko.md +11 -0
  257. package/helps/ops-master/stop-reason.md +11 -0
  258. package/helps/ops-master/test-specification.ko.md +11 -0
  259. package/helps/ops-master/test-specification.md +11 -0
  260. package/package.json +5 -5
  261. package/things-factory.config.js +22 -2
  262. package/translations/en.json +89 -6
  263. package/translations/ja.json +96 -1
  264. package/translations/ko.json +96 -13
  265. package/translations/ms.json +96 -1
  266. package/translations/zh.json +96 -1
  267. package/dist-client/pages/master-list.d.ts +0 -68
  268. package/dist-client/pages/master-list.js +0 -267
  269. package/dist-client/pages/master-list.js.map +0 -1
@@ -0,0 +1,419 @@
1
+ import { __decorate } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/app/selector/ox-selector-resource-object.js';
4
+ import '@operato/input/ox-input-search.js';
5
+ import { html } from 'lit';
6
+ import { customElement } from 'lit/decorators.js';
7
+ import { createRef, ref } from 'lit/directives/ref.js';
8
+ import { OxGristEditor } from '@operato/data-grist';
9
+ import { i18next } from '@operato/i18n';
10
+ import { openPopup } from '@operato/layout';
11
+ import { referenceHref } from './reference-targets.js';
12
+ /**
13
+ * 팝업 목록의 기본 열.
14
+ *
15
+ * **`filter` 를 반드시 선언한다.** 셀렉터는 열에 필터가 하나도 없으면 필터 영역을 숨긴다
16
+ * (`hasFilterColumns` 가 거짓이면 `hideHeadroom`). 처음에 이것을 빠뜨려 영역이 사라졌고, 그래서
17
+ * 맨 검색 입력을 목록 위에 따로 얹었다 — 프레임워크가 이미 갖고 있는 자리를 두고 딴 것을 만든
18
+ * 꼴이었다. [확인함] 2026-09-04
19
+ */
20
+ const DEFAULT_COLUMNS = [
21
+ { name: 'name', width: 220, filter: 'search', sortable: true },
22
+ { name: 'description', width: 260, filter: 'search', sortable: true }
23
+ ];
24
+ /**
25
+ * 참조 칸을 그린다 — **걸어갈 수 있으면 문으로.**
26
+ *
27
+ * 참조가 「ECU-SUB-01」이라고 적어도 거기로 갈 길이 없었다. 그 자리가 무엇인지 보려면 자리 화면을
28
+ * 따로 열고 이름으로 찾아야 했다. 마스터는 그래프인데 화면은 표 열넷으로 흩어져 있어, 축에서 축으로
29
+ * 건너가는 일이 사람의 기억에 맡겨져 있었다.
30
+ *
31
+ * **갈 곳을 모르면 문으로 안 그린다.** 선택했는데 아무 일도 안 나면 사람은 그것을 고장으로 읽는다.
32
+ * 그때는 글자 그대로 둔다.
33
+ *
34
+ * 진짜 `a` 로 그린다. 가운데 버튼으로 새 탭에 열고, 오른쪽 버튼으로 주소를 복사하는 것이 그냥 된다.
35
+ * 셸이 앵커를 물어 화면 안에서 옮긴다.
36
+ */
37
+ /**
38
+ * 목록 칸 — **값 사이가 보여야 한다.**
39
+ *
40
+ * `list` 를 글자 칸으로 떨어뜨렸더니 Lit 이 배열을 이어 붙여 그렸다. 두 사람이 서 있는 자리가
41
+ * 「P-ECU-S-0001P-ECU-S-0002」로 보였다 — 한 사람 이름이 이상하게 긴 것처럼 읽힌다.
42
+ * [확인함] 2026-09-04
43
+ *
44
+ * 쉼표가 아니라 가운뎃점을 쓴다. 이름 안에 쉼표가 든 값이 있을 수 있고(품명·설명), 그때 어디가
45
+ * 경계인지 알 수 없다.
46
+ */
47
+ export const OxGristRendererList = (value) => {
48
+ const items = Array.isArray(value) ? value : value == null || value === '' ? [] : [value];
49
+ return html `<span>${items.join(' · ')}</span>`;
50
+ };
51
+ export const OxGristRendererReference = (value, column) => {
52
+ const text = value ?? '';
53
+ if (!text) {
54
+ return html `<span></span>`;
55
+ }
56
+ const href = referenceHref(column?.record?.reference?.queryName, text, location.pathname);
57
+ if (!href) {
58
+ return html `<span>${text}</span>`;
59
+ }
60
+ return html `<a href=${href} style="text-decoration:none;color:inherit">${text}</a>`;
61
+ };
62
+ let OpsMasterGristEditorReference = class OpsMasterGristEditorReference extends OxGristEditor {
63
+ /* 팝업으로 고친다 — 칸에 표식이 붙는다(OxGristEditor.opensPopup). */
64
+ static { this.opensPopup = true; }
65
+ get editorTemplate() {
66
+ /* 고른 이름을 그대로 보인다. 팝업이 열리기 전에도 지금 값이 무엇인지 보여야 한다. */
67
+ return html `<span tabindex="0" style="flex:1">${this.value ?? ''}</span>`;
68
+ }
69
+ _onclick(e) {
70
+ e.stopPropagation();
71
+ }
72
+ _ondblclick(e) {
73
+ e.stopPropagation();
74
+ this.openSelector();
75
+ }
76
+ _onkeydown(e) {
77
+ if (e.key === 'Enter') {
78
+ e.stopPropagation();
79
+ this.openSelector();
80
+ return;
81
+ }
82
+ /*
83
+ * **지우기는 팝업 없이 된다.** 참조를 비우려고 팝업을 열어 「아무것도 아님」을 고르게 하면
84
+ * 지우는 일이 고르는 일보다 어려워진다.
85
+ */
86
+ if (e.key === 'Delete' || e.key === 'Backspace') {
87
+ e.stopPropagation();
88
+ this.commit(null);
89
+ }
90
+ }
91
+ /**
92
+ * 고른 값을 그리드에 알린다.
93
+ *
94
+ * 부모가 듣는 이름이 `field-change` 다. 이 이름이 아니면 팝업에서 고른 것이 **오류 보고 없이**
95
+ * 사라진다 — 화면에는 잠깐 보였다가 다음 그리기에서 옛 값으로 돌아간다.
96
+ */
97
+ commit(after) {
98
+ const { name = '' } = this.column;
99
+ this.value = (Array.isArray(after) ? after.join(', ') : after) ?? '';
100
+ this._dirtyValue = after;
101
+ this.dispatchEvent(new CustomEvent('field-change', {
102
+ bubbles: true,
103
+ composed: true,
104
+ detail: {
105
+ before: this.record[name],
106
+ after,
107
+ column: this.column,
108
+ record: this.record,
109
+ row: this.row
110
+ }
111
+ }));
112
+ }
113
+ openSelector() {
114
+ const reference = this.column.record?.reference;
115
+ /*
116
+ * 열 선언에 reference 가 없으면 **열지 않고 알린다.** 오류보고없이 넘어가면 두 번 선택해도 아무 일도
117
+ * 안 나는 칸이 되고, 그것은 「고를 것이 없다」처럼 보인다.
118
+ */
119
+ if (!reference?.queryName) {
120
+ console.warn(`참조 칸 ${this.column.name} 에 reference.queryName 이 없습니다 — 고를 표를 모릅니다`);
121
+ return;
122
+ }
123
+ const { queryName, columns = DEFAULT_COLUMNS, valueField = 'name', multiple = false } = reference;
124
+ const title = reference.title || i18next.t('title.select_item');
125
+ if (this.popup) {
126
+ delete this.popup;
127
+ }
128
+ /*
129
+ * 지금 값. 그리드의 칸은 배열을 들고 있을 수도 있고, 편집기가 글자로 이어 붙인 뒤일 수도
130
+ * 있다(commit 이 join 한다). 둘 다 배열로 펴서 넘긴다 — 선택기가 그 값들을 미리 골라 둔다.
131
+ */
132
+ const raw = this.record?.[this.column.name || ''] ?? this.value;
133
+ const currentValues = Array.isArray(raw)
134
+ ? raw.filter(Boolean)
135
+ : String(raw ?? '')
136
+ .split(',')
137
+ .map(one => one.trim())
138
+ .filter(Boolean);
139
+ const confirmCallback = (selected) => {
140
+ /*
141
+ * 아무것도 안 고르고 확정하면 **지운 것으로 본다.** 앞의 값을 남기면 「지우려고 했는데
142
+ * 안 지워졌다」가 되고, 그것을 되돌릴 방법이 화면에 없다.
143
+ *
144
+ * 여럿 고르기에서는 배열이 온다. 빈 배열도 「지웠다」이다 — 선택기가 아무것도 안 고른
145
+ * 확정은 애초에 막으므로(확인이 꺼진다), 여기 오는 빈 배열은 「지정 안 함」뿐이다.
146
+ */
147
+ if (multiple) {
148
+ const picked = (Array.isArray(selected) ? selected : selected ? [selected] : [])
149
+ .map(one => one?.[valueField])
150
+ .filter(Boolean);
151
+ this.commit(picked.length ? picked : null);
152
+ return;
153
+ }
154
+ const one = Array.isArray(selected) ? selected[0] : selected;
155
+ this.commit(one ? (one[valueField] ?? null) : null);
156
+ };
157
+ /*
158
+ * ── 오버레이가 무엇을 해 주고 무엇을 안 해 주나 ─────────────────────────
159
+ *
160
+ * 오버레이의 규칙 두 줄이 이 템플릿의 모양을 정한다.
161
+ *
162
+ * [hovering=center] [content] ::slotted(*) width 100% · height 100%
163
+ * [content] flex 1 · overflow hidden
164
+ *
165
+ * 첫 줄은 **직접 슬롯된 자식에만** 걸린다. 그래서 감싸는 순간 안쪽 셀렉터는 그 혜택을 잃고
166
+ * 자기 host 규칙(폭 50%)으로 돌아간다 — 실제로 목록이 팝업 절반만 차지하고 오른쪽이 비었다.
167
+ * 아래에서 폭을 100% 로 못 박는 이유다.
168
+ *
169
+ * 둘째 줄은 내용 영역이 **스스로 안 구른다**는 뜻이다. 넘치는 것은 자식이 감당해야 하므로
170
+ * 목록에 min-height 0 을 준다.
171
+ *
172
+ * [확인함] 2026-09-04 — ox-floating-overlay.js 의 스타일 블록
173
+ */
174
+ /*
175
+ * 걸러 볼 것이 있나 — **찾기가 아닌 필터**를 하나라도 선언한 열이 있나.
176
+ *
177
+ * 기본 열의 `search` 필터는 위의 찾기가 맡으므로 세지 않는다.
178
+ */
179
+ const filterable = columns.some((column) => column.filter && column.filter !== 'search' && column.filter?.operator !== 'search');
180
+ const selector = createRef();
181
+ /*
182
+ * 고치는 대상. 행의 이름 칸을 그대로 쓴다 — 사람이 목록에서 알아보는 값이 그것이다.
183
+ * 이름이 없는 행(방금 더한 줄)이면 안 보인다. 없는 것을 지어내지 않는다.
184
+ */
185
+ const subject = this.record?.name || undefined;
186
+ /*
187
+ * **지금 들어 있는 값.** 팝업이 이것을 말하지 않으면, 바꾸러 온 사람이 무엇을 바꾸는지 모른다.
188
+ *
189
+ * ── 왜 필요한가 (2026-09-05) ────────────────────────────────────────────
190
+ * 선택기는 값을 **받아온 쪽에서 찾으면** 고른 것으로 표시한다. 그래서 목록이 한 쪽에 다 들어가는
191
+ * 표에서는 잘 보였는데, 여러 쪽이면 그 값이 2쪽에 있을 때 아무것도 안 골라진 채로 열렸다.
192
+ *
193
+ * [확인함] 제조법 · 만드는 품목 · value=MT10097283 · 총 106건 · 1쪽
194
+ * selectedRecords = [] · 1쪽에 그 값 없음
195
+ *
196
+ * 목록만 보고는 값이 안 들어 있는 것과 구별할 수 없다. 그리고 106건을 넘겨 가며 찾을 일도
197
+ * 아니다 — **글자로 적고, 누르면 그 값으로 찾게 한다.**
198
+ */
199
+ /*
200
+ * 여럿을 고르는 칸이면 값이 여러 개다. 그때는 **찾기 버튼으로 두지 않는다** — 「A, B, C」로
201
+ * 찾으면 걸리는 것이 없고, 선택해도 아무 일이 없는 버튼이 된다. 글자로만 적는다.
202
+ *
203
+ * [확인함] 2026-09-05 · 여럿 고르기에서 기존 값으로 검색이 안 된다(사용자)
204
+ */
205
+ const manyValues = !!this.column?.record?.reference?.multiple;
206
+ const current = this.value || undefined;
207
+ const template = html `
208
+ <style>
209
+ [picker] {
210
+ display: flex;
211
+ flex-direction: column;
212
+ height: 100%;
213
+ box-sizing: border-box;
214
+
215
+ /*
216
+ * **면색을 우리가 정한다.** 오버레이는 뒷막만 칠하고 면색은 슬롯된 자식이 갖는다 — 셀렉터의
217
+ * host 규칙이 그것을 갖고 있었다. 감싸는 순간 그 규칙이 우리 상자에 안 걸려서 팝업에
218
+ * 면이 사라졌고, 제목과 검색창과 바닥 버튼이 뒤에 있는 표 위에 그냥 떠 있었다. 목록만
219
+ * 흰 것은 그 안의 표가 자기 색을 칠하기 때문이다.
220
+ *
221
+ * 투명은 「안 칠함」이 아니라 「바탕에 구멍」이다. [확인함] 2026-09-04 · UX 레인 확인
222
+ */
223
+ background-color: var(--md-sys-color-surface);
224
+ }
225
+ /*
226
+ ── 대상과 검색이 한 줄에 선다 ──────────────────────────────────────
227
+ 페이지의 표준이 그렇다 — 제목은 왼쪽, 검색은 그 줄의 오른쪽 끝(174×23). 그 아래가 필터
228
+ 자리이고 그 아래가 표다. [확인함] 2026-09-04 · ox-page-title-bar
229
+
230
+ 한때 검색이 목록 위 한 줄을 통째로 차지했다(781×31 — 팝업 폭의 98%). 그 자리는 **필터가
231
+ 오는 자리**이고, 검색이 그것을 밀어내고 있었다.
232
+
233
+ 머리 제목은 오버레이가 갖는다. 팝업이니까 그 자리가 맞다. 이 줄은 「누구에게 넣나」와
234
+ 「찾기」를 넣는다.
235
+ */
236
+ [picker] > [subject] {
237
+ flex: none;
238
+ display: flex;
239
+ align-items: center;
240
+ gap: var(--spacing-medium, 8px);
241
+ margin: var(--spacing-medium, 8px) var(--spacing-medium, 8px) var(--spacing-small, 4px);
242
+ }
243
+ /*
244
+ 무엇을 고르는 일인지는 머리 제목이 갖는다. 이 줄은 **누구에게 넣나**를 갖는다 — 라벨을 붙여야
245
+ 식별자 하나가 덜렁 놓이지 않는다. 휴대폰에서는 그 줄이 화면에 없으므로 이 줄이 유일한
246
+ 단서다.
247
+ */
248
+ [picker] > [subject] > [now] > [label] {
249
+ font-family: var(--theme-font-mono, monospace);
250
+ font-size: 9.5px;
251
+ letter-spacing: 0.1em;
252
+ text-transform: uppercase;
253
+ color: var(--md-sys-color-outline);
254
+ margin-right: var(--spacing-tiny, 2px);
255
+ }
256
+ [picker] > [subject] > [label] {
257
+ font-family: var(--theme-font-mono, monospace);
258
+ font-size: 9.5px;
259
+ letter-spacing: 0.1em;
260
+ text-transform: uppercase;
261
+ color: var(--md-sys-color-outline);
262
+ }
263
+ [picker] > [subject] > [who] {
264
+ font-size: 0.9em;
265
+ font-weight: 600;
266
+ color: var(--md-sys-color-on-surface);
267
+ }
268
+ /*
269
+ 지금 값 — **대상보다 한 단 약하다.** 대상은 「무엇을 고치나」이고 이것은 「지금 뭐가
270
+ 들어 있나」다. 같은 무게로 두면 둘 중 어느 것이 이 팝업의 주어인지 흐려진다.
271
+
272
+ 누르면 그 값으로 찾는다. 106건을 넘겨 가며 찾게 두지 않는다.
273
+ */
274
+ /*
275
+ ── 이 값이 **지금 값**이라는 것을 글자로 알린다 ─────────────────────
276
+ 처음에는 돋보기 아이콘과 값만 두었다. 그러면 「찾기」로만 읽히고, 그 글자가 지금 이 칸에
277
+ 들어 있는 값이라는 뜻이 전달되지 않았다.
278
+
279
+ 대상 줄과 같은 방식으로 이름을 앞에 붙인다 — 그 줄이 이미 「대상 · 값」의 짝이라, 같은
280
+ 모양의 「기존 · 값」이 나란히 서면 둘의 관계가 형태로 읽힌다. 돋보기는 값 뒤로 물러나
281
+ 누를 수 있다는 것만 말한다.
282
+
283
+ 「지금」이 아니라 **「기존」**이다. 이 팝업은 값을 바꾸러 온 자리이므로, 읽는 사람에게
284
+ 그 값은 「바꾸기 전의 것」이다. 「지금」은 그 관계를 말하지 않는다. 그리고 「대상」과
285
+ 같은 두 글자라 짝이 맞는다.
286
+ */
287
+ [picker] > [subject] > [now] {
288
+ display: flex;
289
+ align-items: center;
290
+ gap: var(--spacing-tiny, 2px);
291
+
292
+ border: none;
293
+ border-radius: var(--md-sys-shape-corner-small, 5px);
294
+ padding: 1px var(--spacing-small, 4px);
295
+ background-color: var(--md-sys-color-surface-container-high);
296
+
297
+ color: var(--md-sys-color-on-surface-variant);
298
+ font-family: var(--theme-font-mono, monospace);
299
+ font-size: 0.82em;
300
+ cursor: pointer;
301
+ }
302
+ /* 여럿일 때는 선택할 수 없다. 찾을 수 없는 값이라 버튼으로 두지 않는다. */
303
+ [picker] > [subject] > [now][static] {
304
+ cursor: default;
305
+ }
306
+ [picker] > [subject] > [now]:not([static]):hover {
307
+ color: var(--md-sys-color-on-surface);
308
+ }
309
+ [picker] > [subject] > [now] md-icon {
310
+ --md-icon-size: 13px;
311
+ font-size: 13px;
312
+ }
313
+ /*
314
+ 찾기. 페이지의 검색 상자와 같은 크기다(174×23) — 남는 폭을 다 먹지 않는다.
315
+
316
+ 모양은 지어내지 않는다. 오버레이가 자기 머리에서 같은 컨트롤을 다루는 값 그대로 쓴다
317
+ (ox-floating-overlay 의 [search] 규칙) — 알약 모양에 안쪽 입력의 자기 칠을 지우는 변수
318
+ 넷. 맨 요소에 여백만 주면 host 와 input 이 각각 칠해 상자 안에 상자가 생긴다.
319
+ */
320
+ [picker] > [subject] > ox-input-search {
321
+ margin-left: auto;
322
+ width: 174px;
323
+ flex: none;
324
+
325
+ --input-search-padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
326
+ --input-search-focus-border-bottom: none;
327
+ --input-search-font: normal 13px var(--theme-font);
328
+ --md-icon-size: 18px;
329
+
330
+ --input-field-background: transparent;
331
+ --ox-input-search-background-color: var(--md-sys-color-surface-container-high);
332
+ --ox-input-search-color: var(--md-sys-color-on-surface);
333
+ --ox-input-search-icon-color: var(--md-sys-color-on-surface-variant);
334
+
335
+ border-radius: 999em;
336
+ padding: 0 var(--spacing-small, 4px);
337
+ box-sizing: border-box;
338
+ }
339
+ [picker] > ox-selector-resource-object {
340
+ flex: 1;
341
+ min-height: 0;
342
+ width: 100%;
343
+ }
344
+ </style>
345
+ <div picker>
346
+ <!--
347
+ 한 줄에 둘이 선다 — 누구에게 넣나(왼쪽)와 찾기(오른쪽 끝). 페이지의 제목줄과 같은 배치다.
348
+
349
+ 안내문은 **입력 방법**을 말한다. 「무엇을 고르는 일인가」는 머리 제목이 계속 들고 있으므로,
350
+ 여기서 같은 말을 두 번 하면 글자 둘이 서로 다른 말을 하는 꼴이 된다. 방법을 말하면 글자를
351
+ 치는 순간 사라져도 잃는 것이 없다.
352
+ -->
353
+ <div subject>
354
+ ${subject ? html `<span label>${i18next.t('label.target')}</span><span who>${subject}</span>` : ''}
355
+ ${current
356
+ ? manyValues
357
+ ? html `<span now static><span label>${i18next.t('label.existing')}</span>${current}</span>`
358
+ : html `<button
359
+ now
360
+ title=${i18next.t('text.find-current-value')}
361
+ @click=${(e) => {
362
+ const box = e.currentTarget
363
+ .parentElement?.querySelector('ox-input-search');
364
+ if (box)
365
+ box.value = current;
366
+ selector.value?.searchText(current);
367
+ }}
368
+ >
369
+ <span label>${i18next.t('label.existing')}</span>${current}<md-icon>search</md-icon>
370
+ </button>`
371
+ : ''}
372
+ <ox-input-search
373
+ .placeholder=${i18next.t('text.search-by-name-or-description')}
374
+ autofocus
375
+ @change=${(e) => selector.value?.searchText(e.target.value ?? '')}
376
+ ></ox-input-search>
377
+ </div>
378
+ <ox-selector-resource-object
379
+ ${ref(selector)}
380
+ .multiple=${multiple}
381
+ .value=${multiple ? currentValues : (this.value ?? '')}
382
+ .confirmCallback=${confirmCallback}
383
+ .queryName=${queryName}
384
+ .columns=${columns}
385
+ .valueField=${valueField}
386
+ ></ox-selector-resource-object>
387
+ </div>
388
+ `;
389
+ this.popup = openPopup(template, {
390
+ backdrop: true,
391
+ size: 'large',
392
+ /* 머리에 뜨는 글자. 검색을 내렸으므로 머리에는 이것과 닫기 · 걸러보기만 남는다. */
393
+ title,
394
+ /*
395
+ * **search 를 안 넘긴다.** 넘기면 오버레이가 머리에 검색창을 하나 더 그려서 두 개가 된다.
396
+ * 찾기는 목록 위 대상 줄의 오른쪽에 있다.
397
+ *
398
+ * ── 걸러보기 버튼은 **걸러 볼 것이 있을 때만** ────────────────────────
399
+ * 이 팝업의 기본 열(이름 · 설명)은 필터가 `search` 형이고, 그것은 위의 찾기가 이미 맡는다.
400
+ * 그 밖에 아무것도 없으면 이 버튼은 **열 것이 없다.** 선택해도 빈 띠가 열린다.
401
+ *
402
+ * 오버레이는 손잡이에 `this.firstElementChild` 를 넘기는데, 감싼 뒤로 그것은 셀렉터가 아니라
403
+ * 위의 div 다. 그래서 넘어오는 것을 안 쓰고 우리 ref 로 셀렉터를 직접 잡는다.
404
+ */
405
+ ...(filterable
406
+ ? {
407
+ filter: {
408
+ handler: () => selector.value?.toggleFilter()
409
+ }
410
+ }
411
+ : {})
412
+ });
413
+ }
414
+ };
415
+ OpsMasterGristEditorReference = __decorate([
416
+ customElement('ops-master-grist-editor-reference')
417
+ ], OpsMasterGristEditorReference);
418
+ export { OpsMasterGristEditorReference };
419
+ //# sourceMappingURL=grist-reference.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grist-reference.js","sourceRoot":"","sources":["../../../client/pages/master/grist-reference.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/app/selector/ox-selector-resource-object.js'\nimport '@operato/input/ox-input-search.js'\n\nimport { html } from 'lit'\nimport { customElement } from 'lit/decorators.js'\nimport { createRef, ref } from 'lit/directives/ref.js'\n\nimport { OxGristEditor } from '@operato/data-grist'\nimport { i18next } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\n\nimport { referenceHref } from './reference-targets.js'\n\n/**\n * 다른 표의 **이름 하나**를 골라 오는 칸.\n *\n * ── 왜 있는 것을 그대로 못 썼나 ────────────────────────────────────────────\n * `@operato/app` 에 `resource-object` 편집기가 있고, 우리가 필요한 「다른 표에서 고르기」 팝업을\n * 이미 갖고 있다. 그런데 그것은 **객체를 저장한다** — 확정할 때 `{ id, name, description, … }` 을\n * 통째로 칸에 넣는다.\n *\n * 우리 칸은 이름 문자열이다. `homeLocation` 은 `'GUNSAN'` 이고 `route` 는 `'ROUTE-ECU-STD'` 다.\n * **커널이 이름으로 주소를 잡기 때문**이다(마스터가 트윈에 낼 때 이름을 id 로 낸다). 저 편집기를\n * 그대로 붙이면 문자열 칸에 객체가 들어가고, 그 뒤로 그 참조는 어디에서도 안 걸린다.\n *\n * 그래서 **고르는 팝업은 그대로 쓰고**(`ox-selector-resource-object`) 확정할 때 이름만 꺼낸다.\n * 팝업을 새로 만들면 검색·페이징·필터를 다시 짜야 하고, 그것은 이미 잘 동작하는 것을 한 번 더 만드는 일이다.\n */\n\n/** 열 선언이 주는 것 — 어느 표에서, 무엇을 보이고, 무엇을 저장하나. */\nexport interface ReferenceOptions {\n /** GraphQL 질의 이름. `operationalLocations` · `routeDefinitions` 처럼. */\n queryName: string\n /**\n * 여러 개를 고르나. 상위 등급 · 요구 검사 · 공정의 차례처럼 **여럿을 다는 칸**이 켠다.\n *\n * 켜면 고른 것들의 배열을 그대로 저장한다. 끄면 하나를 저장한다.\n */\n multiple?: boolean\n /**\n * 팝업 제목. 없으면 **그 칸의 머리말**을 쓴다.\n *\n * 비워 두면 안 된다. 팝업 머리에 글자가 하나도 없으면 회색 띠에 검색창만 떠 있는 꼴이 되고,\n * 무엇을 고르는 중인지 화면이 말하지 않는다. 실제로 그랬다 — 원본 편집기가 이 값을 검색창\n * 안내문으로만 넘기고 팝업 제목으로는 안 넘긴다.\n */\n title?: string\n /** 팝업의 목록에 보일 칸들. 없으면 이름과 설명. */\n columns?: { name: string; header?: string; width?: number }[]\n /**\n * 칸에 저장할 값을 어느 칸에서 꺼내나. 기본은 `name`.\n *\n * **`id` 가 아니다.** 커널이 이름으로 주소를 잡는다.\n */\n valueField?: string\n}\n\n/**\n * 팝업 목록의 기본 열.\n *\n * **`filter` 를 반드시 선언한다.** 셀렉터는 열에 필터가 하나도 없으면 필터 영역을 숨긴다\n * (`hasFilterColumns` 가 거짓이면 `hideHeadroom`). 처음에 이것을 빠뜨려 영역이 사라졌고, 그래서\n * 맨 검색 입력을 목록 위에 따로 얹었다 — 프레임워크가 이미 갖고 있는 자리를 두고 딴 것을 만든\n * 꼴이었다. [확인함] 2026-09-04\n */\nconst DEFAULT_COLUMNS = [\n { name: 'name', width: 220, filter: 'search', sortable: true },\n { name: 'description', width: 260, filter: 'search', sortable: true }\n]\n\n/**\n * 참조 칸을 그린다 — **걸어갈 수 있으면 문으로.**\n *\n * 참조가 「ECU-SUB-01」이라고 적어도 거기로 갈 길이 없었다. 그 자리가 무엇인지 보려면 자리 화면을\n * 따로 열고 이름으로 찾아야 했다. 마스터는 그래프인데 화면은 표 열넷으로 흩어져 있어, 축에서 축으로\n * 건너가는 일이 사람의 기억에 맡겨져 있었다.\n *\n * **갈 곳을 모르면 문으로 안 그린다.** 선택했는데 아무 일도 안 나면 사람은 그것을 고장으로 읽는다.\n * 그때는 글자 그대로 둔다.\n *\n * 진짜 `a` 로 그린다. 가운데 버튼으로 새 탭에 열고, 오른쪽 버튼으로 주소를 복사하는 것이 그냥 된다.\n * 셸이 앵커를 물어 화면 안에서 옮긴다.\n */\n/**\n * 목록 칸 — **값 사이가 보여야 한다.**\n *\n * `list` 를 글자 칸으로 떨어뜨렸더니 Lit 이 배열을 이어 붙여 그렸다. 두 사람이 서 있는 자리가\n * 「P-ECU-S-0001P-ECU-S-0002」로 보였다 — 한 사람 이름이 이상하게 긴 것처럼 읽힌다.\n * [확인함] 2026-09-04\n *\n * 쉼표가 아니라 가운뎃점을 쓴다. 이름 안에 쉼표가 든 값이 있을 수 있고(품명·설명), 그때 어디가\n * 경계인지 알 수 없다.\n */\nexport const OxGristRendererList = (value: unknown) => {\n const items = Array.isArray(value) ? value : value == null || value === '' ? [] : [value]\n\n return html`<span>${items.join(' · ')}</span>`\n}\n\nexport const OxGristRendererReference = (value: string | undefined, column?: any) => {\n const text = value ?? ''\n\n if (!text) {\n return html`<span></span>`\n }\n\n const href = referenceHref(column?.record?.reference?.queryName, text, location.pathname)\n\n if (!href) {\n return html`<span>${text}</span>`\n }\n\n return html`<a href=${href} style=\"text-decoration:none;color:inherit\">${text}</a>`\n}\n\n@customElement('ops-master-grist-editor-reference')\nexport class OpsMasterGristEditorReference extends OxGristEditor {\n /* 팝업으로 고친다 — 칸에 표식이 붙는다(OxGristEditor.opensPopup). */\n static opensPopup = true\n\n private popup?: { close: () => void }\n\n get editorTemplate() {\n /* 고른 이름을 그대로 보인다. 팝업이 열리기 전에도 지금 값이 무엇인지 보여야 한다. */\n return html`<span tabindex=\"0\" style=\"flex:1\">${this.value ?? ''}</span>`\n }\n\n _onclick(e: Event) {\n e.stopPropagation()\n }\n\n _ondblclick(e: Event) {\n e.stopPropagation()\n this.openSelector()\n }\n\n _onkeydown(e: KeyboardEvent) {\n if (e.key === 'Enter') {\n e.stopPropagation()\n this.openSelector()\n return\n }\n\n /*\n * **지우기는 팝업 없이 된다.** 참조를 비우려고 팝업을 열어 「아무것도 아님」을 고르게 하면\n * 지우는 일이 고르는 일보다 어려워진다.\n */\n if (e.key === 'Delete' || e.key === 'Backspace') {\n e.stopPropagation()\n this.commit(null)\n }\n }\n\n /**\n * 고른 값을 그리드에 알린다.\n *\n * 부모가 듣는 이름이 `field-change` 다. 이 이름이 아니면 팝업에서 고른 것이 **오류 보고 없이**\n * 사라진다 — 화면에는 잠깐 보였다가 다음 그리기에서 옛 값으로 돌아간다.\n */\n private commit(after: string | string[] | null) {\n const { name = '' } = this.column\n\n this.value = (Array.isArray(after) ? after.join(', ') : after) ?? ''\n this._dirtyValue = after\n\n this.dispatchEvent(\n new CustomEvent('field-change', {\n bubbles: true,\n composed: true,\n detail: {\n before: this.record[name],\n after,\n column: this.column,\n record: this.record,\n row: this.row\n }\n })\n )\n }\n\n private openSelector() {\n const reference = (this.column.record as any)?.reference as ReferenceOptions | undefined\n\n /*\n * 열 선언에 reference 가 없으면 **열지 않고 알린다.** 오류보고없이 넘어가면 두 번 선택해도 아무 일도\n * 안 나는 칸이 되고, 그것은 「고를 것이 없다」처럼 보인다.\n */\n if (!reference?.queryName) {\n console.warn(`참조 칸 ${this.column.name} 에 reference.queryName 이 없습니다 — 고를 표를 모릅니다`)\n return\n }\n\n const { queryName, columns = DEFAULT_COLUMNS, valueField = 'name', multiple = false } = reference\n const title = reference.title || i18next.t('title.select_item')\n\n if (this.popup) {\n delete this.popup\n }\n\n /*\n * 지금 값. 그리드의 칸은 배열을 들고 있을 수도 있고, 편집기가 글자로 이어 붙인 뒤일 수도\n * 있다(commit 이 join 한다). 둘 다 배열로 펴서 넘긴다 — 선택기가 그 값들을 미리 골라 둔다.\n */\n const raw = this.record?.[this.column.name || ''] ?? this.value\n const currentValues: string[] = Array.isArray(raw)\n ? raw.filter(Boolean)\n : String(raw ?? '')\n .split(',')\n .map(one => one.trim())\n .filter(Boolean)\n\n const confirmCallback = (selected?: Record<string, any> | Record<string, any>[]) => {\n /*\n * 아무것도 안 고르고 확정하면 **지운 것으로 본다.** 앞의 값을 남기면 「지우려고 했는데\n * 안 지워졌다」가 되고, 그것을 되돌릴 방법이 화면에 없다.\n *\n * 여럿 고르기에서는 배열이 온다. 빈 배열도 「지웠다」이다 — 선택기가 아무것도 안 고른\n * 확정은 애초에 막으므로(확인이 꺼진다), 여기 오는 빈 배열은 「지정 안 함」뿐이다.\n */\n if (multiple) {\n const picked = (Array.isArray(selected) ? selected : selected ? [selected] : [])\n .map(one => one?.[valueField])\n .filter(Boolean)\n\n this.commit(picked.length ? picked : null)\n return\n }\n\n const one = Array.isArray(selected) ? selected[0] : selected\n\n this.commit(one ? (one[valueField] ?? null) : null)\n }\n\n /*\n * ── 오버레이가 무엇을 해 주고 무엇을 안 해 주나 ─────────────────────────\n *\n * 오버레이의 규칙 두 줄이 이 템플릿의 모양을 정한다.\n *\n * [hovering=center] [content] ::slotted(*) width 100% · height 100%\n * [content] flex 1 · overflow hidden\n *\n * 첫 줄은 **직접 슬롯된 자식에만** 걸린다. 그래서 감싸는 순간 안쪽 셀렉터는 그 혜택을 잃고\n * 자기 host 규칙(폭 50%)으로 돌아간다 — 실제로 목록이 팝업 절반만 차지하고 오른쪽이 비었다.\n * 아래에서 폭을 100% 로 못 박는 이유다.\n *\n * 둘째 줄은 내용 영역이 **스스로 안 구른다**는 뜻이다. 넘치는 것은 자식이 감당해야 하므로\n * 목록에 min-height 0 을 준다.\n *\n * [확인함] 2026-09-04 — ox-floating-overlay.js 의 스타일 블록\n */\n /*\n * 걸러 볼 것이 있나 — **찾기가 아닌 필터**를 하나라도 선언한 열이 있나.\n *\n * 기본 열의 `search` 필터는 위의 찾기가 맡으므로 세지 않는다.\n */\n const filterable = columns.some(\n (column: any) => column.filter && column.filter !== 'search' && column.filter?.operator !== 'search'\n )\n\n const selector = createRef<any>()\n\n /*\n * 고치는 대상. 행의 이름 칸을 그대로 쓴다 — 사람이 목록에서 알아보는 값이 그것이다.\n * 이름이 없는 행(방금 더한 줄)이면 안 보인다. 없는 것을 지어내지 않는다.\n */\n const subject = this.record?.name || undefined\n\n /*\n * **지금 들어 있는 값.** 팝업이 이것을 말하지 않으면, 바꾸러 온 사람이 무엇을 바꾸는지 모른다.\n *\n * ── 왜 필요한가 (2026-09-05) ────────────────────────────────────────────\n * 선택기는 값을 **받아온 쪽에서 찾으면** 고른 것으로 표시한다. 그래서 목록이 한 쪽에 다 들어가는\n * 표에서는 잘 보였는데, 여러 쪽이면 그 값이 2쪽에 있을 때 아무것도 안 골라진 채로 열렸다.\n *\n * [확인함] 제조법 · 만드는 품목 · value=MT10097283 · 총 106건 · 1쪽\n * selectedRecords = [] · 1쪽에 그 값 없음\n *\n * 목록만 보고는 값이 안 들어 있는 것과 구별할 수 없다. 그리고 106건을 넘겨 가며 찾을 일도\n * 아니다 — **글자로 적고, 누르면 그 값으로 찾게 한다.**\n */\n /*\n * 여럿을 고르는 칸이면 값이 여러 개다. 그때는 **찾기 버튼으로 두지 않는다** — 「A, B, C」로\n * 찾으면 걸리는 것이 없고, 선택해도 아무 일이 없는 버튼이 된다. 글자로만 적는다.\n *\n * [확인함] 2026-09-05 · 여럿 고르기에서 기존 값으로 검색이 안 된다(사용자)\n */\n const manyValues = !!(this.column?.record as any)?.reference?.multiple\n const current = this.value || undefined\n\n const template = html`\n <style>\n [picker] {\n display: flex;\n flex-direction: column;\n height: 100%;\n box-sizing: border-box;\n\n /*\n * **면색을 우리가 정한다.** 오버레이는 뒷막만 칠하고 면색은 슬롯된 자식이 갖는다 — 셀렉터의\n * host 규칙이 그것을 갖고 있었다. 감싸는 순간 그 규칙이 우리 상자에 안 걸려서 팝업에\n * 면이 사라졌고, 제목과 검색창과 바닥 버튼이 뒤에 있는 표 위에 그냥 떠 있었다. 목록만\n * 흰 것은 그 안의 표가 자기 색을 칠하기 때문이다.\n *\n * 투명은 「안 칠함」이 아니라 「바탕에 구멍」이다. [확인함] 2026-09-04 · UX 레인 확인\n */\n background-color: var(--md-sys-color-surface);\n }\n /*\n ── 대상과 검색이 한 줄에 선다 ──────────────────────────────────────\n 페이지의 표준이 그렇다 — 제목은 왼쪽, 검색은 그 줄의 오른쪽 끝(174×23). 그 아래가 필터\n 자리이고 그 아래가 표다. [확인함] 2026-09-04 · ox-page-title-bar\n\n 한때 검색이 목록 위 한 줄을 통째로 차지했다(781×31 — 팝업 폭의 98%). 그 자리는 **필터가\n 오는 자리**이고, 검색이 그것을 밀어내고 있었다.\n\n 머리 제목은 오버레이가 갖는다. 팝업이니까 그 자리가 맞다. 이 줄은 「누구에게 넣나」와\n 「찾기」를 넣는다.\n */\n [picker] > [subject] {\n flex: none;\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n margin: var(--spacing-medium, 8px) var(--spacing-medium, 8px) var(--spacing-small, 4px);\n }\n /*\n 무엇을 고르는 일인지는 머리 제목이 갖는다. 이 줄은 **누구에게 넣나**를 갖는다 — 라벨을 붙여야\n 식별자 하나가 덜렁 놓이지 않는다. 휴대폰에서는 그 줄이 화면에 없으므로 이 줄이 유일한\n 단서다.\n */\n [picker] > [subject] > [now] > [label] {\n font-family: var(--theme-font-mono, monospace);\n font-size: 9.5px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--md-sys-color-outline);\n margin-right: var(--spacing-tiny, 2px);\n }\n [picker] > [subject] > [label] {\n font-family: var(--theme-font-mono, monospace);\n font-size: 9.5px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--md-sys-color-outline);\n }\n [picker] > [subject] > [who] {\n font-size: 0.9em;\n font-weight: 600;\n color: var(--md-sys-color-on-surface);\n }\n /*\n 지금 값 — **대상보다 한 단 약하다.** 대상은 「무엇을 고치나」이고 이것은 「지금 뭐가\n 들어 있나」다. 같은 무게로 두면 둘 중 어느 것이 이 팝업의 주어인지 흐려진다.\n\n 누르면 그 값으로 찾는다. 106건을 넘겨 가며 찾게 두지 않는다.\n */\n /*\n ── 이 값이 **지금 값**이라는 것을 글자로 알린다 ─────────────────────\n 처음에는 돋보기 아이콘과 값만 두었다. 그러면 「찾기」로만 읽히고, 그 글자가 지금 이 칸에\n 들어 있는 값이라는 뜻이 전달되지 않았다.\n\n 대상 줄과 같은 방식으로 이름을 앞에 붙인다 — 그 줄이 이미 「대상 · 값」의 짝이라, 같은\n 모양의 「기존 · 값」이 나란히 서면 둘의 관계가 형태로 읽힌다. 돋보기는 값 뒤로 물러나\n 누를 수 있다는 것만 말한다.\n\n 「지금」이 아니라 **「기존」**이다. 이 팝업은 값을 바꾸러 온 자리이므로, 읽는 사람에게\n 그 값은 「바꾸기 전의 것」이다. 「지금」은 그 관계를 말하지 않는다. 그리고 「대상」과\n 같은 두 글자라 짝이 맞는다.\n */\n [picker] > [subject] > [now] {\n display: flex;\n align-items: center;\n gap: var(--spacing-tiny, 2px);\n\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 5px);\n padding: 1px var(--spacing-small, 4px);\n background-color: var(--md-sys-color-surface-container-high);\n\n color: var(--md-sys-color-on-surface-variant);\n font-family: var(--theme-font-mono, monospace);\n font-size: 0.82em;\n cursor: pointer;\n }\n /* 여럿일 때는 선택할 수 없다. 찾을 수 없는 값이라 버튼으로 두지 않는다. */\n [picker] > [subject] > [now][static] {\n cursor: default;\n }\n [picker] > [subject] > [now]:not([static]):hover {\n color: var(--md-sys-color-on-surface);\n }\n [picker] > [subject] > [now] md-icon {\n --md-icon-size: 13px;\n font-size: 13px;\n }\n /*\n 찾기. 페이지의 검색 상자와 같은 크기다(174×23) — 남는 폭을 다 먹지 않는다.\n\n 모양은 지어내지 않는다. 오버레이가 자기 머리에서 같은 컨트롤을 다루는 값 그대로 쓴다\n (ox-floating-overlay 의 [search] 규칙) — 알약 모양에 안쪽 입력의 자기 칠을 지우는 변수\n 넷. 맨 요소에 여백만 주면 host 와 input 이 각각 칠해 상자 안에 상자가 생긴다.\n */\n [picker] > [subject] > ox-input-search {\n margin-left: auto;\n width: 174px;\n flex: none;\n\n --input-search-padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n --input-search-focus-border-bottom: none;\n --input-search-font: normal 13px var(--theme-font);\n --md-icon-size: 18px;\n\n --input-field-background: transparent;\n --ox-input-search-background-color: var(--md-sys-color-surface-container-high);\n --ox-input-search-color: var(--md-sys-color-on-surface);\n --ox-input-search-icon-color: var(--md-sys-color-on-surface-variant);\n\n border-radius: 999em;\n padding: 0 var(--spacing-small, 4px);\n box-sizing: border-box;\n }\n [picker] > ox-selector-resource-object {\n flex: 1;\n min-height: 0;\n width: 100%;\n }\n </style>\n <div picker>\n <!--\n 한 줄에 둘이 선다 — 누구에게 넣나(왼쪽)와 찾기(오른쪽 끝). 페이지의 제목줄과 같은 배치다.\n\n 안내문은 **입력 방법**을 말한다. 「무엇을 고르는 일인가」는 머리 제목이 계속 들고 있으므로,\n 여기서 같은 말을 두 번 하면 글자 둘이 서로 다른 말을 하는 꼴이 된다. 방법을 말하면 글자를\n 치는 순간 사라져도 잃는 것이 없다.\n -->\n <div subject>\n ${subject ? html`<span label>${i18next.t('label.target')}</span><span who>${subject}</span>` : ''}\n ${current\n ? manyValues\n ? html`<span now static><span label>${i18next.t('label.existing')}</span>${current}</span>`\n : html`<button\n now\n title=${i18next.t('text.find-current-value')}\n @click=${(e: Event) => {\n const box = (e.currentTarget as HTMLElement)\n .parentElement?.querySelector('ox-input-search') as any\n if (box) box.value = current\n selector.value?.searchText(current)\n }}\n >\n <span label>${i18next.t('label.existing')}</span>${current}<md-icon>search</md-icon>\n </button>`\n : ''}\n <ox-input-search\n .placeholder=${i18next.t('text.search-by-name-or-description')}\n autofocus\n @change=${(e: Event) => selector.value?.searchText((e.target as any).value ?? '')}\n ></ox-input-search>\n </div>\n <ox-selector-resource-object\n ${ref(selector)}\n .multiple=${multiple}\n .value=${multiple ? currentValues : (this.value ?? '')}\n .confirmCallback=${confirmCallback}\n .queryName=${queryName}\n .columns=${columns}\n .valueField=${valueField}\n ></ox-selector-resource-object>\n </div>\n `\n\n this.popup = openPopup(template, {\n backdrop: true,\n size: 'large',\n /* 머리에 뜨는 글자. 검색을 내렸으므로 머리에는 이것과 닫기 · 걸러보기만 남는다. */\n title,\n /*\n * **search 를 안 넘긴다.** 넘기면 오버레이가 머리에 검색창을 하나 더 그려서 두 개가 된다.\n * 찾기는 목록 위 대상 줄의 오른쪽에 있다.\n *\n * ── 걸러보기 버튼은 **걸러 볼 것이 있을 때만** ────────────────────────\n * 이 팝업의 기본 열(이름 · 설명)은 필터가 `search` 형이고, 그것은 위의 찾기가 이미 맡는다.\n * 그 밖에 아무것도 없으면 이 버튼은 **열 것이 없다.** 선택해도 빈 띠가 열린다.\n *\n * 오버레이는 손잡이에 `this.firstElementChild` 를 넘기는데, 감싼 뒤로 그것은 셀렉터가 아니라\n * 위의 div 다. 그래서 넘어오는 것을 안 쓰고 우리 ref 로 셀렉터를 직접 잡는다.\n */\n ...(filterable\n ? {\n filter: {\n handler: () => selector.value?.toggleFilter()\n }\n }\n : {})\n })\n }\n}\n"]}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 올린 파일의 줄을 저장할 수 있는 모양으로 바꾼다.
3
+ *
4
+ * 화면에서 떼어 둔 이유는 테스트 때문이다. 여기서 구분하는 것(모르는 칸·이름 없음·같은 이름 두 번·
5
+ * 빈 줄)은 파일마다 실제로 일어나는 일이고, 브라우저를 띄우지 않고 확인할 수 있어야 한다.
6
+ *
7
+ * 말은 여기서 만들지 않는다. 무엇이 잘못됐는지만 돌려주고, 사람에게 보일 문장은 화면이 만든다.
8
+ */
9
+ export type ImportProblem = {
10
+ kind: 'unknown-column';
11
+ line: number;
12
+ column: string;
13
+ } | {
14
+ kind: 'no-name';
15
+ line: number;
16
+ } | {
17
+ kind: 'duplicate-name';
18
+ line: number;
19
+ name: string;
20
+ };
21
+ export type ImportReading = {
22
+ rows: Record<string, unknown>[];
23
+ problems: ImportProblem[];
24
+ };
25
+ /**
26
+ * @param records 파일에서 읽어 온 줄들. 키는 파일의 머리글 그대로다.
27
+ * @param columnOf 머리글 하나를 이 화면의 칸 이름으로 바꾼다. 모르는 머리글이면 `undefined`.
28
+ */
29
+ export declare function readImportRows(records: Record<string, unknown>[], columnOf: (header: string) => string | undefined): ImportReading;
30
+ /**
31
+ * 이미 있는 이름이면 고침(`M`), 없으면 새로 만들기(`+`).
32
+ *
33
+ * 이것을 안 하면 같은 이름이 두 벌 생긴다 — 서버는 이름으로 합쳐 주지 않는다.
34
+ */
35
+ export declare function patchesFor(rows: Record<string, unknown>[], idOf: (name: string) => string | undefined): Record<string, unknown>[];
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 올린 파일의 줄을 저장할 수 있는 모양으로 바꾼다.
3
+ *
4
+ * 화면에서 떼어 둔 이유는 테스트 때문이다. 여기서 구분하는 것(모르는 칸·이름 없음·같은 이름 두 번·
5
+ * 빈 줄)은 파일마다 실제로 일어나는 일이고, 브라우저를 띄우지 않고 확인할 수 있어야 한다.
6
+ *
7
+ * 말은 여기서 만들지 않는다. 무엇이 잘못됐는지만 돌려주고, 사람에게 보일 문장은 화면이 만든다.
8
+ */
9
+ /**
10
+ * @param records 파일에서 읽어 온 줄들. 키는 파일의 머리글 그대로다.
11
+ * @param columnOf 머리글 하나를 이 화면의 칸 이름으로 바꾼다. 모르는 머리글이면 `undefined`.
12
+ */
13
+ export function readImportRows(records, columnOf) {
14
+ const rows = [];
15
+ const problems = [];
16
+ const seen = new Set();
17
+ records.forEach((record, at) => {
18
+ /* 1은 머리글 줄이다. 사람이 엑셀에서 보는 줄 번호로 말한다. */
19
+ const line = at + 2;
20
+ const row = {};
21
+ for (const [header, value] of Object.entries(record)) {
22
+ /*
23
+ * 머리글과 값의 끝을 다듬는다. 윈도우에서 만든 파일은 줄이 CR LF 로 끝나서 마지막 칸의
24
+ * 머리글과 값에 CR 이 붙어 온다.
25
+ */
26
+ const column = columnOf(String(header).trim());
27
+ const trimmed = typeof value === 'string' ? value.trim() : value;
28
+ if (!column) {
29
+ problems.push({ kind: 'unknown-column', line, column: String(header).trim() });
30
+ continue;
31
+ }
32
+ row[column] = trimmed;
33
+ }
34
+ /*
35
+ * 빈 줄은 없는 줄로 친다 — 파일 끝의 줄바꿈 하나가 값이 다 빈 줄 하나를 만든다.
36
+ * 그것을 「이름이 없다」고 물면 멀쩡한 파일이 늘 거절된다.
37
+ */
38
+ if (Object.values(row).every(value => value === '' || value === null || value === undefined)) {
39
+ return;
40
+ }
41
+ const name = String(row.name ?? '').trim();
42
+ if (!name) {
43
+ problems.push({ kind: 'no-name', line });
44
+ return;
45
+ }
46
+ if (seen.has(name)) {
47
+ problems.push({ kind: 'duplicate-name', line, name });
48
+ return;
49
+ }
50
+ seen.add(name);
51
+ rows.push({ ...row, name });
52
+ });
53
+ return { rows, problems };
54
+ }
55
+ /**
56
+ * 이미 있는 이름이면 고침(`M`), 없으면 새로 만들기(`+`).
57
+ *
58
+ * 이것을 안 하면 같은 이름이 두 벌 생긴다 — 서버는 이름으로 합쳐 주지 않는다.
59
+ */
60
+ export function patchesFor(rows, idOf) {
61
+ return rows.map(row => {
62
+ const id = idOf(String(row.name));
63
+ return id ? { ...row, id, cuFlag: 'M' } : { ...row, cuFlag: '+' };
64
+ });
65
+ }
66
+ //# sourceMappingURL=import-rows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-rows.js","sourceRoot":"","sources":["../../../client/pages/master/import-rows.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAYH;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAkC,EAClC,QAAgD;IAEhD,MAAM,IAAI,GAA8B,EAAE,CAAA;IAC1C,MAAM,QAAQ,GAAoB,EAAE,CAAA;IACpC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAE9B,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE;QAC7B,wCAAwC;QACxC,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,CAAA;QACnB,MAAM,GAAG,GAA4B,EAAE,CAAA;QAEvC,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACrD;;;eAGG;YACH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;YAC9C,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;YAEhE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;gBAC9E,SAAQ;YACV,CAAC;YAED,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAA;QACvB,CAAC;QAED;;;WAGG;QACH,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,EAAE,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,EAAE,CAAC;YAC7F,OAAM;QACR,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAE1C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YACxC,OAAM;QACR,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YACrD,OAAM;QACR,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;IAEF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,IAA+B,EAC/B,IAA0C;IAE1C,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QACpB,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;QAEjC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;IACnE,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["/**\n * 올린 파일의 줄을 저장할 수 있는 모양으로 바꾼다.\n *\n * 화면에서 떼어 둔 이유는 테스트 때문이다. 여기서 구분하는 것(모르는 칸·이름 없음·같은 이름 두 번·\n * 빈 줄)은 파일마다 실제로 일어나는 일이고, 브라우저를 띄우지 않고 확인할 수 있어야 한다.\n *\n * 말은 여기서 만들지 않는다. 무엇이 잘못됐는지만 돌려주고, 사람에게 보일 문장은 화면이 만든다.\n */\n\nexport type ImportProblem =\n | { kind: 'unknown-column'; line: number; column: string }\n | { kind: 'no-name'; line: number }\n | { kind: 'duplicate-name'; line: number; name: string }\n\nexport type ImportReading = {\n rows: Record<string, unknown>[]\n problems: ImportProblem[]\n}\n\n/**\n * @param records 파일에서 읽어 온 줄들. 키는 파일의 머리글 그대로다.\n * @param columnOf 머리글 하나를 이 화면의 칸 이름으로 바꾼다. 모르는 머리글이면 `undefined`.\n */\nexport function readImportRows(\n records: Record<string, unknown>[],\n columnOf: (header: string) => string | undefined\n): ImportReading {\n const rows: Record<string, unknown>[] = []\n const problems: ImportProblem[] = []\n const seen = new Set<string>()\n\n records.forEach((record, at) => {\n /* 1은 머리글 줄이다. 사람이 엑셀에서 보는 줄 번호로 말한다. */\n const line = at + 2\n const row: Record<string, unknown> = {}\n\n for (const [header, value] of Object.entries(record)) {\n /*\n * 머리글과 값의 끝을 다듬는다. 윈도우에서 만든 파일은 줄이 CR LF 로 끝나서 마지막 칸의\n * 머리글과 값에 CR 이 붙어 온다.\n */\n const column = columnOf(String(header).trim())\n const trimmed = typeof value === 'string' ? value.trim() : value\n\n if (!column) {\n problems.push({ kind: 'unknown-column', line, column: String(header).trim() })\n continue\n }\n\n row[column] = trimmed\n }\n\n /*\n * 빈 줄은 없는 줄로 친다 — 파일 끝의 줄바꿈 하나가 값이 다 빈 줄 하나를 만든다.\n * 그것을 「이름이 없다」고 물면 멀쩡한 파일이 늘 거절된다.\n */\n if (Object.values(row).every(value => value === '' || value === null || value === undefined)) {\n return\n }\n\n const name = String(row.name ?? '').trim()\n\n if (!name) {\n problems.push({ kind: 'no-name', line })\n return\n }\n\n if (seen.has(name)) {\n problems.push({ kind: 'duplicate-name', line, name })\n return\n }\n\n seen.add(name)\n rows.push({ ...row, name })\n })\n\n return { rows, problems }\n}\n\n/**\n * 이미 있는 이름이면 고침(`M`), 없으면 새로 만들기(`+`).\n *\n * 이것을 안 하면 같은 이름이 두 벌 생긴다 — 서버는 이름으로 합쳐 주지 않는다.\n */\nexport function patchesFor(\n rows: Record<string, unknown>[],\n idOf: (name: string) => string | undefined\n): Record<string, unknown>[] {\n return rows.map(row => {\n const id = idOf(String(row.name))\n\n return id ? { ...row, id, cuFlag: 'M' } : { ...row, cuFlag: '+' }\n })\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ISO 8601 기간을 화면에서 다룬다 — **커널이 읽는 표기 그대로 저장한다.**
3
+ *
4
+ * ── 왜 있는 편집기를 안 썼나 ────────────────────────────────────────────────
5
+ * `@operato/grist-editor` 에 `duration` 편집기와 렌더러가 있다. 그런데 그것은 **초 숫자**를 받는다.
6
+ * 소스를 보면 첫 줄이 `Number(value) || 0` 이다. 우리 칸에는 `PT209S` 가 들어 있고 `Number('PT209S')`
7
+ * 는 NaN 이라 0 으로 떨어져서, 화면에 **아무것도 안 나왔다.** 붙여 놓고 확인하지 않았으면 「소요시간이
8
+ * 비어 있다」로 보였을 것이다. [확인함] 2026-09-04
9
+ *
10
+ * 더 나쁜 것은 쓰는 쪽이다. 그 편집기로 고치면 초 숫자가 저장되고, 커널은 그 칸을 ISO 8601 로
11
+ * 읽으므로 그때부터 이 공정의 소요시간이 「모른다」가 된다. 화면에는 값이 있는데.
12
+ *
13
+ * ── 읽는 쪽은 커널 것을 쓴다 ───────────────────────────────────────────────
14
+ * `parseIsoDuration` 이 계약에 있다. 해석 규칙을 여기서 다시 쓰면 두 곳에 생기고, 커널이 받아주지 않는 것을
15
+ * 화면이 받아들이는 날이 온다(연·월은 달력에 기대므로 커널이 받아주지 않는다).
16
+ *
17
+ * 쓰는 쪽은 계약에 없다. 여기서 만들고 아키텍트에게 올려 두었다 — 읽는 쪽을 가진 곳이 쓰는 쪽도
18
+ * 가지는 편이 맞다.
19
+ */
20
+ /** 초 → ISO 8601. 편집기가 저장할 때 쓴다. */
21
+ export declare function isoDurationOfSeconds(seconds: number): string;
22
+ /** ISO 8601 → 초. 해석할 수 없으면 `undefined` — 0 이 아니다. */
23
+ export declare function secondsOfIsoDuration(text: string | undefined): number | undefined;
24
+ /**
25
+ * 사람이 읽는 글자.
26
+ *
27
+ * **해석할 수 없으면 원문을 그대로 낸다.** 빈 칸으로 두면 「값이 없다」와 「값이 이상하다」가 같아
28
+ * 보인다. `PT30` 처럼 커널이 못 읽는 것이 들어 있으면 그것이 화면에 보여야 고칠 수 있다.
29
+ */
30
+ export declare function durationText(value: string | undefined, unit: {
31
+ hour: string;
32
+ minute: string;
33
+ second: string;
34
+ }): string;