diff --git a/backend/build.gradle b/backend/build.gradle index 7c379620a..d6fa2f7b2 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -62,8 +62,8 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok' - implementation "org.flywaydb:flyway-mysql" - implementation "org.flywaydb:flyway-core" + implementation 'org.flywaydb:flyway-mysql' + implementation 'org.flywaydb:flyway-core' } tasks.named('test') { diff --git a/backend/src/main/java/com/ody/common/config/FlywayConfig.java b/backend/src/main/java/com/ody/common/config/FlywayConfig.java new file mode 100644 index 000000000..7d9a54bad --- /dev/null +++ b/backend/src/main/java/com/ody/common/config/FlywayConfig.java @@ -0,0 +1,17 @@ +package com.ody.common.config; + +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FlywayConfig { + + @Bean + public FlywayMigrationStrategy repairMigrateStrategy() { + return flyway -> { + flyway.repair(); + flyway.migrate(); + }; + } +} diff --git a/backend/src/main/java/com/ody/common/config/ReplicationDataSourceRouter.java b/backend/src/main/java/com/ody/common/config/ReplicationDataSourceRouter.java index 7061f6352..2d42a7eac 100644 --- a/backend/src/main/java/com/ody/common/config/ReplicationDataSourceRouter.java +++ b/backend/src/main/java/com/ody/common/config/ReplicationDataSourceRouter.java @@ -12,7 +12,7 @@ protected Object determineCurrentLookupKey() { boolean isTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); ReplicationType type = ReplicationType.from(isTransactionActive, readOnly); - log.info("(트랜잭션 활성화 여부 : {}) (readOnly : {}) => {} DB 연결", isTransactionActive, isTransactionActive, type); + log.info("(트랜잭션 활성화 여부 : {}) (readOnly : {}) => {} DB 연결", isTransactionActive, readOnly, type); return type; } } diff --git a/backend/src/main/java/com/ody/common/config/ReplicationType.java b/backend/src/main/java/com/ody/common/config/ReplicationType.java index 928553719..3b97e2a42 100644 --- a/backend/src/main/java/com/ody/common/config/ReplicationType.java +++ b/backend/src/main/java/com/ody/common/config/ReplicationType.java @@ -8,8 +8,8 @@ @RequiredArgsConstructor public enum ReplicationType { - READ((transactionActive, readOnly) -> !transactionActive || readOnly), - WRITE((transactionActive, readOnly) -> transactionActive && !readOnly); + READ((transactionActive, readOnly) -> transactionActive && readOnly), + WRITE((transactionActive, readOnly) -> !transactionActive || !readOnly); private final BiPredicate condition; diff --git a/backend/src/main/java/com/ody/common/validator/SupportRegionValidator.java b/backend/src/main/java/com/ody/common/validator/SupportRegionValidator.java index e058750f6..580a648db 100644 --- a/backend/src/main/java/com/ody/common/validator/SupportRegionValidator.java +++ b/backend/src/main/java/com/ody/common/validator/SupportRegionValidator.java @@ -13,6 +13,7 @@ public class SupportRegionValidator implements ConstraintValidator= 0; + return isMissingCoordinate(latitudeValue) + || (MIN_LATITUDE.compareTo(latitudeValue) <= 0 && MAX_LATITUDE.compareTo(latitudeValue) >= 0); } private boolean isInLongitudeRange(String longitude) { BigDecimal longitudeValue = new BigDecimal(longitude); - return MIN_LONGITUDE.compareTo(longitudeValue) <= 0 && MAX_LONGITUDE.compareTo(longitudeValue) >= 0; + return isMissingCoordinate(longitudeValue) + || (MIN_LONGITUDE.compareTo(longitudeValue) <= 0 && MAX_LONGITUDE.compareTo(longitudeValue) >= 0); + } + + private boolean isMissingCoordinate(BigDecimal coordinate) { + return MISSING_COORDINATES.compareTo(coordinate) == 0; } } diff --git a/backend/src/main/java/com/ody/meeting/domain/Location.java b/backend/src/main/java/com/ody/meeting/domain/Location.java index 0dbadfe62..108484f90 100644 --- a/backend/src/main/java/com/ody/meeting/domain/Location.java +++ b/backend/src/main/java/com/ody/meeting/domain/Location.java @@ -1,10 +1,8 @@ package com.ody.meeting.domain; -import com.ody.common.exception.OdyBadRequestException; import jakarta.persistence.Embeddable; import jakarta.persistence.Embedded; import jakarta.validation.constraints.NotNull; -import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -14,8 +12,6 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Location { - private static final List SUPPORT_REGION = List.of("서울", "경기", "인천"); - @NotNull private String address; @@ -28,18 +24,10 @@ public Location(String address, String latitude, String longitude) { } public Location(String address, Coordinates coordinates) { - validateSupportRegion(address); this.address = address; this.coordinates = coordinates; } - private void validateSupportRegion(String address) { - SUPPORT_REGION.stream() - .filter(address::startsWith) - .findAny() - .orElseThrow(() -> new OdyBadRequestException("현재 지원되지 않는 지역입니다.")); - } - public String getLatitude() { return coordinates.getLatitude(); } diff --git a/backend/src/main/java/com/ody/meeting/service/MeetingService.java b/backend/src/main/java/com/ody/meeting/service/MeetingService.java index 86f9805d4..050d06d7f 100644 --- a/backend/src/main/java/com/ody/meeting/service/MeetingService.java +++ b/backend/src/main/java/com/ody/meeting/service/MeetingService.java @@ -23,7 +23,6 @@ import com.ody.util.TimeUtil; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -62,6 +61,7 @@ private void scheduleEtaNotice(Meeting meeting) { LocalDateTime etaNoticeTime = meeting.getMeetingTime().minusMinutes(ETA_NOTICE_TIME_DEFER); Instant startTime = etaNoticeTime.toInstant(TimeUtil.KST_OFFSET); taskScheduler.schedule(() -> fcmPushSender.sendNoticeMessage(noticeMessage), startTime); + log.info("{} 타입 알림 {}에 스케줄링 예약", NotificationType.ETA_NOTICE, startTime.atZone(TimeUtil.KST_OFFSET)); } private String generateUniqueInviteCode() { diff --git a/backend/src/main/java/com/ody/notification/domain/FcmTopic.java b/backend/src/main/java/com/ody/notification/domain/FcmTopic.java index 5725bc18a..c97b94118 100644 --- a/backend/src/main/java/com/ody/notification/domain/FcmTopic.java +++ b/backend/src/main/java/com/ody/notification/domain/FcmTopic.java @@ -3,6 +3,7 @@ import com.ody.meeting.domain.Meeting; import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.time.format.DateTimeFormatter; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,6 +14,7 @@ public class FcmTopic { private static final String TOPIC_NAME_DELIMITER = "_"; + private static final DateTimeFormatter MEETING_CREATE_AT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); @Column(name = "fcm_topic") private String value; @@ -28,6 +30,6 @@ public FcmTopic(String rawValue) { private static String build(Meeting meeting) { return meeting.getId().toString() + TOPIC_NAME_DELIMITER - + meeting.getCreatedAt(); + + meeting.getCreatedAt().format(MEETING_CREATE_AT_FORMAT); } } diff --git a/backend/src/main/java/com/ody/notification/domain/Notification.java b/backend/src/main/java/com/ody/notification/domain/Notification.java index e65684df8..85c2d048e 100644 --- a/backend/src/main/java/com/ody/notification/domain/Notification.java +++ b/backend/src/main/java/com/ody/notification/domain/Notification.java @@ -65,7 +65,7 @@ public static Notification createEntry(Mate mate, FcmTopic fcmTopic) { NotificationType.ENTRY, LocalDateTime.now(), NotificationStatus.DONE, - null + fcmTopic ); } diff --git a/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java b/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java index 4f0a9d5f6..a40756ade 100644 --- a/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java +++ b/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java @@ -3,6 +3,7 @@ import com.ody.notification.domain.Notification; import com.ody.notification.domain.NotificationStatus; import com.ody.notification.domain.NotificationType; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -15,10 +16,10 @@ public interface NotificationRepository extends JpaRepository findAllMeetingLogs(Long meetingId); + List findAllMeetingLogsBeforeThanEqual(Long meetingId, LocalDateTime time); @Query(""" select noti diff --git a/backend/src/main/java/com/ody/notification/service/FcmPushSender.java b/backend/src/main/java/com/ody/notification/service/FcmPushSender.java index b56cccb50..394401442 100644 --- a/backend/src/main/java/com/ody/notification/service/FcmPushSender.java +++ b/backend/src/main/java/com/ody/notification/service/FcmPushSender.java @@ -8,6 +8,8 @@ import com.ody.notification.domain.message.DirectMessage; import com.ody.notification.domain.message.GroupMessage; import com.ody.notification.dto.request.FcmGroupSendRequest; +import com.ody.util.TimeUtil; +import java.time.Instant; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -52,6 +54,7 @@ private void updateDepartureReminderToDone(Notification notification) { public void sendNoticeMessage(GroupMessage groupMessage) { try { FirebaseMessaging.getInstance().send(groupMessage.message()); + log.info("공지 알림 전송 | 전송 시간 : {}", Instant.now().atZone(TimeUtil.KST_OFFSET)); } catch (FirebaseMessagingException exception) { log.error("FCM 공지 전송 실패 : {}", exception.getMessage()); throw new OdyServerErrorException(exception.getMessage()); diff --git a/backend/src/main/java/com/ody/notification/service/NotificationService.java b/backend/src/main/java/com/ody/notification/service/NotificationService.java index bfe36007b..6abbaf40d 100644 --- a/backend/src/main/java/com/ody/notification/service/NotificationService.java +++ b/backend/src/main/java/com/ody/notification/service/NotificationService.java @@ -16,7 +16,6 @@ import com.ody.util.TimeUtil; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -77,10 +76,11 @@ public void scheduleNotification(FcmGroupSendRequest fcmGroupSendRequest) { "{} 타입 {} 상태 알림 {}에 스케줄링 예약", fcmGroupSendRequest.notification().getType(), fcmGroupSendRequest.notification().getStatus(), - startTime + startTime.atZone(TimeUtil.KST_OFFSET) ); } + @Transactional @EventListener(ApplicationReadyEvent.class) public void schedulePendingNotification() { List notifications = notificationRepository.findAllByTypeAndStatus( @@ -93,7 +93,10 @@ public void schedulePendingNotification() { @DisabledDeletedFilter public NotiLogFindResponses findAllMeetingLogs(Long meetingId) { - List notifications = notificationRepository.findAllMeetingLogs(meetingId); + List notifications = notificationRepository.findAllMeetingLogsBeforeThanEqual( + meetingId, + LocalDateTime.now() + ); return NotiLogFindResponses.from(notifications); } diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index f2dcc3120..49e7040a8 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -2,7 +2,6 @@ spring: jpa: hibernate: ddl-auto: validate - defer-datasource-initialization: false sql: init: mode: never @@ -11,9 +10,6 @@ spring: url: ENC(KUJNgDMlvi/imeVHjCO2QekgI4DNxZT9IrW0QlVVkJXJUUQ2DAUOqI91FVH7aFhXVCsfXtf0EUmF/JqN3GpXa/mVj3o4BNbqS3QsU50BgwEEodko+VwHY2ebDHUxlKA1/FXBSmu6p+Rd3rcNEzoBIA==) username: ENC(jNYhZN6xMCmPQ7GZM+Hu3A==) password: ENC(VRtr3RjM+XnPvRdLjrPMHyTSzDlEjR7o) - flyway: - enabled: true - baseline-on-migrate: true log: file: diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index bc3fc6584..cd64f52f0 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -7,19 +7,16 @@ spring: web-allow-others: true datasource: driver-class-name: org.h2.Driver - url: jdbc:h2:mem:database?serverTimezone=Asia/Seoul + url: jdbc:h2:mem:database?serverTimezone=Asia/Seoul;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE username: sa password: jpa: hibernate: ddl-auto: validate - defer-datasource-initialization: false + dialect: org.hibernate.dialect.MySQLDialect sql: init: mode: always - flyway: - enabled: true - baseline-on-migrate: true log: file: diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 8b96fe784..63cb4ee87 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -13,10 +13,6 @@ spring: jdbc-url: jdbc:mysql://database-project.cluster-ro-cqsc6pyqhwww.ap-northeast-2.rds.amazonaws.com:3306/ody username: ody password: ENC(2n3gUzzFjknXpHDYwuF4d5BQw6XqLdsX) - defer-datasource-initialization: false - flyway: - enabled: true - baseline-on-migrate: true log: file: diff --git a/backend/src/main/resources/common.yml b/backend/src/main/resources/common.yml index bba34a04b..f6f2be5e1 100644 --- a/backend/src/main/resources/common.yml +++ b/backend/src/main/resources/common.yml @@ -1,6 +1,10 @@ spring: jpa: open-in-view: false + defer-datasource-initialization: false + flyway: + enabled: true + baseline-on-migrate: true springdoc: default-consumes-media-type: application/json;charset=UTF-8 @@ -15,7 +19,7 @@ springdoc: odsay: url: https://api.odsay.com/v1/api/searchPubTransPathT - api-key: ENC(tHrCTqN0WANDzm2bOdUftqRCbiz64bAyY48Q3FsvMfhRNAnAbpAru/ebTLbwXoDfIcZuSPUFczw=) + api-key: ENC(4+ag02XbL8zS6rWoWHh7WzhKqBj5kaBwBcg04MpLr8nLJefO2PRY8E5o0WpRd3oRtvFv1s9lnWI=) # TODO: 임시로 개인 api-key 추가 -> 팀 key로 변경 필요 google: @@ -39,3 +43,8 @@ jasypt: kakao: url: https://kapi.kakao.com/v1/user/unlink admin-key: ENC(b8lXOQquegCbdIgayVYIDBrhkNeyuucBArJScDGtEA2IWFiWeGUViiDhyZg0XpFE) + +management: + health: + db: + enabled: false diff --git a/backend/src/main/resources/db/migration/V3__alter_character_set.sql b/backend/src/main/resources/db/migration/V3__alter_character_set.sql new file mode 100644 index 000000000..1f1b44532 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__alter_character_set.sql @@ -0,0 +1,6 @@ +/*! ALTER DATABASE ody CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; */ +/*! ALTER TABLE member CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; */ +/*! ALTER TABLE meeting CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; */ +/*! ALTER TABLE mate CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; */ +/*! ALTER TABLE notification CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; */ +/*! ALTER TABLE eta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; */ diff --git a/backend/src/test/java/com/ody/common/validator/SupportRegionValidatorTest.java b/backend/src/test/java/com/ody/common/validator/SupportRegionValidatorTest.java index 8242f7626..7376939c4 100644 --- a/backend/src/test/java/com/ody/common/validator/SupportRegionValidatorTest.java +++ b/backend/src/test/java/com/ody/common/validator/SupportRegionValidatorTest.java @@ -11,6 +11,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -36,6 +37,16 @@ void isValid(String latitude, String longitude, boolean expected) { assertThat(valid).isEqualTo(expected); } + @DisplayName("행방불명 좌표(0.0)는 true를 반환한다") + @Test + void isValidMissingCoordinate() { + MateEtaRequest mateEtaRequest = new MateEtaRequest(false, "0.0", "0.0"); + + boolean valid = supportRegionValidator.isValid(mateEtaRequest, mock(ConstraintValidatorContext.class)); + + assertThat(valid).isEqualTo(true); + } + private static Stream supportRegionTestCases() { return Stream.of( Arguments.of("37.505713", "127.050691", true), // 서울 diff --git a/backend/src/test/java/com/ody/meeting/domain/LocationTest.java b/backend/src/test/java/com/ody/meeting/domain/LocationTest.java deleted file mode 100644 index ac922ddd2..000000000 --- a/backend/src/test/java/com/ody/meeting/domain/LocationTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.ody.meeting.domain; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.ody.common.exception.OdyBadRequestException; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -class LocationTest { - - @DisplayName("서울, 경기, 인천 지역으로만 Location을 생성할 수 있다.") - @ParameterizedTest - @ValueSource(strings = {"서울 강남구 테헤란로 411", "경기 성남시 분당구 서판교로 32", "인천 부평구 갈산로 2"}) - void createLocationSuccess(String address) { - assertThatCode(() -> new Location(address, "0", "0")) - .doesNotThrowAnyException(); - } - - @DisplayName("서울, 경기, 인천 지역이 아니면 Location을 생성시 예외가 발생한다.") - @Test - void createLocationException() { - assertThatThrownBy(() -> new Location("경남 김해시 율하3로 76", "0", "0")) - .isInstanceOf(OdyBadRequestException.class); - } -} diff --git a/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java b/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java index 481a9425e..abe339bd9 100644 --- a/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java +++ b/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java @@ -49,7 +49,7 @@ void findAllMeetingLogsById() { notificationRepository.save(notification1); notificationRepository.save(notification2); - List notifications = notificationRepository.findAllMeetingLogs(odyMeeting.getId()); + List notifications = notificationRepository.findAllMeetingLogsBeforeThanEqual(odyMeeting.getId(), LocalDateTime.now()); assertThat(notifications.size()).isEqualTo(2); } @@ -86,7 +86,7 @@ void findAllNotificationsById() { notificationRepository.save(pastNotification); notificationRepository.save(futureNotification); - List notifications = notificationRepository.findAllMeetingLogs(odyMeeting.getId()); + List notifications = notificationRepository.findAllMeetingLogsBeforeThanEqual(odyMeeting.getId(), LocalDateTime.now()); assertThat(notifications.size()).isOne(); } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 5cd508bab..ae3c7f80c 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -6,7 +6,7 @@ spring: settings: web-allow-others: true datasource: - url: jdbc:h2:mem:database + url: jdbc:h2:mem:database;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE username: sa password: driver-class-name: org.h2.Driver @@ -14,6 +14,7 @@ spring: show-sql: true hibernate: ddl-auto: create-drop + dialect: org.hibernate.dialect.MySQLDialect properties: hibernate: format_sql: true