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:
@@ -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:
|
||||||
|
|||||||
@@ -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>'; });
|
||||||
|
|||||||
Reference in New Issue
Block a user