giftcon_dev/app/Services/MemInfoService.php
2026-02-02 18:35:56 +09:00

672 lines
24 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: 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'=>"비밀번호 입력 5회이상 실패 하셨습니다.\n 비밀번호찾기 후 이용 바랍니다."];
}
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,
'reason' => 'email_unverified',
'email' => (string)$mem->email,
'mem_no' => (int)$mem->mem_no,
'message' => '이메일 인증이 필요합니다.',
];
}
// 로그인 차단 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 ?: '/',
];
});
}
public function getReceive(int $memNo): array
{
$mem = MemInfo::query()->whereKey($memNo)->first();
// ✅ 출금계좌 인증정보 (있으면 1건)
$outAccount = DB::table('mem_account')
->select(['bank_name', 'bank_act_num', 'bank_act_name', 'act_date'])
->where('mem_no', $memNo)
->where('act_type', 'out')
->where('act_state', '3')
->orderByDesc('act_date')
->first();
if (!$mem) {
return [
'rcv_email' => 'n',
'rcv_sms' => 'n',
'rcv_push' => null,
'out_account' => $outAccount ? [
'bank_name' => (string)($outAccount->bank_name ?? ''),
'bank_act_num' => (string)($outAccount->bank_act_num ?? ''),
'bank_act_name'=> (string)($outAccount->bank_act_name ?? ''),
'act_date' => (string)($outAccount->act_date ?? ''),
] : null,
];
}
return [
'rcv_email' => (string)($mem->rcv_email ?? 'n'),
'rcv_sms' => (string)($mem->rcv_sms ?? 'n'),
'rcv_push' => $mem->rcv_push !== null ? (string)$mem->rcv_push : null,
// ✅ 추가
'out_account' => $outAccount ? [
'bank_name' => (string)($outAccount->bank_name ?? ''),
'bank_act_num' => (string)($outAccount->bank_act_num ?? ''),
'bank_act_name'=> (string)($outAccount->bank_act_name ?? ''),
'act_date' => (string)($outAccount->act_date ?? ''),
] : null,
];
}
/*회원정보 수신동의 정보 저장*/
public function setReceiveSelective(int $memNo, string $rcvEmail, string $rcvSms): array
{
$rcvEmail = ($rcvEmail === 'y') ? 'y' : 'n';
$rcvSms = ($rcvSms === 'y') ? 'y' : 'n';
return DB::transaction(function () use ($memNo, $rcvEmail, $rcvSms) {
/** @var MemInfo $mem */
$mem = MemInfo::query()->whereKey($memNo)->lockForUpdate()->firstOrFail();
$beforeEmail = (string) ($mem->rcv_email ?? 'n');
$beforeSms = (string) ($mem->rcv_sms ?? 'n');
$changedEmail = ($beforeEmail !== $rcvEmail);
$changedSms = ($beforeSms !== $rcvSms);
if (!$changedEmail && !$changedSms) {
return [
'changed' => [],
'message' => '변경된 내용이 없습니다.',
];
}
$now = Carbon::now()->format('Y-m-d H:i:s');
if ($changedEmail) {
$mem->rcv_email = $rcvEmail;
$mem->dt_rcv_email = $now;
}
if ($changedSms) {
$mem->rcv_sms = $rcvSms;
$mem->dt_rcv_sms = $now;
}
// 공통 수정일은 변경이 있을 때만
$mem->dt_mod = $now;
$mem->save();
$changed = [];
if ($changedEmail) $changed[] = 'email';
if ($changedSms) $changed[] = 'sms';
$label = [];
if ($changedEmail) $label[] = '이메일';
if ($changedSms) $label[] = 'SMS';
return [
'changed' => $changed,
'message' => implode('·', $label) . ' 수신 동의 설정이 저장되었습니다.',
];
});
}
//회원 탈퇴 최근 구매내역 확인
public function validateWithdraw(int $memNo): array
{
if ($memNo <= 0) {
return ['ok' => false, 'message' => '로그인 정보가 올바르지 않습니다.'];
}
// ✅ 최근 7일 이내 구매내역(stat_pay p/t) 있으면 불가
$from = Carbon::today()->subDays(7)->startOfDay();
$cnt = (int) DB::table('pin_order')
->where('mem_no', $memNo)
->whereIn('stat_pay', ['p','t'])
->where('dt_stat_pay', '>=', $from->toDateTimeString())
->count();
if ($cnt > 0) {
return [
'ok' => false,
'message' => '죄송합니다. 최근 7일 이내 구매내역이 있는 경우 즉시 탈퇴가 불가합니다. 고객센터로 탈퇴 접수를 해주시기 바랍니다',
];
}
return ['ok' => true];
}
//회원탈퇴 진행
public function withdrawMember(int $memNo): array
{
$v = $this->validateWithdraw($memNo);
if (!($v['ok'] ?? false)) return $v;
$now = Carbon::now();
$dtReqOut = $now->copy()->addDays(90)->toDateString(); // Y-m-d
DB::transaction(function () use ($memNo, $now, $dtReqOut) {
// 1) mem_info 비식별/초기화 + 탈퇴 처리
DB::table('mem_info')
->where('mem_no', $memNo)
->update([
'name' => '',
'name_first' => '',
'name_mid' => '',
'name_last' => '',
'birth' => '1900-01-01',
'gender' => '',
'native' => '',
'cell_corp' => '',
'cell_phone' => '',
'ci' => '',
'bank_code' => '',
'bank_name' => '',
'bank_act_num' => '',
'bank_vact_num' => '',
'dt_req_out' => $dtReqOut,
'dt_out' => $now->toDateTimeString(),
'stat_3' => '4',
'dt_mod' => $now->toDateTimeString(),
]);
// 2) mem_account 초기화
DB::table('mem_account')
->where('mem_no', $memNo)
->update([
'bank_code' => '',
'bank_name' => '',
'bank_act_name' => '',
'bank_act_num' => '',
]);
// 3) mem_address 초기화
DB::table('mem_address')
->where('mem_no', $memNo)
->update([
'shipping' => '',
'zipNo' => '',
'roadAddrPart1' => '',
'jibunAddr' => '',
'addrDetail' => '',
]);
// 4) mem_auth 인증 해제
DB::table('mem_auth')
->where('mem_no', $memNo)
->update([
'auth_state' => 'N',
]);
// 5) mem_st_ring 비번/2차 비번 제거
DB::table('mem_st_ring')
->where('mem_no', $memNo)
->update([
'str_0' => '',
'str_1' => '',
'str_2' => '',
'dt_reg' => $now->toDateTimeString(),
'passwd2' => '',
'passwd2_reg' => $now->toDateTimeString(),
]);
});
return ['ok' => true, 'message' => '회원탈퇴가 완료되었습니다.'];
}
}