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
22 changes: 18 additions & 4 deletions .github/workflows/cd-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,19 @@ jobs:
run: |
echo "🧹 Cleaning up all existing containers"

# Stop and remove specific containers
# Stop and remove specific containers (로그는 볼륨에 보존됨)
sudo docker stop github-actions-demo || true
sudo docker rm github-actions-demo || true

sudo docker stop elasticsearch || true
sudo docker rm elasticsearch || true

echo "📋 Ensuring log directory exists on host"
sudo mkdir -p /var/log/wayble
sudo chmod 755 /var/log/wayble

echo "🧯 Cleaning up unused Docker networks"
sudo docker system prune -f || true
echo "🧯 Cleaning up unused Docker networks (excluding volumes)"
sudo docker system prune -f --volumes=false || true

- name: Create Docker network if not exists
run: |
Expand Down Expand Up @@ -184,6 +187,18 @@ jobs:
exit 1
fi

# 로그 파일 상태 확인
echo "=== Log Directory Status ==="
ls -la /var/log/wayble/ || echo "Log directory not found"

if [ -f "/var/log/wayble/wayble-error.log" ]; then
echo "✅ Error log file exists"
echo "📊 Error log file size: $(du -h /var/log/wayble/wayble-error.log | cut -f1)"
echo "📅 Last modified: $(stat -c %y /var/log/wayble/wayble-error.log)"
else
echo "ℹ️ No error log file yet (normal for new deployment)"
fi

# ✅ 배포 성공 알림 (Discord)
- name: Send success webhook to Discord
if: success()
Expand All @@ -204,7 +219,6 @@ jobs:




# on: #이 워크플로우가 언제 실행될지 트리거를 정의함.
# pull_request:
# types : [closed] #누군가가 Pull request를 닫았을 때 실행됨.
Expand Down
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ FROM openjdk:17
# 인자 설정 - JAR_File
ARG JAR_FILE=build/libs/*.jar

# 로그 디렉토리 생성 및 권한 설정
RUN mkdir -p /app/logs && chmod 755 /app/logs

# 작업 디렉토리 설정
WORKDIR /app

# jar 파일 복제
COPY ${JAR_FILE} app.jar

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class GlobalExceptionHandler {

@ExceptionHandler(ApplicationException.class)
public ResponseEntity<CommonResponse> handleApplicationException(ApplicationException e, WebRequest request) {
// 비즈니스 예외 로그 기록 (간결하게)

String path = ((ServletWebRequest) request).getRequest().getRequestURI();
String method = ((ServletWebRequest) request).getRequest().getMethod();

Expand All @@ -47,36 +47,31 @@ public ResponseEntity<CommonResponse> handleApplicationException(ApplicationExce

CommonResponse commonResponse = CommonResponse.error(e.getErrorCase());

HttpStatus status = HttpStatus.valueOf(e.getErrorCase().getHttpStatusCode());
//sendToDiscord(e, request, status);

return ResponseEntity
.status(e.getErrorCase().getHttpStatusCode())
.body(commonResponse);
}

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(BindingResult bindingResult,
MethodArgumentNotValidException ex,
WebRequest request) {
String message = bindingResult.getAllErrors().get(0).getDefaultMessage();
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(MethodArgumentNotValidException ex, WebRequest request) {
String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
Comment on lines +55 to +57
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

검증 예외 메시지 추출 시 안전성 보강 (빈 에러/NPE 방지)

getAllErrors().get(0)는 드물게 비어 있을 수 있어 IndexOutOfBoundsException 가능성이 있습니다. 메시지가 null/blank인 경우도 대비해 기본 메시지를 두는 것이 안전합니다.

다음과 같이 방어 로직을 적용해 주세요:

-        String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
+        String message = "요청 검증에 실패했습니다.";
+        if (ex.getBindingResult() != null) {
+            if (!ex.getBindingResult().getAllErrors().isEmpty()) {
+                String m = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
+                if (m != null && !m.isBlank()) {
+                    message = m;
+                }
+            } else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
+                message = ex.getMessage();
+            }
+        } else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
+            message = ex.getMessage();
+        }
📝 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
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(MethodArgumentNotValidException ex, WebRequest request) {
String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(MethodArgumentNotValidException ex, WebRequest request) {
// 기본 메시지 설정
String message = "요청 검증에 실패했습니다.";
if (ex.getBindingResult() != null) {
if (!ex.getBindingResult().getAllErrors().isEmpty()) {
String m = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
if (m != null && !m.isBlank()) {
message = m;
}
} else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
message = ex.getMessage();
}
} else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
message = ex.getMessage();
}
// 이후 로직 계속 (예: CommonResponse 생성 및 반환)
CommonResponse body = CommonResponse.error(message);
return ResponseEntity.badRequest().body(body);
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 55-57, the current use of
ex.getBindingResult().getAllErrors().get(0).getDefaultMessage() can throw
IndexOutOfBoundsException when the error list is empty and may return
null/blank; update the handler to defensively retrieve the first error only
after checking that getAllErrors() is non-null and non-empty, extract
getDefaultMessage() safely (treating null/blank as absent), and fall back to a
sensible default message such as "Invalid request" (or a constant) if no usable
message exists; ensure no NPEs by null-checking the BindingResult and
DefaultMessage and consider trimming the message before returning it in the
ResponseEntity.


// 에러 로그 기록
String path = ((ServletWebRequest) request).getRequest().getRequestURI();
String method = ((ServletWebRequest) request).getRequest().getMethod();
String errorLocation = getErrorLocation(ex);

log.error("Validation Exception 발생 - Method: {}, Path: {}, Message: {}, Location: {}",
method, path, message, errorLocation, ex);
log.warn("Validation Exception - Method: {}, Path: {}, Message: {}, Location: {}",
method, path, message, errorLocation);

CommonResponse commonResponse = CommonResponse.error(400, message);

sendToDiscord(ex, request, HttpStatus.BAD_REQUEST);

return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(commonResponse);
}


/**
* 모든 예상하지 못한 예외 처리
*/
Expand Down Expand Up @@ -107,6 +102,12 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status)
return;
}

// 특정 예외 타입 및 경로에 대한 Discord 알림 제외
if (shouldSkipDiscordNotification(ex, path)) {
log.debug("Discord 알림 제외 - Exception: {}, Path: {}", ex.getClass().getSimpleName(), path);
return;
}

// Embed 필드 구성
DiscordWebhookPayload.Embed embed = new DiscordWebhookPayload.Embed(
"🚨 서버 에러 발생",
Expand Down Expand Up @@ -136,6 +137,108 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status)
}
}

/**
* Discord 알림을 보내지 않을 예외인지 판단
*/
private boolean shouldSkipDiscordNotification(Exception ex, String path) {
String exceptionName = ex.getClass().getSimpleName();
String message = ex.getMessage();

// 1. NoResourceFoundException 제외 (static resource 요청)
if ("NoResourceFoundException".equals(exceptionName)) {
return true;
}

// 2. 특정 경로 패턴 제외
if (isIgnoredPath(path)) {
return true;
}

// 3. 봇이나 크롤러 요청으로 인한 에러 제외
if (isBotOrCrawlerRequest(message)) {
return true;
}

// 4. 기타 불필요한 예외들
if (isIgnoredException(exceptionName, message)) {
return true;
}

return false;
}

/**
* 무시할 경로인지 확인
*/
private boolean isIgnoredPath(String path) {
String[] ignoredPaths = {
"/favicon.ico",
"/index.html",
"/robots.txt",
"/sitemap.xml",
"/apple-touch-icon",
"/.well-known/",
"/wp-admin/",
"/admin/",
"/phpmyadmin/",
"/xmlrpc.php",
"/.env",
"/config.php"
};

for (String ignoredPath : ignoredPaths) {
if (path.contains(ignoredPath)) {
return true;
}
}

return false;
}

Comment on lines +170 to +197
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

isIgnoredPath: 대소문자/널 안전성 보강

요청 경로가 null이거나 대문자 포함 시 매칭 누락 가능성이 있습니다. 소문자 변환 후 비교하도록 안전성 보강을 권장합니다.

-    private boolean isIgnoredPath(String path) {
-        String[] ignoredPaths = {
+    private boolean isIgnoredPath(String path) {
+        if (path == null) return false;
+        String lowerPath = path.toLowerCase();
+        String[] ignoredPaths = {
             "/favicon.ico",
             "/index.html", 
             "/robots.txt",
             "/sitemap.xml",
             "/apple-touch-icon",
             "/.well-known/",
             "/wp-admin/",
             "/admin/",
             "/phpmyadmin/",
             "/xmlrpc.php",
             "/.env",
             "/config.php"
         };
         
-        for (String ignoredPath : ignoredPaths) {
-            if (path.contains(ignoredPath)) {
+        for (String ignoredPath : ignoredPaths) {
+            if (lowerPath.contains(ignoredPath)) {
                 return true;
             }
         }
         
         return false;
     }
📝 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
/**
* 무시할 경로인지 확인
*/
private boolean isIgnoredPath(String path) {
String[] ignoredPaths = {
"/favicon.ico",
"/index.html",
"/robots.txt",
"/sitemap.xml",
"/apple-touch-icon",
"/.well-known/",
"/wp-admin/",
"/admin/",
"/phpmyadmin/",
"/xmlrpc.php",
"/.env",
"/config.php"
};
for (String ignoredPath : ignoredPaths) {
if (path.contains(ignoredPath)) {
return true;
}
}
return false;
}
private boolean isIgnoredPath(String path) {
if (path == null) return false;
String lowerPath = path.toLowerCase();
String[] ignoredPaths = {
"/favicon.ico",
"/index.html",
"/robots.txt",
"/sitemap.xml",
"/apple-touch-icon",
"/.well-known/",
"/wp-admin/",
"/admin/",
"/phpmyadmin/",
"/xmlrpc.php",
"/.env",
"/config.php"
};
for (String ignoredPath : ignoredPaths) {
if (lowerPath.contains(ignoredPath)) {
return true;
}
}
return false;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 170 to 197, the isIgnoredPath method is not null-safe and is
case-sensitive; update it to first handle a null path (return false immediately)
and normalize the incoming path to lowercase (and optionally trim) before
checking contains against the ignoredPaths entries (which can remain lowercase),
so comparisons succeed regardless of input case and avoid NPEs.

/**
* 봇이나 크롤러 요청인지 확인
*/
private boolean isBotOrCrawlerRequest(String message) {
if (message == null) return false;

String[] botIndicators = {
"No static resource",
"Could not resolve view",
"favicon",
"robots.txt"
};

for (String indicator : botIndicators) {
if (message.contains(indicator)) {
return true;
}
}

return false;
}

Comment on lines +198 to +219
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

봇/크롤러 감지: 대소문자 불일치 보완

메시지 비교를 소문자 기준으로 수행해 탐지 누락을 줄이는 것을 권장합니다.

-    private boolean isBotOrCrawlerRequest(String message) {
-        if (message == null) return false;
-        
-        String[] botIndicators = {
-            "No static resource",
-            "Could not resolve view",
-            "favicon",
-            "robots.txt"
-        };
-        
-        for (String indicator : botIndicators) {
-            if (message.contains(indicator)) {
-                return true;
-            }
-        }
-        
-        return false;
-    }
+    private boolean isBotOrCrawlerRequest(String message) {
+        if (message == null) return false;
+
+        String msg = message.toLowerCase();
+        String[] botIndicators = {
+            "no static resource",
+            "could not resolve view",
+            "favicon",
+            "robots.txt"
+        };
+
+        for (String indicator : botIndicators) {
+            if (msg.contains(indicator)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
📝 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
/**
* 봇이나 크롤러 요청인지 확인
*/
private boolean isBotOrCrawlerRequest(String message) {
if (message == null) return false;
String[] botIndicators = {
"No static resource",
"Could not resolve view",
"favicon",
"robots.txt"
};
for (String indicator : botIndicators) {
if (message.contains(indicator)) {
return true;
}
}
return false;
}
private boolean isBotOrCrawlerRequest(String message) {
if (message == null) return false;
String msg = message.toLowerCase();
String[] botIndicators = {
"no static resource",
"could not resolve view",
"favicon",
"robots.txt"
};
for (String indicator : botIndicators) {
if (msg.contains(indicator)) {
return true;
}
}
return false;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 198 to 219, the bot/crawler detection does a case-sensitive
contains check which can miss matches; normalize the input and indicators to a
consistent case (e.g., call message = message.toLowerCase(Locale.ROOT) and
compare against lower-cased indicator strings or call
message.toLowerCase(Locale.ROOT).contains(indicator.toLowerCase(Locale.ROOT)))
so all comparisons are case-insensitive and deterministic.

/**
* 무시할 예외인지 확인
*/
private boolean isIgnoredException(String exceptionName, String message) {
// 클라이언트 연결 종료 관련
if ("ClientAbortException".equals(exceptionName) ||
"BrokenPipeException".equals(exceptionName)) {
return true;
}

// 타임아웃 관련 (너무 빈번한 경우)
if (message != null && (
message.contains("Connection timed out") ||
message.contains("Read timed out") ||
message.contains("Connection reset")
)) {
return true;
}

return false;
}

Comment on lines +220 to +241
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

무시 예외 케이스 보강: broken pipe 등 메시지 기반 추가, 대소문자 보완

실무에서 “Broken pipe”, “Connection reset by peer” 형태의 메시지가 자주 발생합니다. 메시지 기반 케이스를 소문자로 통일해 보강하세요.

-    private boolean isIgnoredException(String exceptionName, String message) {
+    private boolean isIgnoredException(String exceptionName, String message) {
         // 클라이언트 연결 종료 관련
         if ("ClientAbortException".equals(exceptionName) || 
             "BrokenPipeException".equals(exceptionName)) {
             return true;
         }
         
         // 타임아웃 관련 (너무 빈번한 경우)
-        if (message != null && (
-            message.contains("Connection timed out") ||
-            message.contains("Read timed out") ||
-            message.contains("Connection reset")
-        )) {
-            return true;
-        }
+        if (message != null) {
+            String msg = message.toLowerCase();
+            if (msg.contains("connection timed out")
+                || msg.contains("read timed out")
+                || msg.contains("connection reset")
+                || msg.contains("connection reset by peer")
+                || msg.contains("broken pipe")) {
+                return true;
+            }
+        }
         
         return false;
     }
📝 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
/**
* 무시할 예외인지 확인
*/
private boolean isIgnoredException(String exceptionName, String message) {
// 클라이언트 연결 종료 관련
if ("ClientAbortException".equals(exceptionName) ||
"BrokenPipeException".equals(exceptionName)) {
return true;
}
// 타임아웃 관련 (너무 빈번한 경우)
if (message != null && (
message.contains("Connection timed out") ||
message.contains("Read timed out") ||
message.contains("Connection reset")
)) {
return true;
}
return false;
}
/**
* 무시할 예외인지 확인
*/
private boolean isIgnoredException(String exceptionName, String message) {
// 클라이언트 연결 종료 관련
if ("ClientAbortException".equals(exceptionName) ||
"BrokenPipeException".equals(exceptionName)) {
return true;
}
// 타임아웃 및 연결 종료 메시지 처리 (너무 빈번한 경우)
if (message != null) {
String msg = message.toLowerCase();
if (msg.contains("connection timed out")
|| msg.contains("read timed out")
|| msg.contains("connection reset")
|| msg.contains("connection reset by peer")
|| msg.contains("broken pipe")) {
return true;
}
}
return false;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 220 to 241, the ignored-exception checks should be made
case-insensitive and extended to match common message variants (e.g., "broken
pipe", "connection reset by peer"). Change exceptionName comparisons to use
case-insensitive checks (equalsIgnoreCase) and normalize message to lower-case
(after null check) then check contains(...) for "broken pipe", "connection reset
by peer", "connection timed out", "read timed out", and "connection reset" so
all variants are caught reliably.

/**
* 예외의 스택트레이스에서 실제 에러 발생 위치를 추출
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.wayble.server.explore.service.WaybleZoneRecommendService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

Expand All @@ -24,8 +25,9 @@ public CommonResponse<List<WaybleZoneRecommendResponseDto>> getWaybleZonePersona
@Valid @ModelAttribute WaybleZoneRecommendConditionDto conditionDto,
@RequestParam(name = "size", defaultValue = "1") int size) {

Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

보안 컨텍스트에서 userId 추출 시 타입 검증이 누락되었습니다.

SecurityContextHolder에서 Principal을 Long으로 직접 캐스팅하고 있는데, 타입 안전성을 위해 instanceof 검증을 추가해야 합니다. UserController.java의 updateUserInfo 메서드에서는 이런 검증을 수행하고 있습니다.

다음과 같이 타입 검증을 추가하세요:

-        Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (!(authentication.getPrincipal() instanceof Long)) {
+            throw new ApplicationException(UserErrorCase.FORBIDDEN);
+        }
+        Long userId = (Long) authentication.getPrincipal();
📝 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
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof Long)) {
throw new ApplicationException(UserErrorCase.FORBIDDEN);
}
Long userId = (Long) authentication.getPrincipal();
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java
around line 28, the code directly casts
SecurityContextHolder.getContext().getAuthentication().getPrincipal() to Long;
add a type check before casting (e.g., if (principal instanceof Long) { userId =
(Long) principal; } else if (principal instanceof UserDetails) { extract the id
from the UserDetails implementation } else { throw an appropriate
access/authorization exception or return an error response) } so the principal
is validated and handled safely instead of assuming a Long.

List<WaybleZoneRecommendResponseDto> result = waybleZoneRecommendService.getWaybleZonePersonalRecommend(
conditionDto.userId(),
userId,
conditionDto.latitude(),
conditionDto.longitude(),
size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ public record WaybleZoneRecommendConditionDto(
@DecimalMin(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다.")
@DecimalMax(value = "180.0", message = "경도는 180.0 이하여야 합니다.")
@NotNull(message = "경도 입력은 필수입니다.")
Double longitude,

@NotNull(message = "유저 ID는 필수입니다.")
Long userId
Double longitude
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@
import java.util.List;

@RestController
@RequestMapping("/api/v1/users/{userId}/places")
@RequestMapping("/api/v1/users/places")
@RequiredArgsConstructor
public class UserPlaceController {

private final UserPlaceService userPlaceService;


@PostMapping
@Operation(summary = "유저 장소 저장", description = "유저가 웨이블존을 장소로 저장합니다.")
@ApiResponses({
Expand All @@ -34,18 +33,9 @@ public class UserPlaceController {
@ApiResponse(responseCode = "403", description = "권한이 없습니다.")
})
public CommonResponse<String> saveUserPlace(
@PathVariable Long userId,
@RequestBody @Valid UserPlaceRequestDto request
) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof Long)) {
throw new ApplicationException(UserErrorCase.FORBIDDEN);
}
Long tokenUserId = (Long) authentication.getPrincipal();
if (!userId.equals(tokenUserId)) {
throw new ApplicationException(UserErrorCase.FORBIDDEN);
}

Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

타입 안전성을 위한 instanceof 검증이 누락되었습니다.

SecurityContextHolder에서 Principal을 Long으로 직접 캐스팅하고 있는데, UserController.java의 패턴을 따라 타입 검증을 추가해야 합니다.

-        Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (!(authentication.getPrincipal() instanceof Long)) {
+            throw new ApplicationException(UserErrorCase.FORBIDDEN);
+        }
+        Long userId = (Long) authentication.getPrincipal();
📝 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
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof Long)) {
throw new ApplicationException(UserErrorCase.FORBIDDEN);
}
Long userId = (Long) authentication.getPrincipal();
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/user/controller/UserPlaceController.java
around line 38, the code directly casts
SecurityContextHolder.getContext().getAuthentication().getPrincipal() to Long
without checking the runtime type; update it to first check that the principal
is an instance of Long (or extract the expected user id via the same safe
pattern used in UserController.java), then cast; if the check fails, handle it
by throwing an appropriate authentication/illegal state exception or returning
an unauthorized response so the method remains type-safe.

userPlaceService.saveUserPlace(userId, request); // userId 파라미터로 넘김
return CommonResponse.success("장소가 저장되었습니다.");
}
Expand All @@ -61,16 +51,8 @@ public CommonResponse<String> saveUserPlace(
@ApiResponse(responseCode = "403", description = "권한이 없습니다.")
})
public CommonResponse<List<UserPlaceListResponseDto>> getUserPlaces(
@PathVariable Long userId
) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof Long)) {
throw new ApplicationException(UserErrorCase.FORBIDDEN);
}
Long tokenUserId = (Long) authentication.getPrincipal();
if (!userId.equals(tokenUserId)) {
throw new ApplicationException(UserErrorCase.FORBIDDEN);
}
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

타입 안전성을 위한 instanceof 검증이 누락되었습니다.

동일하게 이 메서드에서도 타입 검증이 필요합니다.

-        Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (!(authentication.getPrincipal() instanceof Long)) {
+            throw new ApplicationException(UserErrorCase.FORBIDDEN);
+        }
+        Long userId = (Long) authentication.getPrincipal();
📝 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
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication.getPrincipal() instanceof Long)) {
throw new ApplicationException(UserErrorCase.FORBIDDEN);
}
Long userId = (Long) authentication.getPrincipal();
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/user/controller/UserPlaceController.java
around line 55, the code casts
SecurityContextHolder.getContext().getAuthentication().getPrincipal() directly
to Long without an instanceof check; add a type safety guard: first check if
getPrincipal() instanceof Long before casting, and handle the else case (throw
an appropriate exception such as BadCredentialsException or return a 401/400
response), or extract the id from a known UserDetails implementation if
applicable; ensure the code only uses the Long value after the instanceof check
to avoid ClassCastException.

List<UserPlaceListResponseDto> places = userPlaceService.getUserPlaces(userId);
return CommonResponse.success(places);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@

@Builder
public record UserPlaceListResponseDto(
Long place_id,
Long placeId,
String title,
WaybleZoneDto wayble_zone
WaybleZoneDto waybleZone
) {
@Builder
public record WaybleZoneDto(
Long wayble_zone_id,
Long waybleZoneId,
String name,
String category,
double rating,
String address,
String image_url
String imageUrl
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,16 @@ public List<UserPlaceListResponseDto> getUserPlaces(Long userId) {
String imageUrl = waybleZone.getMainImageUrl();

return UserPlaceListResponseDto.builder()
.place_id(userPlace.getId())
.placeId(userPlace.getId())
.title(userPlace.getTitle())
.wayble_zone(
.waybleZone(
UserPlaceListResponseDto.WaybleZoneDto.builder()
.wayble_zone_id(waybleZone.getId())
.waybleZoneId(waybleZone.getId())
.name(waybleZone.getZoneName())
.category(waybleZone.getZoneType().toString())
.rating(waybleZone.getRating())
.address(waybleZone.getAddress().toFullAddress())
.image_url(imageUrl)
.imageUrl(imageUrl)
.build()
)
.build();
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<maxFileSize>50MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n%ex{2}</pattern>
Expand Down