Skip to content

Commit

Permalink
feat : ChatGPT API 연동 (#22)
Browse files Browse the repository at this point in the history
* feat: 프로젝트 환경 설정 및 DDD 아키텍처 예시 패키지 및 클래스 작성 (#2)

* feat: 프로젝트 환경 설정

* feat: DDD 아키텍처 예시 패키지 및 클래스 작성

* chore: Spotless 코드 포맷팅 + pre-commit 스크립트 작성 (#4)

chore: Spotless 코드 포맷팅 + pre-commit 스크립트 작성 (#4)

* chore : spotless 설정

* chore : spotless 컨벤션 적용

* chore : spotless pre-commit 적용

* chore : spotless 자바 포맷 변경

* chore : 자바 컨벤션 적용

[Chore] Spotless 코드 포맷팅 + pre-commit 스크립트 작성 (#4)

* chore : spotless 설정

* chore : spotless 컨벤션 적용

* chore : spotless pre-commit 적용

* chore : spotless 자바 포맷 변경

* chore : 자바 컨벤션 적용

* chore: 스웨거 세팅 (#8)

* chore : 스웨거 세팅

* chore : 스웨거 세팅

* chore : application.yml에 스웨거 설정 추가

* chore : 스웨거 버전 2.1 -> 2.8.4로 변경

* chore: 개발 서버 docker compose 기반 환경 구축 및 배포 파이프라인 작성 (#10)

* chore: pr open 시 빌드 체크 워크플로 추가

* chore: 개발 서버 docker-compose.yml 추가

* chore: Dockerfile 작성

* chore: 개발 서버 배포 파이프라인 작성

* chore: docker login 최신 버전 적용

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: 테스트를 위해 임시 배포 조건 설정

* chore: 개행 추가

* chore: docker-compose.yml 잘못된 경로 수정

* chore: 배포 테스트를 위한 on pull_request 조건 제거

* chore: compileJava로 변경

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* �chore : 이슈 템플릿 수정 (#11)

* docs : 이슈 템플릿 생성

* docs : 이슈 템플릿 작성

* docs : 이슈 템플릿 작성

* �docs : 이슈 템플릿 작성

* docs : 이슈 템플릿 작성

* docs : 이슈 템플릿 작성

* chore : 이슈 템플릿 설정

* feat : 글로벌 에러 핸들러 설정 (#13) (#14)

* feat : 글로벌 에러 핸들러 설정 (#13)

* feat : PR 리뷰 수정

* feat : 컨벤션에 맞게 수정

* feat : entity 설계 (#16)

* feat : entity 설계

* feat : gradle 수정

* feat : 리뷰 사항 수정

* chore : 포트폴리오 패키지 정리

* chore : 포트폴리오 패키지 정리

* feat : ChatGPT 프로퍼티 추가

* feat : ChatGPT 프로퍼티 추가

* feat : RestClient를 이용해 ChatGPT API 요청 구현

* feat : RestClient를 이용해 ChatGPT API 요청 구현

* chore : 파일 정리

* chore : 파일 정리

* feat : chatGPT API 연동

* feat : chatGPT API 연동

* chore : gitignore 설정

* chore : gitignore 설정

* chore : 파일 정리

* feat : response 스키마 추가

* feat : ChatGPT 서비스 수정

* chore : 파일 정리

* feat : static import, 기타 코드 리뷰 반영

* feat : static import, 기타 코드 리뷰 반영

---------

Co-authored-by: 이한음 <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Choi Tae gyu <[email protected]>
  • Loading branch information
4 people authored Feb 17, 2025
1 parent f19ccd0 commit 2609287
Show file tree
Hide file tree
Showing 26 changed files with 468 additions and 1 deletion.
43 changes: 43 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Java 관련
*.class
*.war
*.jar

# IDE 설정 파일
/.idea/
/.vscode/
/.project
/.classpath
/.settings/

# OS 관련
.DS_Store
Thumbs.db

# 환경 변수 및 보안 파일
.env
config/*.yml
config/*.properties
src/main/resources/application-*.yml
src/main/resources/application-*.properties

# 로그 및 빌드 파일
/logs/
*.log
/build/
/target/

# Git 관련
*.swp
*.swo
*.bak

# IntelliJ 관련
*.iml
*.ipr
*.iws

# 기타
*.~
*.tmp
*.backup
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ dependencies {
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// HTTP CLIENT
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1'

// MONGO DB
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package depromeet.onepiece.common.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan
public class PropertiesConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package depromeet.onepiece.common.config;

import depromeet.onepiece.feedback.command.infrastructure.ChatGPTConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {
private static final String API_URL = ChatGPTConstants.API_URL;

@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl(API_URL)
.defaultHeader("Content-Type", "application/json")
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package depromeet.onepiece.feedback.command.application;

import depromeet.onepiece.feedback.command.infrastructure.ChatGPTService;
import depromeet.onepiece.feedback.command.presentation.request.ChatGPTRequest;
import depromeet.onepiece.feedback.command.presentation.response.OverallFeedbackResponse;
import depromeet.onepiece.feedback.command.presentation.response.ProjectFeedbackResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class FeedbackCommandService {
private final ChatGPTService chatGPTService;

public OverallFeedbackResponse overallFeedback(String portfolioId) {
// portfolioId 검증
// portfolioId로 포트폴리오 조회
// 포트폴리오 이미지 추출
// chatgptservice의 overallfeedback 포트폴리오 이미지 담아서 호출
ChatGPTRequest requestDto = createOverallFeedbackRequest(portfolioId);
return chatGPTService.overallFeedback(requestDto);
}

public ProjectFeedbackResponse projectFeedback(String portfolioId) {
// portfolioId 검증
// portfolioId로 포트폴리오 조회
// 포트폴리오 이미지 추출
// chatgptservice의 projectfeedback 포트폴리오 이미지 담아서 호출
ChatGPTRequest requestDto = createProjectFeedbackRequest(portfolioId);
return chatGPTService.projectFeedback(requestDto);
}

private ChatGPTRequest createOverallFeedbackRequest(String portfolioId) {
return new ChatGPTRequest(
"gpt-4",
List.of(
new ChatGPTRequest.Message(
"user", List.of(new ChatGPTRequest.Message.Content("text", "프롬프트", null)))),
new ChatGPTRequest.ResponseFormat("json", null),
0.7,
5000,
1.0,
0,
0);
}

private ChatGPTRequest createProjectFeedbackRequest(String portfolioId) {
return new ChatGPTRequest(
"gpt-4",
List.of(
new ChatGPTRequest.Message(
"user", List.of(new ChatGPTRequest.Message.Content("text", "프롬프트", null)))),
new ChatGPTRequest.ResponseFormat("json", null),
0.8,
5000,
1.0,
0,
0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package depromeet.onepiece.feedback.command.application.exception;

/** CommandExceptionCode는 CommandService에서 발생하는 커스텀 예외 코드를 정의해요. */
public enum FeedbackCommandExceptionCode {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package depromeet.onepiece.feedback.command.domain;

/**
* 도메인 주도 개발 DDD 아키텍처에서는 CommandRepository 별도로 구현하여 CQRS 패턴을 적용합니다. Master-Slave 구조에서는 Master DB에 쓰기
* 작업을 하고, Slave DB에서 읽기 작업을 하기 때문에, CommandRepository를 별도로 구현해요.
*/
public interface FeedbackCommandRepository {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package depromeet.onepiece.feedback.command.infrastructure;

import static depromeet.onepiece.feedback.command.infrastructure.ChatGPTConstants.API_URL;

import depromeet.onepiece.feedback.command.presentation.request.ChatGPTRequest;
import depromeet.onepiece.feedback.command.presentation.response.ChatGPTResponse;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
@AllArgsConstructor
public class ChatGPTClient {
private final RestClient restClient;
private final ChatGPTProperties chatGPTProperties;

public ChatGPTResponse sendMessage(ChatGPTRequest request) {
return restClient
.post()
.uri(API_URL)
.header("Authorization", "Bearer " + chatGPTProperties.apiKey())
.header("Content-Type", "application/json")
.body(request)
.retrieve()
.body(ChatGPTResponse.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package depromeet.onepiece.feedback.command.infrastructure;

import lombok.experimental.UtilityClass;

@UtilityClass
public final class ChatGPTConstants {
public static final String API_URL = "https://api.openai.com/v1/chat/completions";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package depromeet.onepiece.feedback.command.infrastructure;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "chatgpt")
public record ChatGPTProperties(
String apiKey, String model, Double temperature, Integer maxTokens) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package depromeet.onepiece.feedback.command.infrastructure;

import depromeet.onepiece.feedback.command.presentation.request.ChatGPTRequest;
import depromeet.onepiece.feedback.command.presentation.response.ChatGPTResponse;
import depromeet.onepiece.feedback.command.presentation.response.OverallFeedbackResponse;
import depromeet.onepiece.feedback.command.presentation.response.ProjectFeedbackResponse;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ChatGPTService {
private final ChatGPTClient chatGPTClient;

public OverallFeedbackResponse overallFeedback(ChatGPTRequest requestDto) {
ChatGPTRequest updatedRequest =
new ChatGPTRequest(
requestDto.model(),
requestDto.messages(),
requestDto.response_format(),
0.7,
5000,
1.0,
0,
0);

ChatGPTResponse response = chatGPTClient.sendMessage(updatedRequest);
return mapToOverallFeedback(response);
}

public ProjectFeedbackResponse projectFeedback(ChatGPTRequest requestDto) {
ChatGPTRequest updatedRequest =
new ChatGPTRequest(
requestDto.model(),
requestDto.messages(),
requestDto.response_format(),
0.8,
5000,
1.0,
0,
0);

ChatGPTResponse response = chatGPTClient.sendMessage(updatedRequest);
return mapToProjectFeedback(response);
}

// TODO: response 수정 후 매핑
private OverallFeedbackResponse mapToOverallFeedback(ChatGPTResponse response) {
return new OverallFeedbackResponse(
new OverallFeedbackResponse.OverallEvaluation(
response.response(),
new OverallFeedbackResponse.EvaluationDetail(80, "직무 적합성이 높습니다."),
new OverallFeedbackResponse.EvaluationDetail(75, "논리적인 사고력이 돋보입니다."),
new OverallFeedbackResponse.EvaluationDetail(85, "글쓰기 표현이 명확합니다."),
new OverallFeedbackResponse.EvaluationDetail(90, "레이아웃이 가독성이 높습니다.")),
List.of(new OverallFeedbackResponse.FeedbackDetail("창의적인 디자인", "다양한 시각적 요소 활용이 뛰어남")),
List.of(new OverallFeedbackResponse.FeedbackDetail("문장 간결화 필요", "텍스트를 조금 더 다듬으면 좋습니다.")));
}

// TODO: response 수정 후 매핑
private ProjectFeedbackResponse mapToProjectFeedback(ChatGPTResponse response) {
return new ProjectFeedbackResponse(
new ProjectFeedbackResponse.ProjectEvaluation(
"프로젝트 이름", "https://example.com/image.png", null, "프로세스 리뷰", "강점 분석", "개선 사항", null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package depromeet.onepiece.feedback.command.infrastructure;

/**
* RepositoryImpl은 Repository 인터페이스를 구현한 클래스로, 실제로 데이터베이스에 접근하여 데이터를 저장하거나 조회하는 역할을 해요. 데이터베이스 접근
* 기술(JPA DATA, QueryDSL, MyBatis 등)을 사용하여 구현합니다. 만약 JPA 및 QueryDSL을 사용한다면, infrastructure 패키지 내에
* PaymentCommandJpaRepository, PaymentCommandQueryDslRepository 같이 구현 클래스를 생성할 수 있어요.
*/
public class FeedbackCommandRepositoryImpl {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package depromeet.onepiece.feedback.command.presentation;

import depromeet.onepiece.feedback.command.application.FeedbackCommandService;
import depromeet.onepiece.feedback.command.presentation.response.OverallFeedbackResponse;
import depromeet.onepiece.feedback.command.presentation.response.ProjectFeedbackResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Portfolio", description = "포트폴리오 관련 API")
@RestController
@RequestMapping("/api/v1/portfolio")
@RequiredArgsConstructor
public class FeedbackCommandController {
private final FeedbackCommandService portfolioCommandService;

@Operation(summary = "포트폴리오 종합평가 반환", description = "종합평가를 반환하는 API")
@GetMapping("/overall")
public OverallFeedbackResponse overallFeedback(@RequestParam String portfolioId) {
return portfolioCommandService.overallFeedback(portfolioId);
}

@Operation(summary = "포트폴리오 프로젝트 별 피드백 반환", description = "프로젝트 별 피드백을 반환하는 API")
@GetMapping("/project")
public ProjectFeedbackResponse projectFeedback(@RequestParam String portfolioId) {
return portfolioCommandService.projectFeedback(portfolioId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package depromeet.onepiece.feedback.command.presentation.request;

import java.util.List;
import java.util.Map;

public record ChatGPTRequest(
String model,
List<Message> messages,
ResponseFormat response_format,
Double temperature,
Integer max_completion_tokens,
Double top_p,
Integer frequency_penalty,
Integer presence_penalty) {
public record Message(String role, List<Content> content) {
public record Content(String type, String text, ImageUrl image_url) {
public record ImageUrl(String url) {}
}
}

public record ResponseFormat(String type, JsonSchema json_schema) {
public record JsonSchema(String name, Boolean strict, Map<String, Object> schema) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package depromeet.onepiece.feedback.command.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;

public record ChatGPTResponse(
@Schema(description = "챗봇 응답", requiredMode = REQUIRED) String response) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package depromeet.onepiece.feedback.command.presentation.response;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public record OverallFeedbackResponse(
@Schema(description = "전체 평가", requiredMode = REQUIRED) OverallEvaluation overallEvaluation,
@Schema(description = "강점 분석", requiredMode = REQUIRED) List<FeedbackDetail> strengths,
@Schema(description = "개선할 점 및 해결방안", requiredMode = REQUIRED)
List<FeedbackDetail> improvements) {

public record OverallEvaluation(
@Schema(description = "종합 평가 요약", requiredMode = REQUIRED) String summary,
@Schema(description = "직무 적합성", requiredMode = REQUIRED) EvaluationDetail jobFit,
@Schema(description = "논리적 사고 평가", requiredMode = REQUIRED) EvaluationDetail logicalThinking,
@Schema(description = "문장 가독성 평가", requiredMode = REQUIRED) EvaluationDetail writingClarity,
@Schema(description = "레이아웃 가독성 평가", requiredMode = REQUIRED)
EvaluationDetail layoutReadability) {}

public record EvaluationDetail(
@Schema(description = "점수 (0-100 범위)", requiredMode = REQUIRED) int score,
@Schema(description = "평가", requiredMode = REQUIRED) String review) {}

public record FeedbackDetail(
@Schema(description = "제목", example = "스토리텔링이 강한 포트폴리오", requiredMode = REQUIRED)
String title,
@Schema(
description = "내용",
example = "단순한 디자인을 넘어서는 창의적인 아이디어가 돋보입니다.",
requiredMode = REQUIRED)
String content) {}
}
Loading

0 comments on commit 2609287

Please sign in to comment.