CoreERP 품목 조회 API Page 전환 및 프론트 연동 기록

최병현·2026년 3월 1일

coreerp project

목록 보기
24/44

1. 이번 단계의 관점

이번 작업은 단순한 품목 조회 화면 구현이 아니라, CoreERP의 “데이터 조회 구조를 실무형으로 전환하는 단계”였다.

기존 List 기반 조회를 Page 기반 API로 변경하고, 프론트는 서버 정렬 및 필터와 완전히 연동되도록 구조를 고정했다.

이 단계의 핵심은 다음 두 가지다.

  • 조회 API를 확장 가능한 구조로 전환
  • UI를 서버 중심 정렬/필터 구조에 맞춰 안정화

2. 작업 범위

  • Item 목록 API를 List → Page 기반으로 전환
  • keyword / itemType / category / status 필터 추가
  • sortKey / sortOrder 서버 정렬 적용
  • 프론트 컬럼 순서 재정렬
  • 모달 상세 조회를 itemId 기준으로 수정
  • null-safe 처리로 렌더링 안정화

3. Backend 설계 의도

실무에서는 단순 List 반환은 거의 사용되지 않는다. 데이터가 증가하면 반드시 페이지네이션, 정렬, 필터가 필요하다.

따라서 Item 조회는 아래 기준으로 설계했다.

  • Page<ItemResponse> 반환
  • 화이트리스트 기반 정렬 허용
  • 동적 필터 조건 처리
  • 응답은 DTO로만 노출 (Entity 직접 반환 금지)

4. 주요 Backend 코드

ItemController

@GetMapping
public Page<ItemResponse> list(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(required = false) String keyword,
        @RequestParam(required = false) String itemType,
        @RequestParam(required = false) String category,
        @RequestParam(required = false) String status,
        @RequestParam(defaultValue = "itemName") String sortKey,
        @RequestParam(defaultValue = "asc") String sortOrder
) {
    return itemService.search(page, size, keyword, itemType, category, status, sortKey, sortOrder);
}

ItemService – 정렬 화이트리스트 처리

Sort sort = Sort.by(
        sortOrder.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC,
        allowedSortKey(sortKey)
);

PageRequest pageRequest = PageRequest.of(page, size, sort);

화이트리스트를 둔 이유는 클라이언트가 임의 필드를 정렬 파라미터로 보내는 것을 방지하기 위함이다.


5. Frontend 설계 의도

프론트는 다음 원칙으로 설계했다.

  • 서버 정렬을 기본으로 사용 (클라이언트 정렬 제거)
  • draft / applied 상태 분리
  • null-safe 처리
  • 상세 조회는 itemId 기반 호출

Page API 연동

const buildQuery = (f: FilterState, p: number, s: number) => {
  const qs = new URLSearchParams();
  qs.set("page", String(p));
  qs.set("size", String(s));

  if (f.keyword) qs.set("keyword", f.keyword);
  if (f.itemType) qs.set("itemType", f.itemType);
  if (f.category) qs.set("category", f.category);
  if (f.status) qs.set("status", f.status);

  qs.set("sortKey", f.sortKey || "itemName");
  qs.set("sortOrder", f.sortOrder);

  return qs.toString();
};

상세 모달 itemId 기준 수정

const fetchDetail = async (itemId: number) => {
  const res = await fetch(`/api/items/${itemId}`);
  const data = await res.json();
  return normalizeRow(data);
};

6. 트러블슈팅

문제 1 – 데이터가 화면에 안 나오는 현상

원인

  • 프론트는 Page 응답을 기대했지만, 초기에는 List 형태 응답이었음
  • data.content가 undefined → rows가 빈 배열 처리됨

해결

  • 백엔드를 Page<ItemResponse>로 전환
  • 프론트에서 PageResponse 타입으로 명확히 처리

문제 2 – 모달 상세 조회 실패

원인

  • 프론트에서 itemCode로 상세 호출
  • 백엔드는 @PathVariable Long id 구조

해결

onClick={() => openMemo(it.itemId)}

상세 조회는 반드시 PK 기준으로 통일해야 한다.

문제 3 – null 값으로 인한 렌더링 오류

원인

  • itemType, category, manufacturer 등이 null로 내려옴
  • UI에서 string 전제로 처리

해결

const safeText = (v: string | null | undefined) =>
  v && v.trim() ? v.trim() : "-";

데이터가 완전하지 않아도 화면이 깨지지 않도록 방어 설계.

7. 이번 단계의 의미

  • List → Page 전환 완료
  • 서버 정렬 구조 고정
  • 프론트/백 완전 연동
  • UI 컬럼 구조 실무형 재배치

이제 Item 조회 구조는 실무 수준으로 안정화되었다. 데이터를 truncate 후 다시 세팅하면 더욱 깔끔한 화면 구성이 가능하다.

8. 다음 단계

  • Item 등록/수정 화면 고도화
  • Enum을 문자열이 아닌 공통 코드 구조로 개선
  • NOT NULL 제약 조건 정리
  • 검색 인덱스 설계
profile
Develop

0개의 댓글