Skip to content

Commit

Permalink
Merge pull request #117 from PawWithU/feat/114-firebase-setting
Browse files Browse the repository at this point in the history
[Feature] Firebase 설정 및 푸시 알림 구현
  • Loading branch information
kyeong-hyeok authored Nov 19, 2023
2 parents 2fe1c33 + 00b8b4c commit ad12056
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 2 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ jobs:
echo "${{ secrets.APPLICATION_DEV_YML }}" > ./application-dev.yml
shell: bash

# firebase-key.json 생성
- name: create firebase-key.json
run: |
cd ./src/main/resources
touch ./firebase-key.json
echo "${{ secrets.FCM }}" | base64 --decode > ./firebase-key.json
ls -la
shell: bash

# ./gradlew 권한 설정
- name: ./gradlew 권한 설정
run: chmod +x ./gradlew
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ out/
application-dev.yml
application-prod.yml
application-oauth.yml
application-jwt.yml
application-jwt.yml

src/main/resources/firebase-key.json
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
// firebase
implementation 'com.google.firebase:firebase-admin:6.8.1'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2'
}

// Querydsl 설정부
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.pawwithu.connectdog.domain.fcm.controller;

import com.pawwithu.connectdog.domain.fcm.dto.request.IntermediaryFcmRequest;
import com.pawwithu.connectdog.domain.fcm.dto.request.VolunteerFcmRequest;
import com.pawwithu.connectdog.domain.fcm.service.FcmService;
import com.pawwithu.connectdog.error.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Fcm", description = "Fcm API")
@RestController
@RequiredArgsConstructor
public class FcmController {

private final FcmService fcmService;

@Operation(summary = "이동봉사자 - FCM 토큰 저장", description = "이동봉사자의 FCM 토큰을 저장합니다.",
responses = {@ApiResponse(responseCode = "200", description = "FCM 토큰 저장 성공")
, @ApiResponse(responseCode = "400"
, description = "V1, fcm 토큰은 필수 입력 값입니다. \t\n M1, 해당 이동봉사자를 찾을 수 없습니다."
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/volunteers/fcm")
public ResponseEntity<Void> saveVolunteerFcm(@AuthenticationPrincipal UserDetails loginUser,
@Valid @RequestBody VolunteerFcmRequest request) {

fcmService.saveVolunteerFcm(loginUser.getUsername(), request);
return ResponseEntity.noContent().build();
}

@Operation(summary = "이동봉사 중개 - FCM 토큰 저장", description = "이동봉사 중개의 FCM 토큰을 저장합니다.",
responses = {@ApiResponse(responseCode = "200", description = "FCM 토큰 저장 성공")
, @ApiResponse(responseCode = "400"
, description = "V1, fcm 토큰은 필수 입력 값입니다. \t\n M2, 해당 이동봉사 중개를 찾을 수 없습니다."
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/intermediaries/fcm")
public ResponseEntity<Void> saveIntermediaryFcm(@AuthenticationPrincipal UserDetails loginUser,
@Valid @RequestBody IntermediaryFcmRequest request) {

fcmService.saveIntermediaryFcm(loginUser.getUsername(), request);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.pawwithu.connectdog.domain.fcm.dto.request;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Builder
@AllArgsConstructor
@Getter
public class FcmMessage {

private boolean validateOnly;
private Message message;

@Builder
@AllArgsConstructor
@Getter
public static class Message {
private Notification notification;
private String token;

}

@Builder
@AllArgsConstructor
@Getter
public static class Notification {
private String title;
private String body;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pawwithu.connectdog.domain.fcm.dto.request;

import com.pawwithu.connectdog.domain.fcm.entity.IntermediaryFcm;
import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import jakarta.validation.constraints.NotBlank;

public record IntermediaryFcmRequest(@NotBlank(message = "fcm 토큰은 필수 입력 값입니다.")
String fcmToken) {

public static IntermediaryFcm IntermediaryToEntity(Intermediary intermediary, IntermediaryFcmRequest request) {
return IntermediaryFcm.builder()
.intermediary(intermediary)
.fcmToken(request.fcmToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pawwithu.connectdog.domain.fcm.dto.request;

import com.pawwithu.connectdog.domain.fcm.entity.VolunteerFcm;
import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import jakarta.validation.constraints.NotBlank;

public record VolunteerFcmRequest(@NotBlank(message = "fcm 토큰은 필수 입력 값입니다.")
String fcmToken) {

public static VolunteerFcm volunteerToEntity(Volunteer volunteer, VolunteerFcmRequest request) {
return VolunteerFcm.builder()
.volunteer(volunteer)
.fcmToken(request.fcmToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.pawwithu.connectdog.domain.fcm.entity;

import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class IntermediaryFcm {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String fcmToken;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "intermediary_id", nullable = false)
private Intermediary intermediary;

@Builder
public IntermediaryFcm(String fcmToken, Intermediary intermediary) {
this.fcmToken = fcmToken;
this.intermediary = intermediary;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.pawwithu.connectdog.domain.fcm.entity;

import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class VolunteerFcm {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String fcmToken;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "volunteer_id", nullable = false)
private Volunteer volunteer;

@Builder
public VolunteerFcm(String fcmToken, Volunteer volunteer) {
this.fcmToken = fcmToken;
this.volunteer = volunteer;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pawwithu.connectdog.domain.fcm.repository;

import com.pawwithu.connectdog.domain.fcm.entity.IntermediaryFcm;
import org.springframework.data.jpa.repository.JpaRepository;

public interface IntermediaryFcmRepository extends JpaRepository<IntermediaryFcm, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pawwithu.connectdog.domain.fcm.repository;

import com.pawwithu.connectdog.domain.fcm.entity.VolunteerFcm;
import org.springframework.data.jpa.repository.JpaRepository;

public interface VolunteerFcmRepository extends JpaRepository<VolunteerFcm, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.pawwithu.connectdog.domain.fcm.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.net.HttpHeaders;
import com.pawwithu.connectdog.domain.fcm.dto.request.FcmMessage;
import com.pawwithu.connectdog.domain.fcm.dto.request.IntermediaryFcmRequest;
import com.pawwithu.connectdog.domain.fcm.dto.request.VolunteerFcmRequest;
import com.pawwithu.connectdog.domain.fcm.entity.IntermediaryFcm;
import com.pawwithu.connectdog.domain.fcm.entity.VolunteerFcm;
import com.pawwithu.connectdog.domain.fcm.repository.IntermediaryFcmRepository;
import com.pawwithu.connectdog.domain.fcm.repository.VolunteerFcmRepository;
import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import com.pawwithu.connectdog.domain.intermediary.repository.IntermediaryRepository;
import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import com.pawwithu.connectdog.domain.volunteer.repository.VolunteerRepository;
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.Arrays;

import static com.pawwithu.connectdog.error.ErrorCode.INTERMEDIARY_NOT_FOUND;
import static com.pawwithu.connectdog.error.ErrorCode.VOLUNTEER_NOT_FOUND;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class FcmService {

@Value("${fcm.config.path}")
private String FIREBASE_CONFIG_PATH;

@Value("${fcm.api.url}")
private String FIREBASE_API_URL;

private final ObjectMapper objectMapper;
private final VolunteerRepository volunteerRepository;
private final VolunteerFcmRepository volunteerFcmRepository;
private final IntermediaryRepository intermediaryRepository;
private final IntermediaryFcmRepository intermediaryFcmRepository;

private String getAccessToken() throws IOException {

// firebase로 부터 access token을 가져온다.
GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(FIREBASE_CONFIG_PATH).getInputStream())
.createScoped(Arrays.asList("https://www.googleapis.com/auth/cloud-platform"));

googleCredentials.refreshIfExpired();
return googleCredentials.getAccessToken().getTokenValue();

}

/**
* makeMessage : 알림 파라미터들을 FCM이 요구하는 body 형태로 가공한다.
* @param targetToken : firebase token
* @param title : 알림 제목
* @param body : 알림 내용
* @return
* */
public String makeMessage(String targetToken, String title, String body) throws JsonProcessingException {

FcmMessage fcmMessage = FcmMessage.builder()
.message(
FcmMessage.Message.builder()
.token(targetToken)
.notification(
FcmMessage.Notification.builder()
.title(title)
.body(body)
.build())
.build()
)
.validateOnly(false)
.build();

return objectMapper.writeValueAsString(fcmMessage);

}

/**
* 알림 푸쉬를 보내는 역할을 하는 메서드
* @param targetToken : 푸쉬 알림을 받을 클라이언트 앱의 식별 토큰
* */
public void sendMessageTo(String targetToken, String title, String body) throws IOException {

String message = makeMessage(targetToken, title, body);

OkHttpClient client = new OkHttpClient();
RequestBody requestBody = RequestBody.create(message, MediaType.get("application/json; charset=utf-8"));

Request request = new Request.Builder()
.url(FIREBASE_API_URL)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer "+getAccessToken())
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build();

Response response = client.newCall(request).execute();
log.info(response.body().string());
return;
}

public void saveVolunteerFcm(String email, VolunteerFcmRequest request) {
Volunteer volunteer = volunteerRepository.findByEmail(email).orElseThrow(() -> new BadRequestException(VOLUNTEER_NOT_FOUND));
VolunteerFcm volunteerFcm = VolunteerFcmRequest.volunteerToEntity(volunteer, request);
volunteerFcmRepository.save(volunteerFcm);
}

public void saveIntermediaryFcm(String email, IntermediaryFcmRequest request) {
Intermediary intermediary = intermediaryRepository.findByEmail(email).orElseThrow(() -> new BadRequestException(INTERMEDIARY_NOT_FOUND));
IntermediaryFcm intermediaryFcm = IntermediaryFcmRequest.IntermediaryToEntity(intermediary, request);
intermediaryFcmRepository.save(intermediaryFcm);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@
import java.time.LocalDate;

public record PostSearchRequest(@RequestParam(value = "postStatus", required = false) PostStatus postStatus,
String departureLoc,
@RequestParam(value = "departureLoc", required = false) String departureLoc,
@RequestParam(value = "arrivalLoc", required = false)
String arrivalLoc,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate startDate,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate endDate,
@RequestParam(value = "dogSize", required = false) DogSize dogSize,
Boolean isKennel,
@RequestParam(value = "intermediaryName", required = false)
String intermediaryName,
@RequestParam(value = "orderCondition", required = false)
String orderCondition) {

}
Loading

0 comments on commit ad12056

Please sign in to comment.