diff --git a/app/modules/dispatch/router.py b/app/modules/dispatch/router.py index 31eb161..ba3a63e 100644 --- a/app/modules/dispatch/router.py +++ b/app/modules/dispatch/router.py @@ -519,10 +519,43 @@ async def stock_deduct( bom_map, _sab = _itemcode_bom_and_sabangnet(request) missing = [c for c in combos if c not in bom_map] if missing: - raise HTTPException( - status_code=400, - detail="콤보 BOM 이 없어 차감할 수 없습니다(세트구성 먼저 등록): " + ", ".join(sorted(missing)), - ) + # 'BOM 누락'을 원인별로 쪼개 정확히 알린다. 그동안 이 메시지가 + # (a)미등록 (b)구성품없음 (c)권한/조회실패 를 한 덩어리로 가려서 + # "고쳐도 또 난다"가 반복됐다. + reader = getattr(request.app.state, "malaysia_itemcode", None) + registered: set[str] = set() + if reader is not None and getattr(reader, "enabled", False): + try: + registered = reader.set_codes_present() + except Exception: # noqa: BLE001 + logger.exception("set_codes_present 조회 실패") + query_failed = bool(getattr(reader, "last_error", "")) if reader else False + + if query_failed: + # set_components 조회 자체가 실패 — 보통 itemcode_ro 권한 문제. + raise HTTPException( + status_code=400, + detail=( + "itemcode_db 의 세트구성(set_components) 조회에 실패해 콤보를 분해할 수 " + "없습니다. 읽기 전용 역할(itemcode_ro)에 set_components SELECT 권한이 " + f"있는지 확인하세요. (상세: {reader.last_error})" + ), + ) + + no_components = sorted(c for c in missing if c in registered) + not_registered = sorted(c for c in missing if c not in registered) + parts: list[str] = [] + if not_registered: + parts.append( + "세트구성 미등록(itemcode_db.set_components 에 코드 자체가 없음): " + + ", ".join(not_registered) + ) + if no_components: + parts.append( + "세트는 등록됐으나 유효한 낱개(MT-/MX-/MZ-) 구성품이 없습니다" + "(구성품 코드·수량 확인): " + ", ".join(no_components) + ) + raise HTTPException(status_code=400, detail="콤보 BOM 문제 — " + " / ".join(parts)) # 마커 선점(레이스/재실행 차단). 충돌 시 이미 차감됨. try: diff --git a/app/modules/malaysia/itemcode.py b/app/modules/malaysia/itemcode.py index 8ef8d76..5ea991a 100644 --- a/app/modules/malaysia/itemcode.py +++ b/app/modules/malaysia/itemcode.py @@ -96,6 +96,30 @@ class MalaysiaItemcodeReader: ) return out + def set_codes_present(self) -> set[str]: + """set_components 에 등록된 MY- 세트 코드 전체(구성품 유효성 무관). + + set_bom_map() 은 낱개(MT/MX/MZ) 구성품이 하나도 없는 세트를 결과에서 + 떨어뜨린다. 그래서 'BOM 누락'이 두 가지 원인을 가린다: + (a) set_components 에 set_code 자체가 없음 → 세트구성 미등록 + (b) set_code 는 있으나 유효 낱개 구성품이 없음 → 구성품 점검 필요 + 이 메서드로 (a)/(b)를 구분해 정확한 에러 메시지를 만든다. + """ + if not self.enabled or not self._pool: + return set() + try: + with self._pool.connection() as conn: + rows = conn.execute( + "SELECT DISTINCT set_code FROM set_components " + "WHERE set_code LIKE 'MY-%'" + ).fetchall() + self.last_error = "" + except Exception as exc: # noqa: BLE001 — 모듈을 죽이지 않음 + self.last_error = f"{type(exc).__name__}: {exc}" + logger.exception("itemcode set_components set_code 조회 실패") + return set() + return {str(r["set_code"]).strip().upper() for r in rows} + def name_map(self) -> dict[str, str]: """낱개+세트 코드 → 이름(itemcode_db 기준). 표시 보강용(선택).""" if not self.enabled or not self._pool: