USER 가 본인의 출고요청/출고완료 발주에 직접: - 신규 품목 추가 (피커 모달 — /api/m/items/list 재고 있는 품목 검색) - 택배비 단가/수량 인라인 수정 + 라인 삭제 - 용차 단가/수량 인라인 수정 + 라인 삭제 신규 API: - /api/m/orders/items/add — ITEM 추가 (재고/숨김/한도 검증, admin 우회) - /api/m/orders/lines/save — 가드 풀어서 REQUESTED + APPROVED 둘 다 허용 UI: - detail modal 상단 액션 바: '+ 품목 추가' / '택배 추가/+1' / '용차 추가/+1' - 표 안의 택배/용차 행에 수량/단가 QtyInput → onBlur 자동저장 - ItemPickerModal — 키워드 검색 + 행별 수량 입력 → 일괄 추가
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState, useRef, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { Download, Image as ImageIcon, X, Eye, Trash2 } from "lucide-react";
|
||||
import { Download, Image as ImageIcon, X, Eye, Trash2, Plus, Truck, Package } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
import { downloadXlsx } from "@/lib/xlsx-export";
|
||||
import { captureAndShare as captureAndShareLib } from "@/lib/capture-share";
|
||||
@@ -248,6 +248,51 @@ function DetailModal({ order, items, supplier, onClose, onCancel, onReload }: {
|
||||
else Swal.fire({ icon: "error", title: "삭제 실패", text: j.message });
|
||||
};
|
||||
|
||||
// 택배/용차 라인 추가/수정 — /api/m/orders/lines/save
|
||||
const upsertExtra = async (line: { objid?: string; kind: "DELIVERY" | "CHARTER"; label?: string; unitPrice: number; qty: number }) => {
|
||||
const res = await fetch("/api/m/orders/lines/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orderObjid: order.OBJID, lines: [line] }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) onReload();
|
||||
else Swal.fire({ icon: "error", title: "택배/용차 저장 실패", text: j.message });
|
||||
};
|
||||
const deleteExtra = async (lineObjid: string, kind: "DELIVERY" | "CHARTER") => {
|
||||
const ok = await Swal.fire({ icon: "question", title: "이 라인을 삭제하시겠습니까?", showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소", confirmButtonColor: "#dc2626" });
|
||||
if (!ok.isConfirmed) return;
|
||||
const res = await fetch("/api/m/orders/lines/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orderObjid: order.OBJID, lines: [{ objid: lineObjid, kind, delete: true, unitPrice: 0, qty: 1 }] }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) onReload();
|
||||
else Swal.fire({ icon: "error", title: "삭제 실패", text: j.message });
|
||||
};
|
||||
const addNewExtra = (kind: "DELIVERY" | "CHARTER") => {
|
||||
const existing = items.find((it) => it.KIND === kind);
|
||||
if (existing) {
|
||||
upsertExtra({ objid: existing.OBJID, kind, label: existing.EXTRA_LABEL || (kind === "DELIVERY" ? "택배비" : "용차비"),
|
||||
unitPrice: Number(existing.UNIT_PRICE), qty: Number(existing.QTY) + 1 });
|
||||
return;
|
||||
}
|
||||
upsertExtra({ kind, label: kind === "DELIVERY" ? "택배비" : "용차비",
|
||||
unitPrice: kind === "DELIVERY" ? 4000 : 5000, qty: 1 });
|
||||
};
|
||||
|
||||
// 품목 추가 (피커 모달)
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const addItems = async (selected: { itemObjid: string; qty: number }[]) => {
|
||||
if (selected.length === 0) return;
|
||||
const res = await fetch("/api/m/orders/items/add", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ orderObjid: order.OBJID, items: selected }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) { setPickerOpen(false); onReload(); }
|
||||
else Swal.fire({ icon: "error", title: "품목 추가 실패", text: j.message });
|
||||
};
|
||||
|
||||
const user = useAuthStore((s) => s.user);
|
||||
// 이미지 공유 버튼은 관리자(admin) 한해서만 노출. 일반 사용자(거래처) 화면에서는 숨김.
|
||||
const showImageShare = user?.isAdmin === true || user?.role === "ADMIN";
|
||||
@@ -328,9 +373,25 @@ function DetailModal({ order, items, supplier, onClose, onCancel, onReload }: {
|
||||
</div>
|
||||
|
||||
{editable && (
|
||||
<p className="mt-3 text-[11px] text-amber-700 bg-amber-50 border border-amber-200 rounded p-2">
|
||||
✏️ {order.STATUS === "REQUESTED" ? "출고요청" : "출고완료"} 상태 — 입금완료 전까지 품목 수량 수정 · [×]로 삭제 가능. 저장은 자동.
|
||||
</p>
|
||||
<>
|
||||
<p className="mt-3 text-[11px] text-amber-700 bg-amber-50 border border-amber-200 rounded p-2">
|
||||
✏️ {order.STATUS === "REQUESTED" ? "출고요청" : "출고완료"} 상태 — 입금완료 전까지 품목 수량/추가/삭제, 택배·용차 라인 직접 수정 가능. 저장은 자동.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5 items-center js-no-export">
|
||||
<button type="button" onClick={() => setPickerOpen(true)}
|
||||
className="inline-flex items-center gap-1 h-7 px-2.5 rounded bg-emerald-100 text-emerald-800 text-[11px] font-bold hover:bg-emerald-200">
|
||||
<Plus size={12} /> 품목 추가
|
||||
</button>
|
||||
<button type="button" onClick={() => addNewExtra("DELIVERY")}
|
||||
className="inline-flex items-center gap-1 h-7 px-2.5 rounded bg-orange-100 text-orange-800 text-[11px] font-bold hover:bg-orange-200">
|
||||
<Truck size={12} /> 택배 추가/+1
|
||||
</button>
|
||||
<button type="button" onClick={() => addNewExtra("CHARTER")}
|
||||
className="inline-flex items-center gap-1 h-7 px-2.5 rounded bg-sky-100 text-sky-800 text-[11px] font-bold hover:bg-sky-200">
|
||||
<Package size={12} /> 용차 추가/+1
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<table className="w-full text-[11px] border border-slate-300 mt-2">
|
||||
<thead className="bg-slate-100">
|
||||
@@ -352,7 +413,8 @@ function DetailModal({ order, items, supplier, onClose, onCancel, onReload }: {
|
||||
const isExtra = it.KIND === "DELIVERY" || it.KIND === "CHARTER";
|
||||
const kindBadge = it.KIND === "DELIVERY" ? "택배" : it.KIND === "CHARTER" ? "용차" : null;
|
||||
const kindBg = it.KIND === "DELIVERY" ? "bg-orange-50" : it.KIND === "CHARTER" ? "bg-sky-50" : "";
|
||||
const canEditItem = editable && !isExtra; // ITEM 라인만 본인이 수정 가능
|
||||
const canEditItem = editable && !isExtra;
|
||||
const canEditExtra = editable && isExtra;
|
||||
return (
|
||||
<tr key={it.OBJID || idx} className={kindBg}>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-center">{idx + 1}</td>
|
||||
@@ -366,9 +428,15 @@ function DetailModal({ order, items, supplier, onClose, onCancel, onReload }: {
|
||||
<td className="border border-slate-300 px-1 py-0.5 text-right">
|
||||
{canEditItem
|
||||
? <QtyInput initial={Number(it.QTY)} onSave={(q) => updateItemLine(it.OBJID, q)} />
|
||||
: fmt(it.QTY)}
|
||||
: canEditExtra
|
||||
? <QtyInput initial={Number(it.QTY)} onSave={(q) => upsertExtra({ objid: it.OBJID, kind: it.KIND as "DELIVERY"|"CHARTER", label: it.EXTRA_LABEL || it.ITEM_NAME, unitPrice: Number(it.UNIT_PRICE), qty: q })} />
|
||||
: fmt(it.QTY)}
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1 py-0.5 text-right">
|
||||
{canEditExtra
|
||||
? <QtyInput initial={Number(it.UNIT_PRICE)} onSave={(p) => upsertExtra({ objid: it.OBJID, kind: it.KIND as "DELIVERY"|"CHARTER", label: it.EXTRA_LABEL || it.ITEM_NAME, unitPrice: p, qty: Number(it.QTY) })} />
|
||||
: fmt(it.UNIT_PRICE)}
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right">{fmt(it.UNIT_PRICE)}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right">{fmt(it.SUPPLY_AMOUNT)}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right">{it.IS_TAX_FREE === "Y" ? "-" : fmt(it.VAT_AMOUNT)}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right font-semibold">{fmt(it.TOTAL_AMOUNT)}</td>
|
||||
@@ -377,7 +445,9 @@ function DetailModal({ order, items, supplier, onClose, onCancel, onReload }: {
|
||||
<td className="border border-slate-300 px-1 py-1 text-center">
|
||||
{canEditItem
|
||||
? <button onClick={() => deleteItemLine(it.OBJID)} className="text-rose-500 hover:text-rose-700" title="삭제"><X size={12} /></button>
|
||||
: <span className="text-slate-300 text-[10px]">자동</span>}
|
||||
: canEditExtra
|
||||
? <button onClick={() => deleteExtra(it.OBJID, it.KIND as "DELIVERY"|"CHARTER")} className="text-rose-500 hover:text-rose-700" title="삭제"><X size={12} /></button>
|
||||
: <span className="text-slate-300 text-[10px]">-</span>}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
@@ -401,10 +471,108 @@ function DetailModal({ order, items, supplier, onClose, onCancel, onReload }: {
|
||||
|
||||
{editable && (
|
||||
<div className="mt-4 pt-3 border-t border-slate-200 text-[11px] text-amber-700 bg-amber-50 rounded p-2">
|
||||
※ {order.STATUS === "REQUESTED" ? "출고요청" : "출고완료"} 상태 — 입금완료 전까지 품목 <b>수량 수정</b> · <b>품목 삭제</b> · <b>주문 취소</b> 가능합니다. 택배/용차 라인은 모모 담당자가 조정합니다.
|
||||
※ {order.STATUS === "REQUESTED" ? "출고요청" : "출고완료"} 상태 — 입금완료 전까지 품목 <b>추가/수정/삭제</b>, 택배·용차 라인 <b>직접 수정</b>, <b>주문 취소</b> 가능합니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pickerOpen && (
|
||||
<ItemPickerModal
|
||||
onClose={() => setPickerOpen(false)}
|
||||
onConfirm={addItems}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemPickerModal({ onClose, onConfirm }: {
|
||||
onClose: () => void;
|
||||
onConfirm: (selected: { itemObjid: string; qty: number }[]) => void;
|
||||
}) {
|
||||
const [items, setItems] = useState<Array<{ OBJID: string; ITEM_CODE: string; ITEM_NAME: string; UNIT_PRICE: number; STOCK_QTY: number; IS_TAX_FREE: string; UNIT: string }>>([]);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [cart, setCart] = useState<Record<string, number>>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/m/items/list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ stockFilter: "AVAILABLE" }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((j) => setItems(j.RESULTLIST ?? []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const kw = keyword.trim().toLowerCase();
|
||||
if (!kw) return items;
|
||||
return items.filter((it) => it.ITEM_NAME?.toLowerCase().includes(kw) || it.ITEM_CODE?.toLowerCase().includes(kw));
|
||||
}, [items, keyword]);
|
||||
|
||||
const confirm = () => {
|
||||
const selected = Object.entries(cart)
|
||||
.filter(([, q]) => q > 0)
|
||||
.map(([itemObjid, qty]) => ({ itemObjid, qty }));
|
||||
onConfirm(selected);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-slate-900/60 z-[60] flex items-center justify-center p-3" onClick={onClose}>
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-200">
|
||||
<h3 className="font-bold">품목 추가</h3>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-slate-700"><X size={18} /></button>
|
||||
</div>
|
||||
<div className="px-4 py-2 border-b border-slate-200">
|
||||
<input value={keyword} onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder="품목명/코드 검색"
|
||||
className="w-full h-9 px-3 rounded border border-slate-300 text-sm" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-slate-50 text-slate-600 sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left p-2">품목</th>
|
||||
<th className="text-right p-2 w-16">재고</th>
|
||||
<th className="text-right p-2 w-20">단가</th>
|
||||
<th className="text-center p-2 w-20">수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.length === 0 ? (
|
||||
<tr><td colSpan={4} className="text-center py-8 text-slate-400">검색 결과 없음</td></tr>
|
||||
) : filtered.slice(0, 100).map((it) => (
|
||||
<tr key={it.OBJID} className="border-t border-slate-100 hover:bg-slate-50">
|
||||
<td className="p-2">
|
||||
<div className="font-semibold">{it.ITEM_NAME}</div>
|
||||
<div className="text-[10px] text-slate-400">{it.ITEM_CODE} · {it.IS_TAX_FREE === "Y" ? "면세" : "과세"}</div>
|
||||
</td>
|
||||
<td className="p-2 text-right tabular-nums">{Number(it.STOCK_QTY).toLocaleString()}</td>
|
||||
<td className="p-2 text-right tabular-nums">{Number(it.UNIT_PRICE).toLocaleString()}</td>
|
||||
<td className="p-2 text-center">
|
||||
<input type="number" min={0} max={Number(it.STOCK_QTY)}
|
||||
value={cart[it.OBJID] ?? 0}
|
||||
onChange={(e) => {
|
||||
const v = Math.min(Number(it.STOCK_QTY), Math.max(0, Number(e.target.value) || 0));
|
||||
setCart((p) => ({ ...p, [it.OBJID]: v }));
|
||||
}}
|
||||
className="w-16 h-7 px-1 border border-slate-200 rounded text-right tabular-nums" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 px-4 py-3 border-t border-slate-200">
|
||||
<button onClick={onClose} className="h-9 px-4 rounded border border-slate-200 text-sm font-semibold">취소</button>
|
||||
<button onClick={confirm}
|
||||
disabled={Object.values(cart).every((q) => !q)}
|
||||
className="h-9 px-5 rounded bg-emerald-700 text-white text-sm font-bold hover:bg-emerald-800 disabled:opacity-40">
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// 발주에 ITEM 라인 추가 — 본인 또는 admin, REQUESTED/APPROVED 상태.
|
||||
// 재고/숨김/한도 검증 후 momo_order_items INSERT + 합계 재집계.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
import { requireMomoUser } from "@/lib/momo-guard";
|
||||
import { calcLine } from "@/lib/momo-pricing";
|
||||
|
||||
interface AddItem { itemObjid: string; qty: number }
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const r = await requireMomoUser();
|
||||
if (r instanceof NextResponse) return r;
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const { orderObjid, items } = body as { orderObjid?: string; items?: AddItem[] };
|
||||
if (!orderObjid || !Array.isArray(items) || items.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "잘못된 요청입니다." }, { status: 400 });
|
||||
}
|
||||
for (const it of items) {
|
||||
if (!it.itemObjid || !Number.isFinite(Number(it.qty)) || Number(it.qty) <= 0) {
|
||||
return NextResponse.json({ success: false, message: "품목/수량 형식이 올바르지 않습니다." }, { status: 400 });
|
||||
}
|
||||
}
|
||||
const isAdmin = r.user.isAdmin || r.user.role === "ADMIN" || r.user.userType === "A";
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const orderRes = await client.query(
|
||||
`SELECT customer_objid, status FROM momo_orders WHERE objid = $1 FOR UPDATE`,
|
||||
[orderObjid]
|
||||
);
|
||||
if (orderRes.rowCount === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "발주를 찾을 수 없습니다." }, { status: 404 });
|
||||
}
|
||||
const order = orderRes.rows[0];
|
||||
const userOwn = r.user.objid ?? r.user.userId;
|
||||
if (!isAdmin && order.customer_objid !== userOwn && order.customer_objid !== r.user.userId) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "권한이 없습니다." }, { status: 403 });
|
||||
}
|
||||
if (order.status !== "REQUESTED" && order.status !== "APPROVED") {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "입금완료 이후 발주는 품목 추가 불가." }, { status: 400 });
|
||||
}
|
||||
|
||||
// 품목 정보 조회 (재고/단가 등)
|
||||
const itemIds = items.map((i) => i.itemObjid);
|
||||
const placeholders = itemIds.map((_, i) => `$${i + 1}`).join(",");
|
||||
const itemsRes = await client.query(
|
||||
`SELECT
|
||||
I.objid, I.item_name, I.unit_price, I.is_tax_free, I.max_order_qty,
|
||||
COALESCE(I.is_hidden, 'N') AS is_hidden,
|
||||
COALESCE((
|
||||
SELECT SUM(S.qty) FROM momo_stocks S
|
||||
JOIN momo_warehouses W ON S.wh_objid = W.objid
|
||||
WHERE S.item_objid = I.objid AND COALESCE(W.is_del,'N') != 'Y'
|
||||
), 0) AS stock_qty
|
||||
FROM momo_items I
|
||||
WHERE I.objid IN (${placeholders}) AND COALESCE(I.is_del, 'N') != 'Y'`,
|
||||
itemIds
|
||||
);
|
||||
const itemMap = new Map(itemsRes.rows.map((row) => [row.objid as string, row]));
|
||||
|
||||
// 검증 (admin 은 재고/한도 우회)
|
||||
if (!isAdmin) {
|
||||
// 사용자별 권한
|
||||
const u = await client.query(
|
||||
`SELECT COALESCE(unlimited_qty,'N') AS u, COALESCE(view_hidden,'N') AS v FROM user_info WHERE user_id = $1`,
|
||||
[order.customer_objid]
|
||||
);
|
||||
const unlimited = u.rows[0]?.u === "Y";
|
||||
const viewHidden = u.rows[0]?.v === "Y";
|
||||
for (const ln of items) {
|
||||
const it = itemMap.get(ln.itemObjid);
|
||||
if (!it) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: `존재하지 않는 품목: ${ln.itemObjid}` }, { status: 400 });
|
||||
}
|
||||
if (it.is_hidden === "Y" && !viewHidden) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: `${it.item_name} 은 발주 불가 품목입니다.` }, { status: 400 });
|
||||
}
|
||||
const stock = Number(it.stock_qty ?? 0);
|
||||
if (Number(ln.qty) > stock) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: `${it.item_name} — 재고(${stock}) 초과` }, { status: 400 });
|
||||
}
|
||||
if (!unlimited) {
|
||||
const maxQ = it.max_order_qty == null ? 0 : Number(it.max_order_qty);
|
||||
if (maxQ > 0 && Number(ln.qty) > maxQ) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: `${it.item_name} — 1회 발주 한도(${maxQ}) 초과` }, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 seq
|
||||
const seqRes = await client.query(
|
||||
`SELECT COALESCE(MAX(seq), 0) AS m FROM momo_order_items WHERE order_objid = $1`,
|
||||
[orderObjid]
|
||||
);
|
||||
let nextSeq = Number(seqRes.rows[0]?.m ?? 0) + 1;
|
||||
|
||||
for (const ln of items) {
|
||||
const it = itemMap.get(ln.itemObjid);
|
||||
if (!it) continue;
|
||||
const unitPrice = Number(it.unit_price);
|
||||
const qty = Number(ln.qty);
|
||||
const isFree = it.is_tax_free === "Y";
|
||||
const calc = calcLine({ unitPrice, qty, isTaxFree: isFree });
|
||||
await client.query(
|
||||
`INSERT INTO momo_order_items (
|
||||
objid, order_objid, item_objid, item_name_snap, unit_price, qty,
|
||||
is_tax_free, supply_amount, vat_amount, total_amount, seq, kind
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'ITEM')`,
|
||||
[createObjectId(), orderObjid, ln.itemObjid, it.item_name, unitPrice, qty,
|
||||
isFree ? "Y" : "N", calc.supplyAmount, calc.vatAmount, calc.totalAmount, nextSeq]
|
||||
);
|
||||
nextSeq++;
|
||||
}
|
||||
|
||||
// 합계 재집계
|
||||
const sumRes = await client.query(
|
||||
`SELECT
|
||||
COALESCE(SUM(supply_amount), 0) AS supply,
|
||||
COALESCE(SUM(vat_amount), 0) AS vat,
|
||||
COALESCE(SUM(total_amount), 0) AS total,
|
||||
COALESCE(SUM(CASE WHEN is_tax_free='Y' THEN supply_amount ELSE 0 END), 0) AS taxfree,
|
||||
COALESCE(SUM(CASE WHEN is_tax_free='N' THEN supply_amount ELSE 0 END), 0) AS taxable,
|
||||
COALESCE(SUM(CASE WHEN kind='DELIVERY' THEN total_amount ELSE 0 END), 0) AS delivery,
|
||||
COALESCE(SUM(CASE WHEN kind='CHARTER' THEN total_amount ELSE 0 END), 0) AS charter
|
||||
FROM momo_order_items WHERE order_objid = $1`,
|
||||
[orderObjid]
|
||||
);
|
||||
const t = sumRes.rows[0];
|
||||
await client.query(
|
||||
`UPDATE momo_orders SET
|
||||
total_supply=$2, total_vat=$3, total_amount=$4,
|
||||
total_taxfree=$5, total_taxable=$6,
|
||||
total_delivery=$7, total_charter=$8,
|
||||
update_date=NOW(), update_id=$9
|
||||
WHERE objid = $1`,
|
||||
[orderObjid, t.supply, t.vat, t.total, t.taxfree, t.taxable, t.delivery, t.charter,
|
||||
r.user.objid || r.user.userId]
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("[orders/items/add]", err);
|
||||
return NextResponse.json({ success: false, message: err instanceof Error ? err.message : "오류" }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -46,15 +46,10 @@ export async function POST(req: NextRequest) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "권한이 없습니다." }, { status: 403 });
|
||||
}
|
||||
// USER: REQUESTED 만, ADMIN: 입금완료(PAID)/취소 전까지 모두
|
||||
if (isAdmin) {
|
||||
if (order.status === "PAID" || order.status === "CANCELED") {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "입금완료 또는 취소된 발주는 수정할 수 없습니다." }, { status: 400 });
|
||||
}
|
||||
} else if (order.status !== "REQUESTED") {
|
||||
// USER: REQUESTED + APPROVED (입금완료 전), ADMIN: 동일
|
||||
if (order.status !== "REQUESTED" && order.status !== "APPROVED") {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "출고 요청 상태에서만 수정할 수 있습니다." }, { status: 400 });
|
||||
return NextResponse.json({ success: false, message: "입금완료 이후 또는 취소된 발주는 수정할 수 없습니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
// 다음 seq (현재 최대 + 1)
|
||||
|
||||
Reference in New Issue
Block a user