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. 지금 상태 (실측)
| 계층 | 참조(정본) | 이름 문자열(레거시 중복) |
|---|---|---|
| DB | process_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-R1 | R(수정 권한) 판정이 이름 문자열 기반 — 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. 목표 계약 (제안)
- 정본은 참조 하나 —
process_node_raci_users.directoryUserId(L1 인용). 노드의 문자열 컬럼은 레거시 읽기 전용으로 내린다. - 표시·검색·내보내기는 조회 시점 해석 — PC 는 디렉터리 사용자 프로필을 이미 로컬에 갖고 있다(
directory_user_profiles,organizationRepository.listUsersWithOrganization) → 조인 해석으로 처리하고 이름을 저장하지 않는다. - 권한 판정(R·A)은 참조만 본다. 문자열 폴백은 유예 기간에만 남기고 마지막에 제거한다.
- 공급(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 무변경 |
아직 하지 않은 것(되돌릴 수 없거나 사람 판단이 필요)
- 문자열 컬럼 쓰기 중단 — 노드 저장이 여전히 문자열을 함께 쓴다(표시·검색·CSV 호환용).
- 읽기 폴백 제거 —
raciStringsByRole·isResponsibleForProcess의 문자열 폴백은 남겨 둔다(백필 미완 노드 보호). - 컬럼 삭제 — 불가역이라 실행하지 않았다. 유예 릴리스 수·미해석 이름 처리(권장안 ①: 참조 원문 유지 + "해석 불가" 표시)를 정한 뒤 진행한다.
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. 결정이 필요한 것 (착수 전)
- 디렉터리 해석 실패 표시 — 퇴사자·미등록 사용자 참조를 화면에 어떻게 보여 줄지(참조 원문 / "해석 불가" 표시).
- D-R5 이름 매칭 제거 시점 — 사용자 identity 에서
name을 빼면 이름 기반으로 저장된 옛 데이터가 권한을 잃는다. 백필과 함께 해야 한다. - 문자열 컬럼 제거 시점 — 5단계는 되돌릴 수 없다. 몇 릴리스 유예를 둘지.
- 검색 전환 — 지금은 클라이언트 검색이 문자열을 본다. 참조 해석 기반으로 바꾸면 서버 검색으로 옮겨야 할 수 있다(노드·사용자 수에 따라).
6. 하지 않는 것
- 프로세스 데이터를 PortalConsole 로 옮기지 않는다 — 소유는 ProcessCenter 그대로다(공급 방식만 공통 규약을 따른다).
- v2 공급 계약(
raci[].directoryUserId)은 바꾸지 않는다 — 이미 목표 형태다.