Skip to content

RACI 참조 전환 계획 (이름 문자열 → 디렉터리 사용자 참조) ​

상태: 계획(제안) · 대상: ProcessCenter RACI 저장·표시·검색·내보내기·권한 판정 소유: ProcessCenter — 프로세스는 L2 참조정보이고, 임직원(L1)은 PortalConsole 디렉터리가 정본이다 공통 규약: portal-foundation docs/REFERENCE_DATA_SUPPLY_CONVENTION.md — "값(이름·라벨)을 복사해 보관하지 않는다. 표시가 필요하면 조회 시점에 해석한다"관련: docs/PROCESS_REFERENCE_CONTRACT.md(§3.3 L1 인용 필드 — raci[].directoryUserId 는 이미 참조로 공급), docs/PROCESS_NODE_GOVERNANCE_PLAN.md(게시 거버넌스), docs/DIRECTORY_AUTHORIZATION_INTEGRATION.md

1. 지금 상태 (실측) ​

계층참조(정본)이름 문자열(레거시 중복)
DBprocess_node_raci_users — (processId, raciRole R/A/C/I, directoryUserId) 유니크 (drizzle/schema.pg.ts:212)process_nodes.raciResponsible·raciAccountable·raciConsulted·raciInformed text(≤1000자, 콤마 문자열) (drizzle/schema.pg.ts:114)
쓰기saveRaciUsers(역할 단위 교체, canEditProcess 요구) — server/services/processGovernanceService.ts:239노드 저장 시 폼의 콤마 문자열 그대로 저장
권한 판정A(승인): listProcessNodeRaciUserIds(id, "A") → isAccountableForProcess 가 참조 우선 (shared/processAccess.ts:73)R(수정): isResponsibleForProcess 가 문자열만 본다 (shared/processAccess.ts:54)
공급(v2)/api/v2/process-nodes 의 raci[].directoryUserId (server/remoteProcessNodesV2.ts:73)공급하지 않음 ✔
화면 표시없음ProcessWorkspace 상세 process-detail-raci(ProcessWorkspace.tsx:280), AdminProcessTree·AdminConsole·Home
검색없음client/src/lib/processSearch.ts:39 — 문자열 4개를 검색 대상에 포함
내보내기(CSV)없음client/src/lib/processExport.ts:90 — RACI 열 4개
카탈로그 전송/가져오기참조 미포함server/services/catalogTransferService.ts:191·259, server/processImport.ts:15 — 문자열만 이동
선택 UI없음RaciUserPicker(ProcessManagementPanel.tsx:495) PortalMultiSelect + 이름 값 → 저장 직전 resolveRaciUserIds 환산(:256)

2. 결함 (정리 대상) ​

#결함근거영향
D-R1R(수정 권한) 판정이 이름 문자열 기반 — A 는 참조 우선으로 이미 해소shared/processAccess.ts:54 (node.raciResponsible.split(...))이름 표기 변경·동명이인에 취약. 사용자 판정 identity 에 name 이 포함돼 있다(:43)
D-R2카탈로그 전송·가져오기가 참조를 옮기지 않는다catalogTransferService.ts:191·259, processImport.ts:15다른 인스턴스로 옮기면 process_node_raci_users 가 비어 A/R 권한이 문자열로만 남는다
D-R3표시·검색·내보내기가 저장된 이름을 그대로 쓴다ProcessWorkspace.tsx:280, processSearch.ts:39, processExport.ts:90디렉터리에서 이름이 바뀌면 화면·검색·CSV 가 옛 이름을 유지한다(공통 규약 위반)
D-R4값(이름)과 참조가 두 곳에 있고 어긋날 수 있다노드 저장 성공 후 참조 저장이 실패해도 되돌리지 않는다(주석: ProcessManagementPanel.tsx:251)A 판정이 옛 참조로 남는다(권한 불일치)
D-R5이름까지 identity 로 매칭shared/processAccess.ts:43이름이 같은 다른 사람이 권한을 얻을 여지(보안)

3. 목표 계약 (제안) ​

  1. 정본은 참조 하나 — process_node_raci_users.directoryUserId (L1 인용). 노드의 문자열 컬럼은 레거시 읽기 전용으로 내린다.
  2. 표시·검색·내보내기는 조회 시점 해석 — PC 는 디렉터리 사용자 프로필을 이미 로컬에 갖고 있다(directory_user_profiles, organizationRepository.listUsersWithOrganization) → 조인 해석으로 처리하고 이름을 저장하지 않는다.
  3. 권한 판정(R·A)은 참조만 본다. 문자열 폴백은 유예 기간에만 남기고 마지막에 제거한다.
  4. 공급(v2 API)은 변경 없음 — 이미 참조만 공급한다.

4. 단계 (되돌리기 쉬운 순서 → 어려운 순서) ​

단계내용되돌리기검증
0. 진단참조 현황 리포트 스크립트: ①문자열↔참조 불일치 노드 ②참조 없이 문자열만 있는 노드 ③문자열로는 매칭되나 참조가 없는 사람 ④디렉터리에서 해석 불가한 참조변경 없음(읽기 전용)pnpm raci:diagnose 리포트 수치
1. 쓰기 단일화RaciUserPicker → PortalUserPicker(다중·검색, 값=디렉터리 참조). 폼 저장 시 참조를 정본으로 저장하고 노드 문자열도 함께 갱신(표시 호환). resolveRaciUserIds 우회 제거코드 되돌리기 쉬움ProcessManagementPanel.test.tsx, raciReferences.test.ts
2. 읽기 해석노드 응답에 역할별 참조(또는 해석된 이름)를 실어 주고, 표시·검색·내보내기를 참조 해석으로 전환. 문자열은 폴백으로만중간processSearch.test.ts, processExport.test.ts, 화면 계약
3. 권한 통일R 도 참조 우선(listProcessNodeRaciUserIds(id, "R")). v2 접근 판정(processNodeSource)도 참조를 받게 한다중간processAccess.test.ts, processNodeSource.test.ts, remoteProcessNodesV2.test.ts
4. 카탈로그전송·가져오기 payload 에 참조를 함께 싣고 insert 한다중간catalogTransferService·processImport 테스트
5. 레거시 정리백필 → 문자열 컬럼 쓰기 중단 → 읽기 폴백 제거 → 컬럼 제거(마이그레이션). 계약 문서 갱신어려움(불가역)마이그레이션 + 전 경로 테스트

0단계 실행 결과 (dev DB, 읽기 전용) ​

bash
pnpm raci:diagnose   # scripts/raci-reference-diagnose.ts · 판정 규칙 shared/raciReferenceAudit.ts (+테스트 8건)
항목값
노드 / 역할별 값이 있는 노드-역할 / 참조 행 / 디렉터리 사용자246 / 4 / 4 / 1
① 불일치 · ② 문자열만 · ③ 문자열로만 권한 · ④a 이름이 참조 자리 · ④b 미해석 참조모두 0건

dev DB 는 표본이 작다(노드 1건만 RACI 값을 갖고, 그 값도 문자열 윤호현 ↔ 참조 hohyun@gmail.com 로 일치한다). 운영 DB 에서 같은 명령을 실행해야 실제 규모가 나온다 — 도구는 읽기 전용이라 운영에서 그대로 돌릴 수 있다.

1단계 적용 결과 (2026-10) ​

  • RaciUserPicker(ProcessManagementPanel.tsx)가 PortalUserPicker(다중·검색)를 쓰고, 값(id)은 디렉터리 사용자 참조다(서버 directoryUserIds 와 같은 값).
  • 노드에 저장하는 문자열은 참조에서 파생한다(raciNamesFromReferences) — 참조가 정본, 문자열은 표시·검색용 사본이다.
  • 저장 경로의 이름→참조 환산을 제거했다. resolveRaciUserIds 가 남은 곳은 레거시 다리 하나다 — 참조 없이 문자열만 있는 노드를 팝업에서 열 때 한 번 환산한다(5단계에서 제거).
  • 팝업을 열 때 참조 테이블(process.raciUserRefs)을 읽어 채우고, 문자열도 그 참조에서 다시 파생한다 — 선택기를 건드리지 않고 저장해도 노드 문자열이 지워지지 않는다(테스트로 고정).
  • 아직 표시·검색·CSV 는 문자열 사본을 읽는다(2단계 대상). 노드 상세·검색·내보내기 코드는 이번에 바꾸지 않았다.

2단계 적용 결과 (2026-10) ​

  • 규칙을 shared/raciReferenceNames.ts 로 올렸다 — 서버(목록 응답)와 클라이언트(선택기·폼)가 같은 변환 규칙을 쓴다. client/src/lib/raciReferences.ts 는 그 규칙을 재수출하고, 레거시 다리(resolveRaciUserIds)만 남긴다. 감사 모듈(shared/raciReferenceAudit.ts)도 같은 상수를 쓴다(중복 제거).
  • 노드 목록 응답(process.list)이 RACI 이름을 참조에서 조회 시점에 해석한다(server/services/processRaciNames.ts → attachDerivedRaciNames). 응답 필드 이름은 그대로 raciResponsible… 이라 표시·검색·CSV 가 코드 변경 없이 참조 기반으로 바뀐다.
  • 참조가 없으면 저장 문자열 폴백, 해석 실패 참조는 원문 유지. 참조 해석은 이메일·openId·표시 이름 어느 신원으로 와도 같은 사람으로 본다.
  • 저장 문자열 그대로가 필요한 경로(진단 스크립트 pnpm raci:diagnose)는 processRepository.listProcessNodes() 를 계속 쓴다 — 파생과 원본을 섞지 않는다.
  • 아직 남은 것: R 판정은 문자열 기반(3단계), 카탈로그 전송·가져오기는 참조를 옮기지 않음(4단계), 레거시 컬럼 제거(5단계).

3단계 적용 결과 (2026-10) ​

  • R(수정 권한) 판정도 참조 우선이 됐다 — shared/processAccess.ts 의 isResponsibleForProcess/canEditProcess 가 R 참조(process_node_raci_users)를 먼저 보고, 저장 문자열도 함께 본다(참조가 아직 없는 노드가 권한을 잃지 않게).
  • 참조 공급: 단일 노드 경로는 responsibleUserIdsOf(node.id)(server/processAccess.ts) 하나로 모았고 — 역할자 지정·수정·첨부 업로드/삭제·검토 요청·담당자 지정·전결규정 수정 판정 —, 목록 경로(결재선 권한)는 참조를 한 번에 읽어 노드별로 나눈다.
  • 공급(v2) 접근 판정(processNodeSource.decideNodeAccess)도 R/A 참조를 받는다. /api/v2/process-node-access 가 참조를 함께 넘긴다.
  • 남은 것: 카탈로그 전송·가져오기(4단계), 레거시 컬럼 제거(5단계).

4단계 적용 결과 (2026-10) ​

  • 전송 파일(PROCESS_CENTER_CATALOG)의 processes[] 에 raciUsers(역할·디렉터리 사용자 참조·순번) 를 싣는다(선택 필드 — 예전 파일도 그대로 읽힌다).
  • 가져오기는 노드를 넣은 뒤 process_node_raci_users 행을 함께 복원한다 — 다른 인스턴스로 옮겨도 A/R 권한이 살아남는다(D-R2).
  • 정리 규칙은 normalizeRaciReferences 하나다(역할 R/A/C/I 만·빈 값 제거·중복 축소·순서 고정) — 내보내기와 가져오기가 같은 함수를 쓴다.
  • 검증: 단위 테스트(catalogTransferService.test.ts) + dev DB 실데이터 확인(노드 L6-000001 의 참조 4행이 그대로 파일 형태로 매핑됨). 전체 내보내기(exportCatalogTransfer())는 DirectoryService(콘솔 dev 서버)가 떠 있어야 실행된다 — dev DB만으로는 그 단계까지 돌릴 수 없다(환경 제약).
  • 아직 남은 것: 엑셀/CSV 일괄 등록(processImport)은 여전히 이름 문자열만 넣는다 — 참조는 5단계 백필(이름→참조)이 함께 채운다.

5단계 1/4 — 백필 도구 적용 결과 (2026-10) ​

반영한 것(되돌릴 수 있는 단계)

  • shared/raciReferenceBackfill.ts — 백필 계획(순수 함수): 저장 문자열은 있는데 참조가 없는 노드-역할을 찾아 { 추가할 참조 } 와 { 해석 못 한 이름 } 을 낸다. 기존 참조는 순서 그대로 두고 없는 것만 더한다(멱등).
  • scripts/raci-reference-backfill.ts + pnpm raci:backfill — 기본 모의 실행, pnpm raci:backfill -- --apply 로 반영. 반영은 역할 단위 교체(replaceProcessNodeRaciUsers)를 써서 중복·순번을 저장소 규칙에 맡긴다.
  • 엑셀/CSV 일괄 등록(processCollectionService.importProcesses)이 이제 저장 문자열과 함께 참조도 남긴다 — 해석 못 한 이름은 문자열로만 남는다(백필과 같은 규칙).
  • 이름→참조 규칙은 shared/raciReferenceNames.raciReferencesFromNames 하나로 모았다(클라이언트 resolveRaciUserIds 도 이걸 쓴다).

검증

확인결과
단위 테스트raciReferenceBackfill.test.ts 4건(계획·멱등·미해석 보고·빈 입력) + 기존 30건 = 34/34
dev DB 모의 실행노드 246 · 참조 4 · 사용자 1 → 채울 것 0건(이미 정합) · 미해석 0건
반영 경로(롤백 검증)참조 없는 임시 노드 생성 → 계획 added: ["hohyun@gmail.com"] → 반영 후 R 참조 확인 → 트랜잭션 롤백으로 DB 무변경

아직 하지 않은 것(되돌릴 수 없거나 사람 판단이 필요)

  1. 문자열 컬럼 쓰기 중단 — 노드 저장이 여전히 문자열을 함께 쓴다(표시·검색·CSV 호환용).
  2. 읽기 폴백 제거 — raciStringsByRole·isResponsibleForProcess 의 문자열 폴백은 남겨 둔다(백필 미완 노드 보호).
  3. 컬럼 삭제 — 불가역이라 실행하지 않았다. 유예 릴리스 수·미해석 이름 처리(권장안 ①: 참조 원문 유지 + "해석 불가" 표시)를 정한 뒤 진행한다.

5단계 2~4/4 — 문자열 컬럼 은퇴 (2026-10) ​

2/4 쓰기 중단

  • 노드 생성·수정 입력(zod·서비스 타입)에서 raciResponsible/Accountable/Consulted/Informed 를 걷어냈다. RACI 는 saveRaciUsers(process_node_raci_users)로만 저장된다.
  • 엑셀/CSV 일괄 등록과 카탈로그 가져오기도 문자열 대신 참조만 남긴다(이름→참조는 raciReferencesFromNames).
  • 클라이언트 수정 폼은 RACI 문자열 상태를 버리고 참조(raciRefs)만 다룬다 — 표시는 목록 응답이 참조에서 해석한 이름을 쓴다.

3/4 읽기 폴백 제거

  • isResponsibleForProcess/isAccountableForProcess/canEditProcess/canApproveProcess 가 사용자 + 참조 목록만 받는다(노드 인자·문자열 폴백 제거). 호출부 8곳을 함께 바꿨다.
  • raciStringsByRole 은 참조에서만 이름을 해석한다(폴백 인자 제거).
  • v2 접근 판정은 목록 응답과 같은 주입 소스(listRaciUsers)를 쓴다.
  • 함께: 무관하게 깨져 있던 processPublishGovernance 테스트 드리프트(mock 누락·listProcessVersions 반환 형태)를 정정했다.

4/4 컬럼 제거

  • drizzle/schema.pg.ts·schema.mysql.ts 에서 process_nodes 의 RACI 문자열 컬럼 4개를 제거했다(스키마가 정본).
  • dev DB 에는 같은 ALTER TABLE … DROP COLUMN 을 적용했고, pnpm db:check-drift 로 없는 컬럼 0개를 확인했다(남은 경고는 기존 master_glossary_links 미생성 1건 — 이 전환과 무관).
  • 실데이터 확인: 컬럼이 사라진 뒤에도 목록 응답이 참조에서 이름을 해석한다 (L6-000001 → R/A/C/I = 윤호현, 저장 행에는 문자열 필드 없음).
  • 레거시 도구 은퇴: 진단 스크립트(raci:diagnose)·백필 스크립트(raci:backfill)·감사/백필 순수 모듈을 지웠다 — 비교할 문자열이 더 이상 존재하지 않는다(기록은 이 문서와 git 이력에 남는다).

마이그레이션 (배포 반영) — 2026-10

  • PG: drizzle/postgres-migrations/0027_drop_process_node_raci_strings.sql (journal 등록 → 배포 시 pnpm db:migrate 가 적용)
  • MySQL: drizzle/0036_drop_process_node_raci_strings.sql (같은 이름 규칙으로 동봉)
  • 재실행 안전: PG 는 DROP COLUMN IF EXISTS(선례 0025 와 같은 방식), MySQL 은 IF EXISTS 미지원이라 그대로 지운다(0000 에서 만든 컬럼).
  • 스냅샷 정합: 두 스냅샷에서 process_nodes 의 RACI 문자열 컬럼을 함께 지웠다(다음 generate 가 되돌리지 않게).
  • 적용 확인(dev DB): pnpm db:migrate → [database] PostgreSQL migrations applied successfully, drizzle.__drizzle_migrations 에 항목 추가(id 49, created_at=1792072513419), pnpm db:check-drift → 없는 컬럼 0개.
  • 생성 방법: 대화형 diff 프롬프트(master_glossary_links 생성/이름변경 질문)를 피하려고 drizzle-kit generate --custom 을 썼다 — 이 저장소의 선례(0027_master_glossary_links, 0036_master_glossary_links 는 저널 미등록 수동 파일)보다 안전하다.

남은 무관 이슈(기록)

  • master_glossary_links 는 선언만 있고 마이그레이션 저널에 없어 dev DB 에 생성되지 않았다(드리프트 경고 1건, 이 전환과 무관).
  • description/descriptionEn 컬럼도 선행 커밋(7bf6e72)에서 코드만 제거돼 스냅샷에 남아 있다(별건).

5. 결정이 필요한 것 (착수 전) ​

  1. 디렉터리 해석 실패 표시 — 퇴사자·미등록 사용자 참조를 화면에 어떻게 보여 줄지(참조 원문 / "해석 불가" 표시).
  2. D-R5 이름 매칭 제거 시점 — 사용자 identity 에서 name 을 빼면 이름 기반으로 저장된 옛 데이터가 권한을 잃는다. 백필과 함께 해야 한다.
  3. 문자열 컬럼 제거 시점 — 5단계는 되돌릴 수 없다. 몇 릴리스 유예를 둘지.
  4. 검색 전환 — 지금은 클라이언트 검색이 문자열을 본다. 참조 해석 기반으로 바꾸면 서버 검색으로 옮겨야 할 수 있다(노드·사용자 수에 따라).

6. 하지 않는 것 ​

  • 프로세스 데이터를 PortalConsole 로 옮기지 않는다 — 소유는 ProcessCenter 그대로다(공급 방식만 공통 규약을 따른다).
  • v2 공급 계약(raci[].directoryUserId)은 바꾸지 않는다 — 이미 목표 형태다.