diff --git a/api/src/main/java/org/sopt/makers/api/controller/admin/alarm/dto/AdminAlarmResponse.java b/api/src/main/java/org/sopt/makers/api/controller/admin/alarm/dto/AdminAlarmResponse.java index 61c02a76..8c3f1738 100644 --- a/api/src/main/java/org/sopt/makers/api/controller/admin/alarm/dto/AdminAlarmResponse.java +++ b/api/src/main/java/org/sopt/makers/api/controller/admin/alarm/dto/AdminAlarmResponse.java @@ -1,5 +1,9 @@ package org.sopt.makers.api.controller.admin.alarm.dto; +import static org.sopt.makers.core.constant.TimeExpressionConstant.DATE; +import static org.sopt.makers.core.constant.TimeExpressionConstant.DATETIME; +import static org.sopt.makers.core.constant.TimeExpressionConstant.TIME; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import java.time.LocalDateTime; @@ -40,9 +44,6 @@ record AlarmDetail( String linkType) implements AdminAlarmResponse { - private static final String DATE_FORMAT = "yyyy-MM-dd"; - private static final String TIME_FORMAT = "HH:mm"; - public static AlarmDetail from(Alarm alarm) { return new AlarmDetail( alarm.status().getDescription(), @@ -64,11 +65,11 @@ public static AlarmDetail from(Alarm alarm) { } private static String toDate(LocalDateTime dt) { - return dt == null ? null : dt.toLocalDate().format(DateTimeFormatter.ofPattern(DATE_FORMAT)); + return dt == null ? null : dt.toLocalDate().format(DateTimeFormatter.ofPattern(DATE)); } private static String toTime(LocalDateTime dt) { - return dt == null ? null : dt.toLocalTime().format(DateTimeFormatter.ofPattern(TIME_FORMAT)); + return dt == null ? null : dt.toLocalTime().format(DateTimeFormatter.ofPattern(TIME)); } } @@ -91,8 +92,6 @@ record AlarmItem( String title, String content) { - private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm"; - private static AlarmItem from(Alarm alarm) { return new AlarmItem( alarm.id(), @@ -108,7 +107,7 @@ private static AlarmItem from(Alarm alarm) { } private static String toDateTime(LocalDateTime dt) { - return Objects.isNull(dt) ? null : dt.format(DateTimeFormatter.ofPattern(DATETIME_FORMAT)); + return Objects.isNull(dt) ? null : dt.format(DateTimeFormatter.ofPattern(DATETIME)); } } } diff --git a/api/src/main/resources/db/migration/V1__create_initial_schema.sql b/api/src/main/resources/db/migration/V1__create_initial_schema.sql index 076f9fb5..a69ed769 100644 --- a/api/src/main/resources/db/migration/V1__create_initial_schema.sql +++ b/api/src/main/resources/db/migration/V1__create_initial_schema.sql @@ -26,6 +26,8 @@ CREATE TABLE generation dark_mode_text_color VARCHAR(5), light_mode_key_color VARCHAR(7), light_mode_text_color VARCHAR(5), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, PRIMARY KEY (id) ); diff --git a/api/src/main/resources/external.yml b/api/src/main/resources/external.yml index 1ce429d1..87b55832 100644 --- a/api/src/main/resources/external.yml +++ b/api/src/main/resources/external.yml @@ -18,12 +18,10 @@ external: playground: url: ${PLAYGROUND_URL} token: ${PLAYGROUND_TOKEN} - alarm: + push: url: ${ALARM_NOTIFICATION_URL} key: ${ALARM_NOTIFICATION_KEY} arn: ${ALARM_NOTIFICATION_ARN} - header-service: operation - app-header-service: app eventbridge: role-arn: ${AWS_EVENTBRIDGE_ROLE_ARN} region: ${AWS_REGION} diff --git a/api/src/test/resources/application.yml b/api/src/test/resources/application.yml index 63f1e1ff..750aa10d 100644 --- a/api/src/test/resources/application.yml +++ b/api/src/test/resources/application.yml @@ -60,12 +60,10 @@ external: playground: url: http://localhost token: test-playground-token - alarm: + push: url: http://localhost key: test-alarm-key arn: arn:aws:scheduler:ap-northeast-2:000000000000:schedule/test - header-service: test - app-header-service: test-app eventbridge: role-arn: arn:aws:iam::000000000000:role/test region: ap-northeast-2 diff --git a/clients/src/main/java/org/sopt/makers/clients/alarm/AlarmInstantSenderAdapter.java b/clients/src/main/java/org/sopt/makers/clients/alarm/AlarmInstantSenderAdapter.java deleted file mode 100644 index 09660632..00000000 --- a/clients/src/main/java/org/sopt/makers/clients/alarm/AlarmInstantSenderAdapter.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.sopt.makers.clients.alarm; - -import java.util.HashMap; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.sopt.makers.clients.notification.NotificationHttpClient; -import org.sopt.makers.domain.admin.alarm.Alarm; -import org.sopt.makers.domain.admin.alarm.AlarmLinkType; -import org.sopt.makers.domain.admin.alarm.AlarmTargetType; -import org.sopt.makers.domain.admin.alarm.exception.AlarmException; -import org.sopt.makers.domain.admin.alarm.exception.AlarmFailure; -import org.sopt.makers.domain.admin.alarm.port.AlarmInstantSenderPort; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClientException; - -@Component -@RequiredArgsConstructor -public class AlarmInstantSenderAdapter implements AlarmInstantSenderPort { - - private final NotificationHttpClient notificationHttpClient; - private final AlarmProperty alarmProperty; - - @Override - public void send(Alarm alarm) { - try { - Map body = buildBody(alarm); - notificationHttpClient.send( - alarmProperty.headerService(), alarm.target().sendAction().getValue(), body); - } catch (RestClientException e) { - throw new AlarmException(AlarmFailure.FAIL_SEND_ALARM); - } - } - - private Map buildBody(Alarm alarm) { - Map body = new HashMap<>(); - body.put("title", alarm.content().title()); - body.put("content", alarm.content().content()); - body.put("category", alarm.content().category()); - - boolean isTargetAll = AlarmTargetType.ALL.equals(alarm.target().targetType()); - if (!isTargetAll) { - body.put("userIds", alarm.target().targetIds()); - } - - AlarmLinkType linkType = alarm.content().linkType(); - if (AlarmLinkType.WEB.equals(linkType)) { - body.put("webLink", alarm.content().linkPath()); - } else if (AlarmLinkType.APP.equals(linkType)) { - body.put("deepLink", alarm.content().linkPath()); - } - return body; - } -} diff --git a/clients/src/main/java/org/sopt/makers/clients/alarm/AlarmProperty.java b/clients/src/main/java/org/sopt/makers/clients/alarm/AlarmProperty.java deleted file mode 100644 index 9345be77..00000000 --- a/clients/src/main/java/org/sopt/makers/clients/alarm/AlarmProperty.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.sopt.makers.clients.alarm; - -import jakarta.validation.constraints.NotBlank; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -@Validated -@ConfigurationProperties(prefix = "external.alarm") -public record AlarmProperty( - @NotBlank String url, - @NotBlank String key, - @NotBlank String arn, - @NotBlank String headerService, - @NotBlank String appHeaderService) {} diff --git a/clients/src/main/java/org/sopt/makers/clients/config/ClientsConfig.java b/clients/src/main/java/org/sopt/makers/clients/config/ClientsConfig.java index 7b340688..90f66132 100644 --- a/clients/src/main/java/org/sopt/makers/clients/config/ClientsConfig.java +++ b/clients/src/main/java/org/sopt/makers/clients/config/ClientsConfig.java @@ -1,7 +1,7 @@ package org.sopt.makers.clients.config; -import org.sopt.makers.clients.alarm.AlarmProperty; import org.sopt.makers.clients.eventbridge.EventBridgeProperty; +import org.sopt.makers.clients.push.PushProperty; import org.sopt.makers.clients.s3.S3Property; import org.sopt.makers.clients.slack.SlackProperties; import org.sopt.makers.clients.sms.GabiaSmsProperty; @@ -15,7 +15,7 @@ OAuthProperty.class, GabiaSmsProperty.class, S3Property.class, - AlarmProperty.class, + PushProperty.class, EventBridgeProperty.class, SlackProperties.class }) diff --git a/clients/src/main/java/org/sopt/makers/clients/eventbridge/.gitkeep b/clients/src/main/java/org/sopt/makers/clients/eventbridge/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleDeleterAdapter.java b/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleDeleterAdapter.java index bbdc38ab..f6f09908 100644 --- a/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleDeleterAdapter.java +++ b/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleDeleterAdapter.java @@ -1,5 +1,8 @@ package org.sopt.makers.clients.eventbridge; +import static org.sopt.makers.core.constant.TimeExpressionConstant.DATE; +import static org.sopt.makers.core.constant.TimeExpressionConstant.FILE_SAFE_TIME; + import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import lombok.RequiredArgsConstructor; @@ -14,9 +17,6 @@ @RequiredArgsConstructor public class AlarmScheduleDeleterAdapter implements AlarmScheduleDeleterPort { - private static final String DATE_FORMAT = "yyyy-MM-dd"; - private static final String SCHEDULE_TIME_FORMAT = "HH-mm"; - private final SchedulerClient schedulerClient; @Override @@ -30,10 +30,9 @@ public void delete(long alarmId, LocalDateTime scheduleDateTime) { } private String buildEventName(long alarmId, LocalDateTime scheduleDateTime) { - String dateData = - scheduleDateTime.toLocalDate().format(DateTimeFormatter.ofPattern(DATE_FORMAT)); + String dateData = scheduleDateTime.toLocalDate().format(DateTimeFormatter.ofPattern(DATE)); String timeData = - scheduleDateTime.toLocalTime().format(DateTimeFormatter.ofPattern(SCHEDULE_TIME_FORMAT)); + scheduleDateTime.toLocalTime().format(DateTimeFormatter.ofPattern(FILE_SAFE_TIME)); return String.format("%s_%s_%d", dateData, timeData, alarmId); } } diff --git a/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleSenderAdapter.java b/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleSenderAdapter.java index 18b81362..cb7a9d0a 100644 --- a/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleSenderAdapter.java +++ b/clients/src/main/java/org/sopt/makers/clients/eventbridge/AlarmScheduleSenderAdapter.java @@ -1,13 +1,17 @@ package org.sopt.makers.clients.eventbridge; +import static org.sopt.makers.core.constant.TimeExpressionConstant.DATE; +import static org.sopt.makers.core.constant.TimeExpressionConstant.FILE_SAFE_TIME; + import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.sopt.makers.clients.alarm.AlarmProperty; import org.sopt.makers.clients.eventbridge.dto.AlarmScheduleEventBridgeBody; import org.sopt.makers.clients.eventbridge.dto.AlarmScheduleEventBridgeHeader; import org.sopt.makers.clients.eventbridge.dto.AlarmScheduleEventBridgeRequest; +import org.sopt.makers.clients.push.PushProperty; +import org.sopt.makers.core.type.ServiceType; import org.sopt.makers.domain.admin.alarm.Alarm; import org.sopt.makers.domain.admin.alarm.AlarmLinkType; import org.sopt.makers.domain.admin.alarm.exception.AlarmException; @@ -26,12 +30,9 @@ @RequiredArgsConstructor public class AlarmScheduleSenderAdapter implements AlarmScheduleSenderPort { - private static final String DATE_FORMAT = "yyyy-MM-dd"; - private static final String SCHEDULE_TIME_FORMAT = "HH-mm"; - private final SchedulerClient schedulerClient; private final ObjectMapper objectMapper; - private final AlarmProperty alarmProperty; + private final PushProperty pushProperty; private final EventBridgeProperty eventBridgeProperty; @Override @@ -56,10 +57,9 @@ public void send(Alarm alarm) { } private String buildEventName(Alarm alarm) { - String dateData = - alarm.intendedAt().toLocalDate().format(DateTimeFormatter.ofPattern(DATE_FORMAT)); + String dateData = alarm.intendedAt().toLocalDate().format(DateTimeFormatter.ofPattern(DATE)); String timeData = - alarm.intendedAt().toLocalTime().format(DateTimeFormatter.ofPattern(SCHEDULE_TIME_FORMAT)); + alarm.intendedAt().toLocalTime().format(DateTimeFormatter.ofPattern(FILE_SAFE_TIME)); return String.format("%s_%s_%d", dateData, timeData, alarm.id()); } @@ -76,9 +76,9 @@ private String buildEventJson(Alarm alarm) throws JacksonException { AlarmScheduleEventBridgeHeader.builder() .alarmId(alarm.id()) .action(alarm.target().sendAction().getValue()) - .xApiKey(alarmProperty.key()) + .xApiKey(pushProperty.key()) .transactionId(UUID.randomUUID().toString()) - .service(alarmProperty.headerService()) + .service(ServiceType.ADMIN.getValue()) .build(); boolean isAppLink = AlarmLinkType.APP.equals(alarm.content().linkType()); @@ -100,7 +100,7 @@ private String buildEventJson(Alarm alarm) throws JacksonException { private Target buildTarget(String eventJson) { return Target.builder() .roleArn(eventBridgeProperty.roleArn()) - .arn(alarmProperty.arn()) + .arn(pushProperty.arn()) .input(eventJson) .build(); } diff --git a/clients/src/main/java/org/sopt/makers/clients/notification/MeetingDemandNotificationSenderAdapter.java b/clients/src/main/java/org/sopt/makers/clients/notification/MeetingDemandNotificationSenderAdapter.java deleted file mode 100644 index 9b1cf333..00000000 --- a/clients/src/main/java/org/sopt/makers/clients/notification/MeetingDemandNotificationSenderAdapter.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.sopt.makers.clients.notification; - -import java.util.HashMap; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.sopt.makers.domain.crew.meeting.demand.notification.MeetingDemandNotification; -import org.sopt.makers.domain.crew.meeting.demand.port.MeetingDemandNotificationSenderPort; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class MeetingDemandNotificationSenderAdapter implements MeetingDemandNotificationSenderPort { - - private static final String SEND_ACTION = "send"; - private static final String SERVICE_NAME = "crew"; - private static final String DEV_WEB_URL = "https://sopt-internal-dev.sopt.org/group"; - private static final String PROD_WEB_URL = "https://playground.sopt.org/group"; - - private final NotificationHttpClient notificationHttpClient; - private final Environment environment; - - @Override - public void send(MeetingDemandNotification notification) { - notificationHttpClient.send(SERVICE_NAME, SEND_ACTION, buildBody(notification)); - } - - private Map buildBody(MeetingDemandNotification notification) { - Map body = new HashMap<>(); - body.put("userIds", notification.userIds().stream().map(String::valueOf).toList()); - body.put("title", notification.title()); - body.put("content", notification.content()); - body.put("category", notification.category()); - body.put("webLink", getWebUrl() + notification.webPath()); - return body; - } - - private String getWebUrl() { - return environment.matchesProfiles("prod") ? PROD_WEB_URL : DEV_WEB_URL; - } -} diff --git a/clients/src/main/java/org/sopt/makers/clients/notification/MeetingNotificationSenderAdapter.java b/clients/src/main/java/org/sopt/makers/clients/notification/MeetingNotificationSenderAdapter.java deleted file mode 100644 index 2e0882bc..00000000 --- a/clients/src/main/java/org/sopt/makers/clients/notification/MeetingNotificationSenderAdapter.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.sopt.makers.clients.notification; - -import static org.sopt.makers.domain.crew.notification.exception.MeetingNotificationFailure.FAIL_SEND_MEETING_NOTIFICATION; - -import java.util.HashMap; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.sopt.makers.domain.crew.notification.MeetingNotification; -import org.sopt.makers.domain.crew.notification.exception.MeetingNotificationException; -import org.sopt.makers.domain.crew.notification.port.MeetingNotificationSenderPort; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClientException; - -@Component -@RequiredArgsConstructor -public class MeetingNotificationSenderAdapter implements MeetingNotificationSenderPort { - - private static final String SEND_ACTION = "send"; - private static final String SERVICE_NAME = "crew"; - private static final String DEV_WEB_URL = "https://sopt-internal-dev.sopt.org/group"; - private static final String PROD_WEB_URL = "https://playground.sopt.org/group"; - - private final NotificationHttpClient notificationHttpClient; - private final Environment environment; - - @Override - public void send(MeetingNotification notification) { - try { - notificationHttpClient.send(SERVICE_NAME, SEND_ACTION, buildBody(notification)); - } catch (RestClientException e) { - throw new MeetingNotificationException(FAIL_SEND_MEETING_NOTIFICATION); - } - } - - private Map buildBody(MeetingNotification notification) { - Map body = new HashMap<>(); - body.put("userIds", notification.userIds().stream().map(String::valueOf).toList()); - body.put("title", notification.title()); - body.put("content", notification.content()); - body.put("category", notification.category()); - body.put("webLink", getWebUrl() + "/detail?id=" + notification.meetingId()); - return body; - } - - private String getWebUrl() { - return environment.matchesProfiles("prod") ? PROD_WEB_URL : DEV_WEB_URL; - } -} diff --git a/clients/src/main/java/org/sopt/makers/clients/push/PushClientAdapter.java b/clients/src/main/java/org/sopt/makers/clients/push/PushClientAdapter.java new file mode 100644 index 00000000..59072976 --- /dev/null +++ b/clients/src/main/java/org/sopt/makers/clients/push/PushClientAdapter.java @@ -0,0 +1,116 @@ +package org.sopt.makers.clients.push; + +import static org.sopt.makers.domain.crew.notification.exception.MeetingNotificationFailure.FAIL_SEND_MEETING_NOTIFICATION; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sopt.makers.clients.push.dto.PushSendRequest; +import org.sopt.makers.clients.push.dto.PushTokenManageRequest; +import org.sopt.makers.core.type.ServiceType; +import org.sopt.makers.domain.admin.alarm.Alarm; +import org.sopt.makers.domain.admin.alarm.exception.AlarmException; +import org.sopt.makers.domain.admin.alarm.exception.AlarmFailure; +import org.sopt.makers.domain.admin.alarm.port.AlarmInstantSenderPort; +import org.sopt.makers.domain.app.push.PushMessage; +import org.sopt.makers.domain.app.push.PushToken; +import org.sopt.makers.domain.app.push.PushTokenPlatform; +import org.sopt.makers.domain.app.push.exception.PushException; +import org.sopt.makers.domain.app.push.exception.PushFailure; +import org.sopt.makers.domain.app.push.port.PushSenderPort; +import org.sopt.makers.domain.crew.meeting.demand.notification.MeetingDemandNotification; +import org.sopt.makers.domain.crew.meeting.demand.port.MeetingDemandNotificationSenderPort; +import org.sopt.makers.domain.crew.notification.MeetingNotification; +import org.sopt.makers.domain.crew.notification.exception.MeetingNotificationException; +import org.sopt.makers.domain.crew.notification.port.MeetingNotificationSenderPort; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PushClientAdapter + implements AlarmInstantSenderPort, + PushSenderPort, + MeetingNotificationSenderPort, + MeetingDemandNotificationSenderPort { + + private static final String ACTION_SEND = "send"; + private static final String ACTION_REGISTER = "register"; + private static final String ACTION_DELETE = "cancel"; + private static final String DEV_CREW_WEB_URL = "https://sopt-internal-dev.sopt.org/group"; + private static final String PROD_CREW_WEB_URL = "https://playground.sopt.org/group"; + + private final PushHttpClient pushHttpClient; + private final Environment environment; + + @Override + public void send(Alarm alarm) { + try { + pushHttpClient.send( + ServiceType.ADMIN, alarm.target().sendAction().getValue(), PushSendRequest.from(alarm)); + } catch (RestClientException e) { + throw new AlarmException(AlarmFailure.FAIL_SEND_ALARM); + } + } + + @Override + public void send(PushMessage message) { + if (message.userIds().isEmpty()) { + return; + } + try { + pushHttpClient.send(ServiceType.APP, ACTION_SEND, PushSendRequest.from(message)); + } catch (RestClientException e) { + log.warn("푸시 발송 실패 - title={}, 대상 {}명", message.title(), message.userIds().size(), e); + throw new PushException(PushFailure.FAIL_SEND_PUSH); + } + } + + @Override + public void register(PushToken pushToken) { + manageToken(ACTION_REGISTER, pushToken); + } + + @Override + public void delete(PushToken pushToken) { + manageToken(ACTION_DELETE, pushToken); + } + + @Override + public void send(MeetingNotification notification) { + try { + pushHttpClient.send( + ServiceType.CREW, ACTION_SEND, PushSendRequest.from(notification, getCrewWebUrl())); + } catch (RestClientException e) { + throw new MeetingNotificationException(FAIL_SEND_MEETING_NOTIFICATION); + } + } + + @Override + public void send(MeetingDemandNotification notification) { + pushHttpClient.send( + ServiceType.CREW, ACTION_SEND, PushSendRequest.from(notification, getCrewWebUrl())); + } + + private void manageToken(String action, PushToken pushToken) { + try { + pushHttpClient.send( + ServiceType.APP, + action, + PushTokenManageRequest.from(pushToken), + toPlatformHeader(pushToken.platform())); + } catch (RestClientException e) { + log.warn("푸시 토큰 {} 실패 - userId={}", action, pushToken.userId(), e); + throw new PushException(PushFailure.FAIL_MANAGE_PUSH_TOKEN); + } + } + + private String toPlatformHeader(PushTokenPlatform platform) { + return platform == PushTokenPlatform.IOS ? "iOS" : "Android"; + } + + private String getCrewWebUrl() { + return environment.matchesProfiles("prod") ? PROD_CREW_WEB_URL : DEV_CREW_WEB_URL; + } +} diff --git a/clients/src/main/java/org/sopt/makers/clients/notification/NotificationHttpClient.java b/clients/src/main/java/org/sopt/makers/clients/push/PushHttpClient.java similarity index 59% rename from clients/src/main/java/org/sopt/makers/clients/notification/NotificationHttpClient.java rename to clients/src/main/java/org/sopt/makers/clients/push/PushHttpClient.java index 4ed5c68e..0d82d4b2 100644 --- a/clients/src/main/java/org/sopt/makers/clients/notification/NotificationHttpClient.java +++ b/clients/src/main/java/org/sopt/makers/clients/push/PushHttpClient.java @@ -1,10 +1,10 @@ -package org.sopt.makers.clients.notification; +package org.sopt.makers.clients.push; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.sopt.makers.clients.alarm.AlarmProperty; +import org.sopt.makers.core.type.ServiceType; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -13,30 +13,30 @@ @Component @RequiredArgsConstructor -public class NotificationHttpClient { +public class PushHttpClient { private final RestTemplate restTemplate; - private final AlarmProperty alarmProperty; + private final PushProperty pushProperty; - public void send(String serviceName, String action, Object body) { - send(serviceName, action, body, null); + public void send(ServiceType serviceType, String action, Object body) { + send(serviceType, action, body, null); } - public void send(String serviceName, String action, Object body, String platform) { + public void send(ServiceType serviceType, String action, Object body, String platform) { restTemplate.postForEntity( - alarmProperty.url(), - new HttpEntity<>(body, buildHeaders(serviceName, action, platform)), + pushProperty.url(), + new HttpEntity<>(body, buildHeaders(serviceType, action, platform)), Object.class); } - private HttpHeaders buildHeaders(String serviceName, String action, String platform) { + private HttpHeaders buildHeaders(ServiceType serviceType, String action, String platform) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8)); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - headers.add("x-api-key", alarmProperty.key()); + headers.add("x-api-key", pushProperty.key()); headers.add("action", action); headers.add("transactionId", UUID.randomUUID().toString()); - headers.add("service", serviceName); + headers.add("service", serviceType.getValue()); if (platform != null) { headers.add("platform", platform); } diff --git a/clients/src/main/java/org/sopt/makers/clients/push/PushProperty.java b/clients/src/main/java/org/sopt/makers/clients/push/PushProperty.java new file mode 100644 index 00000000..2c30b26b --- /dev/null +++ b/clients/src/main/java/org/sopt/makers/clients/push/PushProperty.java @@ -0,0 +1,9 @@ +package org.sopt.makers.clients.push; + +import jakarta.validation.constraints.NotBlank; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@Validated +@ConfigurationProperties(prefix = "external.push") +public record PushProperty(@NotBlank String url, @NotBlank String key, @NotBlank String arn) {} diff --git a/clients/src/main/java/org/sopt/makers/clients/push/PushSenderAdapter.java b/clients/src/main/java/org/sopt/makers/clients/push/PushSenderAdapter.java deleted file mode 100644 index d9f8bb1a..00000000 --- a/clients/src/main/java/org/sopt/makers/clients/push/PushSenderAdapter.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.sopt.makers.clients.push; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.sopt.makers.clients.alarm.AlarmProperty; -import org.sopt.makers.clients.notification.NotificationHttpClient; -import org.sopt.makers.clients.push.dto.PushSendRequest; -import org.sopt.makers.clients.push.dto.PushTokenManageRequest; -import org.sopt.makers.domain.app.push.PushMessage; -import org.sopt.makers.domain.app.push.PushToken; -import org.sopt.makers.domain.app.push.PushTokenPlatform; -import org.sopt.makers.domain.app.push.exception.PushException; -import org.sopt.makers.domain.app.push.exception.PushFailure; -import org.sopt.makers.domain.app.push.port.PushSenderPort; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClientException; - -@Slf4j -@Component -@RequiredArgsConstructor -public class PushSenderAdapter implements PushSenderPort { - - private static final String ACTION_SEND = "send"; - private static final String ACTION_REGISTER = "register"; - private static final String ACTION_DELETE = "cancel"; - - private final NotificationHttpClient notificationHttpClient; - private final AlarmProperty property; - - @Override - public void send(PushMessage message) { - if (message.userIds().isEmpty()) { - return; - } - try { - notificationHttpClient.send( - property.appHeaderService(), ACTION_SEND, PushSendRequest.from(message)); - } catch (RestClientException e) { - log.warn("푸시 발송 실패 - title={}, 대상 {}명", message.title(), message.userIds().size(), e); - throw new PushException(PushFailure.FAIL_SEND_PUSH); - } - } - - @Override - public void register(PushToken pushToken) { - manageToken(ACTION_REGISTER, pushToken); - } - - @Override - public void delete(PushToken pushToken) { - manageToken(ACTION_DELETE, pushToken); - } - - private void manageToken(String action, PushToken pushToken) { - try { - notificationHttpClient.send( - property.appHeaderService(), - action, - PushTokenManageRequest.from(pushToken), - toPlatformHeader(pushToken.platform())); - } catch (RestClientException e) { - log.warn("푸시 토큰 {} 실패 - userId={}", action, pushToken.userId(), e); - throw new PushException(PushFailure.FAIL_MANAGE_PUSH_TOKEN); - } - } - - private String toPlatformHeader(PushTokenPlatform platform) { - return platform == PushTokenPlatform.IOS ? "iOS" : "Android"; - } -} diff --git a/clients/src/main/java/org/sopt/makers/clients/push/dto/PushSendRequest.java b/clients/src/main/java/org/sopt/makers/clients/push/dto/PushSendRequest.java index e929a2ef..ec8b4e76 100644 --- a/clients/src/main/java/org/sopt/makers/clients/push/dto/PushSendRequest.java +++ b/clients/src/main/java/org/sopt/makers/clients/push/dto/PushSendRequest.java @@ -1,12 +1,18 @@ package org.sopt.makers.clients.push.dto; import com.fasterxml.jackson.annotation.JsonInclude; -import java.util.Set; +import java.util.Collection; +import java.util.List; +import org.sopt.makers.domain.admin.alarm.Alarm; +import org.sopt.makers.domain.admin.alarm.AlarmLinkType; +import org.sopt.makers.domain.admin.alarm.AlarmTargetType; import org.sopt.makers.domain.app.push.PushMessage; +import org.sopt.makers.domain.crew.meeting.demand.notification.MeetingDemandNotification; +import org.sopt.makers.domain.crew.notification.MeetingNotification; @JsonInclude(JsonInclude.Include.NON_NULL) public record PushSendRequest( - Set userIds, + Collection userIds, String title, String content, String category, @@ -24,4 +30,41 @@ public static PushSendRequest from(PushMessage message) { message.deepLink(), message.webLink()); } + + public static PushSendRequest from(Alarm alarm) { + boolean isTargetAll = AlarmTargetType.ALL.equals(alarm.target().targetType()); + boolean isAppLink = AlarmLinkType.APP.equals(alarm.content().linkType()); + boolean isWebLink = AlarmLinkType.WEB.equals(alarm.content().linkType()); + return new PushSendRequest( + isTargetAll ? null : alarm.target().targetIds(), + alarm.content().title(), + alarm.content().content(), + alarm.content().category().name(), + isAppLink ? alarm.content().linkPath() : null, + isWebLink ? alarm.content().linkPath() : null); + } + + public static PushSendRequest from(MeetingNotification notification, String crewWebUrl) { + return new PushSendRequest( + toUserIds(notification.userIds()), + notification.title(), + notification.content(), + notification.category(), + null, + crewWebUrl + "/detail?id=" + notification.meetingId()); + } + + public static PushSendRequest from(MeetingDemandNotification notification, String crewWebUrl) { + return new PushSendRequest( + toUserIds(notification.userIds()), + notification.title(), + notification.content(), + notification.category(), + null, + crewWebUrl + notification.webPath()); + } + + private static List toUserIds(List userIds) { + return userIds.stream().map(String::valueOf).toList(); + } } diff --git a/clients/src/test/java/org/sopt/makers/clients/push/PushSenderAdapterTest.java b/clients/src/test/java/org/sopt/makers/clients/push/PushClientAdapterTest.java similarity index 90% rename from clients/src/test/java/org/sopt/makers/clients/push/PushSenderAdapterTest.java rename to clients/src/test/java/org/sopt/makers/clients/push/PushClientAdapterTest.java index afd0e88a..fa621a7f 100644 --- a/clients/src/test/java/org/sopt/makers/clients/push/PushSenderAdapterTest.java +++ b/clients/src/test/java/org/sopt/makers/clients/push/PushClientAdapterTest.java @@ -11,30 +11,29 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.sopt.makers.clients.alarm.AlarmProperty; -import org.sopt.makers.clients.notification.NotificationHttpClient; import org.sopt.makers.domain.app.notification.NotificationCategory; import org.sopt.makers.domain.app.push.PushMessage; import org.sopt.makers.domain.app.push.PushToken; import org.sopt.makers.domain.app.push.PushTokenPlatform; import org.sopt.makers.domain.app.push.exception.PushException; +import org.springframework.core.env.StandardEnvironment; import org.springframework.web.client.RestTemplate; -@DisplayName("PushSenderAdapter 테스트") -class PushSenderAdapterTest { +@DisplayName("PushClientAdapter 테스트") +class PushClientAdapterTest { private MockWebServer pushServer; - private PushSenderAdapter adapter; + private PushClientAdapter adapter; @BeforeEach void setUp() throws Exception { pushServer = new MockWebServer(); pushServer.start(); String baseUrl = pushServer.url("/").toString().replaceAll("/$", ""); - AlarmProperty property = - new AlarmProperty(baseUrl, "test-push-key", "test-arn", "operation", "app"); + PushProperty property = new PushProperty(baseUrl, "test-push-key", "test-arn"); adapter = - new PushSenderAdapter(new NotificationHttpClient(new RestTemplate(), property), property); + new PushClientAdapter( + new PushHttpClient(new RestTemplate(), property), new StandardEnvironment()); } @AfterEach diff --git a/core/src/main/java/org/sopt/makers/core/constant/TimeExpressionConstant.java b/core/src/main/java/org/sopt/makers/core/constant/TimeExpressionConstant.java index d7ca143a..97512abe 100644 --- a/core/src/main/java/org/sopt/makers/core/constant/TimeExpressionConstant.java +++ b/core/src/main/java/org/sopt/makers/core/constant/TimeExpressionConstant.java @@ -4,5 +4,9 @@ public final class TimeExpressionConstant { private TimeExpressionConstant() {} + public static final String DATE = "yyyy-MM-dd"; + public static final String TIME = "HH:mm"; + public static final String FILE_SAFE_TIME = "HH-mm"; + public static final String DATETIME = DATE + " " + TIME; public static final String YEAR_MONTH = "yyyy-MM"; } diff --git a/core/src/main/java/org/sopt/makers/core/type/ServiceType.java b/core/src/main/java/org/sopt/makers/core/type/ServiceType.java new file mode 100644 index 00000000..fb3fde33 --- /dev/null +++ b/core/src/main/java/org/sopt/makers/core/type/ServiceType.java @@ -0,0 +1,17 @@ +package org.sopt.makers.core.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ServiceType { + AUTH("auth"), + OFFICIAL("official"), + ADMIN("admin"), + APP("app"), + PLAYGROUND("playground"), + CREW("crew"); + + private final String value; +} diff --git a/domain/domain-admin/src/main/java/org/sopt/makers/domain/admin/alarm/service/AlarmService.java b/domain/domain-admin/src/main/java/org/sopt/makers/domain/admin/alarm/service/AlarmService.java index aeec1bcf..d15cf43f 100644 --- a/domain/domain-admin/src/main/java/org/sopt/makers/domain/admin/alarm/service/AlarmService.java +++ b/domain/domain-admin/src/main/java/org/sopt/makers/domain/admin/alarm/service/AlarmService.java @@ -1,5 +1,9 @@ package org.sopt.makers.domain.admin.alarm.service; +import static org.sopt.makers.core.constant.TimeExpressionConstant.DATE; +import static org.sopt.makers.core.constant.TimeExpressionConstant.DATETIME; +import static org.sopt.makers.core.constant.TimeExpressionConstant.TIME; + import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -31,10 +35,6 @@ @Transactional(readOnly = true) public class AlarmService { - private static final String DATE_FORMAT = "yyyy-MM-dd"; - private static final String TIME_FORMAT = "HH:mm"; - private static final String DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT; - private final AlarmRepositoryPort alarmRepositoryPort; private final AlarmMemberQueryPort alarmMemberQueryPort; private final AlarmInstantSenderPort alarmInstantSenderPort; @@ -103,7 +103,7 @@ public void updateAlarmStatus(long alarmId, String sendAt) { .findById(alarmId) .orElseThrow(() -> new AlarmException(AlarmFailure.NOT_FOUND_ALARM)); LocalDateTime sendAtDateTime = - LocalDateTime.parse(sendAt, DateTimeFormatter.ofPattern(DATETIME_FORMAT)); + LocalDateTime.parse(sendAt, DateTimeFormatter.ofPattern(DATETIME)); alarmRepositoryPort.save(alarm.complete(sendAtDateTime)); } @@ -131,8 +131,8 @@ private List extractTargetIds(AlarmTarget target) { private LocalDateTime parseDateTime(String date, String time) { try { - LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE_FORMAT)); - LocalTime localTime = LocalTime.parse(time, DateTimeFormatter.ofPattern(TIME_FORMAT)); + LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ofPattern(DATE)); + LocalTime localTime = LocalTime.parse(time, DateTimeFormatter.ofPattern(TIME)); return LocalDateTime.of(localDate, localTime); } catch (Exception e) { throw new AlarmException(AlarmFailure.INVALID_SCHEDULE_ALARM_FORMAT); diff --git a/storage/src/main/java/org/sopt/makers/storage/db/official/entity/GenerationEntity.java b/storage/src/main/java/org/sopt/makers/storage/db/official/entity/GenerationEntity.java index 12795aa8..494881ee 100644 --- a/storage/src/main/java/org/sopt/makers/storage/db/official/entity/GenerationEntity.java +++ b/storage/src/main/java/org/sopt/makers/storage/db/official/entity/GenerationEntity.java @@ -11,12 +11,13 @@ import lombok.NoArgsConstructor; import org.sopt.makers.domain.official.generation.BrandingColor; import org.sopt.makers.domain.official.generation.Generation; +import org.sopt.makers.storage.db.common.BaseEntity; @Entity @Getter @NoArgsConstructor(access = PROTECTED) @Table(name = "generation") -public class GenerationEntity { +public class GenerationEntity extends BaseEntity { @Id @Column(name = "id", nullable = false)