diff --git a/.gitignore b/.gitignore index b61eb11b..61f07020 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ out/ application-secret.yml braindump-prompt.txt -prioritize-prompt.txt \ No newline at end of file +prioritize-prompt.txt +cloud-task-key.json \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 40bced0b..5b89330a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -76,6 +76,16 @@ dependencies { implementation("com.google.api-client:google-api-client:2.2.0") implementation ("com.google.oauth-client:google-oauth-client-jetty:1.34.1") implementation("com.google.apis:google-api-services-calendar:v3-rev20250115-2.0.0") + + //Google Cloud Task + implementation ("com.google.cloud:google-cloud-tasks:2.40.0") + implementation ("com.google.auth:google-auth-library-oauth2-http:1.20.0") + + //queryDsl + implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta") + annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta") + annotationProcessor ("jakarta.annotation:jakarta.annotation-api") + annotationProcessor ("jakarta.persistence:jakarta.persistence-api") } kotlin { diff --git a/src/main/java/com/official/memento/global/config/QueryDslConfig.java b/src/main/java/com/official/memento/global/config/QueryDslConfig.java new file mode 100644 index 00000000..369847bb --- /dev/null +++ b/src/main/java/com/official/memento/global/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package com.official.memento.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.AllArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@AllArgsConstructor +public class QueryDslConfig { + + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory(final EntityManager entityManager) { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/com/official/memento/schedule/controller/ScheduleApiController.java b/src/main/java/com/official/memento/schedule/controller/ScheduleApiController.java index ae599b59..98af07dc 100644 --- a/src/main/java/com/official/memento/schedule/controller/ScheduleApiController.java +++ b/src/main/java/com/official/memento/schedule/controller/ScheduleApiController.java @@ -9,8 +9,12 @@ import com.official.memento.schedule.controller.dto.response.ScheduleAllGetResponse; import com.official.memento.schedule.controller.dto.response.ScheduleDetailResponse; import com.official.memento.schedule.domain.entity.Schedule; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; +import com.official.memento.schedule.service.CloudTaskAdapter; import com.official.memento.schedule.service.command.*; import com.official.memento.schedule.service.usecase.*; +import java.io.IOException; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -31,6 +35,7 @@ public class ScheduleApiController implements ScheduleApiDocs { private final ScheduleGroupDeleteUseCase scheduleGroupDeleteUseCase; private final ScheduleGroupUpdateUseCase scheduleGroupUpdateUseCase; private final ScheduleGroupCreateUseCase scheduleGroupCreateUseCase; + private final CloudTaskAdapter cloudTaskAdapter; @PostMapping @Override diff --git a/src/main/java/com/official/memento/schedule/domain/ScheduleAlarmRepository.java b/src/main/java/com/official/memento/schedule/domain/ScheduleAlarmRepository.java new file mode 100644 index 00000000..5c91e316 --- /dev/null +++ b/src/main/java/com/official/memento/schedule/domain/ScheduleAlarmRepository.java @@ -0,0 +1,9 @@ +package com.official.memento.schedule.domain; + +import com.official.memento.schedule.domain.entity.ScheduleAlarm; +import java.time.LocalDateTime; +import java.util.List; + +public interface ScheduleAlarmRepository { + List findSchedulesWithMemberInfoBetween(final LocalDateTime startTime, final LocalDateTime endTime); +} diff --git a/src/main/java/com/official/memento/schedule/domain/ScheduleRepository.java b/src/main/java/com/official/memento/schedule/domain/ScheduleRepository.java index fbbb3dc6..e7ed2d5d 100644 --- a/src/main/java/com/official/memento/schedule/domain/ScheduleRepository.java +++ b/src/main/java/com/official/memento/schedule/domain/ScheduleRepository.java @@ -1,6 +1,7 @@ package com.official.memento.schedule.domain; import com.official.memento.schedule.domain.entity.Schedule; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/com/official/memento/schedule/domain/entity/ScheduleAlarm.java b/src/main/java/com/official/memento/schedule/domain/entity/ScheduleAlarm.java new file mode 100644 index 00000000..93c09c02 --- /dev/null +++ b/src/main/java/com/official/memento/schedule/domain/entity/ScheduleAlarm.java @@ -0,0 +1,36 @@ +package com.official.memento.schedule.domain.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +public class ScheduleAlarm { + + private final Long scheduleId; + private final Long memberId; + private final String description; + private final LocalDateTime startDate; + private final LocalDateTime endDate; + private final int timeZoneOffset; + + public static ScheduleAlarm of( + final Long scheduleId, + final Long memberId, + final String description, + final LocalDateTime startDate, + final LocalDateTime endDate, + final int timeZoneOffset + ) { + return new ScheduleAlarm( + scheduleId, + memberId, + description, + startDate, + endDate, + timeZoneOffset + ); + } +} diff --git a/src/main/java/com/official/memento/schedule/infrastructure/ScheduleAlarmRepositoryAdapter.java b/src/main/java/com/official/memento/schedule/infrastructure/ScheduleAlarmRepositoryAdapter.java new file mode 100644 index 00000000..678092d2 --- /dev/null +++ b/src/main/java/com/official/memento/schedule/infrastructure/ScheduleAlarmRepositoryAdapter.java @@ -0,0 +1,32 @@ +package com.official.memento.schedule.infrastructure; + +import com.official.memento.global.stereotype.Adapter; +import com.official.memento.schedule.domain.ScheduleAlarmRepository; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; +import com.official.memento.schedule.infrastructure.persistence.ScheduleAlarmCustomRepository; +import com.official.memento.schedule.infrastructure.persistence.projection.ScheduleAlarmProjection; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@Adapter +@RequiredArgsConstructor +public class ScheduleAlarmRepositoryAdapter implements ScheduleAlarmRepository { + + private final ScheduleAlarmCustomRepository scheduleAlarmCustomRepository; + + @Override + public List findSchedulesWithMemberInfoBetween(final LocalDateTime startTime, + final LocalDateTime endTime) { + List scheduleAlarmProjections = scheduleAlarmCustomRepository.findSchedulesWithMemberInfoBetween( + startTime, endTime); + return scheduleAlarmProjections.stream().map(scheduleEntity -> ScheduleAlarm.of( + scheduleEntity.scheduleId(), + scheduleEntity.memberId(), + scheduleEntity.description(), + scheduleEntity.startDate(), + scheduleEntity.endDate(), + scheduleEntity.timeZoneOffset() + )).toList(); + } +} diff --git a/src/main/java/com/official/memento/schedule/infrastructure/ScheduleRepositoryAdapter.java b/src/main/java/com/official/memento/schedule/infrastructure/ScheduleRepositoryAdapter.java index 1ae5a0f0..839292bd 100644 --- a/src/main/java/com/official/memento/schedule/infrastructure/ScheduleRepositoryAdapter.java +++ b/src/main/java/com/official/memento/schedule/infrastructure/ScheduleRepositoryAdapter.java @@ -5,15 +5,19 @@ import com.official.memento.global.stereotype.Adapter; import com.official.memento.schedule.domain.ScheduleRepository; import com.official.memento.schedule.domain.entity.Schedule; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; import com.official.memento.schedule.domain.enums.ScheduleType; +import com.official.memento.schedule.infrastructure.persistence.ScheduleAlarmCustomRepository; import com.official.memento.schedule.infrastructure.persistence.ScheduleEntity; import com.official.memento.schedule.infrastructure.persistence.ScheduleEntityJpaRepository; +import com.official.memento.schedule.infrastructure.persistence.projection.ScheduleAlarmProjection; import com.official.memento.schedule.infrastructure.persistence.projection.ScheduleOrderInfoProjection; +import lombok.RequiredArgsConstructor; + import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; -import lombok.RequiredArgsConstructor; @Adapter @RequiredArgsConstructor diff --git a/src/main/java/com/official/memento/schedule/infrastructure/persistence/ScheduleAlarmCustomRepository.java b/src/main/java/com/official/memento/schedule/infrastructure/persistence/ScheduleAlarmCustomRepository.java new file mode 100644 index 00000000..f8450795 --- /dev/null +++ b/src/main/java/com/official/memento/schedule/infrastructure/persistence/ScheduleAlarmCustomRepository.java @@ -0,0 +1,45 @@ +package com.official.memento.schedule.infrastructure.persistence; + +import com.official.memento.member.infrastructure.persistence.entity.QMemberPersonalInfoEntity; +import com.official.memento.schedule.infrastructure.persistence.projection.ScheduleAlarmProjection; +import com.querydsl.core.Tuple; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ScheduleAlarmCustomRepository { + + private final JPAQueryFactory queryFactory; + QScheduleEntity schedule = QScheduleEntity.scheduleEntity; + QMemberPersonalInfoEntity memberPersonalInfo = QMemberPersonalInfoEntity.memberPersonalInfoEntity; + + public List findSchedulesWithMemberInfoBetween( + final LocalDateTime startDate, + final LocalDateTime endDate + ) { + List results = queryFactory + .select(schedule.id, schedule.memberId, schedule.description, schedule.startDate, schedule.endDate, memberPersonalInfo.timeZoneOffset) + .from(schedule) + .join(memberPersonalInfo).on(schedule.memberId.eq(memberPersonalInfo.memberId)) + .where(schedule.startDate.between(startDate, endDate)) + .fetch(); + + return results.stream() + .map(tuple -> new ScheduleAlarmProjection( + tuple.get(schedule.id), + tuple.get(schedule.memberId), + tuple.get(schedule.description), + tuple.get(schedule.startDate), + tuple.get(schedule.endDate), + tuple.get(memberPersonalInfo.timeZoneOffset) + )) + .collect(Collectors.toList()); + } + + +} diff --git a/src/main/java/com/official/memento/schedule/infrastructure/persistence/ScheduleEntityJpaRepository.java b/src/main/java/com/official/memento/schedule/infrastructure/persistence/ScheduleEntityJpaRepository.java index 2dc46840..ed8c6420 100644 --- a/src/main/java/com/official/memento/schedule/infrastructure/persistence/ScheduleEntityJpaRepository.java +++ b/src/main/java/com/official/memento/schedule/infrastructure/persistence/ScheduleEntityJpaRepository.java @@ -92,10 +92,26 @@ List findSchedulesByMemberIdAndDateOrderedByOrderNu LocalDate date ); + @Query(""" + SELECT s.id as scheduleId, + s.memberId as memberId, + s.description as description, + s.startDate as startDate, + s.endDate as endDate, + m.wakeUpTime as wakeUpTime, + m.windDownTime as windDownTime, + m.timeZoneOffset as timeZoneOffset + FROM ScheduleEntity s + JOIN MemberPersonalInfoEntity m ON s.memberId = m.memberId + WHERE s.startDate BETWEEN :start AND :end + """) + List findSchedulesWithMemberInfoBetween(final LocalDateTime startTime, final LocalDateTime endTime); + List findAllByMemberIdAndType(long memberId, ScheduleType type); void deleteAllByScheduleGroupId(final String groupId); + @Modifying @Query("UPDATE ScheduleEntity s SET s.tagId = :newTagId WHERE s.tagId = :oldTagId") void updateTagForSchedules(@Param("oldTagId") Long oldTagId, @Param("newTagId") Long newTagId); diff --git a/src/main/java/com/official/memento/schedule/infrastructure/persistence/projection/ScheduleAlarmProjection.java b/src/main/java/com/official/memento/schedule/infrastructure/persistence/projection/ScheduleAlarmProjection.java new file mode 100644 index 00000000..9098a0a7 --- /dev/null +++ b/src/main/java/com/official/memento/schedule/infrastructure/persistence/projection/ScheduleAlarmProjection.java @@ -0,0 +1,14 @@ +package com.official.memento.schedule.infrastructure.persistence.projection; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +public record ScheduleAlarmProjection( + Long scheduleId, + Long memberId, + String description, + LocalDateTime startDate, + LocalDateTime endDate, + Integer timeZoneOffset +) { +} \ No newline at end of file diff --git a/src/main/java/com/official/memento/schedule/scheduler/scheduleAlarmScheduler.java b/src/main/java/com/official/memento/schedule/scheduler/scheduleAlarmScheduler.java new file mode 100644 index 00000000..029d7369 --- /dev/null +++ b/src/main/java/com/official/memento/schedule/scheduler/scheduleAlarmScheduler.java @@ -0,0 +1,37 @@ +package com.official.memento.schedule.scheduler; + +import com.official.memento.global.exception.ErrorCode; +import com.official.memento.global.exception.MementoException; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; +import com.official.memento.schedule.service.CloudTaskAdapter; +import com.official.memento.schedule.service.usecase.ScheduleAlarmGetUseCase; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class scheduleAlarmScheduler { + + private final CloudTaskAdapter cloudTaskAdapter; + + private final ScheduleAlarmGetUseCase scheduleAlarmGetUseCase; + + @Scheduled(cron = "0 15 0 * * *", zone = "UTC") // 매일 UTC 기준 0시 15분 + public void setScheduleAlarm() { + LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC")).plusMinutes(30); + LocalDateTime tomorrow = now.plusDays(1); + List schedules = scheduleAlarmGetUseCase.getSchedulesBetween(now, tomorrow); + try{ + for (ScheduleAlarm schedule : schedules) { + cloudTaskAdapter.createScheduleAlarm(schedule); + } + } catch (Exception e) { + throw new MementoException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/official/memento/schedule/service/CloudTaskAdapter.java b/src/main/java/com/official/memento/schedule/service/CloudTaskAdapter.java new file mode 100644 index 00000000..fcd42bcd --- /dev/null +++ b/src/main/java/com/official/memento/schedule/service/CloudTaskAdapter.java @@ -0,0 +1,88 @@ +package com.official.memento.schedule.service; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.tasks.v2.CloudTasksClient; +import com.google.cloud.tasks.v2.CloudTasksSettings; +import com.google.cloud.tasks.v2.HttpMethod; +import com.google.cloud.tasks.v2.HttpRequest; +import com.google.cloud.tasks.v2.QueueName; +import com.google.cloud.tasks.v2.Task; +import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; +import com.official.memento.global.exception.ErrorCode; +import com.official.memento.global.exception.MementoException; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; +import java.io.FileInputStream; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class CloudTaskAdapter { + + @Value("${GCP.PROJECT_ID}") + private String projectId; + + @Value("${GCP.LOCATION_ID}") + private String locationId; + + @Value("${GCP.QUEUE_ID}") + private String queueId; + + @Value("${GCP.TARGET_URL}") + private String targetUrl; + + @Value("${ADMIN.TOKEN_PREFIX}") + private String AUTHORIZATION_HEADER_ADMIN_PREFIX; + + + public void createScheduleAlarm(final ScheduleAlarm scheduleAlarm) throws IOException { + GoogleCredentials credentials = GoogleCredentials.fromStream( + new FileInputStream(System.getenv("GOOGLE_APPLICATION_CREDENTIALS") + )); + CloudTasksSettings settings = CloudTasksSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)) + .build(); + + LocalDateTime executeTime = scheduleAlarm.getStartDate().minusMinutes(15); + + try (CloudTasksClient client = CloudTasksClient.create(settings)) { + String queuePath = QueueName.of(projectId, locationId, queueId).toString(); + + Instant instant = executeTime.toInstant(ZoneOffset.UTC); + Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(instant.getEpochSecond()) + .setNanos(instant.getNano()) + .build(); + + String payload = "{" + + "\"description\":\"" + scheduleAlarm.getDescription() + "\"," + + "\"memberId\":" + scheduleAlarm.getMemberId() + + "\"startTime\":" + scheduleAlarm.getStartDate().toLocalTime() + + "\"endTime\":" + scheduleAlarm.getEndDate().toLocalTime() + + "}"; + + HttpRequest httpRequest = HttpRequest.newBuilder() + .setUrl(targetUrl) + .setHttpMethod(HttpMethod.POST) + .putHeaders("Content-Type", "application/json") + .putHeaders("Authorization","Bearer " + AUTHORIZATION_HEADER_ADMIN_PREFIX + scheduleAlarm.getMemberId()) + .setBody(ByteString.copyFromUtf8(payload)) + .build(); + + Task task = Task.newBuilder() + .setHttpRequest(httpRequest) + .setScheduleTime(timestamp) + .build(); + + client.createTask(queuePath, task); + }catch (Exception e) { + throw new MementoException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/official/memento/schedule/service/ScheduleAlarmService.java b/src/main/java/com/official/memento/schedule/service/ScheduleAlarmService.java new file mode 100644 index 00000000..7c935740 --- /dev/null +++ b/src/main/java/com/official/memento/schedule/service/ScheduleAlarmService.java @@ -0,0 +1,22 @@ +package com.official.memento.schedule.service; + +import com.official.memento.schedule.domain.ScheduleAlarmRepository; +import com.official.memento.schedule.domain.ScheduleRepository; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; +import com.official.memento.schedule.service.usecase.ScheduleAlarmGetUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ScheduleAlarmService implements ScheduleAlarmGetUseCase { + + private final ScheduleAlarmRepository scheduleAlarmRepository; + @Override + public List getSchedulesBetween(final LocalDateTime startDateTime, final LocalDateTime endDateTime) { + return scheduleAlarmRepository.findSchedulesWithMemberInfoBetween(startDateTime, endDateTime); + } +} diff --git a/src/main/java/com/official/memento/schedule/service/usecase/ScheduleAlarmGetUseCase.java b/src/main/java/com/official/memento/schedule/service/usecase/ScheduleAlarmGetUseCase.java new file mode 100644 index 00000000..59ca59da --- /dev/null +++ b/src/main/java/com/official/memento/schedule/service/usecase/ScheduleAlarmGetUseCase.java @@ -0,0 +1,11 @@ +package com.official.memento.schedule.service.usecase; + +import com.official.memento.schedule.domain.entity.Schedule; +import com.official.memento.schedule.domain.entity.ScheduleAlarm; + +import java.time.LocalDateTime; +import java.util.List; + +public interface ScheduleAlarmGetUseCase { + List getSchedulesBetween(final LocalDateTime startDateTime, final LocalDateTime endDateTime); +} diff --git a/src/main/java/com/official/memento/schedule/service/usecase/ScheduleGetUseCase.java b/src/main/java/com/official/memento/schedule/service/usecase/ScheduleGetUseCase.java index 86e63bef..549cf80e 100644 --- a/src/main/java/com/official/memento/schedule/service/usecase/ScheduleGetUseCase.java +++ b/src/main/java/com/official/memento/schedule/service/usecase/ScheduleGetUseCase.java @@ -13,5 +13,4 @@ public interface ScheduleGetUseCase { Schedule getDetail(final long memberId, final long scheduleId); List getSchedules(final long memberId, final LocalDate date); - } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index dba66c13..9da5eeec 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -36,4 +36,4 @@ discord: google: client: id: ${GOOGLE.CLIENT_ID} - secret: ${GOOGLE.CLIENT_SECRET} \ No newline at end of file + secret: ${GOOGLE.CLIENT_SECRET} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index a0b98ee3..9f5b45eb 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -40,4 +40,4 @@ discord: google: client: id: ${GOOGLE.CLIENT_ID} - secret: ${GOOGLE.CLIENT_SECRET} \ No newline at end of file + secret: ${GOOGLE.CLIENT_SECRET}