diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index c9ce3c9..cad39be 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -34,9 +34,9 @@ jobs: - name: 환경 변수를 세팅합니다. run: | cd ./Infra/src/main/resources - sudo touch ./popcornmate-d7ca1-firebase-adminsdk-svbpw-fce737e873.json - echo "$FIREBASE_JSON" | sudo tee ./popcornmate-d7ca1-firebase-adminsdk-svbpw-fce737e873.json > /dev/null - sed -i 's/#/"/g' ./popcornmate-d7ca1-firebase-adminsdk-svbpw-fce737e873.json + sudo touch ./popcornmateprod-firebase-adminsdk-yvb81-02b4302a03.json + echo "$FIREBASE_JSON" | sudo tee ./popcornmateprod-firebase-adminsdk-yvb81-02b4302a03.json > /dev/null + sed -i 's/#/"/g' ./popcornmateprod-firebase-adminsdk-yvb81-02b4302a03.json env: FIREBASE_JSON: ${{ secrets.FCM_SECRET }} diff --git a/.gitignore b/.gitignore index 4564abf..bb8e08a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,4 @@ out/ .env Domain/src/main/generated/**/*.java -Infra/src/main/resources/popcornmate-d7ca1-firebase-adminsdk-svbpw-fce737e873.json +Infra/src/main/resources/popcornmate-d7ca1-firebase-adminsdk-svbpw-343677f710.json \ No newline at end of file diff --git a/Api/src/main/java/com/example/api/config/SecurityConfig.java b/Api/src/main/java/com/example/api/config/SecurityConfig.java index 6546574..38421f2 100644 --- a/Api/src/main/java/com/example/api/config/SecurityConfig.java +++ b/Api/src/main/java/com/example/api/config/SecurityConfig.java @@ -1,7 +1,10 @@ package com.example.api.config; +import com.example.api.config.security.AccessDeniedFilter; import com.example.api.config.security.FilterConfig; +import com.example.api.config.security.JwtExceptionFilter; +import com.example.api.config.security.JwtTokenFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,7 +13,10 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @Configuration @RequiredArgsConstructor @@ -22,6 +28,9 @@ public class SecurityConfig { private final FilterConfig filterConfig; + private final JwtTokenFilter jwtTokenFilter; + private final JwtExceptionFilter jwtExceptionFilter; + private final AccessDeniedFilter accessDeniedFilter; @Bean @@ -41,18 +50,23 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + http.authorizeRequests().expressionHandler(expressionHandler()); http.authorizeRequests() .requestMatchers("/api/auth/**").permitAll() - .requestMatchers( "/","/api/swagger-ui/**", "/api/v3/api-docs/**").permitAll() - .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Preflight Request 허용해주기 + .requestMatchers( "/api/swagger-ui/**", "/api/v3/api-docs/**").permitAll() .requestMatchers("/api/**").authenticated(); http.apply(filterConfig); - - return http.build(); } + + @Bean + public DefaultWebSecurityExpressionHandler expressionHandler() { + DefaultWebSecurityExpressionHandler expressionHandler = + new DefaultWebSecurityExpressionHandler(); + return expressionHandler; + } } diff --git a/Api/src/main/java/com/example/api/config/security/AccessDeniedFilter.java b/Api/src/main/java/com/example/api/config/security/AccessDeniedFilter.java index 8a93e50..6354e73 100644 --- a/Api/src/main/java/com/example/api/config/security/AccessDeniedFilter.java +++ b/Api/src/main/java/com/example/api/config/security/AccessDeniedFilter.java @@ -13,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; diff --git a/Api/src/main/java/com/example/api/fcm/controller/FcmController.java b/Api/src/main/java/com/example/api/fcm/controller/FcmController.java new file mode 100644 index 0000000..53da94a --- /dev/null +++ b/Api/src/main/java/com/example/api/fcm/controller/FcmController.java @@ -0,0 +1,43 @@ +package com.example.api.fcm.controller; + +import com.example.api.config.security.SecurityUtil; +import com.example.fcm.request.FcmRegistrationRequest; +import com.example.fcm.service.FcmService; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static io.netty.handler.codec.http.HttpResponseStatus.CREATED; + +@Slf4j +@RestController +@RequestMapping("/fcm") +@RequiredArgsConstructor +public class FcmController { + + private final FcmService fcmService; + + @PostMapping("/{userId}") + public ResponseEntity fcmTokenRegistration( + @PathVariable("userId") Long userId, + @RequestBody FcmRegistrationRequest request) { +// Long userId = SecurityUtil.getCurrentUserId(); + fcmService.registerFCMToken(userId, request); + return ResponseEntity + .status(CREATED.code()) + .build(); + } + + @GetMapping + public void fcmToken() throws FirebaseAuthException { + String uid = "some-uid"; + + String customToken = FirebaseAuth.getInstance().createCustomToken(uid); + System.out.println(customToken); + } + +} diff --git a/Api/src/main/java/com/example/api/screening/controller/ScreeningController.java b/Api/src/main/java/com/example/api/screening/controller/ScreeningController.java index 2638c99..8cda800 100644 --- a/Api/src/main/java/com/example/api/screening/controller/ScreeningController.java +++ b/Api/src/main/java/com/example/api/screening/controller/ScreeningController.java @@ -57,6 +57,7 @@ public class ScreeningController { private final ScreeningAdaptor screeningAdaptor; private final ReviewAdaptor reviewAdaptor; private final GetBookMarkedScreeningsUseCase getBookMarkedScreeningUseCase; + private final GetPastScreeningListUseCase getPastScreeningListUseCase; @Operation(description = "모임 대표 이미지") @@ -80,8 +81,8 @@ public Screening uploadScreening(@RequestBody PostScreeningRequest request){ } @Operation(summary = "스크리닝 id별로 가져오기", description = "screening id가져와서 요청하기") - @PostMapping("/{screeningId}") - public Screening getScreening(@PathVariable("screeningId") Long screeningId) { + @GetMapping("/{screeningId}") + public ScreeningInfoResponse getScreening(@PathVariable("screeningId") Long screeningId) { return getScreeningUseCase.execute(screeningId); } @@ -201,8 +202,7 @@ public List getRecentScreening() { //TODO 관람예정(찜하기 한 것 중에서 날짜 지난거) - private 0 @GetMapping("/screenings/past") public List getPassedScreenings() { - Long userId = SecurityUtil.getCurrentUserId(); - return screeningAdaptor.getBookmarkedScreenings(userId); + return getPastScreeningListUseCase.execute(); } //TODO 관람예정(찜하기 한 것 중에서 날짜 안지난거) -> try해봐야함() - private 0 diff --git a/Api/src/main/java/com/example/api/screening/dto/response/ScreeningInfoResponse.java b/Api/src/main/java/com/example/api/screening/dto/response/ScreeningInfoResponse.java new file mode 100644 index 0000000..ffcfa08 --- /dev/null +++ b/Api/src/main/java/com/example/api/screening/dto/response/ScreeningInfoResponse.java @@ -0,0 +1,110 @@ +package com.example.api.screening.dto.response; + +import com.example.domains.screening.entity.Screening; +import com.example.domains.screening.enums.Category; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ScreeningInfoResponse { + @Schema(defaultValue = "1", description = "스크리닝 id") + private Long screeningId; + + @Schema(defaultValue = "https://jgjfhdjghsdkjhgkjd", description = "상영회 대표 이미지") + private String posterImgUrl; + + @Schema(defaultValue = "홍익대학교 졸업전시회", description = "상영회 제목") + private String screeningTitle; + + @Schema(defaultValue = "이한비", description = "주최자명") + private String hostName; + + @Schema(defaultValue = "졸업상영", description = "카테고리(\"졸업상영\"/\"과제상영\"/\"정기상영\"/\"특별상영\"/\"기타\")") + private Category category; + + @Schema(defaultValue = "2023-02-13", description = "시작 날짜") + private LocalDateTime screeningStartDate; + + @Schema(defaultValue = "2023-02-14", description = "종료 날짜") + private LocalDateTime screeningEndDate; + + @Schema(defaultValue = "13:00", description = "시작 시간") + private LocalDateTime screeningStartTime; + + @Schema(defaultValue = "홍익대학교", description = "주최 장소") + private String location; + + @Schema(defaultValue = "졸업 작품보러오세요", description = "상영회 정보") + private String information; + + @Schema(defaultValue = "https://sdhgfhsdjkfsjjgsh.com", description = "신청 폼 링크") + private String formUrl; + + @Schema(defaultValue = "010-0000-0000", description = "주최자 전화번호") + private String hostPhoneNumber; + + @Schema(defaultValue = "email1111@email.com", description = "주최자 이메일") + private String hostEmail; + @Schema(defaultValue = "true", description = "정책 동의 여부") + private boolean hasAgreed; + + @Schema(defaultValue = "false", description = "정책 동의 여부") + private boolean isPrivate; + + @Schema(defaultValue = "false", description = "찜하기 여부") + private boolean isBookmarked; + + @Schema(defaultValue = "false", description = "리뷰 여부") + private boolean isReviewed; + + + @Builder + public ScreeningInfoResponse(Long screeningId, + String screeningTitle, String posterImgUrl, String hostName, String hostEmail, String hostPhoneNumber , String location, String formUrl, + String information, boolean hasAgreed, Category category, LocalDateTime screeningStartDate, LocalDateTime screeningEndDate, LocalDateTime screeningStartTime, + boolean isPrivate, boolean isBookmarked, boolean isReviewed + ) { + this.screeningId = screeningId; + this.screeningTitle = screeningTitle; + this.posterImgUrl = posterImgUrl; + this.hostName = hostName; + this.location = location; + this.formUrl = formUrl; + this.information = information; + this.hasAgreed = hasAgreed; + this.screeningStartDate = screeningStartDate; + this.screeningEndDate = screeningEndDate; + this.screeningStartTime = screeningStartTime; + this.category = category; + this.hostEmail = hostEmail; + this.hostPhoneNumber = hostPhoneNumber; + this.isPrivate = isPrivate; + this.isBookmarked = isBookmarked; + this.isReviewed = isReviewed; + } + + public static ScreeningInfoResponse from(Screening screening,boolean isBookmarked, boolean isReviewed) { + return ScreeningInfoResponse .builder() + .screeningId(screening.getId()) + .screeningTitle(screening.getTitle()) + .posterImgUrl(screening.getPosterImgUrl()) + .hostName(screening.getHostInfo().getHostName()) + .location(screening.getLocation()) + .formUrl(screening.getParticipationUrl()) + .information(screening.getInformation()) + .hasAgreed(screening.isHasAgreed()) + .screeningStartDate(screening.getScreeningStartDate()) + .screeningEndDate(screening.getScreeningEndDate()) + .screeningStartTime(screening.getScreeningStartTime()) + .category(screening.getCategory()) + .hostEmail(screening.getHostInfo().getHostEmail()) + .isPrivate(screening.isPrivate()) + .isBookmarked(isBookmarked) + .isReviewed(isReviewed) + .build(); + } + +} diff --git a/Api/src/main/java/com/example/api/screening/service/GetPastScreeningListUseCase.java b/Api/src/main/java/com/example/api/screening/service/GetPastScreeningListUseCase.java new file mode 100644 index 0000000..1cbc985 --- /dev/null +++ b/Api/src/main/java/com/example/api/screening/service/GetPastScreeningListUseCase.java @@ -0,0 +1,22 @@ +package com.example.api.screening.service; + +import com.example.adaptor.UseCase; +import com.example.api.config.security.SecurityUtil; +import com.example.domains.screening.adaptor.ScreeningAdaptor; +import com.example.domains.screening.entity.Screening; +import com.example.domains.userscreening.adaptor.UserScreeningAdaptor; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@UseCase +@RequiredArgsConstructor +public class GetPastScreeningListUseCase { + private final ScreeningAdaptor screeningAdaptor; + public List execute() { + Long userId = SecurityUtil.getCurrentUserId(); + + List screenings = screeningAdaptor.getBookmarkedScreenings(userId); + return screenings; + } +} diff --git a/Api/src/main/java/com/example/api/screening/service/GetScreeningUseCase.java b/Api/src/main/java/com/example/api/screening/service/GetScreeningUseCase.java index d3c111e..60a2516 100644 --- a/Api/src/main/java/com/example/api/screening/service/GetScreeningUseCase.java +++ b/Api/src/main/java/com/example/api/screening/service/GetScreeningUseCase.java @@ -2,10 +2,14 @@ import com.example.adaptor.UseCase; import com.example.api.config.security.SecurityUtil; +import com.example.api.screening.dto.response.ScreeningInfoResponse; import com.example.api.screening.dto.response.ScreeningResponse; import com.example.api.screening.dto.response.ScreeningUploadResponse; import com.example.domains.screening.adaptor.ScreeningAdaptor; import com.example.domains.screening.entity.Screening; +import com.example.domains.screeningReview.adaptor.ReviewAdaptor; +import com.example.domains.screeningReview.entity.ScreeningReview; +import com.example.domains.user.entity.User; import com.example.domains.user.validator.UserValidator; import com.example.domains.userscreening.adaptor.UserScreeningAdaptor; import com.example.domains.userscreening.entity.UserScreening; @@ -19,12 +23,34 @@ public class GetScreeningUseCase { private final UserValidator userValidator; private final ScreeningAdaptor screeningAdaptor; private final UserScreeningAdaptor userScreeningAdaptor; + private final ReviewAdaptor screeningReviewAdaptor; + boolean isReviewed = false; + boolean isBookMarked = false; - public Screening execute(Long screeningId) { + public ScreeningInfoResponse execute(Long screeningId) { Long userId = SecurityUtil.getCurrentUserId(); validateExecution(userId); Screening screening = screeningAdaptor.findById(screeningId); - return screening; + + if(!validateUserScreening(userId,screeningId)){ + isReviewed = false; + isBookMarked = false; + } else { + UserScreening userScreening = userScreeningAdaptor.findByUserAndScreening(userId,screeningId); + isReviewed = validateScreeningReview(userScreening.getId()); + isBookMarked = userScreening.isBookmarked(); + }; + + + return ScreeningInfoResponse.from(screening,isReviewed,isBookMarked); + } + + private boolean validateUserScreening(Long userId, Long screeningId) { + return userScreeningAdaptor.existsByUserAndScreening(userId,screeningId); + } + + private boolean validateScreeningReview(Long id) { + return screeningReviewAdaptor.checkIfExists(id); } private void validateExecution(Long userId) { diff --git a/Api/src/main/java/com/example/api/user/controller/UserController.java b/Api/src/main/java/com/example/api/user/controller/UserController.java index 6f98dab..41908a3 100644 --- a/Api/src/main/java/com/example/api/user/controller/UserController.java +++ b/Api/src/main/java/com/example/api/user/controller/UserController.java @@ -1,8 +1,12 @@ package com.example.api.user.controller; import com.example.api.config.security.SecurityUtil; +import com.example.api.user.model.dto.DuplicateCheckResponse; import com.example.api.user.model.dto.GetUserInfoResponse; +import com.example.api.user.model.dto.UpdateUserInfoRequest; +import com.example.api.user.service.CheckDuplicateUseCase; import com.example.api.user.service.GetUserInfoUseCase; +import com.example.api.user.service.PatchUserInfoUseCase; import com.example.domains.user.enums.Genre; import com.example.domains.user.exception.exceptions.UserNotFoundException; import com.example.domains.user.service.UserService; @@ -12,10 +16,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -27,6 +28,8 @@ public class UserController { private final GetUserInfoUseCase getUserInfoUseCase; private final UserService userService; + private final PatchUserInfoUseCase patchUserInfoUseCase; + private final CheckDuplicateUseCase checkDuplicateUseCase; @Operation(summary = "내 정보를 가져옵니다.") @GetMapping(value = "/info") @@ -46,4 +49,14 @@ public ResponseEntity> getUserGenres() { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } } + + @PatchMapping + public void updateUserNickname(@RequestBody UpdateUserInfoRequest request) { + patchUserInfoUseCase.execute(request); + } + + @PostMapping("/check") + public DuplicateCheckResponse checkDuplicate(@RequestBody UpdateUserInfoRequest request) { + return checkDuplicateUseCase.execute(request); + } } diff --git a/Api/src/main/java/com/example/api/user/model/dto/DuplicateCheckResponse.java b/Api/src/main/java/com/example/api/user/model/dto/DuplicateCheckResponse.java new file mode 100644 index 0000000..7a1415a --- /dev/null +++ b/Api/src/main/java/com/example/api/user/model/dto/DuplicateCheckResponse.java @@ -0,0 +1,22 @@ +package com.example.api.user.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class DuplicateCheckResponse { + @Schema(defaultValue = "true", description = "중복 여부") + private boolean isDuplicate; + + @Builder + public DuplicateCheckResponse(boolean isDuplicate) { + this.isDuplicate = isDuplicate; + } + + public static DuplicateCheckResponse from(boolean isDuplicate) { + return DuplicateCheckResponse.builder() + .isDuplicate(isDuplicate) + .build(); + } +} diff --git a/Api/src/main/java/com/example/api/user/model/dto/UpdateUserInfoRequest.java b/Api/src/main/java/com/example/api/user/model/dto/UpdateUserInfoRequest.java new file mode 100644 index 0000000..02f6007 --- /dev/null +++ b/Api/src/main/java/com/example/api/user/model/dto/UpdateUserInfoRequest.java @@ -0,0 +1,10 @@ +package com.example.api.user.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class UpdateUserInfoRequest { + @Schema(defaultValue = "닉네임", description = "닉네임") + private String nickname; +} \ No newline at end of file diff --git a/Api/src/main/java/com/example/api/user/service/CheckDuplicateUseCase.java b/Api/src/main/java/com/example/api/user/service/CheckDuplicateUseCase.java new file mode 100644 index 0000000..37b3df5 --- /dev/null +++ b/Api/src/main/java/com/example/api/user/service/CheckDuplicateUseCase.java @@ -0,0 +1,20 @@ +package com.example.api.user.service; + +import com.example.adaptor.UseCase; +import com.example.api.user.model.dto.DuplicateCheckResponse; +import com.example.api.user.model.dto.UpdateUserInfoRequest; +import com.example.domains.user.adaptor.UserAdaptor; +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class CheckDuplicateUseCase { + private final UserAdaptor userAdaptor; + public DuplicateCheckResponse execute(UpdateUserInfoRequest request) { + if(userAdaptor.existsByNickname(request.getNickname())){ + return DuplicateCheckResponse.from(true); + } else { + return DuplicateCheckResponse.from(false); + } + } +} diff --git a/Api/src/main/java/com/example/api/user/service/PatchUserInfoUseCase.java b/Api/src/main/java/com/example/api/user/service/PatchUserInfoUseCase.java new file mode 100644 index 0000000..879d89c --- /dev/null +++ b/Api/src/main/java/com/example/api/user/service/PatchUserInfoUseCase.java @@ -0,0 +1,23 @@ +package com.example.api.user.service; + +import com.example.adaptor.UseCase; +import com.example.api.config.security.SecurityUtil; +import com.example.api.user.model.dto.UpdateUserInfoRequest; +import com.example.domains.user.adaptor.UserAdaptor; +import com.example.domains.user.entity.User; +import com.example.domains.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@UseCase +@RequiredArgsConstructor +public class PatchUserInfoUseCase { + private final UserService userService; + + @Transactional + public void execute(UpdateUserInfoRequest request) { + Long userId = SecurityUtil.getCurrentUserId(); + + userService.updateUserById(userId,request.getNickname()); + } +} diff --git a/Core/src/main/java/com/example/error/GlobalErrorCode.java b/Core/src/main/java/com/example/error/GlobalErrorCode.java index bfc34a7..19635a5 100644 --- a/Core/src/main/java/com/example/error/GlobalErrorCode.java +++ b/Core/src/main/java/com/example/error/GlobalErrorCode.java @@ -20,9 +20,6 @@ public enum GlobalErrorCode implements BaseErrorCode { _INTERNAL_SERVER_ERROR(INTERNAL_SERVER, "GLOBAL_500_1", "서버 오류. 관리자에게 문의 부탁드립니다."), INVALID_OAUTH_PROVIDER(INTERNAL_SERVER, "GLOBAL_500_2", "지원하지 않는 OAuth Provider 입니다."), SECURITY_CONTEXT_NOT_FOUND(INTERNAL_SERVER, "GLOBAL_500_3", "security context not found"), - INVALID_LOCK_IDENTIFIER(INTERNAL_SERVER, "GLOBAL_500_4", "잘못된 lock identifier 입니다."), - ALREADY_REDISSON_UNLOCK(INTERNAL_SERVER, "GLOBAL_500_5", "Redisson Lock Already UnLock"), - INTERRUPTED_REDISSON(INTERNAL_SERVER, "GLOBAL_500_6", "Redisson interruption"), /** 토큰 에러 * */ INVALID_TOKEN(UNAUTHORIZED, "AUTH_401_2", "올바르지 않은 토큰입니다."), diff --git a/Domain/src/main/java/com/example/domains/common/ScheduleService.java b/Domain/src/main/java/com/example/domains/common/ScheduleService.java index 691368b..1a74e97 100644 --- a/Domain/src/main/java/com/example/domains/common/ScheduleService.java +++ b/Domain/src/main/java/com/example/domains/common/ScheduleService.java @@ -4,6 +4,8 @@ import com.example.domains.screening.entity.Screening; import com.example.domains.userscreening.adaptor.UserScreeningAdaptor; import com.example.domains.userscreening.entity.UserScreening; +import com.example.fcm.request.NotificationRequest; +import com.example.fcm.service.FcmService; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -15,12 +17,12 @@ @RequiredArgsConstructor public class ScheduleService { -// private final FcmService fcmService; -// -// private final UserScreeningAdaptor userScreeningAdaptor; -// private final ScreeningAdaptor screeningAdaptor; -// -// private static final String NOTIFICATION_TITLE = "상영회 하루 전 알림"; + private final FcmService fcmService; + + private final UserScreeningAdaptor userScreeningAdaptor; + private final ScreeningAdaptor screeningAdaptor; + + private static final String NOTIFICATION_TITLE = "상영회 하루 전 알림"; // @Scheduled(cron = "0 0/1 * * * *") // private void notifyReservation() { // LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0); @@ -45,6 +47,30 @@ public class ScheduleService { // } // } // } +//@Scheduled(cron = "0 0/1 * * * *") +//private void notifyReservation() { +// LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0); +// //LocalDateTime reservationTime = now.plusDays(1); +// +// //userScreening에서 isBookMarked인 것들 중에서 user id, screening id가져와서 List List +// //screening에서 startDate가져와서 startDate가 내일이면 알람을 보낼 수 있게 짜봐 fcm이랑 스프링 쓰고 있어 +// +// List bookmarkedUserScreenings = userScreeningAdaptor.findByBookMarked(); +// +// for (UserScreening userScreening : bookmarkedUserScreenings) { +// LocalDateTime screeningStartDate = userScreening.getScreening().getScreeningStartDate(); +// +// // 오늘이 screeningStartDate의 하루 전인 경우 해당 Screening을 가져옴 +// if (screeningStartDate.toLocalDate().isEqual(now.toLocalDate())) { +// List tomorrowScreenings = screeningAdaptor.findByStartDate(screeningStartDate); +// Long userId = userScreening.getUser().getId(); +// List notificationRequests = tomorrowScreenings.stream() +// .map(tomorrowScreening -> new NotificationRequest(tomorrowScreening, userId, NOTIFICATION_TITLE)) +// .toList(); +// sendNotifications(notificationRequests); +// } +// } +//} // // private void sendNotifications(List requests) { // for (NotificationRequest notificationRequest : requests) { diff --git a/Domain/src/main/java/com/example/domains/screening/adaptor/ScreeningAdaptor.java b/Domain/src/main/java/com/example/domains/screening/adaptor/ScreeningAdaptor.java index 4fe5849..8e8eb6f 100644 --- a/Domain/src/main/java/com/example/domains/screening/adaptor/ScreeningAdaptor.java +++ b/Domain/src/main/java/com/example/domains/screening/adaptor/ScreeningAdaptor.java @@ -88,7 +88,7 @@ public List getTopThree() { QScreening.screening.isPrivate.eq(false) ) .groupBy(QScreening.screening.id, QUserScreening.userScreening.id) - .orderBy(QScreening.screening.screeningStartDate.asc()) + .orderBy(QScreening.screening.screeningStartDate.desc()) .limit(3) .fetch(); } @@ -164,6 +164,7 @@ public List getMostReviewed() { } public List getBookmarkedScreenings(Long userId) { + //찜한 스크리닝 중에서 날짜 지난 걸 가져오고, userId, screeningId를 가지고 userScreening를 가져와서 screeningReview에 review가 있는지에 대한 여부도 같이 넘겨주는 로직 LocalDateTime currentDateTime = LocalDateTime.now(); List bookmarkedScreenings = jpaQueryFactory diff --git a/Domain/src/main/java/com/example/domains/screeningReview/adaptor/ReviewAdaptor.java b/Domain/src/main/java/com/example/domains/screeningReview/adaptor/ReviewAdaptor.java index 4cee929..2813335 100644 --- a/Domain/src/main/java/com/example/domains/screeningReview/adaptor/ReviewAdaptor.java +++ b/Domain/src/main/java/com/example/domains/screeningReview/adaptor/ReviewAdaptor.java @@ -124,4 +124,7 @@ public void incrementComplaintCount(ScreeningReview screeningReview) { .execute(); } + public boolean checkIfExists(Long id) { + return screeningReviewRepository.existsById(id); + } } diff --git a/Domain/src/main/java/com/example/domains/user/adaptor/UserAdaptor.java b/Domain/src/main/java/com/example/domains/user/adaptor/UserAdaptor.java index 9a7f06a..820af18 100644 --- a/Domain/src/main/java/com/example/domains/user/adaptor/UserAdaptor.java +++ b/Domain/src/main/java/com/example/domains/user/adaptor/UserAdaptor.java @@ -40,4 +40,6 @@ public Boolean existsById(Long userId) { return userRepository.existsById(userId); } +// public Boolean existsbyNickname(String ) + } \ No newline at end of file diff --git a/Domain/src/main/java/com/example/domains/user/entity/User.java b/Domain/src/main/java/com/example/domains/user/entity/User.java index 0857b6f..8e4a957 100644 --- a/Domain/src/main/java/com/example/domains/user/entity/User.java +++ b/Domain/src/main/java/com/example/domains/user/entity/User.java @@ -5,6 +5,7 @@ import com.example.domains.user.enums.*; import com.example.domains.user.exception.exceptions.AlreadyDeletedUserException; import com.example.error.exception.ServerForbiddenException; +import com.example.fcm.entity.FCMToken; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; @@ -54,8 +55,8 @@ public class User extends BaseTimeEntity { // private String phoneNumber; private String appleEmail; -// @OneToOne(mappedBy = "user") -// private FcmToken fcmToken; + @OneToOne(mappedBy = "user") + private FCMToken fcmToken; @Builder private User ( @@ -110,12 +111,11 @@ public void validateUserStateNormal() { } } - public void updateInfo(String name, List genres) { + public void updateInfo(String name) { if (!UserState.ACTIVE.equals(this.userState)) { throw ServerForbiddenException.EXCEPTION; } - this.nickname = nickname; - this.genres = genres; + this.nickname = name; } diff --git a/Domain/src/main/java/com/example/domains/user/service/UserService.java b/Domain/src/main/java/com/example/domains/user/service/UserService.java index 4968a78..6e01c1c 100644 --- a/Domain/src/main/java/com/example/domains/user/service/UserService.java +++ b/Domain/src/main/java/com/example/domains/user/service/UserService.java @@ -62,6 +62,11 @@ public void deleteUserById(Long userId) { } + public void updateUserById(Long userId,String name) { + User user = userAdaptor.findById(userId); + user.updateInfo(name); + } + public List getUserGenres(Long userId) { // Retrieve the user from the repository diff --git a/Domain/src/main/java/com/example/domains/userscreening/adaptor/UserScreeningAdaptor.java b/Domain/src/main/java/com/example/domains/userscreening/adaptor/UserScreeningAdaptor.java index 151c60e..96c4a64 100644 --- a/Domain/src/main/java/com/example/domains/userscreening/adaptor/UserScreeningAdaptor.java +++ b/Domain/src/main/java/com/example/domains/userscreening/adaptor/UserScreeningAdaptor.java @@ -81,6 +81,7 @@ public List findBookmarkedUserScreening(Long userId) { .join(userScreening).on(userScreening.screening.eq(screening)) // Join with Screening entity .where( userScreening.isHost.eq(false), + userScreening.isBookmarked.eq(true), userScreening.user.id.eq(userId) ) .fetch(); @@ -115,4 +116,8 @@ public List getReviewListByScreening(Long userId, Lo public List findByScreeningId(Long screeningId) { return userScreeningRepository.findByScreeningId(screeningId); } + + public boolean existsByUserAndScreening(Long userId, Long screeningId) { + return userScreeningRepository.existsByUserIdAndScreeningId(userId, screeningId); + } } diff --git a/Domain/src/main/java/com/example/domains/userscreening/repository/UserScreeningRepository.java b/Domain/src/main/java/com/example/domains/userscreening/repository/UserScreeningRepository.java index ef87b98..861bbf2 100644 --- a/Domain/src/main/java/com/example/domains/userscreening/repository/UserScreeningRepository.java +++ b/Domain/src/main/java/com/example/domains/userscreening/repository/UserScreeningRepository.java @@ -15,4 +15,6 @@ public interface UserScreeningRepository extends JpaRepository findByIsBookmarked(boolean b); List findByScreeningId(Long screeningId); + + boolean existsByUserIdAndScreeningId(Long userId, Long screeningId); } diff --git a/Domain/src/main/java/com/example/fcm/entity/FCMToken.java b/Domain/src/main/java/com/example/fcm/entity/FCMToken.java new file mode 100644 index 0000000..4ad25a0 --- /dev/null +++ b/Domain/src/main/java/com/example/fcm/entity/FCMToken.java @@ -0,0 +1,37 @@ +package com.example.fcm.entity; + +import com.example.domains.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FCMToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "user_id", nullable = false) + @OneToOne(fetch = FetchType.LAZY) + private User user; + + @Column(name = "fcm_token",columnDefinition = "TEXT") + private String fcmToken; + + @Builder(access = AccessLevel.PRIVATE) + private FCMToken(User user, String fcmToken) { + this.user = user; + this.fcmToken = fcmToken; + } + + public static FCMToken createFCMToken(User user, String fcmToken) { + return FCMToken.builder() + .user(user) + .fcmToken(fcmToken) + .build(); + } +} diff --git a/Domain/src/main/java/com/example/fcm/repository/FcmRepository.java b/Domain/src/main/java/com/example/fcm/repository/FcmRepository.java new file mode 100644 index 0000000..9bf411e --- /dev/null +++ b/Domain/src/main/java/com/example/fcm/repository/FcmRepository.java @@ -0,0 +1,7 @@ +package com.example.fcm.repository; + +import com.example.fcm.entity.FCMToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FcmRepository extends JpaRepository { +} \ No newline at end of file diff --git a/Domain/src/main/java/com/example/fcm/request/FcmRegistrationRequest.java b/Domain/src/main/java/com/example/fcm/request/FcmRegistrationRequest.java new file mode 100644 index 0000000..c941422 --- /dev/null +++ b/Domain/src/main/java/com/example/fcm/request/FcmRegistrationRequest.java @@ -0,0 +1,16 @@ +package com.example.fcm.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.constraints.NotEmpty; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class FcmRegistrationRequest { + @NotEmpty + private String fcmToken; +} \ No newline at end of file diff --git a/Domain/src/main/java/com/example/fcm/request/NotificationRequest.java b/Domain/src/main/java/com/example/fcm/request/NotificationRequest.java new file mode 100644 index 0000000..53e4eba --- /dev/null +++ b/Domain/src/main/java/com/example/fcm/request/NotificationRequest.java @@ -0,0 +1,17 @@ +package com.example.fcm.request; + +import com.example.domains.screening.entity.Screening; +import lombok.Getter; + +@Getter +public class NotificationRequest { + private Long userId; + private String title; + private String body; + + public NotificationRequest(Screening screening, Long user, String title) { + this.userId= user; + this.title = title; + this.body = screening.getInformation(); + } +} diff --git a/Domain/src/main/java/com/example/fcm/service/FcmService.java b/Domain/src/main/java/com/example/fcm/service/FcmService.java new file mode 100644 index 0000000..135a9ad --- /dev/null +++ b/Domain/src/main/java/com/example/fcm/service/FcmService.java @@ -0,0 +1,63 @@ +package com.example.fcm.service; + +import com.example.domains.user.entity.User; +import com.example.domains.user.exception.exceptions.UserNotFoundException; +import com.example.domains.user.repository.UserRepository; +import com.example.fcm.entity.FCMToken; +import com.example.fcm.repository.FcmRepository; +import com.example.fcm.request.FcmRegistrationRequest; +import com.example.fcm.request.NotificationRequest; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FcmService { + private final FirebaseMessaging firebaseMessaging; + private final UserRepository userRepository; + private final FcmRepository fcmRepository; + public void registerFCMToken(Long userId, FcmRegistrationRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> UserNotFoundException.EXCEPTION); + FCMToken fcmToken = FCMToken.createFCMToken(user, request.getFcmToken()); + + fcmRepository.save(fcmToken); + } + + + public void sendMessageByToken(NotificationRequest request) { + User user = userRepository.findById(request.getUserId()).orElseThrow(IllegalArgumentException::new); + + String fcmToken = user.getFcmToken().getFcmToken(); + if (!fcmToken.isEmpty()) { + Message message = getMessage(request, fcmToken); + + try { + firebaseMessaging.send(message); + log.info("푸시 알림 전송 완료 userId = {}", user.getId()); + } catch (FirebaseMessagingException e) { + log.info("푸시 알림 전송 실패 userId = {}", user.getId()); + throw new RuntimeException(e); + } + } + } + + private static Message getMessage(NotificationRequest request, String fcmToken) { + Notification notification = Notification.builder() + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build(); + + Message message = Message.builder() + .setToken(fcmToken) + .setNotification(notification) + .build(); + return message; + } +} \ No newline at end of file diff --git a/Infra/src/main/resources/application-infra.yml b/Infra/src/main/resources/application-infra.yml index 70d77fb..919bd5d 100644 --- a/Infra/src/main/resources/application-infra.yml +++ b/Infra/src/main/resources/application-infra.yml @@ -22,7 +22,7 @@ cloud: auto: false app: - firebase-configuration-file: popcornmate-d7ca1-firebase-adminsdk-svbpw-fce737e873.json + firebase-configuration-file: popcornmate-d7ca1-firebase-adminsdk-svbpw-343677f710.json --- spring: @@ -49,5 +49,5 @@ cloud: auto: false app: - firebase-configuration-file: popcornmate-d7ca1-firebase-adminsdk-svbpw-fce737e873.json + firebase-configuration-file: popcornmateprod-firebase-adminsdk-yvb81-02b4302a03.json ---