From f65525f735beb86f297f99a4be017529f80a46c0 Mon Sep 17 00:00:00 2001 From: Lee SeungHeon <51286325+dev-Crayon@users.noreply.github.com> Date: Wed, 14 Feb 2024 18:01:12 +0900 Subject: [PATCH] Release (#88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat]: Refresh token redis에 저장 (#12) Redis 세팅 refresh token 저장 로직 구현 Related to: #11 * [Feat]: 토큰 재발급 API 구현 (#14) 기존의 AuthService 명칭 SocialService로 변경 토큰 재발급 API 구현 Related to: #13 * 로그아웃 API 구현 (#16) * [Feat]: 로그아웃 API 구현 ErrorCode 추가 SuccessCode 추가 Related to: #15 * [Feat]: 로그아웃 API 구현 Spring Security 권한 문제 수정 Related to: #15 * [Feat]: 로그아웃 API 구현 로그아웃 관련 코드 구현 Related to: #15 * 회원 탈퇴 API 구현 (#18) * [Feat]: 회원 탈퇴 API 구현 SuccessCode 작성 Related to: #17 * [Feat]: 회원 탈퇴 API 구현 탈퇴 로직 작성 Related to: #17 * [Hotfix]: 배포 환경에 H2 코드 주석처리 (#21) security config에 H2 주석처리 Related to: #20 * 닉네임 중복확인 API 구현 (#24) * [Feat]: 닉네임 중복확인 API 구현 Security 경로 추가 Related to: #23 * [Feat]: 닉네임 중복확인 API 구현 중복 확인 로직 구현 Related to: #23 * [Feat]: 약 추가 API 구현 (#27) * [Feat]: 약 추가 API 구현 Pill Entity 생성 PillRepository 생성 Related to: #26 * [Feat]: 약 추가 API 구현 Querydsl 세팅 Related to: #26 * [Fix]: 오탈자와 안쓰는 import 삭제 Related to: #26 * [Feat]: 약 추가 API 구현 API 반환 메세지 입력 BadRequest 예외처리 추가 Related to: #26 * [Feat]: 약 추가 API 구현 약 스케줄 Entity 구현 Related to: #26 * [Feat]: 약 추가 API 구현 약 추가 API 관련 로직 작성 관련 Util 함수 구현 Related to: #26 * [Setting]: Swagger JWT 인증 추가 (#29) SwaggerConfig 파일 변경 약 추가 예외 처리 구현 Related to: #28 * [Feat]: 약 개수 조회 API 구현 (#31) 내 약 개수 API 타인 약 개수 API Related to: #30 * Setting/#35 entity class (#36) * [Setting]: Notice Entity 객체 정의 Notice.java 구현 NoticeStatus.java NoticeType.java Related to: #35 * [Setting]: SendPill Entity 정의 Related to: #35 * [Setting]: Friend Entity 정의 Related to: #35 * [Fix]: H2 배포 코드에서 제외 Related to: #35 * [Setting]: Security Exception 정의 (#37) JWT 토큰 예외에 따른 처리 구현 Related to: #33 * 공유 요청 API 구현 (#39) * [Feat]: 공유 요청 API 구현 FriendController 생성 API 반환 메세지 입력 Related to: #34 * [Feat]: 공유 요청 API 구현 SendFriend Entity 생성 SendFriendRepository 생성 Related to: #34 * [Feat]: 공유 요청 API 구현 NoticeRepository 생성 공유 요청 API 로직 작성 Related to: #34 * [Update]: Swagger 메서드 설명 추가 Related to: #34 * [Update]: 변수명 변경 및 메서드 추출 friendName으로 변경 validateUser 메서드 분리 Related to: #34 * [Update]: 메서드명 변경 FriendQueryRepository생성 isAlreadyFriend로 메서드명 변경 Related to: #34 * [Feat]: 약 전송 API 구현 (#41) * [Feat]: 약 전송 API 구현 controller method 구현 service 구현 repository 구현 Related to: #38 * [Setting]: Security Exception 변경 print문 제거 Related to: #38 * [Feat]: 알림 리스트 조회 API (#45) * [Feat]: 알림 리스트 조회 API 관련 파일 생성 querydsl 메서드 생성 Related to: #42 * [Feat]: 알림 리스트 조회 API 테스트용 하드코딩 삭제 안쓰는 메서드 삭제 Related to: #42 * [Feat]: 알림 약 상세조회 API 구현 (#47) 관련 파일 생성 로직 작성 Related to: #46 * [Feat]: 공유 수락 API 구현 (#43) * [Feat]: 공유 수락 API 구현 공유 수락 API 구현 Related to: #40 * [Update]: 공유 요청 및 수락 예외처리 추가 5명 초과 시 예외처리 추가 Related to: #40 * [Update]: UserServiceUtil 적용 UserServiceUtil 적용 Related to: #40 * [Update]: RequestBody로 변경 RequestParam에서 RequestBody로 변경 Related to: #40 * [Feat]: 전달받은 약 수락 | 거절 API (#51) 관련 파일 생성 로직 작성 Related to: #48 * [Feat]: 약 삭제 API (#53) 약 삭제 로직 작성 Related to: #52 * [Feat]: 친구 리스트 조회 API 구현 (#50) * [Feat]: 친구 리스트 조회 API 구현 친구 리스트 조회 API 구현 Related to: #44 * [Fix]: @Transactional 추가 @Transactional 추가 Related to: #44 * [Feat]: 친구 이름 수정 API 구현 (#55) 친구 이름 수정 API 구현 Related to: #49 * [Feat]: 유저 닉네임 조회 API 구현 (#57) 유저 닉네임 조회 API 구현 Related to: #56 * [Feat]: 복용 체크 API 구현 (#60) 복용 체크 완료 API 구현 복용 체크 취소 API 구현 Related to: #58 * [Feat]: FCM 연동 및 로직 구현 (#62) 푸시 알림을 위한 FCM 관련 로직 작성 admin-sdk파일 등록 Related to: #59 * [Setting]: CD 워크플로우 업데이트 (#64) jar 파일에 firebase admin-sdk가 추가되도록 스크립트 추가 Related to: #63 * [Feat]: 닉네임 변경 API 구현 (#65) 닉네임 변경 관련 로직 구현 SuccessCode 추가 Related to: #54 * [Feat]: 내 약 리스트 조회 API (#67) 내 약 리스트 조회 API 로직 구현 Related to: #66 * [Feat]: 내 약 상세조회 API (#69) 약의 scheduleTime을 반환하는 querydsl 구현 관련 로직 작성 Related to: #68 * [Feat]: 친구 요청 여부 확인 API (#71) 관련 로직 작성 Related to: #70 * [Feat]: 스티커 전체조회 API (#73) 스티커 전체조회 API 구현 Related to: #61 * [Feat]: 스티커 전송 API 구현 (#75) LikeSchedule Entity 추가 스티커 전송 API 구현 Related to: #74 * [Feat]: 보낸 스티커 수정 API 구현 (#77) 보낸 스티커 수정 API 구현 Related to: #76 * [Feat]: 받은 스티커 전체 조회 API 구현 (#79) LikeScheduleQueryRepository 추가 받은 스티커 전체 조회 API 구현 Related to: #78 * [Feat]: 내 캘린더 조회 API (#81) controller, service, repositoy 로직 작성 날짜 계산 util 구현 Related to: #72 * [Feat]: 특정 일자 복약 일정 조회 (#83) * [Feat]: 특정 일자 복약 일정 조회 controller, service, repository 로직 구현 Related to: #82 * [Feat]: 특정 일자 복약 일정 조회 주석 코드 삭제 Related to: #82 * [Feat]: 친구 복약 일정 조회 API (#85) controller, service, repository 구현 Related to: #84 * [Feat]: 친구 특정 일자 복약 API 구현 (#87) controller, service, repository 구현 Related to: #86 * [CD]: 워크플로우에 firebase 추가 --------- Co-authored-by: Suhyeon <70002218+onpyeong@users.noreply.github.com> --- .github/workflows/CD-workflow.yml | 9 + .gitignore | 5 +- build.gradle | 3 + .../auth/application/UserService.java | 40 ++++ .../application/util/UserServiceUtil.java | 23 +++ .../io/sobok/SobokSobok/auth/domain/User.java | 4 + .../auth/infrastructure/UserRepository.java | 2 + .../SobokSobok/auth/ui/UserController.java | 42 ++++ .../auth/ui/dto/UsernameRequest.java | 10 + .../auth/ui/dto/UsernameResponse.java | 14 ++ .../io/sobok/SobokSobok/config/FCMConfig.java | 47 +++++ .../SobokSobok/config/SecurityConfig.java | 7 +- .../sobok/SobokSobok/exception/ErrorCode.java | 29 +++ .../SobokSobok/exception/SuccessCode.java | 32 +++ .../exception/model/ForbiddenException.java | 10 + .../firebase/FCMNotificationService.java | 51 +++++ .../firebase/dto/FCMNotificationRequest.java | 16 ++ .../friend/application/FriendService.java | 185 ++++++++++++++++++ .../SobokSobok/friend/domain/Friend.java | 40 ++++ .../SobokSobok/friend/domain/SendFriend.java | 36 ++++ .../infrastructure/FriendQueryRepository.java | 24 +++ .../infrastructure/FriendRepository.java | 12 ++ .../infrastructure/SendFriendRepository.java | 8 + .../friend/ui/FriendController.java | 124 ++++++++++++ .../friend/ui/dto/AddFriendRequest.java | 12 ++ .../friend/ui/dto/AddFriendResponse.java | 14 ++ .../friend/ui/dto/FriendListResponse.java | 12 ++ .../friend/ui/dto/HandleFriendRequest.java | 11 ++ .../ui/dto/HandleFriendRequestResponse.java | 15 ++ .../friend/ui/dto/UpdateFriendName.java | 10 + .../ui/dto/UpdateFriendNameResponse.java | 13 ++ .../notice/application/NoticeService.java | 105 ++++++++++ .../notice/application/NoticeServiceUtil.java | 17 ++ .../SobokSobok/notice/domain/Notice.java | 58 ++++++ .../notice/domain/NoticeStatus.java | 14 ++ .../SobokSobok/notice/domain/NoticeType.java | 14 ++ .../infrastructure/NoticeQueryRepository.java | 105 ++++++++++ .../infrastructure/NoticeRepository.java | 14 ++ .../notice/ui/NoticeController.java | 79 ++++++++ .../ui/dto/CompletePillNoticeRequest.java | 13 ++ .../SobokSobok/notice/ui/dto/NoticeInfo.java | 19 ++ .../notice/ui/dto/NoticeResponse.java | 13 ++ .../ui/dto/ReceivePillInfoResponse.java | 17 ++ .../pill/application/PillScheduleService.java | 101 ++++++++++ .../application/PillScheduleServiceUtil.java | 17 ++ .../pill/application/PillService.java | 122 +++++++++++- .../pill/application/PillServiceUtil.java | 23 +++ .../io/sobok/SobokSobok/pill/domain/Pill.java | 9 + .../SobokSobok/pill/domain/PillSchedule.java | 4 + .../SobokSobok/pill/domain/SendPill.java | 32 +++ .../pill/infrastructure/PillRepository.java | 5 + .../PillScheduleQueryRepository.java | 162 +++++++++++++++ .../PillScheduleRepository.java | 2 + .../infrastructure/SendPillRepository.java | 11 ++ .../SobokSobok/pill/ui/PillController.java | 73 ++++++- .../pill/ui/PillScheduleController.java | 134 +++++++++++++ .../ui/dto/CheckPillScheduleResponse.java | 16 ++ .../pill/ui/dto/DateScheduleResponse.java | 14 ++ .../pill/ui/dto/MonthScheduleResponse.java | 15 ++ .../pill/ui/dto/PillListResponse.java | 12 ++ .../SobokSobok/pill/ui/dto/PillResponse.java | 17 ++ .../pill/ui/dto/PillScheduleInfo.java | 19 ++ .../filter/ExceptionHandlerFilter.java | 53 +++++ .../{jwt => filter}/JwtCustomFilter.java | 3 +- .../SobokSobok/security/jwt/JwtProvider.java | 20 +- .../sticker/application/StickerService.java | 128 ++++++++++++ .../application/StickerServiceUtil.java | 17 ++ .../sticker/domain/LikeSchedule.java | 49 +++++ .../SobokSobok/sticker/domain/Sticker.java | 26 +++ .../LikeScheduleQueryRepository.java | 44 +++++ .../LikeScheduleRepository.java | 9 + .../infrastructure/StickerRepository.java | 8 + .../sticker/ui/StickerController.java | 99 ++++++++++ .../ui/dto/ReceivedStickerResponse.java | 12 ++ .../sticker/ui/dto/StickerActionResponse.java | 16 ++ .../sticker/ui/dto/StickerResponse.java | 11 ++ .../io/sobok/SobokSobok/utils/DateUtil.java | 14 ++ 77 files changed, 2599 insertions(+), 26 deletions(-) create mode 100644 src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java create mode 100644 src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameRequest.java create mode 100644 src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/config/FCMConfig.java create mode 100644 src/main/java/io/sobok/SobokSobok/exception/model/ForbiddenException.java create mode 100644 src/main/java/io/sobok/SobokSobok/external/firebase/FCMNotificationService.java create mode 100644 src/main/java/io/sobok/SobokSobok/external/firebase/dto/FCMNotificationRequest.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/application/FriendService.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/domain/Friend.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/domain/SendFriend.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendQueryRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/infrastructure/SendFriendRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/FriendController.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendRequest.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/dto/FriendListResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequest.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequestResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendName.java create mode 100644 src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendNameResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/application/NoticeService.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/application/NoticeServiceUtil.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/domain/Notice.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/domain/NoticeStatus.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/domain/NoticeType.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeQueryRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/ui/NoticeController.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/ui/dto/CompletePillNoticeRequest.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeInfo.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/notice/ui/dto/ReceivePillInfoResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleService.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleServiceUtil.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/application/PillServiceUtil.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/domain/SendPill.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleQueryRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/infrastructure/SendPillRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/ui/PillScheduleController.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/ui/dto/CheckPillScheduleResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/ui/dto/DateScheduleResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/ui/dto/MonthScheduleResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillListResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillScheduleInfo.java create mode 100644 src/main/java/io/sobok/SobokSobok/security/filter/ExceptionHandlerFilter.java rename src/main/java/io/sobok/SobokSobok/security/{jwt => filter}/JwtCustomFilter.java (94%) create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/application/StickerServiceUtil.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/domain/LikeSchedule.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleQueryRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/infrastructure/StickerRepository.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/ui/dto/ReceivedStickerResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerActionResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerResponse.java create mode 100644 src/main/java/io/sobok/SobokSobok/utils/DateUtil.java diff --git a/.github/workflows/CD-workflow.yml b/.github/workflows/CD-workflow.yml index fade1ed..beca81b 100644 --- a/.github/workflows/CD-workflow.yml +++ b/.github/workflows/CD-workflow.yml @@ -31,6 +31,15 @@ jobs: # application.yml 파일 생성 touch ./application.yml + # firebase 폴더 생성 + mkdir firebase + + # firebase admin-sdk 파일 생성 + touch ./firebase/sobok-76d0a-firebase-adminsdk-qb2ez-cedea5e056.json + + # Github-Actions 에서 설정한 값을 json 파일에 입력 + echo "${{ secrets.SOBOKSOBOK_FIREBASE }}" >> ./firebase/sobok-76d0a-firebase-adminsdk-qb2ez-cedea5e056.json + # GitHub-Actions 에서 설정한 값을 application.yml 파일에 쓰기 echo "${{ secrets.SOBOKSOBOK_DEPLOY }}" >> ./application.yml diff --git a/.gitignore b/.gitignore index 3596642..1feef82 100644 --- a/.gitignore +++ b/.gitignore @@ -178,4 +178,7 @@ gradle-app.setting # Environment Variables application-local.yml application-dev.yml -application-prod.yml \ No newline at end of file +application-prod.yml + +# firebase admin sdk +sobok-76d0a-firebase-adminsdk-qb2ez-cedea5e056.json \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0424d60..e1a6dc3 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,9 @@ dependencies { implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' + // firebase + implementation 'com.google.firebase:firebase-admin:9.2.0' + // Swagger implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.3.0' diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java b/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java index d9ee157..375af2b 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java +++ b/src/main/java/io/sobok/SobokSobok/auth/application/UserService.java @@ -1,7 +1,15 @@ package io.sobok.SobokSobok.auth.application; +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; import io.sobok.SobokSobok.auth.domain.User; import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.auth.ui.dto.UsernameResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.ConflictException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,4 +25,36 @@ public Boolean duplicateNickname(String username) { return userRepository.existsByUsername(username); } + + @Transactional + public List getUsername(Long userId, String username) { + UserServiceUtil.existsUserById(userRepository, userId); + + Optional optionalMember = userRepository.findByUsername(username); + + List result = new ArrayList<>(); + + optionalMember.ifPresent( + member -> result.add(UsernameResponse.builder() + .memberId(member.getId()) + .memberName(member.getUsername()) + .deviceOS(member.getSocialInfo().getSocialType()) + .selfCheck(member.getId().equals(userId)) + .build()) + ); + + return result; + } + + @Transactional + public void changeUsername(Long userId, String username) { + + User user = UserServiceUtil.findUserById(userRepository, userId); + + if (duplicateNickname(username)) { + throw new ConflictException(ErrorCode.ALREADY_USING_USERNAME); + } + + user.changeUsername(username); + } } diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java b/src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java new file mode 100644 index 0000000..dbd1021 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/auth/application/util/UserServiceUtil.java @@ -0,0 +1,23 @@ +package io.sobok.SobokSobok.auth.application.util; + +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserServiceUtil { + + public static User findUserById(UserRepository userRepository, Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_USER)); + } + + public static void existsUserById(UserRepository userRepository, Long id) { + if (!userRepository.existsById(id)) { + throw new NotFoundException(ErrorCode.UNREGISTERED_USER); + } + } +} diff --git a/src/main/java/io/sobok/SobokSobok/auth/domain/User.java b/src/main/java/io/sobok/SobokSobok/auth/domain/User.java index 9bff590..17e5ced 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/domain/User.java +++ b/src/main/java/io/sobok/SobokSobok/auth/domain/User.java @@ -58,6 +58,10 @@ public void updateDeviceToken(String newDeviceToken) { this.deviceToken = newDeviceToken; } + public void changeUsername(String username) { + this.username = username; + } + public void deleteUser() { this.deviceToken = ""; this.username = ""; diff --git a/src/main/java/io/sobok/SobokSobok/auth/infrastructure/UserRepository.java b/src/main/java/io/sobok/SobokSobok/auth/infrastructure/UserRepository.java index 90f3e8b..74763ad 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/infrastructure/UserRepository.java +++ b/src/main/java/io/sobok/SobokSobok/auth/infrastructure/UserRepository.java @@ -13,4 +13,6 @@ public interface UserRepository extends JpaRepository { Boolean existsBySocialInfoSocialId(String socialId); Boolean existsByUsername(String username); + + Optional findByUsername(String username); } diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/UserController.java b/src/main/java/io/sobok/SobokSobok/auth/ui/UserController.java index 72e4c48..8e8ddd0 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/ui/UserController.java +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/UserController.java @@ -1,13 +1,20 @@ package io.sobok.SobokSobok.auth.ui; import io.sobok.SobokSobok.auth.application.UserService; +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.auth.ui.dto.UsernameRequest; +import io.sobok.SobokSobok.auth.ui.dto.UsernameResponse; import io.sobok.SobokSobok.common.dto.ApiResponse; import io.sobok.SobokSobok.exception.SuccessCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; + +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -32,4 +39,39 @@ public ResponseEntity> isNicknameDuplicate(@RequestParam fi userService.duplicateNickname(username) )); } + + @GetMapping("/search") + @Operation( + summary = "유저 닉네임 조회 API 메서드", + description = "공유 멤버(유저) 닉네임을 조회하는 메서드입니다." + ) + public ResponseEntity>> getUsername( + @AuthenticationPrincipal User user, + @RequestParam final String username + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_USERNAME_SUCCESS, + userService.getUsername(user.getId(), username) + )); + } + + @PutMapping("/nickname") + @Operation( + summary = "유저 닉네임 변경 API 메서드", + description = "유저 본인의 닉네임을 변경하는 메서드입니다." + ) + public ResponseEntity> changeUsername( + @AuthenticationPrincipal User user, + @RequestBody @Valid final UsernameRequest request + ) { + + userService.changeUsername(user.getId(), request.username()); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.CHANGE_NICKNAME_SUCCESS + )); + } } diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameRequest.java b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameRequest.java new file mode 100644 index 0000000..44c05e4 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameRequest.java @@ -0,0 +1,10 @@ +package io.sobok.SobokSobok.auth.ui.dto; + +import jakarta.validation.constraints.NotBlank; + +public record UsernameRequest( + + @NotBlank + String username +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameResponse.java b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameResponse.java new file mode 100644 index 0000000..ab8b86c --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/UsernameResponse.java @@ -0,0 +1,14 @@ +package io.sobok.SobokSobok.auth.ui.dto; + +import io.sobok.SobokSobok.auth.domain.SocialType; +import lombok.Builder; + +@Builder +public record UsernameResponse( + Long memberId, + String memberName, + SocialType deviceOS, + Boolean selfCheck +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/config/FCMConfig.java b/src/main/java/io/sobok/SobokSobok/config/FCMConfig.java new file mode 100644 index 0000000..7e91d39 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/config/FCMConfig.java @@ -0,0 +1,47 @@ +package io.sobok.SobokSobok.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +@Configuration +public class FCMConfig { + + @Value("${firebase.admin-sdk}") + String adminSdkFileName; + + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + ClassPathResource resource = new ClassPathResource("firebase/" + adminSdkFileName); + + InputStream refreshToken = resource.getInputStream(); + + FirebaseApp firebaseApp = null; + List firebaseAppList = FirebaseApp.getApps(); + + if (firebaseAppList != null && !firebaseAppList.isEmpty()) { + for (FirebaseApp app : firebaseAppList) { + if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) { + firebaseApp = app; + } + } + } else { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(refreshToken)) + .build(); + + firebaseApp = FirebaseApp.initializeApp(options); + } + + return FirebaseMessaging.getInstance(firebaseApp); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java b/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java index 6871745..efaef6f 100644 --- a/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java +++ b/src/main/java/io/sobok/SobokSobok/config/SecurityConfig.java @@ -1,6 +1,7 @@ package io.sobok.SobokSobok.config; -import io.sobok.SobokSobok.security.jwt.JwtCustomFilter; +import io.sobok.SobokSobok.security.filter.ExceptionHandlerFilter; +import io.sobok.SobokSobok.security.filter.JwtCustomFilter; import io.sobok.SobokSobok.security.jwt.JwtProvider; import lombok.RequiredArgsConstructor; //import org.springframework.boot.autoconfigure.security.servlet.PathRequest; @@ -62,6 +63,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti new JwtCustomFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class ) + .addFilterBefore( + new ExceptionHandlerFilter(), + JwtCustomFilter.class + ) ; return http.build(); diff --git a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java index bcf2806..35d2923 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java +++ b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java @@ -12,6 +12,7 @@ public enum ErrorCode { INVALID_REQUEST_BODY(HttpStatus.BAD_REQUEST, "잘못된 Request body입니다."), BAD_REQUEST_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 형식의 요청입니다."), FILE_SAVE_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "파일 생성에 실패했습니다."), + FORBIDDEN_EXCEPTION(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."), // auth UNREGISTERED_USER(HttpStatus.NOT_FOUND, "등록되지 않은 사용자입니다."), @@ -19,13 +20,41 @@ public enum ErrorCode { NOT_LOGGED_IN_USER(HttpStatus.NOT_FOUND, "로그인되지 않은 사용자입니다."), ALREADY_EXISTS_USER(HttpStatus.CONFLICT, "이미 회원가입이 완료된 사용자입니다."), ALREADY_USING_USERNAME(HttpStatus.CONFLICT, "이미 사용중인 username입니다."), + EMPTY_DEVICE_TOKEN(HttpStatus.NOT_FOUND, "디바이스 토큰이 존재하지 않습니다."), + + // jwt + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), + MALFORMED_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 형식의 토큰입니다"), + NULL_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다."), // pill EXCEEDED_PILL_COUNT(HttpStatus.BAD_REQUEST, "약 개수가 초과됐습니다."), INVALID_PILL_REQUEST_DATA(HttpStatus.BAD_REQUEST, "허용되지 않은 약 추가 요청 데이터입니다."), + UNREGISTERED_PILL(HttpStatus.NOT_FOUND, "등록되지 않은 약입니다."), + UNAUTHORIZED_PILL(HttpStatus.FORBIDDEN, "접근 권한이 없는 약입니다."), + NOT_SEND_PILL(HttpStatus.NOT_FOUND, "전송된 적이 없는 약입니다."), + UNREGISTERED_PILL_SCHEDULE(HttpStatus.NOT_FOUND, "등록되지 않은 약 일정입니다."), + UNCONSUMED_PILL(HttpStatus.BAD_REQUEST, "복용하지 않은 약입니다."), + + + // friend + INVALID_SELF_ADD_FRIEND(HttpStatus.BAD_REQUEST, "자신에게 캘린더 공유 요청을 할 수 없습니다."), + ALREADY_FRIEND(HttpStatus.CONFLICT, "이미 캘린더 공유 요청이 되었습니다."), + EXCEEDED_FRIEND_COUNT(HttpStatus.CONFLICT, "친구 수가 초과됐습니다."), + NOT_FRIEND(HttpStatus.FORBIDDEN, "친구 관계가 아닙니다."), + + // notice + NON_EXISTS_NOTICE(HttpStatus.NOT_FOUND, "존재하지 않는 알림입니다."), + NOT_PILL_NOTICE(HttpStatus.BAD_REQUEST, "약 정보 알림이 아닙니다."), + ALREADY_COMPLETE_NOTICE(HttpStatus.BAD_REQUEST, "이미 처리된 알림입니다."), // external INVALID_EXTERNAL_REQUEST_DATA(HttpStatus.BAD_REQUEST, "외부 API 요청에 잘못된 데이터가 전달됐습니다."), + + //sticker + UNREGISTERED_STICKER(HttpStatus.NOT_FOUND, "등록되지 않은 스티커입니다."), + ALREADY_SEND_STICKER(HttpStatus.CONFLICT, "이미 스티커를 전송했습니다."), + UNREGISTERED_LIKE_SCHEDULE(HttpStatus.NOT_FOUND, "스티커 전송기록이 존재하지 않습니다."), ; private final HttpStatus code; diff --git a/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java b/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java index 716a924..6b3a602 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java +++ b/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java @@ -17,10 +17,42 @@ public enum SuccessCode { // user NICKNAME_CHECK_SUCCESS(HttpStatus.OK, "닉네임 중복 확인에 성공했습니다."), + CHANGE_NICKNAME_SUCCESS(HttpStatus.OK, "닉네임 변경에 성공했습니다."), + GET_USERNAME_SUCCESS(HttpStatus.OK, "유저 이름 조회에 성공했습니다."), // pill ADD_PILL_SUCCESS(HttpStatus.CREATED, "약 추가에 성공했습니다."), GET_PILL_COUNT_SUCCESS(HttpStatus.OK, "약 개수 조회에 성공했습니다."), + GET_PILL_LIST_SUCCESS(HttpStatus.OK, "약 리스트 조회에 성공했습니다."), + GET_PILL_INFO_SUCCESS(HttpStatus.OK, "약 정보 조회에 성공했습니다."), + SEND_PILL_SUCCESS(HttpStatus.CREATED, "약 전송에 성공했습니다."), + DELETE_PILL_SUCCESS(HttpStatus.OK, "약 삭제에 성공했습니다."), + + // schedule + CHECK_PILL_SCHEDULE_SUCCESS(HttpStatus.OK, "복용 완료 체크에 성공했습니다."), + UNCHECK_PILL_SCHEDULE_SUCCESS(HttpStatus.OK, "복용 체크 취소에 성공했습니다."), + GET_MONTH_SCHEDULE_SUCCESS(HttpStatus.OK, "월 스케줄 조회에 성공했습니다"), + GET_DATE_SCHEDULE_SUCCESS(HttpStatus.OK, "일 스케줄 조회에 성공했습니다."), + GET_FRIEND_MONTH_SCHEDULE_SUCCESS(HttpStatus.OK, "친구 월 스케줄 조회에 성공했습니다."), + GET_FRIEND_DATE_SCHEDULE_SUCCESS(HttpStatus.OK, "친구 일 스케줄 조회에 성공했습니다."), + + // friend + ADD_FRIEND_SUCCESS(HttpStatus.OK, "공유 요청에 성공했습니다."), + GET_FRIEND_LIST_SUCCESS(HttpStatus.OK, "친구 리스트 조회에 성공했습니다."), + HANDLE_FRIEND_REQUEST_SUCCESS(HttpStatus.OK, "공유 응답에 성공했습니다."), + UPDATE_FRIEND_NAME_SUCCESS(HttpStatus.OK, "멤버 이름 수정에 성공했습니다."), + GET_REQUEST_FRIEND_SUCCESS(HttpStatus.OK, "친구 요청 여부 조회에 성공했습니다."), + + // notice + GET_NOTICE_LIST_SUCCESS(HttpStatus.OK, "알림 리스트 조회에 성공했습니다."), + GET_RECEIVE_PILL_INFO_SUCCESS(HttpStatus.OK, "전달받은 약 정보 조회에 성공했습니다."), + COMPLETE_PILL_NOTICE(HttpStatus.OK, "약 알림 처리를 완료했습니다."), + + //sticker + GET_STICKER_LIST_SUCCESS(HttpStatus.OK, "스티커 전체 조회에 성공했습니다."), + SEND_STICKER_SUCCESS(HttpStatus.OK, "스티커 전송에 성공했습니다."), + UPDATE_STICKER_SUCCESS(HttpStatus.OK, "보낸 스티커 수정에 성공했습니다."), + GET_RECEIVED_STICKER_SUCCESS(HttpStatus.OK, "받은 스티커 전체 조회에 성공했습니다."), ; private final HttpStatus code; diff --git a/src/main/java/io/sobok/SobokSobok/exception/model/ForbiddenException.java b/src/main/java/io/sobok/SobokSobok/exception/model/ForbiddenException.java new file mode 100644 index 0000000..0213ba1 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/exception/model/ForbiddenException.java @@ -0,0 +1,10 @@ +package io.sobok.SobokSobok.exception.model; + +import io.sobok.SobokSobok.exception.ErrorCode; + +public class ForbiddenException extends SobokException { + + public ForbiddenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/external/firebase/FCMNotificationService.java b/src/main/java/io/sobok/SobokSobok/external/firebase/FCMNotificationService.java new file mode 100644 index 0000000..47f3aaa --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/external/firebase/FCMNotificationService.java @@ -0,0 +1,51 @@ +package io.sobok.SobokSobok.external.firebase; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.external.firebase.dto.FCMNotificationRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class FCMNotificationService { + + private final FirebaseMessaging firebaseMessaging; + private final UserRepository userRepository; + + public void sendNotificationByDeviceToken(FCMNotificationRequest request) { + + User user = UserServiceUtil.findUserById(userRepository, request.userId()); + + if (user.getDeviceToken() == null) { + throw new NotFoundException(ErrorCode.EMPTY_DEVICE_TOKEN); + } + + Notification notification = Notification.builder() + .setTitle(request.title()) + .setBody(request.body()) + .setImage(request.image()) + .build(); + + Message message = Message.builder() + .setToken(user.getDeviceToken()) + .setNotification(notification) + .putAllData(request.data()) + .build(); + + try { + firebaseMessaging.send(message); + } catch (FirebaseMessagingException e) { + log.error("푸시알림 전송에 실패했습니다. userId: " + user.getId() + "\n" + e.getMessage());; + } + } +} diff --git a/src/main/java/io/sobok/SobokSobok/external/firebase/dto/FCMNotificationRequest.java b/src/main/java/io/sobok/SobokSobok/external/firebase/dto/FCMNotificationRequest.java new file mode 100644 index 0000000..766dd61 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/external/firebase/dto/FCMNotificationRequest.java @@ -0,0 +1,16 @@ +package io.sobok.SobokSobok.external.firebase.dto; + +import lombok.Builder; + +import java.util.Map; + +@Builder +public record FCMNotificationRequest( + + Long userId, + String title, + String body, + String image, + Map data +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/application/FriendService.java b/src/main/java/io/sobok/SobokSobok/friend/application/FriendService.java new file mode 100644 index 0000000..9447652 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/application/FriendService.java @@ -0,0 +1,185 @@ +package io.sobok.SobokSobok.friend.application; + +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.BadRequestException; +import io.sobok.SobokSobok.exception.model.ConflictException; +import io.sobok.SobokSobok.exception.model.ForbiddenException; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.friend.domain.Friend; +import io.sobok.SobokSobok.friend.domain.SendFriend; +import io.sobok.SobokSobok.friend.infrastructure.FriendQueryRepository; +import io.sobok.SobokSobok.friend.infrastructure.FriendRepository; +import io.sobok.SobokSobok.friend.infrastructure.SendFriendRepository; +import io.sobok.SobokSobok.friend.ui.dto.AddFriendRequest; +import io.sobok.SobokSobok.friend.ui.dto.AddFriendResponse; +import io.sobok.SobokSobok.friend.ui.dto.FriendListResponse; +import io.sobok.SobokSobok.friend.ui.dto.HandleFriendRequest; +import io.sobok.SobokSobok.friend.ui.dto.HandleFriendRequestResponse; +import io.sobok.SobokSobok.friend.ui.dto.UpdateFriendName; +import io.sobok.SobokSobok.friend.ui.dto.UpdateFriendNameResponse; +import io.sobok.SobokSobok.notice.domain.Notice; +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import io.sobok.SobokSobok.notice.domain.NoticeType; +import io.sobok.SobokSobok.notice.infrastructure.NoticeQueryRepository; +import io.sobok.SobokSobok.notice.infrastructure.NoticeRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class FriendService { + + private final UserRepository userRepository; + private final NoticeRepository noticeRepository; + private final SendFriendRepository sendFriendRepository; + private final FriendRepository friendRepository; + private final NoticeQueryRepository noticeQueryRepository; + private final FriendQueryRepository friendQueryRepository; + + @Transactional + public AddFriendResponse addFriend(Long userId, AddFriendRequest request) { + User sender = UserServiceUtil.findUserById(userRepository, userId); + + if (sender.getId().equals(request.memberId())) { + throw new BadRequestException(ErrorCode.INVALID_SELF_ADD_FRIEND); + } + + User receiver = UserServiceUtil.findUserById(userRepository, request.memberId()); + + if (friendRepository.countBySenderId(sender.getId()) >= 5 || + friendRepository.countBySenderId(receiver.getId()) >= 5) { + throw new ConflictException(ErrorCode.EXCEEDED_FRIEND_COUNT); + } + + if (noticeQueryRepository.isAlreadyFriendRequestFromSender(sender.getId(), receiver.getId()) + || noticeQueryRepository.isAlreadyFriendRequestFromSender(receiver.getId(), + sender.getId())) { + throw new ConflictException(ErrorCode.ALREADY_FRIEND); + } + + Notice notice = noticeRepository.save( + Notice.newInstance( + sender.getId(), + receiver.getId(), + NoticeType.FRIEND, + NoticeStatus.WAITING + ) + ); + + sendFriendRepository.save( + SendFriend.newInstance( + notice.getId(), + request.friendName() + ) + ); + + return AddFriendResponse.builder() + .noticeId(notice.getId()) + .senderName(sender.getUsername()) + .memberName(receiver.getUsername()) + .isOkay(NoticeStatus.WAITING) + .build(); + } + + @Transactional(readOnly = true) + public List getFriendList(Long userId) { + UserServiceUtil.existsUserById(userRepository, userId); + + return friendRepository.findAllBySenderId(userId) + .stream().map(friend -> + FriendListResponse.builder() + .friendId(friend.getId()) + .memberId(friend.getReceiverId()) + .friendName(friend.getFriendName()) + .build() + ).collect(Collectors.toList()); + } + + @Transactional(noRollbackFor = {ConflictException.class}) + public HandleFriendRequestResponse updateNoticeStatus(Long userId, Long noticeId, + HandleFriendRequest request) { + UserServiceUtil.existsUserById(userRepository, userId); + + Notice notice = noticeRepository.findById(noticeId) + .orElseThrow(() -> new BadRequestException(ErrorCode.BAD_REQUEST_EXCEPTION)); + + if (!userId.equals(notice.getReceiverId())) { + throw new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION); + } + + User sender = UserServiceUtil.findUserById(userRepository, notice.getSenderId()); + + if (friendRepository.countBySenderId(userId) >= 5 || + friendRepository.countBySenderId(sender.getId()) >= 5) { + notice.setIsOkay(NoticeStatus.REFUSE); + throw new ConflictException(ErrorCode.EXCEEDED_FRIEND_COUNT); + } + + notice.setIsOkay(request.isOkay()); + + if (request.isOkay() == NoticeStatus.ACCEPT) { + SendFriend sendFriend = sendFriendRepository.findByNoticeId(noticeId); + friendRepository.save(Friend.newInstance( + sender.getId(), + userId, + sendFriend.getFriendName() + )); + + friendRepository.save(Friend.newInstance( + userId, + sender.getId(), + sender.getUsername() + )); + } + + return HandleFriendRequestResponse.builder() + .noticeId(notice.getId()) + .memberName(sender.getUsername()) + .isOkay(request.isOkay()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + @Transactional + public UpdateFriendNameResponse updateFriendName(Long userId, Long friendId, UpdateFriendName request) { + UserServiceUtil.existsUserById(userRepository, userId); + + Friend friend = friendRepository.findById(friendId) + .orElseThrow(() -> new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION)); + + if (!friend.getSenderId().equals(userId)) { + throw new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION); + } + + friend.updateFriendName(request.friendName()); + + return UpdateFriendNameResponse.builder() + .friendId(friendId) + .userId(userId) + .memberId(friend.getReceiverId()) + .friendName(request.friendName()) + .build(); + } + + @Transactional + public Boolean checkFriendRequest(Long userId, Long friendId) { + + boolean AlreadyFriendRequest = false; + + UserServiceUtil.existsUserById(userRepository, userId); + UserServiceUtil.existsUserById(userRepository, friendId); + + if (friendQueryRepository.isAlreadyFriend(userId, friendId) || noticeRepository.existsBySenderIdAndReceiverIdAndIsOkay(userId, friendId, NoticeStatus.WAITING)) { + AlreadyFriendRequest = true; + } + + return AlreadyFriendRequest; + } +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/domain/Friend.java b/src/main/java/io/sobok/SobokSobok/friend/domain/Friend.java new file mode 100644 index 0000000..fb698e8 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/domain/Friend.java @@ -0,0 +1,40 @@ +package io.sobok.SobokSobok.friend.domain; + +import io.sobok.SobokSobok.common.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Friend extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long senderId; + + @Column(nullable = false) + private Long receiverId; + + @Column(nullable = false) + private String friendName; + + private Friend(Long senderId, Long receiverId, String friendName) { + this.senderId = senderId; + this.receiverId = receiverId; + this.friendName = friendName; + } + + public static Friend newInstance(Long senderId, Long receiverId, String friendName) { + return new Friend(senderId, receiverId, friendName); + } + + public void updateFriendName(String friendName) { + this.friendName = friendName; + } +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/domain/SendFriend.java b/src/main/java/io/sobok/SobokSobok/friend/domain/SendFriend.java new file mode 100644 index 0000000..7b8e714 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/domain/SendFriend.java @@ -0,0 +1,36 @@ +package io.sobok.SobokSobok.friend.domain; + +import io.sobok.SobokSobok.common.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SendFriend extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long noticeId; + + @Column(nullable = false) + private String friendName; + + private SendFriend(Long noticeId, String friendName) { + this.noticeId = noticeId; + this.friendName = friendName; + } + + public static SendFriend newInstance(Long noticeId, String friendName) { + return new SendFriend(noticeId, friendName); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendQueryRepository.java b/src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendQueryRepository.java new file mode 100644 index 0000000..9d61da4 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendQueryRepository.java @@ -0,0 +1,24 @@ +package io.sobok.SobokSobok.friend.infrastructure; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import io.sobok.SobokSobok.friend.domain.QFriend; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FriendQueryRepository { + + private final JPAQueryFactory queryFactory; + + public Boolean isAlreadyFriend(Long senderId, Long receiverId) { + QFriend friend = QFriend.friend; + + return queryFactory + .selectFrom(friend) + .where( + friend.senderId.eq(senderId), + friend.receiverId.eq(receiverId) + ).fetchFirst() != null; + } +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendRepository.java b/src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendRepository.java new file mode 100644 index 0000000..899558b --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/infrastructure/FriendRepository.java @@ -0,0 +1,12 @@ +package io.sobok.SobokSobok.friend.infrastructure; + +import io.sobok.SobokSobok.friend.domain.Friend; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FriendRepository extends JpaRepository { + + List findAllBySenderId(Long senderId); + + Integer countBySenderId(Long senderId); +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/infrastructure/SendFriendRepository.java b/src/main/java/io/sobok/SobokSobok/friend/infrastructure/SendFriendRepository.java new file mode 100644 index 0000000..4193758 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/infrastructure/SendFriendRepository.java @@ -0,0 +1,8 @@ +package io.sobok.SobokSobok.friend.infrastructure; + +import io.sobok.SobokSobok.friend.domain.SendFriend; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SendFriendRepository extends JpaRepository { + SendFriend findByNoticeId(Long noticeId); +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/FriendController.java b/src/main/java/io/sobok/SobokSobok/friend/ui/FriendController.java new file mode 100644 index 0000000..ebf8a20 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/FriendController.java @@ -0,0 +1,124 @@ +package io.sobok.SobokSobok.friend.ui; + +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.common.dto.ApiResponse; +import io.sobok.SobokSobok.exception.SuccessCode; +import io.sobok.SobokSobok.friend.application.FriendService; +import io.sobok.SobokSobok.friend.ui.dto.AddFriendRequest; +import io.sobok.SobokSobok.friend.ui.dto.AddFriendResponse; +import io.sobok.SobokSobok.friend.ui.dto.FriendListResponse; +import io.sobok.SobokSobok.friend.ui.dto.HandleFriendRequest; +import io.sobok.SobokSobok.friend.ui.dto.HandleFriendRequestResponse; +import io.sobok.SobokSobok.friend.ui.dto.UpdateFriendName; +import io.sobok.SobokSobok.friend.ui.dto.UpdateFriendNameResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/group") +@Tag(name = "Group", description = "공유 관련 컨트롤러") +public class FriendController { + + final FriendService friendService; + + @PostMapping("") + @Operation( + summary = "공유 요청 API 메서드", + description = "캘린더 공유를 요청하는 메서드입니다." + ) + public ResponseEntity> addFriend( + @AuthenticationPrincipal User user, + @RequestBody @Valid final AddFriendRequest request + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.ADD_FRIEND_SUCCESS, + friendService.addFriend(user.getId(), request) + )); + } + + @PutMapping("/{noticeId}") + @Operation( + summary = "공유 수락 API 메서드", + description = "캘린더 공유를 수락 혹은 거절하는 메서드입니다." + ) + public ResponseEntity> handleFriendRequest( + @AuthenticationPrincipal User user, + @PathVariable Long noticeId, + @RequestBody @Valid HandleFriendRequest request + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.HANDLE_FRIEND_REQUEST_SUCCESS, + friendService.updateNoticeStatus(user.getId(), noticeId, request) + )); + } + + @GetMapping("") + @Operation( + summary = "친구 리스트 조회 API 메서드", + description = "친구 리스트를 조회하는 메서드입니다." + ) + public ResponseEntity>> getFriendList( + @AuthenticationPrincipal User user + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_FRIEND_LIST_SUCCESS, + friendService.getFriendList(user.getId()) + )); + } + + @PutMapping("/{friendId}/name") + @Operation( + summary = "공유 친구 이름 수정 API 메서드", + description = "친구 이름을 수정하는 메서드입니다." + ) + public ResponseEntity> updateFriendName( + @AuthenticationPrincipal User user, + @PathVariable Long friendId, + @RequestBody @Valid UpdateFriendName request + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.UPDATE_FRIEND_NAME_SUCCESS, + friendService.updateFriendName(user.getId(), friendId, request) + )); + } + + @GetMapping("/request/{friendId}") + @Operation( + summary = "친구 신청 여부 확인 API 메서드", + description = "친구 신청을 했는지 확인하는 메서드입니다." + ) + public ResponseEntity> checkFriendRequest( + @AuthenticationPrincipal User user, + @PathVariable Long friendId + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_REQUEST_FRIEND_SUCCESS, + friendService.checkFriendRequest(user.getId(), friendId) + )); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendRequest.java b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendRequest.java new file mode 100644 index 0000000..9ec75cf --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendRequest.java @@ -0,0 +1,12 @@ +package io.sobok.SobokSobok.friend.ui.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record AddFriendRequest( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + Long memberId, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + String friendName +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendResponse.java b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendResponse.java new file mode 100644 index 0000000..7bf7de3 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/AddFriendResponse.java @@ -0,0 +1,14 @@ +package io.sobok.SobokSobok.friend.ui.dto; + +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import lombok.Builder; + +@Builder +public record AddFriendResponse( + Long noticeId, + String senderName, + String memberName, + NoticeStatus isOkay +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/dto/FriendListResponse.java b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/FriendListResponse.java new file mode 100644 index 0000000..0727185 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/FriendListResponse.java @@ -0,0 +1,12 @@ +package io.sobok.SobokSobok.friend.ui.dto; + +import lombok.Builder; + +@Builder +public record FriendListResponse( + Long friendId, + Long memberId, + String friendName +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequest.java b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequest.java new file mode 100644 index 0000000..28b8ebf --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequest.java @@ -0,0 +1,11 @@ +package io.sobok.SobokSobok.friend.ui.dto; + +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import io.swagger.v3.oas.annotations.media.Schema; + +public record HandleFriendRequest( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + NoticeStatus isOkay +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequestResponse.java b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequestResponse.java new file mode 100644 index 0000000..075d712 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/HandleFriendRequestResponse.java @@ -0,0 +1,15 @@ +package io.sobok.SobokSobok.friend.ui.dto; + +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import java.time.LocalDateTime; +import lombok.Builder; + +@Builder +public record HandleFriendRequestResponse( + Long noticeId, + String memberName, + NoticeStatus isOkay, + LocalDateTime updatedAt +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendName.java b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendName.java new file mode 100644 index 0000000..807605c --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendName.java @@ -0,0 +1,10 @@ +package io.sobok.SobokSobok.friend.ui.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record UpdateFriendName( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + String friendName +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendNameResponse.java b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendNameResponse.java new file mode 100644 index 0000000..231ffed --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/friend/ui/dto/UpdateFriendNameResponse.java @@ -0,0 +1,13 @@ +package io.sobok.SobokSobok.friend.ui.dto; + +import lombok.Builder; + +@Builder +public record UpdateFriendNameResponse( + Long friendId, + Long userId, + Long memberId, + String friendName +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/application/NoticeService.java b/src/main/java/io/sobok/SobokSobok/notice/application/NoticeService.java new file mode 100644 index 0000000..3fddbac --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/application/NoticeService.java @@ -0,0 +1,105 @@ +package io.sobok.SobokSobok.notice.application; + +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.BadRequestException; +import io.sobok.SobokSobok.exception.model.ForbiddenException; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.notice.domain.Notice; +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import io.sobok.SobokSobok.notice.infrastructure.NoticeQueryRepository; +import io.sobok.SobokSobok.notice.infrastructure.NoticeRepository; +import io.sobok.SobokSobok.notice.ui.dto.NoticeInfo; +import io.sobok.SobokSobok.notice.ui.dto.NoticeResponse; +import io.sobok.SobokSobok.notice.ui.dto.ReceivePillInfoResponse; +import io.sobok.SobokSobok.pill.application.PillServiceUtil; +import io.sobok.SobokSobok.pill.domain.Pill; +import io.sobok.SobokSobok.pill.domain.SendPill; +import io.sobok.SobokSobok.pill.infrastructure.PillRepository; +import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository; +import io.sobok.SobokSobok.pill.infrastructure.SendPillRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class NoticeService { + + private final UserRepository userRepository; + private final PillRepository pillRepository; + private final PillScheduleRepository pillScheduleRepository; + private final SendPillRepository sendPillRepository; + private final NoticeRepository noticeRepository; + private final NoticeQueryRepository noticeQueryRepository; + + @Transactional + public NoticeResponse getList(Long userId) { + + User user = UserServiceUtil.findUserById(userRepository, userId); + + List noticeList = noticeQueryRepository.getNoticeList(userId); + + return NoticeResponse.builder() + .username(user.getUsername()) + .infoList(noticeList) + .build(); + } + + @Transactional + public ReceivePillInfoResponse getReceivePillInfo(Long userId, Long noticeId, Long pillId) { + + User receiver = UserServiceUtil.findUserById(userRepository, userId); + PillServiceUtil.existsPillById(pillRepository, pillId); + Notice notice = NoticeServiceUtil.findNoticeById(noticeRepository, noticeId); + + if (!notice.isPillNotice()) { + throw new BadRequestException(ErrorCode.NOT_PILL_NOTICE); + } + + if (!notice.getReceiverId().equals(receiver.getId())) { + throw new ForbiddenException(ErrorCode.UNAUTHORIZED_PILL); + } + + if (notice.isCompleteNotice()) { + throw new BadRequestException(ErrorCode.ALREADY_COMPLETE_NOTICE); + } + + return noticeQueryRepository.getReceivePillInfo(noticeId, pillId); + } + + @Transactional + public void completePillNotice(Long userId, Long pillId, NoticeStatus isOkay) { + + User receiver = UserServiceUtil.findUserById(userRepository, userId); + Pill pill = PillServiceUtil.findPillById(pillRepository, pillId); + + SendPill sendPill = sendPillRepository.findByPillId(pillId) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_SEND_PILL)); + Notice notice = noticeRepository.findById(sendPill.getNoticeId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.NON_EXISTS_NOTICE)); + + if (!notice.getReceiverId().equals(receiver.getId())) { + throw new ForbiddenException(ErrorCode.UNAUTHORIZED_PILL); + } + + if (notice.isCompleteNotice()) { + throw new BadRequestException(ErrorCode.ALREADY_COMPLETE_NOTICE); + } + + if (isOkay.equals(NoticeStatus.REFUSE)) { + pillRepository.deleteById(pillId); + pillScheduleRepository.deleteAllByPillId(pillId); + } + + if (isOkay.equals(NoticeStatus.ACCEPT)) { + pill.receivePill(receiver.getId()); + } + + notice.changeNoticeStatus(isOkay); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/application/NoticeServiceUtil.java b/src/main/java/io/sobok/SobokSobok/notice/application/NoticeServiceUtil.java new file mode 100644 index 0000000..c7024f8 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/application/NoticeServiceUtil.java @@ -0,0 +1,17 @@ +package io.sobok.SobokSobok.notice.application; + +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.notice.domain.Notice; +import io.sobok.SobokSobok.notice.infrastructure.NoticeRepository; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NoticeServiceUtil { + + public static Notice findNoticeById(NoticeRepository noticeRepository, Long id) { + return noticeRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.NON_EXISTS_NOTICE)); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/domain/Notice.java b/src/main/java/io/sobok/SobokSobok/notice/domain/Notice.java new file mode 100644 index 0000000..5025587 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/domain/Notice.java @@ -0,0 +1,58 @@ +package io.sobok.SobokSobok.notice.domain; + +import io.sobok.SobokSobok.common.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notice extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long senderId; + + @Column(nullable = false) + private Long receiverId; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private NoticeType section; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private NoticeStatus isOkay; + + public void setIsOkay(NoticeStatus isOkay) { + this.isOkay = isOkay; + } + + private Notice(Long senderId, Long receiverId, NoticeType section, NoticeStatus isOkay) { + this.senderId = senderId; + this.receiverId = receiverId; + this.section = section; + this.isOkay = isOkay; + } + + public static Notice newInstance(Long senderId, Long receiverId, NoticeType section, NoticeStatus isOkay) { + return new Notice(senderId, receiverId, section, isOkay); + } + + public Boolean isPillNotice() { + return this.section == NoticeType.PILL; + } + + public Boolean isCompleteNotice() { + return this.isOkay.equals(NoticeStatus.ACCEPT) || this.isOkay.equals(NoticeStatus.REFUSE); + } + + public void changeNoticeStatus(NoticeStatus isOkay) { + this.isOkay = isOkay; + } +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/domain/NoticeStatus.java b/src/main/java/io/sobok/SobokSobok/notice/domain/NoticeStatus.java new file mode 100644 index 0000000..6000fd9 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/domain/NoticeStatus.java @@ -0,0 +1,14 @@ +package io.sobok.SobokSobok.notice.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NoticeStatus { + + ACCEPT, + WAITING, + REFUSE, + ; +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/domain/NoticeType.java b/src/main/java/io/sobok/SobokSobok/notice/domain/NoticeType.java new file mode 100644 index 0000000..2f58ca4 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/domain/NoticeType.java @@ -0,0 +1,14 @@ +package io.sobok.SobokSobok.notice.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NoticeType { + + PILL, + FRIEND, + ; + +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeQueryRepository.java b/src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeQueryRepository.java new file mode 100644 index 0000000..70eb9cf --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeQueryRepository.java @@ -0,0 +1,105 @@ +package io.sobok.SobokSobok.notice.infrastructure; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import io.sobok.SobokSobok.auth.domain.QUser; +import io.sobok.SobokSobok.friend.domain.QSendFriend; +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import io.sobok.SobokSobok.notice.domain.NoticeType; +import io.sobok.SobokSobok.notice.domain.QNotice; +import io.sobok.SobokSobok.notice.ui.dto.NoticeInfo; +import io.sobok.SobokSobok.notice.ui.dto.ReceivePillInfoResponse; +import io.sobok.SobokSobok.pill.domain.QPill; +import io.sobok.SobokSobok.pill.domain.QPillSchedule; +import io.sobok.SobokSobok.pill.domain.QSendPill; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class NoticeQueryRepository { + + private final JPAQueryFactory queryFactory; + + public List getNoticeList(Long receiverId) { + QUser user = QUser.user; + QPill pill = QPill.pill; + QNotice notice = QNotice.notice; + QSendPill sendPill = QSendPill.sendPill; + QSendFriend sendFriend = QSendFriend.sendFriend; + + return queryFactory + .select( + Projections.constructor( + NoticeInfo.class, + notice.id.as("noticeId"), + notice.section, + notice.isOkay, + notice.createdAt, + user.username.as("senderName"), + pill.pillName, + pill.id.as("pillId"), + sendFriend.id.as("senderGroupId") + ) + ) + .from(notice) + .leftJoin(sendPill).on(notice.id.eq(sendPill.noticeId)) + .leftJoin(sendFriend).on(notice.id.eq(sendFriend.noticeId)) + .leftJoin(user).on(notice.senderId.eq(user.id)) + .leftJoin(pill).on(sendPill.pillId.eq(pill.id)) + .where(notice.receiverId.eq(receiverId)) + .fetch(); + } + + public ReceivePillInfoResponse getReceivePillInfo(Long noticeId, Long pillId) { + QNotice notice = QNotice.notice; + QSendPill sendPill = QSendPill.sendPill; + QPill pill = QPill.pill; + QPillSchedule pillSchedule = QPillSchedule.pillSchedule; + + List result = queryFactory + .selectDistinct(pill.pillName, pill.startDate, pill.endDate, pill.scheduleDay, + pillSchedule.scheduleTime) + .from(notice) + .join(sendPill).on(notice.id.eq(sendPill.noticeId)) + .join(pill).on(sendPill.pillId.eq(pill.id)) + .join(pillSchedule).on(pill.id.eq(pillSchedule.pillId)) + .where( + notice.id.eq(noticeId), + pill.id.eq(pillId) + ) + .fetch(); + + List scheduleTimeList = new ArrayList<>(result.stream() + .map(data -> data.get(4, String.class)).distinct().toList()); + Collections.sort(scheduleTimeList); + + Tuple firstDate = result.get(0); + + return ReceivePillInfoResponse.builder() + .pillName(firstDate.get(0, String.class)) + .scheduleTime(scheduleTimeList) + .startDate(firstDate.get(1, LocalDate.class)) + .endDate(firstDate.get(2, LocalDate.class)) + .scheduleDay(firstDate.get(3, String.class)) + .build(); + } + + public Boolean isAlreadyFriendRequestFromSender(Long senderId, Long receiverId) { + QNotice notice = QNotice.notice; + + return queryFactory + .selectFrom(notice) + .where( + notice.senderId.eq(senderId), + notice.receiverId.eq(receiverId), + notice.section.eq(NoticeType.FRIEND), + notice.isOkay.ne(NoticeStatus.REFUSE) + ).fetchFirst() != null; + } +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeRepository.java b/src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeRepository.java new file mode 100644 index 0000000..57ceb2d --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/infrastructure/NoticeRepository.java @@ -0,0 +1,14 @@ +package io.sobok.SobokSobok.notice.infrastructure; + +import io.sobok.SobokSobok.notice.domain.Notice; +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface NoticeRepository extends JpaRepository { + + // READ + Boolean existsBySenderIdAndReceiverIdAndIsOkay(Long senderId, Long receiverId, NoticeStatus isOkay); +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/ui/NoticeController.java b/src/main/java/io/sobok/SobokSobok/notice/ui/NoticeController.java new file mode 100644 index 0000000..ee05074 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/ui/NoticeController.java @@ -0,0 +1,79 @@ +package io.sobok.SobokSobok.notice.ui; + +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.common.dto.ApiResponse; +import io.sobok.SobokSobok.exception.SuccessCode; +import io.sobok.SobokSobok.notice.application.NoticeService; +import io.sobok.SobokSobok.notice.ui.dto.CompletePillNoticeRequest; +import io.sobok.SobokSobok.notice.ui.dto.NoticeResponse; +import io.sobok.SobokSobok.notice.ui.dto.ReceivePillInfoResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/notice") +@Tag(name = "Notice", description = "알림 관련 컨트롤러") +public class NoticeController { + + private final NoticeService noticeService; + + @GetMapping("") + @Operation( + summary = "알림 전체 조회 API 메서드", + description = "모든 알림을 조회하는 메서드입니다." + ) + public ResponseEntity> getList(@AuthenticationPrincipal User user) { + + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_NOTICE_LIST_SUCCESS, + noticeService.getList(user.getId()) + )); + } + + @GetMapping("/list/{noticeId}/{pillId}") + @Operation( + summary = "전달받은 약 정보 조회 API 메서드", + description = "친구에게 전달받은 약의 상세정보를 조회하는 메서드입니다." + ) + public ResponseEntity> getReceivePillInfo( + @AuthenticationPrincipal User user, + @PathVariable Long noticeId, + @PathVariable Long pillId + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_RECEIVE_PILL_INFO_SUCCESS, + noticeService.getReceivePillInfo(user.getId(), noticeId, pillId) + )); + } + + @PutMapping("/list/{pillId}") + @Operation( + summary = "전달받은 약 수락 | 거절 API 메서드", + description = "친구에게 전달받은 약을 수락 또는 거절하는 메서드입니다." + ) + public ResponseEntity> completePillNotice( + @AuthenticationPrincipal User user, + @PathVariable Long pillId, + @RequestBody @Valid final CompletePillNoticeRequest request + ) { + + noticeService.completePillNotice(5L, pillId, request.isOkay()); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.COMPLETE_PILL_NOTICE + )); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/ui/dto/CompletePillNoticeRequest.java b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/CompletePillNoticeRequest.java new file mode 100644 index 0000000..557537e --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/CompletePillNoticeRequest.java @@ -0,0 +1,13 @@ +package io.sobok.SobokSobok.notice.ui.dto; + +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record CompletePillNoticeRequest( + + @NotNull + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + NoticeStatus isOkay +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeInfo.java b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeInfo.java new file mode 100644 index 0000000..4f856ea --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeInfo.java @@ -0,0 +1,19 @@ +package io.sobok.SobokSobok.notice.ui.dto; + +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import io.sobok.SobokSobok.notice.domain.NoticeType; + +import java.time.LocalDateTime; + +public record NoticeInfo( + + Long noticeId, + NoticeType section, + NoticeStatus isOkay, + LocalDateTime createdAt, + String senderName, + String pillName, + Long pillId, + Long senderGroupId +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeResponse.java b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeResponse.java new file mode 100644 index 0000000..5ad7d06 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/NoticeResponse.java @@ -0,0 +1,13 @@ +package io.sobok.SobokSobok.notice.ui.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record NoticeResponse( + + String username, + List infoList +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/notice/ui/dto/ReceivePillInfoResponse.java b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/ReceivePillInfoResponse.java new file mode 100644 index 0000000..482ae5c --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/notice/ui/dto/ReceivePillInfoResponse.java @@ -0,0 +1,17 @@ +package io.sobok.SobokSobok.notice.ui.dto; + +import lombok.Builder; + +import java.time.LocalDate; +import java.util.List; + +@Builder +public record ReceivePillInfoResponse( + + String pillName, + List scheduleTime, + LocalDate startDate, + LocalDate endDate, + String scheduleDay +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleService.java b/src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleService.java new file mode 100644 index 0000000..dc1dcaa --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleService.java @@ -0,0 +1,101 @@ +package io.sobok.SobokSobok.pill.application; + +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.ForbiddenException; +import io.sobok.SobokSobok.friend.infrastructure.FriendQueryRepository; +import io.sobok.SobokSobok.pill.domain.Pill; +import io.sobok.SobokSobok.pill.domain.PillSchedule; +import io.sobok.SobokSobok.pill.infrastructure.PillRepository; +import io.sobok.SobokSobok.pill.infrastructure.PillScheduleQueryRepository; +import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository; +import io.sobok.SobokSobok.pill.ui.dto.CheckPillScheduleResponse; +import io.sobok.SobokSobok.pill.ui.dto.DateScheduleResponse; +import io.sobok.SobokSobok.pill.ui.dto.MonthScheduleResponse; +import io.sobok.SobokSobok.utils.DateUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PillScheduleService { + + private final UserRepository userRepository; + private final PillRepository pillRepository; + private final PillScheduleRepository pillScheduleRepository; + private final PillScheduleQueryRepository pillScheduleQueryRepository; + private final FriendQueryRepository friendQueryRepository; + + @Transactional + public List getMonthSchedule(Long userId, LocalDate date) { + UserServiceUtil.existsUserById(userRepository, userId); + + LocalDate startDateOfMonth = DateUtil.getStartDateOfMonth(date); + LocalDate endDateOfMonth = DateUtil.getEndDateOfMonth(date); + + return pillScheduleQueryRepository.getMonthSchedule(userId, startDateOfMonth, endDateOfMonth); + } + + @Transactional + public List getDateSchedule(Long userId, LocalDate date) { + UserServiceUtil.existsUserById(userRepository, userId); + + return pillScheduleQueryRepository.getDateSchedule(userId, date); + } + + @Transactional + public List getFriendMonthSchedule(Long userId, Long friendId, LocalDate date) { + UserServiceUtil.existsUserById(userRepository, userId); + UserServiceUtil.existsUserById(userRepository, friendId); + + if (!friendQueryRepository.isAlreadyFriend(userId, friendId)) { + throw new ForbiddenException(ErrorCode.NOT_FRIEND); + } + + LocalDate startDateOfMonth = DateUtil.getStartDateOfMonth(date); + LocalDate endDateOfMonth = DateUtil.getEndDateOfMonth(date); + + return pillScheduleQueryRepository.getMonthSchedule(friendId, startDateOfMonth, endDateOfMonth); + } + + @Transactional + public List getFriendDateSchedule(Long userId, Long friendId, LocalDate date) { + UserServiceUtil.existsUserById(userRepository, userId); + UserServiceUtil.existsUserById(userRepository, friendId); + + if (!friendQueryRepository.isAlreadyFriend(userId, friendId)) { + throw new ForbiddenException(ErrorCode.NOT_FRIEND); + } + + return pillScheduleQueryRepository.getDateSchedule(friendId, date); + } + + @Transactional + public CheckPillScheduleResponse changePillScheduleCheck(Long userId, Long scheduleId, Boolean isCheck) { + UserServiceUtil.existsUserById(userRepository, userId); + + PillSchedule pillSchedule = PillScheduleServiceUtil.findPillScheduleById(pillScheduleRepository, scheduleId); + + Pill pill = PillServiceUtil.findPillById(pillRepository, pillSchedule.getPillId()); + + if (!pill.getUserId().equals(userId)) { + throw new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION); + } + + pillSchedule.changePillScheduleCheck(isCheck); + + return CheckPillScheduleResponse.builder() + .scheduleId(scheduleId) + .pillId(pill.getId()) + .userId(userId) + .scheduleDate(pillSchedule.getScheduleDate().atStartOfDay()) + .scheduleTime(pillSchedule.getScheduleTime()) + .isCheck(isCheck) + .build(); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleServiceUtil.java b/src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleServiceUtil.java new file mode 100644 index 0000000..d6b1d12 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/application/PillScheduleServiceUtil.java @@ -0,0 +1,17 @@ +package io.sobok.SobokSobok.pill.application; + +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.pill.domain.PillSchedule; +import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PillScheduleServiceUtil { + + public static PillSchedule findPillScheduleById(PillScheduleRepository pillScheduleRepository, Long id) { + return pillScheduleRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_PILL_SCHEDULE)); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/application/PillService.java b/src/main/java/io/sobok/SobokSobok/pill/application/PillService.java index 3bdc58b..3d0b0fe 100644 --- a/src/main/java/io/sobok/SobokSobok/pill/application/PillService.java +++ b/src/main/java/io/sobok/SobokSobok/pill/application/PillService.java @@ -1,22 +1,35 @@ package io.sobok.SobokSobok.pill.application; +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; import io.sobok.SobokSobok.auth.domain.User; import io.sobok.SobokSobok.auth.infrastructure.UserRepository; import io.sobok.SobokSobok.exception.ErrorCode; import io.sobok.SobokSobok.exception.model.BadRequestException; +import io.sobok.SobokSobok.exception.model.ForbiddenException; import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.friend.infrastructure.FriendQueryRepository; +import io.sobok.SobokSobok.notice.domain.Notice; +import io.sobok.SobokSobok.notice.domain.NoticeStatus; +import io.sobok.SobokSobok.notice.domain.NoticeType; +import io.sobok.SobokSobok.notice.infrastructure.NoticeRepository; import io.sobok.SobokSobok.pill.domain.Pill; import io.sobok.SobokSobok.pill.domain.PillSchedule; -import io.sobok.SobokSobok.pill.infrastructure.PillQueryRepository; -import io.sobok.SobokSobok.pill.infrastructure.PillRepository; -import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository; +import io.sobok.SobokSobok.pill.domain.SendPill; +import io.sobok.SobokSobok.pill.infrastructure.*; +import io.sobok.SobokSobok.pill.ui.dto.PillListResponse; import io.sobok.SobokSobok.pill.ui.dto.PillRequest; +import io.sobok.SobokSobok.pill.ui.dto.PillResponse; import io.sobok.SobokSobok.utils.PillUtil; import lombok.RequiredArgsConstructor; +import io.sobok.SobokSobok.pill.infrastructure.PillQueryRepository; +import io.sobok.SobokSobok.pill.infrastructure.PillRepository; +import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -24,14 +37,18 @@ public class PillService { private final UserRepository userRepository; private final PillRepository pillRepository; - private final PillQueryRepository pillQueryRepository; private final PillScheduleRepository pillScheduleRepository; + private final SendPillRepository sendPillRepository; + private final NoticeRepository noticeRepository; + + private final FriendQueryRepository friendQueryRepository; + private final PillQueryRepository pillQueryRepository; + private final PillScheduleQueryRepository pillScheduleQueryRepository; @Transactional public void addPill(Long userId, PillRequest request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_USER)); + User user = UserServiceUtil.findUserById(userRepository, userId); validatePillCount(user.getId(), request.pillName().length); validatePillRequest(request.startDate(), request.endDate(), request.day(), request.timeList()); @@ -61,6 +78,49 @@ public void addPill(Long userId, PillRequest request) { } } + @Transactional + public void sendPill(Long userId, Long friendId, PillRequest request) { + + UserServiceUtil.existsUserById(userRepository, userId); + UserServiceUtil.existsUserById(userRepository, friendId); + + if (!friendQueryRepository.isAlreadyFriend(userId, friendId)) { + throw new ForbiddenException(ErrorCode.NOT_FRIEND); + } + + validatePillCount(friendId, request.pillName().length); + validatePillRequest(request.startDate(), request.endDate(), request.day(), request.timeList()); + + Notice newNotice = noticeRepository.save(Notice.newInstance(userId, friendId, NoticeType.PILL, NoticeStatus.WAITING)); + + for (String pill : request.pillName()) { + Pill newPill = pillRepository.save(Pill.builder() + .pillName(pill) + .color(PillUtil.getRandomColorNumber()) + .startDate(request.startDate()) + .endDate(request.endDate()) + .scheduleDay(request.day()) + .userId(null) + .build() + ); + + LocalDate[] scheduleDate = PillUtil.getScheduleDateList(request.startDate(), request.endDate(), request.day().split(", ")); + for (LocalDate date : scheduleDate) { + for (String time : request.timeList()) { + pillScheduleRepository.save(PillSchedule.builder() + .scheduleDate(date) + .scheduleTime(time) + .pillId(newPill.getId()) + .build() + ); + } + } + + sendPillRepository.save(SendPill.newInstance(newNotice.getId(), newPill.getId())); + } + } + + @Transactional public Integer getPillCount(Long userId) { @@ -71,6 +131,56 @@ public Integer getPillCount(Long userId) { return pillQueryRepository.getPillCount(userId); } + @Transactional + public void deletePill(Long userId, Long pillId) { + + UserServiceUtil.existsUserById(userRepository, userId); + Pill pill = PillServiceUtil.findPillById(pillRepository, pillId); + + if (!pill.isPillUser(userId)) { + throw new ForbiddenException(ErrorCode.UNAUTHORIZED_PILL); + } + + pillRepository.delete(pill); + pillScheduleRepository.deleteAllByPillId(pillId); + } + + @Transactional + public List getPillList(Long userId) { + + UserServiceUtil.existsUserById(userRepository, userId); + + List pillList = pillRepository.findAllByUserId(userId); + return pillList.stream() + .map(pill -> PillListResponse.builder() + .id(pill.getId()) + .color(pill.getColor()) + .pillName(pill.getPillName()) + .build()) + .collect(Collectors.toList()); + } + + @Transactional + public PillResponse getPillInfo(Long userId, Long pillId) { + + UserServiceUtil.existsUserById(userRepository, userId); + Pill pill = PillServiceUtil.findPillById(pillRepository, pillId); + + if (!pill.isPillUser(userId)) { + throw new ForbiddenException(ErrorCode.UNAUTHORIZED_PILL); + } + + List scheduleTime = pillScheduleQueryRepository.getPillScheduleTime(pill.getId()); + + return PillResponse.builder() + .pillName(pill.getPillName()) + .scheduleDay(pill.getScheduleDay()) + .startDate(pill.getStartDate()) + .endDate(pill.getEndDate()) + .scheduleTime(scheduleTime) + .build(); + } + private void validatePillCount(Long userId, int requestPillCount) { if (pillQueryRepository.getPillCount(userId) + requestPillCount > 5) { throw new BadRequestException(ErrorCode.EXCEEDED_PILL_COUNT); diff --git a/src/main/java/io/sobok/SobokSobok/pill/application/PillServiceUtil.java b/src/main/java/io/sobok/SobokSobok/pill/application/PillServiceUtil.java new file mode 100644 index 0000000..6e05758 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/application/PillServiceUtil.java @@ -0,0 +1,23 @@ +package io.sobok.SobokSobok.pill.application; + +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.pill.domain.Pill; +import io.sobok.SobokSobok.pill.infrastructure.PillRepository; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PillServiceUtil { + + public static Pill findPillById(PillRepository pillRepository, Long id) { + return pillRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_PILL)); + } + + public static void existsPillById(PillRepository pillRepository, Long id) { + if (!pillRepository.existsById(id)) { + throw new NotFoundException(ErrorCode.UNREGISTERED_PILL); + } + } +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/domain/Pill.java b/src/main/java/io/sobok/SobokSobok/pill/domain/Pill.java index f6e8ac4..e43da03 100644 --- a/src/main/java/io/sobok/SobokSobok/pill/domain/Pill.java +++ b/src/main/java/io/sobok/SobokSobok/pill/domain/Pill.java @@ -10,6 +10,7 @@ import org.hibernate.annotations.DynamicInsert; import java.time.LocalDate; +import java.util.Objects; @Getter @Entity @@ -53,4 +54,12 @@ public Pill(String pillName, Integer color, LocalDate startDate, LocalDate endDa this.scheduleDay = scheduleDay; this.userId = userId; } + + public void receivePill(Long userId) { + this.userId = userId; + } + + public boolean isPillUser(Long userId) { + return Objects.equals(this.userId, userId); + } } diff --git a/src/main/java/io/sobok/SobokSobok/pill/domain/PillSchedule.java b/src/main/java/io/sobok/SobokSobok/pill/domain/PillSchedule.java index 6028c1c..9fa4e0e 100644 --- a/src/main/java/io/sobok/SobokSobok/pill/domain/PillSchedule.java +++ b/src/main/java/io/sobok/SobokSobok/pill/domain/PillSchedule.java @@ -40,4 +40,8 @@ public PillSchedule(LocalDate scheduleDate, String scheduleTime, Long pillId) { this.scheduleTime = scheduleTime; this.pillId = pillId; } + + public void changePillScheduleCheck(Boolean isCheck) { + this.isCheck = isCheck; + } } diff --git a/src/main/java/io/sobok/SobokSobok/pill/domain/SendPill.java b/src/main/java/io/sobok/SobokSobok/pill/domain/SendPill.java new file mode 100644 index 0000000..865dc8d --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/domain/SendPill.java @@ -0,0 +1,32 @@ +package io.sobok.SobokSobok.pill.domain; + +import io.sobok.SobokSobok.common.domain.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SendPill extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long noticeId; + + @Column(nullable = false) + private Long pillId; + + private SendPill(Long noticeId, Long pillId) { + this.noticeId = noticeId; + this.pillId = pillId; + } + + public static SendPill newInstance(Long noticeId, Long pillId) { + return new SendPill(noticeId, pillId); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillRepository.java b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillRepository.java index 63bf154..24f96dc 100644 --- a/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillRepository.java +++ b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillRepository.java @@ -3,5 +3,10 @@ import io.sobok.SobokSobok.pill.domain.Pill; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface PillRepository extends JpaRepository { + + // READ + List findAllByUserId(Long userId); } diff --git a/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleQueryRepository.java b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleQueryRepository.java new file mode 100644 index 0000000..a6f86ce --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleQueryRepository.java @@ -0,0 +1,162 @@ +package io.sobok.SobokSobok.pill.infrastructure; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import io.sobok.SobokSobok.pill.domain.PillSchedule; +import io.sobok.SobokSobok.pill.domain.QPill; +import io.sobok.SobokSobok.pill.domain.QPillSchedule; +import io.sobok.SobokSobok.pill.ui.dto.DateScheduleResponse; +import io.sobok.SobokSobok.pill.ui.dto.MonthScheduleResponse; +import io.sobok.SobokSobok.pill.ui.dto.PillScheduleInfo; +import io.sobok.SobokSobok.sticker.domain.QLikeSchedule; +import io.sobok.SobokSobok.sticker.domain.QSticker; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class PillScheduleQueryRepository { + + private final JPAQueryFactory queryFactory; + + public List getPillScheduleTime(Long pillId) { + + QPillSchedule pillSchedule = QPillSchedule.pillSchedule; + + return queryFactory + .select(pillSchedule.scheduleTime) + .distinct() + .from(pillSchedule) + .where(pillSchedule.pillId.eq(pillId)) + .fetch(); + } + + public List getMonthSchedule(Long userId, LocalDate startDate, LocalDate endDate) { + + QPillSchedule pillSchedule = QPillSchedule.pillSchedule; + QPill pill = QPill.pill; + + List results = queryFactory + .select(pillSchedule.scheduleDate, + pillSchedule.id.count().as("scheduleCount"), + new CaseBuilder() + .when(pillSchedule.isCheck.isTrue()) + .then(1) + .otherwise(0) + .sum() + .as("isCheckCount") + ) + .from(pillSchedule) + .leftJoin(pill).on(pill.id.eq(pillSchedule.pillId)) + .where( + pill.userId.eq(userId), + pill.isStop.eq(false), + pillSchedule.scheduleDate.goe(startDate), + pillSchedule.scheduleDate.loe(endDate) + ) + .groupBy(pillSchedule.scheduleDate) + .fetch(); + + return results.stream() + .map(tuple -> { + LocalDate scheduleDate = tuple.get(pillSchedule.scheduleDate); + Long scheduleCount = Objects.requireNonNull(tuple.get(1, Number.class)).longValue(); + Long isCheckCount = Objects.requireNonNull(tuple.get(2, Number.class)).longValue(); + + String isComplete = determineIsComplete(scheduleCount, isCheckCount); + + return MonthScheduleResponse.builder() + .scheduleDate(scheduleDate) + .scheduleCount(scheduleCount) + .isCheckCount(isCheckCount) + .isComplete(isComplete) + .build(); + }) + .collect(Collectors.toList()); + } + + public List getDateSchedule(Long userId, LocalDate date) { + + QPillSchedule pillSchedule = QPillSchedule.pillSchedule; + QPill pill = QPill.pill; + QSticker sticker = QSticker.sticker; + QLikeSchedule likeSchedule = QLikeSchedule.likeSchedule; + + List pillScheduleTimeList = queryFactory + .select(pillSchedule.scheduleTime) + .distinct() + .from(pillSchedule) + .leftJoin(pill).on(pill.id.eq(pillSchedule.pillId)) + .where(pill.userId.eq(userId), pill.isStop.eq(false), pillSchedule.scheduleDate.eq(date)) + .fetch(); + + List dateScheduleResponses = new ArrayList<>(); + + for (String time : pillScheduleTimeList) { + List pillScheduleIds = queryFactory + .select(pillSchedule.id) + .from(pillSchedule) + .where(pillSchedule.scheduleDate.eq(date), pillSchedule.scheduleTime.eq(time)) + .fetch(); + + Map> stickerIdMap = pillScheduleIds.stream() + .collect(Collectors.toMap(id -> id, id -> queryFactory + .select(likeSchedule.stickerId) + .from(likeSchedule) + .where(likeSchedule.scheduleId.eq(id)) + .fetch())); + + // 결과 매핑 + List pillScheduleInfoList = pillScheduleIds.stream() + .flatMap(scheduleId -> { + List stickerIds = stickerIdMap.getOrDefault(scheduleId, Collections.emptyList()); + return queryFactory + .select( + pillSchedule.id, + pill.id, + pill.pillName, + pillSchedule.isCheck, + pill.color, + likeSchedule.scheduleId.count() + ) + .from(pillSchedule) + .leftJoin(pill).on(pill.id.eq(pillSchedule.pillId)) + .leftJoin(likeSchedule).on(likeSchedule.scheduleId.eq(pillSchedule.id)) + .where(pillSchedule.id.eq(scheduleId)) + .groupBy(pillSchedule.id, pill.id, pill.pillName, pillSchedule.isCheck, pill.color) + .fetch() + .stream() + .map(tuple -> PillScheduleInfo.builder() + .scheduleId(tuple.get(0, Long.class)) + .pillId(tuple.get(1, Long.class)) + .pillName(tuple.get(2, String.class)) + .isCheck(tuple.get(3, Boolean.class)) + .color(tuple.get(4, Integer.class)) + .stickerId(stickerIds) + .stickerTotalCount(tuple.get(5, Long.class)) + .build() + ); + }).collect(Collectors.toList()); + + dateScheduleResponses.add(new DateScheduleResponse(time, pillScheduleInfoList)); + } + + return dateScheduleResponses; + } + + private String determineIsComplete(Long scheduleCount, Long isCheckCount) { + if (scheduleCount.equals(isCheckCount)) { + return "done"; + } else if (1 <= isCheckCount && isCheckCount < scheduleCount) { + return "doing"; + } else { + return "none"; + } + } +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleRepository.java b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleRepository.java index dc0c665..7e4aa4b 100644 --- a/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleRepository.java +++ b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/PillScheduleRepository.java @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface PillScheduleRepository extends JpaRepository { + + void deleteAllByPillId(Long pillId); } diff --git a/src/main/java/io/sobok/SobokSobok/pill/infrastructure/SendPillRepository.java b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/SendPillRepository.java new file mode 100644 index 0000000..2faaa4c --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/infrastructure/SendPillRepository.java @@ -0,0 +1,11 @@ +package io.sobok.SobokSobok.pill.infrastructure; + +import io.sobok.SobokSobok.pill.domain.SendPill; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface SendPillRepository extends JpaRepository { + + Optional findByPillId(Long pillId); +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/PillController.java b/src/main/java/io/sobok/SobokSobok/pill/ui/PillController.java index 6d4869a..5c5eb80 100644 --- a/src/main/java/io/sobok/SobokSobok/pill/ui/PillController.java +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/PillController.java @@ -4,7 +4,9 @@ import io.sobok.SobokSobok.common.dto.ApiResponse; import io.sobok.SobokSobok.exception.SuccessCode; import io.sobok.SobokSobok.pill.application.PillService; +import io.sobok.SobokSobok.pill.ui.dto.PillListResponse; import io.sobok.SobokSobok.pill.ui.dto.PillRequest; +import io.sobok.SobokSobok.pill.ui.dto.PillResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -14,6 +16,8 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/pill") @@ -55,8 +59,8 @@ public ResponseEntity> getPillCount(@AuthenticationPrincipa @GetMapping("/count/{userId}") @Operation( - summary = "타인 약 개수 조회 API 메서드", - description = "타인 약의 개수가 몇 개인지 확인하는 메서드입니다." + summary = "친구 약 개수 조회 API 메서드", + description = "친구 약의 개수가 몇 개인지 확인하는 메서드입니다." ) public ResponseEntity> getUserPillCount(@PathVariable Long userId) { @@ -67,4 +71,69 @@ public ResponseEntity> getUserPillCount(@PathVariable Long pillService.getPillCount(userId) )); } + + @PostMapping("/friend/{friendId}") + @Operation( + summary = "친구에게 약 전송 API 메서드", + description = "친구에게 약을 전송하는 메서드입니다." + ) + public ResponseEntity> sendPillToFriend( + @AuthenticationPrincipal User user, + @PathVariable Long friendId, + @RequestBody @Valid final PillRequest request + ) { + + pillService.sendPill(user.getId(), friendId, request); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessCode.SEND_PILL_SUCCESS)); + } + + @DeleteMapping("/{pillId}") + @Operation( + summary = "약 삭제 API 메서드", + description = "내 약을 삭제하는 메서드입니다." + ) + public ResponseEntity> deletePill(@AuthenticationPrincipal User user, @PathVariable Long pillId) { + + pillService.deletePill(user.getId(), pillId); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.DELETE_PILL_SUCCESS)); + } + + @GetMapping("/list") + @Operation( + summary = "약 리스트 조회 API 메서드", + description = "내 약의 리스트를 조회하는 메서드입니다." + ) + public ResponseEntity>> getPillList(@AuthenticationPrincipal User user) { + + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiResponse.success( + SuccessCode.GET_PILL_LIST_SUCCESS, + pillService.getPillList(user.getId()) + ) + ); + } + + @GetMapping("/{pillId}") + @Operation( + summary = "내 약 상세조회 API 메서드", + description = "내 약의 상세 정보를 조회하는 메서드입니다." + ) + public ResponseEntity> getPillInfo( + @AuthenticationPrincipal User user, + @PathVariable Long pillId + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_PILL_INFO_SUCCESS, + pillService.getPillInfo(user.getId(), pillId) + )); + } } diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/PillScheduleController.java b/src/main/java/io/sobok/SobokSobok/pill/ui/PillScheduleController.java new file mode 100644 index 0000000..934782c --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/PillScheduleController.java @@ -0,0 +1,134 @@ +package io.sobok.SobokSobok.pill.ui; + +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.common.dto.ApiResponse; +import io.sobok.SobokSobok.exception.SuccessCode; +import io.sobok.SobokSobok.pill.application.PillScheduleService; +import io.sobok.SobokSobok.pill.ui.dto.CheckPillScheduleResponse; +import io.sobok.SobokSobok.pill.ui.dto.DateScheduleResponse; +import io.sobok.SobokSobok.pill.ui.dto.MonthScheduleResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/schedule") +@Tag(name = "Schedule", description = "약 일정 관련 컨트롤러") +public class PillScheduleController { + + private final PillScheduleService pillScheduleService; + + @GetMapping("/calendar") + @Operation( + summary = "내 복약 일정 조회 API 메서드", + description = "query string -> date (조회할 달의 아무 날짜)" + ) + public ResponseEntity>> getMonthSchedule( + @AuthenticationPrincipal User user, + @RequestParam LocalDate date + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_MONTH_SCHEDULE_SUCCESS, + pillScheduleService.getMonthSchedule(user.getId(), date) + )); + } + + @GetMapping("/detail") + @Operation( + summary = "특정 일자 복약 일정 조회 API", + description = "query string -> date (조회할 날짜)" + ) + public ResponseEntity>> getDateSchedule( + @AuthenticationPrincipal User user, + @RequestParam LocalDate date + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_DATE_SCHEDULE_SUCCESS, + pillScheduleService.getDateSchedule(user.getId(),date) + )); + } + + @GetMapping("/{memberId}/calendar") + @Operation( + summary = "친구 복약 일정 조회 API 메서드", + description = "path variable -> 친구의 userId, query string -> date (조회할 달의 아무 날짜)" + ) + public ResponseEntity>> getFriendMonthSchedule( + @AuthenticationPrincipal User user, + @PathVariable Long memberId, + @RequestParam LocalDate date + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_FRIEND_MONTH_SCHEDULE_SUCCESS, + pillScheduleService.getFriendMonthSchedule(user.getId(), memberId, date) + )); + } + + @GetMapping("/{memberId}/detail") + @Operation( + summary = "친구 특정 일자 복약 일정 조회 API", + description = "path variable -> 친구의 userId, query string -> date (조회할 날짜)" + ) + public ResponseEntity>> getFriendDateSchedule( + @AuthenticationPrincipal User user, + @PathVariable Long memberId, + @RequestParam LocalDate date + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_FRIEND_DATE_SCHEDULE_SUCCESS, + pillScheduleService.getFriendDateSchedule(user.getId(), memberId, date) + )); + } + + + @PutMapping("/check/{scheduleId}") + @Operation( + summary = "복용 체크 완료 API 메서드", + description = "약 일정의 복용 체크를 완료하는 메서드입니다." + ) + public ResponseEntity> checkPillSchedule( + @AuthenticationPrincipal User user, + @PathVariable Long scheduleId + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.CHECK_PILL_SCHEDULE_SUCCESS, + pillScheduleService.changePillScheduleCheck(user.getId(), scheduleId, true) + )); + } + + @PutMapping("/uncheck/{scheduleId}") + @Operation( + summary = "복용 체크 취소 API 메서드", + description = "약 일정의 복용 체크를 취소하는 메서드입니다." + ) + public ResponseEntity> uncheckPillSchedule( + @AuthenticationPrincipal User user, + @PathVariable Long scheduleId + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.UNCHECK_PILL_SCHEDULE_SUCCESS, + pillScheduleService.changePillScheduleCheck(user.getId(), scheduleId, false) + )); + } + +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/dto/CheckPillScheduleResponse.java b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/CheckPillScheduleResponse.java new file mode 100644 index 0000000..04a8a83 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/CheckPillScheduleResponse.java @@ -0,0 +1,16 @@ +package io.sobok.SobokSobok.pill.ui.dto; + +import java.time.LocalDateTime; +import lombok.Builder; + +@Builder +public record CheckPillScheduleResponse( + Long scheduleId, + Long pillId, + Long userId, + LocalDateTime scheduleDate, + String scheduleTime, + Boolean isCheck +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/dto/DateScheduleResponse.java b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/DateScheduleResponse.java new file mode 100644 index 0000000..9a709ea --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/DateScheduleResponse.java @@ -0,0 +1,14 @@ +package io.sobok.SobokSobok.pill.ui.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record DateScheduleResponse( + + String scheduleTime, + List scheduleList +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/dto/MonthScheduleResponse.java b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/MonthScheduleResponse.java new file mode 100644 index 0000000..89f09c3 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/MonthScheduleResponse.java @@ -0,0 +1,15 @@ +package io.sobok.SobokSobok.pill.ui.dto; + +import lombok.Builder; + +import java.time.LocalDate; + +@Builder +public record MonthScheduleResponse( + + LocalDate scheduleDate, + Long scheduleCount, + Long isCheckCount, + String isComplete +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillListResponse.java b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillListResponse.java new file mode 100644 index 0000000..814d170 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillListResponse.java @@ -0,0 +1,12 @@ +package io.sobok.SobokSobok.pill.ui.dto; + +import lombok.Builder; + +@Builder +public record PillListResponse( + + Long id, + Integer color, + String pillName +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillResponse.java b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillResponse.java new file mode 100644 index 0000000..6a3dce4 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillResponse.java @@ -0,0 +1,17 @@ +package io.sobok.SobokSobok.pill.ui.dto; + +import lombok.Builder; + +import java.time.LocalDate; +import java.util.List; + +@Builder +public record PillResponse( + + String pillName, + String scheduleDay, + LocalDate startDate, + LocalDate endDate, + List scheduleTime +) { +} diff --git a/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillScheduleInfo.java b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillScheduleInfo.java new file mode 100644 index 0000000..a2722aa --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/pill/ui/dto/PillScheduleInfo.java @@ -0,0 +1,19 @@ +package io.sobok.SobokSobok.pill.ui.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record PillScheduleInfo( + + Long scheduleId, + Long pillId, + String pillName, + Boolean isCheck, + Integer color, + List stickerId, + Long stickerTotalCount +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/security/filter/ExceptionHandlerFilter.java b/src/main/java/io/sobok/SobokSobok/security/filter/ExceptionHandlerFilter.java new file mode 100644 index 0000000..1a97f87 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/security/filter/ExceptionHandlerFilter.java @@ -0,0 +1,53 @@ +package io.sobok.SobokSobok.security.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.sobok.SobokSobok.common.dto.ApiResponse; +import io.sobok.SobokSobok.exception.ErrorCode; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +public class ExceptionHandlerFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + try { + filterChain.doFilter(request, response); + } catch (ExpiredJwtException e) { + setErrorResponse(response, ErrorCode.EXPIRED_TOKEN); + } catch (MalformedJwtException e) { + setErrorResponse(response, ErrorCode.MALFORMED_TOKEN); + } catch (IllegalArgumentException e) { + setErrorResponse(response, ErrorCode.NULL_TOKEN); + } + } + private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) { + + ObjectMapper objectMapper = new ObjectMapper(); + + response.setCharacterEncoding("UTF-8"); + response.setStatus(errorCode.getCode().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + try { + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.error(errorCode))); + } catch (IOException e){ + log.error(e.getMessage()); + } + } +} diff --git a/src/main/java/io/sobok/SobokSobok/security/jwt/JwtCustomFilter.java b/src/main/java/io/sobok/SobokSobok/security/filter/JwtCustomFilter.java similarity index 94% rename from src/main/java/io/sobok/SobokSobok/security/jwt/JwtCustomFilter.java rename to src/main/java/io/sobok/SobokSobok/security/filter/JwtCustomFilter.java index 0c3c0b2..40838dc 100644 --- a/src/main/java/io/sobok/SobokSobok/security/jwt/JwtCustomFilter.java +++ b/src/main/java/io/sobok/SobokSobok/security/filter/JwtCustomFilter.java @@ -1,5 +1,6 @@ -package io.sobok.SobokSobok.security.jwt; +package io.sobok.SobokSobok.security.filter; +import io.sobok.SobokSobok.security.jwt.JwtProvider; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java b/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java index 48f3878..fefef9c 100644 --- a/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java +++ b/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java @@ -118,21 +118,11 @@ private String generateToken(String subject, String authorities, String tokenTyp } public boolean validateToken(String token) { - try { - Jwts.parserBuilder() - .setSigningKey(key) - .build() - .parseClaimsJws(token); - return true; - } catch (SecurityException | - MalformedJwtException | - ExpiredJwtException | - UnsupportedJwtException | - IllegalArgumentException e - ) { - System.out.println(e.getMessage()); - return false; - } + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + return true; } private Claims parseClaims(String accessToken) { diff --git a/src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java b/src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java new file mode 100644 index 0000000..a3d903b --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/application/StickerService.java @@ -0,0 +1,128 @@ +package io.sobok.SobokSobok.sticker.application; + +import io.sobok.SobokSobok.auth.application.util.UserServiceUtil; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.BadRequestException; +import io.sobok.SobokSobok.exception.model.ConflictException; +import io.sobok.SobokSobok.exception.model.ForbiddenException; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.friend.infrastructure.FriendQueryRepository; +import io.sobok.SobokSobok.pill.application.PillScheduleServiceUtil; +import io.sobok.SobokSobok.pill.application.PillServiceUtil; +import io.sobok.SobokSobok.pill.domain.Pill; +import io.sobok.SobokSobok.pill.domain.PillSchedule; +import io.sobok.SobokSobok.pill.infrastructure.PillRepository; +import io.sobok.SobokSobok.pill.infrastructure.PillScheduleRepository; +import io.sobok.SobokSobok.sticker.domain.LikeSchedule; +import io.sobok.SobokSobok.sticker.infrastructure.LikeScheduleQueryRepository; +import io.sobok.SobokSobok.sticker.infrastructure.LikeScheduleRepository; +import io.sobok.SobokSobok.sticker.infrastructure.StickerRepository; +import io.sobok.SobokSobok.sticker.ui.dto.ReceivedStickerResponse; +import io.sobok.SobokSobok.sticker.ui.dto.StickerActionResponse; +import io.sobok.SobokSobok.sticker.ui.dto.StickerResponse; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class StickerService { + + private final StickerRepository stickerRepository; + private final UserRepository userRepository; + private final PillScheduleRepository pillScheduleRepository; + private final PillRepository pillRepository; + private final FriendQueryRepository friendQueryRepository; + private final LikeScheduleRepository likeScheduleRepository; + private final LikeScheduleQueryRepository likeScheduleQueryRepository; + + @Transactional + public List getStickerList() { + return stickerRepository.findAll().stream().map( + sticker -> StickerResponse.builder() + .stickerId(sticker.getId()) + .stickerImg(sticker.getStickerImg()) + .build() + ).collect(Collectors.toList()); + } + + @Transactional + public StickerActionResponse sendSticker(Long userId, Long scheduleId, Long stickerId) { + UserServiceUtil.existsUserById(userRepository, userId); + PillSchedule pillSchedule = PillScheduleServiceUtil.findPillScheduleById( + pillScheduleRepository, scheduleId); + StickerServiceUtil.existsStickerById(stickerRepository, stickerId); + + if (!pillSchedule.getIsCheck()) { + throw new BadRequestException(ErrorCode.UNCONSUMED_PILL); + } + + Pill pill = PillServiceUtil.findPillById(pillRepository, pillSchedule.getPillId()); + Long receiverId = pill.getUserId(); + if (!friendQueryRepository.isAlreadyFriend(userId, receiverId)) { + throw new ForbiddenException(ErrorCode.NOT_FRIEND); + } + + if (likeScheduleRepository.existsBySenderIdAndScheduleId(userId, scheduleId)) { + throw new ConflictException(ErrorCode.ALREADY_SEND_STICKER); + } + + LikeSchedule likeSchedule = likeScheduleRepository.save( + LikeSchedule.builder() + .scheduleId(scheduleId) + .senderId(userId) + .stickerId(stickerId) + .build() + ); + + return StickerActionResponse.builder() + .likeScheduleId(likeSchedule.getId()) + .scheduleId(likeSchedule.getScheduleId()) + .senderId(likeSchedule.getSenderId()) + .stickerId(likeSchedule.getStickerId()) + .createdAt(likeSchedule.getCreatedAt()) + .updatedAt(likeSchedule.getUpdatedAt()) + .build(); + } + + @Transactional + public StickerActionResponse updateSendSticker(Long userId, Long likeScheduleId, + Long stickerId) { + UserServiceUtil.existsUserById(userRepository, userId); + LikeSchedule likeSchedule = likeScheduleRepository.findById(likeScheduleId) + .orElseThrow(() -> new NotFoundException(ErrorCode.UNREGISTERED_LIKE_SCHEDULE)); + StickerServiceUtil.existsStickerById(stickerRepository, stickerId); + + if (!likeSchedule.isLikeScheduleSender(userId)) { + throw new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION); + } + + likeSchedule.changeSticker(stickerId); + + return StickerActionResponse.builder() + .likeScheduleId(likeSchedule.getId()) + .scheduleId(likeSchedule.getScheduleId()) + .senderId(likeSchedule.getSenderId()) + .stickerId(likeSchedule.getStickerId()) + .createdAt(likeSchedule.getCreatedAt()) + .updatedAt(likeSchedule.getUpdatedAt()) + .build(); + } + + @Transactional + public List getReceivedStickerList(Long userId, Long scheduleId) { + UserServiceUtil.existsUserById(userRepository, userId); + PillSchedule pillSchedule = PillScheduleServiceUtil.findPillScheduleById( + pillScheduleRepository, scheduleId); + Pill pill = PillServiceUtil.findPillById(pillRepository, pillSchedule.getPillId()); + + if (!pill.isPillUser(userId) && !friendQueryRepository.isAlreadyFriend(userId, pill.getUserId())) { + throw new ForbiddenException(ErrorCode.FORBIDDEN_EXCEPTION); + } + + return likeScheduleQueryRepository.getReceivedStickerList(scheduleId, userId); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/application/StickerServiceUtil.java b/src/main/java/io/sobok/SobokSobok/sticker/application/StickerServiceUtil.java new file mode 100644 index 0000000..2cdc728 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/application/StickerServiceUtil.java @@ -0,0 +1,17 @@ +package io.sobok.SobokSobok.sticker.application; + +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.sticker.infrastructure.StickerRepository; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StickerServiceUtil { + + public static void existsStickerById(StickerRepository stickerRepository, Long id) { + if (!stickerRepository.existsById(id)) { + throw new NotFoundException(ErrorCode.UNREGISTERED_STICKER); + } + } +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/domain/LikeSchedule.java b/src/main/java/io/sobok/SobokSobok/sticker/domain/LikeSchedule.java new file mode 100644 index 0000000..ea6eb93 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/domain/LikeSchedule.java @@ -0,0 +1,49 @@ +package io.sobok.SobokSobok.sticker.domain; + +import io.sobok.SobokSobok.common.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; + +@Getter +@Entity +@DynamicInsert +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LikeSchedule extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long scheduleId; + + @Column(nullable = false) + private Long senderId; + + @Column(nullable = false) + private Long stickerId; + + @Builder + public LikeSchedule(Long scheduleId, Long senderId, Long stickerId) { + this.scheduleId = scheduleId; + this.senderId = senderId; + this.stickerId = stickerId; + } + + public void changeSticker(Long stickerId) { + this.stickerId = stickerId; + } + + public Boolean isLikeScheduleSender(Long userId) { + return Objects.equals(senderId, userId); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java b/src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java new file mode 100644 index 0000000..ee6b0d7 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/domain/Sticker.java @@ -0,0 +1,26 @@ +package io.sobok.SobokSobok.sticker.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +public class Sticker { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String stickerImg; + +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleQueryRepository.java b/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleQueryRepository.java new file mode 100644 index 0000000..0ac06ca --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleQueryRepository.java @@ -0,0 +1,44 @@ +package io.sobok.SobokSobok.sticker.infrastructure; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import io.sobok.SobokSobok.auth.domain.QUser; +import io.sobok.SobokSobok.sticker.domain.QLikeSchedule; +import io.sobok.SobokSobok.sticker.domain.QSticker; +import io.sobok.SobokSobok.sticker.ui.dto.ReceivedStickerResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class LikeScheduleQueryRepository { + + private final JPAQueryFactory queryFactory; + + public List getReceivedStickerList(Long scheduleId, Long userId) { + QLikeSchedule likeSchedule = QLikeSchedule.likeSchedule; + QSticker sticker = QSticker.sticker; + QUser user = QUser.user; + + return queryFactory + .select( + Projections.constructor( + ReceivedStickerResponse.class, + likeSchedule.id.as("likeScheduleId"), + likeSchedule.scheduleId, + sticker.id.as("stickerId"), + sticker.stickerImg, + user.username, + user.id.eq(userId).as("isMySticker") + ) + ) + .from(likeSchedule) + .join(sticker).on(likeSchedule.stickerId.eq(sticker.id)) + .join(user).on(likeSchedule.senderId.eq(user.id)) + .where(likeSchedule.scheduleId.eq(scheduleId)) + .orderBy(likeSchedule.updatedAt.desc()) + .fetch(); + } + +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleRepository.java b/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleRepository.java new file mode 100644 index 0000000..d5cc238 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/LikeScheduleRepository.java @@ -0,0 +1,9 @@ +package io.sobok.SobokSobok.sticker.infrastructure; + +import io.sobok.SobokSobok.sticker.domain.LikeSchedule; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeScheduleRepository extends JpaRepository { + + Boolean existsBySenderIdAndScheduleId(Long senderId, Long scheduleId); +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/StickerRepository.java b/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/StickerRepository.java new file mode 100644 index 0000000..1e2b667 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/infrastructure/StickerRepository.java @@ -0,0 +1,8 @@ +package io.sobok.SobokSobok.sticker.infrastructure; + +import io.sobok.SobokSobok.sticker.domain.Sticker; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StickerRepository extends JpaRepository { + +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java b/src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java new file mode 100644 index 0000000..4dc29d4 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/ui/StickerController.java @@ -0,0 +1,99 @@ +package io.sobok.SobokSobok.sticker.ui; + +import io.sobok.SobokSobok.auth.domain.User; +import io.sobok.SobokSobok.common.dto.ApiResponse; +import io.sobok.SobokSobok.exception.SuccessCode; +import io.sobok.SobokSobok.sticker.application.StickerService; +import io.sobok.SobokSobok.sticker.ui.dto.ReceivedStickerResponse; +import io.sobok.SobokSobok.sticker.ui.dto.StickerActionResponse; +import io.sobok.SobokSobok.sticker.ui.dto.StickerResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/sticker") +@Tag(name = "Sticker", description = "스티커 관련 컨트롤러") +public class StickerController { + + private final StickerService stickerService; + + @GetMapping("") + @Operation( + summary = "전송할 스티커 전체보기 API 메서드", + description = "전송할 스티커 전체를 조회하는 메서드입니다." + ) + public ResponseEntity>> getStickerList() { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_STICKER_LIST_SUCCESS, + stickerService.getStickerList() + )); + } + + @PostMapping("/{scheduleId}") + @Operation( + summary = "스티커 전송 API 메서드", + description = "스티커를 전송하는 메서드입니다." + ) + public ResponseEntity> sendSticker( + @AuthenticationPrincipal User user, + @PathVariable Long scheduleId, + @RequestParam Long stickerId + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.SEND_STICKER_SUCCESS, + stickerService.sendSticker(user.getId(), scheduleId, stickerId) + )); + } + + @PutMapping("/my/{likeScheduleId}") + @Operation( + summary = "보낸 스티커 수정 API 메서드", + description = "보낸 스티커를 수정하는 메서드입니다." + ) + public ResponseEntity> updateSendSticker( + @AuthenticationPrincipal User user, + @PathVariable Long likeScheduleId, + @RequestParam Long stickerId + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.UPDATE_STICKER_SUCCESS, + stickerService.updateSendSticker(user.getId(), likeScheduleId, stickerId) + )); + } + + @GetMapping("/{scheduleId}") + @Operation( + summary = "받은 스티커 전체 조회 API 메서드", + description = "받은 스티커를 전체 조회하는 메서드입니다." + ) + public ResponseEntity>> getReceivedStickerList( + @AuthenticationPrincipal User user, + @PathVariable Long scheduleId + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.GET_STICKER_LIST_SUCCESS, + stickerService.getReceivedStickerList(user.getId(), scheduleId) + )); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/ReceivedStickerResponse.java b/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/ReceivedStickerResponse.java new file mode 100644 index 0000000..a20b20f --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/ReceivedStickerResponse.java @@ -0,0 +1,12 @@ +package io.sobok.SobokSobok.sticker.ui.dto; + +public record ReceivedStickerResponse( + Long likeScheduleId, + Long scheduleId, + Long stickerId, + String stickerImg, + String username, + Boolean isMySticker +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerActionResponse.java b/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerActionResponse.java new file mode 100644 index 0000000..9dd3b88 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerActionResponse.java @@ -0,0 +1,16 @@ +package io.sobok.SobokSobok.sticker.ui.dto; + +import java.time.LocalDateTime; +import lombok.Builder; + +@Builder +public record StickerActionResponse( + Long likeScheduleId, + Long scheduleId, + Long senderId, + Long stickerId, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerResponse.java b/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerResponse.java new file mode 100644 index 0000000..faa862c --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/sticker/ui/dto/StickerResponse.java @@ -0,0 +1,11 @@ +package io.sobok.SobokSobok.sticker.ui.dto; + +import lombok.Builder; + +@Builder +public record StickerResponse( + Long stickerId, + String stickerImg +) { + +} diff --git a/src/main/java/io/sobok/SobokSobok/utils/DateUtil.java b/src/main/java/io/sobok/SobokSobok/utils/DateUtil.java new file mode 100644 index 0000000..3e30d60 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/utils/DateUtil.java @@ -0,0 +1,14 @@ +package io.sobok.SobokSobok.utils; + +import java.time.LocalDate; + +public class DateUtil { + + public static LocalDate getStartDateOfMonth(LocalDate date) { + return date.withDayOfMonth(1); + } + + public static LocalDate getEndDateOfMonth(LocalDate date) { + return date.withDayOfMonth(date.lengthOfMonth()); + } +}