diff --git a/.gitignore b/.gitignore index 6c619b0..d880608 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,8 @@ application.yml ### yml file ### src/main/**/application-local.yml +### Firebase Admin SDK ### +src/main/resources/firebase + ### data.sql ### -src/main/**/data.sql +src/main/**/data.sql \ No newline at end of file diff --git a/build.gradle b/build.gradle index f9c821c..96237f2 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.google.firebase:firebase-admin:9.2.0' // Google Firebase Admin + implementation 'com.fasterxml.jackson.core:jackson-core:2.16.1' // Jackson Data Bind + implementation 'org.springframework.boot:spring-boot-starter-json' //webPush implementation 'org.springframework.boot:spring-boot-starter-websocket' //webrtc implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/vom/spring/domain/webpush/ApiResponseWrapper.java b/src/main/java/vom/spring/domain/webpush/ApiResponseWrapper.java new file mode 100644 index 0000000..ce768a9 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/ApiResponseWrapper.java @@ -0,0 +1,46 @@ +package vom.spring.domain.webpush; + +import lombok.Getter; + +@Getter +public class ApiResponseWrapper { + private int resultCode; + private String resultMsg; + private T result; + + // Builder 클래스를 사용하여 객체를 더 쉽게 생성할 수 있도록 합니다 + public static Builder builder() { + return new Builder<>(); + } + + public static class Builder { + private int resultCode; + private String resultMsg; + private T result; + + public Builder resultCode(int resultCode) { + this.resultCode = resultCode; + return this; + } + + public Builder resultMsg(String resultMsg) { + this.resultMsg = resultMsg; + return this; + } + + public Builder result(T result) { + this.result = result; + return this; + } + + public ApiResponseWrapper build() { + ApiResponseWrapper response = new ApiResponseWrapper<>(); + response.resultCode = this.resultCode; + response.resultMsg = this.resultMsg; + response.result = this.result; + return response; + } + } + + // Getter 및 Setter 메소드는 필요에 따라 추가합니다. +} diff --git a/src/main/java/vom/spring/domain/webpush/FcmController.java b/src/main/java/vom/spring/domain/webpush/FcmController.java new file mode 100644 index 0000000..14daa5e --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/FcmController.java @@ -0,0 +1,37 @@ +package vom.spring.domain.webpush; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +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 java.io.IOException; + +@Slf4j +@RestController +@RequestMapping("/api/v1/fcm") +public class FcmController { + private final FcmService fcmService; + + public FcmController(FcmService fcmService) { + this.fcmService = fcmService; + } + + @PostMapping("/send") + public ResponseEntity> pushMessage(@RequestBody @Validated FcmSendDto fcmSendDto) throws IOException, IOException { + log.debug("[+] 푸시 메시지를 전송합니다. "); + int result = fcmService.sendMessageTo(fcmSendDto); + + ApiResponseWrapper arw = ApiResponseWrapper + .builder() + .result(result) + .resultCode(SuccessCode.SELECT_SUCCESS.getStatus()) + .resultMsg(SuccessCode.SELECT_SUCCESS.getMessage()) + .build(); + return new ResponseEntity<>(arw, HttpStatus.OK); + } +} diff --git a/src/main/java/vom/spring/domain/webpush/FcmMessageDto.java b/src/main/java/vom/spring/domain/webpush/FcmMessageDto.java new file mode 100644 index 0000000..ba57327 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/FcmMessageDto.java @@ -0,0 +1,29 @@ +package vom.spring.domain.webpush; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class FcmMessageDto { + private boolean validateOnly; + private FcmMessageDto.Message message; + + @Builder + @AllArgsConstructor + @Getter + public static class Message { + private FcmMessageDto.Notification notification; + private String token; + } + + @Builder + @AllArgsConstructor + @Getter + public static class Notification { + private String title; + private String body; + private String image; + } +} diff --git a/src/main/java/vom/spring/domain/webpush/FcmSendDto.java b/src/main/java/vom/spring/domain/webpush/FcmSendDto.java new file mode 100644 index 0000000..3c2fc27 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/FcmSendDto.java @@ -0,0 +1,21 @@ +package vom.spring.domain.webpush; + +import lombok.*; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FcmSendDto { + private String token; + + private String title; + + private String body; + + @Builder(toBuilder = true) + public FcmSendDto(String token, String title, String body) { + this.token = token; + this.title = title; + this.body = body; + } +} diff --git a/src/main/java/vom/spring/domain/webpush/FcmService.java b/src/main/java/vom/spring/domain/webpush/FcmService.java new file mode 100644 index 0000000..78637c4 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/FcmService.java @@ -0,0 +1,12 @@ +package vom.spring.domain.webpush; + +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Service +public class FcmService { + int sendMessageTo(FcmSendDto fcmSendDto) throws IOException { + return 0; + } +} diff --git a/src/main/java/vom/spring/domain/webpush/FcmServiceImpl.java b/src/main/java/vom/spring/domain/webpush/FcmServiceImpl.java new file mode 100644 index 0000000..8dca721 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/FcmServiceImpl.java @@ -0,0 +1,82 @@ +package vom.spring.domain.webpush; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auth.oauth2.GoogleCredentials; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.*; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.List; + +public class FcmServiceImpl extends FcmService { + /** + * 푸시 메시지 처리를 수행하는 비즈니스 로직 + * + * @param fcmSendDto 모바일에서 전달받은 Object + * @return 성공(1), 실패(0) + */ + @Override + public int sendMessageTo(FcmSendDto fcmSendDto) throws IOException { + + String message = makeMessage(fcmSendDto); + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + headers.set("Authorization", "Bearer " + getAccessToken()); + + HttpEntity entity = new HttpEntity<>(message, headers); + + String API_URL = "https://fcm.googleapis.com/v1/projects/vomvom-fd09b/messages:send"; +// String API_URL = "https://fcm.googleapis.com/fcm/send"; + + ResponseEntity response = restTemplate.exchange(API_URL, HttpMethod.POST, entity, String.class); + + return response.getStatusCode() == HttpStatus.OK ? 1 : 0; + } + + /** + * Firebase Admin SDK의 비공개 키를 참조하여 Bearer 토큰을 발급 받습니다. + * + * playground에서 발급받은 토큰 + * + * @return Bearer token + */ + private String getAccessToken() throws IOException { + String firebaseConfigPath = "firebase/vomvom-fd09b-firebase-adminsdk-ghtjs-0070b39a4e.json"; + + GoogleCredentials googleCredentials = GoogleCredentials + .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream()) + .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/firebase.messaging")); + + googleCredentials.refreshIfExpired(); + + System.out.println(googleCredentials.getAccessToken()); + + return googleCredentials.getAccessToken().getTokenValue(); + } + + /** + * FCM 전송 정보를 기반으로 메시지를 구성합니다. (Object -> String) + * + * @param fcmSendDto FcmSendDto + * @return String + */ + private String makeMessage(FcmSendDto fcmSendDto) throws JsonProcessingException { + + ObjectMapper om = new ObjectMapper(); + FcmMessageDto fcmMessageDto = FcmMessageDto.builder() + .message(FcmMessageDto.Message.builder() + .token(fcmSendDto.getToken()) + .notification(FcmMessageDto.Notification.builder() + .title(fcmSendDto.getTitle()) + .body(fcmSendDto.getBody()) + .image(null) + .build() + ).build()).validateOnly(false).build(); + + return om.writeValueAsString(fcmMessageDto); + } +} diff --git a/src/main/java/vom/spring/domain/webpush/SuccessCode.java b/src/main/java/vom/spring/domain/webpush/SuccessCode.java new file mode 100644 index 0000000..60dbdd1 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/SuccessCode.java @@ -0,0 +1,23 @@ +package vom.spring.domain.webpush; + +public enum SuccessCode { + SELECT_SUCCESS(200, "데이터 조회 성공"), + UPDATE_SUCCESS(200, "데이터 업데이트 성공"), + CREATE_SUCCESS(201, "데이터 생성 성공"); + + private final int status; + private final String message; + + SuccessCode(int status, String message) { + this.status = status; + this.message = message; + } + + public int getStatus() { + return status; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/vom/spring/domain/webpush/controller/WebpushController.java b/src/main/java/vom/spring/domain/webpush/controller/WebpushController.java new file mode 100644 index 0000000..6960409 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/controller/WebpushController.java @@ -0,0 +1,48 @@ +package vom.spring.domain.webpush.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import vom.spring.domain.webpush.dto.WebpushDto; +import vom.spring.domain.webpush.service.WebpushService; + +import java.util.List; + +@RestController +@Controller +public class WebpushController { + private WebpushService webpushService; + @Autowired + public WebpushController(WebpushService webpushService) { + this.webpushService = webpushService; + } + + @GetMapping(value = "/api/webpush/{member-id}") + public ResponseEntity> getWebpushes( + @PathVariable("member-id") Long memberId + ) { + return new ResponseEntity<>(webpushService.getWebpushes(memberId), HttpStatus.OK); + } + +// @PostMapping("/api/webpush/send") +// public String sendNotification( +// @RequestBody NotificationRequest request) +// { +// webpushService.sendNotification(request.getTargetToken(), request.getTitle(), request.getBody()); +// return "Notification sent successfully!"; +// } +// +// @Getter +// public static class NotificationRequest { +// private String targetToken; +// private String title; +// private String body; +// +// // Getters and Setters +// } + +} diff --git a/src/main/java/vom/spring/domain/webpush/domain/Webpush.java b/src/main/java/vom/spring/domain/webpush/domain/Webpush.java new file mode 100644 index 0000000..617579b --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/domain/Webpush.java @@ -0,0 +1,38 @@ +package vom.spring.domain.webpush.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import vom.spring.domain.member.domain.Member; +import vom.spring.domain.webcam.domain.Status; +import vom.spring.domain.webcam.domain.Webcam; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Webpush { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @ManyToOne + @JoinColumn(referencedColumnName = "id", name = "from_member_id", nullable = false) + private Member fromMember; + + @ManyToOne + @JoinColumn(referencedColumnName = "id", name = "to_member_id", nullable = false) + private Member toMember; + +// @OneToOne +// @JoinColumn(referencedColumnName = "id", name = "webcam_id", nullable = false) +// private Webcam webcam; +} diff --git a/src/main/java/vom/spring/domain/webpush/dto/WebpushDto.java b/src/main/java/vom/spring/domain/webpush/dto/WebpushDto.java new file mode 100644 index 0000000..cf54d66 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/dto/WebpushDto.java @@ -0,0 +1,18 @@ +package vom.spring.domain.webpush.dto; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class WebpushDto { + private Long fromMemberId; + private LocalDateTime createdAt; +// private Long webcamId; + + public WebpushDto(Long fromMemberId, LocalDateTime createdAt) { + this.fromMemberId = fromMemberId; + this.createdAt = createdAt; +// this.webcamId = webcamId; + } +} diff --git a/src/main/java/vom/spring/domain/webpush/repository/WebpushRepository.java b/src/main/java/vom/spring/domain/webpush/repository/WebpushRepository.java new file mode 100644 index 0000000..9e7a849 --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/repository/WebpushRepository.java @@ -0,0 +1,28 @@ +package vom.spring.domain.webpush.repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Repository; +import vom.spring.domain.webpush.domain.Webpush; +import vom.spring.domain.webpush.dto.WebpushDto; + +import java.util.List; + +@Repository +public class WebpushRepository { + @PersistenceContext + private EntityManager em; + + public void save(Webpush webpush) { + em.persist(webpush); + } + + public List findFromMemberIdsByToMemberId(Long toMemberId) { + return em.createQuery( + "select new WebpushDto(w.fromMember.id, w.createdAt) " + + "from Webpush w where w.toMember.id = :toMemberId", + WebpushDto.class) + .setParameter("toMemberId", toMemberId) + .getResultList(); + } +} diff --git a/src/main/java/vom/spring/domain/webpush/service/WebpushService.java b/src/main/java/vom/spring/domain/webpush/service/WebpushService.java new file mode 100644 index 0000000..5bcaf7d --- /dev/null +++ b/src/main/java/vom/spring/domain/webpush/service/WebpushService.java @@ -0,0 +1,68 @@ +package vom.spring.domain.webpush.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vom.spring.domain.member.repository.MemberRepository; +import vom.spring.domain.webpush.dto.WebpushDto; +import vom.spring.domain.webpush.repository.WebpushRepository; + +import java.util.List; + +@Service +public class WebpushService { + + private final WebpushRepository webpushRepository; + private final MemberRepository memberRepository; +// private final WebcamRepository webcamRepository; + + @Autowired + public WebpushService(WebpushRepository webpushRepository, MemberRepository memberRepository) { + this.webpushRepository = webpushRepository; + this.memberRepository = memberRepository; + } + + /** + * 웹푸쉬 조회 + */ + @Transactional + public List getWebpushes(Long member_id) { + return webpushRepository.findFromMemberIdsByToMemberId(member_id); + } + + /** + * FCM + */ +// private static final String FCM_API_URL = "https://fcm.googleapis.com/fcm/send"; +// private static final String SERVER_KEY = ""; // Firebase Console에서 확인한 서버 키 +// +// public void sendNotification(String targetToken, String title, String body) { +// RestTemplate restTemplate = new RestTemplate(); +// +// HttpHeaders headers = new HttpHeaders(); +// headers.set("Authorization", "key=" + SERVER_KEY); +// headers.set("Content-Type", "application/json"); +// +// Map notification = new HashMap<>(); +// notification.put("title", title); +// notification.put("body", body); +// +// Map message = new HashMap<>(); +// message.put("to", targetToken); +// message.put("notification", notification); +// +// HttpEntity> entity = new HttpEntity<>(message, headers); + +// ResponseEntity response = restTemplate.exchange(FCM_API_URL, HttpMethod.POST, entity, String.class); +// System.out.println("FCM Response: " + response.getBody()); + +// try { +// ResponseEntity response = restTemplate.postForEntity(FCM_API_URL, entity, String.class); +// System.out.println("Response: " + response.getBody()); +// } catch (HttpClientErrorException e) { +// System.err.println("Error: " + e.getStatusCode() + " - " + e.getResponseBodyAsString()); +// } +// } + + +}