파트너 포털 대시보드에 판매·가입 실시간 집계 추가
목차
partner-portal 대시보드에 오늘 판매·가입 실시간 집계 카드와 어제 절대값 표시를 추가했다. 변경 파일은 내부 클래스 1개, 뷰/스타일 1개. 숫자만 보면 작은 작업이지만, 실제로는 SQL 집계 설계부터 권한 인터셉터 검증까지 꽤 넓은 범위를 건드렸다.
왜 이 기능이 필요했나
파트너 포털은 내·외부 파트너가 자신의 판매 현황을 직접 조회하는 창구다. 기존 화면은 월·주 단위 집계만 있었고, "오늘 얼마나 팔렸는지"를 실시간으로 볼 방법이 없었다. 당연히 대시보드에 들어와도 가장 궁금한 정보를 못 보니까 다른 화면으로 이리저리 넘어다니거나, 아예 CS로 문의가 들어오는 상황이었다.
요구사항을 정리하면 두 가지였다.
- 오늘(당일 00:00 ~ 현재) 판매·가입 수를 실시간에 준하게 보여줄 것
- 어제 확정된 절대값을 옆에 나란히 표시해서 비교 기준을 줄 것
"실시간"이라는 단어가 들어오는 순간 설계 갈림길이 생긴다. WebSocket이나 SSE로 서버 푸시를 할 것인가, 아니면 폴링으로 갈 것인가. 이번에는 폴링을 선택했다. 이유는 단순한데, 파트너 포털 특성상 동시접속자가 많지 않고, 1-2분 주기 갱신이면 "실시간 체감"으로 충분하다고 판단했기 때문이다. WebSocket 인프라를 추가로 관리하는 비용 대비 효과가 없었다.
구현 흐름
백엔드는 기존 서비스 레이어에 집계 쿼리를 담은 내부 클래스를 하나 추가하고, 신규 API 엔드포인트를 열었다. 쿼리는 날짜 경계를 명확히 잡는 게 핵심이었다.
-- 오늘 판매 집계 예시 (날짜 경계를 서버 타임존 기준으로 명시)
SELECT COUNT(*) AS today_sales
FROM orders
WHERE created_at >= CURDATE()
AND created_at < CURDATE() + INTERVAL 1 DAY
AND status NOT IN ('CANCELLED', 'REFUNDED')
CURDATE() 기준으로 범위를 열어두면 인덱스가 타는지 실행 계획을 먼저 확인했다. created_at에 인덱스가 있으면 range scan으로 들어가는데, 없으면 풀스캔이 되니까 이 확인은 절대 건너뛰지 않는다. 결제·판매 테이블은 행이 쌓이는 속도가 빠른 편이라 쿼리 한 번이 무거워지면 폴링 특성상 db 부하가 배로 쌓인다.
어제 절대값은 당일 배치가 이미 확정해놓은 값을 읽는 방식이 가장 안전했다. 실시간 집계와 달리 어제 숫자는 변하지 않으니, 굳이 매번 쿼리를 태울 이유가 없다. 배치 결과 테이블에서 단건 조회하는 걸로 마무리했다.
프론트는 JSP + AJAX 구조다. 페이지 로드 시 즉시 한 번 호출하고, 이후 setInterval로 주기적으로 갱신한다.
function refreshDashboardStats() {
$.ajax({
url: '/api/partner/dashboard/stats',
method: 'GET',
success: function(data) {
$('#today-sales').text(data.todaySales.toLocaleString());
$('#today-signup').text(data.todaySignup.toLocaleString());
$('#yesterday-sales').text(data.yesterdaySales.toLocaleString());
},
error: function() {
// 네트워크 오류 시 기존 값 유지, 갱신 실패 표시 없음
}
});
}
$(document).ready(function() {
refreshDashboardStats();
setInterval(refreshDashboardStats, 90000); // 90초 주기
});
error 핸들러에서 값을 지우거나 0으로 덮어쓰지 않는 게 포인트다. 일시적인 네트워크 문제가 생겼을 때 기존에 로드된 값이 남아있는 편이 "숫자가 갑자기 사라지는" 것보다 낫다.
권한 처리는 기존 인터셉터가 파트너 세션을 검증하는 구조라 신규 엔드포인트에 같은 인터셉터를 태우는 걸로 충분했다. 다만 확인은 꼼꼼하게 했다. 다른 파트너의 데이터가 노출되는 건 숫자가 틀리는 것보다 훨씬 심각한 문제니까, 세션에서 파트너 ID를 꺼내서 쿼리 조건에 직접 박는 방식인지 재확인했다.
사이드바 메뉴 활성화는 기존 패턴대로 현재 URL 경로를 기준으로 active 클래스를 붙이는 방식으로 처리했다.
검증에서 신경 쓴 것들
구현 후 확인 항목을 간단히 정리하면 아래와 같다.
| 확인 항목 | 방법 |
|---|---|
| 오늘 집계 숫자 정합성 | 기존 어드민 화면의 같은 날짜 값과 대조 |
| 어제 절대값 | 배치 테이블 직접 조회 후 비교 |
| NULL / 데이터 없는 날 | 신규 파트너 계정으로 빈 상태 확인 |
| 권한 분리 | 다른 파트너 계정으로 접속해 타 파트너 데이터 노출 여부 |
| 폴링 중 세션 만료 | 세션 만료 후 AJAX 응답이 로그인 리다이렉트인지 확인 |
특히 NULL 처리는 새 파트너나 오늘 판매가 아직 없는 경우 집계 쿼리가 NULL을 반환할 수 있어서, 백엔드에서 0으로 치환하고 내려보내는 걸로 통일했다. 프론트에서 null 체크를 하면 레이어가 두 곳으로 나뉘어 혼란이 생기기 때문이다.
돌아보며
사내 서비스 기능 추가 작업이 단순한 이유는 없다. 버튼 하나, 숫자 두 개지만 그 아래에는 SQL 집계 설계, 쿼리 실행 계획 검토, 권한 모델 점검, 폴링 주기와 db 부하 사이 트레이드오프, NULL 방어, 화면 cross-check이 다 엮여 있다. 어느 하나 빠지면 "숫자가 가끔 안 맞는다"거나 "특정 파트너 화면이 이상하다"는 피드백이 돌아온다.
금융·결제 도메인에서 숫자 하나가 다른 화면과 다르게 나오면 신뢰 문제로 직결된다. "대충 맞는 것 같은데"로 넘긴 게 나중에 반드시 되돌아오더라. 그래서 이번에도 어드민 화면 수치와 교차 확인하는 걸 마지막 단계로 잡아뒀다.
작업 규모와 무관하게 커밋은 논리적 단위로 쪼개는 습관을 유지하고 있다. 오늘도 쿼리·엔드포인트·뷰·메뉴 활성화를 각각 분리했는데, 실제로 뷰에서 잠깐 오작동이 있었을 때 어느 커밋에서 들어온 건지 바로 찾을 수 있었다. 작은 습관이지만 디버깅 시간을 꽤 줄여준다.
댓글 0
첫 댓글 달아줘.