개발 slecs

CS 문의 엑셀 다운로드에 회원정보 폴백 매칭 적용

목차

CS 문의 데이터를 엑셀로 뽑아달라는 요청이 계속 쌓이고 있었다. 거기에 회원정보가 누락된 케이스가 간간이 섞여 있어서, 그냥 export 하나 만들자니 단순하지 않았던 작업.


배경 — 왜 이 시점에

운영팀에서 CS 티켓 데이터를 주기적으로 정리해야 하는데, 매번 화면 보면서 수기로 옮기고 있었다. 규모가 작을 땐 그냥 넘어가는데, 문의 건수가 쌓이면서 "엑셀 다운로드 하나만 만들어달라"는 요청이 반복됐다. 기능 자체는 단순해 보이지만, 막상 뚜껑을 열면 항상 데이터 품질 문제가 나온다.

이번에도 그랬다. CS 문의 테이블에 회원정보가 항상 깔끔하게 매핑되어 있지 않았다. 가입 경로가 다른 케이스, 탈퇴 후 남은 이력, 비회원 문의 등 여러 이유로 일부 레코드에서 회원 식별 정보가 빠져 있는 상황. 그냥 JOIN 하면 해당 행 자체가 날아가고, 운영팀 입장에서는 "왜 내가 처리한 티켓이 안 보이냐"는 클레임이 된다.


작업 내용

변경이 들어간 파일은 크게 네 군데다.

파일 역할 변경 성격
ap/support/web/내부 클래스 어드민 지원 도메인 컨트롤러 엑셀 다운로드 엔드포인트 추가
co/cs/web/내부 클래스 CS 공통 도메인 웹 레이어 회원정보 폴백 매칭 로직 처리
sqlmap/slecs/ap/쿼리 매퍼 MyBatis 쿼리 정의 엑셀용 SELECT + 폴백 JOIN 쿼리
tickets.jsp 어드민 티켓 목록 뷰 다운로드 버튼 UI 추가

핵심은 쿼리 매퍼 쪽이었다. 회원정보가 있는 경우와 없는 경우를 한 쿼리에서 모두 커버해야 했다. 처음엔 LEFT JOIN으로 퉁치려 했는데, 폴백 매칭 조건이 단순 FK 하나가 아니라 조건이 두 갈래로 나뉘어 있었다. 결국 COALESCE로 우선순위 컬럼을 조합하고, 회원 테이블 JOIN을 분기 처리하는 방식으로 정리했다.

-- 폴백 매칭 패턴 (단순화 예시)
SELECT
    t.ticket_id,
    t.content,
    COALESCE(m.member_name, t.non_member_name, '(정보없음)') AS display_name,
    COALESCE(m.email, t.contact_email)                        AS contact
FROM cs_ticket t
LEFT JOIN member m ON m.member_id = t.member_id

COALESCE 체이닝으로 "회원 테이블 값 → 문의 당시 입력값 → 기본값" 순서로 폴백을 쌓아둔 구조. 이렇게 하면 데이터 누락이 있더라도 엑셀 행 자체가 사라지지 않는다.

컨트롤러에서는 POI 기반 엑셀 생성을 붙였다. 이 프로젝트가 eGovFramework 기반이라 뷰 레이어 처리 방식이 좀 달라서, 별도 다운로드 엔드포인트를 ap/support 쪽에 두고 response에 직접 스트림을 쏘는 구조로 갔다.

// 엑셀 스트림 응답 패턴
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition",
    "attachment; filename=\"cs_tickets.xls\"");

HSSFWorkbook workbook = buildWorkbook(ticketList);
workbook.write(response.getOutputStream());
workbook.close();

파일명 인코딩 이슈는 항상 나오는 문제라 User-Agent 분기 없이 그냥 고정 ASCII 파일명으로 처리했다. 운영 환경이 IE 포함이면 URLEncoder, Chrome이면 RFC5987 방식이 맞는데, 이 시스템 환경에서는 고정 명칭으로도 충분하다고 판단했다.


회고 — 폴백 설계의 중요성

엑셀 다운로드는 기능 자체보다 데이터 완결성이 핵심이다. JOIN이 깨지는 레코드가 하나라도 있으면 운영팀이 "데이터가 이상하다"고 인지하기 전에 그냥 "건수가 맞지 않는다"는 얘기로 돌아온다. 화면에서는 페이지네이션으로 가려지던 게 엑셀 전체 다운로드에서 한 번에 드러나는 경우가 많음.

이 작업하면서 팀원들한테 강조한 것도 그 부분이었다. LEFT JOIN 썼다고 끝이 아니라, null이 내려오는 컬럼이 어디인지, 그게 export 결과물에서 어떻게 보이는지까지 확인하는 습관. 쿼리 리뷰할 때 "JOIN 조건이 맞냐"만 보지 말고 "null row가 있을 때 어떻게 되냐"를 같이 보자는 얘기를 했다.

JSP 쪽 다운로드 버튼 추가는 단순하지만, 권한 처리 위치가 컨트롤러 vs 뷰 어디냐 하는 문제도 잠깐 논의됐다. 버튼 visibility를 JSP에서 role로 분기할 수도 있고, 엔드포인트 자체를 인터셉터로 막을 수도 있다. 이번엔 컨트롤러 레벨 인터셉터 적용 + JSP 버튼도 role 분기 병행으로 갔다. 양쪽 다 막아두는 게 맞다고 봄. 뷰만 숨겨두면 URL 직접 호출에 무방비가 되고, 컨트롤러만 막으면 버튼이 노출돼서 UX가 이상해진다.

작은 기능처럼 보여도 운영 효율에 직접 영향을 주는 작업이라 꼼꼼하게 챙겼다. 끝.

댓글 0

첫 댓글 달아줘.