From 6e8e8b5a576b493ccab456a6ef99d4e3fd700095 Mon Sep 17 00:00:00 2001 From: sungro815 Date: Thu, 12 Feb 2026 20:15:42 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20,=20=EB=8B=A4=EB=82=A0=EC=9D=B8=EC=A6=9D,=20=EA=B3=84?= =?UTF-8?q?=EC=A2=8C=EB=B2=88=ED=98=B8=EC=9D=B8=EC=A6=9D,=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=EB=B3=80=EA=B2=BD,=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EB=A1=9C=EA=B7=B8,=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=B0=A8=EB=8B=A8=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8,=20=EA=B4=80=EB=A6=AC=EC=9E=90=ED=99=9C?= =?UTF-8?q?=EB=8F=99=EB=A1=9C=EA=B7=B8=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/Log/AdminAuditLogController.php | 43 ++ .../Admin/Log/MemberAccountLogController.php | 22 + .../Log/MemberDanalAuthTelLogController.php | 21 + .../Admin/Log/MemberJoinLogController.php | 22 + .../Admin/Log/MemberLoginLogController.php | 22 + .../Log/MemberPasswdModifyLogController.php | 22 + .../Web/Mypage/InfoGateController.php | 7 + .../Admin/Log/AdminAuditLogRepository.php | 99 +++++ .../Admin/Log/MemberAccountLogRepository.php | 97 +++++ .../Log/MemberDanalAuthTelLogRepository.php | 43 ++ .../Admin/Log/MemberJoinLogRepository.php | 83 ++++ .../Admin/Log/MemberLoginLogRepository.php | 88 ++++ .../Log/MemberPasswdModifyLogRepository.php | 85 ++++ .../Member/MemberAuthRepository.php | 23 +- .../Admin/Log/AdminAuditLogService.php | 106 +++++ .../Admin/Log/MemberAccountLogService.php | 160 ++++++++ .../Log/MemberDanalAuthTelLogService.php | 92 +++++ .../Admin/Log/MemberJoinLogService.php | 179 +++++++++ .../Admin/Log/MemberLoginLogService.php | 129 ++++++ .../Log/MemberPasswdModifyLogService.php | 162 ++++++++ app/Services/FindPasswordService.php | 1 + docs/product_table/pfy_phase1_schema.md | 306 ++++++++++++++ .../log/AdminAuditLogController.blade.php | 281 +++++++++++++ .../log/MemberAccountLogController.blade.php | 380 ++++++++++++++++++ .../log/MemberJoinLogController.blade.php | 188 +++++++++ .../log/MemberLoginLogController.blade.php | 246 ++++++++++++ .../MemberPasswdModifyLogController.blade.php | 338 ++++++++++++++++ .../admin/log/_audit_log_modal.blade.php | 61 +++ .../log/member_danalauthtel_log.blade.php | 235 +++++++++++ .../views/admin/partials/sidebar.blade.php | 49 ++- routes/admin.php | 34 +- 31 files changed, 3608 insertions(+), 16 deletions(-) create mode 100644 app/Http/Controllers/Admin/Log/AdminAuditLogController.php create mode 100644 app/Http/Controllers/Admin/Log/MemberAccountLogController.php create mode 100644 app/Http/Controllers/Admin/Log/MemberDanalAuthTelLogController.php create mode 100644 app/Http/Controllers/Admin/Log/MemberJoinLogController.php create mode 100644 app/Http/Controllers/Admin/Log/MemberLoginLogController.php create mode 100644 app/Http/Controllers/Admin/Log/MemberPasswdModifyLogController.php create mode 100644 app/Repositories/Admin/Log/AdminAuditLogRepository.php create mode 100644 app/Repositories/Admin/Log/MemberAccountLogRepository.php create mode 100644 app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php create mode 100644 app/Repositories/Admin/Log/MemberJoinLogRepository.php create mode 100644 app/Repositories/Admin/Log/MemberLoginLogRepository.php create mode 100644 app/Repositories/Admin/Log/MemberPasswdModifyLogRepository.php create mode 100644 app/Services/Admin/Log/AdminAuditLogService.php create mode 100644 app/Services/Admin/Log/MemberAccountLogService.php create mode 100644 app/Services/Admin/Log/MemberDanalAuthTelLogService.php create mode 100644 app/Services/Admin/Log/MemberJoinLogService.php create mode 100644 app/Services/Admin/Log/MemberLoginLogService.php create mode 100644 app/Services/Admin/Log/MemberPasswdModifyLogService.php create mode 100644 docs/product_table/pfy_phase1_schema.md create mode 100644 resources/views/admin/log/AdminAuditLogController.blade.php create mode 100644 resources/views/admin/log/MemberAccountLogController.blade.php create mode 100644 resources/views/admin/log/MemberJoinLogController.blade.php create mode 100644 resources/views/admin/log/MemberLoginLogController.blade.php create mode 100644 resources/views/admin/log/MemberPasswdModifyLogController.blade.php create mode 100644 resources/views/admin/log/_audit_log_modal.blade.php create mode 100644 resources/views/admin/log/member_danalauthtel_log.blade.php diff --git a/app/Http/Controllers/Admin/Log/AdminAuditLogController.php b/app/Http/Controllers/Admin/Log/AdminAuditLogController.php new file mode 100644 index 0000000..115b33b --- /dev/null +++ b/app/Http/Controllers/Admin/Log/AdminAuditLogController.php @@ -0,0 +1,43 @@ +service->indexData($request->query()); + + // ✅ view 파일명: 컨트롤러와 이름 맞춤 (index.blade.php 사용 안함) + return view('admin.log.AdminAuditLogController', $data); + } + + /** + * AJAX 단건 조회 (모달 상세) + */ + public function show(int $id, Request $request) + { + $item = $this->service->getItem($id); + + if (!$item) { + return response()->json([ + 'ok' => false, + 'message' => 'NOT_FOUND', + 'item' => null, + ], 404); + } + + return response()->json([ + 'ok' => true, + 'item' => $item, + ]); + } +} diff --git a/app/Http/Controllers/Admin/Log/MemberAccountLogController.php b/app/Http/Controllers/Admin/Log/MemberAccountLogController.php new file mode 100644 index 0000000..ff1c2ea --- /dev/null +++ b/app/Http/Controllers/Admin/Log/MemberAccountLogController.php @@ -0,0 +1,22 @@ +service->indexData($request->query()); + + // index.blade.php 금지 → 컨트롤러명과 동일 + return view('admin.log.MemberAccountLogController', $data); + } +} diff --git a/app/Http/Controllers/Admin/Log/MemberDanalAuthTelLogController.php b/app/Http/Controllers/Admin/Log/MemberDanalAuthTelLogController.php new file mode 100644 index 0000000..a5ce2ff --- /dev/null +++ b/app/Http/Controllers/Admin/Log/MemberDanalAuthTelLogController.php @@ -0,0 +1,21 @@ +service->indexData($request->query()); + + return view('admin.log.member_danalauthtel_log', $data); + } +} diff --git a/app/Http/Controllers/Admin/Log/MemberJoinLogController.php b/app/Http/Controllers/Admin/Log/MemberJoinLogController.php new file mode 100644 index 0000000..bb0f8ae --- /dev/null +++ b/app/Http/Controllers/Admin/Log/MemberJoinLogController.php @@ -0,0 +1,22 @@ +service->indexData($request->query()); + + // ✅ index.blade.php 금지 → 컨트롤러명과 동일한 뷰 파일 + return view('admin.log.MemberJoinLogController', $data); + } +} diff --git a/app/Http/Controllers/Admin/Log/MemberLoginLogController.php b/app/Http/Controllers/Admin/Log/MemberLoginLogController.php new file mode 100644 index 0000000..73513d5 --- /dev/null +++ b/app/Http/Controllers/Admin/Log/MemberLoginLogController.php @@ -0,0 +1,22 @@ +service->indexData($request->query()); + + // index.blade.php 금지 → 컨트롤러명과 동일 + return view('admin.log.MemberLoginLogController', $data); + } +} diff --git a/app/Http/Controllers/Admin/Log/MemberPasswdModifyLogController.php b/app/Http/Controllers/Admin/Log/MemberPasswdModifyLogController.php new file mode 100644 index 0000000..0a1919e --- /dev/null +++ b/app/Http/Controllers/Admin/Log/MemberPasswdModifyLogController.php @@ -0,0 +1,22 @@ +service->indexData($request->query()); + + // index.blade.php 금지 → 컨트롤러명과 동일 + return view('admin.log.MemberPasswdModifyLogController', $data); + } +} diff --git a/app/Http/Controllers/Web/Mypage/InfoGateController.php b/app/Http/Controllers/Web/Mypage/InfoGateController.php index 34d85e8..c10015b 100644 --- a/app/Http/Controllers/Web/Mypage/InfoGateController.php +++ b/app/Http/Controllers/Web/Mypage/InfoGateController.php @@ -413,6 +413,7 @@ final class InfoGateController extends Controller $repo->updatePasswordOnly($memNo, $newPw); $repo->logPasswordResetSuccess( $memNo, + $email, (string) $request->ip(), (string) $request->userAgent(), 'S' @@ -506,6 +507,12 @@ final class InfoGateController extends Controller try { $repo->updatePin2Only($memNo, $newPin2); + $repo->logPasswordResetSuccess2Only( + $memNo, + $email, + (string) $request->ip(), + (string) $request->userAgent() + ); } catch (\Throwable $e) { Log::error('[mypage] pin2 update failed', [ 'mem_no' => $memNo, diff --git a/app/Repositories/Admin/Log/AdminAuditLogRepository.php b/app/Repositories/Admin/Log/AdminAuditLogRepository.php new file mode 100644 index 0000000..b10916e --- /dev/null +++ b/app/Repositories/Admin/Log/AdminAuditLogRepository.php @@ -0,0 +1,99 @@ +leftJoin('admin_users as au', 'au.id', '=', 'l.actor_admin_user_id') + ->select([ + 'l.id', + 'l.actor_admin_user_id', + 'l.action', + 'l.target_type', + 'l.target_id', + 'l.ip', + 'l.created_at', + 'au.email as actor_email', + 'au.name as actor_name', + ]); + + // 기간 + if (!empty($filters['date_from'])) { + $q->where('l.created_at', '>=', $filters['date_from'] . ' 00:00:00'); + } + if (!empty($filters['date_to'])) { + $q->where('l.created_at', '<=', $filters['date_to'] . ' 23:59:59'); + } + + // actor 검색 (email/name) + $actorQ = trim((string)($filters['actor_q'] ?? '')); + if ($actorQ !== '') { + $like = '%' . $this->escapeLike($actorQ) . '%'; + $q->where(function ($qq) use ($like) { + $qq->where('au.email', 'like', $like) + ->orWhere('au.name', 'like', $like); + }); + } + + // action (prefix match) + $action = trim((string)($filters['action'] ?? '')); + if ($action !== '') { + $q->where('l.action', 'like', $this->escapeLike($action) . '%'); + } + + // target_type (prefix match) + $tt = trim((string)($filters['target_type'] ?? '')); + if ($tt !== '') { + $q->where('l.target_type', 'like', $this->escapeLike($tt) . '%'); + } + + // ip (prefix match) + $ip = trim((string)($filters['ip'] ?? '')); + if ($ip !== '') { + $q->where('l.ip', 'like', $this->escapeLike($ip) . '%'); + } + + // ✅ 최신순 + $q->orderByDesc('l.created_at')->orderByDesc('l.id'); + + return $q->paginate($perPage)->withQueryString(); + } + + public function findOne(int $id): ?array + { + $row = DB::table(self::TABLE . ' as l') + ->leftJoin('admin_users as au', 'au.id', '=', 'l.actor_admin_user_id') + ->select([ + 'l.id', + 'l.actor_admin_user_id', + 'l.action', + 'l.target_type', + 'l.target_id', + 'l.before_json', + 'l.after_json', + 'l.ip', + 'l.user_agent', + 'l.created_at', + 'au.email as actor_email', + 'au.name as actor_name', + ]) + ->where('l.id', $id) + ->first(); + + return $row ? (array)$row : null; + } + + private function escapeLike(string $s): string + { + // MySQL LIKE escape (%, _) + return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $s); + } +} diff --git a/app/Repositories/Admin/Log/MemberAccountLogRepository.php b/app/Repositories/Admin/Log/MemberAccountLogRepository.php new file mode 100644 index 0000000..fbd0cc0 --- /dev/null +++ b/app/Repositories/Admin/Log/MemberAccountLogRepository.php @@ -0,0 +1,97 @@ +select([ + 'l.seq', + 'l.mem_no', + 'l.request_data', + 'l.result_data', + 'l.request_time', + 'l.result_time', + ]); + + // 기간(요청시간 기준) + if (!empty($filters['date_from'])) { + $q->where('l.request_time', '>=', $filters['date_from'] . ' 00:00:00'); + } + if (!empty($filters['date_to'])) { + $q->where('l.request_time', '<=', $filters['date_to'] . ' 23:59:59'); + } + + // mem_no + if (($filters['mem_no'] ?? null) !== null) { + $q->where('l.mem_no', (int)$filters['mem_no']); + } + + // bank_code (요청 JSON) + $bank = trim((string)($filters['bank_code'] ?? '')); + if ($bank !== '') { + $q->whereRaw( + "JSON_UNQUOTE(JSON_EXTRACT(l.request_data, '$.bank_code')) = ?", + [$bank] + ); + } + + // status (결과 JSON) + $status = trim((string)($filters['status'] ?? '')); + if ($status !== '') { + if ($status === 'ok') { + $q->whereRaw("JSON_EXTRACT(l.result_data, '$.status') = 200"); + } elseif ($status === 'fail') { + $q->whereRaw("JSON_EXTRACT(l.result_data, '$.status') <> 200"); + } elseif (ctype_digit($status)) { + $q->whereRaw("JSON_EXTRACT(l.result_data, '$.status') = ?", [(int)$status]); + } + } + + // account (요청 JSON) - 부분검색 + $account = trim((string)($filters['account'] ?? '')); + if ($account !== '') { + $like = '%' . $this->escapeLike($account) . '%'; + $q->whereRaw( + "JSON_UNQUOTE(JSON_EXTRACT(l.request_data, '$.account')) LIKE ?", + [$like] + ); + } + + // name (요청 JSON: mam_accountname) + $name = trim((string)($filters['name'] ?? '')); + if ($name !== '') { + $like = '%' . $this->escapeLike($name) . '%'; + $q->whereRaw( + "JSON_UNQUOTE(JSON_EXTRACT(l.request_data, '$.mam_accountname')) LIKE ?", + [$like] + ); + } + + // q: request/result 전체 LIKE (운영에서 필요할 때만) + $kw = trim((string)($filters['q'] ?? '')); + if ($kw !== '') { + $like = '%' . $this->escapeLike($kw) . '%'; + $q->where(function ($qq) use ($like) { + $qq->where('l.request_data', 'like', $like) + ->orWhere('l.result_data', 'like', $like); + }); + } + + $q->orderByDesc('l.request_time')->orderByDesc('l.seq'); + + return $q->paginate($perPage)->withQueryString(); + } + + private function escapeLike(string $s): string + { + return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $s); + } +} diff --git a/app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php b/app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php new file mode 100644 index 0000000..0601524 --- /dev/null +++ b/app/Repositories/Admin/Log/MemberDanalAuthTelLogRepository.php @@ -0,0 +1,43 @@ +select([ + 'seq','gubun','TID','res_code','mem_no','info','rgdate', + // ✅ 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"), + ]); + + // mem_no 검색: 컬럼 mem_no + info._mno 둘 다 + $memNo = (string)($filters['mem_no'] ?? ''); + if ($memNo !== '') { + $q->where(function ($qq) use ($memNo) { + $qq->where('mem_no', (int)$memNo) + ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(`info`, '$._mno')) = ?", [$memNo]); + }); + } + + // phone 검색: 숫자만 받아서 부분검색 + $pd = (string)($filters['phone_digits'] ?? ''); + if ($pd !== '') { + $q->whereRaw( + "REPLACE(JSON_UNQUOTE(JSON_EXTRACT(`info`, '$.mobile_number')),'-','') LIKE ?", + ['%'.$pd.'%'] + ); + } + + $q->orderByDesc('rgdate')->orderByDesc('seq'); + + return $q->paginate($perPage)->withQueryString(); + } +} diff --git a/app/Repositories/Admin/Log/MemberJoinLogRepository.php b/app/Repositories/Admin/Log/MemberJoinLogRepository.php new file mode 100644 index 0000000..77b5ca4 --- /dev/null +++ b/app/Repositories/Admin/Log/MemberJoinLogRepository.php @@ -0,0 +1,83 @@ +select([ + 'l.seq', + 'l.gubun', + 'l.mem_no', + 'l.cell_corp', + 'l.cell_phone', + 'l.email', + 'l.ip4', + 'l.ip4_c', + 'l.error_code', + 'l.dt_reg', + ]); + + // 기간(dt_reg) + if (!empty($filters['date_from'])) { + $q->where('l.dt_reg', '>=', $filters['date_from'] . ' 00:00:00'); + } + if (!empty($filters['date_to'])) { + $q->where('l.dt_reg', '<=', $filters['date_to'] . ' 23:59:59'); + } + + // gubun + $gubun = trim((string)($filters['gubun'] ?? '')); + if ($gubun !== '') $q->where('l.gubun', $gubun); + + // error_code (2자리) + $err = trim((string)($filters['error_code'] ?? '')); + if ($err !== '') $q->where('l.error_code', $err); + + // mem_no + if (($filters['mem_no'] ?? null) !== null) { + $q->where('l.mem_no', (int)$filters['mem_no']); + } + + // phone exact (encrypt 결과) + if (!empty($filters['phone_enc'])) { + $q->where('l.cell_phone', (string)$filters['phone_enc']); + } + + // email (부분검색) + $email = trim((string)($filters['email'] ?? '')); + if ($email !== '') { + $like = '%' . $this->escapeLike($email) . '%'; + $q->where('l.email', 'like', $like); + } + + // ip4 prefix + $ip4 = trim((string)($filters['ip4'] ?? '')); + if ($ip4 !== '') { + $q->where('l.ip4', 'like', $this->escapeLike($ip4) . '%'); + } + + // ip4_c prefix + $ip4c = trim((string)($filters['ip4_c'] ?? '')); + if ($ip4c !== '') { + $q->where('l.ip4_c', 'like', $this->escapeLike($ip4c) . '%'); + } + + // 최신순 + $q->orderByDesc('l.dt_reg')->orderByDesc('l.seq'); + + return $q->paginate($perPage)->withQueryString(); + } + + private function escapeLike(string $s): string + { + return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $s); + } +} diff --git a/app/Repositories/Admin/Log/MemberLoginLogRepository.php b/app/Repositories/Admin/Log/MemberLoginLogRepository.php new file mode 100644 index 0000000..01e56a3 --- /dev/null +++ b/app/Repositories/Admin/Log/MemberLoginLogRepository.php @@ -0,0 +1,88 @@ +select([ + 'l.seq', + 'l.mem_no', + 'l.sf', + 'l.conn', + 'l.ip4', + 'l.ip4_c', + 'l.error_code', + 'l.dt_reg', + 'l.platform', + 'l.browser', + ]); + + // 기간(dt_reg) + if (!empty($filters['date_from'])) { + $q->where('l.dt_reg', '>=', $filters['date_from'] . ' 00:00:00'); + } + if (!empty($filters['date_to'])) { + $q->where('l.dt_reg', '<=', $filters['date_to'] . ' 23:59:59.999999'); + } + + // mem_no + if (($filters['mem_no'] ?? null) !== null) { + $q->where('l.mem_no', (int)$filters['mem_no']); + } + + // 성공/실패 + if (!empty($filters['sf'])) { + $q->where('l.sf', $filters['sf']); + } + + // 접속경로 1/2 + if (!empty($filters['conn'])) { + $q->where('l.conn', $filters['conn']); + } + + // ip prefix + $ip4 = trim((string)($filters['ip4'] ?? '')); + if ($ip4 !== '') { + $q->where('l.ip4', 'like', $this->escapeLike($ip4) . '%'); + } + + $ip4c = trim((string)($filters['ip4_c'] ?? '')); + if ($ip4c !== '') { + $q->where('l.ip4_c', 'like', $this->escapeLike($ip4c) . '%'); + } + + // error_code + $err = trim((string)($filters['error_code'] ?? '')); + if ($err !== '') { + $q->where('l.error_code', $err); + } + + // platform/browser (contains) + $platform = trim((string)($filters['platform'] ?? '')); + if ($platform !== '') { + $q->where('l.platform', 'like', '%' . $this->escapeLike($platform) . '%'); + } + + $browser = trim((string)($filters['browser'] ?? '')); + if ($browser !== '') { + $q->where('l.browser', 'like', '%' . $this->escapeLike($browser) . '%'); + } + + $q->orderByDesc('l.dt_reg')->orderByDesc('l.seq'); + + return $q->paginate($perPage)->withQueryString(); + } + + private function escapeLike(string $s): string + { + return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $s); + } +} diff --git a/app/Repositories/Admin/Log/MemberPasswdModifyLogRepository.php b/app/Repositories/Admin/Log/MemberPasswdModifyLogRepository.php new file mode 100644 index 0000000..abd4f44 --- /dev/null +++ b/app/Repositories/Admin/Log/MemberPasswdModifyLogRepository.php @@ -0,0 +1,85 @@ +select(['p.seq', 'p.state', 'p.info', 'p.rgdate']); + + // 기간 + if (!empty($filters['date_from'])) { + $q->where('p.rgdate', '>=', $filters['date_from'] . ' 00:00:00'); + } + if (!empty($filters['date_to'])) { + $q->where('p.rgdate', '<=', $filters['date_to'] . ' 23:59:59'); + } + + // state + if (!empty($filters['state'])) { + $q->where('p.state', $filters['state']); + } + + // mem_no (JSON) + if (($filters['mem_no'] ?? null) !== null) { + $q->whereRaw( + "JSON_UNQUOTE(JSON_EXTRACT(p.info, '$.mem_no')) = ?", + [(string)((int)$filters['mem_no'])] + ); + } + + // type (신포맷) + $type = trim((string)($filters['type'] ?? '')); + if ($type !== '') { + $q->whereRaw( + "JSON_UNQUOTE(JSON_EXTRACT(p.info, '$.type')) = ?", + [$type] + ); + } + + // email (구포맷) + $email = trim((string)($filters['email'] ?? '')); + if ($email !== '') { + $like = '%' . $this->escapeLike($email) . '%'; + + $q->where(function ($qq) use ($like) { + $qq->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(p.info, '$.user_email')) LIKE ?", [$like]) + ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(p.info, '$.email')) LIKE ?", [$like]); + }); + } + + // ip: 신포맷 remote_addr prefix OR 구포맷 auth_key/info contains + $ip = trim((string)($filters['ip'] ?? '')); + if ($ip !== '') { + $like = $this->escapeLike($ip) . '%'; + $like2 = '%' . $this->escapeLike($ip) . '%'; + + $q->where(function ($qq) use ($like, $like2) { + $qq->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(p.info, '$.remote_addr')) LIKE ?", [$like]) + ->orWhere('p.info', 'like', $like2); + }); + } + + // q: info 전체 검색(필요할 때만) + $kw = trim((string)($filters['q'] ?? '')); + if ($kw !== '') { + $q->where('p.info', 'like', '%' . $this->escapeLike($kw) . '%'); + } + + $q->orderByDesc('p.rgdate')->orderByDesc('p.seq'); + + return $q->paginate($perPage)->withQueryString(); + } + + private function escapeLike(string $s): string + { + return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $s); + } +} diff --git a/app/Repositories/Member/MemberAuthRepository.php b/app/Repositories/Member/MemberAuthRepository.php index 52dd1b4..4f822b4 100644 --- a/app/Repositories/Member/MemberAuthRepository.php +++ b/app/Repositories/Member/MemberAuthRepository.php @@ -742,7 +742,7 @@ class MemberAuthRepository } - public function logPasswordResetSuccess(int $memNo, string $ip, string $agent, string $state = 'E'): void + public function logPasswordResetSuccess(int $memNo, string $email, string $ip, string $agent, string $state = 'E'): void { $now = now()->format('Y-m-d H:i:s'); @@ -756,6 +756,27 @@ class MemberAuthRepository 'state' => $state, 'info' => json_encode([ 'mem_no' => (string)$memNo, + 'email' => $email, + 'type' => "로그인비밀번호변경", + 'redate' => $now, + 'remote_addr' => $ip, + 'agent' => substr((string)$agent, 0, 500), // 길이 방어(일관 처리) + ], JSON_UNESCAPED_UNICODE), + 'rgdate' => $now, + ]); + } + + public function logPasswordResetSuccess2Only(int $memNo, string $email, string $ip, string $agent): void + { + $now = now()->format('Y-m-d H:i:s'); + $state = 'S'; + + DB::table('mem_passwd_modify')->insert([ + 'state' => $state, + 'info' => json_encode([ + 'mem_no' => (string)$memNo, + 'email' => $email, + 'type' => "2차비밀번호변경", 'redate' => $now, 'remote_addr' => $ip, 'agent' => substr((string)$agent, 0, 500), // 길이 방어(일관 처리) diff --git a/app/Services/Admin/Log/AdminAuditLogService.php b/app/Services/Admin/Log/AdminAuditLogService.php new file mode 100644 index 0000000..99842fe --- /dev/null +++ b/app/Services/Admin/Log/AdminAuditLogService.php @@ -0,0 +1,106 @@ + $this->safeDate($query['date_from'] ?? ''), + 'date_to' => $this->safeDate($query['date_to'] ?? ''), + 'actor_q' => $this->safeStr($query['actor_q'] ?? '', 80), + 'action' => $this->safeStr($query['action'] ?? '', 60), + 'target_type' => $this->safeStr($query['target_type'] ?? '', 80), + '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']]; + } + } + + $page = $this->repo->paginate($filters, 30); + + return [ + 'page' => $page, + 'items' => $page->items(), + 'filters' => $filters, + ]; + } + + public function getItem(int $id): ?array + { + $row = $this->repo->findOne($id); + if (!$row) return null; + + $beforeRaw = $row['before_json'] ?? null; + $afterRaw = $row['after_json'] ?? null; + + return [ + 'id' => (int)($row['id'] ?? 0), + + 'actor_admin_user_id' => (int)($row['actor_admin_user_id'] ?? 0), + 'actor_email' => (string)($row['actor_email'] ?? ''), + 'actor_name' => (string)($row['actor_name'] ?? ''), + + 'action' => (string)($row['action'] ?? ''), + 'target_type' => (string)($row['target_type'] ?? ''), + 'target_id' => (int)($row['target_id'] ?? 0), + + 'ip' => (string)($row['ip'] ?? ''), + 'created_at' => (string)($row['created_at'] ?? ''), + + 'user_agent' => (string)($row['user_agent'] ?? ''), + + // ✅ pretty 출력 (모달에서 바로 textContent로 넣기 좋게) + 'before_pretty' => $this->prettyJson($beforeRaw), + 'after_pretty' => $this->prettyJson($afterRaw), + + // 원문도 필요하면 남겨둠 + 'before_raw' => $beforeRaw, + 'after_raw' => $afterRaw, + ]; + } + + private function safeStr(mixed $v, int $max): string + { + $s = trim((string)$v); + if ($s === '') return ''; + if (mb_strlen($s) > $max) $s = mb_substr($s, 0, $max); + return $s; + } + + private function safeDate(mixed $v): ?string + { + $s = trim((string)$v); + if ($s === '') return null; + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $s)) return null; + return $s; + } + + private function prettyJson(?string $raw): string + { + $raw = $raw !== null ? trim((string)$raw) : ''; + if ($raw === '') return '-'; + + $decoded = json_decode($raw, true); + if (!is_array($decoded) && !is_object($decoded)) { + // 깨진 JSON이면 원문 출력 + return $raw; + } + + return json_encode( + $decoded, + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ) ?: $raw; + } +} diff --git a/app/Services/Admin/Log/MemberAccountLogService.php b/app/Services/Admin/Log/MemberAccountLogService.php new file mode 100644 index 0000000..36e5c88 --- /dev/null +++ b/app/Services/Admin/Log/MemberAccountLogService.php @@ -0,0 +1,160 @@ + $this->safeDate($query['date_from'] ?? ''), + 'date_to' => $this->safeDate($query['date_to'] ?? ''), + + 'mem_no' => $this->safeInt($query['mem_no'] ?? null), + 'bank_code' => $this->safeStr($query['bank_code'] ?? '', 10), + 'status' => $this->safeStr($query['status'] ?? '', 10), // ok/fail/200/520... + + 'account' => $this->safeStr($query['account'] ?? '', 40), + 'name' => $this->safeStr($query['name'] ?? '', 40), + 'q' => $this->safeStr($query['q'] ?? '', 120), + ]; + + 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']]; + } + } + + $page = $this->repo->paginate($filters, 30); + + $items = []; + foreach ($page->items() as $it) { + $r = is_array($it) ? $it : (array)$it; + + $reqRaw = (string)($r['request_data'] ?? ''); + $resRaw = (string)($r['result_data'] ?? ''); + + $req = $this->decodeJson($reqRaw); + $res = $this->decodeJson($resRaw); + + $memNo = (int)($r['mem_no'] ?? 0); + + $account = trim((string)($req['account'] ?? '')); + $accountMasked = $this->maskAccount($account); + + $bankCode = trim((string)($req['bank_code'] ?? '')); + $proType = trim((string)($req['account_protype'] ?? '')); + $name = trim((string)($req['mam_accountname'] ?? '')); + + $status = (int)($res['status'] ?? 0); + $ok = ($status === 200); + + $depositor = trim((string)($res['depositor'] ?? '')); + $errCode = trim((string)($res['error_code'] ?? '')); + $errMsg = trim((string)($res['error_message'] ?? '')); + + // JSON pretty (api_key는 마스킹) + $reqPretty = $this->prettyJson($this->maskApiKey($req), $reqRaw); + $resPretty = $this->prettyJson($res, $resRaw); + + $items[] = array_merge($r, [ + 'mem_link' => $memNo > 0 ? ('/members/' . $memNo) : null, + + 'account' => $account, + 'account_masked' => $accountMasked, + 'bank_code' => $bankCode, + 'account_protype' => $proType, + 'mam_accountname' => $name, + + 'status_int' => $status, + 'status_label' => $ok ? 'SUCCESS' : 'FAIL', + 'status_badge' => $ok ? 'badge--ok' : 'badge--bad', + + 'depositor' => $depositor, + 'error_code' => $errCode, + 'error_message' => $errMsg, + + 'request_pretty' => $reqPretty, + 'result_pretty' => $resPretty, + ]); + } + + return [ + 'filters' => $filters, + 'page' => $page, + 'items' => $items, + ]; + } + + private function decodeJson(string $raw): array + { + $raw = trim($raw); + if ($raw === '') return []; + $arr = json_decode($raw, true); + return is_array($arr) ? $arr : []; + } + + private function prettyJson(array $arr, string $fallbackRaw): string + { + if (!empty($arr)) { + return (string)json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + return $fallbackRaw; + } + + private function maskApiKey(array $req): array + { + if (!isset($req['api_key'])) return $req; + + $v = (string)$req['api_key']; + $v = trim($v); + if ($v === '') return $req; + + // 앞 4 + ... + 뒤 4 + $head = mb_substr($v, 0, 4); + $tail = mb_substr($v, -4); + $req['api_key'] = $head . '…' . $tail; + + return $req; + } + + private function maskAccount(string $account): string + { + $s = trim($account); + if ($s === '') return ''; + + $len = mb_strlen($s); + if ($len <= 4) return $s; + + return str_repeat('*', max(0, $len - 4)) . mb_substr($s, -4); + } + + private function safeStr(mixed $v, int $max): string + { + $s = trim((string)$v); + if ($s === '') return ''; + if (mb_strlen($s) > $max) $s = mb_substr($s, 0, $max); + return $s; + } + + private function safeInt(mixed $v): ?int + { + if ($v === null || $v === '') return null; + if (!is_numeric($v)) return null; + $n = (int)$v; + return $n >= 0 ? $n : null; + } + + private function safeDate(mixed $v): ?string + { + $s = trim((string)$v); + if ($s === '') return null; + return preg_match('/^\d{4}-\d{2}-\d{2}$/', $s) ? $s : null; + } +} diff --git a/app/Services/Admin/Log/MemberDanalAuthTelLogService.php b/app/Services/Admin/Log/MemberDanalAuthTelLogService.php new file mode 100644 index 0000000..053f4db --- /dev/null +++ b/app/Services/Admin/Log/MemberDanalAuthTelLogService.php @@ -0,0 +1,92 @@ + trim((string)($query['mem_no'] ?? '')), + 'phone' => trim((string)($query['phone'] ?? '')), + ]; + + // mem_no: 숫자만 + if ($filters['mem_no'] !== '' && !ctype_digit($filters['mem_no'])) { + $filters['mem_no'] = ''; + } + + // phone: 숫자만 (10~11자리 아니어도 부분검색 가능하게 숫자만 유지) + $filters['phone_digits'] = preg_replace('/\D+/', '', $filters['phone']) ?: ''; + + $page = $this->repo->paginate($filters, 30); + + $rows = []; + foreach ($page->items() as $it) { + $r = is_array($it) ? $it : (array)$it; + + $phone = (string)($r['mobile_number'] ?? ''); + $r['phone_digits'] = preg_replace('/\D+/', '', $phone) ?: ''; + $r['phone_display'] = $this->formatPhone($r['phone_digits']) ?: $phone; + + // JSON(모달용) - 개인정보 최소화 (mem_no/전화 + 결과/거래정보만) + $infoArr = $this->decodeJson((string)($r['info'] ?? '')); + $r['info_sanitized'] = $this->sanitizeInfo($infoArr, $r); + + $rows[] = $r; + } + + return [ + 'page' => $page, + 'rows' => $rows, + 'filters' => $filters, + ]; + } + + private function decodeJson(string $json): array + { + $json = trim($json); + if ($json === '') return []; + $arr = json_decode($json, true); + return is_array($arr) ? $arr : []; + } + + private function sanitizeInfo(array $info, array $row): array + { + // 허용 필드만 남김 (요구사항: mem_no + 전화만 노출) + // + 로그 판단에 필요한 최소 정보(RETURNCODE/RETURNMSG/TID/telecom 등)만 + $out = []; + + $out['gubun'] = (string)($row['gubun'] ?? ''); + $out['res_code'] = (string)($row['res_code'] ?? ''); + $out['TID'] = (string)($row['TID'] ?? ''); + $out['mem_no'] = (string)($row['mem_no'] ?? ''); + + // info에서 가져오되, 화면엔 전화번호만 + $out['mobile_number'] = (string)($info['mobile_number'] ?? ($row['mobile_number'] ?? '')); + + // 결과코드/메시지(있으면) + $out['RETURNCODE'] = (string)($info['RETURNCODE'] ?? ($info['res_code'] ?? '')); + $out['RETURNMSG'] = (string)($info['RETURNMSG'] ?? ''); + + // telecom은 개인식별 정보는 아니지만, 원하면 모달에서도 빼도 됨. + // 지금은 로그 파악에 도움돼서 유지. + if (isset($info['telecom'])) $out['telecom'] = (string)$info['telecom']; + + // CI/DI/name/birthday/sex/email 등은 완전히 제거 + return array_filter($out, fn($v) => $v !== ''); + } + + private function formatPhone(string $digits): string + { + if (!preg_match('/^\d{10,11}$/', $digits)) return ''; + if (strlen($digits) === 11) return substr($digits, 0, 3).'-'.substr($digits, 3, 4).'-'.substr($digits, 7, 4); + return substr($digits, 0, 3).'-'.substr($digits, 3, 3).'-'.substr($digits, 6, 4); + } +} diff --git a/app/Services/Admin/Log/MemberJoinLogService.php b/app/Services/Admin/Log/MemberJoinLogService.php new file mode 100644 index 0000000..821fc55 --- /dev/null +++ b/app/Services/Admin/Log/MemberJoinLogService.php @@ -0,0 +1,179 @@ + $this->safeDate($query['date_from'] ?? ''), + 'date_to' => $this->safeDate($query['date_to'] ?? ''), + + 'gubun' => $this->safeStr($query['gubun'] ?? '', 50), + 'error_code' => $this->safeStr($query['error_code'] ?? '', 2), + + 'mem_no' => $this->safeInt($query['mem_no'] ?? null), + 'email' => $this->safeStr($query['email'] ?? '', 80), + + 'ip4' => $this->safeIpPrefix($query['ip4'] ?? ''), + 'ip4_c' => $this->safeIpPrefix($query['ip4_c'] ?? ''), + + // 사용자 입력 전화번호(plain) + 'phone' => $this->safeStr($query['phone'] ?? '', 30), + // 검색용 encrypt 결과(정확일치) + '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 (정확일치) + $phoneDigits = preg_replace('/\D+/', '', (string)$filters['phone']) ?: ''; + if ($phoneDigits !== '' && preg_match('/^\d{10,11}$/', $phoneDigits)) { + try { + $seed = app(CiSeedCrypto::class); + $filters['phone_enc'] = (string)$seed->encrypt($phoneDigits); + } catch (\Throwable $e) { + // encrypt 실패하면 검색조건 무시(리스트는 정상 출력) + $filters['phone_enc'] = null; + } + } + + $page = $this->repo->paginate($filters, 30); + + // ✅ 리스트 표시용 가공(복호화/포맷) + $seed = app(CiSeedCrypto::class); + + $items = []; + foreach ($page->items() as $it) { + $r = is_array($it) ? $it : (array)$it; + + [$corpLabel, $corpBadge] = $this->corpLabel((string)($r['cell_corp'] ?? 'n')); + + $phoneEnc = (string)($r['cell_phone'] ?? ''); + $phoneDigits = $this->decryptPhoneDigits($seed, $phoneEnc); + $phoneDisplay = $this->formatPhone($phoneDigits); + + $email = trim((string)($r['email'] ?? '')); + if ($email === '' || $email === '-') $email = '-'; + + $memNo = (int)($r['mem_no'] ?? 0); + + $items[] = array_merge($r, [ + 'corp_label' => $corpLabel, + 'corp_badge' => $corpBadge, + + 'phone_plain' => $phoneDigits, + 'phone_display' => ($phoneDisplay !== '' ? $phoneDisplay : ($phoneDigits !== '' ? $phoneDigits : '-')), + + 'email_display' => $email, + + 'mem_no_int' => $memNo, + 'mem_link' => ($memNo > 0) ? ('/members/' . $memNo) : null, + ]); + } + + return [ + 'page' => $page, + 'items' => $items, + 'filters' => $filters, + 'corp_map' => $this->corpMapForUi(), + ]; + } + + private function safeStr(mixed $v, int $max): string + { + $s = trim((string)$v); + if ($s === '') return ''; + if (mb_strlen($s) > $max) $s = mb_substr($s, 0, $max); + return $s; + } + + private function safeInt(mixed $v): ?int + { + if ($v === null || $v === '') return null; + if (!is_numeric($v)) return null; + $n = (int)$v; + return $n >= 0 ? $n : null; + } + + private function safeDate(mixed $v): ?string + { + $s = trim((string)$v); + if ($s === '') return null; + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $s)) return null; + return $s; + } + + private function safeIpPrefix(mixed $v): string + { + $s = trim((string)$v); + if ($s === '') return ''; + // prefix 검색 용도: 숫자/점만 허용 + if (!preg_match('/^[0-9.]+$/', $s)) return ''; + return $s; + } + + private function decryptPhoneDigits(CiSeedCrypto $seed, string $enc): string + { + $enc = trim($enc); + if ($enc === '') return ''; + + try { + $plain = (string)$seed->decrypt($enc); + $digits = preg_replace('/\D+/', '', $plain) ?: ''; + return preg_match('/^\d{10,11}$/', $digits) ? $digits : ''; + } catch (\Throwable $e) { + return ''; + } + } + + private function formatPhone(string $digits): string + { + if (!preg_match('/^\d{10,11}$/', $digits)) return ''; + if (strlen($digits) === 11) { + return substr($digits, 0, 3) . '-' . substr($digits, 3, 4) . '-' . substr($digits, 7, 4); + } + return substr($digits, 0, 3) . '-' . substr($digits, 3, 3) . '-' . substr($digits, 6, 4); + } + + private function corpLabel(string $code): array + { + $map = [ + '01' => ['SKT', 'badge--skt'], + '02' => ['KT', 'badge--kt'], + '03' => ['LGU+', 'badge--lgu'], + '04' => ['SKT(알뜰)', 'badge--mvno'], + '05' => ['KT(알뜰)', 'badge--mvno'], + '06' => ['LGU+(알뜰)', 'badge--mvno'], + 'n' => ['-', 'badge--muted'], + ]; + + return $map[$code] ?? ['-', 'badge--muted']; + } + + private function corpMapForUi(): array + { + return [ + '' => '전체', + '01' => 'SKT', + '02' => 'KT', + '03' => 'LGU+', + '04' => 'SKT(알뜰)', + '05' => 'KT(알뜰)', + '06' => 'LGU+(알뜰)', + ]; + } +} diff --git a/app/Services/Admin/Log/MemberLoginLogService.php b/app/Services/Admin/Log/MemberLoginLogService.php new file mode 100644 index 0000000..750e7bf --- /dev/null +++ b/app/Services/Admin/Log/MemberLoginLogService.php @@ -0,0 +1,129 @@ +availableYears(2018, $currentYear); + + $year = (int)($query['year'] ?? $currentYear); + if (!in_array($year, $years, true)) { + // 테이블 없거나 허용 범위 밖이면 최신년도 fallback + $year = !empty($years) ? max($years) : $currentYear; + } + + $filters = [ + 'year' => $year, + 'date_from' => $this->safeDate($query['date_from'] ?? ''), + 'date_to' => $this->safeDate($query['date_to'] ?? ''), + + 'mem_no' => $this->safeInt($query['mem_no'] ?? null), + + 'sf' => $this->safeEnum($query['sf'] ?? '', ['s','f']), // 성공/실패 + 'conn' => $this->safeEnum($query['conn'] ?? '', ['1','2']), // pc/mobile + + 'ip4' => $this->safeIpPrefix($query['ip4'] ?? ''), + 'ip4_c' => $this->safeIpPrefix($query['ip4_c'] ?? ''), + + 'error_code'=> $this->safeStr($query['error_code'] ?? '', 10), + + 'platform' => $this->safeStr($query['platform'] ?? '', 30), + 'browser' => $this->safeStr($query['browser'] ?? '', 40), + ]; + + // 기간 역전 방지 + 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']]; + } + } + + $page = $this->repo->paginateByYear($year, $filters, 30); + + // 화면 가공(라벨/링크 등) + $items = []; + foreach ($page->items() as $it) { + $r = is_array($it) ? $it : (array)$it; + + $sf = (string)($r['sf'] ?? 's'); + $conn = (string)($r['conn'] ?? '1'); + + $items[] = array_merge($r, [ + 'sf_label' => $sf === 'f' ? '실패' : '성공', + 'sf_badge' => $sf === 'f' ? 'badge--bad' : 'badge--ok', + + 'conn_label' => $conn === '2' ? 'M' : 'PC', + 'conn_badge' => $conn === '2' ? 'badge--mvno' : 'badge--muted', + + 'mem_no_int' => (int)($r['mem_no'] ?? 0), + 'mem_link' => ((int)($r['mem_no'] ?? 0) > 0) ? ('/members/' . (int)$r['mem_no']) : null, + + // 실패가 아니면 에러코드 화면에서 비워도 됨(뷰에서 처리) + ]); + } + + return [ + 'years' => $years, + 'filters' => $filters, + 'page' => $page, + 'items' => $items, + ]; + } + + private function availableYears(int $from, int $to): array + { + $out = []; + for ($y = $from; $y <= $to; $y++) { + $t = 'mem_login_' . $y; + if (Schema::hasTable($t)) $out[] = $y; + } + // 없으면 그냥 범위라도 반환(운영/개발에서 초기 셋업 대비) + return !empty($out) ? $out : [$to]; + } + + private function safeStr(mixed $v, int $max): string + { + $s = trim((string)$v); + if ($s === '') return ''; + if (mb_strlen($s) > $max) $s = mb_substr($s, 0, $max); + return $s; + } + + private function safeEnum(mixed $v, array $allowed): string + { + $s = trim((string)$v); + return in_array($s, $allowed, true) ? $s : ''; + } + + private function safeInt(mixed $v): ?int + { + if ($v === null || $v === '') return null; + if (!is_numeric($v)) return null; + $n = (int)$v; + return $n >= 0 ? $n : null; + } + + private function safeDate(mixed $v): ?string + { + $s = trim((string)$v); + if ($s === '') return null; + return preg_match('/^\d{4}-\d{2}-\d{2}$/', $s) ? $s : null; + } + + private function safeIpPrefix(mixed $v): string + { + $s = trim((string)$v); + if ($s === '') return ''; + return preg_match('/^[0-9.]+$/', $s) ? $s : ''; + } +} diff --git a/app/Services/Admin/Log/MemberPasswdModifyLogService.php b/app/Services/Admin/Log/MemberPasswdModifyLogService.php new file mode 100644 index 0000000..8e16a69 --- /dev/null +++ b/app/Services/Admin/Log/MemberPasswdModifyLogService.php @@ -0,0 +1,162 @@ + $this->safeDate($query['date_from'] ?? ''), + 'date_to' => $this->safeDate($query['date_to'] ?? ''), + + 'state' => $this->safeEnum($query['state'] ?? '', ['S','E']), // S 직접, E 찾기 + 'mem_no' => $this->safeInt($query['mem_no'] ?? null), + + 'type' => $this->safeStr($query['type'] ?? '', 40), // 신포맷 type + 'email' => $this->safeStr($query['email'] ?? '', 80), // 구포맷 user_email + 'ip' => $this->safeIpPrefix($query['ip'] ?? ''), // remote_addr or auth_key contains + 'q' => $this->safeStr($query['q'] ?? '', 120), // info 전체 like + ]; + + 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']]; + } + } + + $page = $this->repo->paginate($filters, 30); + + $items = []; + foreach ($page->items() as $it) { + $r = is_array($it) ? $it : (array)$it; + + $infoRaw = (string)($r['info'] ?? ''); + $infoArr = $this->decodeJson($infoRaw); + + $state = (string)($r['state'] ?? ''); + $memNo = (int)($infoArr['mem_no'] ?? 0); + + $type = trim((string)($infoArr['type'] ?? '')); + $eventType = $type !== '' ? $type : ($state === 'E' ? '비밀번호찾기 변경' : '비밀번호변경'); + + $eventTime = trim((string)($infoArr['redate'] ?? '')); + if ($eventTime === '') $eventTime = (string)($r['rgdate'] ?? ''); + + $ip = trim((string)($infoArr['remote_addr'] ?? '')); + if ($ip === '') { + $ip = $this->extractIpFromAuthKey((string)($infoArr['auth_key'] ?? '')) ?: ''; + } + + $email = trim((string)($infoArr['user_email'] ?? '')); + if ($email === '') $email = trim((string)($infoArr['email'] ?? '')); + + $agent = trim((string)($infoArr['agent'] ?? '')); + + $items[] = array_merge($r, [ + 'info_arr' => $infoArr, + + 'mem_no_int' => $memNo, + 'mem_link' => $memNo > 0 ? ('/members/' . $memNo) : null, + + 'state_label' => $state === 'E' ? '비번찾기' : '직접변경', + 'state_badge' => $state === 'E' ? 'badge--warn' : 'badge--ok', + + 'event_type' => $eventType, + 'event_time' => $eventTime, + + 'ip_norm' => $ip, + 'email_norm' => $email, + 'agent_norm' => $agent, + + // 펼침용(구포맷 핵심 필드) + 'auth_key' => (string)($infoArr['auth_key'] ?? ''), + 'auth_effective_time' => (string)($infoArr['auth_effective_time'] ?? ''), + + // raw pretty + 'info_pretty' => $this->prettyJson($infoArr, $infoRaw), + ]); + } + + return [ + 'filters' => $filters, + 'page' => $page, + 'items' => $items, + + 'stateMap' => [ + 'S' => '직접변경', + 'E' => '비번찾기', + ], + ]; + } + + private function decodeJson(string $raw): array + { + $raw = trim($raw); + if ($raw === '') return []; + $arr = json_decode($raw, true); + return is_array($arr) ? $arr : []; + } + + private function prettyJson(array $arr, string $fallbackRaw): string + { + if (!empty($arr)) { + return (string)json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + return $fallbackRaw; + } + + private function extractIpFromAuthKey(string $authKey): ?string + { + $authKey = trim($authKey); + if ($authKey === '') return null; + + // auth_key 끝에 "1.85.101.111" 같은 IP가 붙는 케이스 대응 (마지막 매치 사용) + if (preg_match_all('/(\d{1,3}(?:\.\d{1,3}){3})/', $authKey, $m) && !empty($m[1])) { + return (string)end($m[1]); + } + return null; + } + + private function safeStr(mixed $v, int $max): string + { + $s = trim((string)$v); + if ($s === '') return ''; + if (mb_strlen($s) > $max) $s = mb_substr($s, 0, $max); + return $s; + } + + private function safeEnum(mixed $v, array $allowed): string + { + $s = trim((string)$v); + return in_array($s, $allowed, true) ? $s : ''; + } + + private function safeInt(mixed $v): ?int + { + if ($v === null || $v === '') return null; + if (!is_numeric($v)) return null; + $n = (int)$v; + return $n >= 0 ? $n : null; + } + + private function safeDate(mixed $v): ?string + { + $s = trim((string)$v); + if ($s === '') return null; + return preg_match('/^\d{4}-\d{2}-\d{2}$/', $s) ? $s : null; + } + + private function safeIpPrefix(mixed $v): string + { + $s = trim((string)$v); + if ($s === '') return ''; + return preg_match('/^[0-9.]+$/', $s) ? $s : ''; + } +} diff --git a/app/Services/FindPasswordService.php b/app/Services/FindPasswordService.php index c6540b6..d81a557 100644 --- a/app/Services/FindPasswordService.php +++ b/app/Services/FindPasswordService.php @@ -228,6 +228,7 @@ class FindPasswordService $this->members->updatePasswordOnly($memNo, $newPassword); $this->members->logPasswordResetSuccess( $memNo, + $member->email, (string) $request->ip(), (string) $request->userAgent(), 'E' diff --git a/docs/product_table/pfy_phase1_schema.md b/docs/product_table/pfy_phase1_schema.md new file mode 100644 index 0000000..30f847c --- /dev/null +++ b/docs/product_table/pfy_phase1_schema.md @@ -0,0 +1,306 @@ +# Phase 1 DB 스키마 (상품등록/전시 + SKU + 판매채널 + 결제수단 + 자사핀재고 + 이미지) + +- 신규 테이블은 모두 `pfy_` 접두사 사용 +- MariaDB (InnoDB, `utf8mb4`) 기준 +- 레거시/기존 회원/관리자 테이블과 FK는 **1단계에서는 걸지 않음(컬럼만 준비)** + → 2단계(주문/결제)부터 FK/연동 강화 + +--- + +## 1) `pfy_categories` : 1차/2차 카테고리 트리 + +- `parent_id` 로 1차/2차 구성 +- `sort` / `is_active` 로 전시 제어 + +```sql +CREATE TABLE IF NOT EXISTS pfy_categories ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'PK', + parent_id BIGINT UNSIGNED NULL COMMENT '상위 카테고리 ID (1차는 NULL, 2차는 1차의 id)', + name VARCHAR(100) NOT NULL COMMENT '카테고리명', + slug VARCHAR(120) NOT NULL COMMENT 'URL/식별용 슬러그(유니크 권장)', + sort INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '정렬값(작을수록 먼저)', + is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '노출 여부(1=노출,0=숨김)', + icon_path VARCHAR(255) NULL COMMENT '아이콘 경로(선택)', + banner_path VARCHAR(255) NULL COMMENT '배너 경로(선택)', + desc_short VARCHAR(255) NULL COMMENT '카테고리 짧은 설명(선택)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (id), + UNIQUE KEY uk_pfy_categories_slug (slug), + KEY idx_pfy_categories_parent_sort (parent_id, sort) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 카테고리(1차/2차 트리)'; +``` + +--- + +## 2) `pfy_media_files` : 업로드된 파일(상품 이미지 등) 메타데이터 + +- 업로드된 이미지를 “선택”해서 상품에 연결하는 용도 + +```sql +CREATE TABLE IF NOT EXISTS pfy_media_files ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'PK', + kind ENUM('image','file') NOT NULL DEFAULT 'image' COMMENT '파일 종류', + disk VARCHAR(40) NOT NULL DEFAULT 'public' COMMENT '스토리지 디스크명(Laravel disks)', + path VARCHAR(500) NOT NULL COMMENT '스토리지 상대 경로', + original_name VARCHAR(255) NULL COMMENT '원본 파일명', + mime VARCHAR(120) NULL COMMENT 'MIME 타입', + size_bytes BIGINT UNSIGNED NULL COMMENT '파일 크기(bytes)', + width INT UNSIGNED NULL COMMENT '이미지 폭(px, 이미지인 경우)', + height INT UNSIGNED NULL COMMENT '이미지 높이(px, 이미지인 경우)', + checksum_sha256 CHAR(64) NULL COMMENT '파일 무결성 체크섬(선택)', + uploaded_by_admin_id BIGINT UNSIGNED NULL COMMENT '업로드 관리자 ID(레거시/기존 admin_users 연동 예정)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (id), + KEY idx_pfy_media_files_kind (kind), + KEY idx_pfy_media_files_created (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 업로드 파일 메타(상품이미지 선택/재사용)'; +``` + +--- + +## 3) `pfy_products` : 상품(전시 단위) + +- 상품명/타입/판매기간/상태/대표이미지/매입가능 여부 + +```sql +CREATE TABLE IF NOT EXISTS pfy_products ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'PK', + category_id BIGINT UNSIGNED NOT NULL COMMENT '2차 카테고리 ID(pfy_categories.id)', + name VARCHAR(160) NOT NULL COMMENT '상품명(수기)', + type ENUM('online','delivery') NOT NULL DEFAULT 'online' COMMENT '상품타입(온라인/배송)', + is_buyback_allowed ENUM('Y','N') NOT NULL DEFAULT 'Y' COMMENT '매입가능여부(Y=가능,N=불가)', + sale_period_type ENUM('always','ranged') NOT NULL DEFAULT 'always' COMMENT '판매기간 타입(상시/기간설정)', + sale_start_at DATETIME NULL COMMENT '판매 시작일(기간설정일 때)', + sale_end_at DATETIME NULL COMMENT '판매 종료일(기간설정일 때)', + status ENUM('active','hidden','soldout') NOT NULL DEFAULT 'active' COMMENT '노출상태(노출/숨김/품절)', + main_image_id BIGINT UNSIGNED NULL COMMENT '대표 이미지(pfy_media_files.id)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (id), + KEY idx_pfy_products_category (category_id), + KEY idx_pfy_products_status (status), + KEY idx_pfy_products_sale_period (sale_period_type, sale_start_at, sale_end_at), + CONSTRAINT fk_pfy_products_category + FOREIGN KEY (category_id) REFERENCES pfy_categories(id) + ON UPDATE CASCADE ON DELETE RESTRICT, + CONSTRAINT fk_pfy_products_main_image + FOREIGN KEY (main_image_id) REFERENCES pfy_media_files(id) + ON UPDATE CASCADE ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 상품(전시 단위)'; +``` + +--- + +## 4) `pfy_product_contents` : 상품 상세 에디터 3종(1:1) + +- 상세설명/이용안내/주의사항 HTML 저장 + +```sql +CREATE TABLE IF NOT EXISTS pfy_product_contents ( + product_id BIGINT UNSIGNED NOT NULL COMMENT 'PK & FK(pfy_products.id)', + detail_html LONGTEXT NULL COMMENT '상세설명(HTML)', + guide_html LONGTEXT NULL COMMENT '이용안내(HTML)', + caution_html LONGTEXT NULL COMMENT '주의사항(HTML)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (product_id), + CONSTRAINT fk_pfy_product_contents_product + FOREIGN KEY (product_id) REFERENCES pfy_products(id) + ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 상품 에디터 컨텐츠(상세/안내/주의)'; +``` + +--- + +## 5) `pfy_product_skus` : 권종/가격 단위(SKU) + +- 정상가/할인율/판매가(스냅샷 계산 저장 추천) +- 재고는 `stock_mode` 로 (연동판매는 `infinite`, 자사핀은 `limited`) + +```sql +CREATE TABLE IF NOT EXISTS pfy_product_skus ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'PK', + product_id BIGINT UNSIGNED NOT NULL COMMENT '상품 ID(pfy_products.id)', + denomination INT UNSIGNED NOT NULL COMMENT '권종 금액(예: 10000, 50000)', + normal_price INT UNSIGNED NOT NULL COMMENT '정상가(원)', + discount_rate DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '할인율(%)', + sale_price INT UNSIGNED NOT NULL COMMENT '판매가(원) - 운영/정산 안정 위해 계산 후 저장 권장', + stock_mode ENUM('infinite','limited') NOT NULL DEFAULT 'infinite' COMMENT '재고방식(무한/한정)', + is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '사용여부(1=사용,0=중지)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (id), + KEY idx_pfy_skus_product (product_id), + KEY idx_pfy_skus_active (product_id, is_active), + KEY idx_pfy_skus_denomination (product_id, denomination), + CONSTRAINT fk_pfy_skus_product + FOREIGN KEY (product_id) REFERENCES pfy_products(id) + ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 상품 SKU(권종/가격 단위)'; +``` + +--- + +## 6) `pfy_sku_sale_channels` : SKU 판매방식/연동채널 (기본: 1 SKU = 1 판매채널) + +### `sale_mode` +- `SELF_PIN` : 자사핀 재고 판매 +- `VENDOR_API_SHOW` : 연동발행 후 우리 사이트에서 핀 표시 +- `VENDOR_SMS` : 업체가 SMS로 즉시 발송(핀 저장 최소화 가능) + +### `vendor` +- `DANAL` / `KORCULTURE` / `KPREPAID` (필요 시 확장) + +```sql +CREATE TABLE IF NOT EXISTS pfy_sku_sale_channels ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'PK', + sku_id BIGINT UNSIGNED NOT NULL COMMENT 'SKU ID(pfy_product_skus.id)', + sale_mode ENUM('SELF_PIN','VENDOR_API_SHOW','VENDOR_SMS') NOT NULL COMMENT '판매모드', + vendor ENUM('NONE','DANAL','KORCULTURE','KPREPAID') NOT NULL DEFAULT 'NONE' COMMENT '연동업체(없으면 NONE)', + vendor_product_code VARCHAR(40) NULL COMMENT '업체 상품코드(예: DANAL CULTURE, KPREPAID 1231 등)', + pg ENUM('NONE','DANAL') NOT NULL DEFAULT 'DANAL' COMMENT '결제 PG(현재 DANAL, 추후 확장)', + is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '사용여부(1=사용,0=중지)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (id), + UNIQUE KEY uk_pfy_sale_channels_sku (sku_id), + KEY idx_pfy_sale_channels_vendor (vendor, vendor_product_code), + CONSTRAINT fk_pfy_sale_channels_sku + FOREIGN KEY (sku_id) REFERENCES pfy_product_skus(id) + ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] SKU 판매채널/연동 설정'; +``` + +--- + +## 7) `pfy_payment_methods` : 결제수단 마스터 + +- `code` 를 PK로 사용(안정적, pivot에 쓰기 편함) + +```sql +CREATE TABLE IF NOT EXISTS pfy_payment_methods ( + code VARCHAR(30) NOT NULL COMMENT '결제수단 코드(PK)', + label VARCHAR(60) NOT NULL COMMENT '표시명(관리자/회원)', + is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '사용여부', + sort INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '노출 정렬', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (code), + KEY idx_pfy_payment_methods_active (is_active, sort) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 결제수단 마스터'; +``` + +--- + +## 8) `pfy_sku_payment_methods` : SKU별 허용 결제수단(pivot) + +- SKU마다 가능한 결제수단 체크박스 용도 + +```sql +CREATE TABLE IF NOT EXISTS pfy_sku_payment_methods ( + sku_id BIGINT UNSIGNED NOT NULL COMMENT 'SKU ID(pfy_product_skus.id)', + payment_method_code VARCHAR(30) NOT NULL COMMENT '결제수단 코드(pfy_payment_methods.code)', + is_enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '허용 여부(1=허용,0=비허용)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (sku_id, payment_method_code), + KEY idx_pfy_sku_pm_method (payment_method_code, is_enabled), + CONSTRAINT fk_pfy_sku_pm_sku + FOREIGN KEY (sku_id) REFERENCES pfy_product_skus(id) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_pfy_sku_pm_method + FOREIGN KEY (payment_method_code) REFERENCES pfy_payment_methods(code) + ON UPDATE CASCADE ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] SKU별 허용 결제수단 매핑'; +``` + +--- + +## 9) `pfy_pin_batches` : 자사핀 입력 작업 단위(업로드/수기 등록) + +```sql +CREATE TABLE IF NOT EXISTS pfy_pin_batches ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'PK', + sku_id BIGINT UNSIGNED NOT NULL COMMENT 'SKU ID(pfy_product_skus.id)', + source_type ENUM('upload','manual') NOT NULL COMMENT '핀 등록 방식(파일/수기)', + original_filename VARCHAR(255) NULL COMMENT '업로드 파일명(업로드일 때)', + total_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '배치 총 핀 개수', + uploaded_by_admin_id BIGINT UNSIGNED NULL COMMENT '업로드 관리자 ID(레거시/기존 admin_users 연동 예정)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (id), + KEY idx_pfy_pin_batches_sku (sku_id, created_at), + CONSTRAINT fk_pfy_pin_batches_sku + FOREIGN KEY (sku_id) REFERENCES pfy_product_skus(id) + ON UPDATE CASCADE ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 자사핀 입력 배치(업로드/수기)'; +``` + +--- + +## 10) `pfy_pin_items` : 핀 1건 단위 재고(암호화 저장) + +- `pin_enc` : 쌍방향 암호화 저장값(복호화는 권한 통과한 구매자만) +- `pin_fingerprint` : 중복핀 방지(유니크) + +### `status` +- `available` : 사용가능(재고) +- `reserved` : 결제 대기/재고 예약 +- `sold` : 판매 완료(주문 라인에 귀속될 예정) +- `revoked` : 폐기/무효 +- `recalled` : 회수(미사용 회수 프로세스에서 사용) + +> 1단계에서는 주문 테이블이 없으므로 `reserved_order_id`, `sold_order_item_id` 는 FK 없이 컬럼만 준비 + +```sql +CREATE TABLE IF NOT EXISTS pfy_pin_items ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'PK', + batch_id BIGINT UNSIGNED NOT NULL COMMENT '배치 ID(pfy_pin_batches.id)', + sku_id BIGINT UNSIGNED NOT NULL COMMENT 'SKU ID(pfy_product_skus.id)', + pin_enc TEXT NOT NULL COMMENT '핀 암호문(쌍방향 암호화 결과)', + pin_fingerprint CHAR(64) NOT NULL COMMENT '중복 방지 해시(sha256 등) - UNIQUE', + status ENUM('available','reserved','sold','revoked','recalled') NOT NULL DEFAULT 'available' COMMENT '재고 상태', + reserved_order_id BIGINT UNSIGNED NULL COMMENT '예약된 주문 ID(2단계에서 orders FK 예정)', + reserved_until DATETIME NULL COMMENT '예약 만료 시각(스케줄러로 자동 해제)', + sold_order_item_id BIGINT UNSIGNED NULL COMMENT '판매된 주문아이템 ID(2단계에서 order_items FK 예정)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (id), + UNIQUE KEY uk_pfy_pin_items_fingerprint (pin_fingerprint), + KEY idx_pfy_pin_items_sku_status (sku_id, status), + KEY idx_pfy_pin_items_batch (batch_id), + KEY idx_pfy_pin_items_reserved_until (status, reserved_until), + CONSTRAINT fk_pfy_pin_items_batch + FOREIGN KEY (batch_id) REFERENCES pfy_pin_batches(id) + ON UPDATE CASCADE ON DELETE RESTRICT, + CONSTRAINT fk_pfy_pin_items_sku + FOREIGN KEY (sku_id) REFERENCES pfy_product_skus(id) + ON UPDATE CASCADE ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='[PFY] 자사핀 재고(암호화 저장)'; +``` + +--- + +## (선택) 결제수단 초기 데이터 예시 + +```sql +INSERT INTO pfy_payment_methods(code,label,sort) VALUES +('CARD_GENERAL','신용카드(일반)',10), +('CARD_CASHBACK','신용카드(환급성)',11), +('MOBILE','휴대폰',20), +('VBANK','무통장입금',30), +('KBANKPAY','케이뱅크페이',40), +('PAYCOIN','페이코인',50); +``` diff --git a/resources/views/admin/log/AdminAuditLogController.blade.php b/resources/views/admin/log/AdminAuditLogController.blade.php new file mode 100644 index 0000000..8cce496 --- /dev/null +++ b/resources/views/admin/log/AdminAuditLogController.blade.php @@ -0,0 +1,281 @@ +{{-- resources/views/admin/log/AdminAuditLogController.blade.php --}} +@extends('admin.layouts.app') + +@section('title', '관리자 감사 로그') +@section('page_title', '관리자 감사 로그') +@section('page_desc', '관리자 행위 감사로그를 조회합니다. (상세는 모달)') +@section('content_class', 'a-content--full') + +@push('head') + +@endpush + +@section('content') + @php + $indexUrl = route('admin.systemlog.admin-audit-logs', [], false); + $showTpl = route('admin.systemlog.admin-audit-logs.show', ['id' => '__ID__'], false); + + $f = $filters ?? []; + $dateFrom = (string)($f['date_from'] ?? ''); + $dateTo = (string)($f['date_to'] ?? ''); + $actorQ = (string)($f['actor_q'] ?? ''); + $action = (string)($f['action'] ?? ''); + $tt = (string)($f['target_type'] ?? ''); + $ip = (string)($f['ip'] ?? ''); + @endphp + +
+
+
+
관리자 감사 로그
+
검색/페이징 지원 · 상세는 AJAX 모달
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + 초기화 +
+
+
+
+ +
+
{{ $page->total() }}
+ +
+ + + + + + + + + + + + + + @forelse(($items ?? []) as $r0) + @php + $r = is_array($r0) ? $r0 : (array)$r0; + $id = (int)($r['id'] ?? 0); + $aEmail = (string)($r['actor_email'] ?? ''); + $aName = (string)($r['actor_name'] ?? ''); + $actorTxt = trim(($aName !== '' ? $aName : '-') . ($aEmail !== '' ? " ({$aEmail})" : '')); + @endphp + + + + + + + + + + @empty + + @endforelse + +
IDActorActionTarget TypeIPCreated상세
{{ $id }} +
{{ $aName !== '' ? $aName : '-' }}
+
{{ $aEmail !== '' ? $aEmail : 'admin_users 미조회' }}
+
{{ $r['action'] ?? '-' }}{{ $r['target_type'] ?? '-' }}{{ $r['ip'] ?? '-' }}{{ $r['created_at'] ?? '-' }} + +
데이터가 없습니다.
+
+ +
+ {{ $page->onEachSide(1)->links('vendor.pagination.admin') }} +
+
+ + @include('admin.log._audit_log_modal') + + +@endsection diff --git a/resources/views/admin/log/MemberAccountLogController.blade.php b/resources/views/admin/log/MemberAccountLogController.blade.php new file mode 100644 index 0000000..59c7d3b --- /dev/null +++ b/resources/views/admin/log/MemberAccountLogController.blade.php @@ -0,0 +1,380 @@ +@extends('admin.layouts.app') + +@section('title', '계좌 성명인증 로그') +@section('page_title', '계좌 성명인증 로그') +@section('page_desc', '더즌 성명인증 요청/결과 로그를 조회합니다.') +@section('content_class', 'a-content--full') + +@push('head') + +@endpush + +@section('content') + @php + $indexUrl = route('admin.systemlog.member-account-logs', [], false); + + $f = $filters ?? []; + $dateFrom = (string)($f['date_from'] ?? ''); + $dateTo = (string)($f['date_to'] ?? ''); + $memNo = (string)($f['mem_no'] ?? ''); + $bank = (string)($f['bank_code'] ?? ''); + $status = (string)($f['status'] ?? ''); + $account = (string)($f['account'] ?? ''); + $name = (string)($f['name'] ?? ''); + $q = (string)($f['q'] ?? ''); + @endphp + +
+
+
+
계좌 성명인증 로그
+
요청/결과 JSON은 모달로 표시 (row 높이 고정)
+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 초기화 +
+
+
+
+ +
+
{{ $page->total() }}
+ +
+ + + + + + + + + + + + + + + + + + + @forelse(($items ?? []) as $r0) + @php + $r = is_array($r0) ? $r0 : (array)$r0; + $seq = (int)($r['seq'] ?? 0); + + $memNoInt = (int)($r['mem_no'] ?? 0); + $memLink = $r['mem_link'] ?? null; + + $bankCode = (string)($r['bank_code'] ?? ''); + $acctMask = (string)($r['account_masked'] ?? ''); + $nameV = (string)($r['mam_accountname'] ?? ''); + $proType = (string)($r['account_protype'] ?? ''); + + $statusInt = (int)($r['status_int'] ?? 0); + $badge = (string)($r['status_badge'] ?? 'badge--bad'); + $label = (string)($r['status_label'] ?? ''); + + $depositor = (string)($r['depositor'] ?? ''); + $errCode = (string)($r['error_code'] ?? ''); + $errMsg = (string)($r['error_message'] ?? ''); + @endphp + + + + + + + + + + + + + + + + + + + + + + + @empty + + @endforelse + +
SEQrequest_timeresult_timemem_nobankaccountnameprotypestatusdepositor/errorJSON
{{ $seq }}{{ $r['request_time'] ?? '-' }}{{ $r['result_time'] ?? '-' }} + @if($memNoInt > 0 && $memLink) + {{ $memNoInt }} + @else + - + @endif + + @if($bankCode !== '') + {{ $bankCode }} + @else + - + @endif + + @if($acctMask !== '') + {{ $acctMask }} + @else + - + @endif + + @if($nameV !== '') + {{ $nameV }} + @else + - + @endif + + @if($proType !== '') + {{ $proType }} + @else + - + @endif + + + {{ $label }} ({{ $statusInt }}) + + + @if($depositor !== '') + {{ $depositor }} + @elseif($errCode !== '' || $errMsg !== '') + {{ trim($errCode.' '.$errMsg) }} + @else + - + @endif + + + + + +
데이터가 없습니다.
+
+ +
+ {{ $page->onEachSide(1)->links('vendor.pagination.admin') }} +
+
+ + {{-- JSON Modal --}} + + + +@endsection diff --git a/resources/views/admin/log/MemberJoinLogController.blade.php b/resources/views/admin/log/MemberJoinLogController.blade.php new file mode 100644 index 0000000..d67be1c --- /dev/null +++ b/resources/views/admin/log/MemberJoinLogController.blade.php @@ -0,0 +1,188 @@ +{{-- resources/views/admin/log/MemberJoinLogController.blade.php --}} +@extends('admin.layouts.app') + +@section('title', '회원가입 필터 로그') +@section('page_title', '회원가입 필터 로그') +@section('page_desc', '회원가입 시 필터에 걸린 기록을 조회합니다.') +@section('content_class', 'a-content--full') + +@push('head') + +@endpush + +@section('content') + @php + $indexUrl = route('admin.systemlog.member-join-logs', [], false); + + $f = $filters ?? []; + + $dateFrom = (string)($f['date_from'] ?? ''); + $dateTo = (string)($f['date_to'] ?? ''); + $gubun = (string)($f['gubun'] ?? ''); + $memNo = (string)($f['mem_no'] ?? ''); + $phone = (string)($f['phone'] ?? ''); + $email = (string)($f['email'] ?? ''); + $ip4 = (string)($f['ip4'] ?? ''); + $ip4c = (string)($f['ip4_c'] ?? ''); + @endphp + +
+
+
+
회원가입 필터 로그
+
리스트에서 전체 정보 표시 · 검색/페이징 지원
+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 초기화 +
+
+
+
+ +
+
{{ $page->total() }}
+ +
+ + + + + + + + + + + + + @forelse(($items ?? []) as $r0) + @php + $r = is_array($r0) ? $r0 : (array)$r0; + $seq = (int)($r['seq'] ?? 0); + + $memNoInt = (int)($r['mem_no_int'] ?? 0); + $memLink = $r['mem_link'] ?? null; + + $emailDisp = (string)($r['email_display'] ?? ''); + if ($emailDisp === '-' ) $emailDisp = ''; + + $ip4v = (string)($r['ip4'] ?? ''); + $ip4cv = (string)($r['ip4_c'] ?? ''); + @endphp + + + + + + + {{-- ✅ 전화/이메일 한줄: "전화번호 바로뒤 이메일" (이메일 없으면 빈값) --}} + + + {{-- ✅ IP 한줄: ip4 + ip4_c --}} + + + + + @empty + + @endforelse + +
SEQ일시회원전화/이메일IPgubun
{{ $seq }}{{ $r['dt_reg'] ?? '-' }} + @if($memNoInt > 0 && $memLink) + {{ $memNoInt }} + @else + 가입차단 + @endif + +
+ {{ $r['corp_label'] ?? '-' }} + {{ $r['phone_display'] ?? '-' }} + @if($emailDisp !== '') + {{ $emailDisp }} + @endif +
+
+ {{ $ip4v !== '' ? $ip4v : '-' }} + @if($ip4cv !== '') + {{ $ip4cv }} + @endif + {{ $r['gubun'] ?? '-' }}
데이터가 없습니다.
+
+ +
+ {{ $page->onEachSide(1)->links('vendor.pagination.admin') }} +
+
+@endsection diff --git a/resources/views/admin/log/MemberLoginLogController.blade.php b/resources/views/admin/log/MemberLoginLogController.blade.php new file mode 100644 index 0000000..e60345d --- /dev/null +++ b/resources/views/admin/log/MemberLoginLogController.blade.php @@ -0,0 +1,246 @@ +{{-- resources/views/admin/log/MemberLoginLogController.blade.php --}} +@extends('admin.layouts.app') + +@section('title', '로그인 로그') +@section('page_title', '로그인 로그') +@section('page_desc', '연도별 로그인 이력을 조회합니다.') +@section('content_class', 'a-content--full') + +@push('head') + +@endpush + +@section('content') + @php + $indexUrl = route('admin.systemlog.member-login-logs', [], false); + + $f = $filters ?? []; + $years = $years ?? []; + + $year = (int)($f['year'] ?? (int)date('Y')); + $dateFrom = (string)($f['date_from'] ?? ''); + $dateTo = (string)($f['date_to'] ?? ''); + + $memNo = (string)($f['mem_no'] ?? ''); + $sf = (string)($f['sf'] ?? ''); + $conn = (string)($f['conn'] ?? ''); + + $ip4 = (string)($f['ip4'] ?? ''); + $ip4c = (string)($f['ip4_c'] ?? ''); + + $err = (string)($f['error_code'] ?? ''); + $platform = (string)($f['platform'] ?? ''); + $browser = (string)($f['browser'] ?? ''); + @endphp + +
+
+
+
로그인 로그
+
연도별(mem_login_YYYY) 테이블 조회 · 검색/페이징
+
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 초기화 +
+
+
+
+ +
+
{{ $page->total() }}
+ +
+ + + + + + + + + + + + + + + @forelse(($items ?? []) as $r0) + @php + $r = is_array($r0) ? $r0 : (array)$r0; + $seq = (int)($r['seq'] ?? 0); + + $memNoInt = (int)($r['mem_no_int'] ?? 0); + $memLink = $r['mem_link'] ?? null; + + $sf = (string)($r['sf'] ?? 's'); + $errCode = trim((string)($r['error_code'] ?? '')); + // 성공이면 실패코드는 비움(요구에 따라 여기서 제어) + $errShow = ($sf === 'f' && $errCode !== '') ? $errCode : ''; + + $ip4 = (string)($r['ip4'] ?? ''); + $ip4c = (string)($r['ip4_c'] ?? ''); + + $plat = trim((string)($r['platform'] ?? '')); + $brow = trim((string)($r['browser'] ?? '')); + @endphp + + + + + + + + + + + + + + + + + @empty + + @endforelse + +
SEQ일시회원결과경로IP플랫폼/브라우저실패코드
{{ $seq }}{{ $r['dt_reg'] ?? '-' }} + @if($memNoInt > 0 && $memLink) + {{ $memNoInt }} + @else + 0 + @endif + + {{ $r['sf_label'] ?? '-' }} + + {{ $r['conn_label'] ?? '-' }} + + {{ $ip4 !== '' ? $ip4 : '-' }} + @if($ip4c !== '') + {{ $ip4c }} + @endif + + {{ $plat !== '' ? $plat : '-' }} + {{ $brow !== '' ? $brow : '-' }} + + @if($errShow !== '') + {{ $errShow }} + @else + - + @endif +
데이터가 없습니다.
+
+ +
+ {{ $page->onEachSide(1)->links('vendor.pagination.admin') }} +
+
+ + +@endsection diff --git a/resources/views/admin/log/MemberPasswdModifyLogController.blade.php b/resources/views/admin/log/MemberPasswdModifyLogController.blade.php new file mode 100644 index 0000000..d52c9f4 --- /dev/null +++ b/resources/views/admin/log/MemberPasswdModifyLogController.blade.php @@ -0,0 +1,338 @@ +{{-- resources/views/admin/log/MemberPasswdModifyLogController.blade.php --}} +@extends('admin.layouts.app') + +@section('title', '비밀번호 변경 로그') +@section('page_title', '비밀번호 변경 로그') +@section('page_desc', '로그인/2차 비밀번호 변경 및 비밀번호 찾기 변경 이력을 조회합니다.') +@section('content_class', 'a-content--full') + +@push('head') + +@endpush + +@section('content') + @php + $indexUrl = route('admin.systemlog.member-passwd-modify-logs', [], false); + + $f = $filters ?? []; + $stateMap = $stateMap ?? ['S'=>'직접변경','E'=>'비번찾기']; + + $dateFrom = (string)($f['date_from'] ?? ''); + $dateTo = (string)($f['date_to'] ?? ''); + $state = (string)($f['state'] ?? ''); + $memNo = (string)($f['mem_no'] ?? ''); + $type = (string)($f['type'] ?? ''); + $email = (string)($f['email'] ?? ''); + $ip = (string)($f['ip'] ?? ''); + $q = (string)($f['q'] ?? ''); + @endphp + +
+
+
+
비밀번호 변경 로그
+
구/신 JSON 혼재 → 핵심 필드 정규화 + 원본 JSON 펼쳐보기
+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 초기화 +
+
+
+
+ +
+
{{ $page->total() }}
+ +
+ + + + + + + + + + + + + + + @forelse(($items ?? []) as $r0) + @php + $r = is_array($r0) ? $r0 : (array)$r0; + $seq = (int)($r['seq'] ?? 0); + + $memNoInt = (int)($r['mem_no_int'] ?? 0); + $memLink = $r['mem_link'] ?? null; + + $agent = (string)($r['agent_norm'] ?? ''); + $emailN = (string)($r['email_norm'] ?? ''); + $ipN = (string)($r['ip_norm'] ?? ''); + + $authKey = (string)($r['auth_key'] ?? ''); + $authEff = (string)($r['auth_effective_time'] ?? ''); + @endphp + + + + + + + + + + + + + + + @php + $agent = (string)($r['agent_norm'] ?? ''); + $authKey = (string)($r['auth_key'] ?? ''); + $authEff = (string)($r['auth_effective_time'] ?? ''); + @endphp + + + + @empty + + @endforelse + +
SEQrgdatestatemem_notypeipemailJSON
{{ $seq }}{{ $r['rgdate'] ?? '-' }} + + {{ $r['state_label'] ?? ($stateMap[$r['state'] ?? ''] ?? '-') }} + + + @if($memNoInt > 0 && $memLink) + {{ $memNoInt }} + @else + - + @endif + {{ $r['event_type'] ?? '-' }} + @if($ipN !== '') + {{ $ipN }} + @else + - + @endif + + @if($emailN !== '') + {{ $emailN }} + @else + - + @endif + + + + {{-- AJAX 없이: row별 JSON을 숨겨두고 모달에서 꺼내씀 --}} + +
데이터가 없습니다.
+
+ +
+ {{ $page->onEachSide(1)->links('vendor.pagination.admin') }} +
+
+ + + + +@endsection diff --git a/resources/views/admin/log/_audit_log_modal.blade.php b/resources/views/admin/log/_audit_log_modal.blade.php new file mode 100644 index 0000000..d776e46 --- /dev/null +++ b/resources/views/admin/log/_audit_log_modal.blade.php @@ -0,0 +1,61 @@ +{{-- resources/views/admin/log/_audit_log_modal.blade.php --}} + diff --git a/resources/views/admin/log/member_danalauthtel_log.blade.php b/resources/views/admin/log/member_danalauthtel_log.blade.php new file mode 100644 index 0000000..8eef598 --- /dev/null +++ b/resources/views/admin/log/member_danalauthtel_log.blade.php @@ -0,0 +1,235 @@ +@extends('admin.layouts.app') + +@section('title', '다날 휴대폰 본인인증 로그') +@section('page_title', '다날 휴대폰 본인인증 로그') +@section('page_desc', 'mem_no / 전화번호로 조회 (이름/CI/DI 등 개인정보 미노출)') +@section('content_class', 'a-content--full') + +@push('head') + +@endpush + +@section('content') +@php +$memNo = (string)($filters['mem_no'] ?? ''); +$phone = (string)($filters['phone'] ?? ''); +$indexUrl = route('admin.systemlog.member-danalauthtel-logs', [], false); +@endphp + +
+
+
+
다날 휴대폰 본인인증 로그
+
mem_no / 전화번호만 검색 · 이름/CI/DI 등은 표시하지 않음
+
+ +
+
+ + +
+
+ + +
+ +
+ + 초기화 +
+
+
+
+ +
+
{{ $page->total() }}
+ +
+ + + + + + + + + + + + + + + @forelse(($rows ?? []) as $r) + @php + $seq = (int)($r['seq'] ?? 0); + $g = (string)($r['gubun'] ?? ''); + $rc = (string)($r['res_code'] ?? ''); + $mno = (int)($r['mem_no'] ?? 0); + $tid = (string)($r['TID'] ?? ''); + $dt = (string)($r['rgdate'] ?? ''); + + $gLabel = ($g === 'J') ? '가입' : (($g === 'M') ? '수정' : '-'); + $resOk = ($rc === '0000'); + $resCls = $resOk ? 'pill--ok' : 'pill--bad'; + + $phoneDisp = (string)($r['phone_display'] ?? ''); + $memberUrl = $mno > 0 ? ('/members/'.$mno) : ''; + @endphp + + + + + + + + + + + + @empty + + @endforelse + +
SEQ구분결과회원번호전화번호TID인증시간
{{ $seq }}{{ $gLabel }}{{ $rc !== '' ? $rc : '-' }} + @if($mno > 0) + {{ $mno }} + @else + 가입차단/미생성 + @endif + + @if($phoneDisp !== '') + {{ $phoneDisp }} + @else + - + @endif + {{ $tid !== '' ? $tid : '-' }}{{ $dt !== '' ? $dt : '-' }} + + + {{-- row별 JSON(모달용) - 안전하게 script에 저장 --}} + +
데이터가 없습니다.
+
+ +
+ {{ $page->onEachSide(1)->links('vendor.pagination.admin') }} +
+
+ +{{-- Modal --}} + + + +@endsection diff --git a/resources/views/admin/partials/sidebar.blade.php b/resources/views/admin/partials/sidebar.blade.php index 6659184..f96a742 100644 --- a/resources/views/admin/partials/sidebar.blade.php +++ b/resources/views/admin/partials/sidebar.blade.php @@ -46,16 +46,35 @@ ], ], [ - 'title' => '상품권 관리', - 'items' => [ - ['label' => '상품 리스트', 'route' => 'admin.products.index','roles' => ['super_admin','product']], - ['label' => '상품 등록', 'route' => 'admin.products.create','roles' => ['super_admin','product']], - ['label' => '판매 코드 관리', 'route' => 'admin.sale-codes.index','roles' => ['super_admin','product']], - ['label' => '핀 번호 관리', 'route' => 'admin.pins.index','roles' => ['super_admin','product']], - ['label' => '메인 노출 관리', 'route' => 'admin.exposure.index','roles' => ['super_admin','product']], - ['label' => '결제 수수료/정책', 'route' => 'admin.fees.index','roles' => ['super_admin','product']], + 'title' => '상품 관리', + 'items' => [ + // 1) 기본 데이터(전시 구조) + ['label' => '카테고리 관리', 'route' => 'admin.categories.index', 'roles' => ['super_admin','product']], // ✅ 추천(신규) + + // 2) 상품 등록/관리 + ['label' => '상품 리스트', 'route' => 'admin.products.index', 'roles' => ['super_admin','product']], + ['label' => '상품 등록', 'route' => 'admin.products.create', 'roles' => ['super_admin','product']], + + // 3) SKU/가격/권종 (상품 상세에서 같이 관리해도 되지만, 별도 메뉴가 있으면 운영 편함) + ['label' => '금액권/가격 관리', 'route' => 'admin.skus.index', 'roles' => ['super_admin','product']], // ✅ 추천(신규) + + // 4) 판매채널/연동코드 (DANAL/KORCULTURE/KPREPAID 코드 매핑) + ['label' => '판매 코드 관리', 'route' => 'admin.sale-codes.index', 'roles' => ['super_admin','product']], + + // 5) 자산(이미지) 관리 + ['label' => '이미지 라이브러리', 'route' => 'admin.media.index', 'roles' => ['super_admin','product']], // ✅ 추천(신규) + + // 6) 자사 핀 재고/회수/추출 + ['label' => '핀 번호 관리', 'route' => 'admin.pins.index', 'roles' => ['super_admin','product']], + // ['label' => '핀 회수/추출', 'route' => 'admin.pins.recalls', 'roles' => ['super_admin','product']], // ✅ 핀 메뉴 내부 탭으로 처리해도 OK + + // 7) 메인 노출/전시 + ['label' => '메인 노출 관리', 'route' => 'admin.exposure.index', 'roles' => ['super_admin','product']], + + // 8) 결제 정책(상품쪽에서 설정하지만 성격상 “정책”이므로 아래쪽) + ['label' => '결제 수수료/정책', 'route' => 'admin.fees.index', 'roles' => ['super_admin','product']], + ], ], - ], [ 'title' => '매입/정산', 'items' => [ @@ -75,11 +94,13 @@ [ 'title' => '시스템 로그', 'items' => [ - ['label' => '로그인 로그', 'route' => 'admin.logs.login','roles' => ['super_admin','finance','product','support']], - ['label' => '다날 인증 로그', 'route' => 'admin.logs.danal','roles' => ['super_admin','finance','product','support']], - ['label' => '결제 로그', 'route' => 'admin.logs.pay','roles' => ['super_admin','finance','product','support']], - ['label' => '기타 로그', 'route' => 'admin.logs.misc','roles' => ['super_admin','finance','product','support']], - ['label' => '관리자 활동 로그', 'route' => 'admin.logs.audit','roles' => ['super_admin','finance','product','support']], + ['label' => '다날 결제 로그', 'route' => 'admin.logs.pay','roles' => ['super_admin','finance','product','support']], + ['label' => '회원 다날 인증 로그', 'route' => 'admin.systemlog.member-danalauthtel-logs','roles' => ['super_admin']], + ['label' => '회원 계좌번호 성명인증 로그', 'route' => 'admin.systemlog.member-account-logs','roles' => ['super_admin']], + ['label' => '회원 비밀번호변경 로그', 'route' => 'admin.systemlog.member-passwd-modify-logs','roles' => ['super_admin']], + ['label' => '회원 로그인 로그', 'route' => 'admin.systemlog.member-login-logs','roles' => ['super_admin']], + ['label' => '회원 가입차단/알림 로그', 'route' => 'admin.systemlog.member-join-logs','roles' => ['super_admin']], + ['label' => '관리자 활동 로그', 'route' => 'admin.systemlog.admin-audit-logs','roles' => ['super_admin']], ], ], ]; diff --git a/routes/admin.php b/routes/admin.php index 169aed3..c430ae5 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -14,7 +14,12 @@ use App\Http\Controllers\Admin\Qna\AdminQnaController; use App\Http\Controllers\Admin\Members\AdminMembersController; use App\Http\Controllers\Admin\Members\AdminMemberMarketingController; use App\Http\Controllers\Admin\Members\AdminMemberJoinFilterController; - +use App\Http\Controllers\Admin\Log\AdminAuditLogController; +use App\Http\Controllers\Admin\Log\MemberJoinLogController; +use App\Http\Controllers\Admin\Log\MemberLoginLogController; +use App\Http\Controllers\Admin\Log\MemberPasswdModifyLogController; +use App\Http\Controllers\Admin\Log\MemberAccountLogController; +use App\Http\Controllers\Admin\Log\MemberDanalAuthTelLogController; use Illuminate\Support\Facades\Route; Route::middleware(['web'])->group(function () { @@ -241,6 +246,33 @@ Route::middleware(['web'])->group(function () { ->name('destroy'); }); + Route::prefix('systemlog') + ->name('admin.systemlog.') + ->middleware('admin.role:super_admin') + ->group(function () { + + Route::get('/admin-audit-logs', [AdminAuditLogController::class, 'index']) + ->name('admin-audit-logs'); + + Route::get('/admin-audit-logs/{id}', [AdminAuditLogController::class, 'show']) + ->whereNumber('id') + ->name('admin-audit-logs.show'); + + Route::get('/member-join-logs', [MemberJoinLogController::class, 'index']) + ->name('member-join-logs'); + + Route::get('/member-login-logs', [MemberLoginLogController::class, 'index']) + ->name('member-login-logs'); + + Route::get('/member-passwd-modify-logs', [MemberPasswdModifyLogController::class, 'index']) + ->name('member-passwd-modify-logs'); + + Route::get('/member-account-logs', [MemberAccountLogController::class, 'index']) + ->name('member-account-logs'); + + Route::get('/member-danalauthtel-logs', [MemberDanalAuthTelLogController::class, 'index']) + ->name('member-danalauthtel-logs'); + }); }); });