Skip to content

Commit 5afcb23

Browse files
authored
Merge pull request #89 from iamseojin/recipe-develop
Recipe develop
2 parents dc59bec + 8ecb38e commit 5afcb23

10 files changed

Lines changed: 218 additions & 40 deletions

File tree

yechef/src/main/java/sejong/capston/yechef/domain/Gpt/controller/GptApiController.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
import lombok.RequiredArgsConstructor;
44
import org.springframework.beans.factory.annotation.Autowired;
55
import org.springframework.beans.factory.annotation.Value;
6-
import org.springframework.web.bind.annotation.GetMapping;
7-
import org.springframework.web.bind.annotation.RequestMapping;
8-
import org.springframework.web.bind.annotation.RequestParam;
9-
import org.springframework.web.bind.annotation.RestController;
6+
import org.springframework.web.bind.annotation.*;
107
import org.springframework.web.client.RestTemplate;
118
import sejong.capston.yechef.domain.Gpt.dto.RecipeParseResultDto;
129
import sejong.capston.yechef.domain.Gpt.service.GptService;
10+
import sejong.capston.yechef.domain.RecipeSteps.dto.RecipeStepDetailDto;
1311

1412
@RestController
1513
@RequiredArgsConstructor
@@ -31,4 +29,11 @@ public class GptApiController {
3129
public RecipeParseResultDto parseRecipe(@RequestParam("recipe") String rawRecipe) {
3230
return gptService.parseRecipe(rawRecipe);
3331
}
32+
33+
@GetMapping("/parseStep/{recipeId}/{stepNumber}")
34+
public RecipeStepDetailDto parseRecipeSteps (
35+
@PathVariable Long recipeId, @PathVariable int stepNumber) {
36+
return gptService.parseRecipeSteps(recipeId, stepNumber);
37+
}
38+
3439
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import lombok.NoArgsConstructor;
66
import lombok.Setter;
77
import sejong.capston.yechef.domain.Recipe.dto.RecipeStepDto;
8+
import sejong.capston.yechef.domain.RecipeSteps.dto.RecipeStepDetailDto;
89

910
import java.util.List;
1011

@@ -17,7 +18,7 @@ public class RecipeParseResultDto {
1718
private String text; // 음식 한 줄 설명
1819
private int servings;
1920
private List<IngredientDto> ingredients;
20-
private List<RecipeStepDto> steps;
21+
private List<RecipeStepDetailDto> steps;
2122

2223
private String sourceImageUrl;
2324

@@ -26,7 +27,7 @@ public RecipeParseResultDto( // TODO: 사용처를 빌더 패턴으로 수정
2627
String text,
2728
int servings,
2829
List<IngredientDto> ingredients,
29-
List<RecipeStepDto> steps
30+
List<RecipeStepDetailDto> steps
3031
) {
3132
this.title = title;
3233
this.text = text;

yechef/src/main/java/sejong/capston/yechef/domain/Gpt/service/GptService.java

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
import org.springframework.stereotype.Service;
1010
import org.springframework.web.client.RestTemplate;
1111
import sejong.capston.yechef.domain.Gpt.dto.*;
12+
import sejong.capston.yechef.domain.Recipe.Recipe;
1213
import sejong.capston.yechef.domain.Recipe.dto.RecipeStepDto;
14+
import sejong.capston.yechef.domain.Recipe.repository.RecipeRepository;
15+
import sejong.capston.yechef.domain.RecipeSteps.RecipeStep;
16+
import sejong.capston.yechef.domain.RecipeSteps.dto.RecipeStepDetailDto;
17+
import sejong.capston.yechef.domain.RecipeSteps.repository.RecipeStepRepository;
1318
import sejong.capston.yechef.global.exception.BaseException;
1419
import sejong.capston.yechef.global.exception.error.ErrorCode;
1520

@@ -21,6 +26,8 @@
2126
@RequiredArgsConstructor
2227
public class GptService {
2328
private final RestTemplate restTemplate;
29+
private final RecipeStepRepository recipeStepRepository;
30+
private final RecipeRepository recipeRepository;
2431
@Value("${OPENAI_MODEL}") private String model;
2532
@Value("${OPENAI_API_URL}") private String apiUrl;
2633
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -85,10 +92,10 @@ public RecipeParseResultDto parseRecipe(String rawRecipe) {
8592
));
8693
}
8794

88-
List<RecipeStepDto> steps = new ArrayList<>();
95+
List<RecipeStepDetailDto> steps = new ArrayList<>();
8996
int index = 1;
9097
for (JsonNode stepNode : root.path("steps")) {
91-
steps.add(RecipeStepDto.builder()
98+
steps.add(RecipeStepDetailDto.builder()
9299
.stepNumber(stepNode.path("order").asInt(index++))
93100
.action(stepNode.path("action").asText(""))
94101
.description(stepNode.path("description").asText(""))
@@ -114,5 +121,81 @@ public String simplePrompt(String prompt) {
114121
return resp.getChoices().get(0).getMessage().getContent().trim();
115122
}
116123

124+
public RecipeStepDetailDto parseRecipeSteps(Long recipeId, int stepNumber) {
125+
String systemPrompt = """
126+
You are an AI that converts a single Korean recipe step description into a JSON object.
127+
◆ Input: a Korean sentence describing one cooking step.
128+
◆ Output: pure JSON only, with exactly these fields:
129+
- "stepNumber": the original step number (integer)
130+
- "action": a short English verb phrase describing the action
131+
- "ingredients": a JSON array of English ingredient names mentioned in the description,
132+
with adjectives removed and excluding "water"
133+
No other keys or commentary.
134+
""";
135+
136+
Recipe recipe = recipeRepository.findById(recipeId)
137+
.orElseThrow(() -> BaseException.from(ErrorCode.RECIPE_NOT_EXIST));
138+
139+
RecipeStep recipeStep = recipeStepRepository.findByRecipeAndStepNumber(recipe, stepNumber)
140+
.orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND));
141+
142+
RecipeStepDetailDto dto = new RecipeStepDetailDto(recipeStep.getStepNumber(),
143+
recipeStep.getAction(), recipeStep.getDescription(), recipeStep.getIngredients());
144+
145+
List<Message> messages = List.of(
146+
new Message("system", systemPrompt),
147+
new Message("user", dto.getDescription())
148+
);
149+
ChatGPTRequest req = new ChatGPTRequest(model, messages);
150+
ChatGPTResponse resp = restTemplate.postForObject(apiUrl, req, ChatGPTResponse.class);
151+
String content = resp.getChoices().get(0).getMessage().getContent().trim();
152+
153+
try {
154+
JsonNode root = objectMapper.readTree(content);
155+
int num = root.path("stepNumber").asInt(dto.getStepNumber());
156+
String action = root.path("action").asText();
157+
List<String> ingredients = new ArrayList<>();
158+
159+
for (JsonNode ing : root.path("ingredients")) {
160+
ingredients.add(ing.asText());
161+
}
162+
163+
return RecipeStepDetailDto.of(num, action, dto.getDescription(), ingredients);
164+
165+
} catch (JsonProcessingException e) {
166+
throw BaseException.from(ErrorCode.GPT_RESPONSE_PARSING_FAILED);
167+
}
168+
}
169+
170+
public String parseRecipeStepsTest(Long recipeId, int stepNumber) {
171+
String systemPrompt = """
172+
You are an AI that converts a single Korean recipe step description into a JSON object.
173+
◆ Input: a Korean sentence describing one cooking step.
174+
◆ Output: pure JSON only, with exactly these fields:
175+
- "stepNumber": the original step number (integer)
176+
- "action": a short English verb phrase describing the action
177+
- "ingredients": a JSON array of English ingredient names mentioned in the description,
178+
with adjectives removed and excluding "water"
179+
No other keys or commentary.
180+
""";
181+
182+
Recipe recipe = recipeRepository.findById(recipeId)
183+
.orElseThrow(() -> BaseException.from(ErrorCode.RECIPE_NOT_EXIST));
184+
185+
RecipeStep recipeStep = recipeStepRepository.findByRecipeAndStepNumber(recipe, stepNumber)
186+
.orElseThrow(() -> BaseException.from(ErrorCode.MEMBER_NOT_FOUND));
187+
188+
RecipeStepDetailDto dto = new RecipeStepDetailDto(recipeStep.getStepNumber(),
189+
recipeStep.getAction(), recipeStep.getDescription(), recipeStep.getIngredients());
190+
191+
List<Message> messages = List.of(
192+
new Message("system", systemPrompt),
193+
new Message("user", dto.getDescription())
194+
);
195+
ChatGPTRequest req = new ChatGPTRequest(model, messages);
196+
ChatGPTResponse resp = restTemplate.postForObject(apiUrl, req, ChatGPTResponse.class);
197+
String content = resp.getChoices().get(0).getMessage().getContent().trim();
198+
return content;
199+
}
117200
}
118201

yechef/src/main/java/sejong/capston/yechef/domain/Recipe/dto/RecipeStepDto.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import lombok.*;
44
import sejong.capston.yechef.domain.RecipeSteps.RecipeStep;
55

6+
import java.util.List;
7+
68
@Getter
79
@Setter
810
@NoArgsConstructor
@@ -12,11 +14,14 @@ public class RecipeStepDto {
1214
private int stepNumber; // 1,2,3,…
1315
private String action; // ex) “떡 자르기”
1416
private String description; // “떡 300g을 먹기 좋은 크기로 썬다”
17+
private List<String> ingredients;
1518

1619
public static RecipeStepDto from(RecipeStep step) {
1720
return RecipeStepDto.builder()
1821
.stepNumber(step.getStepNumber())
22+
.action(step.getAction())
1923
.description(step.getDescription())
24+
.ingredients(step.getIngredients())
2025
.build();
2126
}
2227
}

yechef/src/main/java/sejong/capston/yechef/domain/Recipe/service/RecipeService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import sejong.capston.yechef.domain.Recipe.ai.OcrClient;
2222
import sejong.capston.yechef.domain.Recipe.dto.DetailRecipeDto;
2323
import sejong.capston.yechef.domain.Recipe.dto.RecipeDto;
24+
import sejong.capston.yechef.domain.Recipe.dto.RecipeStepDto;
2425
import sejong.capston.yechef.domain.Recipe.repository.RecipeRepository;
2526
import sejong.capston.yechef.domain.RecipeSteps.service.RecipeStepService;
2627
import sejong.capston.yechef.global.exception.BaseException;

yechef/src/main/java/sejong/capston/yechef/domain/RecipeSteps/RecipeStep.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import sejong.capston.yechef.domain.Recipe.Recipe;
99
import sejong.capston.yechef.global.entity.BaseEntity;
1010

11+
import java.util.List;
12+
1113
@Getter
1214
@Setter
1315
@Entity
@@ -24,20 +26,29 @@ public class RecipeStep extends BaseEntity {
2426

2527
@NotNull @Lob private String description;
2628

29+
@Lob private String action;
30+
private List<String> ingredients;
31+
2732
@ManyToOne(fetch = FetchType.LAZY)
2833
@JoinColumn(name = "recipe_id")
2934
private Recipe recipe;
3035

3136
@Builder
32-
public RecipeStep(int stepNumber, String description, Recipe recipe) {
37+
public RecipeStep (int stepNumber, String action, List<String> ingredients,
38+
String description, Recipe recipe) {
3339
this.stepNumber = stepNumber;
40+
this.action = action;
41+
this.ingredients = ingredients;
3442
this.description = description;
3543
this.recipe = recipe;
3644
}
3745

38-
public static RecipeStep of(int stepNumber, String description, Recipe recipe) {
46+
public static RecipeStep of(int stepNumber, String action, List<String> ingredients,
47+
String description, Recipe recipe) {
3948
return RecipeStep.builder()
4049
.stepNumber(stepNumber)
50+
.action(action)
51+
.ingredients(ingredients)
4152
.description(description)
4253
.recipe(recipe)
4354
.build();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package sejong.capston.yechef.domain.RecipeSteps.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.PathVariable;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
import sejong.capston.yechef.domain.RecipeSteps.dto.RecipeStepDetailDto;
9+
import sejong.capston.yechef.domain.RecipeSteps.service.RecipeStepService;
10+
11+
@RestController
12+
@RequiredArgsConstructor
13+
@RequestMapping("/api/recipesteps")
14+
public class RecipeStepController {
15+
16+
private final RecipeStepService recipeStepService;
17+
18+
@GetMapping("/{recipeId}/{stepNumber}")
19+
public RecipeStepDetailDto getParsedStep(
20+
@PathVariable Long recipeId,
21+
@PathVariable int stepNumber
22+
) {
23+
return recipeStepService.getParsedRecipeStep(recipeId, stepNumber);
24+
}
25+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package sejong.capston.yechef.domain.RecipeSteps.dto;
2+
3+
import lombok.*;
4+
import sejong.capston.yechef.domain.RecipeSteps.RecipeStep;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@Setter
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
@Builder
13+
public class RecipeStepDetailDto {
14+
private int stepNumber; // 1,2,3,…
15+
private String action; // ex) “떡 자르기”
16+
private String description; // “떡 300g을 먹기 좋은 크기로 썬다”
17+
private List<String> ingredients;
18+
19+
public RecipeStepDetailDto(int stepNumber, String action, String description) {
20+
this.stepNumber = stepNumber;
21+
this.action = action;
22+
this.description = description;
23+
}
24+
25+
public static RecipeStepDetailDto from(RecipeStep step) {
26+
return RecipeStepDetailDto.builder()
27+
.stepNumber(step.getStepNumber())
28+
.description(step.getDescription())
29+
.build();
30+
}
31+
32+
public static RecipeStepDetailDto of(int stepNumber, String action, String description, List<String> ingredients) {
33+
return RecipeStepDetailDto.builder()
34+
.stepNumber(stepNumber)
35+
.action(action)
36+
.description(description)
37+
.ingredients(ingredients)
38+
.build();
39+
}
40+
}

yechef/src/main/java/sejong/capston/yechef/domain/RecipeSteps/dto/RecipeStepDto.java

Lines changed: 0 additions & 22 deletions
This file was deleted.

yechef/src/main/java/sejong/capston/yechef/domain/RecipeSteps/service/RecipeStepService.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,56 @@
33
import lombok.RequiredArgsConstructor;
44
import org.springframework.stereotype.Service;
55
import org.springframework.transaction.annotation.Transactional;
6+
import sejong.capston.yechef.domain.Gpt.service.GptService;
67
import sejong.capston.yechef.domain.Recipe.Recipe;
78
import sejong.capston.yechef.domain.Recipe.dto.RecipeStepDto;
9+
import sejong.capston.yechef.domain.Recipe.repository.RecipeRepository;
810
import sejong.capston.yechef.domain.RecipeSteps.RecipeStep;
11+
import sejong.capston.yechef.domain.RecipeSteps.dto.RecipeStepDetailDto;
912
import sejong.capston.yechef.domain.RecipeSteps.repository.RecipeStepRepository;
13+
import sejong.capston.yechef.global.exception.BaseException;
14+
import sejong.capston.yechef.global.exception.error.ErrorCode;
1015

1116
import java.util.List;
1217

1318
@Service
1419
@RequiredArgsConstructor
1520
public class RecipeStepService {
21+
1622
private final RecipeStepRepository recipeStepRepository;
23+
private final RecipeRepository recipeRepository;
24+
25+
private final GptService gptService;
1726

1827
@Transactional
19-
public void saveSteps(List<RecipeStepDto> recipeStepDtos, Recipe recipe) {
20-
for (var dto : recipeStepDtos) {
21-
RecipeStep step = RecipeStep.of(
22-
dto.getStepNumber(),
23-
dto.getDescription(),
24-
recipe
25-
);
26-
recipeStepRepository.save(step);
28+
public void saveSteps(List<RecipeStepDetailDto> recipeStepDetailDtos, Recipe recipe) {
29+
for (var dto : recipeStepDetailDtos) {
30+
RecipeStep step = RecipeStep.of(
31+
dto.getStepNumber(),
32+
dto.getAction(),
33+
dto.getIngredients(),
34+
dto.getDescription(),
35+
recipe
36+
);
37+
recipeStepRepository.save(step);
2738
}
2839
}
40+
41+
@Transactional(readOnly = true)
42+
public RecipeStepDetailDto getRecipeStep(Long recipeId, int stepNumber) {
43+
Recipe recipe = recipeRepository.findById(recipeId)
44+
.orElseThrow(() -> BaseException.from(ErrorCode.RECIPE_NOT_EXIST));
45+
46+
return recipe.getRecipeSteps().stream()
47+
.filter(s -> s.getStepNumber() == stepNumber)
48+
.findFirst()
49+
.map(RecipeStepDetailDto::from)
50+
.orElseThrow(() -> BaseException.from(ErrorCode.RECIPE_NOT_EXIST));
51+
}
52+
53+
@Transactional(readOnly = true)
54+
public RecipeStepDetailDto getParsedRecipeStep(Long recipeId, int stepNumber) {
55+
RecipeStepDetailDto dto = getRecipeStep(recipeId, stepNumber);
56+
return gptService.parseRecipeSteps(recipeId, stepNumber);
57+
}
2958
}

0 commit comments

Comments
 (0)