import { w2grid, w2ui, w2popup, w2alert, w2confirm } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js' /* ------------------------------------------------- 공통 유틸 ------------------------------------------------- */ function destroyGrid(name) { if (w2ui[name]) w2ui[name].destroy() } function fmtNum(val) { return Number(val || 0).toLocaleString() } function fmtPrice(val) { const v = Number(val || 0) return Number.isInteger(v) ? v.toLocaleString() : v.toLocaleString(undefined, { minimumFractionDigits: 2 }) } /* ------------------------------------------------- 서비스 등록 팝업 ------------------------------------------------- */ export function openServiceRegisterPopup(ctx) { destroyGrid('serviceGrid') destroyGrid('productList') w2popup.open({ title: '서비스 등록', width: 1300, height: 720, modal: true, body: `
회원ID : ${ctx.memberId}
회원명 : ${ctx.memberName}
회사명 : ${ctx.company}
사업자번호 : ${ctx.bizNo}
상품 선택 (더블클릭)
공급가액 0
부가세 0
결제금액 0
`, onOpen(event) { event.onComplete = () => { createServiceGrid() createProductList() const buyDateInput = document.getElementById('buyDate') const today = new Date().toISOString().slice(0, 10) // 기본 구매일 = 오늘 buyDateInput.value = today // 자동 조회 loadExistingPurchase(ctx.memberId, today) // 수동 조회 버튼 document.getElementById('btnSearchBuy').addEventListener('click', () => { const buyDate = buyDateInput.value if (!buyDate) { w2alert('구매일을 선택하세요') return } loadExistingPurchase(ctx.memberId, buyDate) }) document.getElementById('btnDeleteRow') .addEventListener('click', deleteSelectedRows) document.getElementById('btnSaveService') .addEventListener('click', () => saveService(ctx)) } } }) } /* ------------------------------------------------- 서비스 Grid ------------------------------------------------- */ function createServiceGrid() { new w2grid({ name: 'serviceGrid', box: '#serviceGrid', show: { footer: true, selectColumn: true }, multiSelect: false, columns: [ { field: 'recid', text: 'NO', size: '50px', attr: 'align=center' }, { field: 'itm_nm', text: '상품명', size: '120px', editable: { type: 'text' } }, { field: 'area', text: '제공량', size: '120px', editable: { type: 'text' } }, { field: 'itm_cd', text: '상품코드', hidden: true }, { field: 'itm_amt', text: '단가', size: '100px', attr: 'align=right', render: r => fmtPrice(r.itm_amt) }, { field: 'itm_qty', text: '수량', size: '80px', attr: 'align=right', editable: { type: 'int' }, render: r => fmtNum(r.itm_qty) }, { field: 'dis_rt', text: '할인율', size: '80px', editable: { type: 'int' } }, { field: 'supply', text: '공급가액', size: '120px', attr: 'align=right', render: r => fmtNum(r.supply) }, { field: 'vat_amt', text: '부가세', size: '100px', attr: 'align=right', render: r => fmtNum(r.vat_amt) }, { field: 'sum_amt', text: '결제금액', size: '120px', attr: 'align=right', render: r => fmtNum(r.sum_amt) }, { field: 'itm_area', text: '적용면적(m²)', size: '100px', attr: 'align=right', editable: { type: 'int' }, render: r => fmtNum(r.itm_area) }, { field: 'add_area', text: '추가면적(m²)', size: '110px', attr: 'align=right', editable: { type: 'int' }, render: r => fmtNum(r.add_area) }, { field: 'sum_area', text: '합계면적(m²)', size: '110px', attr: 'align=right', editable: { type: 'int' }, render: r => fmtNum(r.sum_area) }, { field: 'end_dt', text: '만료일자', size: '110px', editable: { type: 'date' } }, { field: 'ok_yn', text: '승인여부', size: '80px', attr: 'align=center', editable: { type: 'combo', items: [ { id: 'N', text: '미승인' }, { id: 'Y', text: '승인' } ], openOnFocus: true }, render(record) { if (record.ok_yn === 'Y') return '승인' return '미승인' } }, { field: 'rmks', text: '비고', size: '150px', editable: { type: 'text' } } ], records: [], /* ----------------------------- 일반 필드 처리 (date 제외) ----------------------------- */ onChange(event) { event.onComplete = (ev) => { const g = w2ui.serviceGrid const field = g.columns[ev.column].field let val = ev.value_new if (typeof val === 'object' && val !== null) val = val.id g.set(ev.recid, { [field]: val }) if (['itm_qty','dis_rt','add_area'].includes(field)) { recalcRow(ev.recid) } } }, /* ----------------------------- 날짜 전용 처리 (핵심) ----------------------------- */ onEditField(event) { if (event.field !== 'end_dt') return; event.onComplete = () => { const g = w2ui.serviceGrid; const recid = event.recid; const input = document.querySelector( `#grid_${g.name}_edit input` ); if (!input) return; input.addEventListener('change', () => { // 🔥 날짜는 문자열 그대로 즉시 확정 g.set(recid, { end_dt: input.value // YYYY-MM-DD }); // 🔥 w2ui 방식으로 edit 종료 input.blur(); g.refreshRow(recid); }); }; } }); } /* ------------------------------------------------- 상품 목록 ------------------------------------------------- */ function createProductList() { new w2grid({ name: 'productList', box: '#productList', url: '/admin/api/product', columns: [ { field: 'itm_nm', text: '상품명', size: '120px' }, { field: 'area', text: '제공량', size: '100px', attr: 'align=right', render(record) { return Number(record.area || 0).toLocaleString() } }, { field: 'itm_amt', text: '단가', size: '120px', attr: 'align=right', render(record) { const v = Number(record.itm_amt || 0) // 정수면 소수점 제거 return Number.isInteger(v) ? v.toLocaleString() : v.toLocaleString(undefined, { minimumFractionDigits: 2 }) } } ], onDblClick(event) { event.onComplete = () => { const recid = event.detail?.recid if (!recid) return const rec = this.get(recid) if (!rec) return addServiceFromProduct(rec) } } }) } /* ------------------------------------------------- 상품 → 서비스 Grid 추가 ------------------------------------------------- */ function addServiceFromProduct(p) { const g = w2ui.serviceGrid const recid = g.records.length + 1 const qty = 1 const area = Number(p.area || 0) const unit = Number(p.itm_amt || 0) const today = new Date() const endDt = `${today.getFullYear()}-12-31` g.add({ recid, itm_cd: p.itm_cd, itm_nm: p.itm_nm, area: area, // 제공량 itm_qty: qty, itm_amt: unit, dis_rt: 0, itm_area: area * qty, add_area: 0, sum_area: area * qty, supply: unit * qty, vat_amt: Math.floor(unit * qty * 0.1), sum_amt: Math.floor(unit * qty * 1.1), end_dt: endDt, ok_yn: 'N', rmks: '', _new: true }) recalcRow(recid) } /* ------------------------------------------------- 행 삭제 ------------------------------------------------- */ function deleteSelectedRows() { const g = w2ui.serviceGrid const sel = g.getSelection() if (!sel.length) { w2alert('삭제할 서비스를 선택하세요') return } const recid = sel[0] const row = g.get(recid) if (!row || !row.sq_no) { w2alert('삭제할 수 없는 항목입니다.') return } if (row.ok_yn === 'Y') { w2alert('승인된 항목은 삭제할 수 없습니다.') return } w2confirm('선택한 서비스를 삭제하시겠습니까?') .yes(() => { deleteServiceImmediately(row) }) } function deleteServiceImmediately(row) { console.log({ action: 'delete', member_id: row.member_id, sq_no: row.sq_no }) fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', member_id: row.member_id, // ✅ 여기 중요 sq_no: row.sq_no }) }) .then(res => res.json()) .then(res => { if (res.status === 'success') { w2alert('삭제되었습니다.') w2ui.serviceGrid.remove(row.recid) calcSummary() } else { w2alert(res.message || '삭제 실패') } }) .catch(() => w2alert('서버 오류')) } function isServiceItem(r) { return r.itm_cd && r.itm_cd.startsWith('ZET01') || r.itm_nm === '서비스' } /* ------------------------------------------------- 기존 구매 불러오기 ------------------------------------------------- */ function loadExistingPurchase(memberId, buyDate) { fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'list', member_id: memberId, buy_date: buyDate }) }) .then(res => res.json()) .then(json => { if (json.status !== 'success') { w2alert(json.message || '조회 실패') return } const g = w2ui.serviceGrid g.clear() json.records.forEach((r, i) => { g.add({ recid: i + 1, member_id: memberId, sq_no: r.sq_no, itm_cd: r.itm_cd, itm_nm: r.itm_nm, itm_area: Number(r.itm_area || 0), add_area: Number(r.add_area || 0), itm_amt: Number(r.itm_amt), itm_qty: Number(r.itm_qty), // area area: isServiceItem(r) ? 1 : (r.itm_qty ? Number(r.itm_area) / Number(r.itm_qty) : 0), dis_rt: Number(r.dis_rt || 0), // 문자열 → 숫자 supply: Number(r.buy_amt), vat_amt: Number(r.vat_amt), sum_amt: Number(r.sum_amt), end_dt: r.end_dt || '', ok_yn: r.ok_yn || 'N', rmks: r.rmks ?? '', // undefined 방지 _existing: true }) g.set(i + 1, { w2ui: { style: 'background:#f3f3f3;color:#555' } }) }) g.refresh() // 🔥 모든 행 재계산 g.records.forEach(r => { if (!r._deleted) { recalcRow(r.recid) } }) calcSummary() }) .catch(() => w2alert('서버 오류')) } /* ------------------------------------------------- 행 단위 재계산 ------------------------------------------------- */ function recalcRow(recid) { const g = w2ui.serviceGrid const r = g.get(recid) if (!r) return const qty = Number(r.itm_qty || 0) const unit = Number(r.itm_amt || 0) const rate = Number(r.dis_rt || 0) const area = Number(r.area || 0) const add = Number(r.add_area || 0) // 🔥 적용면적 = 제공량 * 수량 r.itm_area = area * qty // 🔥 공급가액 = 단가 * 수량 * (1 - 할인율) const base = unit * qty const discount = Math.floor(base * (rate / 100)) r.supply = base - discount // 🔥 부가세 / 결제금액 r.vat_amt = Math.floor(r.supply * 0.1) r.sum_amt = r.supply + r.vat_amt // 🔥 합계면적 r.sum_area = r.itm_area + add g.refreshRow(recid) calcSummary() } /* ------------------------------------------------- 합계 계산 ------------------------------------------------- */ function calcSummary() { let supply = 0 let vat = 0 let total = 0 w2ui.serviceGrid.records.forEach(r => { if (r._deleted) return supply += Number(r.supply || 0) vat += Number(r.vat_amt || 0) total += Number(r.sum_amt || 0) }) document.getElementById('sumSupply').innerText = supply.toLocaleString() document.getElementById('sumVat').innerText = vat.toLocaleString() document.getElementById('sumTotal').innerText = total.toLocaleString() } function saveService(ctx) { const buyDate = document.getElementById('buyDate').value if (!buyDate) { w2alert('구매일을 선택하세요') return } const g = w2ui.serviceGrid // 🔥🔥🔥 핵심: 현재 편집중인 셀 강제 반영 g.save() const items = g.records.map(r => ({ sq_no: r.sq_no || null, itm_cd: r.itm_cd, itm_area: r.itm_area, add_area: r.add_area || 0, itm_amt: r.itm_amt, itm_qty: r.itm_qty, dis_rt: r.dis_rt, buy_amt: r.supply, vat_amt: r.vat_amt, sum_amt: r.sum_amt, end_dt: dateToYMD(r.end_dt), ok_yn: r.ok_yn || 'N', rmks: r.rmks, _new: r._new || false, _existing: r._existing || false, _deleted: r._deleted || false })) fetch('/admin/api/service', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'save', member_id: ctx.memberId, buy_date: buyDate, items }) }) .then(res => res.json()) .then(res => { if (res.status === 'success') { w2alert('저장되었습니다.') loadExistingPurchase(ctx.memberId, buyDate) } else { w2alert(res.message || '저장 실패') } }) } function normalizeDate(val) { if (!val) return null // 이미 YYYY-MM-DD면 그대로 if (/^\d{4}-\d{2}-\d{2}$/.test(val)) { return val } // MM/DD/YYYY → YYYY-MM-DD const d = new Date(val) if (isNaN(d)) return null return d.toISOString().slice(0, 10) } function dateToYMD(val) { if (!val) return null // Date 객체 if (val instanceof Date) { return [ val.getFullYear(), String(val.getMonth() + 1).padStart(2, '0'), String(val.getDate()).padStart(2, '0') ].join('-') } // 문자열 (MM/DD/YYYY or YYYY-MM-DD) if (typeof val === 'string') { // 이미 YYYY-MM-DD if (/^\d{4}-\d{2}-\d{2}$/.test(val)) { return val } // MM/DD/YYYY const d = new Date(val) if (!isNaN(d)) { return [ d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0') ].join('-') } } return null }