/SimpleHR project
SimpleHR project

이용요금(과금) 정책 스펙

conceptedited by Cairni · 방금 · AIv3

개요

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)

설계-요금제시스템-md


인원 구간별 기본 과금 구조

인원 구간과금 방식
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)

설계-요금제시스템-md


플랜별 1인당 단가 및 기능 범위

플랜 코드플랜명1인당 단가메뉴 범위
PLAN_FREE무료0원이러닝·전자근로계약서 제외
PLAN_BASIC베이직2,000원이러닝·전자근로계약서 제외
PLAN_STD스탠다드3,000원전체 메뉴
PLAN_SPC스페셜10,000원전체 메뉴 + 페이롤 연동

설계-요금제시스템-md


DB 스키마

BillingPlan (신규 테이블)

sql
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 (신규 테이블)

sql
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 (신규 테이블 — 구조만)

sql
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"]

설계-요금제시스템-md


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

설계-요금제시스템-md

회사관리자 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

python
# 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

설계-요금제시스템-md


플랜별 메뉴 접근 제한

권위 소스

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)   (추가: 플랜 체크)
     → 둘 다 통과해야 허용

설계-요금제시스템-md

캐시 전략

Menu.visible_tiers 데이터를 앱 시작 시 메모리에 로드:

python
# {menu_url: set(plan_codes)}  — 해당 메뉴에 접근 가능한 플랜 목록
_MENU_PLAN_CACHE: dict[str, set[str]] = {}
  • 시작 시 1회 로드 (Menu 테이블 전체 조회)
  • 라이센스 메뉴 수정 시 캐시 갱신
  • 매 요청마다 DB 조회 없이 딕셔너리 lookup

설계-요금제시스템-md

메뉴별 플랜 허용 규칙

메뉴 그룹PLAN_FREEPLAN_BASICPLAN_STDPLAN_SPC
급여관리 (4개)OOOO
이러닝센터 (5개)XXOO
전자근로계약서XXOO
나머지 전체OOOO

마이그레이션에서 이러닝/계약서 메뉴의 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개 플랜 열

설계-요금제시스템-md

회사 최고관리자 페이지

정산 페이지 (신규: /admin/billing):

  • 사이드바에 "정산" 메뉴 (is_system 관리자만 표시)
  • 현재 월: 플랜, 인원, 단가, 기본료, 추가분, 합계
  • 청구 이력 테이블 (월별)

설계-요금제시스템-md


과금 프로세스 흐름

마이그레이션 전략

  1. 1.billing_plan_migration.pymain.py startup에 등록 (기존 패턴)
  2. 2.서버 재시작 시 자동 실행
  3. 3.실행 순서:
  • BillingPlan 테이블 자동 생성 (SQLAlchemy create_all)
  • tenants.plan_code에서 PLAN_PRM → PLAN_SPC 치환
  • BillingPlan 시드 4종 삽입 (멱등)
  • Menu.visible_tiers 업데이트 (이러닝/계약서 → STD/SPC만, 나머지 → 전체 플랜)
  • enum에서 PLAN_PRM 제거

설계-요금제시스템-md


신규 파일 생성

  • backend/app/services/billing_service.py
  • backend/app/schemas/billing.py
  • backend/app/api/v2/billing.py
  • backend/app/db/seed/billing_plan_migration.py
  • backend/tests/unit/test_billing_service.py
  • backend/tests/integration/test_billing_api.py
  • frontend/src/app/admin/billing/page.tsx

설계-요금제시스템-md


수정 대상 파일

  • backend/app/db/enums.py
  • backend/app/db/models.py
  • backend/app/main.py
  • backend/app/api/v2/system.py
  • backend/app/api/v2/sysadmin.py
  • backend/app/schemas/system.py
  • backend/app/schemas/sysadmin.py
  • backend/app/services/sysadmin_service.py
  • frontend/src/app/system-admin/plans/page.tsx
  • frontend/src/app/system-admin/companies/page.tsx
  • frontend/src/app/system-admin/license-menus/page.tsx
  • frontend/src/components/admin/Sidebar.tsx
  • frontend/src/lib/api.ts

설계-요금제시스템-md


범위 제외

  • PG 연동
  • 자동 청구
  • 업체별 커스텀 단가 UI (구조만 준비)

설계-요금제시스템-md


관련 페이지

Made with CairniExplore public wikis →