feat(dashboard+orders): 카드별 정확한 필터 전달 + 출고요청 카드 컴팩트화
Deploy momo-erp / deploy (push) Failing after 35s

[대시보드 → 출고처리 카드 필터]
- 승인 대기 / 진행중 / 미수금 → ?status=...&dateFrom=&dateTo= (전체 기간, 빈 날짜)
- 오늘 발주 → ?dateFrom=오늘&dateTo=오늘
- 이번달 매출/누적 → ?dateFrom=이번달1일&dateTo=오늘
- orders 페이지: 쿼리에 dateFrom/dateTo 키가 있으면(빈값 포함) 그 값 사용,
  키가 아예 없을 때만 기본값 오늘. 사용자 모드 페이지도 동일

[출고 요청 카드 그리드]
- grid-cols-3 / md-4 / lg-5 — PC 5개·모바일 3개/줄
- 카드 padding p-3~p-4 → p-2, 폰트/버튼/이미지 라벨 모두 컴팩트
- IS_TAX_FREE/REQUIRES_DELIVERY 배지를 이미지 위 좌상단으로 이동해 공간 절약
- 품목명 line-clamp-2 + min-h-[2em] 로 카드 높이 일정화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-08 15:45:49 +09:00
parent ebabd726f3
commit 2d40678358
4 changed files with 80 additions and 60 deletions
+16 -16
View File
@@ -56,22 +56,22 @@ const todayStr = () => {
export default function AdminOrdersPage() {
const [orders, setOrders] = useState<Order[]>([]);
const [status, setStatus] = useState(() => {
if (typeof window === "undefined") return "";
return new URLSearchParams(window.location.search).get("status") ?? "";
});
// 기본 기간: 오늘 ~ 오늘. 단 ?status= 으로 들어온 경우(대시보드 카드) 는 30일 범위로 넓힘
const [dateFrom, setDateFrom] = useState(() => {
if (typeof window === "undefined") return todayStr();
const hasStatus = !!new URLSearchParams(window.location.search).get("status");
if (hasStatus) {
const d = new Date(); d.setDate(d.getDate() - 30);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
return todayStr();
});
const [dateTo, setDateTo] = useState(todayStr);
const [keyword, setKeyword] = useState("");
// URL 쿼리 우선: dateFrom/dateTo 키가 있으면 그 값 사용 (빈 문자열도 명시적 = 전체 기간).
// 키가 없을 때만 기본값(오늘 ~ 오늘)
const initial = (() => {
if (typeof window === "undefined") return { status: "", dateFrom: todayStr(), dateTo: todayStr(), keyword: "" };
const q = new URLSearchParams(window.location.search);
return {
status: q.get("status") ?? "",
dateFrom: q.has("dateFrom") ? (q.get("dateFrom") ?? "") : todayStr(),
dateTo: q.has("dateTo") ? (q.get("dateTo") ?? "") : todayStr(),
keyword: q.get("keyword") ?? "",
};
})();
const [status, setStatus] = useState(initial.status);
const [dateFrom, setDateFrom] = useState(initial.dateFrom);
const [dateTo, setDateTo] = useState(initial.dateTo);
const [keyword, setKeyword] = useState(initial.keyword);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [activeId, setActiveId] = useState<string>("");
const [detail, setDetail] = useState<{ order: DetailOrder; items: DetailLine[]; supplier: Supplier } | null>(null);
+19 -8
View File
@@ -13,6 +13,9 @@ type DashboardData =
pending: Array<{ OBJID: string; ORDER_NO: string; COMPANY_NAME: string; ORDER_DATE: string; TOTAL_AMOUNT: number }>; };
const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
const pad = (n: number) => String(n).padStart(2, "0");
const todayISO = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
const firstOfMonthISO = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-01`; };
const STATUS_LABEL: Record<string, string> = {
REQUESTED: "출고요청", APPROVED: "출고완료", SHIPPED: "출고완료",
PAID: "입금완료", INVOICED: "계산서발행", CANCELLED: "취소",
@@ -51,10 +54,14 @@ export default function MomoDashboard() {
<p className="text-slate-500 text-sm mt-1"> .</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card title="대기중 발주" value={s.REQUESTED_CNT} suffix="건" tone="amber" href="/m/orders?status=REQUESTED" />
<Card title="진행중 발주" value={s.PROGRESS_CNT} suffix="건" tone="blue" href="/m/orders?status=APPROVED" />
<Card title="이번달 누적" value={fmt(s.MONTH_AMOUNT)} prefix="" tone="emerald" href="/m/orders" />
<Card title="미수금" value={fmt(s.UNPAID)} prefix="₩" tone="rose" href="/m/orders?status=APPROVED" />
<Card title="대기중 발주" value={s.REQUESTED_CNT} suffix="건" tone="amber"
href="/m/orders?status=REQUESTED&dateFrom=&dateTo=" />
<Card title="진행중 발주" value={s.PROGRESS_CNT} suffix="" tone="blue"
href="/m/orders?status=APPROVED&dateFrom=&dateTo=" />
<Card title="이번달 누적" value={fmt(s.MONTH_AMOUNT)} prefix="₩" tone="emerald"
href={`/m/orders?dateFrom=${firstOfMonthISO()}&dateTo=${todayISO()}`} />
<Card title="미수금" value={fmt(s.UNPAID)} prefix="₩" tone="rose"
href="/m/orders?status=APPROVED&dateFrom=&dateTo=" />
</div>
<Link href="/m/orders/new" className="inline-flex items-center gap-2 px-5 h-11 rounded-xl bg-emerald-700 text-white font-bold shadow hover:-translate-y-0.5 transition">
<ShoppingCart size={16} /> <ArrowRight size={16} />
@@ -88,10 +95,14 @@ export default function MomoDashboard() {
<p className="text-slate-500 text-sm mt-1"> · · .</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card title="승인 대기" value={s.PENDING_CNT} suffix="건" tone="amber" icon={<ClipboardList size={18} />} href="/m/admin/orders?status=REQUESTED" />
<Card title="오늘 발주" value={s.TODAY_CNT} suffix="건" tone="blue" icon={<ShoppingCart size={18} />} href="/m/admin/orders" />
<Card title="이번달 매출" value={fmt(s.MONTH_AMOUNT)} prefix="" tone="emerald" icon={<TrendingUp size={18} />} href="/m/admin/orders" />
<Card title="미수금" value={fmt(s.UNPAID)} prefix="₩" tone="rose" icon={<AlertTriangle size={18} />} href="/m/admin/payments" />
<Card title="승인 대기" value={s.PENDING_CNT} suffix="건" tone="amber" icon={<ClipboardList size={18} />}
href="/m/admin/orders?status=REQUESTED&dateFrom=&dateTo=" />
<Card title="오늘 발주" value={s.TODAY_CNT} suffix="" tone="blue" icon={<ShoppingCart size={18} />}
href={`/m/admin/orders?dateFrom=${todayISO()}&dateTo=${todayISO()}`} />
<Card title="이번달 매출" value={fmt(s.MONTH_AMOUNT)} prefix="₩" tone="emerald" icon={<TrendingUp size={18} />}
href={`/m/admin/orders?dateFrom=${firstOfMonthISO()}&dateTo=${todayISO()}`} />
<Card title="미수금" value={fmt(s.UNPAID)} prefix="₩" tone="rose" icon={<AlertTriangle size={18} />}
href="/m/admin/payments" />
</div>
<div className="grid lg:grid-cols-2 gap-6">
+28 -31
View File
@@ -468,7 +468,7 @@ export default function ItemsBrowse() {
onRemove={removeLine}
/>
) : (
<div className="grid grid-cols-2 sm:grid-cols-2 xl:grid-cols-3 gap-2 sm:gap-3">
<div className="grid grid-cols-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
{items.map((it) => {
const cartLine = cart.find((x) => x.item.OBJID === it.OBJID);
const inCart = cartLine?.qty ?? 0;
@@ -477,49 +477,46 @@ export default function ItemsBrowse() {
const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock;
const soldOut = stock === 0;
return (
<div key={it.OBJID} className={`bg-white border rounded-xl p-3 sm:p-4 transition ${inCart > 0 ? "border-emerald-400 ring-2 ring-emerald-100" : "border-slate-200 hover:shadow-md"}`}>
<div className="aspect-square bg-slate-50 rounded-lg mb-2 sm:mb-3 overflow-hidden flex items-center justify-center relative">
<div key={it.OBJID} className={`bg-white border rounded-lg p-2 transition ${inCart > 0 ? "border-emerald-400 ring-2 ring-emerald-100" : "border-slate-200 hover:shadow-md"}`}>
<div className="aspect-square bg-slate-50 rounded mb-1.5 overflow-hidden flex items-center justify-center relative">
{it.IMAGE_URL ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={it.IMAGE_URL} alt={it.ITEM_NAME} className="w-full h-full object-cover" />
) : (
<div className="text-slate-300 text-xs"> </div>
<div className="text-slate-300 text-[10px]"> </div>
)}
{inCart > 0 && (
<span className="absolute top-1 right-1 bg-emerald-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full">
{inCart}
<span className="absolute top-0.5 right-0.5 bg-emerald-600 text-white text-[9px] font-bold px-1 py-0.5 rounded-full">
{inCart}
</span>
)}
</div>
<div className="flex items-start justify-between gap-1 mb-1">
<div className="font-bold text-xs sm:text-sm text-slate-900 leading-tight">{it.ITEM_NAME}</div>
<div className="flex flex-col gap-0.5 items-end shrink-0">
<div className="absolute top-0.5 left-0.5 flex flex-col gap-0.5">
{it.IS_TAX_FREE === "Y" && (
<span className="px-1 py-0.5 rounded bg-violet-100 text-violet-700 text-[9px] font-bold"></span>
<span className="px-1 py-0.5 rounded bg-violet-500/90 text-white text-[9px] font-bold"></span>
)}
{it.REQUIRES_DELIVERY === "Y" && (
<span className="px-1 py-0.5 rounded bg-orange-100 text-orange-700 text-[9px] font-bold"></span>
<span className="px-1 py-0.5 rounded bg-orange-500/90 text-white text-[9px] font-bold"></span>
)}
</div>
</div>
<div className="text-[11px] text-slate-500 mb-1.5 truncate">{it.MAKER_NAME || "-"}</div>
<div className="flex items-baseline justify-between mb-1">
<div className="font-bold text-slate-900 tabular-nums text-sm">{fmt(it.UNIT_PRICE)}</div>
<div className={`text-[11px] font-semibold ${stock > 0 ? "text-emerald-700" : "text-rose-500"}`}>
{fmt(stock)}{it.UNIT}
<div className="font-bold text-[11px] sm:text-xs text-slate-900 leading-tight mb-0.5 line-clamp-2 min-h-[2em]">{it.ITEM_NAME}</div>
<div className="flex items-baseline justify-between mb-1 gap-1">
<div className="font-bold text-slate-900 tabular-nums text-xs sm:text-sm">{fmt(it.UNIT_PRICE)}</div>
<div className={`text-[10px] font-semibold tabular-nums shrink-0 ${stock > 0 ? "text-emerald-700" : "text-rose-500"}`}>
{fmt(stock)}{it.UNIT}
</div>
</div>
{maxQ > 0 && (
<div className="text-[10px] text-sky-700 mb-1">1 {fmt(maxQ)}</div>
<div className="text-[9px] text-sky-700 mb-0.5"> {fmt(maxQ)}</div>
)}
{/* 수량 컨트롤 — 카드 안에서 바로 조절 */}
{soldOut ? (
<div className="w-full mt-1.5 h-9 rounded-lg bg-slate-100 text-slate-400 text-xs font-bold flex items-center justify-center">
<div className="w-full mt-1 h-7 rounded bg-slate-100 text-slate-400 text-[11px] font-bold flex items-center justify-center">
</div>
) : inCart === 0 ? (
<div className="flex gap-1 mt-1.5">
<div className="flex gap-1 mt-1">
<input
type="number"
min={1}
@@ -535,7 +532,7 @@ export default function ItemsBrowse() {
}
}}
id={`qty-${it.OBJID}`}
className="w-12 h-9 px-2 rounded-lg border border-slate-200 text-center text-sm tabular-nums focus:border-emerald-500 outline-none"
className="w-9 h-7 px-1 rounded border border-slate-200 text-center text-[11px] tabular-nums focus:border-emerald-500 outline-none"
/>
<button
onClick={() => {
@@ -545,16 +542,16 @@ export default function ItemsBrowse() {
addManyToCart(it, q);
if (el) el.value = "1";
}}
className="flex-1 h-9 rounded-lg bg-emerald-700 text-white text-xs font-bold hover:bg-emerald-800 flex items-center justify-center gap-1"
className="flex-1 h-7 rounded bg-emerald-700 text-white text-[11px] font-bold hover:bg-emerald-800 flex items-center justify-center gap-0.5"
>
<Plus size={13} />
<Plus size={11} />
</button>
</div>
) : (
<div className="flex items-center gap-1 mt-1.5 bg-emerald-50 border border-emerald-200 rounded-lg p-1">
<div className="flex items-center gap-0.5 mt-1 bg-emerald-50 border border-emerald-200 rounded p-0.5">
<button onClick={() => updateQty(it.OBJID, -1)}
className="w-8 h-8 rounded-md bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center">
<Minus size={14} />
className="w-6 h-6 rounded bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center shrink-0">
<Minus size={11} />
</button>
<input
type="number"
@@ -562,16 +559,16 @@ export default function ItemsBrowse() {
max={limit}
value={inCart}
onChange={(e) => setQty(it.OBJID, Number(e.target.value))}
className="flex-1 h-8 text-center font-bold text-sm tabular-nums bg-white border border-emerald-200 rounded-md"
className="w-full min-w-0 h-6 text-center font-bold text-[11px] tabular-nums bg-white border border-emerald-200 rounded"
/>
<button onClick={() => updateQty(it.OBJID, 1)}
className="w-8 h-8 rounded-md bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center">
<Plus size={14} />
className="w-6 h-6 rounded bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center shrink-0">
<Plus size={11} />
</button>
<button onClick={() => removeLine(it.OBJID)}
title="빼기"
className="w-8 h-8 rounded-md bg-white border border-rose-200 hover:bg-rose-100 text-rose-500 flex items-center justify-center">
<X size={14} />
className="w-6 h-6 rounded bg-white border border-rose-200 hover:bg-rose-100 text-rose-500 flex items-center justify-center shrink-0">
<X size={11} />
</button>
</div>
)}
+17 -5
View File
@@ -45,16 +45,28 @@ const STATUS_COLOR: Record<string, string> = {
export default function MyOrdersPage() {
const [orders, setOrders] = useState<Order[]>([]);
const [status, setStatus] = useState(() => {
if (typeof window === "undefined") return "";
return new URLSearchParams(window.location.search).get("status") ?? "";
});
const initial = (() => {
if (typeof window === "undefined") return { status: "", dateFrom: "", dateTo: "" };
const q = new URLSearchParams(window.location.search);
return {
status: q.get("status") ?? "",
dateFrom: q.has("dateFrom") ? (q.get("dateFrom") ?? "") : "",
dateTo: q.has("dateTo") ? (q.get("dateTo") ?? "") : "",
};
})();
const [status, setStatus] = useState(initial.status);
const [dateFrom] = useState(initial.dateFrom);
const [dateTo] = useState(initial.dateTo);
const [detail, setDetail] = useState<{ order: Order & { CEO_NAME?: string; BIZ_NO?: string; PHONE?: string; ADDRESS?: string; EMAIL?: string; MEMO?: string }; items: DetailLine[]; supplier: Supplier } | null>(null);
const load = async () => {
const res = await fetch("/api/m/orders/list", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: status || undefined }),
body: JSON.stringify({
status: status || undefined,
dateFrom: dateFrom || undefined,
dateTo: dateTo || undefined,
}),
});
const j = await res.json();
setOrders(j.RESULTLIST ?? []);