@things-factory/ops-master 10.1.35 → 10.1.37

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 (66) hide show
  1. package/dist-client/bootstrap.d.ts +1 -0
  2. package/dist-client/bootstrap.js +26 -0
  3. package/dist-client/bootstrap.js.map +1 -1
  4. package/dist-client/master-change-policy-axes.d.ts +26 -0
  5. package/dist-client/master-change-policy-axes.js +18 -0
  6. package/dist-client/master-change-policy-axes.js.map +1 -0
  7. package/dist-client/master-change-policy-panel.d.ts +49 -0
  8. package/dist-client/master-change-policy-panel.js +393 -0
  9. package/dist-client/master-change-policy-panel.js.map +1 -0
  10. package/dist-client/pages/master/applied-when-dialog.d.ts +45 -0
  11. package/dist-client/pages/master/applied-when-dialog.js +139 -0
  12. package/dist-client/pages/master/applied-when-dialog.js.map +1 -0
  13. package/dist-client/pages/master/assignment-fields.d.ts +27 -0
  14. package/dist-client/pages/master/assignment-fields.js +62 -0
  15. package/dist-client/pages/master/assignment-fields.js.map +1 -0
  16. package/dist-client/pages/master/equipment.js +1 -0
  17. package/dist-client/pages/master/equipment.js.map +1 -1
  18. package/dist-client/pages/master/master-page-base.d.ts +12 -0
  19. package/dist-client/pages/master/master-page-base.js +38 -4
  20. package/dist-client/pages/master/master-page-base.js.map +1 -1
  21. package/dist-client/pages/master/operational-location.js +1 -0
  22. package/dist-client/pages/master/operational-location.js.map +1 -1
  23. package/dist-client/tsconfig.tsbuildinfo +1 -1
  24. package/dist-server/service/approval-route.d.ts +37 -0
  25. package/dist-server/service/approval-route.js +34 -0
  26. package/dist-server/service/approval-route.js.map +1 -0
  27. package/dist-server/service/assignment/applied-when.d.ts +24 -0
  28. package/dist-server/service/assignment/applied-when.js +16 -0
  29. package/dist-server/service/assignment/applied-when.js.map +1 -0
  30. package/dist-server/service/assignment/assignment-history.d.ts +92 -0
  31. package/dist-server/service/assignment/assignment-history.js +289 -0
  32. package/dist-server/service/assignment/assignment-history.js.map +1 -0
  33. package/dist-server/service/assignment/assignment-write.d.ts +42 -0
  34. package/dist-server/service/assignment/assignment-write.js +101 -0
  35. package/dist-server/service/assignment/assignment-write.js.map +1 -0
  36. package/dist-server/service/assignment/master-value-claim.d.ts +135 -0
  37. package/dist-server/service/assignment/master-value-claim.js +174 -0
  38. package/dist-server/service/assignment/master-value-claim.js.map +1 -0
  39. package/dist-server/service/equipment/equipment-master-resolver.d.ts +9 -1
  40. package/dist-server/service/equipment/equipment-master-resolver.js +6 -3
  41. package/dist-server/service/equipment/equipment-master-resolver.js.map +1 -1
  42. package/dist-server/service/index.d.ts +6 -1
  43. package/dist-server/service/index.js +10 -0
  44. package/dist-server/service/index.js.map +1 -1
  45. package/dist-server/service/location/location-master-resolver.d.ts +4 -1
  46. package/dist-server/service/location/location-master-resolver.js +6 -3
  47. package/dist-server/service/location/location-master-resolver.js.map +1 -1
  48. package/dist-server/service/master-change-policy.d.ts +65 -2
  49. package/dist-server/service/master-change-policy.js +166 -21
  50. package/dist-server/service/master-change-policy.js.map +1 -1
  51. package/dist-server/service/master-crud.d.ts +11 -0
  52. package/dist-server/service/master-crud.js +12 -1
  53. package/dist-server/service/master-crud.js.map +1 -1
  54. package/dist-server/service/material/material-revision-service.js +13 -0
  55. package/dist-server/service/material/material-revision-service.js.map +1 -1
  56. package/dist-server/service/recipe/recipe-revision-service.js +13 -0
  57. package/dist-server/service/recipe/recipe-revision-service.js.map +1 -1
  58. package/dist-server/service/test-spec/test-spec-revision-service.js +14 -0
  59. package/dist-server/service/test-spec/test-spec-revision-service.js.map +1 -1
  60. package/dist-server/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +6 -4
  62. package/translations/en.json +59 -2
  63. package/translations/ja.json +58 -1
  64. package/translations/ko.json +59 -2
  65. package/translations/ms.json +58 -1
  66. package/translations/zh.json +60 -1
@@ -1,3 +1,4 @@
1
+ import './master-change-policy-panel.js';
1
2
  /**
2
3
  * 그리드 편집기 등록.
3
4
  *
@@ -1,4 +1,9 @@
1
+ import { html } from 'lit';
2
+ import { i18next } from '@operato/i18n';
3
+ import { store } from '@operato/shell';
4
+ import { ADD_SETTING } from '@things-factory/setting-base/dist-client';
1
5
  import { registerEditor as registerGristEditor, registerRenderer as registerGristRenderer } from '@operato/data-grist';
6
+ import './master-change-policy-panel.js';
2
7
  import { OpsMasterGristEditorIsoDuration, OxGristRendererIsoDuration } from './pages/master/grist-iso-duration.js';
3
8
  import { OpsMasterGristEditorReference, OxGristRendererList, OxGristRendererReference } from './pages/master/grist-reference.js';
4
9
  /**
@@ -32,5 +37,26 @@ export default function bootstrap() {
32
37
  * 「P-ECU-S-0001P-ECU-S-0002」로 보였다. 고칠 것은 그리기뿐이라 편집기는 그대로 둔다.
33
38
  */
34
39
  registerGristRenderer('ops-list', OxGristRendererList);
40
+ /*
41
+ * 기준정보 변경 정책을 **테넌트 설정** 탭에 올린다.
42
+ *
43
+ * 개인 설정이 아니다 — 이 테넌트가 어느 축을 승인제로 두는가는 그 사람 취향이 아니라 이곳의 규칙이다.
44
+ * `scope: 'tenant'` 가 그 말을 하고, 설정 화면이 그것으로 탭을 나눈다.
45
+ */
46
+ store.dispatch({
47
+ type: ADD_SETTING,
48
+ setting: {
49
+ seq: 90,
50
+ scope: 'tenant',
51
+ /*
52
+ * 제목은 **담는 쪽이 붙인다.** 같은 표가 업무 상세에도 서고, 거기서는 그 화면이 자기 제목을
53
+ * 붙인다. 부품이 자기 제목을 들고 다니면 두 자리에서 제목이 두 겹이 된다.
54
+ */
55
+ template: html `<setting-let
56
+ ><span slot="title">${i18next.t('master-policy.title')}</span>
57
+ <div slot="content"><master-change-policy-panel></master-change-policy-panel></div>
58
+ </setting-let>`
59
+ }
60
+ });
35
61
  }
36
62
  //# sourceMappingURL=bootstrap.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,IAAI,mBAAmB,EAAE,gBAAgB,IAAI,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAEtH,OAAO,EAAE,+BAA+B,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAA;AAClH,OAAO,EAAE,6BAA6B,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAA;AAEhI;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,qBAAqB,CAAC,cAAc,EAAE,0BAA0B,CAAC,CAAA;IACjE,mBAAmB,CAAC,cAAc,EAAE,+BAA+B,CAAC,CAAA;IAEpE;;;OAGG;IACH,qBAAqB,CAAC,WAAW,EAAE,wBAAwB,CAAC,CAAA;IAC5D,mBAAmB,CAAC,WAAW,EAAE,6BAA6B,CAAC,CAAA;IAE/D;;;OAGG;IACH,qBAAqB,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAA;AACxD,CAAC","sourcesContent":["import { registerEditor as registerGristEditor, registerRenderer as registerGristRenderer } from '@operato/data-grist'\n\nimport { OpsMasterGristEditorIsoDuration, OxGristRendererIsoDuration } from './pages/master/grist-iso-duration.js'\nimport { OpsMasterGristEditorReference, OxGristRendererList, OxGristRendererReference } from './pages/master/grist-reference.js'\n\n/**\n * 그리드 편집기 등록.\n *\n * `select` · `json5` · `datetime` 같은 것은 data-grist 가 기본으로 등록한다.\n *\n * ── 왜 `duration` 이 아니라 `iso-duration` 인가 ────────────────────────────\n * `@operato/grist-editor` 에 `duration` 이 있지만 그것은 **초 숫자**를 받는다. 우리 칸에는\n * `PT209S` 가 들어 있어서 붙여 보니 화면이 빈 칸이었다.\n *\n * 그래서 같은 이름으로 우리 것을 덮어 등록해 봤더니 **그것도 안 됐다.** `worklist` 패키지가 같은\n * 이름으로 초 숫자용을 등록하고 있었고, 나중에 부팅한 쪽이 이긴다. 둘 다 자기가 이겼는지 모른다.\n *\n * 이름을 뺏는 대신 뜻이 다른 이름을 쓴다. 초 숫자와 ISO 8601 은 실제로 다른 타입이고, 이름이 그것을\n * 말해야 한다. `worklist` 의 화면은 초를 저장하는 것이 맞고 우리가 건드릴 일이 아니다.\n *\n * **이 등록소는 앱 전체가 한 벌을 나눠 쓴다** — 이름이 겹치면 조용히 진다. 아키텍트에게 올려 두었다.\n */\nexport default function bootstrap() {\n registerGristRenderer('iso-duration', OxGristRendererIsoDuration)\n registerGristEditor('iso-duration', OpsMasterGristEditorIsoDuration)\n\n /*\n * 다른 표의 이름 하나를 골라 오는 칸. `resource-object` 와 팝업은 같고 저장하는 것이 다르다 —\n * 저쪽은 객체, 이쪽은 이름. 이름이 그것을 말해야 해서 따로 등록한다.\n */\n registerGristRenderer('reference', OxGristRendererReference)\n registerGristEditor('reference', OpsMasterGristEditorReference)\n\n /*\n * 목록 칸. 기본 글자 렌더러는 배열을 **구분 없이 이어 붙인다** — 두 사람이 선 자리가\n * 「P-ECU-S-0001P-ECU-S-0002」로 보였다. 고칠 것은 그리기뿐이라 편집기는 그대로 둔다.\n */\n registerGristRenderer('ops-list', OxGristRendererList)\n}\n"]}
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAA;AACtE,OAAO,EAAE,cAAc,IAAI,mBAAmB,EAAE,gBAAgB,IAAI,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAEtH,OAAO,iCAAiC,CAAA;AAExC,OAAO,EAAE,+BAA+B,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAA;AAClH,OAAO,EAAE,6BAA6B,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAA;AAEhI;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,qBAAqB,CAAC,cAAc,EAAE,0BAA0B,CAAC,CAAA;IACjE,mBAAmB,CAAC,cAAc,EAAE,+BAA+B,CAAC,CAAA;IAEpE;;;OAGG;IACH,qBAAqB,CAAC,WAAW,EAAE,wBAAwB,CAAC,CAAA;IAC5D,mBAAmB,CAAC,WAAW,EAAE,6BAA6B,CAAC,CAAA;IAE/D;;;OAGG;IACH,qBAAqB,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAA;IAEtD;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,CAAC;QACb,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE;YACP,GAAG,EAAE,EAAE;YACP,KAAK,EAAE,QAAQ;YACf;;;eAGG;YACH,QAAQ,EAAE,IAAI,CAAA;8BACU,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC;;qBAEzC;SAChB;KACF,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { html } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { store } from '@operato/shell'\nimport { ADD_SETTING } from '@things-factory/setting-base/dist-client'\nimport { registerEditor as registerGristEditor, registerRenderer as registerGristRenderer } from '@operato/data-grist'\n\nimport './master-change-policy-panel.js'\n\nimport { OpsMasterGristEditorIsoDuration, OxGristRendererIsoDuration } from './pages/master/grist-iso-duration.js'\nimport { OpsMasterGristEditorReference, OxGristRendererList, OxGristRendererReference } from './pages/master/grist-reference.js'\n\n/**\n * 그리드 편집기 등록.\n *\n * `select` · `json5` · `datetime` 같은 것은 data-grist 가 기본으로 등록한다.\n *\n * ── 왜 `duration` 이 아니라 `iso-duration` 인가 ────────────────────────────\n * `@operato/grist-editor` 에 `duration` 이 있지만 그것은 **초 숫자**를 받는다. 우리 칸에는\n * `PT209S` 가 들어 있어서 붙여 보니 화면이 빈 칸이었다.\n *\n * 그래서 같은 이름으로 우리 것을 덮어 등록해 봤더니 **그것도 안 됐다.** `worklist` 패키지가 같은\n * 이름으로 초 숫자용을 등록하고 있었고, 나중에 부팅한 쪽이 이긴다. 둘 다 자기가 이겼는지 모른다.\n *\n * 이름을 뺏는 대신 뜻이 다른 이름을 쓴다. 초 숫자와 ISO 8601 은 실제로 다른 타입이고, 이름이 그것을\n * 말해야 한다. `worklist` 의 화면은 초를 저장하는 것이 맞고 우리가 건드릴 일이 아니다.\n *\n * **이 등록소는 앱 전체가 한 벌을 나눠 쓴다** — 이름이 겹치면 조용히 진다. 아키텍트에게 올려 두었다.\n */\nexport default function bootstrap() {\n registerGristRenderer('iso-duration', OxGristRendererIsoDuration)\n registerGristEditor('iso-duration', OpsMasterGristEditorIsoDuration)\n\n /*\n * 다른 표의 이름 하나를 골라 오는 칸. `resource-object` 와 팝업은 같고 저장하는 것이 다르다 —\n * 저쪽은 객체, 이쪽은 이름. 이름이 그것을 말해야 해서 따로 등록한다.\n */\n registerGristRenderer('reference', OxGristRendererReference)\n registerGristEditor('reference', OpsMasterGristEditorReference)\n\n /*\n * 목록 칸. 기본 글자 렌더러는 배열을 **구분 없이 이어 붙인다** — 두 사람이 선 자리가\n * 「P-ECU-S-0001P-ECU-S-0002」로 보였다. 고칠 것은 그리기뿐이라 편집기는 그대로 둔다.\n */\n registerGristRenderer('ops-list', OxGristRendererList)\n\n /*\n * 기준정보 변경 정책을 **테넌트 설정** 탭에 올린다.\n *\n * 개인 설정이 아니다 — 이 테넌트가 어느 축을 승인제로 두는가는 그 사람 취향이 아니라 이곳의 규칙이다.\n * `scope: 'tenant'` 가 그 말을 하고, 설정 화면이 그것으로 탭을 나눈다.\n */\n store.dispatch({\n type: ADD_SETTING,\n setting: {\n seq: 90,\n scope: 'tenant',\n /*\n * 제목은 **담는 쪽이 붙인다.** 같은 표가 업무 상세에도 서고, 거기서는 그 화면이 자기 제목을\n * 붙인다. 부품이 자기 제목을 들고 다니면 두 자리에서 제목이 두 겹이 된다.\n */\n template: html`<setting-let\n ><span slot=\"title\">${i18next.t('master-policy.title')}</span>\n <div slot=\"content\"><master-change-policy-panel></master-change-policy-panel></div>\n </setting-let>`\n }\n })\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 어느 축을 보일 것인가.
3
+ *
4
+ * 이 표는 두 자리에 선다 — 사업장 설정에서는 **축 전부**를, 업무 상세에서는 **그 업무를 부르는
5
+ * 축만** 보인다. 같은 부품이 두 자리에 서면서 목록을 다르게 고르므로, 그 판단을 여기 한 벌 둔다.
6
+ *
7
+ * ── 왜 업무 쪽에서 좁히나 ──────────────────────────────────────────────────
8
+ * 업무 상세는 「이 업무가 어떻게 처리되나」를 끝내는 자리다. 거기에 축 열여섯을 다 펴면 이 업무와
9
+ * 상관없는 열다섯이 화면을 덮는다. 반대로 사업장 설정에서는 좁히면 안 된다 — 관리자는 「품목을
10
+ * 승인받게 하자」로 생각하지 어느 업무부터 찾자로 생각하지 않는다.
11
+ */
12
+ export type PolicyAxis = {
13
+ axis: string;
14
+ declared?: boolean;
15
+ approvalRequired?: boolean | null;
16
+ reason?: string | null;
17
+ approvalReady?: boolean;
18
+ approvalActivity?: string | null;
19
+ };
20
+ /**
21
+ * `activity` 를 주면 그 업무를 부르는 축만, 안 주면 전부.
22
+ *
23
+ * 빈 글자는 「안 준 것」으로 본다 — 빈 이름으로 좁히면 **아무것도 안 걸려 빈 표**가 되고, 그것은
24
+ * 「이 업무를 부르는 축이 없다」와 구별되지 않는다.
25
+ */
26
+ export declare function axesForActivity(axes?: PolicyAxis[] | null, activity?: string | null): PolicyAxis[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `activity` 를 주면 그 업무를 부르는 축만, 안 주면 전부.
3
+ *
4
+ * 빈 글자는 「안 준 것」으로 본다 — 빈 이름으로 좁히면 **아무것도 안 걸려 빈 표**가 되고, 그것은
5
+ * 「이 업무를 부르는 축이 없다」와 구별되지 않는다.
6
+ */
7
+ export function axesForActivity(axes, activity) {
8
+ const wanted = String(activity ?? '').trim();
9
+ if (!wanted)
10
+ return [...(axes ?? [])];
11
+ /*
12
+ * 양쪽을 다 다듬어 비교한다. 이름은 선언에서 온 글자이고 앞뒤 공백 하나가 붙어 오면 **그 줄만
13
+ * 조용히 사라진다** — 축이 없는 것과 구별되지 않는다. 심은 결함으로 확인하려다 이 자리가
14
+ * 한쪽만 다듬고 있는 것을 찾았다(2026-09-23).
15
+ */
16
+ return (axes ?? []).filter(one => String(one?.approvalActivity ?? '').trim() === wanted);
17
+ }
18
+ //# sourceMappingURL=master-change-policy-axes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"master-change-policy-axes.js","sourceRoot":"","sources":["../client/master-change-policy-axes.ts"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,IAA0B,EAAE,QAAwB;IAClF,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAE5C,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAA;IAErC;;;;OAIG;IACH,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,gBAAgB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,CAAA;AAC1F,CAAC","sourcesContent":["/**\n * 어느 축을 보일 것인가.\n *\n * 이 표는 두 자리에 선다 — 사업장 설정에서는 **축 전부**를, 업무 상세에서는 **그 업무를 부르는\n * 축만** 보인다. 같은 부품이 두 자리에 서면서 목록을 다르게 고르므로, 그 판단을 여기 한 벌 둔다.\n *\n * ── 왜 업무 쪽에서 좁히나 ──────────────────────────────────────────────────\n * 업무 상세는 「이 업무가 어떻게 처리되나」를 끝내는 자리다. 거기에 축 열여섯을 다 펴면 이 업무와\n * 상관없는 열다섯이 화면을 덮는다. 반대로 사업장 설정에서는 좁히면 안 된다 — 관리자는 「품목을\n * 승인받게 하자」로 생각하지 어느 업무부터 찾자로 생각하지 않는다.\n */\nexport type PolicyAxis = {\n axis: string\n declared?: boolean\n approvalRequired?: boolean | null\n reason?: string | null\n approvalReady?: boolean\n approvalActivity?: string | null\n}\n\n/**\n * `activity` 를 주면 그 업무를 부르는 축만, 안 주면 전부.\n *\n * 빈 글자는 「안 준 것」으로 본다 — 빈 이름으로 좁히면 **아무것도 안 걸려 빈 표**가 되고, 그것은\n * 「이 업무를 부르는 축이 없다」와 구별되지 않는다.\n */\nexport function axesForActivity(axes?: PolicyAxis[] | null, activity?: string | null): PolicyAxis[] {\n const wanted = String(activity ?? '').trim()\n\n if (!wanted) return [...(axes ?? [])]\n\n /*\n * 양쪽을 다 다듬어 비교한다. 이름은 선언에서 온 글자이고 앞뒤 공백 하나가 붙어 오면 **그 줄만\n * 조용히 사라진다** — 축이 없는 것과 구별되지 않는다. 심은 결함으로 확인하려다 이 자리가\n * 한쪽만 다듬고 있는 것을 찾았다(2026-09-23).\n */\n return (axes ?? []).filter(one => String(one?.approvalActivity ?? '').trim() === wanted)\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { LitElement } from 'lit';
2
+ import '@operato/help/ox-help-icon.js';
3
+ declare const MasterChangePolicyPanel_base: (new (...args: any[]) => LitElement) & typeof LitElement;
4
+ /**
5
+ * **이 사업장이 어느 기준정보를 승인제로 두는가.**
6
+ *
7
+ * ── 왜 설정이 아닌가 (2026-09-23, 사용자 판단) ────────────────────────────
8
+ * 처음에는 사업장 설정의 한 칸이었다. 설정은 내가 무엇을 보는지 고르는 자리이고, **다른 사람의 일이
9
+ * 늘어나는 것은 설정이 아니다** — 축 하나를 승인제로 돌리면 그 축의 수정이 앞으로 결재로 흘러간다.
10
+ * 그래서 업무를 다루는 화면으로 옮긴다.
11
+ *
12
+ * 그때 표가 두 자리에 선다. **자리마다 고르는 목록이 다르다** — `master-change-policy-axes.ts` 가
13
+ * 그 판단을 갖는다. 제목은 이 부품이 아니라 **담는 쪽**이 붙인다.
14
+ *
15
+ * ── 이 선언이 무엇인가 ─────────────────────────────────────────────────────
16
+ * 사람의 취향이 아니라 **이 사업장의 규칙**이다. 축마다 한 번 정하고, 정하면 그 축의 마스터는
17
+ * 승인을 지나야 바뀐다.
18
+ *
19
+ * ── 왜 화면이 필요했나 ─────────────────────────────────────────────────────
20
+ * 문은 있었는데 **그리로 가는 길이 없었다.** 정책을 안 정한 축은 화면이 열리다 말고 거절을 냈고
21
+ * (`MASTER_POLICY_REQUIRED`), 정하러 들어갈 자리도 없었다.
22
+ *
23
+ * ── 소유자만 정한다 ────────────────────────────────────────────────────────
24
+ * 읽기는 누구나 한다 — 기준정보를 고치려다 막힌 사람이 **왜 막혔는지** 여기서 알아야 한다.
25
+ * 정하는 것은 도메인 소유자뿐이고, 소유자가 아니면 그렇게 적어 준다.
26
+ */
27
+ export declare class MasterChangePolicyPanel extends MasterChangePolicyPanel_base {
28
+ static styles: import("lit").CSSResult[];
29
+ /**
30
+ * 업무 하나의 화면에 설 때 그 업무의 이름. 없으면 축 전부를 보인다.
31
+ *
32
+ * 이름으로 좁힌다 — 축이 어느 업무를 부르는지는 서버가 `approvalActivity` 로 답한다.
33
+ */
34
+ activity?: string;
35
+ private axes;
36
+ private canDeclare;
37
+ connectedCallback(): void;
38
+ private load;
39
+ /**
40
+ * 정하는 것도 바꾸는 것도 여기서 한다 — 바꾸면 정책 줄이 새로 서고, 지난 개정본은 자기 때의 줄을
41
+ * 계속 가리킨다. **결재에 안 태운다**(2026-09-22): 사업장 소유자가 정하는 것이라 확인만 받는다.
42
+ *
43
+ * ⚠ **사유는 사람에게 안 묻는다.** 서버가 필수로 받지만 그것은 「누가 어디서 정했나」를 남기려는
44
+ * 것이다. 시스템 설정에 사람이 글을 쓸 일이 아니다.
45
+ */
46
+ private declare;
47
+ render(): import("lit-html").TemplateResult<1>;
48
+ }
49
+ export {};
@@ -0,0 +1,393 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { css, html, LitElement } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { i18next, localize } from '@operato/i18n';
5
+ import { navigate } from '@operato/shell';
6
+ import { client } from '@operato/graphql';
7
+ import { OxPrompt } from '@operato/popup';
8
+ import gql from 'graphql-tag';
9
+ import '@operato/help/ox-help-icon.js';
10
+ import { axesForActivity } from './master-change-policy-axes.js';
11
+ /**
12
+ * **이 사업장이 어느 기준정보를 승인제로 두는가.**
13
+ *
14
+ * ── 왜 설정이 아닌가 (2026-09-23, 사용자 판단) ────────────────────────────
15
+ * 처음에는 사업장 설정의 한 칸이었다. 설정은 내가 무엇을 보는지 고르는 자리이고, **다른 사람의 일이
16
+ * 늘어나는 것은 설정이 아니다** — 축 하나를 승인제로 돌리면 그 축의 수정이 앞으로 결재로 흘러간다.
17
+ * 그래서 업무를 다루는 화면으로 옮긴다.
18
+ *
19
+ * 그때 표가 두 자리에 선다. **자리마다 고르는 목록이 다르다** — `master-change-policy-axes.ts` 가
20
+ * 그 판단을 갖는다. 제목은 이 부품이 아니라 **담는 쪽**이 붙인다.
21
+ *
22
+ * ── 이 선언이 무엇인가 ─────────────────────────────────────────────────────
23
+ * 사람의 취향이 아니라 **이 사업장의 규칙**이다. 축마다 한 번 정하고, 정하면 그 축의 마스터는
24
+ * 승인을 지나야 바뀐다.
25
+ *
26
+ * ── 왜 화면이 필요했나 ─────────────────────────────────────────────────────
27
+ * 문은 있었는데 **그리로 가는 길이 없었다.** 정책을 안 정한 축은 화면이 열리다 말고 거절을 냈고
28
+ * (`MASTER_POLICY_REQUIRED`), 정하러 들어갈 자리도 없었다.
29
+ *
30
+ * ── 소유자만 정한다 ────────────────────────────────────────────────────────
31
+ * 읽기는 누구나 한다 — 기준정보를 고치려다 막힌 사람이 **왜 막혔는지** 여기서 알아야 한다.
32
+ * 정하는 것은 도메인 소유자뿐이고, 소유자가 아니면 그렇게 적어 준다.
33
+ */
34
+ /*
35
+ * ⚠ 문구 키에 `master-policy.` 를 붙인다. 번역이 한 평면에 병합되고 **마지막이 이기므로**,
36
+ * `text.not-declared` 처럼 흔한 이름은 다른 패키지가 다른 뜻으로 이미 쓰고 있다 — plant 에서는
37
+ * 「등록되지 않았습니다」였고, 이 화면이 그 말로 떴다(2026-09-22 화면에서 확인).
38
+ */
39
+ let MasterChangePolicyPanel = class MasterChangePolicyPanel extends localize(i18next)(LitElement) {
40
+ constructor() {
41
+ super(...arguments);
42
+ this.axes = [];
43
+ this.canDeclare = false;
44
+ }
45
+ static { this.styles = [
46
+ css `
47
+ table {
48
+ width: 100%;
49
+ border-collapse: collapse;
50
+ }
51
+
52
+ /* 머리줄은 내용과 달라야 한다 — 같은 글자에 같은 선이면 첫 줄도 자료로 읽힌다. */
53
+ th {
54
+ text-align: left;
55
+ padding: var(--spacing-small, 8px);
56
+ background: var(--md-sys-color-surface-container-high);
57
+ color: var(--md-sys-color-on-surface-variant);
58
+ font: var(--label-font);
59
+ font-weight: 500;
60
+ border-bottom: 1px solid var(--md-sys-color-outline);
61
+ white-space: nowrap;
62
+ }
63
+
64
+ td {
65
+ text-align: left;
66
+ vertical-align: middle;
67
+ padding: var(--spacing-small, 8px);
68
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
69
+ font: var(--label-font);
70
+ color: var(--md-sys-color-on-surface);
71
+ }
72
+
73
+ tr:last-child td {
74
+ border-bottom: none;
75
+ }
76
+
77
+ /*
78
+ * 둘을 나란히 두고 **지금 것을 채워서** 보인다. 채운 쪽이 지금 서 있는 것이고, 못 누른다.
79
+ * 반대쪽은 비어 있고 눌린다 — 무엇이 서 있는지와 무엇을 고를 수 있는지가 한 자리에서 읽힌다.
80
+ *
81
+ * 처음엔 두 쪽의 차이가 옅은 바탕색 하나였다. 나란히 놓고 보면 둘 다 버튼으로만 보여서 어느
82
+ * 것이 지금 것인지 안 읽혔다(2026-09-22 화면에서 확인). 채움과 비움으로 가른다.
83
+ *
84
+ * 둘 다 비어 있으면 **아직 안 정한 것**이다. 그 줄만 두 쪽이 다 눌린다.
85
+ */
86
+ /*
87
+ * 칸을 flex 로 만들지 않는다. 그러면 그 칸만 표의 줄 높이를 안 따라가서, 옆 칸의 글자와 단추가
88
+ * 서로 다른 높이에 놓인다(2026-09-22 화면에서 확인). 칸은 칸으로 두고 단추 사이만 띄운다.
89
+ */
90
+ .choice {
91
+ white-space: nowrap;
92
+ text-align: right;
93
+ }
94
+
95
+ .choice button + button {
96
+ margin-left: var(--spacing-small, 6px);
97
+ }
98
+
99
+ .choice button {
100
+ padding: 3px 12px;
101
+ border: 1px solid var(--md-sys-color-outline-variant);
102
+ border-radius: var(--border-radius, 4px);
103
+ background: transparent;
104
+ color: var(--md-sys-color-on-surface-variant);
105
+ font: var(--label-font);
106
+ cursor: pointer;
107
+ }
108
+
109
+ .choice button:hover {
110
+ border-color: var(--md-sys-color-primary);
111
+ color: var(--md-sys-color-primary);
112
+ }
113
+
114
+ /*
115
+ * 결재선이 없어 **아직 고를 수 없는 쪽**은 그렇게 보인다. 눌러는 진다 — 왜 못 고르는지는 눌러야
116
+ * 들을 수 있고, 못 누르게 막으면 그 까닭이 어디에도 없다.
117
+ */
118
+ .choice button.blocked {
119
+ border-style: dashed;
120
+ color: var(--md-sys-color-outline);
121
+ }
122
+
123
+ .choice button.on {
124
+ background: var(--md-sys-color-primary);
125
+ color: var(--md-sys-color-on-primary);
126
+ border-color: var(--md-sys-color-primary);
127
+ font-weight: 700;
128
+ cursor: default;
129
+ }
130
+
131
+ /*
132
+ * 축 이름만으로는 무엇을 정하는 것인지 안 읽힌다 — 「라우팅」과 「제조법」이 무엇이 다른지
133
+ * 여기서 알아야 고를 수 있다.
134
+ *
135
+ * 한 줄은 **이름 밑에 그냥 적는다.** 마우스를 올려야 보이는 것은 안 올려 보면 없는 것과 같다.
136
+ *
137
+ * 더 알고 싶은 사람에게는 도움말 아이콘이 있다. 축이 무엇이고 무엇을 좌우하는지는 이 화면이
138
+ * 한 줄로 다 말할 것이 아니라 도움말이 맡을 내용이다 — 여기서는 /master-data/<축> 으로
139
+ * 길만 낸다. 내용은 도움말 쪽이 채운다.
140
+ */
141
+ /*
142
+ * 표준 도움말 아이콘은 18px 에 불투명도 0.75 다. 그것은 폼의 이름표 옆에 하나 놓일 때의 크기고,
143
+ * 여기서는 14px 글자 옆에 열여섯 번 되풀이돼서 이름보다 먼저 보였다. 그래서 **이 표에서만**
144
+ * 글자에 맞춰 줄이고 뒤로 물린다. 색과 동작은 표준 그대로다.
145
+ */
146
+ ox-help-icon {
147
+ vertical-align: middle;
148
+ --help-icon-size: 15px;
149
+ --help-icon-opacity: 0.45;
150
+ }
151
+ .axis {
152
+ white-space: nowrap;
153
+ font-weight: 500;
154
+ }
155
+
156
+ .what {
157
+ color: var(--md-sys-color-on-surface-variant);
158
+ font-size: 11px;
159
+ line-height: 1.35;
160
+ }
161
+
162
+ /* 설명이 남는 자리를 먹고, 이름과 버튼은 자기 폭만 쓴다. */
163
+ .what-column {
164
+ width: 100%;
165
+ }
166
+
167
+ /* 넓을 때는 설명이 자기 칸에 있다. 이름 밑의 것은 접혀 있다. */
168
+ .folded {
169
+ display: none;
170
+ }
171
+
172
+ /*
173
+ * 좁아지면 **설명 칸을 접어 이름 밑으로 넣는다.** 세 칸이 다 남으면 이름과 버튼이 눌려서
174
+ * 낱말마다 줄이 바뀐다 — 고르는 화면인데 고를 것이 안 읽힌다.
175
+ */
176
+ @media (max-width: 720px) {
177
+ .what-column {
178
+ display: none;
179
+ }
180
+
181
+ .folded {
182
+ display: block;
183
+ margin-top: 2px;
184
+ white-space: normal;
185
+ }
186
+
187
+ .axis {
188
+ white-space: normal;
189
+ }
190
+ }
191
+
192
+ /*
193
+ * 더 좁아지면 **설명을 아주 뺀다.** 접어 넣은 두 줄이 이름보다 길어져서, 줄마다 무엇을 고르는
194
+ * 화면인지가 안 보인다. 알고 싶은 사람은 도움말 아이콘으로 간다.
195
+ */
196
+ @media (max-width: 480px) {
197
+ .folded {
198
+ display: none;
199
+ }
200
+ }
201
+
202
+ /*
203
+ * 결재를 타는 축은 **결재선이 있는지 없는지를 여기서 말한다.** 없으면 정하러 가는 길도 같이
204
+ * 낸다 — 결재선이 어디서 정해지는지(업무 목록의 그 줄) 아는 사람이 많지 않다.
205
+ */
206
+ .line {
207
+ margin-top: 3px;
208
+ color: var(--md-sys-color-on-surface-variant);
209
+ font-size: 11px;
210
+ }
211
+
212
+ .line a {
213
+ color: var(--md-sys-color-primary);
214
+ }
215
+
216
+ .note {
217
+ color: var(--md-sys-color-on-surface-variant);
218
+ padding: var(--spacing-small, 8px);
219
+ }
220
+ `
221
+ ]; }
222
+ connectedCallback() {
223
+ super.connectedCallback();
224
+ this.load();
225
+ }
226
+ async load() {
227
+ const response = await client.query({
228
+ query: gql `
229
+ query {
230
+ masterChangePolicies
231
+ }
232
+ `,
233
+ fetchPolicy: 'network-only'
234
+ });
235
+ const found = response.data?.masterChangePolicies;
236
+ this.axes = found?.axes ?? [];
237
+ this.canDeclare = !!found?.canDeclare;
238
+ }
239
+ /**
240
+ * 정하는 것도 바꾸는 것도 여기서 한다 — 바꾸면 정책 줄이 새로 서고, 지난 개정본은 자기 때의 줄을
241
+ * 계속 가리킨다. **결재에 안 태운다**(2026-09-22): 사업장 소유자가 정하는 것이라 확인만 받는다.
242
+ *
243
+ * ⚠ **사유는 사람에게 안 묻는다.** 서버가 필수로 받지만 그것은 「누가 어디서 정했나」를 남기려는
244
+ * 것이다. 시스템 설정에 사람이 글을 쓸 일이 아니다.
245
+ */
246
+ async declare(one, approvalRequired) {
247
+ const { axis } = one;
248
+ const how = i18next.t(approvalRequired ? 'master-policy.approval-required' : 'master-policy.direct-change-allowed');
249
+ const name = i18next.t(`axis.${axis}`, { defaultValue: axis });
250
+ /*
251
+ * **걸 수 없는 것을 골라 보게 하지 않는다.** 결재선이 비어 있으면 서버가 거절하는데, 그것을 누른
252
+ * 뒤에 알면 「예」를 고르고 나서 안 된다는 말을 듣는 꼴이다. 묻기 전에 먼저 본다.
253
+ */
254
+ if (approvalRequired && one.approvalReady === false) {
255
+ await OxPrompt.open({
256
+ type: 'warning',
257
+ title: i18next.t('master-policy.approval-line-missing'),
258
+ text: i18next.t('master-policy.set-the-approval-line', { activity: one.approvalActivity ?? '' }),
259
+ confirmButton: { text: i18next.t('button.confirm') }
260
+ });
261
+ return;
262
+ }
263
+ /*
264
+ * 경고다. 이 사업장의 기준정보가 앞으로 어떻게 바뀌는가를 정하는 자리고, 규제를 푸는 쪽으로
265
+ * 누르면 결재 없이 바로 고쳐지기 시작한다. 되묻는 말투가 아니라 멈춰 세우는 말투여야 한다.
266
+ */
267
+ const yes = await OxPrompt.open({
268
+ type: 'warning',
269
+ title: i18next.t('master-policy.confirm-title'),
270
+ text: i18next.t('master-policy.confirm-text', { master: name, how }),
271
+ confirmButton: { text: i18next.t('button.confirm') },
272
+ cancelButton: { text: i18next.t('button.cancel') }
273
+ });
274
+ if (!yes) {
275
+ return;
276
+ }
277
+ /*
278
+ * 거절을 삼키지 않는다. 앞서는 오류가 있으면 그냥 다시 읽었고, 사람은 눌렀는데 아무 일도 안
279
+ * 일어나는 화면을 봤다. 규제를 거는 쪽은 **결재선이 비어 있으면 거절된다**
280
+ * (`MASTER_APPROVAL_LINE_REQUIRED`) — 그 말을 그대로 보여야 어디를 고칠지 안다.
281
+ */
282
+ try {
283
+ const response = await client.mutate({
284
+ mutation: gql `
285
+ mutation ($axis: String!, $approvalRequired: Boolean!, $reason: String!) {
286
+ declareMasterChangePolicy(axis: $axis, approvalRequired: $approvalRequired, reason: $reason)
287
+ }
288
+ `,
289
+ variables: { axis, approvalRequired, reason: i18next.t('master-policy.reason-from-setting') }
290
+ });
291
+ if (response.errors?.length) {
292
+ throw new Error(response.errors[0].message);
293
+ }
294
+ }
295
+ catch (error) {
296
+ /*
297
+ * 거절은 **틀이 이미 붉은 쪽지로 말한다** — 서버가 그 사람의 말로 지은 문장 그대로. 여기서 또
298
+ * 창을 띄우면 같은 말이 두 번 나온다(2026-09-22 화면에서 확인). 여기서는 다시 읽기만 한다.
299
+ */
300
+ console.warn(error?.message ?? error);
301
+ }
302
+ this.load();
303
+ }
304
+ render() {
305
+ const shown = axesForActivity(this.axes, this.activity);
306
+ /*
307
+ * 좁혔는데 하나도 없으면 **그 사실을 말한다.** 빈 표를 두면 「이 업무를 부르는 축이 없다」와
308
+ * 「아직 못 읽었다」가 같은 화면이 된다.
309
+ */
310
+ if (this.activity && !shown.length) {
311
+ return html `<div class="note">${i18next.t('master-policy.no-axis-for-activity')}</div>`;
312
+ }
313
+ return html `
314
+ <div>
315
+ ${this.canDeclare ? '' : html `<div class="note">${i18next.t('master-policy.only-the-owner')}</div>`}
316
+
317
+ <table>
318
+ <tr>
319
+ <th>${i18next.t('master-policy.field.axis')}</th>
320
+ <th class="what-column">${i18next.t('master-policy.field.what')}</th>
321
+ <th>${i18next.t('master-policy.field.state')}</th>
322
+ </tr>
323
+ ${shown.map(one => html `
324
+ <tr>
325
+ <td class="axis">
326
+ ${i18next.t(`axis.${one.axis}`, { defaultValue: one.axis })}
327
+ <ox-help-icon topic=${`/master-data/${one.axis}`}></ox-help-icon>
328
+ <span class="what folded">${i18next.t(`axis-help.${one.axis}`, { defaultValue: '' })}</span>
329
+ </td>
330
+ <td class="what what-column">${i18next.t(`axis-help.${one.axis}`, { defaultValue: '' })}</td>
331
+ ${!this.canDeclare
332
+ ? html `<td>
333
+ ${one.approvalRequired
334
+ ? i18next.t('master-policy.approval-required')
335
+ : i18next.t('master-policy.direct-change-allowed')}
336
+ </td>`
337
+ : html `<td class="choice">
338
+ <button
339
+ class="${one.approvalRequired === true ? 'on' : ''} ${one.approvalReady === false
340
+ ? 'blocked'
341
+ : ''}"
342
+ ?disabled=${one.approvalRequired === true}
343
+ @click=${() => this.declare(one, true)}
344
+ >
345
+ ${i18next.t('master-policy.button.approval-required')}
346
+ </button>
347
+ <button
348
+ class=${one.approvalRequired === false ? 'on' : ''}
349
+ ?disabled=${one.approvalRequired === false}
350
+ @click=${() => this.declare(one, false)}
351
+ >
352
+ ${i18next.t('master-policy.button.direct-change')}
353
+ </button>
354
+ ${one.approvalActivity
355
+ ? html `<div class="line">
356
+ ${one.approvalReady
357
+ ? i18next.t('master-policy.approval-line-set')
358
+ : this.activity
359
+ ? i18next.t('master-policy.approval-line-none-here')
360
+ : html `${i18next.t('master-policy.approval-line-none')} ·
361
+ <a
362
+ href="javascript:void(0)"
363
+ @click=${() => navigate('activity-detail?name=' + encodeURIComponent(one.approvalActivity))}
364
+ >
365
+ ${i18next.t('master-policy.go-set-the-line')}
366
+ </a>`}
367
+ </div>`
368
+ : ''}
369
+ </td>`}
370
+ </tr>
371
+ `)}
372
+ </table>
373
+ </div>
374
+ `;
375
+ }
376
+ };
377
+ __decorate([
378
+ property({ type: String }),
379
+ __metadata("design:type", String)
380
+ ], MasterChangePolicyPanel.prototype, "activity", void 0);
381
+ __decorate([
382
+ state(),
383
+ __metadata("design:type", Array)
384
+ ], MasterChangePolicyPanel.prototype, "axes", void 0);
385
+ __decorate([
386
+ state(),
387
+ __metadata("design:type", Object)
388
+ ], MasterChangePolicyPanel.prototype, "canDeclare", void 0);
389
+ MasterChangePolicyPanel = __decorate([
390
+ customElement('master-change-policy-panel')
391
+ ], MasterChangePolicyPanel);
392
+ export { MasterChangePolicyPanel };
393
+ //# sourceMappingURL=master-change-policy-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"master-change-policy-panel.js","sourceRoot":"","sources":["../client/master-change-policy-panel.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate } from '@operato/shell'\nimport { client } from '@operato/graphql'\nimport { OxPrompt } from '@operato/popup'\nimport gql from 'graphql-tag'\nimport '@operato/help/ox-help-icon.js'\n\nimport { axesForActivity, PolicyAxis } from './master-change-policy-axes.js'\n\n/**\n * **이 사업장이 어느 기준정보를 승인제로 두는가.**\n *\n * ── 왜 설정이 아닌가 (2026-09-23, 사용자 판단) ────────────────────────────\n * 처음에는 사업장 설정의 한 칸이었다. 설정은 내가 무엇을 보는지 고르는 자리이고, **다른 사람의 일이\n * 늘어나는 것은 설정이 아니다** — 축 하나를 승인제로 돌리면 그 축의 수정이 앞으로 결재로 흘러간다.\n * 그래서 업무를 다루는 화면으로 옮긴다.\n *\n * 그때 표가 두 자리에 선다. **자리마다 고르는 목록이 다르다** — `master-change-policy-axes.ts` 가\n * 그 판단을 갖는다. 제목은 이 부품이 아니라 **담는 쪽**이 붙인다.\n *\n * ── 이 선언이 무엇인가 ─────────────────────────────────────────────────────\n * 사람의 취향이 아니라 **이 사업장의 규칙**이다. 축마다 한 번 정하고, 정하면 그 축의 마스터는\n * 승인을 지나야 바뀐다.\n *\n * ── 왜 화면이 필요했나 ─────────────────────────────────────────────────────\n * 문은 있었는데 **그리로 가는 길이 없었다.** 정책을 안 정한 축은 화면이 열리다 말고 거절을 냈고\n * (`MASTER_POLICY_REQUIRED`), 정하러 들어갈 자리도 없었다.\n *\n * ── 소유자만 정한다 ────────────────────────────────────────────────────────\n * 읽기는 누구나 한다 — 기준정보를 고치려다 막힌 사람이 **왜 막혔는지** 여기서 알아야 한다.\n * 정하는 것은 도메인 소유자뿐이고, 소유자가 아니면 그렇게 적어 준다.\n */\n/*\n * ⚠ 문구 키에 `master-policy.` 를 붙인다. 번역이 한 평면에 병합되고 **마지막이 이기므로**,\n * `text.not-declared` 처럼 흔한 이름은 다른 패키지가 다른 뜻으로 이미 쓰고 있다 — plant 에서는\n * 「등록되지 않았습니다」였고, 이 화면이 그 말로 떴다(2026-09-22 화면에서 확인).\n */\n@customElement('master-change-policy-panel')\nexport class MasterChangePolicyPanel extends localize(i18next)(LitElement) {\n static styles = [\n css`\n table {\n width: 100%;\n border-collapse: collapse;\n }\n\n /* 머리줄은 내용과 달라야 한다 — 같은 글자에 같은 선이면 첫 줄도 자료로 읽힌다. */\n th {\n text-align: left;\n padding: var(--spacing-small, 8px);\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n font: var(--label-font);\n font-weight: 500;\n border-bottom: 1px solid var(--md-sys-color-outline);\n white-space: nowrap;\n }\n\n td {\n text-align: left;\n vertical-align: middle;\n padding: var(--spacing-small, 8px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n font: var(--label-font);\n color: var(--md-sys-color-on-surface);\n }\n\n tr:last-child td {\n border-bottom: none;\n }\n\n /*\n * 둘을 나란히 두고 **지금 것을 채워서** 보인다. 채운 쪽이 지금 서 있는 것이고, 못 누른다.\n * 반대쪽은 비어 있고 눌린다 — 무엇이 서 있는지와 무엇을 고를 수 있는지가 한 자리에서 읽힌다.\n *\n * 처음엔 두 쪽의 차이가 옅은 바탕색 하나였다. 나란히 놓고 보면 둘 다 버튼으로만 보여서 어느\n * 것이 지금 것인지 안 읽혔다(2026-09-22 화면에서 확인). 채움과 비움으로 가른다.\n *\n * 둘 다 비어 있으면 **아직 안 정한 것**이다. 그 줄만 두 쪽이 다 눌린다.\n */\n /*\n * 칸을 flex 로 만들지 않는다. 그러면 그 칸만 표의 줄 높이를 안 따라가서, 옆 칸의 글자와 단추가\n * 서로 다른 높이에 놓인다(2026-09-22 화면에서 확인). 칸은 칸으로 두고 단추 사이만 띄운다.\n */\n .choice {\n white-space: nowrap;\n text-align: right;\n }\n\n .choice button + button {\n margin-left: var(--spacing-small, 6px);\n }\n\n .choice button {\n padding: 3px 12px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: var(--border-radius, 4px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n font: var(--label-font);\n cursor: pointer;\n }\n\n .choice button:hover {\n border-color: var(--md-sys-color-primary);\n color: var(--md-sys-color-primary);\n }\n\n /*\n * 결재선이 없어 **아직 고를 수 없는 쪽**은 그렇게 보인다. 눌러는 진다 — 왜 못 고르는지는 눌러야\n * 들을 수 있고, 못 누르게 막으면 그 까닭이 어디에도 없다.\n */\n .choice button.blocked {\n border-style: dashed;\n color: var(--md-sys-color-outline);\n }\n\n .choice button.on {\n background: var(--md-sys-color-primary);\n color: var(--md-sys-color-on-primary);\n border-color: var(--md-sys-color-primary);\n font-weight: 700;\n cursor: default;\n }\n\n /*\n * 축 이름만으로는 무엇을 정하는 것인지 안 읽힌다 — 「라우팅」과 「제조법」이 무엇이 다른지\n * 여기서 알아야 고를 수 있다.\n *\n * 한 줄은 **이름 밑에 그냥 적는다.** 마우스를 올려야 보이는 것은 안 올려 보면 없는 것과 같다.\n *\n * 더 알고 싶은 사람에게는 도움말 아이콘이 있다. 축이 무엇이고 무엇을 좌우하는지는 이 화면이\n * 한 줄로 다 말할 것이 아니라 도움말이 맡을 내용이다 — 여기서는 /master-data/<축> 으로\n * 길만 낸다. 내용은 도움말 쪽이 채운다.\n */\n /*\n * 표준 도움말 아이콘은 18px 에 불투명도 0.75 다. 그것은 폼의 이름표 옆에 하나 놓일 때의 크기고,\n * 여기서는 14px 글자 옆에 열여섯 번 되풀이돼서 이름보다 먼저 보였다. 그래서 **이 표에서만**\n * 글자에 맞춰 줄이고 뒤로 물린다. 색과 동작은 표준 그대로다.\n */\n ox-help-icon {\n vertical-align: middle;\n --help-icon-size: 15px;\n --help-icon-opacity: 0.45;\n }\n .axis {\n white-space: nowrap;\n font-weight: 500;\n }\n\n .what {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 11px;\n line-height: 1.35;\n }\n\n /* 설명이 남는 자리를 먹고, 이름과 버튼은 자기 폭만 쓴다. */\n .what-column {\n width: 100%;\n }\n\n /* 넓을 때는 설명이 자기 칸에 있다. 이름 밑의 것은 접혀 있다. */\n .folded {\n display: none;\n }\n\n /*\n * 좁아지면 **설명 칸을 접어 이름 밑으로 넣는다.** 세 칸이 다 남으면 이름과 버튼이 눌려서\n * 낱말마다 줄이 바뀐다 — 고르는 화면인데 고를 것이 안 읽힌다.\n */\n @media (max-width: 720px) {\n .what-column {\n display: none;\n }\n\n .folded {\n display: block;\n margin-top: 2px;\n white-space: normal;\n }\n\n .axis {\n white-space: normal;\n }\n }\n\n /*\n * 더 좁아지면 **설명을 아주 뺀다.** 접어 넣은 두 줄이 이름보다 길어져서, 줄마다 무엇을 고르는\n * 화면인지가 안 보인다. 알고 싶은 사람은 도움말 아이콘으로 간다.\n */\n @media (max-width: 480px) {\n .folded {\n display: none;\n }\n }\n\n /*\n * 결재를 타는 축은 **결재선이 있는지 없는지를 여기서 말한다.** 없으면 정하러 가는 길도 같이\n * 낸다 — 결재선이 어디서 정해지는지(업무 목록의 그 줄) 아는 사람이 많지 않다.\n */\n .line {\n margin-top: 3px;\n color: var(--md-sys-color-on-surface-variant);\n font-size: 11px;\n }\n\n .line a {\n color: var(--md-sys-color-primary);\n }\n\n .note {\n color: var(--md-sys-color-on-surface-variant);\n padding: var(--spacing-small, 8px);\n }\n `\n ]\n\n /**\n * 업무 하나의 화면에 설 때 그 업무의 이름. 없으면 축 전부를 보인다.\n *\n * 이름으로 좁힌다 — 축이 어느 업무를 부르는지는 서버가 `approvalActivity` 로 답한다.\n */\n @property({ type: String }) activity?: string\n\n @state() private axes: PolicyAxis[] = []\n @state() private canDeclare = false\n\n connectedCallback() {\n super.connectedCallback()\n this.load()\n }\n\n private async load() {\n const response = await client.query({\n query: gql`\n query {\n masterChangePolicies\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n const found = response.data?.masterChangePolicies\n\n this.axes = found?.axes ?? []\n this.canDeclare = !!found?.canDeclare\n }\n\n /**\n * 정하는 것도 바꾸는 것도 여기서 한다 — 바꾸면 정책 줄이 새로 서고, 지난 개정본은 자기 때의 줄을\n * 계속 가리킨다. **결재에 안 태운다**(2026-09-22): 사업장 소유자가 정하는 것이라 확인만 받는다.\n *\n * ⚠ **사유는 사람에게 안 묻는다.** 서버가 필수로 받지만 그것은 「누가 어디서 정했나」를 남기려는\n * 것이다. 시스템 설정에 사람이 글을 쓸 일이 아니다.\n */\n private async declare(one: { axis: string; approvalReady?: boolean; approvalActivity?: string | null }, approvalRequired: boolean) {\n const { axis } = one\n const how = i18next.t(approvalRequired ? 'master-policy.approval-required' : 'master-policy.direct-change-allowed')\n const name = i18next.t(`axis.${axis}`, { defaultValue: axis })\n\n /*\n * **걸 수 없는 것을 골라 보게 하지 않는다.** 결재선이 비어 있으면 서버가 거절하는데, 그것을 누른\n * 뒤에 알면 「예」를 고르고 나서 안 된다는 말을 듣는 꼴이다. 묻기 전에 먼저 본다.\n */\n if (approvalRequired && one.approvalReady === false) {\n await OxPrompt.open({\n type: 'warning',\n title: i18next.t('master-policy.approval-line-missing'),\n text: i18next.t('master-policy.set-the-approval-line', { activity: one.approvalActivity ?? '' }),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n return\n }\n\n /*\n * 경고다. 이 사업장의 기준정보가 앞으로 어떻게 바뀌는가를 정하는 자리고, 규제를 푸는 쪽으로\n * 누르면 결재 없이 바로 고쳐지기 시작한다. 되묻는 말투가 아니라 멈춰 세우는 말투여야 한다.\n */\n const yes = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('master-policy.confirm-title'),\n text: i18next.t('master-policy.confirm-text', { master: name, how }),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!yes) {\n return\n }\n\n /*\n * 거절을 삼키지 않는다. 앞서는 오류가 있으면 그냥 다시 읽었고, 사람은 눌렀는데 아무 일도 안\n * 일어나는 화면을 봤다. 규제를 거는 쪽은 **결재선이 비어 있으면 거절된다**\n * (`MASTER_APPROVAL_LINE_REQUIRED`) — 그 말을 그대로 보여야 어디를 고칠지 안다.\n */\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($axis: String!, $approvalRequired: Boolean!, $reason: String!) {\n declareMasterChangePolicy(axis: $axis, approvalRequired: $approvalRequired, reason: $reason)\n }\n `,\n variables: { axis, approvalRequired, reason: i18next.t('master-policy.reason-from-setting') }\n })\n\n if (response.errors?.length) {\n throw new Error(response.errors[0].message)\n }\n } catch (error: any) {\n /*\n * 거절은 **틀이 이미 붉은 쪽지로 말한다** — 서버가 그 사람의 말로 지은 문장 그대로. 여기서 또\n * 창을 띄우면 같은 말이 두 번 나온다(2026-09-22 화면에서 확인). 여기서는 다시 읽기만 한다.\n */\n console.warn(error?.message ?? error)\n }\n\n this.load()\n }\n\n render() {\n const shown = axesForActivity(this.axes, this.activity)\n\n /*\n * 좁혔는데 하나도 없으면 **그 사실을 말한다.** 빈 표를 두면 「이 업무를 부르는 축이 없다」와\n * 「아직 못 읽었다」가 같은 화면이 된다.\n */\n if (this.activity && !shown.length) {\n return html`<div class=\"note\">${i18next.t('master-policy.no-axis-for-activity')}</div>`\n }\n\n return html`\n <div>\n ${this.canDeclare ? '' : html`<div class=\"note\">${i18next.t('master-policy.only-the-owner')}</div>`}\n\n <table>\n <tr>\n <th>${i18next.t('master-policy.field.axis')}</th>\n <th class=\"what-column\">${i18next.t('master-policy.field.what')}</th>\n <th>${i18next.t('master-policy.field.state')}</th>\n </tr>\n ${shown.map(\n one => html`\n <tr>\n <td class=\"axis\">\n ${i18next.t(`axis.${one.axis}`, { defaultValue: one.axis })}\n <ox-help-icon topic=${`/master-data/${one.axis}`}></ox-help-icon>\n <span class=\"what folded\">${i18next.t(`axis-help.${one.axis}`, { defaultValue: '' })}</span>\n </td>\n <td class=\"what what-column\">${i18next.t(`axis-help.${one.axis}`, { defaultValue: '' })}</td>\n ${!this.canDeclare\n ? html`<td>\n ${one.approvalRequired\n ? i18next.t('master-policy.approval-required')\n : i18next.t('master-policy.direct-change-allowed')}\n </td>`\n : html`<td class=\"choice\">\n <button\n class=\"${one.approvalRequired === true ? 'on' : ''} ${one.approvalReady === false\n ? 'blocked'\n : ''}\"\n ?disabled=${one.approvalRequired === true}\n @click=${() => this.declare(one, true)}\n >\n ${i18next.t('master-policy.button.approval-required')}\n </button>\n <button\n class=${one.approvalRequired === false ? 'on' : ''}\n ?disabled=${one.approvalRequired === false}\n @click=${() => this.declare(one, false)}\n >\n ${i18next.t('master-policy.button.direct-change')}\n </button>\n ${one.approvalActivity\n ? html`<div class=\"line\">\n ${one.approvalReady\n ? i18next.t('master-policy.approval-line-set')\n : this.activity\n ? i18next.t('master-policy.approval-line-none-here')\n : html`${i18next.t('master-policy.approval-line-none')} ·\n <a\n href=\"javascript:void(0)\"\n @click=${() =>\n navigate(\n 'activity-detail?name=' + encodeURIComponent(one.approvalActivity as string)\n )}\n >\n ${i18next.t('master-policy.go-set-the-line')}\n </a>`}\n </div>`\n : ''}\n </td>`}\n </tr>\n `\n )}\n </table>\n </div>\n `\n }\n}\n"]}