
이번 작업은 단순한 품목 조회 화면 구현이 아니라, CoreERP의 “데이터 조회 구조를 실무형으로 전환하는 단계”였다.
기존 List 기반 조회를 Page 기반 API로 변경하고, 프론트는 서버 정렬 및 필터와 완전히 연동되도록 구조를 고정했다.
이 단계의 핵심은 다음 두 가지다.
실무에서는 단순 List 반환은 거의 사용되지 않는다. 데이터가 증가하면 반드시 페이지네이션, 정렬, 필터가 필요하다.
따라서 Item 조회는 아래 기준으로 설계했다.
@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);
}
Sort sort = Sort.by(
sortOrder.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC,
allowedSortKey(sortKey)
);
PageRequest pageRequest = PageRequest.of(page, size, sort);
화이트리스트를 둔 이유는 클라이언트가 임의 필드를 정렬 파라미터로 보내는 것을 방지하기 위함이다.
프론트는 다음 원칙으로 설계했다.
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();
};
const fetchDetail = async (itemId: number) => {
const res = await fetch(`/api/items/${itemId}`);
const data = await res.json();
return normalizeRow(data);
};
원인
해결
원인
해결
onClick={() => openMemo(it.itemId)}
상세 조회는 반드시 PK 기준으로 통일해야 한다.
원인
해결
const safeText = (v: string | null | undefined) =>
v && v.trim() ? v.trim() : "-";
데이터가 완전하지 않아도 화면이 깨지지 않도록 방어 설계.
이제 Item 조회 구조는 실무 수준으로 안정화되었다. 데이터를 truncate 후 다시 세팅하면 더욱 깔끔한 화면 구성이 가능하다.