Skip to content

Commit 67cf98e

Browse files
authored
Merge pull request #83 from runtiime/revert-82-revert-81-feat/ai-ocr-integration
[fix] revert rollback of AI OCR 기능 추가
2 parents 6e9714a + bc7a749 commit 67cf98e

File tree

9 files changed

+190
-106
lines changed

9 files changed

+190
-106
lines changed

yechef/src/main/java/sejong/capston/yechef/domain/Gpt/dto/RecipeParseResultDto.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,20 @@ public class RecipeParseResultDto {
1818
private int servings;
1919
private List<IngredientDto> ingredients;
2020
private List<RecipeStepDto> steps;
21+
22+
private String sourceImageUrl;
23+
24+
public RecipeParseResultDto( // TODO: 사용처를 빌더 패턴으로 수정하기
25+
String title,
26+
String text,
27+
int servings,
28+
List<IngredientDto> ingredients,
29+
List<RecipeStepDto> steps
30+
) {
31+
this.title = title;
32+
this.text = text;
33+
this.servings = servings;
34+
this.ingredients = ingredients;
35+
this.steps = steps;
36+
}
2137
}

yechef/src/main/java/sejong/capston/yechef/domain/Image/service/ImageService.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import lombok.RequiredArgsConstructor;
44
import org.springframework.stereotype.Service;
55
import org.springframework.transaction.annotation.Transactional;
6+
import org.springframework.web.multipart.MultipartFile;
67
import sejong.capston.yechef.domain.Image.Image;
78
import sejong.capston.yechef.domain.Image.repository.ImageRepository;
89
import sejong.capston.yechef.domain.KakaoImage.service.KakaoImageService;
910
import sejong.capston.yechef.domain.Recipe.Recipe;
1011
import sejong.capston.yechef.domain.Recipe.repository.RecipeRepository;
1112
import sejong.capston.yechef.global.exception.BaseException;
1213
import sejong.capston.yechef.global.exception.error.ErrorCode;
14+
import sejong.capston.yechef.global.s3.service.S3UploadService;
1315

1416
@Service
1517
@RequiredArgsConstructor
@@ -18,6 +20,7 @@ public class ImageService {
1820
private final ImageRepository imageRepository;
1921
private final RecipeRepository recipeRepository;
2022
private final KakaoImageService kakaoImageService;
23+
private final S3UploadService s3UploadService;
2124

2225
@Transactional
2326
public void generateAndSaveThumbnail(Long recipeId) {
@@ -46,4 +49,17 @@ private String extractKeyFromUrl(String url) {
4649
return url.substring(url.indexOf(".com/") + 5);
4750
}
4851

52+
@Transactional
53+
public Image saveImage(MultipartFile file) {
54+
String s3Url = s3UploadService.uploadAndGenerateKey(file);
55+
String s3Key = s3UploadService.extractKeyFromUrl(s3Url);
56+
57+
Image image = Image.builder()
58+
.s3Url(s3Url)
59+
.s3Key(s3Key)
60+
.build();
61+
62+
return imageRepository.save(image);
63+
}
64+
4965
}

yechef/src/main/java/sejong/capston/yechef/domain/KakaoImage/service/KakaoImageService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sejong.capston.yechef.domain.KakaoImage.service;
22

33
import lombok.RequiredArgsConstructor;
4+
import org.springframework.beans.factory.annotation.Qualifier;
45
import org.springframework.stereotype.Service;
56
import org.springframework.web.reactive.function.client.WebClient;
67
import org.springframework.web.reactive.function.client.WebClientResponseException;
@@ -12,11 +13,14 @@
1213
import java.net.URL;
1314

1415
@Service
15-
@RequiredArgsConstructor
1616
public class KakaoImageService {
1717

1818
private final WebClient kakaoImageWebClient;
1919

20+
public KakaoImageService(@Qualifier("kakaoImageWebClient") WebClient kakaoImageWebClient) {
21+
this.kakaoImageWebClient = kakaoImageWebClient;
22+
}
23+
2024
/**
2125
* 카카오 이미지 검색 API 호출
2226
*/
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package sejong.capston.yechef.domain.Recipe.ai;
2+
3+
import java.util.Map;
4+
import org.springframework.beans.factory.annotation.Qualifier;
5+
import org.springframework.http.MediaType;
6+
import org.springframework.http.client.MultipartBodyBuilder;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.multipart.MultipartFile;
9+
import org.springframework.web.reactive.function.client.WebClient;
10+
import sejong.capston.yechef.domain.Gpt.dto.RecipeParseResultDto;
11+
12+
@Component
13+
public class OcrClient {
14+
15+
@Qualifier("aiWebClient")
16+
private final WebClient webClient;
17+
18+
public OcrClient(@Qualifier("aiWebClient") WebClient webClient) {
19+
this.webClient = webClient;
20+
}
21+
22+
23+
public RecipeParseResultDto extractText(MultipartFile imageFile) {
24+
MultipartBodyBuilder builder = new MultipartBodyBuilder();
25+
builder.part("file", imageFile.getResource());
26+
27+
return webClient.post()
28+
.uri("/ocr/parse")
29+
.contentType(MediaType.MULTIPART_FORM_DATA)
30+
.bodyValue(builder.build())
31+
.retrieve()
32+
.bodyToMono(RecipeParseResultDto.class)
33+
.block();
34+
}
35+
36+
}

yechef/src/main/java/sejong/capston/yechef/domain/Recipe/controller/RecipeController.java

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@
33
import java.net.URI;
44
import java.util.List;
55

6-
import com.fasterxml.jackson.databind.ObjectMapper;
76
import lombok.RequiredArgsConstructor;
8-
97
import org.springframework.http.MediaType;
108
import org.springframework.http.ResponseEntity;
119
import org.springframework.web.bind.annotation.*;
1210
import org.springframework.web.multipart.MultipartFile;
1311

14-
import io.swagger.v3.oas.annotations.Operation;
1512
import io.swagger.v3.oas.annotations.tags.Tag;
1613
import io.swagger.v3.oas.annotations.Parameter;
17-
import io.swagger.v3.oas.annotations.responses.ApiResponses;
18-
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1914

2015
import sejong.capston.yechef.domain.Gpt.dto.RecipeParseResultDto;
2116
import sejong.capston.yechef.domain.Recipe.Recipe;
@@ -30,32 +25,30 @@
3025
public class RecipeController {
3126

3227
private final RecipeService recipeService;
33-
@PostMapping(value = "/{memberId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
34-
public ResponseEntity<RecipeDto> createRecipe(
35-
@Parameter(description = "회원 ID", required = true)
36-
@PathVariable("memberId") Long memberId,
3728

38-
@Parameter(description = "GPT 파싱 결과 JSON (문자열 형태)", required = true)
39-
@RequestPart("dto") String dtoText, // String으로 먼저 받음
29+
// ✅ 레시피 저장 (AI에서 받은 OCR 결과 저장)
30+
@PostMapping(value = "/{memberId}", consumes = MediaType.APPLICATION_JSON_VALUE)
31+
public ResponseEntity<RecipeDto> createRecipe(
32+
@Parameter(description = "회원 ID", required = true)
33+
@PathVariable("memberId") Long memberId,
34+
35+
@RequestBody RecipeParseResultDto dto
36+
) {
37+
RecipeDto result = recipeService.create(memberId, dto);
38+
return ResponseEntity
39+
.created(URI.create("/api/recipes/" + result.getId()))
40+
.body(result);
41+
}
4042

41-
@Parameter(description = "레시피 이미지 파일", required = true)
42-
@RequestPart("sourceImageFile") MultipartFile sourceImageFile
43-
) {
44-
try {
45-
// 문자열 → 객체 변환
46-
ObjectMapper objectMapper = new ObjectMapper();
47-
RecipeParseResultDto dto = objectMapper.readValue(dtoText, RecipeParseResultDto.class);
43+
@PostMapping(value = "/ocr/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
44+
public ResponseEntity<RecipeDto> createRecipeFromImage(
45+
@RequestParam("memberId") Long memberId,
46+
@RequestPart("image") MultipartFile imageFile) {
47+
RecipeDto result = recipeService.createRecipeFromImage(memberId, imageFile);
48+
return ResponseEntity.ok(result);
49+
}
4850

49-
RecipeDto result = recipeService.create(memberId, dto, sourceImageFile);
50-
return ResponseEntity
51-
.created(URI.create("/api/recipes/" + result.getId()))
52-
.body(result);
53-
} catch (Exception e) {
54-
// 파싱 실패 시 예외 처리
55-
return ResponseEntity.badRequest().build();
56-
}
57-
}
58-
51+
// 상세 조회
5952
@GetMapping("/{recipeId}")
6053
public ResponseEntity<DetailRecipeDto> getRecipe(
6154
@Parameter(description = "조회할 레시피 ID", required = true)
@@ -64,7 +57,7 @@ public ResponseEntity<DetailRecipeDto> getRecipe(
6457
return ResponseEntity.ok(recipeService.getRecipe(recipeId));
6558
}
6659

67-
60+
// 삭제
6861
@DeleteMapping("/members/{memberId}/recipes/{recipeId}")
6962
public ResponseEntity<Void> deleteRecipe(
7063
@PathVariable("memberId") Long memberId,
@@ -74,9 +67,9 @@ public ResponseEntity<Void> deleteRecipe(
7467
return ResponseEntity.noContent().build();
7568
}
7669

77-
70+
// 공개 목록 조회
7871
@GetMapping("/public")
7972
public ResponseEntity<List<Recipe>> getPublicRecipes() {
8073
return ResponseEntity.ok(recipeService.getPublicRecipes());
8174
}
82-
}
75+
}

yechef/src/main/java/sejong/capston/yechef/domain/Recipe/controller/RecipeProgressController.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import lombok.RequiredArgsConstructor;
44
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.GetMapping;
56
import org.springframework.web.bind.annotation.PathVariable;
67
import org.springframework.web.bind.annotation.PostMapping;
78
import org.springframework.web.bind.annotation.RequestBody;
89
import org.springframework.web.bind.annotation.RequestMapping;
10+
import org.springframework.web.bind.annotation.RequestParam;
911
import org.springframework.web.bind.annotation.RestController;
1012
import sejong.capston.yechef.domain.Recipe.dto.RecipeStepDto;
1113
import sejong.capston.yechef.domain.Recipe.dto.VoiceInputDto;
@@ -18,14 +20,15 @@ public class RecipeProgressController {
1820

1921
private final RecipeProgressService progressService;
2022

21-
@PostMapping("/{recipeId}/step/{stepNumber}/progress")
23+
@GetMapping("/{recipeId}/step/{stepNumber}/progress")
2224
public RecipeStepDto progressStep(
23-
@PathVariable("memberId") Long memberId,
25+
@RequestParam("memberId") Long memberId,
2426
@PathVariable("recipeId") Long recipeId,
2527
@PathVariable("stepNumber") int stepNumber,
26-
@RequestBody VoiceInputDto input
28+
@RequestParam("text") String inputText
2729
) {
28-
return progressService.processStep(memberId, recipeId, stepNumber, input.getText());
30+
return progressService.processStep(memberId, recipeId, stepNumber, inputText);
2931
}
32+
3033
}
3134

0 commit comments

Comments
 (0)