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("
", " ", $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 .= "서버에 접속했습니다.

"; 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 .= "데이터를 다시 확인해 주시기바랍니다.
"; break; // 아래의 사유들은 전송진행이 중단됨. case '85': // "85:전송번호 미등록" $resultMessage .= "등록되지 않는 전송번호 입니다.
"; break; case '87': // "87:인증실패" $resultMessage .= "(정액제-계약확인)인증 받지 못하였습니다.
"; break; case '88': // "88:연동모듈 전송불가" $resultMessage .= "연동모듈 사용이 불가능합니다. 아이코드로 문의하세요.
"; break; case '96': // "96:토큰 검사 실패" $resultMessage .= "사용할 수 없는 토큰키입니다.
"; break; case '97': // "97:잔여코인부족" $resultMessage .= "잔여코인이 부족합니다.
"; break; case '98': // "98:사용기간만료" $resultMessage .= "사용기간이 만료되었습니다.
"; break; case '99': // "99:인증실패" $resultMessage .= "서비스 사용이 불가능합니다. 아이코드로 문의하세요.
"; break; default: // "미 확인 오류" $resultMessage .= "알 수 없는 오류로 전송이 실패하었습니다.
"; break; } $this->error[] = $resultMessage; } else { $resultCode = substr($code, 0, 2); switch ($resultCode) { case '17': // "17: 접수(전송)대기 처리. 지연해소시 전송됨." $resultMessage .= "접수(전송)대기처리 되었습니다."; break; default: // "00: 전송완료." $resultMessage .= "전송되었습니다.
"; break; } $resultMessage .= $phone . "로 (msg seq : ' . $seq . ')
"; } $this->total++; $this->history[$i]['code'] = $resultCode; $this->history[$i]['message'] = addslashes($resultMessage); } $resultMessage .= "
전체(" . $this->total . "건) / 전송완료(" . $this->success . "건) / 전송실패(" . $this->failed . "건)
"; // 전송 결과 저장 $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 ''; } }