diff --git a/app/Http/Controllers/Web/Mypage/InfoGateController.php b/app/Http/Controllers/Web/Mypage/InfoGateController.php index 2cb1edd..34d85e8 100644 --- a/app/Http/Controllers/Web/Mypage/InfoGateController.php +++ b/app/Http/Controllers/Web/Mypage/InfoGateController.php @@ -9,6 +9,7 @@ use App\Services\Dozn\DoznAccountAuthService; use App\Repositories\Member\MemberAuthRepository; use App\Support\LegacyCrypto\CiSeedCrypto; use App\Support\LegacyCrypto\CiPassword; +use Illuminate\Validation\Rule; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Auth; @@ -31,6 +32,7 @@ final class InfoGateController extends Controller $at = (int) ($gate['at'] ?? 0); $ttlSeconds = 5 * 60; $isValid = $ok && $at > 0 && (time() - $at) <= $ttlSeconds; + if ($isValid) { return redirect()->to('/mypage/info_renew'); } @@ -39,7 +41,7 @@ final class InfoGateController extends Controller } - public function info_renew(Request $request) + public function info_renew(Request $request, MemInfoService $memInfoService) { // gate (기존 그대로) $gate = (array) $request->session()->get('mypage_gate', []); @@ -58,17 +60,23 @@ final class InfoGateController extends Controller $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', ''); // 전화번호 복호 (인증 완료 상태라 마스킹 제외) - $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 !== ''); - $agreeEmail = (string)($user->agree_marketing_email ?? $user->agree_email ?? 'n'); - $agreeSms = (string)($user->agree_marketing_sms ?? $user->agree_sms ?? 'n'); + $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 @@ -87,6 +95,7 @@ final class InfoGateController extends Controller 'hasWithdrawAccount' => (bool) $hasWithdrawAccount, 'agreeEmail' => $agreeEmail, 'agreeSms' => $agreeSms, + 'outAccount' => $outAccount, ]); } @@ -252,14 +261,6 @@ final class InfoGateController extends Controller ) { $payload = $request->all(); - if (config('app.debug')) { - Log::info('[MYPAGE][DANAL][RESULT] keys', [ - 'method' => $request->method(), - 'url' => $request->fullUrl(), - 'keys' => array_keys($payload), - ]); - } - $tid = (string)($payload['TID'] ?? ''); if ($tid === '') { return response()->view('web.auth.danal_finish_top', [ @@ -329,14 +330,14 @@ final class InfoGateController extends Controller }else{ return response()->view('web.auth.danal_finish_top', [ 'ok' => false, - 'message' => $check['message'] ?? '연락처 변경에 실패했습니다.\n\n관리자에게 문의하세요!', + 'message' => '요청이 올바르지 않습니다. 다시 시도해 주세요.', 'redirect' => route('web.mypage.info.index'), ]); } // 성공: 마이페이지 인증 플래그 세션 저장 $request->session()->forget('mypage.pass'); - $request->session()->forget('mypage.danal '); + $request->session()->forget('mypage.danal'); $request->session()->save(); @@ -594,6 +595,69 @@ final class InfoGateController extends Controller 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분) */ @@ -620,5 +684,96 @@ final class InfoGateController extends Controller ], 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); + } + } + } diff --git a/app/Http/Middleware/LegacyAuth.php b/app/Http/Middleware/LegacyAuth.php index 325dfaf..f93a998 100644 --- a/app/Http/Middleware/LegacyAuth.php +++ b/app/Http/Middleware/LegacyAuth.php @@ -9,13 +9,28 @@ class LegacyAuth { public function handle(Request $request, Closure $next) { + // 레거시 세션: 로그인 플래그 + memNo 둘 다 확인 $loggedIn = (bool) $request->session()->get('_sess._login_', false); + $memNo = (int) $request->session()->get('_sess._mno', 0); - if (!$loggedIn) { - // 로그인 성공 후 원래 가려던 곳으로 보내기 위해 intended 저장 - // (Laravel auth 안 쓰더라도 이 키는 redirect()->intended()가 알아서 씀) - $request->session()->put('url.intended', $request->fullUrl()); + if (!$loggedIn || $memNo <= 0) { + // GET만 intended 저장 + if ($request->isMethod('get')) { + $request->session()->put('url.intended', $request->fullUrl()); + } + + // JSON을 기대하는 요청이면 JSON 401 + // (fetch/axios는 Accept: application/json 넣으면 expectsJson()이 안정적으로 동작) + if ($request->expectsJson()) { + return response()->json([ + 'ok' => false, + 'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인해 주세요.', + 'redirect' => route('web.auth.login'), + ], 401); + } + + // 일반 웹 요청: 로그인 페이지로 return redirect()->route('web.auth.login') ->with('ui_dialog', [ 'type' => 'alert', diff --git a/app/Services/MemInfoService.php b/app/Services/MemInfoService.php index 5bc54bc..0592f63 100644 --- a/app/Services/MemInfoService.php +++ b/app/Services/MemInfoService.php @@ -13,33 +13,6 @@ use Illuminate\Support\Facades\Schema; class MemInfoService { - /** - * CI: set_receive() - * 프로모션 수신 동의 변경 (행 잠금) - */ - public function setReceive(int $memNo, string $rcvEmail, string $rcvSms, ?string $rcvPush = null): void - { - DB::transaction(function () use ($memNo, $rcvEmail, $rcvSms, $rcvPush) { - /** @var MemInfo $mem */ - $mem = MemInfo::query()->whereKey($memNo)->lockForUpdate()->firstOrFail(); - - $now = Carbon::now()->format('Y-m-d H:i:s'); - - $mem->rcv_email = $rcvEmail; - $mem->rcv_sms = $rcvSms; - $mem->dt_rcv_email = $now; - $mem->dt_rcv_sms = $now; - - if ($rcvPush !== null) { - $mem->rcv_push = $rcvPush; - $mem->dt_rcv_push = $now; - } - - $mem->dt_mod = $now; - $mem->save(); - }); - } - /** * CI: mem_email_vali() */ @@ -491,4 +464,213 @@ class MemInfoService ]; }); } + + public function getReceive(int $memNo): array + { + $mem = MemInfo::query()->whereKey($memNo)->first(); + + // ✅ 출금계좌 인증정보 (있으면 1건) + $outAccount = DB::table('mem_account') + ->select(['bank_name', 'bank_act_num', 'bank_act_name', 'act_date']) + ->where('mem_no', $memNo) + ->where('act_type', 'out') + ->where('act_state', '3') + ->orderByDesc('act_date') + ->first(); + + if (!$mem) { + return [ + 'rcv_email' => 'n', + 'rcv_sms' => 'n', + 'rcv_push' => null, + 'out_account' => $outAccount ? [ + 'bank_name' => (string)($outAccount->bank_name ?? ''), + 'bank_act_num' => (string)($outAccount->bank_act_num ?? ''), + 'bank_act_name'=> (string)($outAccount->bank_act_name ?? ''), + 'act_date' => (string)($outAccount->act_date ?? ''), + ] : null, + ]; + } + + return [ + 'rcv_email' => (string)($mem->rcv_email ?? 'n'), + 'rcv_sms' => (string)($mem->rcv_sms ?? 'n'), + 'rcv_push' => $mem->rcv_push !== null ? (string)$mem->rcv_push : null, + + // ✅ 추가 + 'out_account' => $outAccount ? [ + 'bank_name' => (string)($outAccount->bank_name ?? ''), + 'bank_act_num' => (string)($outAccount->bank_act_num ?? ''), + 'bank_act_name'=> (string)($outAccount->bank_act_name ?? ''), + 'act_date' => (string)($outAccount->act_date ?? ''), + ] : null, + ]; + } + + + /*회원정보 수신동의 정보 저장*/ + public function setReceiveSelective(int $memNo, string $rcvEmail, string $rcvSms): array + { + $rcvEmail = ($rcvEmail === 'y') ? 'y' : 'n'; + $rcvSms = ($rcvSms === 'y') ? 'y' : 'n'; + + return DB::transaction(function () use ($memNo, $rcvEmail, $rcvSms) { + /** @var MemInfo $mem */ + $mem = MemInfo::query()->whereKey($memNo)->lockForUpdate()->firstOrFail(); + + $beforeEmail = (string) ($mem->rcv_email ?? 'n'); + $beforeSms = (string) ($mem->rcv_sms ?? 'n'); + + $changedEmail = ($beforeEmail !== $rcvEmail); + $changedSms = ($beforeSms !== $rcvSms); + + if (!$changedEmail && !$changedSms) { + return [ + 'changed' => [], + 'message' => '변경된 내용이 없습니다.', + ]; + } + + $now = Carbon::now()->format('Y-m-d H:i:s'); + + if ($changedEmail) { + $mem->rcv_email = $rcvEmail; + $mem->dt_rcv_email = $now; + } + + if ($changedSms) { + $mem->rcv_sms = $rcvSms; + $mem->dt_rcv_sms = $now; + } + + // 공통 수정일은 변경이 있을 때만 + $mem->dt_mod = $now; + $mem->save(); + + $changed = []; + if ($changedEmail) $changed[] = 'email'; + if ($changedSms) $changed[] = 'sms'; + + $label = []; + if ($changedEmail) $label[] = '이메일'; + if ($changedSms) $label[] = 'SMS'; + + return [ + 'changed' => $changed, + 'message' => implode('·', $label) . ' 수신 동의 설정이 저장되었습니다.', + ]; + }); + } + + //회원 탈퇴 최근 구매내역 확인 + public function validateWithdraw(int $memNo): array + { + if ($memNo <= 0) { + return ['ok' => false, 'message' => '로그인 정보가 올바르지 않습니다.']; + } + + // ✅ 최근 7일 이내 구매내역(stat_pay p/t) 있으면 불가 + $from = Carbon::today()->subDays(7)->startOfDay(); + + $cnt = (int) DB::table('giftcard_order') // ⚠️ 테이블명이 다르면 여기만 바꾸면 됨 + ->where('mem_no', $memNo) + ->whereIn('stat_pay', ['p','t']) + ->where('dt_stat_pay', '>=', $from->toDateTimeString()) + ->count(); + + if ($cnt > 0) { + return [ + 'ok' => false, + 'message' => '죄송합니다. 최근 7일 이내 구매내역이 있는 경우 즉시 탈퇴가 불가합니다. 고객센터로 탈퇴 접수를 해주시기 바랍니다', + ]; + } + + return ['ok' => true]; + } + + //회원탈퇴 진행 + public function withdrawMember(int $memNo): array + { + $v = $this->validateWithdraw($memNo); + if (!($v['ok'] ?? false)) return $v; + + $now = Carbon::now(); + $dtReqOut = $now->copy()->addDays(90)->toDateString(); // Y-m-d + + DB::transaction(function () use ($memNo, $now, $dtReqOut) { + + // 1) mem_info 비식별/초기화 + 탈퇴 처리 + DB::table('mem_info') + ->where('mem_no', $memNo) + ->update([ + 'name' => '', + 'name_first' => '', + 'name_mid' => '', + 'name_last' => '', + 'birth' => '', + 'gender' => '', + 'native' => '', + 'cell_corp' => '', + 'cell_phone' => '', + 'ci' => '', + + 'bank_code' => '', + 'bank_name' => '', + 'bank_act_num' => '', + 'bank_vact_num' => '', + + 'dt_req_out' => $dtReqOut, + 'dt_out' => $now->toDateTimeString(), + 'stat_3' => '4', + + 'dt_mod' => $now->toDateTimeString(), + ]); + + // 2) mem_account 초기화 + DB::table('mem_account') + ->where('mem_no', $memNo) + ->update([ + 'act_type' => '', + 'act_state' => '', + 'bank_code' => '', + 'bank_name' => '', + 'bank_act_name' => '', + 'bank_act_num' => '', + ]); + + // 3) mem_address 초기화 + DB::table('mem_address') + ->where('mem_no', $memNo) + ->update([ + 'gubun' => '', + 'shipping' => '', + 'zipNo' => '', + 'roadAddrPart1' => '', + 'jibunAddr' => '', + 'addrDetail' => '', + ]); + + // 4) mem_auth 인증 해제 + DB::table('mem_auth') + ->where('mem_no', $memNo) + ->update([ + 'auth_state' => 'N', + ]); + + // 5) mem_st_ring 비번/2차 비번 제거 + DB::table('mem_st_ring') + ->where('mem_no', $memNo) + ->update([ + 'str_0' => '', + 'str_1' => '', + 'str_2' => '', + 'dt_reg' => $now->toDateTimeString(), + + 'passwd2' => '', + 'passwd2_reg' => $now->toDateTimeString(), + ]); + }); + + return ['ok' => true, 'message' => '회원탈퇴가 완료되었습니다.']; + } } diff --git a/public/assets/css/mypage_renew.css b/public/assets/css/mypage_renew.css new file mode 100644 index 0000000..669e5fe --- /dev/null +++ b/public/assets/css/mypage_renew.css @@ -0,0 +1,163 @@ +/* ========================================================= + mypage_renew.css + - 마이페이지 내정보(renew) 상단 히어로(좌/우) + 카드 UI + - 범위: .mypage-info-page 내부만 적용 +========================================================= */ + +/* countdown row */ +.mypage-info-page .mypage-reauth__one{ + display:flex; + align-items:center; + justify-content:space-between; + gap:10px; +} + +.mypage-info-page #reauthCountdown{ + font-size:14px; + letter-spacing:0.5px; +} + +/* --------------------------- + 레이아웃: 타이틀 100% + 아래 좌/우 +--------------------------- */ + +/* 기존 테마가 flex를 써도 강제로 stack 구조로 */ +.mypage-info-page .mypage-hero .mypage-hero__inner.mypage-hero__inner--stack{ + display:block; +} + +/* 헤더(ACCOUNT SETTINGS / 내 정보 관리) */ +.mypage-info-page .mypage-hero .mypage-hero__head{ + width:100%; + margin-bottom:12px; +} + +/* 본문(좌/우 2컬럼) */ +.mypage-info-page .mypage-hero .mypage-hero__body{ + display:grid; + grid-template-columns: 1fr 360px; /* 오른쪽 카드 폭 */ + gap:24px; + align-items:start; +} + +/* 반응형: 모바일은 1컬럼 */ +@media (max-width: 980px){ + .mypage-info-page .mypage-hero .mypage-hero__body{ + grid-template-columns: 1fr; + } +} + +/* --------------------------- + 왼쪽 정보 카드(성명/이메일/휴대폰/출금계좌 등) +--------------------------- */ + +.mypage-info-page .mypage-hero .mypage-hero__me{ + margin-top:0px; + padding:14px; + border-radius:14px; + background:#f7f8fb; + border:1px solid #e5e7eb; + box-shadow: 0 10px 24px rgba(16,24,40,.08); +} + +.mypage-info-page .mypage-hero .mypage-hero__me-row{ + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + padding:10px 12px; + border-radius:12px; + background:#ffffff; + border:1px solid rgba(0,0,0,.04); +} + +.mypage-info-page .mypage-hero .mypage-hero__me-row + .mypage-hero__me-row{ + margin-top:8px; +} + +.mypage-info-page .mypage-hero .mypage-hero__me-row:hover{ + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(16,24,40,.06); +} + +.mypage-info-page .mypage-hero .mypage-hero__me-row .k{ + display:inline-flex; + align-items:center; + gap:8px; + font-size:12px; + font-weight:800; + color:#667085; + white-space:nowrap; +} + +.mypage-info-page .mypage-hero .mypage-hero__me-row .k::before{ + content:''; + width:8px; + height:8px; + border-radius:999px; + background:#2563eb; + box-shadow: 0 0 0 4px rgba(37,99,235,.12); +} + +.mypage-info-page .mypage-hero .mypage-hero__me-row .v{ + font-size:14px; + font-weight:900; + color:#101828; + letter-spacing:.2px; + text-align:right; + word-break:break-all; +} + +/* --------------------------- + 오른쪽 카드(설명 + 잔여시간 + 버튼) +--------------------------- */ + +.mypage-info-page .mypage-hero .mypage-hero__right{ + height:auto; + min-height:0; + align-self:flex-start; + + background:#ffffff; + border:1px solid #e5e7eb; + border-radius:16px; + padding:18px; + box-shadow: 0 10px 24px rgba(16,24,40,.08); + + display:flex; + flex-direction:column; + gap:14px; +} + +.mypage-info-page .mypage-hero .mypage-hero__desc{ + margin:0; + color:#475467; + font-size:13px; + line-height:1.55; +} + +.mypage-info-page .mypage-hero .mypage-reauth{ + background:#f7f8fb; + border:1px solid rgba(0,0,0,.06); + border-radius:14px; + padding:14px; +} + +.mypage-info-page .mypage-hero .mypage-hero__actions .btn{ + width:100%; + border-radius:14px; + padding:12px 14px; +} + +/* 모바일 미세 조정 */ +@media (max-width: 480px){ + .mypage-info-page .mypage-hero .mypage-hero__me{ + padding:12px; + border-radius:12px; + } + .mypage-info-page .mypage-hero .mypage-hero__me-row{ + padding:10px 10px; + } + .mypage-info-page .mypage-hero .mypage-hero__me-row .v{ + font-size:13px; + } +} diff --git a/public/assets/js/mypage_renew.js b/public/assets/js/mypage_renew.js index 5214731..fd6e773 100644 --- a/public/assets/js/mypage_renew.js +++ b/public/assets/js/mypage_renew.js @@ -934,6 +934,274 @@ })(); + (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 ensureStyle(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, sel) { + const box = $(sel, wrap); + return function setError(msg) { + if (!box) return; + const v = String(msg || '').trim(); + if (!v) { + box.style.display = 'none'; + box.textContent = ''; + return; + } + box.textContent = v; + box.style.display = 'block'; + }; + } + + function normalizeYn(v) { + v = String(v || '').toLowerCase().trim(); + if (v === 'y' || v === '1' || v === 'true' || v === 'yes' || v === 'on') return 'y'; + return 'n'; + } + + function badgeText(yn) { + return (yn === 'y') ? '동의' : '미동의'; + } + + function replaceWithClone(el) { + // ✅ 기존 mypage_renew.js에서 "준비중" 리스너가 이미 붙어있으므로 + // 버튼을 clone으로 교체해서 리스너를 모두 제거한다. + const clone = el.cloneNode(true); + el.parentNode.replaceChild(clone, el); + return clone; + } + + function openConsentModal(currentEmail, currentSms) { + ensureStyle('mypageConsentModalStyle', ` + .mypage-consentmodal{position:fixed;inset:0;z-index:220000;display:flex;align-items:center;justify-content:center} + .mypage-consentmodal__dim{position:absolute;inset:0;background:#000;opacity:.55} + .mypage-consentmodal__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-consentmodal__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-consentmodal__ttl{font-weight:900;font-size:14px;color:#111} + .mypage-consentmodal__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-consentmodal__bd{padding:14px} + .mypage-consentmodal__kicker{font-size:12px;font-weight:900;color:#2563eb;margin-bottom:6px} + .mypage-consentmodal__desc{font-size:12px;color:#667085;line-height:1.5;margin-bottom:12px;white-space:pre-line} + .mypage-consentmodal__grid{display:grid;grid-template-columns:1fr;gap:10px} + .mypage-consentmodal__item{border:1px solid #e5e7eb;border-radius:14px;padding:12px;background:#fff} + .mypage-consentmodal__row{display:flex;align-items:center;justify-content:space-between;gap:10px} + .mypage-consentmodal__label{font-weight:900;color:#111;font-size:13px} + .mypage-consentmodal__sub{font-size:12px;color:#667085;margin-top:6px;line-height:1.4} + .mypage-consentmodal__seg{display:inline-flex;border:1px solid rgba(0,0,0,.12);border-radius:999px;overflow:hidden;background:#fff} + .mypage-consentmodal__seg button{height:32px;min-width:64px;padding:0 12px;border:0;background:#fff;cursor:pointer;font-weight:900;font-size:12px;color:#111} + .mypage-consentmodal__seg button.is-on{background:#111;color:#fff} + .mypage-consentmodal__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-consentmodal__ft{display:flex;gap:10px;padding:12px 14px;border-top:1px solid rgba(0,0,0,.08);background:#fff} + .mypage-consentmodal__btn{flex:1;height:42px;border-radius:12px;border:1px solid rgba(0,0,0,.12);background:#fff;font-weight:900;cursor:pointer} + .mypage-consentmodal__btn--primary{border:none;background:#111;color:#fff} + .mypage-consentmodal__btn[disabled]{opacity:.6;cursor:not-allowed} + .mypage-consentmodal__badge{font-size:11px;font-weight:900;padding:4px 10px;border-radius:999px;background:rgba(37,99,235,.10);color:#2563eb} + `); + + const old = document.getElementById('mypageConsentModal'); + if (old) old.remove(); + + const wrap = document.createElement('div'); + wrap.className = 'mypage-consentmodal'; + wrap.id = 'mypageConsentModal'; + + let emailYn = normalizeYn(currentEmail); + let smsYn = normalizeYn(currentSms); + + wrap.innerHTML = ` +
+ + `; + + document.body.appendChild(wrap); + + const setError = makeErrorSetter(wrap, '#consent_error'); + + function close() { wrap.remove(); } + + // 닫기: X / 취소만 + wrap.querySelector('.mypage-consentmodal__close')?.addEventListener('click', close); + wrap.querySelector('[data-act="cancel"]')?.addEventListener('click', close); + + function syncSeg(kind, yn) { + const seg = wrap.querySelector(`.mypage-consentmodal__seg[data-kind="${kind}"]`); + if (!seg) return; + seg.querySelectorAll('button').forEach(btn => { + const v = btn.getAttribute('data-val'); + btn.classList.toggle('is-on', v === yn); + }); + } + + function bindSeg(kind) { + const seg = wrap.querySelector(`.mypage-consentmodal__seg[data-kind="${kind}"]`); + if (!seg) return; + seg.addEventListener('click', (e) => { + const btn = e.target?.closest?.('button[data-val]'); + if (!btn) return; + const yn = btn.getAttribute('data-val') === 'y' ? 'y' : 'n'; + if (kind === 'email') emailYn = yn; + if (kind === 'sms') smsYn = yn; + syncSeg(kind, yn); + }); + } + + syncSeg('email', emailYn); + syncSeg('sms', smsYn); + bindSeg('email'); + bindSeg('sms'); + + async function submit() { + setError(''); + + if (!URLS.marketingConsentUpdate) { + setError('저장 URL이 설정되지 않았습니다.'); + return; + } + + const ok = await showMsg( + `수신 동의 설정을 저장하시겠습니까?\n\n• 이메일: ${badgeText(emailYn)}\n• SMS: ${badgeText(smsYn)}`, + { type: 'confirm', title: '수신 동의 저장' } + ); + if (!ok) return; + + const btn = wrap.querySelector('[data-act="submit"]'); + if (btn) btn.disabled = true; + + try { + const res = await fetch(URLS.marketingConsentUpdate, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken(), + 'Accept': 'application/json', + }, + body: JSON.stringify({ + rcv_email: emailYn, + rcv_sms: smsYn, + }), + }); + + 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.rcv_email?.[0] || data.errors.rcv_sms?.[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(); + } + }); + } + + function boot() { + const btn0 = document.querySelector('[data-action="consent-edit"]'); + if (!btn0) return; + + // ✅ 기존 “준비중” 클릭 리스너 제거 + const btn = replaceWithClone(btn0); + + btn.addEventListener('click', () => { + const currentEmail = normalizeYn(CFG?.consent?.email ?? 'n'); + const currentSms = normalizeYn(CFG?.consent?.sms ?? 'n'); + openConsentModal(currentEmail, currentSms); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', boot); + } else { + boot(); + } + })(); + + // ------------------------------------------- @@ -941,10 +1209,6 @@ // ------------------------------------------- (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( `회원탈퇴를 진행하시겠습니까? diff --git a/resources/views/web/mypage/info/renew.blade.php b/resources/views/web/mypage/info/renew.blade.php index 033ca03..d0053aa 100644 --- a/resources/views/web/mypage/info/renew.blade.php +++ b/resources/views/web/mypage/info/renew.blade.php @@ -26,52 +26,75 @@ {{-- ✅ 상단 상태 카드 --}}
-
-
+
+ + +
ACCOUNT SETTINGS
내 정보 관리
-
-
- 성명 - {{ $memberName ?: '-' }} -
-
- 이메일 - {{ $memberEmail ?: '-' }} -
-
- 휴대폰 - {{ $memberPhone ?: '-' }} -
-
- 가입일 - {{ $memberDtReg ?: '-' }} -
-
-
-
- 연락처·비밀번호·보안 설정을 한 곳에서 관리합니다. - 변경 작업은 보안을 위해 제한된 시간 동안만 가능합니다. -
-
-
- 인증 허용 잔여시간 - - - {{ sprintf('%02d:%02d', intdiv((int)$remainSec, 60), (int)$remainSec % 60) }} - - + +
+
+
+
+ 성명 + {{ $memberName ?: '-' }} +
+
+ 이메일 + {{ $memberEmail ?: '-' }} +
+
+ 휴대폰 + {{ $memberPhone ?: '-' }} +
+ @if(!empty($outAccount)) +
+ 출금계좌 + + {{ $outAccount['bank_name'] ?? '' }} + {{ $outAccount['bank_act_num'] ?? '' }} + ({{ $outAccount['bank_act_name'] ?? '' }}) + +
+
+ 계좌인증일 + {{ $outAccount['act_date'] ?? '' }} +
+ @endif +
+ 가입일 + {{ $memberDtReg ?: '-' }} +
-
- - 인증 다시 하기 - +
+ +
+ 연락처·비밀번호·보안 설정을 한 곳에서 관리합니다. + 변경 작업은 보안을 위해 제한된 시간 동안만 가능합니다. +
+
+
+ 인증 허용 잔여시간 + + + {{ sprintf('%02d:%02d', intdiv((int)$remainSec, 60), (int)$remainSec % 60) }} + + +
+
+ +
@@ -114,19 +137,18 @@
- -