Skip to content

현재 사용법 (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 그린.