프로세스 참조정보 계약 v2 (프로세스 노드 · RACI · 시스템 매핑)
상태: 확정 · 구현: v2 server/remoteProcessNodesV2.ts (v1 경로는 2026-09-20 제거 — 공용 로직은 server/processNodeSource.ts) · 테스트: server/remoteProcessNodesV2.test.ts(16건) · server/processNodeSource.test.ts(8건) 소유: ProcessCenter — 프로세스는 L2 참조정보(2차 기준정보)이며, 소유는 이 도메인에 둔다. 공통 규약: portal-foundation docs/REFERENCE_DATA_SUPPLY_CONVENTION.md (L2 공급 5요소) 관련 문서: docs/PROCESS_NODE_GOVERNANCE_PLAN.md(게시 거버넌스), docs/EXPLORE_ADAPTER_CONTRACT.md(계약 문서 형식 선례), docs/DIRECTORY_AUTHORIZATION_INTEGRATION.md(L1 인용)
1. 위치 — L2 참조정보이고 소유는 ProcessCenter 다
- L1 최상단 기준정보(조직도·임직원·부서코드)의 정본은 PortalConsole 디렉터리다. 프로세스는 그 위에 얹힌 L2 다.
- 프로세스는 도메인 안에서는 운영 기준정보이고, 밖에서는 참조 기준이다 — 신규 시스템이 어느 노드에 매핑되는지 식별하고, 인력 구조를 프로세스 뷰로 보는 교차 조회의 축이 된다.
- 그래서 프로세스 데이터를 PortalConsole 로 옮기지 않는다. 공급 방식만 공통 규약을 따른다(읽기 전용·서버 간·표준 식별자·버전/변경 피드·온보딩).
이미 확보된 L1 인용 재료 (재사용)
| L1 | 프로세스 측 인용처 | 상태 |
|---|---|---|
| 부서코드 | process_nodes.organizationCode | ✅ 있음(레거시 organization 폐기 예정) |
| 임직원 | process_node_raci_users.directoryUserId | ✅ 있음((processId, raciRole, directoryUserId) 유니크) |
| 시스템 코드 | process_nodes.systemId → systems.code(전역 유니크) | ✅ 있음(노드 → 시스템 방향) |
RACI 는 참조가 정본이다(2026-10). 노드에 복사해 두던 이름 문자열 컬럼(
raciResponsible…)은 제거했다. 목록 응답의raciResponsible… 필드는 참조에서 조회 시점에 해석한 값이라 이름이 바뀌면 화면·검색·CSV 가 따라간다. 경위:docs/RACI_REFERENCE_MIGRATION_PLAN.md
2. 이전 v1 계약과 결함 (2026-09-20 제거 — 기록)
v1 경로(
/api/v1/process-nodes*,server/remoteProcessNodes.ts)는 제거했다. 소비자(AgentCenter)가 v2 로 전환을 마쳤고, 아래 결함 D1·D2 를 안은 채로 남겨둘 이유가 없었다. 공용 로직(buildTreeIndex·decideNodeAccess)은server/processNodeSource.ts로 옮겼다.
경로 (인증 Authorization: Bearer <AGENT_CENTER_API_KEY>, 실패 시 401 process_node_service_auth_required)
| 메서드 | 경로 | 응답 |
|---|---|---|
| GET | /api/v1/process-nodes?taxonomyId=&cursor=&changedSince= | { nodes, nextCursor?, snapshotToken, complete } |
| GET | /api/v1/process-nodes/{sourceSystemId}:{taxonomyId}:{externalNodeId} | ProcessNode | 404 |
| POST | /api/v1/process-node-access | { decisions, validUntil } |
taxonomyId 는 L1 루트 노드의 code 다. 노출 상태·RACI 판정은 docs/PROCESS_NODE_GOVERNANCE_PLAN.md 를 따른다.
결함 (참조정보로 쓰기 전에 고쳐야 한다)
| # | 결함 | 근거(코드) | 영향 |
|---|---|---|---|
| D1 | 표준 식별자가 내부 대리 키다 | toProcessNodePayload: externalNodeId: String(node.id), parentExternalNodeId·pathIds 도 id 문자열. findNodeByKey: Number.parseInt(key.externalNodeId) | DB 재적재·병합 시 id 가 바뀌면 참조가 끊긴다. 신규 시스템 매핑처럼 오래 남는 참조에 부적합 |
| D2 | 게시 거버넌스 미반영 | pageTaxonomyNodes 는 taxonomy 필터만 적용 — status 가 DRAFT/IN_REVIEW 인 노드도 목록에 실린다(PORTAL_VISIBLE_STATUSES 는 decideNodeAccess 에만 적용) | 작성 중 값이 참조 기준으로 퍼진다. 게시본(process_node_versions)과 어긋날 수 있다 |
code 에 유일성 제약이 없다 | 스키마 선언(codeUnique: uniqueIndex("process_nodes_code_unique")), PostgreSQL 마이그레이션(postgres-migrations/0000_majestic_tempest.sql:396), MySQL 마이그레이션(0001_fuzzy_ogun.sql:24), 적용 DB(dev DB pg_indexes 확인)에 모두 있다. 중복 0건 | 처음엔 컬럼 정의만 보고 "제약 없음"으로 적었으나 사실이 아니다 — 제약은 마이그레이션 0000/0001 부터 있었다. v2 는 유일성 작업 없이 code 식별자로 갈 수 있다 |
D1·D2 는 계약 v2 에서 해소한다. D3 은 위 정정대로 결함이 아니었다(없는 작업을 만들지 않는다).
3. v2 계약 (확정 제안)
3.1 표준 식별자 — 업무 코드 기반
/** 참조 표준 식별자. 소비자는 이 세 값으로 노드를 지목한다. */
type ProcessNodeRefV2 = {
sourceSystemId: string; // "process-center"
taxonomyCode: string; // L1 루트 노드의 code (분류 축)
nodeCode: string; // 노드의 업무 code (전역 유일)
};- 대리 키(id)는 계약에서 제거한다(D1).
parentNodeCode·pathCodes[]로 계층을 표현한다. - 확정(2026-09-20) — 유일성은 전역 유일:
process_nodes.code의 전역 unique 제약은 이미 적용되어 있다(process_nodes_code_unique— 스키마 선언·마이그레이션 0000/0001·dev DB 확인). 신규 마이그레이션은 필요 없다(dev DB 실측: 노드 19건 · 중복 0건).taxonomyCode(L1 루트)는 분류·스코프 속성으로 유지하고 식별의 필수 요소로 두지 않는다. - 확정 — 발급 규칙(dev DB 실측 2026-09-20): 코드는
<레벨>-<6자리 순번>이다(L1-000001,L2-000001…). 부모 코드를 접두로 쓰지 않는다 — 부모 관계는 내부parentId로만 연결하고 계약에서는parentNodeCode로 표현한다. L1 루트 코드가 분류 축(taxonomyCode)이다. (규칙 전문:docs/PROCESS_NODE_GOVERNANCE_PLAN.md§9.1) - 확정 — 게시 후
code변경 금지(서버 강제): 게시본이 있는 노드(publishedVersion != null)는 저장 시code변경을 서버가 거부한다. 정책 문서만으로 두지 않는다. 불가피한 변경은 새 코드를 발급하고 기존 코드를 승계(successorNodeCodes)로 연결한다. - 업무 코드는 식별자가 아니다(확정 2026-09-20): 사람이 읽는 업무 코드(
IT-OPS등)는 별칭 필드(businessCode)로 분리한다(거버넌스 §9.4). 소비자는 별칭으로 노드를 참조하지 않는다 — 참조는taxonomyCode·nodeCode로 한다(개편 때 별칭이 바뀔 수 있다). process_nodes.code는 이미 검색(like)에 쓰이므로 값 자체는 재사용 가능하다. 바뀌는 것은 참조용 식별자로 승격된다는 점이다.
3.2 노출 기준 — 게시본만 (D2 해소)
publishedVersion이 있는 노드(게시 이력 보유)만 공급한다.DRAFT·IN_REVIEW는 어떤 경로로도 내보내지 않는다.- 응답에
status(ACTIVE/INACTIVE/RETIRED)와publishedVersion을 함께 싣는다 — 소비자가 "게시된 기준정보인지"를 판단한다. snapshotToken은 taxonomy별 게시 버전 기준으로 만든다(pc:{taxonomyCode}:{publishedVersion}).- 게시 거버넌스 정의는
docs/PROCESS_NODE_GOVERNANCE_PLAN.md를 따른다(게시본 불변 스냅샷).
3.3 L1 인용 필드 (재정의 금지)
| 필드 | 인용 대상 | 규칙 |
|---|---|---|
organizationCode | 디렉터리 부서코드 | 없으면 null — 이름을 복사하지 않는다 |
raci[].directoryUserId | 디렉터리 사용자 식별자 | process_node_raci_users 그대로, 역할은 R/A/C/I 로 유지 |
systemCode | systems.code | 노드 → 시스템 참조(코드). 시스템 이름은 소비자가 필요 시 조회 |
successorNodeCodes[] | 같은 taxonomy 의 노드 코드 | 필드만 먼저 둔다 — 값은 프로세스 병합/승계 기능을 만들 때(폐기·병합 승인 시) 채운다. 소비자 규칙(확정): 참조 대상이 RETIRED 이고 승계가 있으면 승계를 따라가고, 승계가 없으면 그 참조를 무효로 표시한다(조용히 다른 노드를 가리키지 않는다) |
3.4 엔드포인트
| 메서드 | 경로 | 용도 |
|---|---|---|
| GET | /api/v2/process-nodes?taxonomyCode=&cursor=&changedSince= | 스냅샷·증분 목록(게시본만) |
| GET | /api/v2/process-nodes/{taxonomyCode}:{nodeCode} | resolve/검증 — 신규 시스템이 "이 노드에 매핑"을 확인할 때 |
| POST | /api/v2/process-node-access | 사용자별 VIEW/REVIEW 판정(코드 기준) |
| GET | /api/v2/systems | 시스템·시스템 그룹 목록(systems.code 전역 유니크) — 노드의 systemCode 가 가리키는 정본. "신규 시스템이 어느 노드에 매핑되는가"를 코드로 잇는 축 |
경로 prefix 를 v2 로 올렸다. v1 은 제거했다(§2·§4) — v1 응답 필드(externalNodeId 등 id 기반)는 더 이상 존재하지 않는다.
3.5 응답 스키마
{
"nodes": [
{
"sourceSystemId": "process-center",
"taxonomyCode": "IT-VC",
"nodeCode": "IT-VC-02",
"parentNodeCode": "IT-VC",
"pathCodes": ["IT-VC", "IT-VC-02"],
"levelCode": "L4",
"name": "가상화 운영",
"status": "ACTIVE",
"publishedVersion": 3,
"sourceVersion": "3",
"organizationCode": "DEPT-IT-001",
"businessCode": "IT-OPS",
"raci": [{ "raciRole": "A", "directoryUserId": "hong.gd" }],
"systemCode": "SYS-VMWARE",
"successorNodeCodes": [],
"updatedAt": "2026-09-20T00:00:00.000Z",
"sourceUrl": "/processes?node=IT-VC-02"
}
],
"nextCursor": null,
"snapshotToken": "pc:IT-VC:3",
"complete": true
}complete는 한 응답에 전체 게시 스냅샷이 담겼을 때만true다(기존 규칙 유지 — 증분 응답을 삭제 판정에 쓰지 않는다).sourceUrl도 코드 기준으로 바꾼다(포털 화면이 코드로 딥링크를 만들 수 있어야 한다).- 부모가 게시본이 아닐 수 있다: 게시되지 않은 상위 노드의 코드라도
parentNodeCode·pathCodes에는 담는다(계층 식별용). 소비자는 "목록에 없는 부모 코드"를 오류로 보지 않는다 — 그 노드는 아직 게시되지 않았을 뿐이다.
구현(2026-09-20): 라우트는 server/remoteProcessNodesV2.ts(registerRemoteProcessNodeV2Routes), server/app.ts 에서 v1 과 함께 등록한다. dev DB 실측 검증 — taxonomy L1-000001 목록에 게시본 1건만 반환되고 게시본이 아닌 노드 누출 0건, resolve 200, snapshotToken=pc:L1-000001:1.
3.6 인증 · 스코프 (퍼블릭 아님)
| 항목 | 규칙 |
|---|---|
| 인증 | Authorization: Bearer <소비자 서비스 키> — 서버 간 전용, 브라우저 노출 금지 |
| 자격증명 | 소비자별 키. 현재 단일 키(AGENT_CENTER_API_KEY)를 소비자 무관 자격증명으로 일반화하고, 소비자마다 발급·회수 |
| 스코프 | process:read (필요 시 process:access) — 조회 외 권한 없음 |
| 발견·온보딩 | PortalControlCenter 카탈로그에 capability 등록 → 신규 소비자는 카탈로그 발견 + 키 발급만으로 온보딩 |
| 접근 정책 | 사용자 단위 판정은 ProcessCenter 가 최종 적용(process-node-access, RACI) — 소비자 화면 숨김에 의존하지 않는다 |
| 참조 추적 | 어떤 소비자가 어떤 노드를 참조하는지 추적 가능해야 한다 — 기록 범위는 아래 확정 참조 |
확정(2026-09-20) — 자격증명 이행 순서: P0 는 ProcessCenter 가 소비자별 키를 자체 관리한다. 설정은 PROCESS_NODE_CONSUMERS_JSON(데이터)이고, v1 키 AGENT_CENTER_API_KEY 는 전환 기간 동안 두 스코프를 가진 소비자로 자동 승격한다. PortalControlCenter 카탈로그 연계는 후속이다 — 카탈로그 등록 메커니즘이 없는데 발급을 카탈로그로 몰아넣으면 신규 소비자를 붙일 수 없다.
# PROCESS_NODE_CONSUMERS_JSON=[{"consumerId":"agent-center","secret":"<키>","scopes":["process:read","process:access"]}]
# scopes 생략 시 두 스코프를 모두 갖는다. JSON 이 깨지면 레거시 키로 폴백하고 경고를 남긴다.스코프 매핑: process:read = 목록·resolve·systems, process:access = process-node-access.
발급 · 순환 절차 (ProcessCenter 에서 실행)
# 발급: 출력된 JSON 한 줄을 PROCESS_NODE_CONSUMERS_JSON 에 넣고 재시작한다
pnpm process-node:consumer-key -- --consumer agent-center --scopes process:read,process:access
# 무중단 순환: 새 키를 추가(옛 키 유지) → 소비자를 새 키로 전환 → 옛 키 제거
pnpm process-node:consumer-key -- --consumer agent-center --scopes process:read,process:access --rotate
pnpm process-node:consumer-key -- --consumer agent-center --remove <옛 secret>
# 현재 목록(비밀은 끝 4자만 출력)
pnpm process-node:consumer-key -- --list- 비밀은 발급 시 한 번만 출력된다 — 소비자 쪽 설정에 바로 넣고 저장소·문서에 남기지 않는다.
- 순환 기간에는 같은
consumerId로 여러 secret 이 함께 유효하다(PROCESS_NODE_CONSUMERS_JSON에 두 항목). - 소비자 측 확인:
pnpm verify:process-integration(AgentCenter) — 상대 서버가 기동을 마친 뒤 실행한다(연결 실패는 종료 코드 2). - 회수:
--remove <secret>로 항목을 빼고 재시작한다(그 키는 즉시 401).
확정 — 참조 추적 기록 범위: 어느 소비자 키가 어느 노드를 resolve/조회했는지의 구조화 요약만 남긴다(원문·비밀정보 없음, §14.1 정신). 보존 기간은 사내 정책 확정 전까지 값을 두지 않는다 — 로그 스키마만 준비하고 임의 보존일수를 코드에 넣지 않는다.
카탈로그 등록 값(예) — 탐색 어댑터가 쓰는 등록 형식(docs/EXPLORE_ADAPTER_CONTRACT.md §2)과 같은 자리에 참조 capability 로 등록한다. 구체 등록 키·발급 절차는 §5-3 확정 대상이다.
{
"portalId": "process-center",
"capabilities": ["reference"],
"reference": {
"kind": "process-nodes",
"endpoint": "/api/v2/process-nodes",
"resolveEndpoint": "/api/v2/process-nodes/{taxonomyCode}:{nodeCode}",
"authMode": "service_token",
"scopes": ["process:read"],
"l1Citations": ["organizationCode", "directoryUserId", "systemCode"]
}
}4. v1 → v2 이행
원칙: 병행 제공 후 소비자 전환, 마지막에 v1 제거. 참조가 끊기면 소비자 매핑이 고아가 되므로 한 번에 바꾸지 않는다.
| 단계 | 내용 | 완료 조건 |
|---|---|---|
| 1 | 식별자 기반 정비 — 유일성 제약은 기존재(작업 없음). 발급 규칙 문서화, 게시 후 code 변경 금지 서버 가드 배선 | 가드 테스트 통과 + 중복 0건 유지 |
| 2 | v2 경로 구현 — /api/v2/process-nodes* + /api/v2/systems, 게시본 기준 노출, code 기반 payload, 소비자별 키(자체 관리) | v2 테스트 통과, v1 무변경 |
| 3 | 소비자 전환 — AgentCenter 를 v2 로 전환(shared/processPortal.ts 의 NodeKey·ProcessNode, 어댑터, 테스트) | 소비자 회귀 테스트 통과 |
| 4 | v1 정리 — externalNodeId(id) 경로 제거, 계약 문서·integration-contract 갱신 | ✅ 완료(2026-09-20) — 라우트 제거, 공용 로직은 server/processNodeSource.ts, v1 테스트는 processNodeSource.test.ts 로 이관 |
이행 완료. 소비자(AgentCenter)는 v2 를 쓰고, v1 경로·payload 는 저장소에 없다.
전환 기간에는 v1 과 v2 응답이 서로 다른 식별자 체계를 갖는다(같은 노드가 id 로도 code 로도 표현됨). 이 기간에 소비자 매핑을 새로 만들지 않는다 — 만들면 두 체계가 섞인다.
소비자(AgentCenter) 측 변경 범위
shared/processPortal.ts:NodeKey→{ sourceSystemId, taxonomyCode, nodeCode },ProcessNode계약 필드 교체server/adapters/process-portal/processCenter.ts: 경로 v2 + payload 매핑- 화면(
client/src/pages/ProcessAgents.tsx)은trpc.processNodes.*계약을 유지하므로 렌더러 불변 - 환경값:
PROCESS_PORTAL_API_KEY→ 소비자별 키로 교체(키 이름 확정 시 반영)
5. 확정된 결정 (2026-09-20)
| # | 항목 | 확정 |
|---|---|---|
| 1 | code 유일성 범위 | 전역 유일 — 이미 적용됨(process_nodes_code_unique, 중복 0건 확인). 신규 마이그레이션 없음. taxonomyCode 는 분류·스코프 속성으로 유지(식별 필수 요소 아님) |
| 2 | 코드 체계·변경 | 발급 규칙(<레벨>-<6자리 순번>) 문서화, 게시 후 code 변경은 서버가 거부, 예외는 새 코드 발급 + 승계 연결 — 배선 완료(2026-09-20): updateProcessNode 경계에서 거부, 기동 코드 정규화는 게시본 제외 |
| 3 | 서비스 키 발급·순환 | P0 는 ProcessCenter 자체 관리(소비자별 키 + process:read, 데이터로 관리) → 후속 PortalControlCenter 카탈로그 연계 |
| 4 | 승계 | successorNodeCodes 는 필드만 먼저 둔다. 값은 폐기·병합 승인 시 채우고, 소비자는 승계 추종/무효 표시 규칙을 따른다 |
| 5 | 참조 추적·감사 보존 | 구조화 요약만 기록. 보존 기간은 사내 정책 대기(임의 값 없음, 로그 스키마만 준비) |
| 6 | systems 공급 | 함께 공급 — GET /api/v2/systems(시스템·그룹, code 전역 유니크) |
| 7 | 업무 코드 | 별칭 필드로 분리(확정 2026-09-20) — code 는 시스템 발급 순번(참조 정본), 사람이 읽는 업무 코드는 businessCode(표시용·전역 유일, 식별자 아님). D2 대상 아님 |
남은 외부 의존은 하나다 — 참조 추적 보존 기간(사내 정책). 그 전까지 추적 로그는 스키마만 두고 보존·삭제 정책을 코드에 넣지 않는다.
6. 검증 계획 (구현 시)
| 대상 | 검증 |
|---|---|
| 식별자 | process_nodes_code_unique 기존재 확인 + 중복 0건(pnpm db:check-code-uniqueness, dev DB 실행 결과 19건·중복 0), resolve 가 code 로만 동작(id 미사용) |
| 코드 불변 | 게시본 보유 노드(publishedVersion != null)의 code 변경 시도가 거부됨 |
| systems | /api/v2/systems 가 시스템·그룹·code 를 주고, 노드 systemCode 와 조인 가능 |
| 게시본 | DRAFT/IN_REVIEW 노드가 목록·resolve·access 어디에도 나오지 않음 |
| L1 인용 | organizationCode·directoryUserId·systemCode 가 값 복사 없이 참조로만 전달됨 |
| 변경 피드 | changedSince 증분 응답이 complete=false, 삭제 판정에 쓰이지 않음 |
| 승계 | 폐기 노드 참조 시 successorNodeCodes 로 이어짐(규칙 확정 후) |
| 인증 | 소비자별 키·스코프, 미인증 401, 스코프 부족 403, 퍼블릭 비노출 |
| 접근 정책 | 사용자별 VIEW/REVIEW 판정이 v1 과 동일 결과를 냄(회귀) |
| 소비자 | AgentCenter v2 전환 후 프로세스 트리·매핑 화면 회귀 테스트 통과 |