mode = (string)($cfg['mode'] ?? 'real'); $this->orgCode = (string)($cfg['org_code'] ?? ''); $this->apiKey = (string)($cfg['api_key'] ?? ''); $this->urlReal = (string)($cfg['url_real'] ?? ''); $this->urlTest = (string)($cfg['url_test'] ?? ''); $this->timeout = (int)($cfg['timeout'] ?? 10); } /** * 출금(out) 계좌 성명 인증 + 저장까지 한 번에 처리 * * @param int $memNo 회원번호 * @param string $memberRealName 회원 실명(서버에서 꺼낸 값) * @param string $inputDepositor 입력한 예금주(화면 입력) * @param string $bankCode 은행코드(001~) * @param string $account 계좌번호(숫자만 권장) * @param bool $requireSameName 입력 예금주 == 회원실명 강제 여부 * * @return array { ok, code, message, status, depositor, telegram_no } */ public function verifyAndSaveOutAccount( int $memNo, string $memberRealName, string $inputDepositor, string $bankCode, string $account, bool $requireSameName = true ): array { $memNo = (int)$memNo; $memberRealName = trim($memberRealName); $inputDepositor = trim($inputDepositor); $bankCode = trim($bankCode); $account = preg_replace('/[^0-9]/', '', (string)$account); // 은행코드 검증 (config/bank_code.php) $bankMap = (array) config('bank_code.flat', []); if ($bankCode === '' || !isset($bankMap[$bankCode])) { return $this->fail('INVALID_BANK', '은행코드가 올바르지 않습니다.'); } if ($account === '') { return $this->fail('INVALID_ACCOUNT', '계좌번호를 입력해 주세요.'); } if ($inputDepositor === '') { return $this->fail('INVALID_DEPOSITOR', '예금주를 입력해 주세요.'); } // 입력 예금주와 회원 실명 일치 강제 (네 CI3 로직과 동일) if ($requireSameName && $memberRealName !== '' && $memberRealName !== $inputDepositor) { // act_state=4(정보불일치) 기록(선택) $this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '4', [ 'reason' => 'member_name_mismatch', 'member_real_name' => $memberRealName, 'input_depositor' => $inputDepositor, ]); return $this->fail('NAME_MISMATCH', '가입자 성명과 예금주가 일치하지 않습니다!'); } // telegram_no 발급 (일별 6자리) $telegramNo = $this->issueTelegramNo(); // 요청 페이로드 (Dozn 스펙) $payload = [ 'org_code' => $this->orgCode, 'api_key' => $this->apiKey, 'telegram_no' => $telegramNo, 'bank_code' => $bankCode, 'account' => $account, 'check_depositor' => 'N', // 기존 코드 유지 (실명체크 identify_no는 추후) // 'identify_no' => '761127', ]; // 요청 로그 insert $logSeq = $this->logRequest($memNo, $payload, [ 'input_depositor' => $inputDepositor, 'mode' => $this->mode, ]); // 진행상태 기록 (2: 인증진행중) $this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '2', [ 'log_seq' => $logSeq, 'telegram_no' => $telegramNo, ]); // Dozn 호출 $resp = $this->callDozn($payload); // 결과 로그 update $this->logResult($logSeq, $resp['raw'] ?? []); $status = (string)($resp['status'] ?? ''); $depositor = (string)($resp['depositor'] ?? ''); // 성공(200) 처리 if ($status === '200') { if ($depositor === '' || $depositor !== $inputDepositor) { // 정보 불일치 (4) $this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '4', [ 'status' => $status, 'returned_depositor' => $depositor, 'input_depositor' => $inputDepositor, 'telegram_no' => $telegramNo, ]); return [ 'ok' => false, 'code' => 'DEPOSITOR_MISMATCH', 'message' => '예금주가 일치하지 않습니다!', 'status' => $status, 'depositor' => $depositor, 'telegram_no' => $telegramNo, ]; } // 성공(3): 계좌 저장 $this->saveOutAccountSuccess($memNo, $bankCode, $account, $depositor, [ 'status' => $status, 'telegram_no' => $telegramNo, 'result' => $resp['raw'] ?? [], ]); return [ 'ok' => true, 'code' => 'OK', 'message' => '인증완료 및 계좌번호가 등록되었습니다.', 'status' => $status, 'depositor' => $depositor, 'telegram_no' => $telegramNo, ]; } // 잘못된 계좌(520) if ($status === '520') { $this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '5', [ 'status' => $status, 'telegram_no' => $telegramNo, 'result' => $resp['raw'] ?? [], ]); return [ 'ok' => false, 'code' => 'INVALID_ACCOUNT', 'message' => '잘 못된 계좌번호 입니다.', 'status' => $status, 'depositor' => $depositor, 'telegram_no' => $telegramNo, ]; } // 기타 오류 $this->upsertOutAccountState($memNo, $bankCode, $account, $inputDepositor, '5', [ 'status' => $status, 'telegram_no' => $telegramNo, 'result' => $resp['raw'] ?? [], ]); return [ 'ok' => false, 'code' => 'DOZN_ERROR', 'message' => ($status ?: 'ERR') . '|인증에 문제가 발생했습니다. 잠시후 다시 시도해주세요.', 'status' => $status, 'depositor' => $depositor, 'telegram_no' => $telegramNo, ]; } // ------------------------------------------------------------------ // Dozn HTTP call // ------------------------------------------------------------------ private function callDozn(array $payload): array { $url = $this->mode === 'test' ? $this->urlTest : $this->urlReal; if ($url === '' || $this->orgCode === '' || $this->apiKey === '') { return [ 'status' => 'CONFIG', 'depositor' => '', 'raw' => ['error' => 'dozn_config_missing'], ]; } try { $res = Http::timeout($this->timeout) ->acceptJson() ->asJson() ->post($url, $payload); $json = $res->json(); if (!is_array($json)) $json = []; return [ 'status' => (string)($json['status'] ?? ''), 'depositor' => (string)($json['depositor'] ?? ''), 'raw' => $json, ]; } catch (\Throwable $e) { Log::warning('[dozn] request failed', [ 'err' => $e->getMessage(), ]); return [ 'status' => 'HTTP', 'depositor' => '', 'raw' => ['error' => 'http_exception', 'message' => $e->getMessage()], ]; } } // ------------------------------------------------------------------ // telegram_no: mem_account_telegram_no 사용 (일별 6자리) // ------------------------------------------------------------------ private function issueTelegramNo(): int { $today = Carbon::today()->toDateString(); return (int) DB::transaction(function () use ($today) { // 오늘 발급된 마지막 telegram_no를 잠그고 다음 번호 생성 $last = DB::table('mem_account_telegram_no') ->where('date', $today) ->lockForUpdate() ->max('telegram_no'); $next = $last ? ((int)$last + 1) : 1; if ($next > 999999) $next = 1; DB::table('mem_account_telegram_no')->insert([ 'telegram_no' => $next, 'date' => $today, ]); return $next; }); } // ------------------------------------------------------------------ // Logs: mem_account_log // ------------------------------------------------------------------ private function logRequest(int $memNo, array $payload, array $meta = []): int { $now = Carbon::now()->format('Y-m-d H:i:s'); $req = array_merge($payload, $meta); $reqJson = json_encode($req, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($reqJson === false) $reqJson = '{}'; return (int) DB::table('mem_account_log')->insertGetId([ 'mem_no' => $memNo, 'request_data' => $reqJson, 'request_time' => $now, 'result_data' => '{}', 'result_time' => $now, ]); } private function logResult(int $seq, array $result): void { if ($seq <= 0) return; $now = Carbon::now()->format('Y-m-d H:i:s'); $resJson = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($resJson === false) $resJson = '{}'; DB::table('mem_account_log') ->where('seq', $seq) ->update([ 'result_data' => $resJson, 'result_time' => $now, ]); } // ------------------------------------------------------------------ // mem_account: 회원당 out 1개 유지 (스키마 유지, 코드로 강제) // ------------------------------------------------------------------ private function upsertOutAccountState( int $memNo, string $bankCode, string $account, string $depositor, string $actState, array $confirmLog = [] ): void { $bankMap = (array) config('bank_code.flat', []); $bankName = (string)($bankMap[$bankCode] ?? ''); $now = Carbon::now()->format('Y-m-d H:i:s'); DB::transaction(function () use ($memNo, $bankCode, $bankName, $account, $depositor, $actState, $confirmLog, $now) { // mem_no + act_type='out' 기준으로 row 잠금 $row = DB::table('mem_account') ->where('mem_no', $memNo) ->where('act_type', 'out') ->lockForUpdate() ->first(); $data = [ 'mem_no' => $memNo, 'act_type' => 'out', 'act_state' => $actState, 'bank_code' => $bankCode, 'bank_name' => $bankName, 'bank_act_name' => $depositor, 'bank_act_num' => $account, 'in_date' => $now, 'act_date' => $now, 'confirm_log' => json_encode($confirmLog ?: new \stdClass(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]; if ($row) { DB::table('mem_account')->where('seq', $row->seq)->update($data); } else { DB::table('mem_account')->insert($data); } }); } private function saveOutAccountSuccess( int $memNo, string $bankCode, string $account, string $depositor, array $confirmLog = [] ): void { // 성공은 act_state=3 $this->upsertOutAccountState($memNo, $bankCode, $account, $depositor, '3', $confirmLog); } private function fail(string $code, string $message): array { return [ 'ok' => false, 'code' => $code, 'message' => $message, ]; } }