fix(orders+payments): SHIPPED 상태 중복 라벨 제거 + 입금관리 검색조건 추가
Deploy momo-erp / deploy (push) Successful in 48s

[출고 상태 셀렉트 중복 fix]
- STATUS_LABEL 에 APPROVED='출고완료' / SHIPPED='출고완료' 둘 다 매핑돼
  셀렉트 옵션에 '출고완료'가 두 번 노출됐음. 운영 DB 분포 확인 결과
  SHIPPED 상태값은 0건(dead) → 라벨/색상 매핑에서 SHIPPED 제거.
  StatementPreview 의 'SHIPPED' OR 분기도 정리

[입금 관리 검색조건]
- 시작일 / 종료일 / 입금 상태(전체·입금 전·입금완료) / 업체명·발주번호 키워드
- 기본 기간: 이번달 1일 ~ 오늘
- 입금 상태: UNPAID = APPROVED, PAID = PAID + INVOICED 묶어서 필터
- 초기화 / 조회 버튼

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-08 17:09:22 +09:00
parent d5954d39e9
commit ed66ca36eb
4 changed files with 92 additions and 17 deletions
+1 -3
View File
@@ -35,7 +35,6 @@ const fmt = (n: number | string | undefined | null) =>
const STATUS_LABEL: Record<string, string> = {
REQUESTED: "출고요청",
APPROVED: "출고완료",
SHIPPED: "출고완료",
PAID: "입금완료",
INVOICED: "계산서발행",
CANCELLED: "취소",
@@ -43,7 +42,6 @@ const STATUS_LABEL: Record<string, string> = {
const STATUS_COLOR: Record<string, string> = {
REQUESTED: "bg-amber-100 text-amber-700 border-amber-200",
APPROVED: "bg-blue-100 text-blue-700 border-blue-200",
SHIPPED: "bg-cyan-100 text-cyan-700 border-cyan-200",
PAID: "bg-emerald-100 text-emerald-700 border-emerald-200",
INVOICED: "bg-violet-100 text-violet-700 border-violet-200",
CANCELLED: "bg-slate-100 text-slate-500 border-slate-200",
@@ -725,7 +723,7 @@ function StatementPreview({
</button>
</div>
)}
{(order.STATUS === "APPROVED" || order.STATUS === "SHIPPED" || order.STATUS === "PAID") && (
{(order.STATUS === "APPROVED" || order.STATUS === "PAID") && (
<div className="flex items-center justify-between pt-3 border-t border-slate-200 gap-2 flex-wrap">
<div className="text-[11px] text-emerald-700 inline-flex items-center gap-1">
<Check size={12} /> {order.APPROVE_DATE && `${order.APPROVE_DATE}`}
+89 -10
View File
@@ -1,21 +1,56 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { RefreshCcw } from "lucide-react";
import Swal from "sweetalert2";
interface Order { OBJID: string; ORDER_NO: string; ORDER_DATE: string; COMPANY_NAME: string; STATUS: string; TOTAL_AMOUNT: number; PAID_AMOUNT: number }
const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
const STATUS_LABEL: Record<string, string> = { APPROVED: "출고완료", PAID: "입금완료", INVOICED: "계산서발행" };
const pad = (n: number) => String(n).padStart(2, "0");
const todayStr = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
const monthStartStr = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-01`; };
// 입금상태 필터: 전체 / 입금 전(APPROVED) / 입금완료(PAID, INVOICED)
type PayFilter = "" | "UNPAID" | "PAID";
export default function PaymentsPage() {
const [list, setList] = useState<Order[]>([]);
const [loading, setLoading] = useState(false);
const load = async () => {
const res = await fetch("/api/m/orders/list", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) });
const all = ((await res.json()).RESULTLIST ?? []) as Order[];
setList(all.filter((o) => ["APPROVED", "PAID", "INVOICED"].includes(o.STATUS)));
};
useEffect(() => { load(); }, []);
const [keyword, setKeyword] = useState("");
const [dateFrom, setDateFrom] = useState(monthStartStr());
const [dateTo, setDateTo] = useState(todayStr());
const [payFilter, setPayFilter] = useState<PayFilter>("");
const load = useCallback(async () => {
setLoading(true);
try {
const res = await fetch("/api/m/orders/list", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dateFrom: dateFrom || undefined,
dateTo: dateTo || undefined,
keyword: keyword || undefined,
}),
});
const all = ((await res.json()).RESULTLIST ?? []) as Order[];
// 출고완료 이후만 (대기/취소 제외) + 상태 필터
const filtered = all.filter((o) => {
if (!["APPROVED", "PAID", "INVOICED"].includes(o.STATUS)) return false;
if (payFilter === "UNPAID" && o.STATUS !== "APPROVED") return false;
if (payFilter === "PAID" && o.STATUS === "APPROVED") return false;
return true;
});
setList(filtered);
} finally {
setLoading(false);
}
}, [dateFrom, dateTo, keyword, payFilter]);
// 최초 1회만 자동 로드. 검색 조건 변경은 [조회] 버튼으로
useEffect(() => { load(); }, []); // eslint-disable-line
const onPay = async (o: Order) => {
const remain = Number(o.TOTAL_AMOUNT) - Number(o.PAID_AMOUNT || 0);
@@ -41,12 +76,56 @@ export default function PaymentsPage() {
const paidCnt = list.filter((o) => o.STATUS !== "APPROVED").length;
return (
<div className="space-y-4">
<div className="space-y-3">
<div>
<h1 className="text-xl sm:text-2xl font-bold"> </h1>
<p className="text-xs sm:text-sm text-slate-500 mt-1"> .</p>
</div>
{/* 검색 영역 — 모바일 1열 / sm 2열 / lg 5열 */}
<div className="bg-white border border-slate-200 rounded-xl p-3">
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-[1fr_1fr_180px_1fr_auto] gap-2">
<div>
<label className="block text-[11px] font-semibold text-slate-500 mb-1"></label>
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm" />
</div>
<div>
<label className="block text-[11px] font-semibold text-slate-500 mb-1"></label>
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm" />
</div>
<div>
<label className="block text-[11px] font-semibold text-slate-500 mb-1"> </label>
<select value={payFilter} onChange={(e) => setPayFilter(e.target.value as PayFilter)}
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm bg-white">
<option value=""></option>
<option value="UNPAID"> ()</option>
<option value="PAID"></option>
</select>
</div>
<div className="col-span-2 sm:col-span-2 lg:col-span-1">
<label className="block text-[11px] font-semibold text-slate-500 mb-1"> / </label>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && load()}
placeholder="검색어"
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm" />
</div>
<div className="col-span-2 sm:col-span-2 lg:col-span-1 flex items-end gap-2">
<button
onClick={() => { setDateFrom(monthStartStr()); setDateTo(todayStr()); setKeyword(""); setPayFilter(""); }}
className="h-9 px-3 rounded-lg border border-slate-200 bg-white text-slate-600 text-xs font-semibold hover:bg-slate-50"
>
</button>
<button onClick={load} disabled={loading}
className="flex-1 lg:flex-none h-9 px-4 rounded-lg bg-slate-800 text-white text-sm font-bold hover:bg-slate-900 disabled:opacity-50 inline-flex items-center justify-center gap-1.5">
<RefreshCcw size={14} className={loading ? "animate-spin" : ""} />
</button>
</div>
</div>
</div>
{/* 요약 카드 */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 sm:gap-3">
<div className="bg-white border border-slate-200 rounded-lg p-3">
@@ -67,7 +146,7 @@ export default function PaymentsPage() {
<div className="space-y-2 sm:hidden">
{list.length === 0 ? (
<div className="bg-white border border-slate-200 rounded-xl p-8 text-center text-slate-400">
.
{loading ? "조회 중..." : "조회 결과가 없습니다."}
</div>
) : list.map((o) => {
const remain = Number(o.TOTAL_AMOUNT) - Number(o.PAID_AMOUNT || 0);
@@ -125,7 +204,7 @@ export default function PaymentsPage() {
</thead>
<tbody>
{list.length === 0 ? (
<tr><td colSpan={8} className="text-center py-12 text-slate-400"> .</td></tr>
<tr><td colSpan={8} className="text-center py-12 text-slate-400">{loading ? "조회 중..." : "조회 결과가 없습니다."}</td></tr>
) : list.map((o) => {
const remain = Number(o.TOTAL_AMOUNT) - Number(o.PAID_AMOUNT || 0);
return (
+1 -2
View File
@@ -17,13 +17,12 @@ 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: "출고완료",
REQUESTED: "출고요청", APPROVED: "출고완료",
PAID: "입금완료", INVOICED: "계산서발행", CANCELLED: "취소",
};
const STATUS_COLOR: Record<string, string> = {
REQUESTED: "bg-amber-100 text-amber-700",
APPROVED: "bg-blue-100 text-blue-700",
SHIPPED: "bg-cyan-100 text-cyan-700",
INVOICED: "bg-violet-100 text-violet-700",
PAID: "bg-emerald-100 text-emerald-700",
CANCELLED: "bg-slate-100 text-slate-500",
+1 -2
View File
@@ -31,13 +31,12 @@ interface Supplier {
const fmt = (n: number | string | undefined) => Number(n || 0).toLocaleString("ko-KR");
const STATUS_LABEL: Record<string, string> = {
REQUESTED: "출고요청", APPROVED: "출고완료", SHIPPED: "출고완료",
REQUESTED: "출고요청", APPROVED: "출고완료",
PAID: "입금완료", INVOICED: "계산서발행", CANCELLED: "취소",
};
const STATUS_COLOR: Record<string, string> = {
REQUESTED: "bg-amber-100 text-amber-700",
APPROVED: "bg-blue-100 text-blue-700",
SHIPPED: "bg-cyan-100 text-cyan-700",
INVOICED: "bg-violet-100 text-violet-700",
PAID: "bg-emerald-100 text-emerald-700",
CANCELLED: "bg-slate-100 text-slate-500",