Crowny ERP — 범용 멀티테넌트 ERP + 동의 기반 업그레이드 전파
개요
AIMED ERP(crowny-aimed, ~13.5k줄/19서버/43libs)를 회사-무관 범용 ERP 코어로 재구성하고, 마스터(erp.crowny.org)가 업그레이드되면 정기 동의절차(4상 거버넌스)를 거쳐 각 테넌트 (에임드·이음미디어)가 업그레이드를 수용하는 시스템 구축.
- 위치:
/Users/ef/crowny-erp - 마스터(제품): erp.crowny.org :9938 (crowny-erp/portal)
- 테넌트: aimed(aimed.crowny.org), eum(이음미디어, eum.crowny.org)
- 울트라씽킹 + 울트라병행(Workflow 2회: Understand 8에이전트 + Build 4에이전트)
무엇을 했는지
1. 아키텍처 (울트라씽킹)
- master + instances 모델: core(범용 코드, 실명·실거래 0건) + instances/<id>(설정·데이터·core핀복사·.pinned-version).
- 각 테넌트는 자기 버전을 핀(pin)하고, 마스터 업그레이드를 개별 동의로 수용.
2. 4상 동의 거버넌스 엔진 (한선씨, novel)
core/동의결정.한선 — 순수 모듈. 입력(수준/파일수/스키마·권한·포트 변경) → 4상 판정:core/동의CLI.한선/.toau — 셸 브리지. 판정 로직 단일 진실(셸 중복 0).3. Understand 워크플로 (병렬 8에이전트) 핵심 발견
- 인물 하드코딩 ~25곳은 기존 권한매트릭스 RBAC를 우회한 것. 워크플로 6기능(영업검증/설계검토/영업확정/사양작성/설계승인/조건검토/법무검토 등) 행을 권한매트릭스에 추가 + 게이트를
워크플로권한()호출로 바꾸면 tenant.json 인물키 0개. - 경로 ~40곳 →
paths.root1개. 포트/서비스 ~30곳 →services[]1배열. - 조직도 헤더 동적해석 + 권한매트릭스 RBAC 단일화 = 테넌트화 두 축.
4. Build 워크플로 (병렬 4에이전트)
core/libs/설정.한선— tenant.json 로더(설정값/설정포트, 점경로 조회). 검증 PASS.bin/erp-tenant.sh(create/list/status) +bin/erp-upgrade.sh(check/consent/apply).portal/서버.한선+index.html— erp.crowny.org :9938 제품사이트(레지스트리·체인지로그 동적).instances/eum— 이음미디어 테넌트(미디어회사 10명 조직도 + 권한매트릭스 105행, 골드→블루 브랜드, 영상업종). 범용성 증명.
5. 종단 검증 (동의-업그레이드 전체 사이클)
- A aimed 1.0.0→1.1.0(minor): 옴 → consent approve → apply(백업·복사·핀·이력) ✓
- B aimed 1.1.0→1.2.0(중간 schema): 집계로 schema=1 포착 → 음(오너필수, 자동적용 차단) ✓
- C eum 1.2.0→1.2.1(patch): 티 → 자동적용 ✓
- 검증 후 1.0.0 베이스라인 복원.
발견·수정한 버그
- CHANGELOG 태그 파서 버그(erp-upgrade.sh): awk
/^## ver/,/^## [0-9]/범위가 시작줄=종료패턴 동시매칭 → 헤더 한 줄로 축소돼 tags를 못 읽고 항상 폴백(minor). A가 우연히 minor라 빌드에이전트 테스트가 놓침. → found 플래그 awk로 수정. - 다중버전 점프 위험 누락: 타겟 버전 태그만 봐서 중간 schema 버전을 건너뜀 →
aggregate_changelog_tags(pinned<v<=master 전 버전 레벨=max·플래그=OR) 추가.
관련 파일
- /Users/ef/crowny-erp/{VERSION, CHANGELOG.md, README.md}
- core/동의결정.한선, core/동의CLI.한선, core/libs/설정.한선
- bin/erp-tenant.sh, bin/erp-upgrade.sh, scripts/erp-upgrade-check.sh
- portal/서버.한선, portal/index.html (:9938)
- instances/aimed/tenant.json, instances/eum/tenant.json, tenants/registry.psv
- LaunchAgent: com.crowny.erp-portal.plist, com.crowny.erp.upgradecheck.plist
잔여 이슈 (다음 배치 — Wave 이관)
워크플로권한() 헬퍼launchctl load -w 활성화는 사용자 1회.추가: Wave 0 + Wave 1 실제 코드 이관 (검증 완료)
"한 코어 위에서 두 테넌트가 실제 구동" 증명.
Wave 0 — RBAC 단일화 (인물 하드코딩 제거)
core/libs/권한검증.한선— 경로 하드코딩 제거 +권한초기화(조직경로, 매트릭스경로)주입 함수 추가(캐시 무효화).- aimed 인스턴스 권한매트릭스 보강: 누락 워크플로 9기능(영업검증/제안작성/설계검토/영업확정/사양작성/설계승인/계약작성/조건검토/법무검토) 행 추가(직급 키). 김아름 직급 "팀장"→"설계팀장"(매트릭스 정합).
- 결과:
멤버기능허용(멤버, 기능)이 인물 비교 없이 직급+매트릭스로 판정. (이전이름=="노형준"게이트 전부 대체 가능)
Wave 1 — 테넌트 서버 (한 코어, 모든 테넌트)
core/servers/테넌트서버.한선—ERP_TENANT환경변수로 인스턴스 지정 → tenant.json 로드(설정.한선) → 데이터 경로 주입(권한초기화) → 서빙. 라우트: /health, /api/tenant/config, /api/org/chart, /api/rbac/check.- 데이터=인스턴스 격리: 데이터 경로는 ERP_TENANT 기준 상대해석(paths.root 아님). 테넌트별 data 완전 분리.
종단 검증 (동일 테넌트서버.toau 바이너리)
- aimed(ERP_TENANT=instances/aimed): brand.domain=aimed.crowny.org, 업종=LED미디어/인테리어, 조직 17명. RBAC 5/5 정확(노형준 기술검토 허용 / 김아름 설계승인 허용 / 이동훈 영업검증 허용·설계승인 거부 / 박정아 법무검토 허용).
- eum(ERP_TENANT=instances/eum): brand.domain=eum.crowny.org, 업종=영상/미디어, 조직 10명. RBAC 2/2 정확.
- 같은 바이너리·다른 환경변수 → 다른 회사 구동. 인물 하드코딩 0.
함정/수정
설정포트("core")가 services[]에만 있어 ports.core 미반영 →설정값("ports.core")(2단계 점경로)로 교체 + ERP_PORT env 오버라이드.- aimed ports.core=9908은 기존 서비스(에임드인트라넷) 점유 → 데모는 ERP_PORT로 빈 포트 사용. 운영 시 ports.core 재배정 필요.
- 초기엔 데이터를 paths.root(crowny-aimed 라이브)에서 읽어 보강 매트릭스 미반영 → 인스턴스 기준 상대경로로 수정(데이터 격리 정본).
운영 실행
bashERP_TENANT=/Users/ef/crowny-erp/instances/aimed ERP_PORT=<free> crownyc-pinned run core/servers/테넌트서버.toau
ERP_TENANT=/Users/ef/crowny-erp/instances/eum ERP_PORT=<free> crownyc-pinned run core/servers/테넌트서버.toau
잔여 (다음 Wave)
- Wave 2~5: 견적·제안·사양·계약·업무·재정·포털 서버 본문 이관(인물게이트→멤버기능허용 치환은 같은 패턴 반복). erp-tenant.sh 에 run/stop 추가. 인스턴스별 영속 LaunchAgent.
추가: Wave 2 — 워크플로 4종 lib RBAC 이관 (울트라병행 4에이전트, 검증 완료)
견적·제안·사양·계약 lib의 인물 하드코딩 게이트를 전부 RBAC로 전환.
전환 (11 게이트, 인물비교 0)
- 견적.한선: 기술검토가능→
멤버기능허용(이름,"기술검토"), 영업검증가능→"영업검증" (견적작성가능은 견적권한 칼럼 기반이라 유지) - 제안서.한선: 제안작성/설계검토/영업확정 (조은호·이동훈·이선우·김경은 4줄 → 1줄)
- 사양서.한선: 사양작성/기술검토/설계승인
- 계약서.한선: 계약작성/조건검토/법무검토
- 패턴:
함수 XX가능(이름) { 만약 (이름=="인물"){반환1}... }→함수 XX가능(이름) { 반환 멤버기능허용(이름, "기능") }+ 가져오기 권한검증.한선(절대경로).
검증 (양 테넌트, 실패 0)
- aimed: 김아름/설계검토=1, 조은호/영업확정=1, 이동훈/영업검증=1·기술검토=0, 박정아/법무검토=1·조건검토=0 …
- eum(같은 코드, 다른 인물): 박서진/영업확정=1, 이하은/설계검토=1, 김도현/조건검토=1·법무검토=0, 윤준혁 전부 거부 …
- 동일 lib 바이너리가 테넌트 매트릭스만 바꿔 완전히 다른 인물집합에 정확히 권한 분기 → 멀티테넌트 범용성 입증.
데이터 보정 (인스턴스 격리)
- aimed 매트릭스에
팀장|제안작성|L3|Y추가(김경은 직급=팀장). instances/aimed 에만, crowny-aimed 라이브 0행(격리 정상).
Wave 1 운영화 (병행)
- erp-tenant.sh 에
run <id> [port]/stop <id>추가(pidfile 관리). eum 기동 200 / 종료 확인. - 함정: bash
local a="$1" b=".../$a/..."한 줄에서$a미평가 →instances//빈 경로. 줄 분리로 수정.
남은 작업
- 워크플로 server 본문(견적서버 등) HTTP 래퍼 이관: 경로/포트/브랜드/오류메시지 인물명 외부화(테넌트서버 패턴 적용 — 기계적).
- 직급 표기 정규화(팀장 vs 기획팀장 등) 테넌트 간 표준화.
추가: 워크플로 데이터 격리 + 워크플로 종단 구동 (검증 완료)
Wave2 lib(인물게이트 RBAC)에 더해 데이터 경로 외부화로 워크플로를 테넌트별 격리 데이터에서 실제 구동.
데이터 외부화 (4 lib, 데이터=인스턴스 격리)
- 견적·제안·사양·계약.한선에
데이터초기화(데이터루트)추가 — 경로상수(견적서/견적품목/표준품목/조직도 등)를데이터루트 + "/data/..."로 주입. 기본값 "" → 서버가 ERP_TENANT 주입. 4/4 컴파일 OK.
견적 워크플로 종단 (양 테넌트, 인스턴스 데이터)
흐름: 견적작성가능(RBAC) → 견적ID생성 → 견적저장 → 품목저장 → 견적총액갱신 → 견적목록읽기("author") → 기술검토가능(RBAC).- aimed: 조은호 견적작성=1, 견적생성, author목록=1, 노형준 기술검토=1.
- eum: 김도현 견적작성=1, 견적생성, author목록=1, 정민재 기술검토=1.
- 데이터 격리 입증: aimed/eum 인스턴스 견적서에 각각 기록, crowny-aimed 라이브 151건 불변.
- 설계 확인: 견적서.psv는 append-log(견적총액갱신이 새 행 추가, 마지막 행=현재상태). 견적목록읽기 역할 enum=author/tech/sales.
남은 작업
- 워크플로 HTTP server(견적서버 등) 래퍼: 테넌트서버 패턴으로 포트/공개루트/AI게이트웨이 외부화 + 데이터초기화·권한초기화 startup 호출. (워크플로 lib·데이터·RBAC는 전부 준비됨 — 서버는 transport 래핑만)
- AI견적생성(상담게이트웨이/Gemini)·경영AI 미들웨어는 테넌트 인프라 연결.
추가: Wave 3 — 3서버 erp-core 이관 (울트라병행, 검증 완료)
견적·일일업무·목표재정 서버를 erp-core에서 실제 HTTP로 기동.
이관 (3 서버, core/servers/)
- 견적서버: 견적/견적필터/프로젝트연계 lib 외부화(데이터초기화/pj데이터초기화), 포트=설정값("ports.quote")+ERP_PORT. /api/quote/create·list·submit·tech-review·sales-verify. AI게이트웨이(9906)·경영AI(9913) curl은 테넌트 인프라로 유지(폴백).
- 일일업무서버: 자체 데이터 4경로(근태/지시/보고/조직) 외부화, 성장연결 lib 데이터초기화. /api/work/tasks·checkin·attendance. 업무지시 권한=직급키워드(인물 하드코딩 0).
- 목표재정서버: 목표/수지 2경로 외부화, 권한검증+성장연결. /api/goal/list·create·progress(write-through). 경영AI(9913) 폴백.
검증 (aimed 기동, curl 실측)
- 견적: /health OK, create→list 왕복, submit→tech-review(노형준 200/이동훈 403 RBAC), 데이터 인스턴스 격리(라이브 151행 불변).
- 일일업무: tasks 15건, checkin(E03 조은호 지각), attendance.
- 목표재정: list 16건, create→progress 100% 달성 write-through.
★회귀 발견·수정 (중요 교훈)
- 견적서버 에이전트가 원본 crowny-aimed 견적.한선을 복사하면서 Wave2 RBAC를 되돌림(인물게이트
이름=="노형준"부활). 데이터초기화만 추가하고 RBAC 유실. - 수정: 견적.한선에 가져오기 권한검증 + 기술검토가능/영업검증가능→멤버기능허용 재적용 + 견적서버 부트스트랩에 권한초기화 추가. 재검증 통과(403/200).
- 교훈: 서브에이전트가 원본을 복사하면 이전 마이그레이션을 덮어쓸 수 있음 → core가 이미 마이그레이션된 경우 원본 아닌 core 버전을 베이스로 지정해야 함.
남은 작업
- 오류메시지 인물명("노형준만 가능") 동적화(설정/매트릭스 역조회). 게이트는 RBAC지만 메시지는 잔존.
- 동시 기동 포트: 검증은 ERP_PORT 순차. tenant.json ports 고정값은 crowny-aimed 라이브와 충돌(별도 대역 배정 필요).
- 성장연결/목표재정 직급·역할 키워드(LED/AMX/총괄) tenant.json 역할사전 외부화.
- 함수명 충돌(_필드/_오늘날짜) lib 네임스페이스 접두어.
- Wave 4(포털·관리·분석) + Wave 5(프론트).
추가: Wave 4 — 포털·관리·분석 erp-core 이관 (울트라병행, 검증 완료)
가장 큰 3서버(관리 1995·분석 1200·포털 1114줄) 이관. 회귀 0(Wave3 교훈 적용).
회귀 방지 (선이관 전략)
- 공유 lib(인증·크라우니인증·사상균형)을 메인이 먼저 core로 선이관 → 에이전트 충돌·덮어쓰기 제거. 인증은
인증초기화(회원경로, 솔트)로 salt 외부화. - 가드레일: "core 마이그레이션 버전 덮어쓰기 금지, 참조만". 베이스라인 백업 후 대조.
- 결과: 7개 마이그레이션 lib 바이트 동일(회귀 0). Wave3 회귀와 대조적 성공.
이관 (3 서버 + 비공유 lib)
- 포털서버(auth): 인증 lib 참조+인증초기화, salt→설정값("auth.tokenSalt"), 자체경로(조직/멤버/슬로건) 외부화, 포트=ports.portal.
- 관리서버(admin): 사상균형·프로젝트연계(migrated) 참조, 비공유 시뮬데이터·조직분석·보안현황 복사, 데이터 12경로 외부화, API namespace→설정값("api.namespace").
- 분석서버(4상 진단, 20 import): 인증·사상균형 참조, 순수로직 lib 14개 복사(경로0) + 경로패치 3개(개인MD/업무진행/인과규칙 초기화), 세션경로 외부화, namespace.
검증 (aimed 동시 기동, curl 실측)
- 포털: /health, /api/me(조은호 실토큰 인증 OK), /api/auth/login(라우트 동작).
- 관리: /health, /api/aimed/admin/overview(총인원30, T2035/O2105/A2175/U1845 4상분포), members 30명.
- 분석: /health, POST /api/aimed/start(sessionId+16문항), answer 진행. 세션=인스턴스 격리(crowny-aimed sessions 비오염).
- auth salt·API namespace·데이터격리 외부화 확인. 7 lib 회귀 0.
발견·수정
- verify 에이전트가 관리서버.toau 누락(에이전트 B 컴파일 단계 빠짐) 발견→재컴파일(2.5MB).
- 분석 start는 영문키 memberId 필요. 관리 namespace는 substring 매칭(느슨, 잘못된 ns도 200 — 단일 인스턴스라 데이터 누출 아님, Wave 후속 정밀화).
남은 작업
- 관리 namespace substring→정확 매칭. 오류메시지 인물명 동적화(Wave3 잔존).
- _공개루트/awk도구/비번변경.sh 등 crowny-aimed 경로 → Wave5 프론트/도구 외부화.
- Wave 5(프론트 ~30 HTML) — /api/tenant/config 동적로드.
추가: Wave 5 — 프론트 테넌트 동적화 (메커니즘 입증)
라이브 30페이지 직접수정 위험 → 공유 로더+데모셸로 메커니즘 입증, 30페이지는 검증된 패턴+체크리스트로 안전 위임.
산출 (core/public/)
- erp-tenant.js — 동적 로더: /api/tenant/config 가져와 CSS --tenant-primary/accent, document.title, [data-tenant=name/product/slogan/domain/logo] 텍스트, [data-tenant-nav] services[] 네비, [data-tenant-types] 업종 드롭다운 적용. config 미가용 시 무시(하위호환).
- erp-shell.html — data-tenant 마커 데모 셸.
- RETROFIT.md — 페이지당 3단계 기계적 리트로핏 가이드.
- 테넌트서버에 /erp-tenant.js·/shell 정적 라우트 추가.
입증 (aimed·eum 동시 기동)
남은 작업 (기계적 위임)
- 라이브 ~30 HTML에 RETROFIT.md 적용(script 추가 + data-tenant 마커 + var(--tenant-primary)). 하위호환이라 라이브 안전.
- 워크플로/포털 서버에 /api/tenant/config 라우트 추가(same-origin fetch).
추가: 멀티서버 /api/tenant/config 결선 (Wave5 프론트 전제, 울트라병행 6에이전트)
다른 세션이 통합서버(leum 린배포)엔 config 엔드포인트를 넣었으나 제 멀티서버 6종엔 없어 Wave5 프론트 로더(erp-tenant.js)가 same-origin으로 못 쓰던 갭 해소.
- 공유 헬퍼
core/libs/테넌트설정응답.한선— ERP_TENANT/tenant.json 직접 응답(서버 변수명 무관, 이식성). - 포털·견적·관리·일일업무·목표재정·분석 6서버에
가져오기+/api/tenant/config라우트 추가(각 서버의 경로/소켓 변수명에 맞춤). - 검증: 6서버 컴파일 OK, 견적 라이브 스모크에서 config(brand.domain=aimed.crowny.org)+/health+/api/quote/list 동시 정상. 견적 RBAC 회귀 0(인물게이트 0/멤버기능허용 2).
- 효과: 모든 멀티서버 페이지에서 erp-tenant.js가 테넌트 브랜드/색/nav/업종 동적 렌더 가능 → Wave5 프론트 동적화 풀스택 전제 완비.
추가: 네 가지 고도화 (A/B/C/D) — 완료
B — leum 멤버 인증 시드: 다른 세션이 이미 완료(leum01~08 토큰 유효, 통합서버 salt=leum/leum1234). 확인만.
C — 오류메시지 인물명 동적화: core/libs/권한검증.한선에 기능담당역할(기능) 역조회 추가. 견적서버 "노형준만 가능"→"담당: 기술팀총괄, 책임 기술영업팀장, 책임 차장, 프로(대리), 설계팀장". 라이브 검증.
D — 배포 토폴로지 수렴: docs/배포토폴로지-수렴.md. tenant.json deploy.mode(lean=통합서버1개/full=멀티서버N개) 단일화 + 적용(aimed=full, leum=lean) + 브로드캐스트. 원칙: 토폴로지=배포선택, 로직=core/libs 단일소스, lean을 core/libs로 수렴.
A — Wave5 프론트 리트로핏 (울트라병행 4에이전트): 견적/제안/사양/계약 작성폼에 erp-tenant.js 로더 + data-tenant 마커(하위호환: 텍스트 유지·색 폴백·폼로직 불변). 재분할로 .pN 반영. 견적서버에 /erp-tenant.js 라우트 추가. 서빙 체인 종단 검증: erp-core 견적서버가 페이지(마커2)+로더(JS200)+config(aimed) same-origin 제공 → 브라우저 동적 브랜딩. 라이브 aimed에선 config 404→정적 폴백(하위호환).
남은 follow-on
- 나머지 5 멀티서버에 /erp-tenant.js 1줄 라우트(견적서버 패턴 복제) + 그 페이지 리트로핏
- deploy.mode 런처 분기(erp-tenant.sh run), lean을 core/libs RBAC로 수렴