프로세스 노드 게시 거버넌스 — 설계·작업 계획
작성 시각: 2026-09-13 범위: ProcessCenter 노드 기본정보 화면 + 노드 버전/게시 승인 워크플로우(스키마·서버·클라이언트) 관련 문서: docs/ANT_DESIGN_REFACTOR.md(공용 UI 전환), docs/COMMON-UI-HANDOFF.md(공용 컴포넌트 인수인계)
이 문서는 프로세스 노드의 기본정보 재구성과 버전·게시 승인 워크플로우의 확정 설계와 진행 상태를 기록한다. 새 세션은 이 문서만 읽고 이어서 진행할 수 있다.
1. 목표
- 노드 상세의 기본정보를 요약형으로 재구성해 읽기 화면의 밀도를 낮춘다(RACI는 4행 표 → 1줄 칩).
- 노드 편집을 드래프트 → 검토 요청 → 승인(게시) 절차로 통제하고, 게시 시점의 불변 게시본(스냅샷) 을 남긴다.
- 승인 권한은 노드의 A(Accountable) 담당자가 갖고, 관리자(
admin)는 예외적으로 허용한다.
2. 확정 결정 (사용자 승인, 2026-09-13)
| # | 항목 | 결정 | 구현 결과 |
|---|---|---|---|
| 1 | 게시본 불변성 | 버전 스냅샷(불변) | process_node_versions는 게시 시 INSERT만 수행하고 수정 경로를 두지 않는다. 노드 행은 드래프트(현재값), 이력은 게시본이 담당한다. |
| 2 | A 승인자 식별 | 사용자 참조 테이블 | process_node_raci_users(raciRole='A', directoryUserId)로 승인자를 보관하고, 판정 시 사용자 식별자(name/email/loginId/openId/id)와 일치 비교한다. 참조가 없으면 레거시 문자열(raciAccountable)로 폴백한다. |
| 3 | 승인 깊이 | 전결규정 결재선 재사용 | 게시 승인 시 resolveApprovalPolicyForL6(L6/L7 노드)로 유효 정책을 해석해 게시본 스냅샷에 결재선을 함께 기록한다. 정책 단계 참여자는 roleMasterId·participantLabel(라벨)이라 사용자 단위 권한은 A 참조로 판정한다(라벨→사용자 매핑은 후속). |
| 4 | 상태 enum | IN_REVIEW 추가 + ACTIVE=게시 | DRAFT / IN_REVIEW / ACTIVE / INACTIVE / RETIRED (기존 데이터·화면 호환 유지). |
3. 데이터 모델
| 대상 | 변경 | 비고 |
|---|---|---|
process_nodes | status에 IN_REVIEW 추가, publishedVersion(마지막 게시 버전), submittedAt(검토 요청 시각), approvedBy/approvedAt(게시 승인자·시각) 추가 | version은 게시 시 증가 |
process_node_versions | 신설 (processId, version) 유니크, payload(게시본 JSON), changeSummary, createdBy, approvedBy, approvedAt | 게시본 불변 스냅샷 |
process_node_raci_users | 신설 (processId, raciRole, directoryUserId) 유니크, sortOrder | R/A/C/I 사용자 참조 |
portal_notifications | 신설 (portalId, recipientId, dedupeKey) 유니크, kind/title/body/priority/processId/readAt | 포털 공통 알림함(PF @hohyun94/portal-operations 계약 준수, 중복 차단) |
process_node_versions (AI) | aiSummary·aiSummaryDetail(bullets·changedAreas·riskLevel)·aiSummaryModel·aiSummaryStatus·aiSummaryGeneratedAt·aiSummaryInputHash 추가 | AI가 직전 게시본과 비교한 변경 요약(참고용 · 확정은 사람) |
process_histories.action | submit_review / approve / reject / retire 추가 | 이력 전용 액션 |
마이그레이션: drizzle/0027_process_node_publish_approval.sql, drizzle/0028_numerous_lily_hollister.sql(MySQL), drizzle/postgres-migrations/0014_process_node_publish_approval.sql(PostgreSQL, 멱등 DDL). PostgreSQL 개발 DB 적용 완료.
검증 범위(2026-09-13 결정): 현 단계는 PostgreSQL만 검증한다. MySQL 마이그레이션(0027·0028)은 파일만 유지하고 생성·적용·검증을 수행하지 않는다(후속, §8-3). 새 마이그레이션도 PostgreSQL(
pnpm db:postgres:generate)만 대상으로 만들고, PostgreSQL DDL은 재실행에 안전하도록 멱등(IF NOT EXISTS)하게 작성한다.DB 전략(2026-09-13 확정): 트랜잭션/업무 데이터는 MySQL·PostgreSQL 이중 지원을 유지하고, 벡터 검색(RAG)만 PostgreSQL+pgvector 또는 외부 벡터 저장소로 분리한다. AI 완성(요약·번역·정합성)은 DB 중립이다 →
docs/DB_STRATEGY.md.
4. 상태 흐름과 권한
DRAFT ──검토 요청──▶ IN_REVIEW ──승인(게시)──▶ ACTIVE ──폐기──▶ RETIRED
▲ │ ▲
└────── 반려 ─────────┘ INACTIVE ──폐기──────────┘| 동작 | 허용 | 상태 조건 |
|---|---|---|
| 생성·수정·첨부·RACI 지정 | R 담당자 · 관리자 | IN_REVIEW·RETIRED에서는 잠금 |
| 검토 요청 | R 담당자 · 관리자 | DRAFT/ACTIVE/INACTIVE |
| 승인·게시 | A 승인자 · 관리자 | IN_REVIEW |
| 반려 | A 승인자 · 관리자 | IN_REVIEW → DRAFT |
| 폐기 | A 승인자 · 관리자 | RETIRED가 아닌 상태 |
IN_REVIEW전이는 전용 절차로만 가능하다(일반 수정 API로 상태만 바꾸는 우회 차단).- 게시 시
version/publishedVersion을 1 증가시키고 게시본 스냅샷을 남긴다.
5. API 표면 (process 라우터)
| 프로시저 | 종류 | 입력 | 설명 |
|---|---|---|---|
submitForReview | mutation | { id, changeSummary? } | 검토 요청(IN_REVIEW) |
approve | mutation | { id, changeSummary? } | 승인·게시(게시본 확정) |
reject | mutation | { id, changeSummary? } | 반려(DRAFT 복귀) |
retire | mutation | { id, changeSummary? } | 폐기(RETIRED) |
versions | query | { id } | 게시본 이력(최신순, 스냅샷 본문 제외) |
raciUserRefs | query | { id } | RACI 사용자 참조 목록 |
saveRaciUsers | mutation | { id, raciRole, directoryUserIds[] } | 역할 단위 참조 교체(검토 중·폐기 잠금) |
inbox | query | — | 내 결재함(파생) — IN_REVIEW ∧ 내가 A 승인자인 노드 목록 |
notifications.list | query | { unreadOnly?, limit? } | 내 알림함(PF 계약 저장형, 최신순) |
notifications.unreadCount | query | — | 미읽음 알림 수(배지) |
notifications.markRead | mutation | { ids? , all? } | 읽음 처리(대상은 로그인 사용자 식별자로 한정) |
analyzeVersion | mutation | { id, version } | 게시본 AI 변경 재분석(A 승인자·관리자, PF @hohyun94/portal-ai) |
6. 구현 현황
- [x] Phase A — 기본정보 요약형: 상세를 식별 정보 / 설명 / 담당(담당조직 + RACI 칩)으로 재구성, 편집 팝업 라벨 정렬 (
ProcessWorkspace.tsx,ProcessManagementPanel.tsx) - [x] Phase B — 스키마·마이그레이션: 3개 마이그레이션 + 개발 DB 적용·검증
- [x] Phase C — 서버:
shared/processAccess.ts(승인 판정·편집 잠금),server/processAccess.ts,server/repositories/processGovernanceRepository.ts,server/services/processGovernanceService.ts, 라우터 7개 프로시저, 일반 수정/첨부 잠금 가드 - [x] Phase D — 클라이언트:
client/src/components/process/ProcessGovernanceBar.tsx(상태 배지 + 검토 요청/승인·게시/반려/폐기) 를 노드 상세 헤더에 연결 - [x] Phase E — 사용자 참조·이력 UI:
- 편집 저장 시 RACI 사용자 참조를 함께 저장(
client/src/lib/raciReferences.ts→saveRaciUsers, 이메일 우선·openId 폴백) — A 승인자 판정이 사용자 참조로 동작 - 상세 패널에 게시본 이력 추가(
client/src/components/process/ProcessVersionHistory.tsx→process.versions)
- 편집 저장 시 RACI 사용자 참조를 함께 저장(
- [x] Phase F — 알림(1단계 + 2단계):
- 1단계 파생 인박스(무저장):
process.inbox—IN_REVIEW∧ 내가 A 승인자인 노드(권한 판정 재사용) - 2단계 저장형 인앱 알림함: PF
@hohyun94/portal-operations(v0.1.0) 계약 편입 →portal_notifications+dispatchProcessNotification(중복 차단) · 검토 요청 시 A 승인자, 승인·반려 시 R 담당자에게 발송 - UI:
client/src/components/process/ProcessNotificationCenter.tsx(노드 상세 헤더의 알림 배지 + 결재 대기/알림 목록 + 읽음 처리 + 노드 이동)
- 1단계 파생 인박스(무저장):
- [x] Phase G — AI 게시본 차이 자동 분석:
- 게시 확정 시 직전 게시본(v{N-1}) vs 신규(v{N}) 스냅샷을 비동기로 비교해
process_node_versions에aiSummary·aiSummaryDetail(bullets·changedAreas·riskLevel)·aiSummaryModel·aiSummaryStatus·aiSummaryGeneratedAt·aiSummaryInputHash기록 - PF 공통 계층(
@hohyun94/portal-ai0.1.0) 사용 — OpenAI 호환 엔드포인트라 사내 LLM과 외부 클라우드 API를 환경변수만 바꿔 지원 - AI 미설정·실패·변경 없음은
SKIPPED·FAILED로만 남기고 게시 절차와 수동 요약은 그대로 유지(A 승인자·관리자만process.analyzeVersion으로 재분석) - UI: 게시본 이력에
PortalAiSummaryCard(요약·불릿·영향 영역·모델·시각·다시 분석) 표시
- 게시 확정 시 직전 게시본(v{N-1}) vs 신규(v{N}) 스냅샷을 비동기로 비교해
- [ ] Phase H — 후속: 결재선 라벨→사용자 매핑, 메일 채널, PF 셸 공용 알림 슬롯 승격, MySQL 마이그레이션 배포 적용
7. 검증
| 항목 | 결과 |
|---|---|
pnpm check(tsc) | 0 errors |
server/processPublishGovernance.test.ts | 검토 요청·게시·반려·폐기·이력·결재선 기록 검증 |
server/processAccess.test.ts | A 승인 판정(참조/레거시/관리자)·편집 잠금 검증 |
client/.../ProcessGovernanceBar.test.tsx | 상태별 버튼 노출·배지 검증 |
client/.../ProcessVersionHistory.test.tsx | 게시본 이력 표시·빈 상태 검증 |
client/src/lib/raciReferences.test.ts | 피커 선택값 → 사용자 참조 환산·원문 보존 검증 |
server/notificationService.test.ts | 수신자 없음·중복 차단·다중 수신자·내 알림함/읽음 처리 검증 |
server/processVersionAiService.test.ts | AI 비활성(SKIPPED)·변경 없음·정상 요약(불릿·영향·해시)·실패(FAILED) 시 게시 이력 유지 검증 |
client/.../ProcessVersionHistory.test.tsx | 게시본 이력 + AI 요약 카드(요약·불릿·모델) 표시 검증 |
client/.../ProcessNotificationCenter.test.tsx | 배지 합산·패널 목록·노드 이동·읽음 처리 검증 |
PostgreSQL 실 DB 스모크(pnpm db:postgres:verify-governance, 트랜잭션 롤백) | 참조 저장(중복 제거)·게시본 생성·상태 전이(IN_REVIEW)·알림 발송/중복 차단/읽음 처리까지 실제 PostgreSQL에서 실행 후 롤백 — 상태/행 불변 확인 |
| 개발 컨테이너 | 마이그레이션 적용 후 http://localhost:3000/ 200 응답 |
8. 후속 작업 메모
- 결재선 사용자 매핑: 전결규정 단계의
participantLabel/roleMasterId를 사용자·역할과 연결해 사용자 단위 승인선까지 확장. - 알림 확장: 메일 채널(
NotificationChannel구현 추가)과 PF 셸 공용 알림 슬롯(알림센터) 승격 — 저장·중복 차단은 이미 PF 계약을 따르므로 채널/UI만 교체하면 된다. - 배포(MySQL, 범위 밖): MySQL 마이그레이션(0027·0028)은 파일만 유지한다. MySQL 환경 생성·적용·검증은 PostgreSQL 검증이 끝난 뒤 별도로 진행한다.
- 개발 스택 주의: 컨테이너 시작 시 마이그레이션이 실행되므로 새 마이그레이션은 멱등하게 작성한다(이미 적용된 DB에서 재실행되어도 실패하지 않도록).
- 기존 실패 테스트(거버넌스와 무관):
TaskFlowMap.test.tsx3건,DirectoryManagementPanel.test.tsx2건. - AI·검색 서비스 경계: 탐색 실행 조합은 ExploreGateway(이 포털은
POST /api/explore/v1/search어댑터 제공), 문서 인덱스·RAG·citation은 RagService, 모델 접근·키·쿼터·비용·감사는 ModelGateway가 소유한다(현재 RAG는 로컬 구현이며 이전 지점은VectorStore·RagService API).
9. 노드 code 정책 (참조정보 계약 v2 확정, 2026-09-20)
노드 code 가 다른 시스템이 인용하는 표준 식별자가 된다(공통 규약: portal-foundation docs/REFERENCE_DATA_SUPPLY_CONVENTION.md, 이 저장소 계약: docs/PROCESS_REFERENCE_CONTRACT.md). 그래서 아래를 규칙으로 고정한다.
9.1 발급 규칙
- 확정 — 발급 규칙(dev DB 실측 2026-09-20): 코드는
<레벨>-<6자리 순번>이다 —L1-000001,L2-000001,L3-000001… 부모 코드를 접두로 쓰지 않는다(예:L3-000001의 부모는L2-000003). 부모 관계는 내부parentId로 연결하고, 계약에서는parentNodeCode로 표현한다. - L1 루트 코드가 분류 축(
taxonomyCode)이다. 새 분류는 루트(L1)부터 만든다. - 전역 유일은 DB unique 제약(
process_nodes_code_unique)이 보장한다. 내부 대리 키(id)는 계약 payload 에 내보내지 않는다.
9.2 변경 규칙 (D2 — 게시 후 코드 불변)
- 게시본이 있는 노드(
publishedVersion != null)의 code 는 변경할 수 없다. 저장 경로(server/repositories/processRepository.tsupdateProcessNode)가assertNodeCodeChangeAllowed로 거부한다(오류:NodeCodeImmutableError). - 기동 코드 정규화도 게시본을 건너뛴다 (
processCodeNormalizationService): 레거시 코드 정비가 참조를 조용히 바꾸지 않도록 게시본을 제외하고 건수·대상을 경고로 보고한다. - 불가피한 변경은 새 code 발급 + 기존 code 를 승계(
successorNodeCodes)로 연결한다. - 게시 전(DRAFT 등) 노드는 자유롭게 고칠 수 있다 — 게시가 기준정보로 굳는 시점이다.
9.3 유일성 점검 (회귀 게이트)
process_nodes.code 의 전역 unique 제약은 이미 있다(process_nodes_code_unique — schema 선언 + 마이그레이션 0000/0001 + 적용 DB). 그래서 이 점검은 "제약을 걸기 위한 선행 작업"이 아니라 데이터 작업 전 정합 확인용이다.
- 데이터 가져오기·병합·복원 전후에 중복 0건을 확인한다(제약이 있어도 적재 경로가 제약을 우회하면 깨질 수 있다).
- 다른 환경(신규 설치·이관)에서 스키마가 실제로 제약을 갖고 있는지 확인한다.
pnpm db:check-code-uniqueness # 읽기 전용. 종료 코드 0=중복 0건 · 1=중복 발견 · 2=점검 불가(DB 설정)- 앞뒤 공백만 다른 code 도 같은 코드로 비교한다(trim 후 판정).
code가 비어 있는 노드는 중복은 아니지만 표준 식별자가 될 수 없으므로 함께 경고한다.- "중복이 있다"(1)와 "점검하지 못했다"(2)를 구분한다 — 게이트가 거짓 통과하지 않게 하기 위해서다.
9.4 업무 코드 별칭 (businessCode — 식별자와 분리, 확정 2026-09-20)
결정: 표준 식별자는 계속 시스템 발급 순번(code)이고, 업무 코드는 별칭 필드로 분리한다.
process_nodes.businessCode(varchar 80, nullable, 전역 유일) — 사람이 읽는 참조용(예:IT-OPS).- 식별자가 아니다: 다른 시스템은 이 값으로 노드를 참조하지 않는다(개편 때 바뀔 수 있다). 참조는
taxonomyCode·nodeCode(=code) 로 한다. - 유일성: 값이 있으면 중복될 수 없다(서비스가
CONFLICT, DB 가 고유 인덱스로 이중 방어). 없으면null. - 변경: D2(게시 후 코드 불변)의 대상이 아니다 — 별칭은 표시용이므로 바꿀 수 있고, 변경은 이력(
changedFields)에 남는다. - 설정 경로: 노드 생성·수정 API(
businessCode). 표기 규칙(대소문자·접두)은 업무가 정하며 시스템은 강제하지 않는다. - 마이그레이션:
drizzle/postgres-migrations/0017_process_node_business_code.sql(MySQL 파일은 0027·0028 과 같이 후속).
왜 code 자체를 업무 코드로 쓰지 않는가:
code는 이미 게시 후 불변인 참조 식별자다. 개편·병합 때 의미가 바뀌는 값을 식별자로 쓰면 참조가 끊기고 승계 절차를 타야 한다. "안정 식별자"와 "사람이 읽는 의미"를 분리하면 둘 다 지킬 수 있다.
dev DB 실행 결과(2026-09-20): 노드 19건 · 게시본 보유 1건 · 중복 0건 ·
process_nodes_code_unique존재.