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
5 changes: 5 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew\

- name: Setup Firebase service key
run: |
mkdir -p src/main/resources/firebase
echo ${{ secrets.FIREBASE_SERVICE_KEY_BASE64_ENCODE }} | base64 -d > src/main/resources/firebase/petlog-firebase-key.json

- name: Build with gradle
run: ./gradlew bootJar -Pspring.profiles.active=dev --info

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ out/

### docker data ###
/docker/data/mysql/petlog_local

# firebase
src/main/resources/firebase
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ dependencies {
//AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// Firebase
implementation 'com.google.firebase:firebase-admin:9.4.3'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/petlog/PetlogApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class PetlogApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.petlog.common.config;
package com.petlog.auth.config;

import com.petlog.common.config.jwt.TokenProvider;
import com.petlog.common.config.jwt.filter.TokenAuthenticationFilter;
import com.petlog.auth.jwt.TokenProvider;
import com.petlog.auth.jwt.filter.TokenAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.petlog.common.config.jwt;
package com.petlog.auth.jwt;

import lombok.Getter;
import lombok.Setter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.petlog.common.config.jwt;
package com.petlog.auth.jwt;

import com.petlog.member.entity.Member;
import io.jsonwebtoken.Claims;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.petlog.common.config.jwt;
package com.petlog.auth.jwt;

public enum TokenType {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.petlog.common.config.jwt.filter;
package com.petlog.auth.jwt.filter;

import com.petlog.common.config.jwt.TokenProvider;
import com.petlog.auth.jwt.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.petlog.auth.resolver;

import com.petlog.common.config.jwt.TokenProvider;
import com.petlog.auth.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/petlog/auth/service/TokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.petlog.auth.entity.RefreshToken;
import com.petlog.auth.repository.RefreshTokenRepository;
import com.petlog.common.config.jwt.TokenProvider;
import com.petlog.auth.jwt.TokenProvider;
import com.petlog.member.entity.Member;
import com.petlog.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/petlog/docs/NotificationControllerDocs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.petlog.docs;

import com.petlog.auth.resolver.Authenticated;
import com.petlog.common.response.ApiResponse;
import com.petlog.notification.controller.dto.request.DeleteNotificationTokenRequestDto;
import com.petlog.notification.controller.dto.request.SaveNotificationTokenRequestDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "푸시 알림 API")
public interface NotificationControllerDocs {

@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "푸시 알림 토큰 저장에 성공하였습니다.")
@Operation(summary = "푸시 알림 토큰 저장 API")
ResponseEntity<ApiResponse<Void>> saveNotificationToken(@Authenticated final Long memberId, @RequestBody final SaveNotificationTokenRequestDto request);

@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "푸시 알림 토큰 삭제에 성공하였습니다.")
@Operation(summary = "푸시 알림 토큰 삭제 API")
ResponseEntity<ApiResponse<Void>> deleteNotificationToken(@Authenticated final Long memberId, @RequestBody final DeleteNotificationTokenRequestDto request);
}
40 changes: 40 additions & 0 deletions src/main/java/com/petlog/notification/config/FirebaseConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.petlog.notification.config;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;

@Slf4j
@Configuration
public class FirebaseConfig {

@Value("${firebase.petlog-firebase-key.path}")
private String SERVICE_ACCOUNT_PATH;

@PostConstruct
public void init() throws IOException {
try {
final InputStream serviceAccount = new ClassPathResource(SERVICE_ACCOUNT_PATH).getInputStream();
final FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();

if (FirebaseApp.getApps().isEmpty()) {
FirebaseApp.initializeApp(options);
}

log.info("FirebaseApp 초기화 성공 - {}", SERVICE_ACCOUNT_PATH);
} catch (final Exception e) {
log.error("FirebaseApp 초기화 실패", e);
throw e;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.petlog.notification.controller;

import com.petlog.auth.resolver.Authenticated;
import com.petlog.common.response.ApiResponse;
import com.petlog.docs.NotificationControllerDocs;
import com.petlog.notification.controller.dto.request.DeleteNotificationTokenRequestDto;
import com.petlog.notification.controller.dto.request.SaveNotificationTokenRequestDto;
import com.petlog.notification.service.NotificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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;

import static com.petlog.notification.controller.NotificationSuccessCode.DELETE_NOTIFICATION_TOKEN;
import static com.petlog.notification.controller.NotificationSuccessCode.SAVE_NOTIFICATION_TOKEN;

@RequiredArgsConstructor
@RequestMapping("api/notification")
@RestController
public class NotificationController implements NotificationControllerDocs {

private final NotificationService notificationService;

@PostMapping("/token")
public ResponseEntity<ApiResponse<Void>> saveNotificationToken(
@Authenticated final Long memberId,
@RequestBody final SaveNotificationTokenRequestDto request
) {
notificationService.saveNotificationToken(memberId, request.token());

return ResponseEntity.ok(
ApiResponse.success(SAVE_NOTIFICATION_TOKEN)
);
}

@DeleteMapping("/token")
public ResponseEntity<ApiResponse<Void>> deleteNotificationToken(
@Authenticated final Long memberId,
@RequestBody final DeleteNotificationTokenRequestDto request
) {
notificationService.deleteNotificationToken(memberId, request.token());

return ResponseEntity.ok(
ApiResponse.success(DELETE_NOTIFICATION_TOKEN)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.petlog.notification.controller;

import com.petlog.common.response.SuccessCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum NotificationSuccessCode implements SuccessCode {

SAVE_NOTIFICATION_TOKEN(HttpStatus.CREATED.value(), "푸시 알림 토큰 저장에 성공하였습니다."),
DELETE_NOTIFICATION_TOKEN(HttpStatus.OK.value(), "푸시 알림 토큰 삭제에 성공하였습니다."),
;

private final int value;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.petlog.notification.controller.dto.request;

public record DeleteNotificationTokenRequestDto(

String token

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.petlog.notification.controller.dto.request;

public record SaveNotificationTokenRequestDto(

String token

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.petlog.notification.entity;

import com.petlog.common.entity.BaseEntity;
import com.petlog.member.entity.Member;
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.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "notification_token")
@Entity
public class NotificationToken extends BaseEntity {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false, updatable = false)
private Member member;

@Column(name = "value", length = 500, nullable = false, unique = true)
private String value;

public NotificationToken(final Member member, final String value) {
this.member = member;
this.value = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.petlog.notification.repository;

import com.petlog.member.entity.Member;
import com.petlog.notification.entity.NotificationToken;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface NotificationTokenRepository extends JpaRepository<NotificationToken, Long> {

List<NotificationToken> findAllByMember_Id(final Long memberId);

void deleteAllByValue(final String value);

boolean existsByValue(final String token);

Optional<NotificationToken> findByMemberAndValue(final Member member, final String value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.petlog.notification.sender;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.petlog.notification.sender.dto.FcmNotificationRequestDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class FcmNotificationSender {

public void send(final FcmNotificationRequestDto request) {
try {
final String response = FirebaseMessaging.getInstance().send(request.convertFcmMessage());
log.info("푸시 알림 전송 완료 - {}", response);
}
catch (final FirebaseMessagingException e) {
log.error("푸시 알림 전송에 실패하였습니다.", e);
handleFcmException(e.getMessage());
}
}

private void handleFcmException(final String errorResponse) {
if (checkInvalidFcmTokenResponse(errorResponse)) {
throw new IllegalArgumentException("유효하지 않은 푸시 알림 토큰입니다.");
}
}

private boolean checkInvalidFcmTokenResponse(final String errorResponse) {
return errorResponse.contains("The registration token is not a valid FCM registration token");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.petlog.notification.sender.dto;

import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;

public record FcmNotificationRequestDto(

String fcmToken,
String title,
String body,
String type

) {

public Message convertFcmMessage() {
return Message.builder()
.setNotification(createNotification())
.setToken(fcmToken)
.setApnsConfig(createApnsConfig())
.setAndroidConfig(createAndroidConfig())
.putData("type", type)
.build();
}

private ApnsConfig createApnsConfig() {
return ApnsConfig.builder()
.setAps(
Aps.builder()
.setContentAvailable(true)
.build())
.build();
}

private static AndroidConfig createAndroidConfig() {
return AndroidConfig.builder()
.setPriority(AndroidConfig.Priority.HIGH)
.build();
}

private Notification createNotification() {
return Notification.builder()
.setTitle(title)
.setBody(body)
.build();
}
}
Loading
Loading