@things-factory/worklist 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 (215) hide show
  1. package/dist-client/activity-cells.d.ts +21 -0
  2. package/dist-client/activity-cells.js +117 -0
  3. package/dist-client/activity-cells.js.map +1 -0
  4. package/dist-client/activity-summary-generator.js +57 -11
  5. package/dist-client/activity-summary-generator.js.map +1 -1
  6. package/dist-client/approval-progress.d.ts +18 -0
  7. package/dist-client/approval-progress.js +31 -0
  8. package/dist-client/approval-progress.js.map +1 -0
  9. package/dist-client/bootstrap.js +6 -11
  10. package/dist-client/bootstrap.js.map +1 -1
  11. package/dist-client/chip-filters.d.ts +53 -0
  12. package/dist-client/chip-filters.js +60 -0
  13. package/dist-client/chip-filters.js.map +1 -0
  14. package/dist-client/components/activity-approval-ribon.d.ts +19 -0
  15. package/dist-client/components/activity-approval-ribon.js +50 -126
  16. package/dist-client/components/activity-approval-ribon.js.map +1 -1
  17. package/dist-client/components/activity-fields.d.ts +50 -0
  18. package/dist-client/components/activity-fields.js +393 -0
  19. package/dist-client/components/activity-fields.js.map +1 -0
  20. package/dist-client/components/activity-instance-ribon.d.ts +19 -0
  21. package/dist-client/components/activity-instance-ribon.js +55 -122
  22. package/dist-client/components/activity-instance-ribon.js.map +1 -1
  23. package/dist-client/components/activity-thread-ribon.d.ts +25 -0
  24. package/dist-client/components/activity-thread-ribon.js +64 -121
  25. package/dist-client/components/activity-thread-ribon.js.map +1 -1
  26. package/dist-client/components/activity-thread-timeline.d.ts +14 -2
  27. package/dist-client/components/activity-thread-timeline.js +234 -104
  28. package/dist-client/components/activity-thread-timeline.js.map +1 -1
  29. package/dist-client/components/state-ribon.d.ts +40 -0
  30. package/dist-client/components/state-ribon.js +200 -0
  31. package/dist-client/components/state-ribon.js.map +1 -0
  32. package/dist-client/due.d.ts +16 -0
  33. package/dist-client/due.js +78 -0
  34. package/dist-client/due.js.map +1 -0
  35. package/dist-client/pages/activity/activity-list-page.js +4 -6
  36. package/dist-client/pages/activity/activity-list-page.js.map +1 -1
  37. package/dist-client/pages/activity-approval/activity-approval-list-page.js +2 -1
  38. package/dist-client/pages/activity-approval/activity-approval-list-page.js.map +1 -1
  39. package/dist-client/pages/activity-approval/activity-approval-page.d.ts +52 -5
  40. package/dist-client/pages/activity-approval/activity-approval-page.js +265 -118
  41. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  42. package/dist-client/pages/activity-instance/activity-instance-list-page.d.ts +0 -1
  43. package/dist-client/pages/activity-instance/activity-instance-list-page.js +8 -24
  44. package/dist-client/pages/activity-instance/activity-instance-list-page.js.map +1 -1
  45. package/dist-client/pages/activity-instance/activity-instance-start-page.d.ts +1 -0
  46. package/dist-client/pages/activity-instance/activity-instance-start-page.js +7 -0
  47. package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
  48. package/dist-client/pages/activity-instance/activity-instance-view.js +1 -1
  49. package/dist-client/pages/activity-instance/activity-instance-view.js.map +1 -1
  50. package/dist-client/pages/activity-stats/activity-stats-list-page.js +2 -1
  51. package/dist-client/pages/activity-stats/activity-stats-list-page.js.map +1 -1
  52. package/dist-client/pages/activity-thread/activity-thread-page.d.ts +30 -4
  53. package/dist-client/pages/activity-thread/activity-thread-page.js +142 -66
  54. package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
  55. package/dist-client/pages/activity-thread/activity-thread-view-page.d.ts +17 -6
  56. package/dist-client/pages/activity-thread/activity-thread-view-page.js +118 -65
  57. package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
  58. package/dist-client/pages/activity-thread/activity-thread-view.d.ts +24 -7
  59. package/dist-client/pages/activity-thread/activity-thread-view.js +84 -308
  60. package/dist-client/pages/activity-thread/activity-thread-view.js.map +1 -1
  61. package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +1 -1
  62. package/dist-client/pages/installable-activity/installable-activity-list-page.js +13 -9
  63. package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
  64. package/dist-client/pages/todo/approval-done-list-page.d.ts +1 -0
  65. package/dist-client/pages/todo/approval-done-list-page.js +111 -58
  66. package/dist-client/pages/todo/approval-done-list-page.js.map +1 -1
  67. package/dist-client/pages/todo/approval-pending-list-page.d.ts +32 -1
  68. package/dist-client/pages/todo/approval-pending-list-page.js +485 -130
  69. package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
  70. package/dist-client/pages/todo/done-list-calendar-page.d.ts +2 -0
  71. package/dist-client/pages/todo/done-list-calendar-page.js +213 -29
  72. package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
  73. package/dist-client/pages/todo/done-list-page.d.ts +1 -0
  74. package/dist-client/pages/todo/done-list-page.js +126 -62
  75. package/dist-client/pages/todo/done-list-page.js.map +1 -1
  76. package/dist-client/pages/todo/draft-list-page.d.ts +1 -0
  77. package/dist-client/pages/todo/draft-list-page.js +82 -34
  78. package/dist-client/pages/todo/draft-list-page.js.map +1 -1
  79. package/dist-client/pages/todo/pickable-list-page.js +6 -10
  80. package/dist-client/pages/todo/pickable-list-page.js.map +1 -1
  81. package/dist-client/pages/todo/todo-list-page.d.ts +1 -1
  82. package/dist-client/pages/todo/todo-list-page.js +102 -78
  83. package/dist-client/pages/todo/todo-list-page.js.map +1 -1
  84. package/dist-client/route.d.ts +1 -1
  85. package/dist-client/route.js +0 -6
  86. package/dist-client/route.js.map +1 -1
  87. package/dist-client/templates/activity-approval-context-template.js +60 -138
  88. package/dist-client/templates/activity-approval-context-template.js.map +1 -1
  89. package/dist-client/templates/activity-instance-context-template.js +13 -172
  90. package/dist-client/templates/activity-instance-context-template.js.map +1 -1
  91. package/dist-client/templates/activity-thread-context-template.js +39 -131
  92. package/dist-client/templates/activity-thread-context-template.js.map +1 -1
  93. package/dist-client/templates/context-panel.d.ts +26 -0
  94. package/dist-client/templates/context-panel.js +182 -0
  95. package/dist-client/templates/context-panel.js.map +1 -0
  96. package/dist-client/tsconfig.tsbuildinfo +1 -1
  97. package/dist-client/when.d.ts +4 -0
  98. package/dist-client/when.js +72 -0
  99. package/dist-client/when.js.map +1 -0
  100. package/dist-server/controllers/activity-approval/_abort.js +2 -1
  101. package/dist-server/controllers/activity-approval/_abort.js.map +1 -1
  102. package/dist-server/controllers/activity-approval/approval-line.d.ts +106 -0
  103. package/dist-server/controllers/activity-approval/approval-line.js +151 -0
  104. package/dist-server/controllers/activity-approval/approval-line.js.map +1 -0
  105. package/dist-server/controllers/activity-approval/approve.js +61 -51
  106. package/dist-server/controllers/activity-approval/approve.js.map +1 -1
  107. package/dist-server/controllers/activity-approval/claim.d.ts +42 -0
  108. package/dist-server/controllers/activity-approval/claim.js +77 -0
  109. package/dist-server/controllers/activity-approval/claim.js.map +1 -0
  110. package/dist-server/controllers/activity-approval/create-approval.d.ts +41 -0
  111. package/dist-server/controllers/activity-approval/create-approval.js +78 -0
  112. package/dist-server/controllers/activity-approval/create-approval.js.map +1 -0
  113. package/dist-server/controllers/activity-approval/delegate.d.ts +16 -0
  114. package/dist-server/controllers/activity-approval/delegate.js +43 -7
  115. package/dist-server/controllers/activity-approval/delegate.js.map +1 -1
  116. package/dist-server/controllers/activity-approval/opinion.d.ts +10 -0
  117. package/dist-server/controllers/activity-approval/opinion.js +43 -0
  118. package/dist-server/controllers/activity-approval/opinion.js.map +1 -0
  119. package/dist-server/controllers/activity-approval/reject.js +21 -1
  120. package/dist-server/controllers/activity-approval/reject.js.map +1 -1
  121. package/dist-server/controllers/activity-approval/save.js +26 -2
  122. package/dist-server/controllers/activity-approval/save.js.map +1 -1
  123. package/dist-server/controllers/activity-installation/activity-installation-controller.d.ts +26 -0
  124. package/dist-server/controllers/activity-installation/activity-installation-controller.js +45 -0
  125. package/dist-server/controllers/activity-installation/activity-installation-controller.js.map +1 -1
  126. package/dist-server/controllers/activity-instance/approval-expectation.d.ts +34 -0
  127. package/dist-server/controllers/activity-instance/approval-expectation.js +18 -0
  128. package/dist-server/controllers/activity-instance/approval-expectation.js.map +1 -0
  129. package/dist-server/controllers/activity-instance/issue.js +32 -0
  130. package/dist-server/controllers/activity-instance/issue.js.map +1 -1
  131. package/dist-server/controllers/activity-thread/restart.js +17 -1
  132. package/dist-server/controllers/activity-thread/restart.js.map +1 -1
  133. package/dist-server/controllers/activity-thread/submit.js +23 -43
  134. package/dist-server/controllers/activity-thread/submit.js.map +1 -1
  135. package/dist-server/controllers/common.js +46 -0
  136. package/dist-server/controllers/common.js.map +1 -1
  137. package/dist-server/controllers/index.d.ts +1 -0
  138. package/dist-server/controllers/index.js +8 -0
  139. package/dist-server/controllers/index.js.map +1 -1
  140. package/dist-server/service/activity/activity-mutation.js +25 -5
  141. package/dist-server/service/activity/activity-mutation.js.map +1 -1
  142. package/dist-server/service/activity/activity-type.d.ts +14 -0
  143. package/dist-server/service/activity/activity-type.js +8 -0
  144. package/dist-server/service/activity/activity-type.js.map +1 -1
  145. package/dist-server/service/activity/activity.d.ts +18 -0
  146. package/dist-server/service/activity/activity.js +5 -0
  147. package/dist-server/service/activity/activity.js.map +1 -1
  148. package/dist-server/service/activity-approval/activity-approval-mutation.js +3 -2
  149. package/dist-server/service/activity-approval/activity-approval-mutation.js.map +1 -1
  150. package/dist-server/service/activity-approval/activity-approval-query.d.ts +19 -1
  151. package/dist-server/service/activity-approval/activity-approval-query.js +186 -56
  152. package/dist-server/service/activity-approval/activity-approval-query.js.map +1 -1
  153. package/dist-server/service/activity-approval/activity-approval.d.ts +29 -1
  154. package/dist-server/service/activity-approval/activity-approval.js +19 -0
  155. package/dist-server/service/activity-approval/activity-approval.js.map +1 -1
  156. package/dist-server/service/activity-approval/my-pending-approvals.d.ts +21 -0
  157. package/dist-server/service/activity-approval/my-pending-approvals.js +40 -0
  158. package/dist-server/service/activity-approval/my-pending-approvals.js.map +1 -0
  159. package/dist-server/service/activity-instance/activity-instance-type.d.ts +20 -0
  160. package/dist-server/service/activity-instance/activity-instance-type.js +11 -0
  161. package/dist-server/service/activity-instance/activity-instance-type.js.map +1 -1
  162. package/dist-server/service/activity-instance/activity-instance.d.ts +6 -0
  163. package/dist-server/service/activity-instance/activity-instance.js +5 -0
  164. package/dist-server/service/activity-instance/activity-instance.js.map +1 -1
  165. package/dist-server/service/activity-stats/activity-stats-query.d.ts +5 -0
  166. package/dist-server/service/activity-stats/activity-stats-query.js +149 -11
  167. package/dist-server/service/activity-stats/activity-stats-query.js.map +1 -1
  168. package/dist-server/service/activity-stats/activity-stats-type.d.ts +19 -6
  169. package/dist-server/service/activity-stats/activity-stats-type.js +37 -10
  170. package/dist-server/service/activity-stats/activity-stats-type.js.map +1 -1
  171. package/dist-server/service/activity-stats/event-placement.d.ts +55 -0
  172. package/dist-server/service/activity-stats/event-placement.js +82 -0
  173. package/dist-server/service/activity-stats/event-placement.js.map +1 -0
  174. package/dist-server/service/activity-thread/activity-thread-query.js +48 -24
  175. package/dist-server/service/activity-thread/activity-thread-query.js.map +1 -1
  176. package/dist-server/service/activity-thread/my-open-threads.d.ts +15 -0
  177. package/dist-server/service/activity-thread/my-open-threads.js +40 -0
  178. package/dist-server/service/activity-thread/my-open-threads.js.map +1 -0
  179. package/dist-server/service/activity-thread/todo-order.d.ts +49 -0
  180. package/dist-server/service/activity-thread/todo-order.js +70 -0
  181. package/dist-server/service/activity-thread/todo-order.js.map +1 -0
  182. package/dist-server/service/installable-activity/installable-activity-mutation.d.ts +6 -0
  183. package/dist-server/service/installable-activity/installable-activity-mutation.js +7 -24
  184. package/dist-server/service/installable-activity/installable-activity-mutation.js.map +1 -1
  185. package/dist-server/service/nulls-last.d.ts +30 -0
  186. package/dist-server/service/nulls-last.js +37 -0
  187. package/dist-server/service/nulls-last.js.map +1 -0
  188. package/dist-server/tsconfig.tsbuildinfo +1 -1
  189. package/helps/dataset/activity-instance-search-page.ko.md +5 -0
  190. package/helps/dataset/activity-instance-search-page.md +13 -0
  191. package/helps/worklist/activity-approval.ko.md +5 -0
  192. package/helps/worklist/activity-approval.md +14 -0
  193. package/helps/worklist/activity-bank-page.ko.md +5 -0
  194. package/helps/worklist/activity-bank-page.md +13 -0
  195. package/helps/worklist/activity-reporter-list.ko.md +5 -0
  196. package/helps/worklist/activity-reporter-list.md +13 -0
  197. package/helps/worklist/activity-store.ko.md +5 -0
  198. package/helps/worklist/activity-store.md +13 -0
  199. package/helps/worklist/activity-template.ko.md +5 -0
  200. package/helps/worklist/activity-template.md +13 -0
  201. package/helps/worklist/installable-activity.ko.md +5 -0
  202. package/helps/worklist/installable-activity.md +13 -0
  203. package/package.json +13 -12
  204. package/things-factory.config.js +0 -5
  205. package/translations/en.json +129 -51
  206. package/translations/ja.json +130 -52
  207. package/translations/ko.json +82 -4
  208. package/translations/ms.json +129 -51
  209. package/translations/zh.json +132 -54
  210. package/dist-client/pages/dashboard/dashboard-home.d.ts +0 -1
  211. package/dist-client/pages/dashboard/dashboard-home.js +0 -22
  212. package/dist-client/pages/dashboard/dashboard-home.js.map +0 -1
  213. package/dist-client/pages/worklist-home.d.ts +0 -1
  214. package/dist-client/pages/worklist-home.js +0 -17
  215. package/dist-client/pages/worklist-home.js.map +0 -1
@@ -0,0 +1,42 @@
1
+ import { EntityManager } from 'typeorm';
2
+ import { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js';
3
+ /**
4
+ * 이 결재를 내가 잡는다 — **대기 중인 것만, 한 번만.**
5
+ *
6
+ * ── 왜 필요한가 ────────────────────────────────────────────────────────────
7
+ * 승인·반려·위임이 다 같은 모양으로 시작한다. 읽고 → 「대기 중인가」 보고 → 저장한다. 그 사이에 아무
8
+ * 잠금이 없다.
9
+ *
10
+ * 역할로 걸린 결재는 **여러 명이 같은 건을 본다.** 대기 목록에 뜬 것을 두 사람이 누르는 것은 공유
11
+ * 대기열의 평범한 실패 방식이다. 지금은 둘 다 검사를 통과하고 **둘 다 다음 결재자를 만든다** — 같은
12
+ * 차례의 결재가 두 벌 서서 결재선이 갈라진다. 오류는 안 난다.
13
+ *
14
+ * ── 검사와 쓰기를 한 문장으로 ──────────────────────────────────────────────
15
+ * `WHERE judgment = ''` 를 갱신에 붙인다. 늦게 온 쪽은 0줄을 고치고, 그것으로 「남이 먼저 처리했다」를
16
+ * 안다.
17
+ *
18
+ * 두 트랜잭션이 겹치면 뒤엣것이 행 잠금 앞에서 멈춰 섰다가, 앞엣것이 커밋한 뒤 조건을 **다시 보고**
19
+ * 0줄을 얻는다. 세 드라이버 다 그렇게 동작한다 — postgres 는 잠금이 풀린 뒤 조건을 다시 보고, mysql
20
+ * InnoDB 는 갱신 시 최신 커밋본을 읽고, sqlite 는 쓰기를 아예 직렬화한다.
21
+ *
22
+ * ── 잡고 나면 그 행은 이 트랜잭션 것이다 ───────────────────────────────────
23
+ * 잡는 순간 행 잠금을 쥐므로, 나머지는 평범하게 저장해도 된다. 그래서 여기서는 **판정 하나만** 원자적으로
24
+ * 바꾼다 — 지키려는 규칙이 「대기 중인 것만 처리할 수 있다」 하나이기 때문이다.
25
+ *
26
+ * ── 왜 `@VersionColumn` 이 아닌가 ──────────────────────────────────────────
27
+ * 버전 칸은 컬럼을 늘려 마이그레이션이 필요하고, 이 규칙과 무관한 저장(의견 수정)까지 충돌시킨다.
28
+ * 이미 코드에 있는 검사를 그대로 갱신 문장으로 옮기는 편이 정확하고, 스키마를 안 건드려 배포해도
29
+ * 기존 데이터에 영향이 없다. (2026-08-31, 사용자 결정)
30
+ *
31
+ * ── 잡은 뒤의 저장은 안전하다 ─────────────────────────────────────────────
32
+ * 부르는 쪽은 잡기 뒤에 `save({ ...approval, judgment, comment, … })` 로 나머지를 쓴다. 손에 든
33
+ * `approval` 은 잡기 **전에** 읽은 것이라 판정이 낡았는데, 그 저장이 판정을 다시 명시하므로 되돌리지
34
+ * 않는다. 그리고 잡는 순간 행 잠금을 쥐었으므로 그 사이에 남이 끼어들 수 없다.
35
+ *
36
+ * 한 문장으로 합치지 않은 이유는 관계 칸 때문이다 — `updater`·`terminator`·`originalApproval` 이
37
+ * 엔티티 객체이고, `save()` 는 이것을 FK 로 풀어 주지만 `UpdateQueryBuilder.set()` 은 버전에 따라
38
+ * 다르다. 풀리지 않으면 「누가 처리했나」가 비고 **오류는 안 난다.**
39
+ *
40
+ * @returns 잡았으면 참. 거짓이면 **남이 먼저 처리했다** — 부르는 쪽이 던져서 트랜잭션을 물려야 한다.
41
+ */
42
+ export declare function claimPendingApproval(tx: EntityManager, approval: ActivityApproval, judgment: ActivityApprovalJudgment): Promise<boolean>;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claimPendingApproval = claimPendingApproval;
4
+ const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
5
+ /**
6
+ * 이 결재를 내가 잡는다 — **대기 중인 것만, 한 번만.**
7
+ *
8
+ * ── 왜 필요한가 ────────────────────────────────────────────────────────────
9
+ * 승인·반려·위임이 다 같은 모양으로 시작한다. 읽고 → 「대기 중인가」 보고 → 저장한다. 그 사이에 아무
10
+ * 잠금이 없다.
11
+ *
12
+ * 역할로 걸린 결재는 **여러 명이 같은 건을 본다.** 대기 목록에 뜬 것을 두 사람이 누르는 것은 공유
13
+ * 대기열의 평범한 실패 방식이다. 지금은 둘 다 검사를 통과하고 **둘 다 다음 결재자를 만든다** — 같은
14
+ * 차례의 결재가 두 벌 서서 결재선이 갈라진다. 오류는 안 난다.
15
+ *
16
+ * ── 검사와 쓰기를 한 문장으로 ──────────────────────────────────────────────
17
+ * `WHERE judgment = ''` 를 갱신에 붙인다. 늦게 온 쪽은 0줄을 고치고, 그것으로 「남이 먼저 처리했다」를
18
+ * 안다.
19
+ *
20
+ * 두 트랜잭션이 겹치면 뒤엣것이 행 잠금 앞에서 멈춰 섰다가, 앞엣것이 커밋한 뒤 조건을 **다시 보고**
21
+ * 0줄을 얻는다. 세 드라이버 다 그렇게 동작한다 — postgres 는 잠금이 풀린 뒤 조건을 다시 보고, mysql
22
+ * InnoDB 는 갱신 시 최신 커밋본을 읽고, sqlite 는 쓰기를 아예 직렬화한다.
23
+ *
24
+ * ── 잡고 나면 그 행은 이 트랜잭션 것이다 ───────────────────────────────────
25
+ * 잡는 순간 행 잠금을 쥐므로, 나머지는 평범하게 저장해도 된다. 그래서 여기서는 **판정 하나만** 원자적으로
26
+ * 바꾼다 — 지키려는 규칙이 「대기 중인 것만 처리할 수 있다」 하나이기 때문이다.
27
+ *
28
+ * ── 왜 `@VersionColumn` 이 아닌가 ──────────────────────────────────────────
29
+ * 버전 칸은 컬럼을 늘려 마이그레이션이 필요하고, 이 규칙과 무관한 저장(의견 수정)까지 충돌시킨다.
30
+ * 이미 코드에 있는 검사를 그대로 갱신 문장으로 옮기는 편이 정확하고, 스키마를 안 건드려 배포해도
31
+ * 기존 데이터에 영향이 없다. (2026-08-31, 사용자 결정)
32
+ *
33
+ * ── 잡은 뒤의 저장은 안전하다 ─────────────────────────────────────────────
34
+ * 부르는 쪽은 잡기 뒤에 `save({ ...approval, judgment, comment, … })` 로 나머지를 쓴다. 손에 든
35
+ * `approval` 은 잡기 **전에** 읽은 것이라 판정이 낡았는데, 그 저장이 판정을 다시 명시하므로 되돌리지
36
+ * 않는다. 그리고 잡는 순간 행 잠금을 쥐었으므로 그 사이에 남이 끼어들 수 없다.
37
+ *
38
+ * 한 문장으로 합치지 않은 이유는 관계 칸 때문이다 — `updater`·`terminator`·`originalApproval` 이
39
+ * 엔티티 객체이고, `save()` 는 이것을 FK 로 풀어 주지만 `UpdateQueryBuilder.set()` 은 버전에 따라
40
+ * 다르다. 풀리지 않으면 「누가 처리했나」가 비고 **오류는 안 난다.**
41
+ *
42
+ * @returns 잡았으면 참. 거짓이면 **남이 먼저 처리했다** — 부르는 쪽이 던져서 트랜잭션을 물려야 한다.
43
+ */
44
+ async function claimPendingApproval(tx, approval, judgment) {
45
+ /*
46
+ * 대기로 잡으면 조건이 통과하는데 판정이 그대로라 **두 번째 호출도 통과한다.** 잡는 뜻이 없어진다.
47
+ *
48
+ * `Pending` 은 빈 문자열이라 이 한 줄에 함께 걸린다 — 타입 검사도 그렇게 좁혀 준다.
49
+ */
50
+ if (!judgment) {
51
+ throw new Error('claimPendingApproval: a claim must move the judgment out of pending');
52
+ }
53
+ const result = await tx
54
+ .createQueryBuilder()
55
+ .update(activity_approval_js_1.ActivityApproval)
56
+ .set({ judgment })
57
+ .where('id = :id', { id: approval.id })
58
+ /*
59
+ * 대기 중은 빈 문자열이다(`ActivityApprovalJudgment.Pending`). `IS NULL` 도 함께 보는 이유는 기본값이
60
+ * 안 들어간 옛 행이 있을 수 있어서다 — 그런 행을 못 잡으면 그 결재는 영영 처리되지 않는다.
61
+ */
62
+ .andWhere('(judgment = :pending OR judgment IS NULL)', { pending: activity_approval_js_1.ActivityApprovalJudgment.Pending })
63
+ .execute();
64
+ /*
65
+ * `?? 0` here is deliberate, and it is the opposite of the same expression elsewhere.
66
+ *
67
+ * A missing row count means we do not know whether this claim took. For a conditional update
68
+ * that hands one person an approval, not knowing has to read as "did not get it": refusing a
69
+ * claim costs a retry, while granting one on an unknown result lets two people judge the same
70
+ * request. The unknown value maps to the conservative answer on purpose.
71
+ *
72
+ * Elsewhere the same `?? 0` was a defect — a missing count drawn as 「0 waiting」 — so the two
73
+ * are worth telling apart rather than tidying into one habit.
74
+ */
75
+ return (result.affected ?? 0) > 0;
76
+ }
77
+ //# sourceMappingURL=claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/claim.ts"],"names":[],"mappings":";;AA2CA,oDAsCC;AA/ED,+FAAiH;AAEjH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACI,KAAK,UAAU,oBAAoB,CACxC,EAAiB,EACjB,QAA0B,EAC1B,QAAkC;IAElC;;;;OAIG;IACH,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAA;IACxF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,EAAE;SACpB,kBAAkB,EAAE;SACpB,MAAM,CAAC,uCAAgB,CAAC;SACxB,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC;SACjB,KAAK,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;QACvC;;;WAGG;SACF,QAAQ,CAAC,2CAA2C,EAAE,EAAE,OAAO,EAAE,+CAAwB,CAAC,OAAO,EAAE,CAAC;SACpG,OAAO,EAAE,CAAA;IAEZ;;;;;;;;;;OAUG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;AACnC,CAAC","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\n\n/**\n * 이 결재를 내가 잡는다 — **대기 중인 것만, 한 번만.**\n *\n * ── 왜 필요한가 ────────────────────────────────────────────────────────────\n * 승인·반려·위임이 다 같은 모양으로 시작한다. 읽고 → 「대기 중인가」 보고 → 저장한다. 그 사이에 아무\n * 잠금이 없다.\n *\n * 역할로 걸린 결재는 **여러 명이 같은 건을 본다.** 대기 목록에 뜬 것을 두 사람이 누르는 것은 공유\n * 대기열의 평범한 실패 방식이다. 지금은 둘 다 검사를 통과하고 **둘 다 다음 결재자를 만든다** — 같은\n * 차례의 결재가 두 벌 서서 결재선이 갈라진다. 오류는 안 난다.\n *\n * ── 검사와 쓰기를 한 문장으로 ──────────────────────────────────────────────\n * `WHERE judgment = ''` 를 갱신에 붙인다. 늦게 온 쪽은 0줄을 고치고, 그것으로 「남이 먼저 처리했다」를\n * 안다.\n *\n * 두 트랜잭션이 겹치면 뒤엣것이 행 잠금 앞에서 멈춰 섰다가, 앞엣것이 커밋한 뒤 조건을 **다시 보고**\n * 0줄을 얻는다. 세 드라이버 다 그렇게 동작한다 — postgres 는 잠금이 풀린 뒤 조건을 다시 보고, mysql\n * InnoDB 는 갱신 시 최신 커밋본을 읽고, sqlite 는 쓰기를 아예 직렬화한다.\n *\n * ── 잡고 나면 그 행은 이 트랜잭션 것이다 ───────────────────────────────────\n * 잡는 순간 행 잠금을 쥐므로, 나머지는 평범하게 저장해도 된다. 그래서 여기서는 **판정 하나만** 원자적으로\n * 바꾼다 — 지키려는 규칙이 「대기 중인 것만 처리할 수 있다」 하나이기 때문이다.\n *\n * ── 왜 `@VersionColumn` 이 아닌가 ──────────────────────────────────────────\n * 버전 칸은 컬럼을 늘려 마이그레이션이 필요하고, 이 규칙과 무관한 저장(의견 수정)까지 충돌시킨다.\n * 이미 코드에 있는 검사를 그대로 갱신 문장으로 옮기는 편이 정확하고, 스키마를 안 건드려 배포해도\n * 기존 데이터에 영향이 없다. (2026-08-31, 사용자 결정)\n *\n * ── 잡은 뒤의 저장은 안전하다 ─────────────────────────────────────────────\n * 부르는 쪽은 잡기 뒤에 `save({ ...approval, judgment, comment, … })` 로 나머지를 쓴다. 손에 든\n * `approval` 은 잡기 **전에** 읽은 것이라 판정이 낡았는데, 그 저장이 판정을 다시 명시하므로 되돌리지\n * 않는다. 그리고 잡는 순간 행 잠금을 쥐었으므로 그 사이에 남이 끼어들 수 없다.\n *\n * 한 문장으로 합치지 않은 이유는 관계 칸 때문이다 — `updater`·`terminator`·`originalApproval` 이\n * 엔티티 객체이고, `save()` 는 이것을 FK 로 풀어 주지만 `UpdateQueryBuilder.set()` 은 버전에 따라\n * 다르다. 풀리지 않으면 「누가 처리했나」가 비고 **오류는 안 난다.**\n *\n * @returns 잡았으면 참. 거짓이면 **남이 먼저 처리했다** — 부르는 쪽이 던져서 트랜잭션을 물려야 한다.\n */\nexport async function claimPendingApproval(\n tx: EntityManager,\n approval: ActivityApproval,\n judgment: ActivityApprovalJudgment\n): Promise<boolean> {\n /*\n * 대기로 잡으면 조건이 통과하는데 판정이 그대로라 **두 번째 호출도 통과한다.** 잡는 뜻이 없어진다.\n *\n * `Pending` 은 빈 문자열이라 이 한 줄에 함께 걸린다 — 타입 검사도 그렇게 좁혀 준다.\n */\n if (!judgment) {\n throw new Error('claimPendingApproval: a claim must move the judgment out of pending')\n }\n\n const result = await tx\n .createQueryBuilder()\n .update(ActivityApproval)\n .set({ judgment })\n .where('id = :id', { id: approval.id })\n /*\n * 대기 중은 빈 문자열이다(`ActivityApprovalJudgment.Pending`). `IS NULL` 도 함께 보는 이유는 기본값이\n * 안 들어간 옛 행이 있을 수 있어서다 — 그런 행을 못 잡으면 그 결재는 영영 처리되지 않는다.\n */\n .andWhere('(judgment = :pending OR judgment IS NULL)', { pending: ActivityApprovalJudgment.Pending })\n .execute()\n\n /*\n * `?? 0` here is deliberate, and it is the opposite of the same expression elsewhere.\n *\n * A missing row count means we do not know whether this claim took. For a conditional update\n * that hands one person an approval, not knowing has to read as \"did not get it\": refusing a\n * claim costs a retry, while granting one on an unknown result lets two people judge the same\n * request. The unknown value maps to the conservative answer on purpose.\n *\n * Elsewhere the same `?? 0` was a defect — a missing count drawn as 「0 waiting」 — so the two\n * are worth telling apart rather than tidying into one habit.\n */\n return (result.affected ?? 0) > 0\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { ApprovalLineItem } from '@things-factory/organization';
2
+ import { ActivityApproval } from '../../service/activity-approval/activity-approval.js';
3
+ import type { ActivityThread } from '../../service/activity-thread/activity-thread.js';
4
+ /** 이 항목이 무슨 자격으로 서나 — 행에도 적는다(목록 질의가 참조를 걸러야 한다). */
5
+ export type Participation = 'approval' | 'reference';
6
+ /**
7
+ * 결재선 항목 하나로 행을 만든다.
8
+ *
9
+ * 역할로 걸린 것은 `approverRole` 만 채워지고 `approver` 는 비어 있다 — 그 역할을 가진 사람 여럿이 같은
10
+ * 건을 본다. 처리하는 사람이 정해지는 것은 누른 순간이다(§`approve.ts`).
11
+ */
12
+ export declare function createApproval(args: {
13
+ item: ApprovalLineItem;
14
+ order: number;
15
+ participation: Participation;
16
+ activityThread: ActivityThread;
17
+ /** 이 행이 왜 생겼나 — `submit` · `escalate` · `notify`. */
18
+ transaction: string;
19
+ }, context: ResolverContext): Promise<ActivityApproval>;
20
+ /**
21
+ * 이번에 알려야 할 참조들의 행을 만든다 — **하나가 실패해도 결재를 세우지 않는다.**
22
+ *
23
+ * 참조는 막지 않는 자격이다. 그런데 참조자를 못 찾아 던지면 그 예외가 상신·승인을 통째로 물린다 —
24
+ * 막지 않기로 한 것이 막게 된다.
25
+ *
26
+ * 그래서 실패를 삼키지 않되 **던지지도 않는다.** 못 만든 참조를 이유와 함께 돌려주고, 부르는 쪽이
27
+ * 그것을 기록한다.
28
+ */
29
+ export declare function createReferences(args: {
30
+ references: {
31
+ item: ApprovalLineItem;
32
+ order: number;
33
+ }[];
34
+ activityThread: ActivityThread;
35
+ }, context: ResolverContext): Promise<{
36
+ created: ActivityApproval[];
37
+ failed: {
38
+ order: number;
39
+ reason: string;
40
+ }[];
41
+ }>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createApproval = createApproval;
4
+ exports.createReferences = createReferences;
5
+ /*
6
+ * **결재선 한 항목을 실제 행으로 만든다** — 한 자리.
7
+ *
8
+ * ── 왜 뺐나 (2026-09-01) ─────────────────────────────────────────────────
9
+ * 같은 열다섯 줄이 두 파일에 있었다 — `submit.ts`(첫 결재자)와 `approve.ts`(다음 결재자). 역할로 걸린
10
+ * 경우와 사람으로 걸린 경우를 가르고, 못 찾으면 던지고, 행을 만든다.
11
+ *
12
+ * 참조까지 넣으면 **세 벌**이 된다. 그때 한쪽만 고쳐지는 날이 오고, 그러면 어떤 자격의 결재자만
13
+ * 조용히 안 만들어진다 — 오류가 안 나고 결재가 안 걸린다.
14
+ */
15
+ const auth_base_1 = require("@things-factory/auth-base");
16
+ const organization_1 = require("@things-factory/organization");
17
+ const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
18
+ const common_js_1 = require("../common.js");
19
+ /**
20
+ * 결재선 항목 하나로 행을 만든다.
21
+ *
22
+ * 역할로 걸린 것은 `approverRole` 만 채워지고 `approver` 는 비어 있다 — 그 역할을 가진 사람 여럿이 같은
23
+ * 건을 본다. 처리하는 사람이 정해지는 것은 누른 순간이다(§`approve.ts`).
24
+ */
25
+ async function createApproval(args, context) {
26
+ const { domain, user, tx } = context.state;
27
+ const { item, order, participation, activityThread, transaction } = args;
28
+ const base = {
29
+ domain,
30
+ activityThread,
31
+ round: activityThread.round,
32
+ order,
33
+ judgment: activity_approval_js_1.ActivityApprovalJudgment.Pending,
34
+ participation,
35
+ transaction,
36
+ creator: user,
37
+ updater: user
38
+ };
39
+ if (item.type == organization_1.OrgMemberTargetType.Role) {
40
+ const roleId = item.value || item.approver?.id;
41
+ if (!roleId) {
42
+ throw new Error(context.t('error.approver role is not set on the approval line', { order }));
43
+ }
44
+ const approverRole = await tx.getRepository(auth_base_1.Role).findOneBy({ id: roleId });
45
+ if (!approverRole) {
46
+ throw new Error(context.t('error.approver role not found', { role: roleId }));
47
+ }
48
+ return await tx.getRepository(activity_approval_js_1.ActivityApproval).save({ ...base, approverRole });
49
+ }
50
+ const approver = await (0, common_js_1.getSystemUserFromOrgMemberItem)(item, context);
51
+ if (!approver) {
52
+ throw new Error(context.t('error.approver is not set on the approval line', { order }));
53
+ }
54
+ return await tx.getRepository(activity_approval_js_1.ActivityApproval).save({ ...base, approver });
55
+ }
56
+ /**
57
+ * 이번에 알려야 할 참조들의 행을 만든다 — **하나가 실패해도 결재를 세우지 않는다.**
58
+ *
59
+ * 참조는 막지 않는 자격이다. 그런데 참조자를 못 찾아 던지면 그 예외가 상신·승인을 통째로 물린다 —
60
+ * 막지 않기로 한 것이 막게 된다.
61
+ *
62
+ * 그래서 실패를 삼키지 않되 **던지지도 않는다.** 못 만든 참조를 이유와 함께 돌려주고, 부르는 쪽이
63
+ * 그것을 기록한다.
64
+ */
65
+ async function createReferences(args, context) {
66
+ const created = [];
67
+ const failed = [];
68
+ for (const { item, order } of args.references) {
69
+ try {
70
+ created.push(await createApproval({ item, order, participation: 'reference', activityThread: args.activityThread, transaction: 'notify' }, context));
71
+ }
72
+ catch (e) {
73
+ failed.push({ order, reason: String(e?.message ?? e) });
74
+ }
75
+ }
76
+ return { created, failed };
77
+ }
78
+ //# sourceMappingURL=create-approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-approval.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/create-approval.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **결재선 한 항목을 실제 행으로 만든다** — 한 자리.\n *\n * ── 왜 뺐나 (2026-09-01) ─────────────────────────────────────────────────\n * 같은 열다섯 줄이 두 파일에 있었다 — `submit.ts`(첫 결재자)와 `approve.ts`(다음 결재자). 역할로 걸린\n * 경우와 사람으로 걸린 경우를 가르고, 못 찾으면 던지고, 행을 만든다.\n *\n * 참조까지 넣으면 **세 벌**이 된다. 그때 한쪽만 고쳐지는 날이 오고, 그러면 어떤 자격의 결재자만\n * 조용히 안 만들어진다 — 오류가 안 나고 결재가 안 걸린다.\n */\nimport { Role } from '@things-factory/auth-base'\nimport type { ApprovalLineItem } from '@things-factory/organization'\nimport { OrgMemberTargetType } from '@things-factory/organization'\n\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport type { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { getSystemUserFromOrgMemberItem } from '../common.js'\n\n/** 이 항목이 무슨 자격으로 서나 — 행에도 적는다(목록 질의가 참조를 걸러야 한다). */\nexport type Participation = 'approval' | 'reference'\n\n/**\n * 결재선 항목 하나로 행을 만든다.\n *\n * 역할로 걸린 것은 `approverRole` 만 채워지고 `approver` 는 비어 있다 — 그 역할을 가진 사람 여럿이 같은\n * 건을 본다. 처리하는 사람이 정해지는 것은 누른 순간이다(§`approve.ts`).\n */\nexport async function createApproval(\n args: {\n item: ApprovalLineItem\n order: number\n participation: Participation\n activityThread: ActivityThread\n /** 이 행이 왜 생겼나 — `submit` · `escalate` · `notify`. */\n transaction: string\n },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n const { item, order, participation, activityThread, transaction } = args\n\n const base = {\n domain,\n activityThread,\n round: activityThread.round,\n order,\n judgment: ActivityApprovalJudgment.Pending,\n participation,\n transaction,\n creator: user,\n updater: user\n }\n\n if (item.type == OrgMemberTargetType.Role) {\n const roleId = item.value || item.approver?.id\n if (!roleId) {\n throw new Error(context.t('error.approver role is not set on the approval line', { order }))\n }\n\n const approverRole = await tx.getRepository(Role).findOneBy({ id: roleId })\n if (!approverRole) {\n throw new Error(context.t('error.approver role not found', { role: roleId }))\n }\n\n return await tx.getRepository(ActivityApproval).save({ ...base, approverRole } as any)\n }\n\n const approver = await getSystemUserFromOrgMemberItem(item, context)\n if (!approver) {\n throw new Error(context.t('error.approver is not set on the approval line', { order }))\n }\n\n return await tx.getRepository(ActivityApproval).save({ ...base, approver } as any)\n}\n\n/**\n * 이번에 알려야 할 참조들의 행을 만든다 — **하나가 실패해도 결재를 세우지 않는다.**\n *\n * 참조는 막지 않는 자격이다. 그런데 참조자를 못 찾아 던지면 그 예외가 상신·승인을 통째로 물린다 —\n * 막지 않기로 한 것이 막게 된다.\n *\n * 그래서 실패를 삼키지 않되 **던지지도 않는다.** 못 만든 참조를 이유와 함께 돌려주고, 부르는 쪽이\n * 그것을 기록한다.\n */\nexport async function createReferences(\n args: {\n references: { item: ApprovalLineItem; order: number }[]\n activityThread: ActivityThread\n },\n context: ResolverContext\n): Promise<{ created: ActivityApproval[]; failed: { order: number; reason: string }[] }> {\n const created: ActivityApproval[] = []\n const failed: { order: number; reason: string }[] = []\n\n for (const { item, order } of args.references) {\n try {\n created.push(\n await createApproval(\n { item, order, participation: 'reference', activityThread: args.activityThread, transaction: 'notify' },\n context\n )\n )\n } catch (e: any) {\n failed.push({ order, reason: String(e?.message ?? e) })\n }\n }\n\n return { created, failed }\n}\n"]}
@@ -1,5 +1,21 @@
1
1
  import { ObjectRef } from '@things-factory/shell';
2
2
  import { ActivityApproval } from '../../service/activity-approval/activity-approval.js';
3
+ /**
4
+ * 결재를 다른 사람에게 넘긴다.
5
+ *
6
+ * ── 넘긴 사람의 행은 끝내고, 받은 사람의 행을 새로 만든다 ──────────────────
7
+ * 두 행이 필요하다. 넘긴 기록이 남아야 「누가 왜 넘겼나」에 답할 수 있고, 받은 사람에게 대기 건이
8
+ * 서야 결재가 이어진다.
9
+ *
10
+ * 앞서는 두 번 저장하면서 **둘 다 같은 행을 덮었다.** `{ ...activityApproval }` 이 `id` 까지 퍼뜨려서
11
+ * TypeORM 이 새로 넣지 않고 고쳤기 때문이다. 그래서 위임하면
12
+ *
13
+ * 받은 사람에게 대기 건이 안 생기고
14
+ * 원본은 `delegated` 로 끝나고
15
+ * 스레드는 `submitted` 인 채로 멈춘다
16
+ *
17
+ * **결재가 죽었다.** 오류는 안 났다.
18
+ */
3
19
  export declare function delegate({ id, to, comment }: {
4
20
  id: string;
5
21
  to: ObjectRef;
@@ -2,8 +2,26 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.delegate = delegate;
4
4
  const auth_base_1 = require("@things-factory/auth-base");
5
+ const opinion_js_1 = require("./opinion.js");
5
6
  const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
6
7
  const check_authority_js_1 = require("./check-authority.js");
8
+ const claim_js_1 = require("./claim.js");
9
+ /**
10
+ * 결재를 다른 사람에게 넘긴다.
11
+ *
12
+ * ── 넘긴 사람의 행은 끝내고, 받은 사람의 행을 새로 만든다 ──────────────────
13
+ * 두 행이 필요하다. 넘긴 기록이 남아야 「누가 왜 넘겼나」에 답할 수 있고, 받은 사람에게 대기 건이
14
+ * 서야 결재가 이어진다.
15
+ *
16
+ * 앞서는 두 번 저장하면서 **둘 다 같은 행을 덮었다.** `{ ...activityApproval }` 이 `id` 까지 퍼뜨려서
17
+ * TypeORM 이 새로 넣지 않고 고쳤기 때문이다. 그래서 위임하면
18
+ *
19
+ * 받은 사람에게 대기 건이 안 생기고
20
+ * 원본은 `delegated` 로 끝나고
21
+ * 스레드는 `submitted` 인 채로 멈춘다
22
+ *
23
+ * **결재가 죽었다.** 오류는 안 났다.
24
+ */
7
25
  async function delegate({ id, to, comment }, context) {
8
26
  const { domain, user, tx } = context.state;
9
27
  var repository = tx.getRepository(activity_approval_js_1.ActivityApproval);
@@ -32,20 +50,38 @@ async function delegate({ id, to, comment }, context) {
32
50
  if (!approver) {
33
51
  throw new Error(context.t(`error.user not found`));
34
52
  }
35
- /* create new delegated approval */
36
- await repository.save({
37
- ...activityApproval,
53
+ /*
54
+ * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야
55
+ * 진 쪽이 다음 결재자를 만들기 전에 물러난다.
56
+ */
57
+ if (!(await (0, claim_js_1.claimPendingApproval)(tx, activityApproval, activity_approval_js_1.ActivityApprovalJudgment.Delegated))) {
58
+ /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */
59
+ throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }));
60
+ }
61
+ /*
62
+ * 받은 사람의 대기 건을 **새로 만든다.**
63
+ *
64
+ * `id` 를 퍼뜨리지 않는다 — 퍼뜨리면 원본을 고치는 것이 되고 새 행이 안 생긴다. 차례(`order`)와
65
+ * 차수(`round`)는 같다. 결재선의 같은 단계이고 사람만 바뀐 것이다.
66
+ */
67
+ await repository.save(repository.create({
68
+ domain: activityApproval.domain,
69
+ activityThread: activityApproval.activityThread,
38
70
  originalApproval: activityApproval,
39
- comment,
71
+ round: activityApproval.round,
72
+ order: activityApproval.order,
73
+ judgment: activity_approval_js_1.ActivityApprovalJudgment.Pending,
74
+ transaction: 'delegate',
75
+ ...(0, opinion_js_1.stampedComment)(comment),
40
76
  approver,
41
77
  creator: user,
42
78
  updater: user
43
- });
44
- /* terminate origin approval */
79
+ }));
80
+ /* 넘긴 사람의 행을 끝낸다. 의견은 `comment` 에 남긴다 — `reason` 은 이 표에 없는 칸이다. */
45
81
  const result = await repository.save({
46
82
  ...activityApproval,
47
83
  judgment: activity_approval_js_1.ActivityApprovalJudgment.Delegated,
48
- reason: comment,
84
+ ...(0, opinion_js_1.stampedComment)(comment),
49
85
  transaction: 'delegate',
50
86
  updater: user,
51
87
  terminator: user,
@@ -1 +1 @@
1
- {"version":3,"file":"delegate.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/delegate.ts"],"names":[],"mappings":";;AAKA,4BAiEC;AAtED,yDAA4D;AAE5D,+FAAiH;AACjH,6DAAqD;AAE9C,KAAK,UAAU,QAAQ,CAC5B,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAkD,EACnE,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE1C,IAAI,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAA;IAEnD,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC9C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QACxC,SAAS,EAAE,CAAC,QAAQ,EAAE,gBAAgB,CAAC;KACxC,CAAC,CAAA;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE;YAC7C,gBAAgB,EAAE,EAAE;SACrB,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM,IAAA,mCAAc,EAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAE/C;;;MAGE;IACF,IAAI,gBAAgB,CAAC,QAAQ,KAAK,+CAAwB,CAAC,OAAO,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,+CAA+C,EAAE;YACzD,gBAAgB,EAAE,EAAE;YACpB,MAAM,EAAE,gBAAgB,CAAC,QAAQ;SAClC,CAAC,CACH,CAAA;IACH,CAAC;IAED,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,gBAAI,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,sBAAU,CAAC,SAAS,EAAE,CAAC,CAAA;IAEpG,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAA;IACpD,CAAC;IAED,mCAAmC;IACnC,MAAM,UAAU,CAAC,IAAI,CAAC;QACpB,GAAG,gBAAgB;QACnB,gBAAgB,EAAE,gBAAgB;QAClC,OAAO;QACP,QAAQ;QACR,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF,+BAA+B;IAC/B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACnC,GAAG,gBAAgB;QACnB,QAAQ,EAAE,+CAAwB,CAAC,SAAS;QAC5C,MAAM,EAAE,OAAO;QACf,WAAW,EAAE,UAAU;QACvB,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,IAAI;QAChB,YAAY,EAAE,IAAI,IAAI,EAAE;KACzB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import { User, UserStatus } from '@things-factory/auth-base'\nimport { ObjectRef } from '@things-factory/shell'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { checkAuthority } from './check-authority.js'\n\nexport async function delegate(\n { id, to, comment }: { id: string; to: ObjectRef; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['domain', 'activityThread']\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Delegation\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityInstance: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n /* TODO user validation .. */\n const approver = await tx.getRepository(User).findOneBy({ id: to.id, status: UserStatus.ACTIVATED })\n\n if (!approver) {\n throw new Error(context.t(`error.user not found`))\n }\n\n /* create new delegated approval */\n await repository.save({\n ...activityApproval,\n originalApproval: activityApproval,\n comment,\n approver,\n creator: user,\n updater: user\n })\n\n /* terminate origin approval */\n const result = await repository.save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Delegated,\n reason: comment,\n transaction: 'delegate',\n updater: user,\n terminator: user,\n terminatedAt: new Date()\n })\n\n return result\n}\n"]}
1
+ {"version":3,"file":"delegate.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/delegate.ts"],"names":[],"mappings":"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","sourcesContent":["import { User, UserStatus } from '@things-factory/auth-base'\nimport { stampedComment } from './opinion.js'\nimport { ObjectRef } from '@things-factory/shell'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { checkAuthority } from './check-authority.js'\nimport { claimPendingApproval } from './claim.js'\n\n/**\n * 결재를 다른 사람에게 넘긴다.\n *\n * ── 넘긴 사람의 행은 끝내고, 받은 사람의 행을 새로 만든다 ──────────────────\n * 두 행이 필요하다. 넘긴 기록이 남아야 「누가 왜 넘겼나」에 답할 수 있고, 받은 사람에게 대기 건이\n * 서야 결재가 이어진다.\n *\n * 앞서는 두 번 저장하면서 **둘 다 같은 행을 덮었다.** `{ ...activityApproval }` 이 `id` 까지 퍼뜨려서\n * TypeORM 이 새로 넣지 않고 고쳤기 때문이다. 그래서 위임하면\n *\n * 받은 사람에게 대기 건이 안 생기고\n * 원본은 `delegated` 로 끝나고\n * 스레드는 `submitted` 인 채로 멈춘다\n *\n * **결재가 죽었다.** 오류는 안 났다.\n */\nexport async function delegate(\n { id, to, comment }: { id: string; to: ObjectRef; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['domain', 'activityThread']\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /*\n Prerequisite of Delegation\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityInstance: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n /* TODO user validation .. */\n const approver = await tx.getRepository(User).findOneBy({ id: to.id, status: UserStatus.ACTIVATED })\n\n if (!approver) {\n throw new Error(context.t(`error.user not found`))\n }\n\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Delegated))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * 받은 사람의 대기 건을 **새로 만든다.**\n *\n * `id` 를 퍼뜨리지 않는다 — 퍼뜨리면 원본을 고치는 것이 되고 새 행이 안 생긴다. 차례(`order`)와\n * 차수(`round`)는 같다. 결재선의 같은 단계이고 사람만 바뀐 것이다.\n */\n await repository.save(\n repository.create({\n domain: activityApproval.domain,\n activityThread: activityApproval.activityThread,\n originalApproval: activityApproval,\n round: activityApproval.round,\n order: activityApproval.order,\n judgment: ActivityApprovalJudgment.Pending,\n transaction: 'delegate',\n ...stampedComment(comment),\n approver,\n creator: user,\n updater: user\n })\n )\n\n /* 넘긴 사람의 행을 끝낸다. 의견은 `comment` 에 남긴다 — `reason` 은 이 표에 없는 칸이다. */\n const result = await repository.save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Delegated,\n ...stampedComment(comment),\n transaction: 'delegate',\n updater: user,\n terminator: user,\n terminatedAt: new Date()\n })\n\n return result\n}\n"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 의견을 지금 말한 것으로 적는다 — **빈 의견에는 시각을 찍지 않는다.**
3
+ *
4
+ * 말하지 않은 것에 시각을 찍으면 「통지받고 말하지 않았다」와 「말했다」가 구별되지 않는다. 침묵은
5
+ * 동의도 반대도 아니고, 그 사실이 화면에 그대로 보여야 한다.
6
+ */
7
+ export declare function stampedComment(comment: string | undefined | null): {
8
+ comment?: string;
9
+ commentedAt?: Date;
10
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /*
3
+ * **의견과 그 시각은 함께 적힌다** — 한 자리에서.
4
+ *
5
+ * ── 왜 시각이 필요한가 (2026-09-02, UX 레인 물음) ──────────────────────────
6
+ * 화면이 「결정이 끝난 뒤에 달린 의견」을 따로 보여 주려 했다. 참조는 결재를 막지 않으므로 늦게 올 수
7
+ * 있고, 그 사실을 표시하지 않으면 사람은 그 의견이 결정에 반영된 줄로 읽는다.
8
+ *
9
+ * 그런데 그 판단에 쓸 값이 없었다. 그래서 `updatedAt > terminatedAt` 으로 추론하려 했는데 **그것은
10
+ * 언제나 참이다.**
11
+ *
12
+ * terminatedAt 닫는 코드가 `new Date()` 로 만든다 — 저장 전
13
+ * updatedAt `@UpdateDateColumn` 이 DB 에 쓰는 순간 찍는다 — 저장 시점
14
+ *
15
+ * 두 값이 몇 밀리초 차이로 늘 어긋나서, 닫힌 참조가 **전부** 「결정 후 의견」으로 보인다. 조용히,
16
+ * 그리고 정확히 반대로.
17
+ *
18
+ * 그래서 사실에 집을 준다. 의견을 언제 말했나는 파생으로 알 수 없다.
19
+ *
20
+ * ── 왜 여섯 자리를 한 함수로 모으나 ────────────────────────────────────────
21
+ * 의견을 적는 곳이 여섯이다(저장·승인·반려·위임 둘·중단). 시각을 각자 찍게 두면 한 곳이 빠지고,
22
+ * 빠진 그 자리의 의견만 **시각 없이** 남는다. 화면은 그것을 「의견 없음」으로 읽는다.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.stampedComment = stampedComment;
26
+ /**
27
+ * 의견을 지금 말한 것으로 적는다 — **빈 의견에는 시각을 찍지 않는다.**
28
+ *
29
+ * 말하지 않은 것에 시각을 찍으면 「통지받고 말하지 않았다」와 「말했다」가 구별되지 않는다. 침묵은
30
+ * 동의도 반대도 아니고, 그 사실이 화면에 그대로 보여야 한다.
31
+ */
32
+ function stampedComment(comment) {
33
+ const said = typeof comment === 'string' ? comment.trim() : '';
34
+ if (!said) {
35
+ /*
36
+ * 빈 값을 그대로 넘긴다 — 앞서 있던 의견을 지우는 것도 사람의 뜻일 수 있다. 다만 시각은 지운다.
37
+ * 그러지 않으면 「의견은 없는데 말한 시각은 있다」가 남는다.
38
+ */
39
+ return { comment: comment ?? undefined, commentedAt: undefined };
40
+ }
41
+ return { comment: said, commentedAt: new Date() };
42
+ }
43
+ //# sourceMappingURL=opinion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opinion.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/opinion.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;AAQH,wCAUC;AAhBD;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,OAAkC;IAC/D,MAAM,IAAI,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IAC9D,IAAI,CAAC,IAAI,EAAE,CAAC;QACV;;;WAGG;QACH,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,CAAA;IAClE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,EAAE,CAAA;AACnD,CAAC","sourcesContent":["/*\n * **의견과 그 시각은 함께 적힌다** — 한 자리에서.\n *\n * ── 왜 시각이 필요한가 (2026-09-02, UX 레인 물음) ──────────────────────────\n * 화면이 「결정이 끝난 뒤에 달린 의견」을 따로 보여 주려 했다. 참조는 결재를 막지 않으므로 늦게 올 수\n * 있고, 그 사실을 표시하지 않으면 사람은 그 의견이 결정에 반영된 줄로 읽는다.\n *\n * 그런데 그 판단에 쓸 값이 없었다. 그래서 `updatedAt > terminatedAt` 으로 추론하려 했는데 **그것은\n * 언제나 참이다.**\n *\n * terminatedAt 닫는 코드가 `new Date()` 로 만든다 — 저장 전\n * updatedAt `@UpdateDateColumn` 이 DB 에 쓰는 순간 찍는다 — 저장 시점\n *\n * 두 값이 몇 밀리초 차이로 늘 어긋나서, 닫힌 참조가 **전부** 「결정 후 의견」으로 보인다. 조용히,\n * 그리고 정확히 반대로.\n *\n * 그래서 사실에 집을 준다. 의견을 언제 말했나는 파생으로 알 수 없다.\n *\n * ── 왜 여섯 자리를 한 함수로 모으나 ────────────────────────────────────────\n * 의견을 적는 곳이 여섯이다(저장·승인·반려·위임 둘·중단). 시각을 각자 찍게 두면 한 곳이 빠지고,\n * 빠진 그 자리의 의견만 **시각 없이** 남는다. 화면은 그것을 「의견 없음」으로 읽는다.\n */\n\n/**\n * 의견을 지금 말한 것으로 적는다 — **빈 의견에는 시각을 찍지 않는다.**\n *\n * 말하지 않은 것에 시각을 찍으면 「통지받고 말하지 않았다」와 「말했다」가 구별되지 않는다. 침묵은\n * 동의도 반대도 아니고, 그 사실이 화면에 그대로 보여야 한다.\n */\nexport function stampedComment(comment: string | undefined | null): { comment?: string; commentedAt?: Date } {\n const said = typeof comment === 'string' ? comment.trim() : ''\n if (!said) {\n /*\n * 빈 값을 그대로 넘긴다 — 앞서 있던 의견을 지우는 것도 사람의 뜻일 수 있다. 다만 시각은 지운다.\n * 그러지 않으면 「의견은 없는데 말한 시각은 있다」가 남는다.\n */\n return { comment: comment ?? undefined, commentedAt: undefined }\n }\n return { comment: said, commentedAt: new Date() }\n}\n"]}
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.reject = reject;
4
4
  const activity_thread_js_1 = require("../../service/activity-thread/activity-thread.js");
5
+ const opinion_js_1 = require("./opinion.js");
5
6
  const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
6
7
  const common_js_1 = require("../common.js");
7
8
  const check_authority_js_1 = require("./check-authority.js");
9
+ const claim_js_1 = require("./claim.js");
8
10
  async function reject({ id, comment }, context) {
9
11
  const { domain, user, tx } = context.state;
10
12
  var repository = tx.getRepository(activity_approval_js_1.ActivityApproval);
@@ -28,11 +30,29 @@ async function reject({ id, comment }, context) {
28
30
  actual: activityApproval.judgment
29
31
  }));
30
32
  }
33
+ /*
34
+ * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야
35
+ * 진 쪽이 다음 결재자를 만들기 전에 물러난다.
36
+ */
37
+ if (!(await (0, claim_js_1.claimPendingApproval)(tx, activityApproval, activity_approval_js_1.ActivityApprovalJudgment.Rejected))) {
38
+ /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */
39
+ throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }));
40
+ }
31
41
  var result = await repository.save({
32
42
  ...activityApproval,
33
43
  judgment: activity_approval_js_1.ActivityApprovalJudgment.Rejected,
34
- comment,
44
+ ...(0, opinion_js_1.stampedComment)(comment),
35
45
  transaction: 'reject',
46
+ /*
47
+ * **누가 처리했는지 남긴다.**
48
+ *
49
+ * 역할로 걸린 결재는 `approver` 가 비어 있고 `approverRole` 만 있다. 처리해도 그 자리가 비어 있으면
50
+ * 두 가지가 함께 무너진다 — 그 역할을 가진 사람들의 **대기 목록에 영원히 남고**(`approver IS NULL`
51
+ * 로 걸리므로), 정작 처리한 사람의 **완료 목록에는 안 나온다**(`approver = 나` 로 거르므로).
52
+ *
53
+ * 그리고 결재 이력에서 「누가 승인했나」는 역할이 아니라 사람이어야 한다.
54
+ */
55
+ approver: activityApproval.approver ?? user,
36
56
  updater: user,
37
57
  terminatedAt: new Date(),
38
58
  terminator: user
@@ -1 +1 @@
1
- {"version":3,"file":"reject.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/reject.ts"],"names":[],"mappings":";;AAKA,wBA6DC;AAlED,yFAAuG;AACvG,+FAAiH;AACjH,4CAA0D;AAC1D,6DAAqD;AAE9C,KAAK,UAAU,MAAM,CAC1B,EAAE,EAAE,EAAE,OAAO,EAAmC,EAChD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE1C,IAAI,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAA;IAEnD,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC9C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QACxC,SAAS,EAAE,CAAC,gBAAgB,CAAC;KAC9B,CAAC,CAAA;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE;YAC7C,gBAAgB,EAAE,EAAE;SACrB,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM,IAAA,mCAAc,EAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAE/C;;;MAGE;IACF,IAAI,gBAAgB,CAAC,QAAQ,KAAK,+CAAwB,CAAC,OAAO,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,+CAA+C,EAAE;YACzD,gBAAgB,EAAE,EAAE;YACpB,MAAM,EAAE,gBAAgB,CAAC,QAAQ;SAClC,CAAC,CACH,CAAA;IACH,CAAC;IAED,IAAI,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACjC,GAAG,gBAAgB;QACnB,QAAQ,EAAE,+CAAwB,CAAC,QAAQ;QAC3C,OAAO;QACP,WAAW,EAAE,QAAQ;QACrB,OAAO,EAAE,IAAI;QACb,YAAY,EAAE,IAAI,IAAI,EAAE;QACxB,UAAU,EAAE,IAAI;KACjB,CAAC,CAAA;IAEF,IAAI,cAAc,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,mCAAc,CAAC,CAAC,IAAI,CAAC;QAC/D,GAAG,gBAAgB,CAAC,cAAc;QAClC,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,OAAO;QACf,KAAK,EAAE,yCAAoB,CAAC,QAAQ;QACpC,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF,MAAM,IAAA,uCAA2B,EAC/B,gBAAgB,CAAC,cAAc,CAAC,kBAAkB,EAClD,EAAE,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE,EAC9B,OAAO,CACR,CAAA;IAED,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { updateActivityInstanceState } from '../common.js'\nimport { checkAuthority } from './check-authority.js'\n\nexport async function reject(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['activityThread']\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Rejection\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityInstance: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n var result = await repository.save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Rejected,\n comment,\n transaction: 'reject',\n updater: user,\n terminatedAt: new Date(),\n terminator: user\n })\n\n var activityThread = await tx.getRepository(ActivityThread).save({\n ...activityApproval.activityThread,\n transaction: 'reject',\n reason: comment,\n state: ActivityThreadStatus.Rejected,\n updater: user\n })\n\n await updateActivityInstanceState(\n activityApproval.activityThread.activityInstanceId,\n { causedBy: [activityThread] },\n context\n )\n\n return result\n}\n"]}
1
+ {"version":3,"file":"reject.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/reject.ts"],"names":[],"mappings":";;AAOA,wBAiFC;AAxFD,yFAAuG;AACvG,6CAA6C;AAC7C,+FAAiH;AACjH,4CAA0D;AAC1D,6DAAqD;AACrD,yCAAiD;AAE1C,KAAK,UAAU,MAAM,CAC1B,EAAE,EAAE,EAAE,OAAO,EAAmC,EAChD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE1C,IAAI,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAA;IAEnD,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC9C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;QACxC,SAAS,EAAE,CAAC,gBAAgB,CAAC;KAC9B,CAAC,CAAA;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE;YAC7C,gBAAgB,EAAE,EAAE;SACrB,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM,IAAA,mCAAc,EAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAE/C;;;MAGE;IACF,IAAI,gBAAgB,CAAC,QAAQ,KAAK,+CAAwB,CAAC,OAAO,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,+CAA+C,EAAE;YACzD,gBAAgB,EAAE,EAAE;YACpB,MAAM,EAAE,gBAAgB,CAAC,QAAQ;SAClC,CAAC,CACH,CAAA;IACH,CAAC;IAGD;;;OAGG;IACH,IAAI,CAAC,CAAC,MAAM,IAAA,+BAAoB,EAAC,EAAE,EAAE,gBAAgB,EAAE,+CAAwB,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAC3F,4DAA4D;QAC5D,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+CAA+C,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IACvG,CAAC;IAED,IAAI,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACjC,GAAG,gBAAgB;QACnB,QAAQ,EAAE,+CAAwB,CAAC,QAAQ;QAC3C,GAAG,IAAA,2BAAc,EAAC,OAAO,CAAC;QAC1B,WAAW,EAAE,QAAQ;QACrB;;;;;;;;WAQG;QACH,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,IAAI,IAAI;QAC3C,OAAO,EAAE,IAAI;QACb,YAAY,EAAE,IAAI,IAAI,EAAE;QACxB,UAAU,EAAE,IAAI;KACjB,CAAC,CAAA;IAEF,IAAI,cAAc,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,mCAAc,CAAC,CAAC,IAAI,CAAC;QAC/D,GAAG,gBAAgB,CAAC,cAAc;QAClC,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,OAAO;QACf,KAAK,EAAE,yCAAoB,CAAC,QAAQ;QACpC,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF,MAAM,IAAA,uCAA2B,EAC/B,gBAAgB,CAAC,cAAc,CAAC,kBAAkB,EAClD,EAAE,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE,EAC9B,OAAO,CACR,CAAA;IAED,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { stampedComment } from './opinion.js'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { updateActivityInstanceState } from '../common.js'\nimport { checkAuthority } from './check-authority.js'\nimport { claimPendingApproval } from './claim.js'\n\nexport async function reject(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['activityThread']\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Rejection\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityInstance: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Rejected))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n var result = await repository.save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Rejected,\n ...stampedComment(comment),\n transaction: 'reject',\n /*\n * **누가 처리했는지 남긴다.**\n *\n * 역할로 걸린 결재는 `approver` 가 비어 있고 `approverRole` 만 있다. 처리해도 그 자리가 비어 있으면\n * 두 가지가 함께 무너진다 — 그 역할을 가진 사람들의 **대기 목록에 영원히 남고**(`approver IS NULL`\n * 로 걸리므로), 정작 처리한 사람의 **완료 목록에는 안 나온다**(`approver = 나` 로 거르므로).\n *\n * 그리고 결재 이력에서 「누가 승인했나」는 역할이 아니라 사람이어야 한다.\n */\n approver: activityApproval.approver ?? user,\n updater: user,\n terminatedAt: new Date(),\n terminator: user\n })\n\n var activityThread = await tx.getRepository(ActivityThread).save({\n ...activityApproval.activityThread,\n transaction: 'reject',\n reason: comment,\n state: ActivityThreadStatus.Rejected,\n updater: user\n })\n\n await updateActivityInstanceState(\n activityApproval.activityThread.activityInstanceId,\n { causedBy: [activityThread] },\n context\n )\n\n return result\n}\n"]}
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.save = save;
4
4
  const shell_1 = require("@things-factory/shell");
5
5
  const check_authority_js_1 = require("./check-authority.js");
6
+ const approval_line_js_1 = require("./approval-line.js");
7
+ const opinion_js_1 = require("./opinion.js");
6
8
  const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
7
9
  const activity_thread_js_1 = require("../../service/activity-thread/activity-thread.js");
8
10
  const common_js_1 = require("../common.js");
@@ -18,12 +20,34 @@ async function save({ id, comment }, context) {
18
20
  }));
19
21
  }
20
22
  await (0, check_authority_js_1.checkAuthority)(activityApproval, context);
23
+ /*
24
+ * ── 결정이 끝난 결재의 의견은 고쳐 쓰지 않는다 (2026-09-02) ────────────────
25
+ * `checkAuthority` 는 **누구인지만** 본다. 그래서 이미 승인·반려한 사람이 자기 행의 의견을 나중에
26
+ * 다시 쓸 수 있었고, 흔적은 `updatedAt` 뿐이었다. 승인 기록을 뒤에서 고쳐 쓰는 것은 규제 산업이
27
+ * 금지하는 바로 그 행위다 — 이 저장소가 옛 판정 표기를 고치지 않고 읽는 자리에서 흡수하는 것과
28
+ * 같은 이유다(§`isApproved`).
29
+ *
30
+ * **참조는 반대다.** 참조가 결재를 막지 않는다는 것의 뜻이 이것이다 — 결정이 끝난 뒤에도 의견을
31
+ * 말할 수 있고, 화면은 그것을 「결정 후 의견」으로 보여 준다. 막으면 참조자가 휴가에서 돌아와
32
+ * 할 수 있는 일이 없다.
33
+ */
34
+ if ((0, approval_line_js_1.isTerminated)(activityApproval.judgment) && !(0, approval_line_js_1.isReferenceStep)(activityApproval)) {
35
+ throw new Error(context.t('error.approval already decided', {
36
+ defaultValue: '이미 결정된 결재의 의견은 고칠 수 없습니다 — 결정 기록은 뒤에서 고쳐 쓰지 않습니다'
37
+ }));
38
+ }
21
39
  const result = await repository.save({
22
40
  ...activityApproval,
23
- comment,
41
+ ...(0, opinion_js_1.stampedComment)(comment),
24
42
  updater: user
25
43
  });
26
- const activityThread = await (0, shell_1.getRepository)(activity_thread_js_1.ActivityThread).findOneBy({
44
+ /*
45
+ * **`tx` 를 넘긴다.** 넘기지 않으면 요청 트랜잭션 밖에서 읽고, 이 트랜잭션이 방금 고친 값을 못 본다.
46
+ * 그 낡은 스레드가 그대로 `causedBy` 로 넘어가 인스턴스 상태 판정에 쓰인다.
47
+ *
48
+ * 지금은 이 함수가 스레드를 안 고쳐서 드러나지 않는다. 드러나지 않는 것과 맞는 것은 다르다.
49
+ */
50
+ const activityThread = await (0, shell_1.getRepository)(activity_thread_js_1.ActivityThread, tx).findOneBy({
27
51
  id: activityApproval.activityThreadId
28
52
  });
29
53
  await (0, common_js_1.updateActivityInstanceState)(activityThread?.activityInstanceId, { causedBy: [activityThread] }, context);
@@ -1 +1 @@
1
- {"version":3,"file":"save.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/save.ts"],"names":[],"mappings":";;AAMA,oBAmCC;AAzCD,iDAAqD;AACrD,6DAAqD;AACrD,+FAAuF;AACvF,yFAAiF;AACjF,4CAA0D;AAEnD,KAAK,UAAU,IAAI,CACxB,EAAE,EAAE,EAAE,OAAO,EAAmC,EAChD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE1C,IAAI,UAAU,GAAG,IAAA,qBAAa,EAAC,uCAAgB,EAAE,EAAE,CAAC,CAAA;IAEpD,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC9C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;KACzC,CAAC,CAAA;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE;YAC7C,gBAAgB,EAAE,EAAE;SACrB,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM,IAAA,mCAAc,EAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAE/C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACnC,GAAG,gBAAgB;QACnB,OAAO;QACP,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF,MAAM,cAAc,GAAG,MAAM,IAAA,qBAAa,EAAC,mCAAc,CAAC,CAAC,SAAS,CAAC;QACnE,EAAE,EAAE,gBAAgB,CAAC,gBAAgB;KACtC,CAAC,CAAA;IAEF,MAAM,IAAA,uCAA2B,EAAC,cAAc,EAAE,kBAAkB,EAAE,EAAE,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAE9G,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import { getRepository } from '@things-factory/shell'\nimport { checkAuthority } from './check-authority.js'\nimport { ActivityApproval } from '../../service/activity-approval/activity-approval.js'\nimport { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { updateActivityInstanceState } from '../common.js'\n\nexport async function save(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = getRepository(ActivityApproval, tx)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n const result = await repository.save({\n ...activityApproval,\n comment,\n updater: user\n })\n\n const activityThread = await getRepository(ActivityThread).findOneBy({\n id: activityApproval.activityThreadId\n })\n\n await updateActivityInstanceState(activityThread?.activityInstanceId, { causedBy: [activityThread] }, context)\n\n return result\n}\n"]}
1
+ {"version":3,"file":"save.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/save.ts"],"names":[],"mappings":";;AAQA,oBA4DC;AApED,iDAAqD;AACrD,6DAAqD;AACrD,yDAAkE;AAClE,6CAA6C;AAC7C,+FAAuF;AACvF,yFAAiF;AACjF,4CAA0D;AAEnD,KAAK,UAAU,IAAI,CACxB,EAAE,EAAE,EAAE,OAAO,EAAmC,EAChD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE1C,IAAI,UAAU,GAAG,IAAA,qBAAa,EAAC,uCAAgB,EAAE,EAAE,CAAC,CAAA;IAEpD,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC9C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;KACzC,CAAC,CAAA;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,mCAAmC,EAAE;YAC7C,gBAAgB,EAAE,EAAE;SACrB,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM,IAAA,mCAAc,EAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAE/C;;;;;;;;;;OAUG;IACH,IAAI,IAAA,+BAAY,EAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAA,kCAAe,EAAC,gBAAuB,CAAC,EAAE,CAAC;QACzF,MAAM,IAAI,KAAK,CACb,OAAO,CAAC,CAAC,CAAC,gCAAgC,EAAE;YAC1C,YAAY,EAAE,kDAAkD;SACjE,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACnC,GAAG,gBAAgB;QACnB,GAAG,IAAA,2BAAc,EAAC,OAAO,CAAC;QAC1B,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF;;;;;OAKG;IACH,MAAM,cAAc,GAAG,MAAM,IAAA,qBAAa,EAAC,mCAAc,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC;QACvE,EAAE,EAAE,gBAAgB,CAAC,gBAAgB;KACtC,CAAC,CAAA;IAEF,MAAM,IAAA,uCAA2B,EAAC,cAAc,EAAE,kBAAkB,EAAE,EAAE,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAE9G,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["import { getRepository } from '@things-factory/shell'\nimport { checkAuthority } from './check-authority.js'\nimport { isReferenceStep, isTerminated } from './approval-line.js'\nimport { stampedComment } from './opinion.js'\nimport { ActivityApproval } from '../../service/activity-approval/activity-approval.js'\nimport { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { updateActivityInstanceState } from '../common.js'\n\nexport async function save(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = getRepository(ActivityApproval, tx)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /*\n * ── 결정이 끝난 결재의 의견은 고쳐 쓰지 않는다 (2026-09-02) ────────────────\n * `checkAuthority` 는 **누구인지만** 본다. 그래서 이미 승인·반려한 사람이 자기 행의 의견을 나중에\n * 다시 쓸 수 있었고, 흔적은 `updatedAt` 뿐이었다. 승인 기록을 뒤에서 고쳐 쓰는 것은 규제 산업이\n * 금지하는 바로 그 행위다 — 이 저장소가 옛 판정 표기를 고치지 않고 읽는 자리에서 흡수하는 것과\n * 같은 이유다(§`isApproved`).\n *\n * **참조는 반대다.** 참조가 결재를 막지 않는다는 것의 뜻이 이것이다 — 결정이 끝난 뒤에도 의견을\n * 말할 수 있고, 화면은 그것을 「결정 후 의견」으로 보여 준다. 막으면 참조자가 휴가에서 돌아와\n * 할 수 있는 일이 없다.\n */\n if (isTerminated(activityApproval.judgment) && !isReferenceStep(activityApproval as any)) {\n throw new Error(\n context.t('error.approval already decided', {\n defaultValue: '이미 결정된 결재의 의견은 고칠 수 없습니다 — 결정 기록은 뒤에서 고쳐 쓰지 않습니다'\n })\n )\n }\n\n const result = await repository.save({\n ...activityApproval,\n ...stampedComment(comment),\n updater: user\n })\n\n /*\n * **`tx` 를 넘긴다.** 넘기지 않으면 요청 트랜잭션 밖에서 읽고, 이 트랜잭션이 방금 고친 값을 못 본다.\n * 그 낡은 스레드가 그대로 `causedBy` 로 넘어가 인스턴스 상태 판정에 쓰인다.\n *\n * 지금은 이 함수가 스레드를 안 고쳐서 드러나지 않는다. 드러나지 않는 것과 맞는 것은 다르다.\n */\n const activityThread = await getRepository(ActivityThread, tx).findOneBy({\n id: activityApproval.activityThreadId\n })\n\n await updateActivityInstanceState(activityThread?.activityInstanceId, { causedBy: [activityThread] }, context)\n\n return result\n}\n"]}