diff --git a/public/manual.html b/public/manual.html index 2ad07b0..d657a5b 100644 --- a/public/manual.html +++ b/public/manual.html @@ -561,6 +561,12 @@
  • [발주 요청] 버튼오른쪽 위 초록색 버튼. 누르면:
    ① 상태가 발주요청으로 바뀌고
    ② 공급업체 이메일로 발주서 메일이 자동 발송됩니다.
    이메일이 없으면 '메일 미발송' 안내가 떠요 — 직접 통보 필요.
  • 물건이 도착하면다음 단계인 [입고 처리] 화면에서 입고 등록
  • +

    발주서 공유 / 엑셀 다운로드

    +

    발주서 양식 위쪽에 두 버튼이 있어요:

    +
    💡 공급업체별 품목 일괄 불러오기

    품목 모달에서 '현재 발주서 공급업체만' 필터를 켜면 그 공급업체에 등록된 모든 품목이 보여요. 헤더 체크박스로 전체 선택 → [선택한 N개 추가] 누르면 한 번에 다 들어가요. 그 후 필요한 것만 남기고 [×] 로 빼면 돼요.

    diff --git a/src/app/(main)/m/admin/procurements/page.tsx b/src/app/(main)/m/admin/procurements/page.tsx index ae47ad4..2618825 100644 --- a/src/app/(main)/m/admin/procurements/page.tsx +++ b/src/app/(main)/m/admin/procurements/page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; -import { Plus, Send, Search, RefreshCcw, X } from "lucide-react"; +import { useEffect, useState, useCallback, useRef } from "react"; +import { Plus, Send, Search, RefreshCcw, X, Download, Image as ImageIcon } from "lucide-react"; import Swal from "sweetalert2"; interface ProcRow { @@ -281,8 +281,53 @@ function ProcurementForm({ detail, vendors, onSetVendor, onSetMemo, onAddPicker, onDeleteLine: (objid: string) => void; }) { const editable = detail.proc.STATUS === "OPEN"; + const formRef = useRef(null); + + const captureAndShare = async () => { + try { + const mod = await import("html-to-image"); + if (!formRef.current) return; + const dataUrl = await mod.toPng(formRef.current, { backgroundColor: "#ffffff", pixelRatio: 2 }); + const blob = await (await fetch(dataUrl)).blob(); + const file = new File([blob], `${detail.proc.PROC_NO}_발주서.png`, { type: "image/png" }); + const navAny = navigator as Navigator & { canShare?: (data: { files: File[] }) => boolean }; + if (navAny.canShare && navAny.canShare({ files: [file] })) { + await navigator.share({ files: [file], title: `${detail.proc.PROC_NO} 발주서`, text: detail.proc.VENDOR_NAME ?? "" }); + return; + } + const a = document.createElement("a"); + a.href = dataUrl; + a.download = `${detail.proc.PROC_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: "잠시 후 다시 시도하세요." }); + } + }; + return (
    + {/* 공유/엑셀 버튼 — 캡처 영역 밖 */} +
    + + + 엑셀 다운로드 + +
    + +

    발 주 서

    @@ -376,6 +421,7 @@ function ProcurementForm({ detail, vendors, onSetVendor, onSetMemo, onAddPicker, {detail.proc.MEMO || 없음}
    )} +
    {/* /formRef capture area */}
    ); } diff --git a/src/app/api/m/items/list/route.ts b/src/app/api/m/items/list/route.ts index 1fbf15c..6c53e16 100644 --- a/src/app/api/m/items/list/route.ts +++ b/src/app/api/m/items/list/route.ts @@ -94,7 +94,7 @@ export async function POST(req: NextRequest) { TO_CHAR(I.regdate, 'YYYY-MM-DD') AS "REGDATE" FROM momo_items I LEFT JOIN momo_makers M ON I.maker_objid = M.objid - LEFT JOIN supply_mng V ON I.vendor_objid = V.objid + LEFT JOIN supply_mng V ON I.vendor_objid = V.objid::text WHERE ${conditions.join(" AND ")} ORDER BY I.item_name ASC `; diff --git a/src/app/api/m/procurements/excel/[id]/route.ts b/src/app/api/m/procurements/excel/[id]/route.ts new file mode 100644 index 0000000..3e9d408 --- /dev/null +++ b/src/app/api/m/procurements/excel/[id]/route.ts @@ -0,0 +1,86 @@ +// 매입 발주서 엑셀 다운로드 — 거래명세표 양식과 별개로 발주서 양식 +import { NextRequest, NextResponse } from "next/server"; +import { queryOne, queryRows } from "@/lib/db"; +import { requireMomoAdmin } from "@/lib/momo-guard"; +import * as XLSX from "xlsx"; + +export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const g = await requireMomoAdmin(); + if (g instanceof NextResponse) return g; + + const { id } = await ctx.params; + const proc = await queryOne>( + `SELECT P.proc_no, TO_CHAR(P.proc_date,'YYYY-MM-DD') AS proc_date, + P.status, P.total_amount, P.memo, + V.supply_name AS vendor_name, + V.charge_user_name AS vendor_contact, + V.supply_tel_no AS vendor_phone, + V.email AS vendor_email, + V.supply_address AS vendor_address + FROM momo_procurements P + LEFT JOIN supply_mng V ON P.vendor_objid = V.objid::text + WHERE P.objid = $1`, + [id] + ); + if (!proc) return NextResponse.json({ success: false, message: "찾을 수 없음" }, { status: 404 }); + + const items = await queryRows>( + `SELECT I.item_code, I.item_name, I.unit, PI.qty, PI.cost_price, PI.total_amount + FROM momo_procurement_items PI JOIN momo_items I ON PI.item_objid = I.objid + WHERE PI.proc_objid = $1 ORDER BY PI.objid`, + [id] + ); + + // 발주서 시트 작성 (이미지 양식 모방) + const wb = XLSX.utils.book_new(); + const aoa: (string | number)[][] = []; + aoa.push(["", "", "발 주 서", "", "", "", ""]); + aoa.push([]); + aoa.push(["분류번호", "매입발주", "", "", "", "", ""]); + aoa.push(["발주서번호", String(proc.proc_no), "", "", "", "", ""]); + aoa.push(["발주일", String(proc.proc_date), "", "", "", "", ""]); + aoa.push(["공급업체", String(proc.vendor_name ?? "-"), "", "", "", "", ""]); + aoa.push(["연락처", String(proc.vendor_phone ?? "-"), "", "", "", "", ""]); + aoa.push(["이메일", String(proc.vendor_email ?? "-"), "", "", "", "", ""]); + aoa.push([]); + aoa.push(["1. 물품의 표시"]); + aoa.push(["#", "품목코드", "품명", "단위", "수량", "단가", "금액"]); + items.forEach((it, idx) => { + aoa.push([ + idx + 1, + String(it.item_code ?? ""), + String(it.item_name ?? ""), + String(it.unit ?? "EA"), + Number(it.qty), + Number(it.cost_price), + Number(it.total_amount), + ]); + }); + aoa.push([]); + aoa.push(["", "", "", "", "", "총액", Number(proc.total_amount)]); + aoa.push(["", "", "", "", "", "(V.A.T 별도)", ""]); + aoa.push([]); + aoa.push(["2. 비고"]); + aoa.push([String(proc.memo ?? "")]); + aoa.push([]); + aoa.push([]); + aoa.push(["", "", "상기와 같이 발주합니다."]); + aoa.push(["", "", String(proc.proc_date)]); + aoa.push([]); + aoa.push(["", "", "발주자: " + (process.env.MOMO_COMPANY_NAME ?? "모모유통") + " " + (process.env.MOMO_COMPANY_CEO ?? "")]); + aoa.push(["", "", "전화: " + (process.env.MOMO_PHONE ?? ""), "이메일: " + (process.env.MOMO_EMAIL ?? "")]); + + const ws = XLSX.utils.aoa_to_sheet(aoa); + ws["!cols"] = [ + { wch: 6 }, { wch: 14 }, { wch: 28 }, { wch: 8 }, { wch: 10 }, { wch: 12 }, { wch: 14 }, + ]; + XLSX.utils.book_append_sheet(wb, ws, "발주서"); + + const buf = XLSX.write(wb, { type: "buffer", bookType: "xlsx" }) as Buffer; + return new NextResponse(new Uint8Array(buf), { + headers: { + "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Content-Disposition": `attachment; filename="발주서_${proc.proc_no}.xlsx"`, + }, + }); +}