[공급자 정보 박스 (우측 상단)] - 결제계좌번호 / 전화번호 / 이메일 표를 거래명세표 우측 상단에 표시 - 환경변수: MOMO_BANK_ACCOUNT / MOMO_PHONE / MOMO_EMAIL / MOMO_COMPANY_CEO 등 - detail API 응답에 supplier 객체 추가 [비고(remark) 컬럼] - 모든 라인(품목/택배/용차)에 비고 입력 가능 - /api/m/orders/items/remark 신설 — REQUESTED 상태에서만 본인/관리자 수정 - 인풋에서 포커스 이탈/엔터 시 자동 저장 - 모든 라인에 momo_order_items.remark 컬럼 활용 (이미 존재) [이미지 공유 + 인쇄] - 거래명세표 위쪽에 [📤 이미지 공유] [🖨 인쇄] 버튼 신설 - html-to-image 라이브러리로 PNG 캡처 → Web Share API 가 있으면 카톡/메신저로 직접 공유, 없으면 PNG 파일 다운로드 (모바일/PC 호환) - statementRef 로 캡처 영역 분리 (버튼은 영역 밖) [엑셀 다운로드 수정] - 기존: SELECT 쿼리에 alias 빠져 있어(`U.user_name, NULL, NULL`) 회사명/대표자/사업자번호가 모두 빈 값 - 수정: company_name/ceo_name/biz_no/phone/address/email 명시 alias - 택배/용차 라인은 [택배]/[용차] 라벨로 출력 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -16,6 +16,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"html-to-image": "^1.11.13",
|
||||
"jose": "^6.2.2",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next": "16.2.2",
|
||||
@@ -5045,6 +5046,12 @@
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-image": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-status-codes": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"html-to-image": "^1.11.13",
|
||||
"jose": "^6.2.2",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next": "16.2.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState, useCallback } from "react";
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from "react";
|
||||
import { Check, Download, X, RefreshCcw, Truck, AlertCircle, Package } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
@@ -21,6 +21,11 @@ interface DetailLine {
|
||||
STOCK_QTY: number;
|
||||
KIND: "ITEM" | "DELIVERY" | "CHARTER";
|
||||
EXTRA_LABEL?: string;
|
||||
REMARK?: string;
|
||||
}
|
||||
interface Supplier {
|
||||
NAME: string; CEO: string; BIZ_NO: string;
|
||||
BANK_ACCOUNT: string; PHONE: string; EMAIL: string; ADDRESS: string;
|
||||
}
|
||||
|
||||
const fmt = (n: number | string | undefined | null) =>
|
||||
@@ -48,7 +53,7 @@ export default function AdminOrdersPage() {
|
||||
const [status, setStatus] = useState("");
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [activeId, setActiveId] = useState<string>("");
|
||||
const [detail, setDetail] = useState<{ order: DetailOrder; items: DetailLine[] } | null>(null);
|
||||
const [detail, setDetail] = useState<{ order: DetailOrder; items: DetailLine[]; supplier: Supplier } | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
@@ -84,7 +89,7 @@ export default function AdminOrdersPage() {
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) {
|
||||
setDetail({ order: j.order as DetailOrder, items: j.items as DetailLine[] });
|
||||
setDetail({ order: j.order as DetailOrder, items: j.items as DetailLine[], supplier: j.supplier as Supplier });
|
||||
}
|
||||
}, [activeId]);
|
||||
|
||||
@@ -99,7 +104,7 @@ export default function AdminOrdersPage() {
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!cancelled && j.success) {
|
||||
setDetail({ order: j.order as DetailOrder, items: j.items as DetailLine[] });
|
||||
setDetail({ order: j.order as DetailOrder, items: j.items as DetailLine[], supplier: j.supplier as Supplier });
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
@@ -311,7 +316,7 @@ export default function AdminOrdersPage() {
|
||||
<div className="text-sm">왼쪽에서 발주를 선택하세요.</div>
|
||||
</div>
|
||||
) : (
|
||||
<StatementPreview order={detail.order} items={detail.items} onCancel={cancelOne} busy={busy} onReload={reloadDetail} />
|
||||
<StatementPreview order={detail.order} items={detail.items} supplier={detail.supplier} onCancel={cancelOne} busy={busy} onReload={reloadDetail} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -323,19 +328,62 @@ export default function AdminOrdersPage() {
|
||||
function StatementPreview({
|
||||
order,
|
||||
items,
|
||||
supplier,
|
||||
onCancel,
|
||||
busy,
|
||||
onReload,
|
||||
}: {
|
||||
order: DetailOrder;
|
||||
items: DetailLine[];
|
||||
supplier: Supplier;
|
||||
onCancel: (o: Order) => void;
|
||||
busy: boolean;
|
||||
onReload: () => void;
|
||||
}) {
|
||||
const statementRef = useRef<HTMLDivElement>(null);
|
||||
const lowStock = items.filter((it) => it.KIND === "ITEM" && Number(it.STOCK_QTY) < Number(it.QTY));
|
||||
const editable = order.STATUS === "REQUESTED";
|
||||
|
||||
// 거래명세표를 이미지로 캡처 → 공유 또는 다운로드
|
||||
const captureAndShare = async () => {
|
||||
try {
|
||||
const mod = await import("html-to-image");
|
||||
if (!statementRef.current) return;
|
||||
const dataUrl = await mod.toPng(statementRef.current, {
|
||||
backgroundColor: "#ffffff",
|
||||
pixelRatio: 2,
|
||||
});
|
||||
const blob = await (await fetch(dataUrl)).blob();
|
||||
const file = new File([blob], `${order.ORDER_NO}_거래명세표.png`, { type: "image/png" });
|
||||
// Web Share API (모바일/지원 브라우저) → 카카오톡/메신저 등으로 공유
|
||||
const navAny = navigator as Navigator & { canShare?: (data: { files: File[] }) => boolean };
|
||||
if (navAny.canShare && navAny.canShare({ files: [file] })) {
|
||||
await navigator.share({ files: [file], title: `${order.ORDER_NO} 거래명세표`, text: order.COMPANY_NAME });
|
||||
return;
|
||||
}
|
||||
// 폴백: 이미지 파일 다운로드
|
||||
const a = document.createElement("a");
|
||||
a.href = dataUrl;
|
||||
a.download = `${order.ORDER_NO}_거래명세표.png`;
|
||||
a.click();
|
||||
Swal.fire({ icon: "success", title: "이미지 저장 완료", text: "다운로드한 이미지를 카톡 등에 첨부해 보내세요.", timer: 1500, showConfirmButton: false });
|
||||
} catch (err) {
|
||||
console.error("[capture]", err);
|
||||
Swal.fire({ icon: "error", title: "이미지 캡처 실패", text: "잠시 후 다시 시도하세요." });
|
||||
}
|
||||
};
|
||||
|
||||
// 비고 저장
|
||||
const saveRemark = async (lineObjid: string, remark: string) => {
|
||||
const res = await fetch("/api/m/orders/items/remark", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ lineObjid, remark }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) onReload();
|
||||
else Swal.fire({ icon: "error", title: "비고 저장 실패", text: j.message });
|
||||
};
|
||||
|
||||
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" },
|
||||
@@ -376,23 +424,58 @@ function StatementPreview({
|
||||
};
|
||||
return (
|
||||
<div className="text-[12px] text-slate-800 space-y-3">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-bold tracking-[0.3em] text-slate-900">거 래 명 세 표</h2>
|
||||
{/* 공유/캡처 버튼 — 캡처 영역 밖에 배치 */}
|
||||
<div className="flex justify-end gap-2 print:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={captureAndShare}
|
||||
className="inline-flex items-center gap-1 h-8 px-3 rounded-lg bg-amber-100 text-amber-800 text-xs font-bold hover:bg-amber-200"
|
||||
title="이미지로 캡처해 카톡 등에 공유"
|
||||
>
|
||||
📤 이미지 공유
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.print()}
|
||||
className="inline-flex items-center gap-1 h-8 px-3 rounded-lg bg-slate-100 text-slate-700 text-xs font-bold hover:bg-slate-200"
|
||||
title="인쇄"
|
||||
>
|
||||
🖨 인쇄
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-[11px]">
|
||||
<div ref={statementRef} className="bg-white p-3">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold tracking-[0.3em] text-slate-900">거 래 명 세 표</h2>
|
||||
</div>
|
||||
|
||||
{/* 좌: 발주 정보 / 우: 공급자 정보 박스 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-3 text-[11px]">
|
||||
<div>
|
||||
<div><b>발주번호</b> · {order.ORDER_NO}</div>
|
||||
<div><b>발주일자</b> · {order.ORDER_DATE}</div>
|
||||
<div><b>현재상태</b> · <span className="font-semibold">{STATUS_LABEL[order.STATUS] ?? order.STATUS}</span></div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div><b>공급자</b> · 모모유통</div>
|
||||
<div className="text-slate-500">대표: 한신숙</div>
|
||||
</div>
|
||||
<table className="text-[11px] border border-slate-400 self-start ml-auto" style={{borderCollapse:'collapse'}}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowSpan={3} className="border border-slate-400 bg-slate-700 text-white text-center font-bold px-2 py-1" style={{writingMode:'vertical-rl',textOrientation:'upright',width:'24px'}}>공급자</td>
|
||||
<td className="border border-slate-400 bg-slate-100 text-center font-semibold px-2 py-1" style={{width:'90px'}}>결제<br/>계좌번호</td>
|
||||
<td className="border border-slate-400 px-3 py-1 font-bold text-slate-900">{supplier.BANK_ACCOUNT}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-400 bg-slate-100 text-center font-semibold px-2 py-1">전화번호</td>
|
||||
<td className="border border-slate-400 px-3 py-1">{supplier.PHONE}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-400 bg-slate-100 text-center font-semibold px-2 py-1">이메일</td>
|
||||
<td className="border border-slate-400 px-3 py-1 text-blue-700">{supplier.EMAIL}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-200 rounded p-2 bg-slate-50/60">
|
||||
<div className="border border-slate-200 rounded p-2 bg-slate-50/60 mt-3">
|
||||
<div className="font-semibold text-slate-900">{order.COMPANY_NAME} <span className="text-slate-500 font-normal">귀하</span></div>
|
||||
<div className="text-[11px] text-slate-600 mt-0.5 leading-relaxed">
|
||||
{order.CEO_NAME && <>대표: {order.CEO_NAME} · </>}
|
||||
@@ -437,7 +520,7 @@ function StatementPreview({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full text-[11px] border border-slate-300">
|
||||
<table className="w-full text-[11px] border border-slate-300 mt-3">
|
||||
<thead className="bg-slate-100">
|
||||
<tr>
|
||||
<th className="border border-slate-300 px-1.5 py-1.5 w-8">#</th>
|
||||
@@ -449,6 +532,7 @@ function StatementPreview({
|
||||
<th className="border border-slate-300 px-1.5 py-1.5">공급가</th>
|
||||
<th className="border border-slate-300 px-1.5 py-1.5">세액</th>
|
||||
<th className="border border-slate-300 px-1.5 py-1.5">합계</th>
|
||||
<th className="border border-slate-300 px-1.5 py-1.5 w-32">비고</th>
|
||||
{editable && <th className="border border-slate-300 px-1 py-1.5 w-8"></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -466,8 +550,10 @@ function StatementPreview({
|
||||
key={it.OBJID}
|
||||
line={it}
|
||||
displaySeq={displaySeq}
|
||||
editable={editable}
|
||||
onSave={(updated) => upsertExtra({ objid: it.OBJID, kind: it.KIND as "DELIVERY" | "CHARTER", ...updated })}
|
||||
onDelete={() => deleteExtra(it.OBJID)}
|
||||
onSaveRemark={(r) => saveRemark(it.OBJID, r)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -490,17 +576,22 @@ function StatementPreview({
|
||||
<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>
|
||||
<td className="border border-slate-300 px-1 py-0.5">
|
||||
{editable
|
||||
? <RemarkInput initial={it.REMARK || ""} onSave={(r) => saveRemark(it.OBJID, r)} />
|
||||
: <span className="text-slate-600 text-[10px]">{it.REMARK || ""}</span>}
|
||||
</td>
|
||||
{editable && <td className="border border-slate-300 px-1 py-1"></td>}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{items.length === 0 && (
|
||||
<tr><td colSpan={editable ? 10 : 9} className="border border-slate-300 px-2 py-6 text-center text-slate-400">품목이 없습니다.</td></tr>
|
||||
<tr><td colSpan={editable ? 11 : 10} className="border border-slate-300 px-2 py-6 text-center text-slate-400">품목이 없습니다.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table className="ml-auto text-[12px] tabular-nums">
|
||||
<table className="ml-auto text-[12px] tabular-nums mt-3">
|
||||
<tbody>
|
||||
<tr><td className="px-3 py-1 text-violet-700">면세 합계</td><td className="px-3 py-1 text-right min-w-[120px]">₩ {fmt(order.TOTAL_TAXFREE)}</td></tr>
|
||||
<tr><td className="px-3 py-1 text-rose-700">과세 공급가</td><td className="px-3 py-1 text-right">₩ {fmt(order.TOTAL_TAXABLE)}</td></tr>
|
||||
@@ -511,6 +602,7 @@ function StatementPreview({
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>{/* /statementRef capture area */}
|
||||
|
||||
{order.STATUS === "REQUESTED" && (
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-slate-200">
|
||||
@@ -538,11 +630,13 @@ function StatementPreview({
|
||||
);
|
||||
}
|
||||
|
||||
function ExtraRow({ line, displaySeq, onSave, onDelete }: {
|
||||
function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark }: {
|
||||
line: DetailLine;
|
||||
displaySeq: number;
|
||||
editable: boolean;
|
||||
onSave: (data: { label: string; unitPrice: number; qty: number }) => void;
|
||||
onDelete: () => void;
|
||||
onSaveRemark: (r: string) => void;
|
||||
}) {
|
||||
const [label, setLabel] = useState(line.EXTRA_LABEL || line.ITEM_NAME);
|
||||
const [unitPrice, setUnitPrice] = useState(Number(line.UNIT_PRICE) || 0);
|
||||
@@ -591,6 +685,11 @@ function ExtraRow({ line, displaySeq, onSave, onDelete }: {
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right tabular-nums">{Number(supply).toLocaleString("ko-KR")}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right tabular-nums">{Number(vat).toLocaleString("ko-KR")}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-right tabular-nums font-semibold">{Number(total).toLocaleString("ko-KR")}</td>
|
||||
<td className="border border-slate-300 px-1 py-0.5">
|
||||
{editable
|
||||
? <RemarkInput initial={line.REMARK || ""} onSave={onSaveRemark} />
|
||||
: <span className="text-slate-600 text-[10px]">{line.REMARK || ""}</span>}
|
||||
</td>
|
||||
<td className="border border-slate-300 px-1 py-1 text-center">
|
||||
<div className="inline-flex gap-0.5">
|
||||
{dirty && (
|
||||
@@ -683,3 +782,21 @@ function IssueEinvoiceButton({ order }: { order: DetailOrder }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RemarkInput({ initial, onSave }: { initial: string; onSave: (r: string) => void }) {
|
||||
const [val, setVal] = useState(initial);
|
||||
useEffect(() => { setVal(initial); }, [initial]);
|
||||
const dirty = val !== initial;
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
value={val}
|
||||
onChange={(e) => setVal(e.target.value)}
|
||||
onBlur={() => { if (dirty) onSave(val); }}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { (e.target as HTMLInputElement).blur(); } }}
|
||||
placeholder="비고"
|
||||
className="w-full h-6 px-1.5 border border-slate-200 rounded text-[10px] bg-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export async function POST(req: NextRequest) {
|
||||
OI.total_amount AS "TOTAL_AMOUNT",
|
||||
COALESCE(OI.kind, 'ITEM') AS "KIND",
|
||||
OI.extra_label AS "EXTRA_LABEL",
|
||||
OI.remark AS "REMARK",
|
||||
I.unit AS "UNIT",
|
||||
I.image_url AS "IMAGE_URL",
|
||||
COALESCE(
|
||||
@@ -74,5 +75,16 @@ export async function POST(req: NextRequest) {
|
||||
[objid]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true, order, items });
|
||||
// 공급자(모모유통) 정보 — 환경변수 또는 기본값
|
||||
const supplier = {
|
||||
NAME: process.env.MOMO_COMPANY_NAME || "모모유통",
|
||||
CEO: process.env.MOMO_COMPANY_CEO || "이상용",
|
||||
BIZ_NO: process.env.MOMO_COMPANY_BIZNO || "",
|
||||
BANK_ACCOUNT: process.env.MOMO_BANK_ACCOUNT || "기업은행 434-115361-01-016 (이상용)",
|
||||
PHONE: process.env.MOMO_PHONE || "010-6369-8443",
|
||||
EMAIL: process.env.MOMO_EMAIL || "momo8443@daum.net",
|
||||
ADDRESS: process.env.MOMO_COMPANY_ADDR || "",
|
||||
};
|
||||
|
||||
return NextResponse.json({ success: true, order, items, supplier });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 발주 라인의 비고(remark) 수정 — 관리자 또는 본인.
|
||||
// REQUESTED 상태에서만 수정 가능. ITEM/DELIVERY/CHARTER 모두 가능.
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { requireMomoUser } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const r = await requireMomoUser();
|
||||
if (r instanceof NextResponse) return r;
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const { lineObjid, remark } = body as { lineObjid?: string; remark?: string };
|
||||
if (!lineObjid) {
|
||||
return NextResponse.json({ success: false, message: "라인 식별자가 필요합니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
const isAdmin = r.user.isAdmin || r.user.role === "ADMIN" || r.user.userType === "A";
|
||||
|
||||
// 권한 확인 — 본인 발주이거나 관리자
|
||||
const own = await pool.query(
|
||||
`SELECT O.customer_objid, O.status
|
||||
FROM momo_order_items OI
|
||||
JOIN momo_orders O ON O.objid = OI.order_objid
|
||||
WHERE OI.objid = $1`,
|
||||
[lineObjid]
|
||||
);
|
||||
if (own.rowCount === 0) {
|
||||
return NextResponse.json({ success: false, message: "라인을 찾을 수 없습니다." }, { status: 404 });
|
||||
}
|
||||
const row = own.rows[0];
|
||||
if (!isAdmin && row.customer_objid !== r.user.objid) {
|
||||
return NextResponse.json({ success: false, message: "권한이 없습니다." }, { status: 403 });
|
||||
}
|
||||
if (row.status !== "REQUESTED") {
|
||||
return NextResponse.json({ success: false, message: "출고 요청 상태에서만 비고를 수정할 수 있습니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`UPDATE momo_order_items SET remark = $2 WHERE objid = $1`,
|
||||
[lineObjid, (remark ?? "").trim() || null]
|
||||
);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -12,7 +12,12 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
const order = await queryOne<Record<string, unknown>>(
|
||||
`SELECT O.objid, O.order_no, TO_CHAR(O.order_date,'YYYY-MM-DD') AS order_date,
|
||||
O.customer_objid,
|
||||
U.user_name, NULL, NULL, U.cell_phone,
|
||||
U.user_name AS company_name,
|
||||
U.ceo_name AS ceo_name,
|
||||
U.biz_no AS biz_no,
|
||||
U.cell_phone AS phone,
|
||||
U.address AS address,
|
||||
U.email AS email,
|
||||
O.total_supply, O.total_vat, O.total_amount,
|
||||
O.total_taxfree, O.total_taxable
|
||||
FROM momo_orders O LEFT JOIN user_info U ON U.user_id = O.customer_objid
|
||||
@@ -54,17 +59,22 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
phone: process.env.MOMO_PHONE ?? "010-6624-5315",
|
||||
email: process.env.SMTP_FROM ?? "momo8443@daum.net",
|
||||
},
|
||||
items: items.map((it, idx) => ({
|
||||
seq: idx + 1,
|
||||
itemName: String(it.item_name_snap),
|
||||
unit: String(it.unit ?? "EA"),
|
||||
qty: Number(it.qty),
|
||||
unitPrice: Number(it.unit_price),
|
||||
supplyAmount: Number(it.supply_amount),
|
||||
vatAmount: Number(it.vat_amount),
|
||||
totalAmount: Number(it.total_amount),
|
||||
isTaxFree: it.is_tax_free === "Y",
|
||||
})),
|
||||
items: items.map((it, idx) => {
|
||||
const isExtra = it.kind === "DELIVERY" || it.kind === "CHARTER";
|
||||
return {
|
||||
seq: idx + 1,
|
||||
itemName: isExtra
|
||||
? (`[${it.kind === "DELIVERY" ? "택배" : "용차"}] ${String(it.extra_label || it.item_name_snap || "")}`)
|
||||
: String(it.item_name_snap || ""),
|
||||
unit: isExtra ? "" : String(it.unit ?? "EA"),
|
||||
qty: Number(it.qty),
|
||||
unitPrice: Number(it.unit_price),
|
||||
supplyAmount: Number(it.supply_amount),
|
||||
vatAmount: Number(it.vat_amount),
|
||||
totalAmount: Number(it.total_amount),
|
||||
isTaxFree: it.is_tax_free === "Y",
|
||||
};
|
||||
}),
|
||||
totals: {
|
||||
supply: Number(order.total_supply),
|
||||
vat: Number(order.total_vat),
|
||||
|
||||
Reference in New Issue
Block a user