diff --git a/src/integrationTest/java/com/ject/vs/vote/adapter/web/VoteApiIntegrationTest.java b/src/integrationTest/java/com/ject/vs/vote/adapter/web/VoteApiIntegrationTest.java new file mode 100644 index 00000000..732f30af --- /dev/null +++ b/src/integrationTest/java/com/ject/vs/vote/adapter/web/VoteApiIntegrationTest.java @@ -0,0 +1,316 @@ +package com.ject.vs.vote.adapter.web; + +import com.ject.vs.user.domain.User; +import com.ject.vs.user.domain.UserRepository; +import com.ject.vs.vote.domain.Vote; +import com.ject.vs.vote.domain.VoteOption; +import com.ject.vs.vote.domain.VoteOptionRepository; +import com.ject.vs.vote.domain.VoteRepository; +import com.ject.vs.vote.domain.VoteStatus; +import com.ject.vs.vote.domain.VoteType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +@DisplayName("Vote API 통합 테스트") +class VoteApiIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private VoteRepository voteRepository; + + @Autowired + private VoteOptionRepository voteOptionRepository; + + @Autowired + private UserRepository userRepository; + + private User testUser; + + @BeforeEach + void setUp() { + // 테스트용 사용자 생성 + testUser = userRepository.save(User.createWithSub("test-sub-" + System.currentTimeMillis())); + } + + private void authenticateAs(Long userId) { + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userId, null, AuthorityUtils.NO_AUTHORITIES); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private void clearAuthentication() { + SecurityContextHolder.clearContext(); + } + + @Nested + @DisplayName("POST /api/votes - 투표 생성") + class CreateVote { + + @Test + @DisplayName("인증된 사용자가 투표 생성 시 DB에 저장된다") + void 인증된_사용자가_투표_생성시_DB에_저장된다() throws Exception { + // given + authenticateAs(testUser.getId()); + + String requestBody = """ + { + "type": "GENERAL", + "title": "테스트 투표 제목", + "content": "테스트 투표 내용", + "thumbnailUrl": "https://example.com/thumb.png", + "duration": "HOURS_12", + "optionA": "선택지 A", + "optionB": "선택지 B" + } + """; + + // when + MvcResult result = mockMvc.perform(post("/api/votes") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.voteId").isNumber()) + .andExpect(jsonPath("$.status").value("ONGOING")) + .andExpect(jsonPath("$.endAt").isNotEmpty()) + .andReturn(); + + // then - DB에 저장 확인 + List votes = voteRepository.findAll(); + assertThat(votes).hasSize(1); + + Vote savedVote = votes.get(0); + assertThat(savedVote.getType()).isEqualTo(VoteType.GENERAL); + assertThat(savedVote.getTitle()).isEqualTo("테스트 투표 제목"); + assertThat(savedVote.getContent()).isEqualTo("테스트 투표 내용"); + assertThat(savedVote.getThumbnailUrl()).isEqualTo("https://example.com/thumb.png"); + assertThat(savedVote.getEndAt()).isNotNull(); + + // VoteOption도 저장 확인 + List options = voteOptionRepository.findByVoteIdOrderByPosition(savedVote.getId()); + assertThat(options).hasSize(2); + assertThat(options.get(0).getLabel()).isEqualTo("선택지 A"); + assertThat(options.get(1).getLabel()).isEqualTo("선택지 B"); + } + + @Test + @DisplayName("몰입형 투표 생성 시 imageUrl도 함께 저장된다") + void 몰입형_투표_생성시_imageUrl도_저장된다() throws Exception { + // given + authenticateAs(testUser.getId()); + + String requestBody = """ + { + "type": "IMMERSIVE", + "title": "몰입형 투표", + "content": "몰입형 내용", + "thumbnailUrl": "https://example.com/thumb.png", + "imageUrl": "https://example.com/image.png", + "duration": "HOURS_24", + "optionA": "A", + "optionB": "B" + } + """; + + // when + mockMvc.perform(post("/api/votes") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isCreated()); + + // then + Vote savedVote = voteRepository.findAll().get(0); + assertThat(savedVote.getType()).isEqualTo(VoteType.IMMERSIVE); + assertThat(savedVote.getImageUrl()).isEqualTo("https://example.com/image.png"); + } + + @Test + @DisplayName("인증되지 않은 사용자는 403 Forbidden") + void 인증되지_않은_사용자는_403() throws Exception { + // given + clearAuthentication(); + + String requestBody = """ + { + "type": "GENERAL", + "title": "테스트", + "thumbnailUrl": "https://example.com/thumb.png", + "duration": "HOURS_12", + "optionA": "A", + "optionB": "B" + } + """; + + // when & then + mockMvc.perform(post("/api/votes") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isForbidden()); + + // DB에 저장되지 않음 + assertThat(voteRepository.findAll()).isEmpty(); + } + + @Test + @DisplayName("다양한 duration으로 투표 생성 시 endAt이 정확히 계산된다") + void 다양한_duration으로_투표_생성시_endAt_계산_확인() throws Exception { + // given + authenticateAs(testUser.getId()); + + String[] durations = {"HOURS_1", "HOURS_6", "HOURS_12", "HOURS_24"}; + + for (String duration : durations) { + String requestBody = String.format(""" + { + "type": "GENERAL", + "title": "투표 %s", + "thumbnailUrl": "https://example.com/thumb.png", + "duration": "%s", + "optionA": "A", + "optionB": "B" + } + """, duration, duration); + + // when + mockMvc.perform(post("/api/votes") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.endAt").isNotEmpty()); + } + + // then + List votes = voteRepository.findAll(); + assertThat(votes).hasSize(4); + + // 모든 투표의 endAt이 설정되어 있는지 확인 + for (Vote vote : votes) { + assertThat(vote.getEndAt()).isNotNull(); + } + } + } + + @Nested + @DisplayName("GET /api/votes/{voteId} - 투표 상세 조회") + class GetVoteDetail { + + @Test + @DisplayName("투표 상세 조회 시 endAt이 응답에 포함된다") + void 투표_상세_조회시_endAt_포함() throws Exception { + // given - 투표 생성 + authenticateAs(testUser.getId()); + + String createRequest = """ + { + "type": "GENERAL", + "title": "상세 조회 테스트", + "content": "내용", + "thumbnailUrl": "https://example.com/thumb.png", + "duration": "HOURS_12", + "optionA": "A", + "optionB": "B" + } + """; + + MvcResult createResult = mockMvc.perform(post("/api/votes") + .contentType(MediaType.APPLICATION_JSON) + .content(createRequest)) + .andExpect(status().isCreated()) + .andReturn(); + + // voteId 추출 + String responseBody = createResult.getResponse().getContentAsString(); + Long voteId = voteRepository.findAll().get(0).getId(); + + // when & then - 상세 조회 + mockMvc.perform(get("/api/votes/{voteId}", voteId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.voteId").value(voteId)) + .andExpect(jsonPath("$.title").value("상세 조회 테스트")) + .andExpect(jsonPath("$.endAt").isNotEmpty()) + .andExpect(jsonPath("$.status").value("ONGOING")); + } + } + + @Nested + @DisplayName("전체 플로우 테스트") + class FullFlowTest { + + @Test + @DisplayName("투표 생성 → 조회 → DB 저장 전체 플로우") + void 투표_생성_조회_DB_저장_전체_플로우() throws Exception { + // 1. 투표 생성 + authenticateAs(testUser.getId()); + + String createRequest = """ + { + "type": "GENERAL", + "title": "전체 플로우 테스트", + "content": "테스트 내용입니다", + "thumbnailUrl": "https://example.com/thumb.png", + "duration": "HOURS_24", + "optionA": "찬성", + "optionB": "반대" + } + """; + + mockMvc.perform(post("/api/votes") + .contentType(MediaType.APPLICATION_JSON) + .content(createRequest)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.voteId").isNumber()) + .andExpect(jsonPath("$.status").value("ONGOING")) + .andExpect(jsonPath("$.endAt").isNotEmpty()); + + // 2. DB 저장 확인 + List votes = voteRepository.findAll(); + assertThat(votes).hasSize(1); + + Vote savedVote = votes.get(0); + assertThat(savedVote.getTitle()).isEqualTo("전체 플로우 테스트"); + assertThat(savedVote.getContent()).isEqualTo("테스트 내용입니다"); + assertThat(savedVote.getEndAt()).isNotNull(); + assertThat(savedVote.getStatus()).isEqualTo(VoteStatus.ONGOING); + + // 3. VoteOption 저장 확인 + List options = voteOptionRepository.findByVoteIdOrderByPosition(savedVote.getId()); + assertThat(options).hasSize(2); + assertThat(options.get(0).getLabel()).isEqualTo("찬성"); + assertThat(options.get(1).getLabel()).isEqualTo("반대"); + + // 4. 투표 상세 조회 + mockMvc.perform(get("/api/votes/{voteId}", savedVote.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("전체 플로우 테스트")) + .andExpect(jsonPath("$.endAt").isNotEmpty()) + .andExpect(jsonPath("$.options[0].label").value("찬성")) + .andExpect(jsonPath("$.options[1].label").value("반대")); + } + } +} diff --git a/src/integrationTest/java/com/ject/vs/vote/domain/VoteRepositoryIntegrationTest.java b/src/integrationTest/java/com/ject/vs/vote/domain/VoteRepositoryIntegrationTest.java new file mode 100644 index 00000000..cebc9229 --- /dev/null +++ b/src/integrationTest/java/com/ject/vs/vote/domain/VoteRepositoryIntegrationTest.java @@ -0,0 +1,337 @@ +package com.ject.vs.vote.domain; + +import com.ject.vs.config.JpaAuditingConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.test.context.ActiveProfiles; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import(JpaAuditingConfig.class) +@DisplayName("Vote 저장/조회 통합 테스트") +class VoteRepositoryIntegrationTest { + + private static final Instant BASE_TIME = Instant.parse("2025-01-01T00:00:00Z"); + private static final Clock FIXED_CLOCK = Clock.fixed(BASE_TIME, ZoneOffset.UTC); + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private VoteRepository voteRepository; + + @Autowired + private VoteOptionRepository voteOptionRepository; + + @Nested + @DisplayName("Vote 저장 테스트") + class VoteSaveTest { + + @Test + @DisplayName("일반형 투표 저장 시 모든 필드가 DB에 정확히 저장된다") + void 일반형_투표_저장_시_모든_필드가_DB에_저장된다() { + // given + Vote vote = Vote.create( + VoteType.GENERAL, + "테스트 제목", + "테스트 내용", + "https://example.com/thumb.png", + null, + Duration.ofHours(24), + FIXED_CLOCK + ); + + // when + Vote savedVote = voteRepository.save(vote); + entityManager.flush(); + entityManager.clear(); + + // then + Vote foundVote = voteRepository.findById(savedVote.getId()).orElseThrow(); + + assertThat(foundVote.getId()).isNotNull(); + assertThat(foundVote.getType()).isEqualTo(VoteType.GENERAL); + assertThat(foundVote.getTitle()).isEqualTo("테스트 제목"); + assertThat(foundVote.getContent()).isEqualTo("테스트 내용"); + assertThat(foundVote.getThumbnailUrl()).isEqualTo("https://example.com/thumb.png"); + assertThat(foundVote.getImageUrl()).isNull(); + assertThat(foundVote.getEndAt()).isEqualTo(BASE_TIME.plus(Duration.ofHours(24))); + assertThat(foundVote.getCreatedAt()).isNotNull(); + assertThat(foundVote.getUpdatedAt()).isNotNull(); + } + + @Test + @DisplayName("몰입형 투표 저장 시 imageUrl이 함께 저장된다") + void 몰입형_투표_저장_시_imageUrl이_함께_저장된다() { + // given + Vote vote = Vote.create( + VoteType.IMMERSIVE, + "몰입형 제목", + "몰입형 내용", + "https://example.com/thumb.png", + "https://example.com/image.png", + Duration.ofHours(48), + FIXED_CLOCK + ); + + // when + Vote savedVote = voteRepository.save(vote); + entityManager.flush(); + entityManager.clear(); + + // then + Vote foundVote = voteRepository.findById(savedVote.getId()).orElseThrow(); + + assertThat(foundVote.getType()).isEqualTo(VoteType.IMMERSIVE); + assertThat(foundVote.getImageUrl()).isEqualTo("https://example.com/image.png"); + assertThat(foundVote.getEndAt()).isEqualTo(BASE_TIME.plus(Duration.ofHours(48))); + } + + @Test + @DisplayName("다양한 duration으로 투표 저장 시 endAt이 정확히 계산되어 저장된다") + void 다양한_duration으로_투표_저장_시_endAt이_정확히_계산된다() { + // given + Vote vote1h = Vote.create(VoteType.GENERAL, "1시간", null, "thumb", null, + Duration.ofHours(1), FIXED_CLOCK); + Vote vote12h = Vote.create(VoteType.GENERAL, "12시간", null, "thumb", null, + Duration.ofHours(12), FIXED_CLOCK); + Vote vote24h = Vote.create(VoteType.GENERAL, "24시간", null, "thumb", null, + Duration.ofHours(24), FIXED_CLOCK); + + // when + voteRepository.saveAll(List.of(vote1h, vote12h, vote24h)); + entityManager.flush(); + entityManager.clear(); + + // then + List allVotes = voteRepository.findAll(); + + Vote found1h = allVotes.stream().filter(v -> v.getTitle().equals("1시간")).findFirst().orElseThrow(); + Vote found12h = allVotes.stream().filter(v -> v.getTitle().equals("12시간")).findFirst().orElseThrow(); + Vote found24h = allVotes.stream().filter(v -> v.getTitle().equals("24시간")).findFirst().orElseThrow(); + + assertThat(found1h.getEndAt()).isEqualTo(BASE_TIME.plus(Duration.ofHours(1))); + assertThat(found12h.getEndAt()).isEqualTo(BASE_TIME.plus(Duration.ofHours(12))); + assertThat(found24h.getEndAt()).isEqualTo(BASE_TIME.plus(Duration.ofHours(24))); + } + } + + @Nested + @DisplayName("Vote + VoteOption 함께 저장 테스트") + class VoteWithOptionsTest { + + @Test + @DisplayName("투표와 옵션이 함께 저장되고 조회된다") + void 투표와_옵션이_함께_저장되고_조회된다() { + // given + Vote vote = Vote.create(VoteType.GENERAL, "선택 투표", "A vs B", + "thumb", null, Duration.ofHours(24), FIXED_CLOCK); + Vote savedVote = voteRepository.save(vote); + + VoteOption optionA = VoteOption.of(savedVote, "선택지 A", 0); + VoteOption optionB = VoteOption.of(savedVote, "선택지 B", 1); + voteOptionRepository.saveAll(List.of(optionA, optionB)); + + entityManager.flush(); + entityManager.clear(); + + // when + List foundOptions = voteOptionRepository.findByVoteIdOrderByPosition(savedVote.getId()); + + // then + assertThat(foundOptions).hasSize(2); + assertThat(foundOptions.get(0).getLabel()).isEqualTo("선택지 A"); + assertThat(foundOptions.get(0).getPosition()).isEqualTo(0); + assertThat(foundOptions.get(1).getLabel()).isEqualTo("선택지 B"); + assertThat(foundOptions.get(1).getPosition()).isEqualTo(1); + } + } + + @Nested + @DisplayName("진행중/종료된 투표 조회 테스트") + class VoteStatusQueryTest { + + private Vote ongoingVote; + private Vote expiredVote; + + @BeforeEach + void setUp() { + // 진행 중인 투표: endAt = BASE_TIME + 24h + ongoingVote = voteRepository.save( + Vote.create(VoteType.GENERAL, "진행중 투표", null, "thumb", null, + Duration.ofHours(24), FIXED_CLOCK) + ); + + // 종료된 투표: endAt = BASE_TIME + 1h (조회 시점 BASE_TIME + 2h 기준으로 종료됨) + expiredVote = voteRepository.save( + Vote.create(VoteType.GENERAL, "종료된 투표", null, "thumb", null, + Duration.ofHours(1), FIXED_CLOCK) + ); + + entityManager.flush(); + entityManager.clear(); + } + + @Test + @DisplayName("findOngoingVotes는 현재 시각 기준 진행 중인 투표만 반환한다") + void findOngoingVotes는_진행중인_투표만_반환한다() { + // given + Instant queryTime = BASE_TIME.plus(Duration.ofHours(2)); // 1시간짜리는 종료됨 + + // when + List ongoingVotes = voteRepository.findOngoingVotes(queryTime); + + // then + assertThat(ongoingVotes).hasSize(1); + assertThat(ongoingVotes.get(0).getTitle()).isEqualTo("진행중 투표"); + } + + @Test + @DisplayName("findExpiredOngoing은 현재 시각 기준 종료된 투표를 반환한다") + void findExpiredOngoing은_종료된_투표를_반환한다() { + // given + Instant queryTime = BASE_TIME.plus(Duration.ofHours(2)); + + // when + List expiredVotes = voteRepository.findExpiredOngoing(queryTime); + + // then + assertThat(expiredVotes).hasSize(1); + assertThat(expiredVotes.get(0).getTitle()).isEqualTo("종료된 투표"); + } + + @Test + @DisplayName("getStatus는 endAt 기준으로 ONGOING/ENDED를 정확히 반환한다") + void getStatus는_endAt_기준으로_상태를_반환한다() { + // given + Vote foundOngoing = voteRepository.findById(ongoingVote.getId()).orElseThrow(); + Vote foundExpired = voteRepository.findById(expiredVote.getId()).orElseThrow(); + + Clock afterExpiryClock = Clock.fixed(BASE_TIME.plus(Duration.ofHours(2)), ZoneOffset.UTC); + + // when & then + assertThat(foundOngoing.getStatus(afterExpiryClock)).isEqualTo(VoteStatus.ONGOING); + assertThat(foundExpired.getStatus(afterExpiryClock)).isEqualTo(VoteStatus.ENDED); + } + } + + @Nested + @DisplayName("종료임박순 조회 테스트") + class EndingSoonQueryTest { + + @Test + @DisplayName("findOngoingOrderByEndAtAsc는 종료 시각이 가까운 순으로 정렬한다") + void findOngoingOrderByEndAtAsc는_종료시각_오름차순_정렬() { + // given + Vote vote3h = voteRepository.save( + Vote.create(VoteType.GENERAL, "3시간 후 종료", null, "thumb", null, + Duration.ofHours(3), FIXED_CLOCK) + ); + Vote vote1h = voteRepository.save( + Vote.create(VoteType.GENERAL, "1시간 후 종료", null, "thumb", null, + Duration.ofHours(1), FIXED_CLOCK) + ); + Vote vote5h = voteRepository.save( + Vote.create(VoteType.GENERAL, "5시간 후 종료", null, "thumb", null, + Duration.ofHours(5), FIXED_CLOCK) + ); + + entityManager.flush(); + entityManager.clear(); + + Instant now = BASE_TIME.plusSeconds(1); // 아직 모두 진행 중 + + // when + Slice result = voteRepository.findOngoingOrderByEndAtAsc(now, PageRequest.of(0, 10)); + + // then + List votes = result.getContent(); + assertThat(votes).hasSize(3); + assertThat(votes.get(0).getTitle()).isEqualTo("1시간 후 종료"); + assertThat(votes.get(1).getTitle()).isEqualTo("3시간 후 종료"); + assertThat(votes.get(2).getTitle()).isEqualTo("5시간 후 종료"); + } + } + + @Nested + @DisplayName("최신순 조회 테스트") + class LatestQueryTest { + + @Test + @DisplayName("findAllByOrderByIdDesc는 최신 투표부터 반환한다") + void findAllByOrderByIdDesc는_최신순_정렬() { + // given + Vote vote1 = voteRepository.save( + Vote.create(VoteType.GENERAL, "첫번째", null, "thumb", null, + Duration.ofHours(24), FIXED_CLOCK) + ); + Vote vote2 = voteRepository.save( + Vote.create(VoteType.GENERAL, "두번째", null, "thumb", null, + Duration.ofHours(24), FIXED_CLOCK) + ); + Vote vote3 = voteRepository.save( + Vote.create(VoteType.GENERAL, "세번째", null, "thumb", null, + Duration.ofHours(24), FIXED_CLOCK) + ); + + entityManager.flush(); + entityManager.clear(); + + // when + Slice result = voteRepository.findAllByOrderByIdDesc(PageRequest.of(0, 10)); + + // then + List votes = result.getContent(); + assertThat(votes).hasSize(3); + assertThat(votes.get(0).getTitle()).isEqualTo("세번째"); + assertThat(votes.get(1).getTitle()).isEqualTo("두번째"); + assertThat(votes.get(2).getTitle()).isEqualTo("첫번째"); + } + } + + @Nested + @DisplayName("AI Insight 캐싱 테스트") + class AiInsightCacheTest { + + @Test + @DisplayName("AI Insight가 저장되고 조회된다") + void AI_Insight가_저장되고_조회된다() { + // given + Vote vote = voteRepository.save( + Vote.create(VoteType.GENERAL, "AI 분석 투표", null, "thumb", null, + Duration.ofHours(24), FIXED_CLOCK) + ); + vote.cacheAiInsight("AI 분석 헤드라인", "AI 분석 본문 내용"); + voteRepository.save(vote); + + entityManager.flush(); + entityManager.clear(); + + // when + Vote foundVote = voteRepository.findById(vote.getId()).orElseThrow(); + + // then + assertThat(foundVote.hasAiInsight()).isTrue(); + assertThat(foundVote.getAiInsightHeadline()).isEqualTo("AI 분석 헤드라인"); + assertThat(foundVote.getAiInsightBody()).isEqualTo("AI 분석 본문 내용"); + } + } +} diff --git a/src/main/java/com/ject/vs/config/OpenApiConfig.java b/src/main/java/com/ject/vs/config/OpenApiConfig.java index 5ad7d419..2081fd96 100644 --- a/src/main/java/com/ject/vs/config/OpenApiConfig.java +++ b/src/main/java/com/ject/vs/config/OpenApiConfig.java @@ -5,16 +5,23 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.List; + @Configuration public class OpenApiConfig { + @Value("${springdoc.server-url:}") + private String serverUrl; + @Bean public OpenAPI openAPI() { String securitySchemeName = "bearerAuth"; - return new OpenAPI() + OpenAPI openAPI = new OpenAPI() .info(new Info() .title("VS Server API") .description("JECT 4기 2팀 VS Server API") @@ -26,5 +33,11 @@ public OpenAPI openAPI() { .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); + + if (serverUrl != null && !serverUrl.isBlank()) { + openAPI.servers(List.of(new Server().url(serverUrl).description("API Server"))); + } + + return openAPI; } } diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 902a7fed..54852355 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -55,6 +55,9 @@ app: allow-credentials: ${APP_CORS_ALLOW_CREDENTIALS:true} max-age: ${APP_CORS_MAX_AGE:3600} +springdoc: + server-url: https://api.vs.io.kr + management: server: port: 8081