558 lines
25 KiB
PHP
558 lines
25 KiB
PHP
@php
|
||
$pageTitle = '나의정보';
|
||
$pageDesc = '계정 정보 관리';
|
||
|
||
$breadcrumbs = [
|
||
['label' => '홈', 'url' => url('/')],
|
||
['label' => '마이페이지', 'url' => url('/mypage/info')],
|
||
['label' => '나의정보 변경', 'url' => url()->current()],
|
||
];
|
||
$mypageActive = 'info';
|
||
@endphp
|
||
|
||
@extends('web.layouts.subpage')
|
||
|
||
@section('title', '나의정보 변경 | PIN FOR YOU')
|
||
@section('meta_description', 'PIN FOR YOU 나의정보 변경 페이지입니다.')
|
||
@section('canonical', url('/mypage/info_renew'))
|
||
|
||
@section('subcontent')
|
||
<div class="mypage-info-page">
|
||
|
||
@include('web.partials.content-head', [
|
||
'title' => '나의정보 변경',
|
||
'desc' => '계정 보안과 개인정보를 안전하게 관리하세요.'
|
||
])
|
||
|
||
{{-- ✅ 상단 상태 카드 --}}
|
||
<div class="mypage-hero mt-3">
|
||
<div class="mypage-hero__inner">
|
||
<div class="mypage-hero__left">
|
||
<div class="mypage-hero__kicker">ACCOUNT SETTINGS</div>
|
||
<div class="mypage-hero__title">내 정보 관리</div>
|
||
<div class="mypage-hero__me mt-2">
|
||
<div class="mypage-hero__me-row">
|
||
<span class="k">성명</span>
|
||
<span class="v">{{ $memberName ?: '-' }}</span>
|
||
</div>
|
||
<div class="mypage-hero__me-row">
|
||
<span class="k">이메일</span>
|
||
<span class="v">{{ $memberEmail ?: '-' }}</span>
|
||
</div>
|
||
<div class="mypage-hero__me-row">
|
||
<span class="k">휴대폰</span>
|
||
<span class="v">{{ $memberPhone ?: '-' }}</span>
|
||
</div>
|
||
<div class="mypage-hero__me-row">
|
||
<span class="k">가입일</span>
|
||
<span class="v">{{ $memberDtReg ?: '-' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mypage-hero__right">
|
||
<div class="mypage-hero__desc">
|
||
연락처·비밀번호·보안 설정을 한 곳에서 관리합니다.
|
||
변경 작업은 보안을 위해 제한된 시간 동안만 가능합니다.
|
||
</div>
|
||
<div class="mypage-reauth mypage-reauth--countdown"
|
||
data-expire="{{ (int)$expireTs }}"
|
||
data-remain="{{ (int)$remainSec }}">
|
||
<div class="mypage-reauth__one">
|
||
<span class="mypage-reauth__label">인증 허용 잔여시간</span>
|
||
<span class="mypage-reauth__value">
|
||
<b id="reauthCountdown">
|
||
{{ sprintf('%02d:%02d', intdiv((int)$remainSec, 60), (int)$remainSec % 60) }}
|
||
</b>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mypage-hero__actions">
|
||
<a href="{{ route('web.mypage.info.gate_reset') }}" class="btn btn-mypage-primary">
|
||
인증 다시 하기
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ✅ 설정 카드 그리드 --}}
|
||
<div class="mypage-grid mt-3">
|
||
<button type="button"
|
||
class="mypage-card mypage-card--btn"
|
||
data-action="phone-change"
|
||
data-ready-url="{{ route('web.mypage.info.pass.ready') }}">
|
||
<div class="mypage-card__icon">📱</div>
|
||
<div class="mypage-card__body">
|
||
<div class="mypage-card__title">연락처 변경</div>
|
||
<div class="mypage-card__desc">PASS 인증 후 휴대폰 번호를 변경합니다.</div>
|
||
<div class="mypage-card__meta">PASS 인증 필요</div>
|
||
</div>
|
||
<div class="mypage-card__arrow">›</div>
|
||
</button>
|
||
|
||
<a class="mypage-card" href="javascript:void(0)" aria-label="비밀번호 변경 (준비중)" data-action="pw-change">
|
||
<div class="mypage-card__icon">🔒</div>
|
||
<div class="mypage-card__body">
|
||
<div class="mypage-card__title">비밀번호 변경</div>
|
||
<div class="mypage-card__desc">보안을 위해 주기적으로 변경을 권장해요</div>
|
||
<div class="mypage-card__meta">준비중</div>
|
||
</div>
|
||
<div class="mypage-card__arrow">›</div>
|
||
</a>
|
||
|
||
<a class="mypage-card" href="javascript:void(0)" aria-label="2차 비밀번호 변경 (준비중)" data-action="pw2-change">
|
||
<div class="mypage-card__icon">🔐</div>
|
||
<div class="mypage-card__body">
|
||
<div class="mypage-card__title">2차비밀번호 변경</div>
|
||
<div class="mypage-card__desc">민감 기능 이용 시 추가로 확인하는 비밀번호</div>
|
||
<div class="mypage-card__meta">준비중</div>
|
||
</div>
|
||
<div class="mypage-card__arrow">›</div>
|
||
</a>
|
||
|
||
<a class="mypage-card" href="javascript:void(0)" aria-label="출금계좌번호 {{ $hasWithdrawAccount ? '수정' : '등록' }}" data-action="withdraw-account">
|
||
<div class="mypage-card__icon">🏦</div>
|
||
<div class="mypage-card__body">
|
||
<div class="mypage-card__title">출금계좌번호</div>
|
||
<div class="mypage-card__desc">
|
||
{{ $hasWithdrawAccount ? '등록된 출금계좌 정보를 수정합니다.' : '출금계좌를 등록해 주세요.' }}
|
||
</div>
|
||
<div class="mypage-card__meta">
|
||
{{ $hasWithdrawAccount ? '수정' : '등록' }}
|
||
</div>
|
||
</div>
|
||
<div class="mypage-card__arrow">›</div>
|
||
</a>
|
||
|
||
<button type="button" class="mypage-card mypage-card--btn" data-action="consent-edit">
|
||
<div class="mypage-card__icon">📩</div>
|
||
<div class="mypage-card__body">
|
||
<div class="mypage-card__title">수신 동의</div>
|
||
<div class="mypage-card__desc">마케팅 정보 수신 여부를 설정합니다.</div>
|
||
<div class="mypage-card__meta">
|
||
이메일: {{ ($agreeEmail === 'y' || $agreeEmail === '1') ? '동의' : '미동의' }}
|
||
· SMS: {{ ($agreeSms === 'y' || $agreeSms === '1') ? '동의' : '미동의' }}
|
||
</div>
|
||
</div>
|
||
<div class="mypage-card__arrow">›</div>
|
||
</button>
|
||
|
||
<button type="button" class="mypage-card mypage-card--btn mypage-card--danger" data-action="withdraw-member">
|
||
<div class="mypage-card__icon">⚠️</div>
|
||
<div class="mypage-card__body">
|
||
<div class="mypage-card__title">회원탈퇴</div>
|
||
<div class="mypage-card__desc">계정을 삭제합니다. 진행 전 주의사항을 확인해 주세요.</div>
|
||
<div class="mypage-card__meta">주의 필요</div>
|
||
</div>
|
||
<div class="mypage-card__arrow">›</div>
|
||
</button>
|
||
</div>
|
||
|
||
{{-- ✅ 안내/주의사항 --}}
|
||
<div class="mypage-note mt-3">
|
||
<div class="mypage-note__title">안내</div>
|
||
<ul class="mypage-note__list">
|
||
<li>개인정보 변경은 보안을 위해 <b>재인증 후 일정 시간</b> 동안만 가능합니다.</li>
|
||
<li>예상치 못한 오류가 발생하면, 새로고침 후 다시 시도해 주세요.</li>
|
||
<li>기능은 다음 단계에서 실제 처리(저장/검증/로그)를 연결합니다.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="mypageDanalStartForm" method="post" action="{{ route('web.mypage.info.danal.start') }}" style="display:none;">
|
||
@csrf
|
||
<input type="hidden" name="platform" id="mypagePlatform" value="web">
|
||
<input type="hidden" name="fields" id="mypagePassFields" value="">
|
||
</form>
|
||
|
||
@push('styles')
|
||
<style>
|
||
.mypage-reauth__one{
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:space-between;
|
||
gap:10px;
|
||
}
|
||
#reauthCountdown{
|
||
font-size:14px;
|
||
letter-spacing:0.5px;
|
||
}
|
||
/* 더 강한 특이도(덮임 방지) + 흰 배경에서도 확실히 보이게 */
|
||
.mypage-info-page .mypage-hero .mypage-hero__me{
|
||
margin-top:12px;
|
||
padding:14px;
|
||
border-radius:14px;
|
||
background:#f7f8fb; /* ✅ 흰배경에서도 티 나게 */
|
||
border:1px solid #e5e7eb;
|
||
box-shadow: 0 10px 24px rgba(16,24,40,.08);
|
||
}
|
||
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row{
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:space-between;
|
||
gap:12px;
|
||
padding:10px 12px;
|
||
border-radius:12px;
|
||
background:#ffffff;
|
||
border:1px solid rgba(0,0,0,.04);
|
||
}
|
||
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row + .mypage-hero__me-row{
|
||
margin-top:8px;
|
||
}
|
||
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row:hover{
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 6px 16px rgba(16,24,40,.06);
|
||
}
|
||
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row .k{
|
||
display:inline-flex;
|
||
align-items:center;
|
||
gap:8px;
|
||
font-size:12px;
|
||
font-weight:800;
|
||
color:#667085;
|
||
white-space:nowrap;
|
||
}
|
||
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row .k::before{
|
||
content:'';
|
||
width:8px;
|
||
height:8px;
|
||
border-radius:999px;
|
||
background:#2563eb; /* 포인트 컬러 */
|
||
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
|
||
}
|
||
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row .v{
|
||
font-size:14px;
|
||
font-weight:900;
|
||
color:#101828;
|
||
letter-spacing:.2px;
|
||
text-align:right;
|
||
word-break:break-all;
|
||
}
|
||
|
||
/* 모바일 */
|
||
@media (max-width: 480px){
|
||
.mypage-info-page .mypage-hero .mypage-hero__me{
|
||
padding:12px;
|
||
border-radius:12px;
|
||
}
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row{
|
||
padding:10px 10px;
|
||
}
|
||
.mypage-info-page .mypage-hero .mypage-hero__me-row .v{
|
||
font-size:13px;
|
||
}
|
||
}
|
||
|
||
</style>
|
||
@endpush
|
||
@push('scripts')
|
||
<script>
|
||
/**
|
||
* 1) ✅ 재인증 타이머 (버튼 유무와 무관하게 항상 실행)
|
||
* - remainSec 기반 카운트다운
|
||
* - 0 되면 alert → info 페이지로 이동
|
||
*/
|
||
(function () {
|
||
const box = document.querySelector('.mypage-reauth--countdown');
|
||
const out = document.getElementById('reauthCountdown');
|
||
if (!box || !out) return;
|
||
|
||
const redirectUrl = "{{ route('web.mypage.info.gate_reset') }}";
|
||
|
||
// ✅ 서버가 내려준 세션 until(문자열) 우선 사용
|
||
const untilStr = (box.getAttribute('data-until') || '').trim();
|
||
// fallback: 렌더링 시점 remain
|
||
const remainFallback = parseInt(box.getAttribute('data-remain') || '0', 10);
|
||
|
||
// until 파싱 (서버가 'YYYY-MM-DD HH:MM:SS'로 주면 JS Date가 못 읽는 경우가 있음)
|
||
// 그래서 'YYYY-MM-DDTHH:MM:SS'로 변환해서 파싱 시도
|
||
function parseUntilMs(s){
|
||
if (!s) return null;
|
||
|
||
// 2026-02-01 16:33:50 -> 2026-02-01T16:33:50
|
||
const isoLike = s.includes('T') ? s : s.replace(' ', 'T');
|
||
const ms = Date.parse(isoLike);
|
||
|
||
return Number.isFinite(ms) ? ms : null;
|
||
}
|
||
|
||
const untilMs = parseUntilMs(untilStr);
|
||
|
||
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');
|
||
}
|
||
|
||
let done = false;
|
||
let timer = null;
|
||
|
||
function expireUI(){
|
||
const chip = document.querySelector('.mypage-chip');
|
||
if (chip) {
|
||
chip.classList.remove('is-ok');
|
||
chip.classList.add('is-warn');
|
||
chip.textContent = '재인증 필요';
|
||
}
|
||
}
|
||
|
||
async function onExpiredOnce() {
|
||
await showMsg("인증 허용 시간이 만료되었습니다.\n\n보안을 위해 다시 재인증이 필요합니다.", { type: 'alert', title: '인증완료' });
|
||
window.location.href = "{{ route('web.mypage.info.gate_reset') }}";
|
||
}
|
||
|
||
|
||
function getRemainSec(){
|
||
// ✅ untilMs가 유효하면 "세션 until - 현재시간"으로 매번 계산
|
||
if (untilMs !== null) {
|
||
const diffMs = untilMs - Date.now();
|
||
return Math.max(0, Math.floor(diffMs / 1000));
|
||
}
|
||
// fallback: remainFallback에서 감소시키는 방식(최후의 수단)
|
||
return Math.max(0, Number.isFinite(remainFallback) ? remainFallback : 0);
|
||
}
|
||
|
||
// fallback용 remain 변수 (untilMs가 없을 때만 사용)
|
||
let remain = Math.max(0, Number.isFinite(remainFallback) ? remainFallback : 0);
|
||
|
||
function tick(){
|
||
const sec = (untilMs !== null) ? getRemainSec() : remain;
|
||
|
||
out.textContent = fmt(sec);
|
||
|
||
if (sec <= 0) {
|
||
if (timer) clearInterval(timer);
|
||
onExpiredOnce();
|
||
return;
|
||
}
|
||
|
||
// untilMs가 없을 때만 1초씩 감소
|
||
if (untilMs === null) {
|
||
remain -= 1;
|
||
}
|
||
}
|
||
|
||
tick();
|
||
timer = setInterval(tick, 1000);
|
||
})();
|
||
|
||
|
||
|
||
/**
|
||
* 2) ✅ 연락처 변경 버튼 → passReady → danal iframe 모달
|
||
* (기존 로직 유지, 타이머와 분리)
|
||
*/
|
||
(function(){
|
||
const btn = document.querySelector('[data-action="phone-change"]');
|
||
const startForm = document.getElementById('mypageDanalStartForm');
|
||
|
||
// 버튼이 없는 페이지에서도 타이머는 돌아야 하므로, 여기서만 return
|
||
if (!btn || !startForm) return;
|
||
|
||
function isMobileUA(){
|
||
const ua = navigator.userAgent || '';
|
||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
function askCancelAndGo() {
|
||
//시스템 confirm
|
||
const ok = window.confirm("인증을 중단하시겠습니까?\n\n닫으면 현재 변경 진행이 취소됩니다.");
|
||
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);
|
||
|
||
const escHandler = (e) => { if (e.key === 'Escape') askCancelAndGo(); };
|
||
window.addEventListener('keydown', escHandler);
|
||
|
||
window.closeIframe = function () {
|
||
window.removeEventListener('keydown', escHandler);
|
||
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 = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||
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 function(){
|
||
const readyUrl = btn.getAttribute('data-ready-url');
|
||
if (!readyUrl) {
|
||
await showMsg('준비 URL이 없습니다(data-ready-url).', { type: 'alert', title: '오류' });
|
||
return;
|
||
}
|
||
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': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
|
||
'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 () {
|
||
const $ = (sel) => document.querySelector(sel);
|
||
|
||
// 비밀번호 변경
|
||
$('[data-action="pw-change"]')?.addEventListener('click', async () => {
|
||
await showMsg('준비중입니다.', { type: 'alert', title: '비밀번호 변경' });
|
||
});
|
||
|
||
// 2차 비밀번호 변경
|
||
$('[data-action="pw2-change"]')?.addEventListener('click', async () => {
|
||
await showMsg('준비중입니다.', { type: 'alert', title: '2차비밀번호 변경' });
|
||
});
|
||
|
||
// 출금계좌 등록/수정
|
||
$('[data-action="withdraw-account"]')?.addEventListener('click', async () => {
|
||
await showMsg('준비중입니다.', { type: 'alert', title: '출금계좌번호' });
|
||
});
|
||
|
||
// 수신동의 수정
|
||
$('[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: '회원탈퇴' });
|
||
});
|
||
})();
|
||
|
||
</script>
|
||
@endpush
|
||
|
||
|
||
|
||
|
||
|
||
@endsection
|