Skip to content

Commit faebc5a

Browse files
authored
Merge pull request #43 from Move-Log/develop
Merge develop to main
2 parents 8727901 + 5f5da0b commit faebc5a

File tree

18 files changed

+405
-27
lines changed

18 files changed

+405
-27
lines changed

src/main/java/com/movelog/domain/auth/presentation/AuthController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public ResponseEntity<AuthRes> login(@RequestBody IdTokenReq idTokenReq) {
6161
authService.findOrCreateUser(idTokenReq.provider(), idTokenReq.idToken());
6262
String email = authService.findEmail(providerId);
6363

64-
String accessToken = jwtUtil.createJwt("access", providerId, "ROLE_USER", 3600000L, email);
64+
String accessToken = jwtUtil.createJwt("access", providerId, "ROLE_USER", 2592000000L, email);
6565
System.out.println("accessToken = " + accessToken);
6666
User user = userRepository.findByProviderId(providerId)
6767
.orElseThrow(EntityNotFoundException::new);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.movelog.domain.gpt.application;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.http.MediaType;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.web.reactive.function.client.WebClient;
10+
import reactor.core.publisher.Mono;
11+
12+
import java.util.HashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
@Slf4j
19+
public class GptService {
20+
21+
private final WebClient gptWebClient; // @Qualifier 제거하고 빈 이름에 맞는 생성자 주입
22+
@Value("${chatgpt.api-key}")
23+
private String apiKey;
24+
@Value("${chatgpt.model}")
25+
private String model;
26+
@Value("${chatgpt.max-tokens}")
27+
private Integer maxTokens;
28+
@Value("${chatgpt.temperature}")
29+
private Double temperature;
30+
@Value("${chatgpt.top-p}")
31+
private Double topP;
32+
33+
public Mono<JsonNode> callChatGpt(String prompt) {
34+
return gptWebClient.post()
35+
.uri("/chat/completions") // 경로에 대한 수정
36+
.header("Authorization", "Bearer " + apiKey)
37+
.contentType(MediaType.APPLICATION_JSON)
38+
.bodyValue(buildRequestBody(prompt))
39+
.retrieve()
40+
.bodyToMono(JsonNode.class)
41+
.doOnSuccess(response -> log.info("GPT API 응답 성공: {}", response))
42+
.doOnError(error -> log.error("GPT API 호출 중 오류 발생: {}", error.getMessage()))
43+
.onErrorResume(error -> {
44+
log.error("GPT API 호출 중 예외 처리 발생: {}", error.getMessage());
45+
return Mono.error(new RuntimeException("GPT API 호출 실패", error));
46+
});
47+
}
48+
49+
private Map<String, Object> buildRequestBody(String prompt) {
50+
Map<String, Object> bodyMap = new HashMap<>();
51+
bodyMap.put("model", model);
52+
bodyMap.put("max_tokens", maxTokens);
53+
bodyMap.put("temperature", temperature);
54+
bodyMap.put("top_p", topP);
55+
bodyMap.put("messages", List.of(
56+
Map.of(
57+
"role", "user",
58+
"content", prompt
59+
)
60+
));
61+
return bodyMap;
62+
}
63+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.movelog.domain.news.application;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.movelog.domain.gpt.application.GptService;
5+
import com.movelog.domain.news.dto.response.HeadLineRes;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.stereotype.Service;
9+
import reactor.core.publisher.Mono;
10+
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
@Slf4j
17+
public class HeadlineGeneratorService {
18+
19+
private final GptService gptService;
20+
21+
public List<HeadLineRes> generateHeadLine(String option, String verb, String noun) {
22+
// 프롬프트 생성
23+
String prompt = generatePrompt(option, verb, noun);
24+
25+
// 비동기 방식으로 GPT API 호출
26+
return gptService.callChatGpt(prompt)
27+
.flatMap(response -> {
28+
if (response == null) {
29+
return Mono.error(new RuntimeException("GPT API 응답이 없습니다."));
30+
}
31+
32+
// 응답 파싱하여 List<CreatePracticeRes> 반환
33+
List<HeadLineRes> practiceResList = parseResponse(response);
34+
return Mono.just(practiceResList);
35+
})
36+
.doOnError(error -> log.error("문제 생성 중 오류 발생: ", error)).block();
37+
}
38+
39+
40+
// 프롬프트 생성
41+
private String generatePrompt(String option, String verb, String noun) {
42+
return String.format(
43+
"다음 내용을 바탕으로 뉴스 헤드라인을 생성해주세요: %s\n" +
44+
"당신은 뉴스 헤드라인을 작성하는 역할이며, 다음 형식을 엄격히 따라 뉴스 헤드라인을 제공해 주세요:\n\n" +
45+
"이 뉴스 헤드라인의 목적은 사용자의 기록을 기반으로 약간의 재미 요소가 들어간 헤드라인을 제공하는 것입니다.\n" +
46+
"각 뉴스 헤드라인은 반드시 격식있는 어체로 구성되어야 하며, 반드시 올바른 어법을 준수해야 합니다.\n" +
47+
"각 헤드라인의 내용은 주어진 옵션, 동사, 명사를 기반으로 생성되어야 하며, 헤드라인 내용에 직접적인 내용이 들어가지 않아도 됩니다." +
48+
"뉴스 헤드라인은 반드시 3개의 후보를 제공해야 하며, 각 후보는 1개의 줄바꿈으로 구분됩니다.\n" +
49+
"각 헤드라인 내용의 구조는 반드시 쉼표로 나뉘어야 하며, 쉼표 이전의 내용에는 헤드라인 강조 옵션에 대한 내용이 포함되어야 합니다." +
50+
"쉼표 앞 뒤의 내용에 대한 글자 수는 각각 16자 이하여야 하며, 쉼표는 반드시 한글자로만 구성되어야 합니다.\n\n" +
51+
52+
"다음은 제공된 정보를 통해 출력해야 하는 뉴스 헤드라인의 출력 예시입니다:\n\n" +
53+
"꾸준한 노력 끝에 완성한 마라톤 기록, 도전의 의미는?\n오랜만의 첫 도전, 무엇이 그를 움직이게 했나?\n드디어 끊어낸 술, 운동으로 극복해내" +
54+
55+
"다음은 제공된 정보를 통해 출력해야 하는 뉴스 헤드라인의 출력 형식입니다:\n\n" +
56+
"첫번째 헤드라인 후보 내용\n두번째 헤드라인 후보 내용\n세번째 헤드라인 후보 내용\n" +
57+
58+
"반드시 뉴스 헤드라인 형식을 준수해 주세요. 정해진 형식을 따르지 않으면 응답을 처리할 수 없습니다." +
59+
"만약 결과 헤드라인 내용이 위 형식과 다르다면, 다시 요청하여 주세요.",
60+
option, verb, noun
61+
);
62+
}
63+
64+
// GPT 응답 파싱
65+
private List<HeadLineRes> parseResponse(JsonNode response) {
66+
List<HeadLineRes> headLineResList = new ArrayList<>();
67+
68+
// GPT 응답에서 질문, 답변, 해설 추출
69+
String textResponse = response.path("choices").get(0).path("message").path("content").asText().trim();
70+
log.info("Full Text Response: " + textResponse); // 전체 응답 확인
71+
72+
String[] headLines = textResponse.split("\n");
73+
// 헤드라인 생성 결과가 3개가 아닌 경우 예외 처리
74+
if (headLines.length != 3) {
75+
throw new RuntimeException("생성된 헤드라인이 3개가 아닙니다.");
76+
}
77+
78+
// 헤드라인 후보 파싱 결과를 리스트에 저장 후 응답 리턴
79+
for (String headLine : headLines) {
80+
headLineResList.add(HeadLineRes.builder().headLine(headLine).build());
81+
}
82+
83+
return headLineResList;
84+
}
85+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.movelog.domain.news.application;
2+
3+
import com.movelog.domain.news.dto.request.NewsHeadLineReq;
4+
import com.movelog.domain.news.dto.response.HeadLineRes;
5+
import com.movelog.domain.user.application.UserService;
6+
import com.movelog.domain.user.domain.User;
7+
import com.movelog.domain.user.domain.repository.UserRepository;
8+
import com.movelog.domain.user.exception.UserNotFoundException;
9+
import com.movelog.global.DefaultAssert;
10+
import com.movelog.global.config.security.token.UserPrincipal;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
import java.util.List;
17+
import java.util.Optional;
18+
19+
@Service
20+
@RequiredArgsConstructor
21+
@Transactional(readOnly = true)
22+
public class NewsService {
23+
private final HeadlineGeneratorService headlineGeneratorService;
24+
private final UserService userService;
25+
private final UserRepository userRepository;
26+
27+
public List<HeadLineRes> createHeadLine(UserPrincipal userPrincipal, NewsHeadLineReq newsHeadLineReq) {
28+
User user = validateUser(userPrincipal);
29+
// id가 5인 유저 정보(테스트용)
30+
// User user = userRepository.findById(5L).orElseThrow(UserNotFoundException::new);
31+
String option = newsHeadLineReq.getOption();
32+
String verb = newsHeadLineReq.getVerb();
33+
String noun = newsHeadLineReq.getNoun();
34+
return headlineGeneratorService.generateHeadLine(option, verb, noun);
35+
}
36+
37+
38+
private User validateUser(UserPrincipal userPrincipal) {
39+
Optional<User> userOptional = userService.findById(userPrincipal.getId());
40+
DefaultAssert.isOptionalPresent(userOptional);
41+
return userOptional.get();
42+
}
43+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.movelog.domain.news.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Builder
10+
@AllArgsConstructor
11+
@NoArgsConstructor
12+
@Getter
13+
public class NewsHeadLineReq {
14+
@Schema( type = "String", example ="첫 도전, 오랜만에 다시, 꾸준히 이어온 기록, 끊어낸 습관 중 택 1", description="뉴스 헤드라인 고정 옵션입니다.")
15+
private String option;
16+
17+
@Schema( type = "String", example ="했어요, 먹었어요, 갔어요 중 택 1", description="사용자가 선택한 동사 정보입니다.")
18+
private String verb;
19+
20+
@Schema( type = "String", example ="클라이밍", description="사용자가 선택한 명사 정보입니다.")
21+
private String noun;
22+
23+
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.movelog.domain.news.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Builder
10+
@AllArgsConstructor
11+
@NoArgsConstructor
12+
@Getter
13+
public class HeadLineRes {
14+
@Schema( type = "String", example ="5년 만의 첫 도전, 무엇이 그를 움직이게 했나?", description="뉴스 헤드라인 추천 내용입니다.")
15+
private String headLine;
16+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.movelog.domain.news.presentation;
2+
3+
import com.movelog.domain.news.application.NewsService;
4+
import com.movelog.domain.news.dto.request.NewsHeadLineReq;
5+
import com.movelog.domain.news.dto.response.HeadLineRes;
6+
import com.movelog.global.config.security.token.CurrentUser;
7+
import com.movelog.global.config.security.token.UserPrincipal;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.Parameter;
10+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
11+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
12+
import io.swagger.v3.oas.annotations.tags.Tag;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.web.bind.annotation.PostMapping;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RestController;
19+
20+
import java.util.List;
21+
22+
@Slf4j
23+
@RestController
24+
@RequiredArgsConstructor
25+
@RequestMapping("/api/v1/news")
26+
@Tag(name = "News", description = "뉴스 관련 API입니다.")
27+
public class NewsController {
28+
29+
private final NewsService newsService;
30+
31+
@Operation(summary = "뉴스 헤드라인 생성 API", description = "뉴스 헤드라인을 생성하는 API입니다.")
32+
@ApiResponses(value = {
33+
@ApiResponse(responseCode = "200", description = "뉴스 헤드라인 생성 성공"),
34+
@ApiResponse(responseCode = "400", description = "뉴스 헤드라인 생성 실패")
35+
})
36+
@PostMapping("/headline")
37+
public List<HeadLineRes> createHeadLine(
38+
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
39+
@Parameter(description = "뉴스 헤드라인 생성 요청", required = true) @RequestBody NewsHeadLineReq newsHeadLineReq
40+
) {
41+
return newsService.createHeadLine(userPrincipal, newsHeadLineReq);
42+
}
43+
44+
45+
46+
}

src/main/java/com/movelog/domain/record/domain/Record.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public class Record extends BaseEntity {
2828
@JoinColumn(name = "keyword_id")
2929
private Keyword keyword;
3030

31-
@Column(name = "verb_type")
32-
private Long verbType;
31+
@Enumerated(EnumType.STRING) // Enum을 문자열로 저장
32+
private VerbType verbType;
3333

3434
@Column(name = "record_image")
3535
private String recordImage;
@@ -38,14 +38,11 @@ public class Record extends BaseEntity {
3838
private java.time.LocalDateTime actionTime;
3939

4040
@Builder
41-
public Record(User user, Keyword keyword, Long verbType, String recordImage) {
41+
public Record(User user, Keyword keyword, VerbType verbType, String recordImage) {
4242
this.user = user;
4343
this.keyword = keyword;
4444
this.verbType = verbType;
4545
this.recordImage = recordImage;
4646
this.actionTime = actionTime == null? LocalDateTime.now():actionTime;
4747
}
48-
49-
50-
5148
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.movelog.domain.record.domain;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum VerbType {
7+
// 했어요, 먹었어요, 갔어요
8+
DO("했어요"), EAT("먹었어요"), GO("갔어요");
9+
10+
private final String verbType;
11+
12+
VerbType(String verbType) {
13+
this.verbType = verbType;
14+
}
15+
16+
// Enum 매핑 메서드 추가
17+
public static VerbType fromValue(String value) {
18+
for (VerbType type : values()) {
19+
if (type.getVerbType().equals(value)) {
20+
return type;
21+
}
22+
}
23+
throw new IllegalArgumentException("No enum constant for value: " + value);
24+
}
25+
}

src/main/java/com/movelog/domain/record/dto/req/CreateRecordReq.java renamed to src/main/java/com/movelog/domain/record/dto/request/CreateRecordReq.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.movelog.domain.record.dto.req;
1+
package com.movelog.domain.record.dto.request;
22

33
import io.swagger.v3.oas.annotations.media.Schema;
44
import lombok.AllArgsConstructor;
@@ -11,8 +11,8 @@
1111
@NoArgsConstructor
1212
@Getter
1313
public class CreateRecordReq {
14-
@Schema(type = "Long", example = "0", description = "0 -> 했어요, 1 -> 먹었어요, 2 -> 갔어요")
15-
private Long verbType;
14+
@Schema(type = "String", example = "했어요", description = "했어요, 먹었어요, 갔어요")
15+
private String verbType;
1616

1717
@Schema(type = "String", example = "헬스", description = "명사 작성")
1818
private String noun;

0 commit comments

Comments
 (0)