Skip to content

Commit

Permalink
Merge pull request #64 from kakao-tech-campus-2nd-step3/feature/63-fcm
Browse files Browse the repository at this point in the history
feat: FCM ํ‘ธ์‰ฌ ์•Œ๋ฆผ ๊ตฌํ˜„
  • Loading branch information
peeerr authored Oct 22, 2024
2 parents 29f8796 + 9842390 commit 939d685
Show file tree
Hide file tree
Showing 19 changed files with 457 additions and 22 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/master_weekly_cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ jobs:
distribution: 'corretto'
java-version: '21'

- name: FCM ์„œ๋น„์Šค ๊ณ„์ • ํ‚ค ํŒŒ์ผ ์ƒ์„ฑ
run: |
echo '${{ secrets.FCM_SERVICE_ACCOUNT_KEY }}' > ./src/main/resources/everymoment.json
- name: AWS S3 ๊ด€๋ จ ์ •๋ณด๋ฅผ ์„ค์ • ํŒŒ์ผ์— ์ฃผ์ž…
uses: microsoft/variable-substitution@v1
with:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pr_weekly_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ jobs:
distribution: 'corretto'
java-version: '21'

- name: FCM ์„œ๋น„์Šค ๊ณ„์ • ํ‚ค ํŒŒ์ผ ์ƒ์„ฑ
run: |
echo '${{ secrets.FCM_SERVICE_ACCOUNT_KEY }}' > ./src/main/resources/everymoment.json
- name: AWS S3 ๊ด€๋ จ ์ •๋ณด๋ฅผ ์„ค์ • ํŒŒ์ผ์— ์ฃผ์ž…
uses: microsoft/variable-substitution@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build/
!**/src/test/**/build/

application-dev.yml
everymoment.json

### STS ###
.apt_generated
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ dependencies {
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.657'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

implementation 'com.google.firebase:firebase-admin:9.2.0'

implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/potatocake/everymoment/config/FcmConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.potatocake.everymoment.config;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import java.io.IOException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

@Configuration
public class FcmConfig {

@Bean
public FirebaseMessaging firebaseMessaging() throws IOException {
GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource("everymoment.json").getInputStream());

FirebaseOptions firebaseOptions = FirebaseOptions.builder()
.setCredentials(googleCredentials)
.build();

FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions);

return FirebaseMessaging.getInstance(app);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.potatocake.everymoment.controller;

import com.potatocake.everymoment.dto.SuccessResponse;
import com.potatocake.everymoment.dto.request.FcmTokenRequest;
import com.potatocake.everymoment.security.MemberDetails;
import com.potatocake.everymoment.service.FcmService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "FCM", description = "FCM ํ† ํฐ ๊ด€๋ฆฌ API")
@RestController
@RequestMapping("/api/fcm")
@RequiredArgsConstructor
public class FcmController {

private final FcmService fcmService;

@Operation(summary = "FCM ํ† ํฐ ๋“ฑ๋ก/๊ฐฑ์‹ ", description = "๋””๋ฐ”์ด์Šค์˜ FCM ํ† ํฐ์„ ๋“ฑ๋กํ•˜๊ฑฐ๋‚˜ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.")
@ApiResponse(responseCode = "200", description = "ํ† ํฐ ๋“ฑ๋ก/๊ฐฑ์‹  ์„ฑ๊ณต")
@PostMapping("/token")
public ResponseEntity<SuccessResponse> registerToken(
@Parameter(description = "์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด", hidden = true)
@AuthenticationPrincipal MemberDetails memberDetails,
@Parameter(description = "FCM ํ† ํฐ ์ •๋ณด", required = true)
@RequestBody @Valid FcmTokenRequest request) {

fcmService.registerToken(memberDetails.getId(), request.getDeviceId(), request.getFcmToken());

return ResponseEntity.ok()
.body(SuccessResponse.ok());
}

@Operation(summary = "FCM ํ† ํฐ ์‚ญ์ œ", description = "๋””๋ฐ”์ด์Šค์˜ FCM ํ† ํฐ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.")
@ApiResponse(responseCode = "200", description = "ํ† ํฐ ์‚ญ์ œ ์„ฑ๊ณต")
@DeleteMapping("/token")
public ResponseEntity<SuccessResponse> removeToken(
@Parameter(description = "์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด", hidden = true)
@AuthenticationPrincipal MemberDetails memberDetails,
@Parameter(description = "๋””๋ฐ”์ด์Šค ID", required = true)
@RequestParam String deviceId) {

fcmService.removeToken(memberDetails.getId(), deviceId);

return ResponseEntity.ok()
.body(SuccessResponse.ok());
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.potatocake.everymoment.dto.request;

import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;

@Getter
public class CommentRequest {

@NotEmpty(message = "๋Œ“๊ธ€ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.")
private String content;

}
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
package com.potatocake.everymoment.dto.request;

import com.potatocake.everymoment.dto.LocationPoint;
import jakarta.validation.constraints.Size;
import java.util.List;
import lombok.Getter;

@Getter
public class DiaryManualCreateRequest {

private List<CategoryRequest> categories;

private LocationPoint locationPoint;

@Size(max = 50, message = "์žฅ์†Œ๋ช…์€ 50์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
private String locationName;

@Size(max = 200, message = "์ฃผ์†Œ๋Š” 200์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
private String address;

private boolean isBookmark;
private boolean isPublic;

@Size(max = 10, message = "์ด๋ชจ์ง€๋Š” 10์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
private String emoji;

@Size(max = 5000, message = "์ผ๊ธฐ ๋‚ด์šฉ์€ 5000์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
private String content;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.potatocake.everymoment.dto.request;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class FcmNotificationRequest {

private String title;
private String body;
private String type;
private Long targetId;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.potatocake.everymoment.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
public class FcmTokenRequest {

@NotBlank(message = "FCM ํ† ํฐ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
private String fcmToken;

@NotBlank(message = "๋””๋ฐ”์ด์Šค ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
private String deviceId;

}
48 changes: 48 additions & 0 deletions src/main/java/com/potatocake/everymoment/entity/DeviceToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.potatocake.everymoment.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DeviceToken extends BaseTimeEntity {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
private Member member;

@Column(nullable = false)
private String fcmToken;

@Column(nullable = false)
@Lob
private String deviceId;

@Builder
public DeviceToken(Member member, String fcmToken, String deviceId) {
this.member = member;
this.fcmToken = fcmToken;
this.deviceId = deviceId;
}

public void updateToken(String fcmToken) {
this.fcmToken = fcmToken;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
public enum ErrorCode {

/* Diary */
DIARY_NOT_PUBLIC("๋น„๊ณต๊ฐœ ์ผ๊ธฐ์ž…๋‹ˆ๋‹ค.", HttpStatus.FORBIDDEN),
DIARY_NOT_FOUND("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ผ๊ธฐ์ž…๋‹ˆ๋‹ค.", NOT_FOUND),

/* Member */
Expand Down Expand Up @@ -60,7 +61,14 @@ public enum ErrorCode {

/* FriendRequestService */
FRIEND_REQUEST_ALREADY_EXISTS("์ด๋ฏธ ์นœ๊ตฌ ์š”์ฒญ์„ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค.", CONFLICT),
FRIEND_REQUEST_NOT_FOUND("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์นœ๊ตฌ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", NOT_FOUND);
FRIEND_REQUEST_NOT_FOUND("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์นœ๊ตฌ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", NOT_FOUND),

/* FCM */
FCM_TOKEN_NOT_FOUND("FCM ํ† ํฐ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND),
FCM_MESSAGE_SEND_FAILED("FCM ๋ฉ”์‹œ์ง€ ์ „์†ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR),

/* Friend */
ALREADY_FRIEND("์ด๋ฏธ ์นœ๊ตฌ ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค.", HttpStatus.CONFLICT);

private final String message;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.potatocake.everymoment.repository;

import com.potatocake.everymoment.entity.DeviceToken;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface DeviceTokenRepository extends JpaRepository<DeviceToken, Long> {

List<DeviceToken> findAllByMemberId(Long memberId);

Optional<DeviceToken> findByMemberIdAndDeviceId(Long memberId, String deviceId);

void deleteByMemberIdAndDeviceId(Long memberId, String deviceId);

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.potatocake.everymoment.service;

import com.potatocake.everymoment.dto.request.CommentRequest;
import com.potatocake.everymoment.dto.request.FcmNotificationRequest;
import com.potatocake.everymoment.dto.response.CommentFriendResponse;
import com.potatocake.everymoment.dto.response.CommentResponse;
import com.potatocake.everymoment.dto.response.CommentsResponse;
Expand Down Expand Up @@ -30,6 +31,7 @@ public class CommentService {
private final CommentRepository commentRepository;
private final DiaryRepository diaryRepository;
private final MemberRepository memberRepository;
private final FcmService fcmService;

// ๋Œ“๊ธ€ ๋ชฉ๋ก ์กฐํšŒ
public CommentsResponse getComments(Long diaryId, int key, int size) {
Expand All @@ -56,13 +58,27 @@ public void createComment(Long memberId, Long diaryId, CommentRequest commentReq
Diary diary = diaryRepository.findById(diaryId)
.orElseThrow(() -> new GlobalException(ErrorCode.DIARY_NOT_FOUND));

if (!diary.isPublic()) {
throw new GlobalException(ErrorCode.DIARY_NOT_PUBLIC);
}

Comment comment = Comment.builder()
.content(commentRequest.getContent())
.member(currentMember)
.diary(diary)
.build();

commentRepository.save(comment);

// ๋‹ค์ด์–ด๋ฆฌ ์ž‘์„ฑ์ž์—๊ฒŒ FCM ์•Œ๋ฆผ ์ „์†ก (์ž์‹ ์˜ ๋Œ“๊ธ€์—๋Š” ์•Œ๋ฆผ ์•ˆ๋ณด๋ƒ„)
if (!diary.getMember().getId().equals(memberId)) {
fcmService.sendNotification(diary.getMember().getId(), FcmNotificationRequest.builder()
.title("์ƒˆ๋กœ์šด ๋Œ“๊ธ€")
.body(currentMember.getNickname() + "๋‹˜์ด ํšŒ์›๋‹˜์˜ ์ผ๊ธฐ์— ๋Œ“๊ธ€์„ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค.")
.type("COMMENT")
.targetId(diary.getId())
.build());
}
}

// ๋Œ“๊ธ€ ์ˆ˜์ •
Expand All @@ -85,15 +101,15 @@ private Comment getExistComment(Long memberId, Long commentId) {
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new GlobalException(ErrorCode.COMMENT_NOT_FOUND));

if(!Objects.equals(currentMember.getId(), comment.getMember().getId())){
if (!Objects.equals(currentMember.getId(), comment.getMember().getId())) {
throw new GlobalException(ErrorCode.COMMENT_NOT_FOUND);
}

return comment;
}

// ์นœ๊ตฌ ํ”„๋กœํ•„ DTO ๋ณ€ํ™˜
private CommentFriendResponse convertToCommentFriendResponseDTO(Member member){
private CommentFriendResponse convertToCommentFriendResponseDTO(Member member) {
return CommentFriendResponse.builder()
.id(member.getId())
.nickname(member.getNickname())
Expand All @@ -102,13 +118,13 @@ private CommentFriendResponse convertToCommentFriendResponseDTO(Member member){
}

// ๋Œ“๊ธ€ DTO ๋ณ€ํ™˜
private CommentResponse convertToCommentResponseDTO(Comment comment){
private CommentResponse convertToCommentResponseDTO(Comment comment) {
return CommentResponse.builder()
.id(comment.getId())
.commentFriendResponse(convertToCommentFriendResponseDTO(comment.getMember()))
.content(comment.getContent())
.createdAt(comment.getCreateAt())
.build();
}
}

}
Loading

0 comments on commit 939d685

Please sign in to comment.