From 8223b1a33f28ffe3fe1f69239504fc94d09f8106 Mon Sep 17 00:00:00 2001 From: HOYA Date: Tue, 9 Dec 2025 01:02:17 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EA=B0=80=EC=83=81=20=EC=B2=B4?= =?UTF-8?q?=ED=97=98=EC=9D=84=20=EC=9A=94=EC=B2=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메인 로직 개발 , 테스트 --- .../controller/ExceptionControllerAdvice.java | 7 +- .../common/util/ByteArrayMultipartFile.java | 48 ++++++++++++++ .../interaction/style/client/AiApiClient.java | 12 ++++ .../style/controller/StyleController.java | 9 +++ .../request/VirtualTryOnRequest.java | 21 ++++++ .../style/entity/StyleAnalysisEntity.java | 7 ++ .../repository/StyleAnalysisRepository.java | 3 + .../style/service/StyleService.java | 36 +++++++++- .../service/dto/AiVirtualTryOnRequest.java | 6 ++ .../service/dto/VirtualTryOnResponse.java | 5 ++ .../request/VirtualTryOnServiceRequest.java | 4 ++ .../style/service/StyleServiceTest.java | 66 ++++++++++++++++++- 12 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/seomse/common/util/ByteArrayMultipartFile.java create mode 100644 src/main/java/com/seomse/interaction/style/controller/request/VirtualTryOnRequest.java create mode 100644 src/main/java/com/seomse/interaction/style/service/dto/AiVirtualTryOnRequest.java create mode 100644 src/main/java/com/seomse/interaction/style/service/dto/VirtualTryOnResponse.java create mode 100644 src/main/java/com/seomse/interaction/style/service/request/VirtualTryOnServiceRequest.java diff --git a/src/main/java/com/seomse/common/controller/ExceptionControllerAdvice.java b/src/main/java/com/seomse/common/controller/ExceptionControllerAdvice.java index a9f2575..e6c6f19 100644 --- a/src/main/java/com/seomse/common/controller/ExceptionControllerAdvice.java +++ b/src/main/java/com/seomse/common/controller/ExceptionControllerAdvice.java @@ -52,8 +52,13 @@ public ResponseEntity bindException(BindException e) { public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) { log.error(e.getMessage(), e); + String errorMessage = e.getBindingResult() + .getAllErrors() + .get(0) + .getDefaultMessage(); + return ResponseEntity.status(BAD_REQUEST) - .body(new ExceptionResult(BAD_REQUEST.name(), e.getMessage())); + .body(new ExceptionResult(BAD_REQUEST.name(), errorMessage)); } /** Handles JWT authentication errors (JwtTokenException). */ diff --git a/src/main/java/com/seomse/common/util/ByteArrayMultipartFile.java b/src/main/java/com/seomse/common/util/ByteArrayMultipartFile.java new file mode 100644 index 0000000..881c151 --- /dev/null +++ b/src/main/java/com/seomse/common/util/ByteArrayMultipartFile.java @@ -0,0 +1,48 @@ +package com.seomse.common.util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.web.multipart.MultipartFile; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ByteArrayMultipartFile implements MultipartFile { + private final byte[] content; + private final String name; + private final String originalFilename; + private final String contentType; + + @Override + public boolean isEmpty() { + return content == null || content.length == 0; + } + + @Override + public long getSize() { + return content.length; + } + + @Override + public byte[] getBytes() throws IOException { + return content; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(content); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + try (FileOutputStream fos = new FileOutputStream(dest)) { + fos.write(content); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/seomse/interaction/style/client/AiApiClient.java b/src/main/java/com/seomse/interaction/style/client/AiApiClient.java index 2e58411..e32b1ed 100644 --- a/src/main/java/com/seomse/interaction/style/client/AiApiClient.java +++ b/src/main/java/com/seomse/interaction/style/client/AiApiClient.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import com.seomse.interaction.style.service.dto.AiVirtualTryOnRequest; import com.seomse.interaction.style.service.dto.StyleAnalysisRequest; import com.seomse.interaction.style.service.dto.StyleAnalysisResponse; @@ -25,4 +26,15 @@ public StyleAnalysisResponse analyzeStyle(StyleAnalysisRequest requestBody) { .bodyToMono(StyleAnalysisResponse.class) .block(); } + + public byte[] virtualTryOn(AiVirtualTryOnRequest requestBody) { + return webClient.post() + .uri("/virtual-try-on") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.IMAGE_PNG) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(byte[].class) + .block(); + } } diff --git a/src/main/java/com/seomse/interaction/style/controller/StyleController.java b/src/main/java/com/seomse/interaction/style/controller/StyleController.java index 339cd1d..cafe0f0 100644 --- a/src/main/java/com/seomse/interaction/style/controller/StyleController.java +++ b/src/main/java/com/seomse/interaction/style/controller/StyleController.java @@ -4,6 +4,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.ResponseStatus; @@ -11,8 +12,10 @@ import org.springframework.web.multipart.MultipartFile; import com.seomse.common.controller.ApiResponse; +import com.seomse.interaction.style.controller.request.VirtualTryOnRequest; import com.seomse.interaction.style.service.StyleService; import com.seomse.interaction.style.service.dto.StyleAnalysisResponse; +import com.seomse.interaction.style.service.dto.VirtualTryOnResponse; import lombok.RequiredArgsConstructor; @@ -28,4 +31,10 @@ public class StyleController { public ApiResponse analyzeStyle(@RequestPart MultipartFile image) throws IOException { return ApiResponse.created(styleService.callAnalyzeStyle(image)); } + + @PostMapping("/virtual-try-on") + @ResponseStatus(HttpStatus.OK) + public ApiResponse virtualTryOn(@RequestBody VirtualTryOnRequest request) throws IOException { + return ApiResponse.created(styleService.callVirtualTryOn(request.toServiceRequest())); + } } diff --git a/src/main/java/com/seomse/interaction/style/controller/request/VirtualTryOnRequest.java b/src/main/java/com/seomse/interaction/style/controller/request/VirtualTryOnRequest.java new file mode 100644 index 0000000..8db57c0 --- /dev/null +++ b/src/main/java/com/seomse/interaction/style/controller/request/VirtualTryOnRequest.java @@ -0,0 +1,21 @@ +package com.seomse.interaction.style.controller.request; + +import com.seomse.interaction.style.service.request.VirtualTryOnServiceRequest; + +import jakarta.validation.constraints.NotBlank; + +public record VirtualTryOnRequest( + @NotBlank(message = "imageUrl is required.") + String imageUrl, + + @NotBlank(message = "targetHairstyle is required.") + String targetHairstyle, + + @NotBlank(message = "targetHairColor is required.") + String targetHairColor + +) { + public VirtualTryOnServiceRequest toServiceRequest() { + return new VirtualTryOnServiceRequest(imageUrl, targetHairstyle, targetHairColor); + } +} diff --git a/src/main/java/com/seomse/interaction/style/entity/StyleAnalysisEntity.java b/src/main/java/com/seomse/interaction/style/entity/StyleAnalysisEntity.java index 4b28f0d..823e360 100644 --- a/src/main/java/com/seomse/interaction/style/entity/StyleAnalysisEntity.java +++ b/src/main/java/com/seomse/interaction/style/entity/StyleAnalysisEntity.java @@ -44,10 +44,17 @@ public class StyleAnalysisEntity extends BaseTimeEntity { @Column(nullable = false) private StyleAnalysisResponse result; + @Column(length = 190) + private String virtualTryOnImage; + public StyleAnalysisEntity(ClientEntity client, String image, StyleAnalysisResponse result) { this.client = client; this.image = image; this.result = result; } + + public void updateVirtualTryOnImage(String virtualTryOnImageUrl) { + this.virtualTryOnImage = virtualTryOnImageUrl; + } } diff --git a/src/main/java/com/seomse/interaction/style/repository/StyleAnalysisRepository.java b/src/main/java/com/seomse/interaction/style/repository/StyleAnalysisRepository.java index 5d7987a..6843c83 100644 --- a/src/main/java/com/seomse/interaction/style/repository/StyleAnalysisRepository.java +++ b/src/main/java/com/seomse/interaction/style/repository/StyleAnalysisRepository.java @@ -1,5 +1,6 @@ package com.seomse.interaction.style.repository; +import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,4 +8,6 @@ import com.seomse.interaction.style.entity.StyleAnalysisEntity; public interface StyleAnalysisRepository extends JpaRepository { + + Optional findByImage(String image); } diff --git a/src/main/java/com/seomse/interaction/style/service/StyleService.java b/src/main/java/com/seomse/interaction/style/service/StyleService.java index 32bc542..76c1687 100644 --- a/src/main/java/com/seomse/interaction/style/service/StyleService.java +++ b/src/main/java/com/seomse/interaction/style/service/StyleService.java @@ -1,16 +1,21 @@ package com.seomse.interaction.style.service; import java.io.IOException; +import java.util.UUID; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import com.seomse.common.util.ByteArrayMultipartFile; import com.seomse.interaction.style.client.AiApiClient; import com.seomse.interaction.style.entity.StyleAnalysisEntity; import com.seomse.interaction.style.repository.StyleAnalysisRepository; +import com.seomse.interaction.style.service.dto.AiVirtualTryOnRequest; import com.seomse.interaction.style.service.dto.StyleAnalysisRequest; import com.seomse.interaction.style.service.dto.StyleAnalysisResponse; +import com.seomse.interaction.style.service.dto.VirtualTryOnResponse; +import com.seomse.interaction.style.service.request.VirtualTryOnServiceRequest; import com.seomse.s3.service.S3Service; import com.seomse.security.jwt.dto.LoginUserInfo; import com.seomse.security.service.SecurityService; @@ -39,7 +44,7 @@ public StyleAnalysisResponse callAnalyzeStyle(MultipartFile image) throws IOExce .orElseThrow(() -> new IllegalArgumentException("User not found.")); final String S3_FOLDER = "style"; - final String s3Key = (image != null && !image.isEmpty()) ? s3Service.upload(image, S3_FOLDER) : null; + final String s3Key = s3Service.upload(image, S3_FOLDER); StyleAnalysisRequest requestBody = new StyleAnalysisRequest(s3Key, client.getGender().toString(), client.getAge().toString()); @@ -51,4 +56,33 @@ public StyleAnalysisResponse callAnalyzeStyle(MultipartFile image) throws IOExce return response; } + + public VirtualTryOnResponse callVirtualTryOn(VirtualTryOnServiceRequest request) throws IOException { + + StyleAnalysisEntity analysisEntity = styleAnalysisRepository.findByImage(request.imageUrl()) + .orElseThrow(() -> new IllegalArgumentException("image not found")); + + LoginUserInfo loginUser = securityService.getCurrentLoginUserInfo(); + ClientEntity client = clientRepository.findById(loginUser.userId()) + .orElseThrow(() -> new IllegalArgumentException("User not found.")); + + AiVirtualTryOnRequest aiRequest = new AiVirtualTryOnRequest(request.imageUrl(), request.targetHairstyle(), + request.targetHairColor(), client.getGender()); + + byte[] generatedImageBytes = aiApiClient.virtualTryOn(aiRequest); + + MultipartFile generatedFile = new ByteArrayMultipartFile( + generatedImageBytes, + "file", + UUID.randomUUID() + ".png", + "image/png" + ); + + final String S3_FOLDER = "style"; + final String resultImageUrl = s3Service.upload(generatedFile, S3_FOLDER); + + analysisEntity.updateVirtualTryOnImage(resultImageUrl); + + return new VirtualTryOnResponse(resultImageUrl); + } } diff --git a/src/main/java/com/seomse/interaction/style/service/dto/AiVirtualTryOnRequest.java b/src/main/java/com/seomse/interaction/style/service/dto/AiVirtualTryOnRequest.java new file mode 100644 index 0000000..33042fa --- /dev/null +++ b/src/main/java/com/seomse/interaction/style/service/dto/AiVirtualTryOnRequest.java @@ -0,0 +1,6 @@ +package com.seomse.interaction.style.service.dto; + +import com.seomse.user.client.enums.Gender; + +public record AiVirtualTryOnRequest(String imageUrl, String targetHairstyle, String targetHairColor, Gender gender) { +} diff --git a/src/main/java/com/seomse/interaction/style/service/dto/VirtualTryOnResponse.java b/src/main/java/com/seomse/interaction/style/service/dto/VirtualTryOnResponse.java new file mode 100644 index 0000000..42d0408 --- /dev/null +++ b/src/main/java/com/seomse/interaction/style/service/dto/VirtualTryOnResponse.java @@ -0,0 +1,5 @@ +package com.seomse.interaction.style.service.dto; + +public record VirtualTryOnResponse(String generatedImageUrl) { +} + diff --git a/src/main/java/com/seomse/interaction/style/service/request/VirtualTryOnServiceRequest.java b/src/main/java/com/seomse/interaction/style/service/request/VirtualTryOnServiceRequest.java new file mode 100644 index 0000000..01a78e8 --- /dev/null +++ b/src/main/java/com/seomse/interaction/style/service/request/VirtualTryOnServiceRequest.java @@ -0,0 +1,4 @@ +package com.seomse.interaction.style.service.request; + +public record VirtualTryOnServiceRequest(String imageUrl, String targetHairstyle, String targetHairColor) { +} diff --git a/src/test/java/com/seomse/interaction/style/service/StyleServiceTest.java b/src/test/java/com/seomse/interaction/style/service/StyleServiceTest.java index 3b01139..6899a61 100644 --- a/src/test/java/com/seomse/interaction/style/service/StyleServiceTest.java +++ b/src/test/java/com/seomse/interaction/style/service/StyleServiceTest.java @@ -5,13 +5,13 @@ import java.io.IOException; import java.util.List; +import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -19,8 +19,11 @@ import com.seomse.IntegrationTestSupport; import com.seomse.interaction.style.entity.StyleAnalysisEntity; import com.seomse.interaction.style.repository.StyleAnalysisRepository; +import com.seomse.interaction.style.service.dto.AiVirtualTryOnRequest; import com.seomse.interaction.style.service.dto.StyleAnalysisRequest; import com.seomse.interaction.style.service.dto.StyleAnalysisResponse; +import com.seomse.interaction.style.service.dto.VirtualTryOnResponse; +import com.seomse.interaction.style.service.request.VirtualTryOnServiceRequest; import com.seomse.security.jwt.dto.LoginUserInfo; import com.seomse.user.auth.enums.Role; import com.seomse.user.client.entity.ClientEntity; @@ -29,6 +32,9 @@ import com.seomse.user.client.enums.SnsType; import com.seomse.user.client.repository.ClientRepository; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + class StyleServiceTest extends IntegrationTestSupport { @Autowired @@ -68,7 +74,6 @@ void callAnalyzeStyle() throws IOException { BDDMockito.given(securityService.getCurrentLoginUserInfo()) .willReturn(new LoginUserInfo(client.getId(), Role.CLIENT)); - ClassPathResource resource = new ClassPathResource("test.jpg"); MockMultipartFile image = new MockMultipartFile( "image", "test.jpg", @@ -104,4 +109,61 @@ void callAnalyzeStyle() throws IOException { assertThat(savedAnalysis.getImage()).isNotNull(); } + + @DisplayName("성공: 기존 분석 이미지를 기반으로 가상 체험을 요청하면 결과 이미지 URL을 반환한다") + @Test + void callVirtualTryOn() throws IOException { + // given + String email = "user@email.com"; + String password = "abc1234!"; + String name = "김섬세"; + SnsType snsType = SnsType.NORMAL; + Gender gender = Gender.MALE; + Age age = Age.TWENTIES; + + ClientEntity client = new ClientEntity(email, bCryptPasswordEncoder.encode(password), name, snsType, gender, + age); + + clientRepository.save(client); + + BDDMockito.given(securityService.getCurrentLoginUserInfo()) + .willReturn(new LoginUserInfo(client.getId(), Role.CLIENT)); + + String originalImageUrl = "https://test.cloudfront.net/style/test.png"; + StyleAnalysisResponse dummyResponse = new StyleAnalysisResponse( + new StyleAnalysisResponse.AnalysisData("계란형", "가을 웜톤"), + new StyleAnalysisResponse.RecommendationsData( + new StyleAnalysisResponse.RecommendationDetail("댄디컷", "이유..."), + new StyleAnalysisResponse.RecommendationDetail("애쉬 브라운", "이유...") + ) + ); + + StyleAnalysisEntity analysisEntity = new StyleAnalysisEntity(client, originalImageUrl, dummyResponse); + styleAnalysisRepository.save(analysisEntity); + + String targetHairstyle = "댄디컷"; + String targetHairColor = "애쉬 브라운"; + VirtualTryOnServiceRequest request = new VirtualTryOnServiceRequest(originalImageUrl, targetHairstyle, + targetHairColor); + + byte[] fakeGeneratedImage = "fake-image-bytes".getBytes(); + BDDMockito.given(aiApiClient.virtualTryOn(any(AiVirtualTryOnRequest.class))) + .willReturn(fakeGeneratedImage); + + BDDMockito.given(s3Client.putObject(any(Consumer.class), any(RequestBody.class))) + .willReturn(PutObjectResponse.builder().build()); + + // when + VirtualTryOnResponse response = styleService.callVirtualTryOn(request); + + // then + assertThat(response).isNotNull(); + assertThat(response.generatedImageUrl()).isNotNull(); + assertThat(response.generatedImageUrl()).contains("/style/"); + assertThat(response.generatedImageUrl()).endsWith(".png"); + + StyleAnalysisEntity updatedEntity = styleAnalysisRepository.findById(analysisEntity.getId()).orElseThrow(); + assertThat(updatedEntity.getVirtualTryOnImage()).isEqualTo(response.generatedImageUrl()); + assertThat(updatedEntity.getImage()).isEqualTo(originalImageUrl); + } } \ No newline at end of file From 530c17c8af96b35351f458d1ad74d275e69cf6d9 Mon Sep 17 00:00:00 2001 From: HOYA Date: Tue, 9 Dec 2025 01:40:25 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: host가 requestBody에도 영향 주는 부분 해결 --- .../asciidoc/interaction/style/style.adoc | 13 +++++ .../style/controller/StyleController.java | 2 +- .../java/com/seomse/docs/RestDocsSupport.java | 17 +++---- .../style/StyleControllerDocsTest.java | 51 +++++++++++++++++++ 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/docs/asciidoc/interaction/style/style.adoc b/src/docs/asciidoc/interaction/style/style.adoc index ddbcee7..3b5abd4 100644 --- a/src/docs/asciidoc/interaction/style/style.adoc +++ b/src/docs/asciidoc/interaction/style/style.adoc @@ -13,3 +13,16 @@ include::{snippets}/style-analysis/request-parts.adoc[] include::{snippets}/style-analysis/http-response.adoc[] include::{snippets}/style-analysis/response-fields.adoc[] + +[[style-virtual-try-on]] +=== 스타일 가상 체험 + +==== HTTP Request + +include::{snippets}/style-virtual-try-on/http-request.adoc[] +include::{snippets}/style-virtual-try-on/request-fields.adoc[] + +==== HTTP Response + +include::{snippets}/style-virtual-try-on/http-response.adoc[] +include::{snippets}/style-virtual-try-on/response-fields.adoc[] \ No newline at end of file diff --git a/src/main/java/com/seomse/interaction/style/controller/StyleController.java b/src/main/java/com/seomse/interaction/style/controller/StyleController.java index cafe0f0..b90b8ab 100644 --- a/src/main/java/com/seomse/interaction/style/controller/StyleController.java +++ b/src/main/java/com/seomse/interaction/style/controller/StyleController.java @@ -33,7 +33,7 @@ public ApiResponse analyzeStyle(@RequestPart MultipartFil } @PostMapping("/virtual-try-on") - @ResponseStatus(HttpStatus.OK) + @ResponseStatus(HttpStatus.CREATED) public ApiResponse virtualTryOn(@RequestBody VirtualTryOnRequest request) throws IOException { return ApiResponse.created(styleService.callVirtualTryOn(request.toServiceRequest())); } diff --git a/src/test/java/com/seomse/docs/RestDocsSupport.java b/src/test/java/com/seomse/docs/RestDocsSupport.java index ba81412..4a095e0 100644 --- a/src/test/java/com/seomse/docs/RestDocsSupport.java +++ b/src/test/java/com/seomse/docs/RestDocsSupport.java @@ -1,9 +1,8 @@ package com.seomse.docs; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; @@ -16,6 +15,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @ExtendWith(RestDocumentationExtension.class) +@AutoConfigureRestDocs(uriHost = "api.seomse.kro.kr") public abstract class RestDocsSupport { protected MockMvc mockMvc; @@ -31,15 +31,10 @@ public RestDocsSupport() { void setup(RestDocumentationContextProvider provider) { this.mockMvc = MockMvcBuilders.standaloneSetup(initController()) .setMessageConverters(new MappingJackson2HttpMessageConverter(this.objectMapper)) - .apply(MockMvcRestDocumentation.documentationConfiguration(provider) - .operationPreprocessors() - .withRequestDefaults(modifyUris() - .scheme("https") - .host("api.seomse.kro.kr") - .removePort(), - prettyPrint()) - .withResponseDefaults(prettyPrint()) - ) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider).uris() + .withScheme("https") + .withHost("api.seomse.kro.kr") + .withPort(443)) .build(); } diff --git a/src/test/java/com/seomse/docs/interaction/style/StyleControllerDocsTest.java b/src/test/java/com/seomse/docs/interaction/style/StyleControllerDocsTest.java index 7f3fa52..852e723 100644 --- a/src/test/java/com/seomse/docs/interaction/style/StyleControllerDocsTest.java +++ b/src/test/java/com/seomse/docs/interaction/style/StyleControllerDocsTest.java @@ -20,8 +20,11 @@ import com.seomse.docs.RestDocsSupport; import com.seomse.interaction.style.controller.StyleController; +import com.seomse.interaction.style.controller.request.VirtualTryOnRequest; import com.seomse.interaction.style.service.StyleService; import com.seomse.interaction.style.service.dto.StyleAnalysisResponse; +import com.seomse.interaction.style.service.dto.VirtualTryOnResponse; +import com.seomse.interaction.style.service.request.VirtualTryOnServiceRequest; public class StyleControllerDocsTest extends RestDocsSupport { @@ -94,4 +97,52 @@ void analyzeStyle() throws Exception { ) )); } + + @DisplayName("가상 헤어스타일 체험 API") + @Test + void virtualTryOn() throws Exception { + // given + VirtualTryOnRequest request = new VirtualTryOnRequest( + "https://test.cloudfront.net/style/test.png", + "댄디컷", + "애쉬 브라운" + ); + + VirtualTryOnResponse response = new VirtualTryOnResponse( + "https://test.cloudfront.net/style/generated_result.png" + ); + + given(styleService.callVirtualTryOn(any(VirtualTryOnServiceRequest.class))) + .willReturn(response); + + // when // then + mockMvc.perform( + post("/interaction/styles/virtual-try-on") + .header(HttpHeaders.AUTHORIZATION, "Bearer ") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andDo(print()) + .andDo(document("style-virtual-try-on", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + + requestFields( + fieldWithPath("imageUrl").type(JsonFieldType.STRING) + .description("원본 이미지 URL"), + fieldWithPath("targetHairstyle").type(JsonFieldType.STRING) + .description("적용할 타겟 헤어스타일 이름 (예: 댄디컷, 리젠트컷)"), + fieldWithPath("targetHairColor").type(JsonFieldType.STRING) + .description("적용할 타겟 헤어 컬러 (예: 애쉬 브라운, 블랙)") + ), + + responseFields( + fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("응답 코드"), + fieldWithPath("data").type(JsonFieldType.OBJECT).description("가상 체험 결과 데이터"), + fieldWithPath("data.generatedImageUrl").type(JsonFieldType.STRING) + .description("생성된 가상 체험 결과 이미지 URL") + ) + )); + } }