From 2aeb2a1323da20bd47ebdd8e9e68def00951c496 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:31:22 +0900 Subject: [PATCH 01/73] =?UTF-8?q?:wrench:=20chore(config):=20checkstyle=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89=20=EB=AC=B8=EC=9E=90=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=B4=20.gitattribut?= =?UTF-8?q?es=EC=97=90=20LF=20=EC=A0=81=EC=9A=A9=20=EB=B2=94=EC=9C=84=20?= =?UTF-8?q?=ED=99=95=EB=8C=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/.gitattributes | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/kobridge/.gitattributes b/kobridge/.gitattributes index 8af972c..0cf2296 100644 --- a/kobridge/.gitattributes +++ b/kobridge/.gitattributes @@ -1,3 +1,13 @@ -/gradlew text eol=lf +# --- 개행 문자 규칙 --- +# 모든 텍스트 파일은 LF로 통일 +# checkstyle 규칙 적용을 위한 설정 (window 개행 문자 문제) +* text=auto eol=lf + +# --- 예외 규칙 --- +# Windows 환경에서만 CRLF가 꼭 필요한 경우 (.bat 파일) *.bat text eol=crlf -*.jar binary + +# --- 파일 종류 --- +# 이미지/바이너리 파일은 변환 금지 +*.png binary +*.jar binary \ No newline at end of file From 397ba4c97b520dbc57aa23f10f0b0171649df8bd Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:33:43 +0900 Subject: [PATCH 02/73] =?UTF-8?q?:sparkles:=20feat:=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=ED=98=95=EC=8B=9D=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/edu/kobridge/global/error/ErrorCode.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/error/ErrorCode.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/ErrorCode.java b/kobridge/src/main/java/com/edu/kobridge/global/error/ErrorCode.java new file mode 100644 index 0000000..45342a8 --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/error/ErrorCode.java @@ -0,0 +1,11 @@ +package com.edu.kobridge.global.error; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + String name(); + + HttpStatus getHttpStatus(); + + String getMessage(); +} \ No newline at end of file From afa43f77a1857744a243e3fe2987e58074898498 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:34:00 +0900 Subject: [PATCH 03/73] =?UTF-8?q?:sparkles:=20feat:=20=EA=B8=80=EB=A1=9C?= =?UTF-8?q?=EB=B2=8C=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/GlobalErrorCode.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java b/kobridge/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java new file mode 100644 index 0000000..1cf72eb --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java @@ -0,0 +1,20 @@ +package com.edu.kobridge.global.error; + +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public enum GlobalErrorCode implements ErrorCode { + MISSING_HEADER(HttpStatus.BAD_REQUEST, "필수 요청 헤더가 누락되었습니다."), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "요청 경로가 지원되지 않습니다."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류가 발생했습니다."); + + private final HttpStatus httpStatus; + private final String message; + + GlobalErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } +} From b23ff247d19bc002f82407ee022aa9577e49ae88 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:34:43 +0900 Subject: [PATCH 04/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20AppException=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/exception/AppException.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/error/exception/AppException.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/exception/AppException.java b/kobridge/src/main/java/com/edu/kobridge/global/error/exception/AppException.java new file mode 100644 index 0000000..d5e0827 --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/error/exception/AppException.java @@ -0,0 +1,18 @@ +package com.edu.kobridge.global.error.exception; + +import com.edu.kobridge.global.error.ErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AppException extends RuntimeException { + private ErrorCode errorCode; + private String message; + + public AppException(ErrorCode errorCode) { + this.errorCode = errorCode; + this.message = errorCode.getMessage(); + } +} From 95461790db94c77da411d21af9694848e72fb6e9 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:38:13 +0900 Subject: [PATCH 05/73] =?UTF-8?q?:sparkles:=20feat:=20=EC=A0=95=ED=95=B4?= =?UTF-8?q?=EC=A7=84=20=EC=9D=91=EB=8B=B5=20=ED=98=95=EC=8B=9D=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/common/DataResponseDto.java | 28 +++++++++++++++++++ .../kobridge/global/common/ResponseDto.java | 27 ++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/common/DataResponseDto.java create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/common/ResponseDto.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/common/DataResponseDto.java b/kobridge/src/main/java/com/edu/kobridge/global/common/DataResponseDto.java new file mode 100644 index 0000000..44aca61 --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/common/DataResponseDto.java @@ -0,0 +1,28 @@ +package com.edu.kobridge.global.common; + +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public class DataResponseDto extends ResponseDto { + private final T data; + + private DataResponseDto(T data, Integer code) { + super(code, HttpStatus.valueOf(code).getReasonPhrase()); + this.data = data; + } + + private DataResponseDto(T data, Integer code, String message) { + super(code, message); + this.data = data; + } + + public static DataResponseDto of(T data, Integer code) { + return new DataResponseDto<>(data, code); + } + + public static DataResponseDto of(T data, Integer code, String message) { + return new DataResponseDto<>(data, code, message); + } +} \ No newline at end of file diff --git a/kobridge/src/main/java/com/edu/kobridge/global/common/ResponseDto.java b/kobridge/src/main/java/com/edu/kobridge/global/common/ResponseDto.java new file mode 100644 index 0000000..5f62ff1 --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/common/ResponseDto.java @@ -0,0 +1,27 @@ +package com.edu.kobridge.global.common; + +import org.springframework.http.HttpStatus; + +import com.edu.kobridge.global.error.ErrorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class ResponseDto { + private final Integer code; + private final String message; + + public static ResponseDto of(Integer code) { + return new ResponseDto(code, HttpStatus.valueOf(code).getReasonPhrase()); + } + + public static ResponseDto of(Integer code, String message) { + return new ResponseDto(code, message); + } + + public static ResponseDto of(ErrorCode e) { + return new ResponseDto(e.getHttpStatus().value(), e.getMessage()); + } +} From 7b4b68256472572220472285ad0a97f4c0229f4d Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:40:44 +0900 Subject: [PATCH 06/73] =?UTF-8?q?:sparkles:=20feat:=20=EA=B8=80=EB=A1=9C?= =?UTF-8?q?=EB=B2=8C=20=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/handler/GlobalExceptionHandler.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java b/kobridge/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..86c9372 --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java @@ -0,0 +1,85 @@ +package com.edu.kobridge.global.error.handler; + +import java.util.Objects; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.edu.kobridge.global.common.ResponseDto; +import com.edu.kobridge.global.error.ErrorCode; +import com.edu.kobridge.global.error.GlobalErrorCode; +import com.edu.kobridge.global.error.exception.AppException; + +import lombok.extern.slf4j.Slf4j; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + // AppException + @ExceptionHandler(AppException.class) + public ResponseEntity exceptionHandler(AppException e) { + int code = e.getErrorCode().getHttpStatus().value(); + String message = e.getMessage(); + + // 내부 서버 에러일 경우, stack trace 출력 + if (code == 500) + e.printStackTrace(); + + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } + + // HttpMessageNotReadableException + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + int code = HttpStatus.BAD_REQUEST.value(); + String message = Objects.requireNonNull(e.getRootCause()).getMessage(); + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } + + // MethodArgumentNotValidException + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity exceptionHandler(MethodArgumentNotValidException e) { + int code = HttpStatus.BAD_REQUEST.value(); + String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } + + // MethodNotAllowed + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity exceptionHandler(HttpRequestMethodNotSupportedException e) { + ErrorCode errorCode = GlobalErrorCode.METHOD_NOT_ALLOWED; + int code = errorCode.getHttpStatus().value(); + String message = errorCode.getMessage(); + + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } + + // RequiredHeaderIsNotExist + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity handleMissingRequestHeaderException(MissingRequestHeaderException e) { + ErrorCode errorCode = GlobalErrorCode.MISSING_HEADER; + int code = errorCode.getHttpStatus().value(); + String message = errorCode.getMessage(); + + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } + + // InternalServerError + @ExceptionHandler(Exception.class) + public ResponseEntity exceptionHandler(Exception e) { + ErrorCode errorCode = GlobalErrorCode.INTERNAL_SERVER_ERROR; + int code = errorCode.getHttpStatus().value(); + String message = errorCode.getMessage(); + + e.printStackTrace(); + + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } +} From c826b0faf27a3bbab14e437210c21774e3f2c52b Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:40:55 +0900 Subject: [PATCH 07/73] =?UTF-8?q?:sparkles:=20feat:=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=20=EA=B8=B0=EB=A1=9D(create=5Fat,=20updat?= =?UTF-8?q?e=5Fat)=20=ED=95=84=EB=93=9C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20Ba?= =?UTF-8?q?seTime=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80=20[KO?= =?UTF-8?q?BG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/global/common/BaseTime.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/common/BaseTime.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/common/BaseTime.java b/kobridge/src/main/java/com/edu/kobridge/global/common/BaseTime.java new file mode 100644 index 0000000..b3c0cc7 --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/common/BaseTime.java @@ -0,0 +1,26 @@ +package com.edu.kobridge.global.common; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseTime { + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} \ No newline at end of file From fe67845d9acc44d161f842e7ddfe9c80f38f8494 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:42:29 +0900 Subject: [PATCH 08/73] =?UTF-8?q?:wrench:=20chore(config):=20OpenAPI(Swagg?= =?UTF-8?q?er)=20=EC=84=A4=EC=A0=95=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/config/OpenApiConfig.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 kobridge/src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java b/kobridge/src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java new file mode 100644 index 0000000..37a6aa1 --- /dev/null +++ b/kobridge/src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java @@ -0,0 +1,49 @@ +package com.edu.kobridge.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; + +@Configuration +public class OpenApiConfig { + + private static final String BEARER_TOKEN_PREFIX = "bearer"; + private static final String securityJwtName = "JWT"; + + @Bean + public OpenAPI customOpenAPI() { + SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityJwtName); + + return new OpenAPI() + .addServersItem(new Server().url("/").description("현재 서버")) + .components(apiComponents()) + .addSecurityItem(securityRequirement) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("Kobridge API 명세서") + .description("다문화 가정 아이들을 위한 한국어 학습 보조 서비스 | Spring Boot를 이용한 REST API") + .version("0.0.1") + .contact(new io.swagger.v3.oas.models.info.Contact() + .name("이예림") + .url("https://github.com/yerim123456") + .email("yearim1226@naver.com")); + } + + private Components apiComponents() { + return new Components() + .addSecuritySchemes(securityJwtName, new SecurityScheme() + .name(securityJwtName) + .type(SecurityScheme.Type.HTTP) + .scheme(BEARER_TOKEN_PREFIX) + .bearerFormat(securityJwtName)); + } +} \ No newline at end of file From 7a6226bad560e72ad2f2bf91b948303cf2e0f23c Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Sat, 16 Aug 2025 20:48:10 +0900 Subject: [PATCH 09/73] =?UTF-8?q?:art:=20style:=20naver=20convention?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EC=88=98=EC=A0=95=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/.github/pull_request_template.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kobridge/.github/pull_request_template.md b/kobridge/.github/pull_request_template.md index 677b353..f7faf04 100644 --- a/kobridge/.github/pull_request_template.md +++ b/kobridge/.github/pull_request_template.md @@ -1,9 +1,11 @@ ## 🔑 JIRA 이슈 키 + --- ## ✒ 진행한 작업 + - [x] 진행한 작업 - [x] 진행한 작업 - [x] 진행한 작업 @@ -11,19 +13,23 @@ --- ## 💡 생겼던 문제 및 해결법 + 1️⃣ 요약 + - 문제: - 해결: --- ## 📢 아쉬운 부분 및 개선점 + - ... --- ## 📚 개발에 참고한 자료 및 포인트 + - 참고한 부분 요약 및 포인트 - - link + - link --- From 3ec71baac960e02b8d7f1a85efb3dd19657274c4 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 05:20:46 +0900 Subject: [PATCH 10/73] =?UTF-8?q?:wrench:=20chore(build):=20Dockerfile=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 kobridge/Dockerfile diff --git a/kobridge/Dockerfile b/kobridge/Dockerfile new file mode 100644 index 0000000..9fadf15 --- /dev/null +++ b/kobridge/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-jdk +LABEL authors="yerim" +ARG JAR_FILE=build/libs/*SNAPSHOT.jar +COPY ${JAR_FILE} /app.jar +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file From a09ca9d6a3172474a2f3dd4765e0022d6594c29a Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 05:22:17 +0900 Subject: [PATCH 11/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20ci?= =?UTF-8?q?cd=EB=A5=BC=20=EC=9C=84=ED=95=9C=20workflow=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/.github/workflows/cd-pipeline.yml | 60 +++++++++++++++++++++ kobridge/.github/workflows/ci-pipeline.yml | 61 ++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 kobridge/.github/workflows/cd-pipeline.yml create mode 100644 kobridge/.github/workflows/ci-pipeline.yml diff --git a/kobridge/.github/workflows/cd-pipeline.yml b/kobridge/.github/workflows/cd-pipeline.yml new file mode 100644 index 0000000..e5abeb3 --- /dev/null +++ b/kobridge/.github/workflows/cd-pipeline.yml @@ -0,0 +1,60 @@ +name: Java CD with Gradle + +on: + push: + branches: + - main + - feature/KOBG-4/initial-setting + + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: 🔄 체크아웃 코드 + uses: actions/checkout@v4 + + - name: ☕ JDK 17 설치 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'corretto' + + - name: 📝 application.yml 생성 + run: | + cd ./src/main/resources + touch ./application.yml + echo "${{ secrets.PROPERTIES }}" > ./application.yml + shell: bash + + - name: 🔧 빌드 권한 부여 + run: chmod +x gradlew + + - name: 🏗️ 프로젝트 jar 파일 제작 + run: | + chmod +x gradlew + ./gradlew bootJar + + - name: 🐳 Docker 로그인 + run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + + - name: 🏗️ Docker 이미지 빌드 및 푸시 + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} . + docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} + + - name: 🚀 EC2에 프로젝트 전송 및 배포 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.KEY }} + script: | + export IMAGE_TAG=${{ github.sha }} + sudo docker compose up -d --pull always + sudo docker ps + sudo docker image prune -f diff --git a/kobridge/.github/workflows/ci-pipeline.yml b/kobridge/.github/workflows/ci-pipeline.yml new file mode 100644 index 0000000..17405f8 --- /dev/null +++ b/kobridge/.github/workflows/ci-pipeline.yml @@ -0,0 +1,61 @@ +name: Java CI with Gradle + +on: + pull_request: + branches: + - main + - develop + +permissions: + contents: read + checks: write + pull-requests: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: 🔄 체크아웃 + uses: actions/checkout@v4 + + - name: ☕ JDK 17 설치 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'corretto' + + - name: 📝 application.yml 생성 + run: | + cd ./src/main/resources + touch ./application.yml + echo "${{ secrets.PROPERTIES }}" > ./application.yml + shell: bash + + - name: 🚀 Redis 실행 + uses: supercharge/redis-github-action@1.1.0 + with: + redis-version: latest + + - name: 🔧 빌드 권한 부여 + run: chmod +x gradlew + + - name: 🛠️ 테스트 프로파일 설정 + run: echo "SPRING_PROFILES_ACTIVE=test" >> $GITHUB_ENV + + - name: 🏗️ 프로젝트 빌드 + run: ./gradlew clean build + + - name: 📊 자코코 리포트 작성 + uses: madrapps/jacoco-report@v1.6.1 + with: + title: 📊 Jacoco Test Coverage + paths: ${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 80 + min-coverage-changed-files: 0 + + - name: ✅ 테스트 리포트 작성 + uses: mikepenz/action-junit-report@v4 + with: + report_paths: '**/build/test-results/test/TEST-*.xml' From b30436666e9a8440a94cdae89b90359c8da1d001 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 05:52:02 +0900 Subject: [PATCH 12/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20cd?= =?UTF-8?q?=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=8B=9C=EC=A0=90=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/.github/workflows/cd-pipeline.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kobridge/.github/workflows/cd-pipeline.yml b/kobridge/.github/workflows/cd-pipeline.yml index e5abeb3..382ddb2 100644 --- a/kobridge/.github/workflows/cd-pipeline.yml +++ b/kobridge/.github/workflows/cd-pipeline.yml @@ -4,8 +4,9 @@ on: push: branches: - main - - feature/KOBG-4/initial-setting - + pull_request: + branches: + - develop permissions: contents: read From 539b94ea3628cfe2a4e1045e84e521c1fb131498 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 05:52:48 +0900 Subject: [PATCH 13/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20cd?= =?UTF-8?q?=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=8B=9C=EC=A0=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/.github/workflows/cd-pipeline.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/kobridge/.github/workflows/cd-pipeline.yml b/kobridge/.github/workflows/cd-pipeline.yml index 382ddb2..7a7e4d1 100644 --- a/kobridge/.github/workflows/cd-pipeline.yml +++ b/kobridge/.github/workflows/cd-pipeline.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - feature/KOBG-4/initial-setting pull_request: branches: - develop From 18e9b14c407ddad1d0fab6dbfebda7e03d2f5b0a Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 05:58:35 +0900 Subject: [PATCH 14/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20cd?= =?UTF-8?q?=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=8B=9C=EC=A0=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/.github/workflows/cd-pipeline.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kobridge/.github/workflows/cd-pipeline.yml b/kobridge/.github/workflows/cd-pipeline.yml index 7a7e4d1..b92b50e 100644 --- a/kobridge/.github/workflows/cd-pipeline.yml +++ b/kobridge/.github/workflows/cd-pipeline.yml @@ -4,10 +4,9 @@ on: push: branches: - main - - feature/KOBG-4/initial-setting pull_request: branches: - - develop + - feature/KOBG-4/initial-setting permissions: contents: read From 00811810e434da3b7b6de838f1bd85898536ab96 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 06:19:21 +0900 Subject: [PATCH 15/73] =?UTF-8?q?:recycle:=20refactor:=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20kobridge=20=ED=8F=B4=EB=8D=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobridge/.gitattributes => .gitattributes | 0 .../.github => .github}/pull_request_template.md | 0 .../.github => .github}/workflows/cd-pipeline.yml | 2 -- .../.github => .github}/workflows/ci-pipeline.yml | 0 kobridge/.gitignore => .gitignore | 0 kobridge/Dockerfile => Dockerfile | 0 kobridge/build.gradle => build.gradle | 0 .../naver-checkstyle-rules.xml | 0 .../naver-checkstyle-suppressions.xml | 0 .../gradle => gradle}/wrapper/gradle-wrapper.jar | Bin .../wrapper/gradle-wrapper.properties | 0 kobridge/gradlew => gradlew | 0 kobridge/gradlew.bat => gradlew.bat | 0 kobridge/settings.gradle => settings.gradle | 0 .../java/com/edu/kobridge/KobridgeApplication.java | 0 .../com/edu/kobridge/global/common/BaseTime.java | 0 .../edu/kobridge/global/common/DataResponseDto.java | 0 .../com/edu/kobridge/global/common/ResponseDto.java | 0 .../edu/kobridge/global/config/OpenApiConfig.java | 0 .../com/edu/kobridge/global/error/ErrorCode.java | 0 .../edu/kobridge/global/error/GlobalErrorCode.java | 0 .../global/error/exception/AppException.java | 0 .../error/handler/GlobalExceptionHandler.java | 0 .../src => src}/main/resources/application.yml | 0 .../com/edu/kobridge/KobridgeApplicationTests.java | 0 25 files changed, 2 deletions(-) rename kobridge/.gitattributes => .gitattributes (100%) rename {kobridge/.github => .github}/pull_request_template.md (100%) rename {kobridge/.github => .github}/workflows/cd-pipeline.yml (98%) rename {kobridge/.github => .github}/workflows/ci-pipeline.yml (100%) rename kobridge/.gitignore => .gitignore (100%) rename kobridge/Dockerfile => Dockerfile (100%) rename kobridge/build.gradle => build.gradle (100%) rename {kobridge/checkstyle => checkstyle}/naver-checkstyle-rules.xml (100%) rename {kobridge/checkstyle => checkstyle}/naver-checkstyle-suppressions.xml (100%) rename {kobridge/gradle => gradle}/wrapper/gradle-wrapper.jar (100%) rename {kobridge/gradle => gradle}/wrapper/gradle-wrapper.properties (100%) rename kobridge/gradlew => gradlew (100%) rename kobridge/gradlew.bat => gradlew.bat (100%) rename kobridge/settings.gradle => settings.gradle (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/KobridgeApplication.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/common/BaseTime.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/common/DataResponseDto.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/common/ResponseDto.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/config/OpenApiConfig.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/error/ErrorCode.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/error/exception/AppException.java (100%) rename {kobridge/src => src}/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java (100%) rename {kobridge/src => src}/main/resources/application.yml (100%) rename {kobridge/src => src}/test/java/com/edu/kobridge/KobridgeApplicationTests.java (100%) diff --git a/kobridge/.gitattributes b/.gitattributes similarity index 100% rename from kobridge/.gitattributes rename to .gitattributes diff --git a/kobridge/.github/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from kobridge/.github/pull_request_template.md rename to .github/pull_request_template.md diff --git a/kobridge/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml similarity index 98% rename from kobridge/.github/workflows/cd-pipeline.yml rename to .github/workflows/cd-pipeline.yml index b92b50e..a2a2f25 100644 --- a/kobridge/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -4,8 +4,6 @@ on: push: branches: - main - pull_request: - branches: - feature/KOBG-4/initial-setting permissions: diff --git a/kobridge/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml similarity index 100% rename from kobridge/.github/workflows/ci-pipeline.yml rename to .github/workflows/ci-pipeline.yml diff --git a/kobridge/.gitignore b/.gitignore similarity index 100% rename from kobridge/.gitignore rename to .gitignore diff --git a/kobridge/Dockerfile b/Dockerfile similarity index 100% rename from kobridge/Dockerfile rename to Dockerfile diff --git a/kobridge/build.gradle b/build.gradle similarity index 100% rename from kobridge/build.gradle rename to build.gradle diff --git a/kobridge/checkstyle/naver-checkstyle-rules.xml b/checkstyle/naver-checkstyle-rules.xml similarity index 100% rename from kobridge/checkstyle/naver-checkstyle-rules.xml rename to checkstyle/naver-checkstyle-rules.xml diff --git a/kobridge/checkstyle/naver-checkstyle-suppressions.xml b/checkstyle/naver-checkstyle-suppressions.xml similarity index 100% rename from kobridge/checkstyle/naver-checkstyle-suppressions.xml rename to checkstyle/naver-checkstyle-suppressions.xml diff --git a/kobridge/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from kobridge/gradle/wrapper/gradle-wrapper.jar rename to gradle/wrapper/gradle-wrapper.jar diff --git a/kobridge/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from kobridge/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties diff --git a/kobridge/gradlew b/gradlew similarity index 100% rename from kobridge/gradlew rename to gradlew diff --git a/kobridge/gradlew.bat b/gradlew.bat similarity index 100% rename from kobridge/gradlew.bat rename to gradlew.bat diff --git a/kobridge/settings.gradle b/settings.gradle similarity index 100% rename from kobridge/settings.gradle rename to settings.gradle diff --git a/kobridge/src/main/java/com/edu/kobridge/KobridgeApplication.java b/src/main/java/com/edu/kobridge/KobridgeApplication.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/KobridgeApplication.java rename to src/main/java/com/edu/kobridge/KobridgeApplication.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/common/BaseTime.java b/src/main/java/com/edu/kobridge/global/common/BaseTime.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/common/BaseTime.java rename to src/main/java/com/edu/kobridge/global/common/BaseTime.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/common/DataResponseDto.java b/src/main/java/com/edu/kobridge/global/common/DataResponseDto.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/common/DataResponseDto.java rename to src/main/java/com/edu/kobridge/global/common/DataResponseDto.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/common/ResponseDto.java b/src/main/java/com/edu/kobridge/global/common/ResponseDto.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/common/ResponseDto.java rename to src/main/java/com/edu/kobridge/global/common/ResponseDto.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java b/src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java rename to src/main/java/com/edu/kobridge/global/config/OpenApiConfig.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/ErrorCode.java b/src/main/java/com/edu/kobridge/global/error/ErrorCode.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/error/ErrorCode.java rename to src/main/java/com/edu/kobridge/global/error/ErrorCode.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java b/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java rename to src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/exception/AppException.java b/src/main/java/com/edu/kobridge/global/error/exception/AppException.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/error/exception/AppException.java rename to src/main/java/com/edu/kobridge/global/error/exception/AppException.java diff --git a/kobridge/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java similarity index 100% rename from kobridge/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java rename to src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java diff --git a/kobridge/src/main/resources/application.yml b/src/main/resources/application.yml similarity index 100% rename from kobridge/src/main/resources/application.yml rename to src/main/resources/application.yml diff --git a/kobridge/src/test/java/com/edu/kobridge/KobridgeApplicationTests.java b/src/test/java/com/edu/kobridge/KobridgeApplicationTests.java similarity index 100% rename from kobridge/src/test/java/com/edu/kobridge/KobridgeApplicationTests.java rename to src/test/java/com/edu/kobridge/KobridgeApplicationTests.java From eedc4c5b54e72a411c730db484d8a5ffdcc6d3bf Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 06:53:59 +0900 Subject: [PATCH 16/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20cd?= =?UTF-8?q?=20IMAGE=5FTAG=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=9C=A0=EC=A7=80=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=9D=84=EC=96=B4=EC=93=B0=EA=B8=B0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index a2a2f25..f98bf98 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -53,7 +53,6 @@ jobs: username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | - export IMAGE_TAG=${{ github.sha }} - sudo docker compose up -d --pull always + export IMAGE_TAG=${{ github.sha }} sudo docker compose up -d --pull always sudo docker ps sudo docker image prune -f From a22fc7f9c87b921d77910fdd54553b92d155551f Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 06:59:09 +0900 Subject: [PATCH 17/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20do?= =?UTF-8?q?cker-compose.yml=20=EA=B2=BD=EB=A1=9C=EC=99=80=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index f98bf98..2378d74 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -53,6 +53,7 @@ jobs: username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | + cd /home/ubuntu/app export IMAGE_TAG=${{ github.sha }} sudo docker compose up -d --pull always sudo docker ps sudo docker image prune -f From 63bf0401731b3f566eaa1a1c2830a0a9e135bcee Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 07:07:11 +0900 Subject: [PATCH 18/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20co?= =?UTF-8?q?ntainer=20=EB=B2=84=EC=A0=84=EC=9D=84=20=EC=9C=84=ED=95=9C=20gi?= =?UTF-8?q?thub.sha=20=EB=B6=80=EB=B6=84=20=EC=9A=B0=EC=84=A0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20[KOBG-4]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 2378d74..22b75c2 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -53,7 +53,6 @@ jobs: username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | - cd /home/ubuntu/app - export IMAGE_TAG=${{ github.sha }} sudo docker compose up -d --pull always + sudo docker compose up -d --pull always sudo docker ps sudo docker image prune -f From 828bb2f83e66669cda9fd4e8f31fe1af62ff7385 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:29:13 +0900 Subject: [PATCH 19/73] =?UTF-8?q?:sparkles:=20feat:=20User=20Entity=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20enum(lang,=20school,=20voice,=20role)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/global/enums/LangType.java | 29 +++++++++++++++++++ .../edu/kobridge/global/enums/SchoolType.java | 29 +++++++++++++++++++ .../kobridge/global/enums/UserRoleType.java | 27 +++++++++++++++++ .../edu/kobridge/global/enums/VoiceType.java | 27 +++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/enums/LangType.java create mode 100644 src/main/java/com/edu/kobridge/global/enums/SchoolType.java create mode 100644 src/main/java/com/edu/kobridge/global/enums/UserRoleType.java create mode 100644 src/main/java/com/edu/kobridge/global/enums/VoiceType.java diff --git a/src/main/java/com/edu/kobridge/global/enums/LangType.java b/src/main/java/com/edu/kobridge/global/enums/LangType.java new file mode 100644 index 0000000..8d34f10 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/LangType.java @@ -0,0 +1,29 @@ +package com.edu.kobridge.global.enums; + +import java.util.Arrays; + +import lombok.Getter; + +@Getter +public enum LangType { + ENG("ENG", "영어"), + VET("VET", "베트남어"), + CHN("CHN", "중국어"), + JPN("JPN", "일본어"), + NONE("NONE", "none"); + + private final String code; + private final String name; + + LangType(String code, String name) { + this.code = code; + this.name = name; + } + + public static UserRoleType of(String code) { + return Arrays.stream(UserRoleType.values()) + .filter(r -> r.getCode().equals(code)) + .findAny() + .orElse(null); + } +} diff --git a/src/main/java/com/edu/kobridge/global/enums/SchoolType.java b/src/main/java/com/edu/kobridge/global/enums/SchoolType.java new file mode 100644 index 0000000..4bb703f --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/SchoolType.java @@ -0,0 +1,29 @@ +package com.edu.kobridge.global.enums; + +import java.util.Arrays; + +import lombok.Getter; + +@Getter +public enum SchoolType { + ELEMENTARY("elementary", "초등학교", 6), + MIDDLE("middle-school", "중학교", 3), + HIGH("high-school", "고등학교", 3); + + private final String code; + private final String name; + private final int maxGrade; + + SchoolType(String code, String name, int maxGrade) { + this.code = code; + this.name = name; + this.maxGrade = maxGrade; + } + + public static SchoolType of(String code) { + return Arrays.stream(SchoolType.values()) + .filter(r -> r.getCode().equals(code)) + .findAny() + .orElse(null); + } +} diff --git a/src/main/java/com/edu/kobridge/global/enums/UserRoleType.java b/src/main/java/com/edu/kobridge/global/enums/UserRoleType.java new file mode 100644 index 0000000..3557475 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/UserRoleType.java @@ -0,0 +1,27 @@ +package com.edu.kobridge.global.enums; + +import java.util.Arrays; + +import lombok.Getter; + +@Getter +public enum UserRoleType { + USER("ROLE_USER", "일반 사용자 권한"), + ADMIN("ROLE_ADMIN", "관리자 권한"), + GUEST("GUEST", "게스트 권한"); + + private final String code; + private final String name; + + UserRoleType(String code, String name) { + this.code = code; + this.name = name; + } + + public static UserRoleType of(String code) { + return Arrays.stream(UserRoleType.values()) + .filter(r -> r.getCode().equals(code)) + .findAny() + .orElse(GUEST); + } +} diff --git a/src/main/java/com/edu/kobridge/global/enums/VoiceType.java b/src/main/java/com/edu/kobridge/global/enums/VoiceType.java new file mode 100644 index 0000000..fed6ac7 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/VoiceType.java @@ -0,0 +1,27 @@ +package com.edu.kobridge.global.enums; + +import java.util.Arrays; + +import lombok.Getter; + +@Getter +public enum VoiceType { + ONE("GIRL_ONE"), + TWO("GIRL_TWO"), + THREE("BOY_ONE"), + FOUR("BOY_TWO"); + + // 추후 voice 확정 되면 name 필드 추가 + private final String code; + + VoiceType(String code) { + this.code = code; + } + + public static VoiceType of(String code) { + return Arrays.stream(VoiceType.values()) + .filter(r -> r.getCode().equals(code)) + .findAny() + .orElse(null); + } +} From 0e1dbb81a0fd4dc9b8aea1333df9d558ba1cc9a8 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:30:28 +0900 Subject: [PATCH 20/73] =?UTF-8?q?:sparkles:=20feat:=20User=20Entity=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/user/domain/entity/User.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/domain/entity/User.java diff --git a/src/main/java/com/edu/kobridge/user/domain/entity/User.java b/src/main/java/com/edu/kobridge/user/domain/entity/User.java new file mode 100644 index 0000000..8f594ba --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/domain/entity/User.java @@ -0,0 +1,82 @@ +package com.edu.kobridge.user.domain.entity; + +import com.edu.kobridge.global.common.BaseTime; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.SchoolType; +import com.edu.kobridge.global.enums.UserRoleType; +import com.edu.kobridge.global.enums.VoiceType; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class User extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Column(unique = true) + private String email; + + private String name; + + @Enumerated(EnumType.STRING) + private LangType lang; + + @Enumerated(EnumType.STRING) + private SchoolType school; + + private byte grade; + + @Enumerated(EnumType.STRING) + private VoiceType voice; + + private int level; + + @NotNull + @Enumerated(EnumType.STRING) + private UserRoleType role; + + public static User of(@NotNull String email) { + return User + .builder() + .email(email) + .role(UserRoleType.USER) + .build(); + } + + public void updateInfo(@NotNull String name, @NotNull LangType lang, @NotNull SchoolType school, + @NotNull byte grade, @NotNull VoiceType voice) { + this.name = name; + this.lang = lang; + this.school = school; + this.grade = grade; + this.voice = voice; + this.level = 1; + } + + public void updateLang(@NotNull LangType lang) { + this.lang = lang; + } + + public void updateLevel(@NotNull int level) { + this.level = level; + } +} From 4e9e6ff6db27d4ce4ce3f8da612f65fc71816a1d Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:31:28 +0900 Subject: [PATCH 21/73] =?UTF-8?q?:sparkles:=20feat:=20User=20Repository=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/domain/repository/UserRepository.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java diff --git a/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java b/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java new file mode 100644 index 0000000..c03bda2 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.edu.kobridge.user.domain.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.edu.kobridge.user.domain.entity.User; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} From b73848ae8d3c3d19452960c5f7c2597f4ce91cc5 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 22:33:06 +0900 Subject: [PATCH 22/73] =?UTF-8?q?:package:=20chore(deps):=20google=20login?= =?UTF-8?q?,=20jwt=20=EA=B4=80=EB=A0=A8=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.gradle b/build.gradle index 7d835eb..0ac4ea0 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // google api client + implementation 'com.google.api-client:google-api-client:2.4.0' + implementation 'com.google.http-client:google-http-client-jackson2:1.41.5' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.2' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' From 60617d857af0fcafbdddeadb45f590a3f21dc1a3 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:25:31 +0900 Subject: [PATCH 23/73] =?UTF-8?q?:sparkles:=20feat:=20redis=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/config/RedisConfig.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/config/RedisConfig.java diff --git a/src/main/java/com/edu/kobridge/global/config/RedisConfig.java b/src/main/java/com/edu/kobridge/global/config/RedisConfig.java new file mode 100644 index 0000000..a700134 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/config/RedisConfig.java @@ -0,0 +1,43 @@ +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.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import lombok.Getter; + +@Configuration +@Getter +@EnableRedisRepositories +public class RedisConfig { + @Value("${spring.data.redis.host}") + private String host; + @Value("${spring.data.redis.port}") + private int port; + + // 레디스 연결 + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(); + redisConfiguration.setHostName(host); + redisConfiguration.setPort(port); + + return new LettuceConnectionFactory(redisConfiguration); + } + + // 문자열 키 직렬화 + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} From 612c440d6f32e4c4fc1f3bef141a892f09eb6f35 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:25:52 +0900 Subject: [PATCH 24/73] =?UTF-8?q?:sparkles:=20feat:=20redis=EC=97=90=20?= =?UTF-8?q?=EA=B0=92=20=EC=A0=80=EC=9E=A5,=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=93=B1=20=EA=B4=80=EB=A0=A8=20util=20=EC=B6=94=EA=B0=80=20[K?= =?UTF-8?q?OBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/global/util/RedisUtil.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/RedisUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/RedisUtil.java b/src/main/java/com/edu/kobridge/global/util/RedisUtil.java new file mode 100644 index 0000000..6228dcf --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/RedisUtil.java @@ -0,0 +1,38 @@ +package com.edu.kobridge.global.util; + +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RedisUtil { + private final RedisTemplate redisTemplate; + + // 레디스에 유효 시간에 맞춰 값 저장 + public void setOpsForValue(String key, String value, int expire_h) { + ValueOperations stringValueOperations = redisTemplate.opsForValue(); + stringValueOperations.set(key, value); + redisTemplate.expire(key, expire_h, TimeUnit.HOURS); + + log.info("[Redis] Data saved successfully. -- " + key); + } + + // 레디스에 저장된 값 가져오기 + public String getOpsForValue(String key) { + return (String)redisTemplate.opsForValue().get(key); + } + + // 레디스에 저장된 값 삭제 + public void delete(String key) { + redisTemplate.delete(key); + + log.info("[Redis] Data deleted successfully. -- " + key); + } +} From b2c7f955c8da5d005fabde904e1481a4dec825bf Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:29:11 +0900 Subject: [PATCH 25/73] =?UTF-8?q?:sparkles:=20feat:=20jwt=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=83=9D=EC=84=B1,=20=EC=9C=A0=ED=9A=A8=EC=84=B1?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EC=B8=A8=20=EB=93=B1=20=EA=B4=80=EB=A0=A8=20util=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/edu/kobridge/global/util/JwtUtil.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/JwtUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/JwtUtil.java b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java new file mode 100644 index 0000000..7d94507 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java @@ -0,0 +1,125 @@ +package com.edu.kobridge.global.util; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +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 io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtUtil { + @Value("${jwt.issuer}") + private String ISSUER; + + @Value("${jwt.secret}") + private String JWT_SECRET_KEY; + + @Value("${jwt.access-token-expiration}") + private int ACCESS_TOKEN_EXPIRATION; + + @Getter + @Value("${jwt.refresh-token-expiration}") + private int REFRESH_TOKEN_EXPIRATION; + + private final String PAYLOAD_KEY_ID = "id"; + + private final UserRepository userRepository; + private final RedisUtil redisUtil; + + public JwtVo generateTokens(User user) { + final String PAYLOAD_KEY_EMAIL = "email"; + + // payload에 사용자 식별 값 추가 + Map payloads = new LinkedHashMap<>(); + payloads.put(PAYLOAD_KEY_ID, user.getId()); + payloads.put(PAYLOAD_KEY_EMAIL, user.getEmail()); + + // Token 유효기간 설정 + Date now = new Date(); + Date accessExp = new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION); + Date refreshExp = new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION); + + // 토큰 생성 + return new JwtVo( + createToken(payloads, accessExp, "access"), + createToken(payloads, refreshExp, "refresh") + ); + } + + // JWT 생성 메서드 + private String createToken(Map payloads, Date expiration, String subject) { + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(payloads) + .setIssuer(ISSUER) + .setIssuedAt(new Date()) + .setExpiration(expiration) + .setSubject(subject) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY.getBytes()) + .compact(); + } + + // 토큰을 검증하고 유효한 사용자 정보를 반환 + @Transactional(readOnly = true) + public User validateToken(boolean isAccessToken, String header) throws AppException { + // Jwt 토큰 및 claims 추출 + String token = extractToken(header); + Claims claims = parseToken(token); + + // 사용자 정보 추출 및 매핑 + Long userId = claims.get(PAYLOAD_KEY_ID, Long.class); + User user = userRepository.findById(userId) + .orElseThrow(() -> new AppException(GlobalErrorCode.USER_NOT_FOUND)); + + if (!isAccessToken) { + // refreshToken 있는지 확인 + String storedRefreshToken = redisUtil.getOpsForValue(user.getId() + "_refresh"); + if (storedRefreshToken == null || !storedRefreshToken.equals(token)) { + throw new AppException(GlobalErrorCode.AUTHORIZATION_FAILED); + } + } + + return user; + } + + // Authorization 헤더에서 Bearer 토큰 추출 + private String extractToken(String header) { + if (header == null || !header.startsWith("Bearer ")) { + throw new AppException(GlobalErrorCode.INVALID_TOKEN); + } + return header.substring(7); + } + + // JWT 토큰에서 클레임(Claims) 추출 + private Claims parseToken(String token) { + try { + return Jwts.parser() + .setSigningKey(JWT_SECRET_KEY.getBytes()) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (JwtException e) { + throw new AppException(GlobalErrorCode.INVALID_TOKEN); + } + } + +} From fd78ac4f3b7066f65017cb0e77fd88931753c572 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:30:05 +0900 Subject: [PATCH 26/73] =?UTF-8?q?:sparkles:=20feat:=20access/refresh=20tok?= =?UTF-8?q?en=20=EB=8B=B4=EC=9D=84=20vo=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/edu/kobridge/global/enums/JwtVo.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/enums/JwtVo.java diff --git a/src/main/java/com/edu/kobridge/global/enums/JwtVo.java b/src/main/java/com/edu/kobridge/global/enums/JwtVo.java new file mode 100644 index 0000000..350b970 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/enums/JwtVo.java @@ -0,0 +1,11 @@ +package com.edu.kobridge.global.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class JwtVo { + private final String accessToken; + private final String refreshToken; +} \ No newline at end of file From 9cd0ebb258b04d69d00abb7def5836b95bce3c8f Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:31:58 +0900 Subject: [PATCH 27/73] =?UTF-8?q?:sparkles:=20feat:=20google=20id=20token?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20util=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/util/GoogleOAuthUtil.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java new file mode 100644 index 0000000..17c7029 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java @@ -0,0 +1,49 @@ +package com.edu.kobridge.global.util; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Collections; + +import org.springframework.beans.factory.annotation.Value; +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.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.gson.GsonFactory; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class GoogleOAuthUtil { + + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String CLIENT_ID; + + public User authenticate(String idToken) + throws GeneralSecurityException, IOException { + // idToken 유효성 확인을 위한 Google 인증 설정 + HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport(); + GsonFactory gsonFactory = GsonFactory.getDefaultInstance(); + GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, gsonFactory) + .setAudience(Collections.singletonList(CLIENT_ID)) + .build(); + + // 전달된 idToken을 검증하여 GoogleIdToken 객체 생성 + GoogleIdToken googleIdToken = verifier.verify(idToken); + if (googleIdToken == null) { + throw new AppException(UserErrorCode.INVALID_ID_TOKEN); + } + + // 검증된 ID 토큰의 페이로드에서 사용자 정보 추출 + GoogleIdToken.Payload payload = googleIdToken.getPayload(); + String email = payload.getEmail(); + + return User.of(email); + } +} From 9f157e00ce2e91956432f6399151d16356c99e42 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:33:41 +0900 Subject: [PATCH 28/73] =?UTF-8?q?:sparkles:=20feat:=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=9D=91=EB=8B=B5=20util=20=EC=B6=94=EA=B0=80=20[K?= =?UTF-8?q?OBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/util/ResponseUtil.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/ResponseUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/ResponseUtil.java b/src/main/java/com/edu/kobridge/global/util/ResponseUtil.java new file mode 100644 index 0000000..690f677 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/ResponseUtil.java @@ -0,0 +1,39 @@ +package com.edu.kobridge.global.util; + +import java.io.IOException; + +import org.springframework.stereotype.Component; + +import com.edu.kobridge.global.common.DataResponseDto; +import com.edu.kobridge.global.common.ResponseDto; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +@RequiredArgsConstructor +public class ResponseUtil { + + // 단순 메시지 응답을 설정 + public static void setResponse(HttpServletResponse response, int status, String message) throws IOException { + ResponseDto dto = ResponseDto.of(status, message); + + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.setStatus(status); + response.getWriter().write(new ObjectMapper().writeValueAsString(dto)); + } + + // 데이터가 포함된 응답을 설정 + public static void setDataResponse(HttpServletResponse response, int status, Object data) throws IOException { + ResponseDto dto = DataResponseDto.of(data, status); + + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.setStatus(status); + response.getWriter().write(new ObjectMapper().writeValueAsString(dto)); + } +} From 9c1f88902195c1ec4247386b8ee1fc5ba45589ce Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:35:52 +0900 Subject: [PATCH 29/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/GlobalErrorCode.java | 7 ++++++ .../kobridge/user/error/UserErrorCode.java | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/error/UserErrorCode.java diff --git a/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java b/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java index 1cf72eb..8db06b5 100644 --- a/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java +++ b/src/main/java/com/edu/kobridge/global/error/GlobalErrorCode.java @@ -6,6 +6,13 @@ @Getter public enum GlobalErrorCode implements ErrorCode { + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "인증에 실패하였습니다."), + AUTHORIZATION_FAILED(HttpStatus.UNAUTHORIZED, "인가에 실패하였습니다."), + ACCESS_TOKEN_REQUIRED(HttpStatus.UNAUTHORIZED, "Access Token이 필요합니다."), + REFRESH_TOKEN_REQUIRED(HttpStatus.UNAUTHORIZED, "Refresh Token이 필요합니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Token이 유효하지 않습니다."), + EXPIRED_JWT(HttpStatus.FORBIDDEN, "Token이 만료되었습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 사용자를 찾을 수 없습니다."), MISSING_HEADER(HttpStatus.BAD_REQUEST, "필수 요청 헤더가 누락되었습니다."), METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "요청 경로가 지원되지 않습니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류가 발생했습니다."); diff --git a/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java b/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java new file mode 100644 index 0000000..12b37e2 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java @@ -0,0 +1,22 @@ +package com.edu.kobridge.user.error; + +import org.springframework.http.HttpStatus; + +import com.edu.kobridge.global.error.ErrorCode; + +import lombok.Getter; + +@Getter +public enum UserErrorCode implements ErrorCode { + ID_TOKEN_REQUIRED(HttpStatus.BAD_REQUEST, "Id Token이 필요합니다."), + INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, "유효한 Id Token이 아닙니다."), + GRADE_SIZE_ERROR(HttpStatus.BAD_REQUEST, "중학생, 고등학생은 3학년까지만 존재합니다."); + + private final HttpStatus httpStatus; + private final String message; + + UserErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } +} From ddc74f62200785dd732e26ab9100f75cf07af814 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:37:09 +0900 Subject: [PATCH 30/73] =?UTF-8?q?:sparkles:=20feat:=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20FillterException=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/exception/FilterException.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/error/exception/FilterException.java diff --git a/src/main/java/com/edu/kobridge/global/error/exception/FilterException.java b/src/main/java/com/edu/kobridge/global/error/exception/FilterException.java new file mode 100644 index 0000000..7168ab6 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/error/exception/FilterException.java @@ -0,0 +1,18 @@ +package com.edu.kobridge.global.error.exception; + +import com.edu.kobridge.global.error.ErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FilterException extends RuntimeException { + private ErrorCode errorCode; + private String message; + + public FilterException(ErrorCode errorCode) { + this.errorCode = errorCode; + this.message = errorCode.getMessage(); + } +} From c8d06e7365143b1c98fe9080b8be009bb69d9554 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:37:43 +0900 Subject: [PATCH 31/73] =?UTF-8?q?:sparkles:=20feat:=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/handler/GlobalExceptionHandler.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java index 86c9372..d4aea85 100644 --- a/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/edu/kobridge/global/error/handler/GlobalExceptionHandler.java @@ -15,6 +15,7 @@ import com.edu.kobridge.global.error.ErrorCode; import com.edu.kobridge.global.error.GlobalErrorCode; import com.edu.kobridge.global.error.exception.AppException; +import com.edu.kobridge.global.error.exception.FilterException; import lombok.extern.slf4j.Slf4j; @@ -35,6 +36,18 @@ public ResponseEntity exceptionHandler(AppException e) { return ResponseEntity.status(code).body(ResponseDto.of(code, message)); } + // FilterException + @ExceptionHandler(FilterException.class) + public ResponseEntity exceptionHandler(FilterException e) { + int code = e.getErrorCode().getHttpStatus().value(); + String message = e.getMessage(); + + if (code == 500) + e.printStackTrace(); + + return ResponseEntity.status(code).body(ResponseDto.of(code, message)); + } + // HttpMessageNotReadableException @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { From 94ad68aeb623747ff0c77e567bd3878989208119 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:40:17 +0900 Subject: [PATCH 32/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B0=84?= =?UTF-8?q?=EB=8B=A8=20=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/user/dto/res/LoginResDto.java | 27 ++++++++++++++ .../edu/kobridge/user/dto/res/UserResDto.java | 36 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java create mode 100644 src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java diff --git a/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java b/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java new file mode 100644 index 0000000..e059245 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java @@ -0,0 +1,27 @@ +package com.edu.kobridge.user.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LoginResDto { + @Schema(description = "accessToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") + private final String accessToken; + + @Schema(description = "refreshToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") + private final String refreshToken; + + @Schema(description = "isFirstLogin : 사용자가 회원가입한 적이 없다면 false 반환 / 사용자가 회원가입한 적이 있다면 true 반환", example = "false") + private final boolean isFirstLogin; + + public static LoginResDto of(String accessToken, String refreshToken, boolean isFirstLogin) { + return LoginResDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .isFirstLogin(isFirstLogin) + .build(); + } +} diff --git a/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java b/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java new file mode 100644 index 0000000..ce76ee0 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java @@ -0,0 +1,36 @@ +package com.edu.kobridge.user.dto.res; + +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.VoiceType; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Schema(description = "사용자 간단 정보 DTO") +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class UserResDto { + + @Schema(description = "이름", example = "김철수") + private final String name; + + @Schema(description = "번역 언어", example = "ENG") + private final LangType lang; + + @Schema(description = "선택한 Voice", example = "ONE") + private final VoiceType voice; + + @Schema(description = "현재 level", example = "3") + private final int level; + + public static UserResDto of(String name, LangType lang, VoiceType voice, int level) { + return UserResDto.builder() + .name(name) + .lang(lang) + .voice(voice) + .level(level) + .build(); + } +} From 0c9794fa4280ca09e021f956f4c7968a6f5a19de Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:40:40 +0900 Subject: [PATCH 33/73] =?UTF-8?q?:sparkles:=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/user/dto/req/SignUpReqDto.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java diff --git a/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java b/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java new file mode 100644 index 0000000..8569fa4 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java @@ -0,0 +1,43 @@ +package com.edu.kobridge.user.dto.req; + +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.SchoolType; +import com.edu.kobridge.global.enums.VoiceType; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Schema(description = "사용자 가입 요청 DTO") +public record SignUpReqDto( + @Schema(description = "이름", example = "김철수") + @NotBlank(message = "이름은 필수 값입니다.") + @Size(min = 2, max = 50, message = "이름은 2자이상 50자 미만이어야 합니다.") + String name, + + @Schema(description = "나이", example = "23") + @Min(value = 1, message = "나이는 1 이상이어야 합니다.") + @Max(value = 100, message = "나이는 100 이하여야 합니다.") + int age, + + @Schema(description = "언어", example = "ENG , VET, CHN, JPN, NONE") + @NotNull(message = "역할은 필수 값입니다.") + LangType lang, + + @Schema(description = "학교", example = "ELEMENTARY , MIDDLE, HIGH") + @NotNull(message = "학교는 필수 값입니다.") + SchoolType school, + + @Schema(description = "학년", example = "3") + @Min(value = 1, message = "학년은 1 이상이어야 합니다.") + @Max(value = 6, message = "학년은 6 이하여야 합니다.") + byte grade, + + @Schema(description = "음성", example = "BASIC, CUSTOM") + @NotNull(message = "음성은 필수 값입니다.") + VoiceType voice +) { +} From 1ddf939ae30e8e18cd32bd5392eb697285569828 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:41:24 +0900 Subject: [PATCH 34/73] =?UTF-8?q?:sparkles:=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/user/service/UserService.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/service/UserService.java diff --git a/src/main/java/com/edu/kobridge/user/service/UserService.java b/src/main/java/com/edu/kobridge/user/service/UserService.java new file mode 100644 index 0000000..9ff5e85 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/service/UserService.java @@ -0,0 +1,127 @@ +package com.edu.kobridge.user.service; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.edu.kobridge.global.enums.JwtVo; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.enums.SchoolType; +import com.edu.kobridge.global.error.ErrorCode; +import com.edu.kobridge.global.error.GlobalErrorCode; +import com.edu.kobridge.global.error.exception.AppException; +import com.edu.kobridge.global.util.GoogleOAuthUtil; +import com.edu.kobridge.global.util.JwtUtil; +import com.edu.kobridge.global.util.RedisUtil; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.domain.repository.UserRepository; +import com.edu.kobridge.user.dto.req.SignUpReqDto; +import com.edu.kobridge.user.dto.res.LoginResDto; +import com.edu.kobridge.user.dto.res.UserResDto; +import com.edu.kobridge.user.error.UserErrorCode; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Slf4j +public class UserService { + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + private final GoogleOAuthUtil googleOAuthUtil; + private final UserRepository userRepository; + + @Transactional + public LoginResDto getGoogleLogin(String idToken) { + // 요청 인자 유효성 검사 + if (idToken.isBlank()) { + throw new AppException(UserErrorCode.ID_TOKEN_REQUIRED); + } + + // 구글 인증 진행 + User googleUser = null; + try { + googleUser = googleOAuthUtil.authenticate(idToken); + } catch (GeneralSecurityException | IOException e) { + throw new AppException(UserErrorCode.INVALID_ID_TOKEN); + } + + // 유저 찾고, 없다면 새 유저 생성 + User finalGoogleUser = googleUser; + User user = userRepository.findByEmail(googleUser.getEmail()) + .orElseGet(() -> userRepository.save(finalGoogleUser)); + + // JWT 토큰 생성 및 refreshToken 저장 + JwtVo jwtVo = jwtUtil.generateTokens(user); + redisUtil.setOpsForValue(user.getId() + "_refresh", jwtVo.getRefreshToken(), + jwtUtil.getREFRESH_TOKEN_EXPIRATION()); + + return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken(), user.getLang() == null); + } + + @Transactional + public LoginResDto getAccessToken(String refreshToken) { + if (refreshToken.isBlank()) { + throw new AppException(GlobalErrorCode.REFRESH_TOKEN_REQUIRED); + } + + // refreshToken 유효성 검사 실행 + User tokenUser; + try { + tokenUser = jwtUtil.validateToken(false, refreshToken); + } catch (JwtException e) { + ErrorCode code = + e instanceof ExpiredJwtException ? GlobalErrorCode.EXPIRED_JWT : GlobalErrorCode.INVALID_TOKEN; + + throw new AppException(code); + } + + // JWT 토큰 생성 및 refreshToken 저장 + JwtVo jwtVo = jwtUtil.generateTokens(tokenUser); + redisUtil.setOpsForValue(tokenUser.getId() + "_refresh", jwtVo.getRefreshToken(), + jwtUtil.getREFRESH_TOKEN_EXPIRATION()); + + return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken(), tokenUser.getLang() == null); + } + + @Transactional + public void postSignUp(User user, SignUpReqDto signUpReq) { + // 학년 유효성 검사 + if (signUpReq.school() == SchoolType.MIDDLE || signUpReq.school() == SchoolType.HIGH || signUpReq.grade() > 3) { + throw new AppException(UserErrorCode.GRADE_SIZE_ERROR); + } + + // 사용자 정보 업데이트 + user.updateInfo(signUpReq.name(), signUpReq.lang(), signUpReq.school(), signUpReq.grade(), signUpReq.voice()); + + userRepository.save(user); + } + + public UserResDto getUserInfo(User user) { + return UserResDto.of(user.getName(), user.getLang(), user.getVoice(), user.getLevel()); + } + + @Transactional + public void updateLang(User user, LangType lang) { + user.updateLang(lang); + userRepository.save(user); + } + + @Transactional + public void updateLevel(User user) { + user.updateLevel(user.getLevel() + 1); + userRepository.save(user); + } + + @Transactional + public void deleteGoogleLogout(User user) { + // 사용자 refreshToken 삭제 + redisUtil.delete(user.getId() + "_refresh"); + } +} From 9596758524b3b005b4693edc1b49456596a0a395 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:41:40 +0900 Subject: [PATCH 35/73] =?UTF-8?q?:sparkles:=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20api=20=EA=B5=AC=ED=98=84=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/controller/UserController.java diff --git a/src/main/java/com/edu/kobridge/user/controller/UserController.java b/src/main/java/com/edu/kobridge/user/controller/UserController.java new file mode 100644 index 0000000..7cc6348 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/controller/UserController.java @@ -0,0 +1,75 @@ +package com.edu.kobridge.user.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.edu.kobridge.global.common.DataResponseDto; +import com.edu.kobridge.global.common.ResponseDto; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.dto.req.SignUpReqDto; +import com.edu.kobridge.user.dto.res.LoginResDto; +import com.edu.kobridge.user.service.UserService; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequestMapping("/api/user") +@RequiredArgsConstructor +@Slf4j +public class UserController implements UserControllerDocs { + private final UserService userService; + + @GetMapping("/google-login") + public ResponseEntity getGoogleLogin(@RequestHeader("id-token") String idToken) { + LoginResDto resDto = userService.getGoogleLogin(idToken); + return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); + } + + @GetMapping("/token") + public ResponseEntity getAccessToken(@RequestHeader("Authorization-Refresh") String refreshToken) { + LoginResDto resDto = userService.getAccessToken(refreshToken); + return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); + } + + @PostMapping + public ResponseEntity postSignUp(@RequestAttribute("user") User user, + @RequestBody @Valid SignUpReqDto signUpReq) { + userService.postSignUp(user, signUpReq); + return ResponseEntity.ok(ResponseDto.of(200)); + } + + @GetMapping + public ResponseEntity getUserInfo(@RequestAttribute("user") User user) { + return ResponseEntity.status(200).body(DataResponseDto.of(userService.getUserInfo(user), 200)); + } + + @PatchMapping("/lang") + public ResponseEntity patchLang(@RequestAttribute("user") User user, LangType lang) { + userService.updateLang(user, lang); + return ResponseEntity.ok(ResponseDto.of(200)); + } + + @PatchMapping("level") + public ResponseEntity patchLevel(@RequestAttribute("user") User user) { + userService.updateLevel(user); + return ResponseEntity.ok(ResponseDto.of(200)); + } + + @DeleteMapping("/google-logout") + public ResponseEntity deleteGoogleLogout(@RequestAttribute("user") User user) { + userService.deleteGoogleLogout(user); + return ResponseEntity.ok(ResponseDto.of(200)); + } + +} From febddc87161ab3cd8de3f1aa434a593121e1991b Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:42:05 +0900 Subject: [PATCH 36/73] =?UTF-8?q?:memo:=20docs:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20api=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20[KO?= =?UTF-8?q?BG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserControllerDocs.java | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java diff --git a/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java b/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java new file mode 100644 index 0000000..a96ff63 --- /dev/null +++ b/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java @@ -0,0 +1,269 @@ +package com.edu.kobridge.user.controller; + +import org.springframework.http.ResponseEntity; + +import com.edu.kobridge.global.common.ResponseDto; +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.user.dto.req.SignUpReqDto; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "User", description = "사용자 관련 API") +public interface UserControllerDocs { + + @Operation(summary = "로그인", description = "구글 로그인을 진행합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Created", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 201, \"message\": \"Created\" }") + ) + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 400, \"message\": \"Id Token이 필요합니다.\" }," + ) + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }," + + "{ \"code\": 404, \"message\": \"해당하는 사용자를 찾을 수 없습니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") + ) + ), + @ApiResponse(responseCode = "404", description = "해당 자원을 찾을 수 없습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 404, \"message\": \"해당하는 사용자를 찾을 수 없습니다.\" }") + ) + ) + }) + ResponseEntity getGoogleLogin(String idToken); + + @Operation(summary = "accessToken 발급", description = "리프레시 토큰을 이용해 엑세스 토큰을 발급합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "Created", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 201, \"message\": \"Created\" }") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Refresh Token이 필요합니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") + ) + ) + }) + ResponseEntity getAccessToken(String refreshToken); + + @Operation(summary = "회원가입", description = "사용자 회원가입을 진행합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 400, \"message\": \"이름은 필수 값입니다.\" }, " + + "{ \"code\": 400, \"message\": \"이름은 2자이상 50자 미만이어야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"나이는 1 이상이어야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"나이는 100 이하여야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"역할은 필수 값입니다.\" }," + + "{ \"code\": 400, \"message\": \"학교는 필수 값입니다.\" }," + + "{ \"code\": 400, \"message\": \"학년은 1 이상이어야 합니다.\" }," + + "{ \"code\": 400, \"message\": \"학년은 6 이하여야 합니다.\" }" + + "{ \"code\": 400, \"message\": \"중학생, 고등학생은 3학년까지만 존재합니다.\" }" + + "{ \"code\": 400, \"message\": \"음성은 필수 값입니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ), + @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") + ) + ) + }) + ResponseEntity postSignUp(User user, SignUpReqDto signUpReq); + + @Operation(summary = "사용자 정보 확인", description = "사용자의 이름과 번역 언어, 레벨을 확인합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{\n" + + " \"code\": 200,\n" + + " \"message\": \"OK\",\n" + + " \"data\": {\n" + + " \"name\": \"김철수\",\n" + + " \"lang\": \"ENG\",\n" + + " \"level\": 1\n" + + " }\n" + + "}") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity getUserInfo(User user); + + @Operation(summary = "번역 언어 변경", description = "사용하는 번역 언어를 변경합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 400, \"message\": \"LANG_TYPE 의 값이 아닙니다.\" }" + ) + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity patchLang(User user, LangType lang); + + @Operation(summary = "레벨 완료", description = "레벨을 완료하여 1단게 높아집니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity patchLevel(User user); + + @Operation(summary = "로그아웃", description = "로그아웃을 진행합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") + ) + ), + @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + + "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + + "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + + "]" + ) + ) + ) + }) + ResponseEntity deleteGoogleLogout(User user); + +} + From 61f6dc653cdc3d8568b21540172e7d879c3f3de3 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:43:01 +0900 Subject: [PATCH 37/73] =?UTF-8?q?:sparkles:=20feat:=20jwt=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?JwtAuthorizationFilter=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/filter/JwtAuthorizationFilter.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java diff --git a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java new file mode 100644 index 0000000..477044e --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java @@ -0,0 +1,98 @@ +package com.edu.kobridge.global.common.filter; + +import java.io.IOException; + +import com.edu.kobridge.global.error.ErrorCode; +import com.edu.kobridge.global.error.GlobalErrorCode; +import com.edu.kobridge.global.error.exception.AppException; +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 io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Slf4j +public class JwtAuthorizationFilter implements Filter { + private final JwtUtil jwtUtil; + + // JWT 검사 제외할 경로 설정 + final String LOGIN_PATH = "/api/user/google-login"; + final String TOKEN_PATH = "/api/user/token"; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + log.info("Filter initialized."); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest)request; + HttpServletResponse res = (HttpServletResponse)response; + + // 요청 URI를 가져옴 + String requestURI = req.getRequestURI(); + + // swagger 인증 필터링 없이 처리 + if (requestURI.startsWith("/swagger-ui/") || + requestURI.startsWith("/webjars/") || + requestURI.startsWith("/v3/api-docs")) { + chain.doFilter(request, response); + return; + } + + // 로그인 및 토큰 재발급 요청은 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.contains("test/no-auth")) { + chain.doFilter(request, response); + return; + } + + try { + // Authorization 헤더에서 JWT 토큰을 가져옴 + String header = req.getHeader("Authorization"); + if (header == null) { + throw new FilterException(GlobalErrorCode.ACCESS_TOKEN_REQUIRED); + } + + // 토큰 유효성 검사 후 사용자 정보 추출 + User user = jwtUtil.validateToken(true, header); + req.setAttribute("user", user); + + // 필터 체인 다음 필터로 이동 + chain.doFilter(request, response); + } catch (FilterException e) { // 토큰이 없는 경우 + ResponseUtil.setResponse(res, e.getErrorCode().getHttpStatus().value(), e.getMessage()); + } catch (AppException e) { // 토큰이 유효하지 않은 경우 + ResponseUtil.setResponse(res, e.getErrorCode().getHttpStatus().value(), e.getMessage()); + } catch (JwtException e) { // JWT 토큰 예외 처리 + ErrorCode code = + e instanceof ExpiredJwtException ? GlobalErrorCode.EXPIRED_JWT : GlobalErrorCode.INVALID_TOKEN; + + ResponseUtil.setResponse(res, code.getHttpStatus().value(), code.getMessage()); + } + } + + @Override + public void destroy() { + log.info("Filter destroyed."); + } +} From e515dee016cad4e90e8042728ff06f3dc5eb82e2 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:44:17 +0900 Subject: [PATCH 38/73] =?UTF-8?q?:sparkles:=20feat:=20JwtAuthorizationFilt?= =?UTF-8?q?er=20=EB=93=B1=EB=A1=9D=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobridge/global/config/WebMvcConfig.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java diff --git a/src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java b/src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java new file mode 100644 index 0000000..39e3bc0 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/config/WebMvcConfig.java @@ -0,0 +1,29 @@ +package com.edu.kobridge.global.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.edu.kobridge.global.common.filter.JwtAuthorizationFilter; +import com.edu.kobridge.global.util.JwtUtil; + +import jakarta.servlet.Filter; +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + private final JwtUtil jwtUtil; + + // jwt 필터 등록 + @Bean + public FilterRegistrationBean filterBean() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new JwtAuthorizationFilter(jwtUtil)); + filterRegistrationBean.setOrder(1); + filterRegistrationBean.addUrlPatterns("/*"); + + return filterRegistrationBean; + } +} From 82fc27e191e6fa3fe6ff26327b36c8773abcdcfa Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Mon, 18 Aug 2025 23:50:26 +0900 Subject: [PATCH 39/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20cd?= =?UTF-8?q?=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=8B=9C=EC=A0=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 22b75c2..4fd3ef1 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -1,10 +1,9 @@ name: Java CD with Gradle on: - push: + pull_request: branches: - - main - - feature/KOBG-4/initial-setting + - develop permissions: contents: read From ecd28abd2307a3b4c8afe314ca5a049804c59250 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 00:07:09 +0900 Subject: [PATCH 40/73] =?UTF-8?q?:bug:=20fix:=20auditing=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/edu/kobridge/KobridgeApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/edu/kobridge/KobridgeApplication.java b/src/main/java/com/edu/kobridge/KobridgeApplication.java index cddcf21..3846808 100644 --- a/src/main/java/com/edu/kobridge/KobridgeApplication.java +++ b/src/main/java/com/edu/kobridge/KobridgeApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class KobridgeApplication { From af36ed671468cc14bb683c151f6011078c1258d8 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 00:25:33 +0900 Subject: [PATCH 41/73] =?UTF-8?q?:wrench:=20chore(deps):=20ci=20pipeline?= =?UTF-8?q?=20rds=20=EC=97=B0=EA=B2=B0=20=EB=B6=88=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20h2=20package=20=EC=B6=94=EA=B0=80=20[KOBG-?= =?UTF-8?q?12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 0ac4ea0..f680a92 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,7 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'org.mockito:mockito-core:5.18.0' testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0' + testImplementation 'com.h2database:h2' } compileJava.options.encoding = 'UTF-8' From 5d8b2bee0ff3734fc69101aed032d7fd04c035e2 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 00:26:07 +0900 Subject: [PATCH 42/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20te?= =?UTF-8?q?st=20=EC=9A=A9=20application.yml=20=EB=94=B0=EB=A1=9C=20propert?= =?UTF-8?q?ies=20=EC=84=A4=EC=A0=95=20[KOBG-12]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 17405f8..3fb3794 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -29,7 +29,7 @@ jobs: run: | cd ./src/main/resources touch ./application.yml - echo "${{ secrets.PROPERTIES }}" > ./application.yml + echo "${{ secrets.PROPERTIES_TEST }}" > ./application.yml shell: bash - name: 🚀 Redis 실행 From 03be3a09d92f176a94a9860453846774987a838b Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 20:29:06 +0900 Subject: [PATCH 43/73] =?UTF-8?q?:recycle:=20refactor:=20user=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/filter/JwtAuthorizationFilter.java | 2 +- .../edu/kobridge/global/util/GoogleOAuthUtil.java | 4 ++-- .../java/com/edu/kobridge/global/util/JwtUtil.java | 4 ++-- .../user}/user/controller/UserController.java | 10 +++++----- .../user}/user/controller/UserControllerDocs.java | 6 +++--- .../{ => module/user}/user/domain/entity/User.java | 2 +- .../user/domain/repository/UserRepository.java | 4 ++-- .../user}/user/dto/req/SignUpReqDto.java | 2 +- .../user}/user/dto/res/LoginResDto.java | 2 +- .../{ => module/user}/user/dto/res/UserResDto.java | 2 +- .../user}/user/error/UserErrorCode.java | 2 +- .../user}/user/service/UserService.java | 14 +++++++------- 12 files changed, 27 insertions(+), 27 deletions(-) rename src/main/java/com/edu/kobridge/{ => module/user}/user/controller/UserController.java (89%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/controller/UserControllerDocs.java (98%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/domain/entity/User.java (97%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/domain/repository/UserRepository.java (68%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/dto/req/SignUpReqDto.java (96%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/dto/res/LoginResDto.java (94%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/dto/res/UserResDto.java (94%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/error/UserErrorCode.java (92%) rename src/main/java/com/edu/kobridge/{ => module/user}/user/service/UserService.java (89%) diff --git a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java index 477044e..ae16b75 100644 --- a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java +++ b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java @@ -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.user.domain.entity.User; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; diff --git a/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java index 17c7029..b81962f 100644 --- a/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java +++ b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java @@ -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.user.domain.entity.User; +import com.edu.kobridge.module.user.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; diff --git a/src/main/java/com/edu/kobridge/global/util/JwtUtil.java b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java index 7d94507..f294fba 100644 --- a/src/main/java/com/edu/kobridge/global/util/JwtUtil.java +++ b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java @@ -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.user.domain.entity.User; +import com.edu.kobridge.module.user.user.domain.repository.UserRepository; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; diff --git a/src/main/java/com/edu/kobridge/user/controller/UserController.java b/src/main/java/com/edu/kobridge/module/user/user/controller/UserController.java similarity index 89% rename from src/main/java/com/edu/kobridge/user/controller/UserController.java rename to src/main/java/com/edu/kobridge/module/user/user/controller/UserController.java index 7cc6348..efc2db8 100644 --- a/src/main/java/com/edu/kobridge/user/controller/UserController.java +++ b/src/main/java/com/edu/kobridge/module/user/user/controller/UserController.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.user.controller; +package com.edu.kobridge.module.user.user.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -14,10 +14,10 @@ import com.edu.kobridge.global.common.DataResponseDto; import com.edu.kobridge.global.common.ResponseDto; import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.user.domain.entity.User; -import com.edu.kobridge.user.dto.req.SignUpReqDto; -import com.edu.kobridge.user.dto.res.LoginResDto; -import com.edu.kobridge.user.service.UserService; +import com.edu.kobridge.module.user.user.domain.entity.User; +import com.edu.kobridge.module.user.user.dto.req.SignUpReqDto; +import com.edu.kobridge.module.user.user.dto.res.LoginResDto; +import com.edu.kobridge.module.user.user.service.UserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java b/src/main/java/com/edu/kobridge/module/user/user/controller/UserControllerDocs.java similarity index 98% rename from src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java rename to src/main/java/com/edu/kobridge/module/user/user/controller/UserControllerDocs.java index a96ff63..88dc856 100644 --- a/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java +++ b/src/main/java/com/edu/kobridge/module/user/user/controller/UserControllerDocs.java @@ -1,11 +1,11 @@ -package com.edu.kobridge.user.controller; +package com.edu.kobridge.module.user.user.controller; import org.springframework.http.ResponseEntity; import com.edu.kobridge.global.common.ResponseDto; import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.user.domain.entity.User; -import com.edu.kobridge.user.dto.req.SignUpReqDto; +import com.edu.kobridge.module.user.user.domain.entity.User; +import com.edu.kobridge.module.user.user.dto.req.SignUpReqDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/com/edu/kobridge/user/domain/entity/User.java b/src/main/java/com/edu/kobridge/module/user/user/domain/entity/User.java similarity index 97% rename from src/main/java/com/edu/kobridge/user/domain/entity/User.java rename to src/main/java/com/edu/kobridge/module/user/user/domain/entity/User.java index 8f594ba..beff77f 100644 --- a/src/main/java/com/edu/kobridge/user/domain/entity/User.java +++ b/src/main/java/com/edu/kobridge/module/user/user/domain/entity/User.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.user.domain.entity; +package com.edu.kobridge.module.user.user.domain.entity; import com.edu.kobridge.global.common.BaseTime; import com.edu.kobridge.global.enums.LangType; diff --git a/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java b/src/main/java/com/edu/kobridge/module/user/user/domain/repository/UserRepository.java similarity index 68% rename from src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java rename to src/main/java/com/edu/kobridge/module/user/user/domain/repository/UserRepository.java index c03bda2..2e5dcf7 100644 --- a/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java +++ b/src/main/java/com/edu/kobridge/module/user/user/domain/repository/UserRepository.java @@ -1,11 +1,11 @@ -package com.edu.kobridge.user.domain.repository; +package com.edu.kobridge.module.user.user.domain.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import com.edu.kobridge.user.domain.entity.User; +import com.edu.kobridge.module.user.user.domain.entity.User; @Repository public interface UserRepository extends JpaRepository { diff --git a/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java b/src/main/java/com/edu/kobridge/module/user/user/dto/req/SignUpReqDto.java similarity index 96% rename from src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java rename to src/main/java/com/edu/kobridge/module/user/user/dto/req/SignUpReqDto.java index 8569fa4..f2e72f5 100644 --- a/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java +++ b/src/main/java/com/edu/kobridge/module/user/user/dto/req/SignUpReqDto.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.user.dto.req; +package com.edu.kobridge.module.user.user.dto.req; import com.edu.kobridge.global.enums.LangType; import com.edu.kobridge.global.enums.SchoolType; diff --git a/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java b/src/main/java/com/edu/kobridge/module/user/user/dto/res/LoginResDto.java similarity index 94% rename from src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java rename to src/main/java/com/edu/kobridge/module/user/user/dto/res/LoginResDto.java index e059245..fea8394 100644 --- a/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java +++ b/src/main/java/com/edu/kobridge/module/user/user/dto/res/LoginResDto.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.user.dto.res; +package com.edu.kobridge.module.user.user.dto.res; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; diff --git a/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java b/src/main/java/com/edu/kobridge/module/user/user/dto/res/UserResDto.java similarity index 94% rename from src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java rename to src/main/java/com/edu/kobridge/module/user/user/dto/res/UserResDto.java index ce76ee0..6dacf6f 100644 --- a/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java +++ b/src/main/java/com/edu/kobridge/module/user/user/dto/res/UserResDto.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.user.dto.res; +package com.edu.kobridge.module.user.user.dto.res; import com.edu.kobridge.global.enums.LangType; import com.edu.kobridge.global.enums.VoiceType; diff --git a/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java b/src/main/java/com/edu/kobridge/module/user/user/error/UserErrorCode.java similarity index 92% rename from src/main/java/com/edu/kobridge/user/error/UserErrorCode.java rename to src/main/java/com/edu/kobridge/module/user/user/error/UserErrorCode.java index 12b37e2..90934b6 100644 --- a/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java +++ b/src/main/java/com/edu/kobridge/module/user/user/error/UserErrorCode.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.user.error; +package com.edu.kobridge.module.user.user.error; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/edu/kobridge/user/service/UserService.java b/src/main/java/com/edu/kobridge/module/user/user/service/UserService.java similarity index 89% rename from src/main/java/com/edu/kobridge/user/service/UserService.java rename to src/main/java/com/edu/kobridge/module/user/user/service/UserService.java index 9ff5e85..c95eecc 100644 --- a/src/main/java/com/edu/kobridge/user/service/UserService.java +++ b/src/main/java/com/edu/kobridge/module/user/user/service/UserService.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.user.service; +package com.edu.kobridge.module.user.user.service; import java.io.IOException; import java.security.GeneralSecurityException; @@ -15,12 +15,12 @@ import com.edu.kobridge.global.util.GoogleOAuthUtil; import com.edu.kobridge.global.util.JwtUtil; import com.edu.kobridge.global.util.RedisUtil; -import com.edu.kobridge.user.domain.entity.User; -import com.edu.kobridge.user.domain.repository.UserRepository; -import com.edu.kobridge.user.dto.req.SignUpReqDto; -import com.edu.kobridge.user.dto.res.LoginResDto; -import com.edu.kobridge.user.dto.res.UserResDto; -import com.edu.kobridge.user.error.UserErrorCode; +import com.edu.kobridge.module.user.user.domain.repository.UserRepository; +import com.edu.kobridge.module.user.user.dto.req.SignUpReqDto; +import com.edu.kobridge.module.user.user.dto.res.UserResDto; +import com.edu.kobridge.module.user.user.domain.entity.User; +import com.edu.kobridge.module.user.user.dto.res.LoginResDto; +import com.edu.kobridge.module.user.user.error.UserErrorCode; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; From 2f770553a2e4df1eac2ae8e62cd5a230129a6d15 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 20:29:36 +0900 Subject: [PATCH 44/73] =?UTF-8?q?:bug:=20fix:=20LangType=20=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=20=EC=A0=95=EC=A0=81=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B0=98=ED=99=98=EA=B0=92,=20=EB=A7=A4=ED=8C=BD?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/edu/kobridge/global/enums/LangType.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/edu/kobridge/global/enums/LangType.java b/src/main/java/com/edu/kobridge/global/enums/LangType.java index 8d34f10..4c6d2c0 100644 --- a/src/main/java/com/edu/kobridge/global/enums/LangType.java +++ b/src/main/java/com/edu/kobridge/global/enums/LangType.java @@ -20,8 +20,8 @@ public enum LangType { 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); From 3e380af11129f79ac03dded8d281fbb4bb71f48d Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Tue, 19 Aug 2025 21:37:24 +0900 Subject: [PATCH 45/73] =?UTF-8?q?:recycle:=20refactor:=20user=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A4=91=EB=B3=B5=20=EC=82=AD=EC=A0=9C=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/filter/JwtAuthorizationFilter.java | 2 +- .../edu/kobridge/global/util/GoogleOAuthUtil.java | 4 ++-- .../java/com/edu/kobridge/global/util/JwtUtil.java | 4 ++-- .../user/{user => }/controller/UserController.java | 10 +++++----- .../{user => }/controller/UserControllerDocs.java | 6 +++--- .../module/user/{user => }/domain/entity/User.java | 2 +- .../domain/repository/UserRepository.java | 4 ++-- .../user/{user => }/dto/req/SignUpReqDto.java | 2 +- .../user/{user => }/dto/res/LoginResDto.java | 2 +- .../module/user/{user => }/dto/res/UserResDto.java | 2 +- .../user/{user => }/error/UserErrorCode.java | 2 +- .../user/{user => }/service/UserService.java | 14 +++++++------- 12 files changed, 27 insertions(+), 27 deletions(-) rename src/main/java/com/edu/kobridge/module/user/{user => }/controller/UserController.java (89%) rename src/main/java/com/edu/kobridge/module/user/{user => }/controller/UserControllerDocs.java (98%) rename src/main/java/com/edu/kobridge/module/user/{user => }/domain/entity/User.java (97%) rename src/main/java/com/edu/kobridge/module/user/{user => }/domain/repository/UserRepository.java (68%) rename src/main/java/com/edu/kobridge/module/user/{user => }/dto/req/SignUpReqDto.java (96%) rename src/main/java/com/edu/kobridge/module/user/{user => }/dto/res/LoginResDto.java (94%) rename src/main/java/com/edu/kobridge/module/user/{user => }/dto/res/UserResDto.java (94%) rename src/main/java/com/edu/kobridge/module/user/{user => }/error/UserErrorCode.java (92%) rename src/main/java/com/edu/kobridge/module/user/{user => }/service/UserService.java (89%) diff --git a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java index ae16b75..1a2c204 100644 --- a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java +++ b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java @@ -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.module.user.user.domain.entity.User; +import com.edu.kobridge.module.user.domain.entity.User; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; diff --git a/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java index b81962f..e9191e3 100644 --- a/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java +++ b/src/main/java/com/edu/kobridge/global/util/GoogleOAuthUtil.java @@ -8,8 +8,8 @@ import org.springframework.stereotype.Component; import com.edu.kobridge.global.error.exception.AppException; -import com.edu.kobridge.module.user.user.domain.entity.User; -import com.edu.kobridge.module.user.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; diff --git a/src/main/java/com/edu/kobridge/global/util/JwtUtil.java b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java index f294fba..0a4ebc7 100644 --- a/src/main/java/com/edu/kobridge/global/util/JwtUtil.java +++ b/src/main/java/com/edu/kobridge/global/util/JwtUtil.java @@ -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.module.user.user.domain.entity.User; -import com.edu.kobridge.module.user.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; diff --git a/src/main/java/com/edu/kobridge/module/user/user/controller/UserController.java b/src/main/java/com/edu/kobridge/module/user/controller/UserController.java similarity index 89% rename from src/main/java/com/edu/kobridge/module/user/user/controller/UserController.java rename to src/main/java/com/edu/kobridge/module/user/controller/UserController.java index efc2db8..508eb58 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/controller/UserController.java +++ b/src/main/java/com/edu/kobridge/module/user/controller/UserController.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.module.user.user.controller; +package com.edu.kobridge.module.user.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -14,10 +14,10 @@ import com.edu.kobridge.global.common.DataResponseDto; import com.edu.kobridge.global.common.ResponseDto; import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.module.user.user.domain.entity.User; -import com.edu.kobridge.module.user.user.dto.req.SignUpReqDto; -import com.edu.kobridge.module.user.user.dto.res.LoginResDto; -import com.edu.kobridge.module.user.user.service.UserService; +import com.edu.kobridge.module.user.domain.entity.User; +import com.edu.kobridge.module.user.dto.req.SignUpReqDto; +import com.edu.kobridge.module.user.dto.res.LoginResDto; +import com.edu.kobridge.module.user.service.UserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/edu/kobridge/module/user/user/controller/UserControllerDocs.java b/src/main/java/com/edu/kobridge/module/user/controller/UserControllerDocs.java similarity index 98% rename from src/main/java/com/edu/kobridge/module/user/user/controller/UserControllerDocs.java rename to src/main/java/com/edu/kobridge/module/user/controller/UserControllerDocs.java index 88dc856..40f4330 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/controller/UserControllerDocs.java +++ b/src/main/java/com/edu/kobridge/module/user/controller/UserControllerDocs.java @@ -1,11 +1,11 @@ -package com.edu.kobridge.module.user.user.controller; +package com.edu.kobridge.module.user.controller; import org.springframework.http.ResponseEntity; import com.edu.kobridge.global.common.ResponseDto; import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.module.user.user.domain.entity.User; -import com.edu.kobridge.module.user.user.dto.req.SignUpReqDto; +import com.edu.kobridge.module.user.domain.entity.User; +import com.edu.kobridge.module.user.dto.req.SignUpReqDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/com/edu/kobridge/module/user/user/domain/entity/User.java b/src/main/java/com/edu/kobridge/module/user/domain/entity/User.java similarity index 97% rename from src/main/java/com/edu/kobridge/module/user/user/domain/entity/User.java rename to src/main/java/com/edu/kobridge/module/user/domain/entity/User.java index beff77f..c32d6fd 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/domain/entity/User.java +++ b/src/main/java/com/edu/kobridge/module/user/domain/entity/User.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.module.user.user.domain.entity; +package com.edu.kobridge.module.user.domain.entity; import com.edu.kobridge.global.common.BaseTime; import com.edu.kobridge.global.enums.LangType; diff --git a/src/main/java/com/edu/kobridge/module/user/user/domain/repository/UserRepository.java b/src/main/java/com/edu/kobridge/module/user/domain/repository/UserRepository.java similarity index 68% rename from src/main/java/com/edu/kobridge/module/user/user/domain/repository/UserRepository.java rename to src/main/java/com/edu/kobridge/module/user/domain/repository/UserRepository.java index 2e5dcf7..ef64bac 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/domain/repository/UserRepository.java +++ b/src/main/java/com/edu/kobridge/module/user/domain/repository/UserRepository.java @@ -1,11 +1,11 @@ -package com.edu.kobridge.module.user.user.domain.repository; +package com.edu.kobridge.module.user.domain.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import com.edu.kobridge.module.user.user.domain.entity.User; +import com.edu.kobridge.module.user.domain.entity.User; @Repository public interface UserRepository extends JpaRepository { diff --git a/src/main/java/com/edu/kobridge/module/user/user/dto/req/SignUpReqDto.java b/src/main/java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java similarity index 96% rename from src/main/java/com/edu/kobridge/module/user/user/dto/req/SignUpReqDto.java rename to src/main/java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java index f2e72f5..66f47a3 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/dto/req/SignUpReqDto.java +++ b/src/main/java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.module.user.user.dto.req; +package com.edu.kobridge.module.user.dto.req; import com.edu.kobridge.global.enums.LangType; import com.edu.kobridge.global.enums.SchoolType; diff --git a/src/main/java/com/edu/kobridge/module/user/user/dto/res/LoginResDto.java b/src/main/java/com/edu/kobridge/module/user/dto/res/LoginResDto.java similarity index 94% rename from src/main/java/com/edu/kobridge/module/user/user/dto/res/LoginResDto.java rename to src/main/java/com/edu/kobridge/module/user/dto/res/LoginResDto.java index fea8394..f641ae8 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/dto/res/LoginResDto.java +++ b/src/main/java/com/edu/kobridge/module/user/dto/res/LoginResDto.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.module.user.user.dto.res; +package com.edu.kobridge.module.user.dto.res; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; diff --git a/src/main/java/com/edu/kobridge/module/user/user/dto/res/UserResDto.java b/src/main/java/com/edu/kobridge/module/user/dto/res/UserResDto.java similarity index 94% rename from src/main/java/com/edu/kobridge/module/user/user/dto/res/UserResDto.java rename to src/main/java/com/edu/kobridge/module/user/dto/res/UserResDto.java index 6dacf6f..ec38c3d 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/dto/res/UserResDto.java +++ b/src/main/java/com/edu/kobridge/module/user/dto/res/UserResDto.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.module.user.user.dto.res; +package com.edu.kobridge.module.user.dto.res; import com.edu.kobridge.global.enums.LangType; import com.edu.kobridge.global.enums.VoiceType; diff --git a/src/main/java/com/edu/kobridge/module/user/user/error/UserErrorCode.java b/src/main/java/com/edu/kobridge/module/user/error/UserErrorCode.java similarity index 92% rename from src/main/java/com/edu/kobridge/module/user/user/error/UserErrorCode.java rename to src/main/java/com/edu/kobridge/module/user/error/UserErrorCode.java index 90934b6..cec5158 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/error/UserErrorCode.java +++ b/src/main/java/com/edu/kobridge/module/user/error/UserErrorCode.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.module.user.user.error; +package com.edu.kobridge.module.user.error; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/edu/kobridge/module/user/user/service/UserService.java b/src/main/java/com/edu/kobridge/module/user/service/UserService.java similarity index 89% rename from src/main/java/com/edu/kobridge/module/user/user/service/UserService.java rename to src/main/java/com/edu/kobridge/module/user/service/UserService.java index c95eecc..03a165a 100644 --- a/src/main/java/com/edu/kobridge/module/user/user/service/UserService.java +++ b/src/main/java/com/edu/kobridge/module/user/service/UserService.java @@ -1,4 +1,4 @@ -package com.edu.kobridge.module.user.user.service; +package com.edu.kobridge.module.user.service; import java.io.IOException; import java.security.GeneralSecurityException; @@ -15,12 +15,12 @@ import com.edu.kobridge.global.util.GoogleOAuthUtil; import com.edu.kobridge.global.util.JwtUtil; import com.edu.kobridge.global.util.RedisUtil; -import com.edu.kobridge.module.user.user.domain.repository.UserRepository; -import com.edu.kobridge.module.user.user.dto.req.SignUpReqDto; -import com.edu.kobridge.module.user.user.dto.res.UserResDto; -import com.edu.kobridge.module.user.user.domain.entity.User; -import com.edu.kobridge.module.user.user.dto.res.LoginResDto; -import com.edu.kobridge.module.user.user.error.UserErrorCode; +import com.edu.kobridge.module.user.domain.entity.User; +import com.edu.kobridge.module.user.domain.repository.UserRepository; +import com.edu.kobridge.module.user.dto.req.SignUpReqDto; +import com.edu.kobridge.module.user.dto.res.LoginResDto; +import com.edu.kobridge.module.user.dto.res.UserResDto; +import com.edu.kobridge.module.user.error.UserErrorCode; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; From 598594affcff9d17ada90548fcc461a99c27d865 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 09:36:54 +0900 Subject: [PATCH 46/73] =?UTF-8?q?:recycle:=20refactor:=20voice=20enum=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=EC=97=90=20=EB=A7=9E=EC=B6=B0=20example=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java b/src/main/java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java index 66f47a3..8fbd395 100644 --- a/src/main/java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java +++ b/src/main/java/com/edu/kobridge/module/user/dto/req/SignUpReqDto.java @@ -36,7 +36,7 @@ public record SignUpReqDto( @Max(value = 6, message = "학년은 6 이하여야 합니다.") byte grade, - @Schema(description = "음성", example = "BASIC, CUSTOM") + @Schema(description = "음성", example = "ONE, TWO, THREE, FOUR") @NotNull(message = "음성은 필수 값입니다.") VoiceType voice ) { From 71e7bc5cdc92219100a4dc16551998a1947c330b Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 09:37:40 +0900 Subject: [PATCH 47/73] =?UTF-8?q?:recycle:=20refactor:=20LangType=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EA=B8=B0=EB=B3=B8=EC=9D=B4=20ENG=20type?= =?UTF-8?q?=EC=9D=B4=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/edu/kobridge/global/enums/LangType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/edu/kobridge/global/enums/LangType.java b/src/main/java/com/edu/kobridge/global/enums/LangType.java index 4c6d2c0..e9365c3 100644 --- a/src/main/java/com/edu/kobridge/global/enums/LangType.java +++ b/src/main/java/com/edu/kobridge/global/enums/LangType.java @@ -24,6 +24,6 @@ public static LangType of(String code) { return Arrays.stream(LangType.values()) .filter(r -> r.getCode().equals(code)) .findAny() - .orElse(null); + .orElse(ENG); } } From b6f363bddf2126686e592757229f48a36882ed16 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:00:08 +0900 Subject: [PATCH 48/73] =?UTF-8?q?:sparkles:=20feat:=20Lesson,=20LessonSent?= =?UTF-8?q?ence,=20LessonChat=20Entity=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../module/lesson/domain/entity/Lesson.java | 56 +++++++++++++ .../lesson/domain/entity/LessonChat.java | 62 ++++++++++++++ .../lesson/domain/entity/LessonSentence.java | 82 +++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/domain/entity/Lesson.java create mode 100644 src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonChat.java create mode 100644 src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonSentence.java diff --git a/build.gradle b/build.gradle index f680a92..17ca273 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/com/edu/kobridge/module/lesson/domain/entity/Lesson.java b/src/main/java/com/edu/kobridge/module/lesson/domain/entity/Lesson.java new file mode 100644 index 0000000..2369c49 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/domain/entity/Lesson.java @@ -0,0 +1,56 @@ +package com.edu.kobridge.module.lesson.domain.entity; + +import java.util.List; + +import com.edu.kobridge.global.common.BaseTime; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class Lesson extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Column(unique = true) + private Integer number; + + @NotNull + private String title; + + @NotNull + private String subject; + + @NotNull + private String subTitle1; + + @NotNull + private String subTitle2; + + @NotNull + private String subTitle3; + + @OneToMany(mappedBy = "lesson", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("number ASC") + private List sentences; + +} diff --git a/src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonChat.java b/src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonChat.java new file mode 100644 index 0000000..18c31a6 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonChat.java @@ -0,0 +1,62 @@ +package com.edu.kobridge.module.lesson.domain.entity; + +import com.edu.kobridge.global.common.BaseTime; +import com.edu.kobridge.global.enums.LangType; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class LessonChat extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private String contentKo; + + @NotNull + private String contentEng; + + @NotNull + private String contentVet; + + @NotNull + private String contentJpn; + + @NotNull + private String contentChn; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "next_chat_id", unique = true) + private LessonChat nextChat; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "lesson_sentence_id", nullable = false, unique = true) + private LessonSentence lessonSentence; + + public String getTransByLang(LangType langType) { + return switch (langType) { + case VET -> contentVet; + case JPN -> contentJpn; + case CHN -> contentChn; + default -> contentEng; + }; + } +} diff --git a/src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonSentence.java b/src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonSentence.java new file mode 100644 index 0000000..6d770ae --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/domain/entity/LessonSentence.java @@ -0,0 +1,82 @@ +package com.edu.kobridge.module.lesson.domain.entity; + +import com.edu.kobridge.global.common.BaseTime; +import com.edu.kobridge.global.enums.LangType; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +public class LessonSentence extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private Integer number; + + @NotNull + private String contentKo; + + @NotNull + private String contentEng; + + @NotNull + private String contentVet; + + @NotNull + private String contentJpn; + + @NotNull + private String contentChn; + + @NotNull + private String pronunciationEng; + + @NotNull + private String pronunciationVet; + + @NotNull + private String pronunciationJpn; + + @NotNull + private String pronunciationChn; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "lesson_id", nullable = false) + private Lesson lesson; + + public String getPronunciationByLang(LangType langType) { + return switch (langType) { + case VET -> pronunciationVet; + case JPN -> pronunciationJpn; + case CHN -> pronunciationChn; + default -> pronunciationEng; + }; + } + + public String getTransByLang(LangType langType) { + return switch (langType) { + case VET -> contentVet; + case JPN -> contentJpn; + case CHN -> contentChn; + default -> contentEng; + }; + } +} From c69664d42ddb90203a3a1426c71fdf879946aea3 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:00:33 +0900 Subject: [PATCH 49/73] =?UTF-8?q?:sparkles:=20feat:=20Lesson,=20LessonSent?= =?UTF-8?q?ence,=20LessonChat=20Repository=20=EC=B6=94=EA=B0=80=20[KOBG-13?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/LessonChatRepository.java | 13 +++++++++++++ .../lesson/domain/repository/LessonRepository.java | 13 +++++++++++++ .../domain/repository/LessonSentenceRepository.java | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonChatRepository.java create mode 100644 src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonRepository.java create mode 100644 src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonSentenceRepository.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonChatRepository.java b/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonChatRepository.java new file mode 100644 index 0000000..3987e58 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonChatRepository.java @@ -0,0 +1,13 @@ +package com.edu.kobridge.module.lesson.domain.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.edu.kobridge.module.lesson.domain.entity.LessonChat; + +@Repository +public interface LessonChatRepository extends JpaRepository { + Optional findByLessonSentenceId(Long lessonSentenceId); +} diff --git a/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonRepository.java b/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonRepository.java new file mode 100644 index 0000000..7f864be --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonRepository.java @@ -0,0 +1,13 @@ +package com.edu.kobridge.module.lesson.domain.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.edu.kobridge.module.lesson.domain.entity.Lesson; + +@Repository +public interface LessonRepository extends JpaRepository { + public List findAllByOrderByNumberAsc(); +} diff --git a/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonSentenceRepository.java b/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonSentenceRepository.java new file mode 100644 index 0000000..4a210cd --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/domain/repository/LessonSentenceRepository.java @@ -0,0 +1,10 @@ +package com.edu.kobridge.module.lesson.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.edu.kobridge.module.lesson.domain.entity.LessonSentence; + +@Repository +public interface LessonSentenceRepository extends JpaRepository { +} From b1477f15adfd24189c367ac6ec1ec4d3d96e3fa9 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:01:57 +0900 Subject: [PATCH 50/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A0=88=EC=8A=A8?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KO?= =?UTF-8?q?BG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/lesson/dto/res/ChatResDto.java | 27 ++++++++++++++++ .../dto/res/LessonSentenceListResDto.java | 30 +++++++++++++++++ .../lesson/dto/res/LessonSentenceResDto.java | 32 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatResDto.java create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceListResDto.java create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceResDto.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatResDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatResDto.java new file mode 100644 index 0000000..1fda378 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatResDto.java @@ -0,0 +1,27 @@ +package com.edu.kobridge.module.lesson.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class ChatResDto { + @Schema(description = "chat id", example = "1") + private final Long id; + + @Schema(description = "질문", example = "숙제 있었는데, 너 다 했어?") + private final String question; + + @Schema(description = "질문 번역본", example = "There was homework, did you finish it?") + private final String questionTrans; + + public static ChatResDto of(Long id, String question, String questionTrans) { + return ChatResDto.builder() + .id(id) + .question(question) + .questionTrans(questionTrans) + .build(); + } +} diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceListResDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceListResDto.java new file mode 100644 index 0000000..8d417c1 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceListResDto.java @@ -0,0 +1,30 @@ +package com.edu.kobridge.module.lesson.dto.res; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LessonSentenceListResDto { + @Schema(description = "주제", example = "오늘은 학교에 가는 상황에서 자주 쓰는 표현들을 배워봐요. 교실에서 친구와 이야기할 때 유용하게 쓸 수 있어요.") + private final String subject; + + @Schema(description = "레슨 문장 리스트") + private final List lessonSentences; + + @Schema(description = "대화 시작 chat 정보") + private final ChatResDto startChat; + + public static LessonSentenceListResDto of(String subject, List lessonSentences, + ChatResDto chatRes) { + return LessonSentenceListResDto.builder() + .subject(subject) + .lessonSentences(lessonSentences) + .startChat(chatRes) + .build(); + } +} diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceResDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceResDto.java new file mode 100644 index 0000000..7481e7f --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonSentenceResDto.java @@ -0,0 +1,32 @@ +package com.edu.kobridge.module.lesson.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LessonSentenceResDto { + + @Schema(description = "문장 id", example = "1") + private final Long id; + + @Schema(description = "오늘의 문장", example = "이름이 뭐야?") + private final String sentence; + + @Schema(description = "문장 번역", example = "What's your name?") + private final String translation; + + @Schema(description = "문장 발음", example = "I-reum-i mwoya?") + private final String pronunciation; + + public static LessonSentenceResDto of(Long id, String sentence, String translation, String pronunciation) { + return LessonSentenceResDto.builder() + .id(id) + .sentence(sentence) + .translation(translation) + .pronunciation(pronunciation) + .build(); + } +} From 8dc0ce972a689457363605f0c79f0d90284e1228 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:02:44 +0900 Subject: [PATCH 51/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A0=88=EC=8A=A8?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=9D=84=20=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lesson/dto/res/LessonBriefResDto.java | 40 +++++++++++++++++++ .../lesson/dto/res/LessonListResDto.java | 25 ++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonBriefResDto.java create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonListResDto.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonBriefResDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonBriefResDto.java new file mode 100644 index 0000000..e2ec43e --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonBriefResDto.java @@ -0,0 +1,40 @@ +package com.edu.kobridge.module.lesson.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LessonBriefResDto { + @Schema(description = "레슨 id", example = "1") + private final Long id; + + @Schema(description = "레슨 순서", example = "1") + private final int number; + + @Schema(description = "레슨 제목", example = "") + private final String title; + + @Schema(description = "레슨 1단계 제목(따라하기)", example = "") + private final String subTitle1; + + @Schema(description = "레슨 2단계 제목(대화하기)", example = "") + private final String subTitle2; + + @Schema(description = "레슨 3단계 제목(평가하기)", example = "") + private final String subTitle3; + + public static LessonBriefResDto of(Long id, int number, String title, String subTitle1, String subTitle2, + String subTitle3) { + return LessonBriefResDto.builder() + .id(id) + .number(number) + .title(title) + .subTitle1(subTitle1) + .subTitle2(subTitle2) + .subTitle3(subTitle3) + .build(); + } +} diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonListResDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonListResDto.java new file mode 100644 index 0000000..b8dd220 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/res/LessonListResDto.java @@ -0,0 +1,25 @@ +package com.edu.kobridge.module.lesson.dto.res; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class LessonListResDto { + @Schema(description = "현재 레벨", example = "1") + private final int level; + + @Schema(description = "레슨 전체 리스트") + private final List lessons; + + public static LessonListResDto of(int level, List lessons) { + return LessonListResDto.builder() + .level(level) + .lessons(lessons) + .build(); + } +} From 261054db58ed9ce73570f01c275b436df06a60d9 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:08:41 +0900 Subject: [PATCH 52/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A0=88=EC=8A=A8?= =?UTF-8?q?=20=EB=8B=B5=EB=B3=80=20=EA=B5=90=EC=A0=95=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=9D=84=20=EB=B0=9B=EA=B8=B0=20=EC=9C=84=ED=95=9C=20dto=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/lesson/dto/req/ChatCorrectionReqDto.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/req/ChatCorrectionReqDto.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/req/ChatCorrectionReqDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/req/ChatCorrectionReqDto.java new file mode 100644 index 0000000..b6d8f3f --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/req/ChatCorrectionReqDto.java @@ -0,0 +1,12 @@ +package com.edu.kobridge.module.lesson.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "채팅 답변 교정 요청 DTO") +public record ChatCorrectionReqDto( + @Schema(description = "채팅 답변", example = "만나서 반가워!") + @NotBlank(message = "답변 값은 필수입니다.") + String answer +) { +} From c5eeaaccbff84894642475ca9ad7fffd085dd133 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:09:07 +0900 Subject: [PATCH 53/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A0=88=EC=8A=A8?= =?UTF-8?q?=20=EB=8B=B5=EB=B3=80=20=EA=B5=90=EC=A0=95=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KO?= =?UTF-8?q?BG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lesson/dto/res/ChatCorrectionResDto.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatCorrectionResDto.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatCorrectionResDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatCorrectionResDto.java new file mode 100644 index 0000000..f2d0ab3 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/res/ChatCorrectionResDto.java @@ -0,0 +1,36 @@ +package com.edu.kobridge.module.lesson.dto.res; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class ChatCorrectionResDto { + @Schema(description = "번역된 문장", example = "I know. When does class start?") + private final String answerTrans; + + @Schema(description = "교정된 문장", example = "그러게~ 수업 언제 시작해?") + private final String correction; + + @Schema(description = "교정된 이유", example = "은제’는 ‘언제’로 교정해야 해요. '시작햐'는 '시작해'로 수정해야 해요.") + private final String reason; + + @Schema(description = "답변에 대한 대답", example = "곧 시작할 것 같아~") + private final String response; + + @Schema(description = "다음 chat 정보") + private final ChatResDto nextChat; + + public static ChatCorrectionResDto of(String answerTrans, String correction, String reason, String response, + ChatResDto chatRes) { + return ChatCorrectionResDto.builder() + .answerTrans(answerTrans) + .correction(correction) + .reason(reason) + .response(response) + .nextChat(chatRes) + .build(); + } +} From 2d919e1aa17b51186a315d8607147213af638b54 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:10:51 +0900 Subject: [PATCH 54/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A0=88=EC=8A=A8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/lesson/error/LessonErrorCode.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/error/LessonErrorCode.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/error/LessonErrorCode.java b/src/main/java/com/edu/kobridge/module/lesson/error/LessonErrorCode.java new file mode 100644 index 0000000..e135de9 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/error/LessonErrorCode.java @@ -0,0 +1,23 @@ +package com.edu.kobridge.module.lesson.error; + +import org.springframework.http.HttpStatus; + +import com.edu.kobridge.global.error.ErrorCode; + +import lombok.Getter; + +@Getter +public enum LessonErrorCode implements ErrorCode { + + LESSON_CHAT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 질문을 찾을 수 없습니다."), + LESSON_SENTENCE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 레슨 문장을 찾을 수 없습니다."), + LESSON_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 레슨을 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String message; + + LessonErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } +} From e402d0b6b74cd3a32e1e3fa9aa5e81d686684962 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:11:20 +0900 Subject: [PATCH 55/73] =?UTF-8?q?:sparkles:=20feat:=20web=20client=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/WebClientConfig.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/config/WebClientConfig.java diff --git a/src/main/java/com/edu/kobridge/global/config/WebClientConfig.java b/src/main/java/com/edu/kobridge/global/config/WebClientConfig.java new file mode 100644 index 0000000..d3b1152 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/config/WebClientConfig.java @@ -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(); + } +} From b6a7a04dcf0907779486b9c5ab89788fde8cb7eb Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:12:01 +0900 Subject: [PATCH 56/73] =?UTF-8?q?:sparkles:=20feat:=20=EC=98=A4=EB=94=94?= =?UTF-8?q?=EC=98=A4=20=ED=8C=8C=EC=9D=BC=20base=2064=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=EC=9D=84=20=EC=9C=84=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20util=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edu/kobridge/global/util/FileUtil.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/global/util/FileUtil.java diff --git a/src/main/java/com/edu/kobridge/global/util/FileUtil.java b/src/main/java/com/edu/kobridge/global/util/FileUtil.java new file mode 100644 index 0000000..c645494 --- /dev/null +++ b/src/main/java/com/edu/kobridge/global/util/FileUtil.java @@ -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); + } + } +} From 00fcb6275bc8f6390a1d0c0c17b47d3402be45e6 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:15:51 +0900 Subject: [PATCH 57/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=B0=9C=EC=9D=8C?= =?UTF-8?q?=20=ED=8F=89=EA=B0=80=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KO?= =?UTF-8?q?BG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/req/PronunciationEvaluationReqDto.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/dto/req/PronunciationEvaluationReqDto.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/dto/req/PronunciationEvaluationReqDto.java b/src/main/java/com/edu/kobridge/module/lesson/dto/req/PronunciationEvaluationReqDto.java new file mode 100644 index 0000000..e37da53 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/dto/req/PronunciationEvaluationReqDto.java @@ -0,0 +1,12 @@ +package com.edu.kobridge.module.lesson.dto.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "발음 평가 요청 DTO") +public record PronunciationEvaluationReqDto( + @Schema(description = "오디오 url", example = "https://{buket-url}/pronumciation-evaluation-audio/{audio-url-name}.m4a") + @NotBlank(message = "오디오 url은 필수 값입니다.") + String audioUrl +) { +} From 1acae798da6923176946838d01335d65c940806e Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:16:43 +0900 Subject: [PATCH 58/73] =?UTF-8?q?:sparkles:=20feat:=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EB=B0=9C=EC=9D=8C=20=ED=8F=89=EA=B0=80=20api=20=EC=97=90=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=84=20=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/epretx/req/PronunciationReqDto.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/epretx/req/PronunciationReqDto.java diff --git a/src/main/java/com/edu/kobridge/infra/api/epretx/req/PronunciationReqDto.java b/src/main/java/com/edu/kobridge/infra/api/epretx/req/PronunciationReqDto.java new file mode 100644 index 0000000..eb585dd --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/epretx/req/PronunciationReqDto.java @@ -0,0 +1,21 @@ +package com.edu.kobridge.infra.api.epretx.req; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PronunciationReqDto { + private Argument argument; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Argument { + private String language_code; + private String script; + private String audio; + } +} From 79e4d0f942c2ce700173a403c56e0d88fc0a78f1 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:17:09 +0900 Subject: [PATCH 59/73] =?UTF-8?q?:sparkles:=20feat:=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EB=B0=9C=EC=9D=8C=20=ED=8F=89=EA=B0=80=20api=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=9D=84=20=EB=A7=A4=ED=95=91=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/epretx/res/PronunciationResDto.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/epretx/res/PronunciationResDto.java diff --git a/src/main/java/com/edu/kobridge/infra/api/epretx/res/PronunciationResDto.java b/src/main/java/com/edu/kobridge/infra/api/epretx/res/PronunciationResDto.java new file mode 100644 index 0000000..22c7ff0 --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/epretx/res/PronunciationResDto.java @@ -0,0 +1,17 @@ +package com.edu.kobridge.infra.api.epretx.res; + +import lombok.Data; + +@Data +public class PronunciationResDto { + private String request_id; + private int result; + private String return_type; + private ReturnObject return_object; + + @Data + public static class ReturnObject { + private String recognized; + private float score; + } +} From f4fa2ef4142e063d1e1c046a79167119902f2c1e Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:21:26 +0900 Subject: [PATCH 60/73] =?UTF-8?q?:sparkles:=20feat:=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EB=B0=9C=EC=9D=8C=20=ED=8F=89=EA=B0=80=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=8B=9C=20=EB=82=98=EC=98=AC=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../epretx/error/PronunciationErrorCode.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/epretx/error/PronunciationErrorCode.java diff --git a/src/main/java/com/edu/kobridge/infra/api/epretx/error/PronunciationErrorCode.java b/src/main/java/com/edu/kobridge/infra/api/epretx/error/PronunciationErrorCode.java new file mode 100644 index 0000000..350ad25 --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/epretx/error/PronunciationErrorCode.java @@ -0,0 +1,25 @@ +package com.edu.kobridge.infra.api.epretx.error; + +import org.springframework.http.HttpStatus; + +import com.edu.kobridge.global.error.ErrorCode; + +import lombok.Getter; + +@Getter +public enum PronunciationErrorCode implements ErrorCode { + PRONUNCIATION_INVALID_KEY(HttpStatus.FORBIDDEN, "API 키 문제 또는 접근 권한 없음"), + PRONUNCIATION_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "발음 평가 API 요청 시간 초과"), + PRONUNCIATION_LIMIT_EXCEEDED(HttpStatus.TOO_MANY_REQUESTS, "호출 제한 횟수 초과"), + PRONUNCIATION_BODY_TOO_LARGE(HttpStatus.PAYLOAD_TOO_LARGE, "전송된 오디오 데이터가 너무 큼"), + PRONUNCIATION_API_SERVER_ERROR(HttpStatus.BAD_GATEWAY, "외부 발음 평가 서버 오류"), + PRONUNCIATION_EVALUATION_API_FAILED(HttpStatus.SERVICE_UNAVAILABLE, "발음 평가 서비스 사용 불가"); + + private final HttpStatus httpStatus; + private final String message; + + PronunciationErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } +} From dd21e3335217b3814d898ad893d235772fe47d50 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:21:36 +0900 Subject: [PATCH 61/73] =?UTF-8?q?:sparkles:=20feat:=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EB=B0=9C=EC=9D=8C=20=ED=8F=89=EA=B0=80=20api=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD-=EC=9D=91=EB=8B=B5=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PronunciationEvaluationService.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/epretx/PronunciationEvaluationService.java diff --git a/src/main/java/com/edu/kobridge/infra/api/epretx/PronunciationEvaluationService.java b/src/main/java/com/edu/kobridge/infra/api/epretx/PronunciationEvaluationService.java new file mode 100644 index 0000000..877c334 --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/epretx/PronunciationEvaluationService.java @@ -0,0 +1,75 @@ +package com.edu.kobridge.infra.api.epretx; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import com.edu.kobridge.global.error.exception.AppException; +import com.edu.kobridge.infra.api.epretx.error.PronunciationErrorCode; +import com.edu.kobridge.infra.api.epretx.req.PronunciationReqDto; +import com.edu.kobridge.infra.api.epretx.res.PronunciationResDto; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +public class PronunciationEvaluationService { + + @Value("${api.e-pre-tx.uri}") + private String uri; + + @Value("${api.e-pre-tx.key}") + private String key; + + private final WebClient pronunciationWebClient; + + public PronunciationResDto checkPronunciation(String script, String audioBase64) { + PronunciationReqDto pronunciationReq = new PronunciationReqDto( + new PronunciationReqDto.Argument("korean", script, audioBase64) + ); + + try { + return pronunciationWebClient.post() + .uri(uri) + .header("Authorization", key) + .bodyValue(pronunciationReq) + .retrieve() + .bodyToMono(PronunciationResDto.class) + .block(); + + } catch (WebClientResponseException e) { + switch (e.getStatusCode().value()) { + case 403: // FORBIDDEN + log.error("[Pronunciation API] api key or else -- {}", e.getResponseBodyAsString()); + throw new AppException(PronunciationErrorCode.PRONUNCIATION_INVALID_KEY); + + case 408: // REQUEST_TIMEOUT + throw new AppException(PronunciationErrorCode.PRONUNCIATION_TIMEOUT); + + case 413: // PAYLOAD_TOO_LARGE + throw new AppException(PronunciationErrorCode.PRONUNCIATION_BODY_TOO_LARGE); + + case 429: // TOO_MANY_REQUESTS + throw new AppException(PronunciationErrorCode.PRONUNCIATION_LIMIT_EXCEEDED); + + default: + if (e.getStatusCode().is5xxServerError()) { + log.error("[Pronunciation API] external server error -- {}", e.getResponseBodyAsString()); + throw new AppException(PronunciationErrorCode.PRONUNCIATION_API_SERVER_ERROR); + } else { + log.error("[Pronunciation API] unexpected error -- {}", e.getResponseBodyAsString(), e); + throw new AppException(PronunciationErrorCode.PRONUNCIATION_EVALUATION_API_FAILED); + } + } + + } catch (Exception e) { + log.error("[Pronunciation API] internal error -- " + e.getMessage()); + throw new AppException(PronunciationErrorCode.PRONUNCIATION_EVALUATION_API_FAILED); + } + } +} \ No newline at end of file From 8c4422fdf2e2326736adb0d464f67120ef904419 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:22:13 +0900 Subject: [PATCH 62/73] =?UTF-8?q?:sparkles:=20feat:=20chat=20gpt=20api=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=84=20=EB=A7=A4=ED=95=91=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KO?= =?UTF-8?q?BG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatgpt/res/ChatGptCorrectionResDto.java | 15 ++++++ .../infra/api/chatgpt/res/ChatGptResDto.java | 53 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptCorrectionResDto.java create mode 100644 src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptResDto.java diff --git a/src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptCorrectionResDto.java b/src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptCorrectionResDto.java new file mode 100644 index 0000000..61073d2 --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptCorrectionResDto.java @@ -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; +} diff --git a/src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptResDto.java b/src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptResDto.java new file mode 100644 index 0000000..aa9a046 --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/chatgpt/res/ChatGptResDto.java @@ -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; + 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; + 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; + } +} \ No newline at end of file From c7e2bd97bacf4b9fc519b8ff591f976c2b308c5e Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:22:41 +0900 Subject: [PATCH 63/73] =?UTF-8?q?:sparkles:=20feat:=20chat=20gpt=20api?= =?UTF-8?q?=EC=97=90=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20dto=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/api/chatgpt/req/ChatGptReqDto.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/chatgpt/req/ChatGptReqDto.java diff --git a/src/main/java/com/edu/kobridge/infra/api/chatgpt/req/ChatGptReqDto.java b/src/main/java/com/edu/kobridge/infra/api/chatgpt/req/ChatGptReqDto.java new file mode 100644 index 0000000..de73931 --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/chatgpt/req/ChatGptReqDto.java @@ -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 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 messages) { + return ChatGptReqDto.builder() + .model(model) + .messages(messages) + .build(); + } +} From 15c9d7bbe3456f8737521f95af603a4ad05d1905 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:23:29 +0900 Subject: [PATCH 64/73] =?UTF-8?q?:sparkles:=20feat:=20chat=20gpt=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=8B=9C=20=EB=82=98=EC=98=AC=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/chatgpt/error/ChatGptErrorCode.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/chatgpt/error/ChatGptErrorCode.java diff --git a/src/main/java/com/edu/kobridge/infra/api/chatgpt/error/ChatGptErrorCode.java b/src/main/java/com/edu/kobridge/infra/api/chatgpt/error/ChatGptErrorCode.java new file mode 100644 index 0000000..1712cfa --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/chatgpt/error/ChatGptErrorCode.java @@ -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; + } +} From 428b3d0d2b039442b1b0d9307d14f396e0017ff9 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:28:57 +0900 Subject: [PATCH 65/73] =?UTF-8?q?:sparkles:=20feat:=20chat=20gpt=20api=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EA=B5=90=EC=A0=95=20=EC=9A=94=EC=B2=AD-?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/api/chatgpt/ChatGptService.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/infra/api/chatgpt/ChatGptService.java diff --git a/src/main/java/com/edu/kobridge/infra/api/chatgpt/ChatGptService.java b/src/main/java/com/edu/kobridge/infra/api/chatgpt/ChatGptService.java new file mode 100644 index 0000000..fc780b9 --- /dev/null +++ b/src/main/java/com/edu/kobridge/infra/api/chatgpt/ChatGptService.java @@ -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 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); + } + } +} \ No newline at end of file From 9d407a9b84c80ebb47570c9cb5ecc04f542e1cd4 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:29:42 +0900 Subject: [PATCH 66/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A0=88=EC=8A=A8?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C,=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C,=20=EB=B0=9C=EC=9D=8C=20?= =?UTF-8?q?=ED=8F=89=EA=B0=80,=20=EB=8B=B5=EB=B3=80=20=EA=B5=90=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/lesson/service/LessonService.java | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/service/LessonService.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/service/LessonService.java b/src/main/java/com/edu/kobridge/module/lesson/service/LessonService.java new file mode 100644 index 0000000..8d87364 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/service/LessonService.java @@ -0,0 +1,137 @@ +package com.edu.kobridge.module.lesson.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.edu.kobridge.global.enums.LangType; +import com.edu.kobridge.global.error.exception.AppException; +import com.edu.kobridge.global.util.FileUtil; +import com.edu.kobridge.infra.api.chatgpt.ChatGptService; +import com.edu.kobridge.infra.api.chatgpt.res.ChatGptCorrectionResDto; +import com.edu.kobridge.infra.api.epretx.PronunciationEvaluationService; +import com.edu.kobridge.module.lesson.domain.entity.Lesson; +import com.edu.kobridge.module.lesson.domain.entity.LessonChat; +import com.edu.kobridge.module.lesson.domain.entity.LessonSentence; +import com.edu.kobridge.module.lesson.domain.repository.LessonChatRepository; +import com.edu.kobridge.module.lesson.domain.repository.LessonRepository; +import com.edu.kobridge.module.lesson.domain.repository.LessonSentenceRepository; +import com.edu.kobridge.module.lesson.dto.req.ChatCorrectionReqDto; +import com.edu.kobridge.module.lesson.dto.req.PronunciationEvaluationReqDto; +import com.edu.kobridge.module.lesson.dto.res.ChatCorrectionResDto; +import com.edu.kobridge.module.lesson.dto.res.ChatResDto; +import com.edu.kobridge.module.lesson.dto.res.LessonBriefResDto; +import com.edu.kobridge.module.lesson.dto.res.LessonListResDto; +import com.edu.kobridge.module.lesson.dto.res.LessonSentenceListResDto; +import com.edu.kobridge.module.lesson.dto.res.LessonSentenceResDto; +import com.edu.kobridge.module.lesson.error.LessonErrorCode; +import com.edu.kobridge.module.user.domain.entity.User; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Slf4j +public class LessonService { + private final LessonRepository lessonRepository; + private final LessonSentenceRepository lessonSentenceRepository; + private final LessonChatRepository lessonChatRepository; + private final PronunciationEvaluationService pronunciationEvaluationService; + private final ChatGptService chatGptService; + private final FileUtil fileUtil; + + // 레슨 상세 조회 + public LessonSentenceListResDto getLessonDetail(User user, Long id) { + // lesson 번호 유효성 검증 + Lesson lesson = lessonRepository.findById(id) + .orElseThrow(() -> new AppException(LessonErrorCode.LESSON_NOT_FOUND)); + + // lesson sentence list 변환하여 dto 에 담기 + List lessonSentences = lesson.getSentences().stream() + .map(sentence -> LessonSentenceResDto.of( + sentence.getId(), + sentence.getContentKo(), + sentence.getTransByLang(user.getLang()), + sentence.getPronunciationByLang(user.getLang()) + )) + .toList(); + + // lesson sentence 첫번째로 발화시킬 lesson chat 가져오기 + LessonChat lessonChat = lessonChatRepository.findByLessonSentenceId(lessonSentences.get(0).getId()) + .orElseThrow(() -> new AppException(LessonErrorCode.LESSON_CHAT_NOT_FOUND)); + + return LessonSentenceListResDto.of(lesson.getSubject(), lessonSentences, + ChatResDto.of(lessonChat.getId(), lessonChat.getContentKo(), lessonChat.getTransByLang(user.getLang()))); + } + + // 레슨 전체 간단 조회 + public LessonListResDto getLessonList(User user) { + // TODO entity 변경에 따라 trans 부분 추가로 매핑해서 전달하기 + + return LessonListResDto.of( + user.getLevel(), + lessonRepository.findAllByOrderByNumberAsc().stream() + .map(lesson -> LessonBriefResDto.of( + lesson.getId(), + lesson.getNumber(), + lesson.getTitle(), + lesson.getSubTitle1(), + lesson.getSubTitle2(), + lesson.getSubTitle3() + )) + .toList() + ); + } + + // 발음 평가 + public Integer postPronunciationEvaluationValue(Long id, PronunciationEvaluationReqDto pronunciationEvaluationReq) { + // sentence id 검증 + LessonSentence lessonSentence = lessonSentenceRepository.findById(id) + .orElseThrow(() -> new AppException(LessonErrorCode.LESSON_SENTENCE_NOT_FOUND)); + + // audio url 을 base 64 값으로 전환 + String audioBase64Url = fileUtil.convertS3UrlToBase64(pronunciationEvaluationReq.audioUrl()); + log.error(audioBase64Url); + + // 외부 api 호출 + float score = pronunciationEvaluationService + .checkPronunciation(lessonSentence.getContentKo(), audioBase64Url) + .getReturn_object() + .getScore(); + + // 소수점 첫째 자리에서 반올림 → 정수 + return Math.round(score); + } + + // 대화 답변 교정 및 전달 + public ChatCorrectionResDto postChatCorrection(Long id, ChatCorrectionReqDto chatCorrectionReq) { + // chat id 검증 + LessonChat lessonChat = lessonChatRepository.findById(id) + .orElseThrow(() -> new AppException(LessonErrorCode.LESSON_CHAT_NOT_FOUND)); + + // 다음 채팅 정보 가져오기 + LessonChat nextChat = lessonChat.getNextChat(); + Boolean isNextChatExist = nextChat != null; + + // TODO : user 받아와서 Lang 매핑 + + // chat gpt 호출 및 응답 확인 + ChatGptCorrectionResDto chatGptCorrectionRes = chatGptService.postAnswerCorrectionAndResponse( + chatCorrectionReq.answer(), isNextChatExist, LangType.ENG); + + // 결과 반환 + return ChatCorrectionResDto.of( + chatGptCorrectionRes.getTranslation(), + chatGptCorrectionRes.getCorrection(), + chatGptCorrectionRes.getReason(), + chatGptCorrectionRes.getResponse(), + nextChat == null ? null : + ChatResDto.of(nextChat.getId(), nextChat.getContentKo(), + nextChat.getTransByLang(LangType.ENG)) + ); + } + +} From 73e399491be2ca92ab4db50900fe201bc93258df Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:29:57 +0900 Subject: [PATCH 67/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=A0=88=EC=8A=A8?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C,=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C,=20=EB=B0=9C=EC=9D=8C=20?= =?UTF-8?q?=ED=8F=89=EA=B0=80,=20=EB=8B=B5=EB=B3=80=20=EA=B5=90=EC=A0=95?= =?UTF-8?q?=20api=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lesson/controller/LessonController.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/controller/LessonController.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/controller/LessonController.java b/src/main/java/com/edu/kobridge/module/lesson/controller/LessonController.java new file mode 100644 index 0000000..1b0787e --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/controller/LessonController.java @@ -0,0 +1,58 @@ +package com.edu.kobridge.module.lesson.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.edu.kobridge.global.common.DataResponseDto; +import com.edu.kobridge.global.common.ResponseDto; +import com.edu.kobridge.module.lesson.dto.req.ChatCorrectionReqDto; +import com.edu.kobridge.module.lesson.dto.req.PronunciationEvaluationReqDto; +import com.edu.kobridge.module.lesson.dto.res.ChatCorrectionResDto; +import com.edu.kobridge.module.lesson.dto.res.LessonListResDto; +import com.edu.kobridge.module.lesson.dto.res.LessonSentenceListResDto; +import com.edu.kobridge.module.lesson.service.LessonService; +import com.edu.kobridge.module.user.domain.entity.User; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequestMapping("/api/lesson") +@RequiredArgsConstructor +@Slf4j +public class LessonController implements LessonControllerDocs { + private final LessonService lessonService; + + @GetMapping("/{id}") + public ResponseEntity getLessonDetail(@RequestAttribute("user") User user, + @PathVariable("id") Long id) { + LessonSentenceListResDto resDto = lessonService.getLessonDetail(user, id); + return ResponseEntity.status(200).body(DataResponseDto.of(resDto, 200)); + } + + @GetMapping + public ResponseEntity getLessonList(@RequestAttribute("user") User user) { + LessonListResDto resDto = lessonService.getLessonList(user); + return ResponseEntity.status(200).body(DataResponseDto.of(resDto, 200)); + } + + @PostMapping("/sentence/{id}/pronunciation-evaluation") + public ResponseEntity postPronunciationEvaluationValue(@PathVariable("id") Long id, + @RequestBody PronunciationEvaluationReqDto pronunciationEvaluationReq) { + Integer res = lessonService.postPronunciationEvaluationValue(id, pronunciationEvaluationReq); + return ResponseEntity.status(200).body(DataResponseDto.of(res, 200)); + } + + @PostMapping("/chat/{id}/correction") + public ResponseEntity postChatCorrection(@PathVariable("id") Long id, + @RequestBody ChatCorrectionReqDto chatCorrectionReq) { + ChatCorrectionResDto resDto = lessonService.postChatCorrection(id, chatCorrectionReq); + return ResponseEntity.status(200).body(DataResponseDto.of(resDto, 200)); + } +} From 05e574c7866f36cfd5abc0ff1e98f4769ed1736a Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:30:19 +0900 Subject: [PATCH 68/73] =?UTF-8?q?:memo:=20docs:=20=EB=A0=88=EC=8A=A8=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C,=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C,=20=EB=B0=9C=EC=9D=8C=20=ED=8F=89?= =?UTF-8?q?=EA=B0=80,=20=EB=8B=B5=EB=B3=80=20=EA=B5=90=EC=A0=95=20api=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/LessonControllerDocs.java | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/main/java/com/edu/kobridge/module/lesson/controller/LessonControllerDocs.java diff --git a/src/main/java/com/edu/kobridge/module/lesson/controller/LessonControllerDocs.java b/src/main/java/com/edu/kobridge/module/lesson/controller/LessonControllerDocs.java new file mode 100644 index 0000000..f8fd529 --- /dev/null +++ b/src/main/java/com/edu/kobridge/module/lesson/controller/LessonControllerDocs.java @@ -0,0 +1,184 @@ +package com.edu.kobridge.module.lesson.controller; + +import org.springframework.http.ResponseEntity; + +import com.edu.kobridge.global.common.ResponseDto; +import com.edu.kobridge.module.lesson.dto.req.ChatCorrectionReqDto; +import com.edu.kobridge.module.lesson.dto.req.PronunciationEvaluationReqDto; +import com.edu.kobridge.module.user.domain.entity.User; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Lesson", description = "학습 관련 API") +public interface LessonControllerDocs { + + @Operation(summary = "레슨 상세 조회", description = "레슨 별 문장과 주제를 확인할 수 있습니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{\n" + + " \"code\": 200,\n" + + " \"message\": \"OK\",\n" + + " \"data\": {\n" + + " \"subject\": \"오늘은 학교에 가는 상황에서 자주 쓰는 표현들을 배워봐요. 교실에서 친구와 이야기할 때 유용하게 쓸 수 있어요.\",\n" + + " \"lessonSentences\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"sentence\": \"우리 1교시 뭐야?\",\n" + + " \"translation\": \"What is our first class?\",\n" + + " \"pronunciation\": \"Uri il-gyosi mwoya?\"\n" + + " },\n" + + " {\n" + + " \"id\": 2,\n" + + " \"sentence\": \"수업 언제 시작해?\",\n" + + " \"translation\": \"When does the class start?\",\n" + + " \"pronunciation\": \"Sueop eonje sijakhae?\"\n" + + " },\n" + + " {\n" + + " \"id\": 3,\n" + + " \"sentence\": \"오늘 숙제 다 했어?\",\n" + + " \"translation\": \"Did you finish today’s homework?\",\n" + + " \"pronunciation\": \"Oneul sukjje da haesseo?\"\n" + + " }\n" + + " ],\n" + + " \"startChat\": {\n" + + " \"id\": 3,\n" + + " \"question\": \"안녕~ 오늘 첫 수업 뭐였지?\",\n" + + " \"questionTrans\": \"Hi~ What was the first class today?\"\n" + + " }\n" + + " }\n" + + "}") + ) + ), + @ApiResponse(responseCode = "404", description = "해당 자원을 찾을 수 없습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "[" + + "{ \"code\": 401, \"message\": \"해당하는 레슨을 찾을 수 없습니다.\" }," + + "{ \"code\": 404, \"message\": \"해당하는 질문을 찾을 수 없습니다.\" }" + + "]" + ) + ) + ) + }) + public ResponseEntity getLessonDetail(User user, Long id); + + @Operation(summary = "레슨 전체 조회", description = "전체 레슨 리스트를 확인할 수있습니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{\n" + + " \"code\": 200,\n" + + " \"message\": \"OK\",\n" + + " \"data\": {\n" + + " \"level\": 1,\n" + + " \"lessons\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"number\": 1,\n" + + " \"title\": \"학교에 가자!\",\n" + + " \"subTitle1\": \"사용할 문장 알아볼까?\",\n" + + " \"subTitle2\": \"같이 대화해볼까?\",\n" + + " \"subTitle3\": \"오늘의 레슨은 어땠어?\"\n" + + " },\n" + + " {\n" + + " \"id\": 2,\n" + + " \"number\": 2,\n" + + " \"title\": \"친구랑 인사하기\",\n" + + " \"subTitle1\": \"친구랑 어떻게 말할까?\",\n" + + " \"subTitle2\": \"인사 연습해보자!\",\n" + + " \"subTitle3\": \"오늘 배운 인사, 어땠어?\"\n" + + " }, ...\n" + + " \n" + + " {\n" + + " \"id\": 15,\n" + + " \"number\": 15,\n" + + " \"title\": \"미래 꿈 이야기\",\n" + + " \"subTitle1\": \"꿈을 어떻게 말할까?\",\n" + + " \"subTitle2\": \"내 꿈 얘기해보자!\",\n" + + " \"subTitle3\": \"꿈 이야기 표현은 쉬웠어?\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}") + ) + ) + }) + public ResponseEntity getLessonList(User user); + + @Operation(summary = "발음 평가", description = "발음에 대한 별점을 확인할 수 있습니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{\n" + + " \"code\": 200,\n" + + " \"message\": \"OK\",\n" + + " \"data\": 3\n" + + "}") + ) + ), + @ApiResponse(responseCode = "404", description = "해당 자원을 찾을 수 없습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 404, \"message\": \"해당하는 레슨 문장을 찾을 수 없습니다.\" }" + ) + ) + ) + }) + public ResponseEntity postPronunciationEvaluationValue(Long id, + PronunciationEvaluationReqDto audioUrl); + + @Operation(summary = "대화 답변 분석 및 다음 질문 확인", description = "사용자 답변의 번역, 교정본, 이유 및 다음 답변의 질문, 번역본 등을 제공한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = + @ExampleObject(value = "{\n" + + " \"code\": 200,\n" + + " \"message\": \"OK\",\n" + + " \"data\": {\n" + + " \"answerTrans\": \"So, what is the first class today?\",\n" + + " \"correction\": \"그러게 오늘 1교시가 뭐지?\",\n" + + " \"reason\": \"‘워지’는 맞지 않아서 ‘뭐지’로 수정했어요. 그리고 1굣은 1교시로 바꿔야 해요.\",\n" + + " \"response\": \"응, 오늘 1교시 수업 국어야!\",\n" + + " \"nextChat\": {\n" + + " \"id\": 2,\n" + + " \"question\": \"우리 쉬는 시간 거의 끝났다..\",\n" + + " \"questionTrans\": \"Our break is almost over..\"\n" + + " }\n" + + " }\n" + + "}") + ) + ), + @ApiResponse(responseCode = "404", description = "해당 자원을 찾을 수 없습니다.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ResponseDto.class), + examples = @ExampleObject(value = "{ \"code\": 404, \"message\": \"해당하는 질문을 찾을 수 없습니다.\" }" + ) + ) + ) + }) + public ResponseEntity postChatCorrection(Long id, ChatCorrectionReqDto chatCorrectionReq); + +} + From 2f5e68e845cf53f4cc8d42c339879e73fed0c5c2 Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:31:08 +0900 Subject: [PATCH 69/73] =?UTF-8?q?:sparkles:=20feat:=20=EB=8B=B5=EB=B3=80?= =?UTF-8?q?=20=EC=B2=B4=ED=97=98=EC=9D=84=20=EC=9C=84=ED=95=B4=20user=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=86=EC=9D=B4=EB=8F=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20jwt=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=A0=9C=EC=99=B8=20path=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/common/filter/JwtAuthorizationFilter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java index 1a2c204..d900e3e 100644 --- a/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java +++ b/src/main/java/com/edu/kobridge/global/common/filter/JwtAuthorizationFilter.java @@ -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 { @@ -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; From 7344fc67a43166736c124cba797ad5dfd1c3448c Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 10:50:20 +0900 Subject: [PATCH 70/73] =?UTF-8?q?:bug:=20fix:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=A4=91=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20use?= =?UTF-8?q?r=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 75 ----- .../user/controller/UserControllerDocs.java | 269 ------------------ .../edu/kobridge/user/domain/entity/User.java | 82 ------ .../domain/repository/UserRepository.java | 13 - .../kobridge/user/dto/req/SignUpReqDto.java | 43 --- .../kobridge/user/dto/res/LoginResDto.java | 27 -- .../edu/kobridge/user/dto/res/UserResDto.java | 36 --- .../kobridge/user/error/UserErrorCode.java | 22 -- .../kobridge/user/service/UserService.java | 127 --------- 9 files changed, 694 deletions(-) delete mode 100644 src/main/java/com/edu/kobridge/user/controller/UserController.java delete mode 100644 src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java delete mode 100644 src/main/java/com/edu/kobridge/user/domain/entity/User.java delete mode 100644 src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java delete mode 100644 src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java delete mode 100644 src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java delete mode 100644 src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java delete mode 100644 src/main/java/com/edu/kobridge/user/error/UserErrorCode.java delete mode 100644 src/main/java/com/edu/kobridge/user/service/UserService.java diff --git a/src/main/java/com/edu/kobridge/user/controller/UserController.java b/src/main/java/com/edu/kobridge/user/controller/UserController.java deleted file mode 100644 index 7cc6348..0000000 --- a/src/main/java/com/edu/kobridge/user/controller/UserController.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.edu.kobridge.user.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestAttribute; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.edu.kobridge.global.common.DataResponseDto; -import com.edu.kobridge.global.common.ResponseDto; -import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.user.domain.entity.User; -import com.edu.kobridge.user.dto.req.SignUpReqDto; -import com.edu.kobridge.user.dto.res.LoginResDto; -import com.edu.kobridge.user.service.UserService; - -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RestController -@RequestMapping("/api/user") -@RequiredArgsConstructor -@Slf4j -public class UserController implements UserControllerDocs { - private final UserService userService; - - @GetMapping("/google-login") - public ResponseEntity getGoogleLogin(@RequestHeader("id-token") String idToken) { - LoginResDto resDto = userService.getGoogleLogin(idToken); - return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); - } - - @GetMapping("/token") - public ResponseEntity getAccessToken(@RequestHeader("Authorization-Refresh") String refreshToken) { - LoginResDto resDto = userService.getAccessToken(refreshToken); - return ResponseEntity.status(201).body(DataResponseDto.of(resDto, 201)); - } - - @PostMapping - public ResponseEntity postSignUp(@RequestAttribute("user") User user, - @RequestBody @Valid SignUpReqDto signUpReq) { - userService.postSignUp(user, signUpReq); - return ResponseEntity.ok(ResponseDto.of(200)); - } - - @GetMapping - public ResponseEntity getUserInfo(@RequestAttribute("user") User user) { - return ResponseEntity.status(200).body(DataResponseDto.of(userService.getUserInfo(user), 200)); - } - - @PatchMapping("/lang") - public ResponseEntity patchLang(@RequestAttribute("user") User user, LangType lang) { - userService.updateLang(user, lang); - return ResponseEntity.ok(ResponseDto.of(200)); - } - - @PatchMapping("level") - public ResponseEntity patchLevel(@RequestAttribute("user") User user) { - userService.updateLevel(user); - return ResponseEntity.ok(ResponseDto.of(200)); - } - - @DeleteMapping("/google-logout") - public ResponseEntity deleteGoogleLogout(@RequestAttribute("user") User user) { - userService.deleteGoogleLogout(user); - return ResponseEntity.ok(ResponseDto.of(200)); - } - -} diff --git a/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java b/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java deleted file mode 100644 index a96ff63..0000000 --- a/src/main/java/com/edu/kobridge/user/controller/UserControllerDocs.java +++ /dev/null @@ -1,269 +0,0 @@ -package com.edu.kobridge.user.controller; - -import org.springframework.http.ResponseEntity; - -import com.edu.kobridge.global.common.ResponseDto; -import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.user.domain.entity.User; -import com.edu.kobridge.user.dto.req.SignUpReqDto; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Tag(name = "User", description = "사용자 관련 API") -public interface UserControllerDocs { - - @Operation(summary = "로그인", description = "구글 로그인을 진행합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "Created", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = - @ExampleObject(value = "{ \"code\": 201, \"message\": \"Created\" }") - ) - ), - @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "{ \"code\": 400, \"message\": \"Id Token이 필요합니다.\" }," - ) - ) - ), - @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + - "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + - "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }," + - "{ \"code\": 404, \"message\": \"해당하는 사용자를 찾을 수 없습니다.\" }" + - "]" - ) - ) - ), - @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") - ) - ), - @ApiResponse(responseCode = "404", description = "해당 자원을 찾을 수 없습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "{ \"code\": 404, \"message\": \"해당하는 사용자를 찾을 수 없습니다.\" }") - ) - ) - }) - ResponseEntity getGoogleLogin(String idToken); - - @Operation(summary = "accessToken 발급", description = "리프레시 토큰을 이용해 엑세스 토큰을 발급합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "Created", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = - @ExampleObject(value = "{ \"code\": 201, \"message\": \"Created\" }") - ) - ), - @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + - "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + - "{ \"code\": 401, \"message\": \"Refresh Token이 필요합니다.\" }" + - "]" - ) - ) - ), - @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") - ) - ) - }) - ResponseEntity getAccessToken(String refreshToken); - - @Operation(summary = "회원가입", description = "사용자 회원가입을 진행합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Ok", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = - @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") - ) - ), - @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 400, \"message\": \"이름은 필수 값입니다.\" }, " + - "{ \"code\": 400, \"message\": \"이름은 2자이상 50자 미만이어야 합니다.\" }," + - "{ \"code\": 400, \"message\": \"나이는 1 이상이어야 합니다.\" }," + - "{ \"code\": 400, \"message\": \"나이는 100 이하여야 합니다.\" }," + - "{ \"code\": 400, \"message\": \"역할은 필수 값입니다.\" }," + - "{ \"code\": 400, \"message\": \"학교는 필수 값입니다.\" }," + - "{ \"code\": 400, \"message\": \"학년은 1 이상이어야 합니다.\" }," + - "{ \"code\": 400, \"message\": \"학년은 6 이하여야 합니다.\" }" + - "{ \"code\": 400, \"message\": \"중학생, 고등학생은 3학년까지만 존재합니다.\" }" + - "{ \"code\": 400, \"message\": \"음성은 필수 값입니다.\" }" + - "]" - ) - ) - ), - @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + - "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + - "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + - "]" - ) - ) - ), - @ApiResponse(responseCode = "403", description = "접근이 허용되지 않습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "{ \"code\": 403, \"message\": \"Token이 만료되었습니다.\" }") - ) - ) - }) - ResponseEntity postSignUp(User user, SignUpReqDto signUpReq); - - @Operation(summary = "사용자 정보 확인", description = "사용자의 이름과 번역 언어, 레벨을 확인합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Ok", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = - @ExampleObject(value = "{\n" - + " \"code\": 200,\n" - + " \"message\": \"OK\",\n" - + " \"data\": {\n" - + " \"name\": \"김철수\",\n" - + " \"lang\": \"ENG\",\n" - + " \"level\": 1\n" - + " }\n" - + "}") - ) - ), - @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + - "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + - "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + - "]" - ) - ) - ) - }) - ResponseEntity getUserInfo(User user); - - @Operation(summary = "번역 언어 변경", description = "사용하는 번역 언어를 변경합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Ok", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = - @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") - ) - ), - @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "{ \"code\": 400, \"message\": \"LANG_TYPE 의 값이 아닙니다.\" }" - ) - ) - ), - @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + - "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + - "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + - "]" - ) - ) - ) - }) - ResponseEntity patchLang(User user, LangType lang); - - @Operation(summary = "레벨 완료", description = "레벨을 완료하여 1단게 높아집니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Ok", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = - @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") - ) - ), - @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + - "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + - "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + - "]" - ) - ) - ) - }) - ResponseEntity patchLevel(User user); - - @Operation(summary = "로그아웃", description = "로그아웃을 진행합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Ok", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = - @ExampleObject(value = "{ \"code\": 200, \"message\": \"Ok\" }") - ) - ), - @ApiResponse(responseCode = "401", description = "인증에 실패하였습니다.", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ResponseDto.class), - examples = @ExampleObject(value = "[" + - "{ \"code\": 401, \"message\": \"인증에 실패하였습니다.\" }," + - "{ \"code\": 401, \"message\": \"Token이 유효하지 않습니다.\" }," + - "{ \"code\": 401, \"message\": \"Access Token이 필요합니다.\" }" + - "]" - ) - ) - ) - }) - ResponseEntity deleteGoogleLogout(User user); - -} - diff --git a/src/main/java/com/edu/kobridge/user/domain/entity/User.java b/src/main/java/com/edu/kobridge/user/domain/entity/User.java deleted file mode 100644 index 8f594ba..0000000 --- a/src/main/java/com/edu/kobridge/user/domain/entity/User.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.edu.kobridge.user.domain.entity; - -import com.edu.kobridge.global.common.BaseTime; -import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.global.enums.SchoolType; -import com.edu.kobridge.global.enums.UserRoleType; -import com.edu.kobridge.global.enums.VoiceType; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@Builder(access = AccessLevel.PRIVATE) -public class User extends BaseTime { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @NotNull - @Column(unique = true) - private String email; - - private String name; - - @Enumerated(EnumType.STRING) - private LangType lang; - - @Enumerated(EnumType.STRING) - private SchoolType school; - - private byte grade; - - @Enumerated(EnumType.STRING) - private VoiceType voice; - - private int level; - - @NotNull - @Enumerated(EnumType.STRING) - private UserRoleType role; - - public static User of(@NotNull String email) { - return User - .builder() - .email(email) - .role(UserRoleType.USER) - .build(); - } - - public void updateInfo(@NotNull String name, @NotNull LangType lang, @NotNull SchoolType school, - @NotNull byte grade, @NotNull VoiceType voice) { - this.name = name; - this.lang = lang; - this.school = school; - this.grade = grade; - this.voice = voice; - this.level = 1; - } - - public void updateLang(@NotNull LangType lang) { - this.lang = lang; - } - - public void updateLevel(@NotNull int level) { - this.level = level; - } -} diff --git a/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java b/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java deleted file mode 100644 index c03bda2..0000000 --- a/src/main/java/com/edu/kobridge/user/domain/repository/UserRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.edu.kobridge.user.domain.repository; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.edu.kobridge.user.domain.entity.User; - -@Repository -public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java b/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java deleted file mode 100644 index 8569fa4..0000000 --- a/src/main/java/com/edu/kobridge/user/dto/req/SignUpReqDto.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.edu.kobridge.user.dto.req; - -import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.global.enums.SchoolType; -import com.edu.kobridge.global.enums.VoiceType; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -@Schema(description = "사용자 가입 요청 DTO") -public record SignUpReqDto( - @Schema(description = "이름", example = "김철수") - @NotBlank(message = "이름은 필수 값입니다.") - @Size(min = 2, max = 50, message = "이름은 2자이상 50자 미만이어야 합니다.") - String name, - - @Schema(description = "나이", example = "23") - @Min(value = 1, message = "나이는 1 이상이어야 합니다.") - @Max(value = 100, message = "나이는 100 이하여야 합니다.") - int age, - - @Schema(description = "언어", example = "ENG , VET, CHN, JPN, NONE") - @NotNull(message = "역할은 필수 값입니다.") - LangType lang, - - @Schema(description = "학교", example = "ELEMENTARY , MIDDLE, HIGH") - @NotNull(message = "학교는 필수 값입니다.") - SchoolType school, - - @Schema(description = "학년", example = "3") - @Min(value = 1, message = "학년은 1 이상이어야 합니다.") - @Max(value = 6, message = "학년은 6 이하여야 합니다.") - byte grade, - - @Schema(description = "음성", example = "BASIC, CUSTOM") - @NotNull(message = "음성은 필수 값입니다.") - VoiceType voice -) { -} diff --git a/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java b/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java deleted file mode 100644 index e059245..0000000 --- a/src/main/java/com/edu/kobridge/user/dto/res/LoginResDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.edu.kobridge.user.dto.res; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder(access = AccessLevel.PRIVATE) -public class LoginResDto { - @Schema(description = "accessToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") - private final String accessToken; - - @Schema(description = "refreshToken", example = "eyJ0eXAiOiJKV1QiLCJhbGc...") - private final String refreshToken; - - @Schema(description = "isFirstLogin : 사용자가 회원가입한 적이 없다면 false 반환 / 사용자가 회원가입한 적이 있다면 true 반환", example = "false") - private final boolean isFirstLogin; - - public static LoginResDto of(String accessToken, String refreshToken, boolean isFirstLogin) { - return LoginResDto.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .isFirstLogin(isFirstLogin) - .build(); - } -} diff --git a/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java b/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java deleted file mode 100644 index ce76ee0..0000000 --- a/src/main/java/com/edu/kobridge/user/dto/res/UserResDto.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.edu.kobridge.user.dto.res; - -import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.global.enums.VoiceType; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; - -@Schema(description = "사용자 간단 정보 DTO") -@Getter -@Builder(access = AccessLevel.PRIVATE) -public class UserResDto { - - @Schema(description = "이름", example = "김철수") - private final String name; - - @Schema(description = "번역 언어", example = "ENG") - private final LangType lang; - - @Schema(description = "선택한 Voice", example = "ONE") - private final VoiceType voice; - - @Schema(description = "현재 level", example = "3") - private final int level; - - public static UserResDto of(String name, LangType lang, VoiceType voice, int level) { - return UserResDto.builder() - .name(name) - .lang(lang) - .voice(voice) - .level(level) - .build(); - } -} diff --git a/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java b/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java deleted file mode 100644 index 12b37e2..0000000 --- a/src/main/java/com/edu/kobridge/user/error/UserErrorCode.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.edu.kobridge.user.error; - -import org.springframework.http.HttpStatus; - -import com.edu.kobridge.global.error.ErrorCode; - -import lombok.Getter; - -@Getter -public enum UserErrorCode implements ErrorCode { - ID_TOKEN_REQUIRED(HttpStatus.BAD_REQUEST, "Id Token이 필요합니다."), - INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, "유효한 Id Token이 아닙니다."), - GRADE_SIZE_ERROR(HttpStatus.BAD_REQUEST, "중학생, 고등학생은 3학년까지만 존재합니다."); - - private final HttpStatus httpStatus; - private final String message; - - UserErrorCode(HttpStatus httpStatus, String message) { - this.httpStatus = httpStatus; - this.message = message; - } -} diff --git a/src/main/java/com/edu/kobridge/user/service/UserService.java b/src/main/java/com/edu/kobridge/user/service/UserService.java deleted file mode 100644 index 9ff5e85..0000000 --- a/src/main/java/com/edu/kobridge/user/service/UserService.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.edu.kobridge.user.service; - -import java.io.IOException; -import java.security.GeneralSecurityException; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.edu.kobridge.global.enums.JwtVo; -import com.edu.kobridge.global.enums.LangType; -import com.edu.kobridge.global.enums.SchoolType; -import com.edu.kobridge.global.error.ErrorCode; -import com.edu.kobridge.global.error.GlobalErrorCode; -import com.edu.kobridge.global.error.exception.AppException; -import com.edu.kobridge.global.util.GoogleOAuthUtil; -import com.edu.kobridge.global.util.JwtUtil; -import com.edu.kobridge.global.util.RedisUtil; -import com.edu.kobridge.user.domain.entity.User; -import com.edu.kobridge.user.domain.repository.UserRepository; -import com.edu.kobridge.user.dto.req.SignUpReqDto; -import com.edu.kobridge.user.dto.res.LoginResDto; -import com.edu.kobridge.user.dto.res.UserResDto; -import com.edu.kobridge.user.error.UserErrorCode; - -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Service -@Transactional(readOnly = true) -@RequiredArgsConstructor -@Slf4j -public class UserService { - private final JwtUtil jwtUtil; - private final RedisUtil redisUtil; - private final GoogleOAuthUtil googleOAuthUtil; - private final UserRepository userRepository; - - @Transactional - public LoginResDto getGoogleLogin(String idToken) { - // 요청 인자 유효성 검사 - if (idToken.isBlank()) { - throw new AppException(UserErrorCode.ID_TOKEN_REQUIRED); - } - - // 구글 인증 진행 - User googleUser = null; - try { - googleUser = googleOAuthUtil.authenticate(idToken); - } catch (GeneralSecurityException | IOException e) { - throw new AppException(UserErrorCode.INVALID_ID_TOKEN); - } - - // 유저 찾고, 없다면 새 유저 생성 - User finalGoogleUser = googleUser; - User user = userRepository.findByEmail(googleUser.getEmail()) - .orElseGet(() -> userRepository.save(finalGoogleUser)); - - // JWT 토큰 생성 및 refreshToken 저장 - JwtVo jwtVo = jwtUtil.generateTokens(user); - redisUtil.setOpsForValue(user.getId() + "_refresh", jwtVo.getRefreshToken(), - jwtUtil.getREFRESH_TOKEN_EXPIRATION()); - - return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken(), user.getLang() == null); - } - - @Transactional - public LoginResDto getAccessToken(String refreshToken) { - if (refreshToken.isBlank()) { - throw new AppException(GlobalErrorCode.REFRESH_TOKEN_REQUIRED); - } - - // refreshToken 유효성 검사 실행 - User tokenUser; - try { - tokenUser = jwtUtil.validateToken(false, refreshToken); - } catch (JwtException e) { - ErrorCode code = - e instanceof ExpiredJwtException ? GlobalErrorCode.EXPIRED_JWT : GlobalErrorCode.INVALID_TOKEN; - - throw new AppException(code); - } - - // JWT 토큰 생성 및 refreshToken 저장 - JwtVo jwtVo = jwtUtil.generateTokens(tokenUser); - redisUtil.setOpsForValue(tokenUser.getId() + "_refresh", jwtVo.getRefreshToken(), - jwtUtil.getREFRESH_TOKEN_EXPIRATION()); - - return LoginResDto.of(jwtVo.getAccessToken(), jwtVo.getRefreshToken(), tokenUser.getLang() == null); - } - - @Transactional - public void postSignUp(User user, SignUpReqDto signUpReq) { - // 학년 유효성 검사 - if (signUpReq.school() == SchoolType.MIDDLE || signUpReq.school() == SchoolType.HIGH || signUpReq.grade() > 3) { - throw new AppException(UserErrorCode.GRADE_SIZE_ERROR); - } - - // 사용자 정보 업데이트 - user.updateInfo(signUpReq.name(), signUpReq.lang(), signUpReq.school(), signUpReq.grade(), signUpReq.voice()); - - userRepository.save(user); - } - - public UserResDto getUserInfo(User user) { - return UserResDto.of(user.getName(), user.getLang(), user.getVoice(), user.getLevel()); - } - - @Transactional - public void updateLang(User user, LangType lang) { - user.updateLang(lang); - userRepository.save(user); - } - - @Transactional - public void updateLevel(User user) { - user.updateLevel(user.getLevel() + 1); - userRepository.save(user); - } - - @Transactional - public void deleteGoogleLogout(User user) { - // 사용자 refreshToken 삭제 - redisUtil.delete(user.getId() + "_refresh"); - } -} From a01ba9a78c119591b07de75efa9480d0edb75aae Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 12:27:04 +0900 Subject: [PATCH 71/73] =?UTF-8?q?:construction=5Fworker:=20chore(ci):=20da?= =?UTF-8?q?ta.sql=20init=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20aws=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=B6=94=EA=B0=80=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 4fd3ef1..adc699c 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -52,6 +52,15 @@ jobs: username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | + # AWS 인증 설정 + export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} + export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} + export AWS_DEFAULT_REGION=ap-northeast-2 + + # S3에서 data.sql 내려받기 + aws s3 cp s3://${{ secrets.AWS_S3_BUCKET_NAME }}/data.sql /home/ubuntu/app/src/main/resources/data.sql + + # Docker 재배포 sudo docker compose up -d --pull always sudo docker ps sudo docker image prune -f From b5bb3638a86c30febd54e70875c00e756dfd132e Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 12:31:17 +0900 Subject: [PATCH 72/73] =?UTF-8?q?:bug:=20chore(ci):=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=EA=B0=80=20=EC=95=84=EB=8B=8C=20jar=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EC=97=90=20data.sql=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index adc699c..63f36c3 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -26,7 +26,11 @@ jobs: run: | cd ./src/main/resources touch ./application.yml - echo "${{ secrets.PROPERTIES }}" > ./application.yml + aws s3 cp s3://${{ secrets.AWS_S3_BUCKET_NAME }}/data.sql ./data.sql + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ap-northeast-2 shell: bash - name: 🔧 빌드 권한 부여 @@ -52,15 +56,6 @@ jobs: username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | - # AWS 인증 설정 - export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} - export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} - export AWS_DEFAULT_REGION=ap-northeast-2 - - # S3에서 data.sql 내려받기 - aws s3 cp s3://${{ secrets.AWS_S3_BUCKET_NAME }}/data.sql /home/ubuntu/app/src/main/resources/data.sql - - # Docker 재배포 sudo docker compose up -d --pull always sudo docker ps sudo docker image prune -f From 233c07befa87847a02342a045b88fc693635ce3a Mon Sep 17 00:00:00 2001 From: yerim1ee Date: Thu, 21 Aug 2025 12:42:54 +0900 Subject: [PATCH 73/73] =?UTF-8?q?:bug:=20chore(ci):=20aws=20s3=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20[KOBG-13]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-pipeline.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/cd-pipeline.yml b/.github/workflows/cd-pipeline.yml index 63f36c3..4fd3ef1 100644 --- a/.github/workflows/cd-pipeline.yml +++ b/.github/workflows/cd-pipeline.yml @@ -26,11 +26,7 @@ jobs: run: | cd ./src/main/resources touch ./application.yml - aws s3 cp s3://${{ secrets.AWS_S3_BUCKET_NAME }}/data.sql ./data.sql - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ap-northeast-2 + echo "${{ secrets.PROPERTIES }}" > ./application.yml shell: bash - name: 🔧 빌드 권한 부여