개발 slecs

파트너 대시보드에 결제 KPI 기간 필터와 실시간 집계 추가

목차

partner-portal 대시보드에 결제 KPI 기간 필터와 실시간 집계를 추가했다. 변경된 파일은 내부 클래스 하나, SQL 매퍼 하나, 뷰/스타일 파일 하나. 파일 수만 보면 작은 작업인데, 실제로는 쿼리 설계부터 화면 렌더링까지 스택 전 층위를 건드렸다.

왜 이 기능이 필요했나

기존 대시보드는 "오늘 현재까지"의 누적값만 보여줬다. 파트너 입장에서는 특정 프로모션 기간이나 월별 정산 구간을 직접 지정해서 수치를 확인하고 싶은 수요가 있었고, 그 요구를 API 레벨에서 막아두고 있었던 셈이다. startDate/endDate 파라미터를 받아서 집계 범위를 동적으로 바꾸는 구조로 전환했다.

단순 UI 추가가 아니라는 게 포인트였다. 날짜 필터가 생기는 순간 SQL 집계 쿼리가 통째로 바뀌고, 그 결과가 기존 화면의 누적 수치와 어떤 관계인지 정의해야 한다. "기간 합계"와 "전체 누적"이 동시에 화면에 뜨는 구조라면 두 숫자가 서로 어떤 기준으로 계산됐는지 명확히 구분되어야 한다. 그렇지 않으면 파트너가 두 숫자를 비교하다가 혼란스러워진다.

구현하면서 결정한 것들

SQL 집계 구조

일별/월별/누적 집계를 각각 분리해서 작성했다. 하나의 쿼리로 GROUP BY를 여러 단계로 쌓는 방법도 있지만, 가독성과 디버깅 편의를 위해 집계 단위별로 쿼리를 나눴다. 대략 이런 구조다.

-- 기간 내 일별 집계
SELECT
    DATE(payment_date)   AS day,
    SUM(amount)          AS daily_total,
    COUNT(*)             AS daily_count
FROM payments
WHERE partner_id = #{partnerId}
  AND payment_date BETWEEN #{startDate} AND #{endDate}
  AND status = 'SETTLED'
GROUP BY DATE(payment_date)
ORDER BY day;

-- 기간 누적 (KPI 카드용)
SELECT
    SUM(amount)  AS total_amount,
    COUNT(*)     AS total_count
FROM payments
WHERE partner_id = #{partnerId}
  AND payment_date BETWEEN #{startDate} AND #{endDate}
  AND status = 'SETTLED';

NULL 처리도 여기서 잡아뒀다. SUM이 빈 결과셋에서 NULL을 반환하면 프론트에서 null 그대로 렌더링돼 "0원" 대신 빈 칸이 뜨는 경우가 생긴다. COALESCE(SUM(amount), 0)으로 방어했고, 매퍼 클래스에서도 null 체크를 한 번 더 걸었다.

폴링 방식 선택

실시간 갱신은 5초 폴링으로 구현했다. WebSocket이나 SSE를 쓰는 게 더 근사해 보이지만, 이 화면의 갱신 빈도와 사용 패턴을 보면 폴링이 현실적이다.

방식 장점 단점 선택 이유
폴링 (5초) 구현 단순, 서버 무상태 유지 불필요한 요청 발생 갱신 빈도 낮고, 연결 유지 비용 대비 이득 없음
WebSocket 푸시 방식, 지연 최소화 연결 관리, 스케일아웃 복잡 대시보드 특성상 과설계
SSE 단방향 푸시, 비교적 단순 브라우저 연결 수 제한 현재 인프라 구성과 맞지 않음

결제 대시보드에서 "5초 이내 반영"이면 충분하고, 파트너가 이 화면을 몇 십 명이 동시에 열어두는 구조도 아니다. 기술적 우아함보다 유지보수 단순함을 택했다.

숫자 포맷 통일

천단위 콤마와 "원" 단위 표기를 뷰 레벨에서 일괄 처리했다. 포맷 함수를 뷰마다 따로 정의하면 나중에 포맷 정책이 바뀔 때 여러 파일을 손봐야 한다. 공통 유틸로 빼두고 같은 함수를 참조하게 했다. 기존 화면들도 동일 유틸을 쓰고 있어서, 이 카드의 숫자가 다른 화면과 포맷 차이 없이 일관되게 표시된다.

검증에서 가장 신경 쓴 부분

기능 구현보다 검증에서 시간을 더 썼다. 결제 도메인에서 숫자 하나 어긋나면 그게 버그인지 정책 차이인지 파악하는 데 드는 비용이 크다. 파트너가 "왜 저쪽 화면이랑 다르냐"고 문의하는 순간 그 티켓 하나 해소하는 데 공수가 만만찮다.

검증 체크리스트를 대략 이렇게 잡았다.

  • 동일 기간을 기존 정산 화면과 신규 KPI 카드에서 각각 조회해서 숫자 일치 여부 확인
  • startDate === endDate인 단일 날짜 케이스
  • 데이터 없는 기간 조회 (0건 응답, null 방어)
  • 권한 없는 파트너 ID로 조회 시 필터링 정상 작동 여부
  • 5초 폴링 중 날짜 필터 변경 시 이전 응답이 덮어씌워지지 않는지 (요청 순서 꼬임)

마지막 항목이 생각보다 놓치기 쉽다. 느린 네트워크에서 사용자가 날짜를 빠르게 바꾸면 이전 요청의 응답이 나중에 도착해서 화면에 뜨는 경우가 있다. 요청마다 타임스탬프나 시퀀스를 붙여서 마지막 요청의 응답만 반영하도록 처리했다.

작업하면서 다시 확인한 것

사내 서비스 기능 하나가 버튼 하나 추가로 끝나지 않는다는 걸 이번에도 체감했다. SQL 집계, null 방어, 폴링 타이밍, 화면 렌더링, 권한 필터, 숫자 포맷까지 어느 하나만 빠져도 어딘가에서 이상한 결과가 나온다. 특히 금융/결제 도메인은 "대충 맞는 것 같다"로 넘어가는 순간 반드시 다시 돌아온다.

커밋은 논리적으로 독립된 단위로 쪼갰다. SQL 매퍼 변경, 클래스 수정, 뷰 변경을 각각 분리해서 커밋했고, 메시지는 "무엇을 바꿨다"보다 "왜 이 방향으로 결정했다"를 담으려 했다. 작게 쪼개두면 나중에 특정 변경에서 문제가 생겼을 때 범위가 바로 보인다. 큰 커밋 하나에 다 묶어두면 git bisect도 무용지물이다.

댓글 0

첫 댓글 달아줘.