@things-factory/worklist 10.1.165 → 10.1.168

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 (34) hide show
  1. package/dist-client/components/activity-starter-form.js +2 -0
  2. package/dist-client/components/activity-starter-form.js.map +1 -1
  3. package/dist-client/pages/activity/activity-list-page.js +1 -0
  4. package/dist-client/pages/activity/activity-list-page.js.map +1 -1
  5. package/dist-client/pages/activity-approval/activity-approval-page.js +1 -0
  6. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  7. package/dist-client/pages/activity-catalog/activity-detail-page.js +9 -2
  8. package/dist-client/pages/activity-catalog/activity-detail-page.js.map +1 -1
  9. package/dist-client/pages/activity-instance/activity-instance-start-page.js +3 -0
  10. package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
  11. package/dist-client/pages/activity-instance/activity-instance-view.js +1 -0
  12. package/dist-client/pages/activity-instance/activity-instance-view.js.map +1 -1
  13. package/dist-client/pages/activity-thread/activity-thread-page.js +1 -0
  14. package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
  15. package/dist-client/pages/activity-thread/activity-thread-view-page.js +1 -0
  16. package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
  17. package/dist-client/pages/todo/draft-list-page.js +1 -0
  18. package/dist-client/pages/todo/draft-list-page.js.map +1 -1
  19. package/dist-client/pages/work-door/work-doors-page.d.ts +1 -15
  20. package/dist-client/pages/work-door/work-doors-page.js +18 -43
  21. package/dist-client/pages/work-door/work-doors-page.js.map +1 -1
  22. package/dist-client/tsconfig.tsbuildinfo +1 -1
  23. package/dist-server/controllers/activity-approval/create-approval.js +2 -1
  24. package/dist-server/controllers/activity-approval/create-approval.js.map +1 -1
  25. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js +2 -1
  26. package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js.map +1 -1
  27. package/dist-server/service/activity-instance/draft-list-states.d.ts +3 -3
  28. package/dist-server/service/activity-instance/draft-list-states.js +3 -3
  29. package/dist-server/service/activity-instance/draft-list-states.js.map +1 -1
  30. package/dist-server/service/work-door/approval-line-standing.d.ts +14 -0
  31. package/dist-server/service/work-door/approval-line-standing.js +23 -5
  32. package/dist-server/service/work-door/approval-line-standing.js.map +1 -1
  33. package/dist-server/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +9 -9
@@ -16,6 +16,7 @@ const auth_base_1 = require("@things-factory/auth-base");
16
16
  const organization_1 = require("@things-factory/organization");
17
17
  const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
18
18
  const common_js_1 = require("../common.js");
19
+ const approval_line_standing_js_1 = require("../../service/work-door/approval-line-standing.js");
19
20
  /**
20
21
  * 결재선 항목 하나로 행을 만든다.
21
22
  *
@@ -37,7 +38,7 @@ async function createApproval(args, context) {
37
38
  updater: user
38
39
  };
39
40
  if (item.type == organization_1.OrgMemberTargetType.Role) {
40
- const roleId = item.value || item.approver?.id;
41
+ const roleId = (0, approval_line_standing_js_1.seatRoleId)(item);
41
42
  if (!roleId) {
42
43
  throw new Error(context.t('error.approver role is not set on the approval line', { order }));
43
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"create-approval.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/create-approval.ts"],"names":[],"mappings":";;AA2BA,wCA8CC;AAWD,4CAwBC;AA5GD;;;;;;;;;GASG;AACH,yDAAgD;AAEhD,+DAAkE;AAElE,+FAAiH;AAEjH,4CAA6D;AAK7D;;;;;GAKG;AACI,KAAK,UAAU,cAAc,CAClC,IAOC,EACD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAC1C,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAExE,MAAM,IAAI,GAAG;QACX,MAAM;QACN,cAAc;QACd,KAAK,EAAE,cAAc,CAAC,KAAK;QAC3B,KAAK;QACL,QAAQ,EAAE,+CAAwB,CAAC,OAAO;QAC1C,aAAa;QACb,WAAW;QACX,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,IAAI;KACd,CAAA;IAED,IAAI,IAAI,CAAC,IAAI,IAAI,kCAAmB,CAAC,IAAI,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAA;QAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,qDAAqD,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;QAC9F,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,gBAAI,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;QAC3E,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;QAC/E,CAAC;QAED,OAAO,MAAM,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAS,CAAC,CAAA;IACxF,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,IAAA,0CAA8B,EAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACpE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,gDAAgD,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IACzF,CAAC;IAED,OAAO,MAAM,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAS,CAAC,CAAA;AACpF,CAAC;AAED;;;;;;;;GAQG;AACI,KAAK,UAAU,gBAAgB,CACpC,IAGC,EACD,OAAwB;IAExB,MAAM,OAAO,GAAuB,EAAE,CAAA;IACtC,MAAM,MAAM,GAAwC,EAAE,CAAA;IAEtD,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QAC9C,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CACV,MAAM,cAAc,CAClB,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,EACvG,OAAO,CACR,CACF,CAAA;QACH,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,CAAA;QACzD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC5B,CAAC","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
+ {"version":3,"file":"create-approval.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/create-approval.ts"],"names":[],"mappings":";;AA4BA,wCA8CC;AAWD,4CAwBC;AA7GD;;;;;;;;;GASG;AACH,yDAAgD;AAEhD,+DAAkE;AAElE,+FAAiH;AAEjH,4CAA6D;AAC7D,iGAA8E;AAK9E;;;;;GAKG;AACI,KAAK,UAAU,cAAc,CAClC,IAOC,EACD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAC1C,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;IAExE,MAAM,IAAI,GAAG;QACX,MAAM;QACN,cAAc;QACd,KAAK,EAAE,cAAc,CAAC,KAAK;QAC3B,KAAK;QACL,QAAQ,EAAE,+CAAwB,CAAC,OAAO;QAC1C,aAAa;QACb,WAAW;QACX,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,IAAI;KACd,CAAA;IAED,IAAI,IAAI,CAAC,IAAI,IAAI,kCAAmB,CAAC,IAAI,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAA,sCAAU,EAAC,IAAI,CAAC,CAAA;QAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,qDAAqD,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;QAC9F,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,gBAAI,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;QAC3E,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;QAC/E,CAAC;QAED,OAAO,MAAM,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAS,CAAC,CAAA;IACxF,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,IAAA,0CAA8B,EAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IACpE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,gDAAgD,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IACzF,CAAC;IAED,OAAO,MAAM,EAAE,CAAC,aAAa,CAAC,uCAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAS,CAAC,CAAA;AACpF,CAAC;AAED;;;;;;;;GAQG;AACI,KAAK,UAAU,gBAAgB,CACpC,IAGC,EACD,OAAwB;IAExB,MAAM,OAAO,GAAuB,EAAE,CAAA;IACtC,MAAM,MAAM,GAAwC,EAAE,CAAA;IAEtD,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QAC9C,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CACV,MAAM,cAAc,CAClB,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,EACvG,OAAO,CACR,CACF,CAAA;QACH,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,CAAA;QACzD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC5B,CAAC","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'\nimport { seatRoleId } from '../../service/work-door/approval-line-standing.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 = seatRoleId(item)\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"]}
@@ -5,6 +5,7 @@ const auth_base_1 = require("@things-factory/auth-base");
5
5
  const organization_1 = require("@things-factory/organization");
6
6
  const common_js_1 = require("../common.js");
7
7
  const approval_line_js_1 = require("./approval-line.js");
8
+ const approval_line_standing_js_1 = require("../../service/work-door/approval-line-standing.js");
8
9
  /**
9
10
  * 결재선을 「자리마다 누가 설 수 있나」로 푼다 — **가져오는 일만 한다.**
10
11
  *
@@ -33,7 +34,7 @@ async function resolveJudgeableSeats(approvalLine, context) {
33
34
  }
34
35
  const order = index + 1;
35
36
  if (item.type == organization_1.OrgMemberTargetType.Role) {
36
- const roleId = item.value || item.approver?.id;
37
+ const roleId = (0, approval_line_standing_js_1.seatRoleId)(item);
37
38
  const role = roleId && (await tx.getRepository(auth_base_1.Role).findOne({ where: { id: roleId }, relations: ['users'] }));
38
39
  seats.push({
39
40
  order,
@@ -1 +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"]}
1
+ {"version":3,"file":"resolve-judgeable-seats.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/resolve-judgeable-seats.ts"],"names":[],"mappings":";;AA2BA,sDA2CC;AAtED,yDAAgD;AAEhD,+DAAkE;AAElE,4CAA6D;AAC7D,yDAAmD;AAEnD,iGAA8E;AAE9E;;;;;;;;;;;;;;;;;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,IAAA,sCAAU,EAAC,IAAI,CAAC,CAAA;YAC/B,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'\nimport { seatRoleId } from '../../service/work-door/approval-line-standing.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 = seatRoleId(item)\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"]}
@@ -2,7 +2,7 @@
2
2
  * 「내가 올린 것」 목록이 어느 상태를 낼지 — **화면이 고르면 그것을, 안 고르면 작성 중만.**
3
3
  *
4
4
  * ── 무엇이 있었나 (2026-09-23) ────────────────────────────────────────────
5
- * 화면에는 상태 고르개가 있고 갈래가 아홉인데, 서버가 `state IN ['draft']` 를 무조건 덧붙였다.
5
+ * 화면에는 상태 선택기가 있고 갈래가 아홉인데, 서버가 `state IN ['draft']` 를 무조건 덧붙였다.
6
6
  * 그래서 작성 중 말고 무엇을 골라도 **항상 빈 화면**이었고, 읽는 사람은 그것을 「그런 건이 없다」로
7
7
  * 읽었다. 상신하는 순간 그 건이 목록에서 사라지는데, 사람이 마지막으로 그 건을 본 자리가 여기다 —
8
8
  * 찾으러 오는 자리에서 없어진다.
@@ -10,11 +10,11 @@
10
10
  * 작성자로 좁히는 일은 이미 서 있었다(`ai.creator = :user`). 묶여 있던 것은 상태 하나다.
11
11
  *
12
12
  * ── 안 고르면 달라지지 않는다 ────────────────────────────────────────────
13
- * 고르개를 안 건드리면 예전처럼 작성 중만 나온다. 기본값을 넓히면 이 화면을 열던 사람이 어느 날
13
+ * 선택기를 안 건드리면 예전처럼 작성 중만 나온다. 기본값을 넓히면 이 화면을 열던 사람이 어느 날
14
14
  * 갑자기 끝난 건까지 보게 된다.
15
15
  *
16
16
  * ── 모르는 상태는 조용히 0 건으로 답하지 않는다 ──────────────────────────
17
- * 화면이 `posted` 와 `escalated` 를 고르개에 담고 있는데 **둘 다 인스턴스 상태가 아니다**
17
+ * 화면이 `posted` 와 `escalated` 를 선택기에 담고 있는데 **둘 다 인스턴스 상태가 아니다**
18
18
  * [확인함 2026-09-23, `ActivityInstanceStatus` 여덟 개와 서버 전수 grep]. `posted` 는 transaction
19
19
  * 값이고 `escalated` 는 결재 판정이다. 그대로 거르게 두면 영원히 0 건이고, 그것은 이 화면이 이미
20
20
  * 앓던 병과 같다 — 「없다」와 「못 가져온다」가 같은 빈칸이 된다. 그래서 이름을 대고 거절한다.
@@ -8,7 +8,7 @@ exports.unknownStates = unknownStates;
8
8
  * 「내가 올린 것」 목록이 어느 상태를 낼지 — **화면이 고르면 그것을, 안 고르면 작성 중만.**
9
9
  *
10
10
  * ── 무엇이 있었나 (2026-09-23) ────────────────────────────────────────────
11
- * 화면에는 상태 고르개가 있고 갈래가 아홉인데, 서버가 `state IN ['draft']` 를 무조건 덧붙였다.
11
+ * 화면에는 상태 선택기가 있고 갈래가 아홉인데, 서버가 `state IN ['draft']` 를 무조건 덧붙였다.
12
12
  * 그래서 작성 중 말고 무엇을 골라도 **항상 빈 화면**이었고, 읽는 사람은 그것을 「그런 건이 없다」로
13
13
  * 읽었다. 상신하는 순간 그 건이 목록에서 사라지는데, 사람이 마지막으로 그 건을 본 자리가 여기다 —
14
14
  * 찾으러 오는 자리에서 없어진다.
@@ -16,11 +16,11 @@ exports.unknownStates = unknownStates;
16
16
  * 작성자로 좁히는 일은 이미 서 있었다(`ai.creator = :user`). 묶여 있던 것은 상태 하나다.
17
17
  *
18
18
  * ── 안 고르면 달라지지 않는다 ────────────────────────────────────────────
19
- * 고르개를 안 건드리면 예전처럼 작성 중만 나온다. 기본값을 넓히면 이 화면을 열던 사람이 어느 날
19
+ * 선택기를 안 건드리면 예전처럼 작성 중만 나온다. 기본값을 넓히면 이 화면을 열던 사람이 어느 날
20
20
  * 갑자기 끝난 건까지 보게 된다.
21
21
  *
22
22
  * ── 모르는 상태는 조용히 0 건으로 답하지 않는다 ──────────────────────────
23
- * 화면이 `posted` 와 `escalated` 를 고르개에 담고 있는데 **둘 다 인스턴스 상태가 아니다**
23
+ * 화면이 `posted` 와 `escalated` 를 선택기에 담고 있는데 **둘 다 인스턴스 상태가 아니다**
24
24
  * [확인함 2026-09-23, `ActivityInstanceStatus` 여덟 개와 서버 전수 grep]. `posted` 는 transaction
25
25
  * 값이고 `escalated` 는 결재 판정이다. 그대로 거르게 두면 영원히 0 건이고, 그것은 이 화면이 이미
26
26
  * 앓던 병과 같다 — 「없다」와 「못 가져온다」가 같은 빈칸이 된다. 그래서 이름을 대고 거절한다.
@@ -1 +1 @@
1
- {"version":3,"file":"draft-list-states.js","sourceRoot":"","sources":["../../../server/service/activity-instance/draft-list-states.ts"],"names":[],"mappings":";;;AA0BA,gCAEC;AAGD,8BAEC;AAQD,sCAQC;AAjDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACU,QAAA,yBAAyB,GAAG,CAAC,OAAO,CAAU,CAAA;AAI3D,qCAAqC;AACrC,SAAgB,UAAU,CAAC,OAA4B;IACrD,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC,CAAA;AAC3D,CAAC;AAED,iDAAiD;AACjD,SAAgB,SAAS,CAAC,OAA4B;IACpD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iCAAyB,CAAA;AACpE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,OAAuC,EAAE,KAAwB;IAC7F,MAAM,KAAK,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC;SACpC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;SACpE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SACpC,MAAM,CAAC,OAAO,CAAC,CAAA;IAElB,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;AAChE,CAAC","sourcesContent":["/**\n * 「내가 올린 것」 목록이 어느 상태를 낼지 — **화면이 고르면 그것을, 안 고르면 작성 중만.**\n *\n * ── 무엇이 있었나 (2026-09-23) ────────────────────────────────────────────\n * 화면에는 상태 고르개가 있고 갈래가 아홉인데, 서버가 `state IN ['draft']` 를 무조건 덧붙였다.\n * 그래서 작성 중 말고 무엇을 골라도 **항상 빈 화면**이었고, 읽는 사람은 그것을 「그런 건이 없다」로\n * 읽었다. 상신하는 순간 그 건이 목록에서 사라지는데, 사람이 마지막으로 그 건을 본 자리가 여기다 —\n * 찾으러 오는 자리에서 없어진다.\n *\n * 작성자로 좁히는 일은 이미 서 있었다(`ai.creator = :user`). 묶여 있던 것은 상태 하나다.\n *\n * ── 안 고르면 달라지지 않는다 ────────────────────────────────────────────\n * 고르개를 안 건드리면 예전처럼 작성 중만 나온다. 기본값을 넓히면 이 화면을 열던 사람이 어느 날\n * 갑자기 끝난 건까지 보게 된다.\n *\n * ── 모르는 상태는 조용히 0 건으로 답하지 않는다 ──────────────────────────\n * 화면이 `posted` 와 `escalated` 를 고르개에 담고 있는데 **둘 다 인스턴스 상태가 아니다**\n * [확인함 2026-09-23, `ActivityInstanceStatus` 여덟 개와 서버 전수 grep]. `posted` 는 transaction\n * 값이고 `escalated` 는 결재 판정이다. 그대로 거르게 두면 영원히 0 건이고, 그것은 이 화면이 이미\n * 앓던 병과 같다 — 「없다」와 「못 가져온다」가 같은 빈칸이 된다. 그래서 이름을 대고 거절한다.\n */\nexport const DRAFT_LIST_DEFAULT_STATES = ['draft'] as const\n\ntype Filterish = { name?: string; operator?: string; value?: any }\n\n/** 화면이 상태를 골랐나 — 골랐으면 서버가 덮지 않는다. */\nexport function picksState(filters?: Filterish[] | null): boolean {\n return (filters || []).some(one => one?.name === 'state')\n}\n\n/** 이 요청이 실제로 걸러야 할 상태들. 화면이 안 골랐을 때만 기본값이 선다. */\nexport function statesFor(filters?: Filterish[] | null): readonly string[] | undefined {\n return picksState(filters) ? undefined : DRAFT_LIST_DEFAULT_STATES\n}\n\n/**\n * 고른 상태가 서버가 아는 것인가 — 아니면 그 이름을 대고 거절한다.\n *\n * `known` 을 받아서 이 파일이 엔티티를 import 하지 않게 한다. 엔티티는 type-graphql 을 끌고 오고,\n * 그러면 이 규칙을 시험이 못 든다.\n */\nexport function unknownStates(filters: Filterish[] | null | undefined, known: readonly string[]): string[] {\n const asked = (filters || [])\n .filter(one => one?.name === 'state')\n .flatMap(one => (Array.isArray(one.value) ? one.value : [one.value]))\n .map(one => String(one ?? '').trim())\n .filter(Boolean)\n\n return [...new Set(asked.filter(one => !known.includes(one)))]\n}\n"]}
1
+ {"version":3,"file":"draft-list-states.js","sourceRoot":"","sources":["../../../server/service/activity-instance/draft-list-states.ts"],"names":[],"mappings":";;;AA0BA,gCAEC;AAGD,8BAEC;AAQD,sCAQC;AAjDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACU,QAAA,yBAAyB,GAAG,CAAC,OAAO,CAAU,CAAA;AAI3D,qCAAqC;AACrC,SAAgB,UAAU,CAAC,OAA4B;IACrD,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC,CAAA;AAC3D,CAAC;AAED,iDAAiD;AACjD,SAAgB,SAAS,CAAC,OAA4B;IACpD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iCAAyB,CAAA;AACpE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,OAAuC,EAAE,KAAwB;IAC7F,MAAM,KAAK,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC;SACpC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;SACpE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SACpC,MAAM,CAAC,OAAO,CAAC,CAAA;IAElB,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;AAChE,CAAC","sourcesContent":["/**\n * 「내가 올린 것」 목록이 어느 상태를 낼지 — **화면이 고르면 그것을, 안 고르면 작성 중만.**\n *\n * ── 무엇이 있었나 (2026-09-23) ────────────────────────────────────────────\n * 화면에는 상태 선택기가 있고 갈래가 아홉인데, 서버가 `state IN ['draft']` 를 무조건 덧붙였다.\n * 그래서 작성 중 말고 무엇을 골라도 **항상 빈 화면**이었고, 읽는 사람은 그것을 「그런 건이 없다」로\n * 읽었다. 상신하는 순간 그 건이 목록에서 사라지는데, 사람이 마지막으로 그 건을 본 자리가 여기다 —\n * 찾으러 오는 자리에서 없어진다.\n *\n * 작성자로 좁히는 일은 이미 서 있었다(`ai.creator = :user`). 묶여 있던 것은 상태 하나다.\n *\n * ── 안 고르면 달라지지 않는다 ────────────────────────────────────────────\n * 선택기를 안 건드리면 예전처럼 작성 중만 나온다. 기본값을 넓히면 이 화면을 열던 사람이 어느 날\n * 갑자기 끝난 건까지 보게 된다.\n *\n * ── 모르는 상태는 조용히 0 건으로 답하지 않는다 ──────────────────────────\n * 화면이 `posted` 와 `escalated` 를 선택기에 담고 있는데 **둘 다 인스턴스 상태가 아니다**\n * [확인함 2026-09-23, `ActivityInstanceStatus` 여덟 개와 서버 전수 grep]. `posted` 는 transaction\n * 값이고 `escalated` 는 결재 판정이다. 그대로 거르게 두면 영원히 0 건이고, 그것은 이 화면이 이미\n * 앓던 병과 같다 — 「없다」와 「못 가져온다」가 같은 빈칸이 된다. 그래서 이름을 대고 거절한다.\n */\nexport const DRAFT_LIST_DEFAULT_STATES = ['draft'] as const\n\ntype Filterish = { name?: string; operator?: string; value?: any }\n\n/** 화면이 상태를 골랐나 — 골랐으면 서버가 덮지 않는다. */\nexport function picksState(filters?: Filterish[] | null): boolean {\n return (filters || []).some(one => one?.name === 'state')\n}\n\n/** 이 요청이 실제로 걸러야 할 상태들. 화면이 안 골랐을 때만 기본값이 선다. */\nexport function statesFor(filters?: Filterish[] | null): readonly string[] | undefined {\n return picksState(filters) ? undefined : DRAFT_LIST_DEFAULT_STATES\n}\n\n/**\n * 고른 상태가 서버가 아는 것인가 — 아니면 그 이름을 대고 거절한다.\n *\n * `known` 을 받아서 이 파일이 엔티티를 import 하지 않게 한다. 엔티티는 type-graphql 을 끌고 오고,\n * 그러면 이 규칙을 시험이 못 든다.\n */\nexport function unknownStates(filters: Filterish[] | null | undefined, known: readonly string[]): string[] {\n const asked = (filters || [])\n .filter(one => one?.name === 'state')\n .flatMap(one => (Array.isArray(one.value) ? one.value : [one.value]))\n .map(one => String(one ?? '').trim())\n .filter(Boolean)\n\n return [...new Set(asked.filter(one => !known.includes(one)))]\n}\n"]}
@@ -58,6 +58,10 @@ export interface ApprovalSeat {
58
58
  type?: string;
59
59
  /** 역할이면 role id, 사람이면 user id. */
60
60
  value?: string;
61
+ /** 편집기가 고른 대상. 편집기는 `value` 를 안 쓰고 여기에만 적는다(§`seatRoleId`). */
62
+ approver?: {
63
+ id?: string;
64
+ };
61
65
  /** 비어 있으면 결재다(`ApprovalLineItem.participation` 의 규약). */
62
66
  participation?: string;
63
67
  }
@@ -87,6 +91,16 @@ export interface ApprovalSeatReader {
87
91
  /** 지목한 그 사람이 이 공장에 있고 계정이 붙어 있나. */
88
92
  personIsHere(personId: string): Promise<boolean>;
89
93
  }
94
+ /**
95
+ * A seat's role, read the way submission reads it (`create-approval`, `resolve-judgeable-seats`).
96
+ *
97
+ * The approval line editor writes what it picked under `approver` and leaves `value` empty; seeded
98
+ * lines carry both. Reading `value` alone here drew 「결재자 없음」 on the door screen for a line that
99
+ * submits. One function, so the screen and submission cannot read a seat two ways again.
100
+ */
101
+ export declare function seatRoleId(seat?: ApprovalSeat | null): string | undefined;
102
+ /** A person seat, read the way submission reads it (`getSystemUserFromOrgMemberItem`: approver first). */
103
+ export declare function seatPersonId(seat?: ApprovalSeat | null): string | undefined;
90
104
  export declare function judgeApprovalLine(line: ApprovalSeat[] | undefined | null, reader: ApprovalSeatReader): Promise<ApprovalLineStanding>;
91
105
  /** 결재선에서 **역할을 지목한 자리**의 id 들 — 부르는 쪽이 미리 읽어 둘 때 쓴다. */
92
106
  export declare function rolesNamedIn(line: ApprovalSeat[] | undefined | null): string[];
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.approvalLineRefusal = approvalLineRefusal;
4
+ exports.seatRoleId = seatRoleId;
5
+ exports.seatPersonId = seatPersonId;
4
6
  exports.judgeApprovalLine = judgeApprovalLine;
5
7
  exports.rolesNamedIn = rolesNamedIn;
6
8
  const shell_1 = require("@things-factory/shell");
@@ -14,6 +16,20 @@ function approvalLineRefusal(refusal) {
14
16
  const params = { role: refusal.params?.role ?? null, personId: refusal.params?.personId ?? null };
15
17
  return new shell_1.Refusal(refusal.code, params, LINE_REFUSAL_ENGLISH[refusal.code].replace('{role}', params.role ?? ''));
16
18
  }
19
+ /**
20
+ * A seat's role, read the way submission reads it (`create-approval`, `resolve-judgeable-seats`).
21
+ *
22
+ * The approval line editor writes what it picked under `approver` and leaves `value` empty; seeded
23
+ * lines carry both. Reading `value` alone here drew 「결재자 없음」 on the door screen for a line that
24
+ * submits. One function, so the screen and submission cannot read a seat two ways again.
25
+ */
26
+ function seatRoleId(seat) {
27
+ return seat?.value || seat?.approver?.id || undefined;
28
+ }
29
+ /** A person seat, read the way submission reads it (`getSystemUserFromOrgMemberItem`: approver first). */
30
+ function seatPersonId(seat) {
31
+ return seat?.approver?.id || seat?.value || undefined;
32
+ }
17
33
  const REFERENCE = 'reference';
18
34
  const ROLE = 'Role';
19
35
  const PERSON = 'Employee';
@@ -24,14 +40,16 @@ async function judgeApprovalLine(line, reader) {
24
40
  }
25
41
  for (const seat of seats) {
26
42
  if (seat?.type === ROLE) {
27
- if (!seat.value || !(await reader.roleHasMembers(seat.value))) {
28
- return { approvers: 0, refusal: { code: 'approval-role-empty', params: { role: seat.value ?? '' } } };
43
+ const role = seatRoleId(seat);
44
+ if (!role || !(await reader.roleHasMembers(role))) {
45
+ return { approvers: 0, refusal: { code: 'approval-role-empty', params: { role: role ?? '' } } };
29
46
  }
30
47
  continue;
31
48
  }
32
49
  if (seat?.type === PERSON) {
33
- if (!seat.value || !(await reader.personIsHere(seat.value))) {
34
- return { approvers: 0, refusal: { code: 'approval-person-absent', params: { personId: seat.value ?? '' } } };
50
+ const person = seatPersonId(seat);
51
+ if (!person || !(await reader.personIsHere(person))) {
52
+ return { approvers: 0, refusal: { code: 'approval-person-absent', params: { personId: person ?? '' } } };
35
53
  }
36
54
  }
37
55
  }
@@ -41,7 +59,7 @@ async function judgeApprovalLine(line, reader) {
41
59
  function rolesNamedIn(line) {
42
60
  return (line ?? [])
43
61
  .filter(seat => seat?.participation !== REFERENCE && seat?.type === ROLE)
44
- .map(seat => seat?.value)
62
+ .map(seat => seatRoleId(seat))
45
63
  .filter((one) => !!one);
46
64
  }
47
65
  //# sourceMappingURL=approval-line-standing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"approval-line-standing.js","sourceRoot":"","sources":["../../../server/service/work-door/approval-line-standing.ts"],"names":[],"mappings":";;AAyFA,kDAIC;AAcD,8CA2BC;AAGD,oCAKC;AA9ID,iDAA+C;AAkF/C,MAAM,oBAAoB,GAAwC;IAChE,uBAAuB,EAAE,sDAAsD;IAC/E,qBAAqB,EAAE,qFAAqF;IAC5G,wBAAwB,EAAE,oGAAoG;CAC/H,CAAA;AAED,iGAAiG;AACjG,SAAgB,mBAAmB,CAAC,OAAqD;IACvF,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,IAAI,IAAI,EAAE,CAAA;IAEjG,OAAO,IAAI,eAAO,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,oBAAoB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAA;AACnH,CAAC;AAUD,MAAM,SAAS,GAAG,WAAW,CAAA;AAC7B,MAAM,IAAI,GAAG,MAAM,CAAA;AACnB,MAAM,MAAM,GAAG,UAAU,CAAA;AAElB,KAAK,UAAU,iBAAiB,CACrC,IAAuC,EACvC,MAA0B;IAE1B,MAAM,KAAK,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,SAAS,CAAC,CAAA;IAE5E,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,CAAA;IACrE,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,CAAC;YACxB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC9D,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,EAAE,CAAA;YACvG,CAAC;YAED,SAAQ;QACV,CAAC;QAED,IAAI,IAAI,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC5D,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,wBAAwB,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,EAAE,CAAA;YAC9G,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE,CAAA;AACpC,CAAC;AAED,wDAAwD;AACxD,SAAgB,YAAY,CAAC,IAAuC;IAClE,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;SAChB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,SAAS,IAAI,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;SACxE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;SACxB,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import { Refusal } from '@things-factory/shell'\n\n/**\n * 결재선이 **차 있나** — 순수 판단. DB 도 TypeORM 도 모른다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 「있다」와 「찼다」가 다르다 ───────────────────────────────────────────\n * 앞서는 `approvalLine?.length` 만 셌다. 그러면 **역할을 지목해 놓고 그 역할에 사람이 하나도 없는**\n * 결재선이 「찼다」로 읽힌다. 문 현황 화면은 「이 문은 섰습니다」라고 그리고, 현장이 상신을 누르는\n * 순간 워크리스트가 `approver is not set` 으로 던진다 — **준비됐다고 말해 놓고 쓸 때 터지는** 부류다.\n *\n * 실 테넌트에서 이것이 예외가 아니다. 미라텍에는 이 제품이 세운 역할이 하나도 없다\n * (§`project_mes_roles_locked_behind_demo`). 역할을 지목한 결재선은 거기서 전부 빈 자리를 가리킨다.\n *\n * ── 불을 돌려주지 않는다. 그리고 **union 으로도 안 돌려준다** ────────────\n * 막은 쪽이 이유를 말한다. `true`/`false` 하나면 화면이 「왜 안 되나」를 **다시 세게 되고**, 그러면\n * 정의가 두 곳이 된다 — 이 판단을 한 곳으로 모은 이유가 없어진다.\n *\n * 그래서 처음에 `{ok:true} | {ok:false, code}` 로 두었는데, 이 패키지의 `server/tsconfig.json` 은\n * **`strict: false`** 라 `strictNullChecks` 가 없고, 그러면 TS 가 **boolean 판별자로 union 을 안\n * 좁힌다.** `if (r.ok) return` 뒤에서 `r.code` 를 읽으면 컴파일이 깨진다 — 하루에 소비자 둘이\n * 연달아 걸렸고 그중 하나는 서버를 아예 못 뜨게 했다.\n *\n * 그래서 **한 객체에 선택 칸**이다. `if (standing.refusal)` 하나로 어디서나 읽힌다.\n * [합의했다] ADR-0047 ⑤(개정)\n *\n * ```\n * approval-line-missing 결재선 자체가 없다 누가 결재하는지를 정해야 한다\n * approval-role-empty 역할은 있는데 사람이 없다 그 역할에 사람을 넣어야 한다\n * approval-person-absent 지목한 사람이 여기 없다 다른 사람을 지목하거나 그 사람을 넣어야 한다\n * ```\n *\n * 관리자가 할 일이 다르므로 코드가 셋이다.\n *\n * ── 참조는 세지 않는다 ───────────────────────────────────────────────────\n * `participation: 'reference'` 는 의견만 내고 결재를 막지 않는다. 참조자 역할이 비었다고 거절하면\n * **결재와 무관한 이유로** 막는 것이 된다. 참조만 있는 결재선은 결재 없이 끝나므로 「없다」로 읽는다.\n *\n * ── 읽는 것은 부르는 쪽이 넘긴다 ─────────────────────────────────────────\n * 포트 하나를 받는다. 이 판단이 `manager` 를 받으면 TypeORM 을 알게 되어 순수 시험이 서지 않고,\n * **어느 도메인으로 좁혔나**가 부르는 쪽에서 안 보이게 된다. 포트의 수명은 **판정 하나**다 — 더\n * 오래 살면 역할에 사람을 넣은 직후에도 옛 답을 내놓는다.\n *\n * ── 사람을 직접 지목한 자리도 읽는다 ─────────────────────────────────────\n * 「이 제품은 역할만 쓴다」로 닫지 않는다. 그렇게 두면 **없는 사람을 지목한 결재선이 「찼다」로\n * 그려지고 상신 순간 터진다** — 역할에서 고친 그 모양이 사람 축으로 다시 나는 것이고, 「없음」이\n * 안전하지 않은 쪽으로 기우는 것이다. 게다가 이 정의는 워크리스트의 `ApprovalLineItem` 을 읽는\n * 공용 자리라, 다른 제품이 사람을 지목하는 날 조용히 틀린다.\n *\n * `Myself` · `MyDepartment` · `MySupervisor` 는 **상신할 때 그 사람으로 풀리는** 자리라 미리 셀 것이\n * 없다. 부서 지목(`Department`)은 부서장이 있어야 풀리는데 그 코드는 아직 없다 — 지어내지 않는다.\n *\n * [합의했다] operato-application/adr/ADR-0047.md ③⑤\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/** 결재선 한 자리 — `ApprovalLineItem` 중 이 판단이 보는 칸만. */\nexport interface ApprovalSeat {\n /** `OrgMemberTargetType` — 역할이면 `'Role'`. */\n type?: string\n /** 역할이면 role id, 사람이면 user id. */\n value?: string\n /** 비어 있으면 결재다(`ApprovalLineItem.participation` 의 규약). */\n participation?: string\n}\n\nexport type ApprovalLineRefusal = 'approval-line-missing' | 'approval-role-empty' | 'approval-person-absent'\n\nexport interface ApprovalLineStanding {\n /** 이 결재선이 지나는 자리 수. 안 서는 결재선이면 0. */\n approvers: number\n /**\n * 왜 안 서나 — **있으면 안 서는 것이다.** 없으면 선다.\n *\n * `params` 는 그 코드가 가리키는 것 하나다: 역할이 비었으면 그 역할, 사람이 없으면 그 사람.\n */\n refusal?: {\n code: ApprovalLineRefusal\n params?: { role?: string; personId?: string }\n }\n}\n\nconst LINE_REFUSAL_ENGLISH: Record<ApprovalLineRefusal, string> = {\n 'approval-line-missing': 'this form has no approval line. set who approves it.',\n 'approval-role-empty': 'nobody holds the role {role} named in the approval line. add a person to that role.',\n 'approval-person-absent': 'the person named in the approval line is not in this site. name someone else in the approval line.'\n}\n\n/** A line that does not stand, as a refusal. The params are the one thing the code points at. */\nexport function approvalLineRefusal(refusal: NonNullable<ApprovalLineStanding['refusal']>): Refusal {\n const params = { role: refusal.params?.role ?? null, personId: refusal.params?.personId ?? null }\n\n return new Refusal(refusal.code, params, LINE_REFUSAL_ENGLISH[refusal.code].replace('{role}', params.role ?? ''))\n}\n\n/** 결재선의 자리가 실제로 사람으로 풀리나 — 읽는 것은 부르는 쪽 일이다. */\nexport interface ApprovalSeatReader {\n /** 그 역할에 사람이 하나 이상 있나. */\n roleHasMembers(roleId: string): Promise<boolean>\n /** 지목한 그 사람이 이 공장에 있고 계정이 붙어 있나. */\n personIsHere(personId: string): Promise<boolean>\n}\n\nconst REFERENCE = 'reference'\nconst ROLE = 'Role'\nconst PERSON = 'Employee'\n\nexport async function judgeApprovalLine(\n line: ApprovalSeat[] | undefined | null,\n reader: ApprovalSeatReader\n): Promise<ApprovalLineStanding> {\n const seats = (line ?? []).filter(seat => seat?.participation !== REFERENCE)\n\n if (!seats.length) {\n return { approvers: 0, refusal: { code: 'approval-line-missing' } }\n }\n\n for (const seat of seats) {\n if (seat?.type === ROLE) {\n if (!seat.value || !(await reader.roleHasMembers(seat.value))) {\n return { approvers: 0, refusal: { code: 'approval-role-empty', params: { role: seat.value ?? '' } } }\n }\n\n continue\n }\n\n if (seat?.type === PERSON) {\n if (!seat.value || !(await reader.personIsHere(seat.value))) {\n return { approvers: 0, refusal: { code: 'approval-person-absent', params: { personId: seat.value ?? '' } } }\n }\n }\n }\n\n return { approvers: seats.length }\n}\n\n/** 결재선에서 **역할을 지목한 자리**의 id 들 — 부르는 쪽이 미리 읽어 둘 때 쓴다. */\nexport function rolesNamedIn(line: ApprovalSeat[] | undefined | null): string[] {\n return (line ?? [])\n .filter(seat => seat?.participation !== REFERENCE && seat?.type === ROLE)\n .map(seat => seat?.value)\n .filter((one): one is string => !!one)\n}\n"]}
1
+ {"version":3,"file":"approval-line-standing.js","sourceRoot":"","sources":["../../../server/service/work-door/approval-line-standing.ts"],"names":[],"mappings":"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","sourcesContent":["import { Refusal } from '@things-factory/shell'\n\n/**\n * 결재선이 **차 있나** — 순수 판단. DB 도 TypeORM 도 모른다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 「있다」와 「찼다」가 다르다 ───────────────────────────────────────────\n * 앞서는 `approvalLine?.length` 만 셌다. 그러면 **역할을 지목해 놓고 그 역할에 사람이 하나도 없는**\n * 결재선이 「찼다」로 읽힌다. 문 현황 화면은 「이 문은 섰습니다」라고 그리고, 현장이 상신을 누르는\n * 순간 워크리스트가 `approver is not set` 으로 던진다 — **준비됐다고 말해 놓고 쓸 때 터지는** 부류다.\n *\n * 실 테넌트에서 이것이 예외가 아니다. 미라텍에는 이 제품이 세운 역할이 하나도 없다\n * (§`project_mes_roles_locked_behind_demo`). 역할을 지목한 결재선은 거기서 전부 빈 자리를 가리킨다.\n *\n * ── 불을 돌려주지 않는다. 그리고 **union 으로도 안 돌려준다** ────────────\n * 막은 쪽이 이유를 말한다. `true`/`false` 하나면 화면이 「왜 안 되나」를 **다시 세게 되고**, 그러면\n * 정의가 두 곳이 된다 — 이 판단을 한 곳으로 모은 이유가 없어진다.\n *\n * 그래서 처음에 `{ok:true} | {ok:false, code}` 로 두었는데, 이 패키지의 `server/tsconfig.json` 은\n * **`strict: false`** 라 `strictNullChecks` 가 없고, 그러면 TS 가 **boolean 판별자로 union 을 안\n * 좁힌다.** `if (r.ok) return` 뒤에서 `r.code` 를 읽으면 컴파일이 깨진다 — 하루에 소비자 둘이\n * 연달아 걸렸고 그중 하나는 서버를 아예 못 뜨게 했다.\n *\n * 그래서 **한 객체에 선택 칸**이다. `if (standing.refusal)` 하나로 어디서나 읽힌다.\n * [합의했다] ADR-0047 ⑤(개정)\n *\n * ```\n * approval-line-missing 결재선 자체가 없다 누가 결재하는지를 정해야 한다\n * approval-role-empty 역할은 있는데 사람이 없다 그 역할에 사람을 넣어야 한다\n * approval-person-absent 지목한 사람이 여기 없다 다른 사람을 지목하거나 그 사람을 넣어야 한다\n * ```\n *\n * 관리자가 할 일이 다르므로 코드가 셋이다.\n *\n * ── 참조는 세지 않는다 ───────────────────────────────────────────────────\n * `participation: 'reference'` 는 의견만 내고 결재를 막지 않는다. 참조자 역할이 비었다고 거절하면\n * **결재와 무관한 이유로** 막는 것이 된다. 참조만 있는 결재선은 결재 없이 끝나므로 「없다」로 읽는다.\n *\n * ── 읽는 것은 부르는 쪽이 넘긴다 ─────────────────────────────────────────\n * 포트 하나를 받는다. 이 판단이 `manager` 를 받으면 TypeORM 을 알게 되어 순수 시험이 서지 않고,\n * **어느 도메인으로 좁혔나**가 부르는 쪽에서 안 보이게 된다. 포트의 수명은 **판정 하나**다 — 더\n * 오래 살면 역할에 사람을 넣은 직후에도 옛 답을 내놓는다.\n *\n * ── 사람을 직접 지목한 자리도 읽는다 ─────────────────────────────────────\n * 「이 제품은 역할만 쓴다」로 닫지 않는다. 그렇게 두면 **없는 사람을 지목한 결재선이 「찼다」로\n * 그려지고 상신 순간 터진다** — 역할에서 고친 그 모양이 사람 축으로 다시 나는 것이고, 「없음」이\n * 안전하지 않은 쪽으로 기우는 것이다. 게다가 이 정의는 워크리스트의 `ApprovalLineItem` 을 읽는\n * 공용 자리라, 다른 제품이 사람을 지목하는 날 조용히 틀린다.\n *\n * `Myself` · `MyDepartment` · `MySupervisor` 는 **상신할 때 그 사람으로 풀리는** 자리라 미리 셀 것이\n * 없다. 부서 지목(`Department`)은 부서장이 있어야 풀리는데 그 코드는 아직 없다 — 지어내지 않는다.\n *\n * [합의했다] operato-application/adr/ADR-0047.md ③⑤\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/** 결재선 한 자리 — `ApprovalLineItem` 중 이 판단이 보는 칸만. */\nexport interface ApprovalSeat {\n /** `OrgMemberTargetType` — 역할이면 `'Role'`. */\n type?: string\n /** 역할이면 role id, 사람이면 user id. */\n value?: string\n /** 편집기가 고른 대상. 편집기는 `value` 를 안 쓰고 여기에만 적는다(§`seatRoleId`). */\n approver?: { id?: string }\n /** 비어 있으면 결재다(`ApprovalLineItem.participation` 의 규약). */\n participation?: string\n}\n\nexport type ApprovalLineRefusal = 'approval-line-missing' | 'approval-role-empty' | 'approval-person-absent'\n\nexport interface ApprovalLineStanding {\n /** 이 결재선이 지나는 자리 수. 안 서는 결재선이면 0. */\n approvers: number\n /**\n * 왜 안 서나 — **있으면 안 서는 것이다.** 없으면 선다.\n *\n * `params` 는 그 코드가 가리키는 것 하나다: 역할이 비었으면 그 역할, 사람이 없으면 그 사람.\n */\n refusal?: {\n code: ApprovalLineRefusal\n params?: { role?: string; personId?: string }\n }\n}\n\nconst LINE_REFUSAL_ENGLISH: Record<ApprovalLineRefusal, string> = {\n 'approval-line-missing': 'this form has no approval line. set who approves it.',\n 'approval-role-empty': 'nobody holds the role {role} named in the approval line. add a person to that role.',\n 'approval-person-absent': 'the person named in the approval line is not in this site. name someone else in the approval line.'\n}\n\n/** A line that does not stand, as a refusal. The params are the one thing the code points at. */\nexport function approvalLineRefusal(refusal: NonNullable<ApprovalLineStanding['refusal']>): Refusal {\n const params = { role: refusal.params?.role ?? null, personId: refusal.params?.personId ?? null }\n\n return new Refusal(refusal.code, params, LINE_REFUSAL_ENGLISH[refusal.code].replace('{role}', params.role ?? ''))\n}\n\n/** 결재선의 자리가 실제로 사람으로 풀리나 — 읽는 것은 부르는 쪽 일이다. */\nexport interface ApprovalSeatReader {\n /** 그 역할에 사람이 하나 이상 있나. */\n roleHasMembers(roleId: string): Promise<boolean>\n /** 지목한 그 사람이 이 공장에 있고 계정이 붙어 있나. */\n personIsHere(personId: string): Promise<boolean>\n}\n\n/**\n * A seat's role, read the way submission reads it (`create-approval`, `resolve-judgeable-seats`).\n *\n * The approval line editor writes what it picked under `approver` and leaves `value` empty; seeded\n * lines carry both. Reading `value` alone here drew 「결재자 없음」 on the door screen for a line that\n * submits. One function, so the screen and submission cannot read a seat two ways again.\n */\nexport function seatRoleId(seat?: ApprovalSeat | null): string | undefined {\n return seat?.value || seat?.approver?.id || undefined\n}\n\n/** A person seat, read the way submission reads it (`getSystemUserFromOrgMemberItem`: approver first). */\nexport function seatPersonId(seat?: ApprovalSeat | null): string | undefined {\n return seat?.approver?.id || seat?.value || undefined\n}\n\nconst REFERENCE = 'reference'\nconst ROLE = 'Role'\nconst PERSON = 'Employee'\n\nexport async function judgeApprovalLine(\n line: ApprovalSeat[] | undefined | null,\n reader: ApprovalSeatReader\n): Promise<ApprovalLineStanding> {\n const seats = (line ?? []).filter(seat => seat?.participation !== REFERENCE)\n\n if (!seats.length) {\n return { approvers: 0, refusal: { code: 'approval-line-missing' } }\n }\n\n for (const seat of seats) {\n if (seat?.type === ROLE) {\n const role = seatRoleId(seat)\n\n if (!role || !(await reader.roleHasMembers(role))) {\n return { approvers: 0, refusal: { code: 'approval-role-empty', params: { role: role ?? '' } } }\n }\n\n continue\n }\n\n if (seat?.type === PERSON) {\n const person = seatPersonId(seat)\n\n if (!person || !(await reader.personIsHere(person))) {\n return { approvers: 0, refusal: { code: 'approval-person-absent', params: { personId: person ?? '' } } }\n }\n }\n }\n\n return { approvers: seats.length }\n}\n\n/** 결재선에서 **역할을 지목한 자리**의 id 들 — 부르는 쪽이 미리 읽어 둘 때 쓴다. */\nexport function rolesNamedIn(line: ApprovalSeat[] | undefined | null): string[] {\n return (line ?? [])\n .filter(seat => seat?.participation !== REFERENCE && seat?.type === ROLE)\n .map(seat => seatRoleId(seat))\n .filter((one): one is string => !!one)\n}\n"]}