902118d46e
- nodemailer + pdf-lib로 실제 SMTP 발송. mail_log INSERT(is_send='N') → 발송 → 성공 시 UPDATE(is_send='Y'), 실패 시 UPDATE(error_log). SMTP_SEND_SWITCH='N'면 발송 스킵.
- SMTP 3계정(ERP/SALES/PURCHASE) host/user/pw 환경변수 분리. 견적서는 SALES. dev는 backend-node/.env, 운영은 deploy/onpremise + docker/prod + docker/deploy 3개 compose에 environment 매핑(호스트 .env에서 실값 주입).
- 다이얼로그(EstimateMailDialog): wace estimateMailFormPopup.jsp 1:1. 고객사 담당자 체크박스 + To/CC/제목/내용 자동채움(GET /sales/estimate/mail-info/:id + .../customer/:id/managers). hasBaseEst/hasAddEst 분기로 PDF 첨부 안내. 본문은 다이얼로그 plain text 입력 → <br> 변환.
- PDF 첨부: 메일 다이얼로그가 hidden iframe으로 최신 차수 template1/2 페이지를 렌더 → window.fn_generateAndUploadPdf(cb) 글로벌 → jsPDF.output('datauristring') base64 추출 → 한 요청에 전달. backend가 견적 PDF + estimate02 N건 pdf-lib로 합본 첨부.
- PDF 캡처 수신처 누락 픽스: CustomerSelect의 /sales/customers 옵션 fetch가 iframe에서 dataLoaded=true 뒤에 끝나 셀렉트 라벨이 빈 상태로 캡처되던 현상. fetchCustomers export + template1/2 setLoading(false) 직전 await + onclone에서 [role="combobox"] 라이브 DOM 텍스트 fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface CustomerSelectProps {
|
|
/** contract_mgmt.customer_objid 형식 ('C_{customer_code}') — 운영 데이터 호환 */
|
|
value: string;
|
|
onValueChange: (value: string) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
let cached: SmartSelectOption[] | null = null;
|
|
let inflight: Promise<SmartSelectOption[]> | null = null;
|
|
|
|
// 운영 wace 데이터: contract_mgmt.customer_objid = 'C_' + customer_mng.customer_code
|
|
// (이전엔 customer_mng.id padded로 매핑했으나 운영 데이터와 어긋났음 — 26C-0801 라온기술/정림유리 미스매치 사례)
|
|
export const fetchCustomers = async (): Promise<SmartSelectOption[]> => {
|
|
if (cached) return cached;
|
|
if (inflight) return inflight;
|
|
inflight = (async () => {
|
|
const res = await apiClient.get("/sales/customers");
|
|
const rows = (res.data?.data ?? []) as any[];
|
|
cached = rows
|
|
.filter((r) => r.customer_code && r.customer_name)
|
|
.map((r) => ({
|
|
code: `C_${r.customer_code}`,
|
|
label: String(r.customer_name),
|
|
}));
|
|
return cached!;
|
|
})();
|
|
try {
|
|
return await inflight;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
};
|
|
|
|
export function CustomerSelect({
|
|
value, onValueChange, placeholder = "거래처 선택", disabled, className,
|
|
}: CustomerSelectProps) {
|
|
const [options, setOptions] = useState<SmartSelectOption[]>(cached ?? []);
|
|
|
|
useEffect(() => {
|
|
let alive = true;
|
|
fetchCustomers().then((opts) => { if (alive) setOptions(opts); }).catch(() => {});
|
|
return () => { alive = false; };
|
|
}, []);
|
|
|
|
return (
|
|
<SmartSelect
|
|
options={options}
|
|
value={value}
|
|
onValueChange={onValueChange}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
className={className}
|
|
/>
|
|
);
|
|
}
|