diff --git a/.github/workflows/code_test_pipeline.yaml b/.github/workflows/code_test_pipeline.yaml new file mode 100644 index 0000000..4d6c153 --- /dev/null +++ b/.github/workflows/code_test_pipeline.yaml @@ -0,0 +1,75 @@ +name: Code Test Pipeline +on: + pull_request: + branches: ["dev", "main"] + +permissions: + contents: read + checks: write + +env: + DB_URL: ${{ secrets.DB_URL }} + DB_USERNAME: ${{ secrets.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + SPRING_ACTIVE_PROFILE: ${{ vars.SPRING_ACTIVE_PROFILE }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + FCM_PROJECT_ID: ${{ secrets.FCM_PROJECT_ID }} + AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + +jobs: + formatting: + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + - name: Formatting Code with Google Java Style Guide + uses: axel-op/googlejavaformat-action@v3 + with: + args: "--replace --aosp" + github-token: ${{ secrets.GITHUB_TOKEN }} + + unit-test: + runs-on: ubuntu-latest + permissions: write-all + needs: [formatting] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: 'gradle' + + - name: Create firebase_admin_sdk_private_key.json from Secrets + run: | + mkdir -p $GITHUB_WORKSPACE/src/main/resources/key + echo "${{ secrets.FIREBASE_ADMIN_SDK_PRIVATE_KEY }}" | base64 --decode > $GITHUB_WORKSPACE/src/main/resources/key/firebase_admin_sdk_private_key.json + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test with Gradle + run: ./gradlew --info test + + - name: Publish Test Report + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: "**/build/test-results/test/TEST-*.xml" diff --git a/.gitignore b/.gitignore index 20c0b81..a66d729 100644 --- a/.gitignore +++ b/.gitignore @@ -38,8 +38,7 @@ out/ ### -**/src/main/resources/application.properties -**/src/main/resources/application.yml - - src/main/java/earlybird/earlybird/user/TestController.java +src/main/resources/key/firebase_admin_sdk_private_key.json + +logs/*.log diff --git a/build.gradle b/build.gradle index 0cb165e..f683fe9 100644 --- a/build.gradle +++ b/build.gradle @@ -27,12 +27,17 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation "org.springframework.retry:spring-retry" + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'com.h2database:h2' implementation 'org.springframework.boot:spring-boot-starter-webflux' @@ -40,6 +45,14 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'com.google.firebase:firebase-admin:9.3.0' + + testImplementation 'org.awaitility:awaitility:4.2.2' + + implementation(platform("software.amazon.awssdk:bom:2.27.21")) + implementation("software.amazon.awssdk:sdk-core") + implementation("software.amazon.awssdk:eventbridge") + implementation("software.amazon.awssdk:dynamodb") } tasks.named('test') { diff --git a/src/main/java/earlybird/earlybird/EarlybirdApplication.java b/src/main/java/earlybird/earlybird/EarlybirdApplication.java index 317b30d..382930f 100644 --- a/src/main/java/earlybird/earlybird/EarlybirdApplication.java +++ b/src/main/java/earlybird/earlybird/EarlybirdApplication.java @@ -2,12 +2,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling +@EnableJpaAuditing @SpringBootApplication public class EarlybirdApplication { - public static void main(String[] args) { - SpringApplication.run(EarlybirdApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(EarlybirdApplication.class, args); + } } diff --git a/src/main/java/earlybird/earlybird/appointment/controller/AppointmentController.java b/src/main/java/earlybird/earlybird/appointment/controller/AppointmentController.java new file mode 100644 index 0000000..3d2e2e6 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/controller/AppointmentController.java @@ -0,0 +1,64 @@ +package earlybird.earlybird.appointment.controller; + +import earlybird.earlybird.appointment.controller.request.CreateAppointmentRequest; +import earlybird.earlybird.appointment.controller.request.UpdateAppointmentRequest; +import earlybird.earlybird.appointment.controller.response.CreateAppointmentResponse; +import earlybird.earlybird.appointment.service.CreateAppointmentService; +import earlybird.earlybird.appointment.service.DeleteAppointmentService; +import earlybird.earlybird.appointment.service.UpdateAppointmentService; +import earlybird.earlybird.appointment.service.request.CreateAppointmentServiceRequest; +import earlybird.earlybird.appointment.service.request.DeleteAppointmentServiceRequest; +import earlybird.earlybird.appointment.service.request.UpdateAppointmentServiceRequest; +import earlybird.earlybird.appointment.service.response.CreateAppointmentServiceResponse; + +import jakarta.validation.Valid; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/appointments") +@RestController +public class AppointmentController { + + private final CreateAppointmentService createAppointmentService; + private final UpdateAppointmentService updateAppointmentService; + private final DeleteAppointmentService deleteAppointmentService; + + @PostMapping + public ResponseEntity createAppointment( + @Valid @RequestBody CreateAppointmentRequest request) { + + CreateAppointmentServiceRequest serviceRequest = + request.toCreateAppointmentServiceRequest(); + CreateAppointmentServiceResponse serviceResponse = + createAppointmentService.create(serviceRequest); + + return ResponseEntity.ok(CreateAppointmentResponse.from(serviceResponse)); + } + + @PatchMapping + public ResponseEntity updateAppointment( + @Valid @RequestBody UpdateAppointmentRequest request) { + + UpdateAppointmentServiceRequest serviceRequest = + request.toUpdateAppointmentServiceRequest(); + updateAppointmentService.update(serviceRequest); + + return ResponseEntity.ok().build(); + } + + @DeleteMapping + public ResponseEntity deleteAppointment( + @RequestHeader("appointmentId") Long appointmentId, + @RequestHeader("clientId") String clientId) { + + DeleteAppointmentServiceRequest serviceRequest = + new DeleteAppointmentServiceRequest(clientId, appointmentId); + deleteAppointmentService.delete(serviceRequest); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/controller/request/CreateAppointmentRequest.java b/src/main/java/earlybird/earlybird/appointment/controller/request/CreateAppointmentRequest.java new file mode 100644 index 0000000..3882e14 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/controller/request/CreateAppointmentRequest.java @@ -0,0 +1,59 @@ +package earlybird.earlybird.appointment.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import earlybird.earlybird.appointment.service.request.CreateAppointmentServiceRequest; +import earlybird.earlybird.common.DayOfWeekUtil; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class CreateAppointmentRequest { + @NotBlank private String clientId; + @NotBlank private String deviceToken; + @NotBlank private String appointmentName; + @NotNull private Duration movingDuration; + @NotNull private Duration preparationDuration; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime firstAppointmentTime; + + // 월화수목금토일 + @NotNull + @Size(min = 7, max = 7, message = "repeatDayOfWeek 의 길이는 7이어야 합니다.") + private List repeatDayOfWeekBoolList; + + public CreateAppointmentServiceRequest toCreateAppointmentServiceRequest() { + + List repeatDayOfWeekList = + DayOfWeekUtil.convertBooleanListToDayOfWeekList(this.repeatDayOfWeekBoolList); + + return CreateAppointmentServiceRequest.builder() + .clientId(this.clientId) + .deviceToken(this.deviceToken) + .appointmentName(this.appointmentName) + .firstAppointmentTime(this.firstAppointmentTime) + .preparationDuration(this.preparationDuration) + .movingDuration(this.movingDuration) + .repeatDayOfWeekList(repeatDayOfWeekList) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/controller/request/UpdateAppointmentRequest.java b/src/main/java/earlybird/earlybird/appointment/controller/request/UpdateAppointmentRequest.java new file mode 100644 index 0000000..aa0f6c5 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/controller/request/UpdateAppointmentRequest.java @@ -0,0 +1,60 @@ +package earlybird.earlybird.appointment.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import earlybird.earlybird.appointment.domain.AppointmentUpdateType; +import earlybird.earlybird.appointment.service.request.UpdateAppointmentServiceRequest; +import earlybird.earlybird.common.DayOfWeekUtil; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import lombok.Getter; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class UpdateAppointmentRequest { + + @NotNull private Long appointmentId; + @NotBlank private String clientId; + @NotBlank private String deviceToken; + @NotBlank private String appointmentName; + @NotNull private Duration movingDuration; + @NotNull private Duration preparationDuration; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime firstAppointmentTime; + + // 월화수목금토일 + @NotNull + @Size(min = 7, max = 7, message = "repeatDayOfWeek 의 길이는 7이어야 합니다.") + private List repeatDayOfWeekBoolList; + + @NotNull private AppointmentUpdateType updateType; + + public UpdateAppointmentServiceRequest toUpdateAppointmentServiceRequest() { + List repeatDayOfWeekList = + DayOfWeekUtil.convertBooleanListToDayOfWeekList(this.repeatDayOfWeekBoolList); + + return UpdateAppointmentServiceRequest.builder() + .appointmentId(appointmentId) + .clientId(clientId) + .deviceToken(deviceToken) + .appointmentName(appointmentName) + .movingDuration(movingDuration) + .preparationDuration(preparationDuration) + .firstAppointmentTime(firstAppointmentTime) + .repeatDayOfWeekList(repeatDayOfWeekList) + .updateType(updateType) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/controller/response/CreateAppointmentResponse.java b/src/main/java/earlybird/earlybird/appointment/controller/response/CreateAppointmentResponse.java new file mode 100644 index 0000000..0fa8a36 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/controller/response/CreateAppointmentResponse.java @@ -0,0 +1,22 @@ +package earlybird.earlybird.appointment.controller.response; + +import earlybird.earlybird.appointment.service.response.CreateAppointmentServiceResponse; + +import lombok.*; + +@Getter +public class CreateAppointmentResponse { + private final Long createdAppointmentId; + + @Builder + private CreateAppointmentResponse(Long createdAppointmentId) { + this.createdAppointmentId = createdAppointmentId; + } + + public static CreateAppointmentResponse from( + CreateAppointmentServiceResponse createAppointmentServiceResponse) { + return CreateAppointmentResponse.builder() + .createdAppointmentId(createAppointmentServiceResponse.getCreatedAppointmentId()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/controller/response/UpdateAppointmentResponse.java b/src/main/java/earlybird/earlybird/appointment/controller/response/UpdateAppointmentResponse.java new file mode 100644 index 0000000..8c40ee7 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/controller/response/UpdateAppointmentResponse.java @@ -0,0 +1,3 @@ +package earlybird.earlybird.appointment.controller.response; + +public class UpdateAppointmentResponse {} diff --git a/src/main/java/earlybird/earlybird/appointment/domain/Appointment.java b/src/main/java/earlybird/earlybird/appointment/domain/Appointment.java new file mode 100644 index 0000000..f901909 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/domain/Appointment.java @@ -0,0 +1,134 @@ +package earlybird.earlybird.appointment.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; + +import jakarta.persistence.*; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +@SQLDelete(sql = "UPDATE appointment SET is_deleted = true WHERE appointment_id = ?") +@SQLRestriction("is_deleted = false") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Appointment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "appointment_id") + private Long id; + + @Column(nullable = false) + private String appointmentName; + + @Column(nullable = false) + private String clientId; + + @Column(nullable = false) + private String deviceToken; + + @JsonIgnoreProperties({"appointment"}) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "appointment") + private List fcmNotifications = new ArrayList<>(); + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "appointment") + @SQLRestriction("is_deleted = false") + private List repeatingDays = new ArrayList<>(); + + private LocalTime appointmentTime; + private Duration preparationDuration; + private Duration movingDuration; + + @Column(nullable = false) + private Boolean isDeleted = false; + + @Builder + private Appointment( + String appointmentName, + String clientId, + String deviceToken, + LocalTime appointmentTime, + Duration preparationDuration, + Duration movingDuration, + List repeatingDayOfWeeks) { + this.appointmentName = appointmentName; + this.clientId = clientId; + this.deviceToken = deviceToken; + this.appointmentTime = appointmentTime; + this.preparationDuration = preparationDuration; + this.movingDuration = movingDuration; + setRepeatingDays(repeatingDayOfWeeks); + } + + public List getRepeatingDays() { + return repeatingDays.stream().filter(repeatingDay -> !repeatingDay.isDeleted()).toList(); + } + + public void addFcmNotification(FcmNotification fcmNotification) { + this.fcmNotifications.add(fcmNotification); + fcmNotification.setAppointment(this); + } + + public void removeFcmNotification(FcmNotification fcmNotification) { + fcmNotifications.remove(fcmNotification); + } + + public void addRepeatingDay(RepeatingDay repeatingDay) { + if (repeatingDays.stream().noneMatch(r -> r.getId().equals(repeatingDay.getId()))) { + this.repeatingDays.add(repeatingDay); + } + } + + public void setRepeatingDays(List dayOfWeeks) { + setRepeatingDaysEmpty(); + dayOfWeeks.forEach(dayOfWeek -> this.repeatingDays.add(new RepeatingDay(dayOfWeek, this))); + } + + public void setRepeatingDaysEmpty() { + this.repeatingDays.forEach(RepeatingDay::setDeleted); + } + + public void changeAppointmentName(String newName) { + this.appointmentName = newName; + } + + public void changeDeviceToken(String newToken) { + this.deviceToken = newToken; + } + + public void changePreparationDuration(Duration newDuration) { + this.preparationDuration = newDuration; + } + + public void changeMovingDuration(Duration newDuration) { + this.movingDuration = newDuration; + } + + public void changeAppointmentTime(LocalTime newTime) { + this.appointmentTime = newTime; + } + + public boolean isDeleted() { + return isDeleted; + } + + public void setDeleted() { + this.isDeleted = true; + setRepeatingDaysEmpty(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/domain/AppointmentRepository.java b/src/main/java/earlybird/earlybird/appointment/domain/AppointmentRepository.java new file mode 100644 index 0000000..80b3c9d --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/domain/AppointmentRepository.java @@ -0,0 +1,5 @@ +package earlybird.earlybird.appointment.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AppointmentRepository extends JpaRepository {} diff --git a/src/main/java/earlybird/earlybird/appointment/domain/AppointmentUpdateType.java b/src/main/java/earlybird/earlybird/appointment/domain/AppointmentUpdateType.java new file mode 100644 index 0000000..6769d35 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/domain/AppointmentUpdateType.java @@ -0,0 +1,13 @@ +package earlybird.earlybird.appointment.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AppointmentUpdateType { + POSTPONE("약속 미루기"), + MODIFY("일정 수정하기"); + + private final String type; +} diff --git a/src/main/java/earlybird/earlybird/appointment/domain/RepeatingDay.java b/src/main/java/earlybird/earlybird/appointment/domain/RepeatingDay.java new file mode 100644 index 0000000..06d344c --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/domain/RepeatingDay.java @@ -0,0 +1,50 @@ +package earlybird.earlybird.appointment.domain; + +import earlybird.earlybird.common.BaseTimeEntity; + +import jakarta.persistence.*; + +import lombok.*; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; +import org.springframework.lang.NonNull; + +import java.time.DayOfWeek; + +@SQLDelete(sql = "UPDATE repeating_day SET is_deleted = true WHERE repeating_day_id = ?") +@SQLRestriction("is_deleted = false") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class RepeatingDay extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "repeating_day_id", nullable = false) + private Long id; + + @NonNull + @Column(nullable = false) + private DayOfWeek dayOfWeek; + + @NonNull + @ManyToOne + @JoinColumn(name = "appointemnt_id", nullable = false) + private Appointment appointment; + + private Boolean isDeleted = false; + + public void setDeleted() { + this.isDeleted = true; + } + + public boolean isDeleted() { + return isDeleted; + } + + protected RepeatingDay(@NonNull DayOfWeek dayOfWeek, @NonNull Appointment appointment) { + this.dayOfWeek = dayOfWeek; + this.appointment = appointment; + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/domain/RepeatingDayRepository.java b/src/main/java/earlybird/earlybird/appointment/domain/RepeatingDayRepository.java new file mode 100644 index 0000000..c10f9ea --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/domain/RepeatingDayRepository.java @@ -0,0 +1,11 @@ +package earlybird.earlybird.appointment.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.DayOfWeek; +import java.util.List; + +public interface RepeatingDayRepository extends JpaRepository { + + List findAllByDayOfWeek(DayOfWeek dayOfWeek); +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/CreateAppointmentService.java b/src/main/java/earlybird/earlybird/appointment/service/CreateAppointmentService.java new file mode 100644 index 0000000..68c4493 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/CreateAppointmentService.java @@ -0,0 +1,61 @@ +package earlybird.earlybird.appointment.service; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.AppointmentRepository; +import earlybird.earlybird.appointment.service.request.CreateAppointmentServiceRequest; +import earlybird.earlybird.appointment.service.response.CreateAppointmentServiceResponse; +import earlybird.earlybird.common.LocalDateTimeUtil; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; +import earlybird.earlybird.scheduler.notification.service.NotificationInfoFactory; +import earlybird.earlybird.scheduler.notification.service.register.RegisterAllNotificationAtSchedulerService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Map; + +@RequiredArgsConstructor +@Service +public class CreateAppointmentService { + + private final AppointmentRepository appointmentRepository; + private final RegisterAllNotificationAtSchedulerService registerService; + private final NotificationInfoFactory factory; + + @Transactional + public CreateAppointmentServiceResponse create(CreateAppointmentServiceRequest request) { + Appointment appointment = createAppointmentInstance(request); + Appointment savedAppointment = appointmentRepository.save(appointment); + + LocalDateTime firstAppointmentTime = request.getFirstAppointmentTime(); + LocalDateTime movingTime = + LocalDateTimeUtil.subtractDuration( + firstAppointmentTime, request.getMovingDuration()); + LocalDateTime preparationTime = + LocalDateTimeUtil.subtractDuration(movingTime, request.getPreparationDuration()); + + Map notificationInfo = + factory.createTargetTimeMap(preparationTime, movingTime, firstAppointmentTime); + registerService.register(appointment, notificationInfo); + + return CreateAppointmentServiceResponse.builder() + .createdAppointmentId(savedAppointment.getId()) + .build(); + } + + private Appointment createAppointmentInstance(CreateAppointmentServiceRequest request) { + return Appointment.builder() + .appointmentName(request.getAppointmentName()) + .clientId(request.getClientId()) + .deviceToken(request.getDeviceToken()) + .repeatingDayOfWeeks(request.getRepeatDayOfWeekList()) + .appointmentTime(request.getFirstAppointmentTime().toLocalTime()) + .preparationDuration(request.getPreparationDuration()) + .movingDuration(request.getMovingDuration()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/DeleteAppointmentService.java b/src/main/java/earlybird/earlybird/appointment/service/DeleteAppointmentService.java new file mode 100644 index 0000000..1212878 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/DeleteAppointmentService.java @@ -0,0 +1,28 @@ +package earlybird.earlybird.appointment.service; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.service.request.DeleteAppointmentServiceRequest; +import earlybird.earlybird.scheduler.notification.service.deregister.DeregisterNotificationService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class DeleteAppointmentService { + + private final FindAppointmentService findAppointmentService; + private final DeregisterNotificationService deregisterNotificationService; + + @Transactional + public void delete(DeleteAppointmentServiceRequest request) { + Appointment appointment = + findAppointmentService.findBy(request.getAppointmentId(), request.getClientId()); + deregisterNotificationService.deregister( + request.toDeregisterFcmMessageAtSchedulerServiceRequest()); + appointment.setRepeatingDaysEmpty(); + appointment.setDeleted(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/FindAppointmentService.java b/src/main/java/earlybird/earlybird/appointment/service/FindAppointmentService.java new file mode 100644 index 0000000..aaa3bfe --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/FindAppointmentService.java @@ -0,0 +1,35 @@ +package earlybird.earlybird.appointment.service; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.AppointmentRepository; +import earlybird.earlybird.error.exception.AppointmentNotFoundException; +import earlybird.earlybird.error.exception.DeletedAppointmentException; +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterFcmMessageAtSchedulerServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class FindAppointmentService { + + private final AppointmentRepository appointmentRepository; + + public Appointment findBy(Long appointmentId, String clientId) { + Appointment appointment = + appointmentRepository + .findById(appointmentId) + .orElseThrow(AppointmentNotFoundException::new); + + if (!appointment.getClientId().equals(clientId)) throw new AppointmentNotFoundException(); + + if (appointment.isDeleted()) throw new DeletedAppointmentException(); + + return appointment; + } + + public Appointment findBy(DeregisterFcmMessageAtSchedulerServiceRequest request) { + return findBy(request.getAppointmentId(), request.getClientId()); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/UpdateAppointmentService.java b/src/main/java/earlybird/earlybird/appointment/service/UpdateAppointmentService.java new file mode 100644 index 0000000..d7b308d --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/UpdateAppointmentService.java @@ -0,0 +1,70 @@ +package earlybird.earlybird.appointment.service; + +import static earlybird.earlybird.appointment.domain.AppointmentUpdateType.MODIFY; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.AppointmentUpdateType; +import earlybird.earlybird.appointment.service.request.UpdateAppointmentServiceRequest; +import earlybird.earlybird.common.LocalDateTimeUtil; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; +import earlybird.earlybird.scheduler.notification.service.NotificationInfoFactory; +import earlybird.earlybird.scheduler.notification.service.deregister.DeregisterNotificationService; +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterNotificationServiceRequestFactory; +import earlybird.earlybird.scheduler.notification.service.register.RegisterAllNotificationAtSchedulerService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Map; + +@RequiredArgsConstructor +@Service +public class UpdateAppointmentService { + + private final DeregisterNotificationService deregisterNotificationService; + private final FindAppointmentService findAppointmentService; + private final RegisterAllNotificationAtSchedulerService registerService; + private final NotificationInfoFactory factory; + + @Transactional + public void update(UpdateAppointmentServiceRequest request) { + + Appointment appointment = + findAppointmentService.findBy(request.getAppointmentId(), request.getClientId()); + AppointmentUpdateType updateType = request.getUpdateType(); + + if (updateType.equals(MODIFY)) { + modifyAppointment(appointment, request); + } + + deregisterNotificationService.deregister( + DeregisterNotificationServiceRequestFactory.create(request)); + + // TODO: create service 코드와 겹치는 코드 개선 방향 찾아보기 + LocalDateTime firstAppointmentTime = request.getFirstAppointmentTime(); + LocalDateTime movingTime = + LocalDateTimeUtil.subtractDuration( + firstAppointmentTime, request.getMovingDuration()); + LocalDateTime preparationTime = + LocalDateTimeUtil.subtractDuration(movingTime, request.getPreparationDuration()); + + Map notificationInfo = + factory.createTargetTimeMap(preparationTime, movingTime, firstAppointmentTime); + + registerService.register(appointment, notificationInfo); + } + + private void modifyAppointment( + Appointment appointment, UpdateAppointmentServiceRequest request) { + appointment.changeAppointmentName(request.getAppointmentName()); + appointment.changeDeviceToken(request.getDeviceToken()); + appointment.changePreparationDuration(request.getPreparationDuration()); + appointment.changeMovingDuration(request.getMovingDuration()); + appointment.changeAppointmentTime(request.getFirstAppointmentTime().toLocalTime()); + appointment.setRepeatingDays(request.getRepeatDayOfWeekList()); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/request/CreateAppointmentServiceRequest.java b/src/main/java/earlybird/earlybird/appointment/service/request/CreateAppointmentServiceRequest.java new file mode 100644 index 0000000..1b854b6 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/request/CreateAppointmentServiceRequest.java @@ -0,0 +1,41 @@ +package earlybird.earlybird.appointment.service.request; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class CreateAppointmentServiceRequest { + + private final String appointmentName; + private final String clientId; + private final String deviceToken; + private final Duration movingDuration; + private final Duration preparationDuration; + private final LocalDateTime firstAppointmentTime; + private final List repeatDayOfWeekList; // 월화수목금토일 + + @Builder + private CreateAppointmentServiceRequest( + @NonNull String appointmentName, + @NonNull String clientId, + @NonNull String deviceToken, + @NonNull LocalDateTime firstAppointmentTime, + @NonNull Duration preparationDuration, + @NonNull Duration movingDuration, + @NonNull List repeatDayOfWeekList) { + + this.appointmentName = appointmentName; + this.clientId = clientId; + this.deviceToken = deviceToken; + this.firstAppointmentTime = firstAppointmentTime; + this.preparationDuration = preparationDuration; + this.movingDuration = movingDuration; + this.repeatDayOfWeekList = repeatDayOfWeekList; + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/request/DeleteAppointmentServiceRequest.java b/src/main/java/earlybird/earlybird/appointment/service/request/DeleteAppointmentServiceRequest.java new file mode 100644 index 0000000..dd9799e --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/request/DeleteAppointmentServiceRequest.java @@ -0,0 +1,30 @@ +package earlybird.earlybird.appointment.service.request; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.*; + +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterFcmMessageAtSchedulerServiceRequest; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class DeleteAppointmentServiceRequest { + + private final String clientId; + private final Long appointmentId; + + @Builder + public DeleteAppointmentServiceRequest(String clientId, Long appointmentId) { + this.clientId = clientId; + this.appointmentId = appointmentId; + } + + public DeregisterFcmMessageAtSchedulerServiceRequest + toDeregisterFcmMessageAtSchedulerServiceRequest() { + return DeregisterFcmMessageAtSchedulerServiceRequest.builder() + .clientId(clientId) + .appointmentId(appointmentId) + .targetNotificationStatus(CANCELLED) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/request/UpdateAppointmentServiceRequest.java b/src/main/java/earlybird/earlybird/appointment/service/request/UpdateAppointmentServiceRequest.java new file mode 100644 index 0000000..b07d3e8 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/request/UpdateAppointmentServiceRequest.java @@ -0,0 +1,47 @@ +package earlybird.earlybird.appointment.service.request; + +import earlybird.earlybird.appointment.domain.AppointmentUpdateType; + +import lombok.Builder; +import lombok.Getter; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class UpdateAppointmentServiceRequest { + + private final Long appointmentId; + private final String appointmentName; + private final String clientId; + private final String deviceToken; + private final Duration preparationDuration; + private final Duration movingDuration; + private final LocalDateTime firstAppointmentTime; + private final AppointmentUpdateType updateType; + private final List repeatDayOfWeekList; + + @Builder + private UpdateAppointmentServiceRequest( + Long appointmentId, + String appointmentName, + String clientId, + String deviceToken, + Duration preparationDuration, + Duration movingDuration, + LocalDateTime firstAppointmentTime, + AppointmentUpdateType updateType, + List repeatDayOfWeekList) { + this.appointmentId = appointmentId; + this.appointmentName = appointmentName; + this.clientId = clientId; + this.deviceToken = deviceToken; + this.preparationDuration = preparationDuration; + this.movingDuration = movingDuration; + this.firstAppointmentTime = firstAppointmentTime; + this.updateType = updateType; + this.repeatDayOfWeekList = repeatDayOfWeekList; + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/response/CreateAppointmentServiceResponse.java b/src/main/java/earlybird/earlybird/appointment/service/response/CreateAppointmentServiceResponse.java new file mode 100644 index 0000000..034a0b2 --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/response/CreateAppointmentServiceResponse.java @@ -0,0 +1,15 @@ +package earlybird.earlybird.appointment.service.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CreateAppointmentServiceResponse { + + private final Long createdAppointmentId; + + @Builder + private CreateAppointmentServiceResponse(Long createdAppointmentId) { + this.createdAppointmentId = createdAppointmentId; + } +} diff --git a/src/main/java/earlybird/earlybird/appointment/service/response/UpdateAppointmentServiceResponse.java b/src/main/java/earlybird/earlybird/appointment/service/response/UpdateAppointmentServiceResponse.java new file mode 100644 index 0000000..b13713a --- /dev/null +++ b/src/main/java/earlybird/earlybird/appointment/service/response/UpdateAppointmentServiceResponse.java @@ -0,0 +1,3 @@ +package earlybird.earlybird.appointment.service.response; + +public class UpdateAppointmentServiceResponse {} diff --git a/src/main/java/earlybird/earlybird/aws/alb/HealthCheckController.java b/src/main/java/earlybird/earlybird/aws/alb/HealthCheckController.java new file mode 100644 index 0000000..26cab8f --- /dev/null +++ b/src/main/java/earlybird/earlybird/aws/alb/HealthCheckController.java @@ -0,0 +1,14 @@ +package earlybird.earlybird.aws.alb; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthCheckController { + + @GetMapping("/health") + public ResponseEntity healthCheck() { + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/earlybird/earlybird/aws/alb/ShowServerIpController.java b/src/main/java/earlybird/earlybird/aws/alb/ShowServerIpController.java new file mode 100644 index 0000000..86da24e --- /dev/null +++ b/src/main/java/earlybird/earlybird/aws/alb/ShowServerIpController.java @@ -0,0 +1,18 @@ +package earlybird.earlybird.aws.alb; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +@Controller +public class ShowServerIpController { + + @GetMapping("/ip") + public String showServerIp(Model model) throws UnknownHostException { + model.addAttribute("ip", InetAddress.getLocalHost().getHostAddress()); + return "showServerIp"; + } +} diff --git a/src/main/java/earlybird/earlybird/common/AsyncConfig.java b/src/main/java/earlybird/earlybird/common/AsyncConfig.java new file mode 100644 index 0000000..18f64f8 --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/AsyncConfig.java @@ -0,0 +1,22 @@ +package earlybird.earlybird.common; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@EnableAsync +@Configuration +public class AsyncConfig { + + @Bean + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + executor.setThreadNamePrefix("Async-"); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/earlybird/earlybird/common/BaseTimeEntity.java b/src/main/java/earlybird/earlybird/common/BaseTimeEntity.java new file mode 100644 index 0000000..22c0173 --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/BaseTimeEntity.java @@ -0,0 +1,22 @@ +package earlybird.earlybird.common; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; + +import lombok.Getter; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public abstract class BaseTimeEntity { + + @CreatedDate private LocalDateTime createdAt; + + @LastModifiedDate private LocalDateTime updatedAt; +} diff --git a/src/main/java/earlybird/earlybird/common/DayOfWeekUtil.java b/src/main/java/earlybird/earlybird/common/DayOfWeekUtil.java new file mode 100644 index 0000000..5c8adf1 --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/DayOfWeekUtil.java @@ -0,0 +1,24 @@ +package earlybird.earlybird.common; + +import java.time.DayOfWeek; +import java.util.ArrayList; +import java.util.List; + +public class DayOfWeekUtil { + + /** + * @param dayOfWeekBooleanList 월화수목금토일 + */ + public static List convertBooleanListToDayOfWeekList( + List dayOfWeekBooleanList) { + if (dayOfWeekBooleanList == null || dayOfWeekBooleanList.size() != 7) + throw new IllegalArgumentException("dayOfWeekBooleanList must contain exactly 7 days"); + + List dayOfWeekList = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + if (dayOfWeekBooleanList.get(i)) dayOfWeekList.add(DayOfWeek.of(i + 1)); + } + + return dayOfWeekList; + } +} diff --git a/src/main/java/earlybird/earlybird/common/DefaultTimeZoneConfig.java b/src/main/java/earlybird/earlybird/common/DefaultTimeZoneConfig.java new file mode 100644 index 0000000..f177761 --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/DefaultTimeZoneConfig.java @@ -0,0 +1,16 @@ +package earlybird.earlybird.common; + +import jakarta.annotation.PostConstruct; + +import org.springframework.context.annotation.Configuration; + +import java.util.TimeZone; + +@Configuration +public class DefaultTimeZoneConfig { + + @PostConstruct + public void setDefaultTimeZoneToKST() { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + } +} diff --git a/src/main/java/earlybird/earlybird/common/InstantUtil.java b/src/main/java/earlybird/earlybird/common/InstantUtil.java new file mode 100644 index 0000000..3657fee --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/InstantUtil.java @@ -0,0 +1,15 @@ +package earlybird.earlybird.common; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class InstantUtil { + public static boolean checkTimeBeforeNow(Instant time) { + return (time.isBefore(Instant.now())); + } + + public static ZonedDateTime getZonedDateTimeFromInstant(Instant instant, ZoneId zoneId) { + return instant.atZone(zoneId); + } +} diff --git a/src/main/java/earlybird/earlybird/common/LocalDateTimeUtil.java b/src/main/java/earlybird/earlybird/common/LocalDateTimeUtil.java new file mode 100644 index 0000000..8128f1b --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/LocalDateTimeUtil.java @@ -0,0 +1,18 @@ +package earlybird.earlybird.common; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public class LocalDateTimeUtil { + public static LocalDateTime getLocalDateTimeNow() { + return LocalDateTime.now(ZoneId.of("Asia/Seoul")); + } + + public static LocalDateTime subtractDuration(LocalDateTime localDateTime, Duration duration) { + if (localDateTime == null || duration == null) { + throw new IllegalArgumentException("localDateTime and duration can't be null"); + } + return localDateTime.minus(duration); + } +} diff --git a/src/main/java/earlybird/earlybird/common/LocalDateUtil.java b/src/main/java/earlybird/earlybird/common/LocalDateUtil.java new file mode 100644 index 0000000..51fcdcd --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/LocalDateUtil.java @@ -0,0 +1,10 @@ +package earlybird.earlybird.common; + +import java.time.LocalDate; +import java.time.ZoneId; + +public class LocalDateUtil { + public static LocalDate getLocalDateNow() { + return LocalDate.now(ZoneId.of("Asia/Seoul")); + } +} diff --git a/src/main/java/earlybird/earlybird/common/RetryConfig.java b/src/main/java/earlybird/earlybird/common/RetryConfig.java new file mode 100644 index 0000000..55bda92 --- /dev/null +++ b/src/main/java/earlybird/earlybird/common/RetryConfig.java @@ -0,0 +1,8 @@ +package earlybird.earlybird.common; + +import org.springframework.context.annotation.Configuration; +import org.springframework.retry.annotation.EnableRetry; + +@EnableRetry +@Configuration +public class RetryConfig {} diff --git a/src/main/java/earlybird/earlybird/error/ErrorCode.java b/src/main/java/earlybird/earlybird/error/ErrorCode.java index 1ae7b98..5435f7b 100644 --- a/src/main/java/earlybird/earlybird/error/ErrorCode.java +++ b/src/main/java/earlybird/earlybird/error/ErrorCode.java @@ -1,16 +1,27 @@ package earlybird.earlybird.error; import lombok.Getter; + import org.springframework.http.HttpStatus; @Getter public enum ErrorCode { INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "올바르지 않은 입력값입니다."), METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "잘못된 HTTP 메서드를 호출했습니다."), - INTERNAL_SERVER_ERRER(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러가 발생했습니다."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러가 발생했습니다."), NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 엔티티입니다."), ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 아티클입니다."), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."); + NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 알림입니다."), + FCM_MESSAGE_TIME_BEFORE_NOW(HttpStatus.INTERNAL_SERVER_ERROR, "FCM 메시지 전송 희망 시간이 현재보다 과거입니다."), + FCM_NOTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 알림입니다."), + ALREADY_SENT_FCM_NOTIFICATION(HttpStatus.BAD_REQUEST, "이미 전송된 FCM 알림입니다."), + INCORRECT_REQUEST_BODY_FORMAT(HttpStatus.BAD_REQUEST, "잘못된 request body 형식입니다."), + INVALID_REQUEST_ARGUMENT(HttpStatus.BAD_REQUEST, "request argument 가 제약 조건을 만족하지 않습니다."), + FCM_DEVICE_TOKEN_MISMATCH( + HttpStatus.BAD_REQUEST, "요청한 알림 ID에 해당하는 디바이스 토큰과 요청한 디바이스 토큰이 일치하지 않습니다."), + APPOINTMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 약속입니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."), + DELETED_APPOINTMENT_EXCEPTION(HttpStatus.NOT_FOUND, "삭제된 일정입니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/earlybird/earlybird/error/ErrorResponse.java b/src/main/java/earlybird/earlybird/error/ErrorResponse.java index fe560e4..e1ec866 100644 --- a/src/main/java/earlybird/earlybird/error/ErrorResponse.java +++ b/src/main/java/earlybird/earlybird/error/ErrorResponse.java @@ -3,6 +3,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; + import org.springframework.http.HttpStatus; @Getter diff --git a/src/main/java/earlybird/earlybird/error/GlobalExceptionHandler.java b/src/main/java/earlybird/earlybird/error/GlobalExceptionHandler.java index babeb32..bc78fc6 100644 --- a/src/main/java/earlybird/earlybird/error/GlobalExceptionHandler.java +++ b/src/main/java/earlybird/earlybird/error/GlobalExceptionHandler.java @@ -1,9 +1,13 @@ package earlybird.earlybird.error; import earlybird.earlybird.error.exception.BusinessBaseException; + import lombok.extern.slf4j.Slf4j; + import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -11,7 +15,8 @@ @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - protected ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + protected ResponseEntity handleHttpRequestMethodNotSupportedException( + HttpRequestMethodNotSupportedException e) { log.error("HttpRequestMethodNotSupportedException", e); return createErrorResponseEntity(ErrorCode.METHOD_NOT_ALLOWED); } @@ -25,7 +30,21 @@ protected ResponseEntity handleBusinessBaseException(BusinessBase @ExceptionHandler(Exception.class) protected ResponseEntity handleException(Exception e) { log.error("Exception", e); - return createErrorResponseEntity(ErrorCode.INTERNAL_SERVER_ERRER); + return createErrorResponseEntity(ErrorCode.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + protected ResponseEntity handleHttpMessageNotReadableException( + HttpMessageNotReadableException e) { + log.error("HttpMessageNotReadableException", e); + return createErrorResponseEntity(ErrorCode.INCORRECT_REQUEST_BODY_FORMAT); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException", e); + return createErrorResponseEntity(ErrorCode.INVALID_REQUEST_ARGUMENT); } private ResponseEntity createErrorResponseEntity(ErrorCode errorCode) { diff --git a/src/main/java/earlybird/earlybird/error/exception/AlreadySentFcmNotificationException.java b/src/main/java/earlybird/earlybird/error/exception/AlreadySentFcmNotificationException.java new file mode 100644 index 0000000..8d0d9c8 --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/AlreadySentFcmNotificationException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import static earlybird.earlybird.error.ErrorCode.ALREADY_SENT_FCM_NOTIFICATION; + +public class AlreadySentFcmNotificationException extends BusinessBaseException { + public AlreadySentFcmNotificationException() { + super(ALREADY_SENT_FCM_NOTIFICATION); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/AppointmentNotFoundException.java b/src/main/java/earlybird/earlybird/error/exception/AppointmentNotFoundException.java new file mode 100644 index 0000000..d5a7851 --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/AppointmentNotFoundException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import earlybird.earlybird.error.ErrorCode; + +public class AppointmentNotFoundException extends NotFoundException { + public AppointmentNotFoundException() { + super(ErrorCode.APPOINTMENT_NOT_FOUND); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/BusinessBaseException.java b/src/main/java/earlybird/earlybird/error/exception/BusinessBaseException.java index 955c8b5..eba6026 100644 --- a/src/main/java/earlybird/earlybird/error/exception/BusinessBaseException.java +++ b/src/main/java/earlybird/earlybird/error/exception/BusinessBaseException.java @@ -1,6 +1,7 @@ package earlybird.earlybird.error.exception; import earlybird.earlybird.error.ErrorCode; + import lombok.Getter; @Getter diff --git a/src/main/java/earlybird/earlybird/error/exception/DeletedAppointmentException.java b/src/main/java/earlybird/earlybird/error/exception/DeletedAppointmentException.java new file mode 100644 index 0000000..be3725d --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/DeletedAppointmentException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import static earlybird.earlybird.error.ErrorCode.DELETED_APPOINTMENT_EXCEPTION; + +public class DeletedAppointmentException extends BusinessBaseException { + public DeletedAppointmentException() { + super(DELETED_APPOINTMENT_EXCEPTION); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/FcmDeviceTokenMismatchException.java b/src/main/java/earlybird/earlybird/error/exception/FcmDeviceTokenMismatchException.java new file mode 100644 index 0000000..cdccd62 --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/FcmDeviceTokenMismatchException.java @@ -0,0 +1,10 @@ +package earlybird.earlybird.error.exception; + +import static earlybird.earlybird.error.ErrorCode.FCM_DEVICE_TOKEN_MISMATCH; + +public class FcmDeviceTokenMismatchException extends BusinessBaseException { + + public FcmDeviceTokenMismatchException() { + super(FCM_DEVICE_TOKEN_MISMATCH); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/FcmMessageTimeBeforeNowException.java b/src/main/java/earlybird/earlybird/error/exception/FcmMessageTimeBeforeNowException.java new file mode 100644 index 0000000..5176879 --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/FcmMessageTimeBeforeNowException.java @@ -0,0 +1,10 @@ +package earlybird.earlybird.error.exception; + +import static earlybird.earlybird.error.ErrorCode.FCM_MESSAGE_TIME_BEFORE_NOW; + +public class FcmMessageTimeBeforeNowException extends BusinessBaseException { + + public FcmMessageTimeBeforeNowException() { + super(FCM_MESSAGE_TIME_BEFORE_NOW); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/FcmNotificationNotFoundException.java b/src/main/java/earlybird/earlybird/error/exception/FcmNotificationNotFoundException.java new file mode 100644 index 0000000..b27d64c --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/FcmNotificationNotFoundException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import earlybird.earlybird.error.ErrorCode; + +public class FcmNotificationNotFoundException extends NotFoundException { + public FcmNotificationNotFoundException() { + super(ErrorCode.NOTIFICATION_NOT_FOUND); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/NotificationNotFoundException.java b/src/main/java/earlybird/earlybird/error/exception/NotificationNotFoundException.java new file mode 100644 index 0000000..4cbd18a --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/NotificationNotFoundException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import earlybird.earlybird.error.ErrorCode; + +public class NotificationNotFoundException extends NotFoundException { + public NotificationNotFoundException() { + super(ErrorCode.NOTIFICATION_NOT_FOUND); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/UserNotFoundException.java b/src/main/java/earlybird/earlybird/error/exception/UserNotFoundException.java index dca9f39..fd85bdc 100644 --- a/src/main/java/earlybird/earlybird/error/exception/UserNotFoundException.java +++ b/src/main/java/earlybird/earlybird/error/exception/UserNotFoundException.java @@ -3,7 +3,7 @@ import earlybird.earlybird.error.ErrorCode; public class UserNotFoundException extends NotFoundException { - public UserNotFoundException() { - super(ErrorCode.USER_NOT_FOUND); - } + public UserNotFoundException() { + super(ErrorCode.USER_NOT_FOUND); + } } diff --git a/src/main/java/earlybird/earlybird/feedback/controller/CreateFeedbackController.java b/src/main/java/earlybird/earlybird/feedback/controller/CreateFeedbackController.java index af59a6f..bc9a348 100644 --- a/src/main/java/earlybird/earlybird/feedback/controller/CreateFeedbackController.java +++ b/src/main/java/earlybird/earlybird/feedback/controller/CreateFeedbackController.java @@ -1,12 +1,19 @@ package earlybird.earlybird.feedback.controller; -import earlybird.earlybird.feedback.service.CreateAnonymousUserFeedbackService; -import earlybird.earlybird.feedback.service.CreateAuthUserFeedbackService; -import earlybird.earlybird.feedback.dto.FeedbackDTO; -import earlybird.earlybird.feedback.dto.FeedbackRequestDTO; +import earlybird.earlybird.feedback.controller.request.CreateFeedbackCommentRequest; +import earlybird.earlybird.feedback.controller.request.CreateFeedbackScoreRequest; +import earlybird.earlybird.feedback.service.anonymous.CreateAnonymousFeedbackCommentService; +import earlybird.earlybird.feedback.service.anonymous.CreateAnonymousFeedbackScoreService; +import earlybird.earlybird.feedback.service.anonymous.request.CreateAnonymousFeedbackCommentServiceRequest; +import earlybird.earlybird.feedback.service.anonymous.request.CreateAnonymousFeedbackScoreServiceRequest; +import earlybird.earlybird.feedback.service.auth.CreateAuthFeedbackCommentService; +import earlybird.earlybird.feedback.service.auth.request.CreateAuthFeedbackCommentServiceRequest; import earlybird.earlybird.security.authentication.oauth2.user.OAuth2UserDetails; -import earlybird.earlybird.user.dto.UserAccountInfoDTO; + +import jakarta.validation.Valid; + import lombok.RequiredArgsConstructor; + import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; @@ -14,40 +21,42 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - @RequiredArgsConstructor @RequestMapping("/api/v1/feedbacks") @RestController public class CreateFeedbackController { - private final CreateAuthUserFeedbackService createAuthUserFeedbackService; - private final CreateAnonymousUserFeedbackService createAnonymousUserFeedbackService; + private final CreateAuthFeedbackCommentService createAuthFeedbackCommentService; + private final CreateAnonymousFeedbackCommentService createAnonymousFeedbackCommentService; + private final CreateAnonymousFeedbackScoreService createAnonymousFeedbackScoreService; - @PostMapping - public ResponseEntity createFeedback( + @PostMapping("/comments") + public ResponseEntity createFeedbackComment( @AuthenticationPrincipal OAuth2UserDetails oAuth2UserDetails, - @RequestBody FeedbackRequestDTO requestDTO) { + @Valid @RequestBody CreateFeedbackCommentRequest request) { if (oAuth2UserDetails != null) { - createAuthUserFeedback(oAuth2UserDetails, requestDTO); - } - else { - createAnonymousUserFeedback(requestDTO); + CreateAuthFeedbackCommentServiceRequest serviceRequest = + CreateAuthFeedbackCommentServiceRequest.of(oAuth2UserDetails, request); + createAuthFeedbackCommentService.create(serviceRequest); + } else { + CreateAnonymousFeedbackCommentServiceRequest serviceRequest = + CreateAnonymousFeedbackCommentServiceRequest.of(request); + createAnonymousFeedbackCommentService.create(serviceRequest); } return ResponseEntity.ok().build(); } - private void createAuthUserFeedback(OAuth2UserDetails oAuth2UserDetails, FeedbackRequestDTO requestDTO) { - FeedbackDTO feedbackDTO = FeedbackDTO.of(oAuth2UserDetails, requestDTO); - createAuthUserFeedbackService.create(feedbackDTO); - } + // TODO: 베타 테스트 이후 로그인 사용자의 피드백 받는 기능 구현 + @PostMapping("/scores") + public ResponseEntity createFeedbackScore( + @Valid @RequestBody CreateFeedbackScoreRequest request) { - private void createAnonymousUserFeedback(FeedbackRequestDTO requestDTO) { - FeedbackDTO feedbackDTO = FeedbackDTO.of(requestDTO); - createAnonymousUserFeedbackService.create(feedbackDTO); - } + CreateAnonymousFeedbackScoreServiceRequest serviceRequest = + CreateAnonymousFeedbackScoreServiceRequest.of(request); + createAnonymousFeedbackScoreService.create(serviceRequest); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/earlybird/earlybird/feedback/controller/request/CreateFeedbackCommentRequest.java b/src/main/java/earlybird/earlybird/feedback/controller/request/CreateFeedbackCommentRequest.java new file mode 100644 index 0000000..4923543 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/controller/request/CreateFeedbackCommentRequest.java @@ -0,0 +1,23 @@ +package earlybird.earlybird.feedback.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CreateFeedbackCommentRequest { + @NotBlank private String comment; + @NotBlank private String clientId; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime createdAt; +} diff --git a/src/main/java/earlybird/earlybird/feedback/controller/request/CreateFeedbackScoreRequest.java b/src/main/java/earlybird/earlybird/feedback/controller/request/CreateFeedbackScoreRequest.java new file mode 100644 index 0000000..df24be0 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/controller/request/CreateFeedbackScoreRequest.java @@ -0,0 +1,23 @@ +package earlybird.earlybird.feedback.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CreateFeedbackScoreRequest { + @NotNull private Integer score; + @NotBlank private String clientId; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime createdAt; +} diff --git a/src/main/java/earlybird/earlybird/feedback/domain/comment/FeedbackComment.java b/src/main/java/earlybird/earlybird/feedback/domain/comment/FeedbackComment.java new file mode 100644 index 0000000..4227222 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/domain/comment/FeedbackComment.java @@ -0,0 +1,35 @@ +package earlybird.earlybird.feedback.domain.comment; + +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.user.entity.User; + +import jakarta.persistence.*; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Entity +public class FeedbackComment extends BaseTimeEntity { + + @Column(name = "feedback_comment_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @Column(name = "feedback_comment", nullable = false) + private String comment; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + private String clientId; + + @Column(name = "feedback_comment_created_time_at_client", nullable = false) + private LocalDateTime createdTimeAtClient; +} diff --git a/src/main/java/earlybird/earlybird/feedback/domain/comment/FeedbackCommentRepository.java b/src/main/java/earlybird/earlybird/feedback/domain/comment/FeedbackCommentRepository.java new file mode 100644 index 0000000..83236e7 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/domain/comment/FeedbackCommentRepository.java @@ -0,0 +1,5 @@ +package earlybird.earlybird.feedback.domain.comment; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FeedbackCommentRepository extends JpaRepository {} diff --git a/src/main/java/earlybird/earlybird/feedback/domain/score/FeedbackScore.java b/src/main/java/earlybird/earlybird/feedback/domain/score/FeedbackScore.java new file mode 100644 index 0000000..533b24b --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/domain/score/FeedbackScore.java @@ -0,0 +1,35 @@ +package earlybird.earlybird.feedback.domain.score; + +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.user.entity.User; + +import jakarta.persistence.*; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Entity +public class FeedbackScore extends BaseTimeEntity { + + @Column(name = "feedback_score_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @Column(name = "feedback_nps_score", nullable = false) + private Integer score; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + private String clientId; + + @Column(name = "feedback_created_time_at_client", nullable = false) + private LocalDateTime createdTimeAtClient; +} diff --git a/src/main/java/earlybird/earlybird/feedback/domain/score/FeedbackScoreRepository.java b/src/main/java/earlybird/earlybird/feedback/domain/score/FeedbackScoreRepository.java new file mode 100644 index 0000000..c5d8086 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/domain/score/FeedbackScoreRepository.java @@ -0,0 +1,5 @@ +package earlybird.earlybird.feedback.domain.score; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FeedbackScoreRepository extends JpaRepository {} diff --git a/src/main/java/earlybird/earlybird/feedback/dto/FeedbackDTO.java b/src/main/java/earlybird/earlybird/feedback/dto/FeedbackDTO.java deleted file mode 100644 index 7a80ad6..0000000 --- a/src/main/java/earlybird/earlybird/feedback/dto/FeedbackDTO.java +++ /dev/null @@ -1,51 +0,0 @@ -package earlybird.earlybird.feedback.dto; - -import earlybird.earlybird.feedback.entity.Feedback; -import earlybird.earlybird.security.authentication.oauth2.user.OAuth2UserDetails; -import earlybird.earlybird.user.dto.UserAccountInfoDTO; -import earlybird.earlybird.user.entity.User; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -import java.time.LocalDateTime; - -@Getter -public class FeedbackDTO { - private Long id; - private String content; - private UserAccountInfoDTO userAccountInfoDTO; - private LocalDateTime createdAt; - - @Builder - private FeedbackDTO(Long id, String content, UserAccountInfoDTO userAccountInfoDTO, LocalDateTime createdAt) { - this.id = id; - this.content = content; - this.userAccountInfoDTO = userAccountInfoDTO; - this.createdAt = createdAt; - } - - public static FeedbackDTO of(OAuth2UserDetails oAuth2UserDetails, FeedbackRequestDTO requestDTO) { - return FeedbackDTO.builder() - .content(requestDTO.getContent()) - .userAccountInfoDTO(oAuth2UserDetails.getUserAccountInfoDTO()) - .createdAt(requestDTO.getCreatedAt()) - .build(); - } - - public static FeedbackDTO of(FeedbackRequestDTO requestDTO) { - return FeedbackDTO.builder() - .content(requestDTO.getContent()) - .createdAt(requestDTO.getCreatedAt()) - .build(); - } - - public static FeedbackDTO of(Feedback feedback) { - return FeedbackDTO.builder() - .id(feedback.getId()) - .content(feedback.getContent()) - .createdAt(feedback.getCreatedAt()) - .userAccountInfoDTO(feedback.getUser().toUserAccountInfoDTO()) - .build(); - } -} diff --git a/src/main/java/earlybird/earlybird/feedback/dto/FeedbackRequestDTO.java b/src/main/java/earlybird/earlybird/feedback/dto/FeedbackRequestDTO.java deleted file mode 100644 index 12f0e18..0000000 --- a/src/main/java/earlybird/earlybird/feedback/dto/FeedbackRequestDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package earlybird.earlybird.feedback.dto; - -import lombok.Builder; -import lombok.Getter; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -@Getter -public class FeedbackRequestDTO { - private String content; - - /** - * format: "yyyy-MM-dd HH:mm:ss.SSS" - */ - private String createdAt; - - public LocalDateTime getCreatedAt() { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); - return LocalDateTime.parse(this.createdAt, formatter); - } -} diff --git a/src/main/java/earlybird/earlybird/feedback/entity/Feedback.java b/src/main/java/earlybird/earlybird/feedback/entity/Feedback.java deleted file mode 100644 index 0a6eae8..0000000 --- a/src/main/java/earlybird/earlybird/feedback/entity/Feedback.java +++ /dev/null @@ -1,30 +0,0 @@ -package earlybird.earlybird.feedback.entity; - -import earlybird.earlybird.user.entity.User; -import jakarta.persistence.*; -import lombok.*; - -import java.time.LocalDateTime; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -@Entity -public class Feedback { - - @Column(name = "feedback_id", nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Id - private Long id; - - @Column(name = "feedback_content", nullable = false) - private String content; - - @ManyToOne - @JoinColumn(name = "user_id") - private User user; - - @Column(name = "feedback_created_at", nullable = false) - private LocalDateTime createdAt; -} diff --git a/src/main/java/earlybird/earlybird/feedback/repository/FeedbackRepository.java b/src/main/java/earlybird/earlybird/feedback/repository/FeedbackRepository.java deleted file mode 100644 index 77e63f8..0000000 --- a/src/main/java/earlybird/earlybird/feedback/repository/FeedbackRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package earlybird.earlybird.feedback.repository; - -import earlybird.earlybird.feedback.entity.Feedback; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface FeedbackRepository extends JpaRepository { -} diff --git a/src/main/java/earlybird/earlybird/feedback/service/CreateAnonymousUserFeedbackService.java b/src/main/java/earlybird/earlybird/feedback/service/CreateAnonymousUserFeedbackService.java deleted file mode 100644 index c136df5..0000000 --- a/src/main/java/earlybird/earlybird/feedback/service/CreateAnonymousUserFeedbackService.java +++ /dev/null @@ -1,25 +0,0 @@ -package earlybird.earlybird.feedback.service; - -import earlybird.earlybird.feedback.entity.Feedback; -import earlybird.earlybird.feedback.repository.FeedbackRepository; -import earlybird.earlybird.feedback.dto.FeedbackDTO; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class CreateAnonymousUserFeedbackService { - - private final FeedbackRepository feedbackRepository; - - public FeedbackDTO create(FeedbackDTO feedbackDTO) { - Feedback feedback = Feedback.builder() - .content(feedbackDTO.getContent()) - .createdAt(feedbackDTO.getCreatedAt()) - .build(); - - Feedback savedFeedback = feedbackRepository.save(feedback); - - return FeedbackDTO.of(savedFeedback); - } -} diff --git a/src/main/java/earlybird/earlybird/feedback/service/CreateAuthUserFeedbackService.java b/src/main/java/earlybird/earlybird/feedback/service/CreateAuthUserFeedbackService.java deleted file mode 100644 index 141f8f6..0000000 --- a/src/main/java/earlybird/earlybird/feedback/service/CreateAuthUserFeedbackService.java +++ /dev/null @@ -1,35 +0,0 @@ -package earlybird.earlybird.feedback.service; - -import earlybird.earlybird.error.exception.UserNotFoundException; -import earlybird.earlybird.feedback.entity.Feedback; -import earlybird.earlybird.feedback.repository.FeedbackRepository; -import earlybird.earlybird.feedback.dto.FeedbackDTO; -import earlybird.earlybird.user.entity.User; -import earlybird.earlybird.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class CreateAuthUserFeedbackService { - - private final FeedbackRepository feedbackRepository; - private final UserRepository userRepository; - - public FeedbackDTO create(FeedbackDTO feedbackDTO) { - - Long userId = feedbackDTO.getUserAccountInfoDTO().getId(); - - User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - - Feedback feedback = Feedback.builder() - .content(feedbackDTO.getContent()) - .user(user) - .createdAt(feedbackDTO.getCreatedAt()) - .build(); - - Feedback savedFeedback = feedbackRepository.save(feedback); - - return FeedbackDTO.of(savedFeedback); - } -} diff --git a/src/main/java/earlybird/earlybird/feedback/service/anonymous/CreateAnonymousFeedbackCommentService.java b/src/main/java/earlybird/earlybird/feedback/service/anonymous/CreateAnonymousFeedbackCommentService.java new file mode 100644 index 0000000..019f125 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/service/anonymous/CreateAnonymousFeedbackCommentService.java @@ -0,0 +1,29 @@ +package earlybird.earlybird.feedback.service.anonymous; + +import earlybird.earlybird.feedback.domain.comment.FeedbackComment; +import earlybird.earlybird.feedback.domain.comment.FeedbackCommentRepository; +import earlybird.earlybird.feedback.service.anonymous.request.CreateAnonymousFeedbackCommentServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class CreateAnonymousFeedbackCommentService { + + private final FeedbackCommentRepository feedbackCommentRepository; + + @Transactional + public void create(CreateAnonymousFeedbackCommentServiceRequest request) { + FeedbackComment feedbackComment = + FeedbackComment.builder() + .comment(request.getComment()) + .clientId(request.getClientId()) + .createdTimeAtClient(request.getCreatedAt()) + .build(); + + feedbackCommentRepository.save(feedbackComment); + } +} diff --git a/src/main/java/earlybird/earlybird/feedback/service/anonymous/CreateAnonymousFeedbackScoreService.java b/src/main/java/earlybird/earlybird/feedback/service/anonymous/CreateAnonymousFeedbackScoreService.java new file mode 100644 index 0000000..a93701a --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/service/anonymous/CreateAnonymousFeedbackScoreService.java @@ -0,0 +1,29 @@ +package earlybird.earlybird.feedback.service.anonymous; + +import earlybird.earlybird.feedback.domain.score.FeedbackScore; +import earlybird.earlybird.feedback.domain.score.FeedbackScoreRepository; +import earlybird.earlybird.feedback.service.anonymous.request.CreateAnonymousFeedbackScoreServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class CreateAnonymousFeedbackScoreService { + + private final FeedbackScoreRepository feedbackScoreRepository; + + @Transactional + public void create(CreateAnonymousFeedbackScoreServiceRequest request) { + FeedbackScore feedbackScore = + FeedbackScore.builder() + .score(request.getScore()) + .clientId(request.getClientId()) + .createdTimeAtClient(request.getCreatedAt()) + .build(); + + feedbackScoreRepository.save(feedbackScore); + } +} diff --git a/src/main/java/earlybird/earlybird/feedback/service/anonymous/request/CreateAnonymousFeedbackCommentServiceRequest.java b/src/main/java/earlybird/earlybird/feedback/service/anonymous/request/CreateAnonymousFeedbackCommentServiceRequest.java new file mode 100644 index 0000000..99eb8d5 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/service/anonymous/request/CreateAnonymousFeedbackCommentServiceRequest.java @@ -0,0 +1,32 @@ +package earlybird.earlybird.feedback.service.anonymous.request; + +import earlybird.earlybird.feedback.controller.request.CreateFeedbackCommentRequest; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CreateAnonymousFeedbackCommentServiceRequest { + private String comment; + private String clientId; + private LocalDateTime createdAt; + + @Builder + private CreateAnonymousFeedbackCommentServiceRequest( + String comment, String clientId, LocalDateTime createdAt) { + this.comment = comment; + this.clientId = clientId; + this.createdAt = createdAt; + } + + public static CreateAnonymousFeedbackCommentServiceRequest of( + CreateFeedbackCommentRequest request) { + return CreateAnonymousFeedbackCommentServiceRequest.builder() + .comment(request.getComment()) + .clientId(request.getClientId()) + .createdAt(request.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/feedback/service/anonymous/request/CreateAnonymousFeedbackScoreServiceRequest.java b/src/main/java/earlybird/earlybird/feedback/service/anonymous/request/CreateAnonymousFeedbackScoreServiceRequest.java new file mode 100644 index 0000000..d07461e --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/service/anonymous/request/CreateAnonymousFeedbackScoreServiceRequest.java @@ -0,0 +1,32 @@ +package earlybird.earlybird.feedback.service.anonymous.request; + +import earlybird.earlybird.feedback.controller.request.CreateFeedbackScoreRequest; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CreateAnonymousFeedbackScoreServiceRequest { + private int score; + private String clientId; + private LocalDateTime createdAt; + + @Builder + private CreateAnonymousFeedbackScoreServiceRequest( + int score, String clientId, LocalDateTime createdAt) { + this.score = score; + this.clientId = clientId; + this.createdAt = createdAt; + } + + public static CreateAnonymousFeedbackScoreServiceRequest of( + CreateFeedbackScoreRequest request) { + return CreateAnonymousFeedbackScoreServiceRequest.builder() + .score(request.getScore()) + .clientId(request.getClientId()) + .createdAt(request.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/feedback/service/auth/CreateAuthFeedbackCommentService.java b/src/main/java/earlybird/earlybird/feedback/service/auth/CreateAuthFeedbackCommentService.java new file mode 100644 index 0000000..532c087 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/service/auth/CreateAuthFeedbackCommentService.java @@ -0,0 +1,38 @@ +package earlybird.earlybird.feedback.service.auth; + +import earlybird.earlybird.error.exception.UserNotFoundException; +import earlybird.earlybird.feedback.domain.comment.FeedbackComment; +import earlybird.earlybird.feedback.domain.comment.FeedbackCommentRepository; +import earlybird.earlybird.feedback.service.auth.request.CreateAuthFeedbackCommentServiceRequest; +import earlybird.earlybird.user.entity.User; +import earlybird.earlybird.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class CreateAuthFeedbackCommentService { + + private final FeedbackCommentRepository feedbackCommentRepository; + private final UserRepository userRepository; + + @Transactional + public void create(CreateAuthFeedbackCommentServiceRequest request) { + + Long userId = request.getUserAccountInfoDTO().getId(); + + User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); + + FeedbackComment feedbackComment = + FeedbackComment.builder() + .comment(request.getComment()) + .user(user) + .createdTimeAtClient(request.getCreatedAt()) + .build(); + + feedbackCommentRepository.save(feedbackComment); + } +} diff --git a/src/main/java/earlybird/earlybird/feedback/service/auth/request/CreateAuthFeedbackCommentServiceRequest.java b/src/main/java/earlybird/earlybird/feedback/service/auth/request/CreateAuthFeedbackCommentServiceRequest.java new file mode 100644 index 0000000..ed90ae9 --- /dev/null +++ b/src/main/java/earlybird/earlybird/feedback/service/auth/request/CreateAuthFeedbackCommentServiceRequest.java @@ -0,0 +1,39 @@ +package earlybird.earlybird.feedback.service.auth.request; + +import earlybird.earlybird.feedback.controller.request.CreateFeedbackCommentRequest; +import earlybird.earlybird.security.authentication.oauth2.user.OAuth2UserDetails; +import earlybird.earlybird.user.dto.UserAccountInfoDTO; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CreateAuthFeedbackCommentServiceRequest { + private Long id; + private String comment; + private UserAccountInfoDTO userAccountInfoDTO; + private LocalDateTime createdAt; + + @Builder + private CreateAuthFeedbackCommentServiceRequest( + Long id, + String comment, + UserAccountInfoDTO userAccountInfoDTO, + LocalDateTime createdAt) { + this.id = id; + this.comment = comment; + this.userAccountInfoDTO = userAccountInfoDTO; + this.createdAt = createdAt; + } + + public static CreateAuthFeedbackCommentServiceRequest of( + OAuth2UserDetails oAuth2UserDetails, CreateFeedbackCommentRequest requestDTO) { + return CreateAuthFeedbackCommentServiceRequest.builder() + .comment(requestDTO.getComment()) + .userAccountInfoDTO(oAuth2UserDetails.getUserAccountInfoDTO()) + .createdAt(requestDTO.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/log/arrive/controller/ArriveOnTimeEventLogController.java b/src/main/java/earlybird/earlybird/log/arrive/controller/ArriveOnTimeEventLogController.java new file mode 100644 index 0000000..68bddf9 --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/arrive/controller/ArriveOnTimeEventLogController.java @@ -0,0 +1,42 @@ +package earlybird.earlybird.log.arrive.controller; + +import earlybird.earlybird.log.arrive.controller.request.ArriveOnTimeEventLoggingRequest; +import earlybird.earlybird.log.arrive.service.ArriveOnTimeEventLogService; +import earlybird.earlybird.log.arrive.service.request.ArriveOnTimeEventLoggingServiceRequest; +import earlybird.earlybird.scheduler.notification.service.update.UpdateNotificationAtArriveOnTimeService; +import earlybird.earlybird.scheduler.notification.service.update.request.UpdateNotificationAtArriveOnTimeServiceRequest; + +import jakarta.validation.Valid; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/log/arrive-on-time-event") +@RestController +public class ArriveOnTimeEventLogController { + + private final ArriveOnTimeEventLogService arriveOnTimeEventLogService; + private final UpdateNotificationAtArriveOnTimeService updateNotificationAtArriveOnTimeService; + + @PostMapping + public ResponseEntity arriveOnTimeEvent( + @Valid @RequestBody ArriveOnTimeEventLoggingRequest request) { + + UpdateNotificationAtArriveOnTimeServiceRequest updateServiceRequest = + UpdateNotificationAtArriveOnTimeServiceRequest.builder() + .appointmentId(request.getAppointmentId()) + .clientId(request.getClientId()) + .build(); + + updateNotificationAtArriveOnTimeService.update(updateServiceRequest); + + ArriveOnTimeEventLoggingServiceRequest loggingServiceRequest = + new ArriveOnTimeEventLoggingServiceRequest(request.getAppointmentId()); + arriveOnTimeEventLogService.create(loggingServiceRequest); + + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/earlybird/earlybird/log/arrive/controller/request/ArriveOnTimeEventLoggingRequest.java b/src/main/java/earlybird/earlybird/log/arrive/controller/request/ArriveOnTimeEventLoggingRequest.java new file mode 100644 index 0000000..2ca1807 --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/arrive/controller/request/ArriveOnTimeEventLoggingRequest.java @@ -0,0 +1,17 @@ +package earlybird.earlybird.log.arrive.controller.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class ArriveOnTimeEventLoggingRequest { + + @NotNull private Long appointmentId; + + @NotBlank private String clientId; +} diff --git a/src/main/java/earlybird/earlybird/log/arrive/domain/ArriveOnTimeEventLog.java b/src/main/java/earlybird/earlybird/log/arrive/domain/ArriveOnTimeEventLog.java new file mode 100644 index 0000000..c82708f --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/arrive/domain/ArriveOnTimeEventLog.java @@ -0,0 +1,26 @@ +package earlybird.earlybird.log.arrive.domain; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.common.BaseTimeEntity; + +import jakarta.persistence.*; + +@Table(name = "arrive_on_time_event_log") +@Entity +public class ArriveOnTimeEventLog extends BaseTimeEntity { + + @Column(name = "arrive_on_time_event_log_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @OneToOne + @JoinColumn(name = "appointment_id") + private Appointment appointment; + + public ArriveOnTimeEventLog() {} + + public ArriveOnTimeEventLog(Appointment appointment) { + this.appointment = appointment; + } +} diff --git a/src/main/java/earlybird/earlybird/log/arrive/domain/ArriveOnTimeEventLogRepository.java b/src/main/java/earlybird/earlybird/log/arrive/domain/ArriveOnTimeEventLogRepository.java new file mode 100644 index 0000000..6f81733 --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/arrive/domain/ArriveOnTimeEventLogRepository.java @@ -0,0 +1,5 @@ +package earlybird.earlybird.log.arrive.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArriveOnTimeEventLogRepository extends JpaRepository {} diff --git a/src/main/java/earlybird/earlybird/log/arrive/service/ArriveOnTimeEventLogService.java b/src/main/java/earlybird/earlybird/log/arrive/service/ArriveOnTimeEventLogService.java new file mode 100644 index 0000000..b54e95e --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/arrive/service/ArriveOnTimeEventLogService.java @@ -0,0 +1,33 @@ +package earlybird.earlybird.log.arrive.service; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.AppointmentRepository; +import earlybird.earlybird.error.exception.AppointmentNotFoundException; +import earlybird.earlybird.log.arrive.domain.ArriveOnTimeEventLog; +import earlybird.earlybird.log.arrive.domain.ArriveOnTimeEventLogRepository; +import earlybird.earlybird.log.arrive.service.request.ArriveOnTimeEventLoggingServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class ArriveOnTimeEventLogService { + + private final ArriveOnTimeEventLogRepository arriveOnTimeEventLogRepository; + private final AppointmentRepository appointmentRepository; + + @Transactional + public void create(ArriveOnTimeEventLoggingServiceRequest request) { + Long appointmentId = request.getAppointmentId(); + Appointment appointment = + appointmentRepository + .findById(appointmentId) + .orElseThrow(AppointmentNotFoundException::new); + + ArriveOnTimeEventLog log = new ArriveOnTimeEventLog(appointment); + arriveOnTimeEventLogRepository.save(log); + } +} diff --git a/src/main/java/earlybird/earlybird/log/arrive/service/request/ArriveOnTimeEventLoggingServiceRequest.java b/src/main/java/earlybird/earlybird/log/arrive/service/request/ArriveOnTimeEventLoggingServiceRequest.java new file mode 100644 index 0000000..9b299db --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/arrive/service/request/ArriveOnTimeEventLoggingServiceRequest.java @@ -0,0 +1,10 @@ +package earlybird.earlybird.log.arrive.service.request; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class ArriveOnTimeEventLoggingServiceRequest { + private final Long appointmentId; +} diff --git a/src/main/java/earlybird/earlybird/log/visit/controller/VisitEventLogController.java b/src/main/java/earlybird/earlybird/log/visit/controller/VisitEventLogController.java new file mode 100644 index 0000000..05e4275 --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/visit/controller/VisitEventLogController.java @@ -0,0 +1,31 @@ +package earlybird.earlybird.log.visit.controller; + +import earlybird.earlybird.log.visit.controller.request.VisitEventLoggingRequest; +import earlybird.earlybird.log.visit.service.VisitEventLogService; +import earlybird.earlybird.log.visit.service.request.VisitEventLoggingServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/log") +@RestController +public class VisitEventLogController { + + private final VisitEventLogService visitEventLogService; + + @PostMapping("/visit-event") + public ResponseEntity visitEventLogging(@RequestBody VisitEventLoggingRequest request) { + if (request.getClientId() == null || request.getClientId().isEmpty()) + return ResponseEntity.badRequest().body("clientId is empty"); + VisitEventLoggingServiceRequest serviceRequest = + new VisitEventLoggingServiceRequest(request.getClientId()); + visitEventLogService.create(serviceRequest); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/earlybird/earlybird/log/visit/controller/request/VisitEventLoggingRequest.java b/src/main/java/earlybird/earlybird/log/visit/controller/request/VisitEventLoggingRequest.java new file mode 100644 index 0000000..190a651 --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/visit/controller/request/VisitEventLoggingRequest.java @@ -0,0 +1,12 @@ +package earlybird.earlybird.log.visit.controller.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class VisitEventLoggingRequest { + + private String clientId; +} diff --git a/src/main/java/earlybird/earlybird/log/visit/domain/VisitEventLog.java b/src/main/java/earlybird/earlybird/log/visit/domain/VisitEventLog.java new file mode 100644 index 0000000..f9c908e --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/visit/domain/VisitEventLog.java @@ -0,0 +1,24 @@ +package earlybird.earlybird.log.visit.domain; + +import earlybird.earlybird.common.BaseTimeEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class VisitEventLog extends BaseTimeEntity { + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + private String clientId; + + public VisitEventLog() {} + + public VisitEventLog(String clientId) { + this.clientId = clientId; + } +} diff --git a/src/main/java/earlybird/earlybird/log/visit/domain/VisitEventLogRepository.java b/src/main/java/earlybird/earlybird/log/visit/domain/VisitEventLogRepository.java new file mode 100644 index 0000000..d9a575a --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/visit/domain/VisitEventLogRepository.java @@ -0,0 +1,5 @@ +package earlybird.earlybird.log.visit.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VisitEventLogRepository extends JpaRepository {} diff --git a/src/main/java/earlybird/earlybird/log/visit/service/VisitEventLogService.java b/src/main/java/earlybird/earlybird/log/visit/service/VisitEventLogService.java new file mode 100644 index 0000000..6221371 --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/visit/service/VisitEventLogService.java @@ -0,0 +1,22 @@ +package earlybird.earlybird.log.visit.service; + +import earlybird.earlybird.log.visit.domain.VisitEventLog; +import earlybird.earlybird.log.visit.domain.VisitEventLogRepository; +import earlybird.earlybird.log.visit.service.request.VisitEventLoggingServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class VisitEventLogService { + + private final VisitEventLogRepository visitEventLogRepository; + + @Transactional + public void create(VisitEventLoggingServiceRequest request) { + visitEventLogRepository.save(new VisitEventLog(request.getClientId())); + } +} diff --git a/src/main/java/earlybird/earlybird/log/visit/service/request/VisitEventLoggingServiceRequest.java b/src/main/java/earlybird/earlybird/log/visit/service/request/VisitEventLoggingServiceRequest.java new file mode 100644 index 0000000..c4203c7 --- /dev/null +++ b/src/main/java/earlybird/earlybird/log/visit/service/request/VisitEventLoggingServiceRequest.java @@ -0,0 +1,11 @@ +package earlybird.earlybird.log.visit.service.request; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class VisitEventLoggingServiceRequest { + + private final String clientId; +} diff --git a/src/main/java/earlybird/earlybird/messaging/MessagingService.java b/src/main/java/earlybird/earlybird/messaging/MessagingService.java new file mode 100644 index 0000000..00cf76e --- /dev/null +++ b/src/main/java/earlybird/earlybird/messaging/MessagingService.java @@ -0,0 +1,15 @@ +package earlybird.earlybird.messaging; + +import earlybird.earlybird.messaging.request.SendMessageByTokenServiceRequest; + +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; + +public interface MessagingService { + @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 1000)) + void send(SendMessageByTokenServiceRequest request); + + @Recover + void recover(SendMessageByTokenServiceRequest request); +} diff --git a/src/main/java/earlybird/earlybird/messaging/MessagingServiceConfig.java b/src/main/java/earlybird/earlybird/messaging/MessagingServiceConfig.java new file mode 100644 index 0000000..ab1678b --- /dev/null +++ b/src/main/java/earlybird/earlybird/messaging/MessagingServiceConfig.java @@ -0,0 +1,27 @@ +package earlybird.earlybird.messaging; + +import earlybird.earlybird.messaging.firebase.FirebaseMessagingService; +import earlybird.earlybird.messaging.firebase.FirebaseMessagingServiceProxy; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class MessagingServiceConfig { + + @Bean + public MessagingService messagingService( + FcmNotificationRepository repository, + @Qualifier("taskExecutor") ThreadPoolTaskExecutor executor) { + return new FirebaseMessagingServiceProxy( + firebaseMessagingService(repository), repository, executor); + } + + @Bean + public FirebaseMessagingService firebaseMessagingService(FcmNotificationRepository repository) { + return new FirebaseMessagingService(repository); + } +} diff --git a/src/main/java/earlybird/earlybird/messaging/firebase/FirebaseMessagingService.java b/src/main/java/earlybird/earlybird/messaging/firebase/FirebaseMessagingService.java new file mode 100644 index 0000000..8660a48 --- /dev/null +++ b/src/main/java/earlybird/earlybird/messaging/firebase/FirebaseMessagingService.java @@ -0,0 +1,78 @@ +package earlybird.earlybird.messaging.firebase; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.*; + +import earlybird.earlybird.messaging.MessagingService; +import earlybird.earlybird.messaging.request.SendMessageByTokenServiceRequest; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; + +import jakarta.annotation.PostConstruct; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class FirebaseMessagingService implements MessagingService { + + @Value("${fcm.service-account-file}") + private String serviceAccountFilePath; + + @Value("${fcm.project-id}") + private String projectId; + + private final FcmNotificationRepository fcmNotificationRepository; + + @PostConstruct + private void init() throws IOException { + if (FirebaseApp.getApps().isEmpty()) { + ClassPathResource resource = new ClassPathResource(serviceAccountFilePath); + FirebaseOptions options = + FirebaseOptions.builder() + .setCredentials( + GoogleCredentials.fromStream( + new ClassPathResource(serviceAccountFilePath) + .getInputStream())) + .setProjectId(projectId) + .build(); + FirebaseApp.initializeApp(options); + } + } + + public void send(SendMessageByTokenServiceRequest request) { + log.info("send notification: {} {}", request.getNotificationId(), request.getTitle()); + try { + FirebaseMessaging.getInstance() + .send( + Message.builder() + .setNotification( + Notification.builder() + .setTitle(request.getTitle()) + .setBody(request.getBody()) + .build()) + .setToken(request.getDeviceToken()) + .build()); + } catch (FirebaseMessagingException e) { + throw new RuntimeException(e); + } + log.info("send success"); + } + + @Transactional + public void recover(SendMessageByTokenServiceRequest request) { + log.error("recover notification: {} {}", request.getNotificationId(), request.getTitle()); + fcmNotificationRepository + .findById(request.getNotificationId()) + .ifPresent(FcmNotification::onSendToFcmFailure); + } +} diff --git a/src/main/java/earlybird/earlybird/messaging/firebase/FirebaseMessagingServiceProxy.java b/src/main/java/earlybird/earlybird/messaging/firebase/FirebaseMessagingServiceProxy.java new file mode 100644 index 0000000..e56b22f --- /dev/null +++ b/src/main/java/earlybird/earlybird/messaging/firebase/FirebaseMessagingServiceProxy.java @@ -0,0 +1,48 @@ +package earlybird.earlybird.messaging.firebase; + +import earlybird.earlybird.messaging.MessagingService; +import earlybird.earlybird.messaging.request.SendMessageByTokenServiceRequest; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; +import earlybird.earlybird.scheduler.notification.domain.NotificationStatus; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +public class FirebaseMessagingServiceProxy implements MessagingService { + + private final FirebaseMessagingService target; + private final FcmNotificationRepository notificationRepository; + private final ThreadPoolTaskExecutor taskExecutor; + + public FirebaseMessagingServiceProxy( + FirebaseMessagingService target, + FcmNotificationRepository notificationRepository, + @Qualifier("taskExecutor") ThreadPoolTaskExecutor taskExecutor) { + this.target = target; + this.notificationRepository = notificationRepository; + this.taskExecutor = taskExecutor; + } + + @Transactional + @Override + public void send(SendMessageByTokenServiceRequest request) { + Long notificationId = request.getNotificationId(); + notificationRepository + .findByIdAndStatusForUpdate(notificationId, NotificationStatus.PENDING) + .ifPresent( + notification -> { + taskExecutor.execute(() -> target.send(request)); + notification.onSendToFcmSuccess(); + }); + } + + @Transactional + @Override + public void recover(SendMessageByTokenServiceRequest request) { + target.recover(request); + } +} diff --git a/src/main/java/earlybird/earlybird/messaging/request/SendMessageByTokenServiceRequest.java b/src/main/java/earlybird/earlybird/messaging/request/SendMessageByTokenServiceRequest.java new file mode 100644 index 0000000..e89b64b --- /dev/null +++ b/src/main/java/earlybird/earlybird/messaging/request/SendMessageByTokenServiceRequest.java @@ -0,0 +1,44 @@ +package earlybird.earlybird.messaging.request; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStep.ONE_HOUR_BEFORE_PREPARATION_TIME; + +import earlybird.earlybird.scheduler.manager.request.AddNotificationToSchedulerServiceRequest; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class SendMessageByTokenServiceRequest { + private String title; + private String body; + private String deviceToken; + private Long notificationId; + + @Builder + private SendMessageByTokenServiceRequest( + String title, String body, String deviceToken, Long notificationId) { + this.title = title; + this.body = body; + this.deviceToken = deviceToken; + this.notificationId = notificationId; + } + + public static SendMessageByTokenServiceRequest from( + AddNotificationToSchedulerServiceRequest request) { + + NotificationStep notificationStep = request.getNotificationStep(); + String appointmentName = request.getAppointment().getAppointmentName(); + String title = + (notificationStep.equals(ONE_HOUR_BEFORE_PREPARATION_TIME)) + ? appointmentName + notificationStep.getTitle() + : notificationStep.getTitle(); + + return SendMessageByTokenServiceRequest.builder() + .title(title) + .body(notificationStep.getBody()) + .deviceToken(request.getDeviceToken()) + .notificationId(request.getNotificationId()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/config/SchedulerConfig.java b/src/main/java/earlybird/earlybird/scheduler/config/SchedulerConfig.java new file mode 100644 index 0000000..3f311d8 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/config/SchedulerConfig.java @@ -0,0 +1,21 @@ +package earlybird.earlybird.scheduler.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +// @EnableAsync +@Configuration +public class SchedulerConfig { + + private static final int POOL_SIZE = 2; // default: 1 + + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(POOL_SIZE); + scheduler.setThreadNamePrefix("EarlyBird-ThreadPool-Scheduler"); + scheduler.initialize(); + return scheduler; + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/manager/NotificationSchedulerManager.java b/src/main/java/earlybird/earlybird/scheduler/manager/NotificationSchedulerManager.java new file mode 100644 index 0000000..91fa987 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/manager/NotificationSchedulerManager.java @@ -0,0 +1,11 @@ +package earlybird.earlybird.scheduler.manager; + +import earlybird.earlybird.scheduler.manager.request.AddNotificationToSchedulerServiceRequest; + +public interface NotificationSchedulerManager { + void init(); + + void add(AddNotificationToSchedulerServiceRequest request); + + void remove(Long notificationId); +} diff --git a/src/main/java/earlybird/earlybird/scheduler/manager/aws/AwsNotificationSchedulerManagerService.java b/src/main/java/earlybird/earlybird/scheduler/manager/aws/AwsNotificationSchedulerManagerService.java new file mode 100644 index 0000000..418065c --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/manager/aws/AwsNotificationSchedulerManagerService.java @@ -0,0 +1,126 @@ +package earlybird.earlybird.scheduler.manager.aws; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.PENDING; + +import earlybird.earlybird.common.LocalDateTimeUtil; +import earlybird.earlybird.error.exception.FcmNotificationNotFoundException; +import earlybird.earlybird.scheduler.manager.NotificationSchedulerManager; +import earlybird.earlybird.scheduler.manager.request.AddNotificationToSchedulerServiceRequest; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; + +import jakarta.annotation.PostConstruct; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +public class AwsNotificationSchedulerManagerService implements NotificationSchedulerManager { + private final DynamoDbClientFactory dynamoDbClientFactory; + private final String tableName = "earlybird-notification"; + private final String partitionKey = "notificationId"; + private final String sortKey = "targetTime"; + private final FcmNotificationRepository fcmNotificationRepository; + private final DateTimeFormatter targetTimeFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public AwsNotificationSchedulerManagerService( + DynamoDbClientFactory dynamoDbClientFactory, + FcmNotificationRepository fcmNotificationRepository) { + this.dynamoDbClientFactory = dynamoDbClientFactory; + this.fcmNotificationRepository = fcmNotificationRepository; + } + + @Override + @PostConstruct + public void init() { + fcmNotificationRepository.findAllByStatusIs(PENDING).stream() + .filter( + notification -> + notification + .getTargetTime() + .isAfter(LocalDateTimeUtil.getLocalDateTimeNow())) + .map(AddNotificationToSchedulerServiceRequest::of) + .forEach(this::add); + } + + @Override + @Transactional + public void add(AddNotificationToSchedulerServiceRequest request) { + Map notificationInfo = createNotificationAttributeMap(request); + + try (DynamoDbClient dynamoDbClient = dynamoDbClientFactory.create()) { + dynamoDbClient.putItem( + PutItemRequest.builder() + .tableName(this.tableName) + .item(notificationInfo) + .build()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + @Transactional + public void remove(Long notificationId) { + Map key = createNotificationAttributeMap(notificationId); + try (DynamoDbClient dynamoDbClient = dynamoDbClientFactory.create()) { + dynamoDbClient.deleteItem( + DeleteItemRequest.builder().tableName(tableName).key(key).build()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Map createNotificationAttributeMap(Long notificationId) { + FcmNotification notification = + fcmNotificationRepository + .findById(notificationId) + .orElseThrow(FcmNotificationNotFoundException::new); + String targetTime = notification.getTargetTime().format(targetTimeFormatter); + String notificationIdStr = String.valueOf(notificationId); + + Map key = new HashMap<>(); + key.put(this.partitionKey, AttributeValue.builder().s(notificationIdStr).build()); + key.put(this.sortKey, AttributeValue.builder().s(targetTime).build()); + return key; + } + + private Map createNotificationAttributeMap( + AddNotificationToSchedulerServiceRequest request) { + String notificationTitle = request.getNotificationStep().getTitle(); + String notificationBody = request.getNotificationStep().getBody(); + String targetTime = + LocalDateTime.ofInstant(request.getTargetTime(), ZoneId.of("Asia/Seoul")) + .format(targetTimeFormatter); + String deviceToken = request.getDeviceToken(); + String notificationId = String.valueOf(request.getNotificationId()); + + Map notificationInfo = new HashMap<>(); + notificationInfo.put("notificationTitle", createStringAttributeValue(notificationTitle)); + notificationInfo.put("notificationBody", createStringAttributeValue(notificationBody)); + notificationInfo.put(this.sortKey, createStringAttributeValue(targetTime)); + notificationInfo.put("deviceToken", createStringAttributeValue(deviceToken)); + notificationInfo.put(this.partitionKey, createStringAttributeValue(notificationId)); + + return notificationInfo; + } + + private AttributeValue createStringAttributeValue(String value) { + return AttributeValue.builder().s(value).build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/manager/aws/DefaultDynamoDbClientFactory.java b/src/main/java/earlybird/earlybird/scheduler/manager/aws/DefaultDynamoDbClientFactory.java new file mode 100644 index 0000000..f6142b5 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/manager/aws/DefaultDynamoDbClientFactory.java @@ -0,0 +1,30 @@ +package earlybird.earlybird.scheduler.manager.aws; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +@Service +public class DefaultDynamoDbClientFactory implements DynamoDbClientFactory { + private final AwsBasicCredentials credentials; + + public DefaultDynamoDbClientFactory( + @Value("${aws.access-key}") String awsAccessKey, + @Value("${aws.secret-access-key}") String awsSecretAccessKey) { + credentials = AwsBasicCredentials.create(awsAccessKey, awsSecretAccessKey); + } + + @Override + @Transactional + public DynamoDbClient create() { + return DynamoDbClient.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(StaticCredentialsProvider.create(this.credentials)) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/manager/aws/DynamoDbClientFactory.java b/src/main/java/earlybird/earlybird/scheduler/manager/aws/DynamoDbClientFactory.java new file mode 100644 index 0000000..9e62f42 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/manager/aws/DynamoDbClientFactory.java @@ -0,0 +1,7 @@ +package earlybird.earlybird.scheduler.manager.aws; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public interface DynamoDbClientFactory { + DynamoDbClient create(); +} diff --git a/src/main/java/earlybird/earlybird/scheduler/manager/request/AddNotificationToSchedulerServiceRequest.java b/src/main/java/earlybird/earlybird/scheduler/manager/request/AddNotificationToSchedulerServiceRequest.java new file mode 100644 index 0000000..3269851 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/manager/request/AddNotificationToSchedulerServiceRequest.java @@ -0,0 +1,34 @@ +package earlybird.earlybird.scheduler.manager.request; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; + +import lombok.Builder; +import lombok.Getter; + +import java.time.Instant; +import java.time.ZoneId; + +@Getter +@Builder +public class AddNotificationToSchedulerServiceRequest { + private Instant targetTime; + private NotificationStep notificationStep; + private String deviceToken; + private String clientId; + private Appointment appointment; + private Long notificationId; + + public static AddNotificationToSchedulerServiceRequest of(FcmNotification notification) { + return AddNotificationToSchedulerServiceRequest.builder() + .appointment(notification.getAppointment()) + .deviceToken(notification.getAppointment().getDeviceToken()) + .notificationStep(notification.getNotificationStep()) + .clientId(notification.getAppointment().getClientId()) + .targetTime( + notification.getTargetTime().atZone(ZoneId.of("Asia/Seoul")).toInstant()) + .notificationId(notification.getId()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/manager/spring/NotificationTaskSchedulerService.java b/src/main/java/earlybird/earlybird/scheduler/manager/spring/NotificationTaskSchedulerService.java new file mode 100644 index 0000000..b9e4e99 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/manager/spring/NotificationTaskSchedulerService.java @@ -0,0 +1,91 @@ +package earlybird.earlybird.scheduler.manager.spring; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.PENDING; + +import earlybird.earlybird.common.LocalDateTimeUtil; +import earlybird.earlybird.messaging.MessagingService; +import earlybird.earlybird.messaging.request.SendMessageByTokenServiceRequest; +import earlybird.earlybird.scheduler.manager.NotificationSchedulerManager; +import earlybird.earlybird.scheduler.manager.request.AddNotificationToSchedulerServiceRequest; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; + +import jakarta.annotation.PostConstruct; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.scheduling.TaskScheduler; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +@Slf4j +@RequiredArgsConstructor +// @Service +public class NotificationTaskSchedulerService implements NotificationSchedulerManager { + + private final TaskScheduler taskScheduler; + private final MessagingService messagingService; + private final FcmNotificationRepository fcmNotificationRepository; + + private final ConcurrentHashMap> notificationIdAndScheduleFutureMap = + new ConcurrentHashMap<>(); + + @PostConstruct + @Override + public void init() { + fcmNotificationRepository.findAllByStatusIs(PENDING).stream() + .filter( + notification -> + notification + .getTargetTime() + .isAfter(LocalDateTimeUtil.getLocalDateTimeNow())) + .map(AddNotificationToSchedulerServiceRequest::of) + .forEach(this::add); + } + + @Transactional + @Override + public void add(AddNotificationToSchedulerServiceRequest request) { + Long notificationId = request.getNotificationId(); + Instant targetTime = request.getTargetTime(); + + if (targetTime.isBefore(getNowInstant())) return; + + SendMessageByTokenServiceRequest sendRequest = + SendMessageByTokenServiceRequest.from(request); + + ScheduledFuture schedule = + taskScheduler.schedule( + () -> { + messagingService.send(sendRequest); + notificationIdAndScheduleFutureMap.remove(notificationId); + }, + targetTime); + + notificationIdAndScheduleFutureMap.put(notificationId, schedule); + } + + private Instant getNowInstant() { + return ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toInstant(); + } + + @Transactional + @Override + public void remove(Long notificationId) { + ScheduledFuture scheduledFuture = notificationIdAndScheduleFutureMap.get(notificationId); + if (scheduledFuture == null) { + return; + } + scheduledFuture.cancel(false); + notificationIdAndScheduleFutureMap.remove(notificationId); + } + + public boolean has(Long notificationId) { + return notificationIdAndScheduleFutureMap.containsKey(notificationId); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/controller/FcmSchedulerController.java b/src/main/java/earlybird/earlybird/scheduler/notification/controller/FcmSchedulerController.java new file mode 100644 index 0000000..c8f5f75 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/controller/FcmSchedulerController.java @@ -0,0 +1,77 @@ +package earlybird.earlybird.scheduler.notification.controller; + +import com.google.firebase.messaging.FirebaseMessagingException; + +import earlybird.earlybird.scheduler.notification.controller.request.DeregisterNotificationByTokenRequest; +import earlybird.earlybird.scheduler.notification.controller.request.RegisterNotificationByTokenRequest; +import earlybird.earlybird.scheduler.notification.controller.request.UpdateNotificationRequest; +import earlybird.earlybird.scheduler.notification.controller.response.RegisterNotificationByTokenResponse; +import earlybird.earlybird.scheduler.notification.controller.response.UpdateNotificationResponse; +import earlybird.earlybird.scheduler.notification.service.deregister.DeregisterNotificationService; +import earlybird.earlybird.scheduler.notification.service.register.RegisterNotificationAtSchedulerService; +import earlybird.earlybird.scheduler.notification.service.register.response.RegisterNotificationServiceResponse; +import earlybird.earlybird.scheduler.notification.service.update.UpdateNotificationService; +import earlybird.earlybird.scheduler.notification.service.update.response.UpdateFcmMessageServiceResponse; + +import jakarta.validation.Valid; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * Deprecated: 클라이언트에서 이 API 를 사용하는 코드가 전부 AppointmentController 의 API 로 변경되면 이 컨트롤러 삭제 TODO: 람다에서 + * 전송 성공하거나 실패했을 때 호출할 컨트롤러 만들기 + */ +@Deprecated +@RequiredArgsConstructor +@RequestMapping("/api/v1/message/fcm/token") +@RestController +public class FcmSchedulerController { + + private final RegisterNotificationAtSchedulerService registerService; + private final DeregisterNotificationService deregisterService; + private final UpdateNotificationService updateService; + + @PostMapping + public ResponseEntity registerTokenNotification( + @Valid @RequestBody RegisterNotificationByTokenRequest request) + throws FirebaseMessagingException { + RegisterNotificationServiceResponse serviceResponse = + registerService.registerFcmMessageForNewAppointment( + request.toRegisterFcmMessageForNewAppointmentAtSchedulerRequest()); + + RegisterNotificationByTokenResponse controllerResponse = + RegisterNotificationByTokenResponse.from(serviceResponse); + + return ResponseEntity.status(HttpStatus.OK).body(controllerResponse); + } + + @PatchMapping + public ResponseEntity updateNotification( + @Valid @RequestBody UpdateNotificationRequest request) { + UpdateFcmMessageServiceResponse serviceResponse = + updateService.update(request.toServiceRequest()); + UpdateNotificationResponse controllerResponse = + UpdateNotificationResponse.from(serviceResponse); + + return ResponseEntity.ok().body(controllerResponse); + } + + @DeleteMapping + public ResponseEntity deregisterTokenNotificationAtScheduler( + @RequestHeader("appointmentId") Long appointmentId, + @RequestHeader("clientId") String clientId) { + + DeregisterNotificationByTokenRequest request = + DeregisterNotificationByTokenRequest.builder() + .appointmentId(appointmentId) + .clientId(clientId) + .build(); + + deregisterService.deregister(request.toServiceRequest()); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/controller/NotificationStatusController.java b/src/main/java/earlybird/earlybird/scheduler/notification/controller/NotificationStatusController.java new file mode 100644 index 0000000..44697ca --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/controller/NotificationStatusController.java @@ -0,0 +1,27 @@ +package earlybird.earlybird.scheduler.notification.controller; + +import earlybird.earlybird.scheduler.notification.service.update.UpdateNotificationStatusService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/notification/status") +@RestController +public class NotificationStatusController { + + private final UpdateNotificationStatusService updateNotificationStatusService; + + // 쿼리 파라미터 이름 수정할 경우 AWS 람다 함수에서도 수정해야함 + @PostMapping + public ResponseEntity setNotificationStatus( + @RequestParam Long notificationId, @RequestParam Boolean sendSuccess) { + updateNotificationStatusService.update(notificationId, sendSuccess); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/DeregisterNotificationByTokenRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/DeregisterNotificationByTokenRequest.java new file mode 100644 index 0000000..16ab5a4 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/DeregisterNotificationByTokenRequest.java @@ -0,0 +1,35 @@ +package earlybird.earlybird.scheduler.notification.controller.request; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.CANCELLED; + +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterFcmMessageAtSchedulerServiceRequest; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class DeregisterNotificationByTokenRequest { + + @NotBlank private String clientId; + @NotNull private Long appointmentId; + + @Builder + public DeregisterNotificationByTokenRequest(String clientId, Long appointmentId) { + this.clientId = clientId; + this.appointmentId = appointmentId; + } + + public DeregisterFcmMessageAtSchedulerServiceRequest toServiceRequest() { + return DeregisterFcmMessageAtSchedulerServiceRequest.builder() + .appointmentId(appointmentId) + .clientId(clientId) + .targetNotificationStatus(CANCELLED) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/RegisterNotificationByTokenRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/RegisterNotificationByTokenRequest.java new file mode 100644 index 0000000..f7e08f9 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/RegisterNotificationByTokenRequest.java @@ -0,0 +1,57 @@ +package earlybird.earlybird.scheduler.notification.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import earlybird.earlybird.scheduler.notification.service.register.request.RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class RegisterNotificationByTokenRequest { + @NotBlank private String clientId; + @NotBlank private String deviceToken; + @NotBlank private String appointmentName; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime appointmentTime; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime preparationTime; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime movingTime; + + public RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest + toRegisterFcmMessageForNewAppointmentAtSchedulerRequest() { + return RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest.builder() + .clientId(this.clientId) + .deviceToken(this.deviceToken) + .appointmentName(this.appointmentName) + .appointmentTime(this.appointmentTime) + .preparationTime(this.preparationTime) + .movingTime(this.movingTime) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/UpdateNotificationRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/UpdateNotificationRequest.java new file mode 100644 index 0000000..b823acf --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/controller/request/UpdateNotificationRequest.java @@ -0,0 +1,58 @@ +package earlybird.earlybird.scheduler.notification.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import earlybird.earlybird.scheduler.notification.domain.NotificationUpdateType; +import earlybird.earlybird.scheduler.notification.service.update.request.UpdateFcmMessageServiceRequest; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class UpdateNotificationRequest { + + @NotNull private Long appointmentId; + @NotBlank private String appointmentName; + @NotBlank private String clientId; + @NotBlank private String deviceToken; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime appointmentTime; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime preparationTime; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime movingTime; + + @NotNull private NotificationUpdateType updateType; + + public UpdateFcmMessageServiceRequest toServiceRequest() { + return UpdateFcmMessageServiceRequest.builder() + .appointmentId(appointmentId) + .appointmentName(appointmentName) + .clientId(clientId) + .deviceToken(deviceToken) + .preparationTime(preparationTime) + .movingTime(movingTime) + .appointmentTime(appointmentTime) + .updateType(updateType) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/controller/response/RegisterNotificationByTokenResponse.java b/src/main/java/earlybird/earlybird/scheduler/notification/controller/response/RegisterNotificationByTokenResponse.java new file mode 100644 index 0000000..060ac5e --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/controller/response/RegisterNotificationByTokenResponse.java @@ -0,0 +1,46 @@ +package earlybird.earlybird.scheduler.notification.controller.response; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.service.register.response.RegisterNotificationServiceResponse; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +public class RegisterNotificationByTokenResponse { + private final Long appointmentId; + private final String appointmentName; + private final String clientId; + private final String deviceToken; + private final List notifications; + + @Builder + private RegisterNotificationByTokenResponse( + Long appointmentId, + String appointmentName, + String clientId, + String deviceToken, + List notifications) { + this.appointmentId = appointmentId; + this.appointmentName = appointmentName; + this.clientId = clientId; + this.deviceToken = deviceToken; + this.notifications = notifications; + } + + public static RegisterNotificationByTokenResponse from( + RegisterNotificationServiceResponse serviceResponse) { + Appointment appointment = serviceResponse.getAppointment(); + + return RegisterNotificationByTokenResponse.builder() + .appointmentId(appointment.getId()) + .appointmentName(appointment.getAppointmentName()) + .clientId(appointment.getClientId()) + .deviceToken(appointment.getDeviceToken()) + .notifications(serviceResponse.getNotifications()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/controller/response/UpdateNotificationResponse.java b/src/main/java/earlybird/earlybird/scheduler/notification/controller/response/UpdateNotificationResponse.java new file mode 100644 index 0000000..3e954fc --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/controller/response/UpdateNotificationResponse.java @@ -0,0 +1,37 @@ +package earlybird.earlybird.scheduler.notification.controller.response; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.service.update.response.UpdateFcmMessageServiceResponse; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +public class UpdateNotificationResponse { + + private final Long appointmentId; + private final String appointmentName; + private final String clientId; + private final String deviceToken; + private final List notifications; + + @Builder + public UpdateNotificationResponse( + Appointment appointment, List notifications) { + this.appointmentId = appointment.getId(); + this.appointmentName = appointment.getAppointmentName(); + this.clientId = appointment.getClientId(); + this.deviceToken = appointment.getDeviceToken(); + this.notifications = notifications; + } + + public static UpdateNotificationResponse from(UpdateFcmMessageServiceResponse serviceResponse) { + return UpdateNotificationResponse.builder() + .appointment(serviceResponse.getAppointment()) + .notifications(serviceResponse.getNotifications()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/domain/FcmNotification.java b/src/main/java/earlybird/earlybird/scheduler/notification/domain/FcmNotification.java new file mode 100644 index 0000000..97a116e --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/domain/FcmNotification.java @@ -0,0 +1,68 @@ +package earlybird.earlybird.scheduler.notification.domain; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.*; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.common.LocalDateTimeUtil; + +import jakarta.persistence.*; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class FcmNotification extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "fcm_notification_id") + private Long id; + + @Setter + @JsonIgnore + @ManyToOne + @JoinColumn(name = "appointment_id", nullable = false) + private Appointment appointment; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private NotificationStep notificationStep; + + @Column(nullable = false) + private LocalDateTime targetTime; + + @JsonIgnore + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private NotificationStatus status = PENDING; + + @JsonIgnore private LocalDateTime sentTime; + + @Builder + private FcmNotification( + Appointment appointment, NotificationStep notificationStep, LocalDateTime targetTime) { + this.appointment = appointment; + this.notificationStep = notificationStep; + this.targetTime = targetTime; + } + + public void onSendToFcmSuccess() { + this.sentTime = LocalDateTimeUtil.getLocalDateTimeNow(); + updateStatusTo(COMPLETED); + } + + public void onSendToFcmFailure() { + this.sentTime = null; + updateStatusTo(FAILED); + } + + public void updateStatusTo(NotificationStatus status) { + this.status = status; + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepository.java b/src/main/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepository.java new file mode 100644 index 0000000..4a4903f --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepository.java @@ -0,0 +1,21 @@ +package earlybird.earlybird.scheduler.notification.domain; + +import jakarta.persistence.LockModeType; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface FcmNotificationRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT n FROM FcmNotification n WHERE n.id = :id AND n.status = :status") + Optional findByIdAndStatusForUpdate( + @Param("id") Long id, @Param("status") NotificationStatus status); + + List findAllByStatusIs(NotificationStatus status); +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationStatus.java b/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationStatus.java new file mode 100644 index 0000000..89b06f8 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationStatus.java @@ -0,0 +1,18 @@ +package earlybird.earlybird.scheduler.notification.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationStatus { + PENDING("전송 대기 중"), + COMPLETED("전송 완료"), + FAILED("전송 실패"), + MODIFIED("변경됨"), + POSTPONE("미뤄짐"), + CANCELLED("취소"), + CANCELLED_BY_ARRIVE_ON_TIME("정시 도착"); + + private final String text; +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationStep.java b/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationStep.java new file mode 100644 index 0000000..c4c3cab --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationStep.java @@ -0,0 +1,21 @@ +package earlybird.earlybird.scheduler.notification.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationStep { + ONE_HOUR_BEFORE_PREPARATION_TIME(1L, " 준비 1시간 전!", "오늘의 준비사항을 확인해봐요 \uD83D\uDE0A"), + FIVE_MINUTES_BEFORE_PREPARATION_TIME(2L, "5분 후에 준비 시작해야 해요!", "허겁지겁 준비하면 후회해요! \uD83E\uDEE2"), + PREPARATION_TIME(3L, "지금 준비 시작 안하면 늦어요 ❗\uFE0F❗\uFE0F❗\uFE0F", "같이 5초 세고, 시작해봐요!"), + TEN_MINUTES_BEFORE_MOVING_TIME(4L, "10분 후에 이동해야 안 늦어요!", "교통정보를 미리 확인해보세요 \uD83D\uDEA5"), + MOVING_TIME(5L, "지금 출발해야 안 늦어요 ❗\uFE0F❗\uFE0F❗\uFE0F", "준비사항 다 체크하셨나요?"), + FIVE_MINUTES_BEFORE_APPOINTMENT_TIME(6L, "약속장소에 도착하셨나요?!", "도착하셨으면 확인버튼을 눌러주세요! \uD83E\uDD29"), + APPOINTMENT_TIME( + 7L, "1분 안에 확인버튼을 눌러주세요!!", "안 누르면 지각처리돼요!!! \uD83D\uDEAB\uD83D\uDEAB\uD83D\uDEAB"); + + private final Long stepNumber; + private final String title; + private final String body; +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationUpdateType.java b/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationUpdateType.java new file mode 100644 index 0000000..8513e56 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/domain/NotificationUpdateType.java @@ -0,0 +1,14 @@ +package earlybird.earlybird.scheduler.notification.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationUpdateType { + POSTPONE("약속 미루기"), + MODIFY("일정 수정하기"), + ARRIVE_ON_TIME("정시 도착"); + + private final String type; +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/NotificationInfoFactory.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/NotificationInfoFactory.java new file mode 100644 index 0000000..aa42cb2 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/NotificationInfoFactory.java @@ -0,0 +1,63 @@ +package earlybird.earlybird.scheduler.notification.service; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStep.*; +import static earlybird.earlybird.scheduler.notification.domain.NotificationStep.APPOINTMENT_TIME; + +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; + +import lombok.Getter; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; + +@Service +public class NotificationInfoFactory { + @Getter private final List notificationStepList; + + public NotificationInfoFactory() { + this.notificationStepList = + List.of( + ONE_HOUR_BEFORE_PREPARATION_TIME, + FIVE_MINUTES_BEFORE_PREPARATION_TIME, + PREPARATION_TIME, + TEN_MINUTES_BEFORE_MOVING_TIME, + MOVING_TIME, + FIVE_MINUTES_BEFORE_APPOINTMENT_TIME, + APPOINTMENT_TIME); + } + + public Map createTargetTimeMap( + Instant preparationTimeInstant, + Instant movingTimeInstant, + Instant appointmentTimeInstant) { + return Map.of( + ONE_HOUR_BEFORE_PREPARATION_TIME, preparationTimeInstant.minus(1, ChronoUnit.HOURS), + FIVE_MINUTES_BEFORE_PREPARATION_TIME, + preparationTimeInstant.minus(5, ChronoUnit.MINUTES), + PREPARATION_TIME, preparationTimeInstant, + TEN_MINUTES_BEFORE_MOVING_TIME, movingTimeInstant.minus(10, ChronoUnit.MINUTES), + MOVING_TIME, movingTimeInstant, + FIVE_MINUTES_BEFORE_APPOINTMENT_TIME, + appointmentTimeInstant.minus(5, ChronoUnit.MINUTES), + APPOINTMENT_TIME, appointmentTimeInstant); + } + + public Map createTargetTimeMap( + LocalDateTime preparationTime, + LocalDateTime movingTime, + LocalDateTime appointmentTime) { + ZoneId seoul = ZoneId.of("Asia/Seoul"); + Instant preparationTimeInstant = preparationTime.atZone(seoul).toInstant(); + Instant movingTimeInstant = movingTime.atZone(seoul).toInstant(); + Instant appointmentTimeInstant = appointmentTime.atZone(seoul).toInstant(); + + return this.createTargetTimeMap( + preparationTimeInstant, movingTimeInstant, appointmentTimeInstant); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/DeregisterNotificationService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/DeregisterNotificationService.java new file mode 100644 index 0000000..6bce574 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/DeregisterNotificationService.java @@ -0,0 +1,40 @@ +package earlybird.earlybird.scheduler.notification.service.deregister; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.CANCELLED; +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.PENDING; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.service.FindAppointmentService; +import earlybird.earlybird.scheduler.manager.NotificationSchedulerManager; +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterFcmMessageAtSchedulerServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class DeregisterNotificationService { + + private final NotificationSchedulerManager notificationSchedulerManager; + private final FindAppointmentService findAppointmentService; + + @Transactional + public void deregister(DeregisterFcmMessageAtSchedulerServiceRequest request) { + Appointment appointment = findAppointmentService.findBy(request); + + appointment.getFcmNotifications().stream() + .filter(notification -> notification.getStatus() == PENDING) + .forEach( + notification -> { + notificationSchedulerManager.remove(notification.getId()); + notification.updateStatusTo(request.getTargetNotificationStatus()); + }); + + if (request.getTargetNotificationStatus().equals(CANCELLED)) { + appointment.setRepeatingDaysEmpty(); + appointment.setDeleted(); + } + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/request/DeregisterFcmMessageAtSchedulerServiceRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/request/DeregisterFcmMessageAtSchedulerServiceRequest.java new file mode 100644 index 0000000..2e05e95 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/request/DeregisterFcmMessageAtSchedulerServiceRequest.java @@ -0,0 +1,14 @@ +package earlybird.earlybird.scheduler.notification.service.deregister.request; + +import earlybird.earlybird.scheduler.notification.domain.NotificationStatus; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class DeregisterFcmMessageAtSchedulerServiceRequest { + private String clientId; + private Long appointmentId; + private NotificationStatus targetNotificationStatus; +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/request/DeregisterNotificationServiceRequestFactory.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/request/DeregisterNotificationServiceRequestFactory.java new file mode 100644 index 0000000..e1caa23 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/deregister/request/DeregisterNotificationServiceRequestFactory.java @@ -0,0 +1,46 @@ +package earlybird.earlybird.scheduler.notification.service.deregister.request; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationUpdateType.MODIFY; +import static earlybird.earlybird.scheduler.notification.domain.NotificationUpdateType.POSTPONE; + +import earlybird.earlybird.appointment.domain.AppointmentUpdateType; +import earlybird.earlybird.appointment.service.request.UpdateAppointmentServiceRequest; +import earlybird.earlybird.scheduler.notification.domain.NotificationStatus; +import earlybird.earlybird.scheduler.notification.domain.NotificationUpdateType; + +public class DeregisterNotificationServiceRequestFactory { + public static DeregisterFcmMessageAtSchedulerServiceRequest create( + Long appointmentId, String clientId, NotificationStatus notificationStatus) { + return DeregisterFcmMessageAtSchedulerServiceRequest.builder() + .appointmentId(appointmentId) + .clientId(clientId) + .targetNotificationStatus(notificationStatus) + .build(); + } + + public static DeregisterFcmMessageAtSchedulerServiceRequest create( + Long appointmentId, String clientId, NotificationUpdateType updateType) { + NotificationStatus targetStatus; + if (updateType.equals(POSTPONE)) targetStatus = NotificationStatus.POSTPONE; + else if (updateType.equals(MODIFY)) targetStatus = NotificationStatus.MODIFIED; + else throw new IllegalArgumentException("Invalid update type: " + updateType); + + return create(appointmentId, clientId, targetStatus); + } + + public static DeregisterFcmMessageAtSchedulerServiceRequest create( + UpdateAppointmentServiceRequest request) { + AppointmentUpdateType updateType = request.getUpdateType(); + NotificationStatus targetStatus = + switch (updateType) { + case POSTPONE -> NotificationStatus.POSTPONE; + case MODIFY -> NotificationStatus.MODIFIED; + }; + + return DeregisterFcmMessageAtSchedulerServiceRequest.builder() + .appointmentId(request.getAppointmentId()) + .clientId(request.getClientId()) + .targetNotificationStatus(targetStatus) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterAllNotificationAtSchedulerService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterAllNotificationAtSchedulerService.java new file mode 100644 index 0000000..6ff8fd9 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterAllNotificationAtSchedulerService.java @@ -0,0 +1,37 @@ +package earlybird.earlybird.scheduler.notification.service.register; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.scheduler.manager.NotificationSchedulerManager; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.Map; + +@Service +public class RegisterAllNotificationAtSchedulerService implements RegisterNotificationService { + + private final RegisterOneNotificationAtSchedulerService registerOneNotificationService; + + public RegisterAllNotificationAtSchedulerService( + FcmNotificationRepository fcmNotificationRepository, + NotificationSchedulerManager notificationSchedulerManager) { + this.registerOneNotificationService = + new RegisterOneNotificationAtSchedulerService( + fcmNotificationRepository, notificationSchedulerManager); + } + + @Override + @Transactional + public void register( + Appointment appointment, Map notificationStepAndTargetTime) { + for (NotificationStep notificationStep : notificationStepAndTargetTime.keySet()) { + registerOneNotificationService.register( + appointment, + Map.of(notificationStep, notificationStepAndTargetTime.get(notificationStep))); + } + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterNotificationAtSchedulerService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterNotificationAtSchedulerService.java new file mode 100644 index 0000000..81987e9 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterNotificationAtSchedulerService.java @@ -0,0 +1,82 @@ +package earlybird.earlybird.scheduler.notification.service.register; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.AppointmentRepository; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; +import earlybird.earlybird.scheduler.notification.service.NotificationInfoFactory; +import earlybird.earlybird.scheduler.notification.service.register.request.RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest; +import earlybird.earlybird.scheduler.notification.service.register.request.RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest; +import earlybird.earlybird.scheduler.notification.service.register.response.RegisterNotificationServiceResponse; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Map; + +@Deprecated +@Slf4j +@RequiredArgsConstructor +@Service +public class RegisterNotificationAtSchedulerService { + + private final AppointmentRepository appointmentRepository; + private final RegisterAllNotificationAtSchedulerService + registerAllNotificationAtSchedulerService; + private final NotificationInfoFactory notificationInfoFactory; + + @Deprecated + @Transactional + public RegisterNotificationServiceResponse registerFcmMessageForNewAppointment( + RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest request) { + Appointment newAppointment = createAppointmentBy(request); + + return registerFcmMessageForExistingAppointment( + RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.from( + request, newAppointment)); + } + + @Transactional + public RegisterNotificationServiceResponse registerFcmMessageForExistingAppointment( + RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest request) { + Appointment appointment = request.getAppointment(); + + Map targetTimeMap = + notificationInfoFactory.createTargetTimeMap( + request.getPreparationTimeInstant(), + request.getMovingTimeInstant(), + request.getAppointmentTimeInstant()); + + registerAllNotificationAtSchedulerService.register(appointment, targetTimeMap); + + return RegisterNotificationServiceResponse.builder() + .appointment(appointment) + .notifications(appointment.getFcmNotifications()) + .build(); + } + + private Appointment createAppointmentBy( + RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest request) { + Appointment appointment = + Appointment.builder() + .appointmentName(request.getAppointmentName()) + .clientId(request.getClientId()) + .deviceToken(request.getDeviceToken()) + .appointmentTime(request.getAppointmentTime().toLocalTime()) + .movingDuration( + Duration.between( + request.getMovingTime(), request.getAppointmentTime())) + .preparationDuration( + Duration.between( + request.getPreparationTime(), request.getMovingTime())) + .repeatingDayOfWeeks(new ArrayList<>()) + .build(); + + return appointmentRepository.save(appointment); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterNotificationService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterNotificationService.java new file mode 100644 index 0000000..194988a --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterNotificationService.java @@ -0,0 +1,12 @@ +package earlybird.earlybird.scheduler.notification.service.register; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; + +import java.time.Instant; +import java.util.Map; + +public interface RegisterNotificationService { + void register( + Appointment appointment, Map notificationStepAndTargetTime); +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterOneNotificationAtSchedulerService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterOneNotificationAtSchedulerService.java new file mode 100644 index 0000000..823ab86 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/RegisterOneNotificationAtSchedulerService.java @@ -0,0 +1,78 @@ +package earlybird.earlybird.scheduler.notification.service.register; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.common.InstantUtil; +import earlybird.earlybird.scheduler.manager.NotificationSchedulerManager; +import earlybird.earlybird.scheduler.manager.request.AddNotificationToSchedulerServiceRequest; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; + +import lombok.RequiredArgsConstructor; + +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.ZoneId; +import java.util.Map; + +@RequiredArgsConstructor +public class RegisterOneNotificationAtSchedulerService implements RegisterNotificationService { + + private final FcmNotificationRepository notificationRepository; + private final NotificationSchedulerManager notificationSchedulerManager; + + @Override + @Transactional + public void register( + Appointment appointment, Map notificationStepAndTargetTime) { + + if (notificationStepAndTargetTime.size() != 1) + throw new IllegalArgumentException("1개의 알림만 등록할 수 있습니다"); + + NotificationStep notificationStep = + notificationStepAndTargetTime.keySet().iterator().next(); + Instant targetTime = notificationStepAndTargetTime.get(notificationStep); + + if (InstantUtil.checkTimeBeforeNow(targetTime)) return; + + String deviceToken = appointment.getDeviceToken(); + + FcmNotification notification = + createNotification(notificationStep, targetTime, appointment); + + AddNotificationToSchedulerServiceRequest addTaskRequest = + createAddTaskRequest( + notificationStep, targetTime, appointment, notification, deviceToken); + + notificationSchedulerManager.add(addTaskRequest); + } + + private FcmNotification createNotification( + NotificationStep notificationStep, Instant targetTime, Appointment appointment) { + FcmNotification notification = + FcmNotification.builder() + .appointment(appointment) + .targetTime(targetTime.atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime()) + .notificationStep(notificationStep) + .build(); + + appointment.addFcmNotification(notification); + return notificationRepository.save(notification); + } + + private AddNotificationToSchedulerServiceRequest createAddTaskRequest( + NotificationStep notificationStep, + Instant targetTime, + Appointment appointment, + FcmNotification notification, + String deviceToken) { + return AddNotificationToSchedulerServiceRequest.builder() + .notificationId(notification.getId()) + .targetTime(targetTime) + .appointment(appointment) + .notificationStep(notificationStep) + .deviceToken(deviceToken) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/repeating/RegisterNotificationForRepeatingAppointmentService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/repeating/RegisterNotificationForRepeatingAppointmentService.java new file mode 100644 index 0000000..8fe2591 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/repeating/RegisterNotificationForRepeatingAppointmentService.java @@ -0,0 +1,79 @@ +package earlybird.earlybird.scheduler.notification.service.register.repeating; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.PENDING; +import static earlybird.earlybird.scheduler.notification.domain.NotificationStep.APPOINTMENT_TIME; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.RepeatingDay; +import earlybird.earlybird.appointment.domain.RepeatingDayRepository; +import earlybird.earlybird.common.LocalDateTimeUtil; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.service.register.RegisterNotificationAtSchedulerService; +import earlybird.earlybird.scheduler.notification.service.register.request.RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.util.List; + +// TODO: AWS에 맞게 로직 수정 +@RequiredArgsConstructor +@Service +public class RegisterNotificationForRepeatingAppointmentService { + + private final RepeatingDayRepository repeatingDayRepository; + private final RegisterNotificationAtSchedulerService registerService; + + @Transactional + @Scheduled(cron = "0 0 0,23 * * ?", zone = "Asia/Seoul") // 매일 0시, 23시 + protected void registerEveryDay() { + switch (LocalDateTimeUtil.getLocalDateTimeNow().getHour()) { + case 0 -> registerAfterDay(0); + case 23 -> registerAfterDay(1); + } + } + + private void registerAfterDay(int plusDays) { + DayOfWeek afterTwoDayFromNow = + LocalDateTimeUtil.getLocalDateTimeNow().plusDays(plusDays).getDayOfWeek(); + List repeatingDays = + repeatingDayRepository.findAllByDayOfWeek(afterTwoDayFromNow); + repeatingDays.forEach( + repeatingDay -> { + Appointment appointment = repeatingDay.getAppointment(); + + RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest + registerRequest = + RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest + .from(appointment); + + boolean notificationIsNotRegistered = + appointment.getFcmNotifications().stream() + .filter( + notification -> + notification + .getNotificationStep() + .equals(APPOINTMENT_TIME)) + .filter( + notification -> + isValidTargetTime( + notification, + registerRequest.getAppointmentTime())) + .noneMatch( + notification -> + notification.getStatus().equals(PENDING)); + + if (notificationIsNotRegistered) + registerService.registerFcmMessageForExistingAppointment(registerRequest); + }); + } + + private boolean isValidTargetTime(FcmNotification notification, LocalDateTime appointmentTime) { + return !notification.getTargetTime().isBefore(appointmentTime); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterAllNotificationServiceRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterAllNotificationServiceRequest.java new file mode 100644 index 0000000..576ad16 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterAllNotificationServiceRequest.java @@ -0,0 +1,17 @@ +package earlybird.earlybird.scheduler.notification.service.register.request; + +import earlybird.earlybird.appointment.domain.Appointment; + +import lombok.Builder; +import lombok.Getter; + +import java.time.Instant; + +@Getter +@Builder +public class RegisterAllNotificationServiceRequest { + private Instant preparationTimeInstant; + private Instant movingTimeInstant; + private Instant appointmentTimeInstant; + private Appointment appointment; +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.java new file mode 100644 index 0000000..30f5590 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.java @@ -0,0 +1,119 @@ +package earlybird.earlybird.scheduler.notification.service.register.request; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.service.request.UpdateAppointmentServiceRequest; +import earlybird.earlybird.common.LocalDateTimeUtil; +import earlybird.earlybird.common.LocalDateUtil; +import earlybird.earlybird.scheduler.notification.service.update.request.UpdateFcmMessageServiceRequest; + +import lombok.Builder; +import lombok.Getter; + +import java.time.*; + +@Getter +public class RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest { + private String clientId; + private String deviceToken; + private LocalDateTime appointmentTime; + private LocalDateTime preparationTime; + private LocalDateTime movingTime; + private Appointment appointment; + + @Builder + private RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest( + String clientId, + String deviceToken, + LocalDateTime appointmentTime, + LocalDateTime preparationTime, + LocalDateTime movingTime, + Appointment appointment) { + this.clientId = clientId; + this.deviceToken = deviceToken; + this.appointmentTime = appointmentTime; + this.preparationTime = preparationTime; + this.movingTime = movingTime; + this.appointment = appointment; + } + + public Instant getAppointmentTimeInstant() { + return appointmentTime.atZone(ZoneId.of("Asia/Seoul")).toInstant(); + } + + public Instant getPreparationTimeInstant() { + return preparationTime.atZone(ZoneId.of("Asia/Seoul")).toInstant(); + } + + public Instant getMovingTimeInstant() { + return movingTime.atZone(ZoneId.of("Asia/Seoul")).toInstant(); + } + + public static RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest from( + RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest request, + Appointment appointment) { + return RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.builder() + .clientId(request.getClientId()) + .deviceToken(request.getDeviceToken()) + .appointment(appointment) + .preparationTime(request.getPreparationTime()) + .movingTime(request.getMovingTime()) + .appointmentTime(request.getAppointmentTime()) + .build(); + } + + public static RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest from( + UpdateFcmMessageServiceRequest request, Appointment appointment) { + return RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.builder() + .clientId(request.getClientId()) + .deviceToken(request.getDeviceToken()) + .appointment(appointment) + .preparationTime(request.getPreparationTime()) + .movingTime(request.getMovingTime()) + .appointmentTime(request.getAppointmentTime()) + .build(); + } + + public static RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest from( + Appointment appointment) { + + LocalDateTime appointmentTime = + appointment + .getAppointmentTime() + .atDate(LocalDateUtil.getLocalDateNow().plusDays(2)); + LocalDateTime movingTime = + LocalDateTimeUtil.subtractDuration( + appointmentTime, appointment.getMovingDuration()); + LocalDateTime preparationTime = + LocalDateTimeUtil.subtractDuration( + movingTime, appointment.getPreparationDuration()); + + return RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.builder() + .clientId(appointment.getClientId()) + .deviceToken(appointment.getDeviceToken()) + .appointment(appointment) + .preparationTime(preparationTime) + .movingTime(movingTime) + .appointmentTime(appointmentTime) + .build(); + } + + public static RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest from( + UpdateAppointmentServiceRequest request, Appointment appointment) { + + LocalDateTime firstAppointmentTime = request.getFirstAppointmentTime(); + LocalDateTime movingTime = + LocalDateTimeUtil.subtractDuration( + firstAppointmentTime, request.getMovingDuration()); + LocalDateTime preparationTime = + LocalDateTimeUtil.subtractDuration(movingTime, request.getPreparationDuration()); + + return RegisterFcmMessageForExistingAppointmentAtSchedulerServiceRequest.builder() + .clientId(request.getClientId()) + .deviceToken(request.getDeviceToken()) + .appointment(appointment) + .preparationTime(preparationTime) + .movingTime(movingTime) + .appointmentTime(firstAppointmentTime) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest.java new file mode 100644 index 0000000..0ef4801 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/request/RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest.java @@ -0,0 +1,46 @@ +package earlybird.earlybird.scheduler.notification.service.register.request; + +import lombok.Builder; +import lombok.Getter; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Getter +public class RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest { + private String clientId; + private String deviceToken; + private String appointmentName; + private LocalDateTime appointmentTime; + private LocalDateTime preparationTime; + private LocalDateTime movingTime; + + @Builder + private RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest( + String clientId, + String deviceToken, + String appointmentName, + LocalDateTime appointmentTime, + LocalDateTime preparationTime, + LocalDateTime movingTime) { + this.clientId = clientId; + this.deviceToken = deviceToken; + this.appointmentName = appointmentName; + this.appointmentTime = appointmentTime; + this.preparationTime = preparationTime; + this.movingTime = movingTime; + } + + public Instant getAppointmentTimeInstant() { + return appointmentTime.atZone(ZoneId.of("Asia/Seoul")).toInstant(); + } + + public Instant getPreparationTimeInstant() { + return preparationTime.atZone(ZoneId.of("Asia/Seoul")).toInstant(); + } + + public Instant getMovingTimeInstant() { + return movingTime.atZone(ZoneId.of("Asia/Seoul")).toInstant(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/register/response/RegisterNotificationServiceResponse.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/response/RegisterNotificationServiceResponse.java new file mode 100644 index 0000000..343e23c --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/register/response/RegisterNotificationServiceResponse.java @@ -0,0 +1,30 @@ +package earlybird.earlybird.scheduler.notification.service.register.response; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +public class RegisterNotificationServiceResponse { + + private final Appointment appointment; + private final List notifications; + + @Builder + private RegisterNotificationServiceResponse( + Appointment appointment, List notifications) { + this.appointment = appointment; + this.notifications = notifications; + } + + public static RegisterNotificationServiceResponse of(Appointment appointment) { + return RegisterNotificationServiceResponse.builder() + .appointment(appointment) + .notifications(appointment.getFcmNotifications()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationAtArriveOnTimeService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationAtArriveOnTimeService.java new file mode 100644 index 0000000..3f46257 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationAtArriveOnTimeService.java @@ -0,0 +1,47 @@ +package earlybird.earlybird.scheduler.notification.service.update; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStatus.CANCELLED_BY_ARRIVE_ON_TIME; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.AppointmentRepository; +import earlybird.earlybird.error.exception.AppointmentNotFoundException; +import earlybird.earlybird.scheduler.notification.service.deregister.DeregisterNotificationService; +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterFcmMessageAtSchedulerServiceRequest; +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterNotificationServiceRequestFactory; +import earlybird.earlybird.scheduler.notification.service.update.request.UpdateNotificationAtArriveOnTimeServiceRequest; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class UpdateNotificationAtArriveOnTimeService { + + private final AppointmentRepository appointmentRepository; + private final DeregisterNotificationService deregisterNotificationService; + + @Transactional + public void update(UpdateNotificationAtArriveOnTimeServiceRequest request) { + Appointment appointment = + appointmentRepository + .findById(request.getAppointmentId()) + .orElseThrow(AppointmentNotFoundException::new); + + if (!isValidClientId(appointment, request.getClientId())) + throw new AppointmentNotFoundException(); + + DeregisterFcmMessageAtSchedulerServiceRequest deregisterRequest = + DeregisterNotificationServiceRequestFactory.create( + appointment.getId(), + appointment.getClientId(), + CANCELLED_BY_ARRIVE_ON_TIME); + + deregisterNotificationService.deregister(deregisterRequest); + } + + private boolean isValidClientId(Appointment appointment, String requestedClientId) { + return appointment.getClientId().equals(requestedClientId); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationService.java new file mode 100644 index 0000000..286bf77 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationService.java @@ -0,0 +1,69 @@ +package earlybird.earlybird.scheduler.notification.service.update; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.service.FindAppointmentService; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; +import earlybird.earlybird.scheduler.notification.domain.NotificationUpdateType; +import earlybird.earlybird.scheduler.notification.service.NotificationInfoFactory; +import earlybird.earlybird.scheduler.notification.service.deregister.DeregisterNotificationService; +import earlybird.earlybird.scheduler.notification.service.deregister.request.DeregisterNotificationServiceRequestFactory; +import earlybird.earlybird.scheduler.notification.service.register.RegisterNotificationService; +import earlybird.earlybird.scheduler.notification.service.update.request.UpdateFcmMessageServiceRequest; +import earlybird.earlybird.scheduler.notification.service.update.response.UpdateFcmMessageServiceResponse; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.Map; + +@RequiredArgsConstructor +@Service +public class UpdateNotificationService { + + private final RegisterNotificationService registerNotificationService; + private final DeregisterNotificationService deregisterNotificationService; + private final FindAppointmentService findAppointmentService; + private final NotificationInfoFactory notificationInfoFactory; + + @Transactional + public UpdateFcmMessageServiceResponse update(UpdateFcmMessageServiceRequest request) { + + Long appointmentId = request.getAppointmentId(); + String clientId = request.getClientId(); + Appointment appointment = + findAppointmentService.findBy(appointmentId, request.getClientId()); + NotificationUpdateType updateType = request.getUpdateType(); + + deregisterNotificationService.deregister( + DeregisterNotificationServiceRequestFactory.create( + appointmentId, clientId, updateType)); + + Map notificationInfo = + notificationInfoFactory.createTargetTimeMap( + request.getPreparationTime(), + request.getMovingTime(), + request.getAppointmentTime()); + + registerNotificationService.register(appointment, notificationInfo); + + changeDeviceTokenIfChanged(appointment, request.getDeviceToken()); + changeAppointmentNameIfChanged(appointment, request.getAppointmentName()); + + return UpdateFcmMessageServiceResponse.of(appointment); + } + + private void changeDeviceTokenIfChanged(Appointment appointment, String deviceToken) { + if (!appointment.getDeviceToken().equals(deviceToken)) { + appointment.changeDeviceToken(deviceToken); + } + } + + private void changeAppointmentNameIfChanged(Appointment appointment, String appointmentName) { + if (!appointment.getAppointmentName().equals(appointmentName)) { + appointment.changeAppointmentName(appointmentName); + } + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationStatusService.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationStatusService.java new file mode 100644 index 0000000..3d1bed1 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationStatusService.java @@ -0,0 +1,27 @@ +package earlybird.earlybird.scheduler.notification.service.update; + +import earlybird.earlybird.error.exception.NotificationNotFoundException; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class UpdateNotificationStatusService { + + private final FcmNotificationRepository notificationRepository; + + @Transactional + public void update(Long notificationId, Boolean sendSuccess) { + FcmNotification notification = + notificationRepository + .findById(notificationId) + .orElseThrow(NotificationNotFoundException::new); + if (sendSuccess) notification.onSendToFcmSuccess(); + else notification.onSendToFcmFailure(); + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/update/request/UpdateFcmMessageServiceRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/request/UpdateFcmMessageServiceRequest.java new file mode 100644 index 0000000..95013b2 --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/request/UpdateFcmMessageServiceRequest.java @@ -0,0 +1,22 @@ +package earlybird.earlybird.scheduler.notification.service.update.request; + +import earlybird.earlybird.scheduler.notification.domain.NotificationUpdateType; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class UpdateFcmMessageServiceRequest { + + private Long appointmentId; + private String appointmentName; + private String clientId; + private String deviceToken; + private LocalDateTime preparationTime; + private LocalDateTime movingTime; + private LocalDateTime appointmentTime; + private NotificationUpdateType updateType; +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/update/request/UpdateNotificationAtArriveOnTimeServiceRequest.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/request/UpdateNotificationAtArriveOnTimeServiceRequest.java new file mode 100644 index 0000000..13e03fd --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/request/UpdateNotificationAtArriveOnTimeServiceRequest.java @@ -0,0 +1,17 @@ +package earlybird.earlybird.scheduler.notification.service.update.request; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class UpdateNotificationAtArriveOnTimeServiceRequest { + + private final Long appointmentId; + private final String clientId; + + @Builder + private UpdateNotificationAtArriveOnTimeServiceRequest(Long appointmentId, String clientId) { + this.appointmentId = appointmentId; + this.clientId = clientId; + } +} diff --git a/src/main/java/earlybird/earlybird/scheduler/notification/service/update/response/UpdateFcmMessageServiceResponse.java b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/response/UpdateFcmMessageServiceResponse.java new file mode 100644 index 0000000..7fc828d --- /dev/null +++ b/src/main/java/earlybird/earlybird/scheduler/notification/service/update/response/UpdateFcmMessageServiceResponse.java @@ -0,0 +1,32 @@ +package earlybird.earlybird.scheduler.notification.service.update.response; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.domain.NotificationStatus; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public class UpdateFcmMessageServiceResponse { + + private final Appointment appointment; + private final List notifications; + + public static UpdateFcmMessageServiceResponse of(Appointment appointment) { + return UpdateFcmMessageServiceResponse.builder() + .appointment(appointment) + .notifications( + appointment.getFcmNotifications().stream() + .filter( + notification -> + notification + .getStatus() + .equals(NotificationStatus.PENDING)) + .toList()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/security/authentication/jwt/JWTAuthenticationFilter.java b/src/main/java/earlybird/earlybird/security/authentication/jwt/JWTAuthenticationFilter.java index e8356dc..968ee42 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/jwt/JWTAuthenticationFilter.java +++ b/src/main/java/earlybird/earlybird/security/authentication/jwt/JWTAuthenticationFilter.java @@ -5,12 +5,16 @@ import earlybird.earlybird.user.dto.UserAccountInfoDTO; import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; + import io.jsonwebtoken.ExpiredJwtException; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import lombok.RequiredArgsConstructor; + import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -30,12 +34,11 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter { private final UserRepository userRepository; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - List passUriList = Arrays.asList( - "/api/v1/login", - "/api/v1/logout", - "/api/v1/reissue" - ); + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + List passUriList = + Arrays.asList("/api/v1/login", "/api/v1/logout", "/api/v1/reissue"); if (passUriList.contains(request.getRequestURI())) { filterChain.doFilter(request, response); @@ -86,14 +89,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse UserAccountInfoDTO userAccountInfoDTO = user.toUserAccountInfoDTO(); List authorities = List.of(new SimpleGrantedAuthority(user.getRole())); - OAuth2UserDetails oAuth2UserDetails = new OAuth2UserDetails(userAccountInfoDTO, authorities); + OAuth2UserDetails oAuth2UserDetails = + new OAuth2UserDetails(userAccountInfoDTO, authorities); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - oAuth2UserDetails, null, oAuth2UserDetails.getAuthorities()); + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + oAuth2UserDetails, null, oAuth2UserDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authToken); filterChain.doFilter(request, response); } } - diff --git a/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationFilter.java b/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationFilter.java index 978ec43..3a54c0a 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationFilter.java +++ b/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationFilter.java @@ -6,11 +6,13 @@ import earlybird.earlybird.security.token.jwt.refresh.JWTRefreshTokenToCookieService; import earlybird.earlybird.security.token.jwt.refresh.SaveJWTRefreshTokenService; import earlybird.earlybird.user.dto.UserAccountInfoDTO; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -22,7 +24,8 @@ import java.util.Optional; public class JWTReissueAuthenticationFilter extends AbstractAuthenticationProcessingFilter { - private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/v1/reissue", "POST"); + private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = + new AntPathRequestMatcher("/api/v1/reissue", "POST"); private final CreateJWTRefreshTokenService createJWTRefreshTokenService; private final CreateJWTAccessTokenService createJWTAccessTokenService; private final JWTRefreshTokenRepository JWTRefreshTokenRepository; @@ -34,8 +37,7 @@ public JWTReissueAuthenticationFilter( CreateJWTRefreshTokenService createJWTRefreshTokenService, JWTRefreshTokenRepository JWTRefreshTokenRepository, SaveJWTRefreshTokenService saveJWTRefreshTokenService, - JWTRefreshTokenToCookieService JWTRefreshTokenToCookieService - ) { + JWTRefreshTokenToCookieService JWTRefreshTokenToCookieService) { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); this.createJWTAccessTokenService = createJWTAccessTokenService; this.createJWTRefreshTokenService = createJWTRefreshTokenService; @@ -45,11 +47,14 @@ public JWTReissueAuthenticationFilter( } @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + public Authentication attemptAuthentication( + HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { - Optional optionalRefreshCookie = Arrays.stream(request.getCookies()) - .filter(cookie -> cookie.getName().equals("refresh")) - .findFirst(); + Optional optionalRefreshCookie = + Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("refresh")) + .findFirst(); if (optionalRefreshCookie.isEmpty()) { throw new AuthenticationServiceException("refresh 토큰이 존재하지 않습니다."); @@ -62,7 +67,12 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ } @Override - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + protected void successfulAuthentication( + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain, + Authentication authResult) + throws IOException, ServletException { final int accessTokenExpiredMs = 60 * 60 * 60; // 60 * 60 * 60 ms = 36분 final int refreshTokenExpiredMs = 86400000; // 86400000 ms = 24 h @@ -70,18 +80,25 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR String accountId = userInfo.getAccountId(); String role = userInfo.getRole(); - String newAccessToken = createJWTAccessTokenService.createAccessToken(userInfo, (long) accessTokenExpiredMs); - String newRefreshToken = createJWTRefreshTokenService.createRefreshToken(userInfo, (long) refreshTokenExpiredMs); + String newAccessToken = + createJWTAccessTokenService.createAccessToken( + userInfo, (long) accessTokenExpiredMs); + String newRefreshToken = + createJWTRefreshTokenService.createRefreshToken( + userInfo, (long) refreshTokenExpiredMs); - String oldRefreshToken = Arrays.stream(request.getCookies()) - .filter(cookie -> cookie.getName().equals("refresh")) - .findFirst() - .get() - .getValue(); + String oldRefreshToken = + Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("refresh")) + .findFirst() + .get() + .getValue(); JWTRefreshTokenRepository.deleteByRefreshToken(oldRefreshToken); response.setHeader("access", newAccessToken); - response.addCookie(JWTRefreshTokenToCookieService.createCookie(newRefreshToken, refreshTokenExpiredMs)); + response.addCookie( + JWTRefreshTokenToCookieService.createCookie( + newRefreshToken, refreshTokenExpiredMs)); } } diff --git a/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationProvider.java b/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationProvider.java index eb7c41f..a670ca6 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationProvider.java +++ b/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationProvider.java @@ -4,8 +4,11 @@ import earlybird.earlybird.user.dto.UserAccountInfoDTO; import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; + import io.jsonwebtoken.ExpiredJwtException; + import lombok.RequiredArgsConstructor; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; @@ -24,7 +27,8 @@ public class JWTReissueAuthenticationProvider implements AuthenticationProvider private final UserRepository userRepository; @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { String refreshToken = (String) authentication.getPrincipal(); if (refreshToken == null) { diff --git a/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationToken.java b/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationToken.java index 88c65ef..b3794d1 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationToken.java +++ b/src/main/java/earlybird/earlybird/security/authentication/jwt/reissue/JWTReissueAuthenticationToken.java @@ -6,12 +6,11 @@ import java.util.Collection; public class JWTReissueAuthenticationToken extends AbstractAuthenticationToken { - /** - * 인증 전: refresh 토큰 - * 인증 성공 후: UserAccountInfoDTO 객체 - */ + /** 인증 전: refresh 토큰 인증 성공 후: UserAccountInfoDTO 객체 */ private final Object principal; - public JWTReissueAuthenticationToken(Collection authorities, Object principal) { + + public JWTReissueAuthenticationToken( + Collection authorities, Object principal) { super(authorities); this.principal = principal; setAuthenticated(true); diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationFilter.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationFilter.java index 6d22535..06f822c 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationFilter.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationFilter.java @@ -9,10 +9,12 @@ import earlybird.earlybird.security.token.oauth2.service.CreateOAuth2TokenService; import earlybird.earlybird.security.token.oauth2.service.DeleteOAuth2TokenService; import earlybird.earlybird.user.dto.UserAccountInfoDTO; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -23,18 +25,20 @@ public class OAuth2AuthenticationFilter extends AbstractAuthenticationProcessingFilter { - private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/v1/login", "POST"); + private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = + new AntPathRequestMatcher("/api/v1/login", "POST"); private final CreateJWTAccessTokenService createJWTAccessTokenService; private final CreateJWTRefreshTokenService createJWTRefreshTokenService; private final JWTRefreshTokenToCookieService jwtRefreshTokenToCookieService; private final CreateOAuth2TokenService createOAuth2TokenService; private final DeleteOAuth2TokenService deleteOAuth2TokenService; - public OAuth2AuthenticationFilter(CreateJWTAccessTokenService createJWTAccessTokenService, - CreateJWTRefreshTokenService createJWTRefreshTokenService, - JWTRefreshTokenToCookieService jwtRefreshTokenToCookieService, - CreateOAuth2TokenService createOAuth2TokenService, - DeleteOAuth2TokenService deleteOAuth2TokenService) { + public OAuth2AuthenticationFilter( + CreateJWTAccessTokenService createJWTAccessTokenService, + CreateJWTRefreshTokenService createJWTRefreshTokenService, + JWTRefreshTokenToCookieService jwtRefreshTokenToCookieService, + CreateOAuth2TokenService createOAuth2TokenService, + DeleteOAuth2TokenService deleteOAuth2TokenService) { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); this.createJWTAccessTokenService = createJWTAccessTokenService; this.createJWTRefreshTokenService = createJWTRefreshTokenService; @@ -44,40 +48,57 @@ public OAuth2AuthenticationFilter(CreateJWTAccessTokenService createJWTAccessTok } @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + public Authentication attemptAuthentication( + HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { String oauth2ProviderName = request.getHeader("Provider-Name"); String oauth2AccessToken = request.getHeader("OAuth2-Access"); String oauth2RefreshToken = request.getHeader("OAuth2-Refresh"); if (oauth2ProviderName == null || oauth2AccessToken == null || oauth2RefreshToken == null) { - throw new AuthenticationServiceException("provider-name 또는 oauth2-access 값이 제공되지 않았습니다."); + throw new AuthenticationServiceException( + "provider-name 또는 oauth2-access 값이 제공되지 않았습니다."); } - OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(oauth2ProviderName, oauth2AccessToken); + OAuth2AuthenticationToken token = + new OAuth2AuthenticationToken(oauth2ProviderName, oauth2AccessToken); return this.getAuthenticationManager().authenticate(token); } @Override - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + protected void successfulAuthentication( + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain, + Authentication authResult) + throws IOException, ServletException { final int accessTokenExpiredMs = 60 * 60 * 60; // 60 * 60 * 60 ms = 36분 final int refreshTokenExpiredMs = 86400000; // 86400000 ms = 24 h - UserAccountInfoDTO userDTO = ((OAuth2UserDetails) authResult.getPrincipal()).getUserAccountInfoDTO(); - String access = createJWTAccessTokenService.createAccessToken(userDTO, (long) accessTokenExpiredMs); - String refresh = createJWTRefreshTokenService.createRefreshToken(userDTO, (long) refreshTokenExpiredMs); + UserAccountInfoDTO userDTO = + ((OAuth2UserDetails) authResult.getPrincipal()).getUserAccountInfoDTO(); + String access = + createJWTAccessTokenService.createAccessToken(userDTO, (long) accessTokenExpiredMs); + String refresh = + createJWTRefreshTokenService.createRefreshToken( + userDTO, (long) refreshTokenExpiredMs); response.setHeader("access", access); - response.addCookie(jwtRefreshTokenToCookieService.createCookie(refresh, refreshTokenExpiredMs)); + response.addCookie( + jwtRefreshTokenToCookieService.createCookie(refresh, refreshTokenExpiredMs)); deleteOAuth2TokenService.deleteByUserId(userDTO.getId()); - OAuth2TokenDTO oAuth2TokenDTO = OAuth2TokenDTO.builder() - .userDTO(userDTO) - .accessToken(request.getHeader("OAuth2-Access")) - .refreshToken(request.getHeader("OAuth2-Refresh")) - .oAuth2ProviderName(OAuth2ProviderName.valueOf(request.getHeader("Provider-Name").toUpperCase())) - .build(); + OAuth2TokenDTO oAuth2TokenDTO = + OAuth2TokenDTO.builder() + .userDTO(userDTO) + .accessToken(request.getHeader("OAuth2-Access")) + .refreshToken(request.getHeader("OAuth2-Refresh")) + .oAuth2ProviderName( + OAuth2ProviderName.valueOf( + request.getHeader("Provider-Name").toUpperCase())) + .build(); createOAuth2TokenService.create(oAuth2TokenDTO); } diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationProvider.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationProvider.java index 62756a1..6e48bf3 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationProvider.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationProvider.java @@ -5,7 +5,9 @@ import earlybird.earlybird.security.authentication.oauth2.proxy.OAuth2UserInfoProxy; import earlybird.earlybird.security.authentication.oauth2.user.OAuth2UserJoinService; import earlybird.earlybird.security.enums.OAuth2ProviderName; + import lombok.RequiredArgsConstructor; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; @@ -19,25 +21,28 @@ @RequiredArgsConstructor public class OAuth2AuthenticationProvider implements AuthenticationProvider { - private final static Map oauth2UserInfoProxyList - = Map.of(OAuth2ProviderName.GOOGLE, new GoogleOAuth2UserInfoProxy()); + private static final Map oauth2UserInfoProxyList = + Map.of(OAuth2ProviderName.GOOGLE, new GoogleOAuth2UserInfoProxy()); private final UserDetailsService userDetailsService; private final OAuth2UserJoinService oAuth2UserJoinService; @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { String oauth2ProviderName = ((String) authentication.getPrincipal()).toLowerCase(); String oauth2AccessToken = (String) authentication.getCredentials(); - for (OAuth2ProviderName oAuth2ProviderName : oauth2UserInfoProxyList.keySet()) { String proxyName = oAuth2ProviderName.name(); if (proxyName.equalsIgnoreCase(oauth2ProviderName)) { - OAuth2UserInfoProxy oAuth2UserInfoProxy = oauth2UserInfoProxyList.get(oAuth2ProviderName); - OAuth2ServerResponse oAuth2UserInfo = oAuth2UserInfoProxy.getOAuth2UserInfo(oauth2AccessToken); + OAuth2UserInfoProxy oAuth2UserInfoProxy = + oauth2UserInfoProxyList.get(oAuth2ProviderName); + OAuth2ServerResponse oAuth2UserInfo = + oAuth2UserInfoProxy.getOAuth2UserInfo(oauth2AccessToken); - String username = oAuth2UserInfo.getProviderName() + " " + oAuth2UserInfo.getProviderId(); + String username = + oAuth2UserInfo.getProviderName() + " " + oAuth2UserInfo.getProviderId(); UserDetails userDetails; try { @@ -47,7 +52,8 @@ public Authentication authenticate(Authentication authentication) throws Authent userDetails = userDetailsService.loadUserByUsername(username); } - return new OAuth2AuthenticationToken(userDetails.getAuthorities(), userDetails, null); + return new OAuth2AuthenticationToken( + userDetails.getAuthorities(), userDetails, null); } } diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationToken.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationToken.java index 633766b..2c2b3df 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationToken.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/OAuth2AuthenticationToken.java @@ -19,7 +19,10 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken { */ private final Object credentials; - public OAuth2AuthenticationToken(Collection authorities, Object principal, Object credentials) { + public OAuth2AuthenticationToken( + Collection authorities, + Object principal, + Object credentials) { super(authorities); this.principal = principal; this.credentials = credentials; diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/dto/OAuth2ServerResponse.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/dto/OAuth2ServerResponse.java index f2c2ae9..02f7073 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/dto/OAuth2ServerResponse.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/dto/OAuth2ServerResponse.java @@ -1,8 +1,6 @@ package earlybird.earlybird.security.authentication.oauth2.dto; -/** - * OAuth2 서버(ex. Google, Apple ...)의 응답을 담은 객체 - */ +/** OAuth2 서버(ex. Google, Apple ...)의 응답을 담은 객체 */ public interface OAuth2ServerResponse { // 제공자 (Ex. naver, google, ...) diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/GoogleOAuth2UserInfoProxy.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/GoogleOAuth2UserInfoProxy.java index e310c74..d7eaff7 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/GoogleOAuth2UserInfoProxy.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/GoogleOAuth2UserInfoProxy.java @@ -2,30 +2,52 @@ import earlybird.earlybird.security.authentication.oauth2.dto.GoogleServerResponse; import earlybird.earlybird.security.authentication.oauth2.dto.OAuth2ServerResponse; + import org.springframework.http.*; import org.springframework.web.reactive.function.client.WebClient; + import reactor.core.publisher.Mono; import javax.naming.AuthenticationException; - public class GoogleOAuth2UserInfoProxy implements OAuth2UserInfoProxy { @Override public OAuth2ServerResponse getOAuth2UserInfo(String accessToken) { String authorization = "Bearer " + accessToken; - Mono responseMono = WebClient.create("https://www.googleapis.com") - .get() - .uri(uriBuilder -> uriBuilder - .scheme("https") - .path("/oauth2/v2/userinfo") - .build(false)) - .header("Authorization", authorization) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new AuthenticationException(String.format("%s Error from google: %s", clientResponse.statusCode(), clientResponse.bodyToMono(String.class))))) - .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new AuthenticationException(String.format("%s Error from google: %s", clientResponse.statusCode(), clientResponse.bodyToMono(String.class))))) - .bodyToMono(GoogleServerResponse.class); + Mono responseMono = + WebClient.create("https://www.googleapis.com") + .get() + .uri( + uriBuilder -> + uriBuilder + .scheme("https") + .path("/oauth2/v2/userinfo") + .build(false)) + .header("Authorization", authorization) + .retrieve() + .onStatus( + HttpStatusCode::is4xxClientError, + clientResponse -> + Mono.error( + new AuthenticationException( + String.format( + "%s Error from google: %s", + clientResponse.statusCode(), + clientResponse.bodyToMono( + String.class))))) + .onStatus( + HttpStatusCode::is5xxServerError, + clientResponse -> + Mono.error( + new AuthenticationException( + String.format( + "%s Error from google: %s", + clientResponse.statusCode(), + clientResponse.bodyToMono( + String.class))))) + .bodyToMono(GoogleServerResponse.class); return responseMono.block(); } diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/OAuth2UserInfoProxy.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/OAuth2UserInfoProxy.java index 1347294..8a52f11 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/OAuth2UserInfoProxy.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/proxy/OAuth2UserInfoProxy.java @@ -6,6 +6,7 @@ public interface OAuth2UserInfoProxy { /** * OAuth2 인증 서버에 액세스 토큰으로 요청을 보내서 유저 정보 획득 & 반환 + * * @param accessToken OAuth2 액세스 토큰 * @return */ diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetails.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetails.java index e7b06df..07835cb 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetails.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetails.java @@ -1,7 +1,9 @@ package earlybird.earlybird.security.authentication.oauth2.user; import earlybird.earlybird.user.dto.UserAccountInfoDTO; + import lombok.RequiredArgsConstructor; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetailsService.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetailsService.java index 1b5bd79..790813d 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetailsService.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserDetailsService.java @@ -2,7 +2,9 @@ import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; + import lombok.RequiredArgsConstructor; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; diff --git a/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserJoinService.java b/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserJoinService.java index 7a55adc..288e910 100644 --- a/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserJoinService.java +++ b/src/main/java/earlybird/earlybird/security/authentication/oauth2/user/OAuth2UserJoinService.java @@ -3,7 +3,9 @@ import earlybird.earlybird.security.authentication.oauth2.dto.OAuth2ServerResponse; import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; @RequiredArgsConstructor diff --git a/src/main/java/earlybird/earlybird/security/config/SecurityConfig.java b/src/main/java/earlybird/earlybird/security/config/SecurityConfig.java index a9a1d8b..8acdff4 100644 --- a/src/main/java/earlybird/earlybird/security/config/SecurityConfig.java +++ b/src/main/java/earlybird/earlybird/security/config/SecurityConfig.java @@ -17,9 +17,12 @@ import earlybird.earlybird.security.token.oauth2.service.DeleteOAuth2TokenService; import earlybird.earlybird.user.dto.UserAccountInfoDTO; import earlybird.earlybird.user.repository.UserRepository; + import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; + import lombok.RequiredArgsConstructor; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -57,92 +60,120 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); - authenticationManagerBuilder.authenticationProvider(new OAuth2AuthenticationProvider(userDetailsService, oAuth2UserJoinService)); + AuthenticationManagerBuilder authenticationManagerBuilder = + http.getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder.authenticationProvider( + new OAuth2AuthenticationProvider(userDetailsService, oAuth2UserJoinService)); AuthenticationManager authenticationManager = authenticationManagerBuilder.build(); - http - .authenticationManager(authenticationManager); - + http.authenticationManager(authenticationManager); - http - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/feedbacks").permitAll() - .anyRequest().authenticated() + http.authorizeHttpRequests( + auth -> + auth + // .anyRequest().authenticated() + .anyRequest() + .permitAll() + // TODO : 베타 테스트 기간에만 permitAll -> 로그인 기능 추가되면 authenticated()로 변경 ); - OAuth2AuthenticationFilter oAuth2AuthenticationFilter - = new OAuth2AuthenticationFilter(createJWTAccessTokenService, createJWTRefreshTokenService, JWTRefreshTokenToCookieService, createOAuth2TokenService, deleteOAuth2TokenService); + OAuth2AuthenticationFilter oAuth2AuthenticationFilter = + new OAuth2AuthenticationFilter( + createJWTAccessTokenService, + createJWTRefreshTokenService, + JWTRefreshTokenToCookieService, + createOAuth2TokenService, + deleteOAuth2TokenService); oAuth2AuthenticationFilter.setAuthenticationManager(authenticationManager); - http - .addFilterAt(oAuth2AuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + http.addFilterAt(oAuth2AuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - JWTReissueAuthenticationFilter jwtReissueAuthenticationFilter - = new JWTReissueAuthenticationFilter(createJWTAccessTokenService, createJWTRefreshTokenService, JWTRefreshTokenRepository, saveJWTRefreshTokenService, JWTRefreshTokenToCookieService); - ProviderManager jwtReissueAuthFilterProviderManager = new ProviderManager(new JWTReissueAuthenticationProvider(jwtUtil, userRepository)); - jwtReissueAuthenticationFilter.setAuthenticationManager(jwtReissueAuthFilterProviderManager); + JWTReissueAuthenticationFilter jwtReissueAuthenticationFilter = + new JWTReissueAuthenticationFilter( + createJWTAccessTokenService, + createJWTRefreshTokenService, + JWTRefreshTokenRepository, + saveJWTRefreshTokenService, + JWTRefreshTokenToCookieService); + ProviderManager jwtReissueAuthFilterProviderManager = + new ProviderManager(new JWTReissueAuthenticationProvider(jwtUtil, userRepository)); + jwtReissueAuthenticationFilter.setAuthenticationManager( + jwtReissueAuthFilterProviderManager); http.addFilterBefore(jwtReissueAuthenticationFilter, OAuth2AuthenticationFilter.class); - JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(jwtUtil, userRepository); - - http - .addFilterAfter(jwtAuthenticationFilter, OAuth2AuthenticationFilter.class); - - http - .logout(logout -> logout - .logoutRequestMatcher(new AntPathRequestMatcher("/api/v1/logout", "POST")) - .logoutSuccessHandler(((request, response, authentication) -> { - UserAccountInfoDTO userInfo = ((OAuth2UserDetails) authentication.getPrincipal()).getUserAccountInfoDTO(); - deleteOAuth2TokenService.deleteByUserAccountInfoDTO(userInfo); - })) - .deleteCookies("JSESSIONID", "refresh") - .invalidateHttpSession(true) - .clearAuthentication(true) - .addLogoutHandler(((request, response, authentication) -> { - Cookie[] cookies = request.getCookies(); - - for (Cookie cookie : cookies) { - if (cookie.getName().equals("refresh")) { - String refresh = cookie.getValue(); - JWTRefreshTokenRepository.deleteByRefreshToken(refresh); - } - } - })) - ); - - http - .sessionManagement((session) -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - - http - .formLogin(AbstractHttpConfigurer::disable) + JWTAuthenticationFilter jwtAuthenticationFilter = + new JWTAuthenticationFilter(jwtUtil, userRepository); + + // TODO: 베타 테스트 이후 살려놓기 + // http + // .addFilterAfter(jwtAuthenticationFilter, + // OAuth2AuthenticationFilter.class); + + http.logout( + logout -> + logout.logoutRequestMatcher( + new AntPathRequestMatcher("/api/v1/logout", "POST")) + .logoutSuccessHandler( + ((request, response, authentication) -> { + UserAccountInfoDTO userInfo = + ((OAuth2UserDetails) + authentication.getPrincipal()) + .getUserAccountInfoDTO(); + deleteOAuth2TokenService.deleteByUserAccountInfoDTO( + userInfo); + })) + .deleteCookies("JSESSIONID", "refresh") + .invalidateHttpSession(true) + .clearAuthentication(true) + .addLogoutHandler( + ((request, response, authentication) -> { + Cookie[] cookies = request.getCookies(); + + for (Cookie cookie : cookies) { + if (cookie.getName().equals("refresh")) { + String refresh = cookie.getValue(); + JWTRefreshTokenRepository.deleteByRefreshToken( + refresh); + } + } + }))); + + http.sessionManagement( + (session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable); - http - .cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() { - - @Override - public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOriginPatterns(Collections.singletonList("*")); - configuration.setAllowedMethods(Collections.singletonList("*")); - configuration.setAllowCredentials(true); - configuration.setAllowedHeaders(Collections.singletonList("*")); - configuration.setMaxAge(3600L); - - configuration.setExposedHeaders(Collections.singletonList("Set-Cookie")); - configuration.setExposedHeaders(Collections.singletonList("Authorization")); - - return configuration; - } - })); - - + http.cors( + corsCustomizer -> + corsCustomizer.configurationSource( + new CorsConfigurationSource() { + + @Override + public CorsConfiguration getCorsConfiguration( + HttpServletRequest request) { + + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOriginPatterns( + Collections.singletonList("*")); + configuration.setAllowedMethods( + Collections.singletonList("*")); + configuration.setAllowCredentials(true); + configuration.setAllowedHeaders( + Collections.singletonList("*")); + configuration.setMaxAge(3600L); + + configuration.setExposedHeaders( + Collections.singletonList("Set-Cookie")); + configuration.setExposedHeaders( + Collections.singletonList("Authorization")); + + return configuration; + } + })); return http.build(); } diff --git a/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterController.java b/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterController.java index 464e23a..4748b85 100644 --- a/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterController.java +++ b/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterController.java @@ -1,13 +1,9 @@ package earlybird.earlybird.security.deregister.oauth2; import earlybird.earlybird.security.authentication.oauth2.user.OAuth2UserDetails; -import earlybird.earlybird.security.deregister.oauth2.revoke.RevokeOAuth2TokenService; -import earlybird.earlybird.security.token.oauth2.service.DeleteOAuth2TokenService; -import earlybird.earlybird.security.token.oauth2.service.FindOAuth2TokenService; -import earlybird.earlybird.security.token.oauth2.OAuth2TokenDTO; -import earlybird.earlybird.user.dto.UserAccountInfoDTO; -import earlybird.earlybird.user.service.DeleteUserService; + import lombok.RequiredArgsConstructor; + import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; diff --git a/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterService.java b/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterService.java index 721b4c6..634a73c 100644 --- a/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterService.java +++ b/src/main/java/earlybird/earlybird/security/deregister/oauth2/OAuth2DeregisterService.java @@ -7,7 +7,9 @@ import earlybird.earlybird.security.token.oauth2.service.FindOAuth2TokenService; import earlybird.earlybird.user.dto.UserAccountInfoDTO; import earlybird.earlybird.user.service.DeleteUserService; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; @RequiredArgsConstructor diff --git a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/RevokeOAuth2TokenService.java b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/RevokeOAuth2TokenService.java index 2b97bbf..bad0e8e 100644 --- a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/RevokeOAuth2TokenService.java +++ b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/RevokeOAuth2TokenService.java @@ -3,7 +3,9 @@ import earlybird.earlybird.security.deregister.oauth2.revoke.proxy.OAuth2TokenRevokeProxy; import earlybird.earlybird.security.enums.OAuth2ProviderName; import earlybird.earlybird.security.token.oauth2.OAuth2TokenDTO; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; import java.util.Map; @@ -16,7 +18,8 @@ public class RevokeOAuth2TokenService { public void revoke(OAuth2TokenDTO oAuth2TokenDTO) { OAuth2ProviderName oAuth2ProviderName = oAuth2TokenDTO.getOAuth2ProviderName(); - OAuth2TokenRevokeProxy oAuth2TokenRevokeProxy = oAuth2TokenRevokeProxyMap.get(oAuth2ProviderName); + OAuth2TokenRevokeProxy oAuth2TokenRevokeProxy = + oAuth2TokenRevokeProxyMap.get(oAuth2ProviderName); String accessToken = oAuth2TokenDTO.getAccessToken(); String refreshToken = oAuth2TokenDTO.getRefreshToken(); diff --git a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/GoogleOAuth2TokenRevokeProxy.java b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/GoogleOAuth2TokenRevokeProxy.java index 3cb2ef5..5caf8f1 100644 --- a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/GoogleOAuth2TokenRevokeProxy.java +++ b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/GoogleOAuth2TokenRevokeProxy.java @@ -1,14 +1,11 @@ package earlybird.earlybird.security.deregister.oauth2.revoke.proxy; -import earlybird.earlybird.security.authentication.oauth2.dto.GoogleServerResponse; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientResponseException; -import reactor.core.publisher.Mono; -import javax.naming.AuthenticationException; +import reactor.core.publisher.Mono; @Component public class GoogleOAuth2TokenRevokeProxy implements OAuth2TokenRevokeProxy { @@ -17,15 +14,21 @@ public void revoke(String token) { try { WebClient.create("https://oauth2.googleapis.com") .post() - .uri(uriBuilder -> uriBuilder - .scheme("https") - .path("/revoke") - .queryParam("token", token) - .build(false)) + .uri( + uriBuilder -> + uriBuilder + .scheme("https") + .path("/revoke") + .queryParam("token", token) + .build(false)) .header("Content-type", MediaType.APPLICATION_FORM_URLENCODED_VALUE) .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new RuntimeException())) - .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new RuntimeException())) + .onStatus( + HttpStatusCode::is4xxClientError, + clientResponse -> Mono.error(new RuntimeException())) + .onStatus( + HttpStatusCode::is5xxServerError, + clientResponse -> Mono.error(new RuntimeException())) .bodyToMono(String.class) .block(); } catch (RuntimeException e) { diff --git a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxy.java b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxy.java index 2015552..832a1ea 100644 --- a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxy.java +++ b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxy.java @@ -4,6 +4,7 @@ public interface OAuth2TokenRevokeProxy { /** * OAuth2 액세스 토큰 또는 리프레시 토큰을 만료시킴 + * * @param token OAuth2 액세스 토큰 또는 리프레시 토큰 */ public void revoke(String token); diff --git a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxyConfig.java b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxyConfig.java index c1c1ec2..e4e279d 100644 --- a/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxyConfig.java +++ b/src/main/java/earlybird/earlybird/security/deregister/oauth2/revoke/proxy/OAuth2TokenRevokeProxyConfig.java @@ -1,6 +1,7 @@ package earlybird.earlybird.security.deregister.oauth2.revoke.proxy; import earlybird.earlybird.security.enums.OAuth2ProviderName; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -11,11 +12,7 @@ public class OAuth2TokenRevokeProxyConfig { @Bean public Map providerNameAndProxyMap( - GoogleOAuth2TokenRevokeProxy googleOAuth2TokenRevokeProxy - ) { - return Map.of( - OAuth2ProviderName.GOOGLE, googleOAuth2TokenRevokeProxy - ); + GoogleOAuth2TokenRevokeProxy googleOAuth2TokenRevokeProxy) { + return Map.of(OAuth2ProviderName.GOOGLE, googleOAuth2TokenRevokeProxy); } - } diff --git a/src/main/java/earlybird/earlybird/security/enums/OAuth2ProviderName.java b/src/main/java/earlybird/earlybird/security/enums/OAuth2ProviderName.java index 978f3d7..fc93796 100644 --- a/src/main/java/earlybird/earlybird/security/enums/OAuth2ProviderName.java +++ b/src/main/java/earlybird/earlybird/security/enums/OAuth2ProviderName.java @@ -1,5 +1,6 @@ package earlybird.earlybird.security.enums; public enum OAuth2ProviderName { - GOOGLE, APPLE + GOOGLE, + APPLE } diff --git a/src/main/java/earlybird/earlybird/security/token/jwt/JWTUtil.java b/src/main/java/earlybird/earlybird/security/token/jwt/JWTUtil.java index 2757b78..b7af788 100644 --- a/src/main/java/earlybird/earlybird/security/token/jwt/JWTUtil.java +++ b/src/main/java/earlybird/earlybird/security/token/jwt/JWTUtil.java @@ -2,14 +2,16 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Date; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + @Component public class JWTUtil { @@ -17,27 +19,51 @@ public class JWTUtil { public JWTUtil(@Value("${spring.jwt.secret}") String secret) { - secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); + secretKey = + new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS256.key().build().getAlgorithm()); } public String getAccountId(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("accountId", String.class); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .get("accountId", String.class); } public String getRole(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .get("role", String.class); } public String getCategory(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("category", String.class); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .get("category", String.class); } public Boolean isExpired(String token) throws ExpiredJwtException { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date(System.currentTimeMillis())); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .getExpiration() + .before(new Date(System.currentTimeMillis())); } public String createJwt(String category, String accountId, String role, Long expiredMs) { diff --git a/src/main/java/earlybird/earlybird/security/token/jwt/access/CreateJWTAccessTokenService.java b/src/main/java/earlybird/earlybird/security/token/jwt/access/CreateJWTAccessTokenService.java index 7882c91..38b83fa 100644 --- a/src/main/java/earlybird/earlybird/security/token/jwt/access/CreateJWTAccessTokenService.java +++ b/src/main/java/earlybird/earlybird/security/token/jwt/access/CreateJWTAccessTokenService.java @@ -2,7 +2,9 @@ import earlybird.earlybird.security.token.jwt.JWTUtil; import earlybird.earlybird.user.dto.UserAccountInfoDTO; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; @RequiredArgsConstructor diff --git a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/CreateJWTRefreshTokenService.java b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/CreateJWTRefreshTokenService.java index 8e58ffa..520262d 100644 --- a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/CreateJWTRefreshTokenService.java +++ b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/CreateJWTRefreshTokenService.java @@ -2,7 +2,9 @@ import earlybird.earlybird.security.token.jwt.JWTUtil; import earlybird.earlybird.user.dto.UserAccountInfoDTO; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; @RequiredArgsConstructor diff --git a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshToken.java b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshToken.java index ec9431f..7768da9 100644 --- a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshToken.java +++ b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshToken.java @@ -16,9 +16,7 @@ public class JWTRefreshToken { private String refreshToken; private String expiration; - public JWTRefreshToken() { - - } + public JWTRefreshToken() {} public JWTRefreshToken(String accountId, String refreshToken, String expiration) { this.accountId = accountId; diff --git a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshTokenToCookieService.java b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshTokenToCookieService.java index d89ae53..a678703 100644 --- a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshTokenToCookieService.java +++ b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/JWTRefreshTokenToCookieService.java @@ -1,6 +1,7 @@ package earlybird.earlybird.security.token.jwt.refresh; import jakarta.servlet.http.Cookie; + import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/SaveJWTRefreshTokenService.java b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/SaveJWTRefreshTokenService.java index 82eeb42..9c8d168 100644 --- a/src/main/java/earlybird/earlybird/security/token/jwt/refresh/SaveJWTRefreshTokenService.java +++ b/src/main/java/earlybird/earlybird/security/token/jwt/refresh/SaveJWTRefreshTokenService.java @@ -1,6 +1,7 @@ package earlybird.earlybird.security.token.jwt.refresh; import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; import java.util.Date; diff --git a/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2Token.java b/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2Token.java index dd7bac6..8c5246c 100644 --- a/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2Token.java +++ b/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2Token.java @@ -1,13 +1,15 @@ package earlybird.earlybird.security.token.oauth2; +import static lombok.AccessLevel.PRIVATE; + import earlybird.earlybird.security.enums.OAuth2ProviderName; import earlybird.earlybird.user.entity.User; + import jakarta.persistence.*; + import lombok.AllArgsConstructor; import lombok.Builder; -import static lombok.AccessLevel.PRIVATE; - @Builder @AllArgsConstructor(access = PRIVATE) @Entity @@ -28,9 +30,7 @@ public class OAuth2Token { @JoinColumn(name = "user_id") private User user; - public OAuth2Token() { - - } + public OAuth2Token() {} public OAuth2TokenDTO toOAuth2TokenDTO() { diff --git a/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenDTO.java b/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenDTO.java index 9c1d3ae..92ff812 100644 --- a/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenDTO.java +++ b/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenDTO.java @@ -2,6 +2,7 @@ import earlybird.earlybird.security.enums.OAuth2ProviderName; import earlybird.earlybird.user.dto.UserAccountInfoDTO; + import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenRepository.java b/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenRepository.java index 04c31ae..75216eb 100644 --- a/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenRepository.java +++ b/src/main/java/earlybird/earlybird/security/token/oauth2/OAuth2TokenRepository.java @@ -1,6 +1,7 @@ package earlybird.earlybird.security.token.oauth2; import earlybird.earlybird.user.entity.User; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/earlybird/earlybird/security/token/oauth2/service/CreateOAuth2TokenService.java b/src/main/java/earlybird/earlybird/security/token/oauth2/service/CreateOAuth2TokenService.java index abd93e5..f07a44d 100644 --- a/src/main/java/earlybird/earlybird/security/token/oauth2/service/CreateOAuth2TokenService.java +++ b/src/main/java/earlybird/earlybird/security/token/oauth2/service/CreateOAuth2TokenService.java @@ -1,12 +1,14 @@ package earlybird.earlybird.security.token.oauth2.service; +import earlybird.earlybird.error.exception.UserNotFoundException; import earlybird.earlybird.security.token.oauth2.OAuth2Token; import earlybird.earlybird.security.token.oauth2.OAuth2TokenDTO; import earlybird.earlybird.security.token.oauth2.OAuth2TokenRepository; import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; -import earlybird.earlybird.error.exception.UserNotFoundException; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -17,14 +19,18 @@ public class CreateOAuth2TokenService { private final UserRepository userRepository; public void create(OAuth2TokenDTO oAuth2TokenDTO) { - User user = userRepository.findById(oAuth2TokenDTO.getUserDTO().getId()).orElseThrow(UserNotFoundException::new); - - OAuth2Token oAuth2Token = OAuth2Token.builder() - .accessToken(oAuth2TokenDTO.getAccessToken()) - .refreshToken(oAuth2TokenDTO.getRefreshToken()) - .oAuth2ProviderName(oAuth2TokenDTO.getOAuth2ProviderName()) - .user(user) - .build(); + User user = + userRepository + .findById(oAuth2TokenDTO.getUserDTO().getId()) + .orElseThrow(UserNotFoundException::new); + + OAuth2Token oAuth2Token = + OAuth2Token.builder() + .accessToken(oAuth2TokenDTO.getAccessToken()) + .refreshToken(oAuth2TokenDTO.getRefreshToken()) + .oAuth2ProviderName(oAuth2TokenDTO.getOAuth2ProviderName()) + .user(user) + .build(); oAuth2TokenRepository.save(oAuth2Token); } diff --git a/src/main/java/earlybird/earlybird/security/token/oauth2/service/DeleteOAuth2TokenService.java b/src/main/java/earlybird/earlybird/security/token/oauth2/service/DeleteOAuth2TokenService.java index 9526723..820b0ae 100644 --- a/src/main/java/earlybird/earlybird/security/token/oauth2/service/DeleteOAuth2TokenService.java +++ b/src/main/java/earlybird/earlybird/security/token/oauth2/service/DeleteOAuth2TokenService.java @@ -1,11 +1,13 @@ package earlybird.earlybird.security.token.oauth2.service; +import earlybird.earlybird.error.exception.UserNotFoundException; import earlybird.earlybird.security.token.oauth2.OAuth2TokenRepository; import earlybird.earlybird.user.dto.UserAccountInfoDTO; import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; -import earlybird.earlybird.error.exception.UserNotFoundException; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; @RequiredArgsConstructor @@ -25,7 +27,8 @@ public void deleteByUserId(Long userId) { } public void deleteByUserAccountInfoDTO(UserAccountInfoDTO userInfo) { - User user = userRepository.findById(userInfo.getId()).orElseThrow(UserNotFoundException::new); + User user = + userRepository.findById(userInfo.getId()).orElseThrow(UserNotFoundException::new); oAuth2TokenRepository.deleteByUser(user); } } diff --git a/src/main/java/earlybird/earlybird/security/token/oauth2/service/FindOAuth2TokenService.java b/src/main/java/earlybird/earlybird/security/token/oauth2/service/FindOAuth2TokenService.java index d8efee6..2ea311d 100644 --- a/src/main/java/earlybird/earlybird/security/token/oauth2/service/FindOAuth2TokenService.java +++ b/src/main/java/earlybird/earlybird/security/token/oauth2/service/FindOAuth2TokenService.java @@ -1,12 +1,14 @@ package earlybird.earlybird.security.token.oauth2.service; +import earlybird.earlybird.error.exception.UserNotFoundException; import earlybird.earlybird.security.token.oauth2.OAuth2Token; import earlybird.earlybird.security.token.oauth2.OAuth2TokenDTO; import earlybird.earlybird.security.token.oauth2.OAuth2TokenRepository; import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; -import earlybird.earlybird.error.exception.UserNotFoundException; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; import java.util.Optional; diff --git a/src/main/java/earlybird/earlybird/user/dto/UserAccountInfoDTO.java b/src/main/java/earlybird/earlybird/user/dto/UserAccountInfoDTO.java index 7f559b7..8600e2a 100644 --- a/src/main/java/earlybird/earlybird/user/dto/UserAccountInfoDTO.java +++ b/src/main/java/earlybird/earlybird/user/dto/UserAccountInfoDTO.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import lombok.Setter; @Getter @Builder @@ -15,8 +14,7 @@ public class UserAccountInfoDTO { @Override public boolean equals(Object obj) { - if (!(obj instanceof UserAccountInfoDTO)) - return false; + if (!(obj instanceof UserAccountInfoDTO)) return false; UserAccountInfoDTO other = (UserAccountInfoDTO) obj; return this.id == other.getId() diff --git a/src/main/java/earlybird/earlybird/user/entity/User.java b/src/main/java/earlybird/earlybird/user/entity/User.java index ea5885a..6d7ce28 100644 --- a/src/main/java/earlybird/earlybird/user/entity/User.java +++ b/src/main/java/earlybird/earlybird/user/entity/User.java @@ -1,8 +1,11 @@ package earlybird.earlybird.user.entity; +import earlybird.earlybird.common.LocalDateTimeUtil; import earlybird.earlybird.security.authentication.oauth2.dto.OAuth2ServerResponse; import earlybird.earlybird.user.dto.UserAccountInfoDTO; + import jakarta.persistence.*; + import lombok.Builder; import lombok.Getter; @@ -18,9 +21,7 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - /** - * 스프링 시큐리티에서 사용하는 값 - */ + /** 스프링 시큐리티에서 사용하는 값 */ @Column(name = "user_account_id", nullable = false, unique = true) private String accountId; @@ -37,7 +38,13 @@ public class User { private LocalDateTime createdAt; @Builder - private User(Long id, String accountId, String name, String email, String role, LocalDateTime createdAt) { + private User( + Long id, + String accountId, + String name, + String email, + String role, + LocalDateTime createdAt) { this.id = id; this.accountId = accountId; this.name = name; @@ -51,12 +58,10 @@ public User(OAuth2ServerResponse userInfo) { this.name = userInfo.getName(); this.email = userInfo.getEmail(); this.role = "USER"; - this.createdAt = LocalDateTime.now(); + this.createdAt = LocalDateTimeUtil.getLocalDateTimeNow(); } - public User() { - - } + public User() {} public UserAccountInfoDTO toUserAccountInfoDTO() { return UserAccountInfoDTO.builder() diff --git a/src/main/java/earlybird/earlybird/user/repository/UserRepository.java b/src/main/java/earlybird/earlybird/user/repository/UserRepository.java index e52bfb6..51bc533 100644 --- a/src/main/java/earlybird/earlybird/user/repository/UserRepository.java +++ b/src/main/java/earlybird/earlybird/user/repository/UserRepository.java @@ -1,6 +1,7 @@ package earlybird.earlybird.user.repository; import earlybird.earlybird.user.entity.User; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -10,7 +11,6 @@ * findByAccountId와 findByUsername 함수는 서로 동일하지만 이름만 다른 함수
* 스프링 시큐리티에서는 accountId 대신 username 이라는 이름을 사용하기 때문에 가독성을 위해 이름이 다른 두 함수를 만들었음 */ - public interface UserRepository extends JpaRepository { Optional findByAccountId(String accountId); diff --git a/src/main/java/earlybird/earlybird/user/service/DeleteUserService.java b/src/main/java/earlybird/earlybird/user/service/DeleteUserService.java index e643313..57f2202 100644 --- a/src/main/java/earlybird/earlybird/user/service/DeleteUserService.java +++ b/src/main/java/earlybird/earlybird/user/service/DeleteUserService.java @@ -1,10 +1,12 @@ package earlybird.earlybird.user.service; +import earlybird.earlybird.error.exception.UserNotFoundException; import earlybird.earlybird.user.dto.UserAccountInfoDTO; import earlybird.earlybird.user.entity.User; import earlybird.earlybird.user.repository.UserRepository; -import earlybird.earlybird.error.exception.UserNotFoundException; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Service; import java.util.Optional; diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..be0da3c --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,13 @@ +spring: + datasource: + url: jdbc:mysql://localhost/earlybird + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + show_sql: true + format_sql: true \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..7047faf --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + max-lifetime: 177000 + jpa: + hibernate: + ddl-auto: validate +server: + tomcat: + basedir: . + accesslog: + enabled: true + pattern: "%{yyyy-MM-dd HH:mm:ss}t %s %r %{User-Agent}i %{Referer}i %a %b %D" diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000..27755cf --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,19 @@ +spring: + h2: + console: + enabled: true + path: /h2-console + datasource: + url: jdbc:mysql://localhost/earlybird_test + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + show_sql: true + use_sql_comments: true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d214789 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,15 @@ +spring: + application: + name: earlybird + profiles: + active: ${SPRING_ACTIVE_PROFILE} + jwt: + secret: ${JWT_SECRET} + +fcm: + service-account-file: key/firebase_admin_sdk_private_key.json + project-id: ${FCM_PROJECT_ID} + +aws: + access-key: ${AWS_ACCESS_KEY} + secret-access-key: ${AWS_SECRET_ACCESS_KEY} \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..ef919d4 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,11 @@ + + + + + EarlyBird-API + + + + EarlyBird API Server + + diff --git a/src/main/resources/templates/showServerIp.html b/src/main/resources/templates/showServerIp.html new file mode 100644 index 0000000..5ff1522 --- /dev/null +++ b/src/main/resources/templates/showServerIp.html @@ -0,0 +1,11 @@ + + + + + EarlyBird-API + + + + server ip + + diff --git a/src/test/java/earlybird/earlybird/EarlybirdApplicationTests.java b/src/test/java/earlybird/earlybird/EarlybirdApplicationTests.java index b427454..8d7f610 100644 --- a/src/test/java/earlybird/earlybird/EarlybirdApplicationTests.java +++ b/src/test/java/earlybird/earlybird/EarlybirdApplicationTests.java @@ -6,8 +6,6 @@ @SpringBootTest class EarlybirdApplicationTests { - @Test - void contextLoads() { - } - + @Test + void contextLoads() {} } diff --git a/src/test/java/earlybird/earlybird/appointment/domain/AppointmentTest.java b/src/test/java/earlybird/earlybird/appointment/domain/AppointmentTest.java new file mode 100644 index 0000000..7cffb28 --- /dev/null +++ b/src/test/java/earlybird/earlybird/appointment/domain/AppointmentTest.java @@ -0,0 +1,79 @@ +// package earlybird.earlybird.appointment.domain; +// +// import org.assertj.core.api.Assertions; +// import org.junit.jupiter.api.BeforeEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.context.annotation.Profile; +// import org.springframework.test.context.ActiveProfiles; +// +// import javax.sql.DataSource; +// import java.sql.SQLException; +// import java.time.DayOfWeek; +// import java.time.Duration; +// import java.time.LocalDateTime; +// import java.time.LocalTime; +// import java.util.ArrayList; +// import java.util.List; +// import java.util.Optional; +// +// import static org.junit.jupiter.api.Assertions.*; +// +// @Profile({"local"}) +// @SpringBootTest +// class AppointmentTest { +// +// @Autowired +// private AppointmentRepository appointmentRepository; +// +// @Autowired +// private RepeatingDayRepository repeatingDayRepository; +// +// @Autowired +// private DataSource dataSource; +// +// @BeforeEach +// void setUp() { +// Appointment appointment = Appointment.builder() +// .appointmentTime(LocalTime.now()) +// .appointmentName("name") +// .deviceToken("token") +// .repeatingDayOfWeeks(List.of(DayOfWeek.MONDAY)) +// .preparationDuration(Duration.ZERO) +// .movingDuration(Duration.ZERO) +// .clientId("clientId") +// .build(); +// Appointment savedAppointment = appointmentRepository.save(appointment); +// appointment.getRepeatingDays().get(0).setDeleted(); +// } +// +// @DisplayName("") +// @Test +// void test() throws SQLException { +// // given +// System.out.println(dataSource.getConnection().getMetaData().getURL()); +// +//// RepeatingDay repeatingDay = savedAppointment.getRepeatingDays().get(0); +//// repeatingDay.setDeleted(); +//// appointmentRepository.save(savedAppointment); +// // when +// +// List all = appointmentRepository.findAll(); +//// List allByDayOfWeek = +// repeatingDayRepository.findAllByDayOfWeek(DayOfWeek.MONDAY); +// List repeatingDays = all.get(0).getRepeatingDays(); +// +// List all2 = repeatingDayRepository.findAll(); +//// savedAppointment.setDeleted(); +// List all1 = appointmentRepository.findAll(); +// Assertions.assertThat("ddd").isEqualTo("d"); +// +// // then +// +// +// } +// +// } diff --git a/src/test/java/earlybird/earlybird/appointment/domain/CreateTestAppointment.java b/src/test/java/earlybird/earlybird/appointment/domain/CreateTestAppointment.java new file mode 100644 index 0000000..5000ef4 --- /dev/null +++ b/src/test/java/earlybird/earlybird/appointment/domain/CreateTestAppointment.java @@ -0,0 +1,20 @@ +package earlybird.earlybird.appointment.domain; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; + +public class CreateTestAppointment { + + public static Appointment create() { + return Appointment.builder() + .appointmentTime(LocalTime.now()) + .repeatingDayOfWeeks(new ArrayList<>()) + .movingDuration(Duration.ofMinutes(10)) + .preparationDuration(Duration.ofMinutes(10)) + .deviceToken("deviceToken") + .clientId("clientId") + .appointmentName("appointmentName") + .build(); + } +} diff --git a/src/test/java/earlybird/earlybird/appointment/domain/RepeatingDayRepositoryTest.java b/src/test/java/earlybird/earlybird/appointment/domain/RepeatingDayRepositoryTest.java new file mode 100644 index 0000000..3a49bbc --- /dev/null +++ b/src/test/java/earlybird/earlybird/appointment/domain/RepeatingDayRepositoryTest.java @@ -0,0 +1,45 @@ +package earlybird.earlybird.appointment.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalTime; +import java.util.List; + +@SpringBootTest +class RepeatingDayRepositoryTest { + + @Autowired private AppointmentRepository appointmentRepository; + + @Autowired private RepeatingDayRepository repeatingDayRepository; + + @DisplayName("") + @Test + void test() { + // given + + Appointment appointment = + Appointment.builder() + .appointmentTime(LocalTime.now()) + .appointmentName("name") + .deviceToken("token") + .repeatingDayOfWeeks(List.of(DayOfWeek.MONDAY)) + .preparationDuration(Duration.ZERO) + .movingDuration(Duration.ZERO) + .clientId("clientId") + .build(); + appointmentRepository.save(appointment); + repeatingDayRepository.findAllByDayOfWeek(DayOfWeek.MONDAY); + + // when + repeatingDayRepository.findAll(); + System.out.println("dd"); + + // then + + } +} diff --git a/src/test/java/earlybird/earlybird/feedback/service/CreateAuthFeedbackCommentServiceTest.java b/src/test/java/earlybird/earlybird/feedback/service/CreateAuthFeedbackCommentServiceTest.java new file mode 100644 index 0000000..a7d8008 --- /dev/null +++ b/src/test/java/earlybird/earlybird/feedback/service/CreateAuthFeedbackCommentServiceTest.java @@ -0,0 +1,101 @@ +// package earlybird.earlybird.feedback.service; +// +// import earlybird.earlybird.error.exception.UserNotFoundException; +// import earlybird.earlybird.feedback.domain.comment.FeedbackCommentRepository; +// import earlybird.earlybird.feedback.service.auth.CreateAuthFeedbackCommentService; +// import earlybird.earlybird.user.dto.UserAccountInfoDTO; +// import earlybird.earlybird.user.entity.User; +// import earlybird.earlybird.user.repository.UserRepository; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.context.SpringBootTest; +// +// import java.time.LocalDateTime; +// +// import static org.assertj.core.api.Assertions.*; +// +// @SpringBootTest +// class CreateAuthFeedbackCommentServiceTest { +// +// @Autowired +// private FeedbackCommentRepository feedbackCommentRepository; +// +// @Autowired +// private UserRepository userRepository; +// +// @Autowired +// private CreateAuthFeedbackCommentService createAuthFeedbackCommentService; +// +// +// @AfterEach +// void tearDown() { +// feedbackCommentRepository.deleteAllInBatch(); +// userRepository.deleteAllInBatch(); +// } +// +// @DisplayName("로그인한 유저의 피드백을 생성한다.") +// @Test +// void create() { +// // given +// LocalDateTime createdAt = LocalDateTime.of(2024, 10, 7, 9, 0); +// Long userId = 1L; +// User user = User.builder() +// .id(userId) +// .accountId("id") +// .email("email@email.com") +// .name("name") +// .role("USER") +// .createdAt(createdAt) +// .build(); +// +// User savedUser = userRepository.save(user); +// +// UserAccountInfoDTO userAccountInfoDTO = savedUser.toUserAccountInfoDTO(); +// +// Long feedbackId = 1L; +// String feedbackContent = "feedback content"; +// FeedbackDTO feedbackDTO = FeedbackDTO.builder() +// .id(feedbackId) +// .content(feedbackContent) +// .userAccountInfoDTO(userAccountInfoDTO) +// .createdAt(createdAt) +// .build(); +// +// // when +// FeedbackDTO createdFeedbackDTO = createAuthFeedbackCommentService.create(feedbackDTO); +// +// // then +// assertThat(feedbackCommentRepository.findAll()).hasSize(1); +// assertThat(createdFeedbackDTO) +// .extracting("id", "content", "createdAt") +// .contains( +// feedbackId, feedbackContent, createdAt +// ); +// assertThat(createdFeedbackDTO.getUserAccountInfoDTO()).isEqualTo(userAccountInfoDTO); +// } +// +// +// @DisplayName("찾을 수 없는 유저가 피드백 생성을 요청하면 예외가 발생한다.") +// @Test +// void createWithNotFoundUser() { +// // given +// UserAccountInfoDTO userAccountInfoDTO = UserAccountInfoDTO.builder() +// .accountId("id") +// .id(1L) +// .email("email@email.com") +// .name("name") +// .role("USER") +// .build(); +// +// FeedbackDTO feedbackDTO = FeedbackDTO.builder() +// .userAccountInfoDTO(userAccountInfoDTO) +// .build(); +// +// // when // then +// assertThatThrownBy(() -> createAuthFeedbackCommentService.create(feedbackDTO)) +// .isInstanceOf(UserNotFoundException.class); +// } +// +// } diff --git a/src/test/java/earlybird/earlybird/feedback/service/CreateAuthUserFeedbackServiceTest.java b/src/test/java/earlybird/earlybird/feedback/service/CreateAuthUserFeedbackServiceTest.java index d01fcab..d4e6254 100644 --- a/src/test/java/earlybird/earlybird/feedback/service/CreateAuthUserFeedbackServiceTest.java +++ b/src/test/java/earlybird/earlybird/feedback/service/CreateAuthUserFeedbackServiceTest.java @@ -1,101 +1,101 @@ -package earlybird.earlybird.feedback.service; - -import earlybird.earlybird.error.exception.UserNotFoundException; -import earlybird.earlybird.feedback.dto.FeedbackDTO; -import earlybird.earlybird.feedback.repository.FeedbackRepository; -import earlybird.earlybird.user.dto.UserAccountInfoDTO; -import earlybird.earlybird.user.entity.User; -import earlybird.earlybird.user.repository.UserRepository; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.LocalDateTime; - -import static org.assertj.core.api.Assertions.*; - -@SpringBootTest -class CreateAuthUserFeedbackServiceTest { - - @Autowired - private FeedbackRepository feedbackRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private CreateAuthUserFeedbackService createAuthUserFeedbackService; - - - @AfterEach - void tearDown() { - feedbackRepository.deleteAllInBatch(); - userRepository.deleteAllInBatch(); - } - - @DisplayName("로그인한 유저의 피드백을 생성한다.") - @Test - void create() { - // given - LocalDateTime createdAt = LocalDateTime.of(2024, 10, 7, 9, 0); - Long userId = 1L; - User user = User.builder() - .id(userId) - .accountId("id") - .email("email@email.com") - .name("name") - .role("USER") - .createdAt(createdAt) - .build(); - - User savedUser = userRepository.save(user); - - UserAccountInfoDTO userAccountInfoDTO = savedUser.toUserAccountInfoDTO(); - - Long feedbackId = 1L; - String feedbackContent = "feedback content"; - FeedbackDTO feedbackDTO = FeedbackDTO.builder() - .id(feedbackId) - .content(feedbackContent) - .userAccountInfoDTO(userAccountInfoDTO) - .createdAt(createdAt) - .build(); - - // when - FeedbackDTO createdFeedbackDTO = createAuthUserFeedbackService.create(feedbackDTO); - - // then - assertThat(feedbackRepository.findAll()).hasSize(1); - assertThat(createdFeedbackDTO) - .extracting("id", "content", "createdAt") - .contains( - feedbackId, feedbackContent, createdAt - ); - assertThat(createdFeedbackDTO.getUserAccountInfoDTO()).isEqualTo(userAccountInfoDTO); - } - - - @DisplayName("찾을 수 없는 유저가 피드백 생성을 요청하면 예외가 발생한다.") - @Test - void createWithNotFoundUser() { - // given - UserAccountInfoDTO userAccountInfoDTO = UserAccountInfoDTO.builder() - .accountId("id") - .id(1L) - .email("email@email.com") - .name("name") - .role("USER") - .build(); - - FeedbackDTO feedbackDTO = FeedbackDTO.builder() - .userAccountInfoDTO(userAccountInfoDTO) - .build(); - - // when // then - assertThatThrownBy(() -> createAuthUserFeedbackService.create(feedbackDTO)) - .isInstanceOf(UserNotFoundException.class); - } - -} \ No newline at end of file +// package earlybird.earlybird.feedback.service; +// +// import earlybird.earlybird.error.exception.UserNotFoundException; +// import earlybird.earlybird.feedback.dto.FeedbackDTO; +// import earlybird.earlybird.feedback.repository.FeedbackRepository; +// import earlybird.earlybird.user.dto.UserAccountInfoDTO; +// import earlybird.earlybird.user.entity.User; +// import earlybird.earlybird.user.repository.UserRepository; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.context.SpringBootTest; +// +// import java.time.LocalDateTime; +// +// import static org.assertj.core.api.Assertions.*; +// +// @SpringBootTest +// class CreateAuthUserFeedbackServiceTest { +// +// @Autowired +// private FeedbackRepository feedbackRepository; +// +// @Autowired +// private UserRepository userRepository; +// +// @Autowired +// private CreateAuthUserFeedbackService createAuthUserFeedbackService; +// +// +// @AfterEach +// void tearDown() { +// feedbackRepository.deleteAllInBatch(); +// userRepository.deleteAllInBatch(); +// } +// +// @DisplayName("로그인한 유저의 피드백을 생성한다.") +// @Test +// void create() { +// // given +// LocalDateTime createdAt = LocalDateTime.of(2024, 10, 7, 9, 0); +// Long userId = 1L; +// User user = User.builder() +// .id(userId) +// .accountId("id") +// .email("email@email.com") +// .name("name") +// .role("USER") +// .createdAt(createdAt) +// .build(); +// +// User savedUser = userRepository.save(user); +// +// UserAccountInfoDTO userAccountInfoDTO = savedUser.toUserAccountInfoDTO(); +// +// Long feedbackId = 1L; +// String feedbackContent = "feedback content"; +// FeedbackDTO feedbackDTO = FeedbackDTO.builder() +// .id(feedbackId) +// .content(feedbackContent) +// .userAccountInfoDTO(userAccountInfoDTO) +// .createdAt(createdAt) +// .build(); +// +// // when +// FeedbackDTO createdFeedbackDTO = createAuthUserFeedbackService.create(feedbackDTO); +// +// // then +// assertThat(feedbackRepository.findAll()).hasSize(1); +// assertThat(createdFeedbackDTO) +// .extracting("id", "content", "createdAt") +// .contains( +// feedbackId, feedbackContent, createdAt +// ); +// assertThat(createdFeedbackDTO.getUserAccountInfoDTO()).isEqualTo(userAccountInfoDTO); +// } +// +// +// @DisplayName("찾을 수 없는 유저가 피드백 생성을 요청하면 예외가 발생한다.") +// @Test +// void createWithNotFoundUser() { +// // given +// UserAccountInfoDTO userAccountInfoDTO = UserAccountInfoDTO.builder() +// .accountId("id") +// .id(1L) +// .email("email@email.com") +// .name("name") +// .role("USER") +// .build(); +// +// FeedbackDTO feedbackDTO = FeedbackDTO.builder() +// .userAccountInfoDTO(userAccountInfoDTO) +// .build(); +// +// // when // then +// assertThatThrownBy(() -> createAuthUserFeedbackService.create(feedbackDTO)) +// .isInstanceOf(UserNotFoundException.class); +// } +// +// } diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/domain/CreateTestFcmNotification.java b/src/test/java/earlybird/earlybird/scheduler/notification/domain/CreateTestFcmNotification.java new file mode 100644 index 0000000..5a6b9a8 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/domain/CreateTestFcmNotification.java @@ -0,0 +1,16 @@ +package earlybird.earlybird.scheduler.notification.domain; + +import earlybird.earlybird.appointment.domain.CreateTestAppointment; + +import java.time.LocalDateTime; + +public class CreateTestFcmNotification { + + public static FcmNotification create() { + return FcmNotification.builder() + .appointment(CreateTestAppointment.create()) + .notificationStep(NotificationStep.APPOINTMENT_TIME) + .targetTime(LocalDateTime.now()) + .build(); + } +} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepositoryStub.java b/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepositoryStub.java new file mode 100644 index 0000000..9a6b3fd --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepositoryStub.java @@ -0,0 +1,164 @@ +package earlybird.earlybird.scheduler.notification.domain; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.FluentQuery; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public class FcmNotificationRepositoryStub implements FcmNotificationRepository { + + private final List fcmNotifications = new ArrayList<>(); + + @Override + public Optional findByIdAndStatusForUpdate( + Long id, NotificationStatus status) { + return Optional.empty(); + } + + @Override + public List findAllByStatusIs(NotificationStatus status) { + return List.of(); + } + + @Override + public void flush() {} + + @Override + public S saveAndFlush(S entity) { + return null; + } + + @Override + public List saveAllAndFlush(Iterable entities) { + return List.of(); + } + + @Override + public void deleteAllInBatch(Iterable entities) {} + + @Override + public void deleteAllByIdInBatch(Iterable longs) {} + + @Override + public void deleteAllInBatch() {} + + @Override + public FcmNotification getOne(Long aLong) { + return null; + } + + @Override + public FcmNotification getById(Long aLong) { + return null; + } + + @Override + public FcmNotification getReferenceById(Long aLong) { + return null; + } + + @Override + public Optional findOne(Example example) { + return Optional.empty(); + } + + @Override + public List findAll(Example example) { + return List.of(); + } + + @Override + public List findAll(Example example, Sort sort) { + return List.of(); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + return null; + } + + @Override + public long count(Example example) { + return 0; + } + + @Override + public boolean exists(Example example) { + return false; + } + + @Override + public R findBy( + Example example, Function, R> queryFunction) { + return null; + } + + @Override + public S save(S entity) { + fcmNotifications.add(entity); + return entity; + } + + @Override + public List saveAll(Iterable entities) { + return List.of(); + } + + @Override + public Optional findById(Long aLong) { + return fcmNotifications.stream() + .filter(fcmNotification -> fcmNotification.getId().equals(aLong)) + .findFirst(); + } + + @Override + public boolean existsById(Long aLong) { + return false; + } + + @Override + public List findAll() { + return fcmNotifications; + } + + @Override + public List findAllById(Iterable longs) { + return List.of(); + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(Long aLong) {} + + @Override + public void delete(FcmNotification entity) {} + + @Override + public void deleteAllById(Iterable longs) {} + + @Override + public void deleteAll(Iterable entities) {} + + @Override + public void deleteAll() {} + + @Override + public List findAll(Sort sort) { + return List.of(); + } + + @Override + public Page findAll(Pageable pageable) { + return null; + } +} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepositoryTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepositoryTest.java new file mode 100644 index 0000000..40d8389 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationRepositoryTest.java @@ -0,0 +1,157 @@ +// package earlybird.earlybird.scheduler.notification.fcm.domain; +// +// import earlybird.earlybird.appointment.domain.Appointment; +// import earlybird.earlybird.appointment.domain.AppointmentRepository; +// import jakarta.persistence.EntityManager; +// import jakarta.persistence.EntityManagerFactory; +// import jakarta.persistence.PersistenceUnit; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.beans.factory.annotation.Qualifier; +// import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +// import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.context.annotation.Import; +// import org.springframework.core.task.TaskExecutor; +// import org.springframework.dao.PessimisticLockingFailureException; +// import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +// import org.springframework.security.core.parameters.P; +// import org.springframework.stereotype.Component; +// import org.springframework.test.context.ActiveProfiles; +// import org.springframework.test.context.web.WebAppConfiguration; +// import org.springframework.transaction.annotation.Transactional; +// +// import java.time.LocalDateTime; +// import java.util.Optional; +// import java.util.concurrent.CompletableFuture; +// +// import static org.assertj.core.api.Assertions.assertThat; +// import static org.assertj.core.api.Assertions.assertThatThrownBy; +// import static org.junit.jupiter.api.Assertions.*; +// +// @Component +// class Transaction { +// +// @Transactional +// public void run(Runnable runnable) { +// try { +// runnable.run(); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// } +// } +// +// @Import(Transaction.class) +// @ActiveProfiles("test") +// @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +// @SpringBootTest +// class FcmNotificationRepositoryTest { +// +// @Autowired +// private FcmNotificationRepository fcmNotificationRepository; +// +// @Autowired +// private AppointmentRepository appointmentRepository; +// +// @Autowired +// private Transaction transaction; +// +// @AfterEach +// void tearDown() { +// fcmNotificationRepository.deleteAllInBatch(); +// appointmentRepository.deleteAllInBatch(); +// } +// +// @DisplayName("ID와 전송 상태로 FCM 메시지를 조회한다.") +// @Test +// void findByIdAndStatus() { +// // given +// Appointment appointment = createAppointment(); +// FcmNotification notification = createNotification(appointment); +// appointment.addFcmNotification(notification); +// appointmentRepository.save(appointment); +// FcmNotification savedNotification = fcmNotificationRepository.save(notification); +// +// +// transaction.run(() -> { +// // when +// FcmNotification result = +// fcmNotificationRepository.findByIdAndStatusForUpdate(savedNotification.getId(), +// savedNotification.getStatus()).get(); +// +// // then +// assertThat(result.getId()).isEqualTo(savedNotification.getId()); +// assertThat(result.getStatus()).isEqualTo(savedNotification.getStatus()); +// }); +// } +// +// @DisplayName("findByIdAndStatusForUpdate 메서드를 실행하면 X-lock 락이 동작한다.") +// @Test +// void pessimisticWriteLock() { +// // given +// Appointment appointment = createAppointment(); +// FcmNotification notification = createNotification(appointment); +// appointment.addFcmNotification(notification); +// appointmentRepository.save(appointment); +// FcmNotification savedNotification = fcmNotificationRepository.save(notification); +// +// // when +// CompletableFuture transaction1 = CompletableFuture.runAsync(() -> transaction.run(() +// -> { +// FcmNotification fcmNotification = +// fcmNotificationRepository.findByIdAndStatusForUpdate(savedNotification.getId(), +// savedNotification.getStatus()).get(); +// threadSleep(5000); +// fcmNotification.updateStatusTo(NotificationStatus.CANCELLED); +// })); +// +// threadSleep(500); +// +// CompletableFuture transaction2 = CompletableFuture.runAsync(() -> { +// transaction.run(() -> { +// FcmNotification fcmNotification = +// fcmNotificationRepository.findById(savedNotification.getId()).get(); +// fcmNotification.updateStatusTo(NotificationStatus.COMPLETED); +// }); +// }); +// +// transaction1.join(); +// transaction2.join(); +// +// // then +// FcmNotification fcmNotification = +// fcmNotificationRepository.findById(savedNotification.getId()).get(); +// assertThat(fcmNotification.getStatus()).isEqualTo(NotificationStatus.COMPLETED); +// } +// +// +// private void threadSleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +// +// private Appointment createAppointment() { +// return appointmentRepository.save(Appointment.builder() +// .appointmentName("appointmentName") +// .deviceToken("deviceToken") +// .clientId("clientId") +// .build()); +// } +// +// private FcmNotification createNotification(Appointment appointment) { +// return FcmNotification.builder() +// .appointment(appointment) +// .targetTime(LocalDateTime.of(2024, 10, 11, 0, 0)) +// .notificationStep(NotificationStep.APPOINTMENT_TIME) +// .build(); +// } +// } +// +// +// diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationTest.java new file mode 100644 index 0000000..7a2f591 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/domain/FcmNotificationTest.java @@ -0,0 +1,40 @@ +// package earlybird.earlybird.scheduler.notification.fcm.domain; +// +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// +// import java.time.LocalDateTime; +// +// import static earlybird.earlybird.scheduler.notification.fcm.domain.NotificationStatus.COMPLETED; +// import static org.assertj.core.api.Assertions.assertThat; +// +// class FcmNotificationTest { +// +// @DisplayName("알림 발송 성공 상태를 업데이트한다.") +// @Test +// void onSendToFcmSuccess() { +// // given +// FcmNotification notification = FcmNotification.builder() +// .build(); +// +// // when +// notification.onSendToFcmSuccess(); +// +// // then +// assertThat(notification.getStatus()).isEqualTo(COMPLETED); +// assertThat(notification.getSentTime()).isNotNull(); +// } +// +// @DisplayName("알림 상태를 수정한다.") +// @Test +// void updateStatusTo() { +// // given +// FcmNotification notification = FcmNotification.builder().build(); +// +// // when +// notification.updateStatusTo(COMPLETED); +// +// // then +// assertThat(notification.getStatus()).isEqualTo(COMPLETED); +// } +// } diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/DeregisterNotificationAtSchedulerServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/DeregisterNotificationAtSchedulerServiceTest.java new file mode 100644 index 0000000..c5a8f73 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/DeregisterNotificationAtSchedulerServiceTest.java @@ -0,0 +1,126 @@ +// package earlybird.earlybird.scheduler.notification.fcm.service; +// +// import earlybird.earlybird.error.exception.AlreadySentFcmNotificationException; +// import earlybird.earlybird.error.exception.FcmDeviceTokenMismatchException; +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotification; +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotificationRepository; +// import +// earlybird.earlybird.scheduler.notification.fcm.service.request.AddTaskToSchedulingTaskListServiceRequest; +// import +// earlybird.earlybird.scheduler.notification.fcm.service.deregister.request.DeregisterFcmMessageAtSchedulerServiceRequest; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.test.context.ActiveProfiles; +// +// import java.time.LocalDateTime; +// import java.time.ZoneId; +// +// import static org.assertj.core.api.Assertions.assertThat; +// import static org.assertj.core.api.Assertions.assertThatThrownBy; +// import static org.junit.jupiter.api.Assertions.*; +// +// @ActiveProfiles("test") +// @SpringBootTest +// class DeregisterNotificationAtSchedulerServiceTest { +// +// @Autowired +// private DeregisterNotificationAtSchedulerService deregisterNotificationAtSchedulerService; +// +// @Autowired +// private FcmNotificationRepository fcmNotificationRepository; +// +// @Autowired +// private SchedulingTaskListService schedulingTaskListService; +// +// @AfterEach +// void tearDown() { +// fcmNotificationRepository.deleteAllInBatch(); +// } +// +// @DisplayName("스케줄러에 등록된 알림을 스케줄러와 DB 에서 삭제한다.") +// @Test +// void deregister() { +// // given +// LocalDateTime targetTime = LocalDateTime.now().plusDays(1); +// FcmNotification notification = createNotification(targetTime); +// +// FcmNotification savedNotification = fcmNotificationRepository.save(notification); +// +// DeregisterFcmMessageAtSchedulerServiceRequest request +// = createDeregisterRequest(savedNotification.getId(), "deviceToken"); +// +// AddTaskToSchedulingTaskListServiceRequest addRequest = +// AddTaskToSchedulingTaskListServiceRequest.builder() +// .targetTime(targetTime.atZone(ZoneId.of("Asia/Seoul")).toInstant()) +// .uuid("uuid") +// .title("title") +// .body("body") +// .deviceToken("deviceToken") +// .build(); +// +// schedulingTaskListService.add(addRequest); +// +// // when +// deregisterNotificationAtSchedulerService.deregister(request); +// +// // then +// assertThat(schedulingTaskListService.has("uuid")).isFalse(); +// assertThat(fcmNotificationRepository.findById(savedNotification.getId())).isEmpty(); +// } +// +// @DisplayName("저장되어 있는 알림의 디바이스 토큰 값과 요청에 담긴 디바이스 토큰 값이 일치하지 않으면 예외가 발생한다.") +// @Test +// void deregisterWithMismatchDeviceToken() { +// // given +// FcmNotification notification = createNotification(LocalDateTime.now().plusDays(1)); +// FcmNotification savedNotification = fcmNotificationRepository.save(notification); +// +// DeregisterFcmMessageAtSchedulerServiceRequest request +// = createDeregisterRequest(savedNotification.getId(), "mismatchedDeviceToken"); +// +// // when // then +// assertThatThrownBy(() -> deregisterNotificationAtSchedulerService.deregister(request)) +// .isInstanceOf(FcmDeviceTokenMismatchException.class); +// } +// +// private DeregisterFcmMessageAtSchedulerServiceRequest createDeregisterRequest(Long +// notificationId, String deviceToken) { +// return DeregisterFcmMessageAtSchedulerServiceRequest.builder() +// .notificationId(notificationId) +// .deviceToken(deviceToken) +// .build(); +// } +// +// @DisplayName("이미 전송된 알림을 삭제하려고 하면 예외가 발생한다.") +// @Test +// void test() { +// // given +// FcmNotification notification = createNotification(LocalDateTime.now()); +// notification.onSendToFcmSuccess("fcmMessageId"); +// FcmNotification savedNotification = fcmNotificationRepository.save(notification); +// +// DeregisterFcmMessageAtSchedulerServiceRequest request +// = createDeregisterRequest(savedNotification.getId(), "deviceToken"); +// +// // when // then +// assertThatThrownBy(() -> deregisterNotificationAtSchedulerService.deregister(request)) +// .isInstanceOf(AlreadySentFcmNotificationException.class); +// } +// +// +// private static FcmNotification createNotification(LocalDateTime targetTime) { +// return FcmNotification.builder() +// .uuid("uuid") +// .title("title") +// .body("body") +// .targetTime(targetTime) +// .deviceToken("deviceToken") +// .build(); +// } +// +// +// } diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/RegisterNotificationAtSchedulerServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/RegisterNotificationAtSchedulerServiceTest.java new file mode 100644 index 0000000..2b8c14c --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/RegisterNotificationAtSchedulerServiceTest.java @@ -0,0 +1,120 @@ +// package earlybird.earlybird.scheduler.notification.fcm.service; +// +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotificationRepository; +// import +// earlybird.earlybird.scheduler.notification.fcm.service.register.request.RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest; +// import earlybird.earlybird.messaging.request.SendMessageByTokenServiceRequest; +// import +// earlybird.earlybird.scheduler.notification.fcm.service.register.response.RegisterFcmMessageAtSchedulerServiceResponse; +// import org.awaitility.Awaitility; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.boot.test.mock.mockito.MockBean; +// import org.springframework.test.context.ActiveProfiles; +// +// import java.time.LocalDateTime; +// import java.util.concurrent.ExecutionException; +// import java.util.concurrent.TimeUnit; +// +// import static org.assertj.core.api.Assertions.assertThat; +// import static org.mockito.ArgumentMatchers.any; +// import static org.mockito.Mockito.*; +// +// @ActiveProfiles("test") +// @SpringBootTest +// class RegisterNotificationAtSchedulerServiceTest { +// +// @MockBean +// private SendMessageToFcmService sendMessageToFcmService; +// +// @Autowired +// private RegisterNotificationAtSchedulerService registerNotificationAtSchedulerService; +// +// @Autowired +// private SchedulingTaskListService schedulingTaskListService; +// +// @Autowired +// private FcmNotificationRepository fcmNotificationRepository; +// +// @AfterEach +// void tearDown() { +// fcmNotificationRepository.deleteAllInBatch(); +// } +// +// @DisplayName("FCM 메시지를 스케줄러에 등록하면 등록된 시간에 FCM 메시지 전송이 실행된다.") +// @Test +// void schedulerExecute() throws ExecutionException, InterruptedException { +// // given +// int targetSecond = 2; +// LocalDateTime targetTime = LocalDateTime.now().plusSeconds(targetSecond); +// RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest request = +// RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest.builder() +// .clientId("clientId") +// .deviceToken("deviceToken") +// .appointmentName("appointmentName") +// .preparationTime(LocalDateTime.now().minusHours(1)) +// .movingTime(LocalDateTime.now().minusHours(1)) +// .appointmentTime(LocalDateTime.now().plusSeconds(targetSecond)) +// .build(); +// +// // when +// registerNotificationAtSchedulerService.registerFcmMessageForNewAppointment(request); +// +// // then +// Awaitility.await() +// .atMost(targetSecond + 1, TimeUnit.SECONDS) +// .untilAsserted(() -> { +// verify(sendMessageToFcmService, +// +// times(1)).sendMessageByToken(any(SendMessageByTokenServiceRequest.class)); +// }); +// } +// +// @DisplayName("요청한 준비 시간, 이동 시간, 약속 시간에 따라 최대 7개의 알림이 스케줄러에 등록된다.") +// @Test +// void registerFcmMessageForNewAppointment() throws ExecutionException, InterruptedException { +// // given +// RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest request = +// RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest.builder() +// .clientId("clientId") +// .deviceToken("deviceToken") +// .appointmentName("appointmentName") +// .preparationTime(LocalDateTime.now().plusHours(2)) +// .movingTime(LocalDateTime.now().plusHours(3)) +// .appointmentTime(LocalDateTime.now().plusHours(4)) +// .build(); +// +// // when +// RegisterFcmMessageAtSchedulerServiceResponse response = +// registerNotificationAtSchedulerService.registerFcmMessageForNewAppointment(request); +// +// // then +// assertThat(response.getNotifications()).hasSize(7); +// } +// +// @DisplayName("알림 목표 시간이 현재보다 과거이면 알림이 등록되지 않는다.") +// @Test +// void targetTimeIsBeforeNow() { +// // given +// RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest request = +// RegisterFcmMessageForNewAppointmentAtSchedulerServiceRequest.builder() +// .clientId("clientId") +// .deviceToken("deviceToken") +// .appointmentName("appointmentName") +// .preparationTime(LocalDateTime.now().minusHours(1)) +// .movingTime(LocalDateTime.now().minusHours(1)) +// .appointmentTime(LocalDateTime.now().minusHours(1)) +// .build(); +// +// // when +// RegisterFcmMessageAtSchedulerServiceResponse response = +// registerNotificationAtSchedulerService.registerFcmMessageForNewAppointment(request); +// +// // then +// assertThat(response.getNotifications()).hasSize(0); +// } +// +// } diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/SchedulingTaskListServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/SchedulingTaskListServiceTest.java new file mode 100644 index 0000000..cf04e69 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/SchedulingTaskListServiceTest.java @@ -0,0 +1,10 @@ +package earlybird.earlybird.scheduler.notification.fcm.service; + +import static org.junit.jupiter.api.Assertions.*; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest +class SchedulingTaskListServiceTest {} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/SendMessageToFcmServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/SendMessageToFcmServiceTest.java new file mode 100644 index 0000000..b1eb16a --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/SendMessageToFcmServiceTest.java @@ -0,0 +1,120 @@ +// package earlybird.earlybird.scheduler.notification.fcm.service; +// +// import com.google.firebase.messaging.FirebaseMessagingException; +// import earlybird.earlybird.appointment.domain.Appointment; +// import earlybird.earlybird.appointment.domain.AppointmentRepository; +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotification; +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotificationRepository; +// import earlybird.earlybird.scheduler.notification.fcm.domain.NotificationStep; +// import earlybird.earlybird.messaging.request.SendMessageByTokenServiceRequest; +// import org.awaitility.Awaitility; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.boot.test.mock.mockito.MockBean; +// import org.springframework.test.context.ActiveProfiles; +// +// import java.time.LocalDateTime; +// import java.util.Optional; +// import java.util.concurrent.ExecutionException; +// import java.util.concurrent.TimeUnit; +// +// import static earlybird.earlybird.scheduler.notification.fcm.domain.NotificationStatus.COMPLETED; +// import static +// earlybird.earlybird.scheduler.notification.fcm.domain.NotificationStep.APPOINTMENT_TIME; +// import static org.assertj.core.api.Assertions.assertThat; +// import static org.mockito.Mockito.doReturn; +// +// @ActiveProfiles("test") +// @SpringBootTest +// class SendMessageToFcmServiceTest { +// +// @Autowired +// private FcmNotificationRepository fcmNotificationRepository; +// +// @Autowired +// private AppointmentRepository appointmentRepository; +// +// @Autowired +// private SendMessageToFcmService sendMessageToFcmService; +// +// @MockBean +// private MessagingService messagingService; +// +// @AfterEach +// void tearDown() { +// fcmNotificationRepository.deleteAllInBatch(); +// appointmentRepository.deleteAllInBatch(); +// } +// +// @DisplayName("FCM 으로 메시지를 보내고 전송 정보를 DB에 업데이트한다.") +// @Test +// void sendMessageByToken() throws FirebaseMessagingException, ExecutionException, +// InterruptedException { +// // given +// String title = "title"; +// String body = "body"; +// String uuid = "uuid"; +// String deviceToken = "deviceToken"; +// LocalDateTime targetTime = LocalDateTime.of(2024, 10, 9, 0, 0); +// +// +// Appointment appointment = createAppointment(); +// FcmNotification fcmNotification = createFcmNotification(appointment, targetTime, +// APPOINTMENT_TIME); +// appointment.addFcmNotification(fcmNotification); +// Appointment savedAppointment = appointmentRepository.save(appointment); +// FcmNotification savedNotification = fcmNotificationRepository.save(fcmNotification); +// +// SendMessageByTokenServiceRequest request = createRequest(title, body, deviceToken, +// savedNotification.getId()); +// +// String fcmMessageId = "fcm-message-id"; +// doReturn(fcmMessageId).when(messagingService).send(request); +// +// // when +// sendMessageToFcmService.sendMessageByToken(request); +// +// // then +// Awaitility.await() +// .atMost(5, TimeUnit.SECONDS) +// .until(() -> +// fcmNotificationRepository.findById(savedNotification.getId()).get().getStatus().equals(COMPLETED)); +// +// Optional optional = +// fcmNotificationRepository.findById(savedNotification.getId()); +// assertThat(optional).isPresent(); +// assertThat(optional.get().getStatus()).isEqualTo(COMPLETED); +// assertThat(optional.get().getSentTime()).isNotNull(); +// } +// +// private Appointment createAppointment() { +// return appointmentRepository.save(Appointment.builder() +// .appointmentName("appointmentName") +// .clientId("clientId") +// .deviceToken("deviceToken") +// .build()); +// } +// +// private FcmNotification createFcmNotification(Appointment appointment, LocalDateTime +// targetTime, NotificationStep notificationStep) { +// return FcmNotification.builder() +// .appointment(appointment) +// .targetTime(targetTime) +// .notificationStep(notificationStep) +// .build(); +// } +// +// private SendMessageByTokenServiceRequest createRequest(String title, String body, String +// deviceToken, Long notificationId) { +// return SendMessageByTokenServiceRequest.builder() +// .title(title) +// .body(body) +// .deviceToken(deviceToken) +// .notificationId(notificationId) +// .build(); +// } +// +// } diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/UpdateNotificationServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/UpdateNotificationServiceTest.java new file mode 100644 index 0000000..dee4112 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/UpdateNotificationServiceTest.java @@ -0,0 +1,165 @@ +// package earlybird.earlybird.scheduler.notification.fcm.service; +// +// import com.google.firebase.messaging.FirebaseMessagingException; +// import earlybird.earlybird.error.exception.AlreadySentFcmNotificationException; +// import earlybird.earlybird.error.exception.FcmDeviceTokenMismatchException; +// import earlybird.earlybird.error.exception.FcmMessageTimeBeforeNowException; +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotification; +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotificationRepository; +// import +// earlybird.earlybird.scheduler.notification.fcm.service.request.AddTaskToSchedulingTaskListServiceRequest; +// import +// earlybird.earlybird.scheduler.notification.fcm.service.update.request.UpdateFcmMessageServiceRequest; +// import +// earlybird.earlybird.scheduler.notification.fcm.service.update.response.UpdateFcmMessageServiceResponse; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.boot.test.mock.mockito.MockBean; +// import org.springframework.test.context.ActiveProfiles; +// +// import java.time.LocalDateTime; +// import java.time.ZoneId; +// +// import static org.assertj.core.api.Assertions.assertThat; +// import static org.assertj.core.api.Assertions.assertThatThrownBy; +// import static org.junit.jupiter.api.Assertions.*; +// import static org.mockito.ArgumentMatchers.any; +// import static org.mockito.Mockito.doReturn; +// +// @ActiveProfiles("test") +// @SpringBootTest +// class UpdateNotificationServiceTest { +// +// @Autowired +// private UpdateNotificationService service; +// +// @Autowired +// private FcmNotificationRepository repository; +// +// @Autowired +// private SchedulingTaskListService schedulingTaskListService; +// +// @MockBean +// private FirebaseMessagingService firebaseMessagingService; +// +// @AfterEach +// void tearDown() { +// repository.deleteAllInBatch(); +// } +// +// @DisplayName("알림 정보와 스케줄링 정보를 업데이트한다.") +// @Test +// void update() throws FirebaseMessagingException { +// // given +// String deviceToken = "deviceToken"; +// LocalDateTime savedTargetTime = LocalDateTime.now().plusDays(1); +// +// FcmNotification notification = createFcmNotification(deviceToken, savedTargetTime); +// FcmNotification savedNotification = repository.save(notification); +// +// AddTaskToSchedulingTaskListServiceRequest addRequest = +// AddTaskToSchedulingTaskListServiceRequest.builder() +// .body(savedNotification.getBody()) +// .deviceToken(deviceToken) +// .title(notification.getTitle()) +// .uuid(notification.getUuid()) +// .targetTime(savedTargetTime.atZone(ZoneId.of("Asia/Seoul")).toInstant()) +// .build(); +// schedulingTaskListService.add(addRequest); +// +// LocalDateTime updatedTargetTime = LocalDateTime.now().plusDays(2).withNano(0); +// +// UpdateFcmMessageServiceRequest request = UpdateFcmMessageServiceRequest.builder() +// .notificationId(savedNotification.getId()) +// .deviceToken(deviceToken) +// .targetTime(updatedTargetTime) +// .build(); +// +// String fcmMessageId = "fcm-message-id"; +// doReturn(fcmMessageId).when(firebaseMessagingService).send(any()); +// +// // when +// UpdateFcmMessageServiceResponse response = service.update(request); +// +// // then +// assertThat(response.getNotificationId()).isEqualTo(savedNotification.getId()); +// assertThat(response.getUpdatedTargetTime()).isEqualTo(updatedTargetTime); +// +// FcmNotification updatedNotification = +// repository.findById(response.getNotificationId()).get(); +// assertThat(updatedNotification.getTargetTime()).isEqualTo(updatedTargetTime); +// } +// +// @DisplayName("현재 시간보다 과거의 시간으로 알림 시간을 수정하려고 하면 예외가 발생한다.") +// @Test +// void updateTargetTimeBeforeNow() { +// // given +// UpdateFcmMessageServiceRequest request = +// UpdateFcmMessageServiceRequest.builder().targetTime(LocalDateTime.now().minusSeconds(1)).build(); +// +// // when // then +// assertThatThrownBy(() -> service.update(request)) +// .isInstanceOf(FcmMessageTimeBeforeNowException.class); +// } +// +// @DisplayName("알림을 수정하려고 할 때 저장되어 있는 알림의 디바이스 토큰과 요청 디바이스 토큰이 다르면 예외가 발생한다.") +// @Test +// void deviceTokenMismatch() { +// // given +// String deviceToken = "deviceToken"; +// LocalDateTime savedTargetTime = LocalDateTime.now(); +// +// FcmNotification notification = createFcmNotification(deviceToken, savedTargetTime); +// FcmNotification savedNotification = repository.save(notification); +// +// String mismatchDeviceToken = "mismatchDeviceToken"; +// UpdateFcmMessageServiceRequest request = UpdateFcmMessageServiceRequest.builder() +// .deviceToken(mismatchDeviceToken) +// .targetTime(LocalDateTime.now().plusDays(1)) +// .notificationId(savedNotification.getId()) +// .build(); +// +// // when // then +// assertThatThrownBy(() -> service.update(request)) +// .isInstanceOf(FcmDeviceTokenMismatchException.class); +// } +// +// @DisplayName("이미 전송한 알림을 수정하려고 하면 예외가 발생한다.") +// @Test +// void alreadySentFcmNotification() { +// // given +// FcmNotification notification = createFcmNotification("deviceToken", +// LocalDateTime.now().minusDays(1)); +// notification.onSendToFcmSuccess("fcmMessageId"); +// repository.save(notification); +// +// UpdateFcmMessageServiceRequest request = UpdateFcmMessageServiceRequest.builder() +// .notificationId(notification.getId()) +// .targetTime(LocalDateTime.now().plusDays(1)) +// .deviceToken(notification.getDeviceToken()) +// .build(); +// +// // when // then +// assertThatThrownBy(() -> service.update(request)) +// .isInstanceOf(AlreadySentFcmNotificationException.class); +// +// } +// +// +// private FcmNotification createFcmNotification(String deviceToken, LocalDateTime +// savedTargetTime) { +// return FcmNotification.builder() +// .body("바디") +// .uuid("uuid") +// .title("제목") +// .deviceToken(deviceToken) +// .targetTime(savedTargetTime) +// .build(); +// } +// +// +// } diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/manager/NotificationSchedulerManagerStub.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/manager/NotificationSchedulerManagerStub.java new file mode 100644 index 0000000..2d757dc --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/manager/NotificationSchedulerManagerStub.java @@ -0,0 +1,30 @@ +package earlybird.earlybird.scheduler.notification.service.manager; + +import earlybird.earlybird.scheduler.manager.NotificationSchedulerManager; +import earlybird.earlybird.scheduler.manager.request.AddNotificationToSchedulerServiceRequest; + +import java.util.ArrayList; +import java.util.List; + +public class NotificationSchedulerManagerStub implements NotificationSchedulerManager { + private final List notificationIds = new ArrayList(); + + @Override + public void init() { + return; + } + + @Override + public void add(AddNotificationToSchedulerServiceRequest request) { + this.notificationIds.add(request.getNotificationId()); + } + + @Override + public void remove(Long notificationId) { + this.notificationIds.remove(notificationId); + } + + public List getNotificationIds() { + return notificationIds; + } +} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/register/AppointmentRepositoryStub.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/register/AppointmentRepositoryStub.java new file mode 100644 index 0000000..909857f --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/register/AppointmentRepositoryStub.java @@ -0,0 +1,149 @@ +package earlybird.earlybird.scheduler.notification.service.register; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.AppointmentRepository; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.FluentQuery; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public class AppointmentRepositoryStub implements AppointmentRepository { + @Override + public void flush() {} + + @Override + public S saveAndFlush(S entity) { + return null; + } + + @Override + public List saveAllAndFlush(Iterable entities) { + return List.of(); + } + + @Override + public void deleteAllInBatch(Iterable entities) {} + + @Override + public void deleteAllByIdInBatch(Iterable longs) {} + + @Override + public void deleteAllInBatch() {} + + @Override + public Appointment getOne(Long aLong) { + return null; + } + + @Override + public Appointment getById(Long aLong) { + return null; + } + + @Override + public Appointment getReferenceById(Long aLong) { + return null; + } + + @Override + public Optional findOne(Example example) { + return Optional.empty(); + } + + @Override + public List findAll(Example example) { + return List.of(); + } + + @Override + public List findAll(Example example, Sort sort) { + return List.of(); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + return null; + } + + @Override + public long count(Example example) { + return 0; + } + + @Override + public boolean exists(Example example) { + return false; + } + + @Override + public R findBy( + Example example, Function, R> queryFunction) { + return null; + } + + @Override + public S save(S entity) { + return null; + } + + @Override + public List saveAll(Iterable entities) { + return List.of(); + } + + @Override + public Optional findById(Long aLong) { + return Optional.empty(); + } + + @Override + public boolean existsById(Long aLong) { + return false; + } + + @Override + public List findAll() { + return List.of(); + } + + @Override + public List findAllById(Iterable longs) { + return List.of(); + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(Long aLong) {} + + @Override + public void delete(Appointment entity) {} + + @Override + public void deleteAllById(Iterable longs) {} + + @Override + public void deleteAll(Iterable entities) {} + + @Override + public void deleteAll() {} + + @Override + public List findAll(Sort sort) { + return List.of(); + } + + @Override + public Page findAll(Pageable pageable) { + return null; + } +} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/register/RegisterAllNotificationAtSchedulerServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/register/RegisterAllNotificationAtSchedulerServiceTest.java new file mode 100644 index 0000000..d44ec45 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/register/RegisterAllNotificationAtSchedulerServiceTest.java @@ -0,0 +1,52 @@ +package earlybird.earlybird.scheduler.notification.service.register; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.CreateTestAppointment; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepositoryStub; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; +import earlybird.earlybird.scheduler.notification.service.manager.NotificationSchedulerManagerStub; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Map; + +class RegisterAllNotificationAtSchedulerServiceTest { + + private FcmNotificationRepository fcmNotificationRepository; + private NotificationSchedulerManagerStub schedulerManager; + + @BeforeEach + void setUp() { + fcmNotificationRepository = new FcmNotificationRepositoryStub(); + schedulerManager = new NotificationSchedulerManagerStub(); + } + + @DisplayName("등록 시점보다 미래의 다수 알림을 등록한다.") + @Test + void register() { + // given + RegisterAllNotificationAtSchedulerService service = + new RegisterAllNotificationAtSchedulerService( + fcmNotificationRepository, schedulerManager); + + Appointment appointment = CreateTestAppointment.create(); + Map notificationInfo = + Map.of( + NotificationStep.APPOINTMENT_TIME, Instant.now().plusSeconds(10), + NotificationStep.MOVING_TIME, Instant.now().plusSeconds(10)); + + // when + service.register(appointment, notificationInfo); + + // then + assertThat(appointment.getFcmNotifications()).hasSize(notificationInfo.size()); + assertThat(schedulerManager.getNotificationIds()).hasSize(notificationInfo.size()); + } +} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/register/RegisterOneNotificationAtSchedulerServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/register/RegisterOneNotificationAtSchedulerServiceTest.java new file mode 100644 index 0000000..03bf207 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/register/RegisterOneNotificationAtSchedulerServiceTest.java @@ -0,0 +1,89 @@ +package earlybird.earlybird.scheduler.notification.service.register; + +import static earlybird.earlybird.scheduler.notification.domain.NotificationStep.*; + +import static org.assertj.core.api.Assertions.*; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.CreateTestAppointment; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepositoryStub; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; +import earlybird.earlybird.scheduler.notification.service.manager.NotificationSchedulerManagerStub; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.*; +import java.util.Map; + +class RegisterOneNotificationAtSchedulerServiceTest { + private FcmNotificationRepository fcmNotificationRepository; + private NotificationSchedulerManagerStub schedulerManager; + + @BeforeEach + void setUp() { + fcmNotificationRepository = new FcmNotificationRepositoryStub(); + schedulerManager = new NotificationSchedulerManagerStub(); + } + + @DisplayName("알림 1개를 스케줄러에 등록한다.") + @Test + void register() { + // given + RegisterOneNotificationAtSchedulerService service = + new RegisterOneNotificationAtSchedulerService( + fcmNotificationRepository, schedulerManager); + + Appointment appointment = CreateTestAppointment.create(); + + Map notificationInfo = + Map.of(APPOINTMENT_TIME, Instant.now().plusSeconds(10)); + + // when + service.register(appointment, notificationInfo); + + // then + assertThat(appointment.getFcmNotifications()).hasSize(1); + assertThat(schedulerManager.getNotificationIds()).hasSize(1); + } + + @DisplayName("1개 이상의 알림 등록을 요청하면 예외가 발생한다.") + @Test + void registerMoreThanOne() { + // given + RegisterOneNotificationAtSchedulerService service = + new RegisterOneNotificationAtSchedulerService( + fcmNotificationRepository, schedulerManager); + + Appointment appointment = CreateTestAppointment.create(); + Map notificationInfo = + Map.of( + APPOINTMENT_TIME, Instant.now().plusSeconds(10), + MOVING_TIME, Instant.now().plusSeconds(10)); + + // when // then + assertThatThrownBy(() -> service.register(appointment, notificationInfo)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("등록 시점보다 과거의 알림은 등록되지 않는다.") + @Test + void test() { + // given + RegisterOneNotificationAtSchedulerService service = + new RegisterOneNotificationAtSchedulerService( + fcmNotificationRepository, schedulerManager); + Appointment appointment = CreateTestAppointment.create(); + Map notificationInfo = + Map.of(APPOINTMENT_TIME, Instant.now().minusSeconds(1)); + + // when + service.register(appointment, notificationInfo); + + // then + assertThat(appointment.getFcmNotifications()).hasSize(0); + assertThat(schedulerManager.getNotificationIds()).hasSize(0); + } +} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/request/RegisterFcmMessageAtSchedulerServiceRequestTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/request/RegisterFcmMessageAtSchedulerServiceRequestTest.java new file mode 100644 index 0000000..b1c4898 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/request/RegisterFcmMessageAtSchedulerServiceRequestTest.java @@ -0,0 +1,91 @@ +// package earlybird.earlybird.scheduler.notification.fcm.service.request; +// +// import earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotification; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +// import org.springframework.test.context.ActiveProfiles; +// +// import java.time.Instant; +// import java.time.LocalDateTime; +// import java.time.ZoneId; +// import java.time.format.DateTimeFormatter; +// +// import static earlybird.earlybird.scheduler.notification.fcm.domain.FcmNotificationStatus.*; +// import static org.assertj.core.api.Assertions.assertThat; +// +// class RegisterFcmMessageAtSchedulerServiceRequestTest { +// +// @DisplayName("LocalDateTime 으로 저장되어 있는 전송 목표 시간을 Instant 로 변환해서 가져온다.") +// @Test +// void getTargetTimeInstant() { +// // given +// LocalDateTime targetTime = LocalDateTime.of(2024, 10, 11, 1, 2, 3); +// RegisterFcmMessageAtSchedulerServiceRequest request = +// RegisterFcmMessageAtSchedulerServiceRequest.builder() +// .targetTime(targetTime) +// .build(); +// +// // when +// Instant result = request.getTargetTimeInstant(); +// +// // then +// assertThat(result.atZone(ZoneId.of("Asia/Seoul")).toString().split("\\+")[0]) +// .isEqualTo(targetTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); +// } +// +// @DisplayName("RegisterFcmMessageAtSchedulerServiceRequest 객체를 FcmNotification 객체로 변환한다.") +// @Test +// void toFcmNotification() { +// // given +// LocalDateTime targetTime = LocalDateTime.of(2024, 10, 11, 1, 2, 3); +// RegisterFcmMessageAtSchedulerServiceRequest request = +// RegisterFcmMessageAtSchedulerServiceRequest.builder() +// .title("제목") +// .body("바디") +// .deviceToken("디바이스 토큰") +// .targetTime(targetTime) +// .build(); +// +// // when +// FcmNotification result = request.toFcmNotification(); +// +// // then +// assertThat(result).extracting("uuid", "title", "body", "deviceToken", "targetTime", +// "status") +// .containsExactly( +// request.getUuid(), request.getTitle(), request.getBody(), +// request.getDeviceToken(), +// targetTime, PENDING +// ); +// +// } +// +// @DisplayName("Builder 패턴을 이용해 객체를 생성할 수 있다.") +// @Test +// void builder() { +// // given +// String title = "title"; +// String body = "body"; +// String deviceToken = "deviceToken"; +// LocalDateTime targetTime = LocalDateTime.of(2024, 10, 11, 1, 2, 3); +// +// // when +// RegisterFcmMessageAtSchedulerServiceRequest result = +// RegisterFcmMessageAtSchedulerServiceRequest.builder() +// .title(title) +// .body(body) +// .deviceToken(deviceToken) +// .targetTime(targetTime) +// .build(); +// +// // then +// assertThat(result).extracting("title", "body", "deviceToken", "targetTime") +// .containsExactly(title, body, deviceToken, targetTime); +// assertThat(result.getUuid()).isNotNull(); +// +// } +// +// +// +// } diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationServiceTest.java new file mode 100644 index 0000000..5614aae --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationServiceTest.java @@ -0,0 +1,136 @@ +package earlybird.earlybird.scheduler.notification.service.update; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import earlybird.earlybird.appointment.domain.Appointment; +import earlybird.earlybird.appointment.domain.CreateTestAppointment; +import earlybird.earlybird.appointment.service.FindAppointmentService; +import earlybird.earlybird.scheduler.notification.domain.NotificationStep; +import earlybird.earlybird.scheduler.notification.domain.NotificationUpdateType; +import earlybird.earlybird.scheduler.notification.service.NotificationInfoFactory; +import earlybird.earlybird.scheduler.notification.service.deregister.DeregisterNotificationService; +import earlybird.earlybird.scheduler.notification.service.register.RegisterNotificationService; +import earlybird.earlybird.scheduler.notification.service.update.request.UpdateFcmMessageServiceRequest; +import earlybird.earlybird.scheduler.notification.service.update.response.UpdateFcmMessageServiceResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.HashMap; + +@ExtendWith(MockitoExtension.class) +class UpdateNotificationServiceTest { + + @Mock private RegisterNotificationService registerNotificationService; + + @Mock private DeregisterNotificationService deregisterNotificationService; + + @Mock private FindAppointmentService findAppointmentService; + + @Mock private NotificationInfoFactory notificationInfoFactory; + + @InjectMocks private UpdateNotificationService updateNotificationService; + + private Appointment appointment; + private Long appointmentId; + private NotificationUpdateType updateType; + private final HashMap notificationInfo = new HashMap<>(); + + @BeforeEach + void setUp() { + this.appointment = CreateTestAppointment.create(); + this.appointmentId = 1L; + this.updateType = NotificationUpdateType.MODIFY; + + when(findAppointmentService.findBy(appointmentId, appointment.getClientId())) + .thenReturn(appointment); + when(notificationInfoFactory.createTargetTimeMap(any(LocalDateTime.class), any(), any())) + .thenReturn(notificationInfo); + } + + @DisplayName("업데이트를 하면 등록되어 있던 알림을 해제하고 새로운 알림을 등록한다.") + @Test + void updateThenDeregisterAndRegisterNotification() { + // given + + UpdateFcmMessageServiceRequest request = + UpdateFcmMessageServiceRequest.builder() + .appointmentId(appointmentId) + .appointmentName(appointment.getAppointmentName()) + .clientId(appointment.getClientId()) + .deviceToken(appointment.getDeviceToken()) + .preparationTime(LocalDateTime.now()) + .movingTime(LocalDateTime.now()) + .appointmentTime(LocalDateTime.now()) + .updateType(updateType) + .build(); + + // when + updateNotificationService.update(request); + + // then + verify(registerNotificationService).register(appointment, notificationInfo); + verify(deregisterNotificationService).deregister(any()); + } + + @DisplayName("디바이스 토큰이 저장되어 있는 값과 업데이트시 요청 값이 다르면 업데이트한다.") + @Test + void changeDeviceTokenWhenRequestDeviceTokenIsDifferent() { + // given + String newDeviceToken = "newDeviceToken"; + + UpdateFcmMessageServiceRequest request = + UpdateFcmMessageServiceRequest.builder() + .appointmentId(appointmentId) + .appointmentName(appointment.getAppointmentName()) + .clientId(appointment.getClientId()) + .deviceToken(newDeviceToken) + .preparationTime(LocalDateTime.now()) + .movingTime(LocalDateTime.now()) + .appointmentTime(LocalDateTime.now()) + .updateType(updateType) + .build(); + + // when + UpdateFcmMessageServiceResponse response = updateNotificationService.update(request); + + // then + assertThat(appointment.getDeviceToken()).isEqualTo(newDeviceToken); + assertThat(response.getAppointment().getDeviceToken()).isEqualTo(newDeviceToken); + } + + @DisplayName("약속 이름이 저장되어 있는 값과 업데이트시 요청 값이 다르면 업데이트한다.") + @Test + void changeAppointmentNameWhenRequestDeviceTokenIsDifferent() { + // given + String newAppointmentName = "newAppointmentName"; + + UpdateFcmMessageServiceRequest request = + UpdateFcmMessageServiceRequest.builder() + .appointmentId(appointmentId) + .appointmentName(newAppointmentName) + .clientId(appointment.getClientId()) + .deviceToken(appointment.getDeviceToken()) + .preparationTime(LocalDateTime.now()) + .movingTime(LocalDateTime.now()) + .appointmentTime(LocalDateTime.now()) + .updateType(updateType) + .build(); + + // when + UpdateFcmMessageServiceResponse response = updateNotificationService.update(request); + + // then + + } +} diff --git a/src/test/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationStatusServiceTest.java b/src/test/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationStatusServiceTest.java new file mode 100644 index 0000000..ff1b892 --- /dev/null +++ b/src/test/java/earlybird/earlybird/scheduler/notification/service/update/UpdateNotificationStatusServiceTest.java @@ -0,0 +1,74 @@ +package earlybird.earlybird.scheduler.notification.service.update; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import earlybird.earlybird.error.exception.NotificationNotFoundException; +import earlybird.earlybird.scheduler.notification.domain.CreateTestFcmNotification; +import earlybird.earlybird.scheduler.notification.domain.FcmNotification; +import earlybird.earlybird.scheduler.notification.domain.FcmNotificationRepository; +import earlybird.earlybird.scheduler.notification.domain.NotificationStatus; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class UpdateNotificationStatusServiceTest { + + @Mock private FcmNotificationRepository notificationRepository; + + @InjectMocks private UpdateNotificationStatusService service; + + @DisplayName("푸시 알림 전송 성공 시 푸시 알림 정보를 성공으로 업데이트한다.") + @Test + void updateToSuccess() { + // given + FcmNotification notification = CreateTestFcmNotification.create(); + + Long notificationId = 1L; + + when(notificationRepository.findById(notificationId)).thenReturn(Optional.of(notification)); + + // when + service.update(notificationId, true); + + // then + assertThat(notification.getStatus()).isEqualTo(NotificationStatus.COMPLETED); + } + + @DisplayName("푸시 알림 전송 실패 시 푸시 알림 정보를 실패로 업데이트한다.") + @Test + void updateToFailure() { + // given + FcmNotification notification = CreateTestFcmNotification.create(); + Long notificationId = 1L; + when(notificationRepository.findById(notificationId)).thenReturn(Optional.of(notification)); + + // when + service.update(notificationId, false); + + // then + assertThat(notification.getStatus()).isEqualTo(NotificationStatus.FAILED); + } + + @DisplayName("업데이트 대상 알림이 존재하지 않으면 예외가 발생한다.") + @Test + void throwExceptionWhenNotificationNotFound() { + // given + Long notificationId = 1L; + when(notificationRepository.findById(notificationId)).thenReturn(Optional.empty()); + + // when // then + assertThatThrownBy(() -> service.update(notificationId, true)) + .isInstanceOf(NotificationNotFoundException.class); + assertThatThrownBy(() -> service.update(notificationId, false)) + .isInstanceOf(NotificationNotFoundException.class); + } +}