From 19c0faf1796eb9a2919154b5a69076be62065524 Mon Sep 17 00:00:00 2001 From: Yoonchul Chung <84674889+Yoonchulchung@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:17:00 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat(#0):=20checkstyle=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/build.gradle b/build.gradle index 00abbc1c..1043e9ab 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.9' id 'io.spring.dependency-management' version '1.1.7' + id 'checkstyle' } group = 'com.example' @@ -25,6 +26,8 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -33,8 +36,38 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Test dependencies + testRuntimeOnly 'com.h2database:h2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' } tasks.named('test') { useJUnitPlatform() } + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +checkstyle { + toolVersion = '10.12.5' + configFile = file("${rootProject.projectDir}/config/checkstyle/checkstyle.xml") + maxWarnings = 0 + ignoreFailures = false +} + +tasks.withType(Checkstyle) { + reports { + xml.required = true + html.required = true + } +} From a4cdd9fd09b1a448d10e26ea0d2f1082e69a1afd Mon Sep 17 00:00:00 2001 From: Yoonchul Chung <84674889+Yoonchulchung@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:18:32 +0900 Subject: [PATCH 02/13] =?UTF-8?q?docs(#0):=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added commands for code style checks and Docker usage. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2e13a235..83815dc8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ git config core.hooksPath .githooks ``` +아래 명령어를 이용해서 코드 스타일을 준수했는지 확인해주세요 +```bash +./gradlew checkstyleMain checkstyleTest +``` + 아래 명령어를 통해 쉽게 도커 컨테이너에서 사용할 수 있습니다. ```bash From b804e913ee0e8e887624dbb2818a080e0e52c0ac Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Mon, 5 Jan 2026 14:49:35 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat(#0):=20global=20exception,=20code=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/presentation/ApiResponse.java | 35 +++++++++ .../advice/DuplicateUserException.java | 8 ++ .../advice/GlobalExceptionHandler.java | 77 +++++++++++++++++++ .../advice/ResourceNotFoundException.java | 8 ++ .../presentation/code/BaseErrorCode.java | 10 +++ .../presentation/code/BaseSuccessCode.java | 10 +++ .../presentation/code/GeneralErrorCode.java | 48 ++++++++++++ .../presentation/code/GeneralSuccessCode.java | 32 ++++++++ 8 files changed, 228 insertions(+) create mode 100644 src/main/java/com/example/RealMatch/global/presentation/ApiResponse.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/advice/DuplicateUserException.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/advice/ResourceNotFoundException.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/code/GeneralErrorCode.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java diff --git a/src/main/java/com/example/RealMatch/global/presentation/ApiResponse.java b/src/main/java/com/example/RealMatch/global/presentation/ApiResponse.java new file mode 100644 index 00000000..b6449dc9 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/ApiResponse.java @@ -0,0 +1,35 @@ +package com.example.RealMatch.global.presentation; + +import com.example.RealMatch.global.presentation.code.BaseErrorCode; +import com.example.RealMatch.global.presentation.code.BaseSuccessCode; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class ApiResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + + @JsonProperty("code") + private final String code; + + @JsonProperty("message") + private final String message; + + @JsonProperty("result") + private T result; + + public static ApiResponse onSuccess(BaseSuccessCode code, T result) { + return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); + } + + public static ApiResponse onFailure(BaseErrorCode code, T result) { + return new ApiResponse<>(false, code.getCode(), code.getMessage(), result); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/DuplicateUserException.java b/src/main/java/com/example/RealMatch/global/presentation/advice/DuplicateUserException.java new file mode 100644 index 00000000..bff3e442 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/DuplicateUserException.java @@ -0,0 +1,8 @@ +package com.example.RealMatch.global.presentation.advice; + +public class DuplicateUserException extends RuntimeException { + + public DuplicateUserException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java b/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java new file mode 100644 index 00000000..1d4d3264 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java @@ -0,0 +1,77 @@ +package com.example.RealMatch.global.presentation.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; + +import com.example.RealMatch.global.presentation.ApiResponse; +import com.example.RealMatch.global.presentation.code.GeneralErrorCode; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { + + log.warn("[IllegalArgumentException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.BAD_REQUEST.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.BAD_REQUEST, null)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolation(ConstraintViolationException e) { + + log.warn("[ConstraintViolationException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.INVALID_PAGE.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); + } + + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity> handleHandlerMethodValidation(HandlerMethodValidationException e) { + + log.warn("[HandlerMethodValidationException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.INVALID_PAGE.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); + } + + @ExceptionHandler(SecurityException.class) + public ResponseEntity> handleSecurityException(SecurityException e) { + + log.warn("[SecurityException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.UNAUTHORIZED.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null)); + } + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleResourceNotFound(ResourceNotFoundException e) { + + log.warn("[ResourceNotFoundException] {}", e.getMessage()); + + return ResponseEntity + .status(GeneralErrorCode.NOT_FOUND.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.NOT_FOUND, null)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnexpectedException(Exception e) { + + log.error("[UnexpectedException]", e); + + return ResponseEntity + .status(GeneralErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + .body(ApiResponse.onFailure(GeneralErrorCode.INTERNAL_SERVER_ERROR, null)); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/ResourceNotFoundException.java b/src/main/java/com/example/RealMatch/global/presentation/advice/ResourceNotFoundException.java new file mode 100644 index 00000000..c312ca5d --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package com.example.RealMatch.global.presentation.advice; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java new file mode 100644 index 00000000..4c387271 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java @@ -0,0 +1,10 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +public interface BaseErrorCode { + + HttpStatus getStatus(); + String getCode(); + String getMessage(); +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java new file mode 100644 index 00000000..962a30e3 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java @@ -0,0 +1,10 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +public interface BaseSuccessCode { + + HttpStatus getStatus(); + String getCode(); + String getMessage(); +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/GeneralErrorCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralErrorCode.java new file mode 100644 index 00000000..a9190d13 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralErrorCode.java @@ -0,0 +1,48 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GeneralErrorCode implements BaseErrorCode { + + BAD_REQUEST(HttpStatus.BAD_REQUEST, + "COMMON400_1", + "잘못된 요청입니다."), + + INVALID_DATA(HttpStatus.BAD_REQUEST, + "COMMON4001_2", + "유효하지 않은 데이터입니다."), + + INVALID_PAGE(HttpStatus.BAD_REQUEST, + "COMMON400_3", + "페이지 번호는 1 이상이어야 합니다."), + + DUPLICATE_MISSION(HttpStatus.BAD_REQUEST, + "COMMON400_4", + "이미 도전 중인 미션입니다."), + + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "COMMON401_1", + "인증이 필요합니다."), + + FORBIDDEN(HttpStatus.FORBIDDEN, + "COMMON403_1", + "접근 권한이 없습니다."), + + NOT_FOUND(HttpStatus.NOT_FOUND, + "COMMON404_1", + "요청한 리소스를 찾을 수 없습니다."), + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, + "COMMON500_1", + "서버 내부 오류가 발생했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java new file mode 100644 index 00000000..2a347bd8 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java @@ -0,0 +1,32 @@ +package com.example.RealMatch.global.presentation.code; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GeneralSuccessCode implements BaseSuccessCode { + + GOOD_REQUEST(HttpStatus.OK, + "COMMON200_1", + "정상적인 요청입니다."), + AUTHORIZED(HttpStatus.CREATED, + "AUTH201_1", + "인증이 확인되었습니다."), + CREATE(HttpStatus.CREATED, + "CREATE200_1", + "성공적으로 생성되었습니다."), + ALLOWED(HttpStatus.ACCEPTED, + "AUTH203_1", + "요청이 허용되었습니다."), + FOUND(HttpStatus.FOUND, + "COMMON302_1", + "요청한 리소스를 찾았습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} From f9ae19ddfa59d2ea7ad229de31f9ff50676277e5 Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Sun, 11 Jan 2026 00:56:10 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat(#0):=20github=20action=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pr-check.yml | 62 ++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index fba7594f..ff2232e0 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,6 +8,21 @@ jobs: build-and-test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: myapp_db + POSTGRES_USER: admin + POSTGRES_PASSWORD: secret + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -18,10 +33,19 @@ jobs: java-version: '21' distribution: 'temurin' cache: gradle + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Make application.properties + run: | + mkdir -p ./src/main/resources + echo "${{ secrets.APPLICATION_YML }}" > ./src/main/resources/application.yml + shell: bash + - name: Check code formatting run: ./gradlew checkstyleMain checkstyleTest continue-on-error: true @@ -33,12 +57,38 @@ jobs: run: ./gradlew test continue-on-error: true - - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - files: | - build/test-results/**/*.xml + - name: Test application startup + run: | + ./gradlew bootRun > bootrun.log 2>&1 & + APP_PID=$! + echo "Started bootRun with PID: $APP_PID" + + # 최대 60초 대기하면서 애플리케이션 시작 확인 + for i in {1..60}; do + # 로그에서 시작 완료 메시지 확인 + if grep -q "Started SpotApplication" bootrun.log 2>/dev/null; then + echo "✅ Application started successfully after ${i}s" + kill $APP_PID 2>/dev/null || true + exit 0 + fi + + # 프로세스가 죽었는지 확인 + if ! kill -0 $APP_PID 2>/dev/null; then + echo "❌ Application process terminated unexpectedly" + echo "=== bootRun log ===" + cat bootrun.log + exit 1 + fi + + sleep 1 + done + + echo "❌ Application failed to start within 60 seconds" + echo "=== bootRun log ===" + cat bootrun.log + kill $APP_PID 2>/dev/null || true + exit 1 + timeout-minutes: 2 - name: Upload build artifacts if: failure() From 8b84a0362836fcb4a796c9fa8364d33d9956f31d Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Sun, 11 Jan 2026 01:25:12 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix(#0):=20Base=20Code=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docker/Dockerfile | 6 ++- build.gradle | 6 ++- docker-compose.yaml | 2 +- .../RealMatch/RealMatchApplication.java | 6 +-- .../RealMatch/global/common/BaseEntity.java | 24 +++++++++++ .../global/common/UpdateBaseEntity.java | 42 +++++++++++++++++++ .../RealMatch/RealMatchApplicationTests.java | 6 +-- 7 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/example/RealMatch/global/common/BaseEntity.java create mode 100644 src/main/java/com/example/RealMatch/global/common/UpdateBaseEntity.java diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 828c250d..d0676913 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -5,6 +5,8 @@ WORKDIR /app ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar -EXPOSE 8080 +EXPOSE 6000 -ENTRYPOINT ["java", "-jar", "app.jar"] +ENV SERVER_PORT=6000 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1043e9ab..6c2b21c4 100644 --- a/build.gradle +++ b/build.gradle @@ -31,9 +31,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -48,6 +49,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.12.6' implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + // mySQL + runtimeOnly 'com.mysql:mysql-connector-j' } tasks.named('test') { diff --git a/docker-compose.yaml b/docker-compose.yaml index 90933b10..d81aed88 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -30,7 +30,7 @@ services: dockerfile: Docker/Dockerfile container_name: spring_app ports: - - "8080:8080" + - "6000:6000" depends_on: - db - redis diff --git a/src/main/java/com/example/RealMatch/RealMatchApplication.java b/src/main/java/com/example/RealMatch/RealMatchApplication.java index 4c59c599..97becbf2 100644 --- a/src/main/java/com/example/RealMatch/RealMatchApplication.java +++ b/src/main/java/com/example/RealMatch/RealMatchApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class RealMatchApplication { - public static void main(String[] args) { - SpringApplication.run(RealMatchApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(RealMatchApplication.class, args); + } } diff --git a/src/main/java/com/example/RealMatch/global/common/BaseEntity.java b/src/main/java/com/example/RealMatch/global/common/BaseEntity.java new file mode 100644 index 00000000..101d8491 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/common/BaseEntity.java @@ -0,0 +1,24 @@ +package com.example.RealMatch.global.common; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public abstract class BaseEntity { + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + +} diff --git a/src/main/java/com/example/RealMatch/global/common/UpdateBaseEntity.java b/src/main/java/com/example/RealMatch/global/common/UpdateBaseEntity.java new file mode 100644 index 00000000..a7bc00bd --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/common/UpdateBaseEntity.java @@ -0,0 +1,42 @@ +package com.example.RealMatch.global.common; + +import java.time.LocalDateTime; + +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; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class UpdateBaseEntity extends BaseEntity { + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Column(name = "is_deleted", nullable = false) + private boolean isDeleted; + + protected UpdateBaseEntity() { + super(); + this.isDeleted = false; + } + + public void softDelete(Integer deletedBy, LocalDateTime deletedAt) { + this.isDeleted = true; + this.deletedAt = deletedAt; + } + + public void restore() { + this.isDeleted = false; + this.deletedAt = null; + } +} diff --git a/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java b/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java index 73c5c16a..1228974e 100644 --- a/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java +++ b/src/test/java/com/example/RealMatch/RealMatchApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class RealMatchApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } From d3932e42f59e0f1f2def299630ffd655e104618e Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Sun, 11 Jan 2026 01:30:03 +0900 Subject: [PATCH 06/13] =?UTF-8?q?fix(#0):=20ignore=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index e63e239a..03a4577d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # 특정 파일 제외 src/main/resources/application-*.yml +src/main/resources/application.yml +src/test/resources/application.yml +src/test/resources/application-*.yml ### Gradle ### .gradle/ @@ -37,3 +40,6 @@ Thumbs.db ### Log files ### *.log logs/ + +### mysql ### +mysql_data/ \ No newline at end of file From d808bd63014645ac04ade58e5f64b991d2d21834 Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Sun, 11 Jan 2026 01:32:32 +0900 Subject: [PATCH 07/13] =?UTF-8?q?fix(#0):=20application.yml=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/.keep | 0 src/main/resources/application.yaml | 3 --- src/test/resources/.keep | 0 3 files changed, 3 deletions(-) create mode 100644 src/main/resources/.keep delete mode 100644 src/main/resources/application.yaml create mode 100644 src/test/resources/.keep diff --git a/src/main/resources/.keep b/src/main/resources/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml deleted file mode 100644 index 5487359f..00000000 --- a/src/main/resources/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -spring: - application: - name: RealMatch diff --git a/src/test/resources/.keep b/src/test/resources/.keep new file mode 100644 index 00000000..e69de29b From d06e0ee9e438ec3a247587335a80f9181f23b6e9 Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Sun, 11 Jan 2026 01:33:13 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix(#0):=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a145f93ce93c0be55807ccd3bdcaf8e01efcf6d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}xR_5N-jX1Wh<#!m){05<$UeysWssfNS)i1X-5AhGk3mkpm&wc+~hlzJYIG z;@dd=!HD>CBWh-n={KFuwA*ht?G7P?R>xk6kSrmDfl7>}q4`AQxYQ+y8BY?BsySL@ z;7*2pHxTYpG&+7G1M=M^iA@A)k}g?%e~)fI@Q0&f@tMZriM3>krC5g5oQ{|{qsC}F ztTqOhvUes#;Lh!ad+xU;^=$4~grkNZwgxJ}_gWBgb>WAem{i3$^oAYI7;2iG3SlCn0E;&un z>(2u|j`xIZn{Sq_KfKg(w@*jQG2V|c{m%F6+&F_U5MdkUo zAeLF#intImfDHT<2ITp`K_yxSBaQ0lfJT)7fC+Fb0bAA*GKU+q3`QDZ1cd5TK%GiW zi9vNb__>L*3`QDtI-#ccpk`%iDio?#hxxe*C)Co2Ei!-%EHV(+-K@O-Prk1I7n87u z3?KvliUFFi9lHu!GI#6J=Hy+=L9aoj$hb)3TM8KJDu!5j73V>ffS*eP&@vcl1P=)Q N2q+q`K?YXJz$;0RXm|hs From c57da156b523b03ccf13f7e60350f808dbeeb8d7 Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Sun, 11 Jan 2026 01:34:05 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix(#0):=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docker/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Docker/.DS_Store diff --git a/Docker/.DS_Store b/Docker/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Sun, 11 Jan 2026 01:34:42 +0900 Subject: [PATCH 10/13] =?UTF-8?q?fix(#0):=20git=20ignore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 03a4577d..9922c12a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,6 @@ Thumbs.db logs/ ### mysql ### -mysql_data/ \ No newline at end of file +mysql_data/ + +bin/ \ No newline at end of file From 461571cb499d479d29d93b1f3d77aaa3b2bbd42c Mon Sep 17 00:00:00 2001 From: yoonchulchung Date: Sun, 11 Jan 2026 23:18:44 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feat(#0):=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/RealMatch/brand/application/service/.keep | 0 src/main/java/com/example/RealMatch/brand/domain/entity/.keep | 0 src/main/java/com/example/RealMatch/brand/domain/repository/.keep | 0 .../com/example/RealMatch/brand/presentation/controller/.keep | 0 .../com/example/RealMatch/brand/presentation/dto/request/.keep | 0 .../com/example/RealMatch/brand/presentation/dto/response/.keep | 0 .../java/com/example/RealMatch/campaign/application/service/.keep | 0 src/main/java/com/example/RealMatch/campaign/domain/entity/.keep | 0 .../java/com/example/RealMatch/campaign/domain/repository/.keep | 0 .../com/example/RealMatch/campaign/presentation/controller/.keep | 0 .../com/example/RealMatch/campaign/presentation/dto/request/.keep | 0 .../example/RealMatch/campaign/presentation/dto/response/.keep | 0 .../java/com/example/RealMatch/chat/application/service/.keep | 0 src/main/java/com/example/RealMatch/chat/domain/entity/.keep | 0 src/main/java/com/example/RealMatch/chat/domain/repository/.keep | 0 .../java/com/example/RealMatch/chat/presentation/controller/.keep | 0 .../com/example/RealMatch/chat/presentation/dto/request/.keep | 0 .../com/example/RealMatch/chat/presentation/dto/response/.keep | 0 .../java/com/example/RealMatch/match/application/service/.keep | 0 src/main/java/com/example/RealMatch/match/domain/entity/.keep | 0 src/main/java/com/example/RealMatch/match/domain/repository/.keep | 0 .../com/example/RealMatch/match/presentation/controller/.keep | 0 .../com/example/RealMatch/match/presentation/dto/request/.keep | 0 .../com/example/RealMatch/match/presentation/dto/response/.keep | 0 .../java/com/example/RealMatch/user/application/service/.keep | 0 src/main/java/com/example/RealMatch/user/domain/entity/.keep | 0 src/main/java/com/example/RealMatch/user/domain/repository/.keep | 0 .../java/com/example/RealMatch/user/presentation/controller/.keep | 0 .../com/example/RealMatch/user/presentation/dto/request/.keep | 0 .../com/example/RealMatch/user/presentation/dto/response/.keep | 0 30 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/java/com/example/RealMatch/brand/application/service/.keep create mode 100644 src/main/java/com/example/RealMatch/brand/domain/entity/.keep create mode 100644 src/main/java/com/example/RealMatch/brand/domain/repository/.keep create mode 100644 src/main/java/com/example/RealMatch/brand/presentation/controller/.keep create mode 100644 src/main/java/com/example/RealMatch/brand/presentation/dto/request/.keep create mode 100644 src/main/java/com/example/RealMatch/brand/presentation/dto/response/.keep create mode 100644 src/main/java/com/example/RealMatch/campaign/application/service/.keep create mode 100644 src/main/java/com/example/RealMatch/campaign/domain/entity/.keep create mode 100644 src/main/java/com/example/RealMatch/campaign/domain/repository/.keep create mode 100644 src/main/java/com/example/RealMatch/campaign/presentation/controller/.keep create mode 100644 src/main/java/com/example/RealMatch/campaign/presentation/dto/request/.keep create mode 100644 src/main/java/com/example/RealMatch/campaign/presentation/dto/response/.keep create mode 100644 src/main/java/com/example/RealMatch/chat/application/service/.keep create mode 100644 src/main/java/com/example/RealMatch/chat/domain/entity/.keep create mode 100644 src/main/java/com/example/RealMatch/chat/domain/repository/.keep create mode 100644 src/main/java/com/example/RealMatch/chat/presentation/controller/.keep create mode 100644 src/main/java/com/example/RealMatch/chat/presentation/dto/request/.keep create mode 100644 src/main/java/com/example/RealMatch/chat/presentation/dto/response/.keep create mode 100644 src/main/java/com/example/RealMatch/match/application/service/.keep create mode 100644 src/main/java/com/example/RealMatch/match/domain/entity/.keep create mode 100644 src/main/java/com/example/RealMatch/match/domain/repository/.keep create mode 100644 src/main/java/com/example/RealMatch/match/presentation/controller/.keep create mode 100644 src/main/java/com/example/RealMatch/match/presentation/dto/request/.keep create mode 100644 src/main/java/com/example/RealMatch/match/presentation/dto/response/.keep create mode 100644 src/main/java/com/example/RealMatch/user/application/service/.keep create mode 100644 src/main/java/com/example/RealMatch/user/domain/entity/.keep create mode 100644 src/main/java/com/example/RealMatch/user/domain/repository/.keep create mode 100644 src/main/java/com/example/RealMatch/user/presentation/controller/.keep create mode 100644 src/main/java/com/example/RealMatch/user/presentation/dto/request/.keep create mode 100644 src/main/java/com/example/RealMatch/user/presentation/dto/response/.keep diff --git a/src/main/java/com/example/RealMatch/brand/application/service/.keep b/src/main/java/com/example/RealMatch/brand/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/domain/entity/.keep b/src/main/java/com/example/RealMatch/brand/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/domain/repository/.keep b/src/main/java/com/example/RealMatch/brand/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/presentation/controller/.keep b/src/main/java/com/example/RealMatch/brand/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/brand/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/brand/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/brand/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/application/service/.keep b/src/main/java/com/example/RealMatch/campaign/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/domain/entity/.keep b/src/main/java/com/example/RealMatch/campaign/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/domain/repository/.keep b/src/main/java/com/example/RealMatch/campaign/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/presentation/controller/.keep b/src/main/java/com/example/RealMatch/campaign/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/campaign/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/campaign/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/campaign/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/application/service/.keep b/src/main/java/com/example/RealMatch/chat/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/domain/entity/.keep b/src/main/java/com/example/RealMatch/chat/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/domain/repository/.keep b/src/main/java/com/example/RealMatch/chat/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/presentation/controller/.keep b/src/main/java/com/example/RealMatch/chat/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/chat/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/chat/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/chat/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/application/service/.keep b/src/main/java/com/example/RealMatch/match/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/domain/entity/.keep b/src/main/java/com/example/RealMatch/match/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/domain/repository/.keep b/src/main/java/com/example/RealMatch/match/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/presentation/controller/.keep b/src/main/java/com/example/RealMatch/match/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/match/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/match/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/match/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/application/service/.keep b/src/main/java/com/example/RealMatch/user/application/service/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/domain/entity/.keep b/src/main/java/com/example/RealMatch/user/domain/entity/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/domain/repository/.keep b/src/main/java/com/example/RealMatch/user/domain/repository/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/presentation/controller/.keep b/src/main/java/com/example/RealMatch/user/presentation/controller/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/presentation/dto/request/.keep b/src/main/java/com/example/RealMatch/user/presentation/dto/request/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/RealMatch/user/presentation/dto/response/.keep b/src/main/java/com/example/RealMatch/user/presentation/dto/response/.keep new file mode 100644 index 00000000..e69de29b From 47f6ed709c83321e18c649d4498d110829e8e82d Mon Sep 17 00:00:00 2001 From: Park-JiYeong <89844427+ParkJiYeoung8297@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:34:51 +0900 Subject: [PATCH 12/13] =?UTF-8?q?fix(#7):=20DI=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related_to : #7 ## 📝 작업 내용 CD 추가 과정에서 있었던 피드백을 반영했습니다. (PR #8 ) ## ✅ 변경 사항 - [x] .env.* 추가 - [x] @RequiredArgsConstructor사용해서 DI로 수정 - [x] 기타 description과 api version 제거 ## 📷 스크린샷 (선택) ## 💬 리뷰어에게 아 로컬에서 PR을 안날리고 다시 했네요... 변경 사항 추가했습니다 --- .github/workflows/cd-prod.yml | 52 +++++++++ .github/workflows/pr-check.yml | 16 +-- .gitignore | 6 +- build.gradle | 6 ++ docker-compose.yaml | 25 ++--- .../global/config/SecurityConfig.java | 76 +++++++++++++ .../global/config/SwaggerConfig.java | 52 +++++++++ .../global/config/jwt/CustomUserDetails.java | 58 ++++++++++ .../config/jwt/JwtAuthenticationFilter.java | 62 +++++++++++ .../global/config/jwt/JwtProvider.java | 101 ++++++++++++++++++ .../global/controller/TestController.java | 58 ++++++++++ .../{ApiResponse.java => CustomResponse.java} | 16 ++- .../advice/CustomAccessDeniedHandler.java | 42 ++++++++ .../advice/CustomAuthEntryPoint.java | 42 ++++++++ .../advice/GlobalExceptionHandler.java | 26 ++--- .../presentation/code/BaseErrorCode.java | 4 +- .../presentation/code/BaseSuccessCode.java | 4 +- .../presentation/code/GeneralSuccessCode.java | 4 +- src/main/resources/application-prod.yml | 48 +++++++++ src/main/resources/application-test.yml | 42 ++++++++ 20 files changed, 697 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/cd-prod.yml create mode 100644 src/main/java/com/example/RealMatch/global/config/SecurityConfig.java create mode 100644 src/main/java/com/example/RealMatch/global/config/SwaggerConfig.java create mode 100644 src/main/java/com/example/RealMatch/global/config/jwt/CustomUserDetails.java create mode 100644 src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/RealMatch/global/config/jwt/JwtProvider.java create mode 100644 src/main/java/com/example/RealMatch/global/controller/TestController.java rename src/main/java/com/example/RealMatch/global/presentation/{ApiResponse.java => CustomResponse.java} (54%) create mode 100644 src/main/java/com/example/RealMatch/global/presentation/advice/CustomAccessDeniedHandler.java create mode 100644 src/main/java/com/example/RealMatch/global/presentation/advice/CustomAuthEntryPoint.java create mode 100644 src/main/resources/application-prod.yml create mode 100644 src/main/resources/application-test.yml diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml new file mode 100644 index 00000000..cc9f1d4b --- /dev/null +++ b/.github/workflows/cd-prod.yml @@ -0,0 +1,52 @@ +name: CD - PROD + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "temurin" + + - uses: gradle/actions/setup-gradle@v3 + + - name: Grant execute permission + run: chmod +x gradlew + + - name: Build JAR + run: ./gradlew build -x test + + - name: Docker Login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/spot-backend:prod + ${{ secrets.DOCKERHUB_USERNAME }}/spot-backend:${{ github.sha }} + + - name: Deploy + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.PROD_SERVER_IP }} + username: ubuntu + key: ${{ secrets.PROD_SERVER_SSH_KEY }} + script: | + cd /home/ubuntu/spot + git pull origin main + docker compose pull + docker compose up -d diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index ff2232e0..ce2c3228 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -23,6 +23,12 @@ jobs: --health-timeout 5s --health-retries 5 + env: + SPRING_PROFILES_ACTIVE: test + JWT_SECRET: pPmJ9ViYt8f6HAh2q5s36QmEUeyEFRcquPaNpnIGUK8er5DjfTKa4xbDsTFXQ7HRVfTLR2DIYs7s9iGdJ+Yb7Q== + JWT_ACCESS_EXPIRE_MS: 600000 + JWT_REFRESH_EXPIRE_MS: 1209600000 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -33,19 +39,13 @@ jobs: java-version: '21' distribution: 'temurin' cache: gradle - + - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Make application.properties - run: | - mkdir -p ./src/main/resources - echo "${{ secrets.APPLICATION_YML }}" > ./src/main/resources/application.yml - shell: bash - - name: Check code formatting run: ./gradlew checkstyleMain checkstyleTest continue-on-error: true @@ -66,7 +66,7 @@ jobs: # 최대 60초 대기하면서 애플리케이션 시작 확인 for i in {1..60}; do # 로그에서 시작 완료 메시지 확인 - if grep -q "Started SpotApplication" bootrun.log 2>/dev/null; then + if grep -q "Started RealMatchApplication" bootrun.log; then echo "✅ Application started successfully after ${i}s" kill $APP_PID 2>/dev/null || true exit 0 diff --git a/.gitignore b/.gitignore index 9922c12a..45116f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ # 특정 파일 제외 -src/main/resources/application-*.yml +#src/main/resources/application-*.yml src/main/resources/application.yml src/test/resources/application.yml src/test/resources/application-*.yml +# 환경 변수 +.env +.env.* + ### Gradle ### .gradle/ build/ diff --git a/build.gradle b/build.gradle index 6c2b21c4..8f359a16 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,12 @@ dependencies { // mySQL runtimeOnly 'com.mysql:mysql-connector-j' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' + + // postgresql (CI test 용) + runtimeOnly 'org.postgresql:postgresql' } tasks.named('test') { diff --git a/docker-compose.yaml b/docker-compose.yaml index d81aed88..a2d2f9df 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,10 +6,10 @@ services: container_name: mysql_db restart: always environment: - MYSQL_DATABASE: myapp_db - MYSQL_USER: admin - MYSQL_PASSWORD: secret - MYSQL_ROOT_PASSWORD: rootsecret + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} ports: - "3306:3306" volumes: @@ -34,12 +34,13 @@ services: depends_on: - db - redis + env_file: + - .env environment: - - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/myapp_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul - - SPRING_DATASOURCE_USERNAME=admin - - SPRING_DATASOURCE_PASSWORD=secret - - SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver - - SPRING_REDIS_HOST=redis - - SPRING_REDIS_PORT=6379 - - SPRING_PROFILES_ACTIVE=dev - + SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} + SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} + SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + SPRING_DATASOURCE_DRIVER_CLASS_NAME: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME} + SPRING_REDIS_HOST: ${SPRING_REDIS_HOST} + SPRING_REDIS_PORT: ${SPRING_REDIS_PORT} + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} diff --git a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java new file mode 100644 index 00000000..1f558a40 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java @@ -0,0 +1,76 @@ +package com.example.RealMatch.global.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import com.example.RealMatch.global.config.jwt.JwtAuthenticationFilter; +import com.example.RealMatch.global.presentation.advice.CustomAccessDeniedHandler; +import com.example.RealMatch.global.presentation.advice.CustomAuthEntryPoint; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + private static final String[] PERMIT_ALL_URL_ARRAY = { + "/api/v1/test", + "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html" + }; + + private static final String[] REQUEST_AUTHENTICATED_ARRAY = { + "/api/v1/test-auth" + }; + + @Value("${cors.allowed-origin}") + private String allowedOrigin; + @Value("${swagger.server-url}") + String swaggerUrl; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAuthEntryPoint customAuthEntryPoint, CustomAccessDeniedHandler customAccessDeniedHandler) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthEntryPoint) // 401 + .accessDeniedHandler(customAccessDeniedHandler) // 403 + ) + + .authorizeHttpRequests(auth -> auth + .requestMatchers(REQUEST_AUTHENTICATED_ARRAY).authenticated() + .requestMatchers(PERMIT_ALL_URL_ARRAY).permitAll() + .anyRequest().denyAll() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of(allowedOrigin, "http://localhost:8080", swaggerUrl)); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); // 쿠키/인증정보 포함 요청 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} + diff --git a/src/main/java/com/example/RealMatch/global/config/SwaggerConfig.java b/src/main/java/com/example/RealMatch/global/config/SwaggerConfig.java new file mode 100644 index 00000000..258b3553 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/SwaggerConfig.java @@ -0,0 +1,52 @@ +package com.example.RealMatch.global.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +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 SwaggerConfig { + + @Value("${swagger.server-url}") + private String swaggerUrl; + + @Bean + public OpenAPI localOpenAPI() { + Info info = new Info() + .title("🔗 RealMatch API") + .version("1.0.0") + .description("RealMatch API 명세서입니다."); + + String jwtSchemeName = "JWT Authentication"; + + io.swagger.v3.oas.models.security.SecurityScheme securityScheme = new io.swagger.v3.oas.models.security.SecurityScheme() + .name("Authorization") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"); + + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); + + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, securityScheme); + + return new OpenAPI() + .info(info) + .addSecurityItem(securityRequirement) + .components(components) + .servers(List.of( + new Server() + .url(swaggerUrl) + )); + } + +} diff --git a/src/main/java/com/example/RealMatch/global/config/jwt/CustomUserDetails.java b/src/main/java/com/example/RealMatch/global/config/jwt/CustomUserDetails.java new file mode 100644 index 00000000..4f6af748 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/jwt/CustomUserDetails.java @@ -0,0 +1,58 @@ +package com.example.RealMatch.global.config.jwt; + +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import lombok.Getter; + +@Getter +public class CustomUserDetails implements UserDetails { + + private final Long userId; // DB PK + private final String providerId; // 소셜 고유 ID + private final String role; // USER / ADMIN + + public CustomUserDetails(Long userId, String providerId, String role) { + this.userId = userId; + this.providerId = providerId; + this.role = role; + } + + @Override + public Collection getAuthorities() { + return List.of(() -> "ROLE_" + role); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return providerId; + } // 소셜 UUID 기준 + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 00000000..37964ae8 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,62 @@ +package com.example.RealMatch.global.config.jwt; + +import java.io.IOException; + +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authHeader.substring(7); + + if (!jwtProvider.validateToken(token)) { + filterChain.doFilter(request, response); + return; + } + + Long userId = jwtProvider.getUserId(token); + String providerId = jwtProvider.getProviderId(token); + String role = jwtProvider.getRole(token); + + CustomUserDetails userDetails = + new CustomUserDetails(userId, providerId, role); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + filterChain.doFilter(request, response); + } + +} diff --git a/src/main/java/com/example/RealMatch/global/config/jwt/JwtProvider.java b/src/main/java/com/example/RealMatch/global/config/jwt/JwtProvider.java new file mode 100644 index 00000000..9071df96 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/config/jwt/JwtProvider.java @@ -0,0 +1,101 @@ +package com.example.RealMatch.global.config.jwt; + +import java.util.Base64; +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +@Component +public class JwtProvider { + + private final SecretKey secretKey; + private final long accessTokenExpireMillis; + private final long refreshTokenExpireMillis; + + public JwtProvider( + @Value("${jwt.secret}") String secret, + @Value("${jwt.access-expire-ms}") long accessTokenExpireMillis, + @Value("${jwt.refresh-expire-ms}") long refreshTokenExpireMillis + ) { + byte[] keyBytes = Base64.getDecoder().decode(secret); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); + this.accessTokenExpireMillis = accessTokenExpireMillis; + this.refreshTokenExpireMillis = refreshTokenExpireMillis; + } + + // ========================= + // 토큰 생성 + // ========================= + public String createAccessToken(Long userId, String providerId, String role) { + return createToken(userId, providerId, role, accessTokenExpireMillis); + } + + public String createRefreshToken(Long userId, String providerId, String role) { + return createToken(userId, providerId, role, refreshTokenExpireMillis); + } + + private String createToken(Long userId, String providerId, String role, long expireMillis) { + long now = System.currentTimeMillis(); + + return Jwts.builder() + .subject(String.valueOf(userId)) + .claim("providerId", providerId) + .claim("role", role) + .issuedAt(new Date(now)) + .expiration(new Date(now + expireMillis)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + // ========================= + // 토큰 검증 + // ========================= + public boolean validateToken(String token) { + try { + parseClaims(token); + return true; + } catch (ExpiredJwtException e) { + return false; // 만료 + } catch (JwtException | IllegalArgumentException e) { + return false; // 변조, 손상 등 + } + } + + // ========================= + // Claims 가져오기 + // ========================= + public Claims getClaims(String token) { + return parseClaims(token).getPayload(); + } + + public Long getUserId(String token) { + return Long.parseLong(getClaims(token).getSubject()); + } + + public String getProviderId(String token) { + return getClaims(token).get("providerId", String.class); + } + + public String getRole(String token) { + return getClaims(token).get("role", String.class); + } + + private Jws parseClaims(String token) { + return Jwts.parser() + .verifyWith(secretKey) // Key 타입 강제 + .build() + .parseSignedClaims(token); + } +} + diff --git a/src/main/java/com/example/RealMatch/global/controller/TestController.java b/src/main/java/com/example/RealMatch/global/controller/TestController.java new file mode 100644 index 00000000..3ee96566 --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/controller/TestController.java @@ -0,0 +1,58 @@ +package com.example.RealMatch.global.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.RealMatch.global.config.jwt.CustomUserDetails; +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.global.presentation.code.GeneralSuccessCode; + +import io.swagger.v3.oas.annotations.Operation; +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 = "test", description = "테스트용 API") +@RestController +@RequestMapping("/api") +public class TestController { + + @Operation(summary = "api 테스트 확인", + description = """ + 테스트용 api입니다. + 만약 이 api가 통과하지 않는다면, SecurityConfig에 url을 추가해야합니다. + + 인증이 필요없다면, PERMIT_ALL_URL_ARRAY에 추가하고, + 인증이 필요하다면, REQUEST_AUTHENTICATED_ARRAY에 추가해주세요. + """) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "테스트 성공") + }) + @GetMapping("/test") + public CustomResponse test() { + String response = "Hello from Spring Boot 👋"; + return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, response); + } + + @Operation(summary = "api 권한 테스트 확인", + description = """ + 테스트용 api입니다. + Swagger에서 Authorize에 토큰을 입력한 후 사용해야 정상 작동합니다. + """) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "테스트 성공"), + @ApiResponse(responseCode = "COMMON401_1", description = "인증이 필요합니다.") + }) + @GetMapping("/test-auth") + public CustomResponse testAuth( + @AuthenticationPrincipal CustomUserDetails user + ) { + String response = "Hello from Spring Boot 👋"; + return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, response); + } + + +} + diff --git a/src/main/java/com/example/RealMatch/global/presentation/ApiResponse.java b/src/main/java/com/example/RealMatch/global/presentation/CustomResponse.java similarity index 54% rename from src/main/java/com/example/RealMatch/global/presentation/ApiResponse.java rename to src/main/java/com/example/RealMatch/global/presentation/CustomResponse.java index b6449dc9..af6f39ae 100644 --- a/src/main/java/com/example/RealMatch/global/presentation/ApiResponse.java +++ b/src/main/java/com/example/RealMatch/global/presentation/CustomResponse.java @@ -2,6 +2,7 @@ import com.example.RealMatch.global.presentation.code.BaseErrorCode; import com.example.RealMatch.global.presentation.code.BaseSuccessCode; +import com.example.RealMatch.global.presentation.code.GeneralSuccessCode; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -11,7 +12,7 @@ @Getter @AllArgsConstructor @JsonPropertyOrder({"isSuccess", "code", "message", "result"}) -public class ApiResponse { +public class CustomResponse { @JsonProperty("isSuccess") private final Boolean isSuccess; @@ -25,11 +26,16 @@ public class ApiResponse { @JsonProperty("result") private T result; - public static ApiResponse onSuccess(BaseSuccessCode code, T result) { - return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); + // 200 OK + public static CustomResponse ok(T result) { + return CustomResponse.onSuccess(GeneralSuccessCode.GOOD_REQUEST, result); } - public static ApiResponse onFailure(BaseErrorCode code, T result) { - return new ApiResponse<>(false, code.getCode(), code.getMessage(), result); + public static CustomResponse onSuccess(BaseSuccessCode code, T result) { + return new CustomResponse<>(true, code.getCode(), code.getMessage(), result); + } + + public static CustomResponse onFailure(BaseErrorCode code, T result) { + return new CustomResponse<>(false, code.getCode(), code.getMessage(), result); } } diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAccessDeniedHandler.java b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..012d123c --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAccessDeniedHandler.java @@ -0,0 +1,42 @@ +package com.example.RealMatch.global.presentation.advice; + +import java.io.IOException; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.global.presentation.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + + response.setStatus(GeneralErrorCode.FORBIDDEN.getStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + + CustomResponse body = + CustomResponse.onFailure( + GeneralErrorCode.FORBIDDEN, + null + ); + + response.getWriter() + .write(objectMapper.writeValueAsString(body)); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAuthEntryPoint.java b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAuthEntryPoint.java new file mode 100644 index 00000000..79fdc7ed --- /dev/null +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/CustomAuthEntryPoint.java @@ -0,0 +1,42 @@ +package com.example.RealMatch.global.presentation.advice; + +import java.io.IOException; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import com.example.RealMatch.global.presentation.CustomResponse; +import com.example.RealMatch.global.presentation.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CustomAuthEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + + response.setStatus(GeneralErrorCode.UNAUTHORIZED.getStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + + CustomResponse body = + CustomResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + response.getWriter() + .write(objectMapper.writeValueAsString(body)); + } +} diff --git a/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java b/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java index 1d4d3264..f40ff4fa 100644 --- a/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java +++ b/src/main/java/com/example/RealMatch/global/presentation/advice/GlobalExceptionHandler.java @@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.HandlerMethodValidationException; -import com.example.RealMatch.global.presentation.ApiResponse; +import com.example.RealMatch.global.presentation.CustomResponse; import com.example.RealMatch.global.presentation.code.GeneralErrorCode; import jakarta.validation.ConstraintViolationException; @@ -16,62 +16,62 @@ public class GlobalExceptionHandler { @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { + public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { log.warn("[IllegalArgumentException] {}", e.getMessage()); return ResponseEntity .status(GeneralErrorCode.BAD_REQUEST.getStatus()) - .body(ApiResponse.onFailure(GeneralErrorCode.BAD_REQUEST, null)); + .body(CustomResponse.onFailure(GeneralErrorCode.BAD_REQUEST, null)); } @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity> handleConstraintViolation(ConstraintViolationException e) { + public ResponseEntity> handleConstraintViolation(ConstraintViolationException e) { log.warn("[ConstraintViolationException] {}", e.getMessage()); return ResponseEntity .status(GeneralErrorCode.INVALID_PAGE.getStatus()) - .body(ApiResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); + .body(CustomResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); } @ExceptionHandler(HandlerMethodValidationException.class) - public ResponseEntity> handleHandlerMethodValidation(HandlerMethodValidationException e) { + public ResponseEntity> handleHandlerMethodValidation(HandlerMethodValidationException e) { log.warn("[HandlerMethodValidationException] {}", e.getMessage()); return ResponseEntity .status(GeneralErrorCode.INVALID_PAGE.getStatus()) - .body(ApiResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); + .body(CustomResponse.onFailure(GeneralErrorCode.INVALID_PAGE, null)); } @ExceptionHandler(SecurityException.class) - public ResponseEntity> handleSecurityException(SecurityException e) { + public ResponseEntity> handleSecurityException(SecurityException e) { log.warn("[SecurityException] {}", e.getMessage()); return ResponseEntity .status(GeneralErrorCode.UNAUTHORIZED.getStatus()) - .body(ApiResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null)); + .body(CustomResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null)); } @ExceptionHandler(ResourceNotFoundException.class) - public ResponseEntity> handleResourceNotFound(ResourceNotFoundException e) { + public ResponseEntity> handleResourceNotFound(ResourceNotFoundException e) { log.warn("[ResourceNotFoundException] {}", e.getMessage()); return ResponseEntity .status(GeneralErrorCode.NOT_FOUND.getStatus()) - .body(ApiResponse.onFailure(GeneralErrorCode.NOT_FOUND, null)); + .body(CustomResponse.onFailure(GeneralErrorCode.NOT_FOUND, null)); } @ExceptionHandler(Exception.class) - public ResponseEntity> handleUnexpectedException(Exception e) { + public ResponseEntity> handleUnexpectedException(Exception e) { log.error("[UnexpectedException]", e); return ResponseEntity .status(GeneralErrorCode.INTERNAL_SERVER_ERROR.getStatus()) - .body(ApiResponse.onFailure(GeneralErrorCode.INTERNAL_SERVER_ERROR, null)); + .body(CustomResponse.onFailure(GeneralErrorCode.INTERNAL_SERVER_ERROR, null)); } } diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java index 4c387271..43c2f3fe 100644 --- a/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java +++ b/src/main/java/com/example/RealMatch/global/presentation/code/BaseErrorCode.java @@ -3,8 +3,10 @@ import org.springframework.http.HttpStatus; public interface BaseErrorCode { - + HttpStatus getStatus(); + String getCode(); + String getMessage(); } diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java index 962a30e3..6ae9b199 100644 --- a/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java +++ b/src/main/java/com/example/RealMatch/global/presentation/code/BaseSuccessCode.java @@ -3,8 +3,10 @@ import org.springframework.http.HttpStatus; public interface BaseSuccessCode { - + HttpStatus getStatus(); + String getCode(); + String getMessage(); } diff --git a/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java index 2a347bd8..b326b6e5 100644 --- a/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java +++ b/src/main/java/com/example/RealMatch/global/presentation/code/GeneralSuccessCode.java @@ -16,8 +16,8 @@ public enum GeneralSuccessCode implements BaseSuccessCode { "AUTH201_1", "인증이 확인되었습니다."), CREATE(HttpStatus.CREATED, - "CREATE200_1", - "성공적으로 생성되었습니다."), + "CREATE200_1", + "성공적으로 생성되었습니다."), ALLOWED(HttpStatus.ACCEPTED, "AUTH203_1", "요청이 허용되었습니다."), diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..83a0b6ef --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,48 @@ +cors: + allowed-origin: http://${SERVER_IP} + +swagger: + server-url: ${SWAGGER_SERVER_URL} + +server: + timezone: Asia/Seoul + +spring: + application: + name: Spot + activate: + on-profile: prod + + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driver-class-name: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME} + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 30000 + connection-timeout: 10000 + max-lifetime: 600000 + + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + + security: + refresh-token: + expire-days: 14 + +logging: + level: + org.hibernate.SQL: warn + +jwt: + secret: ${JWT_SECRET} + access-expire-ms: ${JWT_ACCESS_EXPIRE_MS} + refresh-expire-ms: ${JWT_REFRESH_EXPIRE_MS} + diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 00000000..41ce166c --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,42 @@ +cors: + allowed-origin: http://localhost:3000 + +swagger: + server-url: http://localhost:8080 + +server: + timezone: Asia/Seoul + +spring: + application: + name: Spot + + datasource: + url: jdbc:postgresql://localhost:5432/myapp_db + username: admin + password: secret + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 30000 + connection-timeout: 10000 + max-lifetime: 600000 + + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + show-sql: false + + security: + refresh-token: + expire-days: 14 + +jwt: + secret: ${JWT_SECRET} + access-expire-ms: ${JWT_ACCESS_EXPIRE_MS} + refresh-expire-ms: ${JWT_REFRESH_EXPIRE_MS} From ea853bb40f26edd7d14086304660f7368d8e9c93 Mon Sep 17 00:00:00 2001 From: Park-JiYeong <89844427+ParkJiYeoung8297@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:37:05 +0900 Subject: [PATCH 13/13] =?UTF-8?q?fix(#7):=20CI=EC=99=80=20CD=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related_to : #7 ## 📝 작업 내용 CI와 CD 설정 파일 수정 ## ✅ 변경 사항 - [x] 앱 이름 spot → application으로 수정 - [x] Ci 테스트를 위한 DB postgre → mysql로 수정 ## 📷 스크린샷 (선택) ## 💬 리뷰어에게 CI 테스트 시 실제 운영 db에 연결해 테스트하는 것은 위험할 것 같아서, 테스트용 db로 변경했습니다. 테스트를 위한 가짜 값이기 때문에 따로 시크릿 처리하지 않았습니다. --- .github/workflows/cd-prod.yml | 16 +++++++++++--- .github/workflows/pr-check.yml | 22 ++++++++++--------- build.gradle | 3 --- .../global/config/SecurityConfig.java | 4 ++-- src/main/resources/application-prod.yml | 2 +- src/main/resources/application-test.yml | 12 +++++----- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml index cc9f1d4b..5d68986b 100644 --- a/.github/workflows/cd-prod.yml +++ b/.github/workflows/cd-prod.yml @@ -36,8 +36,8 @@ jobs: context: . push: true tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/spot-backend:prod - ${{ secrets.DOCKERHUB_USERNAME }}/spot-backend:${{ github.sha }} + ${{ secrets.DOCKERHUB_USERNAME }}/realmatch-backend:prod + ${{ secrets.DOCKERHUB_USERNAME }}/realmatch-backend:${{ github.sha }} - name: Deploy uses: appleboy/ssh-action@v1.0.3 @@ -46,7 +46,17 @@ jobs: username: ubuntu key: ${{ secrets.PROD_SERVER_SSH_KEY }} script: | - cd /home/ubuntu/spot + cd /home/ubuntu/realmatch git pull origin main + docker compose pull docker compose up -d + + echo "Waiting for app to start..." + sleep 10 + + if ! docker compose ps | grep "Up"; then + echo "❌ Container is not running" + docker compose logs + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index ce2c3228..5b5dbb93 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -9,19 +9,21 @@ jobs: runs-on: ubuntu-latest services: - postgres: - image: postgres:16 + mysql: + image: mysql:8.0 env: - POSTGRES_DB: myapp_db - POSTGRES_USER: admin - POSTGRES_PASSWORD: secret + MYSQL_DATABASE: test_db + MYSQL_USER: test_user + MYSQL_PASSWORD: test + MYSQL_ROOT_PASSWORD: test_root ports: - - 5432:5432 + - 3306:3306 options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -ptest_root" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + env: SPRING_PROFILES_ACTIVE: test diff --git a/build.gradle b/build.gradle index 8f359a16..f490a7e2 100644 --- a/build.gradle +++ b/build.gradle @@ -55,9 +55,6 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' - - // postgresql (CI test 용) - runtimeOnly 'org.postgresql:postgresql' } tasks.named('test') { diff --git a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java index 1f558a40..c1bfe019 100644 --- a/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java +++ b/src/main/java/com/example/RealMatch/global/config/SecurityConfig.java @@ -26,12 +26,12 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private static final String[] PERMIT_ALL_URL_ARRAY = { - "/api/v1/test", + "/api/test", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html" }; private static final String[] REQUEST_AUTHENTICATED_ARRAY = { - "/api/v1/test-auth" + "/api/test-auth" }; @Value("${cors.allowed-origin}") diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 83a0b6ef..074981e3 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -9,7 +9,7 @@ server: spring: application: - name: Spot + name: Realmatch activate: on-profile: prod diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 41ce166c..d2881f5b 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -9,13 +9,13 @@ server: spring: application: - name: Spot + name: Realmatch datasource: - url: jdbc:postgresql://localhost:5432/myapp_db - username: admin - password: secret - driver-class-name: org.postgresql.Driver + url: jdbc:mysql://localhost:3306/test_db + username: test_user + password: test + driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 5 @@ -28,7 +28,7 @@ spring: ddl-auto: none properties: hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect + dialect: org.hibernate.dialect.MySQL8Dialect format_sql: true show-sql: false