← 목록
기타 2026-07-21 13KB 읽기 13분

ERP P2 — nexus↔ERP 연동 전이 확산 + 승인권자 실사용자 매핑 + 견적ID 충돌 수리

개요

P1(2026-07-18, 2026-07-17-erp-P1-nexus판정-견적서버-라이브배선.md)에서 tech-review 전이 1개에만 nexus 판정 게이트를 배선했던 것을 P2에서 sales-verify·decide 전이로 확산하고, 승인권자를 정적 라벨(넥서스연동.한선의 하드코딩 텍스트)에서 조직도.psv 실사용자 매핑으로 교체했다. 동시에 P1이 남긴 잔여 이슈였던 견적ID 초단위 타임스탬프 충돌을 카운터 nonce로 수리했다.

가드레일(/Users/ef/.claude/templates/한선씨-가드레일.md) 전문을 읽고 준수했다 — 특히 ① 가져오기는 top-level 실행이라 자체시험() 있는 넥서스연동.한선은 여전히 함수 이식 방식만 사용(넥서스판정 계열은 P1이 이미 이식해둔 것 재사용), ② 함수 전방참조 위험(신규 _승인권자JSON_이스케이프 호출 — 처음엔 JSON헬퍼 앞에 넣었다가 컴파일 전 발견해 JSON헬퍼 뒤로 재배치), ③ 공유 인프라(crowny-aimed·crowny-butler·crowny-erp/instances/*· gateway.yaml) 무접촉 원칙.

무엇을 했는지

P2-c 견적ID 초단위 충돌 수리 (다른 항목의 선행조건, 먼저 처리)

core/libs/견적.한선 견적ID생성()date +%Y%m%d%H%M%S(초 단위) 뒤에 프로세스 메모리 카운터(_견적ID마지막ts/_견적ID카운터) nonce 3자리를 접미(Q<ts>_<nnn>). ts가 이전 호출과 같으면 카운터 증가, 바뀌면 리셋. 파일 기반 read-increment-write 대신 인메모리 채택 — 서버가 포트 1개를 단독 bind하는 단일 프로세스 모델이라 파일 락 없는 다중 프로세스 레이스는 애초에 해당 없고, 인메모리가 더 간단·안전.

기존 ID 포맷(Q+14자리)을 고정 길이로 파싱하는 코드는 crowny-erp 전체(core+instances) grep 결과 없음(통합서버.한선의 별도 qid 생성은 이 함수와 무관한 독립 로직) — 접미 추가로 인한 호환성 문제 없음.

P2-b 승인권자 실사용자 매핑

core/servers/견적서버.한선에 신규:

  • _승인권자스캔(직급키워드)_승인조직도경로(부트스트랩에서 인스턴스+"/data/조직/조직도.psv"
주입) PSV를 직접 순회, 직급 컬럼(인덱스3)에 키워드 부분매치 시 이름(사번) 수집. 기존 넥서스필드 헬퍼(P1이 이미 만든 PSV 필드추출) 재사용.
  • 승인권자목록(결정라벨) — 자동승인→["SYSTEM"] / 반려→["차단"] / 팀장검토→직급에
"팀장" 포함 전원 / 임원승인·이사회상신→직급에 "총괄" 포함 전원(최상위) / 매칭 0명이면 ["매핑대상없음(조직도 미정의)"] 정직 반환(허위 배선 금지).
  • _승인권자JSON / _승인권자텍스트 — 위 배열을 JSON 배열 문자열 / 콤마 텍스트로 변환.
권한검증.한선은 지시대로 읽기만(이름조회/멤버기능허용 등 함수는 이미 P1까지의 가져오기 체인으로 사용 가능했으나, "조직도 전원 순회"는 그 파일에 없는 기능이라 서버 자체에 신규 작성 — 라이브러리 수정 없이 요구사항 충족).

P2-a 전이 확산

  1. _처리영업검증(sales-verify) — tech-review와 동일 패턴 삽입: 상태검사(영업검증대기)
통과 직후, 견적상태변경 호출 직전에 넥서스 판정 1콜. 팩트=금액(총액/10000)·유형·요청자 (작성자)·승인자(검증자)·위험어(사유 스캔). 사상 분기: 타=409+상태변경 스킵 / 음=상태변경 스킵+승인큐 적재(사유에 승인권자 텍스트 포함)+"상태":"영업검증대기(승인큐상신)" / 티·옴= 기존 로직대로 진행(옴은 응답에 "검토요구":1). 모든 분기에서 _넥서스감사기록 append, 응답 JSON에 "넥서스판정":{사상,결정,발동규칙,승인권자} 포함.
  1. _처리결정중계(decide) — nexus 4상을 선행 게이트로 재구성: 팩트(전이=decide,
금액·유형)로 먼저 판정. 타=즉시 409 차단(경영AI 호출 안 함) / 티=즉시 승인 응답, 경영AI :9913 호출 완전 스킵(토큰절감) / 옴·음만 기존 경영AI curl 중계 진행, 응답에 넥서스판정을 병기. 모든 분기에서 감사 기록.
  1. 견적승인규칙.psv — sales-verify(R18-R21)·decide(R22-R24) 대표행이 이미 존재
(P1 이후 별도 세션에서 선반영된 것으로 확인, grep 재확인함). 신규 추가 불필요 — 골든셋.psv도 이미 두 전이의 케이스를 포함(영업검증 3케이스+결재게이트 3케이스). 회귀 26/26 그대로 PASS.

P2-d erp-upgrade 채택 절차 (문서만, instances 무편집)

bin/erp-upgrade.sh를 읽고 절차를 정리(§"업그레이드 채택 runbook" 참조). **instances/* 편집·apply 미실행 — 절차 문서화만 지시대로 수행.

검증 (실측)

컴파일: hanseonc_high 견적서버.한선 — exit 0, 경고 3건은 P1 문서와 동일한 기존 충돌 (_필드/_오늘날짜, 권한검증.한선↔견적필터.한선/main) 그대로, 신규 충돌 0건.

:9929(scratchpad erp-p1-tenant, tenant.json ports.quote=9929) 재기동 후 왕복:

견적ID 무충돌 — 같은 초(20260721002643) 연속 3건 create:

Q20260721002643_000
Q20260721002643_001
Q20260721002643_002

sales-verify 3케이스:

  • 티(5만원 운영비, 정상): {"상태":"확정","넥서스판정":{"사상":"티","결정":"자동승인","발동규칙":"R18","승인권자":["SYSTEM"]}}
  • 타(리베이트 사유): HTTP 409 {"error":"넥서스 판정 차단: 반려","넥서스판정":{"사상":"타","결정":"반려","발동규칙":"R1","승인권자":["차단"]}}
  • 음(2천만원 운영비): {"상태":"영업검증대기(승인큐상신)","넥서스판정":{"사상":"음","결정":"이사회상신","발동규칙":"R21","승인권자":["이선우(E01)","이동훈(E02)"]}}
(동일 케이스의 tech-review 단계도 옴/임원승인/R12로 실사용자 이선우·이동훈 노출 — 보너스 확증) 승인큐(/Users/ef/crowny-butler/data/승인대기.psv) 적재 확인: 사유 텍스트에 승인권자: 이선우(E01), 이동훈(E02) 포함.

decide 선행게이트:

  • 티(5만원): 0.072s, {"4상":"티","경영AI":"스킵(넥서스 자동승인, 토큰절감)","넥서스판정":{...R22...}}
  • 음(2천만원): 1.389s, 실제 :9913 경영AI 응답 병기({"상":"T","신뢰도":100,...})+"넥서스판정":{...R24, 승인권자 실이름...}
→ 티 케이스가 옴/음 케이스 대비 약 19배 빠름(0.072s vs 1.389s) — AI 호출 스킵에 따른 토큰·지연 절감 실측 확증.

감사 로그(data/견적/견적판정감사.psv) 8행 append 확인(tech-review 3 + sales-verify 3 + decide 2, 케이스별 정확 매칭).

골든셋 회귀: bash integrations/nexus/회귀.sh26/26 PASS(규칙표에 이미 sales-verify/ decide 대표행이 있어 무변경으로 통과 — catch-all 오탐 없음 확인).

크라우니코드 학습: lookup MISS 확인 후 crownycode-learn.sh add 3건 (ERP견적ID초단위충돌카운터수리, ERP승인권자조직도직급스캔매핑, ERP경영AI중계넥서스선행게이트토큰절감).

무접촉 확인:

  • :9914(crowny-aimed 구 트랙) — 작업 전후 계속 LISTEN, /health 200 정상.
  • /Users/ef/crowny-butler/{libs/규칙엔진.한선,libs/규칙엔진.toau,승인큐서버.js} mtime
7월17일 그대로(오늘 변경 없음).
  • /Users/ef/crowny-erp/instances/* — 신규/변경 파일 0건(find -newer 확인).
  • gateway.yaml 미접촉(참조조차 안 함).
  • /Users/ef/crowny-aimed/data/경영결정로그.psv만 mtime 갱신됨 — 이건 내가 Edit/Write한
파일이 아니라, decide 테스트에서 curl로 :9913 경영AI를 호출했을 때
그 서버 자신이 자기 결정 로그에 append한 부작용(라이브 서비스의 정상 동작). 코드/설정 파일 변경 아님.

touchedExistingFiles = [ /Users/ef/crowny-erp/core/libs/견적.한선, /Users/ef/crowny-erp/core/servers/견적서버.한선, /Users/ef/crowny-erp/core/servers/견적서버.toau(재컴파일 산출물) ] 그 외 파일 Edit/Write 없음.

업그레이드 채택 runbook (P2-d, 실행 안 함 — 절차만)

이번 core 변경(sales-verify/decide 게이트, 승인권자 매핑, 견적ID 수리)을 instances/aimed· instances/leum이 채택하려면 bin/erp-upgrade.sh(동의CLI.toau 4상 판정) 경로를 탄다:

  1. CHANGELOG.md에 새 버전 항목 추가(이번 작업에서는 미실행). 예시 태그:
tags: minor perm — sales-verify/decide 상태기계 자체는 안 바뀌지만(스키마 무변경), 승인 권한 판정 로직(누가 승인자로 노출되는가)이 바뀌므로 perm 플래그가 적절. schema/port 플래그는 불필요(PSV 컬럼·포트 무변경).
  1. VERSION 파일을 2.1.0(minor) 등으로 갱신(현재 2.0.0).
  2. 각 테넌트에 대해 bin/erp-upgrade.sh check <id> 실행:
- aimed.pinned-version=1.0.0(현재 master=2.0.0보다도 낮음 — 2.0.0 자체가 major schema port 태그라 이미 누적 위험 high, 최초 채택 시 필연적으로
음(오너 명시동의 필수)로 판정될 것). 이번 P2 변경만 따로 분리해 적용하려면 aimed를 먼저 2.0.0으로 별도 승인 후, 그 다음 이번 minor로 재실행하는 2단계 권장. - leum.pinned-version=2.0.0(=master) — 이번 신규 버전만 단일 diff이므로 perm 플래그로 인해 동일하게 판정 예상(오너 명시 승인 대기 흐름).
  1. 동의CLI 결과가 "음"이면 동의대기/<ver>.json 생성 → 오너가
erp-upgrade.sh consent <id> <ver> approve|reject로 결정.
  1. approve 시 cmd_applyinstances/<id>/core를 통째로 마스터 core로 교체(직전 자동
백업 core.bak.<timestamp>), .pinned-version 갱신, 이력/업그레이드이력.psv append.
서버 재시작은 TODO 주석 상태로 자동화되어 있지 않음 — 적용 후 bin/erp-run.sh <id> 재기동이 별도로 필요(운영자 수동 트리거, 현재 스크립트 범위 밖).
  1. 적용 후 각 테넌트의 실제 조직도.psv 직급 컬럼에 "팀장"/"총괄" 표기가 있는지 반드시
확인 — 없으면 P2-b 로직이 "매핑대상없음(조직도 미정의)"으로 정직하게 떨어지므로(허위 배선 아님, 설계된 동작), 승인권자가 안 보이는 테넌트는 조직도 데이터 보완이 선행조건.

이번 세션에서는 CHANGELOG/VERSION 갱신도, erp-upgrade.sh apply도 실행하지 않았다** — 지시 범위가 "runbook 텍스트로만"이었고, 실제 버전 태깅·오너 동의는 코드 작업 범위를 넘는 비즈니스 의사결정이라 판단.

관련 파일

  • 수정: /Users/ef/crowny-erp/core/libs/견적.한선(견적ID생성 카운터 nonce)
  • 수정: /Users/ef/crowny-erp/core/servers/견적서버.한선(승인권자 매핑 함수 4종 + sales-verify/
decide 넥서스 게이트)
  • 재컴파일: /Users/ef/crowny-erp/core/servers/견적서버.toau
  • 읽기만(무수정): /Users/ef/crowny-erp/core/libs/권한검증.한선,
/Users/ef/crowny-erp/integrations/nexus/{넥서스연동.한선,견적승인규칙.psv,골든셋.psv,회귀.sh}, /Users/ef/crowny-erp/bin/erp-upgrade.sh
  • 테스트 인프라(기존 P1 재사용, instances/* 밖): scratchpad erp-p1-tenant/
  • 데이터 append(기존 append-only 로그, 의도된 쓰기): data/견적/견적판정감사.psv,
data/견적/견적서.psv, data/견적/견적품목.psv, /Users/ef/crowny-butler/data/승인대기.psv

잔여 이슈

  1. CHANGELOG/VERSION 미갱신 — 실제 테넌트 채택 전 필요(§runbook 1-2단계, 사용자 결정 대기).
  2. _승인권자스캔은 부분매치(포함())라 "팀원"처럼 "팀장"을 포함하지 않는 문자열은
안전하지만, 향후 직급 명명에 "부팀장" 같은 표기가 추가되면 "팀장" 매치에 걸림 (오탐 아님 — 부팀장도 팀장급 승인권자로 포함하는게 의도상 맞을 가능성 높으나, 조직 설계 변경 시 재검토 권장).
  1. 견적ID생성()의 인메모리 카운터는 프로세스 재시작 시 리셋됨 — 같은 초에 재시작
직후 재호출되면 이론상 이전 프로세스가 쓴 것과 같은 ID 가능(극히 낮은 확률, 파일 기반 영속 카운터 대비 트레이드오프로 채택. 실무 영향 없음 판단).
  1. decide 엔드포인트는 GET이라 body가 없어 "승인자"·"위험어"·"긴급" 신호를 팩트에
반영하지 못함(요청자=작성자만, 승인자=""·위험어=""·긴급=0 고정) — SoD·위험어 탐지가 decide 게이트에서는 사실상 비활성. 필요 시 decide를 POST+body로 확장하는 후속 과제.