← 문서 목록

시스템 구성도 · 애플리케이션 아키텍처 · 주요 업무 흐름도

NestPay 선불 지갑 플랫폼 · 실제 코드/DB(nestpay, Flyway V1~V36 적용) 기준 재확인

문서 목적 — 본 문서는 NestPay 서버(paynest-v1)의 배포 구성, 애플리케이션 계층 구조, 그리고 돈이 오가는 주요 업무의 처리 순서와 원장 복식분개(DR/CR/FEE)를 실제 소스 코드와 운영 DB 스키마로 재확인하여 정리한 설계 산출물입니다.

근거: apps/api/docker-compose.yml, apps/api/nginx/*, apps/api/src/main/java/kr/nestpay/api/**, application.yml, build.gradle, Flyway 마이그레이션(V1~V36) 및 운영 DB(nestpay) 조회. 다이어그램은 외부 스크립트 없이 텍스트로 그렸습니다.

목차

1. 기술 스택 요약

구분채택 기술버전 / 근거
언어 · 런타임Java17 (build.gradle toolchain JavaLanguageVersion.of(17) — 발주사 확정)
프레임워크Spring Boot3.3.5 (web · validation · actuator)
SQL 매핑MyBatismybatis-spring-boot-starter 3.0.3 (SQL 을 XML 매퍼에 직접 작성)
스키마 관리Flywayflyway-core + flyway-mysql, 부팅 시 V1~V36 자동 적용(운영 DB 36건 성공 확인)
데이터베이스MariaDB10.3 (utf8mb4 · event-scheduler ON — V35 파티션 수명관리)
파일 스토리지MinIO(S3 호환)io.minio 8.5.12 — 운영은 외부 오브젝트 스토리지로 교체
API 문서Springdoc(Swagger)4개 그룹(app/store/admin/pg), 운영은 내부 스펙 SWAGGER_ENABLED=false
리버스 프록시nginx1.27-alpine — server_name(서브도메인) 기반 신뢰 경계 분리

2. 시스템 구성도

운영은 L4 로드밸런서 뒤에 동일 서버 2대를 두는 무상태(stateless) 구조입니다. 두 대가 하나의 공유 DB하나의 공유 오브젝트 스토리지를 바라보므로, 요청이 어느 서버로 가도 결과가 같습니다. 각 서버 내부에서는 nginx 하나 + Spring Boot 백엔드 하나가 5개 호스트(서브도메인)를 server_name 으로 동시에 서비스합니다(서버를 서브도메인별로 나누지 않음).

                           [ 인터넷 / 외부 매장 서버 / 은행·PG 게이트웨이 ]
                                              │
                                     ┌────────┴────────┐
                                     │  L4 로드밸런서   │   /health 로 각 노드 생사 확인
                                     └────────┬────────┘
                        ┌─────────────────────┴─────────────────────┐
                        │                                            │
              ┌─────────────────────┐                    ┌─────────────────────┐
              │   서버 #1 (무상태)   │   ······ 동일 ······│   서버 #2 (무상태)   │
              │  ┌───────────────┐  │                    │  ┌───────────────┐  │
              │  │ nginx 1.27    │  │                    │  │ nginx 1.27    │  │
              │  │ (server_name  │  │                    │  │ (server_name  │  │
              │  │  가상호스트)  │  │                    │  │  가상호스트)  │  │
              │  └──────┬────────┘  │                    │  └──────┬────────┘  │
              │  ┌──────┴────────┐  │                    │  ┌──────┴────────┐  │
              │  │ Spring Boot   │  │                    │  │ Spring Boot   │  │
              │  │ API :8080     │  │                    │  │ API :8080     │  │
              │  │ (@Scheduled   │  │                    │  │ (@Scheduled   │  │
              │  │  워커 포함)   │  │                    │  │  워커 포함)   │  │
              │  └──────┬────────┘  │                    │  └──────┬────────┘  │
              └─────────┼───────────┘                    └─────────┼───────────┘
                        │            공유 인프라(사설망, 인터넷 미노출)         │
                        └──────────────┬───────────────────┬───────────────────┘
                                       │                   │
                            ┌──────────┴─────────┐  ┌──────┴──────────────────┐
                            │  MariaDB 10.3      │  │  오브젝트 스토리지       │
                            │  (nestpay, 공유)   │  │  storages.nestpay.co.kr │
                            │  · 원장·지갑·거래  │  │  (MinIO/S3 · 파일·이미지)│
                            └────────────────────┘  └─────────────────────────┘

  ※ 워커 동시성: 서버 2대가 같은 @Scheduled 를 돌려도, 각 작업은 claim_token(원자적 UPDATE) /
     FOR UPDATE / 상태 가드 쿼리로 "한 건은 한 번만" 처리되도록 DB 에서 보호합니다(중복 처리 방지).

2.1 호스트(서브도메인) 5종 — nginx server_name 분기

하나의 nginx 가 서브도메인별로 "허용 경로만" 열어 신뢰 경계를 나눕니다. 나머지 경로는 모두 404 로 막아, 관리자 API·내부 Swagger 스펙이 공개(api/pg) 호스트로 새지 않게 합니다.

호스트운영 도메인개발 포트허용 경로 (conf.d)보호 방식
wwwwww.nestpay.co.kr6443회사·서비스 소개 정적 사이트($uri.html 미러)공개(인증 없음)
apiapi.nestpay.co.kr8443/app · /store · /health + 내부 Swagger(app/store/admin, 사내 IP)토큰 + 사내 IP(문서)
adminadmin.nestpay.co.kr9443관리자 정적 SPA + /api/* → 백엔드 /admin화이트 IP + 관리자 토큰(내부 전용)
pgpg.nestpay.co.kr7443/pg/** + 외부 공개 Swagger(pg 그룹만)HMAC 서명 + 매장 화이트 IP
storagesstorages.nestpay.co.kr9000/9001(로컬 MinIO)파일·이미지 실물(S3 API). 앱 서버가 STORAGE_ENDPOINT로 접근사설망 · 액세스키(인터넷 미노출)

· 운영(443): 하나의 서버가 위 4개 서브도메인(www/api/admin/pg)을 server_name 으로 동시 서비스, 알 수 없는 도메인(직접 IP 접근 등)은 default_server 로 404. · 개발(로컬): DNS 가 없어 포트(6443/8443/9443/7443)로 같은 입구에 접속. · MariaDB(3307)·MinIO 는 인터넷에 노출하지 않고 앱 서버에서 사설망으로만 접속.

2.2 컨테이너 구성(로컬 docker compose)

서비스이미지역할기동 조건
dbmariadb:10.3스키마·기본데이터(Flyway) 저장healthcheck(mysqladmin ping)
storageminio (2024-10)파일·이미지 실물healthcheck(mc ready)
api멀티스테이지 빌드Spring Boot API + 워커db · storage service_healthy 후 기동
webnginx:1.27-alpine443/6443/7443/8443/9443 입구api 기동 후

3. 애플리케이션 아키텍처

표준 계층형 구조입니다. Controller(입출력·검증) → Service(업무 판단·트랜잭션 경계) → Mapper(MyBatis XML SQL) → MariaDB. 여러 흐름이 공유하는 계산·조회는 util 패키지의 순수 함수로 뽑아 중복을 제거했습니다(수수료·로트·지급시각 등).

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  Controller  (kr.nestpay.api.controller)                                    │
  │   App*(회원앱)  Store*(매장앱)  Admin*(관리자)  Pg*(외부PG)  BankWebhook    │
  │   · 요청 DTO 검증(jakarta.validation) · ApiResponse 표준 응답            │
  └───────────────────────────────┬────────────────────────────────────────────┘
                                   │  (권한·토큰은 아래 보안 필터에서 이미 판별)
  ┌───────────────────────────────┴────────────────────────────────────────────┐
  │  Service  (@Transactional 경계 = 한 업무 = 한 트랜잭션)                      │
  │   AppPayment · AppWithdraw · AppGift · AppDeposit · StoreSettle · PgPayment  │
  │   ┌──────────────── 공용 유틸(util) — 중복 제거 ────────────────┐           │
  │   │ Fees(수수료)  Lots(로트 소비)  LedgerLookups(시스템지갑·만료) │           │
  │   │ PayoutSchedules(지급시각)  PushPayloads(알림)  Masks(마스킹)  │           │
  │   │ Hashes(SHA-256)  Pagination  BankVerify  BankMaintenance      │           │
  │   └──────────────────────────────────────────────────────────────┘           │
  └───────────────────────────────┬────────────────────────────────────────────┘
  ┌───────────────────────────────┴────────────────────────────────────────────┐
  │  Mapper  (MyBatis · resources/mapper/*.xml)  — 원장/지갑/거래 단일 원장 SQL  │
  └───────────────────────────────┬────────────────────────────────────────────┘
  ┌───────────────────────────────┴────────────────────────────────────────────┐
  │  MariaDB 10.3  —  wallets · lots · transactions · ledger_entries · …         │
  └──────────────────────────────────────────────────────────────────────────────┘

  [외부 연동은 인터페이스로 격리 — 계약 전 스텁 / 계약 후 실연동 교체]
    BankVerifier   ← StubBankVerifier   (실명조회·1원인증)
    IdentityVerifier ← StubIdentityVerifier (본인인증 PASS 등)

3.1 공용 유틸(중복 제거) — 실제 함수

유틸역할계산/규약(코드 원문)
Fees.calc수수료 계산(전 흐름 공통)floor(금액 × rate% ÷ 100) + fixed_amount. 정책 없으면 0. (과거 충전만 floorDiv 로 달랐던 버그를 통일)
Lots.consume유상 포인트 로트 소비(배분·차감)DR 분개 1줄에 대해 오래된 로트부터 insertLotAllocation(배분 기록=취소 복원 근거) + decreaseLotRemaining
LedgerLookups시스템지갑 조회 · 로트 만료개월systemWallet(code), expiryMonths()
PayoutSchedules.from출금·정산 지급 예정 시각delay_days ≤ 0 → 즉시(now), 아니면 오늘+delay_days 일의 execute_time 시각
PushPayloads.of알림 payload(JSON) 생성kind·principalType·principalId·ntype·title·body·deeplink
Masks / Hashes이름 마스킹 · SHA-256 지문전화·카드번호는 sha256Hex 지문으로 조회(원문 미저장)

4. 보안 경계 — nginx → IP → HMAC → 토큰 (3중 필터)

가장 바깥은 nginx(호스트·경로 화이트리스트)이고, 그 안에서 Spring 서블릿 필터가 순서대로 검사합니다. 값싼 검사(IP)를 먼저, 비용이 큰 검사(서명·토큰)를 뒤에 둡니다. 필터 등록·URL 패턴은 SecurityFilterConfig + application.yml 에서 확인.

  요청 ─► [ nginx ]  host(server_name)+경로 화이트리스트 (그 외 404)
            │
            ├─ /admin/*  ─► ① IpWhitelistFilter (order 1)  등록 IP 만 통과(없으면 bootstrap-allow)
            │               ─► ③ AdminAuthFilter (order 3)  관리자 토큰 + 관리자별 개인 허용 IP
            │
            ├─ /pg/*     ─► ② HmacAuthFilter (order 2)  X-Client-Id/Api-Key/Timestamp/Signature
            │                   · 시각 오차 300초 · client_id APPROVED · 키 해시대조(교체유예)
            │                   · HMAC-SHA256(method\npath\nts\nbody) 시간차 없는 비교
            │                   · 서명 통과 후 그 매장의 승인 화이트IP 인지 추가 확인(V7)
            │
            ├─ /store/*  ─► ④ StoreAuthFilter (order 4)  매장 출입증(토큰)
            │
            ├─ /app/me/* ─► ⑤ UserAuthFilter (order 5)  회원 출입증(토큰)
            │               (/app 의 가입·로그인·공개콘텐츠는 무인증)
            │
            └─ /webhooks/bank/*  ─► X-Internal-Key(=INTERNAL_API_KEY) 상수시간 비교 (은행/PG 전용)

  · 진짜 사용자 IP 판별: ClientIpResolver 가 신뢰 프록시(nginx)에서 온 X-Forwarded-For 만 신뢰.
  · 내부 Swagger 스펙(app/store/admin): docsIpWhitelistFilter(order 1)로 사내 IP 에서만 열람.
필터order대상 경로검사 내용
IpWhitelistFilter1/admin/*DB 등록 IP(없으면 bootstrap-allow)
docsIpWhitelistFilter1/v3/api-docs/{app,store,admin}내부 문서 스펙 사내 IP 제한
HmacAuthFilter2/pg/*HMAC 서명 + 매장 화이트 IP
AdminAuthFilter3/admin/*관리자 토큰 + 관리자별 IP
StoreAuthFilter4/store/*매장 토큰
UserAuthFilter5/app/me/*회원 토큰

5. 부팅 안전장치 · 백그라운드 워커

5.1 부팅 안전장치(fail-fast) — 개발 기본값으로 운영 오픈 차단

가드실행 시점차단 조건 (app.env ≠ dev 일 때)
SecretsGuard
(EnvironmentPostProcessor)
DB 연결·Flyway 이전(최초기)Swagger 미차단 또는 개발 기본값 잔존: APP_CRYPTO_KEY · ADMIN_TOKEN_SECRET · INTERNAL_API_KEY · DB_PASSWORD · STORAGE_SECRET_KEY
StubGuard
(ApplicationRunner)
컨텍스트 기동 직후외부 연동이 스텁 그대로: StubBankVerifier(실명조회·1원인증) · StubIdentityVerifier(본인인증)

· 위 두 클래스가 실제 소스에 존재하는 부팅 가드입니다(config/SecretsGuard, config/StubGuard). 조건 미충족 시 IllegalStateException 으로 서버 기동을 중단합니다. 외부 연동(펌뱅킹·본인인증·은행 실명조회)은 계약 전 스텁 상태이며, 실연동 구현으로 교체해야만 운영 기동이 됩니다.

5.2 백그라운드 워커(@Scheduled) — 2대 동시 실행 안전

워커주기역할동시성 보호
DepositNoticeWorker10초입금 통지 처리(적립/미매칭)FOR UPDATE 클레임 + 상태 가드(guardNoticeMatched/Unmatched)
WebhookSender20초PG 매장서버로 결제완료 웹훅 발송webhook_claim_token(V34)
OutboxWorker30초알림함 적재 · 푸시 캠페인 · 고아 회수claim_token 원자적 UPDATE(V11) — MariaDB 10.3 엔 SKIP LOCKED 없음
PgExpirySweeper30초만료된 PG 주문·QR 정리상태 전이 가드
WithdrawWorker60초예정 출금·정산 이체 실행claimWithdrawal(HOLD→PENDING) 원자 전이
GiftExpireWorker5분미수령 링크 선물 만료 반환expireLink 가드(0건이면 타 서버 처리)
RateLimitService1시간레이트리밋 낡은 카운터 청소

6. 원장(복식부기) 공통 규약

모든 자금 이동은 transactions 1건 + ledger_entries 여러 줄로 기록되는 단일 원장(single ledger) 구조입니다. 각 줄은 direction(DR 차변 / CR 대변) · amount · balance_after 를 가지며, 한 거래의 DR 합 = CR 합(균형)입니다. 수수료는 항상 별도 CR 줄로 시스템 수수료 지갑에 적립합니다.

시스템 지갑(wallets.system_code)용도
SETTLEMENT_CLEARING대외 청산(충전 유입·출금/정산 유출의 상대 계정)
FEE_REVENUE플랫폼 수수료 수익
GIFT_ESCROW링크 선물 보류(수령 전 임시 보관)
UNMATCHED주인 못 찾은 입금 보관(관리자 수동 매칭/반환)
EXPIRED / FORFEITED만료·소멸 포인트 귀속(운영 DB 확인)

· 운영 DB 조회 결과 시스템 지갑 6종 확인: SETTLEMENT_CLEARING · UNMATCHED · FEE_REVENUE · GIFT_ESCROW · EXPIRED · FORFEITED. · 회원 지갑에서 돈이 나갈 때는 Lots.consume 로 유상 로트를 소비하고 배분을 남겨 취소·실패 시 정확히 복원합니다.

7. 업무 흐름 ① 회원 QR 결제 AppPaymentService

QR 은 서버가 QR 번호를 AES-GCM 으로 잠근 문자열(NPQR1.+암호문)입니다. 위조 불가·매장정보 은닉. QR 종류 3가지를 하나의 결제 로직으로 처리합니다.

QR 종류금액유효시간결제 subtype
STORE_STATIC(고정 스티커)회원이 입력무제한QR_STORE
ORDER(1회용, 금액 지정)QR 금액 고정기본 5분QR_ORDER
ORDER(1회용, 금액 미설정)회원이 입력기본 5분QR_ORDER
처리 순서(전부 한 트랜잭션)
 회원앱                          API (AppPaymentService.pay)                    DB
   │  스캔 QR + 금액 + pinPass  ─────►
   │                            0) requirePinPass  (PIN 인증 직후 발급된 1회용 표 검증)
   │                            1) loadQr  복호화·존재·활성·만료·매장정상 검증(5.1)
   │                            2) ORDER형이면 burnOrderQr  ← QR 선(先)소각(동시 결제 차단)
   │                            3) 회원지갑·매장지갑 번호순 잠금(데드락 방지)  ─► FOR UPDATE
   │                            4) 수수료 스냅샷(Fees.calc) + 부가세 분리(TAXED)
   │                               취소가능기한(cancelable_until) 계산(3.5)
   │                            5) 로트 잠금(무상 우선) — 가용 합 < 금액이면 결제 불가
   │                            6) transactions INSERT (멱등키 = PAY:{pinSig})
   │                            7) ledger_entries 분개(아래 표)
   │                            8) Lots.consume  회원 DR 줄에 로트 배분·차감(0.4)
   │                            9) 지갑 잔액 갱신 + 양쪽 알림(outbox) + PG주문 연결
   │  ◄─ 결제완료(txnId·잔액)
분개 줄지갑DR/CR금액
회원 차감회원 지갑DR결제금액(payAmount)
매장 적립매장 지갑CR결제금액 − 수수료 (순액)
수수료 수익(수수료>0일 때만)FEE_REVENUECR수수료(feeAmount)

· 멱등: 멱등키 PAY:{pinSig}ux_txn_idem 이 막아 같은 인증표로 두 번 결제 불가. · 수수료는 매장 부담(MERCHANT_PAYMENT 정책 스냅샷). · PG 오픈 API 주문이면 pgOrderService.onQrPaid 로 주문 PAID 연결 + 매장서버 웹훅 예약(돈 이동은 위 단일 원장에만 기록).

8. 업무 흐름 ② 출금 AppWithdrawService + WithdrawWorker

출금은 신청 즉시 지갑에서 선차감(HOLD) 하고, 예정 시각이 되면 워커가 실제 이체를 실행합니다. 출금은 유상(DEPOSIT) 로트만 가능(무상 적립금 출금 불가 — 0.5). 은행 점검시간에는 신청을 막습니다(BankMaintenanceGuard).

8.1 신청 = 선차감 HOLD (한 트랜잭션)
 회원앱  ─ 계좌·금액·pinPass ─►  request()
   · bankGuard.ensureOpen()  (은행 점검시간 차단)
   · requirePinPass · 계좌 존재 · 계좌변경 24h 냉각(cooldown) 확인
   · 지갑 잠금 · 유상로트 가용합 ≥ 금액 확인
   · 수수료(USER_WITHDRAW) → 순액 = 금액 − 수수료 (순액 ≤ 0 이면 거부)
   · 예정시각 = PayoutSchedules.from(정책)
   · transactions INSERT (status HOLD, 멱등키 WDR:{pinSig})
   · 분개 + Lots.consume(유상만) + 지갑 갱신 + 알림
단계분개 줄지갑DR/CR금액
신청(HOLD)회원 차감회원 지갑DR출금액(amount)
청산 대기SETTLEMENT_CLEARINGCR순액(netAmount)
수수료(>0)FEE_REVENUECR수수료
8.2~8.3 실행(WithdrawWorker, 60초) · 8.5 실패 복원
 WithdrawWorker.tick (60s)
   │ scanDue()  예정시각 지난 HOLD 목록(최대 100, 짧은 TX)
   └─ 건별 executeOne() [REQUIRES_NEW]
        · claimWithdrawal  HOLD→PENDING (0건이면 타 서버가 이미 집음 → skip)
        · ★펌뱅킹 이체 = 계약 전 스텁: 항상 성공, bankTranRef = "STUB-{txnId}"
        · confirmWithdrawal → CONFIRMED + "출금 완료" 알림
        (실연동 시: 결과불명은 UNKNOWN 으로 두고 리컨실러(8.4)가 정리)

 실패 시 restoreFailed() [REQUIRES_NEW] — 역분개 + 로트 승계 복원
   · failWithdrawal (상태 전이 가드)
   · 역분개(복원 입금 거래, 멱등키 WDRBACK:{txnId}):
분개 줄(실패 복원)지갑DR/CR금액
청산 회수SETTLEMENT_CLEARINGDR순액
수수료 회수(>0)FEE_REVENUEDR수수료
회원 환급회원 지갑CR출금액(총액)

· 실패 복원 시 restoreLots원래 쓰던 로트의 유형·만료일을 그대로 승계(유효기간 손실 방지 — 8.5). · 멱등키 WDR:{pinSig} 로 이중 신청 차단.

9. 업무 흐름 ③ 선물 AppGiftService

보낼 수 있는 것은 유상 포인트뿐(무상 적립금 선물 불가 — 0.5). 발신인이 수수료 부담(USER_GIFT). 모든 실행은 pinPass 필수. 받는 사람에게는 받은 날부터 새 유효기간의 유상 로트가 생깁니다(insertReceiveLot).

9.1 직접 선물(7.2) — 전화번호/카드번호로 즉시 전달

 sendDirect()  · 수신자 조회(전화 or 카드번호 지문) · 양쪽 지갑 번호순 잠금
   · 수수료 → 순액(≤0 거부) · 유상로트 가용 확인 · transactions(GIFT_DIRECT, CONFIRMED)
분개 줄지갑DR/CR금액
발신 차감발신 지갑DR선물액(총액)
수신 적립수신 지갑CR순액 → 수신자 새 로트 생성
수수료(>0)FEE_REVENUECR수수료

9.2 링크 선물 — 생성(7.3) → 수령(7.4) → 회수/만료반환(7.5·7.6)

 createLink()  돈을 GIFT_ESCROW 로 보류, 링크 토큰은 지금 1번만 노출(DB 엔 SHA-256 지문만)
   ┌ 발신 DR 총액 / GIFT_ESCROW CR 순액 / FEE CR (+발신 로트 소비)   [status HOLD]
 claim(token)  수령자 대표 카드로 전달 (본인이 만든 링크는 수령 불가 → 회수 이용)
   └ GIFT_ESCROW DR 금액 / 수령 CR 금액 (+수령자 새 로트)
 cancelLink(7.6, 회수) / expireOne(7.5, 24h 미수령 자동, GiftExpireWorker 5분)
   └ GIFT_ESCROW DR 금액 / 발신 CR 금액 (+발신자 새 로트 = 새 유효기간)
이벤트DRCR로트
링크 생성발신 지갑(총액)GIFT_ESCROW 순액 · FEE_REVENUE 수수료발신 유상 로트 소비
링크 수령GIFT_ESCROW수령 지갑수령자 새 로트(새 유효기간)
회수/만료반환GIFT_ESCROW발신 지갑발신자 새 로트(새 유효기간)

· 멱등: 생성 시 멱등키 GIFT:{pinSig}. · 수령 가드 claimLink·회수/만료 가드로 이미 처리된 링크 중복 방지.

10. 업무 흐름 ④ 정산 StoreSettleService · AdminSettlementService

10.1 매장 정산 출금(8.1 매장판) — 매장 지갑은 로트 없음

매장이 쌓인 매장 지갑 잔액을 자기 계좌로 빼는 기능. 매장 지갑은 로트가 없어 회원 출금보다 단순합니다(잔액에서 바로 차감). 기본 정책은 즉시 정산(delay 0). 실행·복원은 회원 출금과 같은 WithdrawWorker 가 처리(같은 규약).

 매장앱 ─ 금액 + idempotencyKey ─► request()
   · 정산계좌·잔액 확인 · 수수료(MERCHANT_PAYOUT) → 순액(≤0 거부)
   · 예정시각(PayoutSchedules) · transactions(멱등키 SETTLE:{앱제공키})
   · 분개(아래) + 지갑 갱신 + 알림   → 이후 WithdrawWorker 가 이체 실행
분개 줄지갑DR/CR금액
매장 차감매장 지갑DR정산액(총액)
청산 대기SETTLEMENT_CLEARINGCR순액
수수료(>0)FEE_REVENUECR수수료

주의 앱이 idempotencyKey 를 주면 ux_txn_idem 이 재시도 중복을 막지만, 구버전 앱이 안 주면 새 값이 생성되어 중복 차단이 되지 않습니다(코드 주석 명시).

10.2 관리자 월 마감(재계산) — 멱등

 AdminSettlementService.closeMonth(yyyymm)
   · 월 형식·미래월 검증 → 그 달 [시작, 다음달 시작) 범위로 월 정산서 통째 재계산(17.14)
   · 같은 달을 몇 번 눌러도 값이 다시 계산되어 안전(멱등 — 18장 규약) · 감사로그 기록

· 월 마감은 집계·리포트 성격이라 원장 분개를 만들지 않습니다(자금 이동 없음). 자금 이동은 10.1 정산 출금에서만 발생.

11. 업무 흐름 ⑤ 입금 충전 AppDepositService · DepositNoticeWorker

회원이 앱에서 받은 입금자 코드(NP+8자리)를 은행 이체 입금자명에 적으면, 은행/PG 통지가 웹훅으로 들어오고, 워커가 코드로 주인을 찾아 적립하거나 미매칭 보관합니다. 가상계좌 발급은 펌뱅킹/PG 계약 후 — 현재는 입금자 코드 방식(스텁).

4.1 통지 수신(웹훅, 멱등) → 4.2b 워커 클레임(10초) → 4.3 적립 / 4.4 미매칭
 은행/PG ─ X-Internal-Key ─► BankWebhookController /webhooks/bank/deposit-notice
   · 내부키 상수시간 비교 · receiveNotice():
       dedupKey = SHA256(source:bankTranRef)   ← 펌뱅킹(거래식별자 있음)
                = SHA256(source:rawText)        ← SMS(원문 지문)
     INSERT 시 DuplicateKeyException(1062) → 이미 받은 통지면 조용히 멱등 처리(INSERT IGNORE 금지)

 DepositNoticeWorker.tick (10s)  claimNewNotices(FOR UPDATE, 최대 100)
   └─ 건별 processNotice() [REQUIRES_NEW]
        1) 주인 찾기: 입금자코드 → 회원 → 충전 대상 카드(대표 우선)
        2) 충전 강제 규약: 회원이 "인증계좌(실명+1원)" 보유해야만 자동 적립
        3) 재발행 카드면 새 카드로 갈아탐(REISSUED → reissued_to)
분기DRCR후속
4.3 확정(주인·인증계좌 있음)SETTLEMENT_CLEARING 총액회원 지갑 순액 · FEE_REVENUE 수수료(>0)유상 로트 생성(DEPOSIT 만료개월) · 지갑 갱신 · 충전완료 알림
4.4 미매칭(주인 없음/인증계좌 없음)SETTLEMENT_CLEARING 금액UNMATCHED 금액미매칭 큐 적재 → 관리자 수동 매칭/반환

· 멱등키 DEP:{source}:{dedupKey} 로 같은 통지의 이중 적립을 차단(DuplicateKeyException → TX 롤백). · 워커는 통지별 독립 TX 라 한 건 실패가 다른 건에 영향 없음(실패 통지는 NEW 로 남아 다음 바퀴 재시도).

NestPay 산출물 · (주)페이네스트 · 작성일 2026-07-27 · 실제 코드/DB 기준 · 외부연동(펌뱅킹·본인인증·은행 실명조회)은 계약 전 스텁 상태임을 명시