Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
2aeb2a1
:wrench: chore(config): checkstyle 개행 문자 규칙 적용을 위해 .gitattributes에 LF…
Aug 16, 2025
397ba4c
:sparkles: feat: 에러 코드 형식 정의 [KOBG-4]
Aug 16, 2025
afa43f7
:sparkles: feat: 글로벌 에러 코드 추가 [KOBG-4]
Aug 16, 2025
b23ff24
:sparkles: feat: 내부 예외 처리를 위한 AppException 클래스 추가 [KOBG-4]
Aug 16, 2025
9546179
:sparkles: feat: 정해진 응답 형식을 위한 dto 추가 [KOBG-4]
Aug 16, 2025
7b4b682
:sparkles: feat: 글로벌 예외 핸들러 추가 [KOBG-4]
Aug 16, 2025
c826b0f
:sparkles: feat: 기본 시간 기록(create_at, update_at) 필드를 위한 BaseTime 엔티티 추…
Aug 16, 2025
fe67845
:wrench: chore(config): OpenAPI(Swagger) 설정 클래스 추가 [KOBG-4]
Aug 16, 2025
7a6226b
:art: style: naver convention에 맞춰 수정 [KOBG-4]
Aug 16, 2025
3ec71ba
:wrench: chore(build): Dockerfile 추가 [KOBG-4]
Aug 17, 2025
a09ca9d
:construction_worker: chore(ci): cicd를 위한 workflow 추가 [KOBG-4]
Aug 17, 2025
b304366
:construction_worker: chore(ci): cd 워크플로 실행 트리거 시점 변경 [KOBG-4]
Aug 17, 2025
539b94e
:construction_worker: chore(ci): cd 워크플로 실행 트리거 시점 추가 [KOBG-4]
Aug 17, 2025
18e9b14
:construction_worker: chore(ci): cd 워크플로 실행 트리거 시점 수정 [KOBG-4]
Aug 17, 2025
0081181
:recycle: refactor: 잘못된 kobridge 폴더 삭제[KOBG-4]
Aug 17, 2025
eedc4c5
:construction_worker: chore(ci): cd IMAGE_TAG 환경변수 설정 유지를 위한 띄어쓰기 제거 …
Aug 17, 2025
a22fc7f
:construction_worker: chore(ci): docker-compose.yml 경로와 통일 [KOBG-4]
Aug 17, 2025
63bf040
:construction_worker: chore(ci): container 버전을 위한 github.sha 부분 우선 제거…
Aug 17, 2025
828bb2f
:sparkles: feat: User Entity 위한 enum(lang, school, voice, role) 추가 [K…
Aug 18, 2025
0e1dbb8
:sparkles: feat: User Entity 추가 [KOBG-12]
Aug 18, 2025
4e9e6ff
:sparkles: feat: User Repository 추가 [KOBG-12]
Aug 18, 2025
b73848a
:package: chore(deps): google login, jwt 관련 라이브러리 추가 [KOBG-12]
Aug 18, 2025
60617d8
:sparkles: feat: redis 설정 클래스 추가 [KOBG-12]
Aug 18, 2025
612c440
:sparkles: feat: redis에 값 저장, 삭제 등 관련 util 추가 [KOBG-12]
Aug 18, 2025
b2c7f95
:sparkles: feat: jwt 토큰 생성, 유효성 검증 및 정보 추츨 등 관련 util 추가 [KOBG-12]
Aug 18, 2025
fd78ac4
:sparkles: feat: access/refresh token 담을 vo 추가 [KOBG-12]
Aug 18, 2025
9cd0ebb
:sparkles: feat: google id token 유효성 검증 관련 util 추가 [KOBG-12]
Aug 18, 2025
9f157e0
:sparkles: feat: 필터 예외 처리를 위한 응답 util 추가 [KOBG-12]
Aug 18, 2025
9c1f889
:sparkles: feat: 로그인 및 회원가입 관련 에러 코드 추가 [KOBG-12]
Aug 18, 2025
ddc74f6
:sparkles: feat: 필터 예외 처리를 위한 FillterException 추가 [KOBG-12]
Aug 18, 2025
c8d06e7
:sparkles: feat: 필터 예외 처리를 위해 예외 핸들러에 추가 [KOBG-12]
Aug 18, 2025
94ad68a
:sparkles: feat: 로그인 및 사용자 간단 정보 응답을 위한 dto 추가 [KOBG-12]
Aug 18, 2025
0c9794f
:sparkles: feat: 회원가입 요청을 위한 dto 추가 [KOBG-12]
Aug 18, 2025
1ddf939
:sparkles: feat: 회원가입 및 사용자 정보 조회 및 수정 로직 구현 [KOBG-12]
Aug 18, 2025
9596758
:sparkles: feat: 회원가입 및 사용자 정보 조회 및 수정 api 구현 [KOBG-12]
Aug 18, 2025
febddc8
:memo: docs: 회원가입 및 사용자 정보 조회 및 수정 api 문서 추가 [KOBG-12]
Aug 18, 2025
61f6dc6
:sparkles: feat: jwt 인증 로직을 위한 JwtAuthorizationFilter 추가 [KOBG-12]
Aug 18, 2025
e515dee
:sparkles: feat: JwtAuthorizationFilter 등록 설정 추가 [KOBG-12]
Aug 18, 2025
82fc27e
:construction_worker: chore(ci): cd 트리거 시점 수정 [KOBG-12]
Aug 18, 2025
ecd28ab
:bug: fix: auditing 기능 추가 [KOBG-12]
Aug 18, 2025
af36ed6
:wrench: chore(deps): ci pipeline rds 연결 불가로 인한 에러 수정 위해 h2 package 추…
Aug 18, 2025
5d8b2be
:construction_worker: chore(ci): test 용 application.yml 따로 properties…
Aug 18, 2025
03be3a0
:recycle: refactor: user 파일 위치 변경 [KOBG-13]
Aug 19, 2025
2f77055
:bug: fix: LangType 에 맞춰 정적메서드 반환값, 매팽값 수정 [KOBG-13]
Aug 19, 2025
3e380af
:recycle: refactor: user 파일 중복 삭제 [KOBG-13]
Aug 19, 2025
598594a
:recycle: refactor: voice enum 구성에 맞춰 example 수정 [KOBG-13]
Aug 21, 2025
71e7bc5
:recycle: refactor: LangType 반환 기본이 ENG type이도록 변경 [KOBG-13]
Aug 21, 2025
b6f363b
:sparkles: feat: Lesson, LessonSentence, LessonChat Entity 추가 [KOBG-13]
Aug 21, 2025
c69664d
:sparkles: feat: Lesson, LessonSentence, LessonChat Repository 추가 [KO…
Aug 21, 2025
b1477f1
:sparkles: feat: 레슨 상세 조회 응답을 위한 dto 추가 [KOBG-13]
Aug 21, 2025
8dc0ce9
:sparkles: feat: 레슨 리스트 조회 응답을 위한 dto 추가 [KOBG-13]
Aug 21, 2025
261054d
:sparkles: feat: 레슨 답변 교정 요청을 받기 위한 dto 추가 [KOBG-13]
Aug 21, 2025
c5eeaac
:sparkles: feat: 레슨 답변 교정 응답을 위한 dto 추가 [KOBG-13]
Aug 21, 2025
2d919e1
:sparkles: feat: 레슨 처리 관련 에러 코드 추가 [KOBG-13]
Aug 21, 2025
e402d0b
:sparkles: feat: web client 설정 클래스 추가 [KOBG-13]
Aug 21, 2025
b6a7a04
:sparkles: feat: 오디오 파일 base 64로 변환을 위한 파일 관련 util 추가 [KOBG-13]
Aug 21, 2025
00fcb62
:sparkles: feat: 발음 평가 요청을 받기 위한 dto 추가 [KOBG-13]
Aug 21, 2025
1acae79
:sparkles: feat: 외부 발음 평가 api 에 요청을 하기 위한 dto 추가 [KOBG-13]
Aug 21, 2025
79e4d0f
:sparkles: feat: 외부 발음 평가 api 응답을 매핑하기 위한 dto 추가 [KOBG-13]
Aug 21, 2025
f4fa2ef
:sparkles: feat: 외부 발음 평가 api 연결 시 나올 수 있는 에러 코드 추가 [KOBG-13]
Aug 21, 2025
dd21e33
:sparkles: feat: 외부 발음 평가 api 요청-응답 로직 추가 [KOBG-13]
Aug 21, 2025
8c4422f
:sparkles: feat: chat gpt api 응답을 매핑하기 위한 dto 추가 [KOBG-13]
Aug 21, 2025
c7e2bd9
:sparkles: feat: chat gpt api에 요청을 하기 위한 dto 추가 [KOBG-13]
Aug 21, 2025
15c9d7b
:sparkles: feat: chat gpt api 연결 시 나올 수 있는 에러 코드 추가 [KOBG-13]
Aug 21, 2025
428b3d0
:sparkles: feat: chat gpt api 답변 교정 요청-응답 로직 추가 [KOBG-13]
Aug 21, 2025
9d407a9
:sparkles: feat: 레슨 상세 조회, 전체 조회, 발음 평가, 답변 교정을 위한 로직 추가 [KOBG-13]
Aug 21, 2025
73e3994
:sparkles: feat: 레슨 상세 조회, 전체 조회, 발음 평가, 답변 교정 api 추가 [KOBG-13]
Aug 21, 2025
05e574c
:memo: docs: 레슨 상세 조회, 전체 조회, 발음 평가, 답변 교정 api 문서 추가 [KOBG-13]
Aug 21, 2025
2f5e68e
:sparkles: feat: 답변 체험을 위해 user 정보 없이도 이용하도록 jwt 검증 제외 path 추가 [KOBG-13]
Aug 21, 2025
9aaea56
Merge branch 'develop' into feature/KOBG-13/lesson-api
yerim123456 Aug 21, 2025
7344fc6
:bug: fix: 파일 이동 중 제대로 삭제되지 않은 user 파일 삭제 [KOBG-13]
Aug 21, 2025
a01ba9a
:construction_worker: chore(ci): data.sql init 파일 업로드를 위한 aws 연결 추가 […
Aug 21, 2025
b5bb363
:bug: chore(ci): 외부가 아닌 jar 파일 내부에 data.sql 위치하도록 수정 [KOBG-13]
Aug 21, 2025
233c07b
:bug: chore(ci): aws s3 관련 코드 삭제 [KOBG-13]
Aug 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// google api client
implementation 'com.google.api-client:google-api-client:2.4.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import com.edu.kobridge.global.error.exception.FilterException;
import com.edu.kobridge.global.util.JwtUtil;
import com.edu.kobridge.global.util.ResponseUtil;
import com.edu.kobridge.user.domain.entity.User;
import com.edu.kobridge.module.user.domain.entity.User;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
Expand All @@ -31,6 +31,7 @@ public class JwtAuthorizationFilter implements Filter {
// JWT 검사 제외할 경로 설정
final String LOGIN_PATH = "/api/user/google-login";
final String TOKEN_PATH = "/api/user/token";
final String LESSON_PATH = "/api/lesson/chat";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Expand All @@ -54,13 +55,16 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
return;
}

// 로그인 및 토큰 재발급 요청은 JWT 인증 필터링 없이 처리
// 로그인 및 토큰 재발급 요청, 레슨 시도는 JWT 인증 필터링 없이 처리
if (requestURI.equals(LOGIN_PATH) && req.getMethod().equals("GET")) {
chain.doFilter(request, response);
return;
} else if (requestURI.equals(TOKEN_PATH) && req.getMethod().equals("GET")) {
chain.doFilter(request, response);
return;
} else if (requestURI.startsWith(LESSON_PATH)) {
chain.doFilter(request, response);
return;
} else if (requestURI.contains("test/no-auth")) {
chain.doFilter(request, response);
return;
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/edu/kobridge/global/config/WebClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.edu.kobridge.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}

@Bean(name = "pronunciationWebClient")
public WebClient pronunciationWebClient(WebClient.Builder builder,
@Value("${api.e-pre-tx.url}") String baseUrl) {
return builder.baseUrl(baseUrl).build();
}

@Bean(name = "chatGptWebClient")
public WebClient chatGptWebClient(WebClient.Builder builder,
@Value("${api.chat-gpt.url}") String baseUrl) {
return builder.baseUrl(baseUrl).build();
}
}
8 changes: 4 additions & 4 deletions src/main/java/com/edu/kobridge/global/enums/LangType.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public enum LangType {
this.code = code;
this.name = name;
}

public static UserRoleType of(String code) {
return Arrays.stream(UserRoleType.values())
public static LangType of(String code) {
return Arrays.stream(LangType.values())
.filter(r -> r.getCode().equals(code))
.findAny()
.orElse(null);
.orElse(ENG);
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/edu/kobridge/global/util/FileUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.edu.kobridge.global.util;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Base64;

import org.springframework.stereotype.Component;

import com.edu.kobridge.global.error.GlobalErrorCode;
import com.edu.kobridge.global.error.exception.AppException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class FileUtil {
public String convertS3UrlToBase64(String s3Url) {
try (InputStream in = new URL(s3Url).openStream()) {
byte[] fileBytes = in.readAllBytes();
return Base64.getEncoder().encodeToString(fileBytes);
} catch (IOException e) {
log.error("[FileUtil] file convert error -- " + e.getMessage());
throw new AppException(GlobalErrorCode.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import org.springframework.stereotype.Component;

import com.edu.kobridge.global.error.exception.AppException;
import com.edu.kobridge.user.domain.entity.User;
import com.edu.kobridge.user.error.UserErrorCode;
import com.edu.kobridge.module.user.domain.entity.User;
import com.edu.kobridge.module.user.error.UserErrorCode;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/edu/kobridge/global/util/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import com.edu.kobridge.global.enums.JwtVo;
import com.edu.kobridge.global.error.GlobalErrorCode;
import com.edu.kobridge.global.error.exception.AppException;
import com.edu.kobridge.user.domain.entity.User;
import com.edu.kobridge.user.domain.repository.UserRepository;
import com.edu.kobridge.module.user.domain.entity.User;
import com.edu.kobridge.module.user.domain.repository.UserRepository;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.edu.kobridge.infra.api.chatgpt;

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

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.client.WebClient;

import com.edu.kobridge.global.enums.LangType;
import com.edu.kobridge.global.error.exception.AppException;
import com.edu.kobridge.infra.api.chatgpt.error.ChatGptErrorCode;
import com.edu.kobridge.infra.api.chatgpt.res.ChatGptCorrectionResDto;
import com.edu.kobridge.infra.api.chatgpt.res.ChatGptResDto;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ChatGptService {

@Value("${api.chat-gpt.key}")
private String key;

@Value("${api.chat-gpt.model}")
private String model;

private final WebClient chatGptWebClient;

public ChatGptCorrectionResDto postAnswerCorrectionAndResponse(String userSentence, Boolean isNextChatExist,
LangType lang) {
String userPrompt = String.format(
"User sentence: %s | NextChat: %s | Language: %s:",
userSentence, isNextChatExist, lang.getName()
);

Map<String, Object> request = Map.of(
"model", model,
"input", List.of(
Map.of("role", "system", "content",
"너는 한국어 학습자를 위한 교정 선생님이자 친근한 대화 파트너야. " +
"항상 아래 JSON 형식으로만 답해. 출력은 반드시 한국어로 해.\n\n" +
"{ \"correction\": \"교정된 한국어 문장\", " +
"\"reason\": \"제공하는 교정 이유 (선생님 톤)\", " +
"\"translation\": \"Language 로 번역된 correction\", " +
"\"response\": \"교정된 문장에 대한 답변 / isNextChatExist 이 false 이면 답변 후 대화 마무리\"}\n\n"
+
"규칙:\n" +
"- correction / reason → 선생님 톤\n" +
"- response → 친구처럼 친근한 톤\n" +
"- 반드시 유효한 JSON만 출력하고, JSON 외의 다른 텍스트는 출력하지 마."
),
Map.of("role", "user", "content", userPrompt)
),
"temperature", 0.7
);

ObjectMapper objectMapper = new ObjectMapper();
try {
ChatGptResDto chatGptRes = chatGptWebClient.post()
.header("Authorization", "Bearer " + key)
.bodyValue(request)
.retrieve()
.onStatus(HttpStatusCode::isError, resp ->
resp.bodyToMono(String.class)
.flatMap(body -> {
log.error("[ChatGPT API] connection error -- Status: {}, Body: {}",
resp.statusCode(), body);
return Mono.error(new AppException(ChatGptErrorCode.CHAT_GPT_CHAT_API_FAILED));
})
)
.bodyToMono(ChatGptResDto.class)
.block();

String jsonContent = chatGptRes.getOutput().get(0).getContent().get(0).getText();

return objectMapper.readValue(jsonContent, ChatGptCorrectionResDto.class);

} catch (Exception e) {
log.error("[ChatGPT Chat API] internal error -- " + e.getMessage(), e);
throw new AppException(ChatGptErrorCode.CHAT_GPT_CHAT_API_FAILED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.edu.kobridge.infra.api.chatgpt.error;

import org.springframework.http.HttpStatus;

import com.edu.kobridge.global.error.ErrorCode;

import lombok.Getter;

@Getter
public enum ChatGptErrorCode implements ErrorCode {
CHAT_GPT_CHAT_API_FAILED(HttpStatus.SERVICE_UNAVAILABLE, "chat gpt chat 답변 기능 사용 불가");

private final HttpStatus httpStatus;
private final String message;

ChatGptErrorCode(HttpStatus httpStatus, String message) {
this.httpStatus = httpStatus;
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.edu.kobridge.infra.api.chatgpt.req;

import java.util.List;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder(access = AccessLevel.PRIVATE)
public class ChatGptReqDto {
private final String model;
private final List<Message> messages;

@Getter
@Builder(access = AccessLevel.PRIVATE)
public static class Message {
private final String role;
private final String content;

public static Message of(String role, String content) {
return Message.builder()
.role(role)
.content(content)
.build();
}
}

public static ChatGptReqDto of(String model, List<Message> messages) {
return ChatGptReqDto.builder()
.model(model)
.messages(messages)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.edu.kobridge.infra.api.chatgpt.res;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatGptCorrectionResDto {
private String correction;
private String reason;
private String translation;
private String response;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.edu.kobridge.infra.api.chatgpt.res;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGptResDto {
private String id;
private String object;
private long created_at;
private String status;
private List<Output> output;
private Usage usage;

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Output {
private String id;
private String type;
private String status;
private List<Content> content;
private String role;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Content {
private String type;
private String text; // JSON 문자열 그대로 저장
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Usage {
private int input_tokens;
private int output_tokens;
private int total_tokens;
}
}
Loading