| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- <?php
- namespace App\Models;
- use Exception;
- class SmsLib
- {
- public string $token;
- public string $host;
- public string $port;
- public string $id;
- public string $pw;
- public string $callNum;
- public array $data;
- public array $error;
- public array $history; // 문자 전송 내역 정보
- public array $result; // 문자 전송 결과 정보
- public int $total; // 전송 횟수
- public int $success; // 성공 횟수
- public int $failed; // 실패 횟수
- public array $message; // 처리 결과
- public function __construct()
- {
- $configModel = new Config();
- $this->token = $configModel->item('sms_icode_token', env('SMS_TOKEN'));
- $this->host = $configModel->item('sms_icode_host', env('SMS_IP'));
- $this->port = $configModel->item('sms_icode_port', env('SMS_PORT'));
- $this->id = $configModel->item('sms_icode_id', env('SMS_ID'));
- $this->pw = $configModel->item('sms_icode_pw', env('SMS_PW'));
- $this->callNum = $configModel->item('sms_icode_call_num', env('SMS_NUMBER'));
- $this->_init();
- }
- /**
- * 초기화
- */
- private function _init(): void
- {
- $this->data = [];
- $this->error = [];
- $this->history = [];
- $this->result = [];
- $this->message = [];
- $this->total = 0;
- $this->success = 0;
- $this->failed = 0;
- }
- /**
- * 아이코드 계정 정보 조회
- */
- public function info(): array
- {
- $userInfo = explode(';', getSock("http://www.icodekorea.com/res/userinfo.php?userid={$this->id}&userpw={$this->pw}"));
- return [
- 'code' => $userInfo[0], // 결과코드
- 'coin' => $userInfo[1], // 고객 잔액 (충전제만 해당)
- 'gpay' => $userInfo[2], // 고객의 건수 별 차감액 표시 (충전제만 해당)
- 'payment' => $userInfo[3] // 요금제 표시, A:충전제, C:정액제
- ];
- }
- /**
- * 전송 패킷 생성
- * Add(수신번호목록(배열), 발신번호, 전송내용(2000자이내), 제목(옵션, 30자이내), 예약일자(옵션, 12자리)
- */
- private function _add($sendNumber = [], $sendData = []): bool
- {
- $this->_init();
- $userID = ($sendData['userID'] ?? null);
- $subject = ($sendData['subject'] ?? null);
- $content = ($sendData['content'] ?? null);
- $isReserve = ($sendData['isReserve'] ?? 0);
- $reserveAt = ($sendData['reserveAt'] ?? null);
- $callBack = $this->callNum;
- if(!$userID) {
- $this->error[] = '발신 회원번호가 존재하지 않습니다.';
- return false;
- }
- // 내용 개행치환
- $content = preg_replace("/\r\n/", "\n", $content);
- $content = preg_replace("/\r/", "\n", $content);
- // 문자 타입별 Port 설정.
- $sendType = (strlen($content) > 90 ? 1 : 2); // 0: SMS / 1: LMS
- if($sendType == 1) { // LMS일 경우 제목이 있고 SMS면 제목을 앞에 붙여준다.
- $subject = "";
- }else{
- $content = ($subject . ' ' . $content);
- }
- // 예약날짜 형식 변환
- if($isReserve) {
- $reserveAt = date("YmdHi", strtotime($reserveAt));
- }
- $callBack = getTelNumber($callBack, 0);
- $callBack = $this->_cutChar($callBack, 12); // 회신번호
- /** LMS 제목 **/
- /*
- 제목필드의 값이 없을 경우 단말기 기종및 설정에 따라 표기 방법이 다름
- 1. 설정에서 제목필드보기 설정 Disable -> 제목필드값을 넣어도 미표기
- 2. 설정에서 제목필드보기 설정 Enable -> 제목을 넣지 않을 경우 제목없음으로 자동표시
- 제목의 첫글자에 "<",">", 개행문자가 있을경우 단말기종류 및 통신사에 따라 메세지 전송실패 -> 글자를 체크하거나 취환처리요망
- $strSubject = str_replace("<br/>", " ", $strSubject);
- $strSubject = str_replace("<", "[", $strSubject);
- $strSubject = str_replace(">", "]", $strSubject);
- */
- $subject = $this->_cutCharUtf8($subject, 30);
- $content = $this->_cutCharUtf8($content, 2000);
- /* 필수 항목에 대해 정상적인 코드인지 검사 과정. (개발 방식에 따라 활용) */
- $this->error[] = $this->_checkCommonTypeDest($sendNumber); // 번호 검사
- $this->error[] = $this->_isVaildCallback($callBack);
- $this->error[] = $this->_checkCommonTypeDate($reserveAt);
- if(count($this->errors()) > 0) {
- return false;
- }
- $today = now();
- $alreadyTels = []; // 중복 번호 관리
- foreach ($sendNumber as $uid => $tel) {
- if (empty($tel)) {
- continue;
- }
- $tel = getTelNumber($tel, 1);
- if(in_array($tel, $alreadyTels)) {
- continue;
- }else{
- $alreadyTels[] = $tel;
- }
- $list = [
- "key" => $this->token,
- "tel" => $tel,
- "cb" => $callBack,
- "msg" => $content
- ];
- if (!empty($subject)) {
- $list['title'] = $subject;
- }
- if (!empty($reserveAt)) {
- $list['date'] = $reserveAt;
- }
- $packet = json_encode($list);
- $this->data[$uid] = '06' . str_pad(strlen($packet), 4, "0", STR_PAD_LEFT) . $packet;
- $this->history[$uid] = [
- 'user_id' => ($uid ?: null),
- 'type' => $sendType,
- 'subject' => $subject,
- 'content' => $content,
- 'tel' => $tel,
- 'is_reserve' => $isReserve,
- 'reserve_at' => $reserveAt,
- 'code' => null,
- 'message' => null,
- 'created_at' => $today,
- ];
- }
- // 전송 결과 저장
- $this->result['user_id'] = $userID;
- $this->result['subject'] = $subject;
- $this->result['content'] = $content;
- $this->result['reserve_at'] = $reserveAt;
- return true;
- }
- /**
- * 문자 실제 전송 처리
- */
- private function _push(): bool
- {
- $fSocket = fsockopen($this->host, $this->port, $errorNo, $errorStr, 2);
- if (!$fSocket) {
- return false;
- }
- set_time_limit(300);
- $gets = "";
- foreach ($this->data as $i => $puts) {
- fputs($fSocket, $puts, strlen($puts));
- while (!$gets) {
- $gets = fgets($fSocket, 32);
- }
- preg_match("/\"cb\":\"([0-9]*)\"/", substr($puts, 6), $matches);
- $tel = $matches[1];
- $resultCode = substr($gets, 6, 2);
- if ($resultCode == '00' || $resultCode == '17') { // 17은 접수(전송)대기.
- $this->message[$i] = ($resultCode . ":" . substr($gets, 8, 12) . ":" . substr($gets, 20, 11));
- $this->success++;
- } else {
- $this->message[$i] = ($resultCode . ":" . $tel . ":Error(" . $resultCode . ")");
- $this->failed++;
- if ($resultCode >= "80") {
- break;
- }
- }
- $gets = "";
- }
- fclose($fSocket);
- return true;
- }
- /**
- * 전송 결과 생성
- * $this->message 에 메시지 전송 데이터가 `:` 콜론 형태로 저장되어있음
- */
- public function report(): string
- {
- $resultMessage = "";
- if ($this->message) {
- $resultMessage .= "서버에 접속했습니다.<br/><br/>";
- foreach ($this->message as $i => $result) {
- list($code, $phone, $seq) = explode(":", $result);
- if (substr($seq, 0, 5) == "Error") {
- $resultCode = substr($code, 6, 2);
- echo $phone . ' 전송오류 (' . $resultCode . '): ';
- switch (substr($code, 6, 2)) {
- case '23': // "23:데이터오류, 전송날짜오류, 발신번호미등록"
- $resultMessage .= "데이터를 다시 확인해 주시기바랍니다.<br/>";
- break;
- // 아래의 사유들은 전송진행이 중단됨.
- case '85': // "85:전송번호 미등록"
- $resultMessage .= "등록되지 않는 전송번호 입니다.<br/>";
- break;
- case '87': // "87:인증실패"
- $resultMessage .= "(정액제-계약확인)인증 받지 못하였습니다.<br/>";
- break;
- case '88': // "88:연동모듈 전송불가"
- $resultMessage .= "연동모듈 사용이 불가능합니다. 아이코드로 문의하세요.<br/>";
- break;
- case '96': // "96:토큰 검사 실패"
- $resultMessage .= "사용할 수 없는 토큰키입니다.<br/>";
- break;
- case '97': // "97:잔여코인부족"
- $resultMessage .= "잔여코인이 부족합니다.<br/>";
- break;
- case '98': // "98:사용기간만료"
- $resultMessage .= "사용기간이 만료되었습니다.<br/>";
- break;
- case '99': // "99:인증실패"
- $resultMessage .= "서비스 사용이 불가능합니다. 아이코드로 문의하세요.<br/>";
- break;
- default: // "미 확인 오류"
- $resultMessage .= "알 수 없는 오류로 전송이 실패하었습니다.<br/>";
- break;
- }
- $this->error[] = $resultMessage;
- } else {
- $resultCode = substr($code, 0, 2);
- switch ($resultCode) {
- case '17': // "17: 접수(전송)대기 처리. 지연해소시 전송됨."
- $resultMessage .= "접수(전송)대기처리 되었습니다.";
- break;
- default: // "00: 전송완료."
- $resultMessage .= "전송되었습니다.<br/>";
- break;
- }
- $resultMessage .= $phone . "로 (msg seq : ' . $seq . ')<br/>";
- }
- $this->total++;
- $this->history[$i]['code'] = $resultCode;
- $this->history[$i]['message'] = addslashes($resultMessage);
- }
- $resultMessage .= "<br/> 전체(" . $this->total . "건) / 전송완료(" . $this->success . "건) / 전송실패(" . $this->failed . "건)<br/>";
- // 전송 결과 저장
- $this->result['call_num'] = $this->callNum;
- $this->result['total_count'] = $this->total;
- $this->result['success_count'] = $this->success;
- $this->result['failed_count'] = $this->failed;
- $this->result['message'] = '관리자 직접 전송 ' . $resultMessage;
- $this->result['created_at'] = now();
- }
- return $resultMessage;
- }
- /**
- * 전송하기
- */
- public function send($sendNumber = [], $sendData = []): bool
- {
- try {
- // 문자 전송 목록에 추가
- if(!$this->_add($sendNumber, $sendData)) {
- throw new Exception();
- }
- // 문자 전송
- $this->_push();
- // 전송 결과 생성
- $this->report();
- // DB 저장
- $this->_save();
- return true;
- }catch(Exception $e) {
- $this->error[] = $e->getMessage();
- return false;
- }
- }
- /**
- * 전송 자료 저장
- */
- private function _save(): void
- {
- // 문자 전송 내역 저장
- $keys = (new SmsHistory)->setHistory($this->history);
- // 문자 전송 결과 저장
- $this->result['start_id'] = min($keys);
- $this->result['end_id'] = max($keys);
- (new SmsResult)->insert($this->result);
- }
- /**
- * 오류 조회
- */
- public function errors(): array
- {
- return array_filter($this->error);
- }
- /**
- * 원하는 문자열의 길이를 원하는 길이만큼 공백을 넣어 맞추도록 합니다.
- */
- private function _fillSpace(string $text, int $size): string
- {
- for ($i = 0; $i < $size; $i++) {
- $text .= " ";
- }
- return substr($text, 0, $size);
- }
- /**
- * 원하는 문자열을 원하는 길에 맞는지 확인해서 조정하는 기능을 합니다.
- */
- private function _cutChar(string $word, int $cut): string
- {
- $word = substr($word, 0, $cut); // 필요한 길이만큼 취함.
- for ($k = $cut - 1; $k > 1; $k--) {
- if (ord(substr($word, $k, 1)) < 128) { // 한글값은 160 이상.
- break;
- }
- }
- return substr($word, 0, $cut - ($cut - $k + 1) % 2);;
- }
- private function _cutCharUtf8(string $word, int $cut): string
- {
- preg_match_all('/[\xE0-\xFF][\x80-\xFF]{2}|./', $word, $match); // target for BMP
- $m = $match[0];
- $slen = strlen($word); // length of source string
- if ($slen <= $cut) {
- return $word;
- }
- $ret = [];
- $count = 0;
- for ($i = 0; $i < $cut; $i++) {
- $count += (strlen($m[$i]) > 1) ? 2 : 1;
- if ($count > $cut) {
- break;
- }
- $ret[] = $m[$i];
- }
- return join('', $ret);
- }
- /**
- * 잘못된 수신번호 목록을 리턴합니다.
- */
- private function _checkCommonTypeDest(array $strTelList): string
- {
- $result = '';
- foreach ($strTelList as $tel) {
- $tel = preg_replace("/[^0-9]/", "", $tel);
- if (!preg_match("/^(0[173][0136789])([0-9]{3,4})([0-9]{4})$/", $tel)) {
- $result .= $tel . ',';
- }
- }
- return $result;
- }
- /**
- * 회신번호 유효성 여부조회
- * 한국인터넷진흥원 권고사항
- */
- private function _isVaildCallback(string $callback): string
- {
- $_callback = preg_replace('/[^0-9]/', '', $callback);
- if (!preg_match("/^(02|0[3-6]\d|01(0|1|3|5|6|7|8|9)|070|080|007)\-?\d{3,4}\-?\d{4,5}$/", $_callback) &&
- !preg_match("/^(15|16|18)\d{2}\-?\d{4,5}$/", $_callback)) {
- return "회신번호오류";
- }
- if (preg_match("/^(02|0[3-6]\d|01(0|1|3|5|6|7|8|9)|070|080)\-?0{3,4}\-?\d{4}$/", $_callback)) {
- return "회신번호오류";
- }
- return '';
- }
- /**
- * 문자열을 JSON 사용가능 타입으로 변환한다.
- */
- private function _escapeJsonString(string $value): string
- {
- $escapers = ['\\', '"'];
- $replacements = ['\\\\', '\"'];
- return str_replace($escapers, $replacements, $value);
- }
- /**
- * 예약날짜의 값이 정확한 값인지 확인합니다.
- */
- private function _checkCommonTypeDate(?string $strDate): string
- {
- $strDate = preg_replace("/[^0-9]/", "", $strDate);
- if ($strDate) {
- if (strlen($strDate) != 12) {
- return '예약날짜오류';
- }
- if (!checkdate(substr($strDate, 4, 2), substr($strDate, 6, 2), substr($strDate, 0, 4))) {
- return "예약날짜오류";
- }
- if (substr($strDate, 8, 2) > 23 || substr($strDate, 10, 2) > 59) {
- return "예약시간오류";
- }
- }
- return '';
- }
- }
|