package service import ( "baron-sso-backend/internal/domain" "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "log/slog" "net/http" "os" "strconv" "strings" "time" ) const naverSMSMaxBytes = 90 type SmsServiceImpl struct { accessKey string secretKey string serviceID string senderPhone string } func NewSmsService() domain.SmsService { // Sanitize sender phone number right after reading from env rawSenderPhone := os.Getenv("NAVER_SENDER_PHONE_NUMBER") sanitizedSenderPhone := strings.ReplaceAll(rawSenderPhone, "-", "") slog.Info("[서비스 초기화] 발신자 번호 처리", "원본", rawSenderPhone, "정제후", sanitizedSenderPhone) return &SmsServiceImpl{ accessKey: os.Getenv("NAVER_CLOUD_ACCESS_KEY"), secretKey: os.Getenv("NAVER_CLOUD_SECRET_KEY"), serviceID: os.Getenv("NAVER_CLOUD_SERVICE_ID"), senderPhone: sanitizedSenderPhone, } } func (s *SmsServiceImpl) SendSms(to, content string) error { timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) apiURL := fmt.Sprintf("https://sens.apigw.ntruss.com/sms/v2/services/%s/messages", s.serviceID) slog.Info("[SmsService] Requesting SENS API URL", "url", apiURL) // Naver SENS API requires phone number without '+' sanitizedTo := strings.Replace(to, "+", "", 1) reqBody := buildNaverSmsRequest(s.senderPhone, sanitizedTo, content) if reqBody.Type == "LMS" { slog.Info("[SmsService] Upgrading message type to LMS due to content length", "bytes", len([]byte(content)), ) } jsonBody, err := json.Marshal(reqBody) if err != nil { return fmt.Errorf("error marshalling request body: %w", err) } signature, err := s.makeSignature("POST", fmt.Sprintf("/sms/v2/services/%s/messages", s.serviceID), timestamp) if err != nil { return fmt.Errorf("error creating signature: %w", err) } req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBody)) if err != nil { return fmt.Errorf("error creating request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("x-ncp-apigw-timestamp", timestamp) req.Header.Set("x-ncp-iam-access-key", s.accessKey) req.Header.Set("x-ncp-apigw-signature-v2", signature) client := &http.Client{} resp, err := client.Do(req) if err != nil { return fmt.Errorf("error sending request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("error reading response body: %w", err) } if resp.StatusCode >= 300 { slog.Error("[SmsService] error response from naver cloud sms api", "body", string(respBody)) return fmt.Errorf("error sending sms: status code %d", resp.StatusCode) } slog.Info("[SmsService] sms sent successfully", "body", string(respBody)) return nil } func buildNaverSmsRequest(senderPhone, sanitizedTo, content string) domain.NaverSmsRequest { requestType := "SMS" subject := "" if len([]byte(content)) > naverSMSMaxBytes { requestType = "LMS" subject = "[Baron 로그인]" } return domain.NaverSmsRequest{ Type: requestType, ContentType: "COMM", CountryCode: "82", From: senderPhone, Subject: subject, Content: content, Messages: []domain.SmsMessage{ { To: sanitizedTo, }, }, } } func (s *SmsServiceImpl) makeSignature(method, url, timestamp string) (string, error) { space := " " newLine := "\n" message := method + space + url + newLine + timestamp + newLine + s.accessKey h := hmac.New(sha256.New, []byte(s.secretKey)) _, err := h.Write([]byte(message)) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil }