747 lines
26 KiB
PHP
747 lines
26 KiB
PHP
<?php
|
|
|
|
namespace App\Repositories\Member;
|
|
|
|
use App\Models\Member\MemAuth;
|
|
use App\Models\Member\MemAuthInfo;
|
|
use App\Models\Member\MemAuthLog;
|
|
use App\Models\Member\MemInfo;
|
|
use App\Models\Member\MemJoinFilter;
|
|
use App\Models\Member\MemJoinLog;
|
|
use App\Support\Legacy\Carrier;
|
|
use App\Services\SmsService;
|
|
use App\Services\MemInfoService;
|
|
use App\Support\LegacyCrypto\CiSeedCrypto;
|
|
use App\Support\LegacyCrypto\CiPassword;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
|
|
class MemberAuthRepository
|
|
{
|
|
public function __construct(private readonly MemInfoService $memInfoService)
|
|
{
|
|
}
|
|
|
|
/* =========================================================
|
|
* mem_auth (기존)
|
|
* ========================================================= */
|
|
|
|
public function upsertState(
|
|
int $memNo,
|
|
string $authType,
|
|
string $authState,
|
|
?string $authDate = null
|
|
): void {
|
|
$authDate = $authDate ?: Carbon::now()->toDateString();
|
|
|
|
DB::table('mem_auth')->updateOrInsert(
|
|
['mem_no' => $memNo, 'auth_type' => $authType],
|
|
['auth_state' => $authState, 'auth_date' => $authDate]
|
|
);
|
|
}
|
|
|
|
public function markRequested(int $memNo, string $authType, array $logInfo = []): void
|
|
{
|
|
$this->setStateWithLog($memNo, $authType, MemAuth::STATE_R, MemAuthLog::STATE_P, $logInfo);
|
|
}
|
|
|
|
public function markProcessing(int $memNo, string $authType, array $logInfo = []): void
|
|
{
|
|
$this->setStateWithLog($memNo, $authType, MemAuth::STATE_P, MemAuthLog::STATE_P, $logInfo);
|
|
}
|
|
|
|
public function markSuccess(int $memNo, string $authType, array $logInfo = []): void
|
|
{
|
|
$this->setStateWithLog($memNo, $authType, MemAuth::STATE_Y, MemAuthLog::STATE_S, $logInfo);
|
|
}
|
|
|
|
public function markFail(int $memNo, string $authType, array $logInfo = []): void
|
|
{
|
|
$this->setStateWithLog($memNo, $authType, MemAuth::STATE_N, MemAuthLog::STATE_F, $logInfo);
|
|
}
|
|
|
|
public function mergeAuthInfo(int $memNo, string $authType, array $payload): void
|
|
{
|
|
DB::transaction(function () use ($memNo, $authType, $payload) {
|
|
$row = MemAuthInfo::query()->find($memNo);
|
|
|
|
if (!$row) {
|
|
$row = new MemAuthInfo();
|
|
$row->mem_no = $memNo;
|
|
$row->auth_info = [];
|
|
}
|
|
|
|
$data = $row->auth_info ?: [];
|
|
$data[$authType] = array_merge($data[$authType] ?? [], $payload);
|
|
|
|
$row->auth_info = $data;
|
|
$row->save();
|
|
});
|
|
}
|
|
|
|
private function setStateWithLog(
|
|
int $memNo,
|
|
string $authType,
|
|
string $authState,
|
|
string $logState,
|
|
array $logInfo
|
|
): void {
|
|
DB::transaction(function () use ($memNo, $authType, $authState, $logState, $logInfo) {
|
|
$this->upsertState($memNo, $authType, $authState);
|
|
|
|
MemAuthLog::query()->create([
|
|
'mem_no' => $memNo,
|
|
'type' => $authType,
|
|
'state' => $logState,
|
|
'info' => $logInfo,
|
|
'rgdate' => Carbon::now()->toDateTimeString(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
public function getState(int $memNo, string $authType): ?string
|
|
{
|
|
return DB::table('mem_auth')
|
|
->where('mem_no', $memNo)
|
|
->where('auth_type', $authType)
|
|
->value('auth_state');
|
|
}
|
|
|
|
private function normalizeKoreanPhone(string $raw): ?string
|
|
{
|
|
$digits = preg_replace('/\D+/', '', $raw ?? '');
|
|
|
|
if (strlen($digits) !== 11) {
|
|
return null;
|
|
}
|
|
if (substr($digits, 0, 3) !== '010') {
|
|
return null;
|
|
}
|
|
|
|
return $digits; // ✅ 무조건 11자리 숫자만 리턴
|
|
}
|
|
|
|
/**
|
|
* filter 컬럼에 phone/ip/ip_c가 들어있다는 전제의 기본 구현.
|
|
* - join_block: A 차단 / S 주의(알림) / N 비활성
|
|
*/
|
|
public function checkJoinFilter(string $phone, string $ip4 = '', string $ip4c = ''): ?array
|
|
{
|
|
$targets = array_values(array_filter([$phone, $ip4, $ip4c]));
|
|
if (!$targets) return null;
|
|
|
|
$rows = MemJoinFilter::query()
|
|
->whereIn('filter', $targets)
|
|
->where(function ($q) {
|
|
$q->whereNull('join_block')->orWhere('join_block', '!=', 'N');
|
|
})
|
|
->orderByDesc('seq')
|
|
->get();
|
|
|
|
if ($rows->isEmpty()) return null;
|
|
|
|
foreach ($rows as $r) {
|
|
if ((string)$r->join_block === 'A') {
|
|
return ['hit' => true, 'block' => true, 'gubun' => $r->gubun ?? 'filter_block', 'row' => $r];
|
|
}
|
|
}
|
|
foreach ($rows as $r) {
|
|
if ((string)$r->join_block === 'S') {
|
|
return ['hit' => true, 'block' => false, 'gubun' => $r->gubun ?? 'filter_notice', 'row' => $r];
|
|
}
|
|
}
|
|
|
|
$r = $rows->first();
|
|
return ['hit' => true, 'block' => false, 'gubun' => $r->gubun ?? 'filter_hit', 'row' => $r];
|
|
}
|
|
|
|
/**
|
|
* Step0 통합 처리
|
|
*/
|
|
public function step0PhoneCheck(string $rawPhone, string $carrier = '', string $ip4 = ''): array
|
|
{
|
|
$base = [
|
|
'ok' => false,
|
|
'reason' => '',
|
|
'message' => '',
|
|
'redirect' => null,
|
|
'phone' => null,
|
|
'carrier' => null,
|
|
'filter' => null,
|
|
'admin_phones' => [],
|
|
];
|
|
|
|
// 0) IP 필터 먼저
|
|
$ip4c = $this->ipToCClass($ip4);
|
|
$ipHit = $this->checkJoinFilterByIp($ip4, $ip4c);
|
|
|
|
$carrier = trim($carrier);
|
|
$base['carrier'] = $carrier ?: null;
|
|
|
|
if ($ipHit) {
|
|
$base['filter'] = $ipHit;
|
|
$base['admin_phones'] = $ipHit['admin_phones'] ?? [];
|
|
}
|
|
|
|
if ($ipHit && ($ipHit['join_block'] ?? '') === 'A') {
|
|
return array_merge($base, [
|
|
'ok' => false,
|
|
'reason' => 'blocked_ip',
|
|
'message' => '현재 가입이 제한된 정보입니다. 고객센터로 문의해 주세요.',
|
|
]);
|
|
}
|
|
|
|
// 1) 전화번호 검증
|
|
$phone = $this->normalizeKoreanPhone($rawPhone);
|
|
if (!$phone) {
|
|
return array_merge($base, [
|
|
'ok' => false,
|
|
'reason' => 'invalid_phone',
|
|
'message' => '휴대폰 번호 형식이 올바르지 않습니다.',
|
|
]);
|
|
}
|
|
$base['phone'] = $phone;
|
|
|
|
// 2) 이미 회원인지 체크
|
|
$member = $this->findMemberByPhone($phone, $carrier);
|
|
if ($member && !empty($member->mem_no)) {
|
|
return array_merge($base, [
|
|
'ok' => false,
|
|
'reason' => 'already_member',
|
|
'message' => "이미 가입된 전화번호 입니다.\n\n아이디 찾기로 이동할까요?",
|
|
'redirect' => route('web.auth.find_id'),
|
|
'matched_mem_no' => (int) $member->mem_no,
|
|
'matched_cell_corp' => $member->cell_corp ?? null, // ✅ 필요시
|
|
]);
|
|
}
|
|
|
|
// 3) 기존 phone+ip 필터
|
|
$filter = $this->checkJoinFilter($phone, $ip4, $ip4c);
|
|
if ($filter && ($filter['block'] ?? false) === true) {
|
|
return array_merge($base, [
|
|
'ok' => false,
|
|
'reason' => 'blocked',
|
|
'filter' => $filter,
|
|
'message' => '현재 가입이 제한된 정보입니다. 고객센터로 문의해 주세요.',
|
|
]);
|
|
}
|
|
|
|
// 4) 성공(가입 가능)
|
|
return array_merge($base, [
|
|
'ok' => true,
|
|
'reason' => 'ok',
|
|
'filter' => $filter ?: $ipHit,
|
|
'message' => "회원가입 가능한 전화번호 입니다.\n\n약관동의 페이지로 이동합니다.",
|
|
'redirect' => route('web.auth.register.terms'),
|
|
]);
|
|
}
|
|
|
|
private function findMemberByPhone(string $phone, string $carrier): ?object
|
|
{
|
|
/** @var CiSeedCrypto $seed */
|
|
$seed = app(CiSeedCrypto::class);
|
|
$phoneEnc = $seed->encrypt($phone);
|
|
|
|
// 실제로 매칭되는 회원 1건을 가져옴
|
|
return DB::table('mem_info')
|
|
->select('mem_no', 'cell_phone')
|
|
->where('cell_phone', $phoneEnc)
|
|
->where('cell_corp', $carrier)
|
|
->limit(1)
|
|
->first();
|
|
}
|
|
|
|
public function existsEmail(string $email): bool
|
|
{
|
|
$email = trim($email);
|
|
if ($email === '') return false;
|
|
|
|
return DB::table('mem_info')
|
|
->where('email', $email) // ✅ mem_info.email
|
|
->exists();
|
|
}
|
|
|
|
|
|
public function ipToCClass(string $ip): string
|
|
{
|
|
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
return '';
|
|
}
|
|
$p = explode('.', $ip);
|
|
return count($p) === 4 ? ($p[0] . '.' . $p[1] . '.' . $p[2] . '.0') : '';
|
|
}
|
|
|
|
private function checkJoinFilterByIp(string $ip4, string $ip4c): ?array
|
|
{
|
|
// IPv4 아니면 필터 적용 안 함
|
|
if (!filter_var($ip4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
return null;
|
|
}
|
|
|
|
// 우선순위: A(차단) > S(관리자알림) > N(무시)
|
|
$row = DB::table('mem_join_filter')
|
|
->whereIn('join_block', ['A', 'S'])
|
|
->where(function ($q) use ($ip4, $ip4c) {
|
|
// ✅ C class (보통 gubun_code='01', filter='162.168.0.0')
|
|
$q->where(function ($q2) use ($ip4c) {
|
|
$q2->where('gubun_code', '01')
|
|
->where('filter', $ip4c);
|
|
});
|
|
|
|
// ✅ D class (보통 gubun_code='02', filter='162.168.0.2')
|
|
$q->orWhere(function ($q2) use ($ip4) {
|
|
$q2->where('gubun_code', '02')
|
|
->where('filter', $ip4);
|
|
});
|
|
|
|
// ✅ (레거시) gubun_code가 01인데도 filter에 단일 IP가 들어있는 케이스 방어
|
|
$q->orWhere(function ($q2) use ($ip4) {
|
|
$q2->where('gubun_code', '01')
|
|
->where('filter', $ip4);
|
|
});
|
|
})
|
|
->orderByRaw("CASE join_block WHEN 'A' THEN 0 WHEN 'S' THEN 1 ELSE 9 END")
|
|
->limit(1)
|
|
->first();
|
|
|
|
if (!$row) return null;
|
|
|
|
$adminPhones = $this->parseAdminPhones($row->admin_phone ?? '');
|
|
|
|
return [
|
|
'gubun_code' => $row->gubun_code ?? null,
|
|
'join_block' => $row->join_block ?? null,
|
|
'filter' => $row->filter ?? null,
|
|
'admin_phones' => $adminPhones,
|
|
'ip' => $ip4,
|
|
'ip4c' => $ip4c,
|
|
];
|
|
}
|
|
|
|
private function parseAdminPhones($value): array
|
|
{
|
|
// admin_phone: ["010-3682-8958", ...] 형태(=JSON 배열)라고 했으니 JSON 우선
|
|
if (is_array($value)) return $value;
|
|
|
|
$s = trim((string)$value);
|
|
if ($s === '') return [];
|
|
|
|
$json = json_decode($s, true);
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
|
|
return array_values(array_filter(array_map('trim', $json)));
|
|
}
|
|
|
|
// 혹시 "010...,010..." 같은 문자열이면 콤마 분리 fallback
|
|
$parts = array_map('trim', explode(',', $s));
|
|
return array_values(array_filter($parts));
|
|
}
|
|
|
|
public function createMemberFromSignup(array $final, array $sessionAll = []): array
|
|
{
|
|
// 0) 필수값 최소 체크(컨트롤러에서 검증했지만 방어)
|
|
$email = strtolower(trim((string)($final['email'] ?? '')));
|
|
$pwPlain = (string)($final['password_plain'] ?? '');
|
|
$pin2 = (string)($final['pin2_plain'] ?? '');
|
|
|
|
if ($email === '' || $pwPlain === '' || !preg_match('/^\d{4}$/', $pin2)) {
|
|
throw new \RuntimeException('invalid_payload');
|
|
}
|
|
|
|
$pass = (array)($final['pass'] ?? []);
|
|
$name = (string)($pass['name'] ?? '');
|
|
$carrier = Carrier::toCode((string)($pass['carrier'] ?? 'n')); //통신사 코드로 변경
|
|
$phone = preg_replace('/\D+/', '', (string)($pass['phone'] ?? ''));
|
|
$ci = $pass['ci'] ?? null;
|
|
$di = $pass['di'] ?? null;
|
|
|
|
// DOB: YYYYMMDD -> YYYY-MM-DD
|
|
$dob = preg_replace('/\D+/', '', (string)($pass['dob'] ?? ''));
|
|
$birth = '0000-00-00';
|
|
if (strlen($dob) >= 8) {
|
|
$birth = substr($dob, 0, 4).'-'.substr($dob, 4, 2).'-'.substr($dob, 6, 2);
|
|
}
|
|
|
|
// foreigner/native/country
|
|
$foreigner = (string)($pass['foreigner'] ?? '0'); // 값 정의는 네 PASS 규격대로
|
|
$native = ($foreigner === '1') ? '2' : '1'; // 외국인=2, 내국인=1
|
|
$countryCode = ($native === '1') ? '82' : '';
|
|
$countryName = ($native === '1') ? 'Republic of Korea(대한민국)' : '';
|
|
|
|
// gender mapping (프로젝트 규칙에 맞춰 조정)
|
|
$sex = (string)($pass['sex'] ?? '');
|
|
$gender = match (strtoupper($sex)) {
|
|
'1', 'M', 'MALE' => '1',
|
|
'0', 'F', 'FEMALE', '2' => '0',
|
|
default => '0',
|
|
};
|
|
|
|
$ip = (string)($final['meta']['ip'] ?? request()->ip());
|
|
$promotion = (string)(data_get($sessionAll, 'signup.promotion', '')) === 'on';
|
|
|
|
|
|
return DB::transaction(function () use (
|
|
$email, $pwPlain, $pin2,
|
|
$name, $birth, $carrier, $phone,
|
|
$native, $countryCode, $countryName, $gender,
|
|
$ci, $di, $ip, $promotion, $sessionAll
|
|
) {
|
|
// 1) 접근금지회원 체크(간단 버전)
|
|
// prohibit_access() 정확한 조건이 있으면 그 조건대로 바꾸면 됨)
|
|
if (!empty($ci)) {
|
|
$blocked = DB::table('mem_info')
|
|
->where('ci', $ci)
|
|
->whereIn('stat_3', ['3','4','5'])
|
|
->exists();
|
|
|
|
if ($blocked) {
|
|
throw new \RuntimeException('prohibit_access');
|
|
}
|
|
}
|
|
|
|
// 2) mem_info 생성 (휴대폰 암호화 포함은 MemInfoService::register가 처리)
|
|
$mem = $this->memInfoService->register([
|
|
'email' => $email,
|
|
'name' => $name,
|
|
'pv_sns' => 'self',
|
|
'promotion' => $promotion,
|
|
'ip_reg' => $ip,
|
|
'country_code' => $countryCode,
|
|
'country_name' => $countryName,
|
|
'birth' => $birth,
|
|
'cell_corp' => $carrier,
|
|
'cell_phone' => $phone, // 평문 → register()에서 암호화
|
|
'native' => $native,
|
|
'gender' => $gender,
|
|
'ci' => $ci,
|
|
'di' => $di,
|
|
'ci_v' => '1',
|
|
]);
|
|
|
|
$memNo = (int)$mem->mem_no;
|
|
$now = Carbon::now()->format('Y-m-d H:i:s');
|
|
|
|
//다날 로그 업데이트
|
|
$danalLogSeq = (int) data_get($sessionAll, 'register.danal_log_seq', 0);
|
|
if ($danalLogSeq > 0) {
|
|
$this->updateDanalAuthLogMemNo($danalLogSeq, $memNo);
|
|
}
|
|
|
|
/*회원가입 아이피 필터 업데이트*/
|
|
$ipfResult = (string) data_get($sessionAll, 'signup.ipf_result', '');
|
|
$ipfSeq = (int) data_get($sessionAll, 'signup.ipf_seq', 0);
|
|
|
|
if ($ipfResult === 'S' && $ipfSeq > 0) {
|
|
$this->updateJoinLogAfterSignup($ipfSeq, $memNo, $email);
|
|
}
|
|
/*회원가입 아이피 필터 업데이트*/
|
|
|
|
// 3) mem_st_ring 비번 저장 (CI3 macro->pass + sha512(pin2)와 동일)
|
|
[$str0, $str1, $str2] = CiPassword::makeAll($pwPlain);
|
|
$passwd2 = CiPassword::makePass2($pin2);
|
|
|
|
DB::statement(
|
|
"INSERT INTO mem_st_ring (mem_no, str_0, str_1, str_2, dt_reg, passwd2, passwd2_reg)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE str_0=VALUES(str_0), str_1=VALUES(str_1), str_2=VALUES(str_2),
|
|
dt_reg=VALUES(dt_reg), passwd2=VALUES(passwd2), passwd2_reg=VALUES(passwd2_reg)",
|
|
[$memNo, $str0, $str1, $str2, $now, $passwd2, $now]
|
|
);
|
|
|
|
// 4) mem_auth 휴대폰 인증 처리
|
|
DB::table('mem_auth')->updateOrInsert(
|
|
['mem_no' => $memNo, 'auth_type' => 'cell'],
|
|
['auth_state' => 'Y', 'auth_date' => date('Y-m-d')]
|
|
);
|
|
|
|
// 5) mem_auth_log 가입 로그 저장 (CI3처럼 세션 스냅샷)
|
|
DB::table('mem_auth_log')->insert([
|
|
'mem_no' => $memNo,
|
|
'type' => 'signup',
|
|
'state' => 'S',
|
|
'info' => json_encode($sessionAll, JSON_UNESCAPED_UNICODE),
|
|
'rgdate' => $now,
|
|
]);
|
|
|
|
// 6) (TODO) email_auth init + 메일 발송은 다음 단계에서 연결
|
|
// 지금은 우선 회원 저장까지 완성
|
|
|
|
return ['ok' => true, 'mem_no' => $memNo];
|
|
});
|
|
}
|
|
|
|
public function insertDanalAuthLog(string $gubun, array $res): int
|
|
{
|
|
// gubun: 'J' (회원가입), 'M'(정보수정)
|
|
try {
|
|
return (int) DB::table('mem_danalauthtel_log')->insertGetId([
|
|
'gubun' => $gubun,
|
|
'TID' => (string)($res['TID'] ?? ''),
|
|
'res_code' => (string)($res['RETURNCODE'] ?? ''),
|
|
'mem_no' => null,
|
|
'info' => json_encode($res, JSON_UNESCAPED_UNICODE),
|
|
'rgdate' => now()->format('Y-m-d H:i:s'),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('[danal] insert log failed', [
|
|
'err' => $e->getMessage(),
|
|
]);
|
|
return 0; // 로그 실패해도 플로우는 계속
|
|
}
|
|
}
|
|
|
|
public function updateDanalAuthLogMemNo(int $logSeq, int $memNo): void
|
|
{
|
|
if ($logSeq <= 0 || $memNo <= 0) return;
|
|
|
|
try {
|
|
DB::table('mem_danalauthtel_log')
|
|
->where('seq', $logSeq)
|
|
->update(['mem_no' => $memNo]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('[danal] update log mem_no failed', [
|
|
'seq' => $logSeq,
|
|
'mem_no' => $memNo,
|
|
'err' => $e->getMessage(),
|
|
]);
|
|
// 여기서 throw 할지 말지는 정책인데,
|
|
// 레거시 흐름대로면 "가입은 살리고 로그만 실패"가 맞음.
|
|
}
|
|
}
|
|
|
|
public function precheckJoinIpFilterAndLog(array $userInfo): array
|
|
{
|
|
// $userInfo keys:
|
|
// mem_no(없으면 0), cell_corp, cell_phone(암호화), email('-'), ip4, ip4_c(앞 3옥텟), dt_reg(optional)
|
|
|
|
$ip4 = (string)($userInfo['ip4'] ?? '');
|
|
$ip4c = (string)($userInfo['ip4_c'] ?? '');
|
|
if ($ip4 === '' || $ip4c === '') {
|
|
return ['result' => 'P', 'seq' => 0, 'admin_phones' => []];
|
|
}
|
|
|
|
// join_block 우선순위 A > S
|
|
$row = DB::table('mem_join_filter')
|
|
->whereIn('join_block', ['A', 'S'])
|
|
->whereIn('gubun_code', ['01', '02'])
|
|
->where(function ($q) use ($ip4, $ip4c) {
|
|
$q->where('filter', $ip4c)
|
|
->orWhere('filter', $ip4);
|
|
})
|
|
->orderByRaw("CASE join_block WHEN 'A' THEN 0 WHEN 'S' THEN 1 ELSE 9 END")
|
|
->orderByDesc('seq')
|
|
->first();
|
|
|
|
if (!$row) {
|
|
return ['result' => 'P', 'seq' => 0, 'admin_phones' => []];
|
|
}
|
|
|
|
$result = (string)($row->join_block ?? 'P'); // 'A' or 'S'
|
|
$gubun = (string)($row->gubun_code ?? '');
|
|
|
|
// admin_phone JSON decode
|
|
$adminPhones = [];
|
|
$raw = $row->admin_phone ?? null;
|
|
if (is_string($raw) && $raw !== '') {
|
|
$j = json_decode($raw, true);
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($j)) {
|
|
$adminPhones = array_values(array_filter(array_map('trim', $j)));
|
|
}
|
|
}
|
|
|
|
// 관리자 SMS (S만 보낼지, A도 보낼지는 정책인데)
|
|
// CI3 코드는 S에서만 발송했지만, 주석/정책상 A도 발송하는게 더 안전해서 A도 발송하도록 권장.
|
|
if (in_array($result, ['S','A'], true) && !empty($adminPhones)) {
|
|
foreach ($adminPhones as $phone) {
|
|
$smsPayload = [
|
|
'from_number' => config('services.sms.from', '1833-4856'),
|
|
'to_number' => $phone,
|
|
'message' => '[PIN FOR YOU] 회원가입필터 IP에서 가입 시도됨! 관리자 확인요망 '.date('m-d H:i'),
|
|
'sms_type' => 'sms',
|
|
];
|
|
app(SmsService::class)->send($smsPayload, 'lguplus');
|
|
}
|
|
}
|
|
|
|
// ✅ mem_join_log 기록 (CI3: S 또는 A 일때만 기록)
|
|
$seq = (int) DB::table('mem_join_log')->insertGetId([
|
|
'gubun' => $gubun, // CI3: gubun_code를 gubun에 저장
|
|
'mem_no' => (int)($userInfo['mem_no'] ?? 0), // 가입 전이면 0
|
|
'cell_corp' => (string)($userInfo['cell_corp'] ?? 'n'),
|
|
'cell_phone' => (string)($userInfo['cell_phone'] ?? ''),
|
|
'email' => (string)($userInfo['email'] ?? '-'),
|
|
'ip4' => $ip4,
|
|
'ip4_c' => $ip4c,
|
|
'error_code' => $result, // ✅ join_block을 error_code에 저장(추적용)
|
|
'dt_reg' => $userInfo['dt_reg'] ?? date('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
return [
|
|
'result' => $result, // 'A' or 'S'
|
|
'seq' => $seq,
|
|
'admin_phones' => $adminPhones,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 가입 성공 후 mem_no/email 업데이트 (CI3 ip_check_update 대응)
|
|
*/
|
|
public function updateJoinLogAfterSignup(int $seq, int $memNo, string $email): void
|
|
{
|
|
DB::table('mem_join_log')
|
|
->where('seq', $seq)
|
|
->update([
|
|
'mem_no' => $memNo,
|
|
'email' => $email,
|
|
]);
|
|
}
|
|
|
|
public function findByEmailAndName(string $emailLower, string $name): ?MemInfo
|
|
{
|
|
// ⚠️ 성명 컬럼이 name이 아니면 여기만 바꾸면 됨
|
|
return MemInfo::query()
|
|
->whereNotNull('email')->where('email', '<>', '')
|
|
->whereRaw('LOWER(email) = ?', [$emailLower])
|
|
->where('name', $name)
|
|
->orderByDesc('mem_no')
|
|
->first();
|
|
}
|
|
|
|
public function findByMemNo(int $memNo): ?MemInfo
|
|
{
|
|
return MemInfo::query()
|
|
->where('mem_no', $memNo)
|
|
->first();
|
|
}
|
|
|
|
public function existsByEncryptedPhone(string $phoneEnc): bool
|
|
{
|
|
return DB::table('mem_info')
|
|
->whereNotNull('email')
|
|
->where('email', '<>', '')
|
|
->where('cell_phone', $phoneEnc)
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* 해당 암호화 휴대폰으로 가입된 이메일 목록(중복 제거)
|
|
*/
|
|
public function getEmailsByEncryptedPhone(string $phoneEnc): array
|
|
{
|
|
// 여러 mem_no에서 같은 email이 나올 수 있으니 distinct 처리
|
|
return DB::table('mem_info')
|
|
->select('email')
|
|
->whereNotNull('email')
|
|
->where('email', '<>', '')
|
|
->where('cell_phone', $phoneEnc)
|
|
->orderByDesc('mem_no')
|
|
->distinct()
|
|
->pluck('email')
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
//비밀번호 수정
|
|
public function updatePasswordOnly(int $memNo, string $pwPlain): void
|
|
{
|
|
if ($memNo <= 0 || $pwPlain === '') {
|
|
throw new \InvalidArgumentException('invalid_payload');
|
|
}
|
|
|
|
[$str0, $str1, $str2] = CiPassword::makeAll($pwPlain);
|
|
$now = now()->format('Y-m-d H:i:s');
|
|
|
|
$affected = DB::table('mem_st_ring')
|
|
->where('mem_no', $memNo)
|
|
->update([
|
|
'str_0' => $str0,
|
|
'str_1' => $str1,
|
|
'str_2' => $str2,
|
|
'dt_reg' => $now,
|
|
]);
|
|
|
|
// 기존회원인데 mem_st_ring row가 없다? 이건 데이터 이상으로 보고 명확히 실패 처리
|
|
if ($affected <= 0) {
|
|
throw new \RuntimeException('mem_st_ring_not_found');
|
|
}
|
|
}
|
|
|
|
// 2차 비밀번호 검증
|
|
public function verifyPin2(int $memNo, string $pin2Plain): bool
|
|
{
|
|
if ($memNo <= 0) return false;
|
|
|
|
// 4자리 숫자만 허용
|
|
if (!preg_match('/^\d{4}$/', $pin2Plain)) return false;
|
|
|
|
$stored = (string) DB::table('mem_st_ring')
|
|
->where('mem_no', $memNo)
|
|
->value('passwd2');
|
|
|
|
if ($stored === '') return false;
|
|
|
|
$hash = CiPassword::makePass2($pin2Plain);
|
|
|
|
return hash_equals($stored, $hash);
|
|
}
|
|
|
|
|
|
// 2차 비밀번호 수정
|
|
public function updatePin2Only(int $memNo, string $pin2Plain): void
|
|
{
|
|
if ($memNo <= 0 || $pin2Plain === '') {
|
|
throw new \InvalidArgumentException('invalid_payload');
|
|
}
|
|
|
|
// 4자리 숫자 강제 (컨트롤러에서도 validate 하지만 2중 방어)
|
|
if (!preg_match('/^\d{4}$/', $pin2Plain)) {
|
|
throw new \InvalidArgumentException('invalid_pin2');
|
|
}
|
|
|
|
$passwd2 = CiPassword::makePass2($pin2Plain);
|
|
$now = now()->format('Y-m-d H:i:s');
|
|
|
|
// mem_st_ring 존재 전제: 없으면 데이터 이상으로 실패 처리(비번 변경과 동일 정책)
|
|
$affected = DB::table('mem_st_ring')
|
|
->where('mem_no', $memNo)
|
|
->update([
|
|
'passwd2' => $passwd2,
|
|
'passwd2_reg' => $now,
|
|
]);
|
|
|
|
if ($affected <= 0) {
|
|
throw new \RuntimeException('mem_st_ring_not_found');
|
|
}
|
|
}
|
|
|
|
|
|
public function logPasswordResetSuccess(int $memNo, string $ip, string $agent, string $state = 'E'): void
|
|
{
|
|
$now = now()->format('Y-m-d H:i:s');
|
|
|
|
// state: E=비밀번호찾기 변경, S=직접 변경
|
|
$state = strtoupper(trim($state));
|
|
if (!in_array($state, ['E', 'S'], true)) {
|
|
$state = 'E';
|
|
}
|
|
|
|
DB::table('mem_passwd_modify')->insert([
|
|
'state' => $state,
|
|
'info' => json_encode([
|
|
'mem_no' => (string)$memNo,
|
|
'redate' => $now,
|
|
'remote_addr' => $ip,
|
|
'agent' => substr((string)$agent, 0, 500), // 길이 방어(일관 처리)
|
|
], JSON_UNESCAPED_UNICODE),
|
|
'rgdate' => $now,
|
|
]);
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|