Skip to content

Commit 465f7a4

Browse files
yongjun0511dev2yup
andauthored
[feat/#132] 옷 관련 ai 기능 개발 (현재 PR만 열어둠) (#200)
* feat: webClient 설정 추가 * feat: clothAi 도메인 생성 및 presignedUrl 발급 로직 이전 * feat: ai 관련 기능 가개발 * test/fix: 테스트 오타 수정 * refactor: presigned url 발급 ACL 설정 제거 * feat: 옷 정보 추출, 기록 스타일 추론 API 서비스, DTO * fix: 기록사진추론 API 응답 json 키값 매핑 * refactor: dto 수정(jason 매핑) 및 url 슬라이싱 추가 * refactor: 옷 정보 추출API 상위 카테고리 반환 추가 * refactor: 옷 정보 추출 API 이미지 url 응답 추가 * feat:AI연동 error code 및 예외 처리 구체화 * refactor: 부모 카테고리 조회 N+1문제 해결 * test/refactor: 컨트롤러/서비스 테스트 수정 --------- Co-authored-by: 나용준 <141994188+youngJun99@users.noreply.github.com> Co-authored-by: leedy5521 <leedy5521@naver.com> Co-authored-by: leedy5521 <80202719+dev2yup@users.noreply.github.com>
1 parent 3f9db78 commit 465f7a4

33 files changed

Lines changed: 755 additions & 46 deletions

.github/workflows/dev-cd.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
username: ubuntu
6464
host: ${{ secrets.DEV_EC2_HOST }}
6565
key: ${{ secrets.DEV_EC2_SSH_KEY }}
66-
envs: DOCKERHUB_USERNAME,DEV_MYSQL_HOST,MYSQL_PORT,DB_NAME,DB_USERNAME,DB_PASSWORD,REDIS_HOST,REDIS_PORT,REDIS_PASSWORD,DEV_KAKAO_CLIENT_ID,DEV_KAKAO_CLIENT_SECRET,DEV_APPLE_CLIENT_ID,DEV_APPLE_CLIENT_SECRET,JWT_ACCESS_TOKEN_SECRET,JWT_REFRESH_TOKEN_SECRET,JWT_ACCESS_TOKEN_EXPIRATION_TIME,JWT_REFRESH_TOKEN_EXPIRATION_TIME,JWT_ISSUER,DEV_AWS_ACCESS_KEY_ID,DEV_AWS_SECRET_ACCESS_KEY,AWS_REGION,DEV_S3_BUCKET,DEV_S3_ENDPOINT,SWAGGER_USERNAME,SWAGGER_PASSWORD,FIREBASE_SA_JSON_B64,MEILISEARCH_ENDPOINT,MEILISEARCH_KEY
66+
envs: DOCKERHUB_USERNAME,DEV_MYSQL_HOST,MYSQL_PORT,DB_NAME,DB_USERNAME,DB_PASSWORD,REDIS_HOST,REDIS_PORT,REDIS_PASSWORD,DEV_KAKAO_CLIENT_ID,DEV_KAKAO_CLIENT_SECRET,DEV_APPLE_CLIENT_ID,DEV_APPLE_CLIENT_SECRET,JWT_ACCESS_TOKEN_SECRET,JWT_REFRESH_TOKEN_SECRET,JWT_ACCESS_TOKEN_EXPIRATION_TIME,JWT_REFRESH_TOKEN_EXPIRATION_TIME,JWT_ISSUER,DEV_AWS_ACCESS_KEY_ID,DEV_AWS_SECRET_ACCESS_KEY,AWS_REGION,DEV_S3_BUCKET,DEV_S3_ENDPOINT,SWAGGER_USERNAME,SWAGGER_PASSWORD,FIREBASE_SA_JSON_B64,AI_SERVER_IP,CLOTH_INFERENCE_PATH,STYLE_INFERENCE_PATH,CLOTH_DETECT_PATH,MEILISEARCH_ENDPOINT,MEILISEARCH_KEY
6767
script: |
6868
export DOCKERHUB_NAME=${{ secrets.DOCKERHUB_USERNAME }}
6969
export DOCKER_TAG=dev-app
@@ -102,6 +102,11 @@ jobs:
102102
export MEILISEARCH_ENDPOINT=${{ secrets.MEILISEARCH_ENDPOINT }}
103103
export MEILISEARCH_KEY=${{ secrets.MEILISEARCH_KEY }}
104104
105+
export AI_SERVER_IP=${{ secrets.AI_SERVER_IP }}
106+
export CLOTH_INFERENCE_PATH=${{ secrets.CLOTH_INFERENCE_PATH }}
107+
export STYLE_INFERENCE_PATH=${{ secrets.STYLE_INFERENCE_PATH }}
108+
export CLOTH_DETECT_PATH=${{ secrets.CLOTH_DETECT_PATH }}
109+
105110
sudo mkdir -p /home/ubuntu/secrets
106111
echo "${{ secrets.FIREBASE_SA_JSON_B64 }}" | base64 -d | sudo tee /home/ubuntu/secrets/firebase-sa.json > /dev/null
107112
sudo chmod 600 /home/ubuntu/secrets/firebase-sa.json

.github/workflows/prod-cd.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
username: ubuntu
7474
host: ${{ secrets.PROD_EC2_HOST }}
7575
key: ${{ secrets.PROD_EC2_SSH_KEY }}
76-
envs: DOCKERHUB_USERNAME,SPRING_PROFILES_ACTIVE,PROD_MYSQL_HOST,MYSQL_PORT,DB_NAME,DB_USERNAME,DB_PASSWORD,REDIS_HOST,REDIS_PORT,REDIS_PASSWORD,PROD_KAKAO_CLIENT_ID,PROD_KAKAO_CLIENT_SECRET,PROD_APPLE_CLIENT_ID,PROD_APPLE_CLIENT_SECRET,JWT_ACCESS_TOKEN_SECRET,JWT_REFRESH_TOKEN_SECRET,JWT_ACCESS_TOKEN_EXPIRATION_TIME,JWT_REFRESH_TOKEN_EXPIRATION_TIME,JWT_ISSUER,PROD_AWS_ACCESS_KEY_ID,PROD_AWS_SECRET_ACCESS_KEY,AWS_REGION,PROD_S3_BUCKET,PROD_S3_ENDPOINT,SWAGGER_USERNAME,SWAGGER_PASSWORD,FIREBASE_SA_JSON_B64,MEILISEARCH_ENDPOINT,MEILISEARCH_KEY
76+
envs: DOCKERHUB_USERNAME,SPRING_PROFILES_ACTIVE,PROD_MYSQL_HOST,MYSQL_PORT,DB_NAME,DB_USERNAME,DB_PASSWORD,REDIS_HOST,REDIS_PORT,REDIS_PASSWORD,PROD_KAKAO_CLIENT_ID,PROD_KAKAO_CLIENT_SECRET,PROD_APPLE_CLIENT_ID,PROD_APPLE_CLIENT_SECRET,JWT_ACCESS_TOKEN_SECRET,JWT_REFRESH_TOKEN_SECRET,JWT_ACCESS_TOKEN_EXPIRATION_TIME,JWT_REFRESH_TOKEN_EXPIRATION_TIME,JWT_ISSUER,PROD_AWS_ACCESS_KEY_ID,PROD_AWS_SECRET_ACCESS_KEY,AWS_REGION,PROD_S3_BUCKET,PROD_S3_ENDPOINT,SWAGGER_USERNAME,SWAGGER_PASSWORD,FIREBASE_SA_JSON_B64,AI_SERVER_IP,CLOTH_INFERENCE_PATH,STYLE_INFERENCE_PATH,CLOTH_DETECT_PATH,MEILISEARCH_ENDPOINT,MEILISEARCH_KEY
7777
script: |
7878
export DOCKERHUB_NAME=${{ secrets.DOCKERHUB_USERNAME }}
7979
export DOCKER_TAG=prod-app
@@ -112,6 +112,11 @@ jobs:
112112
export MEILISEARCH_ENDPOINT=${{ secrets.MEILISEARCH_ENDPOINT }}
113113
export MEILISEARCH_KEY=${{ secrets.MEILISEARCH_KEY }}
114114
115+
export AI_SERVER_IP=${{ secrets.AI_SERVER_IP }}
116+
export CLOTH_INFERENCE_PATH=${{ secrets.CLOTH_INFERENCE_PATH }}
117+
export STYLE_INFERENCE_PATH=${{ secrets.STYLE_INFERENCE_PATH }}
118+
export CLOTH_DETECT_PATH=${{ secrets.CLOTH_DETECT_PATH }}
119+
115120
sudo mkdir -p /home/ubuntu/secrets
116121
echo "${{ secrets.FIREBASE_SA_JSON_B64 }}" | base64 -d | sudo tee /home/ubuntu/secrets/firebase-sa.json > /dev/null
117122
sudo chmod 600 /home/ubuntu/secrets/firebase-sa.json

clokey-api/src/main/java/org/clokey/domain/category/repository/CategoryRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package org.clokey.domain.category.repository;
22

33
import java.util.List;
4+
import java.util.Optional;
5+
import java.util.Set;
46
import org.clokey.category.entity.Category;
57
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
610

711
public interface CategoryRepository extends JpaRepository<Category, Long> {
812

@@ -13,4 +17,10 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
1317
List<Category> findAllByParentIsNull();
1418

1519
List<Category> findAllByParentId(Long parentId);
20+
21+
@Query("select c from Category c left join fetch c.parent where c.id = :id")
22+
Optional<Category> findByIdWithParent(@Param("id") Long id);
23+
24+
@Query("select c from Category c left join fetch c.parent where c.id in :ids")
25+
List<Category> findAllByIdWithParent(@Param("ids") Set<Long> ids);
1626
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.clokey.domain.cloth.controller;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.tags.Tag;
5+
import jakarta.validation.Valid;
6+
import lombok.RequiredArgsConstructor;
7+
import org.clokey.code.GlobalBaseSuccessCode;
8+
import org.clokey.domain.cloth.dto.request.ClothDetectRequest;
9+
import org.clokey.domain.cloth.dto.request.ClothImagesUploadRequest;
10+
import org.clokey.domain.cloth.dto.request.ClothInfoExtractRequest;
11+
import org.clokey.domain.cloth.dto.request.HistoryStyleInferenceRequest;
12+
import org.clokey.domain.cloth.dto.response.ClothDetectResponse;
13+
import org.clokey.domain.cloth.dto.response.ClothImagesPresignedUrlResponse;
14+
import org.clokey.domain.cloth.dto.response.ClothInfoExtractResponse;
15+
import org.clokey.domain.cloth.dto.response.HistoryStyleInferenceResponse;
16+
import org.clokey.domain.cloth.service.ClothAiService;
17+
import org.clokey.response.BaseResponse;
18+
import org.springframework.validation.annotation.Validated;
19+
import org.springframework.web.bind.annotation.*;
20+
21+
@RestController
22+
@RequestMapping("/cloth-ai")
23+
@RequiredArgsConstructor
24+
@Tag(name = "17. 옷 AI API", description = "옷 AI 관련 API입니다.")
25+
@Validated
26+
public class ClothAiController {
27+
28+
private final ClothAiService clothAiService;
29+
30+
@PostMapping("/images")
31+
@Operation(
32+
operationId = "ClothAi_getClothUploadPresignedUrl",
33+
summary = "옷 이미지 업로드용 presignedUrl 발급",
34+
description = "옷 이미지 업로드용 presignedUrl을 발급합니다.")
35+
public BaseResponse<ClothImagesPresignedUrlResponse> getClothUploadPresignedUrl(
36+
@Valid @RequestBody ClothImagesUploadRequest request) {
37+
ClothImagesPresignedUrlResponse response =
38+
clothAiService.getClothUploadPresignedUrls(request);
39+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, response);
40+
}
41+
42+
@PostMapping("/extract")
43+
@Operation(
44+
operationId = "ClothAi_extractClothInfo",
45+
summary = "옷 정보 추출",
46+
description = "옷 이미지 URL을 기반으로 AI 서버에서 옷 정보를 추출합니다.")
47+
public BaseResponse<ClothInfoExtractResponse> extractClothInfo(
48+
@Valid @RequestBody ClothInfoExtractRequest request) {
49+
ClothInfoExtractResponse response = clothAiService.extractClothInfo(request);
50+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, response);
51+
}
52+
53+
@PostMapping("/history-style")
54+
@Operation(
55+
operationId = "ClothAi_inferHistoryStyle",
56+
summary = "기록 사진 스타일 추론",
57+
description = "기록 이미지 URL을 통해 스타일을 추론합니다.")
58+
public BaseResponse<HistoryStyleInferenceResponse> inferHistoryStyle(
59+
@Valid @RequestBody HistoryStyleInferenceRequest request) {
60+
HistoryStyleInferenceResponse response = clothAiService.inferHistoryStyle(request);
61+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, response);
62+
}
63+
64+
@PostMapping("/detect")
65+
@Operation(
66+
operationId = "ClothAi_detectClothes",
67+
summary = "사진에서 옷 탐지",
68+
description = "사진에서 내부의 옷들을 탐지합니다.")
69+
public BaseResponse<ClothDetectResponse> detectClothes(
70+
@Valid @RequestBody ClothDetectRequest request) {
71+
ClothDetectResponse response = clothAiService.detectClothes(request);
72+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, response);
73+
}
74+
}

clokey-api/src/main/java/org/clokey/domain/cloth/controller/ClothController.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.clokey.cloth.enums.Season;
1010
import org.clokey.code.GlobalBaseSuccessCode;
1111
import org.clokey.domain.cloth.dto.request.ClothCreateRequests;
12-
import org.clokey.domain.cloth.dto.request.ClothImagesUploadRequest;
1312
import org.clokey.domain.cloth.dto.request.ClothUpdateRequest;
1413
import org.clokey.domain.cloth.dto.response.*;
1514
import org.clokey.domain.cloth.service.ClothService;
@@ -29,18 +28,6 @@ public class ClothController {
2928

3029
private final ClothService clothService;
3130

32-
@PostMapping("/images")
33-
@Operation(
34-
operationId = "Cloth_getClothUploadPresignedUrl",
35-
summary = "옷 이미지 업로드용 presignedUrl 발급",
36-
description = "옷 이미지 업로드용 presignedUrl을 발급합니다.")
37-
public BaseResponse<ClothImagesPresignedUrlResponse> getClothUploadPresignedUrl(
38-
@Valid @RequestBody ClothImagesUploadRequest request) {
39-
ClothImagesPresignedUrlResponse response =
40-
clothService.getClothUploadPresignedUrls(request);
41-
return BaseResponse.onSuccess(GlobalBaseSuccessCode.CREATED, response);
42-
}
43-
4431
@PostMapping
4532
@Operation(operationId = "Cloth_createClothes", summary = "옷 생성", description = "새로운 옷을 생성합니다.")
4633
public BaseResponse<ClothCreateResponse> createClothes(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.clokey.domain.cloth.dto.request;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import java.util.List;
5+
6+
public record ClothDetectAiRequestDTO(
7+
@JsonProperty("download_url") String imageUrl,
8+
@JsonProperty("upload_urls") List<String> presignedUrls) {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.clokey.domain.cloth.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
6+
@Schema(description = "사진에서 옷 탐지 요청")
7+
public record ClothDetectRequest(
8+
@NotBlank(message = "이미지 URL은 비워둘 수 없습니다.")
9+
@Schema(description = "이미지 URL", example = "https://example.com/image.jpg")
10+
String imageUrl) {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.clokey.domain.cloth.dto.request;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import java.util.List;
5+
6+
public record ClothInfoExtractAiRequestDTO(
7+
@JsonProperty("download_urls") List<String> clothImageUrls,
8+
@JsonProperty("upload_urls") List<String> presignedUrls) {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.clokey.domain.cloth.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.NotEmpty;
6+
import jakarta.validation.constraints.Size;
7+
import java.util.List;
8+
9+
@Schema(description = "옷 정보 추출 요청")
10+
public record ClothInfoExtractRequest(
11+
@NotEmpty(message = "옷 이미지 URL 목록은 비워둘 수 없습니다.")
12+
@Size(max = 10, message = "옷 이미지 URL은 최대 10개까지 입력할 수 있습니다.")
13+
@Schema(
14+
description = "옷 이미지 URL 목록 (최대 10개)",
15+
example =
16+
"[\"https://example.com/cloth1.jpg\", \"https://example.com/cloth2.jpg\"]")
17+
List<@NotBlank(message = "옷 이미지 URL은 비워둘 수 없습니다.") String> clothImageUrls) {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.clokey.domain.cloth.dto.request;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
public record HistoryStyleInferenceAiRequestDTO(
6+
@JsonProperty("download_url") String historyImageUrl) {}

0 commit comments

Comments
 (0)