현재 사용법 (2026-09-10,
6fbaee7f): 플랫폼 운영 콘솔 · 기능 마스터. 아래는 작성 당시의 변경 설명이며 현재 전체 동작·재측정 결과를 뜻하지 않습니다.
system-admin 재설계 — API-경유 · 계정 인증 · 멀티 엔드포인트
PR:
feat(system-admin): redesign console to API-routed, account-authenticated, multi-endpoint설계 문서:docs/internal/design-system-admin-redesign.md(Phase 0~3 + §5-2, 진행 현황 포함)
벤더 운영 콘솔(system admin)에 이제 env 에 적힌 공용 아이디/비밀번호가 아니라 운영자 본인 계정으로 로그인하고, 콘솔의 모든 조회·변경은 운영 DB 직접 쿼리가 아니라 각 고객사 API 서버를 경유하며, 모든 변경 행위가 감사 로그에 남습니다.
이 변경의 세 축은 근본 방향이 동일합니다: 콘솔을 DB 클라이언트에서 API 클라이언트로 바꾸는 것입니다. 인증(누가), 데이터 경로(어떻게), 규모(몇 개의 서버를)가 전부 그 전환의 단면입니다.
배경 — 콘솔이 무엇이고 종전에 어떻게 동작했나
tools/system-admin 은 hereby 직원(벤더)이 고객사 조직을 프로비저닝하고 관리자 계정·역할·연동 자격증명·공휴일 등을 관리하는 운영 콘솔입니다 (독립 Fastify 서버 4100 + Alpine UI). 고객사 화면(admin 앱)과 달리 전 조직을 횡단 조회하는 것이 본질이라, 멀티테넌시 격리("조직 간 데이터 노출 금지")의 의도된 예외 표면입니다.
종전 구조는 세 가지 특징이 있었습니다.
첫째, 인증이 env 단일 계정이었습니다. SYSADMIN_USERNAME/SYSADMIN_PASSWORD 에 적힌 하나의 아이디/비밀번호를 아는 사람이 곧 시스템 관리자였고, 따라서 "누가 이 작업을 했는가"를 기록할 수 없었습니다.
둘째, 데이터 경로가 raw SQL 이었습니다. 콘솔 서버가 pg 라이브러리로 운영 DB에 직접 접속해 라우트마다 SQL 을 실행했습니다. API 서버의 코어 로직(필드 암호화, 역할 시드, 조직 프로비저닝)을 재사용할 수 없어 같은 규칙이 SQL 로 복제되어 있었고, 콘솔이 API 의 필드 암호화 키(FIELD_ENCRYPTION_KEY)까지 보유해야 했습니다.
셋째, 단일 서버 전제였습니다. 엔터프라이즈 고객마다 API+DB 를 독립 배포하는 방향(이하 엔드포인트 — 자체 accounts 테이블·JWT 비밀키·DB 를 가진 하나의 API 서버 배포 단위)에서는, 콘솔이 DB n개에 직접 접속하고 스키마 버전까지 따라가야 해 구조가 성립하지 않습니다.
"콘솔의 DB 접속을 유지한 채 인증만 계정 기반으로 바꾸는" 절충안을 채택하지 않은 이유가 여기 있습니다. DB 직결을 유지하면 멀티 엔드포인트에서 접속·스키마 관리가 콘솔로 새고, 마스킹·시드·커밋 판정 같은 계산 로직을 SQL 사본으로 유지해야 해 규칙 불일치가 계속 생깁니다. API 경유는 이 문제를 구조적으로 제거합니다 — 화면 값과 콘솔 값이 같은 코드에서 나옵니다.
변경 전
변경 후
1. 시스템관리자 세션 — 계정 속성으로 판정하는 전 조직 스코프
API 에 systemProcedure 를 신설했습니다. 회원 세션(account 스코프) 위에서 accounts.platformRole 이 OPERATOR(벤더 운영자 — meta account 체계의 플랫폼 스태프 역할)인지 검사하는, 조직에 묶이지 않는 프로시저 타입입니다. 토큰에 특수 클레임을 넣는 방식이 아니라 계정 속성으로 매 요청 재판정하므로, 역할이 해제되면 다음 요청부터 즉시 거부됩니다. 콘솔의 모든 조회·변경 프로시저(platform.system* 40여 개)가 이 게이트 뒤에 있습니다.
시스템관리자가 특정 조직에 "관리자로 진입"하는 경로(enterOrganizationAsSystemAdmin — systemGrant JWT 마커)도 함께 추가되어, 고객사에 매번 권한을 요청하는 루프 없이 전 조직 전권 접근이 가능합니다(§2.1 결정 — grant 발급 주체는 벤더 유지).
2. 멀티 엔드포인트 오케스트레이션 — 레지스트리 · 세션 매니저 · fan-out
엔드포인트는 서로 독립된 인증 체계이므로 공유 로그인(SSO)이 불가능합니다. 콘솔은 env SYSADMIN_ENDPOINTS(JSON 배열)로 엔드포인트 레지스트리를 구성하고, 세션 매니저가 엔드포인트마다 운영자 본인 자격증명으로 로그인해 토큰을 캐시합니다(독립 만료·401 시 해당 엔드포인트만 재로그인).
전 엔드포인트를 합쳐 보여주는 목록 화면은 fan-out — 등록된 엔드포인트 전부에 같은 프로시저를 병렬 호출해 결과를 합치는 공통 유틸 — 을 사용합니다. 부분 실패가 정상 상태이므로 결과는 엔드포인트별 4-way 상태(ok / unauthenticated / unsupported(구버전 서버에 프로시저 없음) / failed(다운·타임아웃))로 집계되어 UI 에 표시됩니다. 조직 하나를 대상으로 하는 화면은 ?endpoint= 지정(단일 등록이면 자동 선택)으로 라우팅합니다.
3. 전 라우트 이관 — 읽기 전부 + 쓰기 전부, 쓰기마다 감사
콘솔의 모든 라우트에서 raw SQL 을 제거하고 대응 systemProcedure 호출로 교체했습니다. 응답 계약(shape)은 유지하여 UI 변경을 최소화했습니다. 이관 과정에서 구조가 개선된 지점이 세 곳 있습니다.
암호화 키가 API 로 격리됩니다. 외부 연동 자격증명(hereby Sign API 키 등)의 암호화·복호화·마스킹이 전부 API 안으로 이동해, 콘솔은 시크릿 원문을 TLS 로 보내기만 하고 FIELD_ENCRYPTION_KEY 를 더 이상 보유하지 않습니다. 마스킹된 값(AK_liv...7890)만 콘솔로 돌아옵니다.
조직 프로비저닝이 단일 진입점으로 통합됩니다. 콘솔의 raw SQL 프로비저닝 사본을 버리고 회원가입 경로가 쓰는 provisionWorkspace 서비스를 재사용합니다. 그 결과 콘솔에서 만든 조직도 법정공제 요율 시드·알림 기본값·SUPER_ADMIN 배정까지 가입 경로와 동일하게 받습니다. 역할/권한 시드도 정본 시더(seedRolesAndPermissions)를 재사용하는데, 이때 기존 역할의 권한을 카탈로그 정의로 동기화(누락 추가가 아니라 교체)하는 의미로 통일된 점은 리뷰에서 확인할 지점입니다.
모든 쓰기가 감사됩니다. 조직 상태 변경·삭제, 역할/관리자 계정 CRUD, 연동 자격증명, 시드, 프로비저닝 등 모든 변경이 audit_logs 에 append-only 로 남습니다(실행 주체 = 운영자 본인 계정, 입력은 자동 redact — password/apiKey 는 [REDACTED]). 시트 템플릿·platformRole 부여·공휴일 동기화처럼 조직에 매이지 않는 플랫폼 레벨 행위를 기록하기 위해 audit_logs.organizationId 를 nullable 로 완화했습니다 — 마이그레이션 1886000000000-AuditLogOrgNullable 이 이 PR 의 유일한 스키마 변경입니다.
4. 콘솔 인증 전환 — env 계정 제거, 본인 계정 + Endpoints 탭
콘솔 로그인이 운영자 본인 계정 기반으로 바뀌었습니다. 로그인 시 대상 엔드포인트의 account.login 으로 자격증명을 검증하고 신설 platform.systemWhoAmI 로 OPERATOR 여부를 확인한 뒤 콘솔 JWT 를 발급하며, 성공한 자격증명은 그 엔드포인트 세션에 자동 등록됩니다. env 단일 계정은 제거되었고 IP allowlist 는 심층 방어로 유지됩니다.
Endpoints 탭이 신설되어 엔드포인트 목록·라이브 상태(연결됨/인증 필요/연결 실패)·엔드포인트별 자격증명 등록(연결)/해제를 화면에서 관리합니다. 자격증명 검증은 verifyOperatorCredential 헬퍼가 세션 매니저 밖에서 수행하고 성공 시에만 등록합니다 — 라이브 테스트에서 발견한 "실패한 로그인 시도가 기존 등록 세션을 파괴하는" 버그를 이 원칙으로 교정했습니다.
이관 완료로 콘솔의 db.ts(raw pg)와 DB_* env 의존이 삭제되어, 콘솔은 DB 를 전혀 모르는 순수 API 클라이언트가 되었습니다.
5. 운영 공백 세 건 — 부트스트랩 · scope · 볼트/프로비저닝
계정 기반 전환이 만든 운영 공백을 같은 PR 에서 닫았습니다.
최초 OPERATOR 부트스트랩. OPERATOR 부여 UI 자체가 OPERATOR 로그인 뒤에 있어 신규 배포에는 최초 로그인 가능한 계정이 없습니다. API 부팅 시 OPERATOR_BOOTSTRAP_EMAIL/PASSWORD env 가 있고 OPERATOR 가 전무할 때만 계정을 생성합니다(멱등 — 이미 있으면 무시하므로 env 상주가 안전).
account.login 의 scope: "account" 옵션. 소속이 정확히 1개인 계정은 로그인 시 조직 세션으로 바로 입장하는 것이 기존 UX 인데, 그 조직 토큰으로는 systemWhoAmI(계정 세션 필요)가 거부되어 워크스페이스를 가진 운영자는 콘솔 로그인이 항상 실패했습니다. scope: "account" 를 지정하면 소속 수와 무관하게 계정 스코프 세션을 발급합니다. 미지정 시 동작은 불변이므로 기존 앱 로그인에는 영향이 없습니다.
자격증명 볼트와 스태프 프로비저닝(§5-2). 세션 매니저의 자격증명은 인메모리라 콘솔 재시작 시 전 엔드포인트 재연결이 필요했습니다. SYSADMIN_VAULT_KEY(API 키와 별개의 전용 키) 설정 시 자격증명을 AES-256-GCM 암호화 파일에 영속해 재시작 후 자동 복원합니다(opt-in, 리프레시 토큰은 비저장). 또한 새 엔드포인트에는 벤더 스태프의 계정 자체가 없어 역할 부여만으로는 부족하므로, Platform Staff 탭에 엔드포인트 선택 축과 스태프 계정 생성(systemCreatePlatformStaff — 생성+역할 부여 원샷)을 추가했습니다.
검증
전 단계를 로컬 라이브로 검증했습니다: 읽기 8종·쓰기 6배치 전 라이프사이클(중복 409·미존재 404·검증 400 포함), 감사 로그 실측(OPERATOR·redact·org=null), 프로비저닝 풀 사이클(생성 계정 로그인까지), 콘솔 인증 전 분기(401/403/502), 볼트 6시나리오(암호화 파일·재시작 복원·오류 키·해제), 2-엔드포인트 시뮬레이션, Playwright UI 워크스루. pnpm lint:check + 전체 pnpm build 그린.