회원관리 변경로그, 댓글 수정
This commit is contained in:
parent
4fb4f2ae32
commit
f4ac32ae33
@ -550,6 +550,7 @@ final class AdminUserRepository
|
|||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$id = (int)$r->id;
|
$id = (int)$r->id;
|
||||||
$map[$id] = [
|
$map[$id] = [
|
||||||
|
'id' => (int)$r->id,
|
||||||
'email' => (string)($r->email ?? ''),
|
'email' => (string)($r->email ?? ''),
|
||||||
'name' => (string)($r->name ?? ''),
|
'name' => (string)($r->name ?? ''),
|
||||||
'nick' => (string)($r->nickname ?? ''),
|
'nick' => (string)($r->nickname ?? ''),
|
||||||
|
|||||||
@ -78,18 +78,29 @@ final class AdminMemberService
|
|||||||
$plainPhone = $this->plainPhone((string)($member->cell_phone ?? ''));
|
$plainPhone = $this->plainPhone((string)($member->cell_phone ?? ''));
|
||||||
$phoneDisplay = $this->formatPhone($plainPhone);
|
$phoneDisplay = $this->formatPhone($plainPhone);
|
||||||
|
|
||||||
$adminMemo = $this->decodeJsonArray($member->admin_memo ?? null);
|
// ✅ 레거시 JSON 파싱 (admin_memo / modify_log)
|
||||||
$modifyLog = $this->decodeJsonArray($member->modify_log ?? null);
|
$adminMemoList = $this->legacyAdminMemoList($member->admin_memo ?? null); // old -> new normalize
|
||||||
$modifyLog = array_reverse($modifyLog);
|
$stateLogList = $this->legacyStateLogList($member->modify_log ?? null); // old -> new normalize
|
||||||
|
|
||||||
$actorIds = [];
|
// 화면은 최신이 위로 오게
|
||||||
|
$adminMemo = array_reverse($adminMemoList);
|
||||||
|
$modifyLog = array_reverse($stateLogList);
|
||||||
|
|
||||||
|
// ✅ adminMap 대상 admin_num 수집
|
||||||
|
$adminSet = [];
|
||||||
|
|
||||||
|
foreach ($adminMemo as $it) {
|
||||||
|
$aid = (int)($it['admin_num'] ?? 0);
|
||||||
|
if ($aid > 0) $adminSet[$aid] = true;
|
||||||
|
}
|
||||||
foreach ($modifyLog as $it) {
|
foreach ($modifyLog as $it) {
|
||||||
$aid = (int)($it['actor_admin_id'] ?? 0);
|
$aid = (int)($it['admin_num'] ?? 0);
|
||||||
if ($aid > 0) $actorIds[] = $aid;
|
if ($aid > 0) $adminSet[$aid] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$adminIds = array_keys($adminSet);
|
||||||
|
|
||||||
// 인증/주소/로그
|
// 인증/주소/로그 (기존 그대로)
|
||||||
$authRows = array_values(array_filter(
|
$authRows = array_values(array_filter(
|
||||||
$this->repo->getAuthRowsForMember($memNo),
|
$this->repo->getAuthRowsForMember($memNo),
|
||||||
fn($r)=> in_array((string)($r['auth_type'] ?? ''), self::AUTH_TYPES_SHOW, true)
|
fn($r)=> in_array((string)($r['auth_type'] ?? ''), self::AUTH_TYPES_SHOW, true)
|
||||||
@ -102,7 +113,7 @@ final class AdminMemberService
|
|||||||
// 계좌 표시(수정 불가 / 표시만)
|
// 계좌 표시(수정 불가 / 표시만)
|
||||||
$bank = $this->buildBankDisplay($member, $authInfo);
|
$bank = $this->buildBankDisplay($member, $authInfo);
|
||||||
|
|
||||||
$adminMap = $this->adminRepo->getMetaMapByIds($actorIds);
|
$adminMap = $this->adminRepo->getMetaMapByIds($adminIds);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'member' => $member,
|
'member' => $member,
|
||||||
@ -110,9 +121,12 @@ final class AdminMemberService
|
|||||||
'plainPhone' => $plainPhone,
|
'plainPhone' => $plainPhone,
|
||||||
'phoneDisplay' => $phoneDisplay,
|
'phoneDisplay' => $phoneDisplay,
|
||||||
|
|
||||||
|
// ✅ 레거시 기반 결과
|
||||||
'adminMemo' => $adminMemo,
|
'adminMemo' => $adminMemo,
|
||||||
'modifyLog' => $modifyLog,
|
'modifyLog' => $modifyLog,
|
||||||
|
'adminMap' => $adminMap,
|
||||||
|
|
||||||
|
// 기존 그대로
|
||||||
'authRows' => $authRows,
|
'authRows' => $authRows,
|
||||||
'authInfo' => $authInfo,
|
'authInfo' => $authInfo,
|
||||||
'authLogs' => $authLogs,
|
'authLogs' => $authLogs,
|
||||||
@ -122,30 +136,29 @@ final class AdminMemberService
|
|||||||
'genderMap' => $this->genderMap(),
|
'genderMap' => $this->genderMap(),
|
||||||
'nativeMap' => $this->nativeMap(),
|
'nativeMap' => $this->nativeMap(),
|
||||||
'corpMap' => $this->corpMap(),
|
'corpMap' => $this->corpMap(),
|
||||||
|
|
||||||
'bank' => $bank,
|
'bank' => $bank,
|
||||||
|
|
||||||
'modifyLog' => $modifyLog,
|
|
||||||
'adminMap' => $adminMap,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ✅ 업데이트 허용: stat_3(1~3만), cell_corp, cell_phone
|
* 업데이트 허용: stat_3(1~3만), cell_corp, cell_phone
|
||||||
* ❌ 금지: 이름/이메일/수신동의/계좌/기타
|
* 금지: 이름/이메일/수신동의/계좌/기타
|
||||||
*/
|
*/
|
||||||
public function updateMember(int $memNo, array $input, int $actorAdminId, string $ip = '', string $ua = ''): array
|
public function updateMember(int $memNo, array $input, int $actorAdminId, string $ip = '', string $ua = ''): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return DB::transaction(function () use ($memNo, $input, $actorAdminId, $ip, $ua) {
|
return DB::transaction(function () use ($memNo, $input, $actorAdminId) {
|
||||||
|
|
||||||
$before = $this->repo->lockMemberForUpdate($memNo);
|
$before = $this->repo->lockMemberForUpdate($memNo);
|
||||||
if (!$before) return $this->fail('회원을 찾을 수 없습니다.');
|
if (!$before) return $this->fail('회원을 찾을 수 없습니다.');
|
||||||
|
|
||||||
$beforeArr = (array)$before;
|
|
||||||
$data = [];
|
$data = [];
|
||||||
$changes = [];
|
|
||||||
|
|
||||||
// ✅ stat_3: 1~3만 변경 허용, 4~6은 시스템 상태로 변경 금지
|
// ✅ 기존 modify_log에서 레거시 state_log[] 뽑기
|
||||||
|
$stateLog = $this->legacyStateLogList($before->modify_log ?? null);
|
||||||
|
|
||||||
|
// ✅ stat_3 변경 (1~3만 변경 허용, 4~6 금지 정책 유지)
|
||||||
if (array_key_exists('stat_3', $input)) {
|
if (array_key_exists('stat_3', $input)) {
|
||||||
$s3 = (string)($input['stat_3'] ?? '');
|
$s3 = (string)($input['stat_3'] ?? '');
|
||||||
if (!in_array($s3, ['1','2','3','4','5','6'], true)) {
|
if (!in_array($s3, ['1','2','3','4','5','6'], true)) {
|
||||||
@ -155,34 +168,68 @@ final class AdminMemberService
|
|||||||
return $this->fail('4~6 상태는 시스템 상태로 관리자 변경이 불가합니다.');
|
return $this->fail('4~6 상태는 시스템 상태로 관리자 변경이 불가합니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($s3 !== (string)($before->stat_3 ?? '')) {
|
$beforeS3 = (string)($before->stat_3 ?? '');
|
||||||
|
if ($s3 !== $beforeS3) {
|
||||||
$data['stat_3'] = $s3;
|
$data['stat_3'] = $s3;
|
||||||
$data['dt_stat_3'] = now()->format('Y-m-d H:i:s');
|
$data['dt_stat_3'] = now()->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// ✅ 레거시 형식 로그
|
||||||
|
$stateLog[] = [
|
||||||
|
'when' => now()->format('y-m-d H:i:s'),
|
||||||
|
'after' => $s3,
|
||||||
|
'title' => '회원상태 접근권한 변경',
|
||||||
|
'before' => $beforeS3,
|
||||||
|
'admin_num' => (string)$actorAdminId,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 통신사
|
// ✅ 통신사 변경
|
||||||
if (array_key_exists('cell_corp', $input)) {
|
if (array_key_exists('cell_corp', $input)) {
|
||||||
$corp = (string)($input['cell_corp'] ?? 'n');
|
$corp = (string)($input['cell_corp'] ?? 'n');
|
||||||
$allowed = ['n','01','02','03','04','05','06'];
|
$allowed = ['n','01','02','03','04','05','06'];
|
||||||
if (!in_array($corp, $allowed, true)) return $this->fail('통신사 코드가 올바르지 않습니다.');
|
if (!in_array($corp, $allowed, true)) return $this->fail('통신사 코드가 올바르지 않습니다.');
|
||||||
if ($corp !== (string)($before->cell_corp ?? 'n')) $data['cell_corp'] = $corp;
|
|
||||||
|
$beforeCorp = (string)($before->cell_corp ?? 'n');
|
||||||
|
if ($corp !== $beforeCorp) {
|
||||||
|
$data['cell_corp'] = $corp;
|
||||||
|
|
||||||
|
$stateLog[] = [
|
||||||
|
'when' => now()->format('y-m-d H:i:s'),
|
||||||
|
'after' => $corp,
|
||||||
|
'title' => '통신사 변경',
|
||||||
|
'before' => $beforeCorp,
|
||||||
|
'admin_num' => (string)$actorAdminId,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 휴대폰(암호화 저장)
|
// ✅ 휴대폰 변경(암호화 저장)
|
||||||
if (array_key_exists('cell_phone', $input)) {
|
if (array_key_exists('cell_phone', $input)) {
|
||||||
$raw = trim((string)($input['cell_phone'] ?? ''));
|
$raw = trim((string)($input['cell_phone'] ?? ''));
|
||||||
if ($raw === '') {
|
|
||||||
// 전화번호 비우는 것 자체는 허용하되 운영 정책에 따라 막고 싶으면 여기서 fail 처리
|
|
||||||
$enc = '';
|
$enc = '';
|
||||||
} else {
|
$afterPlain = '';
|
||||||
|
if ($raw !== '') {
|
||||||
$phone = $this->normalizeKrPhone($raw);
|
$phone = $this->normalizeKrPhone($raw);
|
||||||
if ($phone === '') return $this->fail('휴대폰 번호 형식이 올바르지 않습니다.');
|
if ($phone === '') return $this->fail('휴대폰 번호 형식이 올바르지 않습니다.');
|
||||||
$enc = $this->encryptPhone($phone);
|
$enc = $this->encryptPhone($phone);
|
||||||
|
$afterPlain = $phone;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((string)($before->cell_phone ?? '') !== $enc) {
|
$beforeEnc = (string)($before->cell_phone ?? '');
|
||||||
|
if ($beforeEnc !== $enc) {
|
||||||
$data['cell_phone'] = $enc;
|
$data['cell_phone'] = $enc;
|
||||||
|
|
||||||
|
// 로그는 마스킹(평문 full 저장 원하면 여기만 변경)
|
||||||
|
$beforePlain = $this->plainPhone($beforeEnc);
|
||||||
|
$stateLog[] = [
|
||||||
|
'when' => now()->format('y-m-d H:i:s'),
|
||||||
|
'after' => $this->maskPhoneForLog($afterPlain),
|
||||||
|
'title' => '휴대폰번호 변경',
|
||||||
|
'before' => $this->maskPhoneForLog($beforePlain),
|
||||||
|
'admin_num' => (string)$actorAdminId,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,25 +237,12 @@ final class AdminMemberService
|
|||||||
return $this->ok('변경사항이 없습니다.');
|
return $this->ok('변경사항이 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($data as $k => $v) {
|
// ✅ 레거시 modify_log 저장: 반드시 {"state_log":[...]}
|
||||||
$beforeVal = $beforeArr[$k] ?? null;
|
$stateLog = $this->trimLegacyList($stateLog, 300);
|
||||||
if ((string)$beforeVal !== (string)$v) {
|
$data['modify_log'] = $this->encodeJsonObjectOrNull(['state_log' => $stateLog]);
|
||||||
$changes[$k] = ['before' => $beforeVal, 'after' => $v];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// modify_log append
|
// 최근정보변경일시
|
||||||
$modify = $this->decodeJsonArray($before->modify_log ?? null);
|
$data['dt_mod'] = now()->format('Y-m-d H:i:s');
|
||||||
$modify = $this->appendJson($modify, [
|
|
||||||
'ts' => now()->format('Y-m-d H:i:s'),
|
|
||||||
'actor_admin_id' => $actorAdminId,
|
|
||||||
'action' => 'member_update',
|
|
||||||
'ip' => $ip,
|
|
||||||
'ua' => $ua,
|
|
||||||
'changes' => $changes,
|
|
||||||
], 300);
|
|
||||||
|
|
||||||
$data['modify_log'] = $this->encodeJsonOrNull($modify);
|
|
||||||
|
|
||||||
$ok = $this->repo->updateMember($memNo, $data);
|
$ok = $this->repo->updateMember($memNo, $data);
|
||||||
if (!$ok) return $this->fail('저장에 실패했습니다.');
|
if (!$ok) return $this->fail('저장에 실패했습니다.');
|
||||||
@ -220,6 +254,7 @@ final class AdminMemberService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Maps
|
// Maps
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@ -345,31 +380,6 @@ final class AdminMemberService
|
|||||||
return str_repeat('*', max(0, mb_strlen($d)-4)).mb_substr($d, -4);
|
return str_repeat('*', max(0, mb_strlen($d)-4)).mb_substr($d, -4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// JSON helpers
|
|
||||||
// -------------------------
|
|
||||||
private function decodeJsonArray($jsonOrNull): array
|
|
||||||
{
|
|
||||||
if ($jsonOrNull === null) return [];
|
|
||||||
$s = trim((string)$jsonOrNull);
|
|
||||||
if ($s === '') return [];
|
|
||||||
$arr = json_decode($s, true);
|
|
||||||
return is_array($arr) ? $arr : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function encodeJsonOrNull(array $arr): ?string
|
|
||||||
{
|
|
||||||
if (empty($arr)) return null;
|
|
||||||
return json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function appendJson(array $list, array $entry, int $max = 200): array
|
|
||||||
{
|
|
||||||
array_unshift($list, $entry);
|
|
||||||
if (count($list) > $max) $list = array_slice($list, 0, $max);
|
|
||||||
return $list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addMemo(int $memNo, string $memo, int $actorAdminId, string $ip = '', string $ua = ''): array
|
public function addMemo(int $memNo, string $memo, int $actorAdminId, string $ip = '', string $ua = ''): array
|
||||||
{
|
{
|
||||||
$memo = trim($memo);
|
$memo = trim($memo);
|
||||||
@ -377,39 +387,28 @@ final class AdminMemberService
|
|||||||
if (mb_strlen($memo) > 1000) return $this->fail('메모는 1000자 이내로 입력해 주세요.');
|
if (mb_strlen($memo) > 1000) return $this->fail('메모는 1000자 이내로 입력해 주세요.');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return DB::transaction(function () use ($memNo, $memo, $actorAdminId, $ip, $ua) {
|
return DB::transaction(function () use ($memNo, $memo, $actorAdminId) {
|
||||||
|
|
||||||
// row lock
|
|
||||||
$before = $this->repo->lockMemberForUpdate($memNo);
|
$before = $this->repo->lockMemberForUpdate($memNo);
|
||||||
if (!$before) return $this->fail('회원을 찾을 수 없습니다.');
|
if (!$before) return $this->fail('회원을 찾을 수 없습니다.');
|
||||||
|
|
||||||
// 기존 memo json
|
// ✅ 기존 admin_memo에서 레거시 memo[] 뽑기
|
||||||
$adminMemo = $this->decodeJsonArray($before->admin_memo ?? null);
|
$list = $this->legacyAdminMemoList($before->admin_memo ?? null);
|
||||||
|
|
||||||
// append
|
// ✅ append (레거시 키 유지)
|
||||||
$adminMemo = $this->appendJson($adminMemo, [
|
$list[] = [
|
||||||
'ts' => now()->format('Y-m-d H:i:s'),
|
|
||||||
'actor_admin_id' => $actorAdminId,
|
|
||||||
'ip' => $ip,
|
|
||||||
'ua' => $ua,
|
|
||||||
'memo' => $memo,
|
'memo' => $memo,
|
||||||
], 300);
|
'when' => now()->format('y-m-d H:i:s'), // 레거시(2자리년도)
|
||||||
|
'admin_num' => (string)$actorAdminId,
|
||||||
|
];
|
||||||
|
$list = $this->trimLegacyList($list, 300);
|
||||||
|
|
||||||
// (선택) modify_log에도 남기기
|
// ✅ admin_memo는 반드시 {"memo":[...]} 형태
|
||||||
$modify = $this->decodeJsonArray($before->modify_log ?? null);
|
$obj = ['memo' => $list];
|
||||||
$modify = $this->appendJson($modify, [
|
|
||||||
'ts' => now()->format('Y-m-d H:i:s'),
|
|
||||||
'actor_admin_id' => $actorAdminId,
|
|
||||||
'action' => 'admin_memo_add',
|
|
||||||
'ip' => $ip,
|
|
||||||
'ua' => $ua,
|
|
||||||
'changes' => ['admin_memo' => ['before' => null, 'after' => 'added']],
|
|
||||||
], 300);
|
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'admin_memo' => $this->encodeJsonOrNull($adminMemo),
|
'admin_memo' => $this->encodeJsonObjectOrNull($obj),
|
||||||
'modify_log' => $this->encodeJsonOrNull($modify),
|
'dt_mod' => now()->format('Y-m-d H:i:s'),
|
||||||
'dt_mod' => now()->format('Y-m-d H:i:s'), // 최근정보변경일시 반영
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$ok = $this->repo->updateMember($memNo, $data);
|
$ok = $this->repo->updateMember($memNo, $data);
|
||||||
@ -423,6 +422,114 @@ final class AdminMemberService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private function ok(string $msg): array { return ['ok'=>true,'message'=>$msg]; }
|
private function ok(string $msg): array { return ['ok'=>true,'message'=>$msg]; }
|
||||||
private function fail(string $msg): array { return ['ok'=>false,'message'=>$msg]; }
|
private function fail(string $msg): array { return ['ok'=>false,'message'=>$msg]; }
|
||||||
|
|
||||||
|
private function decodeJsonObject($jsonOrNull): array
|
||||||
|
{
|
||||||
|
if ($jsonOrNull === null) return [];
|
||||||
|
$s = trim((string)$jsonOrNull);
|
||||||
|
if ($s === '') return [];
|
||||||
|
$arr = json_decode($s, true);
|
||||||
|
return is_array($arr) ? $arr : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function encodeJsonObjectOrNull(array $obj): ?string
|
||||||
|
{
|
||||||
|
if (empty($obj)) return null;
|
||||||
|
return json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ admin_memo: {"memo":[{memo,when,admin_num}, ...]}
|
||||||
|
private function legacyAdminMemoList($adminMemoJson): array
|
||||||
|
{
|
||||||
|
$obj = $this->decodeJsonObject($adminMemoJson);
|
||||||
|
|
||||||
|
if (isset($obj['memo']) && is_array($obj['memo'])) {
|
||||||
|
return $obj['memo'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 혹시 예전에 잘못 저장된 list 형태도 최대한 복구해서 보여주기
|
||||||
|
if (is_array($obj) && array_is_list($obj)) {
|
||||||
|
$out = [];
|
||||||
|
foreach ($obj as $it) {
|
||||||
|
if (!is_array($it)) continue;
|
||||||
|
$memo = (string)($it['memo'] ?? '');
|
||||||
|
if ($memo === '') continue;
|
||||||
|
|
||||||
|
$out[] = [
|
||||||
|
'memo' => $memo,
|
||||||
|
'when' => $this->legacyWhen((string)($it['when'] ?? $it['ts'] ?? '')),
|
||||||
|
'admin_num' => (string)($it['admin_num'] ?? $it['actor_admin_id'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ modify_log: {"state_log":[{when,after,title,before,admin_num}, ...]}
|
||||||
|
private function legacyStateLogList($modifyLogJson): array
|
||||||
|
{
|
||||||
|
$obj = $this->decodeJsonObject($modifyLogJson);
|
||||||
|
|
||||||
|
if (isset($obj['state_log']) && is_array($obj['state_log'])) {
|
||||||
|
return $obj['state_log'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 혹시 예전에 잘못 저장된 list 형태도 복구 시도
|
||||||
|
if (is_array($obj) && array_is_list($obj)) {
|
||||||
|
$out = [];
|
||||||
|
foreach ($obj as $it) {
|
||||||
|
if (!is_array($it)) continue;
|
||||||
|
|
||||||
|
$out[] = [
|
||||||
|
'when' => $this->legacyWhen((string)($it['when'] ?? $it['ts'] ?? '')),
|
||||||
|
'after' => (string)($it['after'] ?? ''),
|
||||||
|
'title' => (string)($it['title'] ?? $it['action'] ?? '변경'),
|
||||||
|
'before' => (string)($it['before'] ?? ''),
|
||||||
|
'admin_num' => (string)($it['admin_num'] ?? $it['actor_admin_id'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function legacyWhen(string $any): string
|
||||||
|
{
|
||||||
|
$any = trim($any);
|
||||||
|
if ($any === '') return now()->format('y-m-d H:i:s');
|
||||||
|
|
||||||
|
if (preg_match('/^\d{2}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/', $any)) return $any;
|
||||||
|
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/', $any)) {
|
||||||
|
try { return \Carbon\Carbon::parse($any)->format('y-m-d H:i:s'); }
|
||||||
|
catch (\Throwable $e) { return now()->format('y-m-d H:i:s'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
try { return \Carbon\Carbon::parse($any)->format('y-m-d H:i:s'); }
|
||||||
|
catch (\Throwable $e) { return now()->format('y-m-d H:i:s'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private function trimLegacyList(array $list, int $max = 300): array
|
||||||
|
{
|
||||||
|
$n = count($list);
|
||||||
|
if ($n <= $max) return $list;
|
||||||
|
return array_slice($list, $n - $max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function maskPhoneForLog(string $digits): string
|
||||||
|
{
|
||||||
|
$d = preg_replace('/\D+/', '', $digits) ?? '';
|
||||||
|
if ($d === '') return '';
|
||||||
|
if (strlen($d) <= 4) return str_repeat('*', strlen($d));
|
||||||
|
return substr($d, 0, 3) . str_repeat('*', max(0, strlen($d) - 7)) . substr($d, -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,6 @@ final class AdminQnaService
|
|||||||
private readonly AdminQnaRepository $repo,
|
private readonly AdminQnaRepository $repo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private function t(array &$timers, string $key, float $start): void
|
|
||||||
{
|
|
||||||
$timers[$key] = round((microtime(true) - $start) * 1000, 1); // ms
|
|
||||||
}
|
|
||||||
|
|
||||||
public function stateLabels(): array
|
public function stateLabels(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -24,6 +24,8 @@
|
|||||||
.lbtn:hover{background:rgba(255,255,255,.10);text-decoration:none;}
|
.lbtn:hover{background:rgba(255,255,255,.10);text-decoration:none;}
|
||||||
.lbtn--ghost{background:transparent;}
|
.lbtn--ghost{background:transparent;}
|
||||||
.lbtn--sm{padding:7px 10px;font-size:12px;border-radius:11px;}
|
.lbtn--sm{padding:7px 10px;font-size:12px;border-radius:11px;}
|
||||||
|
.lbtn--primary{background:rgba(59,130,246,.88);border-color:rgba(59,130,246,.95);color:#fff;}
|
||||||
|
.lbtn--primary:hover{background:rgba(59,130,246,.98);}
|
||||||
|
|
||||||
.pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:12px;
|
.pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:12px;
|
||||||
border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06);}
|
border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.06);}
|
||||||
@ -312,9 +314,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:12px;">
|
||||||
<div style="margin-top:14px;">
|
{{ $page->onEachSide(1)->links('vendor.pagination.admin') }}
|
||||||
{{ $page->links() }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,15 @@
|
|||||||
|
|
||||||
.warnbox{border:1px solid rgba(245,158,11,.35);background:rgba(245,158,11,.10);border-radius:16px;padding:12px;}
|
.warnbox{border:1px solid rgba(245,158,11,.35);background:rgba(245,158,11,.10);border-radius:16px;padding:12px;}
|
||||||
.warnbox b{font-weight:900;}
|
.warnbox b{font-weight:900;}
|
||||||
|
|
||||||
|
/* legacy log list */
|
||||||
|
.mlog{margin-top:12px; max-height:320px; overflow:auto; padding-right:6px;}
|
||||||
|
.mlog__item{padding:10px 12px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.03); border-radius:14px;}
|
||||||
|
.mlog__item + .mlog__item{margin-top:10px;}
|
||||||
|
.mlog__meta{display:flex; gap:8px; flex-wrap:wrap; align-items:center;}
|
||||||
|
.mlog__meta .mono{font-size:12px;}
|
||||||
|
.mlog__body{margin-top:8px;font-size:13px;line-height:1.6;}
|
||||||
|
.arrow{opacity:.7; padding:0 6px;}
|
||||||
</style>
|
</style>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@ -77,9 +86,6 @@
|
|||||||
|
|
||||||
$rcvE = (string)($member->rcv_email ?? 'n');
|
$rcvE = (string)($member->rcv_email ?? 'n');
|
||||||
$rcvS = (string)($member->rcv_sms ?? 'n');
|
$rcvS = (string)($member->rcv_sms ?? 'n');
|
||||||
|
|
||||||
// stat_3 select: 4~6은 disabled
|
|
||||||
$editableOptions = ['1','2','3'];
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
||||||
@ -95,7 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="lbtn lbtn--ghost lbtn--sm"
|
<a class="lbtn lbtn--ghost lbtn--sm"
|
||||||
href="{{ route('admin.members.index', request()->only(['q','stat_3','date_from','date_to','page'])) }}">
|
href="{{ route('admin.members.index', request()->only(['qf','q','stat_3','date_from','date_to','page'])) }}">
|
||||||
← 목록
|
← 목록
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -183,17 +189,13 @@
|
|||||||
<label class="a-label">회원상태</label>
|
<label class="a-label">회원상태</label>
|
||||||
<select class="a-input" name="stat_3">
|
<select class="a-input" name="stat_3">
|
||||||
@foreach($stat3Map as $k=>$label)
|
@foreach($stat3Map as $k=>$label)
|
||||||
@php
|
@php $disabled = in_array((string)$k, ['4','5','6'], true) ? 'disabled' : ''; @endphp
|
||||||
$disabled = in_array((string)$k, ['4','5','6'], true) ? 'disabled' : '';
|
|
||||||
@endphp
|
|
||||||
<option value="{{ $k }}" {{ (string)old('stat_3', $s3)===(string)$k ? 'selected' : '' }} {{ $disabled }}>
|
<option value="{{ $k }}" {{ (string)old('stat_3', $s3)===(string)$k ? 'selected' : '' }} {{ $disabled }}>
|
||||||
{{ $label }}
|
{{ $label }}
|
||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
<div class="a-muted" style="font-size:12px;margin-top:6px;">
|
<div class="a-muted" style="font-size:12px;margin-top:6px;">※ 4~6은 시스템 상태로 변경 불가</div>
|
||||||
※ 4~6은 시스템 상태로 변경 불가
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="a-field" style="margin-bottom:12px;">
|
<div class="a-field" style="margin-bottom:12px;">
|
||||||
@ -201,18 +203,14 @@
|
|||||||
<select class="a-input" name="cell_corp">
|
<select class="a-input" name="cell_corp">
|
||||||
@php $corpSel = (string)old('cell_corp', $member->cell_corp ?? 'n'); @endphp
|
@php $corpSel = (string)old('cell_corp', $member->cell_corp ?? 'n'); @endphp
|
||||||
@foreach($corpMap as $k=>$label)
|
@foreach($corpMap as $k=>$label)
|
||||||
<option value="{{ $k }}" {{ $corpSel===(string)$k ? 'selected' : '' }}>
|
<option value="{{ $k }}" {{ $corpSel===(string)$k ? 'selected' : '' }}>{{ $label }}</option>
|
||||||
{{ $label }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="a-field">
|
<div class="a-field">
|
||||||
<label class="a-label">전화번호(숫자만)</label>
|
<label class="a-label">전화번호(숫자만)</label>
|
||||||
<input class="a-input" name="cell_phone"
|
<input class="a-input" name="cell_phone" value="{{ old('cell_phone', $plainPhone ?? '') }}" placeholder="01000000000">
|
||||||
value="{{ old('cell_phone', $plainPhone ?? '') }}"
|
|
||||||
placeholder="01000000000">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="warnbox" style="margin-top:12px;">
|
<div class="warnbox" style="margin-top:12px;">
|
||||||
@ -254,9 +252,7 @@
|
|||||||
@php $st = (string)($r['auth_state'] ?? 'N'); @endphp
|
@php $st = (string)($r['auth_state'] ?? 'N'); @endphp
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class="mono">{{ $r['auth_type'] ?? '-' }}</span></td>
|
<td><span class="mono">{{ $r['auth_type'] ?? '-' }}</span></td>
|
||||||
<td>
|
<td><span class="pill {{ $st==='Y' ? 'pill--ok' : 'pill--muted' }}">● {{ $st }}</span></td>
|
||||||
<span class="pill {{ $st==='Y' ? 'pill--ok' : 'pill--muted' }}">● {{ $st }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="a-muted">{{ $r['auth_date'] ?? '-' }}</td>
|
<td class="a-muted">{{ $r['auth_date'] ?? '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
@ -293,12 +289,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- 관리자 메모 --}}
|
{{-- ✅ 관리자 메모 (레거시: when/admin_num/memo) --}}
|
||||||
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;">
|
<div style="display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;">
|
||||||
<div>
|
<div><div style="font-weight:900;">관리자 메모</div></div>
|
||||||
<div style="font-weight:900;">관리자 메모</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" action="{{ route('admin.members.memo.add', ['memNo'=>$no]) }}"
|
<form method="POST" action="{{ route('admin.members.memo.add', ['memNo'=>$no]) }}"
|
||||||
style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;">
|
style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;">
|
||||||
@ -311,22 +305,20 @@
|
|||||||
<div class="memo" style="margin-top:12px;">
|
<div class="memo" style="margin-top:12px;">
|
||||||
<div style="display:grid; gap:10px;">
|
<div style="display:grid; gap:10px;">
|
||||||
@forelse($adminMemo as $it)
|
@forelse($adminMemo as $it)
|
||||||
<div class="memo__item">
|
|
||||||
<div class="memo__meta">
|
|
||||||
<span class="mono">{{ $it['ts'] ?? '-' }}</span>
|
|
||||||
@php
|
@php
|
||||||
$aid = (int)($it['actor_admin_id'] ?? 0);
|
$aid = (int)($it['admin_num'] ?? 0);
|
||||||
$am = $aid > 0 ? ($adminMap[$aid] ?? null) : null;
|
$am = $aid > 0 ? ($adminMap[$aid] ?? null) : null;
|
||||||
|
|
||||||
$aEmail = is_array($am) ? trim((string)($am['email'] ?? '')) : '';
|
$aEmail = is_array($am) ? trim((string)($am['email'] ?? '')) : '';
|
||||||
$aName = is_array($am) ? trim((string)($am['name'] ?? '')) : '';
|
$aName = is_array($am) ? trim((string)($am['name'] ?? '')) : '';
|
||||||
|
|
||||||
// 출력: email / 이름
|
|
||||||
$aDisp = trim(($aEmail !== '' ? $aEmail : '-')." / ".($aName !== '' ? $aName : '-'));
|
$aDisp = trim(($aEmail !== '' ? $aEmail : '-')." / ".($aName !== '' ? $aName : '-'));
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
|
<div class="memo__item">
|
||||||
|
<div class="memo__meta">
|
||||||
|
<span class="mono">{{ $it['when'] ?? '-' }}</span>
|
||||||
<span class="mono">admin : {{ $aDisp }}</span>
|
<span class="mono">admin : {{ $aDisp }}</span>
|
||||||
@if(!empty($it['ip'])) <span class="mono">{{ $it['ip'] }}</span> @endif
|
|
||||||
</div>
|
</div>
|
||||||
<div style="white-space:pre-wrap; font-size:13px;">{{ $it['memo'] ?? '' }}</div>
|
<div style="white-space:pre-wrap; font-size:13px;">{{ $it['memo'] ?? '' }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -337,79 +329,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- 관리자 변경이력(modify_log) --}}
|
{{-- ✅ 관리자 변경이력 (레거시: state_log) --}}
|
||||||
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
<div class="a-card" style="padding:16px; margin-bottom:16px;">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
|
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
|
||||||
<div style="font-weight:900;">관리자 변경이력</div>
|
<div style="font-weight:900;">관리자 변경이력</div>
|
||||||
<div class="a-muted" style="font-size:12px;">
|
<div class="a-muted" style="font-size:12px;">너무 길면 아래 영역이 스크롤됩니다.</div>
|
||||||
너무 길면 아래 영역이 스크롤됩니다.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.mlog{margin-top:12px; max-height:320px; overflow:auto; padding-right:6px;}
|
|
||||||
.mlog__item{padding:10px 12px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.03); border-radius:14px;}
|
|
||||||
.mlog__item + .mlog__item{margin-top:10px;}
|
|
||||||
|
|
||||||
.mlog__meta{display:flex; gap:8px; flex-wrap:wrap; align-items:center;}
|
|
||||||
.mlog__meta .mono{font-size:12px;}
|
|
||||||
|
|
||||||
/* changes: 한 줄씩 */
|
|
||||||
.mlog__changes{margin-top:8px; display:grid; gap:6px;}
|
|
||||||
.chgline{display:flex; gap:8px; align-items:baseline; flex-wrap:wrap;}
|
|
||||||
.chgline__k{font-size:12px; opacity:.85; font-weight:800;}
|
|
||||||
.chgline__v{font-size:12px; opacity:.95;}
|
|
||||||
.arrow{opacity:.7; padding:0 4px;}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="mlog">
|
<div class="mlog">
|
||||||
<div style="display:grid; gap:10px;">
|
<div style="display:grid; gap:10px;">
|
||||||
@forelse($modifyLog as $it)
|
@forelse($modifyLog as $it)
|
||||||
@php
|
@php
|
||||||
$aid = (int)($it['actor_admin_id'] ?? 0);
|
$aid = (int)($it['admin_num'] ?? 0);
|
||||||
$am = $aid > 0 ? ($adminMap[$aid] ?? null) : null;
|
$am = $aid > 0 ? ($adminMap[$aid] ?? null) : null;
|
||||||
$aLabel = '-';
|
|
||||||
if (is_array($am)) {
|
$aEmail = is_array($am) ? trim((string)($am['email'] ?? '')) : '';
|
||||||
$name = trim((string)($am['name'] ?? ''));
|
$aName = is_array($am) ? trim((string)($am['name'] ?? '')) : '';
|
||||||
$email = trim((string)($am['email'] ?? ''));
|
$aDisp = trim(($aEmail !== '' ? $aEmail : '-')." / ".($aName !== '' ? $aName : '-'));
|
||||||
$aLabel = $email." / ".$name;
|
|
||||||
}
|
$title = (string)($it['title'] ?? '변경');
|
||||||
|
$before = (string)($it['before'] ?? '');
|
||||||
|
$after = (string)($it['after'] ?? '');
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="mlog__item">
|
<div class="mlog__item">
|
||||||
<div class="mlog__meta">
|
<div class="mlog__meta">
|
||||||
<span class="mono">{{ $it['ts'] ?? '-' }}</span>
|
<span class="mono">{{ $it['when'] ?? '-' }}</span>
|
||||||
|
<span class="mono">admin : {{ $aDisp }}</span>
|
||||||
<span class="mono"> admin : {!! ' '.$aLabel !!}</span>
|
<span class="mono">{{ $title }}</span>
|
||||||
|
|
||||||
@if(!empty($it['action'])) <span class="mono">{{ $it['action'] }}</span> @endif
|
|
||||||
@if(!empty($it['ip'])) <span class="mono">{{ $it['ip'] }}</span> @endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(!empty($it['changes']) && is_array($it['changes']))
|
<div class="mlog__body">
|
||||||
<div class="mlog__changes">
|
<b>{{ $before !== '' ? $before : '-' }}</b>
|
||||||
@foreach($it['changes'] as $k=>$chg)
|
|
||||||
@php
|
|
||||||
$b = $chg['before'] ?? null;
|
|
||||||
$a = $chg['after'] ?? null;
|
|
||||||
|
|
||||||
$before = is_scalar($b) ? (string)$b : '[...]';
|
|
||||||
$after = is_scalar($a) ? (string)$a : '[...]';
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<div class="chgline">
|
|
||||||
<span class="chgline__k">{{ $k }}</span>
|
|
||||||
<span class="chgline__v">
|
|
||||||
{{ $before }}
|
|
||||||
<span class="arrow">→</span>
|
<span class="arrow">→</span>
|
||||||
{{ $after }}
|
<b>{{ $after !== '' ? $after : '-' }}</b>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div class="a-muted" style="font-size:12px; margin-top:6px;">변경 상세 없음</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<div class="a-muted">변경이력이 없습니다.</div>
|
<div class="a-muted">변경이력이 없습니다.</div>
|
||||||
@ -418,21 +372,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{{-- 하단 액션바 --}}
|
{{-- 하단 액션바 --}}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="lbtn lbtn--ghost"
|
<a class="lbtn lbtn--ghost"
|
||||||
href="{{ route('admin.members.index', request()->only(['q','stat_3','date_from','date_to','page'])) }}">
|
href="{{ route('admin.members.index', request()->only(['qf','q','stat_3','date_from','date_to','page'])) }}">
|
||||||
← 뒤로가기
|
← 뒤로가기
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="actions__right">
|
<div class="actions__right">
|
||||||
<button class="lbtn lbtn--primary lbtn--wide"
|
<button class="lbtn lbtn--primary lbtn--wide" form="memberEditForm" type="submit" data-submit="save">
|
||||||
form="memberEditForm"
|
|
||||||
type="submit"
|
|
||||||
data-submit="save">
|
|
||||||
저장
|
저장
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
'title' => '회원/정책',
|
'title' => '회원/정책',
|
||||||
'items' => [
|
'items' => [
|
||||||
['label' => '회원 관리', 'route' => 'admin.members.index','roles' => ['super_admin','support']],
|
['label' => '회원 관리', 'route' => 'admin.members.index','roles' => ['super_admin','support']],
|
||||||
['label' => '회원가입 필터 설정', 'route' => 'admin.signup-filter.index','roles' => ['super_admin','support']],
|
['label' => '회원 데이터추출', 'route' => 'admin.signup-filter.index','roles' => ['super_admin','support']],
|
||||||
|
['label' => '로그인/가입 아이피 필터설정', 'route' => 'admin.signup-filter.index','roles' => ['super_admin','support']],
|
||||||
['label' => '블랙리스트/제재', 'route' => 'admin.sanctions.index','roles' => ['super_admin','support']],
|
['label' => '블랙리스트/제재', 'route' => 'admin.sanctions.index','roles' => ['super_admin','support']],
|
||||||
['label' => '마케팅 수신동의', 'route' => 'admin.marketing.index','roles' => ['super_admin','support']],
|
['label' => '마케팅 수신동의', 'route' => 'admin.marketing.index','roles' => ['super_admin','support']],
|
||||||
],
|
],
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
<style>
|
<style>
|
||||||
/* qna index only - match notice/admins */
|
|
||||||
.bar{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;}
|
.bar{display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;}
|
||||||
.bar__left .t{font-weight:900;font-size:16px;}
|
.bar__left .t{font-weight:900;font-size:16px;}
|
||||||
.bar__left .d{font-size:12px;margin-top:4px;}
|
.bar__left .d{font-size:12px;margin-top:4px;}
|
||||||
@ -41,6 +40,7 @@
|
|||||||
.clip{max-width:520px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;vertical-align:bottom;}
|
.clip{max-width:520px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;vertical-align:bottom;}
|
||||||
.row-mine{background:rgba(59,130,246,.06);}
|
.row-mine{background:rgba(59,130,246,.06);}
|
||||||
.table td{vertical-align:top;}
|
.table td{vertical-align:top;}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@ -202,7 +202,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:12px;">
|
<div style="margin-top:12px;">
|
||||||
{{-- ✅ 공지사항과 동일한 커스텀 페이징 + 쿼리 유지 --}}
|
|
||||||
{{ $rows->onEachSide(1)->links('vendor.pagination.admin') }}
|
{{ $rows->onEachSide(1)->links('vendor.pagination.admin') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user