Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6215ac5
chore: 임시 커밋
paragon0107 Apr 1, 2025
11b6b01
Merge branch 'feat/#61-schedule-alarm' of https://github.com/dev-meme…
paragon0107 Apr 1, 2025
db6f17d
chore: 스케줄 엔티티 스케줄링 관련 조회 메서드 네이밍 변경
paragon0107 Apr 2, 2025
4cb50bd
chore: 스케줄 엔티티 스케줄링 관련 조회 프로젝션 추가
paragon0107 Apr 2, 2025
a008577
chore: 스케줄 알람 시간 추가
paragon0107 Apr 2, 2025
f2a582c
feat: querydsl 추가
paragon0107 Apr 5, 2025
9b0e841
feat: querydsl 설정
paragon0107 Apr 5, 2025
6e8fa80
feat: ScheduleAlarmCustomRepository 부분 구현
paragon0107 Apr 5, 2025
be5f723
feat: ScheduleAlarmCustomRepository 부분 구현
paragon0107 Apr 5, 2025
2356c1d
feat: ScheduleAlarmRepository 구현
paragon0107 Apr 5, 2025
d34aa77
feat: ScheduleAlarmService 구현
paragon0107 Apr 5, 2025
b983425
feat: ScheduleAlarmService 분리
paragon0107 Apr 5, 2025
b002e86
feat: ScheduleAlarm 분리
paragon0107 Apr 5, 2025
633250e
feat: ScheduleAlarm 도메인 추가
paragon0107 Apr 5, 2025
8d70a4a
feat: ScheduleAlarm 스케줄러및 클라우드 어댑터 구현
paragon0107 Apr 5, 2025
8aa50c0
chore: 컨벤션 수정
paragon0107 Apr 5, 2025
0e68b63
chore: 스케줄 시간 반영
paragon0107 Apr 5, 2025
8278cce
chore: 컨벤션 수정
paragon0107 Apr 5, 2025
c6ff1de
chore: scheduleAlarmRepository 분리
paragon0107 Apr 5, 2025
b24530e
chore: scheduleAlarmService 수정
paragon0107 Apr 5, 2025
22d1f36
feat: cloudTask 설정 추가
paragon0107 Apr 6, 2025
d69152d
chore: 임시 pr
paragon0107 Apr 6, 2025
0827fcb
chore: 테스트용 코드 삭제
paragon0107 Apr 6, 2025
9bc0c48
chore: 클라우드 태스크 토큰 부분 추가
paragon0107 Apr 6, 2025
8e46197
feat: 파일 read 예외 처리
paragon0107 Apr 6, 2025
d0515ef
chore: 주석 수정
paragon0107 Apr 6, 2025
113c095
chore: 바디 부분에 시간 추가
paragon0107 Apr 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ out/

application-secret.yml
braindump-prompt.txt
prioritize-prompt.txt
prioritize-prompt.txt
cloud-task-key.json
10 changes: 10 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment on lines +84 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryDSL은 현재 OpenFeign에서 fork해서 계속 프로젝트 업데이트를 진행하고 있습니다~

https://github.com/OpenFeign/querydsl

해당 의존성을 사용하면 좋을 것 같아요.

}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ScheduleAlarm> findSchedulesWithMemberInfoBetween(final LocalDateTime startTime, final LocalDateTime endTime);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
@@ -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<ScheduleAlarm> findSchedulesWithMemberInfoBetween(final LocalDateTime startTime,
final LocalDateTime endTime) {
List<ScheduleAlarmProjection> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ScheduleAlarmProjection> findSchedulesWithMemberInfoBetween(
final LocalDateTime startDate,
final LocalDateTime endDate
) {
List<Tuple> 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());
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,26 @@ List<ScheduleOrderInfoProjection> 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<ScheduleEntity> findSchedulesWithMemberInfoBetween(final LocalDateTime startTime, final LocalDateTime endTime);

List<ScheduleEntity> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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<ScheduleAlarm> schedules = scheduleAlarmGetUseCase.getSchedulesBetween(now, tomorrow);
try{
for (ScheduleAlarm schedule : schedules) {
cloudTaskAdapter.createScheduleAlarm(schedule);
}
} catch (Exception e) {
throw new MementoException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -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")
));
Comment on lines +45 to +47
Copy link

Copilot AI Apr 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using try-with-resources to safely manage the FileInputStream for GoogleCredentials, which will prevent potential resource leaks.

Suggested change
GoogleCredentials credentials = GoogleCredentials.fromStream(
new FileInputStream(System.getenv("GOOGLE_APPLICATION_CREDENTIALS")
));
GoogleCredentials credentials;
try (FileInputStream fis = new FileInputStream(System.getenv("GOOGLE_APPLICATION_CREDENTIALS"))) {
credentials = GoogleCredentials.fromStream(fis);
}

Copilot uses AI. Check for mistakes.
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);
}
}
}
Loading