관리자용 회원 활동내역 타임라인 조회 기능 추가
목차
관리자 도구에서 자주 받는 요청 중 하나가 "이 회원이 언제 뭘 했는지 한눈에 볼 수 있나요?"다. CS 대응이나 이상 거래 확인을 위해 결제 테이블, 쿠폰 테이블, 로그인 로그를 각각 뒤져야 했던 구조가 오래 유지되고 있었는데, 이번에 그걸 하나로 모은 타임라인 조회 기능을 붙였다.
기록 대상은 아래 네 가지로 정리했다.
| 활동 타입 | 설명 |
|---|---|
| 로그인/로그아웃 | 접속 이력 |
| 결제/취소 | 거래 이력 |
| 쿠폰 사용 | 쿠폰 상태 변경 |
| 잔액 변동 | 충전/차감 이력 |
이것 외에도 기록할 수 있는 후보가 몇 가지 더 있었는데, 일단 운영에서 실제로 조회 요구가 있는 항목만 먼저 넣었다. 지금 안 쓰는 컬럼을 미리 만들어두면 나중에 그게 데이터가 없는 채로 남아서 오히려 혼란을 준다. 쓸 게 생기면 그때 추가하는 게 맞다.
설계 방향: 중앙 집중 로그 테이블
각 도메인 처리가 완료되는 시점에 공통 로그 기록 메서드를 호출하는 방식으로 구현했다. 결제 서비스에서 결제 완료 처리 후 ActivityLogService.record(memberId, type, detail)를 호출하는 식이다. 별도 이벤트 시스템이나 AOP 인터셉터 없이, 처리 흐름 안에서 명시적으로 호출하게 했다.
Spring MVC + MyBatis 구조라 로그 기록 쿼리도 SQL XML에 넣었다. 테이블 하나에 타입 컬럼으로 구분하는 단순한 구조인데, 타임라인이라는 UI 요건 자체가 "시간 순으로 쭉 보여준다"이기 때문에 이 정도면 충분했다. 복잡한 집계가 필요한 게 아니라 단순 SELECT + ORDER BY 조합이라 MyBatis 쿼리도 깔끔하게 유지됐다.
<!-- activity_log_mapper.xml -->
<select id="selectActivityLog" parameterType="map" resultType="ActivityLogDto">
SELECT
activity_type,
detail,
created_at
FROM activity_log
WHERE member_id = #{memberId}
ORDER BY created_at DESC
LIMIT #{limit} OFFSET #{offset}
</select>
페이징은 간단하게 LIMIT/OFFSET으로 처리했다. 타임라인 형태라 무한 스크롤 방식도 고려했는데, 관리자 도구 특성상 특정 날짜 전후를 짚어서 보는 경우가 많아서 페이지 번호 방식이 낫다고 판단했다.
상태값 매핑과 레이블 표시
로그를 저장할 때 타입은 코드값으로 넣는다. LOGIN, LOGOUT, PAYMENT, CANCEL, COUPON_USE, BALANCE_CHARGE 같은 enum 문자열이다. 이걸 화면에 "로그인", "결제 완료", "쿠폰 사용" 처럼 한국어 레이블로 바꿔서 보여줘야 하는데, 이 변환 로직이 생각보다 여러 군데 흩어지기 쉽다.
이번에 공통 브랜드 매핑 로직을 도입해서 한 곳에서만 관리하게 했다. 간단히 말하면 Map<String, String> 하나를 상수로 들고 있는 유틸 클래스인데, 이후 타입이 추가되거나 레이블이 바뀔 때 파일 하나만 고치면 된다.
public class ActivityTypeLabel {
private static final Map<String, String> LABEL_MAP = Map.of(
"LOGIN", "로그인",
"LOGOUT", "로그아웃",
"PAYMENT", "결제",
"CANCEL", "결제 취소",
"COUPON_USE", "쿠폰 사용",
"BALANCE_CHARGE", "잔액 충전",
"BALANCE_DEDUCT", "잔액 차감"
);
public static String get(String code) {
return LABEL_MAP.getOrDefault(code, code); // 미등록 코드는 원문 그대로
}
}
미등록 코드가 들어왔을 때 예외를 던지지 않고 코드 원문을 그대로 반환하게 한 게 포인트다. 운영 중에 새 타입이 추가됐는데 매핑이 누락된 경우, 화면에 COUPON_EXPIRE 같은 게 뜨더라도 기능이 멈추진 않는다. 어색하지만 동작은 한다. 이후 알아차리고 추가하면 그만이다.
개발하면서 챙긴 것들
트랜잭션 범위가 제일 신경 쓰였다. 결제 처리와 로그 기록이 같은 트랜잭션에 묶이는 게 맞는지 아니면 분리해야 하는지 판단이 필요했다. 결제는 성공했는데 로그 INSERT에서 실패하면 결제까지 롤백되는 건 과도하다 싶어서, 로그 기록은 트랜잭션 바깥에서 별도로 처리하는 방향을 택했다. 로그 누락이 결제 실패보다 훨씬 낮은 심각도라는 판단이었다.
예외 처리도 비슷한 맥락이다. 로그 기록 실패는 RuntimeException을 상위로 전파하지 않고 catch해서 ERROR 로그만 남기게 했다. 본 기능이 실패해서 사용자 응답이 깨지는 걸 막는 게 우선이다.
뷰 레이어는 JSP라 타임라인 렌더링을 JSTL <c:forEach>로 처리했다. 날짜 포맷은 커스텀 포맷터로 통일했는데, DB에서 DATETIME으로 오는 걸 MM-dd HH:mm 형태로 줄여서 보여줬다. 관리자 입장에서 연도는 대부분 올해니까 굳이 다 보여줄 필요가 없었다.
작업 규모 자체는 크지 않았다. 코드 변경량만 보면 작은 커밋이다. 근데 이런 조회 기능 하나가 CS 처리 흐름을 꽤 바꾼다. 여러 탭 열어가며 조각 정보를 맞추던 게 화면 하나에서 해결되니까. 눈에 띄는 기능 개선은 아니지만 운영 쪽에선 의미 있는 변화다.
댓글 0
첫 댓글 달아줘.