@things-factory/worklist 10.1.40 → 10.1.41

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 (38) hide show
  1. package/dist-client/components/activity-starter-form.js +1 -1
  2. package/dist-client/components/activity-starter-form.js.map +1 -1
  3. package/dist-client/pages/work-door/door-sentence.d.ts +63 -0
  4. package/dist-client/pages/work-door/door-sentence.js +109 -0
  5. package/dist-client/pages/work-door/door-sentence.js.map +1 -0
  6. package/dist-client/pages/work-door/door-words.d.ts +7 -0
  7. package/dist-client/pages/work-door/door-words.js +30 -0
  8. package/dist-client/pages/work-door/door-words.js.map +1 -1
  9. package/dist-client/pages/work-door/work-doors-page.d.ts +1 -1
  10. package/dist-client/pages/work-door/work-doors-page.js +49 -17
  11. package/dist-client/pages/work-door/work-doors-page.js.map +1 -1
  12. package/dist-client/route.d.ts +1 -1
  13. package/dist-client/tsconfig.tsbuildinfo +1 -1
  14. package/dist-server/service/index.d.ts +3 -3
  15. package/dist-server/service/work-door/work-door-declaration.d.ts +11 -5
  16. package/dist-server/service/work-door/work-door-declaration.js +5 -3
  17. package/dist-server/service/work-door/work-door-declaration.js.map +1 -1
  18. package/dist-server/service/work-door/work-door-resolver.js +1 -1
  19. package/dist-server/service/work-door/work-door-resolver.js.map +1 -1
  20. package/dist-server/service/work-door/work-door-service.js +3 -14
  21. package/dist-server/service/work-door/work-door-service.js.map +1 -1
  22. package/dist-server/service/work-door/work-door-type.d.ts +43 -6
  23. package/dist-server/service/work-door/work-door-type.js +102 -26
  24. package/dist-server/service/work-door/work-door-type.js.map +1 -1
  25. package/dist-server/service/work-door/work-door.d.ts +65 -16
  26. package/dist-server/service/work-door/work-door.js +53 -90
  27. package/dist-server/service/work-door/work-door.js.map +1 -1
  28. package/dist-server/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +3 -3
  30. package/tests/work-door-door-words.test.ts +35 -1
  31. package/tests/work-door-not-used-refuse.test.ts +6 -6
  32. package/tests/work-door-sentence.test.ts +199 -0
  33. package/tests/work-door.test.ts +63 -56
  34. package/translations/en.json +26 -1
  35. package/translations/ja.json +26 -1
  36. package/translations/ko.json +26 -1
  37. package/translations/ms.json +26 -1
  38. package/translations/zh.json +26 -1
@@ -16,9 +16,9 @@
16
16
  * 그 공장에서는 실적 정정도 폐기도 승인 없이 바로 반영되는데, 화면에는 문이 있는 것처럼 보인다.
17
17
  *
18
18
  * ── 비어 있으면 그냥 통과가 아니다 ─────────────────────────────────────────
19
- * 워크리스트의 거동이 그렇다 — **결재선이 비면 상신이 곧 완료가 되어, 올린 사람이 자기 건을 승인한
20
- * 것이 된다**(`activity-lookup.ts` § `approvalLineStanding`). 「승인을 안 받는다」가 아니라 「자기가
21
- * 승인한다」다. 그 차이를 문장에 담는다.
19
+ * 빈 결재선에서 무엇이 일어나는지는 **문마다 선언한다**(ADR-0077 결정 1, `WhenNoLine`). `run` 문은 상신이
20
+ * 곧 완료가 되어 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 문은 올리는 것 자체가 거절된다.
21
+ * 「승인을 안 받는다」 한 벌로 말하면 둘 다 틀린다 — 판단은 그 선언을 그대로 싣는다.
22
22
  *
23
23
  * ── 기동 로그는 사실의 집이 아니다 ─────────────────────────────────────────
24
24
  * `install-boot.ts` 가 기동할 때 한 줄 찍는다. 그 줄은 한 번 지나가고, 그 뒤로 어느 화면도 이것을
@@ -29,13 +29,14 @@
29
29
  * 문이 두 종류다 — **축이 다르고, 비었을 때 결과도 다르다.**
30
30
  *
31
31
  * ```
32
- * approval 누가 승인하나 결재선이 비면 올린 사람이 곧 승인한 것이 된다 → 통과해 버린다
32
+ * approval 누가 승인하나 결재선이 비면 문의 선언대로 — 통과해 버리거나(run) 올릴 수 없다(refuse)
33
33
  * assignment 누가 받나 역할이 안 묶이면 배정 업무가 아무에게도 안 간다 → 아무도 안 받는다
34
34
  * ```
35
35
  *
36
36
  * 같은 `off` 인데 결과가 반대다. 한 벌 문장으로 뭉개면 관리자가 무엇을 해야 하는지 알 수 없다.
37
37
  */
38
38
  import type { ApprovalLineStanding } from './approval-line-standing.js';
39
+ import type { WhenNoLine, WhenNoLineBecause } from './work-door-declaration.js';
39
40
  export type DoorKind = 'approval' | 'assignment';
40
41
  /** 문 하나가 어떤 상태인가. */
41
42
  export type DoorStanding =
@@ -54,16 +55,56 @@ export type DoorStanding =
54
55
  * **자동으로 추론하지 않는다.** 「석 달 안 썼으니 안 쓰는 것」은 조용한 한 철에 참이 된다.
55
56
  */
56
57
  | 'not-used';
57
- /** 이 문 하나에 대해 아는 것. */
58
+ /**
59
+ * **왜 그 상태인가** — 상태 하나에 까닭이 여럿이고, 까닭마다 관리자가 할 일이 다르다.
60
+ *
61
+ * ```
62
+ * not-used 공장이 안 쓴다고 말했다
63
+ * missing 양식이 기동 때 안 섰다 — 기동 기록을 볼 일
64
+ * line-empty 결재선이 비었다 — 결재선을 채울 일
65
+ * role-empty 결재선이 지목한 역할에 사람이 없다 — 그 역할에 사람을 넣을 일
66
+ * person-absent 결재선이 지목한 사람이 이 공장에 없다 — 다른 사람을 지목할 일
67
+ * no-assignee 배정 문에 받을 역할이 안 묶였다
68
+ * line-filled 결재자가 있다
69
+ * assignee-bound 받을 역할이 묶였다
70
+ * ```
71
+ *
72
+ * 문장이 아니라 값이다. 서버가 한국어 문장을 만들어 보내면 다른 언어 사용자가 한국어를 읽고, 화면이
73
+ * 그 문장을 다시 뜯어 읽게 된다 — 문장은 화면이 이 값과 번역으로 짓는다(`door-sentence.ts`).
74
+ */
75
+ export type DoorReason = 'not-used' | 'missing' | 'line-empty' | 'role-empty' | 'person-absent' | 'no-assignee' | 'line-filled' | 'assignee-bound';
76
+ /** 이 문 하나에 대해 아는 것 — 사실만. 문장은 화면이 짓는다. */
58
77
  export interface WorkDoor {
59
78
  /** 양식 이름. 콜백을 찾는 키다. */
60
79
  name: string;
61
- /** 화면에 낼 이름. */
62
- label: string;
80
+ /**
81
+ * 화면에 낼 이름의 번역 키 — **이름에서 센다**(`activityTKey`). 선언이 고르지 않는다.
82
+ *
83
+ * 선언한 키를 읽던 때 plant 가 `activity.${PascalCase}.label` 로 지어 보냈고 번역은 kebab 이라, 화면에
84
+ * 키 글자가 그대로 떴다(:4000, 2026-09-24). 아무도 고르지 않는 키는 아무도 틀리게 못 고른다.
85
+ */
86
+ labelKey: string;
63
87
  kind: DoorKind;
64
88
  standing: DoorStanding;
89
+ reason: DoorReason;
65
90
  /** 결재자 수. 안 섰거나 비었으면 0. */
66
91
  approvers: number;
92
+ /**
93
+ * **결재선이 비면 이 문이 어떻게 하나** — 선언 그대로(ADR-0077 결정 1). 결재 문에만 있다.
94
+ *
95
+ * 빈 결재선의 결과가 문마다 다르다. `run` 은 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 는
96
+ * 올리는 것 자체가 거절된다. 한 벌 문장으로 뭉개면 거절하는 문에 「자기 건을 승인한다」가 적힌다 —
97
+ * 실제로 그랬다(:4000, 개정 결재 넷, 2026-09-24).
98
+ */
99
+ whenNoLine?: WhenNoLine;
100
+ /** `refuse` · `as-established` 문이 결재를 요구하는 까닭. */
101
+ whenNoLineBecause?: WhenNoLineBecause;
102
+ /** 다른 문의 결재선을 따르면 그 문의 이름 번역 키. 결재선은 그 문에서 정한다. */
103
+ followsLabelKey?: string;
104
+ /** 배정 문에 묶인 역할 이름. */
105
+ assigneeRole?: string;
106
+ /** 공장이 「안 쓴다」고 적은 사유 — 사람이 쓴 글이라 번역하지 않고 그대로 보인다. */
107
+ notUsedBecause?: string;
67
108
  /**
68
109
  * **남의 도메인에서 빌려 온 양식일 때만** 그 도메인의 이름.
69
110
  *
@@ -74,20 +115,18 @@ export interface WorkDoor {
74
115
  * 달고, 그 말은 아무것도 알려 주지 않는다 — 실제로 그랬다(2026-09-12, 화면으로 보고 알았다).
75
116
  */
76
117
  from?: string;
77
- /** 이 상태가 무엇을 뜻하는지 — 화면에 그대로 나가는 문장. */
78
- because: string;
79
118
  }
80
119
  /** 한 문에 대해 읽은 것. 엔티티를 통째로 받지 않는다 — 그러면 이 판단이 DB 를 끌고 온다. */
81
120
  export interface DoorReading {
82
121
  name: string;
83
- label: string;
84
122
  kind: DoorKind;
123
+ /** 결재 문의 선언 — 빈 결재선에서 어떻게 하나와 그 까닭. */
124
+ whenNoLine?: WhenNoLine;
125
+ whenNoLineBecause?: WhenNoLineBecause;
85
126
  /**
86
127
  * 그 도메인(또는 부모)에 선 양식. 못 찾았으면 없다.
87
128
  *
88
129
  * 채워졌나를 보는 칸이 종류마다 다르다 — 결재는 `approvalLine`, 배정은 `assigneeRole` 이다.
89
- */
90
- /**
91
130
  * `borrowedFrom` 은 **다른 도메인의 양식일 때만** 채운다. 자기 것이면 비운다 — 읽는 쪽이
92
131
  * 「빌려 왔나」를 이 칸 하나로 판단한다.
93
132
  */
@@ -99,8 +138,8 @@ export interface DoorReading {
99
138
  /** 공장이 「안 쓴다」고 말했으면 그 사유. 말한 적 없으면 없다. */
100
139
  notUsedBecause?: string;
101
140
  /**
102
- * **다른 문의 결재선을 따르면** 그 문의 이름(화면에 나가는 것). 그러면 `approvalStanding` 도 그 문의
103
- * 것이다 — 이 문에서 결재선을 고치러 가면 안 되고 그 문으로 가야 한다는 것을 문장이 말한다.
141
+ * **다른 문의 결재선을 따르면** 그 문의 양식 이름. 그러면 `approvalStanding` 도 그 문의 것이다 —
142
+ * 이 문에서 결재선을 고치러 가면 안 되고 그 문으로 가야 한다는 것을 화면이 말한다.
104
143
  */
105
144
  follows?: string;
106
145
  /**
@@ -119,8 +158,18 @@ export declare function judgeDoor(reading: DoorReading): WorkDoor;
119
158
  * 그 화면은 잔소리가 된다(아키텍트 판정 2026-09-11).
120
159
  */
121
160
  export declare function doorsToFill(doors: readonly WorkDoor[]): number;
122
- /** 이 공장 전체를 한 문장으로 — 목록만 내면 읽는 사람이 11줄을 세어야 한다. */
123
- export declare function summarizeDoors(doors: readonly WorkDoor[]): string;
161
+ /**
162
+ * 상태별 수 — **문장은 화면이 짓는다.**
163
+ *
164
+ * 목록만 내면 읽는 사람이 열두 줄을 세어야 해서 한 문장으로 요약한다. 그 문장을 서버가 한국어로
165
+ * 지어 보내면 다른 언어 사용자가 한국어를 읽는다 — 그래서 수만 보낸다.
166
+ */
167
+ export declare function countDoors(doors: readonly WorkDoor[]): {
168
+ on: number;
169
+ off: number;
170
+ missing: number;
171
+ notUsed: number;
172
+ };
124
173
  /** The not-used declaration's content columns: which door and why. */
125
174
  export declare const WORK_DOOR_NOT_USED_CONTENT: readonly ["doorName", "because"];
126
175
  /** The declaration as it was — its content, who declared it, and when — written when it is withdrawn. */
@@ -17,9 +17,9 @@
17
17
  * 그 공장에서는 실적 정정도 폐기도 승인 없이 바로 반영되는데, 화면에는 문이 있는 것처럼 보인다.
18
18
  *
19
19
  * ── 비어 있으면 그냥 통과가 아니다 ─────────────────────────────────────────
20
- * 워크리스트의 거동이 그렇다 — **결재선이 비면 상신이 곧 완료가 되어, 올린 사람이 자기 건을 승인한
21
- * 것이 된다**(`activity-lookup.ts` § `approvalLineStanding`). 「승인을 안 받는다」가 아니라 「자기가
22
- * 승인한다」다. 그 차이를 문장에 담는다.
20
+ * 빈 결재선에서 무엇이 일어나는지는 **문마다 선언한다**(ADR-0077 결정 1, `WhenNoLine`). `run` 문은 상신이
21
+ * 곧 완료가 되어 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 문은 올리는 것 자체가 거절된다.
22
+ * 「승인을 안 받는다」 한 벌로 말하면 둘 다 틀린다 — 판단은 그 선언을 그대로 싣는다.
23
23
  *
24
24
  * ── 기동 로그는 사실의 집이 아니다 ─────────────────────────────────────────
25
25
  * `install-boot.ts` 가 기동할 때 한 줄 찍는다. 그 줄은 한 번 지나가고, 그 뒤로 어느 화면도 이것을
@@ -30,78 +30,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
30
30
  exports.WORK_DOOR_NOT_USED_CONTENT = void 0;
31
31
  exports.judgeDoor = judgeDoor;
32
32
  exports.doorsToFill = doorsToFill;
33
- exports.summarizeDoors = summarizeDoors;
33
+ exports.countDoors = countDoors;
34
34
  exports.workDoorNotUsedBefore = workDoorNotUsedBefore;
35
+ const decision_key_declaration_js_1 = require("../activity/decision-key-declaration.js");
35
36
  function judgeDoor(reading) {
36
- const { name, label, kind, installed, notUsedBecause, follows } = reading;
37
- /* 따르는 문이 있으면 결재선을 고치는 자리가 그 문이다. 문장 끝에 그 사실을 붙인다. */
38
- const following = follows ? ` — 「${follows}」 의 결재선을 따르므로, 결재선은 그 문에서 정합니다.` : '';
37
+ const { name, kind, installed, notUsedBecause, follows } = reading;
38
+ const approval = kind === 'approval';
39
+ const known = {
40
+ name,
41
+ labelKey: (0, decision_key_declaration_js_1.activityTKey)(name, 'label'),
42
+ kind,
43
+ standing: 'off',
44
+ reason: 'line-empty',
45
+ approvers: 0,
46
+ ...(approval && reading.whenNoLine ? { whenNoLine: reading.whenNoLine } : {}),
47
+ ...(approval && reading.whenNoLineBecause ? { whenNoLineBecause: reading.whenNoLineBecause } : {}),
48
+ ...(follows ? { followsLabelKey: (0, decision_key_declaration_js_1.activityTKey)(follows, 'label') } : {})
49
+ };
39
50
  /*
40
51
  * **공장이 안 쓴다고 말한 것이 먼저다.** 뒤에 두면 양식이 안 선 문을 「결함」으로 먼저 그리고,
41
52
  * 안 쓰기로 한 공장이 고치라는 말을 계속 듣는다.
42
53
  */
43
54
  if (notUsedBecause) {
44
- return {
45
- name,
46
- label,
47
- kind,
48
- standing: 'not-used',
49
- approvers: 0,
50
- because: `「${label}」 은 이 공장이 쓰지 않기로 한 업무입니다 — ${notUsedBecause}`
51
- };
55
+ return { ...known, standing: 'not-used', reason: 'not-used', notUsedBecause };
52
56
  }
53
57
  if (!installed) {
54
- return {
55
- name,
56
- label,
57
- kind,
58
- standing: 'missing',
59
- approvers: 0,
60
- because: `「${label}」 양식이 이 공장에 서지 않았습니다. 공장이 안 쓰기로 한 것이 아니라 세우지 못한 것이므로 기동 기록을 보십시오.`
61
- };
58
+ return { ...known, standing: 'missing', reason: 'missing' };
59
+ }
60
+ const from = installed.borrowedFrom ? { from: installed.borrowedFrom } : {};
61
+ if (!approval) {
62
+ return installed.assigneeRole
63
+ ? { ...known, ...from, standing: 'on', reason: 'assignee-bound', approvers: 1, assigneeRole: installed.assigneeRole }
64
+ : { ...known, ...from, standing: 'off', reason: 'no-assignee' };
62
65
  }
63
66
  /*
64
67
  * 결재 문은 **줄 수가 아니라 그 줄에 사람이 있는지**로 센다. 역할을 지목해 놓고 그 역할이 빈
65
68
  * 결재선은 화면에 「섰다」로 그려지고 상신하는 순간 터진다(ADR-0047 ③).
66
69
  */
67
- const line = kind === 'approval' ? reading.approvalStanding : undefined;
68
- const filled = kind === 'approval' ? (line && !line.refusal ? line.approvers : 0) : installed.assigneeRole ? 1 : 0;
69
- const roleIsEmpty = line?.refusal?.code === 'approval-role-empty';
70
- const personIsGone = line?.refusal?.code === 'approval-person-absent';
71
- if (!filled) {
72
- return {
73
- name,
74
- label,
75
- kind,
76
- standing: 'off',
77
- approvers: 0,
78
- from: installed.borrowedFrom,
79
- /* 종류마다 결과가 반대다(§`DoorKind`). 그래서 문장도 다르다. */
80
- because: (kind !== 'approval'
81
- ? `「${label}」 을 받을 역할이 묶이지 않았습니다. 이 업무는 만들어져도 아무에게도 가지 않습니다.`
82
- : roleIsEmpty
83
- ? /*
84
- * **관리자가 할 일이 다르다.** 결재선을 정하는 것과 그 역할에 사람을 넣는 것은 다른
85
- * 일이고, 뒤엣것을 앞엣것으로 말하면 이미 정해 둔 결재선을 다시 들여다보게 만든다.
86
- */
87
- `「${label}」 의 결재선이 지목한 역할에 사람이 없습니다. 결재선은 정해져 있으니 그 역할에 사람을 넣어야 이 단계가 섭니다.`
88
- : personIsGone
89
- ? `「${label}」 의 결재선이 지목한 사람이 이 공장에 없습니다. 다른 사람을 지목하거나 그 사람을 넣어야 이 단계가 섭니다.`
90
- : `「${label}」 에 결재자가 없습니다. 이 단계는 서지 않고, 올린 사람이 올리는 순간 자기 건을 승인한 것이 됩니다.`) +
91
- following
92
- };
70
+ const line = reading.approvalStanding;
71
+ const filled = line && !line.refusal ? line.approvers : 0;
72
+ if (filled) {
73
+ return { ...known, ...from, standing: 'on', reason: 'line-filled', approvers: filled };
93
74
  }
94
- return {
95
- name,
96
- label,
97
- kind,
98
- standing: 'on',
99
- approvers: filled,
100
- from: installed.borrowedFrom,
101
- because: kind === 'approval'
102
- ? `「${label}」 은 결재자 ${filled}명을 지납니다.${following}`
103
- : `「${label}」 은 ${installed.assigneeRole} 가 받습니다.`
104
- };
75
+ /*
76
+ * **관리자가 할 일이 다르다.** 결재선을 정하는 것과 그 역할에 사람을 넣는 것은 다른 일이고,
77
+ * 뒤엣것을 앞엣것으로 말하면 이미 정해 둔 결재선을 다시 들여다보게 만든다.
78
+ */
79
+ const reason = line?.refusal?.code === 'approval-role-empty'
80
+ ? 'role-empty'
81
+ : line?.refusal?.code === 'approval-person-absent'
82
+ ? 'person-absent'
83
+ : 'line-empty';
84
+ return { ...known, ...from, standing: 'off', reason };
105
85
  }
106
86
  /**
107
87
  * 아직 채울 것이 몇 개인가 — **안 쓰기로 한 것은 안 센다.**
@@ -112,36 +92,19 @@ function judgeDoor(reading) {
112
92
  function doorsToFill(doors) {
113
93
  return doors.filter(one => one.standing === 'off' || one.standing === 'missing').length;
114
94
  }
115
- /** 이 공장 전체를 한 문장으로 — 목록만 내면 읽는 사람이 11줄을 세어야 한다. */
116
- function summarizeDoors(doors) {
117
- if (!doors.length) {
118
- return '이 제품이 세우는 업무 양식이 없습니다.';
119
- }
120
- const on = doors.filter(one => one.standing === 'on').length;
121
- const off = doors.filter(one => one.standing === 'off').length;
122
- const missing = doors.filter(one => one.standing === 'missing').length;
123
- const notUsed = doors.filter(one => one.standing === 'not-used').length;
124
- /*
125
- * **셀 것은 `off + missing` 뿐이다.** 그래야 0 이 「다 됐다」를 뜻한다. 안 쓰기로 한 것을 남은 수에
126
- * 넣으면 다 채운 공장에서도 수가 안 줄고, 그 화면은 잔소리가 된다.
127
- */
128
- const todo = off + missing;
129
- if (!todo) {
130
- return notUsed
131
- ? `채울 것이 남지 않았습니다. 업무 문 ${on}개가 서 있고 ${notUsed}개는 이 공장이 쓰지 않기로 한 것입니다.`
132
- : `업무 문 ${doors.length}개가 다 서 있습니다.`;
133
- }
134
- const said = [`채울 것이 ${todo}개 남았습니다.`];
135
- if (off) {
136
- said.push(`${off}개는 양식은 섰는데 받을 사람이 없습니다 — 누가 할지 이 공장이 아직 정하지 않은 것입니다.`);
137
- }
138
- if (missing) {
139
- said.push(`${missing}개는 양식이 아예 서지 않았습니다. 이것은 설정이 아니라 결함입니다.`);
140
- }
141
- if (notUsed) {
142
- said.push(`${notUsed}개는 이 공장이 쓰지 않기로 한 것이라 세지 않았습니다.`);
143
- }
144
- return said.join(' ');
95
+ /**
96
+ * 상태별 수 — **문장은 화면이 짓는다.**
97
+ *
98
+ * 목록만 내면 읽는 사람이 열두 줄을 세어야 해서 한 문장으로 요약한다. 그 문장을 서버가 한국어로
99
+ * 지어 보내면 다른 언어 사용자가 한국어를 읽는다 — 그래서 수만 보낸다.
100
+ */
101
+ function countDoors(doors) {
102
+ return {
103
+ on: doors.filter(one => one.standing === 'on').length,
104
+ off: doors.filter(one => one.standing === 'off').length,
105
+ missing: doors.filter(one => one.standing === 'missing').length,
106
+ notUsed: doors.filter(one => one.standing === 'not-used').length
107
+ };
145
108
  }
146
109
  /** The not-used declaration's content columns: which door and why. */
147
110
  exports.WORK_DOOR_NOT_USED_CONTENT = ['doorName', 'because'];
@@ -1 +1 @@
1
- {"version":3,"file":"work-door.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * 이 공장에서 **어느 결재 문이 서 있나** — 순수 판단, DB 를 모른다.\n *\n * 결재선이 찼나를 세는 규칙은 상신하는 쪽과 **같은 함수**를 쓴다(§`approval-line-standing`). 여기서\n * 따로 세면 화면이 「섰다」로 그리고 상신이 거절하는 날이 온다 — 그 자리가 실제로 있었다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 이것을 볼 자리가 필요한가 ───────────────────────────────────────────\n * 양식은 제품이 기동할 때 세우고 **결재선은 그 공장이 채운다.** 채운 것이 「우리는 이 단계를 쓴다」는\n * 선언이다. 그래서 결재선이 비어 있는 것은 결함이 아니다 — 그 공장의 선택이다.\n *\n * 문제는 **그 선택이 어디에도 안 보인다**는 것이다. 업무 카탈로그는 「제품이 이 문을 만들었나」를\n * 보여 주지만 「이 공장에서 이 문이 서 있나」는 말하지 않는다. 두 질문이 다른데 하나만 보인다.\n *\n * 실측(2026-09-11): 미라텍 도메인에 양식 11개가 다 설치돼 있고 **11개 전부 결재선이 비어 있다.**\n * 그 공장에서는 실적 정정도 폐기도 승인 없이 바로 반영되는데, 화면에는 문이 있는 것처럼 보인다.\n *\n * ── 비어 있으면 그냥 통과가 아니다 ─────────────────────────────────────────\n * 워크리스트의 거동이 그렇다 — **결재선이 비면 상신이 곧 완료가 되어, 올린 사람이 자기 건을 승인한\n * 것이 된다**(`activity-lookup.ts` § `approvalLineStanding`). 「승인을 안 받는다」가 아니라 「자기가\n * 승인한다」다. 그 차이를 문장에 담는다.\n *\n * ── 기동 로그는 사실의 집이 아니다 ─────────────────────────────────────────\n * `install-boot.ts` 가 기동할 때 한 줄 찍는다. 그 줄은 한 번 지나가고, 그 뒤로 어느 화면도 이것을\n * 말하지 않는다. 사람이 조치해야 하는 사실은 물어볼 수 있는 자리에 있어야 한다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * 문이 두 종류다 — **축이 다르고, 비었을 때 결과도 다르다.**\n *\n * ```\n * approval 누가 승인하나 결재선이 비면 올린 사람이 곧 승인한 것이 된다 → 통과해 버린다\n * assignment 누가 받나 역할이 안 묶이면 배정 업무가 아무에게도 안 간다 → 아무도 안 받는다\n * ```\n *\n * 같은 `off` 인데 결과가 반대다. 한 벌 문장으로 뭉개면 관리자가 무엇을 해야 하는지 알 수 없다.\n */\nimport type { ApprovalLineStanding } from './approval-line-standing.js'\n\nexport type DoorKind = 'approval' | 'assignment'\n\n/** 문 하나가 어떤 상태인가. */\nexport type DoorStanding =\n /** 채워졌다 — 이 공장은 이 문을 쓴다. */\n | 'on'\n /** 양식은 섰는데 비었다. **조치 대상.** */\n | 'off'\n /** 양식이 아예 안 섰다 — 기동이 못 세웠다. 공장의 선택이 아니라 결함이다. **조치 대상.** */\n | 'missing'\n /**\n * **이 공장은 이 문을 안 쓴다** — 공장이 그렇게 말했다.\n *\n * 조치 대상이 아니다. 이것이 없으면 이 조사는 영원히 잔소리하는 화면이 되고, 그러면 사람들이\n * 안 본다 — 그때 진짜 빈 문 하나가 그 잔소리에 섞여 묻힌다(아키텍트 판정 2026-09-11).\n *\n * **자동으로 추론하지 않는다.** 「석 달 안 썼으니 안 쓰는 것」은 조용한 한 철에 참이 된다.\n */\n | 'not-used'\n\n/** 이 문 하나에 대해 아는 것. */\nexport interface WorkDoor {\n /** 양식 이름. 콜백을 찾는 키다. */\n name: string\n /** 화면에 낼 이름. */\n label: string\n kind: DoorKind\n standing: DoorStanding\n /** 결재자 수. 안 섰거나 비었으면 0. */\n approvers: number\n /**\n * **남의 도메인에서 빌려 온 양식일 때만** 그 도메인의 이름.\n *\n * 본사에서 정하고 공장이 쓰는 것이 정상이라, 자기 것이 없으면 부모 것을 쓴다\n * (`activityNamedIn`). 그 사실을 안 말하면 「우리가 정한 적 없는데 왜 서 있나」가 된다.\n *\n * ⚠ 자기 도메인이면 **없다.** 자기 이름을 채우면 화면이 모든 줄에 「miratek 에서 정했습니다」를\n * 달고, 그 말은 아무것도 알려 주지 않는다 — 실제로 그랬다(2026-09-12, 화면으로 보고 알았다).\n */\n from?: string\n /** 이 상태가 무엇을 뜻하는지 — 화면에 그대로 나가는 문장. */\n because: string\n}\n\n/** 한 문에 대해 읽은 것. 엔티티를 통째로 받지 않는다 — 그러면 이 판단이 DB 를 끌고 온다. */\nexport interface DoorReading {\n name: string\n label: string\n kind: DoorKind\n /**\n * 그 도메인(또는 부모)에 선 양식. 못 찾았으면 없다.\n *\n * 채워졌나를 보는 칸이 종류마다 다르다 — 결재는 `approvalLine`, 배정은 `assigneeRole` 이다.\n */\n /**\n * `borrowedFrom` 은 **다른 도메인의 양식일 때만** 채운다. 자기 것이면 비운다 — 읽는 쪽이\n * 「빌려 왔나」를 이 칸 하나로 판단한다.\n */\n installed?: { approvalLine?: unknown[]; assigneeRole?: string; borrowedFrom?: string }\n /** 공장이 「안 쓴다」고 말했으면 그 사유. 말한 적 없으면 없다. */\n notUsedBecause?: string\n /**\n * **다른 문의 결재선을 따르면** 그 문의 이름(화면에 나가는 것). 그러면 `approvalStanding` 도 그 문의\n * 것이다 — 이 문에서 결재선을 고치러 가면 안 되고 그 문으로 가야 한다는 것을 문장이 말한다.\n */\n follows?: string\n /**\n * 결재선이 찼나 — **상신하는 쪽과 같은 함수**(`judgeApprovalLine`)가 낸 결과를 그대로 받는다.\n *\n * 여기서 다시 세지 않는다. 역할에 사람이 있는지는 DB 를 읽어야 알고, 이 판단은 DB 를 모른다.\n * 없으면 「못 읽었다」로 읽는다 — 「안 읽었다」가 「다 찼다」로 새면 도로 줄 수 세기가 된다.\n */\n approvalStanding?: ApprovalLineStanding\n}\n\nexport function judgeDoor(reading: DoorReading): WorkDoor {\n const { name, label, kind, installed, notUsedBecause, follows } = reading\n /* 따르는 문이 있으면 결재선을 고치는 자리가 그 문이다. 문장 끝에 그 사실을 붙인다. */\n const following = follows ? ` — 「${follows}」 의 결재선을 따르므로, 결재선은 그 문에서 정합니다.` : ''\n\n /*\n * **공장이 안 쓴다고 말한 것이 먼저다.** 뒤에 두면 양식이 안 선 문을 「결함」으로 먼저 그리고,\n * 안 쓰기로 한 공장이 고치라는 말을 계속 듣는다.\n */\n if (notUsedBecause) {\n return {\n name,\n label,\n kind,\n standing: 'not-used',\n approvers: 0,\n because: `「${label}」 은 이 공장이 쓰지 않기로 한 업무입니다 — ${notUsedBecause}`\n }\n }\n\n if (!installed) {\n return {\n name,\n label,\n kind,\n standing: 'missing',\n approvers: 0,\n because: `「${label}」 양식이 이 공장에 서지 않았습니다. 공장이 안 쓰기로 한 것이 아니라 세우지 못한 것이므로 기동 기록을 보십시오.`\n }\n }\n\n /*\n * 결재 문은 **줄 수가 아니라 그 줄에 사람이 있는지**로 센다. 역할을 지목해 놓고 그 역할이 빈\n * 결재선은 화면에 「섰다」로 그려지고 상신하는 순간 터진다(ADR-0047 ③).\n */\n const line = kind === 'approval' ? reading.approvalStanding : undefined\n\n const filled = kind === 'approval' ? (line && !line.refusal ? line.approvers : 0) : installed.assigneeRole ? 1 : 0\n const roleIsEmpty = line?.refusal?.code === 'approval-role-empty'\n const personIsGone = line?.refusal?.code === 'approval-person-absent'\n\n if (!filled) {\n return {\n name,\n label,\n kind,\n standing: 'off',\n approvers: 0,\n from: installed.borrowedFrom,\n /* 종류마다 결과가 반대다(§`DoorKind`). 그래서 문장도 다르다. */\n because:\n (kind !== 'approval'\n ? `「${label}」 을 받을 역할이 묶이지 않았습니다. 이 업무는 만들어져도 아무에게도 가지 않습니다.`\n : roleIsEmpty\n ? /*\n * **관리자가 할 일이 다르다.** 결재선을 정하는 것과 그 역할에 사람을 넣는 것은 다른\n * 일이고, 뒤엣것을 앞엣것으로 말하면 이미 정해 둔 결재선을 다시 들여다보게 만든다.\n */\n `「${label}」 의 결재선이 지목한 역할에 사람이 없습니다. 결재선은 정해져 있으니 그 역할에 사람을 넣어야 이 단계가 섭니다.`\n : personIsGone\n ? `「${label}」 의 결재선이 지목한 사람이 이 공장에 없습니다. 다른 사람을 지목하거나 그 사람을 넣어야 이 단계가 섭니다.`\n : `「${label}」 에 결재자가 없습니다. 이 단계는 서지 않고, 올린 사람이 올리는 순간 자기 건을 승인한 것이 됩니다.`) +\n following\n }\n }\n\n return {\n name,\n label,\n kind,\n standing: 'on',\n approvers: filled,\n from: installed.borrowedFrom,\n because:\n kind === 'approval'\n ? `「${label}」 은 결재자 ${filled}명을 지납니다.${following}`\n : `「${label}」 은 ${installed.assigneeRole} 가 받습니다.`\n }\n}\n\n/**\n * 아직 채울 것이 몇 개인가 — **안 쓰기로 한 것은 안 센다.**\n *\n * 그래야 0 이 「다 됐다」를 뜻한다. 안 쓰는 것을 남은 수에 넣으면 다 채운 공장에서도 수가 안 줄고,\n * 그 화면은 잔소리가 된다(아키텍트 판정 2026-09-11).\n */\nexport function doorsToFill(doors: readonly WorkDoor[]): number {\n return doors.filter(one => one.standing === 'off' || one.standing === 'missing').length\n}\n\n/** 이 공장 전체를 한 문장으로 — 목록만 내면 읽는 사람이 11줄을 세어야 한다. */\nexport function summarizeDoors(doors: readonly WorkDoor[]): string {\n if (!doors.length) {\n return '이 제품이 세우는 업무 양식이 없습니다.'\n }\n\n const on = doors.filter(one => one.standing === 'on').length\n const off = doors.filter(one => one.standing === 'off').length\n const missing = doors.filter(one => one.standing === 'missing').length\n const notUsed = doors.filter(one => one.standing === 'not-used').length\n\n /*\n * **셀 것은 `off + missing` 뿐이다.** 그래야 0 이 「다 됐다」를 뜻한다. 안 쓰기로 한 것을 남은 수에\n * 넣으면 다 채운 공장에서도 수가 안 줄고, 그 화면은 잔소리가 된다.\n */\n const todo = off + missing\n\n if (!todo) {\n return notUsed\n ? `채울 것이 남지 않았습니다. 업무 문 ${on}개가 서 있고 ${notUsed}개는 이 공장이 쓰지 않기로 한 것입니다.`\n : `업무 문 ${doors.length}개가 다 서 있습니다.`\n }\n\n const said = [`채울 것이 ${todo}개 남았습니다.`]\n\n if (off) {\n said.push(`${off}개는 양식은 섰는데 받을 사람이 없습니다 — 누가 할지 이 공장이 아직 정하지 않은 것입니다.`)\n }\n\n if (missing) {\n said.push(`${missing}개는 양식이 아예 서지 않았습니다. 이것은 설정이 아니라 결함입니다.`)\n }\n\n if (notUsed) {\n said.push(`${notUsed}개는 이 공장이 쓰지 않기로 한 것이라 세지 않았습니다.`)\n }\n\n return said.join(' ')\n}\n\n/** The not-used declaration's content columns: which door and why. */\nexport const WORK_DOOR_NOT_USED_CONTENT = ['doorName', 'because'] as const\n\n/** The declaration as it was — its content, who declared it, and when — written when it is withdrawn. */\nexport function workDoorNotUsedBefore(declaration: {\n doorName: string\n because: string\n declaredBy?: { email?: string; name?: string } | null\n createdAt?: Date | null\n}) {\n return {\n ...Object.fromEntries(WORK_DOOR_NOT_USED_CONTENT.map(column => [column, declaration[column] ?? null])),\n declaredBy: declaration.declaredBy?.email ?? declaration.declaredBy?.name ?? null,\n declaredAt: declaration.createdAt ?? null\n }\n}\n"]}
1
+ {"version":3,"file":"work-door.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * 이 공장에서 **어느 결재 문이 서 있나** — 순수 판단, DB 를 모른다.\n *\n * 결재선이 찼나를 세는 규칙은 상신하는 쪽과 **같은 함수**를 쓴다(§`approval-line-standing`). 여기서\n * 따로 세면 화면이 「섰다」로 그리고 상신이 거절하는 날이 온다 — 그 자리가 실제로 있었다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 이것을 볼 자리가 필요한가 ───────────────────────────────────────────\n * 양식은 제품이 기동할 때 세우고 **결재선은 그 공장이 채운다.** 채운 것이 「우리는 이 단계를 쓴다」는\n * 선언이다. 그래서 결재선이 비어 있는 것은 결함이 아니다 — 그 공장의 선택이다.\n *\n * 문제는 **그 선택이 어디에도 안 보인다**는 것이다. 업무 카탈로그는 「제품이 이 문을 만들었나」를\n * 보여 주지만 「이 공장에서 이 문이 서 있나」는 말하지 않는다. 두 질문이 다른데 하나만 보인다.\n *\n * 실측(2026-09-11): 미라텍 도메인에 양식 11개가 다 설치돼 있고 **11개 전부 결재선이 비어 있다.**\n * 그 공장에서는 실적 정정도 폐기도 승인 없이 바로 반영되는데, 화면에는 문이 있는 것처럼 보인다.\n *\n * ── 비어 있으면 그냥 통과가 아니다 ─────────────────────────────────────────\n * 빈 결재선에서 무엇이 일어나는지는 **문마다 선언한다**(ADR-0077 결정 1, `WhenNoLine`). `run` 문은 상신이\n * 곧 완료가 되어 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 문은 올리는 것 자체가 거절된다.\n * 「승인을 안 받는다」 한 벌로 말하면 둘 다 틀린다 — 판단은 그 선언을 그대로 싣는다.\n *\n * ── 기동 로그는 사실의 집이 아니다 ─────────────────────────────────────────\n * `install-boot.ts` 가 기동할 때 한 줄 찍는다. 그 줄은 한 번 지나가고, 그 뒤로 어느 화면도 이것을\n * 말하지 않는다. 사람이 조치해야 하는 사실은 물어볼 수 있는 자리에 있어야 한다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * 문이 두 종류다 — **축이 다르고, 비었을 때 결과도 다르다.**\n *\n * ```\n * approval 누가 승인하나 결재선이 비면 문의 선언대로 — 통과해 버리거나(run) 올릴 수 없다(refuse)\n * assignment 누가 받나 역할이 안 묶이면 배정 업무가 아무에게도 안 간다 → 아무도 안 받는다\n * ```\n *\n * 같은 `off` 인데 결과가 반대다. 한 벌 문장으로 뭉개면 관리자가 무엇을 해야 하는지 알 수 없다.\n */\nimport type { ApprovalLineStanding } from './approval-line-standing.js'\nimport { activityTKey } from '../activity/decision-key-declaration.js'\nimport type { WhenNoLine, WhenNoLineBecause } from './work-door-declaration.js'\n\nexport type DoorKind = 'approval' | 'assignment'\n\n/** 문 하나가 어떤 상태인가. */\nexport type DoorStanding =\n /** 채워졌다 — 이 공장은 이 문을 쓴다. */\n | 'on'\n /** 양식은 섰는데 비었다. **조치 대상.** */\n | 'off'\n /** 양식이 아예 안 섰다 — 기동이 못 세웠다. 공장의 선택이 아니라 결함이다. **조치 대상.** */\n | 'missing'\n /**\n * **이 공장은 이 문을 안 쓴다** — 공장이 그렇게 말했다.\n *\n * 조치 대상이 아니다. 이것이 없으면 이 조사는 영원히 잔소리하는 화면이 되고, 그러면 사람들이\n * 안 본다 — 그때 진짜 빈 문 하나가 그 잔소리에 섞여 묻힌다(아키텍트 판정 2026-09-11).\n *\n * **자동으로 추론하지 않는다.** 「석 달 안 썼으니 안 쓰는 것」은 조용한 한 철에 참이 된다.\n */\n | 'not-used'\n\n/**\n * **왜 그 상태인가** — 상태 하나에 까닭이 여럿이고, 까닭마다 관리자가 할 일이 다르다.\n *\n * ```\n * not-used 공장이 안 쓴다고 말했다\n * missing 양식이 기동 때 안 섰다 — 기동 기록을 볼 일\n * line-empty 결재선이 비었다 — 결재선을 채울 일\n * role-empty 결재선이 지목한 역할에 사람이 없다 — 그 역할에 사람을 넣을 일\n * person-absent 결재선이 지목한 사람이 이 공장에 없다 — 다른 사람을 지목할 일\n * no-assignee 배정 문에 받을 역할이 안 묶였다\n * line-filled 결재자가 있다\n * assignee-bound 받을 역할이 묶였다\n * ```\n *\n * 문장이 아니라 값이다. 서버가 한국어 문장을 만들어 보내면 다른 언어 사용자가 한국어를 읽고, 화면이\n * 그 문장을 다시 뜯어 읽게 된다 — 문장은 화면이 이 값과 번역으로 짓는다(`door-sentence.ts`).\n */\nexport type DoorReason =\n | 'not-used'\n | 'missing'\n | 'line-empty'\n | 'role-empty'\n | 'person-absent'\n | 'no-assignee'\n | 'line-filled'\n | 'assignee-bound'\n\n/** 이 문 하나에 대해 아는 것 — 사실만. 문장은 화면이 짓는다. */\nexport interface WorkDoor {\n /** 양식 이름. 콜백을 찾는 키다. */\n name: string\n /**\n * 화면에 낼 이름의 번역 키 — **이름에서 센다**(`activityTKey`). 선언이 고르지 않는다.\n *\n * 선언한 키를 읽던 때 plant 가 `activity.${PascalCase}.label` 로 지어 보냈고 번역은 kebab 이라, 화면에\n * 키 글자가 그대로 떴다(:4000, 2026-09-24). 아무도 고르지 않는 키는 아무도 틀리게 못 고른다.\n */\n labelKey: string\n kind: DoorKind\n standing: DoorStanding\n reason: DoorReason\n /** 결재자 수. 안 섰거나 비었으면 0. */\n approvers: number\n /**\n * **결재선이 비면 이 문이 어떻게 하나** — 선언 그대로(ADR-0077 결정 1). 결재 문에만 있다.\n *\n * 빈 결재선의 결과가 문마다 다르다. `run` 은 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 는\n * 올리는 것 자체가 거절된다. 한 벌 문장으로 뭉개면 거절하는 문에 「자기 건을 승인한다」가 적힌다 —\n * 실제로 그랬다(:4000, 개정 결재 넷, 2026-09-24).\n */\n whenNoLine?: WhenNoLine\n /** `refuse` · `as-established` 문이 결재를 요구하는 까닭. */\n whenNoLineBecause?: WhenNoLineBecause\n /** 다른 문의 결재선을 따르면 그 문의 이름 번역 키. 결재선은 그 문에서 정한다. */\n followsLabelKey?: string\n /** 배정 문에 묶인 역할 이름. */\n assigneeRole?: string\n /** 공장이 「안 쓴다」고 적은 사유 — 사람이 쓴 글이라 번역하지 않고 그대로 보인다. */\n notUsedBecause?: string\n /**\n * **남의 도메인에서 빌려 온 양식일 때만** 그 도메인의 이름.\n *\n * 본사에서 정하고 공장이 쓰는 것이 정상이라, 자기 것이 없으면 부모 것을 쓴다\n * (`activityNamedIn`). 그 사실을 안 말하면 「우리가 정한 적 없는데 왜 서 있나」가 된다.\n *\n * ⚠ 자기 도메인이면 **없다.** 자기 이름을 채우면 화면이 모든 줄에 「miratek 에서 정했습니다」를\n * 달고, 그 말은 아무것도 알려 주지 않는다 — 실제로 그랬다(2026-09-12, 화면으로 보고 알았다).\n */\n from?: string\n}\n\n/** 한 문에 대해 읽은 것. 엔티티를 통째로 받지 않는다 — 그러면 이 판단이 DB 를 끌고 온다. */\nexport interface DoorReading {\n name: string\n kind: DoorKind\n /** 결재 문의 선언 — 빈 결재선에서 어떻게 하나와 그 까닭. */\n whenNoLine?: WhenNoLine\n whenNoLineBecause?: WhenNoLineBecause\n /**\n * 그 도메인(또는 부모)에 선 양식. 못 찾았으면 없다.\n *\n * 채워졌나를 보는 칸이 종류마다 다르다 — 결재는 `approvalLine`, 배정은 `assigneeRole` 이다.\n * `borrowedFrom` 은 **다른 도메인의 양식일 때만** 채운다. 자기 것이면 비운다 — 읽는 쪽이\n * 「빌려 왔나」를 이 칸 하나로 판단한다.\n */\n installed?: { approvalLine?: unknown[]; assigneeRole?: string; borrowedFrom?: string }\n /** 공장이 「안 쓴다」고 말했으면 그 사유. 말한 적 없으면 없다. */\n notUsedBecause?: string\n /**\n * **다른 문의 결재선을 따르면** 그 문의 양식 이름. 그러면 `approvalStanding` 도 그 문의 것이다 —\n * 이 문에서 결재선을 고치러 가면 안 되고 그 문으로 가야 한다는 것을 화면이 말한다.\n */\n follows?: string\n /**\n * 결재선이 찼나 — **상신하는 쪽과 같은 함수**(`judgeApprovalLine`)가 낸 결과를 그대로 받는다.\n *\n * 여기서 다시 세지 않는다. 역할에 사람이 있는지는 DB 를 읽어야 알고, 이 판단은 DB 를 모른다.\n * 없으면 「못 읽었다」로 읽는다 — 「안 읽었다」가 「다 찼다」로 새면 도로 줄 수 세기가 된다.\n */\n approvalStanding?: ApprovalLineStanding\n}\n\nexport function judgeDoor(reading: DoorReading): WorkDoor {\n const { name, kind, installed, notUsedBecause, follows } = reading\n const approval = kind === 'approval'\n const known: WorkDoor = {\n name,\n labelKey: activityTKey(name, 'label'),\n kind,\n standing: 'off',\n reason: 'line-empty',\n approvers: 0,\n ...(approval && reading.whenNoLine ? { whenNoLine: reading.whenNoLine } : {}),\n ...(approval && reading.whenNoLineBecause ? { whenNoLineBecause: reading.whenNoLineBecause } : {}),\n ...(follows ? { followsLabelKey: activityTKey(follows, 'label') } : {})\n }\n\n /*\n * **공장이 안 쓴다고 말한 것이 먼저다.** 뒤에 두면 양식이 안 선 문을 「결함」으로 먼저 그리고,\n * 안 쓰기로 한 공장이 고치라는 말을 계속 듣는다.\n */\n if (notUsedBecause) {\n return { ...known, standing: 'not-used', reason: 'not-used', notUsedBecause }\n }\n\n if (!installed) {\n return { ...known, standing: 'missing', reason: 'missing' }\n }\n\n const from = installed.borrowedFrom ? { from: installed.borrowedFrom } : {}\n\n if (!approval) {\n return installed.assigneeRole\n ? { ...known, ...from, standing: 'on', reason: 'assignee-bound', approvers: 1, assigneeRole: installed.assigneeRole }\n : { ...known, ...from, standing: 'off', reason: 'no-assignee' }\n }\n\n /*\n * 결재 문은 **줄 수가 아니라 그 줄에 사람이 있는지**로 센다. 역할을 지목해 놓고 그 역할이 빈\n * 결재선은 화면에 「섰다」로 그려지고 상신하는 순간 터진다(ADR-0047 ③).\n */\n const line = reading.approvalStanding\n const filled = line && !line.refusal ? line.approvers : 0\n\n if (filled) {\n return { ...known, ...from, standing: 'on', reason: 'line-filled', approvers: filled }\n }\n\n /*\n * **관리자가 할 일이 다르다.** 결재선을 정하는 것과 그 역할에 사람을 넣는 것은 다른 일이고,\n * 뒤엣것을 앞엣것으로 말하면 이미 정해 둔 결재선을 다시 들여다보게 만든다.\n */\n const reason: DoorReason =\n line?.refusal?.code === 'approval-role-empty'\n ? 'role-empty'\n : line?.refusal?.code === 'approval-person-absent'\n ? 'person-absent'\n : 'line-empty'\n\n return { ...known, ...from, standing: 'off', reason }\n}\n\n/**\n * 아직 채울 것이 몇 개인가 — **안 쓰기로 한 것은 안 센다.**\n *\n * 그래야 0 이 「다 됐다」를 뜻한다. 안 쓰는 것을 남은 수에 넣으면 다 채운 공장에서도 수가 안 줄고,\n * 그 화면은 잔소리가 된다(아키텍트 판정 2026-09-11).\n */\nexport function doorsToFill(doors: readonly WorkDoor[]): number {\n return doors.filter(one => one.standing === 'off' || one.standing === 'missing').length\n}\n\n/**\n * 상태별 수 — **문장은 화면이 짓는다.**\n *\n * 목록만 내면 읽는 사람이 열두 줄을 세어야 해서 한 문장으로 요약한다. 그 문장을 서버가 한국어로\n * 지어 보내면 다른 언어 사용자가 한국어를 읽는다 — 그래서 수만 보낸다.\n */\nexport function countDoors(doors: readonly WorkDoor[]): { on: number; off: number; missing: number; notUsed: number } {\n return {\n on: doors.filter(one => one.standing === 'on').length,\n off: doors.filter(one => one.standing === 'off').length,\n missing: doors.filter(one => one.standing === 'missing').length,\n notUsed: doors.filter(one => one.standing === 'not-used').length\n }\n}\n\n/** The not-used declaration's content columns: which door and why. */\nexport const WORK_DOOR_NOT_USED_CONTENT = ['doorName', 'because'] as const\n\n/** The declaration as it was — its content, who declared it, and when — written when it is withdrawn. */\nexport function workDoorNotUsedBefore(declaration: {\n doorName: string\n because: string\n declaredBy?: { email?: string; name?: string } | null\n createdAt?: Date | null\n}) {\n return {\n ...Object.fromEntries(WORK_DOOR_NOT_USED_CONTENT.map(column => [column, declaration[column] ?? null])),\n declaredBy: declaration.declaredBy?.email ?? declaration.declaredBy?.name ?? null,\n declaredAt: declaration.createdAt ?? null\n }\n}\n"]}