Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
4b91406
Merge pull request #28 from one-day-one-problem/27-bug-submissions-ํ…Œ์ดโ€ฆ
namsh1125 Mar 23, 2025
92d6bfa
Merge pull request #38 from one-day-one-problem/develop
namsh1125 Mar 24, 2025
d7dbb56
Merge pull request #55 from one-day-one-problem/develop
namsh1125 Apr 2, 2025
9ab2e9e
Merge pull request #56 from one-day-one-problem/fix/problem-solved-count
namsh1125 Apr 2, 2025
d56bcb8
Merge pull request #60 from one-day-one-problem/develop
namsh1125 Jul 10, 2025
4fc6f9b
Merge pull request #61 from one-day-one-problem/59-improve-๋ฌธ์ œ-๋ชฉ๋ก-์กฐํšŒ-aโ€ฆ
namsh1125 Jul 10, 2025
557146b
๐Ÿš‘ SonarQube ์‚ฌ๋ง์œผ๋กœ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ์ˆ˜์ •
namsh1125 Jul 10, 2025
a052b32
Merge pull request #62 from one-day-one-problem/hotfix/cicd
namsh1125 Jul 10, 2025
a862e1f
๐ŸŽจErrorCode ๊ธฐ๋ฐ˜ ๊ณตํ†ต ์‘๋‹ต ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฆฌํŒฉํ„ฐ๋ง
mango0422 Nov 19, 2025
346d66d
๐ŸŽจ refactor: ErrorCode์™€ BaseResponse ์—ญํ•  ์ •๋ฆฌ
mango0422 Nov 28, 2025
6e11899
๐ŸŽจ refactor: Security ์—๋Ÿฌ ์‘๋‹ต์„ ErrorCode ๊ธฐ๋ฐ˜์œผ๋กœ ํ†ต์ผ
mango0422 Nov 28, 2025
18f6380
๐ŸŽจ refactor: ์ปจํŠธ๋กค๋Ÿฌ ์„ฑ๊ณต ์‘๋‹ต์„ BaseResponse.onSuccess๋กœ ํ†ต์ผ
mango0422 Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 55 additions & 55 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,55 +1,55 @@
name: Build

on:
push:
branches:
- main
- develop

pull_request:
types: [opened, synchronize, reopened]
branches:
- main
- develop

jobs:
build:
name: Build and analyze
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu' # Alternative distribution options are available.

- name: Set secret.yml file
run: |
echo "$SECRET_YML_CONTENT" > ./src/main/resources/secret.yml
env:
SECRET_YML_CONTENT: ${{ secrets.SECRET_YML_FOR_SONARQUBE }}

- name: Cache SonarQube packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle

- name: Build and analyze
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: ./gradlew build sonar --info
#name: Build
#
#on:
# push:
# branches:
# - main
# - develop
#
# pull_request:
# types: [opened, synchronize, reopened]
# branches:
# - main
# - develop
#
#jobs:
# build:
# name: Build and analyze
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
#
# - name: Set up JDK 17
# uses: actions/setup-java@v4
# with:
# java-version: 17
# distribution: 'zulu' # Alternative distribution options are available.
#
# - name: Set secret.yml file
# run: |
# echo "$SECRET_YML_CONTENT" > ./src/main/resources/secret.yml
# env:
# SECRET_YML_CONTENT: ${{ secrets.SECRET_YML_FOR_SONARQUBE }}
#
# - name: Cache SonarQube packages
# uses: actions/cache@v4
# with:
# path: ~/.sonar/cache
# key: ${{ runner.os }}-sonar
# restore-keys: ${{ runner.os }}-sonar
#
# - name: Cache Gradle packages
# uses: actions/cache@v4
# with:
# path: ~/.gradle/caches
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
# restore-keys: ${{ runner.os }}-gradle
#
# - name: Build and analyze
# env:
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
# run: ./gradlew build sonar --info
82 changes: 41 additions & 41 deletions .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,50 @@ permissions:
actions: read

jobs:
build:
name: Build and analyze
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu' # Alternative distribution options are available.

- name: Set secret.yml file
run: |
echo "$SECRET_YML_CONTENT" > ./src/main/resources/secret.yml
env:
SECRET_YML_CONTENT: ${{ secrets.SECRET_YML_FOR_SONARQUBE }}

- name: Cache SonarQube packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle

- name: Build and analyze
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: ./gradlew build sonar --info
# build:
# name: Build and analyze
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
#
# - name: Set up JDK 17
# uses: actions/setup-java@v4
# with:
# java-version: 17
# distribution: 'zulu' # Alternative distribution options are available.
#
# - name: Set secret.yml file
# run: |
# echo "$SECRET_YML_CONTENT" > ./src/main/resources/secret.yml
# env:
# SECRET_YML_CONTENT: ${{ secrets.SECRET_YML_FOR_SONARQUBE }}
#
# - name: Cache SonarQube packages
# uses: actions/cache@v4
# with:
# path: ~/.sonar/cache
# key: ${{ runner.os }}-sonar
# restore-keys: ${{ runner.os }}-sonar
#
# - name: Cache Gradle packages
# uses: actions/cache@v4
# with:
# path: ~/.gradle/caches
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
# restore-keys: ${{ runner.os }}-gradle
#
# - name: Build and analyze
# env:
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
# run: ./gradlew build sonar --info

push-docker-image-to-ecr:
runs-on: ubuntu-latest
needs: build
# needs: build
steps:
- name: Checkout code
uses: actions/checkout@v3
Expand Down
44 changes: 13 additions & 31 deletions src/main/java/site/haruhana/www/advice/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand All @@ -14,9 +13,10 @@
import site.haruhana.www.dto.BaseResponse;
import site.haruhana.www.exception.InvalidAnswerFormatException;
import site.haruhana.www.exception.ProblemNotFoundException;

import java.util.stream.Collectors;

import static site.haruhana.www.common.ErrorCode.*;

@RestControllerAdvice
public class GlobalExceptionHandler {

Expand All @@ -30,11 +30,9 @@ public ResponseEntity<BaseResponse<Void>> handleMethodArgumentNotValidException(
})
.collect(Collectors.joining(", "));

String errorMessage = "์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: " + errorDetails;
String message = INVALID_INPUT_VALUE.getDefaultMessage() + ": " + errorDetails;

return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BaseResponse.onBadRequest(errorMessage));
return INVALID_INPUT_VALUE.toResponseEntity(message);
}

@ExceptionHandler(ConstraintViolationException.class)
Expand All @@ -47,59 +45,43 @@ public ResponseEntity<BaseResponse<Void>> handleConstraintViolationException(Con
})
.collect(Collectors.joining(", "));

String errorMessage = "์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: " + errorDetails;
String message = INVALID_INPUT_VALUE.getDefaultMessage() + ": " + errorDetails;

return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BaseResponse.onBadRequest(errorMessage));
return INVALID_INPUT_VALUE.toResponseEntity(message);
}

@ExceptionHandler(ProblemNotFoundException.class)
public ResponseEntity<BaseResponse<Void>> handleProblemNotFoundException(ProblemNotFoundException e) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(BaseResponse.onNotFound(e.getMessage()));
return PROBLEM_NOT_FOUND.toResponseEntity(e.getMessage());
}

@ExceptionHandler(InvalidAnswerFormatException.class)
public ResponseEntity<BaseResponse<Void>> handleInvalidAnswerFormatException(InvalidAnswerFormatException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BaseResponse.onBadRequest(e.getMessage()));
return INVALID_ANSWER_FORMAT.toResponseEntity(e.getMessage());
}

@ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity<BaseResponse<Void>> handleExpiredJwtException(ExpiredJwtException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(BaseResponse.onUnauthorized("ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."));
return TOKEN_EXPIRED.toResponseEntity(e.getMessage());
}

@ExceptionHandler(UnsupportedJwtException.class)
public ResponseEntity<BaseResponse<Void>> handleUnsupportedJwtException(UnsupportedJwtException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(BaseResponse.onUnauthorized("์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์˜ ํ† ํฐ์ž…๋‹ˆ๋‹ค."));
return TOKEN_UNSUPPORTED.toResponseEntity(e.getMessage());
}

@ExceptionHandler(MalformedJwtException.class)
public ResponseEntity<BaseResponse<Void>> handleMalformedJwtException(MalformedJwtException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(BaseResponse.onUnauthorized("์ž˜๋ชป๋œ ํ˜•์‹์˜ ํ† ํฐ์ž…๋‹ˆ๋‹ค."));
return TOKEN_MALFORMED.toResponseEntity(e.getMessage());
}

@ExceptionHandler(JwtException.class)
public ResponseEntity<BaseResponse<Void>> handleJwtException(JwtException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(BaseResponse.onUnauthorized("์ธ์ฆ ํ† ํฐ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: " + e.getMessage()));
return TOKEN_ERROR.toResponseEntity(e.getMessage());
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<BaseResponse<Void>> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BaseResponse.onBadRequest("์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค: " + e.getMessage()));
return ILLEGAL_ARGUMENT.toResponseEntity(e.getMessage());
}
}
64 changes: 64 additions & 0 deletions src/main/java/site/haruhana/www/common/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package site.haruhana.www.common;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import site.haruhana.www.dto.BaseResponse;

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

// ====== ๊ณตํ†ต ์—๋Ÿฌ ์ฝ”๋“œ ======
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ํ›„ ์ด์šฉํ•ด์ฃผ์„ธ์š”."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."),
PROBLEM_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค."),
INVALID_ANSWER_FORMAT(HttpStatus.BAD_REQUEST, "์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ๋‹ต์•ˆ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ๊ฐ๊ด€์‹ ๋ฌธ์ œ์˜ ๊ฒฝ์šฐ ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."),
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."),
TOKEN_UNSUPPORTED(HttpStatus.UNAUTHORIZED, "์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์˜ ํ† ํฐ์ž…๋‹ˆ๋‹ค."),
TOKEN_MALFORMED(HttpStatus.UNAUTHORIZED, "์ž˜๋ชป๋œ ํ˜•์‹์˜ ํ† ํฐ์ž…๋‹ˆ๋‹ค."),
TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "์ธ์ฆ ํ† ํฐ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."),
ILLEGAL_ARGUMENT(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."),
SUBMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "์ œ์ถœ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");



private final HttpStatus status;
private final String defaultMessage;

// ====== BaseResponse ์ƒ์„ฑ ํ—ฌํผ ======

public BaseResponse<Void> toResponse() {
return new BaseResponse<>(
false,
status.value(),
defaultMessage,
null
);
}

public BaseResponse<Void> toResponse(String overrideMessage) {
String msg = (overrideMessage != null) ? overrideMessage : defaultMessage;
return new BaseResponse<>(
false,
status.value(),
msg,
null
);
}

public ResponseEntity<BaseResponse<Void>> toResponseEntity() {
return ResponseEntity
.status(status)
.body(toResponse());
}

public ResponseEntity<BaseResponse<Void>> toResponseEntity(String overrideMessage) {
return ResponseEntity
.status(status)
.body(toResponse(overrideMessage));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import site.haruhana.www.common.ErrorCode;
import site.haruhana.www.dto.BaseResponse;

import java.io.IOException;
Expand All @@ -32,7 +33,7 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc
response.setContentType(MediaType.APPLICATION_JSON_VALUE); // Content-Type ์„ค์ •

// ์˜ค๋ฅ˜ ์‘๋‹ต ๊ฐ์ฒด ์ƒ์„ฑ
BaseResponse<Void> errorResponse = BaseResponse.onForbidden("์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");
BaseResponse<Void> errorResponse = BaseResponse.error(ErrorCode.ACCESS_DENIED);

// ์‘๋‹ต ๋ณธ๋ฌธ์— JSON ์ž‘์„ฑ
objectMapper.writeValue(response.getOutputStream(), errorResponse);
Expand Down
Loading