import { w2grid, w2ui, w2popup, w2alert, w2confirm } from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js' let AUTH_ITEMS = [] let CURRENT_MEMBER_ID = null; const USE_ITEMS = [ { id: 'Y', text: '사용' }, { id: 'N', text: '미사용' } ] /* ------------------------------------------------- 공통 유틸 ------------------------------------------------- */ function getTargetMemberId() { return document.getElementById('targetMemberId')?.value?.trim() } function destroyGrid(name) { if (w2ui[name]) { w2ui[name].destroy() } } function loadBaseCode(mainCd) { return fetch(`/kngil/bbs/adm_comp.php?action=base_code&main_cd=${mainCd}`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { throw new Error(json.message || '공통코드 로딩 실패') } return json.items }) } function normalizeAuth(records) { return records.map(r => { const item = AUTH_ITEMS.find(a => a.id === r.auth_bc) return { ...r, auth_bc: item || null //객체 복사 } }) } function normalizeUseYn(records) { return records.map(r => { const item = USE_ITEMS.find(u => u.id === r.use_yn) return { ...r, use_yn: item || null } }) } /* ------------------------------------------------- 기업 관리자 페이지 Grid ------------------------------------------------- */ export async function createUserGrid(boxId, options = {}) { console.log('🔥 createUserGrid 호출됨:', boxId) // 🔥 DB에서 권한 코드 로딩 AUTH_ITEMS = (await loadBaseCode('BS100')).map(r => ({ id: r.id, text: r.text })) const { loadSummary = true, memberId = null } = options destroyGrid('userGrid') const grid = new w2grid({ name: boxId === '#detailGrid' ? 'detailGrid' : 'userGrid', box: boxId, show: { footer: true, lineNumbers: true, selectColumn: true, }, columns: [ // { // field: 'recid', // text: '#', // size: '50px', // attr: 'align=center', // sortable: true, // }, { field: 'user_id', text: 'ID', size: '90px', editable: {type : 'text'}, resizable: true, sortable: true }, { field: 'user_pw', text: 'PW', size: '90px', resizable: true, sortable: false, editable: { type: 'password' // ✅ 입력 시 ●●●● }, render() { return '********' // ✅ 항상 마스킹 } }, { field: 'user_nm', text: '이름', size: '90px', editable: {type : 'text'}, resizable: true, sortable: true }, { field: 'tel_no', text: '연락처', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true }, { field: 'email', text: 'E-mail', size: '180px', editable: {type : 'text'}, resizable: true, sortable: true }, { field: 'dept_nm', text: '부서', size: '120px', editable: {type : 'text'}, resizable: true, sortable: true }, { field: 'use_area', text: '사용량(㎡)', size: '120px', attr: 'align=right', editable: false, resizable: true, sortable: true, render(record) { const v = Number(record.use_area) || 0 return v.toLocaleString() // ✅ 1,000단위 콤마 } }, { field: 'cdt', text: '등록일', size: '100px', editable: false, resizable: true, sortable: true }, { field: 'use_yn', text: '사용', size: '90px', attr: 'align=center', sortable: true, editable: { type: 'list', items: USE_ITEMS, showAll: true, openOnFocus: true }, render(record, extra) { return extra?.value?.text || '' } }, /* ✅ 권한 콤보 (DB 연동) */ { field: 'auth_bc', text: '권한', size: '120px', sortable: true, editable: { type: 'list', items: AUTH_ITEMS, showAll: true, openOnFocus: true }, render(record, extra) { return extra?.value?.text || '' } }, { field: 'rmks', text: '비고', size: '120px', editable: { type: 'text' }, resizable: true, sortable: true } ], onEditField(event) { const pwColIndex = this.getColumn('user_pw').index // 🔥 PW 컬럼일 때만 처리 if (event.column !== pwColIndex) return event.onComplete = function () { // 🔥 현재 편집 세션의 input만 정확히 집기 const box = event.box if (!box) return const input = box.querySelector('input[type="password"]') if (!input) return // PW만 초기화 input.value = '' input.placeholder = '변경 시에만 입력' } }, onChange(event) { event.onComplete = function (ev) { const rec = grid.get(ev.recid); if (!rec) return; const field = grid.columns[ev.column].field; let val = ev.value_new; /* =============================== 📞 전화번호(tel_no) 자동 하이픈 =============================== */ if (field === 'tel_no') { let digits = String(val || '').replace(/\D/g, ''); // 입력 중이면 건드리지 않음 if (digits.length < 9) return; let formatted = digits; if (digits.length === 11) { formatted = `${digits.slice(0,3)}-${digits.slice(3,7)}-${digits.slice(7)}`; } else if (digits.length === 10) { formatted = `${digits.slice(0,3)}-${digits.slice(3,6)}-${digits.slice(6)}`; } // ✅ 화면 값 grid.set(ev.recid, { tel_no: formatted }); // ✅ 변경사항으로 "확정" (이게 핵심) grid.mergeChanges(ev.recid, { tel_no: formatted }); grid.refreshRow(ev.recid); return; } /* =============================== 공통 처리 =============================== */ if (typeof val === 'object' && val !== null) { val = val.text; } grid.set(ev.recid, { [field]: val }); grid.mergeChanges(ev.recid, { [field]: val }); }; }, records: [], }) loadData({ loadSummary, memberId }) } export function loadUsersByMember(memberId) { if (!memberId) return const g = w2ui.detailGrid if (!g) { console.error('detailGrid 없음') return } fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${memberId}`) .then(res => res.json()) .then(d => { if (d.status !== 'success') { w2alert('사용자 조회 실패') return } const records = normalizeAuth(d.records || []) records = normalizeUseYn(records) g.clear() g.add(d.records) }) .catch(err => console.error(err)) } export function setUserGridMode(mode = 'view') { const g = w2ui.userGrid if (!g) return if (mode === 'view') { g.show.toolbar = false g.show.selectColumn = false g.show.toolbarSave = false } else { g.show.toolbar = true g.show.selectColumn = true g.show.toolbarSave = true } g.refresh() } export function loadData({ loadSummary = true } = {}) { fetch('/kngil/bbs/adm_comp.php?action=list') .then(res => res.json()) .then(async d => { if (d.status !== 'success') return let records = normalizeAuth(d.records || []) records = normalizeUseYn(records) const gridName = w2ui.detailGrid ? 'detailGrid' : 'userGrid' w2ui[gridName].clear() w2ui[gridName].add(records) if (loadSummary && d.member_id) { const totalArea = await loadTotalArea() renderSummaryFromRecords({ memberId: d.member_id, records, totalArea }) } }) } function renderSummaryFromRecords({ memberId, records, totalArea }) { if (!records.length) return const first = records[0] const issuedCnt = Number(first.users_tot) || 0 const usedCnt = Number(first.users_y) || 0 const term = first.term || '' // 사용 중 면적 합계 let usedArea = 0 records.forEach(r => { if (r.use_yn?.id === 'Y') { usedArea += Number(r.use_area || 0) } }) const percent = totalArea > 0 ? Math.floor((usedArea / totalArea) * 100) : 0 /* -------- 화면 바인딩 -------- */ document.getElementById('memberId').textContent = memberId document.getElementById('planName').textContent = first.itm_nm || '-' document.getElementById('dateRange').textContent = term document.getElementById('issuedCnt').textContent = issuedCnt document.getElementById('usedCnt').textContent = usedCnt document.getElementById('usedArea').textContent = usedArea.toLocaleString() + '㎡' document.getElementById('totalArea').textContent = totalArea.toLocaleString() + '㎡' /* -------- 사용량 바 -------- */ const bar = document.getElementById('usedBar') bar.style.width = percent + '%' } // =============================== // 저장 버튼 // =============================== document.getElementById('btnSave_comp')?.addEventListener('click', () => { // 현재 사용 중인 grid const g = w2ui.detailGrid || w2ui.userGrid if (!g) return const changes = g.getChanges() if (!changes.length) { w2alert('변경된 내용이 없습니다.') return } const inserts = [] const updates = [] changes.forEach(c => { const rec = g.get(c.recid) if (!rec) return // 🔥 핵심: 원본 rec + 변경값 c 병합 const merged = { ...rec, ...c } if (rec.__isNew) { // INSERT → 전체 row 필요 inserts.push(merged) } else { // UPDATE → PK + 변경 컬럼 updates.push({ member_id : merged.member_id, user_id : merged.user_id, user_nm : merged.user_nm, dept_nm : merged.dept_nm, tel_no : merged.tel_no, email : merged.email, use_yn : merged.use_yn?.id || merged.use_yn, auth_bc : merged.auth_bc?.id || merged.auth_bc, rmks : (merged.memo !== undefined ? merged.memo : '') }) } }) console.log('INSERTS', inserts) console.log('UPDATES', updates) fetch('/kngil/bbs/adm_comp.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'save', // member_id: g.records[0]?.member_id, inserts, updates }) }) .then(res => res.text()) .then(text => { try { const json = JSON.parse(text) if (json.status === 'success') { w2alert('저장 완료') loadData() } else { w2alert(json.message || '저장 실패') } } catch (e) { console.error(text) w2alert('서버 응답 오류 (JSON 아님)') } }) }) //추가(insert) document.getElementById('btnAdd')?.addEventListener('click', () => { const g = w2ui.userGrid || w2ui.detailGrid if (!g) return const defaultAuth = AUTH_ITEMS.find(a => a.id === 'BS100500') // 일반 // 신규 row용 recid (음수로 충돌 방지) const newRecid = -Date.now() g.add({ recid: newRecid, __isNew: true, user_id: '', user_pw: '', user_nm: '', tel_no: '', email: '', dept_nm: '', use_area: 0, use_yn: USE_ITEMS[0], auth_bc: defaultAuth || null, memo: '' }, true) g.select(newRecid) g.scrollIntoView(newRecid) }) //삭제 document.getElementById('btnDelete')?.addEventListener('click', () => { const g = w2ui.detailGrid || w2ui.userGrid if (!g) return const sel = g.getSelection() if (!sel.length) { w2alert('삭제할 사용자를 선택하세요.') return } // 선택된 user_id 수집 const ids = sel .map(recid => { const r = g.get(recid) return r?.user_id }) .filter(Boolean) if (!ids.length) { w2alert('삭제 가능한 항목이 없습니다.') return } w2confirm(`선택한 ${ids.length}명의 사용자를 삭제하시겠습니까?`) .yes(() => { fetch('/kngil/bbs/adm_comp.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', // member_id: g.records[0]?.member_id, ids }) }) .then(res => res.text()) .then(text => { try { const json = JSON.parse(text) if (json.status === 'success') { w2alert('삭제 완료') loadData() } else { w2alert(json.message || '삭제 실패') } } catch (e) { console.error(text) w2alert('서버 응답 오류 (JSON 아님)') } }) }) }) function loadTotalArea(memberId) { return fetch(`/kngil/bbs/adm_comp.php?action=total_area`) .then(res => res.json()) .then(json => { if (json.status !== 'success') { throw new Error('총 구매면적 로딩 실패') } return Number(json.total_area || 0) }) } function doSearch() { const keyword = document.getElementById('schKeyword').value.trim() const type = document.getElementById('schType').value const useYn = document.getElementById('schUseYn').value let p_user_nm = '' let p_dept_nm = '' // DB로 보낼 검색 조건 if (type === 'name') { p_user_nm = keyword } else if (type === 'dept') { p_dept_nm = keyword } else if (type === '') { // 전체 검색 → 이름 OR 부서 p_user_nm = keyword p_dept_nm = keyword } // ⚠️ type === 'id' 는 DB로 안 보냄 fetch(`/kngil/bbs/adm_comp.php?action=list` + `&user_nm=${encodeURIComponent(p_user_nm)}` + `&dept_nm=${encodeURIComponent(p_dept_nm)}` + `&use_yn=${useYn}` ) .then(res => res.json()) .then(d => { if (d.status !== 'success') { w2alert('검색 실패') return } let records = d.records || [] // ID 검색은 프론트 필터 if (type === 'id' && keyword) { records = records.filter(r => (r.user_id || '').toLowerCase() .includes(keyword.toLowerCase()) ) } const g = w2ui.detailGrid || w2ui.userGrid records = normalizeAuth(records) records = normalizeUseYn(records) g.clear() g.add(records) }) } document.getElementById('btnSearch') ?.addEventListener('click', () => { const memberInput = document.getElementById('targetMemberId'); const memberId = memberInput ? memberInput.value.trim() : ''; if (memberInput && memberInput.style.display !== 'none') { CURRENT_MEMBER_ID = memberId; // ⭐ 저장 loadDataByMemberId(memberId); return; } doSearch(); }); document.getElementById('schKeyword') ?.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch() }) document.getElementById('btnBulkCreate') ?.addEventListener('click', () => { const memberId = getTargetMemberId() if (!memberId) { w2alert('회원ID를 입력하세요.') return } // 🔥 여기서 CSV URL 팝업 띄움 openBulkCreatePopup(memberId) }) function openBulkCreatePopup(memberId) { // ⭐ 방어: 객체면 member_id만 사용 if (typeof memberId === 'object' && memberId !== null) { memberId = memberId.member_id || ''; } if (!memberId) { w2alert('회원ID가 올바르지 않습니다.'); return; } w2popup.open({ title: '사용자 일괄 생성 (CSV)', width: 520, height: 220, modal: true, body: `

대상 회원ID: ${memberId}

`, onOpen(event) { event.onComplete = () => { document .getElementById('btnCsvRun') .addEventListener('click', () => { const url = document.getElementById('csvUrl').value.trim() if (!url) { w2alert('CSV URL을 입력하세요.') return } runBulkCreate(memberId, url) }) } } }) } function runBulkCreate(memberId, csvUrl) { fetch('/kngil/bbs/adm_comp.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'bulk_create', member_id: memberId, csv_url: csvUrl }) }) .then(res => res.text()) .then(text => { let d; try { d = JSON.parse(text); } catch (e) { w2alert({ title: '서버 오류', text: `
${text}
` }); return; } const successCnt = Number(d.success_cnt || 0); const failCnt = Number(d.fail_cnt || 0); let errors = d.errors || []; // 배열이지만 요소가 객체일 수 있음 → 문자열로 강제 errors = errors.map(e => { if (typeof e === 'string') return e; try { return JSON.stringify(e); } catch { return String(e); } }); // // ⭐ 핵심: object → array 변환 // if (!Array.isArray(errors) && typeof errors === 'object') { // errors = Object.values(errors); // } console.log('errors raw =', d.errors); // ❌ 전부 실패 if (successCnt === 0) { w2popup.close(); // ⭐ 먼저 CSV 팝업 닫기 w2alert(`
일괄 생성 실패

${errors.map(e => `• ${e}`).join('
')}
`); return; } // ✅ 성공 or 부분 성공 let msg = ''; msg += `✔ 성공: ${successCnt}명
`; msg += `❌ 실패: ${failCnt}명`; if (errors.length > 0) { msg += '
'; msg += '
'; errors.forEach(err => { msg += `• ${err}
`; }); msg += '
'; } w2alert({ title: '일괄 생성 결과', msg: msg }); if (CURRENT_MEMBER_ID) { loadDataByMemberId(CURRENT_MEMBER_ID); } else { loadData(); } w2popup.close(); }) .catch(err => { console.error(err); w2alert('서버 통신 중 오류가 발생했습니다.'); }); } function loadDataByMemberId(memberId) { if (!memberId) { w2alert('회원ID를 입력하세요.'); return; } fetch(`/kngil/bbs/adm_comp.php?action=list&member_id=${encodeURIComponent(memberId)}`) .then(res => res.json()) .then(async d => { if (d.status !== 'success') { w2alert(d.message || '조회 실패'); return; } let records = normalizeAuth(d.records || []); records = normalizeUseYn(records); const gridName = w2ui.detailGrid ? 'detailGrid' : 'userGrid'; w2ui[gridName].clear(); w2ui[gridName].add(records); // ✅ 상단 요약 갱신 const totalArea = await loadTotalArea(memberId); renderSummaryFromRecords({ memberId, records, totalArea }); }); }