Adit 정산 시스템 설계 문서
최종 업데이트: 2026-02-26
1. 개요
1.1 정산 시스템의 역할
Adit 정산 시스템은 캠페인 완료 후 광고주로부터 대금을 수령하고 파트너(매체사)에게 정산금을 지급하는 전체 플로우를 관리합니다.
1.2 핵심 참여자
| 참여자 | 역할 |
|---|---|
| 광고주 | 캠페인 비용 지불, 세금계산서 수령 |
| 파트너(매체사) | 콘텐츠 업로드, 정산금 수령, 세금계산서 발행 |
| Adit/대행사 | 중개, 세금계산서 발행/수령, 수수료 정산, 원천징수 |
| 어드민 | 정산 처리, 입금 확인, 송금 처리 |
2. 정산 유형
Adit 정산 (ADIT_SETTLEMENT)
광고주 → [입금] → Adit/대행사 계좌 → [정산금 송금] → 파트너
↓ ↓
세금계산서 수령 세금계산서 수령
(Bolta 자동 정발행) (파트너 발행 or Bolta 역발행)- 수수료 모델: 캠페인 금액의 N% (파트너별 설정, 기본 20%)
- 정산금: 캠페인 금액 - 수수료
- 원천징수: 개인사업자만 3.3% (정산금 기준)
- 실지급액: 정산금 - 원천징수
정산 유형 변경
어드민이 정산 유형을 ADIT_SETTLEMENT ↔ DIRECT_INVOICE로 변경 가능합니다.
- 변경 시 수수료/원천징수가 재계산됩니다
- 정산 상태가
SETTLEMENT_PENDING으로 초기화됩니다 - API:
PATCH /api/admin/settlements/:id/type
3. 수수료 및 원천징수
3.1 수수료 체계
| 항목 | 기본값 | 설정 위치 |
|---|---|---|
| Adit 정산 수수료율 | 20% | Partner.commissionRate |
| 캠페인별 오버라이드 | 가능 | Campaign.commissionRate |
| 직정산 수수료 | 0% (고정) | - |
수수료율 적용 우선순위:
Campaign.commissionRate(설정된 경우)Partner.commissionRate(파트너 기본값)- 시스템 기본값: 20% (Prisma 스키마 default)
3.2 원천징수
| 조건 | 원천징수율 | 적용 |
|---|---|---|
| 개인사업자(INDIVIDUAL) + ADIT_SETTLEMENT | 3.3% | ✅ |
| 법인사업자(CORPORATE) + ADIT_SETTLEMENT | 0% | ❌ |
| DIRECT_INVOICE (개인/법인 무관) | 0% | ❌ |
- 계산 기준: 정산금(settlementAmount) 기준
- 세율 구성: 소득세 3% + 지방소득세 0.3% = 3.3%
- 코드 위치:
campaign-settlement.service.ts(하드코딩)
3.3 금액 계산 공식
campaignPrice = Campaign.price (캠페인 가격)
commissionRate = Campaign.commissionRate ?? Partner.commissionRate (기본 20%)
commissionAmount = Math.round(campaignPrice × commissionRate / 100)
settlementAmount = campaignPrice - commissionAmount
withholdingTaxRate = (INDIVIDUAL + ADIT_SETTLEMENT) ? 3.3 : 0
withholdingTaxAmount = Math.round(settlementAmount × withholdingTaxRate / 100)
payoutAmount = settlementAmount - withholdingTaxAmount예시 (캠페인 100만원, 수수료 20%, 개인사업자, ADIT 정산):
| 항목 | 계산 | 금액 |
|---|---|---|
| 캠페인 가격 | - | 1,000,000원 |
| 수수료 (20%) | 1,000,000 × 0.2 | 200,000원 |
| 정산금 | 1,000,000 - 200,000 | 800,000원 |
| 원천징수 (3.3%) | 800,000 × 0.033 | 26,400원 |
| 실지급액 | 800,000 - 26,400 | 773,600원 |
4. 3계층 정산 구조
4.1 구조 개요
4.2 정산 모드
| 모드 | 광고주 청구 | 파트너 지급 | 사용 케이스 |
|---|---|---|---|
| 건별 정산 | 캠페인마다 청구서 | 캠페인마다 지급 | 소량 거래, 직정산 |
| 월별 정산 | 월 1회 합산 청구 | 월 1회 합산 지급 | 대량 거래, 정기 정산 |
현재 구현: 건별 정산만 구현됨. 월별 정산은 Phase 2에서 구현 예정.
5. 대행사(Agency) 지원
5.1 개념
정산 주체가 Adit(르도드코퍼레이션) 외에 다른 대행사도 될 수 있음.
| 대행사 | 코드 | 정산 계좌 |
|---|---|---|
| 르도드코퍼레이션 주식회사 (Adit) | ADIT | 신한은행 100-038-478766 (르도드코퍼레이션 주식회사) |
| 미드나잇웨이브 | MNW | 신한은행 110-XXX-XXXXXX (주식회사 미드나잇웨이브) |
5.2 적용 범위
- Campaign.agencyId: 캠페인 담당 대행사
- Settlement.agencyId: 정산 담당 대행사 (Campaign과 동기화)
- AdminUser.agencyId: 어드민 소속 대행사
- AdvertiserInvoice.agencyId: 청구서 발행 주체
- 광고주 입금 계좌: 대행사별로 다름
5.3 권한 분리
| 소속 | 볼 수 있는 데이터 |
|---|---|
슈퍼어드민 (agencyId = null) | 모든 대행사 데이터 |
Adit 어드민 (agencyId = 1) | Adit 담당 캠페인/정산만 |
MNW 어드민 (agencyId = 2) | 미드나잇웨이브 담당 캠페인/정산만 |
5.4 대행사-정산 동기화
syncSettlementAgencyByCampaignId() 함수로 Campaign.agencyId 변경 시 Settlement.agencyId 자동 동기화.
6. 정산 플로우
6.1 Adit 정산 전체 플로우
캠페인 업로드 완료
Campaign.status = UPLOADED
→ Settlement 자동 생성 (수수료/원천징수 계산)
→ AdvertiserInvoice 자동 생성 (ADIT_SETTLEMENT인 경우)
→ Bolta 정발행 자동 요청 (광고주 세금계산서)
파트너가 "정산 요청" 클릭
Settlement.status = AWAITING_ADVERTISER_PAYMENT- 내부팀 슬랙 알림 (@adit_set)
- 법인사업자: Bolta 역발행 자동 요청 → 승인 URL 반환
- 개인사업자: 역발행 스킵 (정산 요청만 처리)
광고주 입금 확인
Settlement.status = ADVERTISER_PAID- 광고주 이메일: "입금이 확인되었습니다"
- 파트너 이메일: "곧 정산금이 지급됩니다"
파트너에게 세금계산서 요청 (지급 시작)
Settlement.status = PARTNER_PAYOUT_PROCESSINGPartnerPayout생성- 파트너 이메일: "세금계산서 발행 부탁드립니다"
파트너 세금계산서 수령 확인
PartnerPayout.partnerInvoiceReceivedAt기록
파트너 정산금 송금
Settlement.status = SETTLEMENT_COMPLETEDCampaign.status = COMPLETED- 파트너 이메일: "정산이 완료되었습니다 (₩금액)"
6.2 매체사 직정산 플로우
캠페인 업로드 완료
Campaign.status = UPLOADED
→ Settlement 자동 생성 (수수료/원천징수 0%)
파트너가 "정산 요청" 클릭
Settlement.status = AWAITING_ADVERTISER_PAYMENT- 광고주 이메일: "매체사에 직접 정산해주세요" + 파트너 계좌 정보
파트너가 세금계산서 발급 완료 표시
Settlement.invoiceIssuedAt기록
파트너가 "입금 확인" 클릭
Settlement.status = SETTLEMENT_COMPLETEDCampaign.status = COMPLETED
6.3 정산 상태 전이 (State Machine)
상태 전이 규칙 (settlement-state-machine.ts):
| 현재 상태 | 가능한 전이 |
|---|---|
| SETTLEMENT_PENDING | → AWAITING_ADVERTISER_PAYMENT, SETTLEMENT_CANCELLED |
| AWAITING_ADVERTISER_PAYMENT | → ADVERTISER_PAID, SETTLEMENT_CANCELLED |
| ADVERTISER_PAID | → PARTNER_PAYOUT_PROCESSING, SETTLEMENT_CANCELLED |
| PARTNER_PAYOUT_PROCESSING | → SETTLEMENT_COMPLETED, SETTLEMENT_CANCELLED |
| SETTLEMENT_COMPLETED | (종료 상태) |
| SETTLEMENT_CANCELLED | (종료 상태) |
7. Bolta 세금계산서 연동
7.1 정발행 (광고주 청구서)
Adit 정산에서 광고주에게 발행하는 세금계산서를 Bolta API로 자동 처리합니다.
| 단계 | 설명 |
|---|---|
| 1 | 캠페인 업로드 완료 시 AdvertiserInvoice 자동 생성 |
| 2 | Bolta API로 정발행 요청 (자동) |
| 3 | 웹훅 SUCCESS → 세금계산서 번호/URL 저장 |
| 4 | Settlement.status → AWAITING_ADVERTISER_PAYMENT (자동 전이) |
| 5 | 실패 시: Slack 알림 + 어드민 수동 재시도 (POST /retry-issuance) |
7.2 역발행 (파트너 세금계산서 · 법인사업자)
법인사업자 파트너의 세금계산서를 Bolta 역발행으로 자동 처리합니다.
| 단계 | 설명 |
|---|---|
| 1 | 정산 요청 시 Bolta 역발행 자동 요청 |
| 2 | 파트너에게 승인 URL 이메일 발송 (10분 유효) |
| 3 | 파트너 승인 → 세금계산서 자동 발행 |
| 4 | 승인 URL 만료 시 파트너 콘솔에서 재발급 가능 |
7.3 BoltaReverseIssuance 모델
model BoltaReverseIssuance {
id BigInt @id @default(autoincrement())
settlementId String @unique // SET{NNN}
issuanceKey String // Bolta 발행 키
status String // PENDING, SUCCESS, FAILURE, CANCELLED
grantUrl String? // 역발행 승인 URL (10분 유효)
failureCode String?
failureMessage String?
}7.4 개인사업자 처리
개인사업자(businessEntityType = INDIVIDUAL)는 Bolta 역발행을 사용하지 않습니다.
- 정산 요청 시 역발행 스킵
- AdvertiserInvoice는 정상 생성
- 파트너가 홈택스 등을 통해 직접 세금계산서 발행
8. DB 스키마
8.1 Settlement (정산)
model Settlement {
id String @id // "SET001"
campaignId BigInt @unique
partnerId BigInt
agencyId BigInt? // null = Adit 기본
// 금액 계산
campaignPrice Decimal(15,2)
commissionRate Decimal(5,2) // 수수료율 (기본 20%)
commissionAmount Decimal(15,2) // 수수료 금액
settlementAmount Decimal(15,2) // 정산금 (price - commission)
withholdingTaxRate Decimal(5,2) // 원천징수율 (0 or 3.3)
withholdingTaxAmount Decimal(15,2) // 원천징수 금액
payoutAmount Decimal(15,2) // 실지급액 (settlement - withholding)
// 상태
status SettlementStatus
settlementType SettlementType
// 2계층 연결
advertiserInvoiceId BigInt? // N:1 → AdvertiserInvoice
partnerPayoutId BigInt? // N:1 → PartnerPayout
// 직정산 전용
invoiceIssuedAt DateTime?
invoicePaidAt DateTime?
// Bolta 역발행 수령 확인
partnerInvoiceReceivedAt DateTime?
// 파트너 계좌 스냅샷
bankName String?
accountNumber String?
accountHolder String?
// 타임스탬프
requestedAt DateTime?
processedAt DateTime?
completedAt DateTime?
adminNotes String? @db.Text
}8.2 Agency (대행사)
model Agency {
id BigInt @id @default(autoincrement())
name String // "르도드코퍼레이션"
code String @unique // "ADIT"
businessName String // 사업자등록증상 상호
businessNumber String // 사업자번호
representativeName String // 대표자명
bankName String // "신한은행"
accountNumber String // "100-038-478766"
accountHolder String // "르도드코퍼레이션 주식회사"
email String
isActive Boolean @default(true)
}8.3 AdvertiserInvoice (광고주 청구서)
model AdvertiserInvoice {
id BigInt @id @default(autoincrement())
invoiceNumber String @unique // "INV-2026-01-001"
advertiserId BigInt
advertiserName String
agencyId BigInt? // 정산 주체 (null이면 Adit 기본값)
// 유형
invoiceType String // PER_CAMPAIGN | MONTHLY
periodStart DateTime? // 월별 정산 시
periodEnd DateTime? // 월별 정산 시
// 금액
subtotal Decimal // 공급가액
taxAmount Decimal // 부가세 (10%)
totalAmount Decimal // 합계
// 세금계산서
taxInvoiceNumber String?
taxInvoiceFileKey String?
taxInvoiceIssuedAt DateTime?
// 입금
status InvoiceStatus // DRAFT, ISSUED, PAID, OVERDUE, CANCELLED
dueDate DateTime?
paidAt DateTime?
// 입금 계좌 스냅샷
bankName String?
accountNumber String?
accountHolder String?
// Relations
settlements Settlement[]
}8.4 PartnerPayout (파트너 지급)
model PartnerPayout {
id BigInt @id @default(autoincrement())
payoutNumber String @unique // "PAY-2026-01-001"
partnerId BigInt
partnerName String
// 유형
payoutType String // PER_CAMPAIGN | MONTHLY
periodStart DateTime? // 월별 정산 시
periodEnd DateTime? // 월별 정산 시
// 금액
subtotal Decimal // 정산금 합계
taxAmount Decimal // 원천징수 금액
totalAmount Decimal // 실지급액 (정산금 - 원천징수)
// 파트너 세금계산서
partnerInvoiceNumber String?
partnerInvoiceFileKey String?
partnerInvoiceReceivedAt DateTime?
// 지급
status PayoutStatus // PENDING, INVOICE_RECEIVED, PROCESSING, COMPLETED, CANCELLED
paidAt DateTime?
transactionId String?
// 계좌 스냅샷
bankName String?
accountNumber String?
accountHolder String?
// Relations
settlements Settlement[]
}9. API 엔드포인트
9.1 어드민 정산 API
| Method | Endpoint | 설명 |
|---|---|---|
| GET | /api/admin/settlements | 정산 목록 조회 (필터: partner, campaign, status, date, agency) |
| GET | /api/admin/settlements/:id | 정산 상세 조회 |
| GET | /api/admin/settlements/:id/reverse-issuance-status | Bolta 역발행 상태 실시간 조회 + DB 동기화 |
| PATCH | /api/admin/settlements/:id/status | 정산 상태 변경 |
| PATCH | /api/admin/settlements/:id/type | 정산 유형 변경 (ADIT ↔ DIRECT, 재계산) |
| POST | /api/admin/settlements/:id/start-payout | 파트너 지급 시작 |
| POST | /api/admin/settlements/:id/complete | 정산 완료 (+ 캠페인 COMPLETED) |
| POST | /api/admin/settlements/:id/retry-issuance | Bolta 정발행 재시도 (ADIT_SETTLEMENT 전용) |
| POST | /api/admin/settlements/:id/invoice-issued | 세금계산서 발급 완료 (DIRECT_INVOICE) |
| POST | /api/admin/settlements/:id/invoice-paid | 입금 확인 (DIRECT_INVOICE → 즉시 완료) |
| POST | /api/admin/settlements/:id/cancel | 정산 취소 |
9.2 파트너 정산 API
| Method | Endpoint | 설명 |
|---|---|---|
| GET | /api/partner/settlements | 내 정산 목록 |
| GET | /api/partner/settlements/summary | 정산 요약 (상태별 건수/금액) |
| GET | /api/partner/settlements/:id | 정산 상세 |
| POST | /api/partner/settlements/:id/request | 정산 요청 (법인: 역발행 자동) |
| POST | /api/partner/settlements/:id/invoice-issued | 세금계산서 발급 완료 (DIRECT) |
| POST | /api/partner/settlements/:id/invoice-paid | 입금 확인 (DIRECT → 완료) |
| GET | /api/partner/settlements/:id/reverse-issuance-grant-url | Bolta 역발행 URL 재발급 |
9.3 AdvertiserInvoice / PartnerPayout (내부 서비스)
AdvertiserInvoice와 PartnerPayout은 별도 API 라우트가 없습니다. Settlement 엔드포인트를 통해 자동 생성/관리됩니다.
- AdvertiserInvoice: 캠페인 업로드 완료 시 자동 생성 + Bolta 정발행
- PartnerPayout:
start-payout호출 시 자동 생성
9.4 대행사 API
| Method | Endpoint | 설명 | 권한 |
|---|---|---|---|
| GET | /api/admin/agencies | 대행사 목록 조회 | Admin |
| GET | /api/admin/agencies/default | 기본 대행사(ADIT) 조회 | Admin |
| GET | /api/admin/agencies/:id | 대행사 상세 조회 | Admin |
| GET | /api/admin/agencies/code/:code | 코드로 대행사 조회 | Admin |
| GET | /api/admin/agencies/:id/settlement-account | 대행사 정산 계좌 조회 | Admin |
| POST | /api/admin/agencies | 대행사 생성 | SUPER_ADMIN |
| PUT | /api/admin/agencies/:id | 대행사 수정 | Admin |
| DELETE | /api/admin/agencies/:id | 대행사 비활성화 | SUPER_ADMIN |
9.5 견적서 API
| Method | Endpoint | 설명 |
|---|---|---|
| GET | /api/admin/quotation | 견적서 목록 조회 |
| POST | /api/admin/quotation | 견적서 저장 (DRAFT) |
| GET | /api/admin/quotation/:quoId | 견적서 상세 조회 |
| PUT | /api/admin/quotation/:quoId | 견적서 수정 (DRAFT만 가능) |
| DELETE | /api/admin/quotation/:quoId | 견적서 삭제 (소프트 → CANCELLED) |
| PATCH | /api/admin/quotation/:quoId/status | 견적서 상태 변경 |
| POST | /api/admin/quotation/:quoId/execute | 견적서 실행 → 캠페인 일괄 생성 |
| GET | /api/admin/quotation/:quoId/download | 견적서 XLSX 다운로드 |
| POST | /api/admin/quotation/generate | 견적서 XLSX 즉시 생성 (저장 없이 다운로드) |
9.6 사업자등록정보 검증 API (NTS)
| Method | Endpoint | 설명 |
|---|---|---|
| POST | /api/public/business/verify | 사업자등록번호 진위확인 + 상태조회 (인증 불필요) |
NTS API는 공개 엔드포인트입니다 (인증 불필요). 가입/온보딩 시 사업자번호 검증에 사용됩니다. Rate limit: 15분당 IP당 30회.
10. 알림 시스템
10.1 광고주 알림
| 이벤트 | 트리거 | 채널 |
|---|---|---|
INVOICE_ISSUED_ADVERTISER | 세금계산서 발행 (Bolta 정발행) | |
PAYMENT_REQUEST_ADVERTISER | 입금 요청 (계좌 안내) | |
PAYMENT_CONFIRMED_ADVERTISER | 입금 확인 완료 | |
DIRECT_SETTLEMENT_REQUEST | 매체사 직정산 안내 |
10.2 파트너 알림
| 이벤트 | 트리거 | 채널 |
|---|---|---|
ADVERTISER_PAID_PARTNER | 광고주 입금 완료 | |
INVOICE_REQUEST_PARTNER | 세금계산서 발행 요청 | |
PAYOUT_COMPLETED_PARTNER | 정산금 송금 완료 |
10.3 내부 알림
| 이벤트 | 트리거 | 채널 |
|---|---|---|
SETTLEMENT_REQUESTED | 파트너 정산 요청 | Slack (@adit_set) |
| 정산 상태 변경 | 각 단계별 | Slack Thread |
11. 구현 현황
11.1 완료 ✅
- Settlement 기본 모델 (수수료, 원천징수, 실지급액)
- 정산 상태 전이 State Machine
- 파트너 정산 요청 API
- 어드민 정산 관리 API (상태 변경, 유형 변경, 취소)
- Agency 모델 및 API
- AdminUser.agencyId, Campaign.agencyId 확장
- Settlement.agencyId 동기화
- AdvertiserInvoice 모델 및 API (건별)
- PartnerPayout 모델 및 API (건별)
- Bolta 정발행 (광고주 세금계산서)
- Bolta 역발행 (법인사업자 파트너 세금계산서)
- 원천징수 계산 (개인사업자 3.3%)
- 광고주 정산 알림 (세금계산서, 입금 요청, 입금 확인)
- 파트너 정산 알림 (세금계산서 요청, 송금 완료)
- 캠페인 상태 변경 알림 (Slack, Email)
- 슬랙 캠페인별 스레드
- 정산 유형 변경 (ADIT ↔ DIRECT, 재계산)
- 매체사 직정산 플로우 (발급/입금 확인)
- 역발행 승인 URL 재발급
11.2 Phase 2 (추후 구현)
- 월별 일괄 정산 (
MONTHLY모드) - SettlementConfig (광고주/파트너별 정산 주기 설정)
- 월별 배치 Job (매월 25일 청구서 생성, 말일 지급 배치)
- 정산 대시보드
부록
A. 용어 정의
| 용어 | 설명 |
|---|---|
| Settlement | 캠페인 단위 정산 레코드 (1:1) |
| AdvertiserInvoice | 광고주에게 발행하는 청구서 |
| PartnerPayout | 파트너에게 지급하는 정산 배치 |
| Agency | 정산 주체 (Adit, 미드나잇웨이브 등) |
| BoltaReverseIssuance | Bolta 역발행 추적 레코드 |
| 건별 정산 | 캠페인 1건 = 청구서/지급 1건 |
| 월별 정산 | 한 달치 캠페인 합산하여 청구/지급 |
| 원천징수 | 개인사업자 소득에 대한 선납 세금 (3.3%) |
| payoutAmount | 실지급액 (정산금 - 원천징수) |
B. 관련 파일 위치
adit-backend/
├── prisma/schema.prisma # DB 스키마
├── src/utils/settlement-state-machine.ts # 상태 전이 규칙
├── src/modules/
│ ├── admin/settlements/ # 어드민 정산 서비스
│ │ ├── settlement.service.ts # 핵심 비즈니스 로직
│ │ ├── settlement.routes.ts # API 라우트
│ │ └── settlement.types.ts # Zod 스키마
│ ├── partner/settlements/ # 파트너 정산 서비스
│ │ └── settlement.service.ts # 정산 요청, 역발행
│ ├── partner/campaigns/
│ │ └── campaign-settlement.service.ts # 정산 레코드 자동 생성
│ ├── admin/invoices/ # 광고주 청구서
│ ├── admin/payouts/ # 파트너 지급
│ ├── admin/agencies/ # 대행사 관리
│ ├── bolta/ # Bolta API 연동
│ └── notification/ # 알림 서비스
└── src/templates/email/ # 이메일 템플릿