diff --git a/Api/build.gradle b/Api/build.gradle index 6e6f90cc..821fa386 100644 --- a/Api/build.gradle +++ b/Api/build.gradle @@ -12,10 +12,18 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springdoc:springdoc-openapi-ui:1.6.12' + implementation ('org.springdoc:springdoc-openapi-ui:1.6.12'){ + dependencies { + implementation('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4') + implementation('com.fasterxml.jackson.core:jackson-core:2.13.4') + implementation('com.fasterxml.jackson.core:jackson-databind:2.13.4') + implementation('com.fasterxml.jackson.core:jackson-annotations:2.13.4') + } + } // implementation 'io.springfox:springfox-boot-starter:3.0.0' implementation 'org.springframework.boot:spring-boot-starter-security' // implementation 'com.slack.api:slack-api-client:1.27.2' + implementation 'io.github.jav:expo-server-sdk:1.1.0' implementation project(':Domain') implementation project(':Core') implementation project(':Infrastructure') diff --git a/Api/src/main/java/tify/server/api/alarm/controller/AlarmHistoryController.java b/Api/src/main/java/tify/server/api/alarm/controller/AlarmHistoryController.java new file mode 100644 index 00000000..bae2acff --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/controller/AlarmHistoryController.java @@ -0,0 +1,31 @@ +package tify.server.api.alarm.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tify.server.api.alarm.model.condition.AlarmCondition; +import tify.server.api.alarm.model.vo.AlarmHistoryVo; +import tify.server.api.alarm.service.RetrieveAlarmHistoryUseCase; + +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "alarm") +@SecurityRequirement(name = "access-token") +@Tag(name = "9. 알림") +public class AlarmHistoryController { + + private final RetrieveAlarmHistoryUseCase retrieveAlarmHistoryUseCase; + + @Operation(summary = "푸시 알림을 조회합니다. (유저, 읽음상태, 제목 필터)") + @GetMapping + public List getAlarmsByUser(@ParameterObject AlarmCondition condition) { + return retrieveAlarmHistoryUseCase.execute(condition); + } +} diff --git a/Api/src/main/java/tify/server/api/alarm/model/condition/AlarmCondition.java b/Api/src/main/java/tify/server/api/alarm/model/condition/AlarmCondition.java new file mode 100644 index 00000000..5c3f6c48 --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/model/condition/AlarmCondition.java @@ -0,0 +1,21 @@ +package tify.server.api.alarm.model.condition; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import tify.server.core.consts.Status; + +@Getter +@Builder +public class AlarmCondition { + + @Schema(description = "필터로 쓰일 유저의 pk값입니다.", example = "1") + private final Long userId; + + @Schema(description = "필터로 쓰일 알림의 조회여부입니다.", implementation = Status.class) + private final Status status; + + @Schema(description = "필터로 쓰일 알림의 제목입니다.", example = "답변 가능한 취향 질문이 남아있어요 \uD83D\uDC40") + private final String title; +} diff --git a/Api/src/main/java/tify/server/api/alarm/model/dto/AnswerKnockEventDto.java b/Api/src/main/java/tify/server/api/alarm/model/dto/AnswerKnockEventDto.java new file mode 100644 index 00000000..2f7720a2 --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/model/dto/AnswerKnockEventDto.java @@ -0,0 +1,22 @@ +package tify.server.api.alarm.model.dto; + + +import lombok.Builder; +import lombok.Getter; +import tify.server.domain.domains.question.domain.Knock; + +@Getter +@Builder +public class AnswerKnockEventDto { + + private final Long fromUserId; + + private final Long toUserId; + + public static AnswerKnockEventDto from(Knock knock) { + return AnswerKnockEventDto.builder() + .fromUserId(knock.getUserId()) + .toUserId(knock.getKnockedUserId()) + .build(); + } +} diff --git a/Api/src/main/java/tify/server/api/alarm/model/dto/CreateKnockEventDto.java b/Api/src/main/java/tify/server/api/alarm/model/dto/CreateKnockEventDto.java new file mode 100644 index 00000000..9e1756b6 --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/model/dto/CreateKnockEventDto.java @@ -0,0 +1,25 @@ +package tify.server.api.alarm.model.dto; + + +import lombok.Builder; +import lombok.Getter; +import tify.server.domain.domains.question.domain.Knock; + +@Getter +@Builder +public class CreateKnockEventDto { + + private final Long fromUserId; + + private final Long toUserId; + + private final Long questionId; + + public static CreateKnockEventDto from(Knock knock) { + return CreateKnockEventDto.builder() + .fromUserId(knock.getUserId()) + .toUserId(knock.getKnockedUserId()) + .questionId(knock.getDailyQuestionId()) + .build(); + } +} diff --git a/Api/src/main/java/tify/server/api/alarm/model/dto/ReceiveApplicationEventDto.java b/Api/src/main/java/tify/server/api/alarm/model/dto/ReceiveApplicationEventDto.java new file mode 100644 index 00000000..8cbd042f --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/model/dto/ReceiveApplicationEventDto.java @@ -0,0 +1,22 @@ +package tify.server.api.alarm.model.dto; + + +import lombok.Builder; +import lombok.Getter; +import tify.server.domain.domains.user.domain.NeighborApplication; + +@Getter +@Builder +public class ReceiveApplicationEventDto { + + private final Long fromUserId; + + private final Long toUserId; + + public static ReceiveApplicationEventDto from(NeighborApplication application) { + return ReceiveApplicationEventDto.builder() + .fromUserId(application.getFromUserId()) + .toUserId(application.getToUserId()) + .build(); + } +} diff --git a/Api/src/main/java/tify/server/api/alarm/model/dto/SendApplicationEventDto.java b/Api/src/main/java/tify/server/api/alarm/model/dto/SendApplicationEventDto.java new file mode 100644 index 00000000..874b3986 --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/model/dto/SendApplicationEventDto.java @@ -0,0 +1,22 @@ +package tify.server.api.alarm.model.dto; + + +import lombok.Builder; +import lombok.Getter; +import tify.server.domain.domains.user.domain.NeighborApplication; + +@Getter +@Builder +public class SendApplicationEventDto { + + private final Long fromUserId; + + private final Long toUserId; + + public static SendApplicationEventDto from(NeighborApplication application) { + return SendApplicationEventDto.builder() + .fromUserId(application.getFromUserId()) + .toUserId(application.getToUserId()) + .build(); + } +} diff --git a/Api/src/main/java/tify/server/api/alarm/model/vo/AlarmHistoryVo.java b/Api/src/main/java/tify/server/api/alarm/model/vo/AlarmHistoryVo.java new file mode 100644 index 00000000..1f24643e --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/model/vo/AlarmHistoryVo.java @@ -0,0 +1,36 @@ +package tify.server.api.alarm.model.vo; + + +import lombok.Builder; +import lombok.Getter; +import tify.server.core.consts.Status; +import tify.server.domain.domains.alarm.domain.AlarmHistory; +import tify.server.domain.domains.alarm.domain.AlarmType; + +@Getter +@Builder +public class AlarmHistoryVo { + + private final Long id; + + private final AlarmType alarmType; + + private final String title; + + private final String content; + + private final Long userId; + + private final Status isRead; + + public static AlarmHistoryVo from(AlarmHistory alarmHistory) { + return AlarmHistoryVo.builder() + .id(alarmHistory.getId()) + .alarmType(alarmHistory.getAlarmType()) + .title(alarmHistory.getTitle()) + .content(alarmHistory.getContent()) + .userId(alarmHistory.getUserId()) + .isRead(alarmHistory.getIsRead()) + .build(); + } +} diff --git a/Api/src/main/java/tify/server/api/alarm/service/CreateAlarmHistoryUseCase.java b/Api/src/main/java/tify/server/api/alarm/service/CreateAlarmHistoryUseCase.java new file mode 100644 index 00000000..76925a65 --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/service/CreateAlarmHistoryUseCase.java @@ -0,0 +1,219 @@ +package tify.server.api.alarm.service; + +import static tify.server.domain.domains.alarm.domain.AlarmType.*; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.TextStyle; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.transaction.annotation.Transactional; +import tify.server.api.alarm.model.dto.AnswerKnockEventDto; +import tify.server.api.alarm.model.dto.CreateKnockEventDto; +import tify.server.api.alarm.model.dto.ReceiveApplicationEventDto; +import tify.server.api.alarm.model.dto.SendApplicationEventDto; +import tify.server.api.utils.AlarmHistoryUtils; +import tify.server.core.annotation.UseCase; +import tify.server.domain.domains.question.adaptor.AnswerAdaptor; +import tify.server.domain.domains.question.adaptor.DailyQuestionAdaptor; +import tify.server.domain.domains.question.adaptor.FavorQuestionAdaptor; +import tify.server.domain.domains.question.adaptor.KnockAdaptor; +import tify.server.domain.domains.question.domain.DailyQuestion; +import tify.server.domain.domains.user.adaptor.UserAdaptor; +import tify.server.domain.domains.user.domain.User; + +@UseCase +@Transactional +@RequiredArgsConstructor +public class CreateAlarmHistoryUseCase { + + private final UserAdaptor userAdaptor; + private final DailyQuestionAdaptor dailyQuestionAdaptor; + private final AnswerAdaptor answerAdaptor; + private final KnockAdaptor knockAdaptor; + private final FavorQuestionAdaptor favorQuestionAdaptor; + + private final AlarmHistoryUtils alarmHistoryUtils; + + @Async + @EventListener + public void executeToReceiveFriendApplicationAlarm(SendApplicationEventDto dto) { + User sender = userAdaptor.query(dto.getFromUserId()); + User receiver = userAdaptor.query(dto.getToUserId()); + String title = String.format("%s님의 친구맺기 요청!", sender.getUserId()); + String content = String.format("수락하고 %s님 취향 살펴보기 >", sender.getUserId()); + HashMap newMap = new HashMap<>(); + newMap.put("sendUserId", sender.getUserId()); + newMap.put("receiveUserId", receiver.getUserId()); + + if (alarmHistoryUtils.checkUserReceiveAlarm(receiver, title, content, FRIEND)) { + alarmHistoryUtils.sendMessage(receiver, title, content, newMap, FRIEND); + } + } + + @Async + @EventListener + public void executeToAcceptFriendApplicationAlarm(ReceiveApplicationEventDto dto) { + // sender : 요청을 보낸 사람, receiver : 요청을 받은 사람(수락을 누른 사람) + User sender = userAdaptor.query(dto.getFromUserId()); + User receiver = userAdaptor.query(dto.getToUserId()); + String title = String.format("%s님의 친구맺기 수락!", receiver.getUserId()); + String content = String.format("%s님 취향 살펴보기 >", receiver.getUserId()); + HashMap newMap = new HashMap<>(); + newMap.put("sendUserId", sender.getUserId()); + newMap.put("receiveUserId", receiver.getUserId()); + + if (alarmHistoryUtils.checkUserReceiveAlarm(sender, title, content, FRIEND)) { + alarmHistoryUtils.sendMessage(sender, title, content, newMap, FRIEND); + } + } + + @Async + @EventListener + public void executeToKnockAlarm(CreateKnockEventDto dto) { + User sender = userAdaptor.query(dto.getFromUserId()); + User receiver = userAdaptor.query(dto.getToUserId()); + DailyQuestion question = dailyQuestionAdaptor.query(dto.getQuestionId()); + int knockCount = + knockAdaptor + .queryAllByDailyQuestionIdAndUserIdAndKnockedUserId( + dto.getQuestionId(), dto.getFromUserId(), dto.getToUserId()) + .size(); + String title = + String.format("%s님이 %d번 쿡 찔렀어요 \uD83D\uDC49", sender.getUserId(), knockCount); + String content = + String.format( + "%s 질문 답변하러 가기 >", + question.getLoadingDate() + .getDayOfWeek() + .getDisplayName(TextStyle.FULL, Locale.KOREA)); + HashMap newMap = new HashMap<>(); + newMap.put("knockUserId", sender.getUserId()); + newMap.put("knockedUserId", receiver.getUserId()); + + if (alarmHistoryUtils.checkUserReceiveAlarm(receiver, title, content, TODAY)) { + alarmHistoryUtils.sendMessage(receiver, title, content, newMap, TODAY); + } + } + + @Async + @EventListener + public void executeToFriendAnswerAlarm(AnswerKnockEventDto dto) { + User sender = userAdaptor.query(dto.getFromUserId()); + User receiver = userAdaptor.query(dto.getToUserId()); + String title = String.format("내가 쿡! 찌른 %s님이 답변했어요 \uD83D\uDE4C", receiver.getUserId()); + String content = String.format("%s님의 투데이 답변 확인하러 가기 >", receiver.getUserId()); + + HashMap newMap = new HashMap<>(); + newMap.put("knockUserId", sender.getUserId()); + newMap.put("knockedUserId", receiver.getUserId()); + + if (alarmHistoryUtils.checkUserReceiveAlarm(sender, title, content, TODAY)) { + alarmHistoryUtils.sendMessage(sender, title, content, newMap, TODAY); + } + } + + @Async + public void executeToNotAnsweredQuestionAlarm(Long questionId) { + String title = "지금 바로 답변 가능한 투데이 질문이 있어요 \uD83D\uDC40"; + String content = "내 답변 남기고, 친구들의 답변 구경하기 >"; + + DailyQuestion question = dailyQuestionAdaptor.query(questionId); + List notAnsweredUserList = userAdaptor.queryNotAnsweredUsers(questionId); + + notAnsweredUserList.forEach( + user -> { + if (alarmHistoryUtils.checkUserReceiveAlarm(user, title, content, TODAY)) { + HashMap newMap = new HashMap<>(); + newMap.put("questionId", question.getContent()); + newMap.put("receiverId", user.getUserId()); + alarmHistoryUtils.sendMessage(user, title, content, newMap, TODAY); + } + }); + } + + @Async + public void executeToFriendBirthDayAlarm(Long userId) { + LocalDateTime today = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + String monthAndYear = + String.format("%02d%02d", today.getMonth().getValue(), today.getDayOfMonth() + 4); + String content = "취향에 딱 맞는 선물을 받을 확률을 올려보세요!\n티피 프로필 공유하러 가기 >"; + + List birthDayNeighbors = userAdaptor.queryBirthDayNeighbors(userId, monthAndYear); + + birthDayNeighbors.forEach( + user -> { + String title = + String.format( + "%s님의 생일이 4일 밖에 안남았대요! ", user.getProfile().getUserName()); + if (alarmHistoryUtils.checkUserReceiveAlarm( + user, title, content, ANNIVERSARY)) { + HashMap newMap = new HashMap<>(); + newMap.put("neighborUserId", user.getUserId()); + alarmHistoryUtils.sendMessage(user, title, content, newMap, ANNIVERSARY); + } + }); + } + + @Async + public void executeToBirthDayAlarm() { + String content = "취향에 딱 맞는 선물을 받을 확률을 올려보세요!\n티피 프로필 공유하러 가기 >"; + + List birthdayUserList = userAdaptor.queryBirthDayUsers(); + + birthdayUserList.forEach( + user -> { + String title = + String.format( + "%s님, 생일 축하드려요 \uD83C\uDF89", user.getProfile().getUserName()); + if (alarmHistoryUtils.checkUserReceiveAlarm( + user, title, content, ANNIVERSARY)) { + HashMap newMap = new HashMap<>(); + newMap.put("userId", user.getUserId()); + alarmHistoryUtils.sendMessage(user, title, content, newMap, ANNIVERSARY); + } + }); + } + + @Async + public void executeToChristmasAlarm() { + String title = "이번 크리스마스엔 센스있는 산타가 되어볼까요?\uD83C\uDF85"; + String content = "D-5, 티피에서 친구들의 취향 확인하고\n크리스마스 선물 고르기 >"; + + List userList = userAdaptor.queryAll(); + + userList.forEach( + user -> { + if (alarmHistoryUtils.checkUserReceiveAlarm( + user, title, content, ANNIVERSARY)) { + HashMap newMap = new HashMap<>(); + newMap.put("userId", user.getUserId()); + alarmHistoryUtils.sendMessage(user, title, content, newMap, ANNIVERSARY); + } + }); + } + + @Async + public void executeToFavorAlarm() { + String title = "답변 가능한 취향 질문이 남아있어요 \uD83D\uDC40"; + String content = "내 취향 남기러 가기 >"; + + int favorQuestionSize = favorQuestionAdaptor.queryAll().size(); + + userAdaptor + .queryNotTotallyFavorAnsweredUsers(favorQuestionSize) + .forEach( + user -> { + if (alarmHistoryUtils.checkUserReceiveAlarm( + user, title, content, FAVOR)) { + HashMap newMap = new HashMap<>(); + newMap.put("userId", user.getUserId()); + alarmHistoryUtils.sendMessage(user, title, content, newMap, FAVOR); + } + }); + } +} diff --git a/Api/src/main/java/tify/server/api/alarm/service/RetrieveAlarmHistoryUseCase.java b/Api/src/main/java/tify/server/api/alarm/service/RetrieveAlarmHistoryUseCase.java new file mode 100644 index 00000000..55c00cc1 --- /dev/null +++ b/Api/src/main/java/tify/server/api/alarm/service/RetrieveAlarmHistoryUseCase.java @@ -0,0 +1,48 @@ +package tify.server.api.alarm.service; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import tify.server.api.alarm.model.condition.AlarmCondition; +import tify.server.api.alarm.model.vo.AlarmHistoryVo; +import tify.server.core.annotation.UseCase; +import tify.server.domain.domains.alarm.adaptor.AlarmHistoryAdaptor; + +@Slf4j +@UseCase +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RetrieveAlarmHistoryUseCase { + + private final AlarmHistoryAdaptor alarmHistoryAdaptor; + + public List execute(AlarmCondition condition) { + List alarms = new ArrayList<>(); + Optional.ofNullable(condition.getUserId()) + .ifPresent( + id -> + alarms.addAll( + alarmHistoryAdaptor.queryByUserId(id).stream() + .map(AlarmHistoryVo::from) + .toList())); + Optional.ofNullable(condition.getTitle()) + .ifPresent( + title -> + alarms.addAll( + alarmHistoryAdaptor.queryByTitle(title).stream() + .map(AlarmHistoryVo::from) + .toList())); + Optional.ofNullable(condition.getStatus()) + .ifPresent( + status -> + alarms.addAll( + alarmHistoryAdaptor.queryByIsRead(status).stream() + .map(AlarmHistoryVo::from) + .toList())); + return alarms; + } +} diff --git a/Api/src/main/java/tify/server/api/answer/service/CreateKnockUseCase.java b/Api/src/main/java/tify/server/api/answer/service/CreateKnockUseCase.java index bdab1c1a..d9f751cf 100644 --- a/Api/src/main/java/tify/server/api/answer/service/CreateKnockUseCase.java +++ b/Api/src/main/java/tify/server/api/answer/service/CreateKnockUseCase.java @@ -2,7 +2,9 @@ import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; +import tify.server.api.alarm.model.dto.CreateKnockEventDto; import tify.server.api.config.security.SecurityUtils; import tify.server.core.annotation.UseCase; import tify.server.domain.domains.question.adaptor.KnockAdaptor; @@ -20,6 +22,8 @@ public class CreateKnockUseCase { private final QuestionValidator questionValidator; private final KnockAdaptor knockAdaptor; + private final ApplicationEventPublisher applicationEventPublisher; + @Transactional public void execute(Long questionId, Long userId) { Long currentUserId = SecurityUtils.getCurrentUserId(); @@ -28,11 +32,13 @@ public void execute(Long questionId, Long userId) { userValidator.isNeighbor(currentUserId, userId); // 친구인지를 검증 questionValidator.isValidateAnswerToQuestion( questionId, userId); // 오늘 날짜의 질문이 맞는지, 친구가 이미 답을 남겼는지 검증 - knockAdaptor.save( + Knock knock = Knock.builder() .userId(currentUserId) .knockedUserId(userId) .dailyQuestionId(questionId) - .build()); + .build(); + knockAdaptor.save(knock); + applicationEventPublisher.publishEvent(CreateKnockEventDto.from(knock)); } } diff --git a/Api/src/main/java/tify/server/api/question/service/CreateAnswerUseCase.java b/Api/src/main/java/tify/server/api/question/service/CreateAnswerUseCase.java index 39d8dcb4..b7206088 100644 --- a/Api/src/main/java/tify/server/api/question/service/CreateAnswerUseCase.java +++ b/Api/src/main/java/tify/server/api/question/service/CreateAnswerUseCase.java @@ -1,10 +1,14 @@ package tify.server.api.question.service; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import tify.server.api.alarm.model.dto.AnswerKnockEventDto; import tify.server.api.config.security.SecurityUtils; import tify.server.api.question.model.request.PostAnswerRequest; import tify.server.core.annotation.UseCase; +import tify.server.domain.domains.question.domain.Knock; import tify.server.domain.domains.question.service.DailyQuestionDomainService; import tify.server.domain.domains.question.validator.QuestionValidator; @@ -15,9 +19,16 @@ public class CreateAnswerUseCase { private final DailyQuestionDomainService dailyQuestionDomainService; private final QuestionValidator questionValidator; + private final ApplicationEventPublisher applicationEventPublisher; + public void execute(Long questionId, PostAnswerRequest body) { questionValidator.isValidDailyQuestion(questionId); - dailyQuestionDomainService.createAnswer( - questionId, SecurityUtils.getCurrentUserId(), body.getAnswer()); + List knockList = + dailyQuestionDomainService.createAnswer( + questionId, SecurityUtils.getCurrentUserId(), body.getAnswer()); + knockList.forEach( + knock -> { + applicationEventPublisher.publishEvent(AnswerKnockEventDto.from(knock)); + }); } } diff --git a/Api/src/main/java/tify/server/api/user/controller/UserController.java b/Api/src/main/java/tify/server/api/user/controller/UserController.java index 19c40236..02243cd0 100644 --- a/Api/src/main/java/tify/server/api/user/controller/UserController.java +++ b/Api/src/main/java/tify/server/api/user/controller/UserController.java @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import tify.server.api.common.slice.SliceResponse; +import tify.server.api.user.model.dto.request.PatchExpoTokenRequest; import tify.server.api.user.model.dto.request.PatchNeighborsOrdersRequest; import tify.server.api.user.model.dto.request.PatchUserFavorRequest; import tify.server.api.user.model.dto.request.PatchUserProfileRequest; @@ -54,6 +55,7 @@ import tify.server.api.user.service.RetrieveUserOpinionUseCase; import tify.server.api.user.service.RetrieveUserReportUseCase; import tify.server.api.user.service.UpdateNeighborUseCase; +import tify.server.api.user.service.UpdateUserExpoTokenUseCase; import tify.server.api.user.service.UpdateUserFavorUseCase; import tify.server.api.user.service.UpdateUserProfileUseCase; import tify.server.api.user.service.UserBlockUseCase; @@ -101,6 +103,7 @@ public class UserController { private final CreateUserOpinionUseCase createUserOpinionUseCase; private final RetrieveUserOpinionUseCase retrieveUserOpinionUseCase; private final RetrieveNeighborFavorBoxUseCase retrieveNeighborFavorBoxUseCase; + private final UpdateUserExpoTokenUseCase updateUserExpoTokenUseCase; @Operation(summary = "유저 정보 조회") @GetMapping("/{userId}") @@ -329,4 +332,11 @@ public List getMyAllOpinion() { public List getNeighborsFavorBox() { return retrieveNeighborFavorBoxUseCase.execute(); } + + @Operation(summary = "유저의 expo 토큰을 업데이트합니다.") + @PatchMapping("/expo-token") + public void patchExpoToken( + @RequestParam Long userId, @RequestBody @Valid PatchExpoTokenRequest request) { + updateUserExpoTokenUseCase.execute(userId, request); + } } diff --git a/Api/src/main/java/tify/server/api/user/model/dto/request/PatchExpoTokenRequest.java b/Api/src/main/java/tify/server/api/user/model/dto/request/PatchExpoTokenRequest.java new file mode 100644 index 00000000..9babc3a4 --- /dev/null +++ b/Api/src/main/java/tify/server/api/user/model/dto/request/PatchExpoTokenRequest.java @@ -0,0 +1,16 @@ +package tify.server.api.user.model.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PatchExpoTokenRequest { + + @Schema(description = "유저에 할당할 expo token입니다.", example = "ExponentPushToken[exampleToken]") + @NotNull(message = "expo token을 입력해주세요.") + private String expoToken; +} diff --git a/Api/src/main/java/tify/server/api/user/service/AcceptanceNeighborApplicationUseCase.java b/Api/src/main/java/tify/server/api/user/service/AcceptanceNeighborApplicationUseCase.java index da7dbc97..438eef4e 100644 --- a/Api/src/main/java/tify/server/api/user/service/AcceptanceNeighborApplicationUseCase.java +++ b/Api/src/main/java/tify/server/api/user/service/AcceptanceNeighborApplicationUseCase.java @@ -3,7 +3,9 @@ import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; +import tify.server.api.alarm.model.dto.ReceiveApplicationEventDto; import tify.server.api.config.security.SecurityUtils; import tify.server.core.annotation.UseCase; import tify.server.domain.domains.user.adaptor.NeighborAdaptor; @@ -22,6 +24,8 @@ public class AcceptanceNeighborApplicationUseCase { private final NeighborValidator neighborValidator; private final UserValidator userValidator; + private final ApplicationEventPublisher applicationEventPublisher; + public void execute(Long neighborApplicationId) { // 나 (친구 신청을 받은 사람) @@ -62,5 +66,8 @@ public void execute(Long neighborApplicationId) { .isNew(true) .order((long) toNeighbors.size() + 1L) .build()); + + applicationEventPublisher.publishEvent( + ReceiveApplicationEventDto.from(neighborApplication)); } } diff --git a/Api/src/main/java/tify/server/api/user/service/CreateNeighborUseCase.java b/Api/src/main/java/tify/server/api/user/service/CreateNeighborUseCase.java index 8934930e..70071977 100644 --- a/Api/src/main/java/tify/server/api/user/service/CreateNeighborUseCase.java +++ b/Api/src/main/java/tify/server/api/user/service/CreateNeighborUseCase.java @@ -2,7 +2,9 @@ import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; +import tify.server.api.alarm.model.dto.SendApplicationEventDto; import tify.server.api.config.security.SecurityUtils; import tify.server.core.annotation.UseCase; import tify.server.domain.domains.user.adaptor.NeighborAdaptor; @@ -22,6 +24,8 @@ public class CreateNeighborUseCase { private final NeighborAdaptor neighborAdaptor; private final UserValidator userValidator; + private final ApplicationEventPublisher applicationEventPublisher; + public void execute(Long toUserId) { Long currentUserId = SecurityUtils.getCurrentUserId(); @@ -40,11 +44,13 @@ public void execute(Long toUserId) { if (!neighborAdaptor .queryByFromUserIdAndToUserId(toUser.getId(), currentUserId) .isPresent()) { - neighborAdaptor.saveNeighborApplication( + NeighborApplication application = NeighborApplication.builder() .fromUserId(SecurityUtils.getCurrentUserId()) .toUserId(toUserId) - .build()); + .build(); + neighborAdaptor.saveNeighborApplication(application); + applicationEventPublisher.publishEvent(SendApplicationEventDto.from(application)); } } } diff --git a/Api/src/main/java/tify/server/api/user/service/UpdateUserExpoTokenUseCase.java b/Api/src/main/java/tify/server/api/user/service/UpdateUserExpoTokenUseCase.java new file mode 100644 index 00000000..bd51deb8 --- /dev/null +++ b/Api/src/main/java/tify/server/api/user/service/UpdateUserExpoTokenUseCase.java @@ -0,0 +1,22 @@ +package tify.server.api.user.service; + + +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; +import tify.server.api.user.model.dto.request.PatchExpoTokenRequest; +import tify.server.core.annotation.UseCase; +import tify.server.domain.domains.user.adaptor.UserAdaptor; +import tify.server.domain.domains.user.domain.User; + +@UseCase +@RequiredArgsConstructor +public class UpdateUserExpoTokenUseCase { + + private final UserAdaptor userAdaptor; + + @Transactional + public void execute(Long userId, PatchExpoTokenRequest request) { + User user = userAdaptor.query(userId); + user.updateUserExpoToken(request.getExpoToken()); + } +} diff --git a/Api/src/main/java/tify/server/api/utils/AlarmHistoryUtils.java b/Api/src/main/java/tify/server/api/utils/AlarmHistoryUtils.java new file mode 100644 index 00000000..6100cdeb --- /dev/null +++ b/Api/src/main/java/tify/server/api/utils/AlarmHistoryUtils.java @@ -0,0 +1,142 @@ +package tify.server.api.utils; + + +import io.github.jav.exposerversdk.ExpoPushMessage; +import io.github.jav.exposerversdk.ExpoPushMessageTicketPair; +import io.github.jav.exposerversdk.ExpoPushTicket; +import io.github.jav.exposerversdk.PushClient; +import io.github.jav.exposerversdk.PushClientException; +import io.github.jav.exposerversdk.PushNotificationException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.GenericJDBCException; +import org.springframework.stereotype.Component; +import tify.server.domain.domains.alarm.adaptor.AlarmHistoryAdaptor; +import tify.server.domain.domains.alarm.domain.AlarmHistory; +import tify.server.domain.domains.alarm.domain.AlarmType; +import tify.server.domain.domains.alarm.exception.ExpoPushTicketException; +import tify.server.domain.domains.alarm.exception.NotValidExpoTokenException; +import tify.server.domain.domains.user.domain.User; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AlarmHistoryUtils { + + private final AlarmHistoryAdaptor alarmHistoryAdaptor; + + public void sendMessage( + User user, String title, String body, Map data, AlarmType alarmType) { + + String token = user.getExpoToken(); + if (token == null) { + log.info("token = {}", user.getExpoToken()); + throw NotValidExpoTokenException.EXCEPTION; + } + if (!PushClient.isExponentPushToken(token)) { + log.info("token = {}", user.getExpoToken()); + throw NotValidExpoTokenException.EXCEPTION; + } + ExpoPushMessage expoPushMessage = new ExpoPushMessage(); + expoPushMessage.getTo().add(token); + expoPushMessage.setTitle(title); + expoPushMessage.setBody(body); + expoPushMessage.setData(data); + + List expoPushMessages = new ArrayList<>(); + expoPushMessages.add(expoPushMessage); + + try { + PushClient pushClient = new PushClient(); + List> chunks = + pushClient.chunkPushNotifications(expoPushMessages); + + List>> messageRepliesFutures = new ArrayList<>(); + + for (List chunk : chunks) { + messageRepliesFutures.add(pushClient.sendPushNotificationsAsync(chunk)); + } + + AlarmHistory alarm = + AlarmHistory.builder() + .title(title) + .content(body) + .userId(user.getId()) + .alarmType(alarmType) + .build(); + alarmHistoryAdaptor.save(alarm); + List allTickets = new ArrayList<>(); + for (CompletableFuture> messageReplyFuture : + messageRepliesFutures) { + try { + allTickets.addAll(messageReplyFuture.get()); + } catch (InterruptedException | ExecutionException e) { + log.error("error message = {}", e.getMessage()); + throw ExpoPushTicketException.EXCEPTION; + } + } + + List> zippedMessagesTickets = + pushClient.zipMessagesTickets(expoPushMessages, allTickets); + + List> okTicketMessages = + pushClient.filterAllSuccessfulMessages(zippedMessagesTickets); + String okTicketMessagesString = + okTicketMessages.stream() + .map(p -> "Title: " + p.message.getTitle() + ", Id:" + p.ticket.getId()) + .collect(Collectors.joining(",")); + log.info( + "Recieved OK ticket for " + + okTicketMessages.size() + + " messages: " + + okTicketMessagesString); + + List> errorTicketMessages = + pushClient.filterAllMessagesWithError(zippedMessagesTickets); + String errorTicketMessagesString = + errorTicketMessages.stream() + .map( + p -> + "id: " + + user.getId() + + ", " + + "Title: " + + p.message.getTitle() + + ", Error: " + + p.ticket.getDetails().getError()) + .collect(Collectors.joining(",")); + log.info( + "Recieved ERROR ticket for " + + errorTicketMessages.size() + + " messages: " + + errorTicketMessagesString); + } catch (PushClientException | PushNotificationException | GenericJDBCException e) { + log.info("error message = {}", e.getMessage()); + throw ExpoPushTicketException.EXCEPTION; + } + } + + public Boolean checkUserReceiveAlarm( + User user, String title, String content, AlarmType alarmType) { + if ((!user.getReceiveAlarm() + || user.getExpoToken() == null + || !user.getExpoToken().startsWith("ExponentPushToken"))) { + AlarmHistory alarm = + AlarmHistory.builder() + .userId(user.getId()) + .title(title) + .content(content) + .alarmType(alarmType) + .build(); + alarmHistoryAdaptor.save(alarm); + return false; + } + return true; + } +} diff --git a/Domain/src/main/java/tify/server/domain/domains/alarm/adaptor/AlarmHistoryAdaptor.java b/Domain/src/main/java/tify/server/domain/domains/alarm/adaptor/AlarmHistoryAdaptor.java index aa842e59..587707e8 100644 --- a/Domain/src/main/java/tify/server/domain/domains/alarm/adaptor/AlarmHistoryAdaptor.java +++ b/Domain/src/main/java/tify/server/domain/domains/alarm/adaptor/AlarmHistoryAdaptor.java @@ -1,8 +1,10 @@ package tify.server.domain.domains.alarm.adaptor; +import java.util.List; import lombok.RequiredArgsConstructor; import tify.server.core.annotation.Adaptor; +import tify.server.core.consts.Status; import tify.server.domain.domains.alarm.domain.AlarmHistory; import tify.server.domain.domains.alarm.exception.AlarmHistoryNotFoundException; import tify.server.domain.domains.alarm.repository.AlarmHistoryRepository; @@ -21,4 +23,16 @@ public AlarmHistory query(Long alarmHistoryId) { public AlarmHistory save(AlarmHistory alarmHistory) { return alarmHistoryRepository.save(alarmHistory); } + + public List queryByUserId(Long userId) { + return alarmHistoryRepository.findAllByUserId(userId); + } + + public List queryByIsRead(Status isRead) { + return alarmHistoryRepository.findAllByIsRead(isRead); + } + + public List queryByTitle(String title) { + return alarmHistoryRepository.findAllByTitle(title); + } } diff --git a/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmHistory.java b/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmHistory.java index fc8ff091..0e0514a2 100644 --- a/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmHistory.java +++ b/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmHistory.java @@ -25,6 +25,8 @@ public class AlarmHistory { @Enumerated(EnumType.STRING) private AlarmType alarmType; + @NotNull private String title; + @NotNull private String content; @NotNull @@ -32,10 +34,11 @@ public class AlarmHistory { private Status isRead; @Builder - public AlarmHistory(Long id, Long userId, AlarmType alarmType, String content) { + public AlarmHistory(Long id, Long userId, AlarmType alarmType, String title, String content) { this.id = id; this.userId = userId; this.alarmType = alarmType; + this.title = title; this.content = content; this.isRead = Status.N; } diff --git a/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmType.java b/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmType.java index 20a59572..dc6c14c1 100644 --- a/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmType.java +++ b/Domain/src/main/java/tify/server/domain/domains/alarm/domain/AlarmType.java @@ -7,7 +7,11 @@ @Getter @AllArgsConstructor public enum AlarmType { - ALARM("쿡 찌르기"); + FRIEND("프렌즈"), + TODAY("투데이 질문"), + ANNIVERSARY("기념일"), + FAVOR("취향 질문"), + ; final String type; } diff --git a/Domain/src/main/java/tify/server/domain/domains/alarm/exception/AlarmHistoryException.java b/Domain/src/main/java/tify/server/domain/domains/alarm/exception/AlarmHistoryException.java index 334af45d..f919c2f2 100644 --- a/Domain/src/main/java/tify/server/domain/domains/alarm/exception/AlarmHistoryException.java +++ b/Domain/src/main/java/tify/server/domain/domains/alarm/exception/AlarmHistoryException.java @@ -1,5 +1,6 @@ package tify.server.domain.domains.alarm.exception; +import static tify.server.core.consts.StaticVal.BAD_REQUEST; import static tify.server.core.consts.StaticVal.NOT_FOUND; import lombok.AllArgsConstructor; @@ -10,7 +11,11 @@ @Getter @AllArgsConstructor public enum AlarmHistoryException implements BaseErrorCode { - ALARM_NOT_FOUND_ERROR(NOT_FOUND, "AlarmHistory_404_1", "알람 히스토리 정보를 찾을 수 없습니다."); + ALARM_NOT_FOUND_ERROR(NOT_FOUND, "AlarmHistory_404_1", "알람 히스토리 정보를 찾을 수 없습니다."), + NOT_VALID_EXPO_TOKEN_ERROR( + BAD_REQUEST, "AlarmHistory_400_1", "알람 히스토리 서비스를 이용하려면 EXPO TOKEN이 필요합니다."), + EXPO_PUSH_TICKET_ERROR(BAD_REQUEST, "AlarmHistory_400_2", "알람 히스토리 expo 서비스 에러입니다."), + ; private final Integer statusCode; private final String errorCode; diff --git a/Domain/src/main/java/tify/server/domain/domains/alarm/exception/ExpoPushTicketException.java b/Domain/src/main/java/tify/server/domain/domains/alarm/exception/ExpoPushTicketException.java new file mode 100644 index 00000000..b370482b --- /dev/null +++ b/Domain/src/main/java/tify/server/domain/domains/alarm/exception/ExpoPushTicketException.java @@ -0,0 +1,13 @@ +package tify.server.domain.domains.alarm.exception; + + +import tify.server.core.exception.BaseException; + +public class ExpoPushTicketException extends BaseException { + + public static final BaseException EXCEPTION = new ExpoPushTicketException(); + + private ExpoPushTicketException() { + super(AlarmHistoryException.EXPO_PUSH_TICKET_ERROR); + } +} diff --git a/Domain/src/main/java/tify/server/domain/domains/alarm/exception/NotValidExpoTokenException.java b/Domain/src/main/java/tify/server/domain/domains/alarm/exception/NotValidExpoTokenException.java new file mode 100644 index 00000000..508c9462 --- /dev/null +++ b/Domain/src/main/java/tify/server/domain/domains/alarm/exception/NotValidExpoTokenException.java @@ -0,0 +1,13 @@ +package tify.server.domain.domains.alarm.exception; + + +import tify.server.core.exception.BaseException; + +public class NotValidExpoTokenException extends BaseException { + + public static final BaseException EXCEPTION = new NotValidExpoTokenException(); + + private NotValidExpoTokenException() { + super(AlarmHistoryException.NOT_VALID_EXPO_TOKEN_ERROR); + } +} diff --git a/Domain/src/main/java/tify/server/domain/domains/alarm/repository/AlarmHistoryRepository.java b/Domain/src/main/java/tify/server/domain/domains/alarm/repository/AlarmHistoryRepository.java index d8a3b687..d3d94884 100644 --- a/Domain/src/main/java/tify/server/domain/domains/alarm/repository/AlarmHistoryRepository.java +++ b/Domain/src/main/java/tify/server/domain/domains/alarm/repository/AlarmHistoryRepository.java @@ -1,7 +1,16 @@ package tify.server.domain.domains.alarm.repository; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import tify.server.core.consts.Status; import tify.server.domain.domains.alarm.domain.AlarmHistory; -public interface AlarmHistoryRepository extends JpaRepository {} +public interface AlarmHistoryRepository extends JpaRepository { + + List findAllByUserId(Long userId); + + List findAllByIsRead(Status isRead); + + List findAllByTitle(String title); +} diff --git a/Domain/src/main/java/tify/server/domain/domains/question/adaptor/FavorQuestionAdaptor.java b/Domain/src/main/java/tify/server/domain/domains/question/adaptor/FavorQuestionAdaptor.java index b4a08e7c..e2ef33e9 100644 --- a/Domain/src/main/java/tify/server/domain/domains/question/adaptor/FavorQuestionAdaptor.java +++ b/Domain/src/main/java/tify/server/domain/domains/question/adaptor/FavorQuestionAdaptor.java @@ -83,4 +83,8 @@ public List queryAllFavorQuestionCategory() { public List queryBySmallCategory(SmallCategory smallCategory) { return favorQuestionCategoryRepository.findBySmallCategory(smallCategory); } + + public List queryAll() { + return favorQuestionRepository.findAll(); + } } diff --git a/Domain/src/main/java/tify/server/domain/domains/question/adaptor/KnockAdaptor.java b/Domain/src/main/java/tify/server/domain/domains/question/adaptor/KnockAdaptor.java index 104f2ebb..a288e9c1 100644 --- a/Domain/src/main/java/tify/server/domain/domains/question/adaptor/KnockAdaptor.java +++ b/Domain/src/main/java/tify/server/domain/domains/question/adaptor/KnockAdaptor.java @@ -43,4 +43,10 @@ public List queryMyKnockList(Long questionId, Long userId) { public List queryKnockToMeList(Long questionId, Long userId) { return knockRepository.searchKnockToMeList(questionId, userId); } + + public List queryAllByKnockedUserIdAndDailyQuestionId( + Long knockedUserId, Long dailyQuestionId) { + return knockRepository.findAllByKnockedUserIdAndDailyQuestionId( + knockedUserId, dailyQuestionId); + } } diff --git a/Domain/src/main/java/tify/server/domain/domains/question/exception/MultipleAnswerException.java b/Domain/src/main/java/tify/server/domain/domains/question/exception/MultipleAnswerException.java new file mode 100644 index 00000000..56cbe418 --- /dev/null +++ b/Domain/src/main/java/tify/server/domain/domains/question/exception/MultipleAnswerException.java @@ -0,0 +1,13 @@ +package tify.server.domain.domains.question.exception; + + +import tify.server.core.exception.BaseException; + +public class MultipleAnswerException extends BaseException { + + public static final BaseException EXCEPTION = new MultipleAnswerException(); + + private MultipleAnswerException() { + super(QuestionException.MULTIPLE_ANSWER_ERROR); + } +} diff --git a/Domain/src/main/java/tify/server/domain/domains/question/exception/QuestionException.java b/Domain/src/main/java/tify/server/domain/domains/question/exception/QuestionException.java index 6c7478e7..d389d933 100644 --- a/Domain/src/main/java/tify/server/domain/domains/question/exception/QuestionException.java +++ b/Domain/src/main/java/tify/server/domain/domains/question/exception/QuestionException.java @@ -16,6 +16,7 @@ public enum QuestionException implements BaseErrorCode { KNOCK_NOT_FOUND_ERROR(NOT_FOUND, "Knock_404_1", "찌르기에 대한 답변 정보를 찾을 수 없습니다."), NOT_VALID_TODAY_QUESTION_ERROR(BAD_REQUEST, "Answer_400_1", "오늘의 질문이 아닙니다."), ALREADY_ANSWERED_QUESTION_ERROR(BAD_REQUEST, "Answer_400_2", "이미 답변하신 질문입니다."), + MULTIPLE_ANSWER_ERROR(BAD_REQUEST, "Answer_400_3", "한 질문에 여러번 답할 수 없습니다."), FAVOR_QUESTION_NOT_FOUND_ERROR(NOT_FOUND, "FavorQuestion_404_1", "취향 질문 정보를 찾을 수 없습니다."), FAVOR_ANSWER_NOT_FOUND_ERROR(NOT_FOUND, "FavorAnswer_404_1", "취향 질문 답변 정보를 찾을 수 없습니다."), ALREADY_ANSWERED_FAVOR_QUESTION_ERROR(BAD_REQUEST, "FavorAnswer_400_2", "이미 답변하신 취향 질문입니다."), diff --git a/Domain/src/main/java/tify/server/domain/domains/question/repository/KnockRepository.java b/Domain/src/main/java/tify/server/domain/domains/question/repository/KnockRepository.java index ed72baf3..aa2503fb 100644 --- a/Domain/src/main/java/tify/server/domain/domains/question/repository/KnockRepository.java +++ b/Domain/src/main/java/tify/server/domain/domains/question/repository/KnockRepository.java @@ -13,4 +13,6 @@ List findAllByDailyQuestionIdAndUserIdAndKnockedUserId( Long questionId, Long userId, Long knockedUserId); List findDistinctByDailyQuestionIdAndKnockedUserId(Long questionId, Long knockedUserId); + + List findAllByKnockedUserIdAndDailyQuestionId(Long knockedUserId, Long dailyQuestionId); } diff --git a/Domain/src/main/java/tify/server/domain/domains/question/service/DailyQuestionDomainService.java b/Domain/src/main/java/tify/server/domain/domains/question/service/DailyQuestionDomainService.java index 2349e7a6..72cfbb0c 100644 --- a/Domain/src/main/java/tify/server/domain/domains/question/service/DailyQuestionDomainService.java +++ b/Domain/src/main/java/tify/server/domain/domains/question/service/DailyQuestionDomainService.java @@ -1,11 +1,14 @@ package tify.server.domain.domains.question.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; import tify.server.core.annotation.DomainService; import tify.server.domain.domains.question.adaptor.AnswerAdaptor; +import tify.server.domain.domains.question.adaptor.KnockAdaptor; import tify.server.domain.domains.question.domain.Answer; +import tify.server.domain.domains.question.domain.Knock; import tify.server.domain.domains.question.validator.QuestionValidator; @DomainService @@ -14,15 +17,18 @@ public class DailyQuestionDomainService { private final AnswerAdaptor answerAdaptor; + private final KnockAdaptor knockAdaptor; private final QuestionValidator questionValidator; @Transactional - public void createAnswer(Long questionId, Long userId, String answer) { + public List createAnswer(Long questionId, Long userId, String answer) { // 답변 가능 여부 검증 questionValidator.isValidateAnswerToQuestion(questionId, userId); // 답변 작성 answerAdaptor.save( Answer.builder().questionId(questionId).userId(userId).content(answer).build()); + + return knockAdaptor.queryAllByKnockedUserIdAndDailyQuestionId(userId, questionId); } } diff --git a/Domain/src/main/java/tify/server/domain/domains/question/service/FavorQuestionDomainService.java b/Domain/src/main/java/tify/server/domain/domains/question/service/FavorQuestionDomainService.java index 14491ade..e9c42a97 100644 --- a/Domain/src/main/java/tify/server/domain/domains/question/service/FavorQuestionDomainService.java +++ b/Domain/src/main/java/tify/server/domain/domains/question/service/FavorQuestionDomainService.java @@ -9,6 +9,7 @@ import tify.server.domain.domains.question.domain.FavorAnswer; import tify.server.domain.domains.question.domain.FavorQuestion; import tify.server.domain.domains.question.dto.model.FavorAnswerDto; +import tify.server.domain.domains.question.exception.MultipleAnswerException; import tify.server.domain.domains.question.validator.FavorQuestionValidator; import tify.server.domain.domains.user.adaptor.UserAdaptor; import tify.server.domain.domains.user.domain.User; @@ -34,6 +35,12 @@ public void createFavorAnswer(Long userId, String categoryName, List numList = answers.stream().map(FavorAnswerDto::getNum).toList(); + if (numList.stream().distinct().toList().size() < numList.size()) { + throw MultipleAnswerException.EXCEPTION; + } + List favorAnswers = answers.stream() .map( diff --git a/Domain/src/main/java/tify/server/domain/domains/user/adaptor/UserAdaptor.java b/Domain/src/main/java/tify/server/domain/domains/user/adaptor/UserAdaptor.java index c4c7708a..24e97983 100644 --- a/Domain/src/main/java/tify/server/domain/domains/user/adaptor/UserAdaptor.java +++ b/Domain/src/main/java/tify/server/domain/domains/user/adaptor/UserAdaptor.java @@ -39,6 +39,10 @@ public User query(Long userId) { return userRepository.findById(userId).orElseThrow(() -> UserNotFoundException.EXCEPTION); } + public List queryAll() { + return userRepository.findAll(); + } + public Optional queryByUserId(String userId) { return userRepository.findByUserId(userId); } @@ -58,7 +62,7 @@ public Slice searchUsers(Pageable pageable, UserCondition condition, Long } public List queryUserFavorBox(Long userId) { - return userRepository.findNeighborsFavorBox(userId); + return userRepository.getNeighborsFavorBox(userId); } public User queryByOauthInfo(OauthInfo oauthInfo) { @@ -70,4 +74,20 @@ public User queryByOauthInfo(OauthInfo oauthInfo) { public Boolean existByUserId(Long userId) { return userRepository.findById(userId).isPresent(); } + + public List queryNotAnsweredUsers(Long questionId) { + return userRepository.getNotDailyAnsweredUserList(questionId); + } + + public List queryBirthDayUsers() { + return userRepository.getBirthDayUserList(); + } + + public List queryBirthDayNeighbors(Long userId, String monthAndYear) { + return userRepository.getBirthDayNeighborList(userId, monthAndYear); + } + + public List queryNotTotallyFavorAnsweredUsers(int favorQuestionSize) { + return userRepository.getNotFavorAnsweredUserList(favorQuestionSize); + } } diff --git a/Domain/src/main/java/tify/server/domain/domains/user/domain/User.java b/Domain/src/main/java/tify/server/domain/domains/user/domain/User.java index 4851682d..31617854 100644 --- a/Domain/src/main/java/tify/server/domain/domains/user/domain/User.java +++ b/Domain/src/main/java/tify/server/domain/domains/user/domain/User.java @@ -8,6 +8,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; import tify.server.domain.domains.AbstractTimeStamp; import tify.server.domain.domains.user.vo.UserInfoVo; import tify.server.domain.domains.user.vo.UserProfileVo; @@ -35,6 +36,9 @@ public class User extends AbstractTimeStamp { private String expoToken; + @ColumnDefault("false") + private Boolean receiveAlarm; + private String appleRefreshToken; @OneToMany( @@ -69,6 +73,7 @@ public User(Profile profile, OauthInfo oauthInfo, String expoToken) { this.oauthInfo = oauthInfo; this.accountRole = AccountRole.USER; this.expoToken = expoToken; + this.receiveAlarm = false; } public void updateProfile(Profile profile) { @@ -93,6 +98,7 @@ public void onBoarding( this.onBoardingStatus = onBoardingStatus; this.userFavors.clear(); this.userFavors.addAll(userFavorList); + this.receiveAlarm = false; } public void updateFavor() { @@ -115,4 +121,8 @@ public void updateUserId(String userId) { public void updateOnBoardingStatus(UserOnBoardingStatus userOnBoardingStatus) { this.onBoardingStatus = userOnBoardingStatus; } + + public void updateUserExpoToken(String expoToken) { + this.expoToken = expoToken; + } } diff --git a/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepository.java b/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepository.java index 83c991a7..9102e286 100644 --- a/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepository.java +++ b/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepository.java @@ -12,5 +12,13 @@ public interface UserCustomRepository { Slice searchUsers(Pageable pageable, UserCondition userCondition, Long currentUserId); - List findNeighborsFavorBox(Long userId); + List getNeighborsFavorBox(Long userId); + + List getNotDailyAnsweredUserList(Long questionId); + + List getBirthDayUserList(); + + List getBirthDayNeighborList(Long userId, String monthAndYear); + + List getNotFavorAnsweredUserList(int favorQuestionSize); } diff --git a/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepositoryImpl.java b/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepositoryImpl.java index 3b991c2d..be2af01e 100644 --- a/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepositoryImpl.java +++ b/Domain/src/main/java/tify/server/domain/domains/user/repository/UserCustomRepositoryImpl.java @@ -1,14 +1,17 @@ package tify.server.domain.domains.user.repository; -import static tify.server.domain.domains.user.domain.QNeighbor.*; +import static tify.server.domain.domains.question.domain.QAnswer.answer; +import static tify.server.domain.domains.question.domain.QFavorAnswer.*; +import static tify.server.domain.domains.user.domain.QNeighbor.neighbor; import static tify.server.domain.domains.user.domain.QUser.user; import static tify.server.domain.domains.user.domain.QUserBlock.userBlock; -import static tify.server.domain.domains.user.domain.QUserOnBoardingStatus.*; -import static tify.server.domain.domains.user.domain.QUserResign.*; +import static tify.server.domain.domains.user.domain.QUserResign.userResign; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -46,7 +49,7 @@ public Slice searchUsers( } @Override - public List findNeighborsFavorBox(Long userId) { + public List getNeighborsFavorBox(Long userId) { return jpaQueryFactory .select( Projections.constructor( @@ -61,6 +64,54 @@ public List findNeighborsFavorBox(Long userId) { .fetch(); } + @Override + public List getNotDailyAnsweredUserList(Long questionId) { + return jpaQueryFactory + .selectFrom(user) + .leftJoin(answer) + .on(user.id.eq(answer.userId), answer.questionId.eq(questionId)) + .where(answer.userId.isNull()) + .fetch(); + } + + @Override + public List getBirthDayUserList() { + LocalDateTime today = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + String monthAndYear = + String.format("%02d%02d", today.getMonth().getValue(), today.getDayOfMonth()); + return jpaQueryFactory + .selectFrom(user) + .where(user.profile.birth.contains(monthAndYear)) + .fetch(); + } + + @Override + public List getBirthDayNeighborList(Long userId, String monthAndYear) { + return jpaQueryFactory + .selectFrom(user) + .join(neighbor) + .on(user.id.eq(neighbor.toUserId)) + .leftJoin(userResign) + .on(neighbor.toUserId.eq(userResign.userId)) + .where( + neighbor.fromUserId.eq(userId), + user.profile.birth.contains(monthAndYear), + userResign.userId.isNull()) + .orderBy(neighbor.order.asc()) + .fetch(); + } + + @Override + public List getNotFavorAnsweredUserList(int favorQuestionSize) { + return jpaQueryFactory + .selectFrom(user) + .join(favorAnswer) + .on(user.id.eq(favorAnswer.userId)) + .groupBy(favorAnswer.userId) + .having(favorAnswer.id.count().lt(favorQuestionSize)) + .fetch(); + } + private BooleanExpression userIdEquals(String userId) { return (userId != null) ? user.userId.eq(userId) : null; } diff --git a/Domain/src/main/java/tify/server/domain/domains/user/validator/UserValidator.java b/Domain/src/main/java/tify/server/domain/domains/user/validator/UserValidator.java index 30700b81..32d0b5b1 100644 --- a/Domain/src/main/java/tify/server/domain/domains/user/validator/UserValidator.java +++ b/Domain/src/main/java/tify/server/domain/domains/user/validator/UserValidator.java @@ -39,9 +39,9 @@ public void isNewUser(OauthInfo oauthInfo) { } public void isNeighbor(Long userId, Long neighborId) { - userResignAdaptor - .optionalQueryByUserId(neighborId) - .orElseThrow(() -> new BaseException(USER_RESIGNED_ERROR)); + if (userResignAdaptor.optionalQueryByUserId(neighborId).isPresent()) { + throw new BaseException(USER_RESIGNED_ERROR); + } neighborAdaptor .queryByFromUserIdAndToUserId(userId, neighborId) .orElseThrow(() -> new BaseException(NOT_NEIGHBOR_ERROR));