giftcon_dev/public/assets/js/mypage_renew.js

964 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* public/assets/js/mypage_renew.js */
(function () {
'use strict';
const CFG = window.mypageRenew || {};
const URLS = (CFG.urls || {});
const $ = (sel, root = document) => root.querySelector(sel);
function csrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
}
function isMobileUA() {
const ua = navigator.userAgent || '';
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
}
// -----------------------------
// 1) 재인증 타이머 (항상 실행)
// - view는 data-expire(unix ts), 기존은 data-until(ISO)
// - 둘 다 지원하도록 수정
// -----------------------------
(function reauthCountdown() {
const box = document.querySelector('.mypage-reauth--countdown');
const out = document.getElementById('reauthCountdown');
if (!box || !out) return;
const untilStr = (box.getAttribute('data-until') || '').trim();
const expireTs = parseInt((box.getAttribute('data-expire') || '0').trim(), 10);
const remainFallback = parseInt(box.getAttribute('data-remain') || '0', 10);
function parseUntilMsFromISO(s) {
if (!s) return null;
const isoLike = s.includes('T') ? s : s.replace(' ', 'T');
const ms = Date.parse(isoLike);
return Number.isFinite(ms) ? ms : null;
}
let untilMs = parseUntilMsFromISO(untilStr);
// ✅ data-expire가 있으면 우선 사용 (unix seconds)
if (!untilMs && Number.isFinite(expireTs) && expireTs > 0) {
untilMs = expireTs * 1000;
}
let remain = Math.max(0, Number.isFinite(remainFallback) ? remainFallback : 0);
let timer = null;
let expiredOnce = false;
function fmt(sec) {
sec = Math.max(0, sec | 0);
const m = Math.floor(sec / 60);
const s = sec % 60;
return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
function getRemainSec() {
if (untilMs !== null) {
const diffMs = untilMs - Date.now();
return Math.max(0, Math.floor(diffMs / 1000));
}
return Math.max(0, remain);
}
async function onExpiredOnce() {
if (expiredOnce) return;
expiredOnce = true;
await showMsg(
"인증 허용 시간이 만료되었습니다.\n\n보안을 위해 다시 재인증이 필요합니다.",
{ type: 'alert', title: '인증 만료' }
);
window.location.href = URLS.gateReset || '/mypage/info';
}
function tick() {
const sec = getRemainSec();
out.textContent = fmt(sec);
if (sec <= 0) {
if (timer) clearInterval(timer);
onExpiredOnce();
return;
}
if (untilMs === null) remain -= 1;
}
tick();
timer = setInterval(tick, 1000);
})();
// -------------------------------------------
// 2) PASS 연락처 변경 (기존 로직 유지)
// -------------------------------------------
(function phoneChange() {
const btn = document.querySelector('[data-action="phone-change"]');
if (!btn) return;
const readyUrl = btn.getAttribute('data-ready-url') || URLS.passReady;
if (!readyUrl) return;
function openIframeModal(popupName = 'danal_authtel_popup', w = 420, h = 750) {
const old = document.getElementById(popupName);
if (old) old.remove();
const wrap = document.createElement('div');
wrap.id = popupName;
wrap.innerHTML = `
<div class="danal-modal-dim" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:200000;background:#000;opacity:.55"></div>
<div class="danal-modal-box" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:${w}px;height:${h}px;background:#fff;border-radius:12px;z-index:200001;overflow:hidden;box-shadow:0 18px 60px rgba(0,0,0,.35);">
<div style="height:46px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;background:rgba(0,0,0,.04);border-bottom:1px solid rgba(0,0,0,.08);">
<div style="font-weight:900;font-size:13px;color:#111;">PASS 본인인증</div>
<button type="button" id="${popupName}_close"
aria-label="인증창 닫기"
style="width:34px;height:34px;border-radius:10px;border:1px solid rgba(0,0,0,.12);background:#fff;cursor:pointer;font-size:18px;line-height:1;color:#111;">×</button>
</div>
<iframe id="${popupName}_iframe" name="${popupName}_iframe" style="width:100%;height:calc(100% - 46px);border:none"></iframe>
</div>
`;
document.body.appendChild(wrap);
function removeModal() {
const el = document.getElementById(popupName);
if (el) el.remove();
}
async function askCancelAndGo() {
const ok = await showMsg(
"인증을 중단하시겠습니까?\n\n닫으면 현재 변경 진행이 취소됩니다.",
{
type: "confirm",
title: "인증취소",
closeOnBackdrop: false,
closeOnX: false,
closeOnEsc: false,
}
);
if (!ok) return;
const ifr = document.getElementById(popupName + "_iframe");
if (ifr) ifr.src = "about:blank";
removeModal();
}
const closeBtn = wrap.querySelector('#' + popupName + '_close');
if (closeBtn) closeBtn.addEventListener('click', askCancelAndGo);
window.closeIframe = function () {
removeModal();
};
return popupName + '_iframe';
}
function postToIframe(url, targetName, fieldsObj) {
const temp = document.createElement('form');
temp.method = 'POST';
temp.action = url;
temp.target = targetName;
const csrf = document.createElement('input');
csrf.type = 'hidden';
csrf.name = '_token';
csrf.value = csrfToken();
temp.appendChild(csrf);
const fields = document.createElement('input');
fields.type = 'hidden';
fields.name = 'fields';
fields.value = JSON.stringify(fieldsObj || {});
temp.appendChild(fields);
const plat = document.createElement('input');
plat.type = 'hidden';
plat.name = 'platform';
plat.value = isMobileUA() ? 'mobile' : 'web';
temp.appendChild(plat);
document.body.appendChild(temp);
temp.submit();
temp.remove();
}
window.addEventListener('message', async (ev) => {
const d = ev.data || {};
if (d.type !== 'danal_result') return;
if (typeof window.closeIframe === 'function') window.closeIframe();
await showMsg(
d.message || (d.ok ? '본인인증이 완료되었습니다.' : '본인인증에 실패했습니다.'),
{ type: 'alert', title: d.ok ? '인증 완료' : '인증 실패' }
);
if (d.redirect) window.location.href = d.redirect;
});
btn.addEventListener('click', async () => {
const ok = await showMsg(
`연락처를 변경하시겠습니까?
• PASS 본인인증은 가입자 본인 명의 휴대전화로만 가능합니다.
• 인증 정보가 기존 회원정보와 일치하지 않으면 변경할 수 없습니다.
계속 진행할까요?`,
{ type: 'confirm', title: '연락처 변경' }
);
if (!ok) return;
btn.disabled = true;
try {
const res = await fetch(readyUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken(),
'Accept': 'application/json',
},
body: JSON.stringify({ purpose: 'mypage_phone_change' }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data.ok === false) {
await showMsg(data.message || '본인인증 준비에 실패했습니다.', { type: 'alert', title: '오류' });
return;
}
if (data.reason === 'danal_ready' && data.popup && data.popup.url) {
const targetName = openIframeModal('danal_authtel_popup', 420, 750);
postToIframe(data.popup.url, targetName, data.popup.fields || {});
return;
}
await showMsg('ready 응답이 올바르지 않습니다. 서버 응답 형식을 확인해 주세요.', { type: 'alert', title: '오류' });
} catch (e) {
await showMsg('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', { type: 'alert', title: '오류' });
} finally {
btn.disabled = false;
}
});
})();
// -------------------------------------------
// 공통: 모달 스타일/유틸
// -------------------------------------------
function ensureCommonModalStyle(styleId, cssText) {
if (document.getElementById(styleId)) return;
const st = document.createElement('style');
st.id = styleId;
st.textContent = cssText;
document.head.appendChild(st);
}
function makeErrorSetter(wrap, errSel) {
const errBox = $(errSel, wrap);
return function setError(message) {
if (!errBox) return;
const msg = (message || '').trim();
if (msg === '') {
errBox.style.display = 'none';
errBox.textContent = '';
return;
}
errBox.textContent = msg;
errBox.style.display = 'block';
};
}
// -------------------------------------------
// 3) 비밀번호 변경 레이어 팝업 (기존 유지 + 내부 에러)
// -------------------------------------------
(function passwordChange() {
const trigger = document.querySelector('[data-action="pw-change"]');
if (!trigger) return;
const postUrl = URLS.passwordUpdate;
if (!postUrl) {
trigger.addEventListener('click', async () => {
await showMsg('비밀번호 변경 URL이 설정되지 않았습니다.', { type: 'alert', title: '오류' });
});
return;
}
ensureCommonModalStyle('mypagePwModalStyle', `
.mypage-pwmodal{position:fixed;inset:0;z-index:220000;display:flex;align-items:center;justify-content:center}
.mypage-pwmodal__dim{position:absolute;inset:0;background:#000;opacity:.55}
.mypage-pwmodal__box{position:relative;width:min(420px,calc(100% - 28px));background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 18px 60px rgba(0,0,0,.35)}
.mypage-pwmodal__hd{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:rgba(0,0,0,.04);border-bottom:1px solid rgba(0,0,0,.08)}
.mypage-pwmodal__ttl{font-weight:900;font-size:14px;color:#111}
.mypage-pwmodal__close{width:34px;height:34px;border-radius:10px;border:1px solid rgba(0,0,0,.12);background:#fff;cursor:pointer;font-size:18px;line-height:1;color:#111}
.mypage-pwmodal__bd{padding:14px}
.mypage-pwmodal__row{margin-top:10px}
.mypage-pwmodal__label{display:block;font-size:12px;font-weight:800;color:#667085;margin-bottom:6px}
.mypage-pwmodal__inp{width:100%;height:42px;border-radius:12px;border:1px solid #e5e7eb;padding:0 12px;font-size:14px;outline:none}
.mypage-pwmodal__hint{margin-top:10px;font-size:12px;color:#667085;line-height:1.4}
.mypage-pwmodal__error{margin-top:12px;padding:10px 12px;border-radius:12px;background:rgba(220,38,38,.08);border:1px solid rgba(220,38,38,.25);color:#b91c1c;font-weight:800;font-size:12px;display:none;white-space:pre-line}
.mypage-pwmodal__ft{display:flex;gap:10px;padding:12px 14px;border-top:1px solid rgba(0,0,0,.08);background:#fff}
.mypage-pwmodal__btn{flex:1;height:42px;border-radius:12px;border:1px solid rgba(0,0,0,.12);background:#fff;font-weight:900;cursor:pointer}
.mypage-pwmodal__btn--primary{border:none;background:#111;color:#fff}
.mypage-pwmodal__btn[disabled]{opacity:.6;cursor:not-allowed}
`);
function validateNewPw(pw) {
if (!pw) return '비밀번호를 입력해 주세요.';
if (pw.length < 8) return '비밀번호는 8자리 이상이어야 합니다.';
if (pw.length > 20) return '비밀번호는 20자리를 초과할 수 없습니다.';
const re = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,20}$/;
if (!re.test(pw)) return '비밀번호는 영문+숫자+특수문자를 포함해야 합니다.';
return '';
}
function openModal() {
const old = $('#mypagePwModal');
if (old) old.remove();
const wrap = document.createElement('div');
wrap.className = 'mypage-pwmodal';
wrap.id = 'mypagePwModal';
wrap.innerHTML = `
<div class="mypage-pwmodal__dim"></div>
<div class="mypage-pwmodal__box" role="dialog" aria-modal="true" aria-labelledby="mypagePwModalTitle">
<div class="mypage-pwmodal__hd">
<div class="mypage-pwmodal__ttl" id="mypagePwModalTitle">비밀번호 변경</div>
<button type="button" class="mypage-pwmodal__close" aria-label="닫기">×</button>
</div>
<div class="mypage-pwmodal__bd">
<div class="mypage-pwmodal__row">
<label class="mypage-pwmodal__label">현재 비밀번호</label>
<input type="password" class="mypage-pwmodal__inp" id="pw_current" autocomplete="current-password" />
</div>
<div class="mypage-pwmodal__row">
<label class="mypage-pwmodal__label">변경할 비밀번호</label>
<input type="password" class="mypage-pwmodal__inp" id="pw_new" autocomplete="new-password" />
</div>
<div class="mypage-pwmodal__row">
<label class="mypage-pwmodal__label">변경할 비밀번호 확인</label>
<input type="password" class="mypage-pwmodal__inp" id="pw_new2" autocomplete="new-password" />
</div>
<div class="mypage-pwmodal__hint">
• 8~20자<br/>
• 영문 + 숫자 + 특수문자 포함
</div>
<div class="mypage-pwmodal__error" id="pw_error"></div>
</div>
<div class="mypage-pwmodal__ft">
<button type="button" class="mypage-pwmodal__btn" data-act="cancel">취소</button>
<button type="button" class="mypage-pwmodal__btn mypage-pwmodal__btn--primary" data-act="submit">변경</button>
</div>
</div>
`;
document.body.appendChild(wrap);
const setError = makeErrorSetter(wrap, '#pw_error');
function close() { wrap.remove(); }
// ✅ 닫기: X / 취소만
wrap.querySelector('.mypage-pwmodal__close')?.addEventListener('click', close);
wrap.querySelector('[data-act="cancel"]')?.addEventListener('click', close);
setTimeout(() => $('#pw_current', wrap)?.focus(), 10);
async function submit() {
setError('');
const cur = ($('#pw_current', wrap)?.value || '').trim();
const pw1 = ($('#pw_new', wrap)?.value || '').trim();
const pw2 = ($('#pw_new2', wrap)?.value || '').trim();
if (!cur) { setError('현재 비밀번호를 입력해 주세요.'); $('#pw_current', wrap)?.focus(); return; }
const err = validateNewPw(pw1);
if (err) { setError(err); $('#pw_new', wrap)?.focus(); return; }
if (pw1 !== pw2) { setError('변경할 비밀번호 확인이 일치하지 않습니다.'); $('#pw_new2', wrap)?.focus(); return; }
const ok = await showMsg('비밀번호를 변경하시겠습니까?', { type: 'confirm', title: '비밀번호 변경' });
if (!ok) return;
const btn = wrap.querySelector('[data-act="submit"]');
if (btn) btn.disabled = true;
try {
const res = await fetch(postUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken(),
'Accept': 'application/json',
},
body: JSON.stringify({
current_password: cur,
password: pw1,
password_confirmation: pw2,
}),
});
const data = await res.json().catch(() => ({}));
if (res.status === 401 && data.redirect) {
await showMsg(data.message || '인증이 필요합니다.', { type: 'alert', title: '인증 필요' });
window.location.href = data.redirect;
return;
}
if (!res.ok || data.ok === false) {
const msg =
(data && data.message) ||
(data && data.errors && (data.errors.password?.[0] || data.errors.current_password?.[0])) ||
'비밀번호 변경에 실패했습니다.';
setError(msg);
return;
}
await showMsg(data.message || '비밀번호가 변경되었습니다.', { type: 'alert', title: '완료' });
close();
} catch (e) {
setError('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.');
} finally {
if (btn) btn.disabled = false;
}
}
wrap.querySelector('[data-act="submit"]')?.addEventListener('click', submit);
wrap.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submit();
}
});
}
trigger.addEventListener('click', openModal);
})();
// -------------------------------------------
// 4) 2차 비밀번호 변경 레이어 팝업 (신규)
// - 로그인 비밀번호 + 현재 2차비번 둘 다 검증 후 변경
// - 에러는 내부 붉은 박스
// - 닫힘: 취소 / X만
// -------------------------------------------
(function pin2Change() {
const trigger = document.querySelector('[data-action="pw2-change"]');
if (!trigger) return;
const postUrl = URLS.pin2Update; // Blade에서 주입 필요
if (!postUrl) {
trigger.addEventListener('click', async () => {
await showMsg('2차 비밀번호 변경 URL이 설정되지 않았습니다.', { type: 'alert', title: '오류' });
});
return;
}
ensureCommonModalStyle('mypagePin2ModalStyle', `
.mypage-pin2modal{position:fixed;inset:0;z-index:220000;display:flex;align-items:center;justify-content:center}
.mypage-pin2modal__dim{position:absolute;inset:0;background:#000;opacity:.55}
.mypage-pin2modal__box{position:relative;width:min(420px,calc(100% - 28px));background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 18px 60px rgba(0,0,0,.35)}
.mypage-pin2modal__hd{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:rgba(0,0,0,.04);border-bottom:1px solid rgba(0,0,0,.08)}
.mypage-pin2modal__ttl{font-weight:900;font-size:14px;color:#111}
.mypage-pin2modal__close{width:34px;height:34px;border-radius:10px;border:1px solid rgba(0,0,0,.12);background:#fff;cursor:pointer;font-size:18px;line-height:1;color:#111}
.mypage-pin2modal__bd{padding:14px}
.mypage-pin2modal__row{margin-top:10px}
.mypage-pin2modal__label{display:block;font-size:12px;font-weight:800;color:#667085;margin-bottom:6px}
.mypage-pin2modal__inp{width:100%;height:42px;border-radius:12px;border:1px solid #e5e7eb;padding:0 12px;font-size:14px;outline:none}
.mypage-pin2modal__inp--pin{letter-spacing:6px;text-align:center;font-weight:900}
.mypage-pin2modal__hint{margin-top:10px;font-size:12px;color:#667085;line-height:1.4}
.mypage-pin2modal__error{margin-top:12px;padding:10px 12px;border-radius:12px;background:rgba(220,38,38,.08);border:1px solid rgba(220,38,38,.25);color:#b91c1c;font-weight:800;font-size:12px;display:none;white-space:pre-line}
.mypage-pin2modal__ft{display:flex;gap:10px;padding:12px 14px;border-top:1px solid rgba(0,0,0,.08);background:#fff}
.mypage-pin2modal__btn{flex:1;height:42px;border-radius:12px;border:1px solid rgba(0,0,0,.12);background:#fff;font-weight:900;cursor:pointer}
.mypage-pin2modal__btn--primary{border:none;background:#111;color:#fff}
.mypage-pin2modal__btn[disabled]{opacity:.6;cursor:not-allowed}
`);
function isPin4(v) {
return /^\d{4}$/.test(v || '');
}
function openModal() {
const old = $('#mypagePin2Modal');
if (old) old.remove();
const wrap = document.createElement('div');
wrap.className = 'mypage-pin2modal';
wrap.id = 'mypagePin2Modal';
wrap.innerHTML = `
<div class="mypage-pin2modal__dim"></div>
<div class="mypage-pin2modal__box" role="dialog" aria-modal="true" aria-labelledby="mypagePin2ModalTitle">
<div class="mypage-pin2modal__hd">
<div class="mypage-pin2modal__ttl" id="mypagePin2ModalTitle">2차 비밀번호 변경</div>
<button type="button" class="mypage-pin2modal__close" aria-label="닫기">×</button>
</div>
<div class="mypage-pin2modal__bd">
<div class="mypage-pin2modal__row">
<label class="mypage-pin2modal__label">현재 로그인 비밀번호</label>
<input type="password" class="mypage-pin2modal__inp" id="pin2_current_password" autocomplete="current-password" />
</div>
<div class="mypage-pin2modal__row">
<label class="mypage-pin2modal__label">이전 2차 비밀번호 (숫자 4자리)</label>
<input type="password" inputmode="numeric" maxlength="4" class="mypage-pin2modal__inp mypage-pin2modal__inp--pin" id="pin2_current" autocomplete="off" />
</div>
<div class="mypage-pin2modal__row">
<label class="mypage-pin2modal__label">새 2차 비밀번호 (숫자 4자리)</label>
<input type="password" inputmode="numeric" maxlength="4" class="mypage-pin2modal__inp mypage-pin2modal__inp--pin" id="pin2_new" autocomplete="off" />
</div>
<div class="mypage-pin2modal__row">
<label class="mypage-pin2modal__label">새 2차 비밀번호 확인</label>
<input type="password" inputmode="numeric" maxlength="4" class="mypage-pin2modal__inp mypage-pin2modal__inp--pin" id="pin2_new2" autocomplete="off" />
</div>
<div class="mypage-pin2modal__hint">
• 보안을 위해 <b>로그인 비밀번호</b>와 <b>이전 2차 비밀번호</b>를 모두 확인합니다.<br/>
• 2차 비밀번호는 <b>숫자 4자리</b>만 가능합니다.
</div>
<div class="mypage-pin2modal__error" id="pin2_error"></div>
</div>
<div class="mypage-pin2modal__ft">
<button type="button" class="mypage-pin2modal__btn" data-act="cancel">취소</button>
<button type="button" class="mypage-pin2modal__btn mypage-pin2modal__btn--primary" data-act="submit">변경</button>
</div>
</div>
`;
document.body.appendChild(wrap);
const setError = makeErrorSetter(wrap, '#pin2_error');
function close() { wrap.remove(); }
// ✅ 닫기: X / 취소만
wrap.querySelector('.mypage-pin2modal__close')?.addEventListener('click', close);
wrap.querySelector('[data-act="cancel"]')?.addEventListener('click', close);
setTimeout(() => $('#pin2_current_password', wrap)?.focus(), 10);
// 숫자만 입력 보정(편의) — 필요 없으면 빼도 됨
function onlyDigitsMax4(el) {
if (!el) return;
el.addEventListener('input', () => {
el.value = (el.value || '').replace(/[^\d]/g, '').slice(0, 4);
});
}
onlyDigitsMax4($('#pin2_current', wrap));
onlyDigitsMax4($('#pin2_new', wrap));
onlyDigitsMax4($('#pin2_new2', wrap));
async function submit() {
setError('');
const curPw = ($('#pin2_current_password', wrap)?.value || '').trim();
const curPin2 = ($('#pin2_current', wrap)?.value || '').trim();
const pin2 = ($('#pin2_new', wrap)?.value || '').trim();
const pin2c = ($('#pin2_new2', wrap)?.value || '').trim();
if (!curPw) { setError('이전 비밀번호를 입력해 주세요.'); $('#pin2_current_password', wrap)?.focus(); return; }
if (!isPin4(curPin2)) { setError('이전 2차 비밀번호는 숫자 4자리여야 합니다.'); $('#pin2_current', wrap)?.focus(); return; }
if (!isPin4(pin2)) { setError('2차 비밀번호는 숫자 4자리여야 합니다.'); $('#pin2_new', wrap)?.focus(); return; }
if (pin2 !== pin2c) { setError('2차 비밀번호 확인이 일치하지 않습니다.'); $('#pin2_new2', wrap)?.focus(); return; }
const ok = await showMsg('2차 비밀번호를 변경하시겠습니까?', { type: 'confirm', title: '2차 비밀번호 변경' });
if (!ok) return;
const btn = wrap.querySelector('[data-act="submit"]');
if (btn) btn.disabled = true;
try {
const res = await fetch(postUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken(),
'Accept': 'application/json',
},
body: JSON.stringify({
current_password: curPw,
current_pin2: curPin2,
pin2: pin2,
pin2_confirmation: pin2c,
}),
});
const data = await res.json().catch(() => ({}));
if (res.status === 401 && data.redirect) {
await showMsg(data.message || '인증이 필요합니다.', { type: 'alert', title: '인증 필요' });
window.location.href = data.redirect;
return;
}
if (!res.ok || data.ok === false) {
// 422 validate or mismatch
const msg =
(data && data.message) ||
(data && data.errors && (
data.errors.current_password?.[0] ||
data.errors.current_pin2?.[0] ||
data.errors.pin2?.[0] ||
data.errors.pin2_confirmation?.[0]
)) ||
'2차 비밀번호 변경에 실패했습니다.';
setError(msg);
return;
}
await showMsg(data.message || '2차 비밀번호가 변경되었습니다.', { type: 'alert', title: '완료' });
close();
} catch (e) {
setError('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.');
} finally {
if (btn) btn.disabled = false;
}
}
wrap.querySelector('[data-act="submit"]')?.addEventListener('click', submit);
wrap.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submit();
}
});
}
trigger.addEventListener('click', openModal);
})();
// -------------------------------------------
// X) 출금계좌 등록/수정 (Dozn 성명인증 + 저장)
// - 입력: 2차 비밀번호(4자리), 금융권, 은행, 계좌번호(숫자만), 예금주
// - showMsg confirm/alert 사용
// - 에러는 모달 내부 붉은 글씨
// - 닫기: X / 취소만 (dim 클릭/ESC 닫기 없음)
// -------------------------------------------
(function withdrawAccount() {
const trigger = document.querySelector('[data-action="withdraw-account"]');
if (!trigger) return;
const postUrl = URLS.withdrawVerifyOut;
const defaultDepositor = (CFG.memberName || '').trim();
// ✅ bankGroups는 config(bank_code.php)의 groups 구조(label/items)로 전달된다고 가정
const BANK_GROUPS = (CFG.bankGroups || {});
if (!postUrl) {
trigger.addEventListener('click', async () => {
await showMsg('출금계좌 인증 URL이 설정되지 않았습니다.', { type: 'alert', title: '오류' });
});
return;
}
ensureCommonModalStyle('mypageWithdrawModalStyle', `
.mypage-withmodal{position:fixed;inset:0;z-index:220000;display:flex;align-items:center;justify-content:center}
.mypage-withmodal__dim{position:absolute;inset:0;background:#000;opacity:.55}
.mypage-withmodal__box{position:relative;width:min(460px,calc(100% - 28px));background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 18px 60px rgba(0,0,0,.35)}
.mypage-withmodal__hd{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background:rgba(0,0,0,.04);border-bottom:1px solid rgba(0,0,0,.08)}
.mypage-withmodal__ttl{font-weight:900;font-size:14px;color:#111}
.mypage-withmodal__close{width:34px;height:34px;border-radius:10px;border:1px solid rgba(0,0,0,.12);background:#fff;cursor:pointer;font-size:18px;line-height:1;color:#111}
.mypage-withmodal__bd{padding:14px}
.mypage-withmodal__row{margin-top:10px}
.mypage-withmodal__label{display:block;font-size:12px;font-weight:800;color:#667085;margin-bottom:6px}
.mypage-withmodal__inp{width:100%;height:42px;border-radius:12px;border:1px solid #e5e7eb;padding:0 12px;font-size:14px;outline:none}
.mypage-withmodal__inp--pin{letter-spacing:6px;text-align:center;font-weight:900}
.mypage-withmodal__select{width:100%;height:42px;border-radius:12px;border:1px solid #e5e7eb;padding:0 10px;font-size:14px;outline:none;background:#fff}
.mypage-withmodal__hint{margin-top:10px;font-size:12px;color:#667085;line-height:1.4}
.mypage-withmodal__error{margin-top:12px;padding:10px 12px;border-radius:12px;background:rgba(220,38,38,.08);border:1px solid rgba(220,38,38,.25);color:#b91c1c;font-weight:800;font-size:12px;display:none;white-space:pre-line}
.mypage-withmodal__ft{display:flex;gap:10px;padding:12px 14px;border-top:1px solid rgba(0,0,0,.08);background:#fff}
.mypage-withmodal__btn{flex:1;height:42px;border-radius:12px;border:1px solid rgba(0,0,0,.12);background:#fff;font-weight:900;cursor:pointer}
.mypage-withmodal__btn--primary{border:none;background:#111;color:#fff}
.mypage-withmodal__btn[disabled]{opacity:.6;cursor:not-allowed}
`);
function isPin4(v){ return /^\d{4}$/.test(v || ''); }
function isDigits(v){ return /^\d+$/.test(v || ''); }
function isBankCode3(v){ return /^\d{3}$/.test(v || ''); }
function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, m => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[m]));
}
function maskAccount(v) {
const s = String(v || '');
if (s.length <= 4) return s;
return '****' + s.slice(-4);
}
function groupOptionsHtml() {
const keys = Object.keys(BANK_GROUPS || {});
const opts = ['<option value="">금융권 선택</option>'];
for (const k of keys) {
const label = BANK_GROUPS[k]?.label || k;
opts.push(`<option value="${escapeHtml(k)}">${escapeHtml(label)}</option>`);
}
return opts.join('');
}
function bankOptionsByGroupHtml(groupKey) {
const group = BANK_GROUPS[groupKey];
const items = group?.items || {};
const codes = Object.keys(items);
if (!groupKey || !group || codes.length === 0) {
return '<option value="">금융권을 먼저 선택해 주세요</option>';
}
// 코드 오름차순
codes.sort();
const opts = ['<option value="">은행 선택</option>'];
for (const code of codes) {
const name = items[code];
opts.push(`<option value="${escapeHtml(code)}">${escapeHtml(code)} - ${escapeHtml(name)}</option>`);
}
return opts.join('');
}
function getBankName(groupKey, bankCode) {
return (BANK_GROUPS[groupKey]?.items && BANK_GROUPS[groupKey].items[bankCode]) ? BANK_GROUPS[groupKey].items[bankCode] : '';
}
function openModal() {
const old = document.getElementById('mypageWithdrawModal');
if (old) old.remove();
const isEdit = trigger.getAttribute('aria-label')?.includes('수정');
const wrap = document.createElement('div');
wrap.className = 'mypage-withmodal';
wrap.id = 'mypageWithdrawModal';
wrap.innerHTML = `
<div class="mypage-withmodal__dim"></div>
<div class="mypage-withmodal__box" role="dialog" aria-modal="true" aria-labelledby="mypageWithdrawModalTitle">
<div class="mypage-withmodal__hd">
<div class="mypage-withmodal__ttl" id="mypageWithdrawModalTitle">출금계좌 ${isEdit ? '수정' : '등록'}</div>
<button type="button" class="mypage-withmodal__close" aria-label="닫기">×</button>
</div>
<div class="mypage-withmodal__bd">
<div class="mypage-withmodal__row">
<label class="mypage-withmodal__label">2차 비밀번호 (숫자 4자리)</label>
<input type="password" inputmode="numeric" maxlength="4"
class="mypage-withmodal__inp mypage-withmodal__inp--pin"
id="with_pin2" autocomplete="off" />
</div>
<div class="mypage-withmodal__row">
<label class="mypage-withmodal__label">금융권</label>
<select class="mypage-withmodal__select" id="with_group">
${groupOptionsHtml()}
</select>
</div>
<div class="mypage-withmodal__row">
<label class="mypage-withmodal__label">은행</label>
<select class="mypage-withmodal__select" id="with_bank">
<option value="">금융권을 먼저 선택해 주세요</option>
</select>
</div>
<div class="mypage-withmodal__row">
<label class="mypage-withmodal__label">계좌번호 (숫자만)</label>
<input type="text" inputmode="numeric"
class="mypage-withmodal__inp" id="with_account"
placeholder="예) 1234567890123" autocomplete="off" />
</div>
<div class="mypage-withmodal__row">
<label class="mypage-withmodal__label">예금주(성명변경불가)</label>
<input type="text" class="mypage-withmodal__inp" id="with_depositor" value="${escapeHtml(defaultDepositor)}" readonly />
</div>
<div class="mypage-withmodal__hint">
• 보안을 위해 <b>2차 비밀번호</b> 확인 후 진행합니다.<br/>
• 예금주는 <b>회원 실명</b>과 동일해야 합니다.<br/>
• 계좌 성명 인증 완료 시, 출금계좌가 저장됩니다.
</div>
<div class="mypage-withmodal__error" id="with_error"></div>
</div>
<div class="mypage-withmodal__ft">
<button type="button" class="mypage-withmodal__btn" data-act="cancel">취소</button>
<button type="button" class="mypage-withmodal__btn mypage-withmodal__btn--primary" data-act="submit">인증/저장</button>
</div>
</div>
`;
document.body.appendChild(wrap);
const setError = makeErrorSetter(wrap, '#with_error');
const close = () => wrap.remove();
// ✅ 닫기: X / 취소만
wrap.querySelector('.mypage-withmodal__close')?.addEventListener('click', close);
wrap.querySelector('[data-act="cancel"]')?.addEventListener('click', close);
const pinEl = document.getElementById('with_pin2');
const groupEl = document.getElementById('with_group');
const bankEl = document.getElementById('with_bank');
const accEl = document.getElementById('with_account');
const depEl = document.getElementById('with_depositor');
// 숫자만 입력 보정
if (pinEl) pinEl.addEventListener('input', () => {
pinEl.value = (pinEl.value || '').replace(/[^\d]/g, '').slice(0, 4);
});
if (accEl) accEl.addEventListener('input', () => {
accEl.value = (accEl.value || '').replace(/[^\d]/g, '');
});
// ✅ 금융권 선택 → 은행 목록 갱신
groupEl?.addEventListener('change', () => {
const g = (groupEl.value || '').trim();
bankEl.innerHTML = bankOptionsByGroupHtml(g);
bankEl.value = '';
});
setTimeout(() => pinEl?.focus(), 10);
async function submit() {
setError('');
const pin2 = (pinEl?.value || '').trim();
const groupKey = (groupEl?.value || '').trim();
const bankCode = (bankEl?.value || '').trim();
const account = (accEl?.value || '').trim();
const depositor = (depEl?.value || '').trim();
if (!isPin4(pin2)) { setError('2차 비밀번호는 숫자 4자리여야 합니다.'); pinEl?.focus(); return; }
if (!groupKey || !BANK_GROUPS[groupKey]) { setError('금융권을 선택해 주세요.'); groupEl?.focus(); return; }
if (!isBankCode3(bankCode)) { setError('은행을 선택해 주세요.'); bankEl?.focus(); return; }
// ✅ 선택한 금융권 안에 실제로 존재하는 은행인지 (클라 방어)
const bankName = getBankName(groupKey, bankCode);
if (!bankName) { setError('선택한 은행 정보가 올바르지 않습니다. 다시 선택해 주세요.'); bankEl?.focus(); return; }
if (!account || !isDigits(account)) { setError('계좌번호는 숫자만 입력해 주세요.'); accEl?.focus(); return; }
if (!depositor) { setError('예금주(성명)를 입력해 주세요.'); depEl?.focus(); return; }
const ok = await showMsg(
`출금계좌를 인증 후 저장하시겠습니까?\n\n금융권: ${BANK_GROUPS[groupKey]?.label || groupKey}\n은행: ${bankName} (${bankCode})\n계좌: ${maskAccount(account)}\n예금주: ${depositor}`,
{ type: 'confirm', title: '출금계좌 인증/저장' }
);
if (!ok) return;
const btn = wrap.querySelector('[data-act="submit"]');
if (btn) btn.disabled = true;
try {
const res = await fetch(postUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken(),
'Accept': 'application/json',
},
body: JSON.stringify({
pin2: pin2,
bank_code: bankCode,
account: account,
depositor: depositor,
// groupKey는 서버 필수는 아니지만, 디버깅/검증 강화용으로 보내도 됨
// bank_group: groupKey,
}),
});
const data = await res.json().catch(() => ({}));
if (res.status === 401 && data.redirect) {
await showMsg(data.message || '인증이 필요합니다.', { type: 'alert', title: '인증 필요' });
window.location.href = data.redirect;
return;
}
if (!res.ok || data.ok === false) {
const msg =
(data && data.message) ||
(data && data.errors && (
data.errors.pin2?.[0] ||
data.errors.bank_code?.[0] ||
data.errors.account?.[0] ||
data.errors.depositor?.[0]
)) ||
'계좌 인증/저장에 실패했습니다.';
setError(msg);
return;
}
await showMsg(data.message || '인증완료 및 계좌번호가 등록되었습니다.', { type: 'alert', title: '완료' });
close();
window.location.reload();
} catch (e) {
setError('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.');
} finally {
if (btn) btn.disabled = false;
}
}
wrap.querySelector('[data-act="submit"]')?.addEventListener('click', submit);
wrap.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submit();
}
});
}
trigger.addEventListener('click', openModal);
})();
// -------------------------------------------
// 5) 기타 버튼들(준비중)
// -------------------------------------------
(function others() {
$('[data-action="consent-edit"]')?.addEventListener('click', async () => {
await showMsg('준비중입니다.', { type: 'alert', title: '수신 동의' });
});
$('[data-action="withdraw-member"]')?.addEventListener('click', async () => {
const ok = await showMsg(
`회원탈퇴를 진행하시겠습니까?
• 탈퇴 시 계정 복구가 어려울 수 있습니다.
• 진행 전 보유 내역/정산/환불 정책을 확인해 주세요.`,
{ type: 'confirm', title: '회원탈퇴' }
);
if (!ok) return;
await showMsg('준비중입니다.', { type: 'alert', title: '회원탈퇴' });
});
})();
})();