feat(cupang): 박스 계산 요약 — 자투리 혼합 박스 시각화
하단 "박스명별 합계" 표를 요약 카드로 교체. - KPI 3개: 총 박스 / 제품별 박스 / 혼합 박스(자투리 N개) - 제품별 카드: 몇 박스 + 자투리 몇 개(딱 맞으면 "딱 맞음") - 자투리 혼합 박스: 박스 1개당 카드로 어떤 상품이 몇 개 들어가는지 표시. 상자 SVG(뚜껑 열림·내용물 차오름), 채움률 바, 카드 등장 애니메이션. prefers-reduced-motion 에서는 애니메이션 비활성. 혼합 계산은 서버 _pack_leftovers: 박스 용량을 1 로 두고 제품 1개 = 1/units_per_box 부피로 환산, 같은 박스명끼리만 채운다. 부동소수 오차를 피하려고 Fraction 사용. 한 제품 자투리가 두 박스로 나뉘는 것은 허용해 박스 수가 최소가 되게 한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import calendar as _calendar
|
import calendar as _calendar
|
||||||
import json
|
import json
|
||||||
|
from fractions import Fraction
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from app.timezone import today_kst
|
from app.timezone import today_kst
|
||||||
@@ -656,14 +657,75 @@ async def box_calc_api(
|
|||||||
agg["required_boxes"] += calc["required_boxes"]
|
agg["required_boxes"] += calc["required_boxes"]
|
||||||
agg["remainder_units"] += calc["remainder_units"]
|
agg["remainder_units"] += calc["remainder_units"]
|
||||||
|
|
||||||
|
mixes = _pack_leftovers(results)
|
||||||
|
grand_total = sum(r["full_boxes"] or 0 for r in results if r["configured"])
|
||||||
|
grand_total += sum(m["box_count"] for m in mixes)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{
|
{
|
||||||
"results": results,
|
"results": results,
|
||||||
"totals": sorted(totals.values(), key=lambda t: t["box_name"]),
|
"totals": sorted(totals.values(), key=lambda t: t["box_name"]),
|
||||||
|
"mixes": mixes,
|
||||||
|
"grand_total_boxes": grand_total,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
"""제품별 자투리(remainder_units)를 같은 박스명끼리 모아 혼합 박스에 담는다.
|
||||||
|
|
||||||
|
- 한 박스의 용량을 1 로 두고, 제품 1개가 차지하는 부피를 1/units_per_box 로 본다.
|
||||||
|
(예: 쿠팡 2호 = 8개들이 → 1개 = 1/8 박스)
|
||||||
|
- 같은 박스명끼리만 섞는다. 서로 다른 입수량이 섞여도 부피 합으로 정확히 계산된다.
|
||||||
|
- 한 제품의 자투리가 두 박스에 나뉘어 담기는 것은 허용(그래야 박스 수가 최소).
|
||||||
|
- 오차 없이 계산하려고 float 대신 Fraction 을 쓴다.
|
||||||
|
"""
|
||||||
|
groups: dict[str, list[dict[str, Any]]] = {}
|
||||||
|
for r in results:
|
||||||
|
if not r["configured"] or not r["remainder_units"]:
|
||||||
|
continue
|
||||||
|
groups.setdefault(r["box_name"], []).append(r)
|
||||||
|
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for box_name in sorted(groups):
|
||||||
|
# 자투리가 많은 제품부터 담아 박스 안 품목 수를 줄인다.
|
||||||
|
items = sorted(groups[box_name], key=lambda r: -r["remainder_units"])
|
||||||
|
boxes: list[dict[str, Any]] = []
|
||||||
|
cur: list[dict[str, Any]] = []
|
||||||
|
free = Fraction(1)
|
||||||
|
|
||||||
|
for r in items:
|
||||||
|
unit = Fraction(1, int(r["units_per_box"]))
|
||||||
|
left = int(r["remainder_units"])
|
||||||
|
while left > 0:
|
||||||
|
take = min(left, int(free / unit))
|
||||||
|
if take == 0: # 남은 자리 없음 → 새 박스
|
||||||
|
boxes.append({"items": cur, "fill_percent": float(round((1 - free) * 100, 1))})
|
||||||
|
cur, free = [], Fraction(1)
|
||||||
|
continue
|
||||||
|
cur.append(
|
||||||
|
{
|
||||||
|
"product_code": r["product_code"],
|
||||||
|
"product_name": r["product_name"],
|
||||||
|
"quantity": take,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
free -= unit * take
|
||||||
|
left -= take
|
||||||
|
if cur:
|
||||||
|
boxes.append({"items": cur, "fill_percent": float(round((1 - free) * 100, 1))})
|
||||||
|
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"box_name": box_name,
|
||||||
|
"box_count": len(boxes),
|
||||||
|
"leftover_units": sum(int(r["remainder_units"]) for r in items),
|
||||||
|
"boxes": boxes,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 설정 — 제품명 카탈로그 관리 (itemcode_db 에서 등록)
|
# 설정 — 제품명 카탈로그 관리 (itemcode_db 에서 등록)
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828s" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828t" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -53,22 +53,17 @@
|
|||||||
|
|
||||||
<div class="erp-card cpg-form-card cpg-calc-sum" id="cpg-calc-sum-card" hidden>
|
<div class="erp-card cpg-form-card cpg-calc-sum" id="cpg-calc-sum-card" hidden>
|
||||||
<div class="cpg-card-head">
|
<div class="cpg-card-head">
|
||||||
<h2>박스명별 합계</h2>
|
<h2>박스 요약</h2>
|
||||||
<span class="erp-muted">가득 채운 박스 / 낱개 포함 총 필요 박스</span>
|
<span class="erp-muted">제품별 박스 + 자투리를 모아 담은 혼합 박스</span>
|
||||||
</div>
|
|
||||||
<div class="erp-table-wrap">
|
|
||||||
<table class="erp-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>박스명</th>
|
|
||||||
<th class="cpg-calc-num">박스 수</th>
|
|
||||||
<th class="cpg-calc-num">남은 낱개 합</th>
|
|
||||||
<th class="cpg-calc-num">총 필요 박스</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="cpg-calc-sum-rows"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="cpg-sum-kpis" id="cpg-sum-kpis"></div>
|
||||||
|
|
||||||
|
<h3 class="cpg-sum-h3">제품별 박스</h3>
|
||||||
|
<div class="cpg-sum-prods" id="cpg-sum-prods"></div>
|
||||||
|
|
||||||
|
<h3 class="cpg-sum-h3">자투리 혼합 박스 <span class="erp-muted" id="cpg-sum-mix-note"></span></h3>
|
||||||
|
<div class="cpg-sum-boxes" id="cpg-sum-boxes"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="application/json" id="cpg-calc-rules">{{ box_rules | tojson }}</script>
|
<script type="application/json" id="cpg-calc-rules">{{ box_rules | tojson }}</script>
|
||||||
@@ -83,7 +78,10 @@
|
|||||||
var resetBtn = document.getElementById("cpg-calc-reset");
|
var resetBtn = document.getElementById("cpg-calc-reset");
|
||||||
var msg = document.getElementById("cpg-calc-msg");
|
var msg = document.getElementById("cpg-calc-msg");
|
||||||
var sumCard = document.getElementById("cpg-calc-sum-card");
|
var sumCard = document.getElementById("cpg-calc-sum-card");
|
||||||
var sumRows = document.getElementById("cpg-calc-sum-rows");
|
var sumKpis = document.getElementById("cpg-sum-kpis");
|
||||||
|
var sumProds = document.getElementById("cpg-sum-prods");
|
||||||
|
var sumBoxes = document.getElementById("cpg-sum-boxes");
|
||||||
|
var sumMixNote = document.getElementById("cpg-sum-mix-note");
|
||||||
|
|
||||||
var rules = [];
|
var rules = [];
|
||||||
try { rules = JSON.parse(document.getElementById("cpg-calc-rules").textContent || "[]"); } catch (e) {}
|
try { rules = JSON.parse(document.getElementById("cpg-calc-rules").textContent || "[]"); } catch (e) {}
|
||||||
@@ -124,7 +122,10 @@
|
|||||||
tr.classList.remove("cpg-calc-warn");
|
tr.classList.remove("cpg-calc-warn");
|
||||||
});
|
});
|
||||||
sumCard.hidden = true;
|
sumCard.hidden = true;
|
||||||
sumRows.innerHTML = "";
|
sumKpis.innerHTML = "";
|
||||||
|
sumProds.innerHTML = "";
|
||||||
|
sumBoxes.innerHTML = "";
|
||||||
|
sumMixNote.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function collect() {
|
function collect() {
|
||||||
@@ -167,26 +168,95 @@
|
|||||||
tr.classList.add("cpg-calc-warn");
|
tr.classList.add("cpg-calc-warn");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var totals = (data && data.totals) || [];
|
renderSummary(results, (data && data.mixes) || [], (data && data.grand_total_boxes) || 0);
|
||||||
if (totals.length) {
|
|
||||||
var html = "";
|
|
||||||
totals.forEach(function (t) {
|
|
||||||
html += "<tr><td>" + esc(t.box_name || "—") + "</td>" +
|
|
||||||
'<td class="cpg-calc-num">' + t.full_boxes + "박스</td>" +
|
|
||||||
'<td class="cpg-calc-num">' + t.remainder_units + "개</td>" +
|
|
||||||
'<td class="cpg-calc-num">' + t.required_boxes + "박스</td></tr>";
|
|
||||||
});
|
|
||||||
sumRows.innerHTML = html;
|
|
||||||
sumCard.hidden = false;
|
|
||||||
} else {
|
|
||||||
sumCard.hidden = true;
|
|
||||||
}
|
|
||||||
msg.textContent = "계산 완료 (" + results.length + "건)";
|
msg.textContent = "계산 완료 (" + results.length + "건)";
|
||||||
})
|
})
|
||||||
.catch(function () { msg.textContent = "계산 실패 — 새로고침 후 다시 시도하세요."; })
|
.catch(function () { msg.textContent = "계산 실패 — 새로고침 후 다시 시도하세요."; })
|
||||||
.then(function () { runBtn.disabled = false; });
|
.then(function () { runBtn.disabled = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 요약 렌더 ─────────────────────────────────────────
|
||||||
|
// 상자 그림: fill(0~100) 만큼 안쪽이 차오르고, 뚜껑이 살짝 열린 채 표시.
|
||||||
|
function boxSvg(fill) {
|
||||||
|
var f = Math.max(0, Math.min(100, fill || 0));
|
||||||
|
var h = 40 * f / 100;
|
||||||
|
return '' +
|
||||||
|
'<svg class="cpg-box-svg" viewBox="0 0 72 64" aria-hidden="true">' +
|
||||||
|
'<rect class="cpg-box-fill" x="10" y="' + (54 - h) + '" width="52" height="' + h + '" rx="2" />' +
|
||||||
|
'<path class="cpg-box-body" d="M10 20 H62 V54 H10 Z" />' +
|
||||||
|
'<path class="cpg-box-flapL" d="M10 20 L2 8 H30 L36 20 Z" />' +
|
||||||
|
'<path class="cpg-box-flapR" d="M62 20 L70 8 H42 L36 20 Z" />' +
|
||||||
|
'<line class="cpg-box-seam" x1="36" y1="20" x2="36" y2="54" />' +
|
||||||
|
'</svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(results, mixes, grandTotal) {
|
||||||
|
var ok = results.filter(function (r) { return r.configured; });
|
||||||
|
if (!ok.length) { sumCard.hidden = true; return; }
|
||||||
|
|
||||||
|
var fullSum = 0, mixSum = 0, leftoverSum = 0;
|
||||||
|
ok.forEach(function (r) { fullSum += r.full_boxes; leftoverSum += r.remainder_units; });
|
||||||
|
mixes.forEach(function (m) { mixSum += m.box_count; });
|
||||||
|
|
||||||
|
sumKpis.innerHTML =
|
||||||
|
kpi("총 박스", (grandTotal || (fullSum + mixSum)) + "박스", "제품별 박스 + 혼합 박스") +
|
||||||
|
kpi("제품별 박스", fullSum + "박스", "한 제품만 가득 채운 박스") +
|
||||||
|
kpi("혼합 박스", mixSum + "박스", "자투리 " + leftoverSum + "개를 모아 담음");
|
||||||
|
|
||||||
|
// 제품별
|
||||||
|
var ph = "";
|
||||||
|
ok.forEach(function (r) {
|
||||||
|
ph += '<div class="cpg-sum-prod">' +
|
||||||
|
'<div class="cpg-sum-prod-head">' +
|
||||||
|
'<span class="cpg-sum-prod-name">' + esc(r.product_name) + "</span>" +
|
||||||
|
'<span class="erp-badge erp-badge-neutral">' + esc(r.box_name) + "</span>" +
|
||||||
|
"</div>" +
|
||||||
|
'<div class="cpg-sum-prod-nums">' +
|
||||||
|
'<strong>' + r.full_boxes + "박스</strong>" +
|
||||||
|
'<span class="erp-muted">' + r.quantity + "개 ÷ " + r.units_per_box + "개</span>" +
|
||||||
|
(r.remainder_units
|
||||||
|
? '<span class="cpg-sum-left">자투리 ' + r.remainder_units + "개</span>"
|
||||||
|
: '<span class="cpg-sum-exact">딱 맞음</span>') +
|
||||||
|
"</div>" +
|
||||||
|
"</div>";
|
||||||
|
});
|
||||||
|
sumProds.innerHTML = ph;
|
||||||
|
|
||||||
|
// 자투리 혼합 박스
|
||||||
|
if (!mixes.length) {
|
||||||
|
sumMixNote.textContent = "— 자투리 없음";
|
||||||
|
sumBoxes.innerHTML = '<p class="erp-muted">남는 낱개가 없습니다. 모든 박스가 딱 맞습니다.</p>';
|
||||||
|
} else {
|
||||||
|
sumMixNote.textContent = "— 같은 박스명끼리만 섞습니다";
|
||||||
|
var bh = "", n = 0;
|
||||||
|
mixes.forEach(function (m) {
|
||||||
|
m.boxes.forEach(function (b, i) {
|
||||||
|
var items = b.items.map(function (it) {
|
||||||
|
return '<li><span>' + esc(it.product_name) + "</span><b>" + it.quantity + "개</b></li>";
|
||||||
|
}).join("");
|
||||||
|
bh += '<div class="cpg-box-card" style="animation-delay:' + (n * 70) + 'ms">' +
|
||||||
|
'<div class="cpg-box-art">' + boxSvg(b.fill_percent) + "</div>" +
|
||||||
|
'<div class="cpg-box-info">' +
|
||||||
|
'<div class="cpg-box-title">' + esc(m.box_name) + " 혼합 #" + (i + 1) + "</div>" +
|
||||||
|
'<ul class="cpg-box-items">' + items + "</ul>" +
|
||||||
|
'<div class="cpg-box-bar"><span style="width:' + b.fill_percent + '%"></span></div>' +
|
||||||
|
'<div class="cpg-box-fillnum">' + b.fill_percent + "% 채움</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"</div>";
|
||||||
|
n += 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
sumBoxes.innerHTML = bh;
|
||||||
|
}
|
||||||
|
sumCard.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function kpi(label, value, hint) {
|
||||||
|
return '<div class="cpg-kpi"><span class="cpg-kpi-label">' + esc(label) + "</span>" +
|
||||||
|
'<strong class="cpg-kpi-value">' + esc(value) + "</strong>" +
|
||||||
|
'<span class="cpg-kpi-hint">' + esc(hint) + "</span></div>";
|
||||||
|
}
|
||||||
|
|
||||||
tbody.addEventListener("click", function (e) {
|
tbody.addEventListener("click", function (e) {
|
||||||
var del = e.target.closest(".cpg-calc-del");
|
var del = e.target.closest(".cpg-calc-del");
|
||||||
if (!del) return;
|
if (!del) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828s" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828t" />{% 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=20260828s" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828t" />{% 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=20260828s" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828t" />{% 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=20260828s" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828t" />{% 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=20260828s" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828t" />{% 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=20260828s" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828t" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -335,3 +335,157 @@
|
|||||||
.cpg-calc-row.cpg-calc-warn { background: color-mix(in srgb, var(--color-callout-red) 8%, transparent); }
|
.cpg-calc-row.cpg-calc-warn { background: color-mix(in srgb, var(--color-callout-red) 8%, transparent); }
|
||||||
.cpg-calc-actions { margin-top: 12px; gap: 8px; align-items: center; }
|
.cpg-calc-actions { margin-top: 12px; gap: 8px; align-items: center; }
|
||||||
.cpg-calc-sum { margin-top: 16px; }
|
.cpg-calc-sum { margin-top: 16px; }
|
||||||
|
|
||||||
|
/* ── 박스 계산 요약 ─────────────────────────────── */
|
||||||
|
.cpg-sum-h3 {
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.cpg-sum-h3 .erp-muted { font-weight: 400; font-size: 13px; }
|
||||||
|
|
||||||
|
.cpg-sum-kpis {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.cpg-kpi {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--color-subtle-ash);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-ghost-gray);
|
||||||
|
}
|
||||||
|
.cpg-kpi-label { font-size: 12px; color: var(--color-midtone-gray); }
|
||||||
|
.cpg-kpi-value { font-size: 24px; line-height: 1.2; }
|
||||||
|
.cpg-kpi-hint { font-size: 12px; color: var(--color-midtone-gray); }
|
||||||
|
|
||||||
|
.cpg-sum-prods {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.cpg-sum-prod {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--color-subtle-ash);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.cpg-sum-prod-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.cpg-sum-prod-name { font-weight: 600; }
|
||||||
|
.cpg-sum-prod-nums {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.cpg-sum-prod-nums strong { font-size: 17px; }
|
||||||
|
.cpg-sum-left { color: var(--color-callout-red); font-weight: 600; }
|
||||||
|
.cpg-sum-exact { color: var(--color-success-green); font-weight: 600; }
|
||||||
|
|
||||||
|
/* 혼합 박스 카드 */
|
||||||
|
.cpg-sum-boxes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.cpg-box-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--color-subtle-ash);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--color-canvas-white);
|
||||||
|
opacity: 0;
|
||||||
|
animation: cpg-box-in .38s ease-out forwards;
|
||||||
|
}
|
||||||
|
@keyframes cpg-box-in {
|
||||||
|
from { opacity: 0; transform: translateY(10px) scale(.96); }
|
||||||
|
to { opacity: 1; transform: none; }
|
||||||
|
}
|
||||||
|
.cpg-box-art { flex: 0 0 auto; }
|
||||||
|
.cpg-box-svg { width: 72px; height: 64px; }
|
||||||
|
.cpg-box-body {
|
||||||
|
fill: #f6e2c3;
|
||||||
|
stroke: #b98a4a;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.cpg-box-flapL, .cpg-box-flapR {
|
||||||
|
fill: #e8cfa6;
|
||||||
|
stroke: #b98a4a;
|
||||||
|
stroke-width: 2;
|
||||||
|
transform-origin: 36px 20px;
|
||||||
|
animation: cpg-flap .5s ease-out both;
|
||||||
|
}
|
||||||
|
.cpg-box-flapL { animation-delay: .12s; }
|
||||||
|
.cpg-box-flapR { animation-delay: .2s; }
|
||||||
|
@keyframes cpg-flap {
|
||||||
|
from { transform: rotate(0deg) translateY(6px); opacity: .3; }
|
||||||
|
to { transform: none; opacity: 1; }
|
||||||
|
}
|
||||||
|
.cpg-box-fill {
|
||||||
|
fill: var(--color-rich-black);
|
||||||
|
opacity: .16;
|
||||||
|
animation: cpg-fill-up .55s ease-out both;
|
||||||
|
transform-origin: 36px 54px;
|
||||||
|
}
|
||||||
|
@keyframes cpg-fill-up {
|
||||||
|
from { transform: scaleY(0); }
|
||||||
|
to { transform: scaleY(1); }
|
||||||
|
}
|
||||||
|
.cpg-box-seam { stroke: #b98a4a; stroke-width: 1.5; opacity: .5; }
|
||||||
|
|
||||||
|
.cpg-box-info { min-width: 0; flex: 1 1 auto; }
|
||||||
|
.cpg-box-title { font-weight: 700; margin-bottom: 6px; }
|
||||||
|
.cpg-box-items {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.cpg-box-items li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 2px 0;
|
||||||
|
border-bottom: 1px dashed var(--color-subtle-ash);
|
||||||
|
}
|
||||||
|
.cpg-box-items li:last-child { border-bottom: 0; }
|
||||||
|
.cpg-box-bar {
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--color-ghost-gray);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cpg-box-bar span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-rich-black);
|
||||||
|
animation: cpg-bar-grow .6s ease-out both;
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
@keyframes cpg-bar-grow {
|
||||||
|
from { transform: scaleX(0); }
|
||||||
|
to { transform: scaleX(1); }
|
||||||
|
}
|
||||||
|
.cpg-box-fillnum {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-midtone-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.cpg-box-card, .cpg-box-fill, .cpg-box-flapL, .cpg-box-flapR, .cpg-box-bar span {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.cpg-box-fill { opacity: .16; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user