From d823f7747f7b57e210b889b6fa2a0dedbaa66ce9 Mon Sep 17 00:00:00 2001 From: yoonho Date: Thu, 27 Nov 2025 07:43:41 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[#2]=20feat:=20Odii=20API=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=20-=20JSON=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=84=EC=9A=94=ED=95=9C=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=EB=A7=8C=20=EC=B6=94=EC=B6=9C=ED=95=B4=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controller/DocentController.java | 22 +++++ .../java/com/earseo/core/entity/OdiiData.java | 25 +++++ .../core/repository/OdiiDataRepository.java | 9 ++ .../earseo/core/service/DocentService.java | 91 +++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 src/main/java/com/earseo/core/controller/DocentController.java create mode 100644 src/main/java/com/earseo/core/entity/OdiiData.java create mode 100644 src/main/java/com/earseo/core/repository/OdiiDataRepository.java create mode 100644 src/main/java/com/earseo/core/service/DocentService.java diff --git a/src/main/java/com/earseo/core/controller/DocentController.java b/src/main/java/com/earseo/core/controller/DocentController.java new file mode 100644 index 0000000..8a1c748 --- /dev/null +++ b/src/main/java/com/earseo/core/controller/DocentController.java @@ -0,0 +1,22 @@ +package com.earseo.core.controller; + +import com.earseo.core.common.BaseResponse; +import com.earseo.core.service.DocentService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class DocentController { + + private final DocentService docentService; + + @PostMapping("/admin/core/docent") + public ResponseEntity> initDocent() { + docentService.initDocent(); + return ResponseEntity.ok(BaseResponse.ok(null)); + } + +} diff --git a/src/main/java/com/earseo/core/entity/OdiiData.java b/src/main/java/com/earseo/core/entity/OdiiData.java new file mode 100644 index 0000000..0df4b14 --- /dev/null +++ b/src/main/java/com/earseo/core/entity/OdiiData.java @@ -0,0 +1,25 @@ +package com.earseo.core.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OdiiData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title") + private String title; + + @Column(name = "script", columnDefinition = "TEXT") + private String script; +} diff --git a/src/main/java/com/earseo/core/repository/OdiiDataRepository.java b/src/main/java/com/earseo/core/repository/OdiiDataRepository.java new file mode 100644 index 0000000..075e4fb --- /dev/null +++ b/src/main/java/com/earseo/core/repository/OdiiDataRepository.java @@ -0,0 +1,9 @@ +package com.earseo.core.repository; + +import com.earseo.core.entity.OdiiData; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OdiiDataRepository extends JpaRepository { + + +} diff --git a/src/main/java/com/earseo/core/service/DocentService.java b/src/main/java/com/earseo/core/service/DocentService.java new file mode 100644 index 0000000..6a8fcba --- /dev/null +++ b/src/main/java/com/earseo/core/service/DocentService.java @@ -0,0 +1,91 @@ +package com.earseo.core.service; + +import com.earseo.core.entity.OdiiData; +import com.earseo.core.repository.OdiiDataRepository; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class DocentService { + + private final OdiiDataRepository odiiDataRepository; + + @Value("${api.key}") + private String ApiKeys; + @Value(("${cloud.aws.s3.bucket}")) + private String bucketName; + + private String key; + private int index; + + @Transactional + public void initDocent(){ + + int pageNo = 1; + int numOfRows = 100; + + while(true){ + JsonNode jsonNode = fetchOdiiApi(pageNo, numOfRows); + JsonNode body = jsonNode.path("response").path("body"); + + if(body.path("numOfRows").asInt() == 0){ + break; + } + + List odiiDataList = new ArrayList<>(); + + if(body.path("items").path("item").isArray()){ + System.out.println("hello"); + for(JsonNode item : body.path("items").path("item")){ + odiiDataList.add(OdiiData.builder() + .title(item.path("title").asText()) + .script(item.path("script").asText()) + .build()); + } + } + + odiiDataRepository.saveAll(odiiDataList); + pageNo++; + } + } + + public JsonNode fetchOdiiApi(int pageNo, int numOfRows) { + this.key = ApiKeys.split(",")[0]; + this.index = 0; + RestClient client = RestClient.create(); + + URI uri = UriComponentsBuilder + .fromHttpUrl("https://apis.data.go.kr/B551011/Odii/storyBasedList") + .queryParam("serviceKey", this.key) + .queryParam("MobileApp", "AppTest") + .queryParam("MobileOS", "ETC") + .queryParam("pageNo", pageNo) + .queryParam("numOfRows", numOfRows) + .queryParam("_type", "json") + .queryParam("langCode", "ko") + .build(true) + .toUri(); + try { + return client.get().uri(uri).retrieve().body(JsonNode.class); + } catch (Exception e) { + System.out.println(e.getMessage()); + if (this.index + 1 < ApiKeys.split(",").length) { + this.index++; + this.key = ApiKeys.split(",")[this.index]; + return fetchOdiiApi(pageNo, numOfRows); + } + return null; + } + } + +} From 89dbb39e7c2b8e8592a26943b5fcfad8f8849378 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 00:24:33 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[#2]=20feat:=20=EA=B4=80=EA=B4=91?= =?UTF-8?q?=EC=A7=80=20=EB=8F=84=EC=8A=A8=ED=8A=B8=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20-=20=EB=A7=88?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=20=ED=85=8C=EC=9D=B4=EB=B8=94=EA=B3=BC=20?= =?UTF-8?q?=EC=98=A4=EB=94=94=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=9D=98=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=B7=A8=ED=95=A9=20-=20?= =?UTF-8?q?=ED=95=A9=EC=B9=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=8F=84=EC=8A=A8=ED=8A=B8=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20-=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20tts=EB=A1=9C=20=EB=B3=80=ED=99=98=20-=20?= =?UTF-8?q?=EB=8F=84=EC=8A=A8=ED=8A=B8=20=ED=85=8C=EC=9D=B4=EB=B8=94=20JSO?= =?UTF-8?q?N=20s3=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- build.gradle | 13 ++ .../earseo/core/common/config/AiConfig.java | 29 +++ .../core/common/config/GoogleCloudConfig.java | 32 +++ .../core/controller/DocentController.java | 3 +- .../earseo/core/dto/etl/DocentItemDto.java | 8 + .../com/earseo/core/dto/etl/JoinItemDto.java | 10 + .../java/com/earseo/core/entity/Docent.java | 28 +++ .../java/com/earseo/core/entity/Master.java | 2 +- .../core/repository/DocentRepository.java | 7 + .../core/repository/OdiiDataRepository.java | 14 +- .../earseo/core/service/DocentService.java | 206 ++++++++++++++++-- .../com/earseo/core/service/ai/Prompts.java | 39 ++++ src/main/resources/application-dev.yaml | 3 + src/main/resources/application-local.yaml | 6 +- src/main/resources/application-test.yaml | 4 +- 16 files changed, 379 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/earseo/core/common/config/AiConfig.java create mode 100644 src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java create mode 100644 src/main/java/com/earseo/core/dto/etl/DocentItemDto.java create mode 100644 src/main/java/com/earseo/core/dto/etl/JoinItemDto.java create mode 100644 src/main/java/com/earseo/core/entity/Docent.java create mode 100644 src/main/java/com/earseo/core/repository/DocentRepository.java create mode 100644 src/main/java/com/earseo/core/service/ai/Prompts.java diff --git a/.gitignore b/.gitignore index 4ea4adf..3bbf44a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ out/ .vscode/ # macOS -.DS_Store \ No newline at end of file +.DS_Store +/src/main/resources/gcp-service-account-key.json diff --git a/build.gradle b/build.gradle index 8ce312e..26b19d6 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,10 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' } +ext { + springAiVersion = "1.1.0" +} + group = 'com.earseo' version = '0.0.1-SNAPSHOT' description = 'backend-core' @@ -49,6 +53,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.kafka:spring-kafka' + + implementation 'org.springframework.ai:spring-ai-starter-model-openai' + implementation 'com.google.cloud:google-cloud-texttospeech:2.78.0' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.ai:spring-ai-bom:$springAiVersion" + } } tasks.named('test') { diff --git a/src/main/java/com/earseo/core/common/config/AiConfig.java b/src/main/java/com/earseo/core/common/config/AiConfig.java new file mode 100644 index 0000000..88c611f --- /dev/null +++ b/src/main/java/com/earseo/core/common/config/AiConfig.java @@ -0,0 +1,29 @@ +package com.earseo.core.common.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class AiConfig { + private final OpenAiChatModel openAiChatModel; + + @Bean + public OpenAiChatOptions openAiChatOptions() { + return OpenAiChatOptions.builder() + .model("gpt-4.1-mini") + .temperature(0.3) + .build(); + } + + @Bean + public ChatClient chatClient() { + return ChatClient.builder(openAiChatModel) + .defaultOptions(openAiChatOptions()) + .build(); + } +} diff --git a/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java b/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java new file mode 100644 index 0000000..30215ff --- /dev/null +++ b/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java @@ -0,0 +1,32 @@ +package com.earseo.core.common.config; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.auth.Credentials; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.texttospeech.v1.TextToSpeechClient; +import com.google.cloud.texttospeech.v1.TextToSpeechSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; + +@Configuration +public class GoogleCloudConfig { + + @Bean + public TextToSpeechClient textToSpeechClient() throws IOException { + ClassPathResource resource = new ClassPathResource("gcp-service-account-key.json"); + + InputStream credentialsStream = resource.getInputStream(); + + Credentials credentials = GoogleCredentials.fromStream(credentialsStream); + + TextToSpeechSettings settings = TextToSpeechSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)) + .build(); + + return TextToSpeechClient.create(settings); + } +} diff --git a/src/main/java/com/earseo/core/controller/DocentController.java b/src/main/java/com/earseo/core/controller/DocentController.java index 8a1c748..819180d 100644 --- a/src/main/java/com/earseo/core/controller/DocentController.java +++ b/src/main/java/com/earseo/core/controller/DocentController.java @@ -16,7 +16,8 @@ public class DocentController { @PostMapping("/admin/core/docent") public ResponseEntity> initDocent() { docentService.initDocent(); + docentService.getDocent(); + docentService.getDocentJson(); return ResponseEntity.ok(BaseResponse.ok(null)); } - } diff --git a/src/main/java/com/earseo/core/dto/etl/DocentItemDto.java b/src/main/java/com/earseo/core/dto/etl/DocentItemDto.java new file mode 100644 index 0000000..59b4229 --- /dev/null +++ b/src/main/java/com/earseo/core/dto/etl/DocentItemDto.java @@ -0,0 +1,8 @@ +package com.earseo.core.dto.etl; + +public record DocentItemDto( + String contentId, + String script, + String docentUrl +) { +} diff --git a/src/main/java/com/earseo/core/dto/etl/JoinItemDto.java b/src/main/java/com/earseo/core/dto/etl/JoinItemDto.java new file mode 100644 index 0000000..e253135 --- /dev/null +++ b/src/main/java/com/earseo/core/dto/etl/JoinItemDto.java @@ -0,0 +1,10 @@ +package com.earseo.core.dto.etl; + +public record JoinItemDto( + Long id, + String contentId, + String title, + String outl, + String script +) { +} diff --git a/src/main/java/com/earseo/core/entity/Docent.java b/src/main/java/com/earseo/core/entity/Docent.java new file mode 100644 index 0000000..28e0f6a --- /dev/null +++ b/src/main/java/com/earseo/core/entity/Docent.java @@ -0,0 +1,28 @@ +package com.earseo.core.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Docent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "content_id", nullable = false, unique = true) + private String contentId; + + @Column(name = "script", columnDefinition = "TEXT") + private String script; + + @Column(name = "docent_url") + private String docentUrl; +} diff --git a/src/main/java/com/earseo/core/entity/Master.java b/src/main/java/com/earseo/core/entity/Master.java index 0ac42bf..8602683 100644 --- a/src/main/java/com/earseo/core/entity/Master.java +++ b/src/main/java/com/earseo/core/entity/Master.java @@ -67,7 +67,7 @@ public class Master { @Column(name = "modifiedtime") private String modifiedtime; - @Column(name = "tel") + @Column(name = "tel", columnDefinition = "TEXT") private String tel; @Column(name = "m_level") diff --git a/src/main/java/com/earseo/core/repository/DocentRepository.java b/src/main/java/com/earseo/core/repository/DocentRepository.java new file mode 100644 index 0000000..df006a5 --- /dev/null +++ b/src/main/java/com/earseo/core/repository/DocentRepository.java @@ -0,0 +1,7 @@ +package com.earseo.core.repository; + +import com.earseo.core.entity.Docent; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DocentRepository extends JpaRepository { +} diff --git a/src/main/java/com/earseo/core/repository/OdiiDataRepository.java b/src/main/java/com/earseo/core/repository/OdiiDataRepository.java index 075e4fb..be1d781 100644 --- a/src/main/java/com/earseo/core/repository/OdiiDataRepository.java +++ b/src/main/java/com/earseo/core/repository/OdiiDataRepository.java @@ -1,9 +1,21 @@ package com.earseo.core.repository; +import com.earseo.core.dto.etl.JoinItemDto; import com.earseo.core.entity.OdiiData; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; -public interface OdiiDataRepository extends JpaRepository { +import java.util.List; +public interface OdiiDataRepository extends JpaRepository { + @Query(value = """ + SELECT DISTINCT m.id, m.content_id, m.title, MIN(o.script), m.outl + FROM odii_data o + RIGHT JOIN master m ON m.title = o.title + WHERE m.title IS NOT NULL + GROUP BY m.id, m.content_id, m.title, m.outl + ORDER BY m.id + """, nativeQuery = true) + List joinWithMaster(); } diff --git a/src/main/java/com/earseo/core/service/DocentService.java b/src/main/java/com/earseo/core/service/DocentService.java index 6a8fcba..1b38b4d 100644 --- a/src/main/java/com/earseo/core/service/DocentService.java +++ b/src/main/java/com/earseo/core/service/DocentService.java @@ -1,56 +1,84 @@ package com.earseo.core.service; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.earseo.core.dto.etl.DocentItemDto; +import com.earseo.core.dto.etl.JoinItemDto; +import com.earseo.core.entity.Docent; import com.earseo.core.entity.OdiiData; +import com.earseo.core.repository.DocentRepository; import com.earseo.core.repository.OdiiDataRepository; +import com.fasterxml.jackson.core.StreamWriteConstraints; import com.fasterxml.jackson.databind.JsonNode; -import jakarta.transaction.Transactional; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.google.cloud.texttospeech.v1.*; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestClient; import org.springframework.web.util.UriComponentsBuilder; +import java.io.ByteArrayInputStream; +import java.io.File; import java.net.URI; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import static com.earseo.core.service.ai.Prompts.DOCENT_SCRIPT; + @Service @RequiredArgsConstructor +@Slf4j public class DocentService { private final OdiiDataRepository odiiDataRepository; + private final DocentRepository docentRepository; + private final ChatClient chatClient; + private final AmazonS3 amazonS3; + private final TextToSpeechClient textToSpeechClient; + private final ObjectMapper objectMapper; + @Value("${api.key}") private String ApiKeys; @Value(("${cloud.aws.s3.bucket}")) private String bucketName; + @Value("${cloud.aws.cloudfront.domain}") + private String cloudFrontDomain; + + List voices = List.of("Kore", "Algieba", "Despina", "Enceladus"); private String key; private int index; @Transactional - public void initDocent(){ + public void initDocent() { int pageNo = 1; int numOfRows = 100; - while(true){ + while (true) { JsonNode jsonNode = fetchOdiiApi(pageNo, numOfRows); JsonNode body = jsonNode.path("response").path("body"); - if(body.path("numOfRows").asInt() == 0){ + if (body.path("numOfRows").asInt() == 0) { break; } List odiiDataList = new ArrayList<>(); - if(body.path("items").path("item").isArray()){ + if (body.path("items").path("item").isArray()) { System.out.println("hello"); - for(JsonNode item : body.path("items").path("item")){ - odiiDataList.add(OdiiData.builder() - .title(item.path("title").asText()) - .script(item.path("script").asText()) - .build()); + for (JsonNode item : body.path("items").path("item")) { + odiiDataList.add(OdiiData.builder().title(item.path("title").asText()).script(item.path("script").asText()).build()); } } @@ -59,22 +87,158 @@ public void initDocent(){ } } + public void getDocent() { + List joinItems = odiiDataRepository.joinWithMaster(); + + int chunkSize = 10; + for (int i = 287; i < 500; i += chunkSize) { + List chunk = joinItems.subList(i, Math.min(i + chunkSize, joinItems.size())); + + try { + processChunk(chunk); + } catch (Exception e) { + return; + } + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void processChunk(List chunk) { + List docents = new ArrayList<>(); + + for (JoinItemDto data : chunk) { + log.info("current item id : " + data.id()); + String source = data.outl(); + + if (data.script() != null) { + source = data.script(); + } + + String prompt = String.format(DOCENT_SCRIPT.message, data.title(), source); + try { + String script = chatClient.prompt(prompt).call().content(); + String docentUrl = getDocentUrl(data.contentId(), script, "ko"); + + Docent docent = Docent.builder() + .contentId(data.contentId()) + .script(script) + .docentUrl(docentUrl) + .build(); + + docents.add(docent); + } catch (Exception e) { + log.error(e.getMessage()); + log.info("failed item id : " + data.id()); + } + } + + docentRepository.saveAll(docents); + } + + public String getDocentJson(){ + try{ + List docents = docentRepository.findAll(); + + List exportData = docents.stream() + .map(docent -> new DocentItemDto( + docent.getContentId(), + docent.getScript(), + docent.getDocentUrl() + )) + .toList(); + + File tempFile = File.createTempFile("docent_data_", ".json"); + + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + objectMapper.writeValue(tempFile, exportData); + + String s3Key = "core/docent/docent_data.json"; + + objectMapper + .getFactory() + .setStreamWriteConstraints( + StreamWriteConstraints.builder() + .maxNestingDepth(3000) + .build() + ); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType("application/json"); + metadata.setContentLength(tempFile.length()); + + amazonS3.putObject( + new PutObjectRequest(bucketName, s3Key, tempFile) + ); + + tempFile.delete(); + + return String.format("https://%s/%s", cloudFrontDomain, s3Key); + } + catch (Exception e){ + return null; + } + } + + public String getDocentUrl(String contentId, String script, String lang) { + try { + SynthesisInput input = SynthesisInput.newBuilder() + .setText(script) + .build(); + + String langCode = switch (lang) { + case "ko" -> "ko-KR"; + case "en" -> "en-US"; + default -> throw new IllegalStateException("Unexpected value: " + lang); + }; + + String voiceName = voices.get(Integer.parseInt(contentId) % voices.size()); + + VoiceSelectionParams voice = VoiceSelectionParams.newBuilder() + .setLanguageCode(langCode) + .setName(langCode + "-Chirp3-HD-"+ voiceName) + .build(); + + AudioConfig audioConfig = AudioConfig.newBuilder() + .setAudioEncoding(AudioEncoding.MP3) + .setSpeakingRate(1.0) + .build(); + + SynthesizeSpeechResponse response = textToSpeechClient.synthesizeSpeech(input, voice, audioConfig); + + byte[] audioContent = response.getAudioContent().toByteArray(); + String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String fileName = contentId + "_" + lang + "_" + date + ".mp3"; + + String s3Key = "core/docent/" + contentId + "/" + fileName; + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(audioContent.length); + metadata.setContentType("audio/mpeg"); + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(audioContent)) { + amazonS3.putObject( + new PutObjectRequest( + bucketName, + s3Key, + inputStream, + metadata + ) + ); + } + + return String.format("https://%s/%s", cloudFrontDomain, s3Key); + + } catch (Exception e) { + log.error("Failed to generate TTS for: {}", contentId, e); + return null; + } + } + public JsonNode fetchOdiiApi(int pageNo, int numOfRows) { this.key = ApiKeys.split(",")[0]; this.index = 0; RestClient client = RestClient.create(); - URI uri = UriComponentsBuilder - .fromHttpUrl("https://apis.data.go.kr/B551011/Odii/storyBasedList") - .queryParam("serviceKey", this.key) - .queryParam("MobileApp", "AppTest") - .queryParam("MobileOS", "ETC") - .queryParam("pageNo", pageNo) - .queryParam("numOfRows", numOfRows) - .queryParam("_type", "json") - .queryParam("langCode", "ko") - .build(true) - .toUri(); + URI uri = UriComponentsBuilder.fromHttpUrl("https://apis.data.go.kr/B551011/Odii/storyBasedList").queryParam("serviceKey", this.key).queryParam("MobileApp", "AppTest").queryParam("MobileOS", "ETC").queryParam("pageNo", pageNo).queryParam("numOfRows", numOfRows).queryParam("_type", "json").queryParam("langCode", "ko").build(true).toUri(); try { return client.get().uri(uri).retrieve().body(JsonNode.class); } catch (Exception e) { diff --git a/src/main/java/com/earseo/core/service/ai/Prompts.java b/src/main/java/com/earseo/core/service/ai/Prompts.java new file mode 100644 index 0000000..521cf1b --- /dev/null +++ b/src/main/java/com/earseo/core/service/ai/Prompts.java @@ -0,0 +1,39 @@ +package com.earseo.core.service.ai; + +public enum Prompts { + + DOCENT_SCRIPT(""" + 다음 관광지에 대한 도슨트 스크립트를 생성해주세요. + + 제목: %s + 설명: %s + + # 작성 규칙 + - 존댓말 사용 (~이에요, ~보세요, ~할 수 있어요) + - 역할극, 대사 형식, 3인칭 서술 금지 + - 질문형 문장 금지 + - 차분하고 따뜻한 해설톤 유지 + - 분량: 900~1,500자 (약 3분 분량) + + # 금지 표현 + "안녕하세요", "와, 정말 멋지죠?", "저는 ~입니다", 해설자 인칭 표현 + + # 구성 + 1. 시작 (2~3문장): 현재 장소 소개 + 2. 본론 (500~800자): 역사, 구조, 특징, 풍경 + 3. 확장 (200~400자): 관련 인물, 전설, 문화행사 + 4. 마무리 (2~3문장): 감성 정리 + + # 표현 방식 + - "이곳은", "여기에서는", "보시면" 활용 + - 객관적 사실 + 감성적 묘사 균형 + - 담백한 감정 표현 + - 정보를 자연스럽게 재구성하되, 핵심 내용은 빠뜨리지 않기 + """), + ; + + public final String message; + Prompts(String message) { + this.message = message; + } +} diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index cb07d82..d9485c7 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -52,6 +52,9 @@ spring: master: ${SPRING_DATA_REDIS_SENTINEL_MASTER:valkey-master} nodes: ${SPRING_DATA_REDIS_SENTINEL_NODES:localhost:26379} database: 1 + ai: + openai: + api-key: ${OPEN_AI_API_KEY} otel: propagators: diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 7af9a03..c1f2c72 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -50,7 +50,9 @@ spring: master: ${SPRING_DATA_REDIS_SENTINEL_MASTER:valkey-master} nodes: ${SPRING_DATA_REDIS_SENTINEL_NODES:localhost:26379,localhost:26380,localhost:26381} database: 6 - + ai: + openai: + api-key: ${OPEN_AI_API_KEY} management: endpoints: web: @@ -85,4 +87,4 @@ logging: org.hibernate.SQL: ${LOGGING_LEVEL_ORG_HIBERNATE_SQL:debug} api: - key: ${API_KEY} + key: ${API_KEY} \ No newline at end of file diff --git a/src/main/resources/application-test.yaml b/src/main/resources/application-test.yaml index baa0d53..cc77d1b 100644 --- a/src/main/resources/application-test.yaml +++ b/src/main/resources/application-test.yaml @@ -46,7 +46,9 @@ spring: database: 6 host: localhost port: 6379 - + ai: + openai: + api-key: openaikey management: endpoints: web: From 8d396978022cd534986f62b7fcf9cbb2a0d8b7ed Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 00:27:06 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[#2]=20fix:=20=EB=8F=84=EC=8A=A8=ED=8A=B8?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EB=B2=94=EC=9C=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/earseo/core/service/DocentService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/earseo/core/service/DocentService.java b/src/main/java/com/earseo/core/service/DocentService.java index 1b38b4d..b3c6e44 100644 --- a/src/main/java/com/earseo/core/service/DocentService.java +++ b/src/main/java/com/earseo/core/service/DocentService.java @@ -91,7 +91,7 @@ public void getDocent() { List joinItems = odiiDataRepository.joinWithMaster(); int chunkSize = 10; - for (int i = 287; i < 500; i += chunkSize) { + for (int i = 0; i < joinItems.size(); i += chunkSize) { List chunk = joinItems.subList(i, Math.min(i + chunkSize, joinItems.size())); try { From 29a9549c495a7b6d29470abfb8dc0dded1dbb02e Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 02:37:38 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[#2]=20feat:=20=EC=9D=B4=EC=95=BC?= =?UTF-8?q?=EA=B8=B0=20=EB=8F=84=EC=8A=A8=ED=8A=B8=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?API=20-=20=EC=9D=B4=EC=95=BC=EA=B8=B0=20=EC=8A=A4=ED=8C=9F=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EB=8F=84=EC=8A=A8=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../earseo/core/BackendCoreApplication.java | 2 + .../internal/CoreInternalController.java | 23 +++++ .../core/dto/internal/StoryDocentRequest.java | 9 ++ .../dto/internal/StoryDocentResponse.java | 8 ++ .../service/internal/InternalService.java | 93 +++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 src/main/java/com/earseo/core/controller/internal/CoreInternalController.java create mode 100644 src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java create mode 100644 src/main/java/com/earseo/core/dto/internal/StoryDocentResponse.java create mode 100644 src/main/java/com/earseo/core/service/internal/InternalService.java diff --git a/build.gradle b/build.gradle index 26b19d6..a661b34 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ plugins { ext { springAiVersion = "1.1.0" + springCloudVersion = "2025.0.0" } group = 'com.earseo' @@ -25,6 +26,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' runtimeOnly 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -61,6 +63,7 @@ dependencies { dependencyManagement { imports { mavenBom "org.springframework.ai:spring-ai-bom:$springAiVersion" + mavenBom "org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion" } } diff --git a/src/main/java/com/earseo/core/BackendCoreApplication.java b/src/main/java/com/earseo/core/BackendCoreApplication.java index 51c5c14..41bf335 100644 --- a/src/main/java/com/earseo/core/BackendCoreApplication.java +++ b/src/main/java/com/earseo/core/BackendCoreApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication +@EnableFeignClients public class BackendCoreApplication { public static void main(String[] args) { diff --git a/src/main/java/com/earseo/core/controller/internal/CoreInternalController.java b/src/main/java/com/earseo/core/controller/internal/CoreInternalController.java new file mode 100644 index 0000000..76e43c4 --- /dev/null +++ b/src/main/java/com/earseo/core/controller/internal/CoreInternalController.java @@ -0,0 +1,23 @@ +package com.earseo.core.controller.internal; + +import com.earseo.core.dto.internal.StoryDocentRequest; +import com.earseo.core.dto.internal.StoryDocentResponse; +import com.earseo.core.service.internal.InternalService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class CoreInternalController { + + private final InternalService internalService; + + @PostMapping("/internal/core/docent/story") + public List getStoryDocent(@RequestBody List request) { + return internalService.createStorySpotDocent(request); + } +} diff --git a/src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java b/src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java new file mode 100644 index 0000000..b221e9a --- /dev/null +++ b/src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java @@ -0,0 +1,9 @@ +package com.earseo.core.dto.internal; + +public record StoryDocentRequest( + Long storySpotId, + Long summaryId, + String summary, + String locale +) { +} diff --git a/src/main/java/com/earseo/core/dto/internal/StoryDocentResponse.java b/src/main/java/com/earseo/core/dto/internal/StoryDocentResponse.java new file mode 100644 index 0000000..f65c03c --- /dev/null +++ b/src/main/java/com/earseo/core/dto/internal/StoryDocentResponse.java @@ -0,0 +1,8 @@ +package com.earseo.core.dto.internal; + +public record StoryDocentResponse( + Long summaryId, + String docentScript, + String docentUrl +) { +} diff --git a/src/main/java/com/earseo/core/service/internal/InternalService.java b/src/main/java/com/earseo/core/service/internal/InternalService.java new file mode 100644 index 0000000..6f693df --- /dev/null +++ b/src/main/java/com/earseo/core/service/internal/InternalService.java @@ -0,0 +1,93 @@ +package com.earseo.core.service.internal; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.earseo.core.dto.internal.StoryDocentRequest; +import com.earseo.core.dto.internal.StoryDocentResponse; +import com.google.cloud.texttospeech.v1.*; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class InternalService { + + private final AmazonS3 amazonS3; + private final TextToSpeechClient textToSpeechClient; + + @Value(("${cloud.aws.s3.bucket}")) + private String bucketName; + @Value("${cloud.aws.cloudfront.domain}") + private String cloudFrontDomain; + + List voices = List.of("Kore", "Algieba", "Despina", "Enceladus"); + + public List createStorySpotDocent(List requests) { + List responses = new ArrayList<>(); + + for(StoryDocentRequest request : requests) { + try { + SynthesisInput input = SynthesisInput.newBuilder() + .setText(request.summary()) + .build(); + + String langCode = switch (request.locale()) { + case "KO" -> "ko-KR"; + case "EN" -> "en-US"; + default -> throw new IllegalStateException("Unexpected value: " + request.locale()); + }; + + int index = (int) (Math.random() * 4); + + String voiceName = voices.get(index); + + VoiceSelectionParams voice = VoiceSelectionParams.newBuilder() + .setLanguageCode(langCode) + .setName(langCode + "-Chirp3-HD-"+ voiceName) + .build(); + + AudioConfig audioConfig = AudioConfig.newBuilder() + .setAudioEncoding(AudioEncoding.MP3) + .setSpeakingRate(1.0) + .build(); + + SynthesizeSpeechResponse response = textToSpeechClient.synthesizeSpeech(input, voice, audioConfig); + + byte[] audioContent = response.getAudioContent().toByteArray(); + String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddhhmm")); + + String s3Key = "story/docent/" + request.storySpotId() + "/" + request.summaryId() + "/" + date; + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(audioContent.length); + metadata.setContentType("audio/mpeg"); + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(audioContent)) { + amazonS3.putObject( + new PutObjectRequest( + bucketName, + s3Key, + inputStream, + metadata + ) + ); + } + + String docentUrl = String.format("https://%s/%s", cloudFrontDomain, s3Key); + + responses.add(new StoryDocentResponse(request.summaryId(), request.summary(), docentUrl)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + return responses; + } +} From 6faf258f2f861b983824fe37fe2167a6dbb13b19 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 03:11:23 +0900 Subject: [PATCH 05/12] =?UTF-8?q?=20[#2]=20refactor:=20gcp=20=ED=81=AC?= =?UTF-8?q?=EB=A0=88=EB=8D=B4=EC=85=9C=20=EC=A0=81=EC=9A=A9=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../earseo/core/common/config/GoogleCloudConfig.java | 12 ++++++++---- src/main/resources/application-local.yaml | 6 +++++- src/main/resources/application-test.yaml | 6 +++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java b/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java index 30215ff..117d92b 100644 --- a/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java +++ b/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java @@ -5,22 +5,26 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.texttospeech.v1.TextToSpeechClient; import com.google.cloud.texttospeech.v1.TextToSpeechSettings; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Base64; @Configuration public class GoogleCloudConfig { + @Value("${gcp.credentials.base64:}") + private String credentialsBase64; + @Bean public TextToSpeechClient textToSpeechClient() throws IOException { - ClassPathResource resource = new ClassPathResource("gcp-service-account-key.json"); - - InputStream credentialsStream = resource.getInputStream(); - + byte[] decodedBytes = Base64.getDecoder().decode(credentialsBase64); + InputStream credentialsStream = new ByteArrayInputStream(decodedBytes); Credentials credentials = GoogleCredentials.fromStream(credentialsStream); TextToSpeechSettings settings = TextToSpeechSettings.newBuilder() diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index c1f2c72..f54246b 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -87,4 +87,8 @@ logging: org.hibernate.SQL: ${LOGGING_LEVEL_ORG_HIBERNATE_SQL:debug} api: - key: ${API_KEY} \ No newline at end of file + key: ${API_KEY} + +gcp: + credentials: + base64: ${GCP_CREDENTIALS_BASE64:key} \ No newline at end of file diff --git a/src/main/resources/application-test.yaml b/src/main/resources/application-test.yaml index cc77d1b..0c77a7b 100644 --- a/src/main/resources/application-test.yaml +++ b/src/main/resources/application-test.yaml @@ -77,4 +77,8 @@ cloud: domain: domain api: - key: key \ No newline at end of file + key: key + +gcp: + credentials: + base64: ${GCP_CREDENTIALS_BASE64:key} \ No newline at end of file From 4431fd80280fcfe53a2819b1e1060167ebf266fa Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 03:35:29 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[#2]=20fix:=20=EC=9D=B4=EC=95=BC=EA=B8=B0?= =?UTF-8?q?=20=EC=8A=A4=ED=8C=9F=20=EB=8F=84=EC=8A=A8=ED=8A=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=99=95=EC=9E=A5=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/earseo/core/service/internal/InternalService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/earseo/core/service/internal/InternalService.java b/src/main/java/com/earseo/core/service/internal/InternalService.java index 6f693df..9b94cff 100644 --- a/src/main/java/com/earseo/core/service/internal/InternalService.java +++ b/src/main/java/com/earseo/core/service/internal/InternalService.java @@ -64,7 +64,7 @@ public List createStorySpotDocent(List byte[] audioContent = response.getAudioContent().toByteArray(); String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddhhmm")); - String s3Key = "story/docent/" + request.storySpotId() + "/" + request.summaryId() + "/" + date; + String s3Key = "story/docent/" + request.storySpotId() + "/" + request.summaryId() + "/" + date + ".mp3"; ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(audioContent.length); metadata.setContentType("audio/mpeg"); From 92c999d848d9713c4cee54f45c7d69923141cb69 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 03:46:43 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[#2]=20refactor:=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=EC=82=AC=ED=95=AD=EC=97=90=20=EB=A7=9E=EA=B2=8C=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/earseo/core/dto/internal/StoryDocentRequest.java | 1 - .../java/com/earseo/core/service/internal/InternalService.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java b/src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java index b221e9a..f59964e 100644 --- a/src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java +++ b/src/main/java/com/earseo/core/dto/internal/StoryDocentRequest.java @@ -1,7 +1,6 @@ package com.earseo.core.dto.internal; public record StoryDocentRequest( - Long storySpotId, Long summaryId, String summary, String locale diff --git a/src/main/java/com/earseo/core/service/internal/InternalService.java b/src/main/java/com/earseo/core/service/internal/InternalService.java index 9b94cff..e8d50d9 100644 --- a/src/main/java/com/earseo/core/service/internal/InternalService.java +++ b/src/main/java/com/earseo/core/service/internal/InternalService.java @@ -64,7 +64,7 @@ public List createStorySpotDocent(List byte[] audioContent = response.getAudioContent().toByteArray(); String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddhhmm")); - String s3Key = "story/docent/" + request.storySpotId() + "/" + request.summaryId() + "/" + date + ".mp3"; + String s3Key = "story/docent/" + request.summaryId() + "/" + date + ".mp3"; ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(audioContent.length); metadata.setContentType("audio/mpeg"); From c2f4c7319022047238ca8c4e00defce285bd4a88 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 03:54:42 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[#2]=20test:=20test=20tts=20config=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/config/TestGoogleCloudConfig.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java diff --git a/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java b/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java new file mode 100644 index 0000000..5a7b242 --- /dev/null +++ b/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java @@ -0,0 +1,17 @@ +package com.earseo.core.config; + +import com.google.cloud.texttospeech.v1.TextToSpeechClient; +import org.mockito.Mockito; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +@TestConfiguration +public class TestGoogleCloudConfig { + + @Bean + @Primary + public TextToSpeechClient textToSpeechClientMock() { + return Mockito.mock(TextToSpeechClient.class); + } +} \ No newline at end of file From 0d7023367984caea89e7447d6e4c4b6cc3231962 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 04:10:23 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[#2]=20test:=20test=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java b/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java index 5a7b242..09b84e1 100644 --- a/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java +++ b/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java @@ -5,8 +5,10 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; @TestConfiguration +@Profile("test") public class TestGoogleCloudConfig { @Bean From c65ec9b9f66c05045c3f67ac37096e95dddfeb04 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 04:15:48 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[#2]=20test:=20test=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../earseo/core/BackendCoreApplicationTests.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/earseo/core/BackendCoreApplicationTests.java b/src/test/java/com/earseo/core/BackendCoreApplicationTests.java index b304de8..8f95712 100644 --- a/src/test/java/com/earseo/core/BackendCoreApplicationTests.java +++ b/src/test/java/com/earseo/core/BackendCoreApplicationTests.java @@ -1,13 +1,27 @@ package com.earseo.core; +import com.google.cloud.texttospeech.v1.TextToSpeechClient; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; import org.springframework.test.context.ActiveProfiles; @ActiveProfiles("test") @SpringBootTest class BackendCoreApplicationTests { + @TestConfiguration + static class TestConfig { + @Bean + @Primary + public TextToSpeechClient textToSpeechClient() { + return Mockito.mock(TextToSpeechClient.class); + } + } + @Test void contextLoads() { } From 4418566487a932dff62b1e44d9551168bc9c58d6 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 04:20:07 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[#2]=20test:=20test=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/config/TestGoogleCloudConfig.java | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java diff --git a/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java b/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java deleted file mode 100644 index 09b84e1..0000000 --- a/src/test/java/com/earseo/core/config/TestGoogleCloudConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.earseo.core.config; - -import com.google.cloud.texttospeech.v1.TextToSpeechClient; -import org.mockito.Mockito; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; - -@TestConfiguration -@Profile("test") -public class TestGoogleCloudConfig { - - @Bean - @Primary - public TextToSpeechClient textToSpeechClientMock() { - return Mockito.mock(TextToSpeechClient.class); - } -} \ No newline at end of file From eda2c3e9b4697ee014272f1ee0005bec74b43510 Mon Sep 17 00:00:00 2001 From: yoonho Date: Mon, 1 Dec 2025 04:25:24 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[#2]=20refactor:=20config=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/earseo/core/common/config/GoogleCloudConfig.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java b/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java index 117d92b..3135a25 100644 --- a/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java +++ b/src/main/java/com/earseo/core/common/config/GoogleCloudConfig.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.ClassPathResource; import java.io.ByteArrayInputStream; @@ -16,6 +17,7 @@ import java.util.Base64; @Configuration +@Profile("!test") public class GoogleCloudConfig { @Value("${gcp.credentials.base64:}")