Skip to content

Conversation

@kaswhy
Copy link
Member

@kaswhy kaswhy commented Aug 26, 2025

📌 연관된 이슈

✨ 작업 내용

  • global 예외 처리 핸들링 수정
  • 에러 응답 수정
  • 도메인 별 예외 클래스, 예외 코드 생성

💬 리뷰 요구사항(선택)

Summary by CodeRabbit

  • 신규 기능
    • 에러 응답을 표준 포맷(ApiResponse)으로 통합하고 요청 경로/타임스탬프 메타데이터를 포함합니다.
    • 도메인별 ErrorCode/예외 타입(User/Study/Recruit/Resource 등)과 메시지 포맷 지원이 추가되었습니다.
  • 버그 수정
    • 누락 헤더, 타입 불일치, 유효성 실패 등에 대해 명확한 400/404/500 응답을 제공합니다.
    • 업로드 파일이 10MB 초과 시 413 응답을 반환합니다.
  • 리팩터링
    • 인증·리소스·스터디 등 에러 처리 경로를 도메인 예외로 정리하고 일부 응답 DTO에 사용자 정보(major, studentId) 필드를 추가했습니다.
  • 설정
    • 정적 리소스 자동 매핑을 비활성화하고 보안 허용 경로를 버전화("/api/v1/...")했습니다.

@kaswhy kaswhy self-assigned this Aug 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

도메인별 ErrorCode/Exception 추가 및 표준화(인증·리소스·스터디·유저·리크루트), GlobalExceptionHandler를 ApiResponse<Void, ErrorMeta> 기반으로 전면 개편, 일부 DTO를 record로 전환하고 서비스 로직 일부를 메서드로 추출했습니다.

Changes

Cohort / File(s) Summary
Global error handling overhaul
src/main/java/inha/gdgoc/global/error/BusinessException.java, .../GlobalErrorCode.java, .../GlobalExceptionHandler.java, .../NotFoundException.java
BusinessException에 Lombok @Getter 적용(명시적 getter 제거). GlobalErrorCodeMISSING_HEADERformat(...) 추가. GlobalExceptionHandlerApiResponse<Void, ErrorMeta> 중심으로 재작성해 여러 핸들러 통합. NotFoundException 삭제.
Auth domain exception migration
src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java, src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java
AuthController의 throw 타입을 BusinessExceptionAuthException으로 변경. AuthException 클래스 추가(BusinessException 상속). 공개 시그니처 불변.
Resource domain refactor
src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java, .../ResourceErrorCode.java, .../controller/ResourceController.java
ResourceException을 enum→class로 변경(비즈니스 예외 상속). ResourceErrorCode enum 추가(INVALID_BIG_FILE). 컨트롤러의 예외 던지기 타입을 ResourceException(ResourceErrorCode)로 조정.
Recruit member exception & DTO update
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java, .../RecruitMemberException.java, .../service/RecruitMemberService.java, .../dto/response/SpecifiedMemberResponse.java, .../controller/RecruitMemberController.java
RecruitMemberErrorCode·RecruitMemberException 추가. Service에서 Optional.get()orElseThrow(RecruitMemberException). SpecifiedMemberResponse를 mutable class→record로 전환(정적 from 팩토리 유지). Controller에 무해한 공백 추가.
Study domain exception adoption
src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java, .../StudyException.java, .../StudyAttendeeErrorCode.java, .../StudyAttendeeException.java, .../service/StudyService.java, .../service/StudyAttendeeService.java
Study/StudyAttendee 에러코드·예외 추가. 기존 RuntimeException 등 경로를 도메인 예외(StudyException, StudyAttendeeException, UserException)로 대체. Attendee 응답 DTO에 major·studentId 등 필드 추가.
User domain exception adoption
src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java, .../UserException.java, .../service/UserService.java
USER_NOT_FOUND ErrorCode와 UserException 추가. findUserById에서 not-found 경로를 UserException(USER_NOT_FOUND)로 변경. getAllUserIds() 제거.
Game ranking refactor
src/main/java/inha/gdgoc/domain/game/service/GameUserService.java
랭킹 계산 로직을 findUserRankings()로 추출하여 saveGameResultAndGetRanking이 재사용하도록 변경(동작 불변).
Misc small formatting / config / security
src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java, src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java, src/main/resources/application-*.yml, src/main/java/inha/gdgoc/global/security/SecurityConfig.java
소소한 공백/포맷 변경, 인증 경로를 버전화된 API 경로로 조정, CORS origin/설정 변경, spring.web.resources.add-mappings: false 추가(로컬/dev/prod).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Controller
  participant Service
  participant GlobalHandler as GlobalExceptionHandler
  note right of GlobalHandler #e6f7ff: ApiResponse<Void, ErrorMeta> 기반 에러 응답

  Client->>Controller: HTTP 요청
  Controller->>Service: 도메인 로직 호출
  alt 정상 흐름
    Service-->>Controller: 결과 반환
    Controller-->>Client: 200 OK + ApiResponse.success
  else 도메인/검증 오류
    Service--xController: throw DomainException (e.g. AuthException, ResourceException, StudyException, UserException, RecruitMemberException)
    Controller--xGlobalHandler: 예외 전파
    GlobalHandler->>GlobalHandler: ErrorCode 결정 → ErrorMeta(URI,timestamp) 생성
    GlobalHandler-->>Client: HTTP status(from ErrorCode) + ApiResponse.error(message, meta)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

당근 들고 달려온 토끼 한 마리,
에러코드들에 이름표를 달아주네.
ApiResponse에 메타를 실어 보내고,
record로 가볍게 인사 나누며 깡충.
로그 한 줄에 웃음 짓는 작은 토끼 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf3203 and 39fc019.

📒 Files selected for processing (1)
  • src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (8 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-197

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

github-actions bot commented Aug 26, 2025

CI status
✅ Assemble 성공
✅ Test 성공

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/main/java/inha/gdgoc/domain/game/service/GameUserService.java (1)

31-35: 하루 경계 계산 개선 필요: 23:59:59 상한은 서브초 누락 위험
현재 findUserRankings()에서

LocalDateTime startOfDay = today.atStartOfDay();        // 00:00:00  
LocalDateTime endOfDay   = today.atTime(23, 59, 59);   // 23:59:59  
gameUserRepository.findAllByCreatedAtBetweenOrderByTypingSpeedAsc(startOfDay, endOfDay);

처럼 조회할 경우, Instant createdAt에 서브초(예: 23:59:59.123…)가 포함된 레코드는 범위 밖으로 빠져나갈 수 있습니다.

  • 최소 변경 (endInclusive 보정)
    - LocalDateTime startOfDay = today.atStartOfDay();
    - LocalDateTime endOfDay   = today.atTime(23, 59, 59);
    + LocalDateTime startOfDay = today.atStartOfDay();
    + LocalDateTime endOfDay   = today.plusDays(1).atStartOfDay().minusNanos(1);
  • 권장(refactor): 반개구간 사용 및 레포지토리 시그니처 변경
    // GameUserRepository.java
    List<GameUser> findAllByCreatedAtGreaterThanEqualAndCreatedAtLessThanOrderByTypingSpeedAsc(
        Instant startInclusive, Instant endExclusive);
    // GameUserService.java 호출부
    Instant start        = today.atStartOfDay(ZoneId.of("Asia/Seoul")).toInstant();
    Instant endExclusive = today.plusDays(1).atStartOfDay(ZoneId.of("Asia/Seoul")).toInstant();
    gameUserRepository.findAllByCreatedAtGreaterThanEqualAndCreatedAtLessThanOrderByTypingSpeedAsc(
        start, endExclusive);

이렇게 변경 시 DB/칼럼 정밀도와 무관하게 하루 전체 데이터를 안전하게 조회할 수 있습니다.

src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java (1)

49-51: RuntimeException 대신 도메인 예외와 에러코드 사용으로 일관성 유지

전역 예외 처리 표준화 취지에 맞게, 업로드 실패도 ResourceException + 전용 ErrorCode로 처리해 주세요. 추가로 로깅도 남기는 것을 권장합니다.

적용 예시 (이 파일 내 변경):

-        } catch (IOException e) {
-            throw new RuntimeException("s3 upload fail" + e);
-        }
+        } catch (IOException e) {
+            // TODO: 업로드 실패 에러코드가 없다면 아래 항목을 ResourceErrorCode에 추가하세요.
+            throw new ResourceException(ResourceErrorCode.S3_UPLOAD_FAILED);
+        }

ResourceErrorCode에 항목 추가(참고용):

// src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java
S3_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다.");

선택 사항:

  • 로깅 도입(@slf4j) 후 log.error("S3 upload failed. userId={}, s3key={}", userId, s3key, e); 추가.
  • BusinessException에 cause를 받는 생성자 추가 시, 원인 예외를 함께 전달 가능(아래 BusinessException 코멘트 참조).
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)

33-41: JSON 변환 실패도 도메인 예외 + 에러코드로 통일

현재 RuntimeException("JSON 변환 오류")를 던지고 있어 전역 에러 포맷과 어긋납니다. 리쿠르트 도메인 전용 에러코드를 정의해 일관화하세요.

적용 예시(이 파일 내 변경):

-                    } catch (Exception e) {
-                        throw new RuntimeException("JSON 변환 오류", e);
-                    }
+                    } catch (Exception e) {
+                        // 전용 에러코드 도입 후 아래처럼 통일
+                        throw new RecruitMemberException(RecruitMemberErrorCode.JSON_CONVERT_FAILED);
+                    }

에러코드 예시(참고용):

// src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java
JSON_CONVERT_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "지원서 응답 직렬화에 실패했습니다.");

원인 예외를 보존하려면 BusinessException(ErrorCode, Throwable) 오버로드(별도 코멘트 참조) 추가 후 그 생성자를 사용하세요.

src/main/java/inha/gdgoc/domain/study/service/StudyService.java (1)

63-67: pageCount 필드 의미 명확화 및 계산 로직 수정 필요

현재 StudyServiceStudyAttendeeService에서 DTO의 pageCount에 총 레코드 수(count, StudyAttendeeCount)를 그대로 할당하고 있습니다.
하지만 DTO 필드 이름(pageCount)만으로는 “총 페이지 수”인지 “총 레코드 수”인지 명확하지 않으므로, 사양에 따라 다음과 같이 리팩터링이 필요합니다.

• 수정 대상 위치

  • src/main/java/inha/gdgoc/domain/study/service/StudyService.java: 63–67행
  • src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java: 69–73행
  • src/main/java/inha/gdgoc/domain/study/dto/StudyListWithMetaDto.java: private Long pageCount;
  • src/main/java/inha/gdgoc/domain/study/dto/StudyAttendeeListWithMetaDto.java: private Long pageCount;

• 제안하는 변경 예시

-                .pageCount(count)
+                // 총 페이지 수 계산 (limit: 페이지당 항목 수)
+                .pageCount((count + limit - 1) / limit)
+                // 별도 필드로 총 레코드 수도 반환할 경우
+                // .totalCount(count)

• 추가 권장 사항

  • DTO에 totalCount (혹은 recordCount) 필드를 추가해 “총 레코드 수”를 명시적으로 반환
  • pageCount는 “총 페이지 수” 계산 결과만 담도록 역할을 분리

위 리팩터링으로 API 응답 구조와 필드 의미가 더욱 명확해집니다.

src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (2)

139-147: 계정 존재 여부 노출(User Enumeration) — 패스워드 재설정 요청

존재하지 않는 계정에서 USER_NOT_FOUND를 던지면 공격자가 사용자 존재 여부를 손쉽게 알아낼 수 있습니다. 보안상 모든 경우 동일한 성공 응답을 반환하고, 내부적으로만 처리하세요.

-        if (userRepository.existsByNameAndEmail(sendingCodeRequest.name(),
-                sendingCodeRequest.email())) {
+        if (userRepository.existsByNameAndEmail(sendingCodeRequest.name(),
+                sendingCodeRequest.email())) {
             String code = mailService.sendAuthCode(sendingCodeRequest.email());
             authCodeService.saveAuthCode(sendingCodeRequest.email(), code);
-
-            return ResponseEntity.ok(ApiResponse.ok(CODE_CREATION_SUCCESS));
-        }
-        throw new AuthException(USER_NOT_FOUND);
+        }
+        // 존재 여부와 무관하게 동일 응답 반환
+        return ResponseEntity.ok(ApiResponse.ok(CODE_CREATION_SUCCESS));

160-175: 비밀번호 재설정: 서버측 검증 부재(중대한 보안 이슈)

현재 confirm 단계에서 이메일과 새 비밀번호만으로 업데이트가 이뤄집니다. 이전 단계에서 코드 검증이 있더라도, 서버는 “해당 이메일이 방금 검증되었는지”를 확인하지 않습니다. 이 상태에선 클라이언트가 verify를 건너뛰고 바로 confirm을 호출해도(또는 타인의 이메일로) 갱신이 가능해질 수 있습니다.

권장 방안:

  • 토큰 기반 흐름 채택: request에서 일회성 resetToken 발급 → confirm에서 resetToken 검증 후 비밀번호 변경.
  • 최소 수정안: PasswordResetRequest에 코드(또는 토큰)를 포함시키고, authCodeService.verify(email, code)를 통과한 경우에만 업데이트 진행. 성공/실패 이후에는 코드를 무효화하세요.

예시(개념용):

if (!authCodeService.verify(passwordResetRequest.email(), passwordResetRequest.code())) {
    throw new AuthException(INVALID_VERIFICATION_CODE);
}

원하시면 DTO/서비스 계층까지 포함한 안전한 reset 토큰 플로우(만료, 1회성, 재사용 방지) 설계/패치안을 제안드리겠습니다.

src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (1)

156-171: 요청한 attendeeId 일부가 조회 결과에 없을 때 NPE 위험 — 누락 검증 및 접근자 체크 일관화

  • studyAttendeeMap.get(...)가 null일 수 있어 setStatus에서 NPE가 발생할 수 있습니다. 요청 ID 전체가 유효한지 검증하고 아닐 경우 4xx로 단락 종료하세요.
  • 접근 권한 체크는 Study.isCreatedBy가 이미 있으니 일관되게 사용하는 편이 가독성이 좋습니다.

아래처럼 방어 코드를 추가하세요.

-        if (!Objects.equals(userId, study.getUser().getId())) {
-            throw new StudyException(STUDY_APPLICANT_ACCESS_DENIED);
-        }
+        if (!study.isCreatedBy(userId)) {
+            throw new StudyException(STUDY_APPLICANT_ACCESS_DENIED);
+        }
@@
-        attendees.forEach(attendee -> {
-            StudyAttendee studyAttendee = studyAttendeeMap.get(attendee.getAttendeeId());
-            studyAttendee.setStatus(attendee.getStatus());
-        });
+        attendees.forEach(attendee -> {
+            StudyAttendee studyAttendee = studyAttendeeMap.get(attendee.getAttendeeId());
+            if (studyAttendee == null) {
+                throw new StudyAttendeeException(STUDY_ATTENDEE_NOT_FOUND);
+            }
+            studyAttendee.setStatus(attendee.getStatus());
+        });
🧹 Nitpick comments (37)
src/main/java/inha/gdgoc/domain/game/service/GameUserService.java (4)

28-29: 메서드 추출은 👍. 다만 읽기 전용 트랜잭션으로 분리 권장

save 로직과 조회 로직을 분리한 점은 재사용성과 응집도 측면에서 좋습니다. 조회 메서드는 읽기 전용 트랜잭션을 명시해 불필요한 flush/lock 을 피하는 편이 안전합니다. 스프링 @transactional로 통일하면 readOnly 옵션을 줄 수 있습니다.

적용 예시(diff):

-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;
@@
-    @Transactional
+    @Transactional
     public List<GameUserResponse> saveGameResultAndGetRanking(GameUserRequest gameUserRequest) {
@@
-    public List<GameUserResponse> findUserRankings() {
+    @Transactional(readOnly = true)
+    public List<GameUserResponse> findUserRankings() {

31-35: 타임존/타임스탬프 일관성 확인 필요

today는 Asia/Seoul 기준(LocalDate)로 계산되지만, createdAt의 보관/저장 타임존(UTC/서버로컬)과 매칭되는지 확인 부탁드립니다. 저장 시점이 UTC이고 조회 경계가 KST(LocalDateTime)라면 경계 미스매치가 발생할 수 있습니다. 가능하면 저장은 UTC(Instant/OffsetDateTime), 조회는 ZonedDateTime으로 경계 산출 후 DB 기준에 맞춰 변환하는 패턴을 권장합니다.


40-42: O(n²) 랭킹 계산: indexOf 사용으로 비효율 → 인덱스 기반 매핑으로 변경

results.indexOf(user)가 매 원소마다 선형 탐색을 하므로 O(n²)입니다. IntStream으로 0-based 인덱스를 활용해 O(n)으로 개선하세요.

적용 예시(diff):

+import java.util.stream.IntStream;
@@
-        return results.stream()
-                .map(user -> new GameUserResponse(results.indexOf(user) + 1, user))
-                .collect(Collectors.toList());
+        return IntStream.range(0, results.size())
+                .mapToObj(i -> new GameUserResponse(i + 1, results.get(i)))
+                .collect(Collectors.toList());

또한 동점(같은 typingSpeed) 처리 정책이 “공동 순위 부여”인지 “단순 1,2,3…”인지 정의가 필요합니다. 공동 순위가 필요하면 다음과 같이 랭크 고정을 적용할 수 있습니다(참고):

int rank = 0;
Double prev = null;
List<GameUserResponse> out = new ArrayList<>(results.size());
for (int i = 0; i < results.size(); i++) {
    GameUser u = results.get(i);
    Double s = u.getTypingSpeed();
    if (!Objects.equals(prev, s)) { rank = i + 1; prev = s; }
    out.add(new GameUserResponse(rank, u));
}
return out;

31-43: 결과 집합 잠재적 폭증 대비 페이징/상한 고려

일별 전체 랭킹을 전량 반환하면 데이터가 누적될수록 비용이 커집니다. 기본 상한(예: top 100) 또는 페이지네이션/커서 기반 API를 고려해 주세요. 상위권만 필요하다면 DB에서 limit 처리하는 것이 가장 효율적입니다.

src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java (1)

64-70: 가입자 조회 메서드 시그니처 정리 및 userId 검증 애노테이션 추가 제안

아래 사항을 선택적으로 보강하면 컨트롤러 단에서 빠른 버그 차단과 코드 스타일 일관성을 유지할 수 있습니다:

  • RecruitMemberController.java
    • 64행: 메서드명과 여는 괄호 사이 불필요한 공백 제거
    • 65행: @RequestParam@jakarta.validation.constraints.Positive 검증 애노테이션 추가
  • RecruitMemberController.java
    • 62행: 더 이상 사용되지 않는 // TODO 코어 멤버 인증 리팩토링 … 주석 삭제 또는 정리

아울러 전역 예외 처리도 정상 동작함을 확인했습니다.
GlobalExceptionHandler는 BusinessException(및 그 하위인 RecruitMemberException)을 ApiResponse.error(...) 형태로 매핑하고 있습니다.

-    public ResponseEntity<ApiResponse<SpecifiedMemberResponse, Void>> getSpecifiedMember (
-            @RequestParam Long userId
+    public ResponseEntity<ApiResponse<SpecifiedMemberResponse, Void>> getSpecifiedMember(
+            @RequestParam @jakarta.validation.constraints.Positive Long userId
src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (1)

6-11: AuthException의 cause 전파용 생성자 추가 방안

AuthException은 ErrorCode만 받는 생성자를 잘 구현했습니다. 다만, 예외 추적성을 위해 cause(원인 예외) 전파용 생성자를 함께 제공하려면, 현재 BusinessException에 해당 시그니처의 생성자가 없음을 확인했습니다.

검토 결과

  • 파일: src/main/java/inha/gdgoc/global/error/BusinessException.java
  • 10행: public BusinessException(ErrorCode errorCode) { … }
    (ErrorCode, Throwable) 생성자는 미구현 상태

필요 시 아래와 같이 두 곳에 생성자 오버로드를 추가하는 것을 권장드립니다:

// src/main/java/inha/gdgoc/global/error/BusinessException.java
 public BusinessException(ErrorCode errorCode) {
     super(errorCode.getMessage());
     this.errorCode = errorCode;
 }
+ public BusinessException(ErrorCode errorCode, Throwable cause) {
+     super(errorCode.getMessage(), cause);
+     this.errorCode = errorCode;
 }

// src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java
 public AuthException(ErrorCode errorCode) {
     super(errorCode);
 }
+ public AuthException(ErrorCode errorCode, Throwable cause) {
+     super(errorCode, cause);
+ }

이 변경은 선택사항이며, 추적성이 중요한 시나리오에서만 반영하시면 됩니다.

src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1)

6-11: 도메인별 예외 도입이 잘 되어 있습니다.

RecruitMemberErrorCode와 함께 사용하기에 충분합니다. 운영 중 원인 예외를 보존해야 할 케이스가 있다면 cause 전파 생성자 추가를 고려해 주세요.

가능 시:

     public RecruitMemberException(ErrorCode errorCode) {
         super(errorCode);
     }
+    public RecruitMemberException(ErrorCode errorCode, Throwable cause) {
+        super(errorCode, cause);
+    }
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (2)

10-10: 헤더·파라미터 누락 예외 분리 처리 권장

현재 GlobalErrorCode에는 헤더 누락만을 위한 MISSING_HEADER 상수만 정의되어 있으며, GlobalExceptionHandler에서도 MissingRequestHeaderException만 처리하고 있습니다.
@RequestParam 누락 시 발생하는 MissingServletRequestParameterException은 별도 매핑이 없어 500 에러 등 의도치 않은 응답이 반환될 수 있으므로, 아래와 같이 파라미터 누락 전용 코드를 추가하는 것을 권장합니다.

• 수정 위치

  • src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java
  • src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java

• 예시 diff

--- a/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java
+++ b/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java
@@ -8,6 +8,7 @@ public enum GlobalErrorCode implements ErrorCode {
     INVALID_JSON_REQUEST(HttpStatus.BAD_REQUEST, "JSON 형식이 올바르지 않습니다."),
     MISSING_HEADER(HttpStatus.BAD_REQUEST, "요청 헤더 '%s'가 누락되었습니다."),
+    MISSING_PARAMETER(HttpStatus.BAD_REQUEST, "요청 파라미터 '%s'가 누락되었습니다."),

     // 403 FORBIDDEN
     INVALID_JWT_REQUEST(HttpStatus.FORBIDDEN, "잘못된 JWT 토큰입니다."),
--- a/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java
+++ b/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java
@@ -44,6 +44,20 @@ public class GlobalExceptionHandler {
         return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                 .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta));
     }
+
+    @ExceptionHandler(MissingServletRequestParameterException.class)
+    public ResponseEntity<ApiResponse<Void, ErrorMeta>> handleMissingRequestParam(
+            MissingServletRequestParameterException ex,
+            HttpServletRequest request
+    ) {
+        log.error("요청 파라미터 {}가 누락되었습니다.", ex.getParameterName());
+        String message = GlobalErrorCode.MISSING_PARAMETER.format(ex.getParameterName());
+        ErrorMeta meta = createMeta(request);
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta));
+    }

이렇게 헤더와 파라미터 누락을 분리 처리하면 디버깅이 용이해지고, 클라이언트에 보다 정확한 오류 메시지를 전달할 수 있습니다.


42-44: ErrorCode 인터페이스에 기본 format(...) 메서드 도입 제안 (선택 리팩토링)

검토 결과,

  • format(...) 메서드는 현재 GlobalErrorCode에서만 제공되고
  • 호출부(GlobalExceptionHandler)에서도 오직 GlobalErrorCode.MISSING_HEADER.format(...), GlobalErrorCode.BAD_REQUEST.format(...) 형태로만 사용되고 있어
    인터페이스에 default 메서드로 올리더라도 기존 호출부에 영향을 주지 않습니다.

안내 사항:

  • 파일: src/main/java/inha/gdgoc/global/error/ErrorCode.java
       public interface ErrorCode {
           HttpStatus getStatus();
           String getMessage();
    
  •    // 기본 포맷터 도입
    
  •    default String format(Object... args) {
    
  •        return String.format(getMessage(), args);
    
  •    }
    
    }
    
    
  • 파일: src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java

    public enum GlobalErrorCode implements ErrorCode {
        // ... 생략
    
    -   @Override
    -   public String format(Object... args) {
    -       return String.format(this.message, args);
    -   }
    }
  • 호출부(예: GlobalExceptionHandler)
    기존 GlobalErrorCode.MISSING_HEADER.format(...) 호출은 그대로 유지 가능합니다.

추가로, 향후 숫자/날짜 포맷이 필요하다면 String.format(Locale.X, …) 도입을 검토하세요.
이 리팩토링은 선택 사항이며, 코드 일관성과 재사용성을 위해 권장합니다.

src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1)

6-11: 일관된 예외 계층 정립 LGTM.

에러코드 기반 도메인 예외로 정리되어 유지보수성이 좋아졌습니다. 필요 시 원인 예외 전파 생성자도 동일 패턴으로 확장 가능합니다.

선택: 직렬화가 필요한 환경이라면 serialVersionUID 추가를 고려하세요.

src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java (1)

27-41: 입력값 검증 보강 제안(파일 비어있음/콘텐츠 타입 등)

  • file.isEmpty() 체크로 빈 파일 업로드 차단.
  • contentType 화이트리스트(PNG/JPEG 등) 검증.
  • 최대 용량은 전역 설정(multipart max-file-size)과도 동기화 권장.

업무 로직 단순화를 위해 해당 검증은 별도 Validator/Service로 분리하는 것도 고려해 주세요.

src/main/java/inha/gdgoc/global/error/BusinessException.java (1)

6-15: 원인 예외(cause) 보존 및 직렬화 ID 추가 제안

S3 업로드 실패 등 하위 계층의 원인 예외를 함께 전달할 수 있도록 오버로드 생성자를 제공하면 추적성/로깅 품질이 향상됩니다. 또한 예외 클래스에는 serialVersionUID 추가를 권장합니다.

 public class BusinessException extends RuntimeException {
 
+    private static final long serialVersionUID = 1L;
+
     private final ErrorCode errorCode;
 
     public BusinessException(ErrorCode errorCode) {
         super(errorCode.getMessage());
         this.errorCode = errorCode;
     }
+
+    public BusinessException(ErrorCode errorCode, Throwable cause) {
+        super(errorCode.getMessage(), cause);
+        this.errorCode = errorCode;
+    }
 }
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (2)

37-38: 매직 스트링 제거 권장: SurveyType 키 상수화

SurveyType.fromType("recruit form")의 문자열은 상수/enum으로 추출해 오탈자 위험을 줄이세요. 예: SurveyType.RECRUIT_FORM 혹은 SurveyType.fromType(SurveyType.RECRUIT_FORM.getType()).


56-61: 조회 전용 트랜잭션 어노테이션 전환 범위 정리 및 제안

현재 코드베이스에는 jakarta.transaction.Transactional과 Spring의 org.springframework.transaction.annotation.Transactional이 혼용되어 사용되고 있습니다. readOnly 속성을 활용하려면 Spring 쪽으로 전환이 필요하며, 전환 범위는 다음과 같습니다.

jakarta.transaction.Transactional 사용 서비스 클래스

  • GameUserService.java
  • RecruitMemberService.java
  • RefreshTokenService.java

테스트 클래스 (조회 전용 X)

  • StudyAttendeeServiceTest.java
  • StudyServiceTest.java

Spring @Transactional 사용 서비스 클래스

  • StudyAttendeeService.java
  • StudyService.java
  • AuthCodeService.java
  • UserService.java

==> 제안

  1. 읽기 전용 메서드가 포함된 서비스 클래스에 한해, 클래스 또는 메서드 레벨에 @Transactional(readOnly = true)을 적용하기 위해 Spring 트랜잭션 어노테이션으로 전환을 검토해주세요.
  2. 전사적으로 일관된 트랜잭션 정책 유지를 위해, 서비스 계층 전체를 Spring @Transactional로 전환하고, 필요 시 Jakarta 쪽 사용을 중단하는 방안을 논의하는 것을 권장합니다.
  3. 전환 대상 파일 예시
    • src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java
    • src/main/java/inha/gdgoc/domain/game/service/GameUserService.java
    • src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java
src/main/java/inha/gdgoc/domain/user/service/UserService.java (2)

65-82: maskEmail의 입력 유효성 가드 추가 권장 (비정상 이메일 대응, 잠재적 IndexOutOfBounds 방지)

현재 email.indexOf("@")-1인 경우 substring(-1)로 런타임 예외가 발생합니다. DB 무결성을 전제하더라도 방어적 가드를 두는 편이 안전합니다.

다음과 같이 가드를 추가해 주세요:

-    private String maskEmail(String email) {
-        int atIndex = email.indexOf("@");
-        if (atIndex <= 5) {
-            return email.charAt(0) + "*****" + email.substring(atIndex);
-        }
+    private String maskEmail(String email) {
+        if (email == null || email.isBlank()) {
+            return "*****";
+        }
+        int atIndex = email.indexOf("@");
+        if (atIndex < 1) { // '@' 없음 또는 로컬 파트가 비어있음
+            return "*****";
+        }
+        if (atIndex <= 5) {
+            return email.charAt(0) + "*****" + email.substring(atIndex);
+        }

40-55: findId() 예외 처리 도메인 일관성 검토 필요

스크립트 실행 결과, USER_NOT_FOUND는 다음과 같이 사용되고 있습니다.

  • User 도메인:
    • UserService.findUserById(...)UserException(USER_NOT_FOUND)
    • StudyAttendeeService 영역 → UserException(USER_NOT_FOUND)
  • 인증(Auth) 도메인:
    • AuthController 영역 → AuthException(USER_NOT_FOUND)

현재 findId() 메서드는 IllegalArgumentException을 던지고 있으며, 요청/응답 DTO(FindIdRequest/FindIdResponse)는 인증 컨텍스트(auth dto)에서 재사용되고 있어, Consumer(컨트롤러) 입장에서 혼동의 여지가 있습니다.

따라서 아래 중 한 방향으로 예외 처리 방식을 팀 내 컨센서스로 확정하고, findId()의 예외 처리 및 import를 일관되게 변경해 주세요.

  • 인증 흐름으로 취급할 경우
     if (user.isEmpty()) {
  •  throw new IllegalArgumentException("해당 정보를 가진 사용자를 찾을 수 없습니다.");
    
  •  throw new AuthException(USER_NOT_FOUND);
    
    }
  • 사용자 도메인 기능으로 취급할 경우
     if (user.isEmpty()) {
  •  throw new IllegalArgumentException("해당 정보를 가진 사용자를 찾을 수 없습니다.");
    
  •  throw new UserException(USER_NOT_FOUND);
    
    }
    
    
  • 대상 파일:
    • src/main/java/inha/gdgoc/domain/user/service/UserService.java (Lines 40–55)

팀 내 도메인 책임 경계와 에러 응답 컨벤션을 정립한 후, 관련 코드를 리팩토링해 주세요.

src/main/java/inha/gdgoc/domain/study/service/StudyService.java (2)

96-102: Optional: Optional 중복 언랩 및 null 전달 제거

이미 빈 값 체크 후인데 study.orElse(null)study.get()을 동시에 사용하고 있습니다. 변수로 한 번만 언랩하고 null 전달을 제거하세요.

-        return detailedStudyResponse(study.orElse(null), study.get().getUser());
+        Study s = study.get();
+        return detailedStudyResponse(s, s.getUser());

130-147: s3 URL 변환 시 null 안전성 보강 제안

imagePath가 null일 경우 s3Service.getS3FileUrl이 NPE나 IllegalArgument를 유발할 수 있습니다. 방어 로직을 추가하는 편이 안전합니다.

-                .imagePath(s3Service.getS3FileUrl(study.getImagePath()))
+                .imagePath(study.getImagePath() == null ? null : s3Service.getS3FileUrl(study.getImagePath()))
src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1)

7-15: enum 생성자 스타일 일관화 및 정적 임포트 충돌 방지 제안

프로젝트 내 다른 도메인 enum은 명시적 생성자를 사용하고 있으므로, UserErrorCode에서도 Lombok @RequiredArgsConstructor 대신 명시적 생성자를 추가하여 스타일을 맞추면 가독성과 일관성을 높일 수 있습니다.
또한 USER_NOT_FOUNDAuthErrorCodeUserErrorCode에 중복 정의되어 있어, 아래 파일들에서 서로 다른 enum을 정적 임포트하며 충돌 가능성이 있습니다.

발견된 정적 임포트 위치:

  • src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java
    import static inha.gdgoc.domain.auth.exception.AuthErrorCode.USER_NOT_FOUND;
  • src/main/java/inha/gdgoc/domain/user/service/UserService.java
    import static inha.gdgoc.domain.user.exception.UserErrorCode.USER_NOT_FOUND;
  • src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java
    import static inha.gdgoc.domain.auth.exception.AuthErrorCode.USER_NOT_FOUND;

충돌 방지 방안:

  • 정적 임포트를 지양하고, AuthErrorCode.USER_NOT_FOUND / UserErrorCode.USER_NOT_FOUND처럼 풀 네임을 사용
  • 파일별로 하나의 도메인 error code만 정적 임포트하도록 제한

또한 선택 사항으로, Lombok 의존을 제거하고 아래와 같이 명시적 생성자를 추가할 수 있습니다:

-import lombok.RequiredArgsConstructor;
-@RequiredArgsConstructor
 public enum UserErrorCode implements ErrorCode {

     // 404 NOT FOUND
     USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다.");

     private final HttpStatus status;
     private final String message;
+
+    UserErrorCode(HttpStatus status, String message) {
+        this.status = status;
+        this.message = message;
+    }
 }
src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (4)

8-9: 에러 코드 네이밍 및 메시지 단위 표기 개선 제안

  • 상수명 INVALID_BIG_FILE은 의미가 모호합니다. 일반적으로 FILE_TOO_LARGE 또는 FILE_SIZE_EXCEEDED가 더 직관적입니다.
  • 메시지의 단위가 10Mb(megabit)로 표기되어 있습니다. 파일 용량은 보통 바이트 단위이므로 10MB(megabyte)로 수정하는 것이 적절합니다.

하위 호환성(호출부 상수명 변경)을 고려해야 하므로, 수용 시 전역 참조를 함께 업데이트하세요.

다음 변경을 제안합니다:

-    INVALID_BIG_FILE(HttpStatus.PAYLOAD_TOO_LARGE, "파일 크기는 10Mb를 넘을 수 없습니다.");
+    FILE_TOO_LARGE(HttpStatus.PAYLOAD_TOO_LARGE, "파일 크기는 10MB를 넘을 수 없습니다.");

3-7: 보일러플레이트 제거 및 스타일 일관화(@requiredargsconstructor 사용)

동일 프로젝트 내 다른 enum(ErrorCode 구현체)와의 스타일 일관성을 위해 Lombok의 @RequiredArgsConstructor 적용을 권장합니다. 아래 diff는 import 추가와 enum 선언부에 어노테이션을 부여합니다.

 import inha.gdgoc.global.error.ErrorCode;
 import org.springframework.http.HttpStatus;
+import lombok.RequiredArgsConstructor;

-public enum ResourceErrorCode implements ErrorCode {
+@RequiredArgsConstructor
+public enum ResourceErrorCode implements ErrorCode {

14-17: 생성자 제거(어노테이션 적용 시 불필요 코드)

위 코멘트의 @RequiredArgsConstructor를 적용한다면, 수동 생성자는 제거 가능합니다.

-    ResourceErrorCode(HttpStatus status, String message) {
-        this.status = status;
-        this.message = message;
-    }

9-9: 하드코딩된 최대 파일 크기 제거(설정값과 메시지 동기화)

메시지의 “10MB” 상수는 실제 업로드 제한 설정(spring.servlet.multipart.max-file-size, 리버스 프록시/게이트웨이 제한 등)과 상이할 수 있습니다. 설정값을 단일 소스로 관리해 메시지를 동적으로 구성하거나, 메시지를 일반화(“허용된 최대 파일 크기를 초과했습니다”)하는 방식을 고려해 주세요.

src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (1)

7-8: ErrorCode enum 스타일 일관화

본 파일은 @RequiredArgsConstructor를 사용하지만, 다른 ErrorCode(enum)들은 수동 생성자를 사용합니다. 팀 규칙에 맞춰 한 가지 방식으로 통일하는 것이 유지보수에 유리합니다. 두 가지 대안:

src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (1)

14-16: 에러 코드명과 메시지 의미 불일치

STUDY_ALREADY_APPLIED(이미 지원함)인데 메시지는 “이미 가입한 스터디입니다.”로 “가입”을 사용합니다. 의미를 일치시키는 것이 좋습니다. 우선 위험도 낮은 메시지 수정만 제안합니다.

-    STUDY_ALREADY_APPLIED(HttpStatus.CONFLICT, "이미 가입한 스터디입니다.");
+    STUDY_ALREADY_APPLIED(HttpStatus.CONFLICT, "이미 지원한 스터디입니다.");
src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (1)

5-10: 필드 네이밍 오탈자 정정(isPayed → isPaid) + JSON 호환성 유지

비즈니스·영문 규칙상 ‘paid’가 맞습니다. API 호환을 위해 JSON 필드는 유지하고, 코드 내 필드명만 교정하는 방식을 권장합니다. 아래는 레코드 필드에 @JsonProperty("isPayed")를 부여하여 외부 JSON은 그대로, 내부 명칭만 isPaid로 사용하는 예시이며, 앞선 NPE 방지도 반영했습니다.

-import inha.gdgoc.domain.recruit.entity.RecruitMember;
+import inha.gdgoc.domain.recruit.entity.RecruitMember;
+import com.fasterxml.jackson.annotation.JsonProperty;
@@
-public record SpecifiedMemberResponse(
+public record SpecifiedMemberResponse(
         String name,
         String major,
         String studentId,
-        boolean isPayed
+        @JsonProperty("isPayed") boolean isPaid
 ) {
@@
     public static SpecifiedMemberResponse from(RecruitMember member) {
         return new SpecifiedMemberResponse(
                 member.getName(),
                 member.getMajor(),
                 member.getStudentId(),
-                member.getIsPayed()
+                Boolean.TRUE.equals(member.getIsPayed())
         );
     }
 }

Also applies to: 12-18

src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (3)

70-70: 반환 타입 제네릭 명시로 컨트롤러 시그니처 일관성 확보

다른 메서드들과 동일하게 ResponseEntity<ApiResponse<AccessTokenResponse, Void>>로 명시하면 가독성이 좋아집니다.

-    public ResponseEntity<?> refreshAccessToken(
+    public ResponseEntity<ApiResponse<AccessTokenResponse, Void>> refreshAccessToken(

135-137: 메서드 명 오타/의미 개선

responseResponseEntity는 오타로 보입니다. 의도를 드러내도록 requestPasswordReset 등으로 변경하세요.

-    public ResponseEntity<ApiResponse<Void, Void>> responseResponseEntity(
+    public ResponseEntity<ApiResponse<Void, Void>> requestPasswordReset(

95-96: 요청 DTO 유효성 검증 활성화(@Valid)

입력값 유효성 검증을 위해 컨트롤러 파라미터에 @Valid를 부여하세요. (예: 이메일 형식, 비밀번호 정책 등)

-    public ResponseEntity<ApiResponse<LoginResponse, Void>> login(
-            @RequestBody UserLoginRequest userLoginRequest,
+    public ResponseEntity<ApiResponse<LoginResponse, Void>> login(
+            @Valid @RequestBody UserLoginRequest userLoginRequest,
@@
-    public ResponseEntity<ApiResponse<Void, Void>> requestPasswordReset(
-            @RequestBody SendingCodeRequest sendingCodeRequest
+    public ResponseEntity<ApiResponse<Void, Void>> requestPasswordReset(
+            @Valid @RequestBody SendingCodeRequest sendingCodeRequest
@@
-    public ResponseEntity<ApiResponse<CodeVerificationResponse, Void>> verifyCode(
-            @RequestBody CodeVerificationRequest request
+    public ResponseEntity<ApiResponse<CodeVerificationResponse, Void>> verifyCode(
+            @Valid @RequestBody CodeVerificationRequest request
@@
-    public ResponseEntity<ApiResponse<Void, Void>> resetPassword(
-            @RequestBody PasswordResetRequest passwordResetRequest
+    public ResponseEntity<ApiResponse<Void, Void>> resetPassword(
+            @Valid @RequestBody PasswordResetRequest passwordResetRequest

추가로 상단 import에 다음을 포함하세요(선택):

import jakarta.validation.Valid;

Also applies to: 136-137, 151-152, 162-163

src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (3)

62-73: pageCount에 ‘총 건수’를 그대로 세팅하는 듯합니다 — 총 페이지 수 계산·네이밍 정리 권장

findAllByStudyIdStudyAttendeeCount는 네이밍상 “총 참석자 수”를 반환할 가능성이 큽니다. 빌더의 pageCount가 “총 페이지 수”라면 현재 값이 부정확합니다. 또한 지역변수 StudyAttendeeCount는 카멜케이스 규칙을 위배합니다.

  • DTO의 pageCount 의미가 “총 페이지 수”인지 확인 바랍니다.
  • 아래처럼 총 페이지 수를 계산하고 변수명을 정리하는 것을 제안합니다.
-        Long StudyAttendeeCount = studyAttendeeRepository.findAllByStudyIdStudyAttendeeCount(
-                studyId);
+        Long totalCount = studyAttendeeRepository.findAllByStudyIdStudyAttendeeCount(studyId);
         List<StudyAttendeeDto> attendees = studyAttendeeRepository.pageAllByStudyId(studyId, limit,
                         offset).stream()
                 .map(this::studyAttendeeEntityToDto)
                 .toList();
 
         return StudyAttendeeListWithMetaDto.builder()
                 .attendees(attendees)
-                .pageCount(StudyAttendeeCount)
+                .pageCount((totalCount + limit - 1) / limit)  // ceil(totalCount / limit)
                 .page(page)
                 .build();

184-187: 중복 지원 방지 로직은 서비스 레벨에서 OK, 단 DB 유니크 인덱스로 강제 권장

서비스 레벨의 existsByStudyIdAndUserId 체크는 경쟁상황에서 허점을 가질 수 있습니다. (동시 요청) DB에 (study_id, user_id) 유니크 인덱스를 추가하여 최종 무결성을 보장하고, 위반 시 STUDY_ALREADY_APPLIED로 매핑하세요.


76-90: 메서드 파라미터 및 변수명 명확화 권장

  • StudyAttendeeService#getStudyAttendee 시그니처의 세 번째 파라미터명을 attendeeIdattendeeUserId로 변경
  • 지역 변수 stuatAttendeeUser 오타 수정 → attendeeUser
  • 파라미터명 변경은 위치 기반 인자 전달 방식이므로, StudyAttendeeController와 StudyAttendeeServiceTest의 호출부에는 영향이 없습니다. (추가 수정 불필요)
  • (선택) 컨트롤러 메서드의 @PathVariable 변수명도 변경할 경우, 기존 경로 변수명({attendeeId})과 매핑하려면 @PathVariable("attendeeId") Long attendeeUserId처럼 어노테이션 속성 지정이 필요합니다.

변경 예시:

--- src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java
@@ -76,7 +76,7 @@
-    public GetStudyAttendeeResponse getStudyAttendee(Long authenticatedUser, Long studyId,
-            Long attendeeId) {
+    public GetStudyAttendeeResponse getStudyAttendee(Long authenticatedUser, Long studyId,
+            Long attendeeUserId) {
         Study study = studyRepository.findById(studyId)
                 .orElseThrow(() -> new StudyException(STUDY_NOT_FOUND));
         if (!userRepository.existsById(attendeeUserId)) {
@@ -86,7 +86,7 @@
         StudyAttendee studyAttendee = studyAttendeeRepository
                 .findStudyAttendeeByStudyIdAndUserId(studyId, attendeeUserId)
                 .orElseThrow(() -> new StudyAttendeeException(STUDY_ATTENDEE_NOT_FOUND));
-        User stuatAttendeeUser = studyAttendee.getUser();
+        User attendeeUser = studyAttendee.getUser();

         return GetStudyAttendeeResponse.builder()
-                .name(stuatAttendeeUser.getName())
-                .phone(stuatAttendeeUser.getPhoneNumber())
-                .major(stuatAttendeeUser.getMajor())
-                .studentId(stuatAttendeeUser.getStudentId())
+                .name(attendeeUser.getName())
+                .phone(attendeeUser.getPhoneNumber())
+                .major(attendeeUser.getMajor())
+                .studentId(attendeeUser.getStudentId())
                 .introduce(studyAttendee.getIntroduce())
                 .activityTime(studyAttendee.getActivityTime())
                 .build();
src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (5)

20-32: 스택트레이스 미출력 — 운영 디버깅을 위해 예외 객체와 함께 로그 남기기

현재 메시지만 출력되어 원인 추적이 어렵습니다. 예외 객체를 함께 넘겨 스택트레이스를 남기세요.

-        log.error("BusinessException 발생: {}", ex.getMessage());
+        log.error("BusinessException 발생: {}", ex.getMessage(), ex);

48-63: 검증 메시지 추출 보강 — FieldError 우선 + 오류 없음 방어로 안전성 확보

getAllErrors().get(0)는 오류가 없을 때 IndexOutOfBoundsException를 유발할 수 있습니다. 또한 필드 에러 메시지를 우선 사용하는 편이 친화적입니다.

-        String message = ex.getBindingResult()
-                .getAllErrors()
-                .get(0)
-                .getDefaultMessage();
+        String message;
+        if (ex.getBindingResult().getFieldError() != null) {
+            message = ex.getBindingResult().getFieldError().getDefaultMessage();
+        } else if (ex.getBindingResult().getGlobalError() != null) {
+            message = ex.getBindingResult().getGlobalError().getDefaultMessage();
+        } else {
+            message = GlobalErrorCode.BAD_REQUEST.getMessage();
+        }
-        log.error("MethodArgumentNotValidException 발생: {}", message);
+        log.error("MethodArgumentNotValidException 발생: {}", message, ex);

65-77: TypeMismatch 로그/메시지 개선 (파라미터 값·타입 포함)

운영 분석 시 어떤 값이 어떤 타입에 매칭 실패했는지 함께 남기면 유용합니다. 또한 로그에 스택트레이스를 포함하세요.

-        log.error("MethodArgumentTypeMismatchException 발생: {}", ex.getMessage());
-        String message = GlobalErrorCode.BAD_REQUEST.format(ex.getName());
+        log.error("MethodArgumentTypeMismatchException 발생: {}", ex.getMessage(), ex);
+        String message = GlobalErrorCode.BAD_REQUEST.format(ex.getName())
+                + " (value=" + ex.getValue() + ", requiredType="
+                + (ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown")
+                + ")";

92-103: 500 핸들러도 스택트레이스를 로그에 포함하세요

운영 이슈 분석을 위해 500 처리에서도 예외 객체를 함께 기록하는 것이 좋습니다.

-        log.error("서버 내부 오류 발생: {}", ex.getMessage());
+        log.error("서버 내부 오류 발생: {}", ex.getMessage(), ex);

16-18: 통일된 에러 스키마 유지 위해 405/JSON 파싱 오류 핸들러 추가 제안

현 구성에서는 405(Method Not Allowed), JSON 파싱 오류(HttpMessageNotReadableException)가 스프링 기본 응답으로 나갈 수 있습니다. 전역 스키마(ApiResponse<Void, ErrorMeta>)를 유지하려면 아래 핸들러를 추가하는 것을 권장합니다.

추가 메서드 예시(클래스 내부 어느 곳에나):

@ExceptionHandler(org.springframework.web.HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiResponse<Void, ErrorMeta>> handleMethodNotSupported(
        org.springframework.web.HttpRequestMethodNotSupportedException ex,
        HttpServletRequest request
) {
    log.error("HttpRequestMethodNotSupportedException 발생: {}", ex.getMessage(), ex);
    ErrorMeta meta = createMeta(request);
    return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
            .body(ApiResponse.error(GlobalErrorCode.METHOD_NOT_ALLOWED, meta));
}

@ExceptionHandler(org.springframework.http.converter.HttpMessageNotReadableException.class)
public ResponseEntity<ApiResponse<Void, ErrorMeta>> handleNotReadable(
        org.springframework.http.converter.HttpMessageNotReadableException ex,
        HttpServletRequest request
) {
    log.error("HttpMessageNotReadableException 발생: {}", ex.getMessage(), ex);
    ErrorMeta meta = createMeta(request);
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(ApiResponse.error(GlobalErrorCode.BAD_REQUEST, meta));
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f5bff8b and d8c56bb.

📒 Files selected for processing (24)
  • src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (7 hunks)
  • src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/game/service/GameUserService.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (8 hunks)
  • src/main/java/inha/gdgoc/domain/study/service/StudyService.java (5 hunks)
  • src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/user/service/UserService.java (2 hunks)
  • src/main/java/inha/gdgoc/global/error/BusinessException.java (1 hunks)
  • src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (2 hunks)
  • src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (1 hunks)
  • src/main/java/inha/gdgoc/global/error/NotFoundException.java (0 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/inha/gdgoc/global/error/NotFoundException.java
🧰 Additional context used
🧬 Code graph analysis (22)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (2)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (3)
  • ErrorCode (5-10)
  • getStatus (7-7)
  • getMessage (9-9)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)
  • Getter (7-23)
src/main/java/inha/gdgoc/domain/user/exception/UserException.java (2)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (3)
  • AuthErrorCode (6-35)
  • AuthErrorCode (21-24)
  • Override (31-34)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)
  • ErrorResponse (13-16)
src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (2)
  • AuthErrorCode (6-35)
  • Override (26-29)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (2)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (3)
  • AuthErrorCode (6-35)
  • Override (31-34)
  • AuthErrorCode (21-24)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)
  • ErrorResponse (13-16)
src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (3)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (4)
  • AuthErrorCode (6-35)
  • AuthErrorCode (21-24)
  • Override (26-29)
  • Override (31-34)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)
  • ErrorResponse (13-16)
src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (5)
src/main/java/inha/gdgoc/domain/study/entity/StudyAttendee.java (3)
  • Entity (22-77)
  • create (51-65)
  • setStatus (67-69)
src/main/java/inha/gdgoc/domain/study/controller/message/StudyAttendeeMessage.java (1)
  • StudyAttendeeMessage (3-8)
src/main/java/inha/gdgoc/domain/user/entity/User.java (1)
  • addStudyAttendee (112-117)
src/main/java/inha/gdgoc/domain/study/entity/Study.java (1)
  • Entity (29-129)
src/main/java/inha/gdgoc/domain/study/controller/StudyAttendeeController.java (4)
  • PostMapping (75-89)
  • RequestMapping (31-102)
  • GetMapping (59-72)
  • PatchMapping (91-101)
src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (3)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (2)
  • ErrorCode (5-10)
  • getStatus (7-7)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (4)
  • AuthErrorCode (6-35)
  • Override (26-29)
  • AuthErrorCode (21-24)
  • Override (31-34)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)
  • Getter (7-23)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (3)
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)
  • RequiredArgsConstructor (20-62)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (4)
  • AuthErrorCode (6-35)
  • AuthErrorCode (21-24)
  • Override (26-29)
  • Override (31-34)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (2)
  • ErrorCode (5-10)
  • getStatus (7-7)
src/main/java/inha/gdgoc/domain/game/service/GameUserService.java (2)
src/main/java/inha/gdgoc/domain/game/controller/message/GameUserMessage.java (1)
  • GameUserMessage (3-6)
src/main/java/inha/gdgoc/domain/game/controller/GameUserController.java (3)
  • GetMapping (35-40)
  • PostMapping (26-33)
  • RequestMapping (19-41)
src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (2)
src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (1)
  • AuthException (6-11)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (1)
  • AuthErrorCode (6-35)
src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java (1)
src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (1)
  • ResourceException (6-11)
src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (3)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (4)
  • AuthErrorCode (6-35)
  • Override (26-29)
  • Override (31-34)
  • AuthErrorCode (21-24)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)
  • Getter (7-23)
src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (3)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (3)
  • AuthErrorCode (6-35)
  • AuthErrorCode (21-24)
  • Override (26-29)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)
  • Getter (7-23)
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (2)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1)
  • RecruitMemberException (6-11)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (3)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (2)
  • ErrorCode (5-10)
  • getStatus (7-7)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (4)
  • AuthErrorCode (6-35)
  • Override (26-29)
  • AuthErrorCode (21-24)
  • Override (31-34)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (2)
  • Getter (7-23)
  • ErrorResponse (13-16)
src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (3)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (4)
  • AuthErrorCode (6-35)
  • Override (26-29)
  • AuthErrorCode (21-24)
  • Override (31-34)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (2)
  • ErrorCode (5-10)
  • getStatus (7-7)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (2)
  • Getter (7-23)
  • ErrorResponse (13-16)
src/main/java/inha/gdgoc/domain/study/service/StudyService.java (2)
src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1)
  • StudyException (6-11)
src/main/java/inha/gdgoc/domain/study/entity/Study.java (1)
  • Entity (29-129)
src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (2)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/global/entity/BaseEntity.java (1)
  • Getter (12-24)
src/main/java/inha/gdgoc/domain/user/service/UserService.java (1)
src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1)
  • UserException (6-11)
src/main/java/inha/gdgoc/global/error/BusinessException.java (3)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (2)
  • Getter (7-23)
  • ErrorResponse (13-16)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (2)
  • ErrorCode (5-10)
  • getMessage (9-9)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (2)
  • Override (26-29)
  • Override (31-34)
src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (3)
src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (1)
  • StudyAttendeeException (6-11)
src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1)
  • StudyException (6-11)
src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1)
  • UserException (6-11)
src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (2)
src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (1)
  • Slf4j (47-176)
src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java (1)
  • JsonInclude (6-36)
🔇 Additional comments (18)
src/main/java/inha/gdgoc/domain/game/service/GameUserService.java (1)

37-38: 정렬 방향 확인 요청: 타이핑 속도(typingSpeed)는 클수록 상위?

현재 Asc 정렬은 값이 작은 유저가 1등이 됩니다. 비즈니스 정의가 “속도가 빠를수록(값이 클수록) 상위”라면 Desc가 맞습니다. 레포지토리 메서드를 …OrderByTypingSpeedDesc로 바꾸는 편이 명확합니다. 필요시 컨트롤러 메시지/문서도 함께 맞춰주세요.

src/main/java/inha/gdgoc/domain/user/exception/UserException.java (2)

6-10: 도메인 전용 예외 타입 도입 LGTM

BusinessException 기반으로 ErrorCode만 받도록 제한한 생성자 구성, 현재 아키텍처와 일관적입니다.


1-11: USER_NOT_FOUND 중복 정의 확인 및 통합 검토 요청

USER_NOT_FOUND 코드가 Auth 도메인(AuthErrorCode)과 User 도메인(UserErrorCode)에 각각 정의되어 있으며, 여러 서비스에서 서로 다른 에러코드를 사용하고 있습니다. 이로 인해 에러 핸들링 일관성이 저해되고, 혼선이 발생할 수 있으므로 다음 사항을 검토해주세요.

  • 정의 위치
    • src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (라인 11)
      USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다.");
    • src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (라인 16)
      USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다");
  • 사용처
    • User 도메인
      • UserService.java (라인 37): throw new UserException(USER_NOT_FOUND);
    • Auth 도메인
      • AuthController.java (라인 114, 146, 167): throw new AuthException(USER_NOT_FOUND);
    • 교차 도메인
      • StudyAttendeeService.java (라인 81, 123): throw new UserException(USER_NOT_FOUND); (AuthErrorCode 사용)

제안

  • 사용자 조회 실패 에러는 UserErrorCode.USER_NOT_FOUND 하나로 통합하고, Auth나 타 도메인에서는 이를 재사용하도록 변경
  • 또는 Auth 전용 에러임을 명확히 드러내기 위해 AUTH_USER_NOT_FOUND 등으로 코드명을 변경

[eoptional_refactors_recommended]

src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (1)

6-10: 일관된 예외 계층 적용 LGTM

BusinessException 확장 및 ErrorCode 주입 방식이 전역 예외 처리와 잘 맞습니다.

src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java (1)

40-41: 대용량 파일 검증 시 도메인 예외로 전환된 점 좋습니다

ResourceException(ResourceErrorCode.INVALID_BIG_FILE)로 일관된 에러 응답을 기대할 수 있습니다.

src/main/java/inha/gdgoc/global/error/BusinessException.java (1)

5-15: Lombok @Getter 도입으로 보일러플레이트 제거, 일관성 유지 LGTM

에러 메시지를 super(errorCode.getMessage())로 설정하는 패턴도 전역 핸들러와 합치됩니다.

src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)

56-61: 조회 로직의 예외 처리 및 DTO 팩토리 사용 전환 LGTM

orElseThrow(new RecruitMemberException(RECRUIT_MEMBER_NOT_FOUND))로 명확해졌고, SpecifiedMemberResponse.from(member)로 DTO 생성 책임이 응집되었습니다.

src/main/java/inha/gdgoc/domain/user/service/UserService.java (1)

3-3: 도메인별 에러코드 도입 사용은 적절합니다

USER_NOT_FOUND를 정적 임포트하여 서비스 계층에서 도메인 예외로 승격한 방향이 표준화 정책과 부합합니다.

src/main/java/inha/gdgoc/domain/study/service/StudyService.java (2)

3-5: 도메인 예외 전환 전반 LGTM

INVALID_PAGE, STUDY_NOT_FOUND를 통한 StudyException 전환이 글로벌 에러 정책과 일치합니다. 런타임 예외 제거 방향 좋습니다.

Also applies to: 17-18, 47-50


150-157: GetDetailedStudyResponse 포맷팅 변경만 있음 – 로직 영향 없음

라인 브레이크만 변경되었으며, 인자 순서/의미는 동일해 보입니다.

src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (1)

6-16: 에러코드 구성 적절, 메시지/HTTP 매핑 합리적

INVALID_PAGE, STUDY_APPLICANT_ACCESS_DENIED, STUDY_NOT_FOUND로 클라이언트 피드백을 명확히 했습니다. 전반적으로 글로벌 규약과 일치합니다.

Also applies to: 17-34

src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (1)

6-11: 이전 enum 상수 사용 흔적 없음 – 검증 완료

rg 검색 결과, ResourceException. 형태의 레거시 참조는 없었으며, new ResourceException(...) 호출은 오직 아래 위치에서만 확인되었습니다.

  • src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java:40 (throw new ResourceException(ResourceErrorCode.INVALID_BIG_FILE);)

과거 enum 상수 방식의 잔재가 없음을 확인했습니다.

src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (1)

10-12: LGTM — 404 Not Found 매핑 적절

RECRUIT_MEMBER_NOT_FOUND를 404로 매핑한 점과 메시지 표현이 명확합니다. 전역 예외 처리 체계와도 잘 맞습니다.

src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (1)

9-10: 페이지 인덱스 기준 확인 필요

메시지가 “page가 1보다 작을 수 없습니다.”로 1-based 기준을 가정합니다. Spring Pageable 기본은 0-based 입니다. API 스펙이 1-based라면, 컨트롤러/리졸버에서 1→0 변환을 일괄 처리하는지 확인 바랍니다. 불일치 시 off-by-one 오류가 날 수 있습니다.

src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (2)

108-110: 익명 인증 처리 확인(AnonymousAuthenticationToken 케이스)

isAuthenticated()true여도 익명 토큰인 경우가 있습니다. 익명 사용자를 명시적으로 차단하거나, 메서드에 @PreAuthorize("isAuthenticated()")를 적용하는 방안을 검토하세요. 간단한 보완 예시는 아래와 같습니다.

-        if (authentication == null || !authentication.isAuthenticated()) {
+        if (authentication == null
+                || !authentication.isAuthenticated()
+                || "anonymousUser".equals(String.valueOf(authentication.getPrincipal()))) {
             throw new AuthException(UNAUTHORIZED_USER);
         }

또는 컨트롤러 메서드에 @PreAuthorize("isAuthenticated()") 추가(권장).


161-163: throws 절 유지가 필요합니다

resetPassword 메서드 내부에서

foundUser.updatePassword(passwordResetRequest.password());

를 호출하고 있으며, 이 updatePassword 메서드는 다음과 같이 NoSuchAlgorithmException, InvalidKeyException을 throws 하도록 선언되어 있습니다.

  • src/main/java/inha/gdgoc/domain/user/entity/User.java:
    public void updatePassword(String password) throws NoSuchAlgorithmException, InvalidKeyException {
        this.password = EncryptUtil.encrypt(password, this.salt);
    }
  • EncryptUtil.encrypt 및 generateHashedValue 메서드 역시 동일 예외를 던집니다.

따라서 현재 구현에서는 controller에서 예외를 잡아 처리하지 않으려면 throws 절을 그대로 유지해야 합니다. 변경이 필요 없다면 해당 제안은 무시하셔도 됩니다.

Likely an incorrect or invalid review comment.

src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (2)

122-126: USER_NOT_FOUND 에러코드 도메인 일치 재확인

위 임포트 수정이 반영되면 여기서도 UserErrorCode.USER_NOT_FOUND가 사용됩니다. 이 변경이 전역 응답 스킴과 상태코드에 영향을 주지 않는지 한 번만 빌드·실행으로 확인해 주세요.


174-182: DTO 매핑 확장(LGTM)

이름/전공/학번 포함 매핑은 일관되고 명확합니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (2)

158-172: Null 안전성: 존재하지 않는 attendeeId 업데이트 시 NPE 위험

studyAttendeeMap.get(...)가 null이면 즉시 setStatus에서 NPE가 발생합니다. 요청에 포함된 모든 ID가 실제 스터디의 참석자 목록에 속하는지 검증하고, 누락 시 도메인 예외를 던지세요.

권장 diff:

         attendees.forEach(attendee -> {
             StudyAttendee studyAttendee = studyAttendeeMap.get(attendee.getAttendeeId());
-            studyAttendee.setStatus(attendee.getStatus());
+            if (studyAttendee == null) {
+                throw new StudyAttendeeException(STUDY_ATTENDEE_NOT_FOUND);
+            }
+            studyAttendee.setStatus(attendee.getStatus());
         });

53-74: StudyAttendeeService: pageCount에 총 레코드 수를 할당하는 로직 수정 및 변수명 정리
StudyAttendeeListWithMetaDto에는 Javadoc이 없어 필드 의미를 명확히 파악할 수 없으나, 기존 StudyService의 구현(.pageCount(count))과 관례를 볼 때 pageCount는 “총 페이지 수”를 기대하는 것으로 보입니다. 현재 코드는 총 레코드 수(totalCount)를 그대로 내려 보내고 있어,

  1. 변수명(StudyAttendeeCount)이 UpperCamelCase이고 의미도 혼동할 수 있으며
  2. pageCount = ceil(totalCount / pageSize)로 계산되어야 합니다.

아래 위치에서 리팩터링을 적용해주세요.

  • 파일: src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java
    범위: 메서드 getStudyAttendeeList(Long, Optional<Long>) 내부 (약 53–74행)

권장 diff:

     public StudyAttendeeListWithMetaDto getStudyAttendeeList(Long studyId, Optional<Long> _page) {
         Long page = _page.orElse(1L);
         if (page < 1) {
             throw new StudyAttendeeException(INVALID_PAGE);
         }

-        Long limit = STUDY_ATTENDEE_PAGE_COUNT;
-        Long offset = (page - 1) * limit;
-
-        Long StudyAttendeeCount = studyAttendeeRepository.findAllByStudyIdStudyAttendeeCount(
-                studyId);
-        List<StudyAttendeeDto> attendees = studyAttendeeRepository.pageAllByStudyId(studyId, limit,
-                        offset).stream()
+        long limit = STUDY_ATTENDEE_PAGE_COUNT;
+        long offset = (page - 1) * limit;
+
+        long totalCount = studyAttendeeRepository.findAllByStudyIdStudyAttendeeCount(studyId);
+        long pageCount  = (totalCount + limit - 1) / limit;
+        List<StudyAttendeeDto> attendees = studyAttendeeRepository
+                .pageAllByStudyId(studyId, limit, offset).stream()
                  .map(this::studyAttendeeEntityToDto)
                  .toList();

         return StudyAttendeeListWithMetaDto.builder()
-                .attendees(attendees)
-                .pageCount(StudyAttendeeCount)
+                .attendees(attendees)
+                .pageCount(pageCount)
                 .page(page)
                 .build();
     }

추가 검토:

  • 변수명은 lowerCamelCase(totalCount, pageCount)로 통일
  • 동일 패턴이 StudyService에도 존재하므로( .pageCount(count) ), 필요 시 함께 리팩터링 검토 권장
🧹 Nitpick comments (2)
src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (2)

50-51: 상수/변수 네이밍 정리: PAGE_COUNT → PAGE_SIZE가 의미에 부합

STUDY_ATTENDEE_PAGE_COUNT는 “페이지 개수”가 아니라 “페이지 당 항목 수(limit)”를 의미하므로 혼동을 줄이기 위해 PAGE_SIZE로 변경을 권장합니다. 또, 불필요한 박싱을 줄이기 위해 primitive long 사용이 낫습니다.

적용 diff:

-    private static final Long STUDY_ATTENDEE_PAGE_COUNT = 10L;
+    private static final long STUDY_ATTENDEE_PAGE_SIZE = 10L;
@@
-        Long limit = STUDY_ATTENDEE_PAGE_COUNT;
-        Long offset = (page - 1) * limit;
+        long limit = STUDY_ATTENDEE_PAGE_SIZE;
+        long offset = (page - 1) * limit;

Also applies to: 59-61


87-97: 타이포/네이밍 수정: stuatAttendeeUserattendeeUser

가독성을 위해 변수명을 정정하세요.

적용 diff:

-        User stuatAttendeeUser = studyAttendee.getUser();
+        User attendeeUser = studyAttendee.getUser();
@@
-                .name(stuatAttendeeUser.getName())
-                .phone(stuatAttendeeUser.getPhoneNumber())
-                .major(stuatAttendeeUser.getMajor())
-                .studentId(stuatAttendeeUser.getStudentId())
+                .name(attendeeUser.getName())
+                .phone(attendeeUser.getPhoneNumber())
+                .major(attendeeUser.getMajor())
+                .studentId(attendeeUser.getStudentId())
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d8c56bb and 7b50f20.

📒 Files selected for processing (4)
  • src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (7 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (8 hunks)
  • src/main/java/inha/gdgoc/domain/user/service/UserService.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java
  • src/main/java/inha/gdgoc/domain/user/service/UserService.java
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (4)
src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (1)
  • StudyAttendeeException (6-11)
src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1)
  • StudyException (6-11)
src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1)
  • UserException (6-11)
src/main/java/inha/gdgoc/domain/study/entity/Study.java (1)
  • Entity (29-129)
🔇 Additional comments (6)
src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (6)

3-9: 도메인별 에러코드/예외 정합성 확보됨 (특히 USER_NOT_FOUND 정적 임포트 교정 확인)

Auth가 아닌 User 도메인의 USER_NOT_FOUND를 사용하도록 수정되어 도메인 경계가 명확해졌습니다. 좋습니다.


20-27: 예외 타입 분리(Study/StudyAttendee/User) 적용 적절

StudyException, StudyAttendeeException, UserException을 도메인별로 명확히 나눠 사용하고 있어 에러 의미가 분명합니다.


56-56: INVALID_PAGE에 대한 도메인 일치 처리 확인

INVALID_PAGEStudyAttendeeException을 던지도록 교정되어 일관성 좋습니다.


116-146: 도메인별 예외 처리 일관성 좋음

createAttendee에서 사용자/스터디 부재를 각 도메인 예외로 분리하고, 중복 지원 방지 검증과 Validator 호출 순서도 자연스럽습니다.


184-187: 중복 지원 방지 검증 로직 적절

선행 존재 여부 체크로 도메인 규칙을 잘 보장하고 있습니다.


102-114: 이미 Fetch Join이 적용되어 있습니다
StudyAttendeeRepositoryImplfindAllByUserId(Long userId) 구현을 보면

queryFactory
  .selectFrom(studyAttendee)
  .innerJoin(studyAttendee.study, study).fetchJoin()
  …  

와 같이 study 엔티티를 페치 조인하고 있어, 호출 시점에 연관된 스터디를 한 번에 로딩합니다. 따라서 N+1 문제는 발생하지 않습니다. 추가 변경은 불필요합니다.

@kaswhy kaswhy merged commit e2062cf into develop Aug 26, 2025
1 check passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/resources/application-local.yml (1)

55-57: 오타로 인해 로깅 설정이 적용되지 않음(org.hibername → org.hibernate)

현재 키가 org.hibername.type로 오타가 있어 의도한 로깅 레벨이 전혀 적용되지 않습니다. dev/prod와의 일관성 관점에서도 보수 권장합니다.

다음과 같이 수정하세요(dev/prod에 맞춰 off 권장. 의도대로 TRACE면 값만 trace로 두세요):

 logging:
   level:
     org.hibernate.SQL: debug
-    org.hibername.type: trace
+    org.hibernate.type: off
🧹 Nitpick comments (4)
src/main/resources/application-prod.yml (1)

5-7: spring.web.resources.add-mappings: false 설정 영향 검증 완료

스크립트 실행 결과를 종합하면 다음과 같습니다:

  • 정적 리소스 디렉터리
    src/main/resources/static, src/main/resources/public 등 별도의 정적 콘텐츠 디렉터리 미존재
    src/main/resourcessrc/test/resources 디렉터리만 확인됨
  • 사용자 정의 리소스 핸들러
    WebMvcConfigurer 구현 클래스 없음
    addResourceHandlers(...) 호출 없음
  • Swagger UI (springdoc-openapi) 의존성
    • Gradle 빌드 파일에 org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0 확인

결론적으로,
• 프로젝트 내 자체 정적 자산이 없으므로 기본 매핑(add-mappings: false) 비활성화로 인한 404 이슈는 없습니다.
• Swagger UI는 springdoc-openapi-starter-webmvc-ui 스타터가 제공하는 자동 리소스 핸들러로 서빙되므로 영향을 받지 않습니다.

–––
필요 시 정적 콘텐츠를 제공하려면 아래 예시처럼 명시적 매핑을 추가하세요.

@Configuration
public class StaticResourceConfig implements WebMvcConfigurer {
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCachePeriod(3600);
    // WebJars 지원이 필요하다면:
    // registry.addResourceHandler("/webjars/**")
    //         .addResourceLocations("classpath:/META-INF/resources/webjars/");
  }
}
src/main/resources/application-dev.yml (1)

5-7: 기본 정적 리소스 매핑 비활성화(dev): 의도/영향 확인

dev 환경에서도 기본 리소스 매핑이 꺼집니다. 로컬/프로덕션과 일관성은 좋지만, 개발 중 Swagger UI 또는 정적 자산 확인이 어렵게 될 수 있습니다. 개발 생산성을 위해 dev에서만 true로 두는 선택지도 있습니다.

필요 시 dev만 다음처럼 두는 방안 검토:

 spring:
   web:
     resources:
-      add-mappings: false
+      add-mappings: true

위 변경 전/후로 Swagger UI, 정적 파일 경로(있는 경우)가 정상 노출되는지 확인 부탁드립니다.

src/main/resources/application-local.yml (1)

5-7: 로컬 환경의 기본 정적 리소스 매핑 비활성화: 확인 사항 동일

local에서도 기본 매핑이 꺼집니다. 로컬에서 정적 파일/Swagger UI 확인이 필요 없다면 문제 없습니다. 필요하다면 명시적 ResourceHandler 추가 또는 local에서만 true 유지하는 방안 고려 바랍니다.

로컬 개발 편의를 위해 아래와 같이 유지하는 것도 한 방법입니다:

 spring:
   web:
     resources:
-      add-mappings: false
+      add-mappings: true
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (1)

78-86: CORS: 출처/헤더는 합리적이나, 환경별 외부화와 메서드/헤더 보강을 고려하세요

  • 운영/개발 프로필별로 allowedOrigins를 application-*.yml로 외부화하면 변경/확장이 수월합니다.
  • 일부 API가 PATCH를 사용할 가능성이 있으면 미리 허용해두는 편이 안전합니다.
  • 프론트가 응답 헤더에서 Authorization/Set-Cookie를 읽어야 한다면 exposedHeaders도 설정해야 합니다.

예시 패치:

-        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+        config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
@@
-        config.setAllowedHeaders(
-                List.of("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization"));
+        config.setAllowedHeaders(
+                List.of("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization"));
+        // 필요 시 노출 헤더 설정
+        // config.setExposedHeaders(List.of("Authorization", "Set-Cookie"));

또한 로컬 개발에서 http://127.0.0.1:3000 등을 사용한다면 해당 출처도 허용 목록에 포함해야 합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7b50f20 and 4bf3203.

📒 Files selected for processing (4)
  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java (2 hunks)
  • src/main/resources/application-dev.yml (1 hunks)
  • src/main/resources/application-local.yml (1 hunks)
  • src/main/resources/application-prod.yml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (3)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)
  • Configuration (11-57)
src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (1)
  • Slf4j (47-175)
src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (1)
  • Override (25-37)
🔇 Additional comments (2)
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (2)

33-37: 표면 축소 및 보안 기본값 비활성화 구성은 적절합니다

CSRF/FormLogin/HttpBasic 비활성화 + CORS 활성화 구성 흐름은 JWT 기반의 stateless API에 맞습니다.


50-53: 세션 stateless + 필터 순서 배치는 적절합니다

JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 두고, 세션을 STATELESS로 고정한 선택은 적절합니다.

Comment on lines +38 to +49
.requestMatchers(
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-ui.html",
"/api/v1/auth/**",
"/api/v1/game/**",
"/api/v1/apply/**",
"/api/v1/check/**")
.permitAll()
.anyRequest()
.authenticated()
)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

퍼블릭 경로(v1)와 TokenAuthenticationFilter 화이트리스트(구 경로) 불일치 — 퍼블릭 API에서 인증 시도/403 발생 가능

SecurityConfig는 퍼블릭 엔드포인트를 /api/v1/auth/**, /api/v1/game/**, /api/v1/apply/**, /api/v1/check/**로 설정합니다. 그러나 TokenAuthenticationFilter.shouldNotFilter는 여전히 /auth/, /game/, /apply/, /check/ 등 구(비버전) 경로를 건너뜁니다. 결과적으로 퍼블릭 v1 경로에서도 토큰 필터가 동작해, 토큰 부재/무효 시 AuthenticationEntryPoint로 흘러 403을 반환할 수 있습니다. 퍼블릭 엔드포인트는 필터를 우회하거나, 최소한 토큰 부재 시 무시되도록 동작이 일치해야 합니다.

아래 스크립트로 현재 불일치를 확인하세요(리포지토리 루트에서 실행).

권장 수정(참고용: TokenAuthenticationFilter 쪽 패치 예시):

-        return p.startsWith("/v3/api-docs")
+        return p.startsWith("/v3/api-docs")
             || p.startsWith("/swagger-ui")
             || p.equals("/swagger-ui.html")
-            || p.startsWith("/auth/")
-            || p.startsWith("/test/")
-            || p.startsWith("/game/")
-            || p.startsWith("/apply/")
-            || p.startsWith("/check/")
+            || p.startsWith("/api/v1/auth/")
+            || p.startsWith("/api/v1/game/")
+            || p.startsWith("/api/v1/apply/")
+            || p.startsWith("/api/v1/check/")
             || "OPTIONS".equalsIgnoreCase(request.getMethod());

또는 중복/드리프트 방지를 위해 공용 상수(예: SecurityPaths.PUBLIC_PREFIXES)나 설정 바인딩(@ConfigurationProperties)을 도입해 SecurityConfig와 Filter가 같은 소스를 참조하도록 하는 것을 권장합니다.


🏁 Script executed:

#!/bin/bash
# 퍼블릭 경로(v1)와 shouldNotFilter 내 경로를 비교
echo "[SecurityConfig 퍼블릭 경로]"
rg -n -C2 'requestMatchers\(' src/main/java | sed -n '1,120p'

echo
echo "[TokenAuthenticationFilter.shouldNotFilter 내부 경로]"
rg -n -C2 'shouldNotFilter|startsWith\(\"/' src/main/java/inha/gdgoc/global/security

# 기대: shouldNotFilter에도 /api/v1/auth|game|apply|check 가 포함되어야 함

Length of output: 1023


다음 스크립트로 TokenAuthenticationFilter의 실제 화이트리스트 경로를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# SecurityConfig에서 설정된 퍼블릭 엔드포인트
echo "[SecurityConfig 퍼블릭 경로]"
rg -n -C2 'requestMatchers\(' src/main/java/inha/gdgoc/global/security/SecurityConfig.java

echo

# TokenAuthenticationFilter.shouldNotFilter 메서드 전체 내용
echo "[TokenAuthenticationFilter.shouldNotFilter 메서드 내용]"
sed -n '/public boolean shouldNotFilter/,/}/p' src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java

echo

# startsWith로 체크하는 경로들만 별도 추출
echo "[startsWith 호출 경로 목록]"
rg -n 'p\.startsWith' -n src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java

Length of output: 1119


TokenAuthenticationFilter 화이트리스트와 SecurityConfig 퍼블릭 엔드포인트 불일치 확인 및 수정 필요

SecurityConfig에는 다음 v1 엔드포인트를 퍼블릭으로 허용(permitAll)하도록 설정되어 있습니다.

  • /api/v1/auth/**
  • /api/v1/game/**
  • /api/v1/apply/**
  • /api/v1/check/**

그러나 TokenAuthenticationFilter.shouldNotFilter()/auth/, /game/, /apply/, /check/ 등 구(非-v1) 경로만 화이트리스트 처리하고 있어, 실제로 /api/v1/... 요청에도 필터가 적용되어 토큰이 없거나 유효하지 않은 경우 403 오류로 처리될 수 있습니다.

  • 수정이 필요한 위치

    • 파일: src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java
    • 메서드: public boolean shouldNotFilter(HttpServletRequest request)
  • 예상 패치 예시

-        return p.startsWith("/v3/api-docs")
+        return p.startsWith("/v3/api-docs")
             || p.startsWith("/swagger-ui")
             || p.equals("/swagger-ui.html")
-            || p.startsWith("/auth/")
-            || p.startsWith("/test/")
-            || p.startsWith("/game/")
-            || p.startsWith("/apply/")
-            || p.startsWith("/check/")
+            || p.startsWith("/api/v1/auth/")
+            || p.startsWith("/api/v1/game/")
+            || p.startsWith("/api/v1/apply/")
+            || p.startsWith("/api/v1/check/")
             || "OPTIONS".equalsIgnoreCase(request.getMethod());
  • 추가 제안
    중복·드리프트 방지를 위해 퍼블릭 경로 목록을 상수 또는 @ConfigurationProperties로 추출해, SecurityConfig와 Filter가 동일한 출처를 참조하도록 리팩터링을 권장합니다.
📝 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
.requestMatchers(
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-ui.html",
"/api/v1/auth/**",
"/api/v1/game/**",
"/api/v1/apply/**",
"/api/v1/check/**")
.permitAll()
.anyRequest()
.authenticated()
)
// In src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String p = request.getRequestURI();
return p.startsWith("/v3/api-docs")
|| p.startsWith("/swagger-ui")
|| p.equals("/swagger-ui.html")
|| p.startsWith("/api/v1/auth/")
|| p.startsWith("/api/v1/game/")
|| p.startsWith("/api/v1/apply/")
|| p.startsWith("/api/v1/check/")
|| "OPTIONS".equalsIgnoreCase(request.getMethod());
}
🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java in
method public boolean shouldNotFilter(HttpServletRequest request), the filter
currently whitelists legacy paths like "/auth/", "/game/", "/apply/", "/check/"
but not the v1 API paths permitted in SecurityConfig (/api/v1/auth/**,
/api/v1/game/**, /api/v1/apply/**, /api/v1/check/**), causing requests to
/api/v1/... to be filtered and potentially rejected; update the shouldNotFilter
logic to include the same v1 patterns used in
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (add
"/api/v1/auth/**", "/api/v1/game/**", "/api/v1/apply/**", "/api/v1/check/**"),
and as a follow-up refactor extract the public path list into a shared constant
or @ConfigurationProperties so both SecurityConfig and TokenAuthenticationFilter
reference the same source to avoid drift.

Comment on lines +54 to +69
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json; charset=UTF-8");

// ErrorResponse 생성
ErrorResponse errorResponse = new ErrorResponse(
GlobalErrorCode.INVALID_JWT_REQUEST);
// ErrorResponse 생성
ErrorResponse errorResponse = new ErrorResponse(
GlobalErrorCode.INVALID_JWT_REQUEST);

// JSON 직렬화 후 응답에 쓰기
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
response.getWriter().flush();
})
);
// JSON 직렬화 후 응답에 쓰기
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter()
.write(objectMapper.writeValueAsString(errorResponse));
response.getWriter().flush();
})
);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

AuthenticationEntryPoint가 403을 반환하고, ObjectMapper를 매 요청 생성 — 401로 교체하고 빈 주입 사용 권장. AccessDenied(403)도 JSON 포맷으로 통일 필요

  • 인증 실패(미인증)는 401이 표준입니다. 현재 403을 내려 클라이언트/프론트 처리(리다이렉트, 토큰 재발급 트리거 등)가 어긋날 수 있습니다.
  • 매 요청마다 new ObjectMapper() 생성은 퍼포먼스/일관성(모듈 등록, 설정) 측면에서 좋지 않습니다. 스프링 빈으로 주입해 사용하세요.
  • 권한 거부(인가 실패, 403)는 AccessDeniedHandler에서 처리하도록 추가해 에러 포맷을 통일하는 것을 권장합니다.
  • 본 PR의 목표(전역 에러 표준화)에 맞춰 ErrorResponse 대신 표준 응답(ApiResponse/ErrorMeta 기반)을 사용하도록 합의 후 치환하세요.

아래 최소 패치는 상태코드/직렬화 부분만 안전하게 수정합니다(필수). AccessDeniedHandler 추가와 에러 포맷 표준화는 후속 변경을 권장합니다.

                 .exceptionHandling(ex -> ex
                         .authenticationEntryPoint((request, response, authException) -> {
-                            response.setStatus(HttpStatus.FORBIDDEN.value());
-                            response.setContentType("application/json; charset=UTF-8");
+                            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+                            response.setContentType("application/json; charset=UTF-8");
+                            response.setCharacterEncoding("UTF-8");
 
                             // ErrorResponse 생성
                             ErrorResponse errorResponse = new ErrorResponse(
                                     GlobalErrorCode.INVALID_JWT_REQUEST);
 
                             // JSON 직렬화 후 응답에 쓰기
-                            ObjectMapper objectMapper = new ObjectMapper();
-                            response.getWriter()
-                                    .write(objectMapper.writeValueAsString(errorResponse));
+                            response.getWriter()
+                                    .write(objectMapper.writeValueAsString(errorResponse));
                             response.getWriter().flush();
                         })
                 );

위 패치를 적용하려면 ObjectMapper를 빈으로 주입해야 합니다. 클래스 상단에 다음 필드를 추가하세요:

 public class SecurityConfig {
 
-    private final TokenAuthenticationFilter tokenAuthenticationFilter;
+    private final TokenAuthenticationFilter tokenAuthenticationFilter;
+    private final ObjectMapper objectMapper;

추가 권장(선택):

  • AccessDeniedHandler를 등록해 403도 JSON 표준 포맷으로 응답하도록 구성하세요.
  • GlobalErrorCode 값이 실제 도메인 에러 코드 정책(예: AuthErrorCode.UNAUTHORIZED_TOKEN 등)과 맞는지 점검하세요. 표준 응답(ApiResponse<Void, ErrorMeta>)로의 치환 시그니처는 팀 합의에 맞춰 적용 바랍니다.

🏁 Script executed:

#!/bin/bash
# 1. 클래스 헤더에서 ObjectMapper 빈 주입 여부 확인
sed -n '1,30p' src/main/java/inha/gdgoc/global/security/SecurityConfig.java

# 2. exceptionHandling 설정 확인 (라인 50~80)
sed -n '50,80p' src/main/java/inha/gdgoc/global/security/SecurityConfig.java

# 3. new ObjectMapper 인스턴스화 사례 검색
grep -Rn "new ObjectMapper" -n src/main/java/inha/gdgoc/global/security/SecurityConfig.java || echo "No instantiation found"

# 4. authenticationEntryPoint 상태 코드 확인
grep -Rn "setStatus" -n src/main/java/inha/gdgoc/global/security/SecurityConfig.java

Length of output: 3358


인증 실패 처리(401) 및 ObjectMapper 빈 주입 적용 필요

다음 사항을 반영해주세요:

  • AuthenticationEntryPoint는 미인증 시 401(Unauthorized)이 표준입니다. 기존 403(FORBIDDEN) → 401(UNAUTHORIZED)으로 변경해야 클라이언트에서 올바르게 토큰 재발급 흐름을 처리할 수 있습니다.
  • new ObjectMapper()를 매 요청마다 생성하지 말고, 스프링 빈으로 주입된 ObjectMapper를 재사용하도록 수정하세요. (퍼포먼스 및 모듈 등록·설정 일관성 확보)
  • 권한 거부(인가 실패, 403)는 별도 AccessDeniedHandler에서 JSON 포맷으로 응답하도록 추가 구성해 에러 포맷을 통일하는 것을 권장합니다.
  • 전역 에러 표준화(PR 목표)에 따라 ErrorResponse 대신 팀 합의된 표준 응답(ApiResponse/ErrorMeta 기반)으로 교체하는 후속 작업을 진행하세요.

아래는 최소 패치 예시입니다:

 src/main/java/inha/gdgoc/global/security/SecurityConfig.java
@@ -54,15 +54,18 @@ public class SecurityConfig {
                 .exceptionHandling(ex -> ex
                         .authenticationEntryPoint((request, response, authException) -> {
-                            response.setStatus(HttpStatus.FORBIDDEN.value());
+                            response.setStatus(HttpStatus.UNAUTHORIZED.value());
                             response.setContentType("application/json");
+                            response.setCharacterEncoding("UTF-8");
 
                             // ErrorResponse 생성
                             ErrorResponse errorResponse = new ErrorResponse(
                                     GlobalErrorCode.INVALID_JWT_REQUEST);
 
                             // JSON 직렬화 후 응답에 쓰기
-                            ObjectMapper objectMapper = new ObjectMapper();
-                            response.getWriter()
-                                    .write(objectMapper.writeValueAsString(errorResponse));
+                            response.getWriter()
+                                    .write(objectMapper.writeValueAsString(errorResponse));
                             response.getWriter().flush();
                         })
                 );

ObjectMapper 빈 주입을 위해 클래스 상단에 필드를 추가하세요:

 public class SecurityConfig {
 
-    private final TokenAuthenticationFilter tokenAuthenticationFilter;
+    private final TokenAuthenticationFilter tokenAuthenticationFilter;
+    private final ObjectMapper objectMapper;
  • AccessDeniedHandler 등록 예시:
    http.exceptionHandling()
        .accessDeniedHandler((request, response, accessDeniedException) -> {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            // 표준 ErrorResponse(ErrorMeta) 작성 후 응답
        });
  • GlobalErrorCode.INVALID_JWT_REQUEST 외에 권한 거부용 에러 코드 정책(AuthErrorCode.UNAUTHORIZED_TOKEN 등)도 함께 검토 바랍니다.

위 변경은 필수(401 변경·ObjectMapper 주입)이며, 403 통일 처리 및 표준 응답으로의 완전 전환은 후속 리팩토링으로 진행해주세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants