CoreERP 품목 등록 API 프론트 연동 기록

최병현·2026년 3월 1일

coreerp project

목록 보기
25/44

1. 이번 단계의 관점

CoreERP에서 Master Data는 모든 트랜잭션(발주/입고/출고/재고)의 출발점이다.

이번 단계는 단순히 “품목 등록 화면을 만든다”가 아니라, 실무형 기준으로 식별자 정책과 무결성 기준을 확정하고 프론트/백엔드를 동일한 규칙으로 묶는 작업이었다.

결론적으로 정책은 다음처럼 확정했다.

  • 품번(Item Code): 수동 입력 (중복 검증 필수)
  • 바코드(Barcode): 등록 시 자동 생성 (사용자는 입력하지 않음)
  • 제조사(Manufacturer): 필수 입력으로 승격

2. 작업 범위

  • Backend ItemService 생성 정책 확정: 품번 수동 입력 + 바코드 자동 생성
  • 중복 검증: itemCode / barcode
  • 필수값 검증 강화: itemName, itemCode, manufacturer
  • Frontend ItemCreate 페이지: 백엔드 정책에 맞춰 UI/검증/요청 payload 동기화
  • 바코드 입력 영역을 “자동생성 안내” UI로 전환
  • 등록 완료 시 서버가 생성한 barcode를 사용자에게 표시

3. 설계 의도

3.1 품번을 자동 생성으로 가지 않은 이유

품번은 카테고리별 체계가 회사마다 다르고, 현재 프로젝트에서도 카테고리 정책이 확정되지 않은 상태였다.

이 상태에서 품번 자동생성을 고정하면 추후 기준이 바뀌었을 때 데이터 정리/마이그레이션 비용이 커진다.

따라서 이번 단계에서는 “수동 입력 + 백엔드 중복 검증”을 기준으로 두고, 카테고리 정책 확정 이후 자동 생성 도입 여부를 판단하기로 했다.

3.2 바코드를 자동 생성으로 확정한 이유

바코드는 사람이 직접 생성할 이유가 거의 없고, 유일성이 중요하다.

또한 스캐너/외부 연동 가능성까지 고려하면 “자동 생성 + 중복 방지”가 가장 안정적이다.

결론적으로 DocumentNoGenerator를 통해 바코드 자동 생성 정책을 확정했다.


4. 데이터 흐름

  • Frontend(React)에서 사용자가 itemCode/itemName/... 입력
  • POST /api/items로 JSON payload 전송
  • Backend(Spring Boot) ItemService에서 검증(필수/중복) + barcode 자동생성
  • DB(Item 테이블)에 저장 후 ItemResponse 반환
  • Frontend는 응답의 itemCode/barcode를 안내(alert)로 표시

5. 주요 코드

5.1 Backend – barcode 자동 생성 + 품번 수동 입력 + 제조사 필수 검증

@Transactional public ItemResponse create(CreateItemRequest req) {

if (req.itemCode() == null || req.itemCode().isBlank()) {
    throw new IllegalArgumentException("품번은 필수입니다.");
}

if (req.itemName() == null || req.itemName().isBlank()) {
    throw new IllegalArgumentException("품명은 필수입니다.");
}

if (req.manufacturer() == null || req.manufacturer().isBlank()) {
    throw new IllegalArgumentException("제조사는 필수입니다.");
}

String itemCode = req.itemCode().trim();

if (itemRepository.existsByItemCode(itemCode)) {
    throw new IllegalArgumentException("이미 존재하는 품번입니다.");
}

String barcode = documentNoGenerator.next(
        "BC",
        LocalDate.now(),
        itemRepository::existsByBarcode
);

Item saved = itemRepository.save(
        Item.builder()
                .itemCode(itemCode)
                .barcode(barcode)
                .itemName(req.itemName().trim())
                .itemType(normalizeExact(req.itemType()))
                .category(normalizeExact(req.category()))
                .spec(normalizeExact(req.spec()))
                .unit(defaultIfBlank(normalizeExact(req.unit()), "EA"))
                .manufacturer(req.manufacturer().trim())
                .memo(normalizeExact(req.memo()))
                .build()
);

return toResponse(saved);

5.2 Backend – DocumentNoGenerator를 이용한 번호 생성

@Component
public class DocumentNoGenerator {

    public String next(String prefix, LocalDate date, Predicate<String> existsFn) {
        return next(prefix, date, existsFn, 50);
    }

    public String next(String prefix, LocalDate date, Predicate<String> existsFn, int maxRetry) {
        Objects.requireNonNull(prefix, "prefix");
        Objects.requireNonNull(date, "date");
        Objects.requireNonNull(existsFn, "existsFn");

        if (maxRetry <= 0) {
            throw new IllegalArgumentException("maxRetry must be positive");
        }

        String base = date.toString().replace("-", "");

        for (int i = 0; i < maxRetry; i++) {
            String rand = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
            String no = prefix + "-" + base + "-" + rand;
            if (!existsFn.test(no)) return no;
        }

        throw new IllegalStateException(prefix + " 번호 생성에 실패했습니다. 다시 시도해주세요.");
    }
}

5.3 Frontend – 바코드 UI를 “자동 생성 안내”로 전환

<div className="field"> 
  <div className="filter-label"> 바코드 (Barcode) 
    <span style={{ opacity: 0.7 }}>(auto)</span> 
  </div> 
  <div style={{ display: "flex", gap: "8px", alignItems: "center" }}> 
    <div style={{ position: "relative", flex: 1 }}> 
      <input className="input" value="" placeholder="바코드는 등록 시 자동 생성됩니다." 
        autoComplete="off" disabled style={{ backgroundColor: "#f3f4f6", color: "#6b7280", cursor: "not-allowed" }} /> 
    </div> 
    </div> 
</div>

5.4 Frontend – 등록 요청 및 응답 barcode 표시

const res = await fetch("/api/items", 
                        { method: "POST", 
                         headers: { "Content-Type": "application/json" }, 
                         body: JSON.stringify(payload), });

if (!res.ok) {
const text = await res.text().catch(() => "");
alert(품목 등록 실패: ${res.status}\n${text});
return;
}

const data = (await res.json()) as Partial<ItemResponse>;
const createdCode = toText(data.itemCode) || payload.itemCode;
const createdBarcode = toText(data.barcode) || "-";

alert(품목 등록 완료\nItem Code: ${createdCode}\nBarcode(자동생성): ${createdBarcode});

6. 트러블슈팅 (원인 / 해결 결과)

6.1 비고(memo)가 “상세보기”에서 안 보이는 문제

원인: 저장 로직 문제가 아니라, 등록 시 memo를 입력하지 않아 DB에 null로 저장된 상태였다.

해결 결과: 응답 JSON을 직접 확인해 null 여부를 확인했고, 이후에는 입력 여부를 먼저 체크하는 습관으로 전환했다.

6.2 제조사가 optional로 저장되는 문제

원인: 프론트에서는 manufacturer를 비워도 통과했고, 백엔드에서도 manufacturer 필수 검증이 없었다.

해결 결과: ItemService.create에서 manufacturer 필수 검증을 추가했고, 프론트 validate도 동일하게 맞춰 무결성 기준을 통일했다.

6.3 품번 자동/수동 정책 혼란

원인: 카테고리 정책이 확정되지 않은 상태에서 품번 자동 생성 도입 여부를 결정하지 못해 설계가 흔들렸다.

해결 결과: 이번 단계에서는 “품번 수동 입력”으로 고정하고, “바코드만 자동 생성”으로 분리해 안정적으로 마무리했다.


7. 이번 단계의 의미

이번 작업은 단순 CRUD가 아니라, Master Data의 기준을 고정한 단계였다.

  • 식별자 정책 확정 (itemCode 수동 / barcode 자동)
  • 프론트/백 검증 규칙 통일
  • DB 무결성 기준을 Service 레이어에서 강제

Item은 모든 재고 트랜잭션이 의존하는 엔티티이기 때문에, 여기서 정책이 흔들리면 이후 발주/입고/출고 흐름 전체가 흔들린다.

이번 단계에서 그 기반을 고정했다는 점이 가장 크다.


8. 다음 단계 계획

  • Item 수정 페이지/로직 정리 (Update 정책 확정)
  • safetyStock / unitCost 필수 여부 및 UI 반영
  • 카테고리 체계 확정 후 품번 자동 생성 도입 여부 재검토
  • 재고/입출고 모듈에서 Item 검색/선택 UX 고도화
profile
Develop

0개의 댓글