Skip to content

Commit

Permalink
Merge pull request #119 from KCY-Fit-a-Pet/feat/39
Browse files Browse the repository at this point in the history
๐Ÿšง Push Notification Test API
  • Loading branch information
heejinnn authored Feb 27, 2024
2 parents c999cf9 + 32e088b commit 36dd4cc
Show file tree
Hide file tree
Showing 37 changed files with 1,193 additions and 25 deletions.
5 changes: 4 additions & 1 deletion fitapet-app-external-api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ bin/
.vscode/

### Mac OS ###
.DS_Store
.DS_Store

## TEST API ##
## **/test
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import kr.co.fitapet.api.apis.profile.dto.DeviceTokenReq;
import kr.co.fitapet.api.apis.profile.usecase.MemberAccountUseCase;
import kr.co.fitapet.api.common.response.SuccessResponse;
import kr.co.fitapet.api.common.security.authentication.CustomUserDetails;
Expand Down Expand Up @@ -51,6 +52,14 @@ public ResponseEntity<?> getProfile(@PathVariable("id") Long id) {
return ResponseEntity.ok(SuccessResponse.from(member));
}

@Operation(summary = "๋””๋ฐ”์ด์Šค ํ† ํฐ ๋“ฑ๋ก")
@PostMapping("/{id}/device-token")
@PreAuthorize("isAuthenticated() and #id == principal.userId")
public ResponseEntity<?> postDeviceToken(@PathVariable("id") Long id, @RequestBody @Valid DeviceTokenReq req) {
memberAccountUseCase.registerDeviceToken(id, req);
return ResponseEntity.ok(SuccessResponse.noContent());
}

@Operation(summary = "ํ”„๋กœํ•„ ๊ฒ€์ƒ‰")
@Parameter(name = "search", description = "๊ฒ€์ƒ‰ํ•  ๋‹‰๋„ค์ž„", in = ParameterIn.QUERY, required = true)
@GetMapping("")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.co.fitapet.api.apis.profile.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import kr.co.fitapet.domain.domains.device.domain.DeviceToken;
import kr.co.fitapet.domain.domains.member.domain.Member;

@Schema(description = "๋””๋ฐ”์ด์Šค ํ† ํฐ ๋“ฑ๋ก ์š”์ฒญ")
public record DeviceTokenReq(
@Schema(description = "๋””๋ฐ”์ด์Šค ํ† ํฐ", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
String deviceToken,
@Schema(description = "๋””๋ฐ”์ด์Šค OS", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
String os,
@Schema(description = "๋””๋ฐ”์ด์Šค ๋ชจ๋ธ๋ช…", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
String deviceModel
) {
public DeviceToken toEntity(Member member) {
return DeviceToken.of(deviceToken, os, deviceModel, member);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package kr.co.fitapet.api.apis.profile.usecase;

import kr.co.fitapet.api.apis.auth.mapper.SmsRedisMapper;
import kr.co.fitapet.api.apis.profile.dto.AccountSearchReq;
import kr.co.fitapet.api.apis.profile.dto.DeviceTokenReq;
import kr.co.fitapet.api.apis.profile.dto.ProfilePatchReq;
import kr.co.fitapet.common.annotation.UseCase;
import kr.co.fitapet.common.execption.BaseErrorCode;
import kr.co.fitapet.common.execption.GlobalErrorException;
import kr.co.fitapet.domain.common.redis.sms.type.SmsPrefix;
import kr.co.fitapet.domain.domains.device.domain.DeviceToken;
import kr.co.fitapet.domain.domains.device.service.DeviceTokenSaveService;
import kr.co.fitapet.domain.domains.device.service.DeviceTokenSearchService;
import kr.co.fitapet.domain.domains.manager.domain.Manager;
import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService;
import kr.co.fitapet.domain.domains.member.domain.Member;
import kr.co.fitapet.domain.domains.member.domain.MemberNickname;
import kr.co.fitapet.domain.domains.member.dto.AccountProfileRes;
import kr.co.fitapet.api.apis.profile.dto.AccountSearchReq;
import kr.co.fitapet.api.apis.profile.dto.ProfilePatchReq;
import kr.co.fitapet.domain.domains.member.dto.MemberInfo;
import kr.co.fitapet.domain.domains.member.dto.UidRes;
import kr.co.fitapet.domain.domains.member.exception.AccountErrorCode;
Expand Down Expand Up @@ -41,9 +45,11 @@
@Slf4j
public class MemberAccountUseCase {
private final MemberSearchService memberSearchService;

private final ManagerSearchService managerSearchService;

private final DeviceTokenSearchService deviceTokenSearchService;
private final DeviceTokenSaveService deviceTokenSaveService;

private final ScheduleSearchService scheduleSearchService;
private final MemoSearchService memoSearchService;

Expand All @@ -57,6 +63,17 @@ public AccountProfileRes getProfile(Long userId) {
return AccountProfileRes.from(member);
}

@Transactional
public void registerDeviceToken(Long memberId, DeviceTokenReq req) {
Member member = memberSearchService.findById(memberId);

if (!deviceTokenSearchService.isExistByMemberIdAndDeviceToken(memberId, req.deviceToken())) {
log.info("๋””๋ฐ”์ด์Šค ํ† ํฐ ๋“ฑ๋ก: {}", req);
DeviceToken deviceToken = req.toEntity(member);
deviceTokenSaveService.save(deviceToken);
}
}

@Transactional(readOnly = true)
public MemberInfo searchProfile(Long requesterId, String search) {
return memberSearchService.findMemberInfo(requesterId, search);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.co.fitapet.api.apis.test;

import io.swagger.v3.oas.annotations.tags.Tag;
import kr.co.fitapet.api.common.security.authentication.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "ํ‘ธ์‹œ ํ…Œ์ŠคํŠธ API", description = "push notification์„ ์œ„ํ•œ ๊ฐœ๋ฐœ์ž์šฉ ์ž„์‹œ API ์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ํ›„ ์‚ญ์ œ ์˜ˆ์ •")
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/api/v2/test/members")
public class NotificationTestController {
private final NotificationTestService notificationTestService;

@RequestMapping("/push")
@PreAuthorize("isAuthenticated()")
public void push(@AuthenticationPrincipal CustomUserDetails user) {
notificationTestService.push(user.getUserId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package kr.co.fitapet.api.apis.test;

import kr.co.fitapet.domain.domains.device.domain.DeviceToken;
import kr.co.fitapet.domain.domains.device.service.DeviceTokenSearchService;
import kr.co.fitapet.infra.client.fcm.NotificationDataKey;
import kr.co.fitapet.infra.client.fcm.NotificationService;
import kr.co.fitapet.infra.client.fcm.request.NotificationRequest;
import kr.co.fitapet.infra.client.fcm.request.NotificationSingleRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class NotificationTestService {
private final DeviceTokenSearchService deviceTokenSearchService;
private final NotificationService notificationService;

public void push(Long userId) {
log.info("push to {}", userId);

List<DeviceToken> deviceTokens = deviceTokenSearchService.findDeviceTokensByMemberId(userId);
log.info("deviceTokens: {}", deviceTokens);

log.info("send push message");
deviceTokens.forEach(deviceToken -> {
NotificationSingleRequest request = NotificationSingleRequest.builder()
.token(deviceToken.getDeviceToken())
.title("ํ‘ธ์‹œ ํ…Œ์ŠคํŠธ")
.content("ํ‘ธ์‹œ ํ…Œ์ŠคํŠธ ๋ฉ”์‹œ์ง€")
.build();

notificationService.sendMessage(request);
});

log.info("send push message with image");
deviceTokens.forEach(deviceToken -> {
NotificationSingleRequest request = NotificationSingleRequest.builder()
.title("์ด๋ฏธ์ง€ ํ…Œ์ŠคํŠธ")
.content("ํ‘ธ์‹œ ์ด๋ฏธ์ง€ ํ…Œ์ŠคํŠธ ๋ฉ”์‹œ์ง€")
.imageUrl("https://pkcy.kr.object.ncloudstorage.com/profile/0bb25dde9020.jpeg")
.build();

notificationService.sendMessage(request);
});

log.info("send push message with data");
deviceTokens.forEach(deviceToken -> {
NotificationSingleRequest request = NotificationSingleRequest.builder()
.title("๋ฐ์ดํ„ฐ ํ…Œ์ŠคํŠธ")
.content("ํ‘ธ์‹œ ๋ฐ์ดํ„ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์‹œ์ง€")
.data(
Map.of(
NotificationDataKey.NOTICE_TYPE.getField(), "TEST_TYPE",
NotificationDataKey.TO_ID.getField(), userId.toString()
)
)
.build();

notificationService.sendMessage(request);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.co.fitapet.api.common.event.notification;

import kr.co.fitapet.domain.domains.notification.domain.Notification;
import kr.co.fitapet.domain.domains.notification.type.NotificationType;
import lombok.Builder;

@Builder
public record AnnouncementEvent(
String title,
String content,
String imageUrl
) {
public Notification toEntity(Long toId) {
return Notification.builder()
.title(title)
.content(content)
.imageUrl(imageUrl)
.ctype(NotificationType.ANNOUNCEMENT)
.toId(toId)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package kr.co.fitapet.api.common.event.notification;

import kr.co.fitapet.domain.domains.notification.domain.Notification;
import kr.co.fitapet.domain.domains.notification.type.NotificationType;
import kr.co.fitapet.infra.client.fcm.NotificationDataKey;
import lombok.Builder;

import java.util.Map;
import java.util.stream.Collectors;

/**
* FCM Push Notification Event๋ฅผ ์œ„ํ•œ Data Type Class
*
* @param memberId Long : ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ฐ›์„ ํšŒ์› ID
* @param noticeType {@link NoticeType} : push ์•Œ๋ฆผ์„ ์œ„ํ•œ ์•Œ๋ฆผ ํƒ€์ž…
* @param notificationType {@link NotificationType} : DB์— ์ €์žฅ๋  ์•Œ๋ฆผ ํƒ€์ž…
* @param title String : ํ‘ธ์‹œ ์•Œ๋ฆผ ์ œ๋ชฉ
* @param content String : ํ‘ธ์‹œ ์•Œ๋ฆผ ๋‚ด์šฉ. {@link NoticeType}์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์ƒ์„ฑ
* @param imageUrl String : ํ‘ธ์‹œ ์•Œ๋ฆผ ์ด๋ฏธ์ง€ URL (optional)
* @param data Map<{@link NotificationDataKey}, String> : ํ‘ธ์‹œ ์•Œ๋ฆผ์— ํฌํ•จ๋  ๋ฐ์ดํ„ฐ
*/
@Builder
public record NoticeEvent(
Long memberId,
NoticeType noticeType,
NotificationType notificationType,
String title,
String content,
String imageUrl,
Map<NotificationDataKey, String> data
) {
public Notification toEntity() {
Map<String, Long> idFields = getIdFields();

return Notification.builder()
.title(title)
.content(getFormattedContent())
.imageUrl(imageUrl)
.ctype(notificationType)
.fromId(idFields.getOrDefault(NotificationDataKey.FROM_ID.getField(), null))
.toId(idFields.getOrDefault(NotificationDataKey.TO_ID.getField(), null))
.domainId(idFields.getOrDefault(NotificationDataKey.DOMAIN_ID.getField(), null))
.subjectId(idFields.getOrDefault(NotificationDataKey.SUBJECT_ID.getField(), null))
.build();
}

/**
* data์˜ ์ •๋ณด๋ฅผ {@link NoticeType}์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋œ content๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
* @return String : ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋œ content
*/
public String getFormattedContent() {
return noticeType.createFormattedContent(getNameFields());
}

private Map<String, Long> getIdFields() {
return data.entrySet().stream()
.filter(e -> e.getKey().isIdField())
.collect(
Collectors.toMap(
e -> e.getKey().getField(),
e -> Long.parseLong(e.getValue())
)
);
}

private Map<String, String> getNameFields() {
return data.entrySet().stream()
.filter(e -> e.getKey().isNameField())
.collect(
Collectors.toMap(
e -> e.getKey().getField(),
Map.Entry::getValue
)
);
}
}
Loading

0 comments on commit 36dd4cc

Please sign in to comment.