feat(cupang): 제품 카탈로그에 쿠팡상품코드 + 상태 토글
- cupang_products.coupang_item_code 컬럼 추가 (마이그레이션 004, init.sql 동기화)
- 설정 화면: 쿠팡상품코드 열 표시/정렬, 수기 추가 폼 입력란 추가
- 선택 등록 시 바로 등록하지 않고 쿠팡상품코드 입력 팝업을 먼저 표시
- 상태 배지를 클릭 토글로 변경 (POST /cupang/api/products/{id}/toggle)
- 빈 쿠팡상품코드는 "미입력"으로 처리해 기존 값을 덮어쓰지 않음
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -294,24 +294,38 @@ class CupangDBStore:
|
|||||||
return [self._product_serialize(r) for r in rows]
|
return [self._product_serialize(r) for r in rows]
|
||||||
|
|
||||||
def upsert_product(
|
def upsert_product(
|
||||||
self, *, product_code: str, product_name: str, sort_order: int = 0
|
self,
|
||||||
|
*,
|
||||||
|
product_code: str,
|
||||||
|
product_name: str,
|
||||||
|
coupang_item_code: str | None = None,
|
||||||
|
sort_order: int = 0,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
"""제품 등록/수정.
|
||||||
|
|
||||||
|
coupang_item_code 가 None 이면 기존 값을 유지한다(빈 문자열은 지우기).
|
||||||
|
"""
|
||||||
code = (product_code or "").strip()
|
code = (product_code or "").strip()
|
||||||
name = (product_name or "").strip()
|
name = (product_name or "").strip()
|
||||||
|
cic = None if coupang_item_code is None else coupang_item_code.strip()
|
||||||
if not code or not name:
|
if not code or not name:
|
||||||
raise ValueError("제품코드와 제품명 모두 필요합니다.")
|
raise ValueError("제품코드와 제품명 모두 필요합니다.")
|
||||||
with self._pool.connection() as conn:
|
with self._pool.connection() as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO cupang_products (product_code, product_name, sort_order)
|
INSERT INTO cupang_products
|
||||||
VALUES (%s, %s, %s)
|
(product_code, product_name, coupang_item_code, sort_order)
|
||||||
|
VALUES (%(code)s, %(name)s, COALESCE(%(cic)s, ''), %(sort)s)
|
||||||
ON CONFLICT (product_code) DO UPDATE
|
ON CONFLICT (product_code) DO UPDATE
|
||||||
SET product_name = EXCLUDED.product_name,
|
SET product_name = EXCLUDED.product_name,
|
||||||
|
coupang_item_code = COALESCE(
|
||||||
|
%(cic)s, cupang_products.coupang_item_code
|
||||||
|
),
|
||||||
sort_order = EXCLUDED.sort_order,
|
sort_order = EXCLUDED.sort_order,
|
||||||
active = TRUE
|
active = TRUE
|
||||||
RETURNING *
|
RETURNING *
|
||||||
""",
|
""",
|
||||||
(code, name, sort_order),
|
{"code": code, "name": name, "cic": cic, "sort": sort_order},
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return self._product_serialize(row)
|
return self._product_serialize(row)
|
||||||
|
|
||||||
@@ -324,6 +338,18 @@ class CupangDBStore:
|
|||||||
if cur.rowcount == 0:
|
if cur.rowcount == 0:
|
||||||
raise KeyError(product_id)
|
raise KeyError(product_id)
|
||||||
|
|
||||||
|
def toggle_product_active(self, *, product_id: int) -> dict[str, Any]:
|
||||||
|
"""활성 ↔ 비활성 뒤집기. 갱신된 행을 반환."""
|
||||||
|
with self._pool.connection() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"UPDATE cupang_products SET active = NOT active "
|
||||||
|
"WHERE id = %s RETURNING *",
|
||||||
|
(product_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise KeyError(product_id)
|
||||||
|
return self._product_serialize(row)
|
||||||
|
|
||||||
def delete_product(self, *, product_id: int) -> None:
|
def delete_product(self, *, product_id: int) -> None:
|
||||||
with self._pool.connection() as conn:
|
with self._pool.connection() as conn:
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
@@ -642,6 +668,7 @@ class CupangDBStore:
|
|||||||
out["id"] = int(out["id"])
|
out["id"] = int(out["id"])
|
||||||
out["active"] = bool(out.get("active", True))
|
out["active"] = bool(out.get("active", True))
|
||||||
out["sort_order"] = int(out.get("sort_order", 0))
|
out["sort_order"] = int(out.get("sort_order", 0))
|
||||||
|
out["coupang_item_code"] = (out.get("coupang_item_code") or "").strip()
|
||||||
for k in ("created_at", "updated_at"):
|
for k in ("created_at", "updated_at"):
|
||||||
v = out.get(k)
|
v = out.get(k)
|
||||||
if isinstance(v, datetime):
|
if isinstance(v, datetime):
|
||||||
@@ -685,6 +712,7 @@ class CupangDBStore:
|
|||||||
out["id"] = int(out["id"])
|
out["id"] = int(out["id"])
|
||||||
out["active"] = bool(out.get("active", True))
|
out["active"] = bool(out.get("active", True))
|
||||||
out["sort_order"] = int(out.get("sort_order", 0))
|
out["sort_order"] = int(out.get("sort_order", 0))
|
||||||
|
out["coupang_item_code"] = (out.get("coupang_item_code") or "").strip()
|
||||||
for k in ("created_at", "updated_at"):
|
for k in ("created_at", "updated_at"):
|
||||||
v = out.get(k)
|
v = out.get(k)
|
||||||
if isinstance(v, datetime):
|
if isinstance(v, datetime):
|
||||||
|
|||||||
@@ -846,7 +846,11 @@ async def product_bulk(
|
|||||||
items: list[dict[str, Any]] = Body(..., embed=True),
|
items: list[dict[str, Any]] = Body(..., embed=True),
|
||||||
user: dict[str, Any] = Depends(_require_user),
|
user: dict[str, Any] = Depends(_require_user),
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""선택한 상품들을 일괄 등록(upsert). body: {"items":[{"code","name"}, ...]}."""
|
"""선택한 상품들을 일괄 등록(upsert).
|
||||||
|
|
||||||
|
body: {"items":[{"code","name","coupang_item_code"}, ...]}
|
||||||
|
coupang_item_code 키가 없으면 기존 값을 유지한다.
|
||||||
|
"""
|
||||||
store = _store(request)
|
store = _store(request)
|
||||||
if store is None:
|
if store is None:
|
||||||
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
||||||
@@ -854,10 +858,15 @@ async def product_bulk(
|
|||||||
for it in items:
|
for it in items:
|
||||||
code = str(it.get("code") or "").strip()
|
code = str(it.get("code") or "").strip()
|
||||||
name = str(it.get("name") or "").strip()
|
name = str(it.get("name") or "").strip()
|
||||||
|
cic = it.get("coupang_item_code")
|
||||||
if not code or not name:
|
if not code or not name:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
store.upsert_product(product_code=code, product_name=name)
|
store.upsert_product(
|
||||||
|
product_code=code,
|
||||||
|
product_name=name,
|
||||||
|
coupang_item_code=(None if cic is None else str(cic)),
|
||||||
|
)
|
||||||
added += 1
|
added += 1
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
@@ -869,6 +878,7 @@ async def product_upsert(
|
|||||||
request: Request,
|
request: Request,
|
||||||
product_code: str = Form(...),
|
product_code: str = Form(...),
|
||||||
product_name: str = Form(...),
|
product_name: str = Form(...),
|
||||||
|
coupang_item_code: str = Form(""),
|
||||||
sort_order: int = Form(0),
|
sort_order: int = Form(0),
|
||||||
user: dict[str, Any] = Depends(_require_user),
|
user: dict[str, Any] = Depends(_require_user),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
@@ -877,7 +887,11 @@ async def product_upsert(
|
|||||||
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
||||||
try:
|
try:
|
||||||
store.upsert_product(
|
store.upsert_product(
|
||||||
product_code=product_code, product_name=product_name, sort_order=sort_order
|
product_code=product_code,
|
||||||
|
product_name=product_name,
|
||||||
|
# 빈 값은 "미입력" 으로 보고 기존 쿠팡상품코드를 유지한다.
|
||||||
|
coupang_item_code=(coupang_item_code.strip() or None),
|
||||||
|
sort_order=sort_order,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
@@ -903,6 +917,23 @@ async def product_set_active(
|
|||||||
return RedirectResponse(url="/cupang/products", status_code=303)
|
return RedirectResponse(url="/cupang/products", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/products/{product_id:int}/toggle")
|
||||||
|
async def product_toggle_active(
|
||||||
|
request: Request,
|
||||||
|
product_id: int,
|
||||||
|
user: dict[str, Any] = Depends(_require_user),
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""상태 배지 클릭 → 활성/비활성 토글(페이지 새로고침 없이)."""
|
||||||
|
store = _store(request)
|
||||||
|
if store is None:
|
||||||
|
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
||||||
|
try:
|
||||||
|
product = store.toggle_product_active(product_id=product_id)
|
||||||
|
except KeyError:
|
||||||
|
raise HTTPException(status_code=404, detail="제품을 찾을 수 없습니다.")
|
||||||
|
return JSONResponse({"ok": True, "active": bool(product["active"])})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/products/{product_id:int}/delete")
|
@router.post("/products/{product_id:int}/delete")
|
||||||
async def product_delete(
|
async def product_delete(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831k" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831k" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831k" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831k" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831k" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831k" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260831k" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
<input class="erp-input" type="text" name="product_name" required placeholder="예: 미라네 12호 세트" /></label>
|
<input class="erp-input" type="text" name="product_name" required placeholder="예: 미라네 12호 세트" /></label>
|
||||||
<label class="erp-field"><span>제품코드 *</span>
|
<label class="erp-field"><span>제품코드 *</span>
|
||||||
<input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1012" /></label>
|
<input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1012" /></label>
|
||||||
|
<label class="erp-field"><span>쿠팡상품코드</span>
|
||||||
|
<input class="erp-input" type="text" name="coupang_item_code" placeholder="예: 1234567890" /></label>
|
||||||
<button type="submit" class="erp-btn erp-btn-primary">추가</button>
|
<button type="submit" class="erp-btn erp-btn-primary">추가</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -57,29 +59,28 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th><button type="button" class="cpg-sort" data-sort-key="name">제품명<span class="cpg-sort-ind" aria-hidden="true"></span></button></th>
|
<th><button type="button" class="cpg-sort" data-sort-key="name">제품명<span class="cpg-sort-ind" aria-hidden="true"></span></button></th>
|
||||||
<th><button type="button" class="cpg-sort" data-sort-key="code">제품코드<span class="cpg-sort-ind" aria-hidden="true"></span></button></th>
|
<th><button type="button" class="cpg-sort" data-sort-key="code">제품코드<span class="cpg-sort-ind" aria-hidden="true"></span></button></th>
|
||||||
|
<th><button type="button" class="cpg-sort" data-sort-key="cpcode">쿠팡상품코드<span class="cpg-sort-ind" aria-hidden="true"></span></button></th>
|
||||||
<th>상태</th>
|
<th>상태</th>
|
||||||
<th>동작</th>
|
<th>동작</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="cpg-prod-tbody">
|
<tbody id="cpg-prod-tbody">
|
||||||
{% for p in products %}
|
{% for p in products %}
|
||||||
<tr data-name="{{ p.product_name }}" data-code="{{ p.product_code }}" {% if not p.active %}style="opacity:.55"{% endif %}>
|
<tr data-name="{{ p.product_name }}" data-code="{{ p.product_code }}"
|
||||||
|
data-cpcode="{{ p.coupang_item_code }}"
|
||||||
|
class="cpg-prod-row{% if not p.active %} is-inactive{% endif %}">
|
||||||
<td>{{ p.product_name }}</td>
|
<td>{{ p.product_name }}</td>
|
||||||
<td>{{ p.product_code }}</td>
|
<td>{{ p.product_code }}</td>
|
||||||
<td>{% if p.active %}<span class="erp-badge erp-badge-success">활성</span>{% else %}<span class="erp-badge erp-badge-neutral">비활성</span>{% endif %}</td>
|
<td class="cpg-cpcode">{% if p.coupang_item_code %}{{ p.coupang_item_code }}{% else %}<span class="erp-muted">—</span>{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
<!-- 배지 자체가 토글 버튼: 클릭 → 활성/비활성 전환 -->
|
||||||
|
<button type="button" class="erp-badge cpg-active-toggle
|
||||||
|
{% if p.active %}erp-badge-success{% else %}erp-badge-neutral{% endif %}"
|
||||||
|
data-toggle-id="{{ p.id }}" aria-pressed="{{ 'true' if p.active else 'false' }}"
|
||||||
|
title="클릭하면 상태가 바뀝니다">{% if p.active %}활성{% else %}비활성{% endif %}</button>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="cpg-row-actions">
|
<div class="cpg-row-actions">
|
||||||
{% if p.active %}
|
|
||||||
<form method="post" action="/cupang/products/{{ p.id }}/active" class="cpg-inline-form">
|
|
||||||
<input type="hidden" name="active" value="0" />
|
|
||||||
<button type="submit" class="erp-btn erp-btn-outline">비활성</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<form method="post" action="/cupang/products/{{ p.id }}/active" class="cpg-inline-form">
|
|
||||||
<input type="hidden" name="active" value="1" />
|
|
||||||
<button type="submit" class="erp-btn erp-btn-outline">활성</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
<form method="post" action="/cupang/products/{{ p.id }}/delete" class="cpg-inline-form"
|
<form method="post" action="/cupang/products/{{ p.id }}/delete" class="cpg-inline-form"
|
||||||
onsubmit="return confirm('완전 삭제합니다. 계속할까요?');">
|
onsubmit="return confirm('완전 삭제합니다. 계속할까요?');">
|
||||||
<button type="submit" class="erp-btn erp-btn-danger">삭제</button>
|
<button type="submit" class="erp-btn erp-btn-danger">삭제</button>
|
||||||
@@ -89,7 +90,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not products %}
|
{% if not products %}
|
||||||
<tr class="cpg-no-sort"><td colspan="4" class="erp-muted">등록된 제품명이 없습니다. 왼쪽에서 선택해 등록하세요.</td></tr>
|
<tr class="cpg-no-sort"><td colspan="5" class="erp-muted">등록된 제품명이 없습니다. 왼쪽에서 선택해 등록하세요.</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -136,7 +137,50 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 상태 배지 클릭 → 활성/비활성 토글 (페이지 새로고침 없음).
|
||||||
|
(function () {
|
||||||
|
var tbody = document.getElementById("cpg-prod-tbody");
|
||||||
|
if (!tbody) return;
|
||||||
|
tbody.addEventListener("click", function (e) {
|
||||||
|
var btn = e.target.closest(".cpg-active-toggle");
|
||||||
|
if (!btn || btn.disabled) return;
|
||||||
|
var id = btn.getAttribute("data-toggle-id");
|
||||||
|
btn.disabled = true;
|
||||||
|
fetch("/cupang/api/products/" + id + "/toggle", { method: "POST" })
|
||||||
|
.then(function (r) { if (!r.ok) throw new Error("fail"); return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
var on = !!(data && data.active);
|
||||||
|
btn.textContent = on ? "활성" : "비활성";
|
||||||
|
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||||
|
btn.classList.toggle("erp-badge-success", on);
|
||||||
|
btn.classList.toggle("erp-badge-neutral", !on);
|
||||||
|
var row = btn.closest("tr");
|
||||||
|
if (row) row.classList.toggle("is-inactive", !on);
|
||||||
|
})
|
||||||
|
.catch(function () { alert("상태 변경 실패"); })
|
||||||
|
.finally(function () { btn.disabled = false; });
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
{% if search_enabled %}
|
{% if search_enabled %}
|
||||||
|
<!-- 선택 등록 팝업 — 선택한 상품마다 쿠팡상품코드를 입력한 뒤 등록 -->
|
||||||
|
<div class="cpg-modal" id="cpg-reg-dlg" hidden>
|
||||||
|
<div class="cpg-modal-back" data-reg-close></div>
|
||||||
|
<div class="cpg-modal-box cpg-reg-box" role="dialog" aria-modal="true" aria-labelledby="cpg-reg-title">
|
||||||
|
<h3 id="cpg-reg-title">쿠팡상품코드 입력</h3>
|
||||||
|
<p class="erp-muted" style="margin:0;font-size:12px;">
|
||||||
|
선택한 상품을 등록합니다. 쿠팡상품코드는 비워두면 나중에 채울 수 있습니다.
|
||||||
|
</p>
|
||||||
|
<div class="cpg-reg-list" id="cpg-reg-list"></div>
|
||||||
|
<div class="cpg-dlg-actions erp-page-actions">
|
||||||
|
<button type="button" class="erp-btn erp-btn-outline" data-reg-close>취소</button>
|
||||||
|
<button type="button" class="erp-btn erp-btn-primary" id="cpg-reg-submit">등록</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="application/json" id="cpg-registered">{{ registered_codes | tojson }}</script>
|
<script type="application/json" id="cpg-registered">{{ registered_codes | tojson }}</script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
@@ -180,18 +224,63 @@
|
|||||||
|
|
||||||
filter.addEventListener("input", render);
|
filter.addEventListener("input", render);
|
||||||
|
|
||||||
|
// ── 선택 등록: 바로 등록하지 않고 쿠팡상품코드 입력 팝업을 먼저 띄운다 ──
|
||||||
|
var regDlg = document.getElementById("cpg-reg-dlg");
|
||||||
|
var regList = document.getElementById("cpg-reg-list");
|
||||||
|
var regSubmit = document.getElementById("cpg-reg-submit");
|
||||||
|
var pending = [];
|
||||||
|
|
||||||
|
function closeRegDlg() {
|
||||||
|
regDlg.hidden = true;
|
||||||
|
regSubmit.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegDlg(items) {
|
||||||
|
pending = items;
|
||||||
|
var html = "";
|
||||||
|
items.forEach(function (it, i) {
|
||||||
|
html += '<label class="erp-field cpg-reg-item">' +
|
||||||
|
'<span>' + esc(it.name) + ' <em class="cpg-reg-code">' + esc(it.code) + '</em></span>' +
|
||||||
|
'<input class="erp-input" type="text" data-reg-idx="' + i + '" ' +
|
||||||
|
'placeholder="쿠팡상품코드" /></label>';
|
||||||
|
});
|
||||||
|
regList.innerHTML = html;
|
||||||
|
regDlg.hidden = false;
|
||||||
|
var first = regList.querySelector("input");
|
||||||
|
if (first) first.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(regDlg.querySelectorAll("[data-reg-close]"), function (el) {
|
||||||
|
el.addEventListener("click", closeRegDlg);
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Escape" && !regDlg.hidden) closeRegDlg();
|
||||||
|
});
|
||||||
|
|
||||||
regBtn.addEventListener("click", function () {
|
regBtn.addEventListener("click", function () {
|
||||||
var items = all.filter(function (it) { return selected[it.code]; })
|
var items = all.filter(function (it) { return selected[it.code]; })
|
||||||
.map(function (it) { return { code: it.code, name: it.name }; });
|
.map(function (it) { return { code: it.code, name: it.name }; });
|
||||||
if (!items.length) { alert("등록할 상품을 선택하세요."); return; }
|
if (!items.length) { alert("등록할 상품을 선택하세요."); return; }
|
||||||
regBtn.disabled = true;
|
openRegDlg(items);
|
||||||
|
});
|
||||||
|
|
||||||
|
regSubmit.addEventListener("click", function () {
|
||||||
|
// 빈 칸은 키 자체를 보내지 않는다 → 이미 등록된 제품의 쿠팡상품코드를 지우지 않음.
|
||||||
|
Array.prototype.forEach.call(regList.querySelectorAll("input[data-reg-idx]"), function (inp) {
|
||||||
|
var idx = parseInt(inp.getAttribute("data-reg-idx"), 10);
|
||||||
|
var val = (inp.value || "").trim();
|
||||||
|
if (!pending[idx]) return;
|
||||||
|
if (val) { pending[idx].coupang_item_code = val; }
|
||||||
|
else { delete pending[idx].coupang_item_code; }
|
||||||
|
});
|
||||||
|
regSubmit.disabled = true;
|
||||||
fetch("/cupang/products/bulk", {
|
fetch("/cupang/products/bulk", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ items: items })
|
body: JSON.stringify({ items: pending })
|
||||||
}).then(function (r) { return r.json(); })
|
}).then(function (r) { if (!r.ok) throw new Error("fail"); return r.json(); })
|
||||||
.then(function () { location.reload(); })
|
.then(function () { location.reload(); })
|
||||||
.catch(function () { regBtn.disabled = false; alert("등록 실패"); });
|
.catch(function () { regSubmit.disabled = false; alert("등록 실패"); });
|
||||||
});
|
});
|
||||||
|
|
||||||
fetch("/cupang/api/products/all")
|
fetch("/cupang/api/products/all")
|
||||||
|
|||||||
@@ -1055,3 +1055,33 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
|
|||||||
60% { opacity: .95; transform: rotate(-12deg) scale(.92); }
|
60% { opacity: .95; transform: rotate(-12deg) scale(.92); }
|
||||||
100% { opacity: .9; transform: rotate(-12deg) scale(1); }
|
100% { opacity: .9; transform: rotate(-12deg) scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 설정(제품명) — 상태 토글 배지 / 쿠팡상품코드 / 선택 등록 팝업 ── */
|
||||||
|
.cpg-active-toggle {
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.cpg-active-toggle:hover { filter: brightness(.94); }
|
||||||
|
.cpg-active-toggle:disabled { opacity: .6; cursor: progress; }
|
||||||
|
.cpg-prod-row.is-inactive { opacity: .55; }
|
||||||
|
.cpg-cpcode { font-family: var(--font-geist-mono); font-size: 13px; }
|
||||||
|
|
||||||
|
.cpg-reg-box { width: min(520px, calc(100vw - 32px)); }
|
||||||
|
.cpg-reg-list {
|
||||||
|
max-height: min(48vh, 380px);
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.cpg-reg-item > span { font-size: 13px; }
|
||||||
|
.cpg-reg-code {
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--color-midtone-gray);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- cupang_db 마이그레이션 004 — 제품명 카탈로그에 쿠팡상품코드 추가
|
||||||
|
-- =====================================================================
|
||||||
|
-- 사유: 설정(제품명) 화면에서 미라네 제품코드와 별개로 쿠팡 쪽 상품코드를
|
||||||
|
-- 같이 보관·표시하기 위함. 등록 시(수기 추가 / 선택 등록 팝업) 입력.
|
||||||
|
--
|
||||||
|
-- 멱등: ADD COLUMN IF NOT EXISTS. 기존 행은 빈 문자열('')로 채워진다.
|
||||||
|
-- DROP/TRUNCATE 없음, 데이터 삭제 없음.
|
||||||
|
--
|
||||||
|
-- 실행:
|
||||||
|
-- docker exec -i postgres-db psql -U postgres -d cupang_db \
|
||||||
|
-- < /opt/www/main/scripts/sql/cupang_db_004_product_coupang_item_code.sql
|
||||||
|
--
|
||||||
|
-- 확인:
|
||||||
|
-- docker exec -i postgres-db psql -U postgres -d cupang_db \
|
||||||
|
-- -c "\d cupang_products"
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
\connect cupang_db
|
||||||
|
|
||||||
|
ALTER TABLE cupang_products
|
||||||
|
ADD COLUMN IF NOT EXISTS coupang_item_code TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cupang_products_coupang_code
|
||||||
|
ON cupang_products (coupang_item_code);
|
||||||
|
|
||||||
|
SELECT 'cupang_db 004 done' AS status;
|
||||||
@@ -99,12 +99,14 @@ CREATE TABLE IF NOT EXISTS cupang_products (
|
|||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
product_code TEXT NOT NULL UNIQUE,
|
product_code TEXT NOT NULL UNIQUE,
|
||||||
product_name TEXT NOT NULL,
|
product_name TEXT NOT NULL,
|
||||||
|
coupang_item_code TEXT NOT NULL DEFAULT '',
|
||||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_cupang_products_name ON cupang_products (product_name);
|
CREATE INDEX IF NOT EXISTS idx_cupang_products_name ON cupang_products (product_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cupang_products_coupang_code ON cupang_products (coupang_item_code);
|
||||||
|
|
||||||
DROP TRIGGER IF EXISTS trg_cupang_products_updated ON cupang_products;
|
DROP TRIGGER IF EXISTS trg_cupang_products_updated ON cupang_products;
|
||||||
CREATE TRIGGER trg_cupang_products_updated
|
CREATE TRIGGER trg_cupang_products_updated
|
||||||
|
|||||||
Reference in New Issue
Block a user