CoreERP Vendor 등록 / 현황 프론트 · 백엔드 통합 및 UI 정리 기록

최병현·2026년 2월 28일

coreerp project

목록 보기
23/44

이번 단계에서는 Vendor(업체) 관리 기능을 프론트엔드(React + TypeScript)와 백엔드(Spring Boot) 전체 흐름으로 정리했다. 단순 CRUD가 아니라, 페이지네이션 · 정렬 · 옵션 필터 · 상태 배지 · 모달 상세보기까지 포함하여 실제 ERP에서 사용할 수 있는 수준의 구조로 다듬는 것을 목표로 했다.


1. 이번 단계의 핵심 목적

  • Vendor 목록 API와 프론트 완전 연동
  • Page 기반 검색/정렬 구조 확립
  • UI 헤더 구조 정리 (타이틀 + 결과 수 + 페이지네이션 분리)
  • 등록/수정 모달 완성
  • 에러 발생 케이스 정리 및 구조 개선

2. Backend – Vendor 조회 API 구조

Backend는 Spring Boot + JPA 기반이며, Page 객체를 그대로 반환하는 구조로 설계했다. Controller는 단순히 파라미터를 받고, Service에서 동적 조건을 구성하도록 역할을 분리했다.

Controller

@GetMapping
public Page<VendorResponse> list(
        @RequestParam(required = false) String keyword,
        @RequestParam(required = false) VendorStatus status,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size
) {
    Pageable pageable = PageRequest.of(page, size);
    return vendorService.search(keyword, status, pageable);
}

Controller는 Presentation Layer 역할만 수행한다. 검색 조건을 직접 처리하지 않고 Service로 위임한다.

Service (핵심 로직)

public Page<VendorResponse> search(String keyword, VendorStatus status, Pageable pageable) {
    return vendorRepository.search(keyword, status, pageable)
            .map(VendorResponse::from);
}

Entity → DTO 변환은 map을 통해 처리한다. JPA Page를 그대로 활용하여 totalElements, totalPages 등을 유지한다.


3. Frontend – API 통합 구조

Frontend는 React + TypeScript 기반이며, PageResponse 타입을 정의하여 Backend Page 구조와 정확히 맞춰서 사용했다.

PageResponse 타입

type PageResponse<T> = {
  content: T[];
  totalElements: number;
  totalPages: number;
  number: number;
  size: number;
};

조회 API 호출

async function apiListVendors(filter: VendorFilter, page: number, size: number): Promise<PageResponse<Vendor>> {
  const qs = buildQuery(filter, page, size);
  const res = await fetch(`/api/vendors?${qs}`, { method: "GET" });
  if (!res.ok) throw new Error(`list vendors failed: ${res.status}`);
  return await res.json();
}

Backend Page 구조와 완전히 동일하게 받아오기 때문에, 추가 가공 없이 totalPages 기반 페이지네이션 구현이 가능해졌다.


4. 페이지네이션 UI 설계

기존에는 숫자 버튼이 한 개만 표시되거나, 레이아웃이 오른쪽에 과도하게 몰려 있었다. 이번에 window 기반 페이지 계산 함수로 개선했다.

function buildPageWindow(page: number, totalPages: number, windowSize = 5) {
  const half = Math.floor(windowSize / 2);
  let start = Math.max(0, page - half);
  let end = Math.min(totalPages - 1, start + windowSize - 1);
  start = Math.max(0, end - windowSize + 1);

  const pages: number[] = [];
  for (let p = start; p <= end; p++) pages.push(p);
  return pages;
}

이 방식은 현재 페이지를 중심으로 5개씩 보여주는 ERP 스타일 페이지 구조다.


5. Header 레이아웃 개선

기존에는 "검색 결과 n건"이 오른쪽 액션 영역에 포함되어 시선이 분산되었다. 타이틀과 함께 묶는 구조로 변경했다.

<div className="panel-head">
  <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
    <h2 className="panel-title">업체 정보</h2>
    <span className="badge">{`검색 결과 ${totalElements}건`}</span>
  </div>

  <div className="panel-actions">
    {/* pagination + 등록 버튼 */}
  </div>
</div>

좌측: 상태 요약 우측: 조작 영역 으로 명확히 분리하였다.


6. 주요 트러블슈팅

① 페이지 버튼이 하나만 표시되는 문제

원인

  • totalPages 값이 0으로 초기화된 상태에서 렌더링
  • pageWindow 계산 로직이 없음

해결

  • Backend Page.totalPages 정확히 수신
  • window 기반 계산 함수 도입

② 검색 결과 뱃지가 오른쪽에 몰려 UI가 복잡해 보이는 문제

원인

  • panel-actions 내부에 모든 요소를 몰아넣음

해결

  • 타이틀 + 결과 수 묶음 div 추가
  • 조작 영역과 시각적으로 분리

③ 업체 등록 버튼이 너무 튀는 문제

원인

  • primary 색상이 페이지 버튼과 동일 강도
  • 헤더 영역 대비 과도한 시선 집중

해결

  • soft-primary 톤 적용
  • border + 연한 배경 색상으로 다운톤 처리

7. 데이터 흐름 정리

  1. 사용자 필터 입력
  2. React state → appliedFilter 반영
  3. useEffect 트리거
  4. /api/vendors 호출
  5. Spring Controller → Service → Repository
  6. Page 반환
  7. Frontend 상태 반영

Frontend는 API 통신 계층, Backend는 비즈니스 계층으로 분리되어 있으며, DTO를 통해 의존성을 최소화했다.


8. 이번 단계의 의미

  • 단순 CRUD를 넘어 실제 ERP 구조 완성
  • Frontend–Backend Pagination 완전 일치
  • UI 구조 기준 고정
  • Page 기반 아키텍처 확립

이제 Vendor는 구조가 완전히 정리된 상태이며, Inbound/Outbound/Transfer 등 다른 도메인도 동일 패턴으로 확장 가능하다.

profile
Develop

0개의 댓글