이용요금(과금) 정책 스펙
개요
Simple HR 프로젝트의 이용요금 정책은 인원 규모에 따른 구간제 과금과 기능 범위에 따른 플랜 선택을 결합한 구조다. 2026년 6월 설계를 바탕으로 DB 스키마, API, 서비스 레이어, 프론트엔드를 통합 구현한다. 설계-요금제시스템-md
구현 목표
| # | 목표 | 대상 |
|---|---|---|
| 1 | 시스템관리자가 플랜 단가를 설정/수정 | SYS_ADMIN |
| 2 | 테넌트별 사용량·과금 실시간 조회 | SYS_ADMIN |
| 3 | 체험판 연장/단축/정식 전환 | SYS_ADMIN |
| 4 | 월별 과금 스냅샷 생성 및 이력 관리 | SYS_ADMIN |
| 5 | 플랜별 메뉴 접근 제한 실동작 | 전체 |
| 6 | 회사 최고관리자 정산 페이지 (읽기 전용) | COMP_ADMIN (is_system) |
인원 구간별 기본 과금 구조
| 인원 구간 | 과금 방식 |
|---|---|
| 5명 이하 | 무료 |
| 5명 초과 ~ 10명 이하 | 월 20,000원 (기본비용 정액 청구) |
| 10명 초과 | 기본 20,000원 + 초과 인원 × 플랜 단가 |
재직인원 기준: Employee.employment_status != "EMP_RETIRED" AND Employee.tenant_id == tenant_id 설계-요금제시스템-md
구간별 과금 공식
재직인원 ≤ 5명 → 0원
5 < 재직인원 ≤ 10명 → 정액 20,000원 (PLAN_FREE는 0원)
재직인원 > 10명 → 정액 + (재직인원 - 10) × 1인당 단가예시:
- 스탠다드 11명: 20,000 + 1 × 3,000 = 23,000원
- 무료 15명: 0원 (단가 0, 정액 0)
플랜별 1인당 단가 및 기능 범위
| 플랜 코드 | 플랜명 | 1인당 단가 | 메뉴 범위 |
|---|---|---|---|
| PLAN_FREE | 무료 | 0원 | 이러닝·전자근로계약서 제외 |
| PLAN_BASIC | 베이직 | 2,000원 | 이러닝·전자근로계약서 제외 |
| PLAN_STD | 스탠다드 | 3,000원 | 전체 메뉴 |
| PLAN_SPC | 스페셜 | 10,000원 | 전체 메뉴 + 페이롤 연동 |
DB 스키마
BillingPlan (신규 테이블)
CREATE TABLE billing_plans (
id SERIAL PRIMARY KEY,
plan_code VARCHAR(30) UNIQUE NOT NULL,
name_ko VARCHAR(100) NOT NULL,
name_en VARCHAR(100),
price_per_user INTEGER NOT NULL DEFAULT 0,
base_free_seats INTEGER NOT NULL DEFAULT 5,
flat_fee_seats INTEGER NOT NULL DEFAULT 10,
flat_fee_amount INTEGER NOT NULL DEFAULT 20000,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
description TEXT,
created_by UUID,
updated_by UUID,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);시드 데이터: PLAN_FREE, PLAN_BASIC, PLAN_STD, PLAN_SPC (4종 자동 삽입) 설계-요금제시스템-md
BillingSnapshot (신규 테이블)
CREATE TABLE billing_snapshots (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
billing_month VARCHAR(7) NOT NULL, -- "2026-06"
plan_code VARCHAR(30) NOT NULL,
active_employee_count INTEGER NOT NULL,
price_per_user INTEGER NOT NULL,
base_amount INTEGER NOT NULL DEFAULT 0,
extra_amount INTEGER NOT NULL DEFAULT 0,
total_amount INTEGER NOT NULL,
memo TEXT,
created_by UUID,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (tenant_id, billing_month)
);월별 테넌트 과금 이력 관리 설계-요금제시스템-md
TenantBillingOverride (신규 테이블 — 구조만)
CREATE TABLE tenant_billing_overrides (
id UUID PRIMARY KEY,
tenant_id UUID UNIQUE NOT NULL REFERENCES tenants(id),
custom_price_per_user INTEGER,
custom_flat_fee INTEGER,
discount_reason VARCHAR(500),
valid_from TIMESTAMP,
valid_until TIMESTAMP,
created_by UUID,
updated_by UUID,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);업체별 정산 및 커스텀 단가 관리용 (후순위) 설계-요금제시스템-md
기존 모델 변경
- PlanCode enum (enums.py): PLAN_FREE, PLAN_BASIC, PLAN_STD, PLAN_SPC (PLAN_PRM 제거)
- Menu.visible_tiers (models.py): 플랜별 메뉴 접근 가능 목록 (JSONB)
- 이러닝·계약서 메뉴:
["PLAN_STD", "PLAN_SPC"] - 나머지 메뉴:
["PLAN_FREE", "PLAN_BASIC", "PLAN_STD", "PLAN_SPC"]
API 설계
시스템관리자 API
GET /v2/system/billing-plans
→ { plans: BillingPlanInfo[] }
PUT /v2/system/billing-plans/{plan_code}
← { name_ko?, price_per_user?, flat_fee_amount?, ... }
→ BillingPlanInfo
GET /v2/system/usage
→ { items: TenantUsageInfo[], summary: UsageSummary }
POST /v2/system/billing-snapshots/generate
← { billing_month: str }
→ { created_count: int, total_amount: int }
GET /v2/system/billing-snapshots?month=2026-06
→ { items: BillingSnapshotInfo[], total: int }
PATCH /v2/system/tenants/{id}/trial
← { action: "extend"|"shorten"|"convert_to_active", days?: int }
→ TenantDetailInfo회사관리자 API (최고관리자 전용, 읽기 전용)
GET /v2/admin/billing/current
→ { plan_code, plan_name, active_employee_count,
price_per_user, base_amount, extra_amount, total_amount, billing_month }
GET /v2/admin/billing/history
→ { items: BillingSnapshotInfo[], total: int }권한: require_company_admin + employee.admin_role.is_system == True 체크 설계-요금제시스템-md
서비스 레이어
파일: backend/app/services/billing_service.py
# 1. 순수 계산 함수 (단위 테스트 대상)
def calc_monthly_bill(active_count, price_per_user,
base_free_seats=5, flat_fee_seats=10,
flat_fee_amount=20000) -> dict
# 2. DB 의존 함수
async def count_active_employees(db, tenant_id) -> int
async def get_billing_plan(db, plan_code) -> BillingPlan | None
async def get_all_billing_plans(db) -> list[BillingPlan]
async def update_billing_plan(db, plan_code, data, admin_id) -> BillingPlan
# 3. 사용량 조회
async def get_all_tenants_usage(db) -> list[dict]
async def get_tenant_current_billing(db, tenant_id) -> dict
# 4. 스냅샷
async def generate_monthly_snapshots(db, billing_month, admin_id) -> dict
async def get_billing_snapshots(db, tenant_id=None, month=None) -> list[dict]
# 5. 체험판 관리
async def update_tenant_trial(db, tenant_id, action, days, admin_id) -> Tenant플랜별 메뉴 접근 제한
권위 소스
Menu.visible_tiers JSONB 필드를 권위 소스로 사용. 메뉴 데이터와 허용 플랜 정보가 한 곳에 있어 동기화 이슈 없음. 설계-요금제시스템-md
연동 방식
기존 menu_permission_guard 미들웨어에 플랜 체크 추가:
요청 → path_to_menu(path) → menu_key 추출
→ [1] menu_allowed(menu_key, user.menu_keys) (기존: 관리자 역할 체크)
→ [2] plan_menu_allowed(tenant_id, menu_key) (추가: 플랜 체크)
→ 둘 다 통과해야 허용캐시 전략
Menu.visible_tiers 데이터를 앱 시작 시 메모리에 로드:
# {menu_url: set(plan_codes)} — 해당 메뉴에 접근 가능한 플랜 목록
_MENU_PLAN_CACHE: dict[str, set[str]] = {}- 시작 시 1회 로드 (Menu 테이블 전체 조회)
- 라이센스 메뉴 수정 시 캐시 갱신
- 매 요청마다 DB 조회 없이 딕셔너리 lookup
메뉴별 플랜 허용 규칙
| 메뉴 그룹 | PLAN_FREE | PLAN_BASIC | PLAN_STD | PLAN_SPC |
|---|---|---|---|---|
| 급여관리 (4개) | O | O | O | O |
| 이러닝센터 (5개) | X | X | O | O |
| 전자근로계약서 | X | X | O | O |
| 나머지 전체 | O | O | O | O |
마이그레이션에서 이러닝/계약서 메뉴의 visible_tiers를 ["PLAN_STD", "PLAN_SPC"]로 설정. 설계-요금제시스템-md
프론트엔드 변경
시스템관리자 페이지
- 요금제 관리 (
/system-admin/plans): GET /v2/system/billing-plans 연동, 플랜 카드 4개, 단가 수정 가능 - 회사 관리 (
/system-admin/companies): 플랜 선택 드롭다운 4종, 필터에 무료/베이직 추가 - 사용량 페이지 (신규): GET /v2/system/usage 연동, 테넌트별 과금 테이블, 요약 카드, 스냅샷 생성 버튼
- 라이센스 메뉴 관리 (
/system-admin/license-menus): Menu.visible_tiers 직접 읽고 쓰기, 4개 플랜 열
회사 최고관리자 페이지
정산 페이지 (신규: /admin/billing):
- 사이드바에 "정산" 메뉴 (is_system 관리자만 표시)
- 현재 월: 플랜, 인원, 단가, 기본료, 추가분, 합계
- 청구 이력 테이블 (월별)
과금 프로세스 흐름
마이그레이션 전략
- 1.
billing_plan_migration.py를main.pystartup에 등록 (기존 패턴) - 2.서버 재시작 시 자동 실행
- 3.실행 순서:
- BillingPlan 테이블 자동 생성 (SQLAlchemy create_all)
- tenants.plan_code에서 PLAN_PRM → PLAN_SPC 치환
- BillingPlan 시드 4종 삽입 (멱등)
- Menu.visible_tiers 업데이트 (이러닝/계약서 → STD/SPC만, 나머지 → 전체 플랜)
- enum에서 PLAN_PRM 제거
신규 파일 생성
backend/app/services/billing_service.pybackend/app/schemas/billing.pybackend/app/api/v2/billing.pybackend/app/db/seed/billing_plan_migration.pybackend/tests/unit/test_billing_service.pybackend/tests/integration/test_billing_api.pyfrontend/src/app/admin/billing/page.tsx
수정 대상 파일
backend/app/db/enums.pybackend/app/db/models.pybackend/app/main.pybackend/app/api/v2/system.pybackend/app/api/v2/sysadmin.pybackend/app/schemas/system.pybackend/app/schemas/sysadmin.pybackend/app/services/sysadmin_service.pyfrontend/src/app/system-admin/plans/page.tsxfrontend/src/app/system-admin/companies/page.tsxfrontend/src/app/system-admin/license-menus/page.tsxfrontend/src/components/admin/Sidebar.tsxfrontend/src/lib/api.ts