@things-factory/worklist 10.1.18 → 10.1.24

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 (46) hide show
  1. package/dist-client/components/activity-fields.d.ts +20 -0
  2. package/dist-client/components/activity-fields.js +23 -0
  3. package/dist-client/components/activity-fields.js.map +1 -1
  4. package/dist-client/components/decision-slots.d.ts +26 -0
  5. package/dist-client/components/decision-slots.js +24 -0
  6. package/dist-client/components/decision-slots.js.map +1 -1
  7. package/dist-client/pages/activity-approval/activity-approval-page.d.ts +13 -0
  8. package/dist-client/pages/activity-approval/activity-approval-page.js +29 -7
  9. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  10. package/dist-client/pages/activity-instance/activity-instance-search-page.js +2 -1
  11. package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
  12. package/dist-client/pages/activity-instance/activity-instance-view.js +2 -1
  13. package/dist-client/pages/activity-instance/activity-instance-view.js.map +1 -1
  14. package/dist-client/tsconfig.tsbuildinfo +1 -1
  15. package/dist-server/controllers/activity-approval/check-authority.d.ts +17 -0
  16. package/dist-server/controllers/activity-approval/check-authority.js +53 -0
  17. package/dist-server/controllers/activity-approval/check-authority.js.map +1 -1
  18. package/dist-server/controllers/activity-approval/own-request.d.ts +162 -0
  19. package/dist-server/controllers/activity-approval/own-request.js +135 -0
  20. package/dist-server/controllers/activity-approval/own-request.js.map +1 -0
  21. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.d.ts +21 -0
  22. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js +59 -0
  23. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js.map +1 -0
  24. package/dist-server/controllers/activity-installation/activity-installation-controller.js +2 -0
  25. package/dist-server/controllers/activity-installation/activity-installation-controller.js.map +1 -1
  26. package/dist-server/controllers/activity-installation/registry-conflict.d.ts +15 -0
  27. package/dist-server/controllers/activity-installation/registry-conflict.js +26 -0
  28. package/dist-server/controllers/activity-installation/registry-conflict.js.map +1 -1
  29. package/dist-server/controllers/activity-instance/approval-expectation.d.ts +1 -1
  30. package/dist-server/controllers/activity-instance/approval-expectation.js +1 -1
  31. package/dist-server/controllers/activity-instance/approval-expectation.js.map +1 -1
  32. package/dist-server/controllers/activity-instance/issue.js +38 -0
  33. package/dist-server/controllers/activity-instance/issue.js.map +1 -1
  34. package/dist-server/service/activity/activity-decision-key-item-type.d.ts +49 -0
  35. package/dist-server/service/activity/activity-decision-key-item-type.js +9 -0
  36. package/dist-server/service/activity/activity-decision-key-item-type.js.map +1 -1
  37. package/dist-server/service/activity/decision-key-declaration.d.ts +32 -0
  38. package/dist-server/service/activity/decision-key-declaration.js +81 -0
  39. package/dist-server/service/activity/decision-key-declaration.js.map +1 -1
  40. package/dist-server/service/activity-approval/my-pending-approvals.d.ts +16 -4
  41. package/dist-server/service/activity-approval/my-pending-approvals.js +19 -4
  42. package/dist-server/service/activity-approval/my-pending-approvals.js.map +1 -1
  43. package/dist-server/service/activity-summary/activity-summary-query.js +35 -14
  44. package/dist-server/service/activity-summary/activity-summary-query.js.map +1 -1
  45. package/dist-server/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +11 -11
@@ -1,2 +1,19 @@
1
1
  import { ActivityApproval } from '../../service/activity-approval/activity-approval.js';
2
+ /**
3
+ * 이 사람이 이 결재 행을 다룰 수 있나 — approve · reject · abort · delegate · save 가 함께 쓴다.
4
+ *
5
+ * ── 왜 상신자를 여기서 읽나 (ADR-0052 ②) ─────────────────────────────────
6
+ * 상신한 사람은 자기 상신의 결재자가 될 수 없다. 그 답은 `ActivityInstance.issuer` 에 있는데 이 함수는
7
+ * 결재 행만 받는다. 부르는 다섯이 인스턴스를 싣는 모양이 제각각이라 —
8
+ *
9
+ * reject `checkAuthority` **앞**에서 스레드와 인스턴스를 함께 싣는다
10
+ * approve **뒤**에서 싣는다
11
+ * abort 스레드만 싣는다
12
+ * delegate 스레드만 싣는다
13
+ * save 아예 안 싣는다
14
+ *
15
+ * — 부르는 쪽을 맞추는 길은 **지금 다섯만** 맞추는 것이고 구조가 아니다. 여섯째가 생기면 또 어긋나고,
16
+ * 그때 어긋나는 방식은 「막혀야 할 것이 안 막힌다」다. 그래서 이 함수가 자기에게 필요한 것을 자기가
17
+ * 읽는다 — 이미 역할을 보려고 User 를 한 번 읽고 있다.
18
+ */
2
19
  export declare function checkAuthority(activityApproval: ActivityApproval, context: ResolverContext): Promise<void>;
@@ -3,8 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkAuthority = checkAuthority;
4
4
  const shell_1 = require("@things-factory/shell");
5
5
  const auth_base_1 = require("@things-factory/auth-base");
6
+ const activity_thread_js_1 = require("../../service/activity-thread/activity-thread.js");
7
+ const own_request_js_1 = require("./own-request.js");
8
+ /**
9
+ * 이 사람이 이 결재 행을 다룰 수 있나 — approve · reject · abort · delegate · save 가 함께 쓴다.
10
+ *
11
+ * ── 왜 상신자를 여기서 읽나 (ADR-0052 ②) ─────────────────────────────────
12
+ * 상신한 사람은 자기 상신의 결재자가 될 수 없다. 그 답은 `ActivityInstance.issuer` 에 있는데 이 함수는
13
+ * 결재 행만 받는다. 부르는 다섯이 인스턴스를 싣는 모양이 제각각이라 —
14
+ *
15
+ * reject `checkAuthority` **앞**에서 스레드와 인스턴스를 함께 싣는다
16
+ * approve **뒤**에서 싣는다
17
+ * abort 스레드만 싣는다
18
+ * delegate 스레드만 싣는다
19
+ * save 아예 안 싣는다
20
+ *
21
+ * — 부르는 쪽을 맞추는 길은 **지금 다섯만** 맞추는 것이고 구조가 아니다. 여섯째가 생기면 또 어긋나고,
22
+ * 그때 어긋나는 방식은 「막혀야 할 것이 안 막힌다」다. 그래서 이 함수가 자기에게 필요한 것을 자기가
23
+ * 읽는다 — 이미 역할을 보려고 User 를 한 번 읽고 있다.
24
+ */
6
25
  async function checkAuthority(activityApproval, context) {
7
26
  const { user, tx } = context.state;
27
+ await refuseIfTheyRaisedIt(activityApproval, context);
8
28
  if (activityApproval.approverId && activityApproval.approverId === user.id) {
9
29
  return;
10
30
  }
@@ -20,4 +40,37 @@ async function checkAuthority(activityApproval, context) {
20
40
  }
21
41
  throw new Error(context.t(`error.not authorized to handle this task`));
22
42
  }
43
+ /**
44
+ * 상신한 사람이면 거절한다 — **판정하는 자리에서만.**
45
+ *
46
+ * 참조 자리는 막지 않는다. 참조는 판정하지 않으므로 자기가 올린 건에 의견을 남기는 것이 이상하지 않다.
47
+ * `save` 가 이 함수를 지나는 이유가 그 자리다 — 여기서 참조까지 막으면 상신자가 자기 건에 덧붙이는 말이
48
+ * 사라진다.
49
+ *
50
+ * 누구인지 보기 **전에** 선다. 뒤에 두면 역할로 걸린 자리에서 `approver` 를 이 사람으로 채워 놓고 나서
51
+ * 거절하게 되고, 그 한 줄은 트랜잭션이 살아 있는 동안 남는다.
52
+ */
53
+ async function refuseIfTheyRaisedIt(activityApproval, context) {
54
+ const { user, tx } = context.state;
55
+ if (!(0, own_request_js_1.judges)(activityApproval.participation)) {
56
+ return;
57
+ }
58
+ /*
59
+ * 스레드를 타고 인스턴스까지 간다. 행이 없으면 막지 않는다 — 여기는 「없는 것」을 판정하는 자리가
60
+ * 아니고, 없다는 사실은 부르는 쪽이 자기 문장으로 말한다.
61
+ */
62
+ const activityThread = await (0, shell_1.getRepository)(activity_thread_js_1.ActivityThread, tx).findOne({
63
+ where: { id: activityApproval.activityThreadId },
64
+ relations: ['activityInstance']
65
+ });
66
+ /* 판단은 `own-request.ts` 가 한다 — 이 파일은 시험이 못 들어서 값을 가져오는 일만 맡는다. */
67
+ if ((0, own_request_js_1.refuseToJudge)({
68
+ participation: activityApproval.participation,
69
+ issuerId: activityThread?.activityInstance?.issuerId,
70
+ userId: user.id
71
+ })) {
72
+ /* 사람에게는 문장을, 붙잡는 쪽에는 `code` 를 — 커널의 같은 문이 같은 낱말을 던진다. */
73
+ throw (0, own_request_js_1.selfApprovalRefused)(context.t(own_request_js_1.OWN_REQUEST_REFUSAL_KEY));
74
+ }
75
+ }
23
76
  //# sourceMappingURL=check-authority.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"check-authority.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/check-authority.ts"],"names":[],"mappings":";;AAIA,wCAsBC;AA1BD,iDAAqD;AACrD,yDAAgD;AAGzC,KAAK,UAAU,cAAc,CAAC,gBAAkC,EAAE,OAAwB;IAC/F,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElC,IAAI,gBAAgB,CAAC,UAAU,IAAI,gBAAgB,CAAC,UAAU,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3E,OAAM;IACR,CAAC;IAED,IAAI,CAAC,gBAAgB,CAAC,QAAQ,IAAI,gBAAgB,CAAC,cAAc,EAAE,CAAC;QAClE,MAAM,KAAK,GAAG,CACZ,MAAM,IAAA,qBAAa,EAAC,gBAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;YACpC,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;YACtB,SAAS,EAAE,CAAC,OAAO,CAAC;SACrB,CAAC,CACH,EAAE,KAAK,CAAA;QAER,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,gBAAgB,CAAC,cAAc,CAAC,EAAE,CAAC;YACnE,gBAAgB,CAAC,QAAQ,GAAG,IAAI,CAAA;YAChC,OAAM;QACR,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAA;AACxE,CAAC","sourcesContent":["import { getRepository } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { ActivityApproval } from '../../service/activity-approval/activity-approval.js'\n\nexport async function checkAuthority(activityApproval: ActivityApproval, context: ResolverContext) {\n const { user, tx } = context.state\n\n if (activityApproval.approverId && activityApproval.approverId === user.id) {\n return\n }\n\n if (!activityApproval.approver && activityApproval.approverRoleId) {\n const roles = (\n await getRepository(User, tx).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n )?.roles\n\n if (roles.find(role => role.id == activityApproval.approverRoleId)) {\n activityApproval.approver = user\n return\n }\n }\n\n throw new Error(context.t(`error.not authorized to handle this task`))\n}\n"]}
1
+ {"version":3,"file":"check-authority.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/check-authority.ts"],"names":[],"mappings":";;AAuBA,wCAwBC;AA/CD,iDAAqD;AACrD,yDAAgD;AAEhD,yFAAiF;AACjF,qDAAsG;AAEtG;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,cAAc,CAAC,gBAAkC,EAAE,OAAwB;IAC/F,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElC,MAAM,oBAAoB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAErD,IAAI,gBAAgB,CAAC,UAAU,IAAI,gBAAgB,CAAC,UAAU,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3E,OAAM;IACR,CAAC;IAED,IAAI,CAAC,gBAAgB,CAAC,QAAQ,IAAI,gBAAgB,CAAC,cAAc,EAAE,CAAC;QAClE,MAAM,KAAK,GAAG,CACZ,MAAM,IAAA,qBAAa,EAAC,gBAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;YACpC,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;YACtB,SAAS,EAAE,CAAC,OAAO,CAAC;SACrB,CAAC,CACH,EAAE,KAAK,CAAA;QAER,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,gBAAgB,CAAC,cAAc,CAAC,EAAE,CAAC;YACnE,gBAAgB,CAAC,QAAQ,GAAG,IAAI,CAAA;YAChC,OAAM;QACR,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAA;AACxE,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,oBAAoB,CAAC,gBAAkC,EAAE,OAAwB;IAC9F,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElC,IAAI,CAAC,IAAA,uBAAM,EAAC,gBAAgB,CAAC,aAAa,CAAC,EAAE,CAAC;QAC5C,OAAM;IACR,CAAC;IAED;;;OAGG;IACH,MAAM,cAAc,GAAG,MAAM,IAAA,qBAAa,EAAC,mCAAc,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC;QACrE,KAAK,EAAE,EAAE,EAAE,EAAE,gBAAgB,CAAC,gBAAgB,EAAE;QAChD,SAAS,EAAE,CAAC,kBAAkB,CAAC;KAChC,CAAC,CAAA;IAEF,iEAAiE;IACjE,IACE,IAAA,8BAAa,EAAC;QACZ,aAAa,EAAE,gBAAgB,CAAC,aAAa;QAC7C,QAAQ,EAAE,cAAc,EAAE,gBAAgB,EAAE,QAAQ;QACpD,MAAM,EAAE,IAAI,CAAC,EAAE;KAChB,CAAC,EACF,CAAC;QACD,yDAAyD;QACzD,MAAM,IAAA,oCAAmB,EAAC,OAAO,CAAC,CAAC,CAAC,wCAAuB,CAAC,CAAC,CAAA;IAC/D,CAAC;AACH,CAAC","sourcesContent":["import { getRepository } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { ActivityApproval } from '../../service/activity-approval/activity-approval.js'\nimport { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { OWN_REQUEST_REFUSAL_KEY, judges, refuseToJudge, selfApprovalRefused } from './own-request.js'\n\n/**\n * 이 사람이 이 결재 행을 다룰 수 있나 — approve · reject · abort · delegate · save 가 함께 쓴다.\n *\n * ── 왜 상신자를 여기서 읽나 (ADR-0052 ②) ─────────────────────────────────\n * 상신한 사람은 자기 상신의 결재자가 될 수 없다. 그 답은 `ActivityInstance.issuer` 에 있는데 이 함수는\n * 결재 행만 받는다. 부르는 다섯이 인스턴스를 싣는 모양이 제각각이라 —\n *\n * reject `checkAuthority` **앞**에서 스레드와 인스턴스를 함께 싣는다\n * approve **뒤**에서 싣는다\n * abort 스레드만 싣는다\n * delegate 스레드만 싣는다\n * save 아예 안 싣는다\n *\n * — 부르는 쪽을 맞추는 길은 **지금 다섯만** 맞추는 것이고 구조가 아니다. 여섯째가 생기면 또 어긋나고,\n * 그때 어긋나는 방식은 「막혀야 할 것이 안 막힌다」다. 그래서 이 함수가 자기에게 필요한 것을 자기가\n * 읽는다 — 이미 역할을 보려고 User 를 한 번 읽고 있다.\n */\nexport async function checkAuthority(activityApproval: ActivityApproval, context: ResolverContext) {\n const { user, tx } = context.state\n\n await refuseIfTheyRaisedIt(activityApproval, context)\n\n if (activityApproval.approverId && activityApproval.approverId === user.id) {\n return\n }\n\n if (!activityApproval.approver && activityApproval.approverRoleId) {\n const roles = (\n await getRepository(User, tx).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n )?.roles\n\n if (roles.find(role => role.id == activityApproval.approverRoleId)) {\n activityApproval.approver = user\n return\n }\n }\n\n throw new Error(context.t(`error.not authorized to handle this task`))\n}\n\n/**\n * 상신한 사람이면 거절한다 — **판정하는 자리에서만.**\n *\n * 참조 자리는 막지 않는다. 참조는 판정하지 않으므로 자기가 올린 건에 의견을 남기는 것이 이상하지 않다.\n * `save` 가 이 함수를 지나는 이유가 그 자리다 — 여기서 참조까지 막으면 상신자가 자기 건에 덧붙이는 말이\n * 사라진다.\n *\n * 누구인지 보기 **전에** 선다. 뒤에 두면 역할로 걸린 자리에서 `approver` 를 이 사람으로 채워 놓고 나서\n * 거절하게 되고, 그 한 줄은 트랜잭션이 살아 있는 동안 남는다.\n */\nasync function refuseIfTheyRaisedIt(activityApproval: ActivityApproval, context: ResolverContext) {\n const { user, tx } = context.state\n\n if (!judges(activityApproval.participation)) {\n return\n }\n\n /*\n * 스레드를 타고 인스턴스까지 간다. 행이 없으면 막지 않는다 — 여기는 「없는 것」을 판정하는 자리가\n * 아니고, 없다는 사실은 부르는 쪽이 자기 문장으로 말한다.\n */\n const activityThread = await getRepository(ActivityThread, tx).findOne({\n where: { id: activityApproval.activityThreadId },\n relations: ['activityInstance']\n })\n\n /* 판단은 `own-request.ts` 가 한다 — 이 파일은 시험이 못 들어서 값을 가져오는 일만 맡는다. */\n if (\n refuseToJudge({\n participation: activityApproval.participation,\n issuerId: activityThread?.activityInstance?.issuerId,\n userId: user.id\n })\n ) {\n /* 사람에게는 문장을, 붙잡는 쪽에는 `code` 를 — 커널의 같은 문이 같은 낱말을 던진다. */\n throw selfApprovalRefused(context.t(OWN_REQUEST_REFUSAL_KEY))\n }\n}\n"]}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * 상신한 사람은 자기 상신의 결재자가 될 수 없다 — **한 물음, 두 자리.**
3
+ *
4
+ * ── 왜 규칙이 필요한가 (ADR-0052, 2026-09-16) ─────────────────────────────
5
+ * plant 의 「정비 완료 확인」은 상신에 `maintenance:mutation` 이 필요하고 결재선이 `MAINTENANCE-LEAD`
6
+ * 한 단계인데, 그 권한을 보전팀장과 정비원이 **둘 다** 든다. 정비원이 끝내고 팀장이 확인하면 두 사람이지만,
7
+ * **팀장이 직접 고치고 자기가 확인하면 한 사람**이다. 결재선을 고쳐서는 못 막는다 — 팀장을 빼면 정비가
8
+ * 끝났는지 아는 유일한 자리가 사라진다. 그날 그 사람이 둘 다인 것이 문제라 실행 시점에 막는다.
9
+ *
10
+ * ── 왜 `creator` 가 아니라 `issuer` 인가 ──────────────────────────────────
11
+ * 두 칸이 갈리는 길이 있다. `issue()` 는 `creator: user` 를 먼저 적고 `...origin` 으로 draft 를 펼치는데,
12
+ * `origin` 이 `creator` 를 관계로 싣고 있어 **덮인다.** `issuer: user` 는 그 뒤라 안 덮인다. 그리고
13
+ * `assertMayWorkOnDraft` 가 매니저에게 남의 draft 상신을 허용하므로, 매니저가 정비원의 draft 를 올리면
14
+ * creator 는 정비원이고 issuer 는 매니저다.
15
+ *
16
+ * 그때 `creator` 로 막으면 **올리지 않은 사람을 막고, 실제로 올린 사람은 자기 상신을 결재한다** — 규칙이
17
+ * 정확히 거꾸로 선다. 상신은 올리는 행위이고 그 판단에 책임지는 사람이 `issuer` 다. 직접 상신이면 둘이
18
+ * 같으므로 흔한 경우는 달라지지 않는다. (2026-09-16, 아키텍트 판정)
19
+ *
20
+ * ── 참조는 막지 않는다 ───────────────────────────────────────────────────
21
+ * 규칙이 막는 것은 **결재자** 자리다. 참조는 판정하지 않으므로 자기가 올린 건에 의견을 남기는 것이 이상하지
22
+ * 않다 — 오히려 상신한 사람이 덧붙이는 말이 결재자에게 쓸모 있다.
23
+ *
24
+ * ── 두 자리가 같은 것을 봐야 한다 ────────────────────────────────────────
25
+ * 누르는 자리(`checkAuthority`)와 목록에 담는 자리(`myPendingApprovalsWhere`)가 갈리면 **목록에 떴는데
26
+ * 누르면 거절된다.** 그것이 결함이다. 한쪽은 JS 로 묻고 다른 쪽은 SQL 로 물어야 해서 코드를 그대로 나눌
27
+ * 수는 없으므로, 두 표현을 이 파일에 나란히 두고 시험이 둘이 같은 칸을 본다는 것을 센다.
28
+ */
29
+ /** 이 사람이 이 건을 올렸나. 상신자가 없는 건(`issuer` 가 빈 옛 행)은 아무도 막지 않는다. */
30
+ export declare function raisedIt(issuerId: string | undefined | null, userId: string | undefined | null): boolean;
31
+ /** 이 자리가 판정하는 자리인가 — 빈 값은 결재다(`ApprovalLineItem.participation` 과 같은 규약). */
32
+ export declare function judges(participation: string | undefined | null): boolean;
33
+ /**
34
+ * 거절하나 — **판단은 이 함수가 한다.**
35
+ *
36
+ * `checkAuthority` 안에 두면 시험이 못 든다. 그 파일은 엔티티를 끌어오고 엔티티는 type-graphql 을
37
+ * 끌어와서, 시험 러너가 파일을 여는 순간 죽는다. 그래서 거기에 남겨 두면 **원문을 읽는 시험밖에** 못
38
+ * 쓰게 되고, 원문 시험은 「그 글자가 있나」를 셀 뿐 「그래서 거절하나」를 못 센다.
39
+ *
40
+ * 값만 받는 함수로 빼면 네 갈래를 다 돌려 볼 수 있다 — 결재 자리 · 참조 자리 × 상신자 · 아닌 사람.
41
+ * 부르는 쪽에 남는 것은 값을 **가져오는** 일뿐이고, 그건 원문으로 확인해도 되는 종류다.
42
+ */
43
+ export declare function refuseToJudge({ participation, issuerId, userId }: {
44
+ participation?: string | null;
45
+ issuerId?: string | null;
46
+ userId?: string | null;
47
+ }): boolean;
48
+ /**
49
+ * 같은 물음의 SQL — **`ai`(ActivityInstance)를 조인한 질의에서만** 쓴다.
50
+ *
51
+ * `ai.issuer != :userId` 만 쓰면 상신자가 빈 행이 SQL 의 NULL 셈법으로 조용히 빠진다 — 참이 아니라
52
+ * 알 수 없음이 되고, 그 행들이 아무의 결재함에도 안 뜬다. 그래서 빈 것을 먼저 통과시킨다.
53
+ *
54
+ * 참조 자리는 이 조각이 다루지 않는다. 목록 질의는 참조를 이미 앞에서 걸러 낸다.
55
+ */
56
+ export declare const NOT_RAISED_BY_ME_SQL = "(ai.issuer IS NULL OR ai.issuer != :userId)";
57
+ /**
58
+ * 상신한 사람이 결재하려 했다 — **커널이 쓰는 낱말과 같은 값이어야 한다.**
59
+ *
60
+ * `headless-twin/server/service/actuation/command-dispatcher.ts` 의 `SELF_APPROVAL` 과 짝이다.
61
+ * **두 벌이 아니라 두 문이다** — 커널은 업무 목록을 안 지나는 길(API · AI 발의)에서 자기 커맨드를
62
+ * 지키고, 여기는 결재 문을 지킨다. 커널↔호스트 경계를 넘어 합치지 않는다.
63
+ *
64
+ * 두 문이 위험한 것은 **서로 다른 답을 낼 때**다. 그래서 문장이 아니라 코드를 맞춘다 — 문장은 번역도
65
+ * 집계도 안 되고, 고치는 날 두 문이 다른 말을 한다. 상수를 서로 import 하면 두 패키지가 묶이므로,
66
+ * **양쪽이 각자 글자를 시험에 못 박는다.** 그래야 움직인 쪽이 빨개진다. (ADR-0052 ③)
67
+ *
68
+ * ⚠ 아래 `only-the-issuer-could-judge` 와 **다른 사실**이다. 이쪽은 「결재하려 한다」이고 그쪽은
69
+ * 「설 사람이 상신자뿐이라 못 올린다」다. 글자 모양이 눈에 띄게 다른 것도 일부러다.
70
+ */
71
+ export declare const SELF_APPROVAL = "self-approval";
72
+ /** 거절할 때 쓰는 키 — 무엇을 해야 하는지까지 문구가 말한다. */
73
+ export declare const OWN_REQUEST_REFUSAL_KEY = "error.activity-approval cannot be judged by the person who raised it";
74
+ /** 사람에게는 문장을, 붙잡는 쪽에는 `code` 를 준다 — 커널이 같은 모양으로 던진다. */
75
+ export declare function selfApprovalRefused(said: string): Error & {
76
+ code: string;
77
+ };
78
+ /**
79
+ * ═══════════════════════════════════════════════════════════════════════════
80
+ * 같은 규칙을 **상신하는 순간**에 본다 — 누르는 순간에 거절만 하면 멈춘다.
81
+ * ═══════════════════════════════════════════════════════════════════════════
82
+ *
83
+ * 위의 `refuseToJudge` 는 자기 상신을 결재하려는 사람을 막는다. 그것만 세우면 **결재선에 그 사람밖에
84
+ * 없는 건이 아무에게도 안 간다.** 정비 완료 확인이 그 모양이면 설비가 정비중으로 남고 그 라인이 선다.
85
+ * 시드 주석이 걱정한 것이 이것이다.
86
+ *
87
+ * 그래서 올리는 자리에서 먼저 본다. 올릴 때 막으면 **고칠 수 있는 사람이 보는 자리**에서 걸리고,
88
+ * 무엇을 고쳐야 하는지 그 자리에서 말할 수 있다. 결재 중에 걸리면 이미 설비가 멈춘 뒤다.
89
+ * (2026-09-16, ADR-0052 ② · 아키텍트 판정 (다))
90
+ *
91
+ * ── 왜 「한 자리라도」가 아니라 「모든 자리」인가 ──────────────────────────
92
+ * 결재선의 결재 자리는 차례로 **전부** 판정을 받아야 끝난다. 한 자리만 막혀도 그 건은 거기서 영영
93
+ * 선다. 그래서 자리마다 본다.
94
+ *
95
+ * ── 아무도 못 서는 자리는 **같은 관문에서 다른 낱말로** 거절한다 ──────────
96
+ * 그 역할을 가진 사람이 **하나도 없는** 자리도 똑같이 건을 멈춘다. 그런데 상신자 탓이 아니므로,
97
+ * 같은 사유로 묶으면 「당신이 올려서 막혔다」는 **틀린 이유**를 대게 된다.
98
+ *
99
+ * 자리가 같다고 사유를 합치지 않는다 — **관리자가 할 일이 다르다.** 하나는 「그 자리에 다른 결재자를」
100
+ * 이고 하나는 「그 역할에 사람을」이다. 낱말은 이미 있다: `approval-role-empty`(ADR-0047 ②).
101
+ * (2026-09-16, 아키텍트 판정)
102
+ *
103
+ * 사람 자리가 **아무에게도 안 풀리는** 경우도 같은 자리에서 세 번째 낱말로 거절한다 — 없어진 직원을
104
+ * 가리키는 자리다. 역할 문제가 아니므로 `approval-role-empty` 로 부르면 그것도 틀린 이유다.
105
+ * 세 낱말이 자리를 선언하는 세 방법을 그대로 따른다:
106
+ *
107
+ * approval-line-missing 결재선 자체가 없다 (§`approval-expectation.ts`)
108
+ * approval-role-empty 지목한 역할에 사람이 0명
109
+ * approval-person-missing 지목한 사람 자리가 안 풀린다
110
+ *
111
+ * 전에는 `submit` 이 `error.approver is not set on the approval line` 로 잡았는데, 그때는 이미 올라간
112
+ * 뒤다. (2026-09-16, 아키텍트 판정)
113
+ */
114
+ /** 결재 자리 하나와, 거기 설 수 있는 사람들. 역할 자리면 그 역할을 가진 사람 전부. */
115
+ export type JudgeableSeat = {
116
+ /** 사람이 세는 차례 — 1차 결재, 2차 결재. 거절 문구가 이 수를 말한다. */
117
+ order: number;
118
+ /** 역할로 건 자리인가 사람으로 건 자리인가 — **거절 사유가 갈린다.** */
119
+ kind: 'role' | 'person';
120
+ /** 그 자리에 설 수 있는 사람들의 id. 역할 자리면 그 역할을 가진 사람 전부. */
121
+ couldJudge: string[];
122
+ /** 화면에 낼 자리 이름 — 역할 이름이나 사람 이름. */
123
+ name?: string;
124
+ /**
125
+ * 그 자리가 **가리키던 식별자.** 안 풀리는 자리를 고치려면 「누구 자리였는지」를 알아야 한다 —
126
+ * 이름은 못 찾으면 비는데 가리키던 값은 결재선에 그대로 남아 있다.
127
+ */
128
+ pointsAt?: string;
129
+ };
130
+ /** 거절 사유 하나와, 그 사유로 막힌 자리들. */
131
+ export type SeatRefusal = {
132
+ because: 'only-the-issuer-could-judge' | 'approval-role-empty' | 'approval-person-missing';
133
+ seats: {
134
+ order: number;
135
+ name?: string;
136
+ pointsAt?: string;
137
+ }[];
138
+ };
139
+ export type StandingVerdict = {
140
+ ok: true;
141
+ } | {
142
+ ok: false;
143
+ refusals: SeatRefusal[];
144
+ };
145
+ /**
146
+ * 이 결재선을 이 사람이 올려도 되나.
147
+ *
148
+ * 사유가 둘이면 **둘 다 돌려준다.** 하나씩 말하면 관리자가 하나 고치고 다시 걸린다 — 자리를 전부
149
+ * 말하는 것과 같은 이유다.
150
+ */
151
+ export declare function judgeSomeoneElseCanJudge(input: {
152
+ issuerId?: string | null;
153
+ /** **결재 자리만.** 참조는 판정하지 않으므로 여기 오지 않는다. */
154
+ seats: JudgeableSeat[];
155
+ }): StandingVerdict;
156
+ export declare function standingBroken(verdict: StandingVerdict): verdict is Extract<StandingVerdict, {
157
+ ok: false;
158
+ }>;
159
+ /** 사유마다 문구가 다르다 — 관리자가 할 일이 다르기 때문이다. */
160
+ export declare const STANDING_REFUSAL_KEYS: {
161
+ [because in SeatRefusal['because']]: string;
162
+ };
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * 상신한 사람은 자기 상신의 결재자가 될 수 없다 — **한 물음, 두 자리.**
4
+ *
5
+ * ── 왜 규칙이 필요한가 (ADR-0052, 2026-09-16) ─────────────────────────────
6
+ * plant 의 「정비 완료 확인」은 상신에 `maintenance:mutation` 이 필요하고 결재선이 `MAINTENANCE-LEAD`
7
+ * 한 단계인데, 그 권한을 보전팀장과 정비원이 **둘 다** 든다. 정비원이 끝내고 팀장이 확인하면 두 사람이지만,
8
+ * **팀장이 직접 고치고 자기가 확인하면 한 사람**이다. 결재선을 고쳐서는 못 막는다 — 팀장을 빼면 정비가
9
+ * 끝났는지 아는 유일한 자리가 사라진다. 그날 그 사람이 둘 다인 것이 문제라 실행 시점에 막는다.
10
+ *
11
+ * ── 왜 `creator` 가 아니라 `issuer` 인가 ──────────────────────────────────
12
+ * 두 칸이 갈리는 길이 있다. `issue()` 는 `creator: user` 를 먼저 적고 `...origin` 으로 draft 를 펼치는데,
13
+ * `origin` 이 `creator` 를 관계로 싣고 있어 **덮인다.** `issuer: user` 는 그 뒤라 안 덮인다. 그리고
14
+ * `assertMayWorkOnDraft` 가 매니저에게 남의 draft 상신을 허용하므로, 매니저가 정비원의 draft 를 올리면
15
+ * creator 는 정비원이고 issuer 는 매니저다.
16
+ *
17
+ * 그때 `creator` 로 막으면 **올리지 않은 사람을 막고, 실제로 올린 사람은 자기 상신을 결재한다** — 규칙이
18
+ * 정확히 거꾸로 선다. 상신은 올리는 행위이고 그 판단에 책임지는 사람이 `issuer` 다. 직접 상신이면 둘이
19
+ * 같으므로 흔한 경우는 달라지지 않는다. (2026-09-16, 아키텍트 판정)
20
+ *
21
+ * ── 참조는 막지 않는다 ───────────────────────────────────────────────────
22
+ * 규칙이 막는 것은 **결재자** 자리다. 참조는 판정하지 않으므로 자기가 올린 건에 의견을 남기는 것이 이상하지
23
+ * 않다 — 오히려 상신한 사람이 덧붙이는 말이 결재자에게 쓸모 있다.
24
+ *
25
+ * ── 두 자리가 같은 것을 봐야 한다 ────────────────────────────────────────
26
+ * 누르는 자리(`checkAuthority`)와 목록에 담는 자리(`myPendingApprovalsWhere`)가 갈리면 **목록에 떴는데
27
+ * 누르면 거절된다.** 그것이 결함이다. 한쪽은 JS 로 묻고 다른 쪽은 SQL 로 물어야 해서 코드를 그대로 나눌
28
+ * 수는 없으므로, 두 표현을 이 파일에 나란히 두고 시험이 둘이 같은 칸을 본다는 것을 센다.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.STANDING_REFUSAL_KEYS = exports.OWN_REQUEST_REFUSAL_KEY = exports.SELF_APPROVAL = exports.NOT_RAISED_BY_ME_SQL = void 0;
32
+ exports.raisedIt = raisedIt;
33
+ exports.judges = judges;
34
+ exports.refuseToJudge = refuseToJudge;
35
+ exports.selfApprovalRefused = selfApprovalRefused;
36
+ exports.judgeSomeoneElseCanJudge = judgeSomeoneElseCanJudge;
37
+ exports.standingBroken = standingBroken;
38
+ /** 이 사람이 이 건을 올렸나. 상신자가 없는 건(`issuer` 가 빈 옛 행)은 아무도 막지 않는다. */
39
+ function raisedIt(issuerId, userId) {
40
+ return !!issuerId && !!userId && issuerId === userId;
41
+ }
42
+ /** 이 자리가 판정하는 자리인가 — 빈 값은 결재다(`ApprovalLineItem.participation` 과 같은 규약). */
43
+ function judges(participation) {
44
+ return participation !== 'reference';
45
+ }
46
+ /**
47
+ * 거절하나 — **판단은 이 함수가 한다.**
48
+ *
49
+ * `checkAuthority` 안에 두면 시험이 못 든다. 그 파일은 엔티티를 끌어오고 엔티티는 type-graphql 을
50
+ * 끌어와서, 시험 러너가 파일을 여는 순간 죽는다. 그래서 거기에 남겨 두면 **원문을 읽는 시험밖에** 못
51
+ * 쓰게 되고, 원문 시험은 「그 글자가 있나」를 셀 뿐 「그래서 거절하나」를 못 센다.
52
+ *
53
+ * 값만 받는 함수로 빼면 네 갈래를 다 돌려 볼 수 있다 — 결재 자리 · 참조 자리 × 상신자 · 아닌 사람.
54
+ * 부르는 쪽에 남는 것은 값을 **가져오는** 일뿐이고, 그건 원문으로 확인해도 되는 종류다.
55
+ */
56
+ function refuseToJudge({ participation, issuerId, userId }) {
57
+ return judges(participation) && raisedIt(issuerId, userId);
58
+ }
59
+ /**
60
+ * 같은 물음의 SQL — **`ai`(ActivityInstance)를 조인한 질의에서만** 쓴다.
61
+ *
62
+ * `ai.issuer != :userId` 만 쓰면 상신자가 빈 행이 SQL 의 NULL 셈법으로 조용히 빠진다 — 참이 아니라
63
+ * 알 수 없음이 되고, 그 행들이 아무의 결재함에도 안 뜬다. 그래서 빈 것을 먼저 통과시킨다.
64
+ *
65
+ * 참조 자리는 이 조각이 다루지 않는다. 목록 질의는 참조를 이미 앞에서 걸러 낸다.
66
+ */
67
+ exports.NOT_RAISED_BY_ME_SQL = '(ai.issuer IS NULL OR ai.issuer != :userId)';
68
+ /**
69
+ * 상신한 사람이 결재하려 했다 — **커널이 쓰는 낱말과 같은 값이어야 한다.**
70
+ *
71
+ * `headless-twin/server/service/actuation/command-dispatcher.ts` 의 `SELF_APPROVAL` 과 짝이다.
72
+ * **두 벌이 아니라 두 문이다** — 커널은 업무 목록을 안 지나는 길(API · AI 발의)에서 자기 커맨드를
73
+ * 지키고, 여기는 결재 문을 지킨다. 커널↔호스트 경계를 넘어 합치지 않는다.
74
+ *
75
+ * 두 문이 위험한 것은 **서로 다른 답을 낼 때**다. 그래서 문장이 아니라 코드를 맞춘다 — 문장은 번역도
76
+ * 집계도 안 되고, 고치는 날 두 문이 다른 말을 한다. 상수를 서로 import 하면 두 패키지가 묶이므로,
77
+ * **양쪽이 각자 글자를 시험에 못 박는다.** 그래야 움직인 쪽이 빨개진다. (ADR-0052 ③)
78
+ *
79
+ * ⚠ 아래 `only-the-issuer-could-judge` 와 **다른 사실**이다. 이쪽은 「결재하려 한다」이고 그쪽은
80
+ * 「설 사람이 상신자뿐이라 못 올린다」다. 글자 모양이 눈에 띄게 다른 것도 일부러다.
81
+ */
82
+ exports.SELF_APPROVAL = 'self-approval';
83
+ /** 거절할 때 쓰는 키 — 무엇을 해야 하는지까지 문구가 말한다. */
84
+ exports.OWN_REQUEST_REFUSAL_KEY = 'error.activity-approval cannot be judged by the person who raised it';
85
+ /** 사람에게는 문장을, 붙잡는 쪽에는 `code` 를 준다 — 커널이 같은 모양으로 던진다. */
86
+ function selfApprovalRefused(said) {
87
+ const error = new Error(said);
88
+ error.code = exports.SELF_APPROVAL;
89
+ return error;
90
+ }
91
+ /**
92
+ * 이 결재선을 이 사람이 올려도 되나.
93
+ *
94
+ * 사유가 둘이면 **둘 다 돌려준다.** 하나씩 말하면 관리자가 하나 고치고 다시 걸린다 — 자리를 전부
95
+ * 말하는 것과 같은 이유다.
96
+ */
97
+ function judgeSomeoneElseCanJudge(input) {
98
+ const { issuerId, seats } = input;
99
+ const named = (of) => of.map(({ order, name, pointsAt }) => ({ order, name, pointsAt }));
100
+ /* 역할을 지목했는데 그 역할에 사람이 없다 — 상신자가 누구든 이 자리는 아무도 못 선다. */
101
+ const roleEmpty = seats.filter(seat => seat.kind === 'role' && seat.couldJudge.length === 0);
102
+ /*
103
+ * 설 수 있는 사람이 있는데 그게 상신자뿐이다. 상신자를 모르면 이 사유는 성립하지 않는다 —
104
+ * 「없음」을 「그 사람」으로 읽지 않는다.
105
+ */
106
+ const onlyIssuer = issuerId
107
+ ? seats.filter(seat => seat.couldJudge.length > 0 && seat.couldJudge.every(one => one === issuerId))
108
+ : [];
109
+ /*
110
+ * 지목한 사람이 안 풀린다 — 없어진 직원을 가리키는 자리다. 역할 문제가 아니므로 위와 다른 낱말이다.
111
+ * 세 낱말이 자리를 선언하는 세 방법을 그대로 따른다: line · role · person.
112
+ */
113
+ const personMissing = seats.filter(seat => seat.kind === 'person' && seat.couldJudge.length === 0);
114
+ const refusals = [];
115
+ if (onlyIssuer.length) {
116
+ refusals.push({ because: 'only-the-issuer-could-judge', seats: named(onlyIssuer) });
117
+ }
118
+ if (roleEmpty.length) {
119
+ refusals.push({ because: 'approval-role-empty', seats: named(roleEmpty) });
120
+ }
121
+ if (personMissing.length) {
122
+ refusals.push({ because: 'approval-person-missing', seats: named(personMissing) });
123
+ }
124
+ return refusals.length ? { ok: false, refusals } : { ok: true };
125
+ }
126
+ function standingBroken(verdict) {
127
+ return !verdict.ok;
128
+ }
129
+ /** 사유마다 문구가 다르다 — 관리자가 할 일이 다르기 때문이다. */
130
+ exports.STANDING_REFUSAL_KEYS = {
131
+ 'only-the-issuer-could-judge': 'error.approval line has nobody but the person raising it',
132
+ 'approval-role-empty': 'error.approval line names a role that nobody holds',
133
+ 'approval-person-missing': 'error.approval line names a person who cannot be found'
134
+ };
135
+ //# sourceMappingURL=own-request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"own-request.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/own-request.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;;;AAGH,4BAEC;AAGD,wBAEC;AAYD,sCAUC;AAgCD,kDAKC;AAsED,4DAyCC;AAED,wCAEC;AAtLD,+DAA+D;AAC/D,SAAgB,QAAQ,CAAC,QAAmC,EAAE,MAAiC;IAC7F,OAAO,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,QAAQ,KAAK,MAAM,CAAA;AACtD,CAAC;AAED,4EAA4E;AAC5E,SAAgB,MAAM,CAAC,aAAwC;IAC7D,OAAO,aAAa,KAAK,WAAW,CAAA;AACtC,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,QAAQ,EACR,MAAM,EAKP;IACC,OAAO,MAAM,CAAC,aAAa,CAAC,IAAI,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;;;GAOG;AACU,QAAA,oBAAoB,GAAG,6CAA6C,CAAA;AAEjF;;;;;;;;;;;;;GAaG;AACU,QAAA,aAAa,GAAG,eAAe,CAAA;AAE5C,yCAAyC;AAC5B,QAAA,uBAAuB,GAAG,sEAAsE,CAAA;AAE7G,wDAAwD;AACxD,SAAgB,mBAAmB,CAAC,IAAY;IAC9C,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,IAAI,CAA6B,CAAA;IAEzD,KAAK,CAAC,IAAI,GAAG,qBAAa,CAAA;IAC1B,OAAO,KAAK,CAAA;AACd,CAAC;AAgED;;;;;GAKG;AACH,SAAgB,wBAAwB,CAAC,KAIxC;IACC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IAEjC,MAAM,KAAK,GAAG,CAAC,EAAmB,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;IAEzG,uDAAuD;IACvD,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAA;IAE5F;;;OAGG;IACH,MAAM,UAAU,GAAG,QAAQ;QACzB,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC;QACpG,CAAC,CAAC,EAAE,CAAA;IAEN;;;OAGG;IACH,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAA;IAElG,MAAM,QAAQ,GAAkB,EAAE,CAAA;IAElC,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IACrF,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC;QACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;IAC5E,CAAC;IAED,IAAI,aAAa,CAAC,MAAM,EAAE,CAAC;QACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;IACpF,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACjE,CAAC;AAED,SAAgB,cAAc,CAAC,OAAwB;IACrD,OAAO,CAAC,OAAO,CAAC,EAAE,CAAA;AACpB,CAAC;AAED,yCAAyC;AAC5B,QAAA,qBAAqB,GAAoD;IACpF,6BAA6B,EAAE,0DAA0D;IACzF,qBAAqB,EAAE,oDAAoD;IAC3E,yBAAyB,EAAE,wDAAwD;CACpF,CAAA","sourcesContent":["/**\n * 상신한 사람은 자기 상신의 결재자가 될 수 없다 — **한 물음, 두 자리.**\n *\n * ── 왜 규칙이 필요한가 (ADR-0052, 2026-09-16) ─────────────────────────────\n * plant 의 「정비 완료 확인」은 상신에 `maintenance:mutation` 이 필요하고 결재선이 `MAINTENANCE-LEAD`\n * 한 단계인데, 그 권한을 보전팀장과 정비원이 **둘 다** 든다. 정비원이 끝내고 팀장이 확인하면 두 사람이지만,\n * **팀장이 직접 고치고 자기가 확인하면 한 사람**이다. 결재선을 고쳐서는 못 막는다 — 팀장을 빼면 정비가\n * 끝났는지 아는 유일한 자리가 사라진다. 그날 그 사람이 둘 다인 것이 문제라 실행 시점에 막는다.\n *\n * ── 왜 `creator` 가 아니라 `issuer` 인가 ──────────────────────────────────\n * 두 칸이 갈리는 길이 있다. `issue()` 는 `creator: user` 를 먼저 적고 `...origin` 으로 draft 를 펼치는데,\n * `origin` 이 `creator` 를 관계로 싣고 있어 **덮인다.** `issuer: user` 는 그 뒤라 안 덮인다. 그리고\n * `assertMayWorkOnDraft` 가 매니저에게 남의 draft 상신을 허용하므로, 매니저가 정비원의 draft 를 올리면\n * creator 는 정비원이고 issuer 는 매니저다.\n *\n * 그때 `creator` 로 막으면 **올리지 않은 사람을 막고, 실제로 올린 사람은 자기 상신을 결재한다** — 규칙이\n * 정확히 거꾸로 선다. 상신은 올리는 행위이고 그 판단에 책임지는 사람이 `issuer` 다. 직접 상신이면 둘이\n * 같으므로 흔한 경우는 달라지지 않는다. (2026-09-16, 아키텍트 판정)\n *\n * ── 참조는 막지 않는다 ───────────────────────────────────────────────────\n * 규칙이 막는 것은 **결재자** 자리다. 참조는 판정하지 않으므로 자기가 올린 건에 의견을 남기는 것이 이상하지\n * 않다 — 오히려 상신한 사람이 덧붙이는 말이 결재자에게 쓸모 있다.\n *\n * ── 두 자리가 같은 것을 봐야 한다 ────────────────────────────────────────\n * 누르는 자리(`checkAuthority`)와 목록에 담는 자리(`myPendingApprovalsWhere`)가 갈리면 **목록에 떴는데\n * 누르면 거절된다.** 그것이 결함이다. 한쪽은 JS 로 묻고 다른 쪽은 SQL 로 물어야 해서 코드를 그대로 나눌\n * 수는 없으므로, 두 표현을 이 파일에 나란히 두고 시험이 둘이 같은 칸을 본다는 것을 센다.\n */\n\n/** 이 사람이 이 건을 올렸나. 상신자가 없는 건(`issuer` 가 빈 옛 행)은 아무도 막지 않는다. */\nexport function raisedIt(issuerId: string | undefined | null, userId: string | undefined | null): boolean {\n return !!issuerId && !!userId && issuerId === userId\n}\n\n/** 이 자리가 판정하는 자리인가 — 빈 값은 결재다(`ApprovalLineItem.participation` 과 같은 규약). */\nexport function judges(participation: string | undefined | null): boolean {\n return participation !== 'reference'\n}\n\n/**\n * 거절하나 — **판단은 이 함수가 한다.**\n *\n * `checkAuthority` 안에 두면 시험이 못 든다. 그 파일은 엔티티를 끌어오고 엔티티는 type-graphql 을\n * 끌어와서, 시험 러너가 파일을 여는 순간 죽는다. 그래서 거기에 남겨 두면 **원문을 읽는 시험밖에** 못\n * 쓰게 되고, 원문 시험은 「그 글자가 있나」를 셀 뿐 「그래서 거절하나」를 못 센다.\n *\n * 값만 받는 함수로 빼면 네 갈래를 다 돌려 볼 수 있다 — 결재 자리 · 참조 자리 × 상신자 · 아닌 사람.\n * 부르는 쪽에 남는 것은 값을 **가져오는** 일뿐이고, 그건 원문으로 확인해도 되는 종류다.\n */\nexport function refuseToJudge({\n participation,\n issuerId,\n userId\n}: {\n participation?: string | null\n issuerId?: string | null\n userId?: string | null\n}): boolean {\n return judges(participation) && raisedIt(issuerId, userId)\n}\n\n/**\n * 같은 물음의 SQL — **`ai`(ActivityInstance)를 조인한 질의에서만** 쓴다.\n *\n * `ai.issuer != :userId` 만 쓰면 상신자가 빈 행이 SQL 의 NULL 셈법으로 조용히 빠진다 — 참이 아니라\n * 알 수 없음이 되고, 그 행들이 아무의 결재함에도 안 뜬다. 그래서 빈 것을 먼저 통과시킨다.\n *\n * 참조 자리는 이 조각이 다루지 않는다. 목록 질의는 참조를 이미 앞에서 걸러 낸다.\n */\nexport const NOT_RAISED_BY_ME_SQL = '(ai.issuer IS NULL OR ai.issuer != :userId)'\n\n/**\n * 상신한 사람이 결재하려 했다 — **커널이 쓰는 낱말과 같은 값이어야 한다.**\n *\n * `headless-twin/server/service/actuation/command-dispatcher.ts` 의 `SELF_APPROVAL` 과 짝이다.\n * **두 벌이 아니라 두 문이다** — 커널은 업무 목록을 안 지나는 길(API · AI 발의)에서 자기 커맨드를\n * 지키고, 여기는 결재 문을 지킨다. 커널↔호스트 경계를 넘어 합치지 않는다.\n *\n * 두 문이 위험한 것은 **서로 다른 답을 낼 때**다. 그래서 문장이 아니라 코드를 맞춘다 — 문장은 번역도\n * 집계도 안 되고, 고치는 날 두 문이 다른 말을 한다. 상수를 서로 import 하면 두 패키지가 묶이므로,\n * **양쪽이 각자 글자를 시험에 못 박는다.** 그래야 움직인 쪽이 빨개진다. (ADR-0052 ③)\n *\n * ⚠ 아래 `only-the-issuer-could-judge` 와 **다른 사실**이다. 이쪽은 「결재하려 한다」이고 그쪽은\n * 「설 사람이 상신자뿐이라 못 올린다」다. 글자 모양이 눈에 띄게 다른 것도 일부러다.\n */\nexport const SELF_APPROVAL = 'self-approval'\n\n/** 거절할 때 쓰는 키 — 무엇을 해야 하는지까지 문구가 말한다. */\nexport const OWN_REQUEST_REFUSAL_KEY = 'error.activity-approval cannot be judged by the person who raised it'\n\n/** 사람에게는 문장을, 붙잡는 쪽에는 `code` 를 준다 — 커널이 같은 모양으로 던진다. */\nexport function selfApprovalRefused(said: string): Error & { code: string } {\n const error = new Error(said) as Error & { code: string }\n\n error.code = SELF_APPROVAL\n return error\n}\n\n/**\n * ═══════════════════════════════════════════════════════════════════════════\n * 같은 규칙을 **상신하는 순간**에 본다 — 누르는 순간에 거절만 하면 멈춘다.\n * ═══════════════════════════════════════════════════════════════════════════\n *\n * 위의 `refuseToJudge` 는 자기 상신을 결재하려는 사람을 막는다. 그것만 세우면 **결재선에 그 사람밖에\n * 없는 건이 아무에게도 안 간다.** 정비 완료 확인이 그 모양이면 설비가 정비중으로 남고 그 라인이 선다.\n * 시드 주석이 걱정한 것이 이것이다.\n *\n * 그래서 올리는 자리에서 먼저 본다. 올릴 때 막으면 **고칠 수 있는 사람이 보는 자리**에서 걸리고,\n * 무엇을 고쳐야 하는지 그 자리에서 말할 수 있다. 결재 중에 걸리면 이미 설비가 멈춘 뒤다.\n * (2026-09-16, ADR-0052 ② · 아키텍트 판정 (다))\n *\n * ── 왜 「한 자리라도」가 아니라 「모든 자리」인가 ──────────────────────────\n * 결재선의 결재 자리는 차례로 **전부** 판정을 받아야 끝난다. 한 자리만 막혀도 그 건은 거기서 영영\n * 선다. 그래서 자리마다 본다.\n *\n * ── 아무도 못 서는 자리는 **같은 관문에서 다른 낱말로** 거절한다 ──────────\n * 그 역할을 가진 사람이 **하나도 없는** 자리도 똑같이 건을 멈춘다. 그런데 상신자 탓이 아니므로,\n * 같은 사유로 묶으면 「당신이 올려서 막혔다」는 **틀린 이유**를 대게 된다.\n *\n * 자리가 같다고 사유를 합치지 않는다 — **관리자가 할 일이 다르다.** 하나는 「그 자리에 다른 결재자를」\n * 이고 하나는 「그 역할에 사람을」이다. 낱말은 이미 있다: `approval-role-empty`(ADR-0047 ②).\n * (2026-09-16, 아키텍트 판정)\n *\n * 사람 자리가 **아무에게도 안 풀리는** 경우도 같은 자리에서 세 번째 낱말로 거절한다 — 없어진 직원을\n * 가리키는 자리다. 역할 문제가 아니므로 `approval-role-empty` 로 부르면 그것도 틀린 이유다.\n * 세 낱말이 자리를 선언하는 세 방법을 그대로 따른다:\n *\n * approval-line-missing 결재선 자체가 없다 (§`approval-expectation.ts`)\n * approval-role-empty 지목한 역할에 사람이 0명\n * approval-person-missing 지목한 사람 자리가 안 풀린다\n *\n * 전에는 `submit` 이 `error.approver is not set on the approval line` 로 잡았는데, 그때는 이미 올라간\n * 뒤다. (2026-09-16, 아키텍트 판정)\n */\n\n/** 결재 자리 하나와, 거기 설 수 있는 사람들. 역할 자리면 그 역할을 가진 사람 전부. */\nexport type JudgeableSeat = {\n /** 사람이 세는 차례 — 1차 결재, 2차 결재. 거절 문구가 이 수를 말한다. */\n order: number\n /** 역할로 건 자리인가 사람으로 건 자리인가 — **거절 사유가 갈린다.** */\n kind: 'role' | 'person'\n /** 그 자리에 설 수 있는 사람들의 id. 역할 자리면 그 역할을 가진 사람 전부. */\n couldJudge: string[]\n /** 화면에 낼 자리 이름 — 역할 이름이나 사람 이름. */\n name?: string\n /**\n * 그 자리가 **가리키던 식별자.** 안 풀리는 자리를 고치려면 「누구 자리였는지」를 알아야 한다 —\n * 이름은 못 찾으면 비는데 가리키던 값은 결재선에 그대로 남아 있다.\n */\n pointsAt?: string\n}\n\n/** 거절 사유 하나와, 그 사유로 막힌 자리들. */\nexport type SeatRefusal = {\n because: 'only-the-issuer-could-judge' | 'approval-role-empty' | 'approval-person-missing'\n seats: { order: number; name?: string; pointsAt?: string }[]\n}\n\nexport type StandingVerdict = { ok: true } | { ok: false; refusals: SeatRefusal[] }\n\n/**\n * 이 결재선을 이 사람이 올려도 되나.\n *\n * 사유가 둘이면 **둘 다 돌려준다.** 하나씩 말하면 관리자가 하나 고치고 다시 걸린다 — 자리를 전부\n * 말하는 것과 같은 이유다.\n */\nexport function judgeSomeoneElseCanJudge(input: {\n issuerId?: string | null\n /** **결재 자리만.** 참조는 판정하지 않으므로 여기 오지 않는다. */\n seats: JudgeableSeat[]\n}): StandingVerdict {\n const { issuerId, seats } = input\n\n const named = (of: JudgeableSeat[]) => of.map(({ order, name, pointsAt }) => ({ order, name, pointsAt }))\n\n /* 역할을 지목했는데 그 역할에 사람이 없다 — 상신자가 누구든 이 자리는 아무도 못 선다. */\n const roleEmpty = seats.filter(seat => seat.kind === 'role' && seat.couldJudge.length === 0)\n\n /*\n * 설 수 있는 사람이 있는데 그게 상신자뿐이다. 상신자를 모르면 이 사유는 성립하지 않는다 —\n * 「없음」을 「그 사람」으로 읽지 않는다.\n */\n const onlyIssuer = issuerId\n ? seats.filter(seat => seat.couldJudge.length > 0 && seat.couldJudge.every(one => one === issuerId))\n : []\n\n /*\n * 지목한 사람이 안 풀린다 — 없어진 직원을 가리키는 자리다. 역할 문제가 아니므로 위와 다른 낱말이다.\n * 세 낱말이 자리를 선언하는 세 방법을 그대로 따른다: line · role · person.\n */\n const personMissing = seats.filter(seat => seat.kind === 'person' && seat.couldJudge.length === 0)\n\n const refusals: SeatRefusal[] = []\n\n if (onlyIssuer.length) {\n refusals.push({ because: 'only-the-issuer-could-judge', seats: named(onlyIssuer) })\n }\n\n if (roleEmpty.length) {\n refusals.push({ because: 'approval-role-empty', seats: named(roleEmpty) })\n }\n\n if (personMissing.length) {\n refusals.push({ because: 'approval-person-missing', seats: named(personMissing) })\n }\n\n return refusals.length ? { ok: false, refusals } : { ok: true }\n}\n\nexport function standingBroken(verdict: StandingVerdict): verdict is Extract<StandingVerdict, { ok: false }> {\n return !verdict.ok\n}\n\n/** 사유마다 문구가 다르다 — 관리자가 할 일이 다르기 때문이다. */\nexport const STANDING_REFUSAL_KEYS: { [because in SeatRefusal['because']]: string } = {\n 'only-the-issuer-could-judge': 'error.approval line has nobody but the person raising it',\n 'approval-role-empty': 'error.approval line names a role that nobody holds',\n 'approval-person-missing': 'error.approval line names a person who cannot be found'\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import type { ApprovalLineItem } from '@things-factory/organization';
2
+ import type { JudgeableSeat } from './own-request.js';
3
+ /**
4
+ * 결재선을 「자리마다 누가 설 수 있나」로 푼다 — **가져오는 일만 한다.**
5
+ *
6
+ * 판단은 `own-request.ts` 의 `judgeSomeoneElseCanJudge` 가 든다. 이 파일은 DB 를 읽어서 시험이 못
7
+ * 드는 쪽이고, 그래서 여기에는 규칙을 한 줄도 두지 않는다.
8
+ *
9
+ * ── 자리를 푸는 규칙은 `create-approval.ts` 를 그대로 따른다 ──────────────
10
+ * 실제로 행을 만드는 자리가 거기다. 여기서 다르게 풀면 **여기서는 괜찮다고 하고 거기서는 다른 사람을
11
+ * 앉히는** 일이 난다. 그쪽이 가르는 축은 하나다 — `type` 이 `Role` 이면 역할로 걸고, 아니면 사람
12
+ * 하나로 푼다.
13
+ *
14
+ * ⚠ `getSystemUserFromOrgMemberItem` 도 `Role` 을 받아 주는데, 그것은 **그 역할의 첫 사람 하나**를
15
+ * 돌려준다. 실제 거동은 그게 아니라 「그 역할을 가진 사람 누구나」이므로 그 갈래를 먼저 가른다.
16
+ *
17
+ * ── `Myself` 는 올리는 사람이다 ───────────────────────────────────────────
18
+ * 그 자리는 `context.state.user` 로 풀리고, 상신하는 동안 그 사람이 곧 상신자다. 그래서 `Myself` 가
19
+ * 결재 자리에 있으면 **언제나** 자기결재다 — 이 문이 정확히 그것을 잡는다.
20
+ */
21
+ export declare function resolveJudgeableSeats(approvalLine: ApprovalLineItem[] | undefined | null, context: ResolverContext): Promise<JudgeableSeat[]>;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveJudgeableSeats = resolveJudgeableSeats;
4
+ const auth_base_1 = require("@things-factory/auth-base");
5
+ const organization_1 = require("@things-factory/organization");
6
+ const common_js_1 = require("../common.js");
7
+ const approval_line_js_1 = require("./approval-line.js");
8
+ /**
9
+ * 결재선을 「자리마다 누가 설 수 있나」로 푼다 — **가져오는 일만 한다.**
10
+ *
11
+ * 판단은 `own-request.ts` 의 `judgeSomeoneElseCanJudge` 가 든다. 이 파일은 DB 를 읽어서 시험이 못
12
+ * 드는 쪽이고, 그래서 여기에는 규칙을 한 줄도 두지 않는다.
13
+ *
14
+ * ── 자리를 푸는 규칙은 `create-approval.ts` 를 그대로 따른다 ──────────────
15
+ * 실제로 행을 만드는 자리가 거기다. 여기서 다르게 풀면 **여기서는 괜찮다고 하고 거기서는 다른 사람을
16
+ * 앉히는** 일이 난다. 그쪽이 가르는 축은 하나다 — `type` 이 `Role` 이면 역할로 걸고, 아니면 사람
17
+ * 하나로 푼다.
18
+ *
19
+ * ⚠ `getSystemUserFromOrgMemberItem` 도 `Role` 을 받아 주는데, 그것은 **그 역할의 첫 사람 하나**를
20
+ * 돌려준다. 실제 거동은 그게 아니라 「그 역할을 가진 사람 누구나」이므로 그 갈래를 먼저 가른다.
21
+ *
22
+ * ── `Myself` 는 올리는 사람이다 ───────────────────────────────────────────
23
+ * 그 자리는 `context.state.user` 로 풀리고, 상신하는 동안 그 사람이 곧 상신자다. 그래서 `Myself` 가
24
+ * 결재 자리에 있으면 **언제나** 자기결재다 — 이 문이 정확히 그것을 잡는다.
25
+ */
26
+ async function resolveJudgeableSeats(approvalLine, context) {
27
+ const { tx } = context.state;
28
+ const seats = [];
29
+ for (const [index, item] of (approvalLine || []).entries()) {
30
+ /* 참조는 판정하지 않는다. 자리는 차지하므로 차례는 그대로 센다. */
31
+ if (!(0, approval_line_js_1.isApprovalStep)(item)) {
32
+ continue;
33
+ }
34
+ const order = index + 1;
35
+ if (item.type == organization_1.OrgMemberTargetType.Role) {
36
+ const roleId = item.value || item.approver?.id;
37
+ const role = roleId && (await tx.getRepository(auth_base_1.Role).findOne({ where: { id: roleId }, relations: ['users'] }));
38
+ seats.push({
39
+ order,
40
+ kind: 'role',
41
+ couldJudge: (role?.users || []).map(one => one.id),
42
+ name: role?.name || item.approver?.name,
43
+ pointsAt: roleId || undefined
44
+ });
45
+ continue;
46
+ }
47
+ const approver = await (0, common_js_1.getSystemUserFromOrgMemberItem)(item, context);
48
+ seats.push({
49
+ order,
50
+ kind: 'person',
51
+ couldJudge: approver?.id ? [approver.id] : [],
52
+ name: approver?.name || item.approver?.name,
53
+ /* 안 풀리는 자리는 이름도 비므로, 결재선에 적힌 값이 관리자가 가진 유일한 실마리다. */
54
+ pointsAt: item.approver?.id || item.value || undefined
55
+ });
56
+ }
57
+ return seats;
58
+ }
59
+ //# sourceMappingURL=resolve-judgeable-seats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-judgeable-seats.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/resolve-judgeable-seats.ts"],"names":[],"mappings":";;AA0BA,sDA2CC;AArED,yDAAgD;AAEhD,+DAAkE;AAElE,4CAA6D;AAC7D,yDAAmD;AAGnD;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,qBAAqB,CACzC,YAAmD,EACnD,OAAwB;IAExB,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAC5B,MAAM,KAAK,GAAoB,EAAE,CAAA;IAEjC,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC3D,yCAAyC;QACzC,IAAI,CAAC,IAAA,iCAAc,EAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,SAAQ;QACV,CAAC;QAED,MAAM,KAAK,GAAG,KAAK,GAAG,CAAC,CAAA;QAEvB,IAAI,IAAI,CAAC,IAAI,IAAI,kCAAmB,CAAC,IAAI,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAA;YAC9C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,aAAa,CAAC,gBAAI,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;YAE9G,KAAK,CAAC,IAAI,CAAC;gBACT,KAAK;gBACL,IAAI,EAAE,MAAM;gBACZ,UAAU,EAAE,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClD,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,IAAI;gBACvC,QAAQ,EAAE,MAAM,IAAI,SAAS;aAC9B,CAAC,CAAA;YAEF,SAAQ;QACV,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,IAAA,0CAA8B,EAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QAEpE,KAAK,CAAC,IAAI,CAAC;YACT,KAAK;YACL,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;YAC7C,IAAI,EAAE,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,IAAI;YAC3C,qDAAqD;YACrD,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,IAAI,CAAC,KAAK,IAAI,SAAS;SACvD,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { Role } from '@things-factory/auth-base'\nimport type { ApprovalLineItem } from '@things-factory/organization'\nimport { OrgMemberTargetType } from '@things-factory/organization'\n\nimport { getSystemUserFromOrgMemberItem } from '../common.js'\nimport { isApprovalStep } from './approval-line.js'\nimport type { JudgeableSeat } from './own-request.js'\n\n/**\n * 결재선을 「자리마다 누가 설 수 있나」로 푼다 — **가져오는 일만 한다.**\n *\n * 판단은 `own-request.ts` 의 `judgeSomeoneElseCanJudge` 가 든다. 이 파일은 DB 를 읽어서 시험이 못\n * 드는 쪽이고, 그래서 여기에는 규칙을 한 줄도 두지 않는다.\n *\n * ── 자리를 푸는 규칙은 `create-approval.ts` 를 그대로 따른다 ──────────────\n * 실제로 행을 만드는 자리가 거기다. 여기서 다르게 풀면 **여기서는 괜찮다고 하고 거기서는 다른 사람을\n * 앉히는** 일이 난다. 그쪽이 가르는 축은 하나다 — `type` 이 `Role` 이면 역할로 걸고, 아니면 사람\n * 하나로 푼다.\n *\n * ⚠ `getSystemUserFromOrgMemberItem` 도 `Role` 을 받아 주는데, 그것은 **그 역할의 첫 사람 하나**를\n * 돌려준다. 실제 거동은 그게 아니라 「그 역할을 가진 사람 누구나」이므로 그 갈래를 먼저 가른다.\n *\n * ── `Myself` 는 올리는 사람이다 ───────────────────────────────────────────\n * 그 자리는 `context.state.user` 로 풀리고, 상신하는 동안 그 사람이 곧 상신자다. 그래서 `Myself` 가\n * 결재 자리에 있으면 **언제나** 자기결재다 — 이 문이 정확히 그것을 잡는다.\n */\nexport async function resolveJudgeableSeats(\n approvalLine: ApprovalLineItem[] | undefined | null,\n context: ResolverContext\n): Promise<JudgeableSeat[]> {\n const { tx } = context.state\n const seats: JudgeableSeat[] = []\n\n for (const [index, item] of (approvalLine || []).entries()) {\n /* 참조는 판정하지 않는다. 자리는 차지하므로 차례는 그대로 센다. */\n if (!isApprovalStep(item)) {\n continue\n }\n\n const order = index + 1\n\n if (item.type == OrgMemberTargetType.Role) {\n const roleId = item.value || item.approver?.id\n const role = roleId && (await tx.getRepository(Role).findOne({ where: { id: roleId }, relations: ['users'] }))\n\n seats.push({\n order,\n kind: 'role',\n couldJudge: (role?.users || []).map(one => one.id),\n name: role?.name || item.approver?.name,\n pointsAt: roleId || undefined\n })\n\n continue\n }\n\n const approver = await getSystemUserFromOrgMemberItem(item, context)\n\n seats.push({\n order,\n kind: 'person',\n couldJudge: approver?.id ? [approver.id] : [],\n name: approver?.name || item.approver?.name,\n /* 안 풀리는 자리는 이름도 비므로, 결재선에 적힌 값이 관리자가 가진 유일한 실마리다. */\n pointsAt: item.approver?.id || item.value || undefined\n })\n }\n\n return seats\n}\n"]}
@@ -5,6 +5,7 @@ exports.installActivity = installActivity;
5
5
  const shell_1 = require("@things-factory/shell");
6
6
  const activity_js_1 = require("../../service/activity/activity.js");
7
7
  const registry_conflict_js_1 = require("./registry-conflict.js");
8
+ const decision_key_declaration_js_1 = require("../../service/activity/decision-key-declaration.js");
8
9
  const installable_to_activity_js_1 = require("./installable-to-activity.js");
9
10
  /**
10
11
  * 등록된 양식을 **도메인에 실제로 세운다.**
@@ -54,6 +55,7 @@ class ActivityInstallations {
54
55
  static { this.intended = {}; }
55
56
  static installActivityTemplate(template) {
56
57
  (0, registry_conflict_js_1.assertNotDeclaredIntended)(template.name, ActivityInstallations.intended);
58
+ (0, registry_conflict_js_1.assertDoorSlugIsFree)(template.name, decision_key_declaration_js_1.doorSlug, ActivityInstallations.templates);
57
59
  ActivityInstallations.templates[template.name] = template;
58
60
  }
59
61
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"activity-installation-controller.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/activity-installation-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { Activity, ActivityStatus } from '../../service/activity/activity.js'\nimport { InstallableActivity } from '../../service/installable-activity/installable-activity.js'\nimport { ActivityReadiness } from '../../service/installable-activity/activity-readiness.js'\nimport { assertNotAlreadyInstalled, assertNotDeclaredIntended } from './registry-conflict.js'\nimport { activityRowFrom } from './installable-to-activity.js'\n\n/**\n * 등록된 양식을 **도메인에 실제로 세운다.**\n *\n * ── 왜 함수로 내놓나 ───────────────────────────────────────────────────────\n * 앞서는 이 일이 화면 뮤테이션 안에만 있었다. 그래서 양식을 세우는 길이 **사람이 화면에서 누르는 것\n * 하나뿐**이었고, 새로 배포하거나 도메인을 하나 더 만들 때마다 그 클릭을 기억해야 했다. 기억하지\n * 못하면 그 기능이 오류 보고 없이 없다.\n *\n * 앱이 「내 기능이 서려면 이 양식이 있어야 한다」를 아는 경우가 있다. 그런 앱은 기동할 때 스스로\n * 세울 수 있어야 한다. 언제 세울지는 앱이 정하고, 세우는 방법은 여기가 한 벌만 갖는다.\n *\n * ── 있는 것을 덮되 결재선은 건드리지 않는다 ────────────────────────────────\n * 이미 있으면 양식의 선언(모델 · 보는 화면 · 시작 방식)으로 덮는다. 판이 오르면 그 값이 따라와야\n * 하기 때문이다.\n *\n * 그런데 **결재선은 양식의 것이 아니라 그 현장의 것**이다. 누가 결재하는지는 공장이 정한다. 그래서\n * 양식에 결재선 선언이 없으면 이미 있는 결재선이 그대로 남는다 — 판을 올릴 때마다 결재자가 지워지면\n * 그 뒤로 상신이 곧 완료가 된다.\n */\nexport async function installActivity(\n name: string,\n domain: Domain,\n options: { manager?: EntityManager; user?: any } = {}\n): Promise<Activity> {\n const template = ActivityInstallations.get(name)\n\n if (!template) {\n /*\n * 없는 이름을 세울 수는 없다. 조용히 넘기면 부르는 쪽은 세운 줄 알고, 그 기능은 없는 채로 선다.\n */\n throw new Error(\n `installActivity: there is no installable activity named \"${name}\". ` +\n `known: ${ActivityInstallations.list()\n .map(one => one.name)\n .join(', ')}`\n )\n }\n\n const { manager, user } = options\n const repository = getRepository(Activity, manager)\n\n const existing = await repository.findOneBy({ domain: { id: domain.id }, name })\n\n return await repository.save({\n ...(existing ?? {}),\n ...activityRowFrom(template),\n state: ActivityStatus.Released,\n domain,\n creator: existing ? (existing as any).creator : user,\n updater: user\n } as Activity)\n}\n\n/**\n * Work a module has identified as a person's decision and **has not built.**\n *\n * Deliberately not an InstallableActivity. A template with no callback can be installed and\n * issued, and then someone approves a request and nothing happens — on the twin side that means a\n * person presses approve and no equipment moves. So these live in their own registry: the catalog\n * reads them, `installActivity` cannot reach them.\n *\n * They belong on the screen because the catalog is the plan of record. The question \"what is left\"\n * should be answered where people look, not in one person's head.\n */\nexport type IntendedActivity = {\n /** What the activity's `name` will be once it exists. */\n key: string\n /** What the catalog shows. The activity's own name is usually not a sentence a person reads. */\n label: string\n category?: string\n /** What this work is, for the person who will receive it. Draft wording is fine. */\n guide?: string[]\n readiness: ActivityReadiness\n}\n\nexport class ActivityInstallations {\n static templates: { [name: string]: InstallableActivity } = {}\n\n /** Keyed separately from `templates`, so nothing here can be installed by mistake. */\n static intended: { [key: string]: IntendedActivity } = {}\n\n static installActivityTemplate(template: InstallableActivity) {\n assertNotDeclaredIntended(template.name, ActivityInstallations.intended)\n\n ActivityInstallations.templates[template.name] = template\n }\n\n /**\n * Declare work that should reach someone's worklist and does not yet.\n *\n * A module calls this for each item it has identified. When the real thing is built, delete the\n * call and register the template instead — the catalog row moves from 예정 to 동작 확인됨 on its\n * own.\n */\n static declareIntendedActivity(intended: IntendedActivity) {\n assertNotAlreadyInstalled(intended.key, ActivityInstallations.templates)\n\n ActivityInstallations.intended[intended.key] = intended\n }\n\n static get(name: string): InstallableActivity {\n return ActivityInstallations.templates[name]\n }\n\n static list(): InstallableActivity[] {\n return Object.values(ActivityInstallations.templates)\n }\n\n static listIntended(): IntendedActivity[] {\n return Object.values(ActivityInstallations.intended)\n }\n\n /**\n * 액티비티인스턴스의 변화가 발생했을 때, 호출되는 콜백함수\n * @param name 액티비티인스턴스가 속한 액티비티명\n * @param activityInstance 해당 액티비티인스턴스\n * @param activityThreads 액티비티인스턴스의 변화를 일으킨 원인이 되는 액티비티쓰레드들\n * @param context 서비스 컨텍스트 : domain, user, translation, ..\n * @returns\n */\n static async callback(name: string, activityInstance: any, activityThreads: any[], context: ResolverContext) {\n const installableActivity = ActivityInstallations.templates[name]\n if (!installableActivity) {\n return\n }\n\n const { callback } = installableActivity\n\n callback && (await callback(activityInstance, { causedBy: activityThreads }, context))\n }\n}\n"]}
1
+ {"version":3,"file":"activity-installation-controller.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/activity-installation-controller.ts"],"names":[],"mappings":";;;AA8BA,0CAgCC;AA5DD,iDAA6D;AAE7D,oEAA6E;AAG7E,iEAAmH;AACnH,oGAA6E;AAC7E,6EAA8D;AAE9D;;;;;;;;;;;;;;;;;;GAkBG;AACI,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,MAAc,EACd,UAAmD,EAAE;IAErD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAEhD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd;;WAEG;QACH,MAAM,IAAI,KAAK,CACb,4DAA4D,IAAI,KAAK;YACnE,UAAU,qBAAqB,CAAC,IAAI,EAAE;iBACnC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC;iBACpB,IAAI,CAAC,IAAI,CAAC,EAAE,CAClB,CAAA;IACH,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,OAAO,CAAA;IACjC,MAAM,UAAU,GAAG,IAAA,qBAAa,EAAC,sBAAQ,EAAE,OAAO,CAAC,CAAA;IAEnD,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;IAEhF,OAAO,MAAM,UAAU,CAAC,IAAI,CAAC;QAC3B,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC;QACnB,GAAG,IAAA,4CAAe,EAAC,QAAQ,CAAC;QAC5B,KAAK,EAAE,4BAAc,CAAC,QAAQ;QAC9B,MAAM;QACN,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAE,QAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;QACpD,OAAO,EAAE,IAAI;KACF,CAAC,CAAA;AAChB,CAAC;AAwBD,MAAa,qBAAqB;aACzB,cAAS,GAA4C,EAAE,CAAA;IAE9D,sFAAsF;aAC/E,aAAQ,GAAwC,EAAE,CAAA;IAEzD,MAAM,CAAC,uBAAuB,CAAC,QAA6B;QAC1D,IAAA,gDAAyB,EAAC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAA;QACxE,IAAA,2CAAoB,EAAC,QAAQ,CAAC,IAAI,EAAE,sCAAQ,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;QAE9E,qBAAqB,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;IAC3D,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,uBAAuB,CAAC,QAA0B;QACvD,IAAA,gDAAyB,EAAC,QAAQ,CAAC,GAAG,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAA;QAExE,qBAAqB,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAA;IACzD,CAAC;IAED,MAAM,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,qBAAqB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IAC9C,CAAC;IAED,MAAM,CAAC,IAAI;QACT,OAAO,MAAM,CAAC,MAAM,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAA;IACvD,CAAC;IAED,MAAM,CAAC,YAAY;QACjB,OAAO,MAAM,CAAC,MAAM,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAA;IACtD,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,gBAAqB,EAAE,eAAsB,EAAE,OAAwB;QACzG,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QACjE,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACzB,OAAM;QACR,CAAC;QAED,MAAM,EAAE,QAAQ,EAAE,GAAG,mBAAmB,CAAA;QAExC,QAAQ,IAAI,CAAC,MAAM,QAAQ,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,eAAe,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;IACxF,CAAC;;AAvDH,sDAwDC","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { Activity, ActivityStatus } from '../../service/activity/activity.js'\nimport { InstallableActivity } from '../../service/installable-activity/installable-activity.js'\nimport { ActivityReadiness } from '../../service/installable-activity/activity-readiness.js'\nimport { assertDoorSlugIsFree, assertNotAlreadyInstalled, assertNotDeclaredIntended } from './registry-conflict.js'\nimport { doorSlug } from '../../service/activity/decision-key-declaration.js'\nimport { activityRowFrom } from './installable-to-activity.js'\n\n/**\n * 등록된 양식을 **도메인에 실제로 세운다.**\n *\n * ── 왜 함수로 내놓나 ───────────────────────────────────────────────────────\n * 앞서는 이 일이 화면 뮤테이션 안에만 있었다. 그래서 양식을 세우는 길이 **사람이 화면에서 누르는 것\n * 하나뿐**이었고, 새로 배포하거나 도메인을 하나 더 만들 때마다 그 클릭을 기억해야 했다. 기억하지\n * 못하면 그 기능이 오류 보고 없이 없다.\n *\n * 앱이 「내 기능이 서려면 이 양식이 있어야 한다」를 아는 경우가 있다. 그런 앱은 기동할 때 스스로\n * 세울 수 있어야 한다. 언제 세울지는 앱이 정하고, 세우는 방법은 여기가 한 벌만 갖는다.\n *\n * ── 있는 것을 덮되 결재선은 건드리지 않는다 ────────────────────────────────\n * 이미 있으면 양식의 선언(모델 · 보는 화면 · 시작 방식)으로 덮는다. 판이 오르면 그 값이 따라와야\n * 하기 때문이다.\n *\n * 그런데 **결재선은 양식의 것이 아니라 그 현장의 것**이다. 누가 결재하는지는 공장이 정한다. 그래서\n * 양식에 결재선 선언이 없으면 이미 있는 결재선이 그대로 남는다 — 판을 올릴 때마다 결재자가 지워지면\n * 그 뒤로 상신이 곧 완료가 된다.\n */\nexport async function installActivity(\n name: string,\n domain: Domain,\n options: { manager?: EntityManager; user?: any } = {}\n): Promise<Activity> {\n const template = ActivityInstallations.get(name)\n\n if (!template) {\n /*\n * 없는 이름을 세울 수는 없다. 조용히 넘기면 부르는 쪽은 세운 줄 알고, 그 기능은 없는 채로 선다.\n */\n throw new Error(\n `installActivity: there is no installable activity named \"${name}\". ` +\n `known: ${ActivityInstallations.list()\n .map(one => one.name)\n .join(', ')}`\n )\n }\n\n const { manager, user } = options\n const repository = getRepository(Activity, manager)\n\n const existing = await repository.findOneBy({ domain: { id: domain.id }, name })\n\n return await repository.save({\n ...(existing ?? {}),\n ...activityRowFrom(template),\n state: ActivityStatus.Released,\n domain,\n creator: existing ? (existing as any).creator : user,\n updater: user\n } as Activity)\n}\n\n/**\n * Work a module has identified as a person's decision and **has not built.**\n *\n * Deliberately not an InstallableActivity. A template with no callback can be installed and\n * issued, and then someone approves a request and nothing happens — on the twin side that means a\n * person presses approve and no equipment moves. So these live in their own registry: the catalog\n * reads them, `installActivity` cannot reach them.\n *\n * They belong on the screen because the catalog is the plan of record. The question \"what is left\"\n * should be answered where people look, not in one person's head.\n */\nexport type IntendedActivity = {\n /** What the activity's `name` will be once it exists. */\n key: string\n /** What the catalog shows. The activity's own name is usually not a sentence a person reads. */\n label: string\n category?: string\n /** What this work is, for the person who will receive it. Draft wording is fine. */\n guide?: string[]\n readiness: ActivityReadiness\n}\n\nexport class ActivityInstallations {\n static templates: { [name: string]: InstallableActivity } = {}\n\n /** Keyed separately from `templates`, so nothing here can be installed by mistake. */\n static intended: { [key: string]: IntendedActivity } = {}\n\n static installActivityTemplate(template: InstallableActivity) {\n assertNotDeclaredIntended(template.name, ActivityInstallations.intended)\n assertDoorSlugIsFree(template.name, doorSlug, ActivityInstallations.templates)\n\n ActivityInstallations.templates[template.name] = template\n }\n\n /**\n * Declare work that should reach someone's worklist and does not yet.\n *\n * A module calls this for each item it has identified. When the real thing is built, delete the\n * call and register the template instead — the catalog row moves from 예정 to 동작 확인됨 on its\n * own.\n */\n static declareIntendedActivity(intended: IntendedActivity) {\n assertNotAlreadyInstalled(intended.key, ActivityInstallations.templates)\n\n ActivityInstallations.intended[intended.key] = intended\n }\n\n static get(name: string): InstallableActivity {\n return ActivityInstallations.templates[name]\n }\n\n static list(): InstallableActivity[] {\n return Object.values(ActivityInstallations.templates)\n }\n\n static listIntended(): IntendedActivity[] {\n return Object.values(ActivityInstallations.intended)\n }\n\n /**\n * 액티비티인스턴스의 변화가 발생했을 때, 호출되는 콜백함수\n * @param name 액티비티인스턴스가 속한 액티비티명\n * @param activityInstance 해당 액티비티인스턴스\n * @param activityThreads 액티비티인스턴스의 변화를 일으킨 원인이 되는 액티비티쓰레드들\n * @param context 서비스 컨텍스트 : domain, user, translation, ..\n * @returns\n */\n static async callback(name: string, activityInstance: any, activityThreads: any[], context: ResolverContext) {\n const installableActivity = ActivityInstallations.templates[name]\n if (!installableActivity) {\n return\n }\n\n const { callback } = installableActivity\n\n callback && (await callback(activityInstance, { causedBy: activityThreads }, context))\n }\n}\n"]}
@@ -29,4 +29,19 @@ type KeyedByName = {
29
29
  export declare function assertNotAlreadyInstalled(key: string, installed: KeyedByName): void;
30
30
  /** Called when a template arrives. */
31
31
  export declare function assertNotDeclaredIntended(key: string, intended: KeyedByName): void;
32
+ /**
33
+ * Two doors must not fold to the same slug — **their value sentences would overwrite each other.**
34
+ *
35
+ * The i18n key for a decision slot is `approval.<door>.<inputPath>` and `<door>` is the activity
36
+ * name kebab-cased, so two names differing only where the slug does not look (`Batch Release` and
37
+ * `BatchRelease`) land on one key. Then one door's sentences are drawn on the other's card, in
38
+ * whichever order the locale file happened to be written.
39
+ *
40
+ * Nothing throws when that happens and nothing is logged — the card simply says the wrong thing
41
+ * about an approval somebody is about to give. It cannot happen today, which is why it is checked
42
+ * now rather than after a door finds it.
43
+ *
44
+ * Refused where the second template arrives, so the message can name both.
45
+ */
46
+ export declare function assertDoorSlugIsFree(name: string, slugOf: (name: string) => string, installed: KeyedByName): void;
32
47
  export {};