492 lines
17 KiB
PHP
492 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Member\MemInfo;
|
|
use App\Support\LegacyCrypto\CiSeedCrypto;
|
|
use App\Support\LegacyCrypto\CiPassword;
|
|
use App\Support\Legacy\LoginReason;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class MemInfoService
|
|
{
|
|
/**
|
|
* CI: set_receive()
|
|
* 프로모션 수신 동의 변경 (행 잠금)
|
|
*/
|
|
public function setReceive(int $memNo, string $rcvEmail, string $rcvSms, ?string $rcvPush = null): void
|
|
{
|
|
DB::transaction(function () use ($memNo, $rcvEmail, $rcvSms, $rcvPush) {
|
|
/** @var MemInfo $mem */
|
|
$mem = MemInfo::query()->whereKey($memNo)->lockForUpdate()->firstOrFail();
|
|
|
|
$now = Carbon::now()->format('Y-m-d H:i:s');
|
|
|
|
$mem->rcv_email = $rcvEmail;
|
|
$mem->rcv_sms = $rcvSms;
|
|
$mem->dt_rcv_email = $now;
|
|
$mem->dt_rcv_sms = $now;
|
|
|
|
if ($rcvPush !== null) {
|
|
$mem->rcv_push = $rcvPush;
|
|
$mem->dt_rcv_push = $now;
|
|
}
|
|
|
|
$mem->dt_mod = $now;
|
|
$mem->save();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* CI: mem_email_vali()
|
|
*/
|
|
public function emailInfo(string $email): ?MemInfo
|
|
{
|
|
return MemInfo::query()
|
|
->select(['mem_no','stat_3','dt_req_out','email'])
|
|
->where('email', strtolower($email))
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* CI: mem_reg() (간소화 버전)
|
|
* - 실제로는 validation은 FormRequest에서 처리 권장
|
|
*/
|
|
public function register(array $data): MemInfo
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
|
|
$email = strtolower((string)($data['email'] ?? ''));
|
|
|
|
// 중복 체크 + 잠금 (CI for update)
|
|
$exists = MemInfo::query()
|
|
->where('email', $email)
|
|
->lockForUpdate()
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
throw new \RuntimeException('이미 가입된 아이디 입니다. 다른 아이디로 진행해 주세요.');
|
|
}
|
|
|
|
$now = Carbon::now()->format('Y-m-d H:i:s');
|
|
$notnull = "1000-01-01 00:00:00";
|
|
|
|
$mem = new MemInfo();
|
|
$mem->email = $email;
|
|
$mem->name = (string)($data['name'] ?? '');
|
|
$mem->pv_sns = (string)($data['pv_sns'] ?? 'self');
|
|
|
|
$promotion = !empty($data['promotion']) ? 'y' : 'n';
|
|
$mem->rcv_email = $promotion;
|
|
$mem->rcv_sms = $promotion;
|
|
$mem->rcv_push = $promotion;
|
|
|
|
$mem->dt_reg = $now;
|
|
$mem->dt_login = $now;
|
|
|
|
$mem->dt_rcv_email = $now;
|
|
$mem->dt_rcv_sms = $now;
|
|
$mem->dt_rcv_push = $now;
|
|
$mem->dt_stat_1 = $now;
|
|
$mem->dt_stat_2 = $now;
|
|
$mem->dt_stat_3 = $now;
|
|
$mem->dt_stat_4 = $now;
|
|
$mem->dt_stat_5 = $now;
|
|
|
|
$mem->dt_mod = $notnull;
|
|
$mem->dt_vact = $notnull;
|
|
$mem->dt_dor = $notnull;
|
|
$mem->dt_ret_dor = $notnull;
|
|
$mem->dt_req_out = "1000-01-01";
|
|
$mem->dt_out = $notnull;
|
|
|
|
$mem->ip_reg = (string)($data['ip_reg'] ?? request()->ip());
|
|
|
|
// 국가/본인인증 값들
|
|
$mem->country_code = (string)($data['country_code'] ?? '');
|
|
$mem->country_name = (string)($data['country_name'] ?? '');
|
|
$mem->birth = (string)($data['birth'] ?? '0000-00-00');
|
|
$mem->cell_corp = (string)($data['cell_corp'] ?? 'n');
|
|
|
|
// 휴대폰 암호화 (빈값이면 빈값)
|
|
$rawPhone = (string)($data['cell_phone'] ?? '');
|
|
if ($rawPhone !== '') {
|
|
/** @var CiSeedCrypto $seed */
|
|
$seed = app(CiSeedCrypto::class);
|
|
$mem->cell_phone = (string)$seed->encrypt($rawPhone);
|
|
} else {
|
|
$mem->cell_phone = '';
|
|
}
|
|
|
|
$mem->native = (string)($data['native'] ?? 'n');
|
|
$mem->ci = $data['ci'] ?? null;
|
|
$mem->ci_v = (string)($data['ci_v'] ?? '');
|
|
$mem->di = $data['di'] ?? null;
|
|
$mem->gender = (string)($data['gender'] ?? 'n');
|
|
|
|
// 1969년 이전 출생 접근금지(stat_3=3)
|
|
$birthY = (int)substr((string)$mem->birth, 0, 4);
|
|
if ($birthY > 0 && $birthY <= 1969) {
|
|
$mem->stat_3 = '3';
|
|
}
|
|
|
|
$mem->save();
|
|
|
|
return $mem;
|
|
});
|
|
}
|
|
|
|
public function updateLastLogin(int $memNo): void
|
|
{
|
|
MemInfo::query()
|
|
->whereKey($memNo)
|
|
->update([
|
|
'dt_login' => Carbon::now()->format('Y-m-d H:i:s'),
|
|
'login_fail_cnt' => 0,
|
|
'dt_mod' => Carbon::now()->format('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
public function incrementLoginFail(int $memNo): void
|
|
{
|
|
MemInfo::query()
|
|
->whereKey($memNo)
|
|
->update([
|
|
'login_fail_cnt' => DB::raw('login_fail_cnt + 1'),
|
|
'dt_mod' => Carbon::now()->format('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
private function dt6(): string
|
|
{
|
|
return Carbon::now()->format('Y-m-d H:i:s.u');
|
|
}
|
|
|
|
private function ip4c(string $ip): string
|
|
{
|
|
$oct = explode('.', $ip);
|
|
if (count($oct) >= 3) {
|
|
return $oct[0].'.'.$oct[1].'.'.$oct[2];
|
|
}
|
|
return $ip;
|
|
}
|
|
|
|
private function parseUa(string $ua): array
|
|
{
|
|
$platform = '';
|
|
if (stripos($ua, 'Windows') !== false) $platform = 'Windows';
|
|
elseif (stripos($ua, 'Mac OS X') !== false) $platform = 'macOS';
|
|
elseif (stripos($ua, 'Android') !== false) $platform = 'Android';
|
|
elseif (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false) $platform = 'iOS';
|
|
elseif (stripos($ua, 'Linux') !== false) $platform = 'Linux';
|
|
|
|
$browser = 'Unknown';
|
|
$version = '';
|
|
$candidates = [
|
|
'Edg/' => 'Edge',
|
|
'Chrome/' => 'Chrome',
|
|
'Firefox/' => 'Firefox',
|
|
'Safari/' => 'Safari',
|
|
];
|
|
|
|
foreach ($candidates as $needle => $name) {
|
|
$pos = stripos($ua, $needle);
|
|
if ($pos !== false) {
|
|
$browser = $name;
|
|
$sub = substr($ua, $pos + strlen($needle));
|
|
$version = preg_split('/[^0-9\.]/', $sub)[0] ?? '';
|
|
// Safari는 Chrome UA에도 같이 끼므로 Chrome 우선순위를 위에서 처리
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [$platform, trim($browser), trim($version)];
|
|
}
|
|
|
|
/**
|
|
* mem_auth 기반 레벨 계산 (CI3 get_mem_level 이식)
|
|
*/
|
|
private function getMemLevel(int $memNo): array
|
|
{
|
|
$rows = DB::table('mem_auth')
|
|
->select(['auth_type','auth_state'])
|
|
->where('mem_no', $memNo)
|
|
->where('auth_state', 'Y')
|
|
->limit(50)
|
|
->get();
|
|
|
|
$state = ['email'=>false,'cell'=>false,'account'=>false,'otp'=>false,'ars'=>false];
|
|
|
|
foreach ($rows as $r) {
|
|
$t = strtolower((string)$r->auth_type);
|
|
if (array_key_exists($t, $state)) $state[$t] = true;
|
|
}
|
|
|
|
$level = 0;
|
|
if ($state['email']) $level = 1;
|
|
if ($level === 1 && $state['cell']) $level = 2;
|
|
if ($level === 2 && $state['account']) $level = 3;
|
|
if ($level === 3 && $state['otp']) $level = 4;
|
|
if ($level === 2 && $state['ars']) $level = 5;
|
|
|
|
return ['level'=>$level, 'auth_state'=>$state];
|
|
}
|
|
|
|
/**
|
|
* 연도별 로그인 테이블 자동 생성 (B 선택)
|
|
* - mem_login_recent 스키마를 그대로 복제
|
|
*/
|
|
private function ensureLoginYearlyTable(int $year): string
|
|
{
|
|
$year = (int)$year;
|
|
if ($year < 2000 || $year > 2100) {
|
|
// 안전장치
|
|
return 'mem_login_recent';
|
|
}
|
|
|
|
$table = "mem_login_{$year}";
|
|
|
|
// DB마다 동작이 달라서 가장 단순하고 확실한 DDL로 처리
|
|
// CREATE TABLE IF NOT EXISTS mem_login_YYYY LIKE mem_login_recent
|
|
DB::statement("CREATE TABLE IF NOT EXISTS `{$table}` LIKE `mem_login_recent`");
|
|
|
|
return $table;
|
|
}
|
|
|
|
private function insertLoginLog(string $table, array $d): void
|
|
{
|
|
DB::statement(
|
|
"INSERT INTO `{$table}`
|
|
SET mem_no=?,
|
|
sf=?,
|
|
conn=?,
|
|
ip4_aton=inet_aton(?),
|
|
ip4=?,
|
|
ip4_c=SUBSTRING_INDEX(?,'.',3),
|
|
dt_reg=?,
|
|
platform=?,
|
|
browser=?,
|
|
pattern=?,
|
|
error_code=?",
|
|
[
|
|
$d['mem_no'],
|
|
$d['sf'],
|
|
$d['conn'],
|
|
$d['ip4'],
|
|
$d['ip4'],
|
|
$d['ip4'],
|
|
$d['dt_reg'],
|
|
$d['platform'],
|
|
$d['browser'],
|
|
$d['pattern'],
|
|
$d['error_code'],
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
public function attemptLegacyLogin(array $in): array
|
|
{
|
|
$email = strtolower(trim((string)($in['email'] ?? '')));
|
|
$pw = (string)($in['password'] ?? '');
|
|
$ip = (string)($in['ip'] ?? request()->ip());
|
|
$ua = (string)($in['ua'] ?? '');
|
|
$returnUrl = (string)($in['return_url'] ?? '/');
|
|
|
|
if ($email === '' || $pw === '') {
|
|
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.'];
|
|
}
|
|
|
|
$dtNow6 = $this->dt6();
|
|
$dtY = (int)substr($dtNow6, 0, 4);
|
|
|
|
// UA 파싱
|
|
[$platform, $browser, $version] = $this->parseUa($ua);
|
|
$browserFull = trim($browser.' '.$version);
|
|
|
|
//$yearTable = $this->ensureLoginYearlyTable((int)$dtY);
|
|
|
|
|
|
return DB::transaction(function () use ($email, $pw, $ip, $ua, $returnUrl, $dtNow6, $dtY, $platform, $browserFull) {
|
|
$yearTable = "mem_login_".(int)$dtY;
|
|
|
|
/** @var MemInfo|null $mem */
|
|
$mem = MemInfo::query()
|
|
->select([
|
|
'mem_no','email','name','cell_phone','cell_corp',
|
|
'dt_login','dt_reg','login_fail_cnt',
|
|
'pv_sns',
|
|
'stat_1','stat_2','stat_3','stat_4','stat_5',
|
|
'native','country_code'
|
|
])
|
|
->where('email', $email)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
// 아이디 없음 -> mem_no가 없으니 fail_count/log 저장 불가. 메시지만 통일.
|
|
if (!$mem) {
|
|
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.1'];
|
|
}
|
|
|
|
// stat_3 차단 로직 (CI3 id_exists 반영)
|
|
if ((string)$mem->stat_3 === '3') {
|
|
return ['ok'=>false, 'message'=>"접근금지 계정입니다.<br><br>고객센터 1833-4856로 문의 하세요"];
|
|
}
|
|
if ((string)$mem->stat_3 === '4') {
|
|
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.2'];
|
|
}
|
|
if ((string)$mem->stat_3 === '5') {
|
|
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.3'];
|
|
}
|
|
|
|
// 휴면(stat_3=6) 처리: 지금은 테이블 저장 + 안내까지만 (메일 연결은 다음 단계에서)
|
|
if ((string)$mem->stat_3 === '6') {
|
|
// TODO: mem_dormancy insert + authnum 생성 + 메일 발송 연결
|
|
return ['ok'=>false, 'message'=>'회원님 계정은 휴면계정입니다. 이메일 인증 후 이용 가능합니다.4'];
|
|
}
|
|
|
|
// mem_st_ring 비번 로드
|
|
$ring = DB::table('mem_st_ring')->where('mem_no', $mem->mem_no)->first();
|
|
if (!$ring || empty($ring->str_0)) {
|
|
$reason = config('legacy.login_reason.L_NOT_EXISTS_PASS');
|
|
// 실패 카운트 + 실패 로그
|
|
$this->incrementLoginFail((int)$mem->mem_no);
|
|
|
|
$log = [
|
|
'mem_no' => (int)$mem->mem_no,
|
|
'sf' => 'f',
|
|
'conn' => '1',
|
|
'ip4' => $ip,
|
|
'ip4_c' => $this->ip4c($ip),
|
|
'dt_reg' => $dtNow6,
|
|
'platform' => $platform,
|
|
'browser' => $browserFull,
|
|
'pattern' => 'self',
|
|
'error_code' => $reason,
|
|
];
|
|
|
|
|
|
$this->insertLoginLog('mem_login_recent', $log);
|
|
$this->insertLoginLog($yearTable, $log);
|
|
|
|
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.5'];
|
|
}
|
|
|
|
// 비번 검증 (PASS_SET=0)
|
|
$try = CiPassword::make($pw, 0);
|
|
$dbPass = (string)$ring->str_0;
|
|
|
|
if ($try === '' || strcmp($try, $dbPass) !== 0) {
|
|
$reason = config('legacy.login_reason.L_INCORRECT_PASS');
|
|
$failCnt = (int)$mem->login_fail_cnt;
|
|
|
|
// 5회 실패 안내(>=4면 이번이 5회)
|
|
if ($failCnt >= 4) {
|
|
$reason = config('legacy.login_reason.L_LOGIN_FAIL');
|
|
}
|
|
|
|
// 실패 카운트 + 실패 로그
|
|
$this->incrementLoginFail((int)$mem->mem_no);
|
|
|
|
$log = [
|
|
'mem_no' => (int)$mem->mem_no,
|
|
'sf' => 'f',
|
|
'conn' => '1',
|
|
'ip4' => $ip,
|
|
'ip4_c' => $this->ip4c($ip),
|
|
'dt_reg' => $dtNow6,
|
|
'platform' => $platform,
|
|
'browser' => $browserFull,
|
|
'pattern' => 'self',
|
|
'error_code' => $reason,
|
|
];
|
|
|
|
$this->insertLoginLog('mem_login_recent', $log);
|
|
$this->insertLoginLog($yearTable, $log);
|
|
|
|
if ($failCnt >= 4) {
|
|
return ['ok'=>false, 'message'=>"<a href='/member/find_pass?menu=pass' style='color: #fff;font-size:13px'>5회이상 실패시 비밀번호찾기 후 이용 바랍니다.(클릭)</a>"];
|
|
}
|
|
|
|
return ['ok'=>false, 'message'=>"비밀번호가 일치하지 않습니다.\n비밀번호 실패횟수 : ".($failCnt+1)."\n5회 이상 실패시 인증을 다시받아야 합니다."];
|
|
}
|
|
|
|
// 레벨 체크 (email 인증 필수)
|
|
$levelInfo = $this->getMemLevel((int)$mem->mem_no);
|
|
if (($levelInfo['level'] ?? 0) < 1 || empty($levelInfo['auth_state']['email'])) {
|
|
return [
|
|
'ok'=>false,
|
|
'message'=>"<br>이메일 인증 완료후 이용가능합니다. \n이메일주소(".$mem->email.") 메일을 확인하세요\n",
|
|
];
|
|
}
|
|
|
|
// 로그인 차단 IP 대역 체크
|
|
$ip4c = $this->ip4c($ip);
|
|
$blocked = DB::table('filter_login_ip_reject')
|
|
->where('mem_no', $mem->mem_no)
|
|
->where('ip4_c', $ip4c)
|
|
->exists();
|
|
|
|
if ($blocked) {
|
|
return ['ok'=>false, 'message'=>"회원님의 설정에 의해 접속이 차단되었습니다.6"];
|
|
}
|
|
|
|
// 최근 로그인 업데이트(성공 시 fail_count reset)
|
|
$this->updateLastLogin((int)$mem->mem_no);
|
|
|
|
// 성공 로그 저장(최근 + 연도별)
|
|
$log = [
|
|
'mem_no' => (int)$mem->mem_no,
|
|
'sf' => 's',
|
|
'conn' => '1',
|
|
'ip4' => $ip,
|
|
'ip4_c' => $ip4c,
|
|
'dt_reg' => $dtNow6,
|
|
'platform' => $platform,
|
|
'browser' => $browserFull,
|
|
'pattern' => 'self',
|
|
'error_code' => '',
|
|
];
|
|
|
|
$this->insertLoginLog('mem_login_recent', $log);
|
|
$this->insertLoginLog($yearTable, $log);
|
|
|
|
// 첫 로그인 여부
|
|
$login1st = 'n';
|
|
if ((string)$mem->dt_login === (string)$mem->dt_reg) {
|
|
$login1st = 'y';
|
|
}
|
|
|
|
// ✅ 세션 payload (CI3 키 유지)
|
|
$session = [
|
|
'_login_' => true,
|
|
'_mid' => (string)$mem->email,
|
|
'_mno' => (int)$mem->mem_no,
|
|
'_mname' => (string)$mem->name,
|
|
'_mstat_1' => (string)$mem->stat_1,
|
|
'_mstat_2' => (string)$mem->stat_2,
|
|
'_mstat_3' => (string)$mem->stat_3,
|
|
'_mstat_4' => (string)$mem->stat_4,
|
|
'_mstat_5' => (string)$mem->stat_5,
|
|
'_mcell' => (string)$mem->cell_phone,
|
|
'_mpv_sns' => (string)$mem->pv_sns,
|
|
'_mnative' => (string)$mem->native,
|
|
'_mcountry_code' => (string)$mem->country_code,
|
|
'_ip' => $ip,
|
|
'_login_1st' => $login1st,
|
|
'_dt_reg' => (string)$mem->dt_reg,
|
|
'auth_ars' => !empty($levelInfo['auth_state']['ars']) ? 'Y' : 'N',
|
|
];
|
|
|
|
return [
|
|
'ok' => true,
|
|
'session' => $session,
|
|
'redirect' => $returnUrl ?: '/',
|
|
];
|
|
});
|
|
}
|
|
}
|