feat(cupang): 제품명 카탈로그 설정 + 달력 공휴일 색상 + 폼 UI 개선

- 설정 화면 추가: /cupang/products — itemcode_db 검색 등록 + 수동 등록.
  제품명 드롭다운 소스. cupang_products 테이블 신규(init.sql 멱등 재실행 필요)
- 폼 품목라인: 제품명 드롭다운 선택 → 제품코드 자동 입력
- 라인 컬럼 순서 제품명·제품코드·수량·입수량·박스계산·메모
- 제품코드 140px, 수량·입수량 60px 폭 고정
- 공통 헤더 입력란 겹침 수정(min-width:0, width:100%, box-sizing)
- 달력: 일/공휴일 빨강, 토 파랑, 글자 크기 확대. 한국 공휴일 모듈(holidays.py)
- document_no null → "None" 출력 버그 수정

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 03:24:05 +09:00
parent 9e707be5ac
commit 34c7e2f964
9 changed files with 457 additions and 106 deletions
@@ -55,7 +55,7 @@
<label class="erp-field"><span>문서번호(선택)</span>
<input class="erp-input" type="text" name="document_no"
value="{{ shipment.document_no if shipment else '' }}" /></label>
value="{{ shipment.document_no or '' if shipment else '' }}" /></label>
</div>
<label class="erp-field cpg-full"><span>출고/박스 요약 (수동 보정 메모)</span>
@@ -71,8 +71,8 @@
<div class="cpg-card-head">
<h2>품목 라인</h2>
<span class="erp-muted">
{% if search_enabled %}제품코드 검색 → 제품명 자동 채움.{% else %}상품 검색 비활성(수동 입력).{% endif %}
수량 입력 시 박스 수 자동 계산.
제품명 선택 시 제품코드 자동 입력. 수량 입력 시 박스 수 자동 계산.
{% if not products %}<a href="/cupang/products">설정에서 제품명 먼저 등록</a>{% endif %}
</span>
</div>
@@ -80,7 +80,7 @@
<table class="erp-table cpg-lines">
<thead>
<tr>
<th>#</th><th>제품코드</th><th>제품</th><th>수량</th>
<th>#</th><th>제품</th><th>제품코드</th><th>수량</th>
<th>입수량</th><th>박스 계산</th><th>라인메모</th><th></th>
</tr>
</thead>
@@ -110,6 +110,9 @@
<script type="application/json" id="cpg-box-rules">
{{ box_rules | tojson }}
</script>
<script type="application/json" id="cpg-products">
{{ products | tojson }}
</script>
<script>
window.CPG = {
searchEnabled: {{ 'true' if search_enabled else 'false' }},
@@ -8,6 +8,7 @@
<!-- 페이지 액션 -->
<div class="erp-page-actions cpg-actions">
<a class="erp-btn erp-btn-primary" href="/cupang/new">+ 신규 등록</a>
<a class="erp-btn erp-btn-outline" href="/cupang/products">설정(제품명)</a>
<a class="erp-btn erp-btn-outline" href="/cupang/centers">입고센터 관리</a>
<a class="erp-btn erp-btn-outline" href="/cupang/box-rules">박스 입수량</a>
<a class="erp-btn erp-btn-outline" href="/cupang/export?year={{ year }}&month={{ month }}">엑셀({{ month }}월)</a>
@@ -34,7 +35,8 @@
<a class="cpg-cal-cell
{% if not cell.in_month %}cpg-out{% endif %}
{% if cell.is_today %}cpg-today{% endif %}
{% if cell.is_selected %}cpg-selected{% endif %}"
{% if cell.is_selected %}cpg-selected{% endif %}
{% if cell.is_sunday or cell.is_holiday %}cpg-red{% elif cell.is_saturday %}cpg-blue{% endif %}"
href="/cupang/?year={{ year }}&month={{ month }}&date={{ cell.date }}">
<span class="cpg-cal-day">{{ cell.day }}</span>
<span class="cpg-cal-badges">
@@ -0,0 +1,111 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css" />{% endblock %}
{% block content %}
<section class="cpg">
<div class="erp-page-actions">
<a class="erp-btn erp-btn-outline" href="/cupang/">← 달력</a>
</div>
<!-- itemcode_db 검색 → 등록 -->
<div class="erp-card cpg-form-card">
<div class="cpg-card-head">
<h2>itemcode_db 에서 제품 검색·등록</h2>
<span class="erp-muted">
{% if search_enabled %}제품코드/제품명으로 검색 후 "등록".{% else %}
상품 검색 비활성: {{ search_reason }} — 아래에서 수동 등록하세요.{% endif %}
</span>
</div>
{% if search_enabled %}
<div class="cpg-inline-form">
<input class="erp-input" type="text" id="cpg-prod-q" placeholder="제품코드 또는 제품명" style="min-width:240px" />
<button type="button" class="erp-btn erp-btn-outline" id="cpg-prod-search">검색</button>
</div>
<div id="cpg-prod-results" class="cpg-prod-results"></div>
{% endif %}
</div>
<!-- 수동 등록 -->
<div class="erp-card cpg-form-card">
<div class="cpg-card-head"><h2>제품명 수동 등록 / 수정</h2>
<span class="erp-muted">같은 제품코드는 덮어씁니다.</span></div>
<form method="post" action="/cupang/products" class="cpg-rule-grid">
<label class="erp-field"><span>제품코드 *</span>
<input class="erp-input" type="text" name="product_code" required placeholder="예: MS-1001" /></label>
<label class="erp-field"><span>제품명 *</span>
<input class="erp-input" type="text" name="product_name" required placeholder="예: 미라네 1호세트" /></label>
<label class="erp-field"><span>정렬</span>
<input class="erp-input" type="number" name="sort_order" value="0" /></label>
<div><button type="submit" class="erp-btn erp-btn-primary">등록</button></div>
</form>
</div>
<!-- 등록된 제품명 목록 -->
<div class="erp-card cpg-form-card">
<div class="cpg-card-head"><h2>등록된 제품명 ({{ products|length }})</h2>
<span class="erp-muted">폼의 제품명 드롭다운에 노출됩니다.</span></div>
<div class="erp-table-wrap">
<table class="erp-table">
<thead><tr><th>제품명</th><th>제품코드</th><th>정렬</th><th>상태</th><th></th></tr></thead>
<tbody>
{% for p in products %}
<tr {% if not p.active %}style="opacity:.55"{% endif %}>
<td>{{ p.product_name }}</td>
<td>{{ p.product_code }}</td>
<td>{{ p.sort_order }}</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>
{% if p.active %}
<form method="post" action="/cupang/products/{{ p.id }}/delete" class="cpg-inline-form"
onsubmit="return confirm('비활성화합니다. 계속할까요?');">
<button type="submit" class="erp-btn erp-btn-danger">비활성화</button>
</form>
{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
{% if search_enabled %}
<script>
(function () {
var q = document.getElementById("cpg-prod-q");
var btn = document.getElementById("cpg-prod-search");
var box = document.getElementById("cpg-prod-results");
function esc(s){ var d=document.createElement("div"); d.textContent=s||""; return d.innerHTML; }
function run() {
var term = (q.value || "").trim();
if (!term) { box.innerHTML = ""; return; }
box.innerHTML = '<p class="erp-muted">검색중…</p>';
fetch("/cupang/api/products/search?q=" + encodeURIComponent(term))
.then(function (r) { return r.json(); })
.then(function (data) {
var rows = (data && data.results) || [];
if (!rows.length) { box.innerHTML = '<p class="erp-muted">결과 없음</p>'; return; }
var html = '<table class="erp-table"><thead><tr><th>제품코드</th><th>제품명</th><th>구분</th><th></th></tr></thead><tbody>';
rows.forEach(function (it) {
html += '<tr><td>' + esc(it.code) + '</td><td>' + esc(it.name) + '</td><td>' + esc(it.type) + '</td>' +
'<td><form method="post" action="/cupang/products" class="cpg-inline-form">' +
'<input type="hidden" name="product_code" value="' + esc(it.code) + '">' +
'<input type="hidden" name="product_name" value="' + esc(it.name) + '">' +
'<button type="submit" class="erp-btn erp-btn-outline">등록</button></form></td></tr>';
});
html += "</tbody></table>";
box.innerHTML = html;
})
.catch(function () { box.innerHTML = '<p class="erp-muted">검색 실패</p>'; });
}
btn.addEventListener("click", run);
q.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); run(); } });
})();
</script>
{% endif %}
{% endblock %}