Skip to content

Commit

Permalink
feat: FCM을 통해 푸쉬 알림 구현 (#605)
Browse files Browse the repository at this point in the history
* feat: Member fcmToken 필드 추가 및 로그인 필드 추가

* feat: 공모 참여 시 총대에게 알림 전송

* feat: 공모 참여 취소 시 총대에게 알림 전송

* feat: 댓글방 상태 변경 시 참여자에게 알림 전송

* feat: 댓글 작성 시 작성자 제외 참여자에게 알림 전송

* refactor: FcmMessageManager 메시지 생성 로직 추출

* chore: FCM key 관리

* style: .gitignore EOF

* chore: FCM key path 관리

* chore: dev CI/CD script 수정

* chore: dev CI/CD script 트리거 수정

* chore: dev CI/CD script 트리거 수정

* chore: dev CI/CD script 수정

* chore: fcm key 빈 파일 생성

* chore: properties 파일 수정

* chore: yml 파일 수정

* chore: dev CI/CD 파일 수정

* chore: test fcm key 파일 추가

* chore: dev CI/CD 스크립트 수정

* chore: dev CI/CD 스크립트 수정

* chore: dev CI/CD 스크립트 수정

* chore: dev CI/CD 스크립트 수정

* chore: dev CI/CD 스크립트 수정

* chore: dev CI/CD 스크립트 수정

* chore: dev CI/CD 스크립트 수정

* chore: dev CI/CD 스크립트 수정

* feat: notification 방식에서 data 방식으로 변경

* feat: 전달 데이터에 offering_id 추가

* chore: 파일 읽는 방식 변경

* fix: 토픽 이름 변경

* refactor: QA 위한 로그

* refactor: 로그인 시 fcmToken 비교 후 다를 경우 갱신

* refactor: 거래 상태 알림 문구 변경

* feat: 안드로이드 리다이렉트를 위한 필드 추가

* refactor: 필드명 변경 및 패키지 정리

* refactor: FcmTopic 구현

* refactor: 토큰 갱신 여부 로깅

* feat: 공모 작성 시 본인 제외 broadcasting

* refactor: 공모 작성 시 본인 제외 broadcasting 주제 구독 방식

* refactor: FcmCondition, FcmTopic 도메인 추출

* refactor: notification 상수 정리

* refactor: 로그인 요청시 fcmToken 필드 비어있는지 검증

* refactor: offering_member 토픽 이름 변경

* feat: 유효하지 않은 토큰을 가진 사용자에 대한 예외 처리

* refactor: 개발 환경과 로컬 환경 분리

* chore: CI/CD 스크립트 정리

* refactor: notificationService에서 repository 의존성 제거

* refactor: MessageManager 계층 도메인에서 서비스로 이동

* chore: dev CI/CD 스크립트 트리거 수정

* fix: 오래된 토큰을 가진 사용자에 대한 알림 전송 무시

* chore: dev CI/CD 트리거 변경

* refactor: yml 중복 필드 제거

* refactor: 불필요한 어노테이션 제거

* refactor: 민감 정보 로깅 제거

* refactor: fcmToken null 처리

* refactor: 방상태 변경 시 토픽 변경

* refactor: FcmMessageCreator 빈 등록

* refactor: FcmNotificationService 코드 순서 변경

* refactor: NotificationService 반환값 변경

* test: 리팩터링 변경 사항 반영

* chore: dev CI/CD 복구
  • Loading branch information
helenason authored Oct 23, 2024
1 parent 017e1e4 commit 491212d
Show file tree
Hide file tree
Showing 36 changed files with 515 additions and 330 deletions.
16 changes: 10 additions & 6 deletions .github/workflows/backend-dev-ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ on:
- "backend/**"
- ".github/workflows/backend-dev-ci-cd.yml"
- "Dockerfile"
# pull_request:
# branches: [ "develop" ]
# paths:
# - "backend/**"
# - ".github/workflows/backend-dev-ci-cd.yml"
# - "Dockerfile"
# pull_request:
# branches: [ "develop" ]
# paths:
# - "backend/**"
# - ".github/workflows/backend-dev-ci-cd.yml"
# - "Dockerfile"

jobs:

Expand Down Expand Up @@ -44,6 +44,10 @@ jobs:
- name: Set Application yml for dev
run: |
echo "${{ secrets.APPLICATION_PROPERTIES_DEV }}" > src/main/resources/application.properties
mkdir -p src/main/resources/fcm
echo '${{ secrets.FCM_SECRET_KEY }}' > src/main/resources/fcm/chongdaemarket-fcm-key.json
mkdir -p src/test/resources/fcm
echo '${{ secrets.FCM_SECRET_KEY }}' > src/test/resources/fcm/chongdaemarket-fcm-key.json
working-directory: ./backend

- name: Build with Gradle Wrapper
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/backend-prod-ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ jobs:
- name: Set Application yml for prod
run: |
echo "${{ secrets.APPLICATION_PROPERTIES_PROD }}" > src/main/resources/application.properties
mkdir -p src/main/resources/fcm
echo '${{ secrets.FCM_SECRET_KEY }}' > src/main/resources/fcm/chongdaemarket-fcm-key.json
mkdir -p src/test/resources/fcm
echo '${{ secrets.FCM_SECRET_KEY }}' > src/test/resources/fcm/chongdaemarket-fcm-key.json
working-directory: ./backend

- name: Build with Gradle Wrapper
Expand Down
2 changes: 1 addition & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ openapi3.yaml
.idea

### FCM ###
/src/main/resources/fcm
/src/main/resources/fcm
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -28,7 +27,7 @@ public class AuthController {
@LoggingMasked
@PostMapping("/auth/login/kakao")
public ResponseEntity<LoginResponse> kakaoLogin(
@RequestBody @Valid KakaoLoginRequest request, HttpServletResponse servletResponse) {
@RequestBody KakaoLoginRequest request, HttpServletResponse servletResponse) {
AuthInfoDto authInfo = authService.kakaoLogin(request);
addTokenToHttpServletResponse(authInfo.authToken(), servletResponse);
LoginResponse response = new LoginResponse(authInfo.authMember());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,67 @@
import com.zzang.chongdae.member.repository.MemberRepository;
import com.zzang.chongdae.member.repository.entity.MemberEntity;
import com.zzang.chongdae.member.service.NicknameGenerator;
import com.zzang.chongdae.notification.service.FcmNotificationService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@RequiredArgsConstructor
@Service
public class AuthService {

private final FcmNotificationService notificationService;
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final NicknameGenerator nickNameGenerator;
private final AuthClient authClient;

@WriterDatabase
@Transactional
public AuthInfoDto kakaoLogin(KakaoLoginRequest request) {
String loginId = authClient.getKakaoUserInfo(request.accessToken());
AuthProvider provider = AuthProvider.KAKAO;
MemberEntity member = memberRepository.findByLoginId(loginId)
.orElseGet(() -> signup(provider, loginId, request.fcmToken()));
return login(member);
return login(member, request.fcmToken());
}

private MemberEntity signup(AuthProvider provider, String loginId, String fcmToken) {
String password = passwordEncoder.encode(UUID.randomUUID().toString());
MemberEntity member = new MemberEntity(nickNameGenerator.generate(), provider, loginId, password, fcmToken);
MemberEntity member = createMember(provider, loginId, fcmToken, password);
return memberRepository.save(member);
}

private AuthInfoDto login(MemberEntity member) {
private MemberEntity createMember(AuthProvider provider, String loginId, String fcmToken, String password) {
if (fcmToken == null) {
return new MemberEntity(nickNameGenerator.generate(), provider, loginId, password);
}
return new MemberEntity(nickNameGenerator.generate(), provider, loginId, password, fcmToken);
}

private AuthInfoDto login(MemberEntity member, String fcmToken) {
AuthMemberDto authMember = new AuthMemberDto(member);
AuthTokenDto authToken = jwtTokenProvider.createAuthToken(member.getId().toString());
checkFcmToken(member, fcmToken);
notificationService.login(member);
return new AuthInfoDto(authMember, authToken);
}

private void checkFcmToken(MemberEntity member, String fcmToken) {
if (fcmToken == null) {
member.updateFcmTokenDefault();
return;
}
if (!memberRepository.existsByIdAndFcmToken(member.getId(), fcmToken)) {
log.info("토큰 갱신 사용자 id: {}", member.getId());
member.updateFcmToken(fcmToken);
}
}

public AuthTokenDto refresh(String refreshToken) {
Long memberId = jwtTokenProvider.getMemberIdByRefreshToken(refreshToken);
return jwtTokenProvider.createAuthToken(memberId.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package com.zzang.chongdae.auth.service.dto;

import javax.annotation.Nullable;

public record KakaoLoginRequest(String accessToken,

@Nullable
String fcmToken) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public Long saveComment(CommentSaveRequest request, MemberEntity member) {
CommentEntity comment = new CommentEntity(member, offering, request.content());
CommentEntity savedComment = commentRepository.save(comment);

List<OfferingMemberEntity> members = offeringMemberRepository.findAllByOffering(offering);
notificationService.saveComment(savedComment, members);
List<OfferingMemberEntity> offeringMembers = offeringMemberRepository.findAllByOffering(offering);
notificationService.saveComment(savedComment, offeringMembers);
return savedComment.getId();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
boolean existsByNickname(String nickname);

Optional<MemberEntity> findByLoginId(String loginId);

boolean existsByIdAndFcmToken(Long id, String fcmToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
Expand All @@ -25,6 +26,8 @@
@Entity
public class MemberEntity extends BaseTimeEntity {

private static final String DEFAULT_FCM_TOKEN = "invalid";

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -51,7 +54,19 @@ public MemberEntity(String nickname, AuthProvider provider, String loginId, Stri
this(null, nickname, provider, loginId, password, fcmToken);
}

public MemberEntity(String nickname, AuthProvider provider, String loginId, String password) {
this(null, nickname, provider, loginId, password, DEFAULT_FCM_TOKEN + UUID.randomUUID());
}

public boolean isSame(MemberEntity other) {
return this.equals(other);
}

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

public void updateFcmTokenDefault() {
this.fcmToken = DEFAULT_FCM_TOKEN + UUID.randomUUID();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.zzang.chongdae.global.exception.MarketException;
import com.zzang.chongdae.notification.exception.NotificationErrorCode;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.io.InputStream;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -29,23 +24,16 @@ public void initialize() {
return;
}
try {
URL url = this.getClass().getResource(secretKeyPath);
if (url == null) {
throw new MarketException(NotificationErrorCode.CANNOT_FIND_URL);
}
File secretKeyFile = new File(url.toURI());
FileInputStream secretKey = new FileInputStream(secretKeyFile);
InputStream secretKey = this.getClass().getResourceAsStream(secretKeyPath);
FirebaseApp.initializeApp(fcmOptions(secretKey));
log.info("성공적으로 FCM 앱을 초기화하였습니다.");
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (URISyntaxException e) { // todo
throw new RuntimeException(e);
}
}

private FirebaseOptions fcmOptions(FileInputStream secretKey) throws IOException {
private FirebaseOptions fcmOptions(InputStream secretKey) throws IOException {
return FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(secretKey))
.build();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package com.zzang.chongdae.notification.domain;

import com.zzang.chongdae.offering.repository.entity.OfferingEntity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class FcmCondition {

private static final String CONDITION_FORMAT_TRUE_AND_FALSE = "'%s' in topics && !('%s' in topics)";

private final String value;

public static FcmCondition offeringCondition(OfferingEntity offering) {
FcmTopic memberTopic = FcmTopic.memberTopic();
FcmTopic proposerTopic = FcmTopic.proposerTopic(offering);
String value = CONDITION_FORMAT_TRUE_AND_FALSE.formatted(memberTopic.getValue(), proposerTopic.getValue());
return new FcmCondition(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.zzang.chongdae.notification.domain;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FcmData {

private final Map<String, String> data = new HashMap<>();

public void addData(String key, Object value) {
data.put(key, value.toString());
}

public Map<String, String> getData() {
data.forEach(this::logWithoutBody);
return Collections.unmodifiableMap(data);
}

private void logWithoutBody(String key, String value) {
if (!key.equals("body")) {
log.info("{} : {}", key, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.zzang.chongdae.notification.domain;

import com.zzang.chongdae.member.repository.entity.MemberEntity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class FcmToken {

private final String value;

public FcmToken(MemberEntity member) {
this.value = member.getFcmToken();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private FcmTokens(List<FcmToken> tokens) {

public static FcmTokens from(List<MemberEntity> members) {
List<FcmToken> tokens = members.stream()
.map(member -> new FcmToken(member.getFcmToken()))
.map(FcmToken::new)
.toList();
return new FcmTokens(tokens);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.zzang.chongdae.notification.domain;

import com.zzang.chongdae.offering.repository.entity.OfferingEntity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class FcmTopic {

private static final String TOPIC_FORMAT_MEMBER = "member";
private static final String TOPIC_FORMAT_OFFERING_PROPOSER = "proposer_of_offering_%d";
private static final String TOPIC_FORMAT_OFFERING_PARTICIPANT = "participant_of_offering_%d";

private final String value;

public static FcmTopic proposerTopic(OfferingEntity offering) {
String value = TOPIC_FORMAT_OFFERING_PROPOSER.formatted(offering.getId());
return new FcmTopic(value);
}

public static FcmTopic participantTopic(OfferingEntity offering) {
String value = TOPIC_FORMAT_OFFERING_PARTICIPANT.formatted(offering.getId());
return new FcmTopic(value);
}

public static FcmTopic memberTopic() {
return new FcmTopic(TOPIC_FORMAT_MEMBER);
}
}
Loading

0 comments on commit 491212d

Please sign in to comment.