2차비밀번호 변경, 계좌변경 및 추가 (아직개발 ip 허용되지 않아 테스트 하지 못함)
This commit is contained in:
parent
5cb2bc299f
commit
9db798dee8
@ -449,7 +449,7 @@ class RegisterController extends Controller
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// 비밀번호는 로그에 남기면 사고남 → 마스킹/제거
|
// 비밀번호는 마스킹/제거
|
||||||
$logFinal = $final;
|
$logFinal = $final;
|
||||||
$logFinal['password_plain'] = '***masked***';
|
$logFinal['password_plain'] = '***masked***';
|
||||||
$logFinal['pin2_plain'] = '***masked***';
|
$logFinal['pin2_plain'] = '***masked***';
|
||||||
|
|||||||
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Web\Mypage;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\MemInfoService;
|
use App\Services\MemInfoService;
|
||||||
use App\Services\Danal\DanalAuthtelService;
|
use App\Services\Danal\DanalAuthtelService;
|
||||||
|
use App\Services\Dozn\DoznAccountAuthService;
|
||||||
use App\Repositories\Member\MemberAuthRepository;
|
use App\Repositories\Member\MemberAuthRepository;
|
||||||
use App\Support\LegacyCrypto\CiSeedCrypto;
|
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||||
use App\Support\LegacyCrypto\CiPassword;
|
use App\Support\LegacyCrypto\CiPassword;
|
||||||
@ -40,7 +41,7 @@ final class InfoGateController extends Controller
|
|||||||
|
|
||||||
public function info_renew(Request $request)
|
public function info_renew(Request $request)
|
||||||
{
|
{
|
||||||
// ✅ gate (기존 그대로)
|
// gate (기존 그대로)
|
||||||
$gate = (array) $request->session()->get('mypage_gate', []);
|
$gate = (array) $request->session()->get('mypage_gate', []);
|
||||||
$gateOk = (bool) Arr::get($gate, 'ok', false);
|
$gateOk = (bool) Arr::get($gate, 'ok', false);
|
||||||
$gateAt = (int) Arr::get($gate, 'at', 0);
|
$gateAt = (int) Arr::get($gate, 'at', 0);
|
||||||
@ -51,14 +52,14 @@ final class InfoGateController extends Controller
|
|||||||
$remainSec = ($expireTs > 0) ? max(0, $expireTs - $nowTs) : 0;
|
$remainSec = ($expireTs > 0) ? max(0, $expireTs - $nowTs) : 0;
|
||||||
$isGateValid = ($remainSec > 0);
|
$isGateValid = ($remainSec > 0);
|
||||||
|
|
||||||
// ✅ 회원정보: _sess 기반
|
// 회원정보: _sess 기반
|
||||||
$sess = (array) $request->session()->get('_sess', []);
|
$sess = (array) $request->session()->get('_sess', []);
|
||||||
|
|
||||||
$memberName = (string) Arr::get($sess, '_mname', '');
|
$memberName = (string) Arr::get($sess, '_mname', '');
|
||||||
$memberEmail = (string) Arr::get($sess, '_mid', '');
|
$memberEmail = (string) Arr::get($sess, '_mid', '');
|
||||||
$dtReg = (string) Arr::get($sess, '_dt_reg', '');
|
$dtReg = (string) Arr::get($sess, '_dt_reg', '');
|
||||||
|
|
||||||
// ✅ 전화번호 복호 (인증 완료 상태라 마스킹 제외)
|
// 전화번호 복호 (인증 완료 상태라 마스킹 제외)
|
||||||
$rawPhone = (string) Arr::get($sess, '_mcell', '');
|
$rawPhone = (string) Arr::get($sess, '_mcell', '');
|
||||||
$memberPhone = (string) $this->seed->decrypt($rawPhone);
|
$memberPhone = (string) $this->seed->decrypt($rawPhone);
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
@ -91,20 +92,35 @@ final class InfoGateController extends Controller
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 재인증을 위한 세셔초기화
|
* 재인증을 위한 연장 및 세셔초기화
|
||||||
*/
|
*/
|
||||||
public function gateReset(Request $request)
|
public function gateReset(Request $request)
|
||||||
{
|
{
|
||||||
// 게이트 인증 세션만 초기화
|
$ttlSec = 5 * 60;
|
||||||
$request->session()->forget('mypage_gate');
|
$now = time();
|
||||||
|
|
||||||
// (선택) reauth도 같이 초기화하고 싶으면
|
$gate = (array) $request->session()->get('mypage_gate', []);
|
||||||
$request->session()->forget('mypage.reauth.at');
|
$gateOk = (bool)($gate['ok'] ?? false);
|
||||||
$request->session()->forget('mypage.reauth.until');
|
$gateAt = (int) ($gate['at'] ?? 0);
|
||||||
|
|
||||||
$request->session()->save();
|
$expireTs = ($gateOk && $gateAt > 0) ? ($gateAt + $ttlSec) : 0;
|
||||||
|
$isValid = ($expireTs > 0) && ($expireTs > $now);
|
||||||
|
|
||||||
// 게이트 페이지로 이동
|
if ($isValid) {
|
||||||
|
// 남은 시간이 있으면: 5분 연장(= at를 현재로 갱신)
|
||||||
|
$request->session()->put('mypage_gate', [
|
||||||
|
'ok' => true,
|
||||||
|
'email' => (string)($gate['email'] ?? ''),
|
||||||
|
'at' => $now,
|
||||||
|
]);
|
||||||
|
$request->session()->save();
|
||||||
|
|
||||||
|
}else{
|
||||||
|
// 시간이 끝났으면: 초기화
|
||||||
|
$request->session()->forget('mypage_gate');
|
||||||
|
$request->session()->save();
|
||||||
|
}
|
||||||
|
// 인덱스로 보내기
|
||||||
return redirect()->route('web.mypage.info.index');
|
return redirect()->route('web.mypage.info.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +287,7 @@ final class InfoGateController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 목적이 마이페이지 연락처 변경일 때만 추가 검증
|
// 목적이 마이페이지 연락처 변경일 때만 추가 검증
|
||||||
$purpose = (string) $request->session()->get('mypage.pass_purpose', '');
|
$purpose = (string) $request->session()->get('mypage.pass_purpose', '');
|
||||||
if ($purpose === 'mypage_phone_change') {
|
if ($purpose === 'mypage_phone_change') {
|
||||||
|
|
||||||
@ -331,5 +347,278 @@ final class InfoGateController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*비밀번호 변경 저장*/
|
||||||
|
public function passwordUpdate(Request $request, MemInfoService $memInfoService, MemberAuthRepository $repo)
|
||||||
|
{
|
||||||
|
if (!$this->isGateOk($request)) {
|
||||||
|
return $this->gateFailJson($request, '인증 시간이 만료되었습니다. 다시 인증해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인 세션
|
||||||
|
$sess = (array) $request->session()->get('_sess', []);
|
||||||
|
$isLogin = (bool) ($sess['_login_'] ?? false);
|
||||||
|
$email = (string) ($sess['_mid'] ?? '');
|
||||||
|
$memNo = (int) ($sess['_mno'] ?? 0);
|
||||||
|
|
||||||
|
if (!$isLogin || $email === '' || $memNo <= 0) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인해 주세요.',
|
||||||
|
'redirect' => route('web.auth.login'),
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검증 (요청하신 메시지 그대로)
|
||||||
|
$request->validate([
|
||||||
|
'current_password' => ['required', 'string'],
|
||||||
|
'password' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'min:8',
|
||||||
|
'max:20',
|
||||||
|
// 영문+숫자+특수문자 포함
|
||||||
|
'regex:/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,20}$/',
|
||||||
|
'confirmed', // password_confirmation
|
||||||
|
],
|
||||||
|
], [
|
||||||
|
'password.required' => '비밀번호를 입력해 주세요.',
|
||||||
|
'password.min' => '비밀번호는 8자리 이상이어야 합니다.',
|
||||||
|
'password.max' => '비밀번호는 20자리를 초과할 수 없습니다.',
|
||||||
|
'password.regex' => '비밀번호는 영문+숫자+특수문자를 포함해야 합니다.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$currentPw = (string) $request->input('current_password');
|
||||||
|
$newPw = (string) $request->input('password');
|
||||||
|
|
||||||
|
// 현재 비밀번호 확인(기존 attemptLegacyLogin 로직 재사용)
|
||||||
|
$res = $memInfoService->attemptLegacyLogin([
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $currentPw,
|
||||||
|
'ip' => $request->ip(),
|
||||||
|
'ua' => substr((string) $request->userAgent(), 0, 500),
|
||||||
|
'return_url' => url('/mypage/info_renew'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ok = (bool)($res['ok'] ?? $res['success'] ?? false);
|
||||||
|
if (!$ok) {
|
||||||
|
// 레이어에서 보여줄 메시지
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => (string)($res['message'] ?? '현재 비밀번호가 일치하지 않습니다.'),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 변경 저장(레포 이미 존재) + 성공로그(레포 이미 존재)
|
||||||
|
$repo->updatePasswordOnly($memNo, $newPw);
|
||||||
|
$repo->logPasswordResetSuccess(
|
||||||
|
$memNo,
|
||||||
|
(string) $request->ip(),
|
||||||
|
(string) $request->userAgent(),
|
||||||
|
'S'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'ok' => true,
|
||||||
|
'message' => '비밀번호가 변경되었습니다.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//2차 비밀번호 변경
|
||||||
|
public function updatePin2(
|
||||||
|
Request $request,
|
||||||
|
MemInfoService $memInfoService,
|
||||||
|
MemberAuthRepository $repo
|
||||||
|
) {
|
||||||
|
if (!$this->isGateOk($request)) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => '인증 시간이 만료되었습니다. 다시 인증해 주세요.',
|
||||||
|
], 419);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력 검증(메시지 그대로)
|
||||||
|
$request->validate([
|
||||||
|
'current_password' => ['required', 'string'],
|
||||||
|
'current_pin2' => ['required', 'digits:4'],
|
||||||
|
'pin2' => ['required', 'digits:4'],
|
||||||
|
'pin2_confirmation' => ['required', 'same:pin2'],
|
||||||
|
], [
|
||||||
|
'current_password.required' => '이전 비밀번호를 입력해 주세요.',
|
||||||
|
|
||||||
|
'current_pin2.required' => '이전 2차 비밀번호(숫자 4자리)를 입력해 주세요.',
|
||||||
|
'current_pin2.digits' => '이전 2차 비밀번호는 숫자 4자리여야 합니다.',
|
||||||
|
|
||||||
|
'pin2.required' => '2차 비밀번호(숫자 4자리)를 입력해 주세요.',
|
||||||
|
'pin2.digits' => '2차 비밀번호는 숫자 4자리여야 합니다.',
|
||||||
|
'pin2_confirmation.required' => '2차 비밀번호 확인을 입력해 주세요.',
|
||||||
|
'pin2_confirmation.same' => '2차 비밀번호 확인이 일치하지 않습니다.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 세션에서 mem_no/email 확보
|
||||||
|
$sess = (array) $request->session()->get('_sess', []);
|
||||||
|
$isLogin = (bool) ($sess['_login_'] ?? false);
|
||||||
|
$email = (string) ($sess['_mid'] ?? '');
|
||||||
|
$memNo = (int) ($sess['_mno'] ?? 0);
|
||||||
|
|
||||||
|
if (!$isLogin || $email === '' || $memNo <= 0) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인 후 시도해 주세요.',
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 로그인 비밀번호(1차) 검증 (기존 attemptLegacyLogin 재사용)
|
||||||
|
$currentPw = (string) $request->input('current_password');
|
||||||
|
|
||||||
|
$res = $memInfoService->attemptLegacyLogin([
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $currentPw,
|
||||||
|
'ip' => $request->ip(),
|
||||||
|
'ua' => substr((string) $request->userAgent(), 0, 500),
|
||||||
|
'return_url' => url('/mypage/info_renew'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pwOk = (bool)($res['ok'] ?? $res['success'] ?? false);
|
||||||
|
if (!$pwOk) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'reason' => 'invalid_current_password',
|
||||||
|
'message' => (string)($res['message'] ?? '이전 비밀번호가 일치하지 않습니다.'),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 현재 2차 비밀번호 검증
|
||||||
|
$currentPin2 = (string) $request->input('current_pin2');
|
||||||
|
$pin2Ok = $repo->verifyPin2($memNo, $currentPin2);
|
||||||
|
|
||||||
|
if (!$pin2Ok) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'reason' => 'invalid_current_pin2',
|
||||||
|
'message' => '이전 2차 비밀번호가 일치하지 않습니다.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 새 2차 비밀번호 저장
|
||||||
|
$newPin2 = (string) $request->input('pin2');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$repo->updatePin2Only($memNo, $newPin2);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('[mypage] pin2 update failed', [
|
||||||
|
'mem_no' => $memNo,
|
||||||
|
'err' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => '2차 비밀번호 변경에 실패했습니다. 잠시 후 다시 시도해 주세요.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'ok' => true,
|
||||||
|
'message' => '2차 비밀번호가 변경되었습니다.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function verifyOut(Request $request, DoznAccountAuthService $dozn, MemberAuthRepository $repo)
|
||||||
|
{
|
||||||
|
// 게이트 만료 처리(프로젝트 정책대로)
|
||||||
|
if (!$this->isGateOk($request)) {
|
||||||
|
return $this->gateFailJson($request, '인증이 만료되었습니다. 다시 인증해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레거시 세션 기준으로 memNo / 실명 확정
|
||||||
|
$sess = (array) $request->session()->get('_sess', []);
|
||||||
|
$memNo = (int) ($sess['_mno'] ?? 0);
|
||||||
|
$memberName = trim((string) ($sess['_mname'] ?? ''));
|
||||||
|
|
||||||
|
if ($memNo <= 0 || $memberName === '') {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인 후 시도해 주세요.',
|
||||||
|
'redirect' => route('web.auth.login'),
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'pin2' => ['required','digits:4'],
|
||||||
|
'bank_code' => ['required','string'],
|
||||||
|
'account' => ['required','regex:/^\d+$/'],
|
||||||
|
'depositor' => ['required','string','max:50'],
|
||||||
|
], [
|
||||||
|
'pin2.required' => '2차 비밀번호(숫자 4자리)를 입력해 주세요.',
|
||||||
|
'pin2.digits' => '2차 비밀번호는 숫자 4자리여야 합니다.',
|
||||||
|
'bank_code.required' => '은행을 선택해 주세요.',
|
||||||
|
'account.required' => '계좌번호를 입력해 주세요.',
|
||||||
|
'account.regex' => '계좌번호는 숫자만 입력해 주세요.',
|
||||||
|
'depositor.required' => '예금주(성명)를 입력해 주세요.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pin2 = trim((string) $request->input('pin2', ''));
|
||||||
|
$bankCode = trim((string) $request->input('bank_code', ''));
|
||||||
|
$account = trim((string) $request->input('account', ''));
|
||||||
|
$depositor = trim((string) $request->input('depositor', ''));
|
||||||
|
|
||||||
|
if ($depositor !== $memberName) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => '가입자 성명과 예금주가 일치하지 않습니다.',
|
||||||
|
'errors' => ['depositor' => ['가입자 성명과 예금주가 일치하지 않습니다.']],
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 2차비번 검증 먼저
|
||||||
|
$okPin2 = $repo->verifyPin2($memNo, $pin2);
|
||||||
|
if (!$okPin2) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => '2차 비밀번호가 올바르지 않습니다.',
|
||||||
|
'errors' => ['pin2' => ['2차 비밀번호가 올바르지 않습니다.']],
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Dozn 인증 + 저장
|
||||||
|
$res = $dozn->verifyAndSaveOutAccount(
|
||||||
|
$memNo,
|
||||||
|
$memberName, // 세션 실명
|
||||||
|
$depositor, // 위에서 실명 일치 확인됨
|
||||||
|
$bankCode,
|
||||||
|
$account,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json($res, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gate 유효성 체크 (mypage_gate.at만 사용, TTL=5분)
|
||||||
|
*/
|
||||||
|
private function isGateOk(Request $request, int $ttlSec = 300): bool
|
||||||
|
{
|
||||||
|
$gate = (array) $request->session()->get('mypage_gate', []);
|
||||||
|
$ok = (bool)($gate['ok'] ?? false);
|
||||||
|
$at = (int)($gate['at'] ?? 0);
|
||||||
|
|
||||||
|
if (!$ok || $at <= 0) return false;
|
||||||
|
|
||||||
|
return (time() - $at) <= $ttlSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gate 만료 공통 JSON 응답
|
||||||
|
*/
|
||||||
|
private function gateFailJson(Request $request, string $message = '인증이 필요합니다.')
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'message' => $message,
|
||||||
|
'redirect' => route('web.mypage.info.index'), // gate 페이지로 유도
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -432,10 +432,6 @@ class MemberAuthRepository
|
|||||||
$ipfResult = (string) data_get($sessionAll, 'signup.ipf_result', '');
|
$ipfResult = (string) data_get($sessionAll, 'signup.ipf_result', '');
|
||||||
$ipfSeq = (int) data_get($sessionAll, 'signup.ipf_seq', 0);
|
$ipfSeq = (int) data_get($sessionAll, 'signup.ipf_seq', 0);
|
||||||
|
|
||||||
// if ($ipfResult === 'A') {
|
|
||||||
// // 이 케이스는 원래 step0에서 막혀야 정상이지만, 방어적으로 한번 더
|
|
||||||
// throw new \RuntimeException('회원가입이 제한된 IP입니다.');
|
|
||||||
// }
|
|
||||||
if ($ipfResult === 'S' && $ipfSeq > 0) {
|
if ($ipfResult === 'S' && $ipfSeq > 0) {
|
||||||
$this->updateJoinLogAfterSignup($ipfSeq, $memNo, $email);
|
$this->updateJoinLogAfterSignup($ipfSeq, $memNo, $email);
|
||||||
}
|
}
|
||||||
@ -665,29 +661,85 @@ class MemberAuthRepository
|
|||||||
'dt_reg' => $now,
|
'dt_reg' => $now,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ✅ 기존회원인데 mem_st_ring row가 없다? 이건 데이터 이상으로 보고 명확히 실패 처리
|
// 기존회원인데 mem_st_ring row가 없다? 이건 데이터 이상으로 보고 명확히 실패 처리
|
||||||
|
if ($affected <= 0) {
|
||||||
|
throw new \RuntimeException('mem_st_ring_not_found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2차 비밀번호 검증
|
||||||
|
public function verifyPin2(int $memNo, string $pin2Plain): bool
|
||||||
|
{
|
||||||
|
if ($memNo <= 0) return false;
|
||||||
|
|
||||||
|
// 4자리 숫자만 허용
|
||||||
|
if (!preg_match('/^\d{4}$/', $pin2Plain)) return false;
|
||||||
|
|
||||||
|
$stored = (string) DB::table('mem_st_ring')
|
||||||
|
->where('mem_no', $memNo)
|
||||||
|
->value('passwd2');
|
||||||
|
|
||||||
|
if ($stored === '') return false;
|
||||||
|
|
||||||
|
$hash = CiPassword::makePass2($pin2Plain);
|
||||||
|
|
||||||
|
return hash_equals($stored, $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 2차 비밀번호 수정
|
||||||
|
public function updatePin2Only(int $memNo, string $pin2Plain): void
|
||||||
|
{
|
||||||
|
if ($memNo <= 0 || $pin2Plain === '') {
|
||||||
|
throw new \InvalidArgumentException('invalid_payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4자리 숫자 강제 (컨트롤러에서도 validate 하지만 2중 방어)
|
||||||
|
if (!preg_match('/^\d{4}$/', $pin2Plain)) {
|
||||||
|
throw new \InvalidArgumentException('invalid_pin2');
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwd2 = CiPassword::makePass2($pin2Plain);
|
||||||
|
$now = now()->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// mem_st_ring 존재 전제: 없으면 데이터 이상으로 실패 처리(비번 변경과 동일 정책)
|
||||||
|
$affected = DB::table('mem_st_ring')
|
||||||
|
->where('mem_no', $memNo)
|
||||||
|
->update([
|
||||||
|
'passwd2' => $passwd2,
|
||||||
|
'passwd2_reg' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
if ($affected <= 0) {
|
if ($affected <= 0) {
|
||||||
throw new \RuntimeException('mem_st_ring_not_found');
|
throw new \RuntimeException('mem_st_ring_not_found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function logPasswordResetSuccess(int $memNo, string $ip, string $agent): void
|
public function logPasswordResetSuccess(int $memNo, string $ip, string $agent, string $state = 'E'): void
|
||||||
{
|
{
|
||||||
$now = now()->format('Y-m-d H:i:s');
|
$now = now()->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// state: E=비밀번호찾기 변경, S=직접 변경
|
||||||
|
$state = strtoupper(trim($state));
|
||||||
|
if (!in_array($state, ['E', 'S'], true)) {
|
||||||
|
$state = 'E';
|
||||||
|
}
|
||||||
|
|
||||||
DB::table('mem_passwd_modify')->insert([
|
DB::table('mem_passwd_modify')->insert([
|
||||||
'state' => 'E',
|
'state' => $state,
|
||||||
'info' => json_encode([
|
'info' => json_encode([
|
||||||
'mem_no' => (string)$memNo,
|
'mem_no' => (string)$memNo,
|
||||||
'redate' => $now,
|
'redate' => $now,
|
||||||
'remote_addr' => $ip,
|
'remote_addr' => $ip,
|
||||||
'agent' => $agent,
|
'agent' => substr((string)$agent, 0, 500), // 길이 방어(일관 처리)
|
||||||
], JSON_UNESCAPED_UNICODE),
|
], JSON_UNESCAPED_UNICODE),
|
||||||
'rgdate' => $now,
|
'rgdate' => $now,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
360
app/Services/Dozn/DoznAccountAuthService.php
Normal file
360
app/Services/Dozn/DoznAccountAuthService.php
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Dozn;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
final class DoznAccountAuthService
|
||||||
|
{
|
||||||
|
private string $mode;
|
||||||
|
private string $orgCode;
|
||||||
|
private string $apiKey;
|
||||||
|
private string $urlReal;
|
||||||
|
private string $urlTest;
|
||||||
|
private int $timeout;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$cfg = config('services.dozn', []);
|
||||||
|
|
||||||
|
$this->mode = (string)($cfg['mode'] ?? 'real');
|
||||||
|
$this->orgCode = (string)($cfg['org_code'] ?? '');
|
||||||
|
$this->apiKey = (string)($cfg['api_key'] ?? '');
|
||||||
|
$this->urlReal = (string)($cfg['url_real'] ?? '');
|
||||||
|
$this->urlTest = (string)($cfg['url_test'] ?? '');
|
||||||
|
$this->timeout = (int)($cfg['timeout'] ?? 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출금(out) 계좌 성명 인증 + 저장까지 한 번에 처리
|
||||||
|
*
|
||||||
|
* @param int $memNo 회원번호
|
||||||
|
* @param string $memberRealName 회원 실명(서버에서 꺼낸 값)
|
||||||
|
* @param string $inputDepositor 입력한 예금주(화면 입력)
|
||||||
|
* @param string $bankCode 은행코드(001~)
|
||||||
|
* @param string $account 계좌번호(숫자만 권장)
|
||||||
|
* @param bool $requireSameName 입력 예금주 == 회원실명 강제 여부
|
||||||
|
*
|
||||||
|
* @return array { ok, code, message, status, depositor, telegram_no }
|
||||||
|
*/
|
||||||
|
public function verifyAndSaveOutAccount(
|
||||||
|
int $memNo,
|
||||||
|
string $memberRealName,
|
||||||
|
string $inputDepositor,
|
||||||
|
string $bankCode,
|
||||||
|
string $account,
|
||||||
|
bool $requireSameName = true
|
||||||
|
): array {
|
||||||
|
$memNo = (int)$memNo;
|
||||||
|
$memberRealName = trim($memberRealName);
|
||||||
|
$inputDepositor = trim($inputDepositor);
|
||||||
|
$bankCode = trim($bankCode);
|
||||||
|
$account = preg_replace('/[^0-9]/', '', (string)$account);
|
||||||
|
|
||||||
|
// 은행코드 검증 (config/bank_code.php)
|
||||||
|
$bankMap = (array) config('bank_code.flat', []);
|
||||||
|
if ($bankCode === '' || !isset($bankMap[$bankCode])) {
|
||||||
|
return $this->fail('INVALID_BANK', '은행코드가 올바르지 않습니다.');
|
||||||
|
}
|
||||||
|
if ($account === '') {
|
||||||
|
return $this->fail('INVALID_ACCOUNT', '계좌번호를 입력해 주세요.');
|
||||||
|
}
|
||||||
|
if ($inputDepositor === '') {
|
||||||
|
return $this->fail('INVALID_DEPOSITOR', '예금주를 입력해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 입력 예금주와 회원 실명 일치 강제 (네 CI3 로직과 동일)
|
||||||
|
if ($requireSameName && $memberRealName !== '' && $memberRealName !== $inputDepositor) {
|
||||||
|
// act_state=4(정보불일치) 기록(선택)
|
||||||
|
$this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '4', [
|
||||||
|
'reason' => 'member_name_mismatch',
|
||||||
|
'member_real_name' => $memberRealName,
|
||||||
|
'input_depositor' => $inputDepositor,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->fail('NAME_MISMATCH', '가입자 성명과 예금주가 일치하지 않습니다!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// telegram_no 발급 (일별 6자리)
|
||||||
|
$telegramNo = $this->issueTelegramNo();
|
||||||
|
|
||||||
|
// 요청 페이로드 (Dozn 스펙)
|
||||||
|
$payload = [
|
||||||
|
'org_code' => $this->orgCode,
|
||||||
|
'api_key' => $this->apiKey,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
'bank_code' => $bankCode,
|
||||||
|
'account' => $account,
|
||||||
|
'check_depositor' => 'N', // 기존 코드 유지 (실명체크 identify_no는 추후)
|
||||||
|
// 'identify_no' => '761127',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 요청 로그 insert
|
||||||
|
$logSeq = $this->logRequest($memNo, $payload, [
|
||||||
|
'input_depositor' => $inputDepositor,
|
||||||
|
'mode' => $this->mode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 진행상태 기록 (2: 인증진행중)
|
||||||
|
$this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '2', [
|
||||||
|
'log_seq' => $logSeq,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Dozn 호출
|
||||||
|
$resp = $this->callDozn($payload);
|
||||||
|
|
||||||
|
// 결과 로그 update
|
||||||
|
$this->logResult($logSeq, $resp['raw'] ?? []);
|
||||||
|
|
||||||
|
$status = (string)($resp['status'] ?? '');
|
||||||
|
$depositor = (string)($resp['depositor'] ?? '');
|
||||||
|
|
||||||
|
// 성공(200) 처리
|
||||||
|
if ($status === '200') {
|
||||||
|
if ($depositor === '' || $depositor !== $inputDepositor) {
|
||||||
|
// 정보 불일치 (4)
|
||||||
|
$this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '4', [
|
||||||
|
'status' => $status,
|
||||||
|
'returned_depositor' => $depositor,
|
||||||
|
'input_depositor' => $inputDepositor,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'code' => 'DEPOSITOR_MISMATCH',
|
||||||
|
'message' => '예금주가 일치하지 않습니다!',
|
||||||
|
'status' => $status,
|
||||||
|
'depositor' => $depositor,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 성공(3): 계좌 저장
|
||||||
|
$this->saveOutAccountSuccess($memNo, $bankCode, $account, $depositor, [
|
||||||
|
'status' => $status,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
'result' => $resp['raw'] ?? [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'code' => 'OK',
|
||||||
|
'message' => '인증완료 및 계좌번호가 등록되었습니다.',
|
||||||
|
'status' => $status,
|
||||||
|
'depositor' => $depositor,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 잘못된 계좌(520)
|
||||||
|
if ($status === '520') {
|
||||||
|
$this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '5', [
|
||||||
|
'status' => $status,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
'result' => $resp['raw'] ?? [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'code' => 'INVALID_ACCOUNT',
|
||||||
|
'message' => '잘 못된 계좌번호 입니다.',
|
||||||
|
'status' => $status,
|
||||||
|
'depositor' => $depositor,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기타 오류
|
||||||
|
$this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '5', [
|
||||||
|
'status' => $status,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
'result' => $resp['raw'] ?? [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'code' => 'DOZN_ERROR',
|
||||||
|
'message' => ($status ?: 'ERR') . '|인증에 문제가 발생했습니다. 잠시후 다시 시도해주세요.',
|
||||||
|
'status' => $status,
|
||||||
|
'depositor' => $depositor,
|
||||||
|
'telegram_no' => $telegramNo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Dozn HTTP call
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private function callDozn(array $payload): array
|
||||||
|
{
|
||||||
|
$url = $this->mode === 'test' ? $this->urlTest : $this->urlReal;
|
||||||
|
if ($url === '' || $this->orgCode === '' || $this->apiKey === '') {
|
||||||
|
return [
|
||||||
|
'status' => 'CONFIG',
|
||||||
|
'depositor' => '',
|
||||||
|
'raw' => ['error' => 'dozn_config_missing'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$res = Http::timeout($this->timeout)
|
||||||
|
->acceptJson()
|
||||||
|
->asJson()
|
||||||
|
->post($url, $payload);
|
||||||
|
|
||||||
|
$json = $res->json();
|
||||||
|
if (!is_array($json)) $json = [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => (string)($json['status'] ?? ''),
|
||||||
|
'depositor' => (string)($json['depositor'] ?? ''),
|
||||||
|
'raw' => $json,
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('[dozn] request failed', [
|
||||||
|
'err' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'HTTP',
|
||||||
|
'depositor' => '',
|
||||||
|
'raw' => ['error' => 'http_exception', 'message' => $e->getMessage()],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// telegram_no: mem_account_telegram_no 사용 (일별 6자리)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private function issueTelegramNo(): int
|
||||||
|
{
|
||||||
|
$today = Carbon::today()->toDateString();
|
||||||
|
|
||||||
|
return (int) DB::transaction(function () use ($today) {
|
||||||
|
// 오늘 발급된 마지막 telegram_no를 잠그고 다음 번호 생성
|
||||||
|
$last = DB::table('mem_account_telegram_no')
|
||||||
|
->where('date', $today)
|
||||||
|
->lockForUpdate()
|
||||||
|
->max('telegram_no');
|
||||||
|
|
||||||
|
$next = $last ? ((int)$last + 1) : 1;
|
||||||
|
if ($next > 999999) $next = 1;
|
||||||
|
|
||||||
|
DB::table('mem_account_telegram_no')->insert([
|
||||||
|
'telegram_no' => $next,
|
||||||
|
'date' => $today,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Logs: mem_account_log
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private function logRequest(int $memNo, array $payload, array $meta = []): int
|
||||||
|
{
|
||||||
|
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$req = array_merge($payload, $meta);
|
||||||
|
|
||||||
|
$reqJson = json_encode($req, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($reqJson === false) $reqJson = '{}';
|
||||||
|
|
||||||
|
return (int) DB::table('mem_account_log')->insertGetId([
|
||||||
|
'mem_no' => $memNo,
|
||||||
|
'request_data' => $reqJson,
|
||||||
|
'request_time' => $now,
|
||||||
|
'result_data' => '{}',
|
||||||
|
'result_time' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function logResult(int $seq, array $result): void
|
||||||
|
{
|
||||||
|
if ($seq <= 0) return;
|
||||||
|
|
||||||
|
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$resJson = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($resJson === false) $resJson = '{}';
|
||||||
|
|
||||||
|
DB::table('mem_account_log')
|
||||||
|
->where('seq', $seq)
|
||||||
|
->update([
|
||||||
|
'result_data' => $resJson,
|
||||||
|
'result_time' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// mem_account: 회원당 out 1개 유지 (스키마 유지, 코드로 강제)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private function upsertOutAccountState(
|
||||||
|
int $memNo,
|
||||||
|
string $bankCode,
|
||||||
|
string $account,
|
||||||
|
string $depositor,
|
||||||
|
string $actState,
|
||||||
|
array $confirmLog = []
|
||||||
|
): void {
|
||||||
|
$bankMap = (array) config('bank_code.flat', []);
|
||||||
|
$bankName = (string)($bankMap[$bankCode] ?? '');
|
||||||
|
|
||||||
|
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
DB::transaction(function () use ($memNo, $bankCode, $bankName, $account, $depositor, $actState, $confirmLog, $now) {
|
||||||
|
// mem_no + act_type='out' 기준으로 row 잠금
|
||||||
|
$row = DB::table('mem_account')
|
||||||
|
->where('mem_no', $memNo)
|
||||||
|
->where('act_type', 'out')
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'mem_no' => $memNo,
|
||||||
|
'act_type' => 'out',
|
||||||
|
'act_state' => $actState,
|
||||||
|
'bank_code' => $bankCode,
|
||||||
|
'bank_name' => $bankName,
|
||||||
|
'bank_act_name' => $depositor,
|
||||||
|
'bank_act_num' => $account,
|
||||||
|
'in_date' => $now,
|
||||||
|
'act_date' => $now,
|
||||||
|
'confirm_log' => json_encode($confirmLog ?: new \stdClass(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
DB::table('mem_account')->where('seq', $row->seq)->update($data);
|
||||||
|
} else {
|
||||||
|
DB::table('mem_account')->insert($data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveOutAccountSuccess(
|
||||||
|
int $memNo,
|
||||||
|
string $bankCode,
|
||||||
|
string $account,
|
||||||
|
string $depositor,
|
||||||
|
array $confirmLog = []
|
||||||
|
): void {
|
||||||
|
// 성공은 act_state=3
|
||||||
|
$this->upsertOutAccountState($memNo, $bankCode, $account, $depositor, '3', $confirmLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fail(string $code, string $message): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'code' => $code,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -229,9 +229,11 @@ class FindPasswordService
|
|||||||
$this->members->logPasswordResetSuccess(
|
$this->members->logPasswordResetSuccess(
|
||||||
$memNo,
|
$memNo,
|
||||||
(string) $request->ip(),
|
(string) $request->ip(),
|
||||||
(string) $request->userAgent()
|
(string) $request->userAgent(),
|
||||||
|
'E'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// 6) 세션 정리
|
// 6) 세션 정리
|
||||||
$request->session()->forget('find_pw');
|
$request->session()->forget('find_pw');
|
||||||
$request->session()->save();
|
$request->session()->save();
|
||||||
|
|||||||
@ -50,19 +50,19 @@ final class MypageInfoService
|
|||||||
// PASS 전화번호 암호화 (CI 레거시 방식)
|
// PASS 전화번호 암호화 (CI 레거시 방식)
|
||||||
$encPassPhone = (string) $this->seed->encrypt($rawPhone);
|
$encPassPhone = (string) $this->seed->encrypt($rawPhone);
|
||||||
|
|
||||||
// (1) 기존 회원 전화번호와 동일하면 변경 불가
|
// 기존 회원 전화번호와 동일하면 변경 불가
|
||||||
$encMemberPhone = (string) ($sess['_mcell'] ?? '');
|
$encMemberPhone = (string) ($sess['_mcell'] ?? '');
|
||||||
if ($encMemberPhone !== '' && $encPassPhone !== '' && hash_equals($encMemberPhone, $encPassPhone)) {
|
if ($encMemberPhone !== '' && $encPassPhone !== '' && hash_equals($encMemberPhone, $encPassPhone)) {
|
||||||
return $this->fail('현재 등록된 연락처와 동일합니다. 다른 번호로 인증해 주세요.');
|
return $this->fail('현재 등록된 연락처와 동일합니다. 다른 번호로 인증해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// (2) PASS 전화번호가 DB에 존재하면 변경 불가
|
// PASS 전화번호가 DB에 존재하면 변경 불가
|
||||||
// 요구사항: "존재한다면 이전에 가입된 전화번호가 있습니다. 관리자 문의"
|
// "존재한다면 이전에 가입된 전화번호가 있습니다. 관리자 문의"
|
||||||
if ($this->repo->existsEncryptedCell($encPassPhone, $memNo)) {
|
if ($this->repo->existsEncryptedCell($encPassPhone, $memNo)) {
|
||||||
return $this->fail('이미 가입된 휴대폰 번호가 있습니다. 관리자에게 문의해 주세요.');
|
return $this->fail('이미 가입된 휴대폰 번호가 있습니다. 관리자에게 문의해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// (3) CI가 회원정보 mem_info.ci와 일치해야 통과
|
// CI가 회원정보 mem_info.ci와 일치해야 통과
|
||||||
$memberCi = $this->repo->getMemberCi($memNo);
|
$memberCi = $this->repo->getMemberCi($memNo);
|
||||||
if ($memberCi === '' || !hash_equals($memberCi, $ci)) {
|
if ($memberCi === '' || !hash_equals($memberCi, $ci)) {
|
||||||
return $this->fail('가입된 회원정보와 일치하지 않습니다. 관리자에게 문의해 주세요.');
|
return $this->fail('가입된 회원정보와 일치하지 않습니다. 관리자에게 문의해 주세요.');
|
||||||
@ -80,8 +80,6 @@ final class MypageInfoService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 최종 저장: mem_info.cell 업데이트
|
* 최종 저장: mem_info.cell 업데이트
|
||||||
* - 컨트롤러는 DB 처리 안 한다고 했으니, /mypage/info 저장 버튼에서 이 메서드만 호출하면 됨.
|
|
||||||
*
|
|
||||||
* @return array ['ok'=>bool, 'message'=>string, '_cell'=>string]
|
* @return array ['ok'=>bool, 'message'=>string, '_cell'=>string]
|
||||||
*/
|
*/
|
||||||
public function commitPhoneChange(int $memNo, array $passPayload): array
|
public function commitPhoneChange(int $memNo, array $passPayload): array
|
||||||
|
|||||||
214
config/bank_code.php
Normal file
214
config/bank_code.php
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bank / Financial Institution Codes
|
||||||
|
* - Source: provided <option> list
|
||||||
|
* - Grouped for UI rendering + flat for fast lookup
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - name by code: config('bank_code.flat.004') // "KB국민은행"
|
||||||
|
* - groups: config('bank_code.groups.bank_1st')
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
// ✅ UI용 그룹 (원하는 순서대로 렌더링)
|
||||||
|
'groups' => [
|
||||||
|
'bank_1st' => [
|
||||||
|
'label' => '메이저 1금융권',
|
||||||
|
'items' => [
|
||||||
|
'001' => '한국은행',
|
||||||
|
'002' => '산업은행',
|
||||||
|
'003' => '기업은행',
|
||||||
|
'004' => 'KB국민은행',
|
||||||
|
'011' => 'NH농협은행',
|
||||||
|
'020' => '우리은행',
|
||||||
|
'023' => 'SC제일은행',
|
||||||
|
'027' => '한국씨티은행',
|
||||||
|
'031' => '대구은행',
|
||||||
|
'032' => '부산은행',
|
||||||
|
'034' => '광주은행',
|
||||||
|
'035' => '제주은행',
|
||||||
|
'037' => '전북은행',
|
||||||
|
'039' => '경남은행',
|
||||||
|
'071' => '우체국',
|
||||||
|
'081' => '하나은행',
|
||||||
|
'088' => '신한은행',
|
||||||
|
'089' => '케이뱅크',
|
||||||
|
'090' => '카카오뱅크',
|
||||||
|
'007' => '수협은행',
|
||||||
|
'008' => '수출입은행',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'bank_2nd' => [
|
||||||
|
'label' => '2금융권/협동조합/서민금융',
|
||||||
|
'items' => [
|
||||||
|
'012' => '농․축협',
|
||||||
|
'045' => '새마을금고',
|
||||||
|
'048' => '신협',
|
||||||
|
'050' => '저축은행',
|
||||||
|
'064' => '산림조합중앙회',
|
||||||
|
'102' => '대신저축은행',
|
||||||
|
'103' => '에스비아이저축은행',
|
||||||
|
'104' => '에이치케이저축은행',
|
||||||
|
'105' => '웰컴저축은행',
|
||||||
|
'106' => '신한저축은행',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'global' => [
|
||||||
|
'label' => '글로벌/외국계 은행',
|
||||||
|
'items' => [
|
||||||
|
'052' => '모건스탠리은행',
|
||||||
|
'054' => 'HSBC은행',
|
||||||
|
'055' => '도이치은행',
|
||||||
|
'057' => '제이피모간체이스은행',
|
||||||
|
'058' => '미즈호은행',
|
||||||
|
'059' => '엠유에프지은행',
|
||||||
|
'060' => 'BOA은행',
|
||||||
|
'061' => '비엔피파리바은행',
|
||||||
|
'062' => '중국공상은행',
|
||||||
|
'063' => '중국은행',
|
||||||
|
'065' => '대화은행',
|
||||||
|
'066' => '교통은행',
|
||||||
|
'067' => '중국건설은행',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'securities' => [
|
||||||
|
'label' => '증권사',
|
||||||
|
'items' => [
|
||||||
|
'209' => '유안타증권',
|
||||||
|
'218' => 'KB증권',
|
||||||
|
'221' => '상상인증권',
|
||||||
|
'222' => '한양증권',
|
||||||
|
'223' => '리딩투자증권',
|
||||||
|
'224' => 'BNK투자증권',
|
||||||
|
'225' => 'IBK투자증권',
|
||||||
|
'227' => 'KTB투자증권',
|
||||||
|
'238' => '미래에셋대우',
|
||||||
|
'240' => '삼성증권',
|
||||||
|
'243' => '한국투자증권',
|
||||||
|
'247' => 'NH투자증권',
|
||||||
|
'261' => '교보증권',
|
||||||
|
'262' => '하이투자증권',
|
||||||
|
'263' => '현대차증권',
|
||||||
|
'264' => '키움증권',
|
||||||
|
'265' => '이베스트투자증권',
|
||||||
|
'266' => 'SK증권',
|
||||||
|
'267' => '대신증권',
|
||||||
|
'269' => '한화투자증권',
|
||||||
|
'270' => '하나금융투자',
|
||||||
|
'278' => '신한금융투자',
|
||||||
|
'279' => 'DB금융투자',
|
||||||
|
'280' => '유진투자증권',
|
||||||
|
'287' => '메리츠증권',
|
||||||
|
'288' => '카카오페이증권',
|
||||||
|
'290' => '부국증권',
|
||||||
|
'291' => '신영증권',
|
||||||
|
'292' => '케이프투자증권',
|
||||||
|
'293' => '한국증권금융',
|
||||||
|
'294' => '한국포스증권',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
// 필요하면 UI에서 숨겨도 됨 (flat에는 유지)
|
||||||
|
'others' => [
|
||||||
|
'label' => '기타/유관기관',
|
||||||
|
'items' => [
|
||||||
|
'041' => '우리카드',
|
||||||
|
'044' => '외환카드',
|
||||||
|
'076' => '신용보증기금',
|
||||||
|
'077' => '기술보증기금',
|
||||||
|
'101' => '한국신용정보원',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ 코드→이름 빠른 조회용 flat 맵
|
||||||
|
*/
|
||||||
|
'flat' => [
|
||||||
|
'001' => '한국은행',
|
||||||
|
'002' => '산업은행',
|
||||||
|
'003' => '기업은행',
|
||||||
|
'004' => 'KB국민은행',
|
||||||
|
'007' => '수협은행',
|
||||||
|
'008' => '수출입은행',
|
||||||
|
'011' => 'NH농협은행',
|
||||||
|
'012' => '농․축협',
|
||||||
|
'020' => '우리은행',
|
||||||
|
'023' => 'SC제일은행',
|
||||||
|
'027' => '한국씨티은행',
|
||||||
|
'031' => '대구은행',
|
||||||
|
'032' => '부산은행',
|
||||||
|
'034' => '광주은행',
|
||||||
|
'035' => '제주은행',
|
||||||
|
'037' => '전북은행',
|
||||||
|
'039' => '경남은행',
|
||||||
|
'041' => '우리카드',
|
||||||
|
'044' => '외환카드',
|
||||||
|
'045' => '새마을금고',
|
||||||
|
'048' => '신협',
|
||||||
|
'050' => '저축은행',
|
||||||
|
'052' => '모건스탠리은행',
|
||||||
|
'054' => 'HSBC은행',
|
||||||
|
'055' => '도이치은행',
|
||||||
|
'057' => '제이피모간체이스은행',
|
||||||
|
'058' => '미즈호은행',
|
||||||
|
'059' => '엠유에프지은행',
|
||||||
|
'060' => 'BOA은행',
|
||||||
|
'061' => '비엔피파리바은행',
|
||||||
|
'062' => '중국공상은행',
|
||||||
|
'063' => '중국은행',
|
||||||
|
'064' => '산림조합중앙회',
|
||||||
|
'065' => '대화은행',
|
||||||
|
'066' => '교통은행',
|
||||||
|
'067' => '중국건설은행',
|
||||||
|
'071' => '우체국',
|
||||||
|
'076' => '신용보증기금',
|
||||||
|
'077' => '기술보증기금',
|
||||||
|
'081' => '하나은행',
|
||||||
|
'088' => '신한은행',
|
||||||
|
'089' => '케이뱅크',
|
||||||
|
'090' => '카카오뱅크',
|
||||||
|
'101' => '한국신용정보원',
|
||||||
|
'102' => '대신저축은행',
|
||||||
|
'103' => '에스비아이저축은행',
|
||||||
|
'104' => '에이치케이저축은행',
|
||||||
|
'105' => '웰컴저축은행',
|
||||||
|
'106' => '신한저축은행',
|
||||||
|
'209' => '유안타증권',
|
||||||
|
'218' => 'KB증권',
|
||||||
|
'221' => '상상인증권',
|
||||||
|
'222' => '한양증권',
|
||||||
|
'223' => '리딩투자증권',
|
||||||
|
'224' => 'BNK투자증권',
|
||||||
|
'225' => 'IBK투자증권',
|
||||||
|
'227' => 'KTB투자증권',
|
||||||
|
'238' => '미래에셋대우',
|
||||||
|
'240' => '삼성증권',
|
||||||
|
'243' => '한국투자증권',
|
||||||
|
'247' => 'NH투자증권',
|
||||||
|
'261' => '교보증권',
|
||||||
|
'262' => '하이투자증권',
|
||||||
|
'263' => '현대차증권',
|
||||||
|
'264' => '키움증권',
|
||||||
|
'265' => '이베스트투자증권',
|
||||||
|
'266' => 'SK증권',
|
||||||
|
'267' => '대신증권',
|
||||||
|
'269' => '한화투자증권',
|
||||||
|
'270' => '하나금융투자',
|
||||||
|
'278' => '신한금융투자',
|
||||||
|
'279' => 'DB금융투자',
|
||||||
|
'280' => '유진투자증권',
|
||||||
|
'287' => '메리츠증권',
|
||||||
|
'288' => '카카오페이증권',
|
||||||
|
'290' => '부국증권',
|
||||||
|
'291' => '신영증권',
|
||||||
|
'292' => '케이프투자증권',
|
||||||
|
'293' => '한국증권금융',
|
||||||
|
'294' => '한국포스증권',
|
||||||
|
],
|
||||||
|
];
|
||||||
@ -41,4 +41,13 @@ return [
|
|||||||
'min_score' => (float) env('RECAPTCHA_MIN_SCORE', 0.5),
|
'min_score' => (float) env('RECAPTCHA_MIN_SCORE', 0.5),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'dozn' => [
|
||||||
|
'mode' => env('DOZN_MODE', 'real'), // real|test
|
||||||
|
'org_code' => env('DOZN_ORG_CODE', ''),
|
||||||
|
'api_key' => env('DOZN_API_KEY', ''),
|
||||||
|
'url_real' => env('DOZN_URL_REAL', ''),
|
||||||
|
'url_test' => env('DOZN_URL_TEST', ''),
|
||||||
|
'timeout' => (int) env('DOZN_TIMEOUT', 10),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
963
public/assets/js/mypage_renew.js
Normal file
963
public/assets/js/mypage_renew.js
Normal file
@ -0,0 +1,963 @@
|
|||||||
|
/* 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 => ({
|
||||||
|
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||||
|
}[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: '회원탈퇴' });
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
})();
|
||||||
@ -18,7 +18,7 @@
|
|||||||
box-shadow: 0 20px 60px rgba(0,0,0,.55);
|
box-shadow: 0 20px 60px rgba(0,0,0,.55);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.ui-dialog { position: fixed; inset: 0; z-index: 200000; display: none; }
|
.ui-dialog { position: fixed; inset: 0; z-index: 300000; display: none; }
|
||||||
.ui-dialog__header { display:flex; align-items:center; justify-content:space-between; padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,.10); }
|
.ui-dialog__header { display:flex; align-items:center; justify-content:space-between; padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,.10); }
|
||||||
.ui-dialog__title { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.2px; }
|
.ui-dialog__title { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.2px; }
|
||||||
.ui-dialog__x {
|
.ui-dialog__x {
|
||||||
|
|||||||
@ -3,6 +3,13 @@ class UiDialog {
|
|||||||
this.root = document.getElementById(rootId);
|
this.root = document.getElementById(rootId);
|
||||||
if (!this.root) return;
|
if (!this.root) return;
|
||||||
|
|
||||||
|
// ✅ stacking context 문제 방지: 무조건 body 직속으로 이동
|
||||||
|
try {
|
||||||
|
if (this.root.parentElement !== document.body) {
|
||||||
|
document.body.appendChild(this.root);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
this.titleEl = this.root.querySelector("#uiDialogTitle");
|
this.titleEl = this.root.querySelector("#uiDialogTitle");
|
||||||
this.msgEl = this.root.querySelector("#uiDialogMessage");
|
this.msgEl = this.root.querySelector("#uiDialogMessage");
|
||||||
this.okBtn = this.root.querySelector("#uiDialogOk");
|
this.okBtn = this.root.querySelector("#uiDialogOk");
|
||||||
@ -11,23 +18,21 @@ class UiDialog {
|
|||||||
this._resolver = null;
|
this._resolver = null;
|
||||||
this._type = "alert";
|
this._type = "alert";
|
||||||
this._lastFocus = null;
|
this._lastFocus = null;
|
||||||
|
this._closePolicy = { backdrop: false, x: false, esc: false, enter: true };
|
||||||
|
|
||||||
// close triggers
|
// close triggers
|
||||||
this.root.addEventListener("click", (e) => {
|
this.root.addEventListener("click", (e) => {
|
||||||
if (!e.target?.dataset?.uidialogClose) return;
|
if (!e.target?.dataset?.uidialogClose) return;
|
||||||
|
|
||||||
// 어떤 close인지 구분 (backdrop / x)
|
|
||||||
const isBackdrop = !!e.target.closest(".ui-dialog__backdrop");
|
const isBackdrop = !!e.target.closest(".ui-dialog__backdrop");
|
||||||
const isX = !!e.target.closest(".ui-dialog__x");
|
const isX = !!e.target.closest(".ui-dialog__x");
|
||||||
|
|
||||||
// 정책에 따라 차단 (기본: 모두 차단)
|
|
||||||
if (isBackdrop && !this._closePolicy?.backdrop) return;
|
if (isBackdrop && !this._closePolicy?.backdrop) return;
|
||||||
if (isX && !this._closePolicy?.x) return;
|
if (isX && !this._closePolicy?.x) return;
|
||||||
|
|
||||||
this._resolve(false);
|
this._resolve(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// keyboard
|
// keyboard
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (!this.isOpen()) return;
|
if (!this.isOpen()) return;
|
||||||
@ -37,13 +42,28 @@ class UiDialog {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
// Enter는 OK로 처리 (기본 true)
|
|
||||||
if (this._closePolicy?.enter !== false) this._resolve(true);
|
if (this._closePolicy?.enter !== false) this._resolve(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.okBtn.addEventListener("click", () => this._resolve(true));
|
|
||||||
this.cancelBtn.addEventListener("click", () => this._resolve(false));
|
// 버튼 이벤트(존재할 때만)
|
||||||
|
this.okBtn?.addEventListener("click", () => this._resolve(true));
|
||||||
|
this.cancelBtn?.addEventListener("click", () => this._resolve(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 현재 페이지 최상단 z-index 탐색 (모달 겹침 대응)
|
||||||
|
static getTopZIndex() {
|
||||||
|
let maxZ = 0;
|
||||||
|
const nodes = document.querySelectorAll("body *");
|
||||||
|
for (const el of nodes) {
|
||||||
|
const cs = getComputedStyle(el);
|
||||||
|
if (cs.position === "fixed" || cs.position === "absolute" || cs.position === "sticky") {
|
||||||
|
const z = parseInt(cs.zIndex, 10);
|
||||||
|
if (!Number.isNaN(z)) maxZ = Math.max(maxZ, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
isOpen() {
|
isOpen() {
|
||||||
@ -74,6 +94,13 @@ class UiDialog {
|
|||||||
closeOnEnter = true,
|
closeOnEnter = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
// ✅ 항상 최상단: DOM 마지막 + z-index 최상단
|
||||||
|
try {
|
||||||
|
document.body.appendChild(this.root);
|
||||||
|
const topZ = UiDialog.getTopZIndex();
|
||||||
|
this.root.style.zIndex = String(Math.max(300000, topZ + 10)); // 300000 안전 마진
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
this._closePolicy = {
|
this._closePolicy = {
|
||||||
backdrop: !!closeOnBackdrop,
|
backdrop: !!closeOnBackdrop,
|
||||||
x: !!closeOnX,
|
x: !!closeOnX,
|
||||||
@ -84,21 +111,22 @@ class UiDialog {
|
|||||||
this._type = type;
|
this._type = type;
|
||||||
this._lastFocus = document.activeElement;
|
this._lastFocus = document.activeElement;
|
||||||
|
|
||||||
this.titleEl.textContent = title;
|
// 요소가 없으면 조용히 실패(페이지별 차이 방어)
|
||||||
this.msgEl.textContent = message ?? "";
|
if (this.titleEl) this.titleEl.textContent = title;
|
||||||
|
if (this.msgEl) this.msgEl.textContent = message ?? "";
|
||||||
|
|
||||||
this.okBtn.textContent = okText;
|
if (this.okBtn) this.okBtn.textContent = okText;
|
||||||
this.cancelBtn.textContent = cancelText;
|
if (this.cancelBtn) this.cancelBtn.textContent = cancelText;
|
||||||
|
|
||||||
// alert면 cancel 숨김
|
// alert면 cancel 숨김
|
||||||
if (type === "alert") {
|
if (this.cancelBtn) {
|
||||||
this.cancelBtn.style.display = "none";
|
this.cancelBtn.style.display = (type === "alert") ? "none" : "";
|
||||||
} else {
|
|
||||||
this.cancelBtn.style.display = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// danger 스타일
|
// danger 스타일
|
||||||
this.okBtn.classList.toggle("ui-dialog__btn--danger", !!dangerous);
|
if (this.okBtn) {
|
||||||
|
this.okBtn.classList.toggle("ui-dialog__btn--danger", !!dangerous);
|
||||||
|
}
|
||||||
|
|
||||||
// open
|
// open
|
||||||
this.root.classList.add("is-open");
|
this.root.classList.add("is-open");
|
||||||
@ -106,7 +134,9 @@ class UiDialog {
|
|||||||
document.documentElement.style.overflow = "hidden";
|
document.documentElement.style.overflow = "hidden";
|
||||||
|
|
||||||
// focus
|
// focus
|
||||||
setTimeout(() => this.okBtn.focus(), 0);
|
setTimeout(() => {
|
||||||
|
try { this.okBtn?.focus?.(); } catch (e) {}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this._resolver = resolve;
|
this._resolver = resolve;
|
||||||
@ -137,33 +167,20 @@ window.uiDialog = new UiDialog();
|
|||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// ✅ Global showMsg / clearMsg (공통 사용)
|
// ✅ Global showMsg / clearMsg (공통 사용)
|
||||||
// - 페이지에서는 showMsg/clearMsg만 호출
|
|
||||||
// - await showMsg(...) 하면 버튼 클릭 전까지 다음 코드 진행이 멈춤
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
(function () {
|
(function () {
|
||||||
let cachedHelpEl = null;
|
let cachedHelpEl = null;
|
||||||
|
|
||||||
function getHelpEl(opt = {}) {
|
function getHelpEl(opt = {}) {
|
||||||
// opt.helpId를 주면 그걸 우선 사용
|
|
||||||
if (opt.helpId) {
|
if (opt.helpId) {
|
||||||
const el = document.getElementById(opt.helpId);
|
const el = document.getElementById(opt.helpId);
|
||||||
if (el) return el;
|
if (el) return el;
|
||||||
}
|
}
|
||||||
// 기본 fallback id (너가 쓰는 reg_phone_help)
|
|
||||||
if (cachedHelpEl && document.contains(cachedHelpEl)) return cachedHelpEl;
|
if (cachedHelpEl && document.contains(cachedHelpEl)) return cachedHelpEl;
|
||||||
cachedHelpEl = document.getElementById("reg_phone_help");
|
cachedHelpEl = document.getElementById("reg_phone_help");
|
||||||
return cachedHelpEl;
|
return cachedHelpEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* showMsg(msg, opt)
|
|
||||||
* opt: {
|
|
||||||
* type:'alert'|'confirm',
|
|
||||||
* title, okText, cancelText, dangerous,
|
|
||||||
* redirect, helpId
|
|
||||||
* }
|
|
||||||
* return: Promise<boolean> (confirm: true/false, alert: true)
|
|
||||||
*/
|
|
||||||
window.showMsg = async function (msg, opt = {}) {
|
window.showMsg = async function (msg, opt = {}) {
|
||||||
const d = Object.assign({
|
const d = Object.assign({
|
||||||
type: "alert",
|
type: "alert",
|
||||||
@ -173,23 +190,23 @@ window.uiDialog = new UiDialog();
|
|||||||
dangerous: false,
|
dangerous: false,
|
||||||
redirect: "",
|
redirect: "",
|
||||||
helpId: "",
|
helpId: "",
|
||||||
|
// ✅ 닫기 정책 기본값(너가 말한 “다른 공간 클릭해도 닫히지 않게”와 일치)
|
||||||
|
closeOnBackdrop: false,
|
||||||
|
closeOnX: false,
|
||||||
|
closeOnEsc: false,
|
||||||
|
closeOnEnter: true,
|
||||||
}, opt || {});
|
}, opt || {});
|
||||||
|
|
||||||
// ✅ 모달이 있으면 모달로 (여기서 await로 멈춤)
|
|
||||||
if (window.uiDialog && typeof window.uiDialog[d.type] === "function") {
|
if (window.uiDialog && typeof window.uiDialog[d.type] === "function") {
|
||||||
const ok = await window.uiDialog[d.type](msg || "", d);
|
const ok = await window.uiDialog[d.type](msg || "", d);
|
||||||
|
|
||||||
if (d.redirect) {
|
if (d.redirect && ok) {
|
||||||
// alert: OK(true)일 때만 / confirm: OK(true)일 때만
|
window.location.href = d.redirect;
|
||||||
if (ok) {
|
|
||||||
window.location.href = d.redirect;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.type === "confirm" ? !!ok : true;
|
return d.type === "confirm" ? !!ok : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ fallback (모달이 없으면 help 영역에 표시)
|
|
||||||
const helpEl = getHelpEl(d);
|
const helpEl = getHelpEl(d);
|
||||||
if (helpEl) {
|
if (helpEl) {
|
||||||
helpEl.style.display = "block";
|
helpEl.style.display = "block";
|
||||||
@ -197,14 +214,10 @@ window.uiDialog = new UiDialog();
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마지막 fallback
|
|
||||||
alert(msg || "");
|
alert(msg || "");
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* clearMsg(helpId?)
|
|
||||||
*/
|
|
||||||
window.clearMsg = function (helpId = "") {
|
window.clearMsg = function (helpId = "") {
|
||||||
const el = helpId ? document.getElementById(helpId) : getHelpEl({});
|
const el = helpId ? document.getElementById(helpId) : getHelpEl({});
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@ -223,21 +236,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
let payload = null;
|
let payload = null;
|
||||||
try { payload = JSON.parse(flashEl.textContent || "{}"); } catch (e) {}
|
try { payload = JSON.parse(flashEl.textContent || "{}"); } catch (e) {}
|
||||||
|
|
||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
|
|
||||||
/**
|
|
||||||
* payload 예시:
|
|
||||||
* { type:'alert'|'confirm', message:'...', title:'...', okText:'...', cancelText:'...', redirect:'/...' }
|
|
||||||
*/
|
|
||||||
const type = payload.type || "alert";
|
const type = payload.type || "alert";
|
||||||
const ok = await window.uiDialog[type](payload.message || "", payload);
|
const ok = await window.uiDialog[type](payload.message || "", payload);
|
||||||
|
|
||||||
// confirm이고 OK일 때만 redirect 같은 후속 처리
|
|
||||||
if (type === "confirm") {
|
if (type === "confirm") {
|
||||||
if (ok && payload.redirect) window.location.href = payload.redirect;
|
if (ok && payload.redirect) window.location.href = payload.redirect;
|
||||||
} else {
|
} else {
|
||||||
if (payload.redirect) window.location.href = payload.redirect;
|
if (payload.redirect) window.location.href = payload.redirect;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="font-family:Apple SD Gothic Neo,Malgun Gothic,Segoe UI,Arial,sans-serif;
|
<div style="font-family:Apple SD Gothic Neo,Malgun Gothic,Segoe UI,Arial,sans-serif;
|
||||||
color:#98A2B3;font-size:11px;margin-top:10px;">
|
color:#98A2B3;font-size:11px;margin-top:10px;">
|
||||||
© {{ date('Y') }} {{ $brand }}. All rights reserved.
|
© 2018 {{ $brand }}. All rights reserved.
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -758,19 +758,28 @@
|
|||||||
async function askCancelAndGo() {
|
async function askCancelAndGo() {
|
||||||
const msg = "인증을 중단합니다.\n처음부터 다시 진행 하시겠습니까?";
|
const msg = "인증을 중단합니다.\n처음부터 다시 진행 하시겠습니까?";
|
||||||
|
|
||||||
// ✅ 시스템 confirm (레이어/모달 z-index 영향을 안 받음)
|
const ok = await showMsg(msg, {
|
||||||
if (window.confirm(msg)) {
|
type: "confirm",
|
||||||
// iframe 강제 종료(선택)
|
title: "인증취소",
|
||||||
const ifr = document.getElementById(popupName + '_iframe');
|
okText: "처음부터",
|
||||||
if (ifr) ifr.src = 'about:blank';
|
cancelText: "계속 진행",
|
||||||
|
closeOnBackdrop: false,
|
||||||
|
closeOnX: false,
|
||||||
|
closeOnEsc: false,
|
||||||
|
});
|
||||||
|
|
||||||
// 모달 제거
|
if (!ok) return;
|
||||||
const el = document.getElementById(popupName);
|
|
||||||
if (el) el.remove();
|
|
||||||
|
|
||||||
// Step0로 이동
|
// iframe 강제 종료(선택)
|
||||||
window.location.href = @json(route('web.auth.register'));
|
const ifr = document.getElementById(popupName + "_iframe");
|
||||||
}
|
if (ifr) ifr.src = "about:blank";
|
||||||
|
|
||||||
|
// 모달 제거
|
||||||
|
const el = document.getElementById(popupName);
|
||||||
|
if (el) el.remove();
|
||||||
|
|
||||||
|
// Step0로 이동
|
||||||
|
window.location.href = @json(route('web.auth.register'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -97,22 +97,28 @@
|
|||||||
<div class="mypage-card__body">
|
<div class="mypage-card__body">
|
||||||
<div class="mypage-card__title">비밀번호 변경</div>
|
<div class="mypage-card__title">비밀번호 변경</div>
|
||||||
<div class="mypage-card__desc">보안을 위해 주기적으로 변경을 권장해요</div>
|
<div class="mypage-card__desc">보안을 위해 주기적으로 변경을 권장해요</div>
|
||||||
<div class="mypage-card__meta">준비중</div>
|
<div class="mypage-card__meta">변경</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mypage-card__arrow">›</div>
|
<div class="mypage-card__arrow">›</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="mypage-card" href="javascript:void(0)" aria-label="2차 비밀번호 변경 (준비중)" data-action="pw2-change">
|
<button type="button"
|
||||||
|
class="mypage-card mypage-card--btn"
|
||||||
|
data-action="pw2-change"
|
||||||
|
aria-label="2차 비밀번호 변경">
|
||||||
<div class="mypage-card__icon">🔐</div>
|
<div class="mypage-card__icon">🔐</div>
|
||||||
<div class="mypage-card__body">
|
<div class="mypage-card__body">
|
||||||
<div class="mypage-card__title">2차비밀번호 변경</div>
|
<div class="mypage-card__title">2차비밀번호 변경</div>
|
||||||
<div class="mypage-card__desc">민감 기능 이용 시 추가로 확인하는 비밀번호</div>
|
<div class="mypage-card__desc">민감 기능 이용 시 추가로 확인하는 비밀번호</div>
|
||||||
<div class="mypage-card__meta">준비중</div>
|
<div class="mypage-card__meta">변경</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mypage-card__arrow">›</div>
|
<div class="mypage-card__arrow">›</div>
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<a class="mypage-card" href="javascript:void(0)" aria-label="출금계좌번호 {{ $hasWithdrawAccount ? '수정' : '등록' }}" data-action="withdraw-account">
|
<button type="button"
|
||||||
|
class="mypage-card mypage-card--btn"
|
||||||
|
aria-label="출금계좌번호 {{ $hasWithdrawAccount ? '수정' : '등록' }}"
|
||||||
|
data-action="withdraw-account">
|
||||||
<div class="mypage-card__icon">🏦</div>
|
<div class="mypage-card__icon">🏦</div>
|
||||||
<div class="mypage-card__body">
|
<div class="mypage-card__body">
|
||||||
<div class="mypage-card__title">출금계좌번호</div>
|
<div class="mypage-card__title">출금계좌번호</div>
|
||||||
@ -124,7 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mypage-card__arrow">›</div>
|
<div class="mypage-card__arrow">›</div>
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="mypage-card mypage-card--btn" data-action="consent-edit">
|
<button type="button" class="mypage-card mypage-card--btn" data-action="consent-edit">
|
||||||
<div class="mypage-card__icon">📩</div>
|
<div class="mypage-card__icon">📩</div>
|
||||||
@ -255,303 +261,32 @@
|
|||||||
@endpush
|
@endpush
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
/**
|
|
||||||
* 1) ✅ 재인증 타이머 (버튼 유무와 무관하게 항상 실행)
|
|
||||||
* - remainSec 기반 카운트다운
|
|
||||||
* - 0 되면 alert → info 페이지로 이동
|
|
||||||
*/
|
|
||||||
(function () {
|
(function () {
|
||||||
const box = document.querySelector('.mypage-reauth--countdown');
|
const prev = window.mypageRenew || {};
|
||||||
const out = document.getElementById('reauthCountdown');
|
|
||||||
if (!box || !out) return;
|
|
||||||
|
|
||||||
const redirectUrl = "{{ route('web.mypage.info.gate_reset') }}";
|
window.mypageRenew = Object.assign({}, prev, {
|
||||||
|
memberName: @json($memberName),
|
||||||
// ✅ 서버가 내려준 세션 until(문자열) 우선 사용
|
bankGroups: @json(config('bank_code.groups')),
|
||||||
const untilStr = (box.getAttribute('data-until') || '').trim();
|
bankFlat: @json(config('bank_code.flat')),
|
||||||
// fallback: 렌더링 시점 remain
|
bankGroupLabels: {
|
||||||
const remainFallback = parseInt(box.getAttribute('data-remain') || '0', 10);
|
bank_1st: '메이저 1금융권',
|
||||||
|
bank_2nd: '2금융권/협동조합/서민금융',
|
||||||
// until 파싱 (서버가 'YYYY-MM-DD HH:MM:SS'로 주면 JS Date가 못 읽는 경우가 있음)
|
global: '글로벌/외국계 은행',
|
||||||
// 그래서 'YYYY-MM-DDTHH:MM:SS'로 변환해서 파싱 시도
|
securities: '증권사',
|
||||||
function parseUntilMs(s){
|
others: '기타/유관기관',
|
||||||
if (!s) return null;
|
},
|
||||||
|
urls: Object.assign({}, (prev.urls || {}), {
|
||||||
// 2026-02-01 16:33:50 -> 2026-02-01T16:33:50
|
gateReset: @json(route('web.mypage.info.gate_reset')),
|
||||||
const isoLike = s.includes('T') ? s : s.replace(' ', 'T');
|
passReady: @json(route('web.mypage.info.pass.ready')),
|
||||||
const ms = Date.parse(isoLike);
|
danalStart: @json(route('web.mypage.info.danal.start')),
|
||||||
|
passwordUpdate: @json(route('web.mypage.info.password.update')),
|
||||||
return Number.isFinite(ms) ? ms : null;
|
pin2Update: @json(route('web.mypage.info.pin2.update')),
|
||||||
}
|
withdrawVerifyOut: @json(route('web.mypage.info.withdraw.verify_out')),
|
||||||
|
})
|
||||||
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>
|
</script>
|
||||||
|
<script src="{{ asset('assets/js/mypage_renew.js') }}" defer></script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -40,11 +40,13 @@ Route::prefix('mypage')->name('web.mypage.')
|
|||||||
Route::post('info', [InfoGateController::class, 'verify'])->name('info.verify');
|
Route::post('info', [InfoGateController::class, 'verify'])->name('info.verify');
|
||||||
|
|
||||||
Route::get('info_renew', [InfoGateController::class, 'info_renew'])->name('info.renew');
|
Route::get('info_renew', [InfoGateController::class, 'info_renew'])->name('info.renew');
|
||||||
|
Route::post('info/password-update', [InfoGateController::class, 'passwordUpdate'])->name('info.password.update');
|
||||||
|
Route::post('info/pin2', [InfoGateController::class, 'updatePin2'])->name('info.pin2.update');
|
||||||
Route::post('info/pass/ready', [InfoGateController::class, 'passReady'])->name('info.pass.ready');
|
Route::post('info/pass/ready', [InfoGateController::class, 'passReady'])->name('info.pass.ready');
|
||||||
Route::post('info/danal/start', [InfoGateController::class, 'danalStart'])->name('info.danal.start');
|
Route::post('info/danal/start', [InfoGateController::class, 'danalStart'])->name('info.danal.start');
|
||||||
Route::post('info/danal/result', [InfoGateController::class, 'danalResult'])->name('info.danal.result');
|
Route::post('info/danal/result', [InfoGateController::class, 'danalResult'])->name('info.danal.result');
|
||||||
Route::get('info/gate-reset', [InfoGateController::class, 'gateReset'])->name('info.gate_reset');
|
Route::get('info/gate-reset', [InfoGateController::class, 'gateReset'])->name('info.gate_reset');
|
||||||
|
Route::post('info/withdraw/verify-out', [InfoGateController::class, 'verifyOut'])->name('info.withdraw.verify_out');
|
||||||
|
|
||||||
Route::view('usage', 'web.mypage.usage.index')->name('usage.index');
|
Route::view('usage', 'web.mypage.usage.index')->name('usage.index');
|
||||||
Route::view('exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
Route::view('exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user