개발 slecs

공급사 포탈에 판매대금·판매현황 메뉴와 접근제어 추가

목차

supplier-portal 영역에 꽤 손이 많이 가는 작업을 했다. 표면적으론 메뉴 두 개 추가지만, 실제로는 공급사 메뉴 타입 분리부터 SQL 설계, 백엔드 API, 프론트 연동, URL 접근제어까지 세로로 전 레이어를 건드렸다. 변경 파일도 내부 클래스 3개, SQL 파일 1개, SQL 매퍼 1개, 뷰/스타일 1개로 얇게 퍼졌다.

왜 이번에 손을 댔나

기존 포탈은 공급사 입장에서 "내가 얼마나 팔았는지", "정산이 얼마나 됐는지"를 한눈에 볼 수 있는 화면이 없었다. 데이터가 없는 게 아니라, 화면과 API가 없었던 것. 어드민 쪽엔 이미 집계 쿼리가 있었지만 그걸 공급사 포탈에 그대로 붙이면 권한 분리가 무너진다. 공급사는 자기 데이터만 봐야 하고, 다른 공급사 데이터가 절대 섞이면 안 된다.

그래서 단순 UI 복붙이 아니라 쿼리 레벨부터 다시 설계했다. 집계 단위, 필터 조건, 결과 컬럼 모두 공급사 컨텍스트에 맞게 재정의하고, 매퍼도 별도로 뺐다.

또 하나 트리거가 된 건 메뉴 타입 분리였다. 포탈 사이드바가 공급사 유형에 따라 다르게 보여야 하는 요구사항이 생기면서, 기존에 단일 타입으로 관리하던 메뉴 구조를 분리할 수밖에 없었다. 이게 선행 작업이 됐고, 그 위에 판매대금·판매현황 메뉴를 얹는 구조로 진행했다.

구현하면서 신경 쓴 부분

공급사별 데이터 격리가 핵심이었다. 쿼리 조건에 공급사 ID를 강제로 바인딩하고, 매퍼에서도 supplier_id 필터가 빠지면 컴파일 단계에서 잡히도록 설계했다. 대충 짜면 로그인한 공급사가 다른 공급사 매출을 조회할 수 있는 구멍이 생긴다.

SQL 쪽은 집계 컬럼 정의에 시간을 많이 썼다. 판매대금과 판매현황은 집계 기준이 다를 수 있어서, 어느 시점 기준으로 잡는지, 취소/환불은 포함인지 제외인지를 명시적으로 정했다. 이게 불명확하면 화면마다 숫자가 다르게 나오고, 나중에 "왜 이 숫자가 달라요?"라는 질문이 반드시 온다.

-- 판매대금 집계 예시 (실제 쿼리 아님, 설계 관점 패턴)
SELECT
    s.supplier_id,
    SUM(CASE WHEN o.status NOT IN ('CANCEL', 'REFUND') THEN o.amount ELSE 0 END) AS net_sales,
    COUNT(CASE WHEN o.status NOT IN ('CANCEL', 'REFUND') THEN 1 END) AS order_cnt
FROM orders o
JOIN supplier_mapping s ON o.supplier_id = s.supplier_id
WHERE s.supplier_id = #{supplierId}       -- 반드시 바인딩
  AND o.settled_at BETWEEN #{from} AND #{to}
GROUP BY s.supplier_id

취소/환불 처리 방식, 날짜 기준 컬럼 선택, NULL 방어를 쿼리 자체에서 해결하는 게 원칙이다. 앱 레이어에서 보정하면 같은 로직이 두 군데 생기고 언젠가 둘이 달라진다.

URL 접근제어는 인터셉터 방식으로 처리했다. 신규 메뉴 URL 패턴을 인터셉터 설정에 추가하고, 공급사 세션이 없거나 해당 메뉴 권한이 없는 경우 차단. JSP 화면만 숨긴다고 끝이 아니라, API 엔드포인트도 동일하게 인터셉터를 통과하게 해야 한다. 프론트에서 버튼이 안 보여도 URL 직접 접근은 막아야 하기 때문.

인터셉터 설정 예시는 대략 이런 구조다.

// 인터셉터 등록 (WebMvcConfigurer)
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(supplierAuthInterceptor)
        .addPathPatterns("/supplier/**")        // 전체 포탈
        .excludePathPatterns("/supplier/login", "/supplier/error");
}

패턴 하나 빠지면 그 URL만 구멍이 된다. 이번엔 신규 경로를 추가하면서 기존 인터셉터 커버리지를 다시 한번 훑었다.

사이드바 메뉴 활성화는 작은 것 같아도 UX에서 생각보다 중요하다. 현재 URL과 메뉴 항목을 매핑해서 active 클래스를 붙이는 방식인데, 메뉴 타입 분리 이후로 매핑 로직이 복잡해져서 뷰 단에서 조건문이 늘어났다. 이 부분은 나중에 정리 여지가 있다.

검증 방식과 작업 후기

구현 후엔 직접 화면에서 케이스를 돌려봤다. 기존 어드민 화면의 집계 숫자와 신규 공급사 포탈 화면의 숫자가 일치하는지 cross-check가 핵심이었다. 금융/결제 도메인에서 숫자 불일치는 곧 신뢰 문제다. "대충 맞는 것 같다"는 검증이 아니라, 같은 기간, 같은 공급사 조건으로 두 화면을 나란히 놓고 확인했다.

빈 데이터 케이스도 따로 확인했다. 신규 공급사나 해당 기간 매출이 없는 경우 화면이 어떻게 나오는지, NULL이 집계 컬럼에 섞였을 때 프론트에서 이상한 값이 표시되지 않는지.

커밋은 논리 단위로 쪼갰다. 메뉴 타입 분리, SQL/매퍼 추가, API 구현, 프론트 연동, 인터셉터 설정 순으로 각각 커밋을 분리하면, 나중에 특정 시점에서 문제가 생겼을 때 어느 변경이 원인인지 bisect가 훨씬 쉽다. 한 커밋에 다 때려넣으면 롤백할 단위도 없어진다.

커밋 메시지는 "무엇을"보다 "왜"를 담으려는 습관을 계속 유지 중이다. 판매대금 화면 추가보다 공급사 정산 가시성 확보 - 공급사별 순매출/주문건수 집계 API 및 화면 구현이 3개월 뒤에 git log를 봤을 때 맥락을 복원하는 데 훨씬 도움이 된다.

사내 포탈 하나에 기능을 추가하는 작업인데, 결국 SQL 집계 설계, 상태 필터 정의, 권한 격리, 프론트 렌더링, 인터셉터 설정이 전부 엮인다. 어느 하나만 빠뜨려도 숫자가 안 맞거나, 잘못된 사람이 잘못된 데이터를 보게 된다. 레이어가 많아서 귀찮은 게 아니라, 그 레이어 각각이 역할이 있는 거다.

댓글 0

첫 댓글 달아줘.