fix(cupang): itemcode 조회 오류를 로그+API+화면에 노출 (원인 진단)

- ItemcodeReader.last_error 추가, _run 실패 시 logger.exception + 메시지 보존
- /api/products/all 응답에 error/count 추가
- products 화면: 결과 0 일 때 조회오류/사유 표시 (그냥 '결과 없음' 대신)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 21:16:54 +09:00
parent 8f3761aeae
commit cb43fe1257
3 changed files with 20 additions and 3 deletions
+8 -1
View File
@@ -28,10 +28,13 @@ product_name_snapshot 만 보존한다.
from __future__ import annotations from __future__ import annotations
import logging
import os import os
import re import re
from typing import Any from typing import Any
logger = logging.getLogger("cupang.itemcode")
# 안전한 SQL 식별자(테이블/컬럼)만 허용 — 인젝션 방지. # 안전한 SQL 식별자(테이블/컬럼)만 허용 — 인젝션 방지.
_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$") _IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$")
@@ -55,6 +58,7 @@ class ItemcodeReader:
self._sql: str | None = None self._sql: str | None = None
self.enabled = False self.enabled = False
self.reason = "" self.reason = ""
self.last_error = "" # 마지막 조회 오류(진단용, 비밀값 없음)
self._configure() self._configure()
def _configure(self) -> None: def _configure(self) -> None:
@@ -130,7 +134,10 @@ class ItemcodeReader:
rows = conn.execute( rows = conn.execute(
self._sql, {"q": like, "limit": int(limit)} self._sql, {"q": like, "limit": int(limit)}
).fetchall() ).fetchall()
except Exception: # noqa: BLE001 — 비밀값 노출 없이 빈 결과. 상세는 서버 로그. self.last_error = ""
except Exception as exc: # noqa: BLE001 — 모듈을 죽이지 않음. 원인은 로그 + last_error.
self.last_error = f"{type(exc).__name__}: {exc}"
logger.exception("itemcode 조회 실패 (SQL/스키마 확인 필요)")
return [] return []
out: list[dict[str, Any]] = [] out: list[dict[str, Any]] = []
for r in rows: for r in rows:
+4 -1
View File
@@ -604,11 +604,14 @@ async def product_all(
) -> JSONResponse: ) -> JSONResponse:
"""itemcode_db 전체 상품 목록(낱개+세트). 설정 화면 왼쪽 리스트 소스.""" """itemcode_db 전체 상품 목록(낱개+세트). 설정 화면 왼쪽 리스트 소스."""
reader = _itemcode(request) reader = _itemcode(request)
results = reader.list_all() if reader else []
return JSONResponse( return JSONResponse(
{ {
"enabled": bool(reader and reader.enabled), "enabled": bool(reader and reader.enabled),
"reason": (reader.reason if reader else "itemcode 리더 미초기화"), "reason": (reader.reason if reader else "itemcode 리더 미초기화"),
"results": reader.list_all() if reader else [], "error": (getattr(reader, "last_error", "") if reader else ""),
"count": len(results),
"results": results,
} }
) )
@@ -139,7 +139,14 @@
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (data) { .then(function (data) {
all = (data && data.results) || []; all = (data && data.results) || [];
if (!all.length) { listBox.innerHTML = '<p class="erp-muted">itemcode_db 결과 없음</p>'; return; } if (!all.length) {
var msg = "itemcode_db 결과 없음";
if (data && data.error) { msg += " — 조회 오류: " + esc(data.error); }
else if (data && !data.enabled && data.reason) { msg += " — " + esc(data.reason); }
else { msg += " (테이블이 비었거나 검색 SQL 조건 불일치)"; }
listBox.innerHTML = '<p class="erp-muted">' + msg + '</p>';
return;
}
render(); render();
}) })
.catch(function () { listBox.innerHTML = '<p class="erp-muted">목록 로드 실패</p>'; }); .catch(function () { listBox.innerHTML = '<p class="erp-muted">목록 로드 실패</p>'; });