Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 0 additions & 9 deletions .claude/settings.local.json

This file was deleted.

14 changes: 14 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: Bug report
about: Create a report to help us improve
title: '[FIX] '
labels: ''
assignees: ''

---

# 버그 내용

# 스크린샷

# 참고 사항
12 changes: 12 additions & 0 deletions .github/ISSUE_TEMPLATE/feat-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Feat request
about: Suggest an idea for this project
title: '[FEAT] '
labels: ''
assignees: ''

---

# 투두 리스트

# 참고 사항
12 changes: 12 additions & 0 deletions .github/ISSUE_TEMPLATE/refactor-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: Refactor request
about: Suggest an idea for this project
title: '[REFACTOR] '
labels: ''
assignees: ''

---

# 투두 리스트

# 참고 사항
5 changes: 5 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
closed #

# 작업 내용

# 참고 사항
15 changes: 0 additions & 15 deletions .github/workflows/cd-api-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,7 @@ on:
branches: [ develop ]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
api_changed: ${{ steps.filter.outputs.api }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
base: develop
filters: |
api:
- 'api/**'

build-api:
needs: detect-changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
15 changes: 0 additions & 15 deletions .github/workflows/cd-api-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,7 @@ on:
branches: [ main ]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
api_changed: ${{ steps.filter.outputs.api }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
base: main
filters: |
api:
- 'api/**'

build-api:
needs: detect-changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
15 changes: 0 additions & 15 deletions .github/workflows/cd-internal-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,7 @@ on:
branches: [ develop ]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
internal_changed: ${{ steps.filter.outputs.internal }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
base: develop
filters: |
internal:
- 'internal/**'

build-internal:
needs: detect-changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
16 changes: 1 addition & 15 deletions .github/workflows/cd-internal-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,7 @@ on:
branches: [ main ]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
internal_changed: ${{ steps.filter.outputs.internal }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
base: main
filters: |
internal:
- 'internal/**'

build-internal:
needs: detect-changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -101,6 +86,7 @@ jobs:
SPRING_DATASOURCE_USERNAME_PROD: ${{ secrets.SPRING_DATASOURCE_USERNAME_PROD }}
SPRING_DATASOURCE_PASSWORD_PROD: ${{ secrets.SPRING_DATASOURCE_PASSWORD_PROD }}
SPRING_DATASOURCE_URL_PROD: ${{ secrets.SPRING_DATASOURCE_URL_PROD }}
WIDGET_SECRET_KEY_PROD: ${{ secrets.WIDGET_SECRET_KEY_PROD }}
run: |
export HOSTNAME=$(hostname)
cd kokomen-payment/docker/prod
Expand Down
14 changes: 0 additions & 14 deletions .github/workflows/ci-api-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,7 @@ on:
branches: [ main, develop ]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
api_changed: ${{ steps.filter.outputs.api }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
api:
- 'api/**'

build:
needs: detect-changes
runs-on: ubuntu-latest
permissions:
checks: write
Expand Down
14 changes: 0 additions & 14 deletions .github/workflows/ci-internal-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,7 @@ on:
branches: [ main, develop ]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
internal_changed: ${{ steps.filter.outputs.internal }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
internal:
- 'internal/**'

build:
needs: detect-changes
runs-on: ubuntu-latest
permissions:
checks: write
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ out/

### VS Code ###
.vscode/
.claude
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.samhap.kokomen.global.exception;

import lombok.Getter;

@Getter
public enum ApiErrorMessage {

AUTHENTICATION_ANNOTATION_REQUIRED("MemberAuth 파라미터는 @Authentication 어노테이션이 있어야 합니다."),
LOGIN_REQUIRED("로그인이 필요합니다"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

LOGIN_REQUIRED 메시지에 마침표(.) 누락.

다른 메시지들은 모두 마침표로 끝나지만, LOGIN_REQUIRED("로그인이 필요합니다")만 마침표가 없습니다.

✏️ 수정 제안
-    LOGIN_REQUIRED("로그인이 필요합니다"),
+    LOGIN_REQUIRED("로그인이 필요합니다."),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LOGIN_REQUIRED("로그인이 필요합니다"),
LOGIN_REQUIRED("로그인이 필요합니다."),
🤖 Prompt for AI Agents
In `@api/src/main/java/com/samhap/kokomen/global/exception/ApiErrorMessage.java`
at line 9, The enum constant LOGIN_REQUIRED in ApiErrorMessage is missing a
trailing period; update the enum entry LOGIN_REQUIRED("로그인이 필요합니다") to include a
final period in its message so it matches the punctuation style of the other
enum values (e.g., LOGIN_REQUIRED("로그인이 필요합니다.")). Ensure no other enum usages
or tests rely on the exact string without the period.

MEMBER_ID_NOT_IN_SESSION("세션에 MEMBER_ID가 없습니다."),
INVALID_REQUEST("잘못된 요청입니다."),
MISSING_REQUEST_PARAMETER("필수 요청 파라미터가 누락되었습니다."),
INVALID_REQUEST_FORMAT("잘못된 요청 형식입니다. JSON 형식을 확인해주세요."),
JSON_PARSE_ERROR("JSON 파싱 오류: 유효하지 않은 값이 전달되었습니다."),
INTERNAL_SERVER_ERROR("서버에 문제가 발생하였습니다.");

private final String message;

ApiErrorMessage(String message) {
this.message = message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.samhap.kokomen.global.dto.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand All @@ -11,7 +12,6 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// TODO: HttpMessageNotReadableException 예외 처리 추가
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
Expand All @@ -25,12 +25,12 @@ public ResponseEntity<ErrorResponse> handleKokomenException(KokomenException e)

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String defaultErrorMessageForUser = "잘못된 요청입니다.";
String defaultErrorMessageForUser = ApiErrorMessage.INVALID_REQUEST.getMessage();
String message = e.getBindingResult()
.getFieldErrors()
.stream()
.findFirst()
.map(error -> error.getDefaultMessage())
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.orElse(defaultErrorMessageForUser);

if (message.equals(defaultErrorMessageForUser)) {
Expand All @@ -44,35 +44,33 @@ public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Metho
}

@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ErrorResponse> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
String message = "필수 요청 파라미터 '" + e.getParameterName() + "'가 누락되었습니다.";
log.warn("MissingServletRequestParameterException :: message: {}", message);
public ResponseEntity<ErrorResponse> handleMissingServletRequestParameterException(
MissingServletRequestParameterException e) {
log.warn("MissingServletRequestParameterException :: parameterName: {}", e.getParameterName());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(message));
.body(new ErrorResponse(ApiErrorMessage.MISSING_REQUEST_PARAMETER.getMessage()));
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
String message = "잘못된 요청 형식입니다. JSON 형식을 확인해주세요.";
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ApiErrorMessage.JSON_PARSE_ERROR.getMessage()));
}
Comment on lines 56 to 62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

InvalidFormatException 발생 시 API 응답으로 일반적인 오류 메시지를 반환하도록 변경되었는데, 이는 API를 사용하는 개발자 입장에서 디버깅을 어렵게 만들 수 있습니다.

이전 구현처럼 어떤 필드에서 어떤 값 때문에 오류가 발생했는지에 대한 구체적인 정보를 포함하여 응답하는 것을 고려해 보세요. 민감한 정보를 노출하지 않는 선에서, 개발자에게 유용한 정보를 제공하는 것이 좋습니다.

예를 들어, 다음과 같이 상세 메시지를 생성하여 반환할 수 있습니다.

if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
    String fieldName = invalidFormatException.getPath().get(0).getFieldName();
    String invalidValue = String.valueOf(invalidFormatException.getValue());
    log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
    String detailedMessage = String.format("JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: %s)", fieldName, invalidValue);
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse(detailedMessage));
}
Suggested change
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
message = String.format(
"JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: '%s')",
fieldName,
invalidValue
);
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ApiErrorMessage.JSON_PARSE_ERROR.getMessage()));
}
if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
String fieldName = invalidFormatException.getPath().get(0).getFieldName();
String invalidValue = String.valueOf(invalidFormatException.getValue());
log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
String detailedMessage = String.format("JSON 파싱 오류: '%s' 필드에 유효하지 않은 값이 전달되었습니다. (전달된 값: %s)", fieldName, invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(detailedMessage));
}

Comment on lines 56 to 62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

invalidFormatException.getPath().get(0)에서 IndexOutOfBoundsException 발생 가능

InvalidFormatExceptiongetPath()가 빈 리스트를 반환하는 경우 get(0) 호출 시 IndexOutOfBoundsException이 발생할 수 있습니다. path가 비어있는 경우에 대한 방어 로직이 필요합니다.

🛡️ 방어 코드 제안
     if (e.getCause() instanceof InvalidFormatException invalidFormatException) {
-        String fieldName = invalidFormatException.getPath().get(0).getFieldName();
-        String invalidValue = String.valueOf(invalidFormatException.getValue());
-        log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
+        var path = invalidFormatException.getPath();
+        if (!path.isEmpty()) {
+            String fieldName = path.get(0).getFieldName();
+            String invalidValue = String.valueOf(invalidFormatException.getValue());
+            log.warn("HttpMessageNotReadableException :: fieldName: {}, invalidValue: {}", fieldName, invalidValue);
+        } else {
+            log.warn("HttpMessageNotReadableException :: invalidValue: {}", invalidFormatException.getValue());
+        }
         return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                 .body(new ErrorResponse(ApiErrorMessage.JSON_PARSE_ERROR.getMessage()));
     }
🤖 Prompt for AI Agents
In
`@api/src/main/java/com/samhap/kokomen/global/exception/GlobalExceptionHandler.java`
around lines 56 - 62, In GlobalExceptionHandler where you inspect
InvalidFormatException (the block using e.getCause() instanceof
InvalidFormatException invalidFormatException), guard against an empty path
before calling invalidFormatException.getPath().get(0): check if
invalidFormatException.getPath() is null or empty and only extract fieldName and
value when present; otherwise set a safe fallback (e.g., "unknown" or omit
fieldName) and still log the invalidValue. Update the log.warn call to use the
fallback fieldName and keep returning the same BAD_REQUEST ErrorResponse so
behavior is preserved.


log.warn("HttpMessageNotReadableException :: message: {}", message);
log.warn("HttpMessageNotReadableException :: message: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(message));
.body(new ErrorResponse(ApiErrorMessage.INVALID_REQUEST_FORMAT.getMessage()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Exception :: status: {}, message: {}, stackTrace: ", HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage(), e);
log.error("Exception :: status: {}, message: {}, stackTrace: ", HttpStatus.INTERNAL_SERVER_ERROR,
e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("서버에 문제가 발생하였습니다."));
.body(new ErrorResponse(ApiErrorMessage.INTERNAL_SERVER_ERROR.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.samhap.kokomen.global.annotation.Authentication;
import com.samhap.kokomen.global.dto.MemberAuth;
import com.samhap.kokomen.global.exception.ApiErrorMessage;
import com.samhap.kokomen.global.exception.InternalServerErrorException;
import com.samhap.kokomen.global.exception.UnauthorizedException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
Expand Down Expand Up @@ -29,7 +31,7 @@ public Object resolveArgument(MethodParameter parameter,
WebDataBinderFactory binderFactory) throws Exception {
Authentication authentication = parameter.getParameterAnnotation(Authentication.class);
if (authentication == null) {
throw new IllegalStateException("MemberAuth 파라미터는 @Authentication 어노테이션이 있어야 합니다.");
throw new InternalServerErrorException(ApiErrorMessage.AUTHENTICATION_ANNOTATION_REQUIRED.getMessage());
}
boolean authenticationRequired = authentication.required();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
Expand All @@ -47,16 +49,16 @@ public Object resolveArgument(MethodParameter parameter,

private void validateAuthentication(HttpSession session, boolean authenticationRequired) {
if (session == null && authenticationRequired) {
throw new UnauthorizedException("로그인이 필요합니다");
throw new UnauthorizedException(ApiErrorMessage.LOGIN_REQUIRED.getMessage());
}
}

private void validateAuthentication(Long memberId, boolean authenticationRequired) {
if (memberId == null) {
log.error("세션에 MEMBER_ID가 없습니다.");
log.error(ApiErrorMessage.MEMBER_ID_NOT_IN_SESSION.getMessage());
}
if (memberId == null && authenticationRequired) {
throw new IllegalStateException("세션에 MEMBER_ID가 없습니다.");
throw new UnauthorizedException(ApiErrorMessage.MEMBER_ID_NOT_IN_SESSION.getMessage());
}
}
}
4 changes: 2 additions & 2 deletions common/run-test-redis.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/bin/bash

if ! docker ps --format '{{.Names}}' | grep -q '^payment-test-redis$'; then
echo "payment-test-redis 컨테이너가 실행 중이 아닙니다. docker-compose -f test.yml up -d payment-test-redis로 시작합니다..."
docker compose -f test.yml up -d payment-test-redis
echo "payment-test-redis 컨테이너가 실행 중이 아닙니다. docker compose -f test-docker-compose.yml up -d payment-test-redis로 시작합니다..."
docker compose -f "$(git rev-parse --show-toplevel)/test-docker-compose.yml" up -d payment-test-redis
else
echo "payment-test-redis 컨테이너가 이미 실행 중입니다."
fi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.samhap.kokomen.global.exception;

public class InternalServerErrorException extends KokomenException {

public InternalServerErrorException(String message) {
super(message, 500);
}

public InternalServerErrorException(String message, Throwable cause) {
super(message, cause, 500);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.samhap.kokomen.global.exception;

public class NotFoundException extends KokomenException {

public NotFoundException(String message) {
super(message, 404);
}
}
16 changes: 0 additions & 16 deletions common/test.yml

This file was deleted.

Loading