본문 바로가기
Spring Boot :

[FN] 모임 신청 개발 일지[1] / Spring Boot - FCM 푸시 알림 전송

by 밍코딩코 2025. 2. 21.

클라이언트에서 Spring Boot 서버로 FCM 토큰을 보내는 테스트 과정은 아래 게시글 참고하기

 

[FN] Spring Boot API / FCM Token 전송

모임 신청을 하고 신청에 대한 알림을 FCM으로 모임장에게 띄워주기 위해서React Native에서 Spring Boot 서버로 FCM 토큰을 보내주어야하는데 로컬호스트:포트 번호로 연결할 수는 있지만,다른 기기나

codingco.tistory.com

이제 알림을 보내는 기능을 구현해보자.

신청서를 작성하고 작성 완료 버튼을 누르면 -> Spring Boot 서버로 요청을 보내고 -> 서버에서는 모임장에게 알림 전송

 

1. DTO

클라이언트와 서버 간 데이터를 주고받을 때 사용하는 객체로 DTO 파일을 생성한다.

( React Native에서 백엔드(Spring Boot)로 푸시 알림 요청을 보낼 때 JSON 데이터를 담는 용도)

코드를 간결하게 짜기 위해 Lombok을 사용하기로 하였다.

 

의존성 추가

implementation 'org.projectlombok:lombok:1.18.28' 
annotationProcessor 'org.projectlombok:lombok:1.18.28'

 

NotificationRequest.java

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class NotificationRequest {
    private String targetToken; // 알림 받을 사용자의 FCM 토큰
    private String title; // 알림 제목
    private String message; // 알림 메시지
}

 

2. FCM 알림 전송 서비스

FcmService.java

  • Message : FCM을 통해 보낼 메시지를 생성하는 클래스
  • Notification : 푸시 알림의 제목과 내용을 설정함 (title, body)
  • Map : 데이터 형태로 추가 정보를 전달하기 위해 (key - value)

 

  • Notification notification = Notification.builder() : 알림 생성
  • Message firebaseMessage = Message.builder() : 푸시 메시지 생성
  • FirebaseMessaging.getInstance().send(firebaseMessage): 실제로 FCM 서버에 요청하여 푸시 알림을 보냄
package com.example.project;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class FcmService {

    public void sendNotification(String targetToken, String title, String message) {
    	System.out.println("전달받은 targetToken: " + targetToken);
        if (targetToken == null || targetToken.isEmpty()) {
            System.out.println("유효하지 않은 FCM 토큰입니다.");
            return;
        }

        Notification notification = Notification.builder()
                .setTitle(title)
                .setBody(message)
                .build();

       Message firebaseMessage = Message.builder()
                .setToken(targetToken)
                .setNotification(notification)
                .putAllData(Map.of(
                        "title", title,
                        "message", message
                ))
                .build();

        try {
            FirebaseMessaging.getInstance().send(firebaseMessage);
            System.out.println("푸시 알림 전송 성공!");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("푸시 알림 전송 실패: " + e.getMessage());
        }
    }
}

 

다음으로는 API 엔드포인트를 설정해준다 api/send-norification 으로 엔드포인트 설정

 

NotificationController.java

package com.example.project;

import com.example.festanow_meeting.NotificationRequest;
import com.example.festanow_meeting.FcmService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class NotificationController {

    private final FcmService fcmService;

    public NotificationController(FcmService fcmService) {
        this.fcmService = fcmService;
    }

    @PostMapping("/send-notification")
    public ResponseEntity<String> sendNotification(@RequestBody NotificationRequest request) {
        fcmService.sendNotification(request.getTargetToken(), request.getTitle(), request.getMessage());
        return ResponseEntity.ok("푸시 알림 전송 완료!");
    }
}

 

3. React Native 신청서 작성 페이지

API가 구현되었다면 클라이언트에서 모임장의 FCM 토큰을 조회하고 서버로 요청을 보내고 푸시 알림이 전송되도록 구현해야한다.

 

<흐름>

  • MeetingContentScreen에서 postId를 MeetingJoinScreen으로 전달
  • MeetingJoinScreen에서 firestore의 meetings/{postId}/applications에 신청 정보 저장
  • authorId를 기반으로 users 컬렉션에서 fcmToken 조회
  • 조회한 fcmToken으로 푸시 알림 전송

MeetingJoinScreen.tsx

 

route로 이전 화면에서 postId, userId, authorId 등의 데이터 전달

type MeetingJoinScreenProps = StackNavigationProp<any, 'MeetingJoin'>;
type MeetingJoinScreenRouteProps = RouteProp<any, 'MeetingJoin'>;

type Props = {
    navigation: MeetingJoinScreenProps;
    route: MeetingJoinScreenRouteProps;
};

 

모임 신청자의 데이터를 가져오는 코드

const getApplicantNicName = async (userId: string) => {
        try {
            const userDoc = await firestore().collection("users").doc(userId).get();
            console.log("Firestore에서 가져온 신청자 문서:", userDoc.data()); // 로그 추가
            
            return userDoc.exists ? userDoc.data()?.nicName : "익명 사용자";
        } catch (error) {
            console.error("신청자의 nicName 조회 실패:", error);
            return "익명 사용자"; 
        }
    };

 

firestore에 users 컬렉션의 저장되어 있는 hostId 를 기반으로 모임장의 fcmToken을 가져온다.

hostId 는 authorId의 역할을 함. 즉, 모임을 만든 작성자의 userId가 hostId로 사용된 것임.

firestore에서 모임장의 데이터를 받아옴 -> 모임장의 FCM 토큰 존재 여부 체크

const getHostFcmToken = async (hostId: string) => {
        try {
            const hostDoc = await firestore().collection("users").doc(hostId).get();
            console.log("Firestore에서 가져온 host 문서:", hostDoc.data()); 
    
            const fcmToken = hostDoc.exists ? hostDoc.data()?.fcmToken : null;
            if (!fcmToken) {
                console.error("모임장의 FCM 토큰이 Firestore에 저장되어 있지 않습니다.");
            }
            return fcmToken;
        } catch (error) {
            console.error("모임장 FCM 토큰 조회 실패:", error);
            return null;
        }
    };

 

  • getHostFcmToken(hostId)를 호출하여 모임장의 FCM 토큰을 가져옴
  • axios를 이용해 서버로 FCM 요청을 보냄
  • 푸시 알림에는 모임 제목, 신청자 이름이 포함된다.
const sendPushNotification = async (hostId: string, meetingTitle: string, applicantName: string) => {
    const fcmToken = await getHostFcmToken(hostId);
    if (!fcmToken) {
        console.error("모임장의 FCM 토큰이 없습니다.");
        return;
    }
    try {
        await axios.post("http://ip주소/포트번호/api/send-notification", {
            targetToken: fcmToken,
            title: "새로운 모임 신청",
            body: `${applicantNicName}님이 "${meetingTitle}" 모임에 신청했습니다!`,
        });
        console.log("모임장에게 푸시 알림 전송 성공");
    } catch (error) {
        console.error("푸시 알림 전송 실패:", error);
    }
};

 

  • 작성 완료 버튼을 눌렀을 때 handleSubmit 함수 실행
  • meetings 컬렉션에서 postId 문서의 applications 하위 컬렉션에 신청 데이터 추가
  • meetings 컬렉션에서 postId 문서 조회
  • sendPushNotification 호출하여 모임장에게 FCM 푸시 알림 전송
  • 신청자의 닉네임 정보 applicantNicName
const handleSubmit = async () => {
    if (message.trim() === "") {
        Alert.alert("알림", "자기소개와 메시지를 입력해 주세요.");
        return;
    }
    try {
        const db = firestore();
        await db.collection("meetings").doc(postId).collection("applications").add({
            userId, 
            authorId, 
            message, 
            createdAt: firestore.FieldValue.serverTimestamp(),
        });
        const applicantNicName = await getApplicantNicName(userId);
        const meetingDoc = await db.collection("meetings").doc(postId).get();
        const meetingTitle = meetingDoc.exists ? meetingDoc.data()?.title : "모임";
        await sendPushNotification(authorId, meetingTitle, applicantNicName);
        Alert.alert("신청 완료", "모임 참여 신청이 완료되었습니다!");
        navigation.navigate("Home");
    } catch (error) {
        console.error("신청 실패:", error);
        Alert.alert("오류", "신청을 처리하는 중 문제가 발생했습니다.");
    }
};

 

결과

Firestore meeting 컬렉션 하위 applications 에 신청서 데이터가 잘 저장된다.
클라이언트에서 서버로 요청 성공 + 일단은 신청 알림을 콘솔에 신청서 데이터 전부 다 찍히게 해둠(서버에서 푸시 알림 전송하면 콘솔에 신청서 데이터가 찍힘)

 

서버에서 모임장 FCM 토큰 받고 푸시 알림 전송