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'=>"접근금지 계정입니다.

고객센터 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' => '회원탈퇴가 완료되었습니다.']; } }