견적서 자동 채움 보강 + 시행일자 picker 형식 정정

1) getById 응답 구조 fix
   - salesEstimateService.getById는 { ...contract_mgmt 헤더, items } 평면 객체 반환
   - 페이지가 contractInfo.header.customer_objid로 잘못 접근해 고객사·환율·통화 자동 채움이 안 됐던 버그
   - contractInfo.customer_objid 직접 접근으로 수정 (template1·template2 동일)

2) /auth/me 응답에 cellPhone/tel 추가
   - AuthService.getUserInfo는 이미 두 필드 반환하나 authController가 응답에서 누락
   - 견적서 연락처가 wace MailUtil 패턴(cell_phone 우선 → tel 폴백)으로 자동 채워지도록 노출

3) 시행일자 picker 표시 형식 정정
   - input[type="date"]는 한국 Chrome이 "YYYY. MM. DD." 강제 (CSS 변경 불가)
   - text input + hidden type=date 조합: 표시는 YYYY-MM-DD, 클릭 시 hidden picker showPicker() 트리거
   - template1 executor + template2 executor_date 모두 적용

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-11 15:07:25 +09:00
parent f88c5e3e40
commit 0afa8b03cf
3 changed files with 56 additions and 28 deletions
@@ -376,6 +376,8 @@ export class AuthController {
userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선 userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선
userTypeName: dbUserInfo.userTypeName || "일반사용자", userTypeName: dbUserInfo.userTypeName || "일반사용자",
email: dbUserInfo.email || "", email: dbUserInfo.email || "",
tel: dbUserInfo.tel || "",
cellPhone: dbUserInfo.cellPhone || "",
photo: dbUserInfo.photo, photo: dbUserInfo.photo,
locale: dbUserInfo.locale || "KR", // locale 정보 추가 locale: dbUserInfo.locale || "KR", // locale 정보 추가
deptCode: dbUserInfo.deptCode, // 추가 필드 deptCode: dbUserInfo.deptCode, // 추가 필드
@@ -80,6 +80,7 @@ export default function EstimateTemplate1Page() {
// 헤더 필드 // 헤더 필드
const [executor, setExecutor] = useState<string>(""); // 시행일자 (YYYY-MM-DD) const [executor, setExecutor] = useState<string>(""); // 시행일자 (YYYY-MM-DD)
const executorDateRef = useRef<HTMLInputElement>(null);
const [recipient, setRecipient] = useState<string>(""); // 수신처 (customer_objid) const [recipient, setRecipient] = useState<string>(""); // 수신처 (customer_objid)
const [estimateNo, setEstimateNo] = useState<string>(""); const [estimateNo, setEstimateNo] = useState<string>("");
const [contactPerson, setContactPerson] = useState<string>(""); const [contactPerson, setContactPerson] = useState<string>("");
@@ -161,15 +162,12 @@ export default function EstimateTemplate1Page() {
} }
} }
if (contractInfo?.header) { // getById 응답: { ...contract_mgmt 헤더 필드들, items: [...] } (평면 구조)
const h = contractInfo.header; if (contractInfo && !cancel) {
if (!cancel) { setExchangeRate(parseFloat(contractInfo.exchange_rate || "1") || 1);
setExchangeRate(parseFloat(h.exchange_rate || "1") || 1); setCurrencyName(contractInfo.contract_currency_name || contractInfo.contract_currency || "KRW");
// 통화명: comm_code lookup이 필요한데 detail이 raw코드만 줄 수도 있음 → 일단 코드 사용 // 수신처는 customer_objid (예: 'C_RPS001') — 견적요청의 고객사를 견적서에 자동 채움
setCurrencyName(h.contract_currency_name || h.contract_currency || "KRW"); if (contractInfo.customer_objid) setRecipient(contractInfo.customer_objid);
// 수신처는 customer_objid (예: 'C_RPS001')
if (h.customer_objid) setRecipient(h.customer_objid);
}
} }
// 2) 기존 견적 차수 수정 (templateObjid 지정) — 우선 // 2) 기존 견적 차수 수정 (templateObjid 지정) — 우선
@@ -241,7 +239,8 @@ export default function EstimateTemplate1Page() {
if (!user) return; if (!user) return;
if (templateObjidParam) return; // 기존 견적서 수정 모드는 건드리지 않음 if (templateObjidParam) return; // 기존 견적서 수정 모드는 건드리지 않음
setManagerName(prev => prev || `${user.deptName ?? ""} ${user.userName ?? ""}`.trim()); setManagerName(prev => prev || `${user.deptName ?? ""} ${user.userName ?? ""}`.trim());
setManagerContact(prev => prev || user.tel ?? ""); // wace MailUtil 패턴: cell_phone 우선, 없으면 tel
setManagerContact(prev => prev || user.cellPhone || user.tel || "");
}, [user, templateObjidParam]); }, [user, templateObjidParam]);
// ─── 저장 (wace fn_save 1:1) ───────────────────────────────── // ─── 저장 (wace fn_save 1:1) ─────────────────────────────────
@@ -511,13 +510,26 @@ export default function EstimateTemplate1Page() {
<tr> <tr>
<td className="label"></td> <td className="label"></td>
<td> <td>
{/* 표시는 YYYY-MM-DD (text), 클릭 시 숨겨진 type=date의 showPicker 트리거 */}
<div style={{ position: "relative", display: "inline-block", width: 150 }}>
<input <input
type="text"
value={executor}
readOnly
placeholder="YYYY-MM-DD"
onClick={() => { if (!readOnly) executorDateRef.current?.showPicker?.(); }}
style={{ width: "100%", cursor: readOnly ? "default" : "pointer" }}
/>
<input
ref={executorDateRef}
type="date" type="date"
value={executor} value={executor}
onChange={e => setExecutor(e.target.value)} onChange={e => setExecutor(e.target.value)}
readOnly={readOnly} tabIndex={-1}
style={{ width: 150 }} aria-hidden="true"
style={{ position: "absolute", left: 0, top: 0, width: 1, height: 1, opacity: 0, pointerEvents: "none" }}
/> />
</div>
</td> </td>
<td rowSpan={4} style={{ border: "none" }}></td> <td rowSpan={4} style={{ border: "none" }}></td>
<td rowSpan={4} style={{ textAlign: "center", border: "none", verticalAlign: "middle", padding: 0 }}> <td rowSpan={4} style={{ textAlign: "center", border: "none", verticalAlign: "middle", padding: 0 }}>
@@ -109,6 +109,7 @@ export default function EstimateTemplate2Page() {
// 헤더 // 헤더
const [executorDate, setExecutorDate] = useState<string>(""); const [executorDate, setExecutorDate] = useState<string>("");
const executorDateRef = React.useRef<HTMLInputElement>(null);
const [recipient, setRecipient] = useState<string>(""); const [recipient, setRecipient] = useState<string>("");
const [partName, setPartName] = useState<string>(""); const [partName, setPartName] = useState<string>("");
const [partObjid, setPartObjid] = useState<string>(""); const [partObjid, setPartObjid] = useState<string>("");
@@ -201,10 +202,10 @@ export default function EstimateTemplate2Page() {
console.warn("영업정보 로드 실패", e); console.warn("영업정보 로드 실패", e);
} }
} }
if (contractInfo?.header && !cancel) { // getById 응답: { ...contract_mgmt 헤더 필드들, items: [...] } (평면 구조)
const h = contractInfo.header; if (contractInfo && !cancel) {
setExchangeRate(parseFloat(h.exchange_rate || "1") || 1); setExchangeRate(parseFloat(contractInfo.exchange_rate || "1") || 1);
if (h.customer_objid) setRecipient(h.customer_objid); if (contractInfo.customer_objid) setRecipient(contractInfo.customer_objid);
} }
// 신규: contract_item[0]의 part_name을 품명/Model로 // 신규: contract_item[0]의 part_name을 품명/Model로
if (contractInfo?.items?.[0] && !cancel) { if (contractInfo?.items?.[0] && !cancel) {
@@ -454,13 +455,26 @@ export default function EstimateTemplate2Page() {
<div style={{ textAlign: "left", fontSize: "10pt", lineHeight: 2 }}> <div style={{ textAlign: "left", fontSize: "10pt", lineHeight: 2 }}>
<div> <div>
<strong> :</strong>{" "} <strong> :</strong>{" "}
{/* 표시는 YYYY-MM-DD (text), 클릭 시 숨겨진 type=date의 showPicker 트리거 */}
<span style={{ position: "relative", display: "inline-block", width: 200 }}>
<input <input
type="text"
value={executorDate}
readOnly
placeholder="YYYY-MM-DD"
onClick={() => { if (!readOnly) executorDateRef.current?.showPicker?.(); }}
style={{ width: "100%", borderBottom: "1px solid #999", padding: "2px 5px", cursor: readOnly ? "default" : "pointer" }}
/>
<input
ref={executorDateRef}
type="date" type="date"
value={executorDate} value={executorDate}
onChange={e => setExecutorDate(e.target.value)} onChange={e => setExecutorDate(e.target.value)}
readOnly={readOnly} tabIndex={-1}
style={{ width: 200, borderBottom: "1px solid #999", padding: "2px 5px" }} aria-hidden="true"
style={{ position: "absolute", left: 0, top: 0, width: 1, height: 1, opacity: 0, pointerEvents: "none" }}
/> />
</span>
</div> </div>
<div> <div>
<strong> :</strong>{" "} <strong> :</strong>{" "}