diff --git a/app/Http/Controllers/Admin/AdminAdminsController.php b/app/Http/Controllers/Admin/AdminAdminsController.php index d8ef9a4..bbff0eb 100644 --- a/app/Http/Controllers/Admin/AdminAdminsController.php +++ b/app/Http/Controllers/Admin/AdminAdminsController.php @@ -47,7 +47,7 @@ final class AdminAdminsController ]); } - // ✅ 수정 후에는 edit(GET)으로 보내기 + // 수정 후에는 edit(GET)으로 보내기 return redirect() ->route('admin.admins.edit', ['id' => $id]) ->with('toast', [ @@ -75,7 +75,7 @@ final class AdminAdminsController ]); } - // ✅ 수정 후에는 edit(GET)으로 보내기 + // 수정 후에는 edit(GET)으로 보내기 return redirect() ->route('admin.admins.edit', ['id' => $id]) ->with('toast', [ @@ -159,7 +159,7 @@ final class AdminAdminsController return back()->withErrors(['email' => (string)($res['message'] ?? '등록에 실패했습니다.')])->withInput(); } - // ✅ 임시 비밀번호는 “1회 안내”를 위해 메시지에 포함(원하면 문구만 바꿔도 됨) + // 임시 비밀번호는 “1회 안내”를 위해 메시지에 포함(원하면 문구만 바꿔도 됨) $temp = (string)($res['temp_password'] ?? ''); return redirect() diff --git a/app/Http/Controllers/Admin/Auth/AdminAuthController.php b/app/Http/Controllers/Admin/Auth/AdminAuthController.php index 50d5b5f..3d5bf8e 100644 --- a/app/Http/Controllers/Admin/Auth/AdminAuthController.php +++ b/app/Http/Controllers/Admin/Auth/AdminAuthController.php @@ -47,7 +47,7 @@ final class AdminAuthController extends Controller $state = (string) ($res['state'] ?? ''); - // ✅ 1) 계정 잠김 + // 1) 계정 잠김 if ($state === 'locked') { $msg = '계정이 잠금 상태입니다. 최고관리자에게 잠금 해제를 요청해 주세요.'; @@ -61,7 +61,7 @@ final class AdminAuthController extends Controller ]); } - // ✅ 2) 비번 불일치/계정없음 (남은 시도 횟수 포함) + // 2) 비번 불일치/계정없음 (남은 시도 횟수 포함) if ($state === 'invalid') { $left = $res['attempts_left'] ?? null; @@ -80,7 +80,7 @@ final class AdminAuthController extends Controller ]); } - // ✅ 3) 차단/비활성 계정 + // 3) 차단/비활성 계정 if ($state === 'blocked') { $msg = '로그인 할 수 없는 계정입니다.'; @@ -94,7 +94,7 @@ final class AdminAuthController extends Controller ]); } - // ✅ 4) SMS 발송 실패 + // 4) SMS 발송 실패 if ($state === 'sms_error') { $msg = '인증 SMS 발송에 실패했습니다. 잠시 후 다시 시도해 주세요.'; @@ -108,7 +108,7 @@ final class AdminAuthController extends Controller ]); } - // ✅ 5) 비번 재설정 강제 + // 5) 비번 재설정 강제 if ($state === 'must_reset') { $request->session()->put('admin_pwreset', [ 'admin_id' => (int) ($res['admin_id'] ?? 0), @@ -156,7 +156,7 @@ final class AdminAuthController extends Controller ]); } - // ✅ 6) OTP 발송 성공 + // 6) OTP 발송 성공 if ($state === 'otp_sent') { $request->session()->put('admin_2fa', [ 'challenge_id' => (string) ($res['challenge_id'] ?? ''), @@ -176,7 +176,7 @@ final class AdminAuthController extends Controller ]); } - // ✅ 방어: 예상치 못한 상태 + // 방어: 예상치 못한 상태 return back() ->withInput() ->withErrors(['login_id' => '로그인 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.']) diff --git a/app/Http/Controllers/Admin/Log/AdminAuditLogController.php b/app/Http/Controllers/Admin/Log/AdminAuditLogController.php index 115b33b..c699655 100644 --- a/app/Http/Controllers/Admin/Log/AdminAuditLogController.php +++ b/app/Http/Controllers/Admin/Log/AdminAuditLogController.php @@ -16,7 +16,7 @@ final class AdminAuditLogController extends Controller { $data = $this->service->indexData($request->query()); - // ✅ view 파일명: 컨트롤러와 이름 맞춤 (index.blade.php 사용 안함) + // view 파일명: 컨트롤러와 이름 맞춤 (index.blade.php 사용 안함) return view('admin.log.AdminAuditLogController', $data); } diff --git a/app/Http/Controllers/Admin/Log/MemberJoinLogController.php b/app/Http/Controllers/Admin/Log/MemberJoinLogController.php index bb0f8ae..5d0cd58 100644 --- a/app/Http/Controllers/Admin/Log/MemberJoinLogController.php +++ b/app/Http/Controllers/Admin/Log/MemberJoinLogController.php @@ -16,7 +16,7 @@ final class MemberJoinLogController extends Controller { $data = $this->service->indexData($request->query()); - // ✅ index.blade.php 금지 → 컨트롤러명과 동일한 뷰 파일 + // index.blade.php 금지 → 컨트롤러명과 동일한 뷰 파일 return view('admin.log.MemberJoinLogController', $data); } } diff --git a/app/Http/Controllers/Admin/Mail/AdminMailController.php b/app/Http/Controllers/Admin/Mail/AdminMailController.php index ead2145..392f594 100644 --- a/app/Http/Controllers/Admin/Mail/AdminMailController.php +++ b/app/Http/Controllers/Admin/Mail/AdminMailController.php @@ -27,13 +27,13 @@ final class AdminMailController extends Controller public function store(Request $request) { - // ✅ 템플릿 선택값(옵션): Blade에서 로 보내면 여기서 받음 $hasTemplate = $request->filled('template_id'); $rules = [ 'send_mode' => ['required','in:one,many,csv,db'], - // ✅ schedule_type 추가 + 예약이면 scheduled_at 필수 + // schedule_type 추가 + 예약이면 scheduled_at 필수 'schedule_type' => ['required','in:now,schedule'], 'scheduled_at' => ['nullable','date_format:Y-m-d H:i','required_if:schedule_type,schedule'], @@ -41,7 +41,7 @@ final class AdminMailController extends Controller 'from_name' => ['nullable','string','max:120'], 'reply_to' => ['nullable','email','max:190'], - // ✅ 템플릿 선택이면(스킨 select disabled 등) skin_key가 누락될 수 있으니 nullable 허용 + 서버에서 기본값 세팅 + // 템플릿 선택이면(스킨 select disabled 등) skin_key가 누락될 수 있으니 nullable 허용 + 서버에서 기본값 세팅 'skin_key' => $hasTemplate ? ['nullable','in:hero,newsletter,minimal,clean,dark'] : ['required','in:hero,newsletter,minimal,clean,dark'], @@ -49,7 +49,7 @@ final class AdminMailController extends Controller // 템플릿 선택용(옵션) 'template_id' => ['nullable','integer','min:1'], - // ✅ subject/body는 무조건 들어가야 함(템플릿이면 적용 버튼으로 채워지게) + // subject/body는 무조건 들어가야 함(템플릿이면 적용 버튼으로 채워지게) 'subject' => ['required','string','max:190'], 'body' => ['required','string','max:20000'], @@ -74,7 +74,7 @@ final class AdminMailController extends Controller $v = Validator::make($request->all(), $rules); - // ✅ 모드별 추가 검증(필수값 체크) + // 모드별 추가 검증(필수값 체크) $v->after(function ($validator) use ($request) { $mode = (string)$request->input('send_mode'); @@ -101,14 +101,14 @@ final class AdminMailController extends Controller $data = $v->validate(); - // ✅ skin_key가 폼에서 누락되면(템플릿 선택 시 disabled 등) 기본값 채움 + // skin_key가 폼에서 누락되면(템플릿 선택 시 disabled 등) 기본값 채움 // (템플릿이면 서비스가 템플릿의 skin_key로 덮어쓰도록 하는 게 정석) $data['skin_key'] = (string)($data['skin_key'] ?? ''); if ($data['skin_key'] === '') { $data['skin_key'] = 'clean'; } - // ✅ 컨트롤러에서 “정상 규칙”으로 파싱 결과를 만들어 request에 심어둠 + // 컨트롤러에서 “정상 규칙”으로 파싱 결과를 만들어 request에 심어둠 // → 서비스에서 이 parsed_rows를 우선 사용하면 토큰 밀림(덮어쓰기) 문제를 확실히 잡을 수 있음. if ($data['send_mode'] === 'many') { $rows = $this->parseManyText((string)($data['to_emails_text'] ?? '')); @@ -166,7 +166,7 @@ final class AdminMailController extends Controller 'heroUrl' => ['nullable','string','max:500'], ]); - // ✅ 미리보기 샘플도 실제 규칙과 동일하게 + // 미리보기 샘플도 실제 규칙과 동일하게 // {_text_02_}=이름, {_text_03_}=금액, {_text_04_}=상품유형 $sample = [ '{_text_02_}' => '홍길동', @@ -204,7 +204,7 @@ final class AdminMailController extends Controller $tokens = array_values($tokens); foreach ($tokens as $i => $v) { - $key = sprintf('{_text_%02d_}', $i + 2); // ✅ 무조건 02부터 + $key = sprintf('{_text_%02d_}', $i + 2); // 무조건 02부터 $vars[$key] = trim((string)$v); } @@ -218,7 +218,7 @@ final class AdminMailController extends Controller private function bodyToHtml(string $text): string { - // ✅ 줄바꿈 유지 + XSS 방지 + // 줄바꿈 유지 + XSS 방지 return nl2br(e($text)); } @@ -235,7 +235,7 @@ final class AdminMailController extends Controller $line = trim((string)$line); if ($line === '') continue; - // ✅ 빈열 유지(열 밀림 방지) + // 빈열 유지(열 밀림 방지) $cols = array_map('trim', explode(',', $line)); $email = strtolower(trim($cols[0] ?? '')); diff --git a/app/Http/Controllers/Admin/MeController.php b/app/Http/Controllers/Admin/MeController.php index 5a48d39..6215a9e 100644 --- a/app/Http/Controllers/Admin/MeController.php +++ b/app/Http/Controllers/Admin/MeController.php @@ -47,7 +47,7 @@ final class MeController ]); } - // ✅ 핵심: 성공도 view() 말고 redirect + // 핵심: 성공도 view() 말고 redirect return redirect() ->route('admin.me') ->with('toast', [ @@ -79,7 +79,7 @@ final class MeController ]); } - // ✅ 핵심: 성공도 view() 말고 redirect + // 핵심: 성공도 view() 말고 redirect return redirect() ->route('admin.me') ->with('toast', [ diff --git a/app/Http/Controllers/Admin/Members/AdminMemberJoinFilterController.php b/app/Http/Controllers/Admin/Members/AdminMemberJoinFilterController.php index ee7c9b6..6c02930 100644 --- a/app/Http/Controllers/Admin/Members/AdminMemberJoinFilterController.php +++ b/app/Http/Controllers/Admin/Members/AdminMemberJoinFilterController.php @@ -14,7 +14,7 @@ final class AdminMemberJoinFilterController extends Controller public function index(Request $request) { - // ✅ query 전달(전역 request 의존 줄이기) + // query 전달(전역 request 의존 줄이기) $data = $this->service->indexData($request->query()); return view('admin.members.join_filters.index', $data); @@ -129,7 +129,7 @@ final class AdminMemberJoinFilterController extends Controller 'admin_phone' => ['nullable', 'array'], 'admin_phone.*' => ['string', 'max:30'], - // ✅ 모달에서 유지용(검증대상 아님) - withInput을 위해 받아둠 + // 모달에서 유지용(검증대상 아님) - withInput을 위해 받아둠 'filter_seq' => ['nullable', 'string', 'max:20'], ]); diff --git a/app/Http/Controllers/Admin/Members/AdminMemberMarketingController.php b/app/Http/Controllers/Admin/Members/AdminMemberMarketingController.php index 9cfb520..d3c7108 100644 --- a/app/Http/Controllers/Admin/Members/AdminMemberMarketingController.php +++ b/app/Http/Controllers/Admin/Members/AdminMemberMarketingController.php @@ -75,7 +75,7 @@ final class AdminMemberMarketingController $res = $this->service->exportZip($data, $zipPassword); if (!($res['ok'] ?? false)) { - // ✅ 실패도 기록(원인 추적용) — 비밀번호는 기록하지 않음 + // 실패도 기록(원인 추적용) — 비밀번호는 기록하지 않음 $this->audit->log( actorAdminId: $actorAdminId, action: 'admin.member.export.fail', @@ -100,7 +100,7 @@ final class AdminMemberMarketingController $zipPath = (string)($res['zip_path'] ?? ''); $downloadName = (string)($res['download_name'] ?? 'members.zip'); - // ✅ 성공 기록 + // 성공 기록 $bytes = (is_string($zipPath) && $zipPath !== '' && file_exists($zipPath)) ? @filesize($zipPath) : null; $this->audit->log( diff --git a/app/Http/Controllers/Admin/Notice/AdminNoticeController.php b/app/Http/Controllers/Admin/Notice/AdminNoticeController.php index 3c04afd..481a9bb 100644 --- a/app/Http/Controllers/Admin/Notice/AdminNoticeController.php +++ b/app/Http/Controllers/Admin/Notice/AdminNoticeController.php @@ -24,7 +24,7 @@ final class AdminNoticeController extends Controller $templates = $this->service->paginate($filters, 15); return view('admin.notice.index', [ - 'templates' => $templates, // ✅ blade 호환 + 'templates' => $templates, // blade 호환 'filters' => $filters, ]); } @@ -95,7 +95,7 @@ final class AdminNoticeController extends Controller ); return redirect() - ->route('admin.notice.edit', ['id' => $id] + $request->query()) // ✅ 쿼리 유지 + ->route('admin.notice.edit', ['id' => $id] + $request->query()) // 쿼리 유지 ->with('ok', '공지사항이 저장되었습니다.'); } @@ -104,7 +104,7 @@ final class AdminNoticeController extends Controller $this->service->delete($id); return redirect() - ->route('admin.notice.index', $request->query()) // ✅ 쿼리 유지 + ->route('admin.notice.index', $request->query()) // 쿼리 유지 ->with('ok', '공지사항이 삭제되었습니다.'); } diff --git a/app/Http/Controllers/Admin/Product/AdminMediaController.php b/app/Http/Controllers/Admin/Product/AdminMediaController.php index cc34bb6..bc00e6c 100644 --- a/app/Http/Controllers/Admin/Product/AdminMediaController.php +++ b/app/Http/Controllers/Admin/Product/AdminMediaController.php @@ -39,7 +39,7 @@ final class AdminMediaController $request->only('folder_name'), $file, $customName, - (int) $index, // ✅ [핵심 수정] (int)를 붙여서 정수로 강제 변환 + (int) $index, // [핵심 수정] (int)를 붙여서 정수로 강제 변환 $actorId, $ip, $ua diff --git a/app/Http/Controllers/Admin/Product/AdminPinController.php b/app/Http/Controllers/Admin/Product/AdminPinController.php index ea32fae..a9a73bf 100644 --- a/app/Http/Controllers/Admin/Product/AdminPinController.php +++ b/app/Http/Controllers/Admin/Product/AdminPinController.php @@ -23,7 +23,7 @@ final class AdminPinController return redirect()->back()->with('toast', ['type' => 'danger', 'message' => '잘못된 접근입니다.']); } - // ✅ 수정된 Service 반환값(배열) 받기 + // 수정된 Service 반환값(배열) 받기 $pinData = $this->service->getPinsBySku($skuId, $request->all()); $pins = $pinData['pins']; $stats = $pinData['stats']; // 통계 데이터 diff --git a/app/Http/Controllers/Web/Auth/FindIdController.php b/app/Http/Controllers/Web/Auth/FindIdController.php index 46274b7..1e4e83a 100644 --- a/app/Http/Controllers/Web/Auth/FindIdController.php +++ b/app/Http/Controllers/Web/Auth/FindIdController.php @@ -260,7 +260,7 @@ class FindIdController extends Controller return $head . str_repeat('*', max(1, $localLen - 3)) . $tail . '@' . $domain; } - // ✅ 기본 규칙: 앞 3글자 + ***** + 뒤 2글자 + // 기본 규칙: 앞 3글자 + ***** + 뒤 2글자 $head = mb_substr($local, 0, 3, 'UTF-8'); $tail = mb_substr($local, -2, 2, 'UTF-8'); diff --git a/app/Http/Controllers/Web/Auth/FindPasswordController.php b/app/Http/Controllers/Web/Auth/FindPasswordController.php index f8e0f39..34a8847 100644 --- a/app/Http/Controllers/Web/Auth/FindPasswordController.php +++ b/app/Http/Controllers/Web/Auth/FindPasswordController.php @@ -92,7 +92,7 @@ class FindPasswordController extends Controller ]); } - // ✅ 인증완료 세션 세팅 + // 인증완료 세션 세팅 if (!empty($res['session'])) { $request->session()->put('find_pw', $res['session']); } diff --git a/app/Http/Controllers/Web/Auth/RegisterController.php b/app/Http/Controllers/Web/Auth/RegisterController.php index 00d5fb9..67dffdd 100644 --- a/app/Http/Controllers/Web/Auth/RegisterController.php +++ b/app/Http/Controllers/Web/Auth/RegisterController.php @@ -357,7 +357,7 @@ class RegisterController extends Controller $email = (string) $request->input('login_id'); - // ✅ repo에서 mem_info.email 중복 체크 + // repo에서 mem_info.email 중복 체크 $exists = $repo->existsEmail($email); return response()->json([ diff --git a/app/Http/Controllers/Web/Mypage/InfoGateController.php b/app/Http/Controllers/Web/Mypage/InfoGateController.php index c10015b..59f0a03 100644 --- a/app/Http/Controllers/Web/Mypage/InfoGateController.php +++ b/app/Http/Controllers/Web/Mypage/InfoGateController.php @@ -729,7 +729,7 @@ final class InfoGateController extends Controller $pw = (string) $request->input('password'); $pin2 = (string) $request->input('pin2'); - // ✅ 1차 비밀번호 검증(Repo) + // 1차 비밀번호 검증(Repo) if (!$repo->verifyLegacyPassword($memNo, $pw)) { return response()->json([ 'ok' => false, @@ -738,7 +738,7 @@ final class InfoGateController extends Controller ], 422); } - // ✅ 2차 비밀번호 검증(Repo) + // 2차 비밀번호 검증(Repo) if (!$repo->verifyPin2($memNo, $pin2)) { return response()->json([ 'ok' => false, @@ -747,7 +747,7 @@ final class InfoGateController extends Controller ], 422); } - // ✅ 탈퇴 가능 조건 검증 + 처리(Service) + // 탈퇴 가능 조건 검증 + 처리(Service) try { $res = $memInfoService->withdrawMember($memNo); diff --git a/app/Http/Controllers/Web/Mypage/UsageController.php b/app/Http/Controllers/Web/Mypage/UsageController.php index 2ed7997..f17e9a5 100644 --- a/app/Http/Controllers/Web/Mypage/UsageController.php +++ b/app/Http/Controllers/Web/Mypage/UsageController.php @@ -13,13 +13,14 @@ final class UsageController extends Controller ) {} /** - * GET /mypage/usage?attempt_id=... + * GET /mypage/usage + * - 리스트 + 검색 + 페이징 + * - 호환: /mypage/usage?attempt_id=123 -> show로 redirect */ public function index(Request $request) { - // legacy.auth가 있지만, 결제 플로우 안전장치로 한 번 더 if ((bool)session('_sess._login_') !== true) { - return redirect()->route('web.auth.login'); // 프로젝트 로그인 라우트에 맞춰 조정 + return redirect()->route('web.auth.login'); } $memNo = (int)session('_sess._mno', 0); @@ -28,8 +29,95 @@ final class UsageController extends Controller $attemptId = $request->query('attempt_id'); $attemptId = is_numeric($attemptId) ? (int)$attemptId : null; - $data = $this->service->buildPageData($attemptId, $memNo); + if ($attemptId !== null && $attemptId >= 1) { + return redirect()->route('web.mypage.usage.show', ['attemptId' => $attemptId]); + } + + $filters = [ + 'q' => trim((string)$request->query('q', '')), + 'method' => trim((string)$request->query('method', '')), + 'status' => trim((string)$request->query('status', '')), + 'from' => trim((string)$request->query('from', '')), + 'to' => trim((string)$request->query('to', '')), + ]; + + $data = $this->service->buildListPageData($memNo, $filters); return view('web.mypage.usage.index', $data); } + + /** + * GET /mypage/usage/{attemptId} + */ + public function show(Request $request, int $attemptId) + { + if ((bool)session('_sess._login_') !== true) { + return redirect()->route('web.auth.login'); + } + + $memNo = (int)session('_sess._mno', 0); + if ($memNo <= 0) abort(403); + + $data = $this->service->buildDetailPageData($attemptId, $memNo); + + return view('web.mypage.usage.show', $data); + } + + /** + * POST /mypage/usage/{attemptId}/open + * - 핀 "오픈(확인)" 처리: ret_data에 pin_opened_at 기록 + */ + public function openPins(Request $request, int $attemptId) + { + if ((bool)session('_sess._login_') !== true) { + return redirect()->route('web.auth.login'); + } + + $memNo = (int)session('_sess._mno', 0); + if ($memNo <= 0) abort(403); + + $out = $this->service->openPins($attemptId, $memNo); + + if (!($out['ok'] ?? false)) { + return redirect()->back()->with('error', (string)($out['message'] ?? '처리 실패')); + } + + return redirect()->route('web.mypage.usage.show', ['attemptId' => $attemptId]) + ->with('success', '핀 확인이 완료되었습니다.'); + } + + /** + * POST /mypage/usage/{attemptId}/cancel + * - 결제완료 후 취소(핀 오픈 전만) + */ + public function cancel(Request $request, int $attemptId) + { + if ((bool)session('_sess._login_') !== true) { + return redirect()->route('web.auth.login'); + } + + $memNo = (int)session('_sess._mno', 0); + if ($memNo <= 0) abort(403); + + $data = $request->validate([ + 'reason' => ['nullable','string','max:255'], + ]); + + $out = $this->service->cancelPaidAttempt($attemptId, $memNo, (string)($data['reason'] ?? '사용자 요청')); + + if (!($out['ok'] ?? false)) { + return redirect()->back()->with('error', (string)($out['message'] ?? '취소 실패')); + } + + return redirect()->route('web.mypage.usage.show', array_filter([ + 'attemptId' => $attemptId, + 'q' => $request->input('q'), + 'method' => $request->input('method'), + 'status' => $request->input('status'), + 'from' => $request->input('from'), + 'to' => $request->input('to'), + 'page' => $request->input('page'), + ], fn ($v) => $v !== null && $v !== '')) + ->with('success', '결제가 취소되었습니다.'); + } } diff --git a/app/Http/Controllers/Web/Payment/DanalController.php b/app/Http/Controllers/Web/Payment/DanalController.php index 4ac3ea4..fc6c9df 100644 --- a/app/Http/Controllers/Web/Payment/DanalController.php +++ b/app/Http/Controllers/Web/Payment/DanalController.php @@ -56,13 +56,11 @@ final class DanalController extends Controller if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'paid') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); - $redirect = url("/mypage/usage?attempt_id={$attemptId}"); - return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제완료', 'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.', - 'redirect' => url($redirect), + 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } @@ -83,13 +81,11 @@ final class DanalController extends Controller if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'issued') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); - $redirect = url("/mypage/usage?attempt_id={$attemptId}"); - return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '가상계좌 발급', 'message' => '가상계좌가 발급되었습니다. 입금 후 결제가 완료됩니다. 구매페이지로 이동합니다.', - 'redirect' => url($redirect), + 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } @@ -120,13 +116,11 @@ final class DanalController extends Controller if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'paid') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); - $redirect = url("/mypage/usage?attempt_id={$attemptId}"); - return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제완료', 'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.', - 'redirect' => url($redirect), + 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } @@ -177,13 +171,11 @@ final class DanalController extends Controller if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'paid') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); - $redirect = url("/mypage/usage?attempt_id={$attemptId}"); - return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제완료', 'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.', - 'redirect' => url($redirect), + 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } @@ -214,7 +206,7 @@ final class DanalController extends Controller $out = $this->service->handleCancel($token); - // ✅ 취소면: iframe 닫고 showMsg 실행 + // 취소면: iframe 닫고 showMsg 실행 if (($out['meta']['code'] ?? '') === 'CANCEL') { return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', diff --git a/app/Http/Middleware/AdminIpAllowlist.php b/app/Http/Middleware/AdminIpAllowlist.php index 0d627f1..ec287ae 100644 --- a/app/Http/Middleware/AdminIpAllowlist.php +++ b/app/Http/Middleware/AdminIpAllowlist.php @@ -12,7 +12,7 @@ final class AdminIpAllowlist { $allowed = config('admin.allowed_ips', []); - // ✅ 개발(local/testing)에서는 allowlist 비어있으면 전체 허용 + // 개발(local/testing)에서는 allowlist 비어있으면 전체 허용 if (!$allowed && !app()->environment('production')) { return $next($request); } diff --git a/app/Models/AdminUser.php b/app/Models/AdminUser.php index 36ac82e..b406534 100644 --- a/app/Models/AdminUser.php +++ b/app/Models/AdminUser.php @@ -24,7 +24,7 @@ class AdminUser extends Authenticatable 'password_changed_at' => 'datetime', 'totp_enabled' => 'boolean', 'totp_confirmed_at' => 'datetime', - 'password' => 'hashed', // ✅ 이걸로 통일 + 'password' => 'hashed', // 이걸로 통일 'must_reset_password' => 'boolean', 'totp_enabled' => 'boolean', 'totp_verified_at' => 'datetime', diff --git a/app/Models/Payments/GcPinOrder.php b/app/Models/Payments/GcPinOrder.php index 35f94b2..cb33349 100644 --- a/app/Models/Payments/GcPinOrder.php +++ b/app/Models/Payments/GcPinOrder.php @@ -10,7 +10,7 @@ final class GcPinOrder extends Model protected $table = 'gc_pin_order'; protected $fillable = [ - 'oid','mem_no','order_type','stat_pay','stat_tax', + 'oid','mem_no','products_id','products_name','order_type','stat_pay','stat_tax', 'subtotal_amount','fee_amount','pg_fee_amount','discount_amount','pay_money', 'provider','pay_method','pg_tid','ret_code','ret_msg', 'pay_data','ret_data','ordered_at','paid_at','cancelled_at', diff --git a/app/Providers/Danal/Gateways/CardGateway.php b/app/Providers/Danal/Gateways/CardGateway.php index 0a7bfe0..f5b8ba3 100644 --- a/app/Providers/Danal/Gateways/CardGateway.php +++ b/app/Providers/Danal/Gateways/CardGateway.php @@ -97,4 +97,43 @@ final class CardGateway $s = str_replace(["&","'","\"","\\","<",">","|","\r","\n","," , "+"], " ", $s); return trim(preg_replace('/\s+/', ' ', $s)) ?: '상품권'; } + + public function cancel( + GcPinOrder $order, + string $cardKind, + string $tid, + ?int $amount = null, + string $cancelType = 'C', + string $requester = '', + string $desc = '' + ): array { + $c = $this->cfg->card($cardKind); + + $amt = $amount ?? (int)$order->pay_money; + + $req = [ + 'TID' => $tid, + 'AMOUNT' => (string)$amt, + 'CANCELTYPE' => $cancelType, // C:전체, P:부분 + 'TXTYPE' => 'CANCEL', + 'SERVICETYPE' => 'DANALCARD', + ]; + + // optional + $requester = trim($requester); + $desc = $this->safeCancelDesc($desc); + + if ($requester !== '') $req['CANCELREQUESTER'] = $requester; + if ($desc !== '') $req['CANCELDESC'] = $desc; + + $res = $this->client->call($c['url'], $c['cpid'], $req, $c['key'], $c['iv']); + return ['req' => $req, 'res' => $res]; + } + + private function safeCancelDesc(string $s): string + { + // 다날 DATA 암복호화 구간에서 문제 일으키는 문자 최소 제거 + $s = str_replace(["&","'","\"","\\","<",">","|","\r","\n","," , "+"], " ", $s); + return trim(preg_replace('/\s+/', ' ', $s)); + } } diff --git a/app/Providers/Danal/Gateways/PhoneGateway.php b/app/Providers/Danal/Gateways/PhoneGateway.php index b059875..fdc985e 100644 --- a/app/Providers/Danal/Gateways/PhoneGateway.php +++ b/app/Providers/Danal/Gateways/PhoneGateway.php @@ -258,4 +258,22 @@ final class PhoneGateway $s = str_replace(["&","\"","\\","<",">","," , "+"], " ", $s); return trim(preg_replace('/\s+/', ' ', $s)) ?: '상품권'; } + + public function billCancel(string $mode, string $tid): array + { + $c = $this->cfg->phone($mode); + $binPath = rtrim($c['bin_path'], '/'); + + $req = [ + 'Command' => 'BILL_CANCEL', + 'OUTPUTOPTION' => '3', + 'ID' => $c['cpid'], + 'PWD' => $c['pwd'], + 'TID' => $tid, + ]; + + $res = $this->runSClient($binPath, $req); + + return ['req' => $req, 'res' => $res]; + } } diff --git a/app/Providers/Danal/Gateways/WireGateway.php b/app/Providers/Danal/Gateways/WireGateway.php index 215e712..2d57f83 100644 --- a/app/Providers/Danal/Gateways/WireGateway.php +++ b/app/Providers/Danal/Gateways/WireGateway.php @@ -114,4 +114,41 @@ final class WireGateway $s = str_replace(["&","'","\"","\\","<",">","|","\r","\n","," , "+"], " ", $s); return trim(preg_replace('/\s+/', ' ', $s)) ?: '상품권'; } + + public function cancel( + GcPinOrder $order, + string $tid, + ?int $amount = null, + string $cancelType = 'C', + string $requester = '', + string $desc = '' + ): array { + $c = $this->cfg->wiretransfer(); + + $amt = $amount ?? (int)$order->pay_money; + + $req = [ + 'TXTYPE' => 'CANCEL', + 'SERVICETYPE' => 'WIRETRANSFER', + 'TID' => $tid, + 'AMOUNT' => (string)$amt, + 'CANCELTYPE' => $cancelType, // C:전체, P:부분 + ]; + + // optional + $requester = trim($requester); + $desc = $this->safeCancelDesc($desc); + + if ($requester !== '') $req['CANCELREQUESTER'] = $requester; + if ($desc !== '') $req['CANCELDESC'] = $desc; + + $res = $this->client->call((string)$c['tx_url'], (string)$c['cpid'], $req, (string)$c['key'], (string)$c['iv']); + return ['req' => $req, 'res' => $res]; + } + + private function safeCancelDesc(string $s): string + { + $s = str_replace(["&","'","\"","\\","<",">","|","\r","\n","," , "+"], " ", $s); + return trim(preg_replace('/\s+/', ' ', $s)); + } } diff --git a/app/Repositories/Admin/AdminUserRepository.php b/app/Repositories/Admin/AdminUserRepository.php index 6ec8839..b8a04b4 100644 --- a/app/Repositories/Admin/AdminUserRepository.php +++ b/app/Repositories/Admin/AdminUserRepository.php @@ -69,7 +69,7 @@ final class AdminUserRepository // ========================= public function setPassword(AdminUser $admin, string $plainPassword): void { - // ✅ AdminUser 모델에 password cast(hashed)가 있으면 plain을 넣어도 해싱됨 + // AdminUser 모델에 password cast(hashed)가 있으면 plain을 넣어도 해싱됨 $data = [ 'password' => $plainPassword, 'must_reset_password' => 0, @@ -217,7 +217,7 @@ final class AdminUserRepository ->all(); } - /** ✅ (중복 선언 금지) 상세/내정보/관리페이지 공용 */ + /** (중복 선언 금지) 상세/내정보/관리페이지 공용 */ public function getRolesForUser(int $adminUserId): array { if (!Schema::hasTable('admin_roles') || !Schema::hasTable('admin_role_user')) return []; @@ -411,7 +411,7 @@ final class AdminUserRepository 'updated_at' => now(), ]; - // ✅ 3회 이상이면 "영구잠금" + // 3회 이상이면 "영구잠금" if (!$locked && $next >= $limit) { $update['locked_until'] = now(); $locked = true; diff --git a/app/Repositories/Admin/Log/AdminAuditLogRepository.php b/app/Repositories/Admin/Log/AdminAuditLogRepository.php index b10916e..1793f11 100644 --- a/app/Repositories/Admin/Log/AdminAuditLogRepository.php +++ b/app/Repositories/Admin/Log/AdminAuditLogRepository.php @@ -61,7 +61,7 @@ final class AdminAuditLogRepository $q->where('l.ip', 'like', $this->escapeLike($ip) . '%'); } - // ✅ 최신순 + // 최신순 $q->orderByDesc('l.created_at')->orderByDesc('l.id'); return $q->paginate($perPage)->withQueryString(); diff --git a/app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php b/app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php index 0601524..188a21d 100644 --- a/app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php +++ b/app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php @@ -13,7 +13,7 @@ final class MemberDanalAuthTelLogRepository { $q = DB::table(self::TABLE)->select([ 'seq','gubun','TID','res_code','mem_no','info','rgdate', - // ✅ MariaDB: CAST(... AS JSON) 금지 → info에 바로 JSON_EXTRACT + // MariaDB: CAST(... AS JSON) 금지 → info에 바로 JSON_EXTRACT DB::raw("JSON_UNQUOTE(JSON_EXTRACT(`info`, '$.mobile_number')) AS mobile_number"), DB::raw("JSON_UNQUOTE(JSON_EXTRACT(`info`, '$._mno')) AS info_mno"), ]); diff --git a/app/Repositories/Admin/Member/AdminMemberJoinFilterRepository.php b/app/Repositories/Admin/Member/AdminMemberJoinFilterRepository.php index f96baa7..b215600 100644 --- a/app/Repositories/Admin/Member/AdminMemberJoinFilterRepository.php +++ b/app/Repositories/Admin/Member/AdminMemberJoinFilterRepository.php @@ -44,7 +44,7 @@ final class AdminMemberJoinFilterRepository return (int) DB::table(self::TABLE)->insertGetId($data); } - // ✅ bool 말고 affected rows + // bool 말고 affected rows public function update(int $seq, array $data): int { return (int) DB::table(self::TABLE)->where('seq', $seq)->update($data); diff --git a/app/Repositories/Admin/Member/AdminMemberMarketingRepository.php b/app/Repositories/Admin/Member/AdminMemberMarketingRepository.php index 59ced20..5267300 100644 --- a/app/Repositories/Admin/Member/AdminMemberMarketingRepository.php +++ b/app/Repositories/Admin/Member/AdminMemberMarketingRepository.php @@ -86,7 +86,7 @@ final class AdminMemberMarketingRepository $q = DB::table('mem_marketing_stats as ms') ->join('mem_info as mi', 'mi.mem_no', '=', 'ms.mem_no'); - // ✅ 스냅샷(기준일) 고정: 기본은 최신 + // 스냅샷(기준일) 고정: 기본은 최신 $asOf = trim((string)($filters['as_of_date'] ?? '')); if ($asOf === '') $asOf = $this->getAsOfDate() ?: ''; if ($asOf !== '') $q->where('ms.as_of_date', $asOf); diff --git a/app/Repositories/Admin/Notice/AdminNoticeRepository.php b/app/Repositories/Admin/Notice/AdminNoticeRepository.php index edbf355..3d713d9 100644 --- a/app/Repositories/Admin/Notice/AdminNoticeRepository.php +++ b/app/Repositories/Admin/Notice/AdminNoticeRepository.php @@ -24,7 +24,7 @@ final class AdminNoticeRepository $query->where($field, 'like', '%'.$q.'%'); } - // ✅ 상단공지 먼저 + 최신순 + // 상단공지 먼저 + 최신순 $query->orderByDesc('first_sign') ->orderByDesc('regdate') ->orderByDesc('seq'); diff --git a/app/Repositories/Member/MemberAuthRepository.php b/app/Repositories/Member/MemberAuthRepository.php index 4f822b4..7f87a4c 100644 --- a/app/Repositories/Member/MemberAuthRepository.php +++ b/app/Repositories/Member/MemberAuthRepository.php @@ -119,7 +119,7 @@ class MemberAuthRepository return null; } - return $digits; // ✅ 무조건 11자리 숫자만 리턴 + return $digits; // 무조건 11자리 숫자만 리턴 } /** @@ -212,7 +212,7 @@ class MemberAuthRepository 'message' => "이미 가입된 전화번호 입니다.\n\n아이디 찾기로 이동할까요?", 'redirect' => route('web.auth.find_id'), 'matched_mem_no' => (int) $member->mem_no, - 'matched_cell_corp' => $member->cell_corp ?? null, // ✅ 필요시 + 'matched_cell_corp' => $member->cell_corp ?? null, // 필요시 ]); } @@ -258,7 +258,7 @@ class MemberAuthRepository if ($email === '') return false; return DB::table('mem_info') - ->where('email', $email) // ✅ mem_info.email + ->where('email', $email) // mem_info.email ->exists(); } @@ -283,19 +283,19 @@ class MemberAuthRepository $row = DB::table('mem_join_filter') ->whereIn('join_block', ['A', 'S']) ->where(function ($q) use ($ip4, $ip4c) { - // ✅ C class (보통 gubun_code='01', filter='162.168.0.0') + // C class (보통 gubun_code='01', filter='162.168.0.0') $q->where(function ($q2) use ($ip4c) { $q2->where('gubun_code', '01') ->where('filter', $ip4c); }); - // ✅ D class (보통 gubun_code='02', filter='162.168.0.2') + // D class (보통 gubun_code='02', filter='162.168.0.2') $q->orWhere(function ($q2) use ($ip4) { $q2->where('gubun_code', '02') ->where('filter', $ip4); }); - // ✅ (레거시) gubun_code가 01인데도 filter에 단일 IP가 들어있는 케이스 방어 + // (레거시) gubun_code가 01인데도 filter에 단일 IP가 들어있는 케이스 방어 $q->orWhere(function ($q2) use ($ip4) { $q2->where('gubun_code', '01') ->where('filter', $ip4); @@ -563,7 +563,7 @@ class MemberAuthRepository } } - // ✅ mem_join_log 기록 (CI3: S 또는 A 일때만 기록) + // mem_join_log 기록 (CI3: S 또는 A 일때만 기록) $seq = (int) DB::table('mem_join_log')->insertGetId([ 'gubun' => $gubun, // CI3: gubun_code를 gubun에 저장 'mem_no' => (int)($userInfo['mem_no'] ?? 0), // 가입 전이면 0 @@ -572,7 +572,7 @@ class MemberAuthRepository 'email' => (string)($userInfo['email'] ?? '-'), 'ip4' => $ip4, 'ip4_c' => $ip4c, - 'error_code' => $result, // ✅ join_block을 error_code에 저장(추적용) + 'error_code' => $result, // join_block을 error_code에 저장(추적용) 'dt_reg' => $userInfo['dt_reg'] ?? date('Y-m-d H:i:s'), ]); @@ -675,7 +675,7 @@ class MemberAuthRepository return false; } - // ✅ 1차 비번은 mem_st_ring.str_0 + // 1차 비번은 mem_st_ring.str_0 $stored = (string) (DB::table('mem_st_ring') ->where('mem_no', $memNo) ->value('str_0') ?? ''); @@ -684,7 +684,7 @@ class MemberAuthRepository return false; } - // ✅ CI 방식(PASS_SET=0) - 기존 attemptLegacyLogin과 동일 + // CI 방식(PASS_SET=0) - 기존 attemptLegacyLogin과 동일 $try = (string) CiPassword::make($pwPlain, 0); if ($try === '') { return false; diff --git a/app/Repositories/Mypage/UsageRepository.php b/app/Repositories/Mypage/UsageRepository.php index 5faee44..3759e5a 100644 --- a/app/Repositories/Mypage/UsageRepository.php +++ b/app/Repositories/Mypage/UsageRepository.php @@ -2,6 +2,7 @@ namespace App\Repositories\Mypage; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; final class UsageRepository @@ -28,10 +29,18 @@ final class UsageRepository 'a.created_at as attempt_created_at', 'a.updated_at as attempt_updated_at', + // 추가: cancel_status 필드들 + 'a.cancel_status as attempt_cancel_status', + 'a.cancel_requested_at as attempt_cancel_requested_at', + 'a.cancel_done_at as attempt_cancel_done_at', + 'a.cancel_last_code as attempt_cancel_last_code', + 'a.cancel_last_msg as attempt_cancel_last_msg', + 'o.id as order_id', 'o.oid as order_oid', 'o.mem_no as order_mem_no', 'o.stat_pay as order_stat_pay', + 'o.products_name as order_product_name', 'o.provider as order_provider', 'o.pay_method as order_pay_method', 'o.pg_tid as order_pg_tid', @@ -45,11 +54,125 @@ final class UsageRepository 'o.ret_data as order_ret_data', 'o.created_at as order_created_at', 'o.updated_at as order_updated_at', + + // 추가: order cancel_status 필드들 + 'o.cancel_status as order_cancel_status', + 'o.cancel_requested_at as order_cancel_requested_at', + 'o.cancel_done_at as order_cancel_done_at', + 'o.cancel_last_code as order_cancel_last_code', + 'o.cancel_last_msg as order_cancel_last_msg', + 'o.cancel_reason as order_cancel_reason', ]) ->where('a.id', $attemptId) ->first(); } + /** + * 리스트: 검색/페이징 + */ + public function paginateAttemptsWithOrder(int $memNo, array $filters, int $perPage = 20): LengthAwarePaginator + { + $q = trim((string)($filters['q'] ?? '')); + $method = trim((string)($filters['method'] ?? '')); + $status = trim((string)($filters['status'] ?? '')); + $from = trim((string)($filters['from'] ?? '')); + $to = trim((string)($filters['to'] ?? '')); + + // order_items 집계 서브쿼리 (group by를 메인 쿼리에서 피해서 paginate 안정) + $oiAgg = DB::table('gc_pin_order_items') + ->selectRaw('order_id, SUM(qty) as total_qty, MIN(item_name) as first_item_name') + ->groupBy('order_id'); + + $qb = DB::table('gc_payment_attempts as a') + ->leftJoin('gc_pin_order as o', 'o.id', '=', 'a.order_id') + ->leftJoinSub($oiAgg, 'oi', 'oi.order_id', '=', 'o.id') + ->where('a.mem_no', $memNo) + + // ✅ 중요: OR 조건 전체를 반드시 하나의 where 그룹으로 묶어야 함 + ->where(function ($s) { + // 1) 취소완료 + $s->where(function ($x) { + $x->where('a.cancel_status', 'success') + ->orWhere('o.cancel_status', 'success'); + }) + + // 2) 결제완료 + ->orWhere(function ($x) { + $x->where('a.status', 'paid') + ->orWhere('o.stat_pay', 'p'); + }) + + // 3) 입금대기(가상계좌) + ->orWhere(function ($x) { + $x->where('a.status', 'issued') + ->orWhere('o.stat_pay', 'w'); + }); + }) + + ->select([ + 'a.id as attempt_id', + 'o.oid as order_oid', + DB::raw("COALESCE(o.products_name, oi.first_item_name) as product_name"), + 'oi.first_item_name as item_name', + DB::raw("COALESCE(oi.total_qty, 0) as total_qty"), + 'a.pay_method as pay_method', + 'o.pay_money as pay_money', + 'a.status as attempt_status', + 'o.stat_pay as order_stat_pay', + 'a.cancel_status as attempt_cancel_status', + 'o.cancel_status as order_cancel_status', + 'a.created_at as created_at', + ]) + ->orderByDesc('a.id'); + + // ✅ q 검색: 거래번호(o.oid / a.oid) 정확히 일치 + if ($q !== '') { + $qb->where(function ($w) use ($q) { + $w->where('o.oid', $q) + ->orWhere('a.oid', $q); + }); + } + + // 결제수단 + if ($method !== '') { + $qb->where('a.pay_method', $method); + } + + // ✅ 상태 필터도 화면 의미에 맞게 묶어서 처리 + // (현재 화면 기준: paid / issued / cancel를 의미 상태로 쓰는 경우) + if ($status !== '') { + if ($status === 'paid') { + $qb->where(function ($x) { + $x->where('a.status', 'paid') + ->orWhere('o.stat_pay', 'p'); + }); + } elseif ($status === 'issued') { + $qb->where(function ($x) { + $x->where('a.status', 'issued') + ->orWhere('o.stat_pay', 'w'); + }); + } elseif (in_array($status, ['cancel', 'cancelled', 'canceled'], true)) { + $qb->where(function ($x) { + $x->where('a.cancel_status', 'success') + ->orWhere('o.cancel_status', 'success'); + }); + } else { + // 기타 상태는 attempts 기준으로 그대로 + $qb->where('a.status', $status); + } + } + + // 날짜 필터 (date 기준) + if ($from !== '') { + $qb->whereDate('a.created_at', '>=', $from); + } + if ($to !== '') { + $qb->whereDate('a.created_at', '<=', $to); + } + + return $qb->paginate($perPage)->appends($filters); + } + public function getOrderItems(int $orderId) { return DB::table('gc_pin_order_items') @@ -74,9 +197,34 @@ final class UsageRepository ->get(); $out = []; - foreach ($rows as $r) { - $out[(string)$r->status] = (int)$r->cnt; - } + foreach ($rows as $r) $out[(string)$r->status] = (int)$r->cnt; return $out; } + + /** + * 핀 목록(오픈 전/후 표시용) — 핀 반납(홀드 해제)은 이번 범위 제외 + */ + public function getPinsForOrder(int $orderId): array + { + $rows = DB::table('gc_pins') + ->where('order_id', $orderId) + ->orderBy('id', 'asc') + ->get(); + + return array_map(fn($r) => (array)$r, $rows->all()); + } + + /** + * 취소 로그 조회 + */ + public function getCancelLogsForAttempt(int $attemptId, int $limit = 20): array + { + $rows = DB::table('gc_payment_cancel_logs') + ->where('attempt_id', $attemptId) + ->orderByDesc('id') + ->limit($limit) + ->get(); + + return array_map(fn($r) => (array)$r, $rows->all()); + } } diff --git a/app/Rules/RecaptchaV3Rule.php b/app/Rules/RecaptchaV3Rule.php index b3cc1c9..9901a90 100644 --- a/app/Rules/RecaptchaV3Rule.php +++ b/app/Rules/RecaptchaV3Rule.php @@ -15,7 +15,7 @@ class RecaptchaV3Rule implements ValidationRule { $token = (string) $value; - // ✅ 개발환경에서만 + 전용 로그파일 + // 개발환경에서만 + 전용 로그파일 if (app()->environment(['local', 'development', 'staging'])) { Log::channel('google_recaptcha')->info('[incoming]', [ 'expected_action' => $this->action, diff --git a/app/Services/Admin/AdminAuthService.php b/app/Services/Admin/AdminAuthService.php index 2c36e51..ba97cda 100644 --- a/app/Services/Admin/AdminAuthService.php +++ b/app/Services/Admin/AdminAuthService.php @@ -37,22 +37,22 @@ final class AdminAuthService { $admin = $this->users->findByEmail($email); - // ✅ 계정 없으면 invalid (계정 존재 여부 노출 방지) + // 계정 없으면 invalid (계정 존재 여부 노출 방지) if (!$admin) { return ['state' => 'invalid']; } - // ✅ 상태 체크: active만 로그인 허용 (너 DB가 active/blocked라면 여기만 쓰자) + // 상태 체크: active만 로그인 허용 (너 DB가 active/blocked라면 여기만 쓰자) if (($admin->status ?? 'blocked') !== 'active') { return ['state' => 'blocked']; } - // ✅ 잠금 체크: locked_until != null 이면 "잠금 상태"(영구잠금) + // 잠금 체크: locked_until != null 이면 "잠금 상태"(영구잠금) if (($admin->locked_until ?? null) !== null) { return ['state' => 'locked', 'admin_id' => (int)$admin->id]; } - // ✅ 비밀번호 검증 + // 비밀번호 검증 if (!Hash::check($password, (string)$admin->password)) { // 실패 카운트 +1, 3회 이상이면 잠금 @@ -66,17 +66,17 @@ final class AdminAuthService return ['state' => 'invalid', 'attempts_left' => $left]; } - // ✅ 3회 안에 성공하면 실패/잠금 초기화 + // 3회 안에 성공하면 실패/잠금 초기화 if ((int)($admin->failed_login_count ?? 0) > 0 || ($admin->locked_until ?? null) !== null) { $this->users->clearLoginFailAndUnlock((int)$admin->id); } - // ✅ 비번 리셋 강제 정책 + // 비번 리셋 강제 정책 if ((int)($admin->must_reset_password ?? 0) === 1) { return ['state' => 'must_reset', 'admin_id' => (int)$admin->id]; } - // ✅ TOTP 모드면 SMS 발송 없이 "TOTP 입력"으로 보냄 + // TOTP 모드면 SMS 발송 없이 "TOTP 입력"으로 보냄 $totpEnabled = (int)($admin->totp_enabled ?? 0) === 1; $totpReady = $totpEnabled && !empty($admin->totp_secret_enc) @@ -313,7 +313,7 @@ final class AdminAuthService $enc = (string) $admin->phone_enc; if ($enc === '') return ''; - // ✅ 1) encryptString() 로 저장한 값은 decryptString() 으로 복호화해야 함 + // 1) encryptString() 로 저장한 값은 decryptString() 으로 복호화해야 함 try { $raw = Crypt::decryptString($enc); $digits = preg_replace('/\D+/', '', (string) $raw) ?: ''; @@ -329,7 +329,7 @@ final class AdminAuthService return $digits; } catch (\Throwable $e1) { - // ✅ 2) 혹시 예전 encrypt() (serialize 기반)로 저장한 데이터가 섞였으면 이걸로 복구 + // 2) 혹시 예전 encrypt() (serialize 기반)로 저장한 데이터가 섞였으면 이걸로 복구 try { Log::warning('[admin-auth] phone decrypt failed', [ 'admin_id' => $admin->id, @@ -341,7 +341,7 @@ final class AdminAuthService $digits = preg_replace('/\D+/', '', (string) $raw) ?: ''; return preg_match('/^\d{9,15}$/', $digits) ? $digits : ''; } catch (\Throwable $e2) { - // ✅ 3) 진짜 평문 숫자만 예외적으로 허용(암호문에서 숫자 긁는 fallback 절대 금지) + // 3) 진짜 평문 숫자만 예외적으로 허용(암호문에서 숫자 긁는 fallback 절대 금지) if (preg_match('/^\d{9,15}$/', $enc)) { return $enc; } diff --git a/app/Services/Admin/AdminMeService.php b/app/Services/Admin/AdminMeService.php index fd55e90..9256b6b 100644 --- a/app/Services/Admin/AdminMeService.php +++ b/app/Services/Admin/AdminMeService.php @@ -52,8 +52,8 @@ final class AdminMeService $phone = trim((string) $request->input('phone', '')); $data = $request->validate([ - 'nickname' => ['required', 'string', 'min:2', 'max:80'], // ✅ 닉네임(예: super admin) - 'name' => ['required', 'string', 'min:2', 'max:80'], // ✅ 성명(본명) + 'nickname' => ['required', 'string', 'min:2', 'max:80'], // 닉네임(예: super admin) + 'name' => ['required', 'string', 'min:2', 'max:80'], // 성명(본명) 'phone' => ['nullable', 'string', 'max:30'], ]); @@ -92,7 +92,7 @@ final class AdminMeService 'phone_enc' => $phoneEnc, 'phone_hash' => $phoneHash, - // ✅ updated_by 컬럼이 없어도 Repository가 자동 제거함 + // updated_by 컬럼이 없어도 Repository가 자동 제거함 'updated_by' => $adminId, ]; diff --git a/app/Services/Admin/Log/AdminAuditLogService.php b/app/Services/Admin/Log/AdminAuditLogService.php index 99842fe..f1981fd 100644 --- a/app/Services/Admin/Log/AdminAuditLogService.php +++ b/app/Services/Admin/Log/AdminAuditLogService.php @@ -21,7 +21,7 @@ final class AdminAuditLogService 'ip' => $this->safeStr($query['ip'] ?? '', 45), ]; - // ✅ 기간 역전 방지 + // 기간 역전 방지 if ($filters['date_from'] && $filters['date_to']) { if (strcmp($filters['date_from'], $filters['date_to']) > 0) { [$filters['date_from'], $filters['date_to']] = [$filters['date_to'], $filters['date_from']]; @@ -61,7 +61,7 @@ final class AdminAuditLogService 'user_agent' => (string)($row['user_agent'] ?? ''), - // ✅ pretty 출력 (모달에서 바로 textContent로 넣기 좋게) + // pretty 출력 (모달에서 바로 textContent로 넣기 좋게) 'before_pretty' => $this->prettyJson($beforeRaw), 'after_pretty' => $this->prettyJson($afterRaw), diff --git a/app/Services/Admin/Log/MemberJoinLogService.php b/app/Services/Admin/Log/MemberJoinLogService.php index 821fc55..7af133b 100644 --- a/app/Services/Admin/Log/MemberJoinLogService.php +++ b/app/Services/Admin/Log/MemberJoinLogService.php @@ -32,14 +32,14 @@ final class MemberJoinLogService 'phone_enc' => null, ]; - // ✅ 기간 역전 방지 + // 기간 역전 방지 if ($filters['date_from'] && $filters['date_to']) { if (strcmp($filters['date_from'], $filters['date_to']) > 0) { [$filters['date_from'], $filters['date_to']] = [$filters['date_to'], $filters['date_from']]; } } - // ✅ 전화번호 검색: encrypt(숫자) = cell_phone (정확일치) + // 전화번호 검색: encrypt(숫자) = cell_phone (정확일치) $phoneDigits = preg_replace('/\D+/', '', (string)$filters['phone']) ?: ''; if ($phoneDigits !== '' && preg_match('/^\d{10,11}$/', $phoneDigits)) { try { @@ -53,7 +53,7 @@ final class MemberJoinLogService $page = $this->repo->paginate($filters, 30); - // ✅ 리스트 표시용 가공(복호화/포맷) + // 리스트 표시용 가공(복호화/포맷) $seed = app(CiSeedCrypto::class); $items = []; diff --git a/app/Services/Admin/Mail/AdminMailService.php b/app/Services/Admin/Mail/AdminMailService.php index 394f88d..af334ed 100644 --- a/app/Services/Admin/Mail/AdminMailService.php +++ b/app/Services/Admin/Mail/AdminMailService.php @@ -100,7 +100,7 @@ final class AdminMailService $id = $this->tplRepo->create($payload); - // ✅ 성공시에만 감사로그 (기존 흐름 방해 X) + // 성공시에만 감사로그 (기존 흐름 방해 X) if ((int)$id > 0) { $req = request(); $this->audit->log( @@ -121,7 +121,7 @@ final class AdminMailService public function updateTemplate(int $adminId, int $id, array $data): array { - // ✅ before 스냅샷: repo에 get/find가 있으면 사용, 없으면 null로 진행(흐름 방해 X) + // before 스냅샷: repo에 get/find가 있으면 사용, 없으면 null로 진행(흐름 방해 X) $before = null; try { if (method_exists($this->tplRepo, 'find')) { @@ -148,7 +148,7 @@ final class AdminMailService $ok = ($affected >= 0); - // ✅ 성공시에만 감사로그 + // 성공시에만 감사로그 if ($ok) { $req = request(); @@ -228,7 +228,7 @@ final class AdminMailService { $mode = (string)$data['send_mode']; - // ✅ (선택) 템플릿을 서버에서 강제 적용하고 싶으면 template_id가 넘어왔을 때 덮어쓰기 + // (선택) 템플릿을 서버에서 강제 적용하고 싶으면 template_id가 넘어왔을 때 덮어쓰기 // Blade에서 tplSelect에 name="template_id" 꼭 넣어야 들어옴 $tplId = (int)($data['template_id'] ?? 0); if ($tplId > 0) { @@ -297,7 +297,7 @@ final class AdminMailService foreach ($list as $r) { $tokens = $r['tokens'] ?? []; - // ✅ strtr로 치환(키-값 배열) + // strtr로 치환(키-값 배열) $subjectFinal = $this->applyTokens((string)$data['subject'], $tokens); $bodyFinal = $this->applyTokens((string)$data['body'], $tokens); @@ -374,7 +374,7 @@ final class AdminMailService { // 형식: // email,token1,token2,... - // ✅ 1열=email, 2열={_text_02_}, 3열={_text_03_} ... + // 1열=email, 2열={_text_02_}, 3열={_text_03_} ... $lines = preg_split('/\r\n|\r|\n/', $text) ?: []; $out = []; @@ -382,7 +382,7 @@ final class AdminMailService $line = trim((string)$line); if ($line === '') continue; - // ✅ 빈열 유지(열 밀림 방지) => filter 하지 않음 + // 빈열 유지(열 밀림 방지) => filter 하지 않음 $cols = array_map('trim', explode(',', $line)); $email = $cols[0] ?? ''; diff --git a/app/Services/Admin/Member/AdminMemberJoinFilterService.php b/app/Services/Admin/Member/AdminMemberJoinFilterService.php index 8bbbbe1..d759501 100644 --- a/app/Services/Admin/Member/AdminMemberJoinFilterService.php +++ b/app/Services/Admin/Member/AdminMemberJoinFilterService.php @@ -93,7 +93,7 @@ final class AdminMemberJoinFilterService $seq = $this->repo->insert($data); if ($seq <= 0) return $this->fail('등록에 실패했습니다.'); - // ✅ 감사로그: 성공시에만 (기존 흐름 방해 X) + // 감사로그: 성공시에만 (기존 흐름 방해 X) $aid = (int)($actorAdminId ?? auth('admin')->id() ?? 0); if ($aid > 0) { $req = request(); @@ -133,11 +133,11 @@ final class AdminMemberJoinFilterService $affected = $this->repo->update($seq, $data); - // ✅ 0건(변경 없음)도 성공 처리 (기존 정책 유지) + // 0건(변경 없음)도 성공 처리 (기존 정책 유지) if ($affected === 0) return $this->ok('변경사항이 없습니다. (저장 완료)'); if ($affected > 0) { - // ✅ 감사로그: 성공시에만 + // 감사로그: 성공시에만 $aid = (int)($actorAdminId ?? auth('admin')->id() ?? 0); if ($aid > 0) { $req = request(); @@ -185,7 +185,7 @@ final class AdminMemberJoinFilterService $affected = $this->repo->delete($seq); if ($affected <= 0) return $this->fail('삭제에 실패했습니다.'); - // ✅ 감사로그: 성공시에만 + // 감사로그: 성공시에만 $aid = (int)($actorAdminId ?? auth('admin')->id() ?? 0); if ($aid > 0) { $req = request(); diff --git a/app/Services/Admin/Member/AdminMemberMarketingService.php b/app/Services/Admin/Member/AdminMemberMarketingService.php index f929d19..a213b3e 100644 --- a/app/Services/Admin/Member/AdminMemberMarketingService.php +++ b/app/Services/Admin/Member/AdminMemberMarketingService.php @@ -138,7 +138,7 @@ final class AdminMemberMarketingService } // ------------------------- - // ✅ 다운로드 헤더(한글) + // 다운로드 헤더(한글) // ------------------------- private function headersKorean(): array { diff --git a/app/Services/Admin/Member/AdminMemberService.php b/app/Services/Admin/Member/AdminMemberService.php index 25333ef..ff843e6 100644 --- a/app/Services/Admin/Member/AdminMemberService.php +++ b/app/Services/Admin/Member/AdminMemberService.php @@ -80,7 +80,7 @@ final class AdminMemberService $plainPhone = $this->plainPhone((string)($member->cell_phone ?? '')); $phoneDisplay = $this->formatPhone($plainPhone); - // ✅ 레거시 JSON 파싱 (admin_memo / modify_log) + // 레거시 JSON 파싱 (admin_memo / modify_log) $adminMemoList = $this->legacyAdminMemoList($member->admin_memo ?? null); // old -> new normalize $stateLogList = $this->legacyStateLogList($member->modify_log ?? null); // old -> new normalize @@ -88,7 +88,7 @@ final class AdminMemberService $adminMemo = array_reverse($adminMemoList); $modifyLog = array_reverse($stateLogList); - // ✅ adminMap 대상 admin_num 수집 + // adminMap 대상 admin_num 수집 $adminSet = []; foreach ($adminMemo as $it) { @@ -123,7 +123,7 @@ final class AdminMemberService 'plainPhone' => $plainPhone, 'phoneDisplay' => $phoneDisplay, - // ✅ 레거시 기반 결과 + // 레거시 기반 결과 'adminMemo' => $adminMemo, 'modifyLog' => $modifyLog, 'adminMap' => $adminMap, @@ -161,7 +161,7 @@ final class AdminMemberService $data = []; - // ✅ 기존 modify_log에서 레거시 state_log[] 뽑기 + // 기존 modify_log에서 레거시 state_log[] 뽑기 $stateLog = $this->legacyStateLogList($before->modify_log ?? null); $beforeStateLogLast = empty($stateLog) ? null : $stateLog[count($stateLog) - 1]; @@ -180,7 +180,7 @@ final class AdminMemberService // (audit용) after 스냅샷은 before에서 변경분만 덮어쓰기 $afterAudit = $beforeAudit; - // ✅ stat_3 변경 (1~3만 변경 허용, 4~6 금지 정책 유지) + // stat_3 변경 (1~3만 변경 허용, 4~6 금지 정책 유지) if (array_key_exists('stat_3', $input)) { $s3 = (string)($input['stat_3'] ?? ''); if (!in_array($s3, ['1','2','3','4','5','6'], true)) { @@ -208,7 +208,7 @@ final class AdminMemberService } } - // ✅ 통신사 변경 + // 통신사 변경 if (array_key_exists('cell_corp', $input)) { $corp = (string)($input['cell_corp'] ?? 'n'); $allowed = ['n','01','02','03','04','05','06']; @@ -230,7 +230,7 @@ final class AdminMemberService } } - // ✅ 휴대폰 변경(암호화 저장) + // 휴대폰 변경(암호화 저장) $afterPhoneMasked = null; if (array_key_exists('cell_phone', $input)) { $raw = trim((string)($input['cell_phone'] ?? '')); @@ -266,7 +266,7 @@ final class AdminMemberService return $this->ok('변경사항이 없습니다.'); } - // ✅ 레거시 modify_log 저장: 반드시 {"state_log":[...]} + // 레거시 modify_log 저장: 반드시 {"state_log":[...]} $stateLog = $this->trimLegacyList($stateLog, 300); $data['modify_log'] = $this->encodeJsonObjectOrNull(['state_log' => $stateLog]); @@ -276,7 +276,7 @@ final class AdminMemberService $ok = $this->repo->updateMember($memNo, $data); if (!$ok) return $this->fail('저장에 실패했습니다.'); - // ✅ audit log (update 성공 후, 같은 트랜잭션 안에서) + // audit log (update 성공 후, 같은 트랜잭션 안에서) $afterStateLogLast = empty($stateLog) ? null : $stateLog[count($stateLog) - 1]; $afterAudit['dt_mod'] = $nowStr; $afterAudit['state_log_last'] = $afterStateLogLast; @@ -442,12 +442,12 @@ final class AdminMemberService $nowStr = $now->format('Y-m-d H:i:s'); $legacyWhen = $now->format('y-m-d H:i:s'); // 레거시(2자리년도) - // ✅ 기존 admin_memo에서 레거시 memo[] 뽑기 + // 기존 admin_memo에서 레거시 memo[] 뽑기 $list = $this->legacyAdminMemoList($before->admin_memo ?? null); $beforeCount = is_array($list) ? count($list) : 0; $beforeLast = $beforeCount > 0 ? $list[$beforeCount - 1] : null; - // ✅ append (레거시 키 유지) + // append (레거시 키 유지) $newItem = [ 'memo' => $memo, 'when' => $legacyWhen, @@ -456,7 +456,7 @@ final class AdminMemberService $list[] = $newItem; $list = $this->trimLegacyList($list, 300); - // ✅ admin_memo는 반드시 {"memo":[...]} 형태 + // admin_memo는 반드시 {"memo":[...]} 형태 $obj = ['memo' => $list]; $data = [ @@ -467,7 +467,7 @@ final class AdminMemberService $ok = $this->repo->updateMember($memNo, $data); if (!$ok) return $this->fail('메모 저장에 실패했습니다.'); - // ✅ audit (성공 후) + // audit (성공 후) $afterCount = count($list); $afterLast = $afterCount > 0 ? $list[$afterCount - 1] : null; @@ -533,7 +533,7 @@ final class AdminMemberService return json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } -// ✅ admin_memo: {"memo":[{memo,when,admin_num}, ...]} +// admin_memo: {"memo":[{memo,when,admin_num}, ...]} private function legacyAdminMemoList($adminMemoJson): array { $obj = $this->decodeJsonObject($adminMemoJson); @@ -562,7 +562,7 @@ final class AdminMemberService return []; } -// ✅ modify_log: {"state_log":[{when,after,title,before,admin_num}, ...]} +// modify_log: {"state_log":[{when,after,title,before,admin_num}, ...]} private function legacyStateLogList($modifyLogJson): array { $obj = $this->decodeJsonObject($modifyLogJson); diff --git a/app/Services/Admin/Notice/AdminNoticeService.php b/app/Services/Admin/Notice/AdminNoticeService.php index 5f7e8a3..51b4af6 100644 --- a/app/Services/Admin/Notice/AdminNoticeService.php +++ b/app/Services/Admin/Notice/AdminNoticeService.php @@ -44,7 +44,7 @@ final class AdminNoticeService if ($wantPinned) { $max = $this->repo->maxFirstSignForUpdate(); // lock 포함 - $firstSign = $max + 1; // ✅ 상단공지 순번(큰 값이 먼저) + $firstSign = $max + 1; // 상단공지 순번(큰 값이 먼저) } $content = (string)($data['content'] ?? ''); @@ -68,7 +68,7 @@ final class AdminNoticeService $row = $this->repo->create($payload); $id = (int) $row->getKey(); - // ✅ 감사로그: 성공시에만, 실패해도 본 로직 영향 X + // 감사로그: 성공시에만, 실패해도 본 로직 영향 X if ($id > 0) { try { $req = request(); @@ -122,7 +122,7 @@ final class AdminNoticeService $row = $this->repo->lockForUpdate($id); - // ✅ 감사로그 before(업데이트 전) + // 감사로그 before(업데이트 전) $beforeContent = (string)($row->content ?? ''); $beforeAudit = [ 'id' => (int)$id, @@ -140,7 +140,7 @@ final class AdminNoticeService $hiding = !empty($data['hiding']) ? 'Y' : 'N'; - // ✅ first_sign 로직 유지 + // first_sign 로직 유지 $wantPinned = !empty($data['first_sign']); $firstSign = (int)($row->first_sign ?? 0); @@ -177,7 +177,7 @@ final class AdminNoticeService $this->repo->update($row, $payload); - // ✅ 감사로그: 성공시에만, 실패해도 본 로직 영향 X + // 감사로그: 성공시에만, 실패해도 본 로직 영향 X try { $actorAdminId = (int)(auth('admin')->id() ?? 0); if ($actorAdminId > 0) { @@ -216,7 +216,7 @@ final class AdminNoticeService throw $e; } - // ✅ 커밋 이후에만 기존 파일 삭제 + // 커밋 이후에만 기존 파일 삭제 $this->deletePhysicalFiles($oldToDelete); } @@ -235,7 +235,7 @@ final class AdminNoticeService $this->repo->delete($row); - // ✅ 감사로그: 성공시에만, 실패해도 본 로직 영향 X + // 감사로그: 성공시에만, 실패해도 본 로직 영향 X try { $actorAdminId = (int)(auth('admin')->id() ?? 0); if ($actorAdminId > 0) { diff --git a/app/Services/Admin/Product/AdminMediaService.php b/app/Services/Admin/Product/AdminMediaService.php index da7bdfc..923aec9 100644 --- a/app/Services/Admin/Product/AdminMediaService.php +++ b/app/Services/Admin/Product/AdminMediaService.php @@ -8,7 +8,7 @@ use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -// ✅ 이미지 처리 라이브러리 (Intervention Image v3) +// 이미지 처리 라이브러리 (Intervention Image v3) use Intervention\Image\ImageManager; use Intervention\Image\Drivers\Gd\Driver; diff --git a/app/Services/Admin/Qna/AdminQnaService.php b/app/Services/Admin/Qna/AdminQnaService.php index 4c94c48..4a77126 100644 --- a/app/Services/Admin/Qna/AdminQnaService.php +++ b/app/Services/Admin/Qna/AdminQnaService.php @@ -137,7 +137,7 @@ final class AdminQnaService ]); } - // ✅ 감사로그 before (변경 전) + // 감사로그 before (변경 전) $beforeAudit = [ 'year' => $year, 'seq' => $seq, @@ -157,7 +157,7 @@ final class AdminQnaService $this->repo->update($year, $seq, $payload); - // ✅ 감사로그 after (변경 후) — 감사로그 실패해도 본 로직 영향 X + // 감사로그 after (변경 후) — 감사로그 실패해도 본 로직 영향 X try { $req = request(); @@ -197,7 +197,7 @@ final class AdminQnaService ]); } - // ✅ 감사로그 before (변경 전) + // 감사로그 before (변경 전) $beforeAudit = [ 'year' => $year, 'seq' => $seq, @@ -214,7 +214,7 @@ final class AdminQnaService 'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE), ]); - // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) + // 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) try { $req = request(); @@ -252,7 +252,7 @@ final class AdminQnaService ]); } - // ✅ 감사로그 before (변경 전) + // 감사로그 before (변경 전) $beforeAudit = [ 'year' => $year, 'seq' => $seq, @@ -269,7 +269,7 @@ final class AdminQnaService 'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE), ]); - // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) + // 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) try { $req = request(); @@ -314,7 +314,7 @@ final class AdminQnaService ]); } - // ✅ 감사로그 before (변경 전) + // 감사로그 before (변경 전) $beforeAudit = [ 'year' => $year, 'seq' => $seq, @@ -336,7 +336,7 @@ final class AdminQnaService $this->repo->update($year, $seq, $payload); - // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) + // 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) try { $req = request(); @@ -387,7 +387,7 @@ final class AdminQnaService if ($answerContent === '') abort(422, '관리자 답변을 입력해 주세요.'); if ($answerSms === '') abort(422, 'SMS 답변을 입력해 주세요.'); - // ✅ 감사로그 before (변경 전) — 원문은 저장하지 않고 길이/해시만 + // 감사로그 before (변경 전) — 원문은 저장하지 않고 길이/해시만 $beforeAudit = [ 'year' => $year, 'seq' => $seq, @@ -404,7 +404,7 @@ final class AdminQnaService 'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE), ]); - // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) + // 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) try { $req = request(); @@ -449,7 +449,7 @@ final class AdminQnaService ]); } - // ✅ 감사로그 before (변경 전) + // 감사로그 before (변경 전) $beforeAudit = [ 'year' => $year, 'seq' => $seq, @@ -484,7 +484,7 @@ final class AdminQnaService 'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE), ]); - // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) + // 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) try { $req = request(); @@ -545,7 +545,7 @@ final class AdminQnaService // 메모는 배정자만 제한할지, 모든 관리자 가능할지 선택. 관리자 모두 // if ((int)($row->answer_admin_num ?? 0) !== $adminId) abort(403); - // ✅ 감사로그 before (변경 전) — 메모 원문은 저장하지 않음 + // 감사로그 before (변경 전) — 메모 원문은 저장하지 않음 $beforeAudit = [ 'year' => $year, 'seq' => $seq, @@ -559,7 +559,7 @@ final class AdminQnaService 'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE), ]); - // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) + // 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X) try { $req = request(); diff --git a/app/Services/Admin/Sms/AdminSmsLogService.php b/app/Services/Admin/Sms/AdminSmsLogService.php index 49ccac8..672408a 100644 --- a/app/Services/Admin/Sms/AdminSmsLogService.php +++ b/app/Services/Admin/Sms/AdminSmsLogService.php @@ -48,7 +48,7 @@ final class AdminSmsLogService { $page = $this->repo->paginateBatches($filters, $perPage); - // ✅ 페이지 이동 시 필터 유지 + // 페이지 이동 시 필터 유지 $clean = array_filter($filters, fn($v) => $v !== null && $v !== ''); $page->appends($clean); @@ -64,7 +64,7 @@ final class AdminSmsLogService { $page = $this->repo->paginateItems($batchId, $filters, $perPage); - // ✅ 상세 페이지에서도 필터 유지 + // 상세 페이지에서도 필터 유지 $clean = array_filter($filters, fn($v) => $v !== null && $v !== ''); $page->appends($clean); diff --git a/app/Services/Admin/Sms/AdminSmsTemplateService.php b/app/Services/Admin/Sms/AdminSmsTemplateService.php index 93ad50b..5065dbc 100644 --- a/app/Services/Admin/Sms/AdminSmsTemplateService.php +++ b/app/Services/Admin/Sms/AdminSmsTemplateService.php @@ -41,7 +41,7 @@ final class AdminSmsTemplateService $id = $this->repo->insert($after); - // ✅ 성공시에만 감사로그 (id가 0/음수면 기록 안 함) + // 성공시에만 감사로그 (id가 0/음수면 기록 안 함) if ($id > 0) { $req = request(); $this->audit->log( @@ -62,7 +62,7 @@ final class AdminSmsTemplateService public function update(int $id, array $data): array { - // ✅ before 스냅샷(가능하면) — repo에 find/get이 없으면 lockForUpdate 같은 걸로 맞춰야 함 + // before 스냅샷(가능하면) — repo에 find/get이 없으면 lockForUpdate 같은 걸로 맞춰야 함 // 여기서는 "repo->find($id)"가 있다고 가정하지 않고, 안전하게 try로 감쌈. $before = null; try { @@ -84,10 +84,10 @@ final class AdminSmsTemplateService $affected = $this->repo->update($id, $payload); - // ✅ 기존 리턴 정책 유지 + // 기존 리턴 정책 유지 $ok = ($affected >= 0); - // ✅ 성공시에만 감사로그 + // 성공시에만 감사로그 if ($ok) { $req = request(); $actorAdminId = (int)(auth('admin')->id() ?? 0); diff --git a/app/Services/FindPasswordService.php b/app/Services/FindPasswordService.php index d81a557..29850f9 100644 --- a/app/Services/FindPasswordService.php +++ b/app/Services/FindPasswordService.php @@ -37,7 +37,7 @@ class FindPasswordService } RateLimiter::hit($key, 600); - // ✅ 가입자 확인(DB) + // 가입자 확인(DB) $member = $this->members->findByEmailAndName($emailLower, $name); if (!$member) { return [ @@ -51,7 +51,7 @@ class FindPasswordService $memNo = (int)$member->mem_no; - // ✅ DB 저장 없이 signed URL만 생성 + // DB 저장 없이 signed URL만 생성 $nonce = bin2hex(random_bytes(10)); $link = URL::temporarySignedRoute( @@ -63,7 +63,7 @@ class FindPasswordService ] ); - // ✅ 메일 발송 + // 메일 발송 try { $this->mail->sendTemplate( $emailLower, diff --git a/app/Services/MailService.php b/app/Services/MailService.php index 9e88ea7..e3236df 100644 --- a/app/Services/MailService.php +++ b/app/Services/MailService.php @@ -12,7 +12,7 @@ class MailService { $toList = is_array($to) ? $to : [$to]; - // ✅ 웹 요청 기준으로 실제 mail 설정을 로그로 남김 + // 웹 요청 기준으로 실제 mail 설정을 로그로 남김 Log::info('mail_send_debug', [ 'queue' => $queue, 'queue_default' => config('queue.default'), diff --git a/app/Services/MemInfoService.php b/app/Services/MemInfoService.php index e62906c..073a5e1 100644 --- a/app/Services/MemInfoService.php +++ b/app/Services/MemInfoService.php @@ -436,7 +436,7 @@ class MemInfoService $login1st = 'y'; } - // ✅ 세션 payload (CI3 키 유지) + // 세션 payload (CI3 키 유지) $session = [ '_login_' => true, '_mid' => (string)$mem->email, @@ -469,7 +469,7 @@ class MemInfoService { $mem = MemInfo::query()->whereKey($memNo)->first(); - // ✅ 출금계좌 인증정보 (있으면 1건) + // 출금계좌 인증정보 (있으면 1건) $outAccount = DB::table('mem_account') ->select(['bank_name', 'bank_act_num', 'bank_act_name', 'act_date']) ->where('mem_no', $memNo) @@ -497,7 +497,7 @@ class MemInfoService '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 ?? ''), @@ -569,7 +569,7 @@ class MemInfoService return ['ok' => false, 'message' => '로그인 정보가 올바르지 않습니다.']; } - // ✅ 최근 7일 이내 구매내역(stat_pay p/t) 있으면 불가 + // 최근 7일 이내 구매내역(stat_pay p/t) 있으면 불가 $from = Carbon::today()->subDays(7)->startOfDay(); $cnt = (int) DB::table('pin_order') diff --git a/app/Services/Mypage/UsageService.php b/app/Services/Mypage/UsageService.php index a54a32c..4f703c4 100644 --- a/app/Services/Mypage/UsageService.php +++ b/app/Services/Mypage/UsageService.php @@ -3,21 +3,132 @@ namespace App\Services\Mypage; use App\Repositories\Mypage\UsageRepository; +use App\Repositories\Payments\GcPinOrderRepository; +use App\Services\Payments\PaymentCancelService; final class UsageService { public function __construct( private readonly UsageRepository $repo, + private readonly GcPinOrderRepository $orders, + private readonly PaymentCancelService $cancelSvc, ) {} + /** + * 리스트(검색/페이징) + */ + public function buildListPageData(int $sessionMemNo, array $filters): array + { + $rows = $this->repo->paginateAttemptsWithOrder($sessionMemNo, $filters, 5); + + return [ + 'pageTitle' => '구매내역', + 'pageDesc' => '구매내역을 확인하고 상세에서 핀 확인/취소를 진행할 수 있습니다.', + 'mypageActive' => 'usage', + 'filters' => $filters, + 'rows' => $rows, + ]; + } + + + /** + * 상세 + */ + public function buildDetailPageData(int $attemptId, int $sessionMemNo): array + { + // 기존 detail 빌더 재사용 (호환/안정) + $data = $this->buildPageData($attemptId, $sessionMemNo); + if (($data['mode'] ?? '') !== 'detail') abort(404); + + $order = (array)($data['order'] ?? []); + $orderId = (int)($order['id'] ?? 0); + + $pins = $this->repo->getPinsForOrder($orderId); + $cancelLogs = $this->repo->getCancelLogsForAttempt($attemptId, 20); + + $retData = $order['ret_data'] ?? null; + $retArr = is_array($retData) ? $retData : []; + $pinsOpened = !empty($retArr['pin_opened_at']); + + // 결제완료 조건 + $attempt = (array)($data['attempt'] ?? []); + $isPaid = (($order['stat_pay'] ?? '') === 'p') || (($attempt['status'] ?? '') === 'paid'); + + // cancel_status 기반 버튼 제어 + $aCancel = (string)($attempt['cancel_status'] ?? 'none'); + $canCancel = $isPaid && !$pinsOpened && in_array($aCancel, ['none','failed'], true); + + $data['pins'] = $pins; + $data['pinsOpened'] = $pinsOpened; + $data['canCancel'] = $canCancel; + $data['cancelLogs'] = $cancelLogs; + + return $data; + } + + /** + * 핀 오픈(확인): ret_data에 pin_opened_at 기록 + */ + public function openPins(int $attemptId, int $sessionMemNo): array + { + $row = $this->repo->findAttemptWithOrder($attemptId); + if (!$row) return ['ok'=>false, 'message'=>'결제내역을 찾을 수 없습니다.']; + + $attemptMem = (int)($row->attempt_mem_no ?? 0); + $orderMem = (int)($row->order_mem_no ?? 0); + if ($attemptMem !== $sessionMemNo || ($orderMem > 0 && $orderMem !== $sessionMemNo)) { + return ['ok'=>false, 'message'=>'권한이 없습니다.']; + } + + $attemptStatus = (string)($row->attempt_status ?? ''); + $orderStatPay = (string)($row->order_stat_pay ?? ''); + if (!(($orderStatPay === 'p') || ($attemptStatus === 'paid'))) { + return ['ok'=>false, 'message'=>'결제완료 상태에서만 핀 확인이 가능합니다.']; + } + + $oid = (string)($row->order_oid ?? ''); + if ($oid === '') return ['ok'=>false, 'message'=>'주문정보가 올바르지 않습니다.']; + + $order = $this->orders->findByOidForUpdate($oid); + if (!$order) return ['ok'=>false, 'message'=>'주문을 찾을 수 없습니다.']; + + $ret = (array)($order->ret_data ?? []); + if (!empty($ret['pin_opened_at'])) { + return ['ok'=>true]; + } + + $ret['pin_opened_at'] = now()->toDateTimeString(); + $order->ret_data = $ret; + $order->save(); + + return ['ok'=>true]; + } + + /** + * 결제완료 후 취소(핀 오픈 전만) + */ + public function cancelPaidAttempt(int $attemptId, int $sessionMemNo, string $reason): array + { + $data = $this->buildDetailPageData($attemptId, $sessionMemNo); + + $pinsOpened = (bool)($data['pinsOpened'] ?? false); + + return $this->cancelSvc->cancelByAttempt( + $attemptId, + ['type' => 'user', 'mem_no' => $sessionMemNo, 'id' => $sessionMemNo], + $reason, + $pinsOpened + ); + } + + /** + * 기존 상세 빌더(첨부 파일 그대로 유지) + */ public function buildPageData(?int $attemptId, int $sessionMemNo): array { - // attempt_id 없으면 "구매내역(리스트) 준비중" 모드로 렌더 if (!$attemptId || $attemptId <= 0) { return [ 'mode' => 'empty', - 'pageTitle' => '구매내역', - 'pageDesc' => '결제 완료 후 핀 확인/발급/매입을 진행할 수 있습니다.', 'mypageActive' => 'usage', ]; } @@ -25,7 +136,6 @@ final class UsageService $row = $this->repo->findAttemptWithOrder($attemptId); if (!$row) abort(404); - // 소유자 검증 (존재 여부 숨김) $attemptMem = (int)($row->attempt_mem_no ?? 0); $orderMem = (int)($row->order_mem_no ?? 0); @@ -39,9 +149,7 @@ final class UsageService $items = $this->repo->getOrderItems($orderId); $requiredQty = 0; - foreach ($items as $it) { - $requiredQty += (int)($it->qty ?? 0); - } + foreach ($items as $it) $requiredQty += (int)($it->qty ?? 0); $assignedPinsCount = $this->repo->countAssignedPins($orderId); $pinsSummary = $this->repo->getAssignedPinsStatusSummary($orderId); @@ -54,12 +162,13 @@ final class UsageService return [ 'mode' => 'detail', 'pageTitle' => '구매내역', - 'pageDesc' => '결제 상태 및 핀 발급/매입 진행을 확인합니다.', + 'pageDesc' => '결제 상태 및 핀 확인/발급/매입 진행을 확인합니다.', 'mypageActive' => 'usage', 'attempt' => $this->attemptViewModel($row), 'order' => $this->orderViewModel($row), 'items' => $this->itemsViewModel($items), + 'productname' => $row->order_product_name, 'requiredQty' => $requiredQty, 'assignedPinsCount' => $assignedPinsCount, @@ -73,27 +182,13 @@ final class UsageService private function resolveStepKey(string $attemptStatus, string $orderStatPay, int $requiredQty, int $assignedPinsCount): string { - // 취소/실패 우선 - if (in_array($orderStatPay, ['c','f'], true) || in_array($attemptStatus, ['cancelled','failed'], true)) { - return 'failed'; - } - - // 가상계좌 입금대기 - if ($orderStatPay === 'w' || $attemptStatus === 'issued') { - return 'deposit_wait'; - } - - // 결제완료 + if (in_array($orderStatPay, ['c','f'], true) || in_array($attemptStatus, ['cancelled','failed'], true)) return 'failed'; + if ($orderStatPay === 'w' || $attemptStatus === 'issued') return 'deposit_wait'; if ($orderStatPay === 'p' || $attemptStatus === 'paid') { if ($requiredQty > 0 && $assignedPinsCount >= $requiredQty) return 'pin_done'; return 'pin_check'; } - - // 결제 진행 중/확인 중 - if (in_array($attemptStatus, ['ready','redirected','auth_ok'], true) || $orderStatPay === 'ready') { - return 'pay_processing'; - } - + if (in_array($attemptStatus, ['ready','redirected','auth_ok'], true) || $orderStatPay === 'ready') return 'pay_processing'; return 'pay_processing'; } @@ -120,6 +215,14 @@ final class UsageService 'pg_tid' => (string)($row->attempt_pg_tid ?? ''), 'return_code' => (string)($row->attempt_return_code ?? ''), 'return_msg' => (string)($row->attempt_return_msg ?? ''), + + // 추가: cancel 상태 요약 + 'cancel_status' => (string)($row->attempt_cancel_status ?? 'none'), + 'cancel_last_code' => (string)($row->attempt_cancel_last_code ?? ''), + 'cancel_last_msg' => (string)($row->attempt_cancel_last_msg ?? ''), + 'cancel_requested_at' => (string)($row->attempt_cancel_requested_at ?? ''), + 'cancel_done_at' => (string)($row->attempt_cancel_done_at ?? ''), + 'payloads' => [ 'request' => $this->jsonDecodeOrRaw($row->attempt_request_payload ?? null), 'response' => $this->jsonDecodeOrRaw($row->attempt_response_payload ?? null), @@ -142,6 +245,15 @@ final class UsageService 'pg_tid' => (string)($row->order_pg_tid ?? ''), 'ret_code' => (string)($row->order_ret_code ?? ''), 'ret_msg' => (string)($row->order_ret_msg ?? ''), + 'products_name' => (string)($row->products_name ?? ''), + + 'cancel_status' => (string)($row->order_cancel_status ?? 'none'), + 'cancel_last_code' => (string)($row->order_cancel_last_code ?? ''), + 'cancel_last_msg' => (string)($row->order_cancel_last_msg ?? ''), + 'cancel_reason' => (string)($row->order_cancel_reason ?? ''), + 'cancel_requested_at' => (string)($row->order_cancel_requested_at ?? ''), + 'cancel_done_at' => (string)($row->order_cancel_done_at ?? ''), + 'amounts' => [ 'subtotal' => (int)($row->order_subtotal_amount ?? 0), 'fee' => (int)($row->order_fee_amount ?? 0), @@ -172,7 +284,6 @@ final class UsageService private function extractVactInfo(object $row): array { - // 어떤 키로 들어오든 "있으면 보여주기" 수준의 안전한 추출 $candidates = [ $this->jsonDecodeOrArray($row->order_pay_data ?? null), $this->jsonDecodeOrArray($row->order_ret_data ?? null), @@ -187,13 +298,7 @@ final class UsageService return null; }; - $info = [ - 'bank' => null, - 'account' => null, - 'holder' => null, - 'amount' => null, - 'expire_at' => null, - ]; + $info = ['bank'=>null,'account'=>null,'holder'=>null,'amount'=>null,'expire_at'=>null]; foreach ($candidates as $a) { if (!$a) continue; @@ -204,7 +309,6 @@ final class UsageService $info['expire_at'] ??= $pick($a, ['expire_at', 'Vdate', 'vdate', 'ExpireDate']); } - // 다 null이면 빈 배열로 처리 $hasAny = false; foreach ($info as $v) { if ($v !== null) { $hasAny = true; break; } } return $hasAny ? $info : []; @@ -230,7 +334,6 @@ final class UsageService $decoded = json_decode($s, true); if (json_last_error() === JSON_ERROR_NONE) return $decoded; - // JSON이 아니라면 원문 그대로(운영 디버그용) return $s; } } diff --git a/app/Services/Order/OrderCheckoutService.php b/app/Services/Order/OrderCheckoutService.php index 0df1028..4159d7f 100644 --- a/app/Services/Order/OrderCheckoutService.php +++ b/app/Services/Order/OrderCheckoutService.php @@ -68,6 +68,8 @@ final class OrderCheckoutService $order = GcPinOrder::create([ 'oid' => $oid, 'mem_no' => $memNo, + 'products_id' => (int)($sku->product_id ?? 0) ?: null, + 'products_name' => (string)($sku->product_name ?? $sku->name ?? ''), 'order_type' => 'self', 'stat_pay' => 'ready', 'stat_tax' => 'taxfree', diff --git a/app/Services/Payments/CheckoutService.php b/app/Services/Payments/CheckoutService.php index b6e013a..a3453cf 100644 --- a/app/Services/Payments/CheckoutService.php +++ b/app/Services/Payments/CheckoutService.php @@ -18,6 +18,8 @@ final class CheckoutService $order = GcPinOrder::create([ 'oid' => $oid, 'mem_no' => $memNo, + 'products_id' => null, + 'products_name' => '테스트 상품권', 'stat_pay' => 'ready', 'stat_tax' => 'taxfree', 'subtotal_amount' => $amount, diff --git a/app/Services/Payments/PaymentCancelService.php b/app/Services/Payments/PaymentCancelService.php new file mode 100644 index 0000000..6b6a5f6 --- /dev/null +++ b/app/Services/Payments/PaymentCancelService.php @@ -0,0 +1,245 @@ +whereKey($attemptId)->lockForUpdate()->first(); + if (!$attempt) return ['ok'=>false, 'message'=>'결제내역을 찾을 수 없습니다.']; + + $order = $this->orders->findByOidForUpdate((string)$attempt->oid); + if (!$order) return ['ok'=>false, 'message'=>'주문을 찾을 수 없습니다.']; + + // 권한: user면 소유자만 + if (($actor['type'] ?? 'user') === 'user') { + if ((int)$attempt->mem_no !== (int)($actor['mem_no'] ?? 0)) { + return ['ok'=>false, 'message'=>'권한이 없습니다.']; + } + } + + // 상태 체크 + if ((string)$attempt->status !== 'paid' || (string)$order->stat_pay !== 'p') { + return ['ok'=>false, 'message'=>'취소 가능한 상태가 아닙니다.']; + } + + // 이미 성공 취소 + if ((string)($attempt->cancel_status ?? 'none') === 'success' || (string)($order->cancel_status ?? 'none') === 'success') { + return ['ok'=>false, 'message'=>'이미 취소 완료된 결제입니다.']; + } + + // 핀 오픈 전 조건 (이번 범위에서 “핀 반납” 제외지만, 오픈 후 취소는 금지) + if ($pinsOpened) { + return ['ok'=>false, 'message'=>'핀을 확인한 이후에는 취소할 수 없습니다.']; + } + + $tid = (string)($attempt->pg_tid ?: $order->pg_tid); + $amount = (int)$order->pay_money; + if ($tid === '' || $amount <= 0) return ['ok'=>false, 'message'=>'취소에 필요한 거래정보가 부족합니다.']; + + // 1) requested 상태 선반영 + $now = now(); + $attempt->cancel_status = 'requested'; + $attempt->cancel_requested_at = $now; + $attempt->cancel_last_msg = null; + $attempt->cancel_last_code = null; + $attempt->save(); + + $order->cancel_status = 'requested'; + $order->cancel_requested_at = $now; + $order->cancel_reason = $reason; + $order->save(); + + // 2) PG 취소 호출 + $payMethod = (string)$attempt->pay_method; + $req = []; + $res = []; + $ok = false; + $code = null; + $msg = null; + + try { + if ($payMethod === 'card') { + $cardKind = (string)($attempt->card_kind ?: 'general'); + $cfg = $this->danalCfg->card($cardKind); + + $req = [ + 'TID' => $tid, + 'AMOUNT' => (string)$amount, + 'CANCELTYPE' => 'C', + 'CANCELREQUESTER' => $this->requesterLabel($actor), + 'CANCELDESC' => $reason ?: '사용자 요청', + 'TXTYPE' => 'CANCEL', + 'SERVICETYPE' => 'DANALCARD', + ]; + $res = $this->cpcgi->call($cfg['url'], $cfg['cpid'], $req, $cfg['key'], $cfg['iv']); + + $code = (string)($res['RETURNCODE'] ?? ''); + $msg = (string)($res['RETURNMSG'] ?? ''); + $ok = ($code === '0000'); + + } elseif ($payMethod === 'wire') { + $cfg = $this->danalCfg->wiretransfer(); + + $req = [ + 'TID' => $tid, + 'AMOUNT' => (string)$amount, + 'CANCELTYPE' => 'C', + 'CANCELREQUESTER' => $this->requesterLabel($actor), + 'CANCELDESC' => $reason ?: '사용자 요청', + 'TXTYPE' => 'CANCEL', + 'SERVICETYPE' => 'WIRETRANSFER', + ]; + $res = $this->cpcgi->call((string)$cfg['tx_url'], (string)$cfg['cpid'], $req, (string)$cfg['key'], (string)$cfg['iv']); + + $code = (string)($res['RETURNCODE'] ?? ''); + $msg = (string)($res['RETURNMSG'] ?? ''); + $ok = ($code === '0000'); + + } elseif ($payMethod === 'phone') { + $mode = $this->resolvePhoneModeFromAttempt($attempt->request_payload); + $out = $this->phoneGateway->billCancel($mode, $tid); // (수정: PhoneGateway에 메서드 추가 필요) + $req = $out['req'] ?? []; + $res = $out['res'] ?? []; + + $code = (string)($res['Result'] ?? ''); + $msg = (string)($res['ErrMsg'] ?? ''); + $ok = ($code === '0'); + + } else { + $code = 'METHOD'; + $msg = '지원하지 않는 결제수단입니다.'; + $ok = false; + } + + } catch (\Throwable $e) { + $code = $code ?: 'EX'; + $msg = $msg ?: ('취소 처리 중 오류: ' . $e->getMessage()); + $ok = false; + } + + // 3) 로그 저장 (req/res 전문) + $logId = DB::table('gc_payment_cancel_logs')->insertGetId([ + 'attempt_id' => (int)$attempt->id, + 'order_id' => (int)$order->id, + 'mem_no' => (int)$attempt->mem_no, + 'provider' => (string)($attempt->provider ?: 'danal'), + 'pay_method' => $payMethod, + 'tid' => $tid, + 'amount' => $amount, + 'cancel_type' => 'C', + 'status' => $ok ? 'success' : 'failed', + 'requester_type' => (string)($actor['type'] ?? 'user'), + 'requester_id' => (int)($actor['id'] ?? 0) ?: null, + 'reason' => $reason, + 'req_payload' => json_encode($req, JSON_UNESCAPED_UNICODE), + 'res_payload' => json_encode($res, JSON_UNESCAPED_UNICODE), + 'result_code' => $code, + 'result_msg' => $msg, + 'requested_at' => $now, + 'done_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // 4) 결과 반영 (상태값 변경 + 요약 저장) + if ($ok) { + $attempt->status = 'cancelled'; + $attempt->cancel_status = 'success'; + $attempt->cancel_done_at = now(); + $attempt->cancel_last_code = $code; + $attempt->cancel_last_msg = $msg; + $attempt->save(); + + $order->stat_pay = 'c'; + $order->cancelled_at = now(); + $order->cancel_status = 'success'; + $order->cancel_done_at = now(); + $order->cancel_last_code = $code; + $order->cancel_last_msg = $msg; + $order->cancel_reason = $reason; + + // ret_data에 “취소 요약”만 append (전문은 logs 테이블) + $ret = (array)($order->ret_data ?? []); + $ret['cancel_last'] = [ + 'log_id' => (int)$logId, + 'method' => $payMethod, + 'tid' => $tid, + 'code' => $code, + 'msg' => $msg, + 'at' => now()->toDateTimeString(), + ]; + $order->ret_data = $ret; + $order->save(); + + return ['ok'=>true, 'message'=>'결제가 취소되었습니다.', 'meta'=>['log_id'=>$logId]]; + } + + // 실패: status는 paid 유지, cancel_status만 failed + $attempt->cancel_status = 'failed'; + $attempt->cancel_done_at = now(); + $attempt->cancel_last_code = (string)$code; + $attempt->cancel_last_msg = (string)$msg; + $attempt->save(); + + $order->cancel_status = 'failed'; + $order->cancel_done_at = now(); + $order->cancel_last_code = (string)$code; + $order->cancel_last_msg = (string)$msg; + $order->save(); + + return ['ok'=>false, 'message'=>($msg ?: '취소 실패'), 'meta'=>['code'=>$code]]; + }); + } + + private function requesterLabel(array $actor): string + { + $t = (string)($actor['type'] ?? 'user'); + $id = (int)($actor['id'] ?? 0); + if ($t === 'admin') return "admin:{$id}"; + if ($t === 'system') return "system:{$id}"; + return "user:" . (int)($actor['mem_no'] ?? $id); + } + + private function resolvePhoneModeFromAttempt($requestPayload): string + { + // request_payload가 array/json/string 어떤 형태든 대응 + $arr = []; + if (is_array($requestPayload)) $arr = $requestPayload; + elseif (is_string($requestPayload) && trim($requestPayload) !== '') { + $tmp = json_decode($requestPayload, true); + if (is_array($tmp)) $arr = $tmp; + } + + $id = (string)($arr['ID'] ?? ''); + $prod = (string)config('danal.phone.prod.cpid', ''); + $dev = (string)config('danal.phone.dev.cpid', ''); + + if ($id !== '' && $dev !== '' && $id === $dev) return 'dev'; + if ($id !== '' && $prod !== '' && $id === $prod) return 'prod'; + + return (string)config('danal.phone.default_mode', 'prod'); + } +} diff --git a/app/Services/Payments/PaymentService.php b/app/Services/Payments/PaymentService.php index 93794d8..db7d8c7 100644 --- a/app/Services/Payments/PaymentService.php +++ b/app/Services/Payments/PaymentService.php @@ -447,7 +447,7 @@ final class PaymentService return DB::transaction(function () use ($post) { $attemptId = 0; - $result = $this->phone->confirmAndBill($post, function (string $oid, string $token, string $tid, int $amount, array $payload) { + $result = $this->phone->confirmAndBill($post, function (string $oid, string $token, string $tid, int $amount, array $payload) use (&$attemptId) { $attempt = $this->attempts->findByTokenForUpdate('phone', $token); if (!$attempt) return; diff --git a/app/Services/SmsService.php b/app/Services/SmsService.php index bd0c6fa..59b8936 100644 --- a/app/Services/SmsService.php +++ b/app/Services/SmsService.php @@ -81,7 +81,7 @@ class SmsService } /** - * ✅ 예약시간 처리(CI3 로직 그대로) + * 예약시간 처리(CI3 로직 그대로) * - scheduled_at이 없으면 now() * - 'Y-m-d H:i'면 ':00' 붙임 * - 미래면 그 시간, 과거/형식오류면 now() @@ -111,7 +111,7 @@ class SmsService * CI: lguplus_send 이식 * - 메시지 길이(EUC-KR) 기준 90 이하 sms, 초과 mms * - sms_type이 명시되면 그걸 우선 - * - ✅ scheduled_at이 미래면 업체 예약 컬럼에 해당 시간으로 insert + * - scheduled_at이 미래면 업체 예약 컬럼에 해당 시간으로 insert * - SMS: SC_TRAN.TR_SENDDATE * - MMS: MMS_MSG.REQDATE */ @@ -120,7 +120,7 @@ class SmsService $conn = DB::connection('sms_server'); $smsSendType = $this->resolveSendType($data); - // ✅ 예약/즉시 결정 + // 예약/즉시 결정 $sendDate = $this->resolveProviderSendDate($data['scheduled_at'] ?? null); return (bool) $conn->transaction(function () use ($conn, $smsSendType, $data, $sendDate) { diff --git a/app/Support/LegacyCrypto/Seed.php b/app/Support/LegacyCrypto/Seed.php index 2cbabe3..38c8f4a 100644 --- a/app/Support/LegacyCrypto/Seed.php +++ b/app/Support/LegacyCrypto/Seed.php @@ -652,7 +652,7 @@ class Seed $Data = []; $len = strlen($pbUserKey); for ($i = 0; $i < $len; $i++) { - $Data[$i] = ord($pbUserKey[$i]); // ✅ {} -> [] + $Data[$i] = ord($pbUserKey[$i]); // {} -> [] } $this->SeedRoundKey($pdwRoundKey, $Data); } @@ -662,7 +662,7 @@ class Seed $Data = []; $len = strlen($pbData); for ($i = 0; $i < $len; $i++) { - $Data[$i] = ord($pbData[$i]); // ✅ {} -> [] + $Data[$i] = ord($pbData[$i]); // {} -> [] } $this->SeedEncrypt($Data, $pdwRoundKey, $outData); } @@ -672,7 +672,7 @@ class Seed $Data = []; $len = strlen($pbData); for ($i = 0; $i < $len; $i++) { - $Data[$i] = ord($pbData[$i]); // ✅ {} -> [] + $Data[$i] = ord($pbData[$i]); // {} -> [] } $this->SeedDecrypt($Data, $pdwRoundKey, $outData); } diff --git a/bootstrap/app.php b/bootstrap/app.php index e756734..26c48b4 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -14,7 +14,7 @@ return Application::configure(basePath: dirname(__DIR__)) ) ->withMiddleware(function (Middleware $middleware): void { - // ✅ Reverse Proxy 신뢰(정확한 client ip, https 판단) + // Reverse Proxy 신뢰(정확한 client ip, https 판단) $middleware->trustProxies(at: [ '192.168.100.0/24', '127.0.0.0/8', @@ -22,10 +22,10 @@ return Application::configure(basePath: dirname(__DIR__)) '172.16.0.0/12', ]); - // ✅ trustHosts는 요청 시점에 config 기반으로 적용 + // trustHosts는 요청 시점에 config 기반으로 적용 $middleware->prepend(\App\Http\Middleware\TrustedHostsFromConfig::class); - // ✅ CSRF 예외 처리 + // CSRF 예외 처리 $middleware->validateCsrfTokens(except: [ 'auth/register/danal/result', #다날인증 'mypage/info/danal/result', #다날인증 @@ -39,7 +39,7 @@ return Application::configure(basePath: dirname(__DIR__)) 'pay/danal/cancel', ]); - // ✅ alias 등록 + // alias 등록 $middleware->alias([ 'legacy.auth' => \App\Http\Middleware\LegacyAuth::class, 'legacy.guest' => \App\Http\Middleware\LegacyGuest::class, @@ -47,7 +47,7 @@ return Application::configure(basePath: dirname(__DIR__)) 'admin.role' => \App\Http\Middleware\AdminRole::class, ]); - // ✅ guest redirect (auth 미들웨어가 login 라우트 찾다 터지는거 방지) + // guest redirect (auth 미들웨어가 login 라우트 찾다 터지는거 방지) $middleware->redirectGuestsTo(function (Request $request) { $adminHost = (string) config('app.admin_domain', ''); return ($adminHost !== '' && $request->getHost() === $adminHost) diff --git a/config/bank_code.php b/config/bank_code.php index ba0c9bf..fb510d7 100644 --- a/config/bank_code.php +++ b/config/bank_code.php @@ -12,7 +12,7 @@ return [ - // ✅ UI용 그룹 (원하는 순서대로 렌더링) + // UI용 그룹 (원하는 순서대로 렌더링) 'groups' => [ 'bank_1st' => [ 'label' => '메이저 1금융권', @@ -127,7 +127,7 @@ return [ ], /** - * ✅ 코드→이름 빠른 조회용 flat 맵 + * 코드→이름 빠른 조회용 flat 맵 */ 'flat' => [ '001' => '한국은행', diff --git a/database/seeders/AdminRbacSeeder.php b/database/seeders/AdminRbacSeeder.php index 74f1fda..a8601c6 100644 --- a/database/seeders/AdminRbacSeeder.php +++ b/database/seeders/AdminRbacSeeder.php @@ -191,7 +191,7 @@ final class AdminRbacSeeder extends Seeder } /** - * ✅ phone_hash는 NOT NULL이므로 항상 채운다. + * phone_hash는 NOT NULL이므로 항상 채운다. * - 운영 정책용(=진짜 조회키)일 때: phoneDigits만 * - seed에서 같은 번호 10명 만들 때: phoneDigits + email로 유니크하게 */ diff --git a/public/assets/css/cs-notice.css b/public/assets/css/cs-notice.css index 723a09b..83ee9e4 100644 --- a/public/assets/css/cs-notice.css +++ b/public/assets/css/cs-notice.css @@ -43,12 +43,12 @@ .notice-head-row{ display:flex; align-items:center; - justify-content: space-between; /* ✅ 양끝 배치 */ + justify-content: space-between; /* 양끝 배치 */ gap:12px; flex-wrap: nowrap; padding-bottom: 12px; - border-bottom: 1px solid rgba(0,0,0,.08); /* ✅ 라이트 배경 기준: 검정 계열 */ + border-bottom: 1px solid rgba(0,0,0,.08); /* 라이트 배경 기준: 검정 계열 */ } /* 다크 테마일 때만 흰색 라인으로(선택) */ @@ -59,12 +59,12 @@ } .notice-count{ - flex: 0 0 auto; /* ✅ 줄어들지 않게 */ + flex: 0 0 auto; /* 줄어들지 않게 */ white-space: nowrap; } .notice-toolbar{ - margin-left: auto; /* ✅ 오른쪽으로 밀기(보험) */ + margin-left: auto; /* 오른쪽으로 밀기(보험) */ display:flex; justify-content:flex-end; } @@ -78,7 +78,7 @@ display:flex; align-items:center; gap:8px; - flex-wrap: nowrap; /* ✅ 검색 영역도 한 줄 */ + flex-wrap: nowrap; /* 검색 영역도 한 줄 */ } /* 검색 input 폭이 너무 커서 내려가는 걸 방지 */ @@ -119,7 +119,7 @@ gap: 8px; } - /* ✅ 핵심: 기존 width:240px 죽이고, flex로 꽉 채우기 */ + /* 핵심: 기존 width:240px 죽이고, flex로 꽉 채우기 */ .nt-search input{ width: auto; flex: 1 1 auto; @@ -300,7 +300,7 @@ line-height: 1.35; overflow:hidden; display:-webkit-box; - -webkit-line-clamp: 2; /* ✅ 두 줄 */ + -webkit-line-clamp: 2; /* 두 줄 */ -webkit-box-orient: vertical; } @@ -318,7 +318,7 @@ .nv-head{ display:flex; align-items:flex-start; - justify-content:space-between; /* ✅ 웹: 한 줄 양끝 */ + justify-content:space-between; /* 웹: 한 줄 양끝 */ gap:12px; } @@ -329,8 +329,8 @@ } .nv-meta{ - margin-top: 0; /* ✅ 웹: 제목과 같은 줄 */ - white-space: nowrap; /* ✅ 메타 줄바꿈 방지 */ + margin-top: 0; /* 웹: 제목과 같은 줄 */ + white-space: nowrap; /* 메타 줄바꿈 방지 */ flex: 0 0 auto; } @@ -350,7 +350,7 @@ .nv-actions{ margin-top: 16px; display:grid; - grid-template-columns: 1fr 1fr; /* ✅ 이전/다음 2칸 */ + grid-template-columns: 1fr 1fr; /* 이전/다음 2칸 */ gap:10px; } @@ -379,7 +379,7 @@ margin-bottom: 28px; } -/* ✅ 첨부/링크: 큰 박스 -> 가벼운 섹션 */ +/* 첨부/링크: 큰 박스 -> 가벼운 섹션 */ .nv-resources{ margin-top: 18px; /* 본문에서 아래로 떨어짐 */ padding-top: 14px; @@ -480,7 +480,7 @@ border-color: rgba(59,130,246,.18); } -/* ✅ 이전/다음: 아래로 더 내려서 답답함 제거 */ +/* 이전/다음: 아래로 더 내려서 답답함 제거 */ .nv-actions{ margin-top: 26px; /* 더 아래로 */ padding-top: 18px; diff --git a/public/assets/css/mypage_qna.css b/public/assets/css/mypage_qna.css index bf17320..f8cdb01 100644 --- a/public/assets/css/mypage_qna.css +++ b/public/assets/css/mypage_qna.css @@ -1,6 +1,6 @@ .mypage-qna-page .mq-topbar{ display:flex; - justify-content:space-between; /* ✅ 좌/우 끝으로 */ + justify-content:space-between; /* 좌/우 끝으로 */ align-items:center; margin: 0 0 14px 0; } @@ -298,7 +298,7 @@ padding: 12px; } - /* ✅ 메타(문의분류/처리상태/등록일) : 3컬럼 -> 세로 카드형 */ + /* 메타(문의분류/처리상태/등록일) : 3컬럼 -> 세로 카드형 */ .mq-detail-meta{ border-radius: 10px; } @@ -357,7 +357,7 @@ white-space: normal; } - /* ✅ 본문 카드: 2컬럼 -> 1컬럼 */ + /* 본문 카드: 2컬럼 -> 1컬럼 */ .mq-cards{ grid-template-columns: 1fr !important; gap: 10px; diff --git a/public/assets/js/mypage_renew.js b/public/assets/js/mypage_renew.js index fb9e23f..caf25da 100644 --- a/public/assets/js/mypage_renew.js +++ b/public/assets/js/mypage_renew.js @@ -39,7 +39,7 @@ let untilMs = parseUntilMsFromISO(untilStr); - // ✅ data-expire가 있으면 우선 사용 (unix seconds) + // data-expire가 있으면 우선 사용 (unix seconds) if (!untilMs && Number.isFinite(expireTs) && expireTs > 0) { untilMs = expireTs * 1000; } @@ -367,7 +367,7 @@ function close() { wrap.remove(); } - // ✅ 닫기: X / 취소만 + // 닫기: X / 취소만 wrap.querySelector('.mypage-pwmodal__close')?.addEventListener('click', close); wrap.querySelector('[data-act="cancel"]')?.addEventListener('click', close); @@ -547,7 +547,7 @@ function close() { wrap.remove(); } - // ✅ 닫기: X / 취소만 + // 닫기: X / 취소만 wrap.querySelector('.mypage-pin2modal__close')?.addEventListener('click', close); wrap.querySelector('[data-act="cancel"]')?.addEventListener('click', close); @@ -660,7 +660,7 @@ const postUrl = URLS.withdrawVerifyOut; const defaultDepositor = (CFG.memberName || '').trim(); - // ✅ bankGroups는 config(bank_code.php)의 groups 구조(label/items)로 전달된다고 가정 + // bankGroups는 config(bank_code.php)의 groups 구조(label/items)로 전달된다고 가정 const BANK_GROUPS = (CFG.bankGroups || {}); if (!postUrl) { @@ -813,7 +813,7 @@ const setError = makeErrorSetter(wrap, '#with_error'); const close = () => wrap.remove(); - // ✅ 닫기: X / 취소만 + // 닫기: X / 취소만 wrap.querySelector('.mypage-withmodal__close')?.addEventListener('click', close); wrap.querySelector('[data-act="cancel"]')?.addEventListener('click', close); @@ -831,7 +831,7 @@ accEl.value = (accEl.value || '').replace(/[^\d]/g, ''); }); - // ✅ 금융권 선택 → 은행 목록 갱신 + // 금융권 선택 → 은행 목록 갱신 groupEl?.addEventListener('change', () => { const g = (groupEl.value || '').trim(); bankEl.innerHTML = bankOptionsByGroupHtml(g); @@ -853,7 +853,7 @@ 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; } @@ -980,7 +980,7 @@ } function replaceWithClone(el) { - // ✅ 기존 mypage_renew.js에서 "준비중" 리스너가 이미 붙어있으므로 + // 기존 mypage_renew.js에서 "준비중" 리스너가 이미 붙어있으므로 // 버튼을 clone으로 교체해서 리스너를 모두 제거한다. const clone = el.cloneNode(true); el.parentNode.replaceChild(clone, el); @@ -1184,7 +1184,7 @@ const btn0 = document.querySelector('[data-action="consent-edit"]'); if (!btn0) return; - // ✅ 기존 “준비중” 클릭 리스너 제거 + // 기존 “준비중” 클릭 리스너 제거 const btn = replaceWithClone(btn0); btn.addEventListener('click', () => { diff --git a/resources/css/admin.css b/resources/css/admin.css index 71cb808..72365e6 100644 --- a/resources/css/admin.css +++ b/resources/css/admin.css @@ -220,8 +220,8 @@ html,body{ height:100%; } border: 1px solid rgba(0,0,0,.12); padding: 12px 12px; margin: 0 0 12px; - background: #fff; /* ✅ 흰색 배경 */ - color: #111; /* ✅ 기본 검정 */ + background: #fff; /* 흰색 배경 */ + color: #111; /* 기본 검정 */ } .a-alert__title{ font-weight:900; margin-bottom:4px; font-size:13px; color:#111; } .a-alert__body{ font-size:13px; color:#111; } @@ -230,7 +230,7 @@ html,body{ height:100%; } } .a-alert--danger .a-alert__title, .a-alert--danger .a-alert__body{ - color: #d32f2f; /* ✅ 실패 붉은색 */ + color: #d32f2f; /* 실패 붉은색 */ } .a-alert--warn{ border-color: rgba(255,176,32,.28); background: rgba(255,176,32,.10); } .a-alert--info{ border-color: rgba(43,127,255,.28); background: rgba(43,127,255,.10); } @@ -671,7 +671,7 @@ html,body{ height:100%; } .a-toast{ border-radius: 14px; border: 1px solid rgba(0,0,0,.12); - background: #fff; /* ✅ 흰색 배경 */ + background: #fff; /* 흰색 배경 */ box-shadow: 0 12px 30px rgba(0,0,0,.18); padding: 12px 12px; animation: aToastIn .16s ease-out; @@ -686,19 +686,19 @@ html,body{ height:100%; } .a-toast__msg{ font-size: 13px; line-height: 1.35; - color: #111; /* ✅ 성공은 검정 */ + color: #111; /* 성공은 검정 */ } -/* ✅ 성공: 검정 유지 */ +/* 성공: 검정 유지 */ .a-toast--success{} -/* ✅ 실패(오류): 붉은색 */ +/* 실패(오류): 붉은색 */ .a-toast--danger{ border-color: rgba(255,77,79,.45); } .a-toast--danger .a-toast__title, .a-toast--danger .a-toast__msg{ - color: #d32f2f; /* ✅ 붉은색 */ + color: #d32f2f; /* 붉은색 */ } /* (선택) warn/info도 보기 좋게 */ @@ -713,7 +713,7 @@ html,body{ height:100%; } /* ===== Highlight button (Password Change) ===== */ .a-btn--highlight{ border: 1px solid rgba(43,127,255,.35); - background: #2b7fff; /* ✅ 단색 */ + background: #2b7fff; /* 단색 */ color: #fff; box-shadow: 0 14px 34px rgba(43,127,255,.18); } @@ -721,9 +721,9 @@ html,body{ height:100%; } .a-btn--highlight:active{ transform: translateY(1px); } .a-btn{ - display: block; /* ✅ a 태그도 폭/레이아웃 적용 */ + display: block; /* a 태그도 폭/레이아웃 적용 */ width:100%; - text-align:center; /* ✅ a 태그 텍스트 가운데 */ + text-align:center; /* a 태그 텍스트 가운데 */ border-radius: var(--a-radius-sm); border:1px solid rgba(255,255,255,.14); padding:12px 14px; @@ -758,7 +758,7 @@ html,body{ height:100%; } } .a-meinfo__v{ - min-width: 0; /* ✅ 긴 텍스트/칩 overflow 방지 */ + min-width: 0; /* 긴 텍스트/칩 overflow 방지 */ color: rgba(255,255,255,.92); } @@ -1200,7 +1200,7 @@ html,body{ height:100%; } cursor: pointer; } -/* ✅ 칩/원형/캡슐 제거: 텍스트는 그냥 텍스트 */ +/* 칩/원형/캡슐 제거: 텍스트는 그냥 텍스트 */ .a-nav__titletext{ display: inline-block; @@ -1223,7 +1223,7 @@ html,body{ height:100%; } color: rgba(255,255,255,.88); } -/* ✅ selected(open): 버튼 "전체"를 아이템보다 더 강하게 강조 */ +/* selected(open): 버튼 "전체"를 아이템보다 더 강하게 강조 */ .a-nav__group.is-open .a-nav__titlebtn{ background: linear-gradient( 135deg, @@ -1241,8 +1241,8 @@ html,body{ height:100%; } /* --- Items: smaller + indent + guide line --- */ .a-nav__items{ margin-left: 8px; /* 그룹 자체 살짝 들여쓰기 */ - padding-left: 14px; /* ✅ 아이템 들여쓰기 */ - border-left: 1px solid rgba(255,255,255,.08); /* ✅ 가이드 라인 */ + padding-left: 14px; /* 아이템 들여쓰기 */ + border-left: 1px solid rgba(255,255,255,.08); /* 가이드 라인 */ } /* item size + spacing */ @@ -1250,7 +1250,7 @@ html,body{ height:100%; } padding: 9px 10px; border-radius: 12px; - font-size: 12.5px; /* ✅ 글씨 크기 줄임 */ + font-size: 12.5px; /* 글씨 크기 줄임 */ line-height: 1.2; color: rgba(255,255,255,.82); diff --git a/resources/css/web.css b/resources/css/web.css index cff8f14..44f8445 100644 --- a/resources/css/web.css +++ b/resources/css/web.css @@ -565,7 +565,7 @@ body { } .original-price{ - margin-left: auto; /* ✅ 여기서 오른쪽으로 밀어줌 */ + margin-left: auto; /* 여기서 오른쪽으로 밀어줌 */ text-decoration: line-through; color: var(--color-text-tertiary); font-size: 13px; @@ -821,7 +821,7 @@ body { margin-right: 8px; /* 여백 줄이기 */ } - /* ✅ 검색폼 거의 풀폭 */ + /* 검색폼 거의 풀폭 */ .search-bar{ margin: 0 !important; /* 양쪽 여백 제거 */ flex: 1 1 auto; @@ -1051,7 +1051,7 @@ body { background:#FFFFFF; border: 1px solid #E5E7EB; border-radius: 16px; - padding: 12px 14px; /* ✅ 여백 축소 */ + padding: 12px 14px; /* 여백 축소 */ box-shadow: 0 1px 2px rgba(15,23,42,.04); } @@ -1060,7 +1060,7 @@ body { align-items:center; justify-content:space-between; gap: 10px; - margin-bottom: 6px; /* ✅ 타이트 */ + margin-bottom: 6px; /* 타이트 */ } .notice-title{ @@ -1084,7 +1084,7 @@ body { display:grid; } -/* ✅ 공지 사이 얇은 줄 */ +/* 공지 사이 얇은 줄 */ .notice-item + .notice-item{ border-top: 1px solid #EEF2F7; } @@ -1094,7 +1094,7 @@ body { grid-template-columns: 44px 1fr auto; align-items:center; gap: 10px; - padding: 10px 2px; /* ✅ 줄간격/여백 축소 */ + padding: 10px 2px; /* 줄간격/여백 축소 */ border-radius: 10px; transition: background-color .16s ease; } @@ -1146,7 +1146,7 @@ body { overflow: hidden; border-radius: 16px; border: 1px solid #E5E7EB; - padding: 14px 14px; /* ✅ 타이트 */ + padding: 14px 14px; /* 타이트 */ background: #FFFFFF; box-shadow: 0 1px 2px rgba(15,23,42,.04); transition: transform .16s ease, border-color .16s ease; @@ -1156,7 +1156,7 @@ body { border-color: #BFDBFE; } -/* ✅ “배경 이미지 느낌” (그라데이션 없이, 패턴/하이라이트) */ +/* “배경 이미지 느낌” (그라데이션 없이, 패턴/하이라이트) */ .support-banner::before{ content:""; position:absolute; @@ -1440,7 +1440,7 @@ body { border-radius: 16px; border: 1px solid #E5E7EB; background: #F8FAFC; - height: 160px; /* ✅ 메인보다 낮게 */ + height: 160px; /* 메인보다 낮게 */ } .subhero-track{ display:flex; @@ -1458,7 +1458,7 @@ body { .subhero-slide::after{ content:""; position:absolute; inset:0; - background: rgba(255,255,255,.72); /* ✅ 과하지 않게 텍스트 가독 */ + background: rgba(255,255,255,.72); /* 과하지 않게 텍스트 가독 */ } .subhero-content{ position: relative; @@ -1656,7 +1656,7 @@ body { .hero-content{ max-width: var(--container-width); width:100%; text-align:left; padding: 0 8px; position: relative; z-index: 1;} -/* ✅ compact에서 타이포/여백만 줄이기 */ +/* compact에서 타이포/여백만 줄이기 */ .hero-slider--compact .hero-slide{ padding: 0 18px; } .hero-slider--compact .hero-title{ font-size: 22px; margin-bottom: 6px; } .hero-slider--compact .hero-desc{ font-size: 13px; margin-bottom: 10px; } @@ -1676,7 +1676,7 @@ body { background-image: var(--hero-bg-mobile, var(--hero-bg)); } } -/* ✅ 텍스트 가독성 오버레이(왼쪽은 밝게, 오른쪽은 투명) */ +/* 텍스트 가독성 오버레이(왼쪽은 밝게, 오른쪽은 투명) */ .hero-slide::after{ content:""; position:absolute; @@ -1701,7 +1701,7 @@ body { } -/* ✅ 왼쪽/오른쪽 화살표 버튼이 컨텐츠를 가리지 않게 여백 확보 */ +/* 왼쪽/오른쪽 화살표 버튼이 컨텐츠를 가리지 않게 여백 확보 */ .slider-arrow{ position:absolute; top:50%; transform: translateY(-50%); width: 34px; height: 34px; @@ -1715,7 +1715,7 @@ body { .slider-arrow.prev{ left: 10px; } .slider-arrow.next{ right: 10px; } -/* ✅ 컨텐츠 좌우 패딩(화살표 영역만큼) */ +/* 컨텐츠 좌우 패딩(화살표 영역만큼) */ .hero-slider--compact .hero-content{ padding-left: 44px; padding-right: 44px; } /* dots */ @@ -1821,7 +1821,7 @@ body { background: rgba(0,0,0,.08); } -/* ✅ 박스 자체 */ +/* 박스 자체 */ .subnav--side .subnav-box{ background: #fff; border: 1px solid rgba(0,0,0,.08); @@ -2252,7 +2252,7 @@ body { color: rgba(0,0,0,.6); } -/* ✅ Policy tables: mobile overflow fix */ +/* Policy tables: mobile overflow fix */ .policy-table-wrap{ width:100%; overflow-x:auto; @@ -2319,7 +2319,7 @@ body { font-weight:800; } -/* ✅ 모바일: 행을 카드처럼 쌓아서 “넘침” 자체를 제거 */ +/* 모바일: 행을 카드처럼 쌓아서 “넘침” 자체를 제거 */ @media (max-width: 640px){ /* 래퍼 스크롤 제거 + 카드 형태로 */ .policy-table-wrap{ @@ -2453,11 +2453,11 @@ img, video, svg, iframe, canvas { max-width: 100%; height: auto; } /* 탭(메뉴) 버튼 */ .subnav-tab{ display: flex; - align-items: center; /* ✅ 세로 중앙 */ - justify-content: center; /* ✅ 가로 중앙 */ + align-items: center; /* 세로 중앙 */ + justify-content: center; /* 가로 중앙 */ text-align: center; - min-width: 0; /* ✅ grid item overflow 방지 */ + min-width: 0; /* grid item overflow 방지 */ min-height: 42px; padding: 10px 10px; @@ -2473,12 +2473,12 @@ img, video, svg, iframe, canvas { max-width: 100%; height: auto; } color: rgba(0,0,0,.78); text-decoration: none; - /* ✅ 한 줄 유지 + 길면 ... */ + /* 한 줄 유지 + 길면 ... */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - /* ✅ '메뉴'같은 클릭감 */ + /* '메뉴'같은 클릭감 */ transition: background-color .12s ease, border-color .12s ease, color .12s ease, transform .06s ease; } @@ -2524,13 +2524,13 @@ img, video, svg, iframe, canvas { max-width: 100%; height: auto; } color: rgba(0,0,0,.65); line-height:1.6; - /* ✅ 데스크톱에서는 한 줄에 잘리게(캡처처럼) */ + /* 데스크톱에서는 한 줄에 잘리게(캡처처럼) */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -/* ✅ 모바일: desc를 아래로 떨어뜨리기 */ +/* 모바일: desc를 아래로 떨어뜨리기 */ @media (max-width: 640px){ .subpage-header{ flex-direction: column; @@ -3003,8 +3003,8 @@ body.is-drawer-open{ .faq-cats{ display:flex; - flex-wrap:wrap; /* ✅ 넘치면 줄바꿈 */ - justify-content:center; /* ✅ 중앙정렬 */ + flex-wrap:wrap; /* 넘치면 줄바꿈 */ + justify-content:center; /* 중앙정렬 */ gap:8px; } @@ -3333,7 +3333,7 @@ body.is-drawer-open{ min-height:100dvh; } -/* ✅ 중앙 박스형 리듬 */ +/* 중앙 박스형 리듬 */ .auth-page{ padding: 40px 16px 72px; } @@ -3464,7 +3464,7 @@ body.is-drawer-open{ } .auth-check input{ transform: translateY(1px); } -/* ✅ 링크 전용 줄 */ +/* 링크 전용 줄 */ .auth-links-inline{ display:flex; align-items:center; @@ -3640,7 +3640,7 @@ body.is-drawer-open{ font-weight: 900; } -/* ✅ Auth: Desktop에서만 +150px 넓게 */ +/* Auth: Desktop에서만 +150px 넓게 */ @media (min-width: 961px){ .auth-container{ max-width: 410px !important; /* (기존 560px 기준 +150) */ @@ -3663,7 +3663,7 @@ body.is-drawer-open{ } } -/* ✅ Mobile은 기존 유지(혹시 덮였으면 안전하게) */ +/* Mobile은 기존 유지(혹시 덮였으면 안전하게) */ @media (max-width: 960px){ .auth-container{ max-width: 100% !important; @@ -3673,9 +3673,9 @@ body.is-drawer-open{ /* ===== Pagination (badge / pill) ===== */ .notice-pager, .pagination-wrap { - margin-top: 22px; /* ✅ 상단 여백 */ + margin-top: 22px; /* 상단 여백 */ display: flex; - justify-content: center; /* ✅ 가운데 정렬 */ + justify-content: center; /* 가운데 정렬 */ } .pg{ @@ -3683,7 +3683,7 @@ body.is-drawer-open{ align-items:center; justify-content:center; gap:8px; - padding: 0; /* ✅ 박스 제거 */ + padding: 0; /* 박스 제거 */ border: 0; background: transparent; } @@ -3700,7 +3700,7 @@ body.is-drawer-open{ .pg-ellipsis{ height: 34px; padding: 0 12px; - border-radius: 999px; /* ✅ pill */ + border-radius: 999px; /* pill */ display:inline-flex; align-items:center; justify-content:center; @@ -3810,14 +3810,14 @@ body.is-drawer-open{ position: absolute; right: 0; top: 100%; /* 버튼 바로 아래에 붙임 */ - margin-top: 0; /* ✅ gap 제거 */ + margin-top: 0; /* gap 제거 */ width: 192px; display: none; z-index: 50; } .profile-dropdown .profile-card { - margin-top: 10px; /* ✅ 시각적 간격은 카드에만 */ + margin-top: 10px; /* 시각적 간격은 카드에만 */ } /* show on hover (group-hover:block replacement) */ diff --git a/resources/css/web.css.backup b/resources/css/web.css.backup index bd7dfe4..7751772 100644 --- a/resources/css/web.css.backup +++ b/resources/css/web.css.backup @@ -556,7 +556,7 @@ h1, h2, h3, h4, h5, h6 { } .original-price{ - margin-left: auto; /* ✅ 여기서 오른쪽으로 밀어줌 */ + margin-left: auto; /* 여기서 오른쪽으로 밀어줌 */ text-decoration: line-through; color: var(--color-text-tertiary); font-size: 13px; @@ -812,7 +812,7 @@ h1, h2, h3, h4, h5, h6 { margin-right: 8px; /* 여백 줄이기 */ } - /* ✅ 검색폼 거의 풀폭 */ + /* 검색폼 거의 풀폭 */ .search-bar{ margin: 0 !important; /* 양쪽 여백 제거 */ flex: 1 1 auto; @@ -1042,7 +1042,7 @@ h1, h2, h3, h4, h5, h6 { background:#FFFFFF; border: 1px solid #E5E7EB; border-radius: 16px; - padding: 12px 14px; /* ✅ 여백 축소 */ + padding: 12px 14px; /* 여백 축소 */ box-shadow: 0 1px 2px rgba(15,23,42,.04); } @@ -1051,7 +1051,7 @@ h1, h2, h3, h4, h5, h6 { align-items:center; justify-content:space-between; gap: 10px; - margin-bottom: 6px; /* ✅ 타이트 */ + margin-bottom: 6px; /* 타이트 */ } .notice-title{ @@ -1075,7 +1075,7 @@ h1, h2, h3, h4, h5, h6 { display:grid; } -/* ✅ 공지 사이 얇은 줄 */ +/* 공지 사이 얇은 줄 */ .notice-item + .notice-item{ border-top: 1px solid #EEF2F7; } @@ -1085,7 +1085,7 @@ h1, h2, h3, h4, h5, h6 { grid-template-columns: 44px 1fr auto; align-items:center; gap: 10px; - padding: 10px 2px; /* ✅ 줄간격/여백 축소 */ + padding: 10px 2px; /* 줄간격/여백 축소 */ border-radius: 10px; transition: background-color .16s ease; } @@ -1137,7 +1137,7 @@ h1, h2, h3, h4, h5, h6 { overflow: hidden; border-radius: 16px; border: 1px solid #E5E7EB; - padding: 14px 14px; /* ✅ 타이트 */ + padding: 14px 14px; /* 타이트 */ background: #FFFFFF; box-shadow: 0 1px 2px rgba(15,23,42,.04); transition: transform .16s ease, border-color .16s ease; @@ -1147,7 +1147,7 @@ h1, h2, h3, h4, h5, h6 { border-color: #BFDBFE; } -/* ✅ “배경 이미지 느낌” (그라데이션 없이, 패턴/하이라이트) */ +/* “배경 이미지 느낌” (그라데이션 없이, 패턴/하이라이트) */ .support-banner::before{ content:""; position:absolute; @@ -1430,7 +1430,7 @@ h1, h2, h3, h4, h5, h6 { border-radius: 16px; border: 1px solid #E5E7EB; background: #F8FAFC; - height: 160px; /* ✅ 메인보다 낮게 */ + height: 160px; /* 메인보다 낮게 */ } .subhero-track{ display:flex; @@ -1448,7 +1448,7 @@ h1, h2, h3, h4, h5, h6 { .subhero-slide::after{ content:""; position:absolute; inset:0; - background: rgba(255,255,255,.72); /* ✅ 과하지 않게 텍스트 가독 */ + background: rgba(255,255,255,.72); /* 과하지 않게 텍스트 가독 */ } .subhero-content{ position: relative; @@ -1646,13 +1646,13 @@ h1, h2, h3, h4, h5, h6 { .hero-content{ max-width: var(--container-width); width:100%; text-align:left; padding: 0 8px; } -/* ✅ compact에서 타이포/여백만 줄이기 */ +/* compact에서 타이포/여백만 줄이기 */ .hero-slider--compact .hero-slide{ padding: 0 18px; } .hero-slider--compact .hero-title{ font-size: 22px; margin-bottom: 6px; } .hero-slider--compact .hero-desc{ font-size: 13px; margin-bottom: 10px; } .hero-slider--compact .btn.hero-cta{ padding: 8px 14px; font-size: 13px; } -/* ✅ 왼쪽/오른쪽 화살표 버튼이 컨텐츠를 가리지 않게 여백 확보 */ +/* 왼쪽/오른쪽 화살표 버튼이 컨텐츠를 가리지 않게 여백 확보 */ .slider-arrow{ position:absolute; top:50%; transform: translateY(-50%); width: 34px; height: 34px; @@ -1666,7 +1666,7 @@ h1, h2, h3, h4, h5, h6 { .slider-arrow.prev{ left: 10px; } .slider-arrow.next{ right: 10px; } -/* ✅ 컨텐츠 좌우 패딩(화살표 영역만큼) */ +/* 컨텐츠 좌우 패딩(화살표 영역만큼) */ .hero-slider--compact .hero-content{ padding-left: 44px; padding-right: 44px; } /* dots */ @@ -1772,7 +1772,7 @@ h1, h2, h3, h4, h5, h6 { background: rgba(0,0,0,.08); } -/* ✅ 박스 자체 */ +/* 박스 자체 */ .subnav--side .subnav-box{ background: #fff; border: 1px solid rgba(0,0,0,.08); @@ -2203,7 +2203,7 @@ h1, h2, h3, h4, h5, h6 { color: rgba(0,0,0,.6); } -/* ✅ Policy tables: mobile overflow fix */ +/* Policy tables: mobile overflow fix */ .policy-table-wrap{ width:100%; overflow-x:auto; @@ -2280,7 +2280,7 @@ h1, h2, h3, h4, h5, h6 { font-weight:800; } -/* ✅ 모바일: 행을 카드처럼 쌓아서 “넘침” 자체를 제거 */ +/* 모바일: 행을 카드처럼 쌓아서 “넘침” 자체를 제거 */ @media (max-width: 640px){ /* 래퍼 스크롤 제거 + 카드 형태로 */ .policy-table-wrap{ diff --git a/resources/css/web/mypage.css b/resources/css/web/mypage.css index 6d59c8e..a0d5fee 100644 --- a/resources/css/web/mypage.css +++ b/resources/css/web/mypage.css @@ -3,14 +3,14 @@ 적용 범위: body.is-mypage 내부만 ========================================= */ -/* ✅ 래퍼: 가운데 정렬 + 여백 */ +/* 래퍼: 가운데 정렬 + 여백 */ .is-mypage .mypage-gate-wrap{ display:flex; justify-content:center; padding: 6px 0 18px; } -/* ✅ 카드 */ +/* 카드 */ .is-mypage .mypage-gate-card{ width:100%; max-width: 680px; @@ -23,12 +23,12 @@ margin-bottom: 50px; } -/* ✅ 바디 */ +/* 바디 */ .is-mypage .mypage-gate-body{ padding: 28px; } -/* ✅ 타이틀/설명 */ +/* 타이틀/설명 */ .is-mypage .mypage-gate-title{ font-size: 20px; font-weight: 800; @@ -43,7 +43,7 @@ line-height: 1.45; } -/* ✅ 안내 박스 */ +/* 안내 박스 */ .is-mypage .mypage-gate-note{ margin: 0 0 18px; padding: 12px 14px; @@ -55,7 +55,7 @@ line-height: 1.5; } -/* ✅ 한 줄 입력 + 버튼 */ +/* 한 줄 입력 + 버튼 */ .is-mypage .mypage-gate-row{ display:flex; gap: 10px; @@ -75,7 +75,7 @@ box-shadow: 0 0 0 4px rgba(255,122,0,.14); } -/* ✅ 버튼 */ +/* 버튼 */ .is-mypage .mypage-gate-btn{ height: 46px; padding: 0 18px; @@ -100,8 +100,8 @@ height: 52px; min-height: 52px; padding: 0 16px; - font-size: 16px; /* ✅ 모바일에서 얇아보이는/줌 이슈 방지 */ - line-height: 52px; /* ✅ 세로 가운데 정렬 확실 */ + font-size: 16px; /* 모바일에서 얇아보이는/줌 이슈 방지 */ + line-height: 52px; /* 세로 가운데 정렬 확실 */ border-radius: 14px; border: 1px solid #d0d5dd; box-sizing: border-box; @@ -121,7 +121,7 @@ min-height: 120px; } -/* ✅ 모바일: 버튼 아래로 */ +/* 모바일: 버튼 아래로 */ @media (max-width: 575.98px){ .is-mypage .mypage-gate-card{ margin-top: 0px; diff --git a/resources/js/admin.js b/resources/js/admin.js index 22bbd77..6aff821 100644 --- a/resources/js/admin.js +++ b/resources/js/admin.js @@ -1,5 +1,5 @@ (function () { - // ✅ 스크립트가 실수로 2번 로드돼도 바인딩 1번만 + // 스크립트가 실수로 2번 로드돼도 바인딩 1번만 if (window.__ADMIN_UI_JS_BOUND__) return; window.__ADMIN_UI_JS_BOUND__ = true; @@ -22,7 +22,7 @@ }); // ----------------------- - // ✅ data-confirm (ONLY ONCE) + // data-confirm (ONLY ONCE) // - capture 단계에서 먼저 실행되어 // 취소 시 다른 submit 리스너(버튼 disable 등) 실행 안 됨 // ----------------------- diff --git a/resources/js/ui/dialog.js b/resources/js/ui/dialog.js index 1c8505a..a2b6557 100644 --- a/resources/js/ui/dialog.js +++ b/resources/js/ui/dialog.js @@ -3,7 +3,7 @@ class UiDialog { this.root = document.getElementById(rootId); if (!this.root) return; - // ✅ stacking context 문제 방지: 무조건 body 직속으로 이동 + // stacking context 문제 방지: 무조건 body 직속으로 이동 try { if (this.root.parentElement !== document.body) { document.body.appendChild(this.root); @@ -52,7 +52,7 @@ class UiDialog { this.cancelBtn?.addEventListener("click", () => this._resolve(false)); } - // ✅ 현재 페이지 최상단 z-index 탐색 (모달 겹침 대응) + // 현재 페이지 최상단 z-index 탐색 (모달 겹침 대응) static getTopZIndex() { let maxZ = 0; const nodes = document.querySelectorAll("body *"); @@ -87,14 +87,14 @@ class UiDialog { cancelText = "취소", dangerous = false, - // ✅ 기본: 밖 클릭/닫기(X)/ESC로 닫기 금지 + // 기본: 밖 클릭/닫기(X)/ESC로 닫기 금지 closeOnBackdrop = false, closeOnX = false, closeOnEsc = false, closeOnEnter = true, } = options; - // ✅ 항상 최상단: DOM 마지막 + z-index 최상단 + // 항상 최상단: DOM 마지막 + z-index 최상단 try { document.body.appendChild(this.root); const topZ = UiDialog.getTopZIndex(); @@ -166,7 +166,7 @@ window.uiDialog = new UiDialog(); // ====================================================== -// ✅ Global showMsg / clearMsg (공통 사용) +// Global showMsg / clearMsg (공통 사용) // ====================================================== (function () { let cachedHelpEl = null; @@ -190,7 +190,7 @@ window.uiDialog = new UiDialog(); dangerous: false, redirect: "", helpId: "", - // ✅ 닫기 정책 기본값(너가 말한 “다른 공간 클릭해도 닫히지 않게”와 일치) + // 닫기 정책 기본값(너가 말한 “다른 공간 클릭해도 닫히지 않게”와 일치) closeOnBackdrop: false, closeOnX: false, closeOnEsc: false, diff --git a/resources/views/admin/auth/login.blade.php b/resources/views/admin/auth/login.blade.php index be809a0..1a59a36 100644 --- a/resources/views/admin/auth/login.blade.php +++ b/resources/views/admin/auth/login.blade.php @@ -2,7 +2,7 @@ @section('hide_flash', '1') @section('title', '로그인') -{{-- ✅ reCAPTCHA 스크립트는 이 페이지에서만 로드 --}} +{{-- reCAPTCHA 스크립트는 이 페이지에서만 로드 --}} @push('head') @php $siteKey = (string) config('services.recaptcha.site_key'); @@ -20,7 +20,7 @@ @csrf - {{-- ✅ 에러는 폼 상단에 1개만 --}} + {{-- 에러는 폼 상단에 1개만 --}} @if ($errors->any())
로그인 실패
@@ -131,7 +131,7 @@ const isProd = @json(app()->environment('production')); const hasKey = @json((bool) config('services.recaptcha.site_key')); - // ✅ 운영에서만 토큰 생성 (서버와 동일 정책) + // 운영에서만 토큰 생성 (서버와 동일 정책) if (isProd && hasKey) { const hidden = ensureHiddenRecaptcha(); hidden.value = ''; diff --git a/resources/views/admin/log/MemberJoinLogController.blade.php b/resources/views/admin/log/MemberJoinLogController.blade.php index d67be1c..e437e25 100644 --- a/resources/views/admin/log/MemberJoinLogController.blade.php +++ b/resources/views/admin/log/MemberJoinLogController.blade.php @@ -34,7 +34,7 @@ .badge--muted{opacity:.9;} .badge--bad{border-color:rgba(244,63,94,.35);background:rgba(244,63,94,.10);} - /* ✅ 회원번호 링크 색상 흰색 */ + /* 회원번호 링크 색상 흰색 */ a.memlink{color:#fff;text-decoration:none;} a.memlink:hover{color:#fff;text-decoration:underline;} @@ -153,7 +153,7 @@ @endif - {{-- ✅ 전화/이메일 한줄: "전화번호 바로뒤 이메일" (이메일 없으면 빈값) --}} + {{-- 전화/이메일 한줄: "전화번호 바로뒤 이메일" (이메일 없으면 빈값) --}}
{{ $r['corp_label'] ?? '-' }} @@ -164,7 +164,7 @@
- {{-- ✅ IP 한줄: ip4 + ip4_c --}} + {{-- IP 한줄: ip4 + ip4_c --}} {{ $ip4v !== '' ? $ip4v : '-' }} @if($ip4cv !== '') diff --git a/resources/views/admin/log/MemberLoginLogController.blade.php b/resources/views/admin/log/MemberLoginLogController.blade.php index bf7c3c5..6721092 100644 --- a/resources/views/admin/log/MemberLoginLogController.blade.php +++ b/resources/views/admin/log/MemberLoginLogController.blade.php @@ -240,7 +240,7 @@ if (!sel || !frm) return; sel.addEventListener('change', () => { - // ✅ 선택 즉시 이동(= GET submit), page 파라미터는 자동 리셋 + // 선택 즉시 이동(= GET submit), page 파라미터는 자동 리셋 frm.submit(); }); })(); diff --git a/resources/views/admin/mail/send.blade.php b/resources/views/admin/mail/send.blade.php index cf4ac80..383f24f 100644 --- a/resources/views/admin/mail/send.blade.php +++ b/resources/views/admin/mail/send.blade.php @@ -94,13 +94,13 @@ {{-- mode --}} - {{-- ✅ 여러건 파싱 결과(JSON) 서버 전달용 --}} + {{-- 여러건 파싱 결과(JSON) 서버 전달용 --}} - {{-- ✅ 토큰 시작 컬럼: 2열부터 {_text_02_} (서버에서 참고용) --}} + {{-- 토큰 시작 컬럼: 2열부터 {_text_02_} (서버에서 참고용) --}} - {{-- ✅ subject/body 미러 (백엔드 키 불일치 대비) --}} + {{-- subject/body 미러 (백엔드 키 불일치 대비) --}} @@ -121,7 +121,7 @@ - {{-- ✅ 직접 입력 불가(실수 방지): readonly + 선택 버튼 --}} + {{-- 직접 입력 불가(실수 방지): readonly + 선택 버튼 --}} 수신자 (여러명)
+ placeholder=" 1줄 = 1명 (줄바꿈 기준) 1열: 이메일, 2열~: 토큰(콤마로 구분) 예) sungro81@gmail.com, 이상도, 10000, 쿠폰">
- 예: sungro81@gmail.com, 홍길동, 10000, 쿠폰
@@ -175,7 +175,7 @@
- {{-- ✅ 파싱 미리보기 --}} + {{-- 파싱 미리보기 --}}
파싱 미리보기 (상위 5줄)
@@ -253,7 +253,7 @@
- {{-- ✅ 템플릿 선택값 서버로도 보내기 (선택사항이지만 디버깅/로그에 도움됨) --}} + {{-- 템플릿 선택값 서버로도 보내기 (선택사항이지만 디버깅/로그에 도움됨) --}} - {{-- ✅ join_block != S 일 때 disabled된 체크값을 hidden으로 보강 제출 --}} + {{-- join_block != S 일 때 disabled된 체크값을 hidden으로 보강 제출 --}}
- {{-- ✅ 서버 에러가 있으면 모달 안에서 바로 보이게 --}} + {{-- 서버 에러가 있으면 모달 안에서 바로 보이게 --}} @if($errors->any())
저장 실패
@@ -508,7 +508,7 @@ submitBtn.disabled = true; }); - // ✅ edit -> AJAX get + // edit -> AJAX get Array.from(document.querySelectorAll('.btnEdit')).forEach(btn => { btn.addEventListener('click', async () => { const seq = btn.getAttribute('data-seq'); @@ -537,7 +537,7 @@ const payload = await res.json(); - // ✅ 컨트롤러 응답이 {ok,row} 인데 row를 직접 쓰던게 버그였음 + // 컨트롤러 응답이 {ok,row} 인데 row를 직접 쓰던게 버그였음 const row = payload?.row ?? payload; if (!row) throw new Error('NOT_FOUND'); @@ -569,9 +569,9 @@ // default pickJoinBlock('S'); - // ✅ 서버 validation 에러가 있으면 모달 다시 열어서 입력값 복구 + // 서버 validation 에러가 있으면 모달 다시 열어서 입력값 복구 const hasErrors = @json($errors->any()); - const oldPayload = @js($oldPayload); // ✅ ParseError 방지 + JS 안전 변환 + const oldPayload = @js($oldPayload); // ParseError 방지 + JS 안전 변환 if (hasErrors) { const seq = String(oldPayload.filter_seq || ''); diff --git a/resources/views/admin/members/marketing.blade.php b/resources/views/admin/members/marketing.blade.php index e76f2c3..29f712f 100644 --- a/resources/views/admin/members/marketing.blade.php +++ b/resources/views/admin/members/marketing.blade.php @@ -60,7 +60,7 @@ $regFrom = (string)($f['reg_from'] ?? ''); $regTo = (string)($f['reg_to'] ?? ''); - // ✅ 로그인 필터: mode + days (둘 중 하나만) + // 로그인 필터: mode + days (둘 중 하나만) $loginMode = (string)($f['login_mode'] ?? 'none'); // none|inactive|recent $loginDays = (string)($f['login_days'] ?? ''); @@ -382,7 +382,7 @@ @push('scripts') @@ -88,7 +88,7 @@ const $phone = document.getElementById('fi_phone'); const $code = document.getElementById('fi_code'); - // ✅ 결과 박스는 id로 고정 (절대 흔들리지 않음) + // 결과 박스는 id로 고정 (절대 흔들리지 않음) const resultBox = document.getElementById('findIdResult'); // 메시지 영역: 항상 "현재 활성 패널"의 actions 위로 이동/생성 @@ -119,7 +119,7 @@ }; const render = () => { - // ✅ 1) 전환 전에 현재 포커스 제거 (경고 원인 제거) + // 1) 전환 전에 현재 포커스 제거 (경고 원인 제거) const activeEl = document.activeElement; if (activeEl && root.contains(activeEl)) { activeEl.blur(); @@ -131,7 +131,7 @@ p.classList.toggle('is-active', on); p.style.display = on ? 'block' : 'none'; - // ✅ 2) aria-hidden은 유지하되, 포커스/클릭 차단은 inert로 처리 + // 2) aria-hidden은 유지하되, 포커스/클릭 차단은 inert로 처리 // on=false인 패널은 inert 적용(포커스 못 감) if (!on) { p.setAttribute('aria-hidden', 'true'); @@ -149,7 +149,7 @@ mkMsg(); - // ✅ 3) 전환 후 포커스 이동(접근성/UX) + // 3) 전환 후 포커스 이동(접근성/UX) // 현재 step 패널의 첫 input 또는 버튼으로 포커스 const target = root.querySelector(`.auth-panel[data-step="${step}"] input, .auth-panel[data-step="${step}"] button, .auth-panel[data-step="${step}"] a`); target?.focus?.(); @@ -185,7 +185,7 @@ const postJson = async (url, data) => { const res = await fetch(url, { method: 'POST', - credentials: 'same-origin', // ✅ include 대신 same-origin 권장(같은 도메인일 때) + credentials: 'same-origin', // include 대신 same-origin 권장(같은 도메인일 때) headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf(), @@ -196,7 +196,7 @@ }); const ct = res.headers.get('content-type') || ''; - const raw = await res.text(); // ✅ 먼저 text로 받는다 + const raw = await res.text(); // 먼저 text로 받는다 let json = null; if (ct.includes('application/json')) { @@ -297,13 +297,13 @@ setMsg('확인 중입니다...', 'info'); try { - const token = await getRecaptchaToken('find_id'); // ✅ 컨트롤러 Rule action과 동일 + const token = await getRecaptchaToken('find_id'); // 컨트롤러 Rule action과 동일 const json = await postJson(@json(route('web.auth.find_id.send_code')), { phone: raw, 'g-recaptcha-response': token, }); - // ✅ 성공 (ok true) + // 성공 (ok true) setMsg(json.message || '인증번호를 발송했습니다.', 'success'); step = 2; @@ -316,7 +316,7 @@ // } } catch (err) { - // ✅ 여기서 404(PHONE_NOT_FOUND)도 UX로 처리 + // 여기서 404(PHONE_NOT_FOUND)도 UX로 처리 const p = err.payload || {}; if (err.status === 404 && p.code === 'PHONE_NOT_FOUND') { @@ -350,11 +350,11 @@ }); - // ✅ 먼저 step 이동 + 렌더 (패널 표시 보장) + // 먼저 step 이동 + 렌더 (패널 표시 보장) step = 3; render(); - // ✅ 결과 반영은 렌더 후 + // 결과 반영은 렌더 후 const maskedList = Array.isArray(json.masked_emails) ? json.masked_emails : []; if (resultBox) { if (maskedList.length > 0) { diff --git a/resources/views/web/auth/find_password.blade.php b/resources/views/web/auth/find_password.blade.php index 9f42287..f4c647d 100644 --- a/resources/views/web/auth/find_password.blade.php +++ b/resources/views/web/auth/find_password.blade.php @@ -9,7 +9,7 @@ @section('card_aria', '비밀번호 찾기 폼') @section('show_cs_links', true) -{{-- ✅ reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}} +{{-- reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}} @push('recaptcha') @@ -203,7 +203,7 @@ // -------- helpers ---------- const csrf = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; - // ✅ recaptcha token getter + // recaptcha token getter const getRecaptchaToken = async (action) => { // production에서만 검증하지만, 프론트는 그냥 항상 시도해도 OK const siteKey = window.__recaptchaSiteKey || ''; diff --git a/resources/views/web/auth/login.blade.php b/resources/views/web/auth/login.blade.php index 56a240b..e08c679 100644 --- a/resources/views/web/auth/login.blade.php +++ b/resources/views/web/auth/login.blade.php @@ -10,7 +10,7 @@ @section('subheadline', '로그인 후 구매/문의 내역을 빠르게 확인할 수 있어요.') @section('card_aria', '로그인 폼') -{{-- ✅ reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}} +{{-- reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}} @push('recaptcha') @@ -80,7 +80,7 @@ const form = document.getElementById('loginForm'); if (!form) return; - // ✅ 너 템플릿이 id를 뭘 쓰든 대응 (id 우선, 없으면 name으로 fallback) + // 너 템플릿이 id를 뭘 쓰든 대응 (id 우선, 없으면 name으로 fallback) const emailEl = document.getElementById('login_id') || form.querySelector('input[name="mem_email"]') @@ -179,7 +179,7 @@ // 버튼 잠금 if (btn) btn.disabled = true; try { - // ✅ 운영에서만 recaptcha 토큰 넣기 (서버도 동일 정책) + // 운영에서만 recaptcha 토큰 넣기 (서버도 동일 정책) const isProd = @json(app()->environment('production')); const hasKey = @json((bool) config('services.recaptcha.site_key')); @@ -190,7 +190,7 @@ try { const token = await getRecaptchaToken('login'); hidden.value = token || ''; - // ✅ 토큰이 비면 submit 막아야 서버 required 안 터짐 + // 토큰이 비면 submit 막아야 서버 required 안 터짐 if (!hidden.value) { if (btn) btn.disabled = false; await showMsg("보안 검증(reCAPTCHA) 토큰 생성에 실패했습니다. 새로고침 후 다시 시도해 주세요.", { type: 'alert', title: '보안검증 실패' }); @@ -198,7 +198,7 @@ try { } } - // ✅ 실제 전송 + // 실제 전송 form.submit(); } catch (err) { diff --git a/resources/views/web/auth/profile.blade.php b/resources/views/web/auth/profile.blade.php index 3ebdfa3..978f65c 100644 --- a/resources/views/web/auth/profile.blade.php +++ b/resources/views/web/auth/profile.blade.php @@ -168,7 +168,7 @@ flex:1; text-align:center; padding:10px 12px; - border-radius:999px; /* ✅ badge */ + border-radius:999px; /* badge */ font-size:12.5px; font-weight:800; letter-spacing:-0.2px; @@ -188,7 +188,7 @@ overflow:hidden; } - /* ✅ 비활성도 살짝 그라데이션 느낌(은은하게) */ + /* 비활성도 살짝 그라데이션 느낌(은은하게) */ .terms-step::before{ content:""; position:absolute; @@ -202,7 +202,7 @@ pointer-events:none; } - /* ✅ 활성: 선명한 그라데이션 */ + /* 활성: 선명한 그라데이션 */ .terms-step.is-active{ opacity:1; color:#fff; @@ -274,7 +274,7 @@ const p2 = document.getElementById('pin2'); const p2c = document.getElementById('pin2_confirmation'); - const loginHelp = document.getElementById('login_id_help'); // ✅ 그대로 사용 (blur에서만 갱신) + const loginHelp = document.getElementById('login_id_help'); // 그대로 사용 (blur에서만 갱신) const pwRuleHelp = document.getElementById('password_help'); const pwMatchHelp = document.getElementById('password_confirmation_help'); const p2MatchHelp = document.getElementById('pin2_confirmation_help'); @@ -290,7 +290,7 @@ let isSubmitting = false; function showAlert(message, title = '안내') { - // ✅ 네가 쓰는 함수명에 맞춰 호출 (없으면 alert fallback) + // 네가 쓰는 함수명에 맞춰 호출 (없으면 alert fallback) if (typeof window.showAlert === 'function') return window.showAlert(message, title); if (typeof window.showMsg === 'function') return window.showMsg(message, { type:'alert', title }); alert(`${title}\n\n${message}`); @@ -341,7 +341,7 @@ if (!pwHasLetter(s)) return `비밀번호에 영문(A-Z, a-z)을 포함해 주세요.`; if (!pwHasDigit(s)) return `비밀번호에 숫자를 포함해 주세요.`; if (!pwHasAllowedSpecial(s)) { - // ✅ 줄바꿈: CSS(pre-line) 적용돼 있으니 \n 사용 + // 줄바꿈: CSS(pre-line) 적용돼 있으니 \n 사용 return `특수문자를 입력해 주세요.\n(허용: ${ALLOWED_SPECIALS_TEXT})`; } if (pwHasDisallowedChar(s)) { @@ -436,7 +436,7 @@ // 이미 같은 값에 대해 통과한 중복체크가 있으면 skip if (duplicateOk && lastCheckedLogin === v) return true; - // ✅ 중복체크 시도 + // 중복체크 시도 try { // loginHelp는 “그대로 표시” 원칙이라 여기서 메시지 바꾸지 않음 const { res, data } = await postJson("{{ route('web.auth.register.check_login_id') }}", { login_id: v }); @@ -461,7 +461,7 @@ } } - // ✅ loginHelp는 그대로: blur에서만 업데이트 (시각적 도움용) + // loginHelp는 그대로: blur에서만 업데이트 (시각적 도움용) loginId.addEventListener('blur', async () => { const v = (loginId.value || '').trim(); @@ -491,7 +491,7 @@ } }); - // ✅ 사용자가 이메일을 수정하면 중복체크 결과 무효화 (loginHelp는 굳이 건드리지 않음) + // 사용자가 이메일을 수정하면 중복체크 결과 무효화 (loginHelp는 굳이 건드리지 않음) loginId.addEventListener('input', () => { duplicateOk = false; lastCheckedLogin = ''; @@ -503,7 +503,7 @@ el.addEventListener('input', () => validatePasswordFields(true)); }); - // ✅ 가입 버튼 눌렀을 때 “무조건 반응” + 단계별 안내 + // 가입 버튼 눌렀을 때 “무조건 반응” + 단계별 안내 form.addEventListener('submit', async (e) => { e.preventDefault(); if (isSubmitting) return; diff --git a/resources/views/web/auth/register.blade.php b/resources/views/web/auth/register.blade.php index 649a414..b2c8a5e 100644 --- a/resources/views/web/auth/register.blade.php +++ b/resources/views/web/auth/register.blade.php @@ -24,7 +24,7 @@
@csrf - {{-- ✅ hidden input만 생성(토큰은 JS에서 발급 후 payload에 포함) --}} + {{-- hidden input만 생성(토큰은 JS에서 발급 후 payload에 포함) --}}
@@ -78,7 +78,7 @@ flex:1; text-align:center; padding:10px 12px; - border-radius:999px; /* ✅ badge */ + border-radius:999px; /* badge */ font-size:12.5px; font-weight:800; letter-spacing:-0.2px; @@ -98,7 +98,7 @@ overflow:hidden; } - /* ✅ 비활성도 살짝 그라데이션 느낌(은은하게) */ + /* 비활성도 살짝 그라데이션 느낌(은은하게) */ .terms-step::before{ content:""; position:absolute; @@ -112,7 +112,7 @@ pointer-events:none; } - /* ✅ 활성: 선명한 그라데이션 */ + /* 활성: 선명한 그라데이션 */ .terms-step.is-active{ opacity:1; color:#fff; @@ -153,7 +153,7 @@ } - {{-- ✅ reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}} + {{-- reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}} @push('recaptcha') @@ -167,7 +167,7 @@ const help = document.getElementById('reg_phone_help'); const btn = document.getElementById('reg_next_btn'); - // ✅ 통신사 + // 통신사 const carrierHidden = document.getElementById('reg_carrier'); const carrierGroup = document.getElementById('reg_carrier_group'); @@ -186,7 +186,7 @@ return (value || '').replace(/\D/g, ''); } - // ✅ 통신사 버튼 레이아웃/동일 크기(기존 CSS 크게 안 건드리고 JS로 스타일만 주입) + // 통신사 버튼 레이아웃/동일 크기(기존 CSS 크게 안 건드리고 JS로 스타일만 주입) function applyCarrierButtonLayout() { if (!carrierGroup) return; @@ -208,7 +208,7 @@ }); } - // ✅ 통신사 선택 UI + // 통신사 선택 UI function bindCarrierButtons() { if (!carrierGroup || !carrierHidden) return; @@ -229,7 +229,7 @@ }); } - // ✅ 휴대폰 입력 UX + // 휴대폰 입력 UX input.addEventListener('input', function () { const formatted = formatPhone(input.value); if (input.value !== formatted) input.value = formatted; @@ -253,7 +253,7 @@ applyCarrierButtonLayout(); bindCarrierButtons(); - // ✅ submit + // submit form.addEventListener('submit', async function () { // clearMsg()가 기존에 전역으로 있다면 유지 if (typeof clearMsg === 'function') clearMsg(); @@ -280,7 +280,7 @@ btn.disabled = true; try { - // ✅ 공통 함수로 토큰 발급 (한 줄) + // 공통 함수로 토큰 발급 (한 줄) const token = await window.recaptchaV3Token('register_phone_check', form); const res = await fetch("{{ route('web.auth.register.phone_check') }}", { diff --git a/resources/views/web/auth/register_terms.blade.php b/resources/views/web/auth/register_terms.blade.php index a3961a7..1718c89 100644 --- a/resources/views/web/auth/register_terms.blade.php +++ b/resources/views/web/auth/register_terms.blade.php @@ -464,7 +464,7 @@ flex:1; text-align:center; padding:10px 12px; - border-radius:999px; /* ✅ badge */ + border-radius:999px; /* badge */ font-size:12.5px; font-weight:800; letter-spacing:-0.2px; @@ -484,7 +484,7 @@ overflow:hidden; } - /* ✅ 비활성도 살짝 그라데이션 느낌(은은하게) */ + /* 비활성도 살짝 그라데이션 느낌(은은하게) */ .terms-step::before{ content:""; position:absolute; @@ -498,7 +498,7 @@ pointer-events:none; } - /* ✅ 활성: 선명한 그라데이션 */ + /* 활성: 선명한 그라데이션 */ .terms-step.is-active{ opacity:1; color:#fff; @@ -724,7 +724,7 @@ const wrap = document.createElement('div'); wrap.id = popupName; - // ✅ step0(전화번호 입력 페이지) 이동 URL + // step0(전화번호 입력 페이지) 이동 URL const backUrl = @json(route('web.auth.register')); wrap.innerHTML = ` @@ -732,7 +732,7 @@
- +
PASS 본인인증
- {{-- ✅ “정확한 카카오 채널 URL”이 있으면 여기 href만 교체하면 됨 --}} + {{-- “정확한 카카오 채널 URL”이 있으면 여기 href만 교체하면 됨 --}} 카카오톡에서 검색하기 diff --git a/resources/views/web/cs/notice/show.blade.php b/resources/views/web/cs/notice/show.blade.php index fa0e88e..556bf93 100644 --- a/resources/views/web/cs/notice/show.blade.php +++ b/resources/views/web/cs/notice/show.blade.php @@ -48,7 +48,7 @@
- {{-- ✅ 내용 박스 라인 + 내부 패딩 --}} + {{-- 내용 박스 라인 + 내부 패딩 --}}
{!! $notice->content !!} diff --git a/resources/views/web/cs/qna/index.blade.php b/resources/views/web/cs/qna/index.blade.php index facbfd1..031e587 100644 --- a/resources/views/web/cs/qna/index.blade.php +++ b/resources/views/web/cs/qna/index.blade.php @@ -112,7 +112,7 @@ placeholder="문제 상황을 자세히 적어주세요. 예) 주문시각/결제수단/금액/오류메시지/상품명" required>{{ old('enquiry_content') }}
- 정확한 안내를 위해 개인정보(주민번호/전체 카드번호 등)는 작성하지 마세요. + 정확한 안내를 위해 내용은 상세하게 작성해 주시고, 개인정보(주민번호/전체 카드번호 등)는 작성하지 마세요.
@@ -135,7 +135,6 @@ 이메일 답변
-
현재는 UI만 제공되며, 실제 알림 연동은 추후 적용됩니다.
diff --git a/resources/views/web/layouts/layout.blade.php b/resources/views/web/layouts/layout.blade.php index 9cb48bc..8df5c44 100644 --- a/resources/views/web/layouts/layout.blade.php +++ b/resources/views/web/layouts/layout.blade.php @@ -59,7 +59,7 @@ @include('web.company.header')
- {{-- ✅ 페이지에서 @section('content')가 여기로 들어옴 --}} + {{-- 페이지에서 @section('content')가 여기로 들어옴 --}} @yield('content')
diff --git a/resources/views/web/mypage/info/gate.blade.php b/resources/views/web/mypage/info/gate.blade.php index 8de446a..61e4267 100644 --- a/resources/views/web/mypage/info/gate.blade.php +++ b/resources/views/web/mypage/info/gate.blade.php @@ -68,7 +68,7 @@
- {{-- ✅ 서브메뉴(사이드바) --}} + {{-- 서브메뉴(사이드바) --}} @@ -84,7 +84,7 @@ const pwEl = document.getElementById('password'); const btn = document.getElementById('btnGateSubmit'); - // ✅ 공통 레이어 알림(showMsg) 우선 사용 + // 공통 레이어 알림(showMsg) 우선 사용 async function alertMsg(msg, title = '오류') { if (!msg) return; if (typeof showMsg === 'function') { @@ -96,7 +96,7 @@ } } - // ✅ 서버에서 내려온 에러를 레이어로 표시 (DOM 로드 후 1회) + // 서버에서 내려온 에러를 레이어로 표시 (DOM 로드 후 1회) document.addEventListener('DOMContentLoaded', async () => { const pwErr = @json($errors->first('password')); const loginErr = @json($errors->first('login')); // 혹시 login 키도 쓰는 경우 대비 @@ -105,12 +105,12 @@ const msg = pwErr || gateErr || loginErr || flashErr; if (msg) { - if (btn) btn.disabled = false; // ✅ 에러로 돌아온 경우 버튼 다시 활성화 + if (btn) btn.disabled = false; // 에러로 돌아온 경우 버튼 다시 활성화 await alertMsg(msg, '확인 실패'); } }); - // ✅ 제출 검증 + 버튼 잠금 + // 제출 검증 + 버튼 잠금 form.addEventListener('submit', async function (e) { const pw = (pwEl?.value || '').trim(); diff --git a/resources/views/web/mypage/info/renew.blade.php b/resources/views/web/mypage/info/renew.blade.php index bf2d83a..0901145 100644 --- a/resources/views/web/mypage/info/renew.blade.php +++ b/resources/views/web/mypage/info/renew.blade.php @@ -24,17 +24,17 @@ 'desc' => '계정 보안과 개인정보를 안전하게 관리하세요.' ]) - {{-- ✅ 상단 상태 카드 --}} + {{-- 상단 상태 카드 --}}
- +
ACCOUNT SETTINGS
내 정보 관리
- +
@@ -100,7 +100,7 @@
- {{-- ✅ 설정 카드 그리드 --}} + {{-- 설정 카드 그리드 --}}
+
+
+

+ 핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요. +

+ +
+
+
+ + {{-- 옵션 2 --}} +
+ +
+
+

+ SMS 발송 시 핀번호는 저장되지 않습니다. 문자 수신 후 즉시 확인하세요. + +

+ +
+
+
+ + {{-- 옵션 3 --}} +
+ +
+
+

+ 구매하신 상품권을 판매합니다. 계좌번호가 등록되어 있어야 하며, + 매입 처리 후 회원님 계좌로 입금됩니다. + +

+ +
+
+
+
+
+ @endif + +
+ + @if(!$isCancelledAfterPaid) + + {{-- 핀 목록 --}} + @if($showPinsNow) +
+
+

핀 목록

+
+ 핀 발행이 완료되면 이 영역에서 핀 정보를 확인할 수 있습니다. (현재는 UI 확인을 위해 표시 중) +
+
+ + @if(empty($pins)) +

표시할 핀이 없습니다.

+ @else +
    + @foreach($pins as $p) + @php + $id = (int)($p['id'] ?? 0); + $status = (string)($p['status'] ?? ''); + $raw = (string)($p['pin'] ?? $p['pin_code'] ?? $p['pin_no'] ?? ''); + $masked = (string)($p['pin_masked'] ?? $p['pin_mask'] ?? ''); + if ($masked === '' && $raw !== '') { + $digits = preg_replace('/\s+/', '', $raw); + $masked = (mb_strlen($digits) >= 8) + ? (mb_substr($digits,0,4).str_repeat('*',4).mb_substr($digits,-2)) + : '****'; + } + // 오픈 전에는 마스킹 우선 + $display = $pinsOpened ? ($raw ?: $masked) : ($masked ?: '****'); + @endphp +
  • + #{{ $id }} + @if($status !== '') {{ $status }} @endif + {{ $display }} +
  • + @endforeach +
+ @endif +
+ @endif + + {{-- 취소 버튼은 맨 아래, 작게 --}} +
+

결제 취소

+
+ 핀을 확인/발행한 이후에는 취소가 제한될 수 있습니다.
+ 결제 후 취소는 처리 시간이 소요될 수 있으며, 취소 결과는 본 페이지에 반영됩니다. +
+ + @if($canCancel) +
+ @csrf + + + + + + + + +
+ @else +
+ 현재 상태에서는 결제 취소가 불가능합니다. +
+ @endif +
+ + @endif +
+ + + + +@endsection diff --git a/resources/views/web/partials/cs-tabs.blade.php b/resources/views/web/partials/cs-tabs.blade.php index a7b72ef..c769db2 100644 --- a/resources/views/web/partials/cs-tabs.blade.php +++ b/resources/views/web/partials/cs-tabs.blade.php @@ -1,10 +1,10 @@ @php - // ✅ CS 헤더 + // CS 헤더 $nav = config('web.cs_nav', []); $navTitle = $nav['title'] ?? '고객센터'; $navSubtitle = $nav['subtitle'] ?? null; - // ✅ CS items + // CS items $rawTabs = config('web.cs_tabs', []); $items = collect($rawTabs)->map(function ($t) { diff --git a/resources/views/web/partials/dev_session_overlay.blade.php b/resources/views/web/partials/dev_session_overlay.blade.php index 14e29b3..ad5673f 100644 --- a/resources/views/web/partials/dev_session_overlay.blade.php +++ b/resources/views/web/partials/dev_session_overlay.blade.php @@ -1,9 +1,9 @@ {{-- resources/views/web/partials/dev_session_overlay.blade.php --}} @php - // ✅ 개발 모드에서만 노출 + // 개발 모드에서만 노출 $show = config('app.debug') || app()->environment('local'); - // ✅ 이 overlay 자체가 세션을 수정하는 "dev action" 처리(컨트롤러/라우트 없이) + // 이 overlay 자체가 세션을 수정하는 "dev action" 처리(컨트롤러/라우트 없이) if ($show && request()->isMethod('post') && request()->has('_dev_sess_action')) { // CSRF는 web 미들웨어에 걸려있으니 토큰 포함된 요청만 처리됨. $action = request()->input('_dev_sess_action'); @@ -28,7 +28,7 @@ } } - // ✅ POST 재전송 방지 + 현재 페이지로 되돌리기 + // POST 재전송 방지 + 현재 페이지로 되돌리기 $redir = url()->current(); $qs = request()->query(); if (!empty($qs)) $redir .= '?' . http_build_query($qs); @@ -36,10 +36,10 @@ exit; } - // ✅ 세션 전체 + // 세션 전체 $sess = session()->all(); - // ✅ 민감값 마스킹 + // 민감값 마스킹 $maskKeys = []; // $maskKeys = [ // 'password', 'passwd', 'pw', 'token', 'access_token', 'refresh_token', @@ -56,7 +56,7 @@ return $val; }; - // ✅ key:value 라인 생성(재귀) + // key:value 라인 생성(재귀) $lines = []; $dump = function ($data, $prefix = '') use (&$dump, &$lines, $mask) { foreach ((array)$data as $k => $v) { @@ -112,7 +112,7 @@ {{ request()->method() }} {{ request()->path() }}
- {{-- ✅ Controls --}} + {{-- Controls --}}
@csrf diff --git a/resources/views/web/partials/mobile-drawer.blade.php b/resources/views/web/partials/mobile-drawer.blade.php index dc4ed51..ee42a63 100644 --- a/resources/views/web/partials/mobile-drawer.blade.php +++ b/resources/views/web/partials/mobile-drawer.blade.php @@ -20,7 +20,7 @@
- {{-- ✅ 트렌디한 “프로필 카드”: 로그인 전/후 UI 분기 --}} + {{-- 트렌디한 “프로필 카드”: 로그인 전/후 UI 분기 --}}
{{--
--}} {{--
--}} @@ -81,7 +81,7 @@
- {{-- ✅ 메뉴 섹션(주메뉴/CS/정책/마이페이지) --}} + {{-- 메뉴 섹션(주메뉴/CS/정책/마이페이지) --}}
@foreach($sections as $secKey => $sec) @php diff --git a/resources/views/web/partials/policy-tabs.blade.php b/resources/views/web/partials/policy-tabs.blade.php index 6c54da5..c28b76d 100644 --- a/resources/views/web/partials/policy-tabs.blade.php +++ b/resources/views/web/partials/policy-tabs.blade.php @@ -1,10 +1,10 @@ @php - // ✅ Policy 헤더 + // Policy 헤더 $nav = config('web.policy_nav', []); $navTitle = $nav['title'] ?? 'PIN FOR YOU'; $navSubtitle = $nav['subtitle'] ?? '약관 및 정책'; - // ✅ policy tabs items + // policy tabs items $rawTabs = config('web.policy_tabs', []); $items = collect($rawTabs)->map(function ($t) { diff --git a/resources/views/web/partials/subpage-sidenav.blade.php b/resources/views/web/partials/subpage-sidenav.blade.php index cf43b32..222e171 100644 --- a/resources/views/web/partials/subpage-sidenav.blade.php +++ b/resources/views/web/partials/subpage-sidenav.blade.php @@ -22,7 +22,7 @@ @endphp @if($mode === 'tabs') - {{-- ✅ 모바일 전용: 한 줄에 2개씩 배치되는 그리드 디자인 --}} + {{-- 모바일 전용: 한 줄에 2개씩 배치되는 그리드 디자인 --}} @else - {{-- ✅ 데스크톱 전용: 프리미엄 사이드바 (기존 유지) --}} + {{-- 데스크톱 전용: 프리미엄 사이드바 (기존 유지) --}}