From 2eae1670680931b427e191573a02fa7773f6b442 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 18 Aug 2026 20:35:37 +0900 Subject: [PATCH 1/7] =?UTF-8?q?build:=20TTS=20=EB=AF=B8=EB=A6=AC=EB=93=A3?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=8C=EC=9B=90=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + build.gradle | 2 ++ docker-compose.prod.yml | 1 + src/main/resources/application.yml | 4 +++ .../V1__add_tts_preview_audio_key.sql | 30 +++++++++++++++++++ 5 files changed, 38 insertions(+) create mode 100644 src/main/resources/db/migration/V1__add_tts_preview_audio_key.sql diff --git a/.env.example b/.env.example index 6e977d4..21dfa11 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ DB_URL=jdbc:mysql://your-db-host:3306/moru?serverTimezone=Asia/Seoul&characterEn DB_USERNAME= DB_PASSWORD= JPA_DDL_AUTO=validate +FLYWAY_ENABLED=true JWT_SECRET= JWT_ACCESS_TOKEN_EXPIRATION=1h diff --git a/build.gradle b/build.gradle index 60bad8b..1ca479a 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-flyway' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-security' @@ -43,6 +44,7 @@ dependencies { compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'org.flywaydb:flyway-mysql' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b96612b..4b46afe 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -26,6 +26,7 @@ services: SPRING_JPA_HIBERNATE_DDL_AUTO: ${JPA_DDL_AUTO:-validate} SPRING_JPA_OPEN_IN_VIEW: "false" SPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL: "false" + SPRING_FLYWAY_ENABLED: ${FLYWAY_ENABLED:-true} LOGGING_LEVEL_ORG_HIBERNATE_SQL: info LOGGING_LEVEL_ORG_HIBERNATE_ORM_JDBC_BIND: off diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5a650d7..04a3e57 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,6 +3,10 @@ spring: import: optional:file:.env[.properties] application: name: moru-server + flyway: + enabled: ${FLYWAY_ENABLED:false} + baseline-on-migrate: true + baseline-version: 0 jpa: properties: hibernate: diff --git a/src/main/resources/db/migration/V1__add_tts_preview_audio_key.sql b/src/main/resources/db/migration/V1__add_tts_preview_audio_key.sql new file mode 100644 index 0000000..08f609f --- /dev/null +++ b/src/main/resources/db/migration/V1__add_tts_preview_audio_key.sql @@ -0,0 +1,30 @@ +SET @preview_audio_key_exists = ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'tts' + AND column_name = 'preview_audio_key' +); + +SET @add_preview_audio_key_sql = IF( + @preview_audio_key_exists = 0, + 'ALTER TABLE `tts` ADD COLUMN `preview_audio_key` VARCHAR(500) NULL', + 'SELECT 1' +); + +PREPARE add_preview_audio_key_statement FROM @add_preview_audio_key_sql; +EXECUTE add_preview_audio_key_statement; +DEALLOCATE PREPARE add_preview_audio_key_statement; + +UPDATE `tts` +SET `preview_audio_key` = CASE `name` + WHEN 'Leda' THEN 'tts/previews/v1/leda.mp3' + WHEN 'Kore' THEN 'tts/previews/v1/kore.mp3' + WHEN 'Despina' THEN 'tts/previews/v1/despina.mp3' + WHEN 'Charon' THEN 'tts/previews/v1/charon.mp3' + WHEN 'Orus' THEN 'tts/previews/v1/orus.mp3' + WHEN 'Alnilam' THEN 'tts/previews/v1/alnilam.mp3' + ELSE `preview_audio_key` +END +WHERE (`preview_audio_key` IS NULL OR `preview_audio_key` = '') + AND `name` IN ('Leda', 'Kore', 'Despina', 'Charon', 'Orus', 'Alnilam'); From 22608d22ddbd6d1180c122822d2f13f2c6d5adb9 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 18 Aug 2026 20:36:56 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20TTS=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=8C=EC=9B=90=20=EC=9D=91=EB=8B=B5=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/tts/dto/TTSResponseDTO.java | 28 +++++++++++++++++++ .../moru/server/domain/tts/entity/TTS.java | 10 +++++++ .../tts/entity/enums/TtsAudioStatus.java | 6 ++++ 3 files changed, 44 insertions(+) create mode 100644 src/main/java/com/moru/server/domain/tts/entity/enums/TtsAudioStatus.java diff --git a/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java b/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java index 4203367..1ca7fdc 100644 --- a/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java +++ b/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java @@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; +import com.moru.server.domain.tts.entity.enums.TtsAudioStatus; + public record TTSResponseDTO() { @Builder @@ -36,6 +38,32 @@ public record VoiceResponse( ) String previewAudioUrl, + @Schema(description = "목소리 미리듣기 음원 생성 상태", example = "READY") + TtsAudioStatus previewAudioStatus, + + @Schema( + description = "루틴 완료 공통 음원 URL", + example = "https://moru-prod-preview-assets.s3.ap-northeast-2.amazonaws.com/tts/common/v1/leda/done.mp3", + nullable = true + ) + String doneAudioUrl, + + @Schema(description = "루틴 완료 공통 음원 생성 상태", example = "READY") + TtsAudioStatus doneAudioStatus, + + @Schema( + description = "루틴 리마인드 공통 음원 URL", + example = "https://moru-prod-preview-assets.s3.ap-northeast-2.amazonaws.com/tts/common/v1/leda/remind.mp3", + nullable = true + ) + String remindAudioUrl, + + @Schema(description = "루틴 리마인드 공통 음원 생성 상태", example = "READY") + TtsAudioStatus remindAudioStatus, + + @Schema(description = "음성 공통 음원 캐시 버전", example = "1") + Integer selectionVersion, + @Schema(description = "PRO 전용 여부", example = "false") Boolean proOnly ) { diff --git a/src/main/java/com/moru/server/domain/tts/entity/TTS.java b/src/main/java/com/moru/server/domain/tts/entity/TTS.java index b8d5e57..ac4899e 100644 --- a/src/main/java/com/moru/server/domain/tts/entity/TTS.java +++ b/src/main/java/com/moru/server/domain/tts/entity/TTS.java @@ -31,6 +31,16 @@ public class TTS extends BaseEntity { @Column(name = "preview_audio_key", length = 500) private String previewAudioKey; + @Column(name = "done_audio_key", length = 500) + private String doneAudioKey; + + @Column(name = "remind_audio_key", length = 500) + private String remindAudioKey; + + @Column(name = "selection_version", nullable = false) + @Builder.Default + private Integer selectionVersion = 1; + @Column(name = "is_pro_only", nullable = false) @Builder.Default private Boolean isProOnly = false; diff --git a/src/main/java/com/moru/server/domain/tts/entity/enums/TtsAudioStatus.java b/src/main/java/com/moru/server/domain/tts/entity/enums/TtsAudioStatus.java new file mode 100644 index 0000000..b7a6e19 --- /dev/null +++ b/src/main/java/com/moru/server/domain/tts/entity/enums/TtsAudioStatus.java @@ -0,0 +1,6 @@ +package com.moru.server.domain.tts.entity.enums; + +public enum TtsAudioStatus { + PENDING, + READY +} From 84ab81a212831861634fad01e841719dd8e37962 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 18 Aug 2026 20:38:15 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20TTS=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=8C=EC=9B=90=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V2__add_tts_common_audio_keys.sql | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/main/resources/db/migration/V2__add_tts_common_audio_keys.sql diff --git a/src/main/resources/db/migration/V2__add_tts_common_audio_keys.sql b/src/main/resources/db/migration/V2__add_tts_common_audio_keys.sql new file mode 100644 index 0000000..f16d781 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_tts_common_audio_keys.sql @@ -0,0 +1,81 @@ +SET @done_audio_key_exists = ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'tts' + AND column_name = 'done_audio_key' +); + +SET @add_done_audio_key_sql = IF( + @done_audio_key_exists = 0, + 'ALTER TABLE `tts` ADD COLUMN `done_audio_key` VARCHAR(500) NULL', + 'SELECT 1' +); + +PREPARE add_done_audio_key_statement FROM @add_done_audio_key_sql; +EXECUTE add_done_audio_key_statement; +DEALLOCATE PREPARE add_done_audio_key_statement; + +SET @remind_audio_key_exists = ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'tts' + AND column_name = 'remind_audio_key' +); + +SET @add_remind_audio_key_sql = IF( + @remind_audio_key_exists = 0, + 'ALTER TABLE `tts` ADD COLUMN `remind_audio_key` VARCHAR(500) NULL', + 'SELECT 1' +); + +PREPARE add_remind_audio_key_statement FROM @add_remind_audio_key_sql; +EXECUTE add_remind_audio_key_statement; +DEALLOCATE PREPARE add_remind_audio_key_statement; + +SET @selection_version_exists = ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'tts' + AND column_name = 'selection_version' +); + +SET @add_selection_version_sql = IF( + @selection_version_exists = 0, + 'ALTER TABLE `tts` ADD COLUMN `selection_version` INT NOT NULL DEFAULT 1', + 'SELECT 1' +); + +PREPARE add_selection_version_statement FROM @add_selection_version_sql; +EXECUTE add_selection_version_statement; +DEALLOCATE PREPARE add_selection_version_statement; + +UPDATE `tts` +SET + `done_audio_key` = CASE + WHEN `done_audio_key` IS NULL OR `done_audio_key` = '' THEN CASE `name` + WHEN 'Leda' THEN 'tts/common/v1/leda-done.mp3' + WHEN 'Kore' THEN 'tts/common/v1/kore-done.mp3' + WHEN 'Despina' THEN 'tts/common/v1/despina-done.mp3' + WHEN 'Charon' THEN 'tts/common/v1/charon-done.mp3' + WHEN 'Orus' THEN 'tts/common/v1/orus-done.mp3' + WHEN 'Alnilam' THEN 'tts/common/v1/alnilam-done.mp3' + ELSE `done_audio_key` + END + ELSE `done_audio_key` + END, + `remind_audio_key` = CASE + WHEN `remind_audio_key` IS NULL OR `remind_audio_key` = '' THEN CASE `name` + WHEN 'Leda' THEN 'tts/common/v1/leda-remind.mp3' + WHEN 'Kore' THEN 'tts/common/v1/kore-remind.mp3' + WHEN 'Despina' THEN 'tts/common/v1/despina-remind.mp3' + WHEN 'Charon' THEN 'tts/common/v1/charon-remind.mp3' + WHEN 'Orus' THEN 'tts/common/v1/orus-remind.mp3' + WHEN 'Alnilam' THEN 'tts/common/v1/alnilam-remind.mp3' + ELSE `remind_audio_key` + END + ELSE `remind_audio_key` + END +WHERE `name` IN ('Leda', 'Kore', 'Despina', 'Charon', 'Orus', 'Alnilam'); From f0857bdd7f45164be4448c233ae2d035a55c2b76 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 18 Aug 2026 20:38:57 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20TTS=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=8C=EC=9B=90=20URL=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/tts/converter/TTSConverter.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/moru/server/domain/tts/converter/TTSConverter.java b/src/main/java/com/moru/server/domain/tts/converter/TTSConverter.java index 6f62a1c..9fc172b 100644 --- a/src/main/java/com/moru/server/domain/tts/converter/TTSConverter.java +++ b/src/main/java/com/moru/server/domain/tts/converter/TTSConverter.java @@ -6,6 +6,7 @@ import com.moru.server.domain.tts.dto.TTSResponseDTO; import com.moru.server.domain.tts.entity.TTS; +import com.moru.server.domain.tts.entity.enums.TtsAudioStatus; public class TTSConverter { @@ -30,10 +31,22 @@ public static TTSResponseDTO.VoiceResponse toVoiceResponse( .displayName(voice.getLabel()) .description(voice.getDescription()) .previewAudioUrl(resolvePublicAssetUrl(publicAssetBaseUrl, voice.getPreviewAudioKey())) + .previewAudioStatus(resolveAudioStatus(voice.getPreviewAudioKey())) + .doneAudioUrl(resolvePublicAssetUrl(publicAssetBaseUrl, voice.getDoneAudioKey())) + .doneAudioStatus(resolveAudioStatus(voice.getDoneAudioKey())) + .remindAudioUrl(resolvePublicAssetUrl(publicAssetBaseUrl, voice.getRemindAudioKey())) + .remindAudioStatus(resolveAudioStatus(voice.getRemindAudioKey())) + .selectionVersion(voice.getSelectionVersion()) .proOnly(voice.getIsProOnly()) .build(); } + private static TtsAudioStatus resolveAudioStatus(String objectKey) { + return StringUtils.hasText(objectKey) + ? TtsAudioStatus.READY + : TtsAudioStatus.PENDING; + } + private static String resolvePublicAssetUrl(String baseUrl, String objectKey) { if (!StringUtils.hasText(baseUrl) || !StringUtils.hasText(objectKey)) { return null; From 140d0b0858acfc63ab4ed51b01e8cf7757c52bf9 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 18 Aug 2026 20:40:26 +0900 Subject: [PATCH 5/7] =?UTF-8?q?test:=20TTS=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=8C=EC=9B=90=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tts/converter/TTSConverterTest.java | 69 +++++++++++++++---- .../query/TTSQueryServiceImplTest.java | 40 +++++++++-- 2 files changed, 93 insertions(+), 16 deletions(-) diff --git a/src/test/java/com/moru/server/domain/tts/converter/TTSConverterTest.java b/src/test/java/com/moru/server/domain/tts/converter/TTSConverterTest.java index f986f01..17853f7 100644 --- a/src/test/java/com/moru/server/domain/tts/converter/TTSConverterTest.java +++ b/src/test/java/com/moru/server/domain/tts/converter/TTSConverterTest.java @@ -8,6 +8,7 @@ import com.moru.server.domain.tts.dto.TTSResponseDTO; import com.moru.server.domain.tts.entity.TTS; +import com.moru.server.domain.tts.entity.enums.TtsAudioStatus; class TTSConverterTest { @@ -15,8 +16,15 @@ class TTSConverterTest { "https://moru-prod-preview-assets.s3.ap-northeast-2.amazonaws.com"; @Test - void convertsPreviewAudioKeyToPublicUrl() { - TTS voice = createVoice(1L, "Leda", "tts/previews/v1/leda.mp3"); + void convertsAudioKeysToPublicUrls() { + TTS voice = createVoice( + 1L, + "Leda", + "tts/previews/v1/leda.mp3", + "tts/common/v1/leda-done.mp3", + "tts/common/v1/leda-remind.mp3", + 2 + ); TTSResponseDTO.VoiceListResponse response = TTSConverter.toVoiceListResponse( List.of(voice), @@ -29,44 +37,81 @@ void convertsPreviewAudioKeyToPublicUrl() { assertThat(result.previewAudioUrl()).isEqualTo( PUBLIC_ASSET_BASE_URL + "/tts/previews/v1/leda.mp3" ); + assertThat(result.previewAudioStatus()).isEqualTo(TtsAudioStatus.READY); + assertThat(result.doneAudioUrl()).isEqualTo( + PUBLIC_ASSET_BASE_URL + "/tts/common/v1/leda-done.mp3" + ); + assertThat(result.doneAudioStatus()).isEqualTo(TtsAudioStatus.READY); + assertThat(result.remindAudioUrl()).isEqualTo( + PUBLIC_ASSET_BASE_URL + "/tts/common/v1/leda-remind.mp3" + ); + assertThat(result.remindAudioStatus()).isEqualTo(TtsAudioStatus.READY); + assertThat(result.selectionVersion()).isEqualTo(2); }); } @Test - void returnsNullPreviewUrlWhenObjectKeyIsMissing() { - TTS voice = createVoice(1L, "Leda", null); + void returnsPendingStatusAndNullUrlsWhenAudioKeysAreMissing() { + TTS voice = createVoice(1L, "Leda", null, null, null, 1); TTSResponseDTO.VoiceListResponse response = TTSConverter.toVoiceListResponse( List.of(voice), PUBLIC_ASSET_BASE_URL ); - assertThat(response.voices()).singleElement().satisfies(result -> - assertThat(result.previewAudioUrl()).isNull() - ); + assertThat(response.voices()).singleElement().satisfies(result -> { + assertThat(result.previewAudioUrl()).isNull(); + assertThat(result.previewAudioStatus()).isEqualTo(TtsAudioStatus.PENDING); + assertThat(result.doneAudioUrl()).isNull(); + assertThat(result.doneAudioStatus()).isEqualTo(TtsAudioStatus.PENDING); + assertThat(result.remindAudioUrl()).isNull(); + assertThat(result.remindAudioStatus()).isEqualTo(TtsAudioStatus.PENDING); + }); } @Test void returnsNullPreviewUrlWhenBaseUrlIsMissing() { - TTS voice = createVoice(1L, "Leda", "tts/previews/v1/leda.mp3"); + TTS voice = createVoice( + 1L, + "Leda", + "tts/previews/v1/leda.mp3", + "tts/common/v1/leda-done.mp3", + "tts/common/v1/leda-remind.mp3", + 1 + ); TTSResponseDTO.VoiceListResponse response = TTSConverter.toVoiceListResponse( List.of(voice), "" ); - assertThat(response.voices()).singleElement().satisfies(result -> - assertThat(result.previewAudioUrl()).isNull() - ); + assertThat(response.voices()).singleElement().satisfies(result -> { + assertThat(result.previewAudioUrl()).isNull(); + assertThat(result.doneAudioUrl()).isNull(); + assertThat(result.remindAudioUrl()).isNull(); + assertThat(result.previewAudioStatus()).isEqualTo(TtsAudioStatus.READY); + assertThat(result.doneAudioStatus()).isEqualTo(TtsAudioStatus.READY); + assertThat(result.remindAudioStatus()).isEqualTo(TtsAudioStatus.READY); + }); } - private TTS createVoice(Long id, String name, String previewAudioKey) { + private TTS createVoice( + Long id, + String name, + String previewAudioKey, + String doneAudioKey, + String remindAudioKey, + Integer selectionVersion + ) { return TTS.builder() .id(id) .name(name) .label(name) .description("미리듣기 테스트 음성") .previewAudioKey(previewAudioKey) + .doneAudioKey(doneAudioKey) + .remindAudioKey(remindAudioKey) + .selectionVersion(selectionVersion) .isProOnly(false) .build(); } diff --git a/src/test/java/com/moru/server/domain/tts/service/query/TTSQueryServiceImplTest.java b/src/test/java/com/moru/server/domain/tts/service/query/TTSQueryServiceImplTest.java index 58058a2..6ac1f2d 100644 --- a/src/test/java/com/moru/server/domain/tts/service/query/TTSQueryServiceImplTest.java +++ b/src/test/java/com/moru/server/domain/tts/service/query/TTSQueryServiceImplTest.java @@ -14,6 +14,7 @@ import com.moru.server.domain.tts.dto.TTSResponseDTO; import com.moru.server.domain.tts.entity.TTS; +import com.moru.server.domain.tts.entity.enums.TtsAudioStatus; import com.moru.server.domain.tts.repository.TTSRepository; import com.moru.server.global.config.AssetProperties; @@ -33,9 +34,23 @@ void setUp() { } @Test - void returnsVoicesInRepositoryOrderWithPreviewUrls() { - TTS leda = createVoice(1L, "Leda", "tts/previews/v1/leda.mp3"); - TTS kore = createVoice(2L, "Kore", "tts/previews/v1/kore.mp3"); + void returnsVoicesInRepositoryOrderWithAudioUrls() { + TTS leda = createVoice( + 1L, + "Leda", + "tts/previews/v1/leda.mp3", + "tts/common/v1/leda-done.mp3", + "tts/common/v1/leda-remind.mp3", + 1 + ); + TTS kore = createVoice( + 2L, + "Kore", + "tts/previews/v1/kore.mp3", + "tts/common/v1/kore-done.mp3", + "tts/common/v1/kore-remind.mp3", + 2 + ); when(ttsRepository.findAllByOrderByIdAsc()).thenReturn(List.of(leda, kore)); TTSResponseDTO.VoiceListResponse response = ttsQueryService.getVoices(); @@ -49,16 +64,33 @@ void returnsVoicesInRepositoryOrderWithPreviewUrls() { "https://assets.example.com/tts/previews/v1/leda.mp3", "https://assets.example.com/tts/previews/v1/kore.mp3" ); + assertThat(response.voices()).allSatisfy(voice -> { + assertThat(voice.doneAudioStatus()).isEqualTo(TtsAudioStatus.READY); + assertThat(voice.remindAudioStatus()).isEqualTo(TtsAudioStatus.READY); + }); + assertThat(response.voices()) + .extracting(TTSResponseDTO.VoiceResponse::selectionVersion) + .containsExactly(1, 2); verify(ttsRepository).findAllByOrderByIdAsc(); } - private TTS createVoice(Long id, String name, String previewAudioKey) { + private TTS createVoice( + Long id, + String name, + String previewAudioKey, + String doneAudioKey, + String remindAudioKey, + Integer selectionVersion + ) { return TTS.builder() .id(id) .name(name) .label(name) .description("미리듣기 테스트 음성") .previewAudioKey(previewAudioKey) + .doneAudioKey(doneAudioKey) + .remindAudioKey(remindAudioKey) + .selectionVersion(selectionVersion) .isProOnly(false) .build(); } From 05e8db25fa3c5c2d4e3ea4d094f77c721dda7dc1 Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 18 Aug 2026 21:44:06 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=EB=B9=88=20DB=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 +- .../db/migration/V1__initialize_schema.sql | 147 ++++++++++++++++++ ....sql => V2__add_tts_preview_audio_key.sql} | 0 ....sql => V3__add_tts_common_audio_keys.sql} | 0 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V1__initialize_schema.sql rename src/main/resources/db/migration/{V1__add_tts_preview_audio_key.sql => V2__add_tts_preview_audio_key.sql} (100%) rename src/main/resources/db/migration/{V2__add_tts_common_audio_keys.sql => V3__add_tts_common_audio_keys.sql} (100%) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 04a3e57..ce20f36 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,7 +6,7 @@ spring: flyway: enabled: ${FLYWAY_ENABLED:false} baseline-on-migrate: true - baseline-version: 0 + baseline-version: 1 jpa: properties: hibernate: diff --git a/src/main/resources/db/migration/V1__initialize_schema.sql b/src/main/resources/db/migration/V1__initialize_schema.sql new file mode 100644 index 0000000..e68de29 --- /dev/null +++ b/src/main/resources/db/migration/V1__initialize_schema.sql @@ -0,0 +1,147 @@ +CREATE TABLE IF NOT EXISTS `tts` ( + `is_pro_only` BIT(1) NOT NULL, + `selection_version` INT NOT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `updated_at` DATETIME(6) NOT NULL, + `name` VARCHAR(50) NOT NULL, + `description` VARCHAR(100) NULL, + `google_voice_name` VARCHAR(100) NULL, + `label` VARCHAR(100) NOT NULL, + `done_audio_key` VARCHAR(500) NULL, + `preview_audio_key` VARCHAR(500) NULL, + `remind_audio_key` VARCHAR(500) NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `members` ( + `onboarding_completed` BIT(1) NOT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `tts_id` BIGINT NULL, + `updated_at` DATETIME(6) NOT NULL, + `nickname` VARCHAR(50) NULL, + `profile_image_key` VARCHAR(500) NULL, + `oauth_id` VARCHAR(255) NOT NULL, + `login_type` ENUM('APPLE', 'GOOGLE', 'KAKAO', 'NAVER') NOT NULL, + `role` ENUM('ADMIN', 'MEMBER') NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_member_login_type_oauth_id` (`login_type`, `oauth_id`), + KEY `fk_members_tts` (`tts_id`), + CONSTRAINT `fk_members_tts` FOREIGN KEY (`tts_id`) REFERENCES `tts` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `terms` ( + `is_required` BIT(1) NOT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `updated_at` DATETIME(6) NOT NULL, + `title` VARCHAR(100) NOT NULL, + `content` TEXT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `routine_group` ( + `alarm_time` TIME NULL, + `is_active` BIT(1) NOT NULL, + `is_template` BIT(1) NOT NULL, + `weather_notification_enabled` BIT(1) NOT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `member_id` BIGINT NULL, + `updated_at` DATETIME(6) NOT NULL, + `alarm_days` VARCHAR(100) NULL, + `description` VARCHAR(100) NULL, + `title` VARCHAR(100) NOT NULL, + `goal_type` ENUM('HABIT', 'HEALTH', 'STABILITY', 'VITALITY') NULL, + PRIMARY KEY (`id`), + KEY `fk_routine_group_member` (`member_id`), + CONSTRAINT `fk_routine_group_member` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `routine` ( + `order_index` INT NOT NULL, + `timer` INT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `routine_group_id` BIGINT NOT NULL, + `updated_at` DATETIME(6) NOT NULL, + `title` VARCHAR(100) NOT NULL, + `type` ENUM('CHECK', 'INPUT', 'TIMER') NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_routine_routine_group` (`routine_group_id`), + CONSTRAINT `fk_routine_routine_group` FOREIGN KEY (`routine_group_id`) REFERENCES `routine_group` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `routine_execution` ( + `actual_wake_time` TIME NULL, + `duration_second` INT NULL, + `executed_date` DATE NOT NULL, + `is_completed` BIT(1) NOT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `routine_id` BIGINT NOT NULL, + `updated_at` DATETIME(6) NOT NULL, + `ai_response` VARCHAR(500) NULL, + `member_input` VARCHAR(500) NULL, + PRIMARY KEY (`id`), + KEY `fk_routine_execution_routine` (`routine_id`), + CONSTRAINT `fk_routine_execution_routine` FOREIGN KEY (`routine_id`) REFERENCES `routine` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `routine_tts` ( + `order_index` INT NOT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `routine_id` BIGINT NOT NULL, + `updated_at` DATETIME(6) NOT NULL, + `content` VARCHAR(255) NOT NULL, + `s3_url` VARCHAR(255) NULL, + `tts_done` VARCHAR(255) NULL, + `tts_intro` VARCHAR(255) NULL, + `tts_status` ENUM('COMPLETED', 'FAILED', 'PENDING') NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_routine_tts_routine` (`routine_id`), + CONSTRAINT `fk_routine_tts_routine` FOREIGN KEY (`routine_id`) REFERENCES `routine` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `member_term` ( + `is_agreed` BIT(1) NOT NULL, + `agreed_at` DATETIME(6) NOT NULL, + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `member_id` BIGINT NOT NULL, + `term_id` BIGINT NOT NULL, + `updated_at` DATETIME(6) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_member_term` (`member_id`, `term_id`), + KEY `fk_member_term_term` (`term_id`), + CONSTRAINT `fk_member_term_member` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`), + CONSTRAINT `fk_member_term_term` FOREIGN KEY (`term_id`) REFERENCES `terms` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `subscriptions` ( + `created_at` DATETIME(6) NOT NULL, + `expires_at` DATETIME(6) NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `member_id` BIGINT NOT NULL, + `started_at` DATETIME(6) NOT NULL, + `updated_at` DATETIME(6) NOT NULL, + `store_transaction_id` VARCHAR(255) NULL, + `plan` ENUM('FREE', 'PRO') NOT NULL, + `store` ENUM('APP_STORE', 'GOOGLE_PLAYSTORE') NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_subscriptions_member` (`member_id`), + CONSTRAINT `fk_subscriptions_member` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS `apple_oauth_credentials` ( + `created_at` DATETIME(6) NOT NULL, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `member_id` BIGINT NOT NULL, + `updated_at` DATETIME(6) NOT NULL, + `encrypted_refresh_token` TEXT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_apple_oauth_credentials_member` (`member_id`), + CONSTRAINT `fk_apple_oauth_credentials_member` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/src/main/resources/db/migration/V1__add_tts_preview_audio_key.sql b/src/main/resources/db/migration/V2__add_tts_preview_audio_key.sql similarity index 100% rename from src/main/resources/db/migration/V1__add_tts_preview_audio_key.sql rename to src/main/resources/db/migration/V2__add_tts_preview_audio_key.sql diff --git a/src/main/resources/db/migration/V2__add_tts_common_audio_keys.sql b/src/main/resources/db/migration/V3__add_tts_common_audio_keys.sql similarity index 100% rename from src/main/resources/db/migration/V2__add_tts_common_audio_keys.sql rename to src/main/resources/db/migration/V3__add_tts_common_audio_keys.sql From aae9fd7610578e03d50de34521cefad141cb9f8a Mon Sep 17 00:00:00 2001 From: issuejong Date: Tue, 18 Aug 2026 21:48:33 +0900 Subject: [PATCH 7/7] =?UTF-8?q?docs:=20TTS=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=8C=EC=9B=90=20Swagger=20=EC=98=88=EC=8B=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/moru/server/domain/tts/dto/TTSResponseDTO.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java b/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java index 1ca7fdc..50db3aa 100644 --- a/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java +++ b/src/main/java/com/moru/server/domain/tts/dto/TTSResponseDTO.java @@ -43,7 +43,7 @@ public record VoiceResponse( @Schema( description = "루틴 완료 공통 음원 URL", - example = "https://moru-prod-preview-assets.s3.ap-northeast-2.amazonaws.com/tts/common/v1/leda/done.mp3", + example = "https://moru-prod-preview-assets.s3.ap-northeast-2.amazonaws.com/tts/common/v1/leda-done.mp3", nullable = true ) String doneAudioUrl, @@ -53,7 +53,7 @@ public record VoiceResponse( @Schema( description = "루틴 리마인드 공통 음원 URL", - example = "https://moru-prod-preview-assets.s3.ap-northeast-2.amazonaws.com/tts/common/v1/leda/remind.mp3", + example = "https://moru-prod-preview-assets.s3.ap-northeast-2.amazonaws.com/tts/common/v1/leda-remind.mp3", nullable = true ) String remindAudioUrl,