import requests import json import time import hashlib import hmac import base64 from django.conf import settings from typing import Dict, Any, Optional class NaverCloudSMS: """네이버 클라우드 플랫폼 SMS 서비스 클래스""" def __init__(self): self.access_key = getattr(settings, 'NAVER_CLOUD_ACCESS_KEY', '') self.secret_key = getattr(settings, 'NAVER_CLOUD_SECRET_KEY', '') self.service_id = getattr(settings, 'NAVER_CLOUD_SMS_SERVICE_ID', '') self.sender_phone = getattr(settings, 'NAVER_CLOUD_SMS_SENDER_PHONE', '') # API 엔드포인트 self.base_url = "https://sens.apigw.ntruss.com" self.sms_url = f"{self.base_url}/sms/v2/services/{self.service_id}/messages" def _make_signature(self, timestamp: str) -> str: """네이버 클라우드 API 서명 생성""" space = " " new_line = "\n" method = "POST" url = f"/sms/v2/services/{self.service_id}/messages" message = method + space + url + new_line + timestamp + new_line + self.access_key message = message.encode('utf-8') signing_key = base64.b64encode( hmac.new( self.secret_key.encode('utf-8'), message, digestmod=hashlib.sha256 ).digest() ).decode('utf-8') return signing_key def send_sms(self, phone_number: str, message: str) -> Dict[str, Any]: """SMS 발송""" try: timestamp = str(int(time.time() * 1000)) signature = self._make_signature(timestamp) headers = { 'Content-Type': 'application/json; charset=utf-8', 'x-ncp-apigw-timestamp': timestamp, 'x-ncp-iam-access-key': self.access_key, 'x-ncp-apigw-signature-v2': signature } data = { 'type': 'SMS', 'contentType': 'COMM', 'countryCode': '82', 'from': self.sender_phone, 'content': message, 'messages': [ { 'to': phone_number } ] } response = requests.post( self.sms_url, headers=headers, data=json.dumps(data) ) if response.status_code == 202: result = response.json() return { 'success': True, 'request_id': result.get('requestId'), 'status_code': result.get('statusCode'), 'status_name': result.get('statusName') } else: return { 'success': False, 'error': f'HTTP {response.status_code}: {response.text}' } except Exception as e: return { 'success': False, 'error': str(e) } def send_verification_code(self, phone_number: str, verification_code: str) -> Dict[str, Any]: """인증번호 SMS 발송""" message = f"[신라AMP] 인증번호는 [{verification_code}] 입니다. 3분 이내에 입력해주세요." # 발신번호가 설정되지 않은 경우 에러 반환 if not self.sender_phone: print(f"[ERROR] 발신번호가 설정되지 않았습니다.") return { 'success': False, 'error': '발신번호가 설정되지 않았습니다. .env 파일을 확인해주세요.' } print(f"[INFO] 실제 SMS 발송 시도: {phone_number} - {verification_code}") return self.send_sms(phone_number, message) def send_withdrawal_approval_sms(self, phone_number: str, name: str) -> Dict[str, Any]: """탈퇴 승인 SMS 발송""" message = f"[신라AMP] {name}님의 회원탈퇴 요청이 처리되었습니다." if not self.sender_phone: print(f"[ERROR] 발신번호가 설정되지 않았습니다.") return { 'success': False, 'error': '발신번호가 설정되지 않았습니다.' } print(f"[INFO] 탈퇴 승인 SMS 발송: {phone_number} - {name}") return self.send_sms(phone_number, message) def send_withdrawal_rejection_sms(self, phone_number: str, name: str, reason: str = None) -> Dict[str, Any]: """탈퇴 거부 SMS 발송""" if reason: # message = f"[신라AMP] {name}님의 회원탈퇴 요청이 거부되었습니다. 사유: {reason}" message = f"[신라AMP] {name}님의 회원탈퇴 요청이 거부되었습니다. 자세한 내용은 관리자에게 문의해주세요." else: message = f"[신라AMP] {name}님의 회원탈퇴 요청이 거부되었습니다. 자세한 내용은 관리자에게 문의해주세요." if not self.sender_phone: print(f"[ERROR] 발신번호가 설정되지 않았습니다.") return { 'success': False, 'error': '발신번호가 설정되지 않았습니다.' } print(f"[INFO] 탈퇴 거부 SMS 발송: {phone_number} - {name}") return self.send_sms(phone_number, message) # 전역 인스턴스 sms_service = NaverCloudSMS() def send_verification_sms(phone_number: str, verification_code: str) -> Dict[str, Any]: """인증번호 SMS 발송 함수 (편의 함수)""" # SMS 테스트 모드 확인 sms_test_mode = getattr(settings, 'SMS_TEST_MODE', False) if sms_test_mode: # 테스트 모드: 콘솔에 인증번호 출력 print("=" * 60) print("🔔 [SMS 테스트 모드] 인증번호 발송") print(f"📱 수신번호: {phone_number}") print(f"🔑 인증번호: {verification_code}") print(f"📝 메시지: [신라AMP] 인증번호는 [{verification_code}]입니다. 3분 이내에 입력해주세요.") print("=" * 60) return { 'success': True, 'message': 'SMS 테스트 모드: 콘솔에 인증번호가 출력되었습니다.', 'test_mode': True, 'verification_code': verification_code # 테스트 모드에서만 포함 } else: # 실제 SMS 발송 모드 print(f"[DEBUG] 실제 SMS 발송 시도: {phone_number} - {verification_code}") print(f"[DEBUG] Access Key: {sms_service.access_key}") print(f"[DEBUG] Service ID: {sms_service.service_id}") print(f"[DEBUG] Sender Phone: {sms_service.sender_phone}") result = sms_service.send_verification_code(phone_number, verification_code) print(f"[DEBUG] SMS 발송 결과: {result}") return result def send_withdrawal_approval_sms(phone_number: str, name: str) -> Dict[str, Any]: """탈퇴 승인 SMS 발송 함수 (편의 함수)""" sms_test_mode = getattr(settings, 'SMS_TEST_MODE', False) if sms_test_mode: message = f"[신라AMP] {name}님의 회원탈퇴가 승인되었습니다. 그동안 이용해주셔서 감사했습니다." print("=" * 60) print("🔔 [SMS 테스트 모드] 탈퇴 승인 알림") print(f"📱 수신번호: {phone_number}") print(f"👤 대상자: {name}") print(f"📝 메시지: {message}") print("=" * 60) return { 'success': True, 'message': 'SMS 테스트 모드: 탈퇴 승인 알림이 콘솔에 출력되었습니다.', 'test_mode': True } else: return sms_service.send_withdrawal_approval_sms(phone_number, name) def send_withdrawal_rejection_sms(phone_number: str, name: str, reason: str = None) -> Dict[str, Any]: """탈퇴 거부 SMS 발송 함수 (편의 함수)""" sms_test_mode = getattr(settings, 'SMS_TEST_MODE', False) if sms_test_mode: if reason: message = f"[신라AMP] {name}님의 회원탈퇴 요청이 거부되었습니다. 사유: {reason}" else: message = f"[신라AMP] {name}님의 회원탈퇴 요청이 거부되었습니다. 자세한 사항은 관리자에게 문의해주세요." print("=" * 60) print("🔔 [SMS 테스트 모드] 탈퇴 거부 알림") print(f"📱 수신번호: {phone_number}") print(f"👤 대상자: {name}") print(f"📝 메시지: {message}") if reason: print(f"❌ 거부 사유: {reason}") print("=" * 60) return { 'success': True, 'message': 'SMS 테스트 모드: 탈퇴 거부 알림이 콘솔에 출력되었습니다.', 'test_mode': True } else: return sms_service.send_withdrawal_rejection_sms(phone_number, name, reason)