Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/github_actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
S3_REGION: ${{ secrets.S3_REGION }}
FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}

steps:
# 1. GitHub Repository 파일 불러오기
Expand Down Expand Up @@ -52,7 +53,13 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

# 6. Docker 이미지 생성 및 Push
# 6. firebase 설정 파일 생성
- name: Create firebase.json from Secret
run: |
mkdir -p src/main/resources/firebase
echo "${{ secrets.FIREBASE_JSON }}" > src/main/resources/firebase/firebase.json

# 7. Docker 이미지 생성 및 Push
- name: Docker 이미지 생성 및 push
uses: docker/build-push-action@v6
with:
Expand All @@ -74,9 +81,10 @@ jobs:
S3_SECRET_KEY= ${{ secrets.S3_SECRET_KEY }}
S3_BUCKET_NAME= ${{ secrets.S3_BUCKET_NAME }}
S3_REGION= ${{ secrets.S3_REGION }}
FIREBASE_PROJECT_ID= ${{ secrets.FIREBASE_PROJECT_ID }}

## CD (Continuous Deployment) 파트
# 7. EC2에 SSH로 접속하여 Docker 컨테이너 실행
# 8. EC2에 SSH로 접속하여 Docker 컨테이너 실행
- name: EC2에 배포
uses: appleboy/[email protected]
with:
Expand Down Expand Up @@ -105,4 +113,5 @@ jobs:
-e S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }} \
-e S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }} \
-e S3_REGION=${{ secrets.S3_REGION }} \
-e FIREBASE_PROJECT_ID=${{ secrets.FIREBASE_PROJECT_ID }} \
${{ secrets.DOCKERHUB_USERNAME }}/challenge:${{ github.sha }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ build/
/src/main/generated
/src/main/resources/application-test.yml
/src/main/resources/application-local.yml
/src/main/resources/firebase/**


# General
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ ARG S3_ACCESS_KEY
ARG S3_SECRET_KEY
ARG S3_BUCKET_NAME
ARG S3_REGION
ARG FIREBASE_PROJECT_ID

# 라이브러리 설치에 필요한 파일만 복사
COPY build.gradle settings.gradle ./
Expand Down
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ dependencies {

// s3
implementation "software.amazon.awssdk:s3:2.13.0"

// Google Firebase
implementation 'com.google.firebase:firebase-admin:9.1.1'

}

tasks.withType(JavaCompile) {
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/com/challenge/api/controller/fcm/FcmController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.challenge.api.controller.fcm;

import com.challenge.annotation.AuthMember;
import com.challenge.api.ApiResponse;
import com.challenge.api.controller.fcm.request.FcmSendByIdRequest;
import com.challenge.api.controller.fcm.request.FcmSendByTokenRequest;
import com.challenge.api.controller.fcm.request.TokenSaveRequest;
import com.challenge.api.service.fcm.FcmService;
import com.challenge.domain.member.Member;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/fcm")
public class FcmController {

private final FcmService fcmService;

@PostMapping("/send/token")
public ApiResponse<String> sendMessageByToken(@Valid @RequestBody FcmSendByTokenRequest request) {
return ApiResponse.ok(fcmService.sendMessage(FcmSendByTokenRequest.toFcmMessage(request)));
}

@PostMapping("/send/member")
public ApiResponse<String> sendMessageByMemberId(@Valid @RequestBody FcmSendByIdRequest request) {
return ApiResponse.ok(fcmService.sendMessageById(request));
}

@PostMapping
public ApiResponse<String> saveToken(@Valid @RequestBody TokenSaveRequest request,
@AuthMember Member member) {
return ApiResponse.ok(fcmService.saveToken(request.toServiceRequest(), member));
}

@DeleteMapping
public ApiResponse<String> deleteToken(@AuthMember Member member) {
return ApiResponse.ok(fcmService.deleteToken(member));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.challenge.api.controller.fcm.request;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class FcmSendByIdRequest {

@NotNull
private Long memberId;

@NotEmpty
private String title;

@NotEmpty
private String body;

@Builder
private FcmSendByIdRequest(Long memberId, String title, String body) {
this.memberId = memberId;
this.title = title;
this.body = body;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.challenge.api.controller.fcm.request;

import com.challenge.api.service.fcm.request.FcmMessage;
import jakarta.validation.constraints.NotEmpty;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class FcmSendByTokenRequest {

@NotEmpty
private String token;

@NotEmpty
private String title;

@NotEmpty
private String body;

@Builder
private FcmSendByTokenRequest(String token, String title, String body) {
this.token = token;
this.title = title;
this.body = body;
}

public static FcmMessage toFcmMessage(FcmSendByTokenRequest request) {
return FcmMessage.of(request.getToken(), request.getTitle(), request.getBody());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.challenge.api.controller.fcm.request;

import com.challenge.api.service.fcm.request.TokenSaveServiceRequest;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class TokenSaveRequest {

@NotBlank(message = "token은 필수 입력값입니다.")
String token;

@Builder
private TokenSaveRequest(String token) {
this.token = token;
}

public TokenSaveServiceRequest toServiceRequest() {
return TokenSaveServiceRequest.builder()
.token(this.token)
.build();
}

}
86 changes: 86 additions & 0 deletions src/main/java/com/challenge/api/service/fcm/FcmService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.challenge.api.service.fcm;

import com.challenge.api.controller.fcm.request.FcmSendByIdRequest;
import com.challenge.api.service.fcm.request.FcmMessage;
import com.challenge.api.service.fcm.request.TokenSaveServiceRequest;
import com.challenge.domain.member.Member;
import com.challenge.domain.member.MemberRepository;
import com.challenge.domain.notification.Notification;
import com.challenge.domain.notification.NotificationRepository;
import com.challenge.exception.ErrorCode;
import com.challenge.exception.GlobalException;
import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.ApsAlert;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Slf4j
@Service
@RequiredArgsConstructor
public class FcmService {

private final FirebaseMessaging firebaseMessaging;
private final MemberRepository memberRepository;
private final NotificationRepository notificationRepository;

public String sendMessage(FcmMessage request) {
try {
// fcm 메시지 발송
Message message = request.buildMessage().setApnsConfig(getApnsConfig(request)).build();
firebaseMessaging.send(message);
} catch (Exception exception) {
log.error(exception.getMessage(), exception);
throw new GlobalException(ErrorCode.FCM_SERVICE_UNAVAILABLE);
}

return "fcm 푸시 발송 성공";
}

@Transactional
public String sendMessageById(FcmSendByIdRequest request) {
Member member = memberRepository.findById(request.getMemberId()).orElseThrow(
() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND));

if (member.getFcmToken() == null) {
throw new GlobalException(ErrorCode.FCM_TOKEN_NOT_FOUND);
}

// fcm 메시지 전송
FcmMessage fcmMessage = FcmMessage.of(member.getFcmToken(), request.getTitle(), request.getBody());
String result = sendMessage(fcmMessage);

// 알림 발송 내역 저장
Notification notification = Notification.of(request.getTitle(), request.getBody(), LocalDateTime.now(), member);
notificationRepository.save(notification);

return result;
}

@Transactional
public String saveToken(TokenSaveServiceRequest request, Member member) {
member.updateFcmToken(request.getToken());

return "fcm 토큰 저장 성공";
}

@Transactional
public String deleteToken(Member member) {
member.updateFcmToken(null);

return "fcm 토큰 삭제 성공";
}

private ApnsConfig getApnsConfig(FcmMessage request) {
ApsAlert alert = ApsAlert.builder().setTitle(request.getTitle()).setBody(request.getBody()).build();
Aps aps = Aps.builder().setAlert(alert).setSound("default").build();
return ApnsConfig.builder().setAps(aps).build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.challenge.api.service.fcm.request;

import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class FcmMessage {

String token;
String title;
String body;

public static FcmMessage of(String token, String title, String body) {
return FcmMessage.builder()
.token(token)
.title(title)
.body(body)
.build();
}

public Message.Builder buildMessage() {
return Message.builder()
.setToken(token)
.setNotification(toNotification());
}

public Notification toNotification() {
return Notification.builder()
.setTitle(title)
.setBody(body)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.challenge.api.service.fcm.request;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class TokenSaveServiceRequest {

String token;

@Builder
private TokenSaveServiceRequest(String token) {
this.token = token;
}

}
Loading
Loading