session()->get('mypage_gate', []); $ok = (bool) ($gate['ok'] ?? false); $at = (int) ($gate['at'] ?? 0); $ttlSeconds = 5 * 60; $isValid = $ok && $at > 0 && (time() - $at) <= $ttlSeconds; if ($isValid) { return redirect()->to('/mypage/info_renew'); } return view('web.mypage.info.gate'); } public function info_renew(Request $request, MemInfoService $memInfoService) { // gate (기존 그대로) $gate = (array) $request->session()->get('mypage_gate', []); $gateOk = (bool) Arr::get($gate, 'ok', false); $gateAt = (int) Arr::get($gate, 'at', 0); $ttlSec = 5 * 60; $nowTs = now()->timestamp; $expireTs = ($gateOk && $gateAt > 0) ? ($gateAt + $ttlSec) : 0; $remainSec = ($expireTs > 0) ? max(0, $expireTs - $nowTs) : 0; $isGateValid = ($remainSec > 0); // 회원정보: _sess 기반 $sess = (array) $request->session()->get('_sess', []); $memberName = (string) Arr::get($sess, '_mname', ''); $memberEmail = (string) Arr::get($sess, '_mid', ''); $dtReg = (string) Arr::get($sess, '_dt_reg', ''); $memNo = (int) Arr::get($sess, '_mno', 0); $rawPhone = (string) Arr::get($sess, '_mcell', ''); // 전화번호 복호 (인증 완료 상태라 마스킹 제외) $memberPhone = (string) $this->seed->decrypt($rawPhone); $user = $request->user(); $withdrawBankName = (string)($user->withdraw_bank_name ?? $user->bank_name ?? ''); $withdrawAccount = (string)($user->withdraw_account ?? $user->bank_account ?? ''); $hasWithdrawAccount = ($withdrawBankName !== '' && $withdrawAccount !== ''); $recv = $memNo > 0 ? $memInfoService->getReceive($memNo) : ['rcv_email' => 'n', 'rcv_sms' => 'n', 'out_account' => null]; $agreeEmail = $recv['rcv_email']; $agreeSms = $recv['rcv_sms']; $outAccount = $recv['out_account'] ?? null; return view('web.mypage.info.renew', [ // gate 'ttlSec' => $ttlSec, 'expireTs' => (int) $expireTs, 'remainSec' => (int) $remainSec, 'isGateValid' => (bool) $isGateValid, // member (sess) 'memberName' => $memberName, 'memberEmail' => $memberEmail, 'memberPhone' => $memberPhone, 'memberDtReg' => $dtReg, // etc 'hasWithdrawAccount' => (bool) $hasWithdrawAccount, 'agreeEmail' => $agreeEmail, 'agreeSms' => $agreeSms, 'outAccount' => $outAccount, ]); } /** * 재인증을 위한 연장 및 세셔초기화 */ public function gateReset(Request $request) { $ttlSec = 5 * 60; $now = time(); $gate = (array) $request->session()->get('mypage_gate', []); $gateOk = (bool)($gate['ok'] ?? false); $gateAt = (int) ($gate['at'] ?? 0); $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'); } /** * 비밀번호 재인증 처리 */ public function verify(Request $request, MemInfoService $memInfoService) { $request->validate([ 'password' => ['required', 'string'], ], [ 'password.required' => '비밀번호를 입력해 주세요.', ]); $sess = (array) $request->session()->get('_sess', []); $isLogin = (bool) ($sess['_login_'] ?? false); $email = (string) ($sess['_mid'] ?? ''); if (!$isLogin || $email === '') { return redirect()->route('web.auth.login', ['return_url' => url('/mypage/info')]); } $pw = (string) $request->input('password'); $res = $memInfoService->attemptLegacyLogin([ 'email' => $email, 'password' => $pw, 'ip' => $request->ip(), 'ua' => substr((string) $request->userAgent(), 0, 500), 'return_url' => url('/mypage/info'), ]); $ok = (bool)($res['ok'] ?? $res['success'] ?? false); if (!$ok) { $msg = (string)($res['message'] ?? '비밀번호가 일치하지 않습니다.'); return back() ->withErrors(['password' => $msg]) // 레이어 알림 스크립트가 이걸 잡음 ->withInput($request->except('password')); } // 게이트 통과 세션 (예: 30분) $request->session()->put('mypage_gate', [ 'ok' => true, 'email' => $email, 'at' => time(), ]); return redirect()->route('web.mypage.info.renew'); } public function passReady(Request $request) { // 목적 저장 (result에서 분기용) $purpose = (string) $request->input('purpose', 'mypage_phone_change'); $request->session()->put('mypage.pass_purpose', $purpose); $request->session()->save(); $danal = app(\App\Services\Danal\DanalAuthtelService::class)->prepare([ 'targetUrl' => route('web.mypage.info.danal.result'), 'backUrl' => route('web.mypage.info.renew'), // 취소/뒤로가기 'cpTitle' => request()->getHost(), ]); if (!($danal['ok'] ?? false)) { return response()->json([ 'ok' => false, 'message' => $danal['message'] ?? '본인인증 준비에 실패했습니다. 잠시 후 다시 시도해 주세요.', ], 500); } // 필요하면 txid 저장 (회원가입과 동일) $request->session()->put('mypage.danal', [ 'txid' => $danal['txid'] ?? null, 'created_at' => now()->toDateTimeString(), 'purpose' => $purpose, ]); $request->session()->save(); return response()->json([ 'ok' => true, 'reason' => 'danal_ready', 'popup' => [ 'url' => route('web.mypage.info.danal.start'), 'fields' => $danal['fields'], ], ]); } public function danalStart(Request $request) { $fieldsJson = (string) $request->input('fields', ''); $fields = json_decode($fieldsJson, true); if (!is_array($fields) || empty($fields)) { abort(400, 'Invalid Danal fields'); } $platform = strtolower((string) $request->input('platform', ($fields['platform'] ?? ''))); $isMobile = false; if ($platform === 'mobile') { $isMobile = true; } elseif ($platform === 'web') { $isMobile = false; } else { $ua = (string) $request->header('User-Agent', ''); $isMobile = (bool) preg_match('/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i', $ua); } $action = $isMobile ? 'https://wauth.teledit.com/Danal/WebAuth/Mobile/Start.php' : 'https://wauth.teledit.com/Danal/WebAuth/Web/Start.php'; unset($fields['platform']); // 기존 autosubmit 뷰 재사용 OK return view('web.auth.danal_autosubmit', [ 'action' => $action, 'fields' => $fields, 'isMobile' => $isMobile, ]); } public function danalResult( Request $request, DanalAuthtelService $danal, MemberAuthRepository $repo ) { $payload = $request->all(); $tid = (string)($payload['TID'] ?? ''); if ($tid === '') { return response()->view('web.auth.danal_finish_top', [ 'ok' => false, 'message' => 'TID가 없습니다.', 'redirect' => route('web.mypage.info.index'), ]); } // CI와 동일: TID로 CONFIRM $res = $danal->confirm($tid, 0, 1); // 로그 저장 (성공/실패 무조건) $logSeq = $repo->insertDanalAuthLog('M', (array) $res); if ($logSeq > 0) { $request->session()->put('mypage.pass.danal_log_seq', $logSeq); } $ok = (($res['RETURNCODE'] ?? '') === '0000'); if (!$ok) { return response()->view('web.auth.danal_finish_top', [ 'ok' => false, 'message' => ($res['RETURNMSG'] ?? '본인인증에 실패했습니다.') . ' (' . ($res['RETURNCODE'] ?? 'NO_CODE') . ')', 'redirect' => route('web.mypage.info.index'), ]); } // 목적이 마이페이지 연락처 변경일 때만 추가 검증 $purpose = (string) $request->session()->get('mypage.pass_purpose', ''); if ($purpose === 'mypage_phone_change') { $sess = (array) $request->session()->get('_sess', []); $memNo = (int) ($sess['_mno'] ?? 0); if ($memNo <= 0) { return response()->view('web.auth.danal_finish_top', [ 'ok' => false, 'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인 후 시도해 주세요.', 'redirect' => route('web.auth.login'), ]); } $svc = app(\App\Services\MypageInfoService::class); //연락처 검증 $check = $svc->validatePassPhoneChange($sess, (array) $res); if (!($check['ok'] ?? false)) { return response()->view('web.auth.danal_finish_top', [ 'ok' => false, 'message' => $check['message'] ?? '연락처 변경 검증에 실패했습니다.', 'redirect' => route('web.mypage.info.index'), ]); } //전화번호 저장 $check = $svc->commitPhoneChange($memNo, (array) $res); if (!($check['ok'] ?? false)) { return response()->view('web.auth.danal_finish_top', [ 'ok' => false, 'message' => $check['message'] ?? '연락처 저장에 실패했습니다.', 'redirect' => route('web.mypage.info.index'), ]); } $request->session()->put('_sess._mcell', $check['_cell'] ?? ''); //전화번호 변경 }else{ return response()->view('web.auth.danal_finish_top', [ 'ok' => false, 'message' => '요청이 올바르지 않습니다. 다시 시도해 주세요.', 'redirect' => route('web.mypage.info.index'), ]); } // 성공: 마이페이지 인증 플래그 세션 저장 $request->session()->forget('mypage.pass'); $request->session()->forget('mypage.danal'); $request->session()->save(); return response()->view('web.auth.danal_finish_top', [ 'ok' => true, 'message' => '본인인증이 완료되었습니다.', 'redirect' => url('/mypage/info_renew'), ]); } /*비밀번호 변경 저장*/ 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, $email, (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); $repo->logPasswordResetSuccess2Only( $memNo, $email, (string) $request->ip(), (string) $request->userAgent() ); } 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); } /** * 마케팅 수신 동의 저장 (email/sms 각각 변경 시 해당 dt만 업데이트) * - gate(5분) 유효해야 함 * - mem_info: rcv_email, rcv_sms, dt_rcv_email, dt_rcv_sms */ public function marketingUpdate(Request $request, MemInfoService $memInfoService) { if (!$this->isGateOk($request)) { return $this->gateFailJson($request, '인증 시간이 만료되었습니다. 다시 인증해 주세요.'); } // 레거시 세션 기준 $sess = (array) $request->session()->get('_sess', []); $isLogin = (bool) ($sess['_login_'] ?? false); $memNo = (int) ($sess['_mno'] ?? 0); if (!$isLogin || $memNo <= 0) { return response()->json([ 'ok' => false, 'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인해 주세요.', 'redirect' => route('web.auth.login'), ], 401); } $request->validate([ 'rcv_email' => ['required', Rule::in(['y','n'])], 'rcv_sms' => ['required', Rule::in(['y','n'])], ], [ 'rcv_email.required' => '이메일 수신 동의 값을 선택해 주세요.', 'rcv_sms.required' => 'SMS 수신 동의 값을 선택해 주세요.', 'rcv_email.in' => '이메일 수신 동의 값이 올바르지 않습니다.', 'rcv_sms.in' => 'SMS 수신 동의 값이 올바르지 않습니다.', ]); $rcvEmail = (string) $request->input('rcv_email', 'n'); $rcvSms = (string) $request->input('rcv_sms', 'n'); try { $result = $memInfoService->setReceiveSelective($memNo, $rcvEmail, $rcvSms); // 세션/표시용 값이 따로 있으면 여기서 같이 갱신해도 됨(선택) // 예: $request->session()->put('_sess.rcv_email', $rcvEmail); return response()->json([ 'ok' => true, 'message' => $result['message'] ?? '수신 동의 설정이 저장되었습니다.', 'changed' => $result['changed'] ?? [], ], 200); } catch (\Throwable $e) { Log::error('[mypage] marketing consent update failed', [ 'mem_no' => $memNo, 'err' => $e->getMessage(), ]); return response()->json([ 'ok' => false, 'message' => '수신 동의 저장에 실패했습니다. 잠시 후 다시 시도해 주세요.', ], 500); } } /** * 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); } //회원탈퇴 public function withdraw( Request $request, MemInfoService $memInfoService, MemberAuthRepository $repo ) { // gate(5분) 유효해야 함 (지금 페이지가 info_renew니까 정책 그대로) if (!$this->isGateOk($request)) { return $this->gateFailJson($request, '인증 시간이 만료되었습니다. 다시 인증해 주세요.'); } // 입력 검증 $request->validate([ 'agree' => ['required', 'in:1'], 'password' => ['required', 'string'], 'pin2' => ['required', 'digits:4'], ], [ 'agree.required' => '안내사항 동의가 필요합니다.', 'agree.in' => '안내사항 동의가 필요합니다.', 'password.required' => '비밀번호를 입력해 주세요.', 'pin2.required' => '2차 비밀번호(숫자 4자리)를 입력해 주세요.', 'pin2.digits' => '2차 비밀번호는 숫자 4자리여야 합니다.', ]); $sess = (array) $request->session()->get('_sess', []); $memNo = (int) Arr::get($sess, '_mno', 0); if ($memNo <= 0) { return response()->json([ 'ok' => false, 'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인해 주세요.', 'redirect' => route('web.auth.login'), ], 401); } $pw = (string) $request->input('password'); $pin2 = (string) $request->input('pin2'); // ✅ 1차 비밀번호 검증(Repo) if (!$repo->verifyLegacyPassword($memNo, $pw)) { return response()->json([ 'ok' => false, 'message' => '비밀번호가 일치하지 않습니다.', 'errors' => ['password' => ['비밀번호가 일치하지 않습니다.']], ], 422); } // ✅ 2차 비밀번호 검증(Repo) if (!$repo->verifyPin2($memNo, $pin2)) { return response()->json([ 'ok' => false, 'message' => '2차 비밀번호가 일치하지 않습니다.', 'errors' => ['pin2' => ['2차 비밀번호가 일치하지 않습니다.']], ], 422); } // ✅ 탈퇴 가능 조건 검증 + 처리(Service) try { $res = $memInfoService->withdrawMember($memNo); if (!($res['ok'] ?? false)) { return response()->json([ 'ok' => false, 'message' => $res['message'] ?? '회원탈퇴가 불가합니다.', ], 422); } // 세션 종료 (레거시 세션 포함) $request->session()->flush(); $request->session()->invalidate(); $request->session()->regenerateToken(); return response()->json([ 'ok' => true, 'message' => $res['message'] ?? '회원탈퇴가 완료되었습니다.', 'redirect' => route('web.auth.login'), ]); } catch (\Throwable $e) { Log::error('[mypage] withdraw failed', [ 'mem_no' => $memNo, 'err' => $e->getMessage(), ]); return response()->json([ 'ok' => false, 'message' => '회원탈퇴 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', ], 500); } } }