Skip to content

Conversation

@CSE-Shaco
Copy link
Contributor

@CSE-Shaco CSE-Shaco commented Jan 14, 2026

📌 연관된 이슈

ex) #이슈번호, #이슈번호

✨ 작업 내용

  • 운영진 리크루팅 도메인을 새 domain/recruit/core 구조와 admin 워크플로로 전환하면서 구 버전 제거 및 세션 자동 계산 도입.
  • AccessGuard를 추가해 Auth/관리자/게스트북/출석 API 전반에 역할·팀 조건을 일관되게 적용.
  • 리크루팅 멤버 모듈을 domain/recruit/member로 재구조화하고 SemesterCalculator 빈을 주입받도록 변경.
  • 관련 테스트/설정 파일 정리 및 application-*.yml 포맷 통일.

💬 리뷰 요구사항(선택)

Summary by CodeRabbit

  • New Features

    • Recruit Core: 지원서 제출·사전작성·내 지원 조회·상세 조회 기능 및 관리자용 지원 목록/수락/거절 워크플로 추가
    • 지원 자격 확인(세션 기반) 및 중복 신청 차단, 심사 결과 오류 응답 개선
  • Refactor

    • 모집 모듈 재구성 및 패키지 정리, 기존 Core Recruit API 제거·재설계
    • 권한 검사 통합(AccessGuard)으로 보안 규칙 표준화
  • Tests

    • 관리자/지원자 서비스 단위 테스트 추가, 일부 레거시 테스트 제거
  • Chores

    • 설정 파일 정리 및 DB 마이그레이션 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

Walkthrough

모집 시스템을 재구조화해 신규 recruit/core 모듈(사용자·관리자 API, 엔티티, 서비스, DTO, 예외, 리포지토리)을 추가하고 레거시 core/recruit 모듈을 제거했으며, recruit/member 패키지 재배치와 AccessGuard 기반 권한 중앙화 및 설정·마이그레이션을 적용했습니다.

Changes

Cohort / File(s) 요약
Recruit Core — 컨트롤러(관리자)
src/main/java/inha/gdgoc/domain/admin/recruit/core/controller/RecruitCoreAdminController.java
관리자용 지원서 목록 조회, 승인(accept), 거절(reject) 엔드포인트 추가(권한·검증 포함)
Recruit Core — 서비스(관리자)
src/main/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminService.java
검색(Specification), 승인/거절 로직(상태 전이, 사용자 역할/팀 업데이트, 트랜잭션, 예외 처리) 추가
Recruit Core — DTOs(관리자 응답/요청 / 페이징)
src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/request/RecruitCoreApplicationAcceptRequest.java, ...RejectRequest.java, ...response/*.java
수락/거절 요청 DTO, 응답 DTO(요약·결정·페이지 래퍼) 추가 및 매핑 팩토리 포함
Recruit Core — 사용자 모듈
src/main/java/inha/gdgoc/domain/recruit/core/controller/RecruitCoreController.java, .../service/RecruitCoreApplicationService.java, .../entity/RecruitCoreApplication.java, .../repository/RecruitCoreApplicationRepository.java, .../enums/RecruitCoreResultStatus.java
사용자용 지원 생성/조회/자격검증/상태관리 API·엔티티·리포지토리·서비스 추가
Recruit Core — 예외·핸들러
src/main/java/inha/gdgoc/domain/recruit/core/exception/*
중복지원·조회실패 등 전용 예외, 에러코드 enum, 컨트롤러 어드바이스 추가
Recruit Core — 유틸/설정
src/main/java/inha/gdgoc/domain/recruit/core/config/RecruitCoreSessionResolver.java, src/main/resources/db/migration/V20260114__core_recruit_applications_session_and_status.sql
현재 학기 계산 컴포넌트 추가 및 core_recruit_applications 스키마 마이그레이션 추가
레거시 core/recruit 제거
src/main/java/inha/gdgoc/domain/core/recruit/* (삭제된 파일들)
기존 core/recruit 컨트롤러·서비스·엔티티·DTO·리포지토리 전면 삭제
Recruit Member — 패키지 재조직
src/main/java/inha/gdgoc/domain/recruit/member/... (다수)
recruit/member로 패키지 이동(엔티티·DTO·리포지토리·서비스·enum 등) 및 관련 import 수정
AccessGuard 권한 중앙화
src/main/java/inha/gdgoc/global/security/AccessGuard.java
역할/팀 조건 빌더(AccessCondition) 제공, 인증 검사·require 유틸 추가
컨트롤러 권한 리팩토링
src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java, .../CoreAttendanceController.java, .../GuestbookController.java, .../UserAdminController.java, .../RecruitMemberController.java
기존 인라인 @PreAuthorize 표현식을 AccessGuard 호출이나 상수로 대체
보안·설정 변경
src/main/java/inha/gdgoc/global/security/SecurityConfig.java, src/main/resources/*.yml
/api/v1/core-recruit 공개 제거 및 YAML 재구성(AWS, Flyway, JPA 등)
SemesterCalculator -> 컴포넌트화
src/main/java/inha/gdgoc/global/util/SemesterCalculator.java
static 유틸에서 @Component로 전환(Clock 주입 가능)
테스트 추가/삭제/수정
src/test/java/...
RecruitCore 서비스용 단위 테스트 추가; 일부 레거시 테스트 삭제(Study 관련), RecruitMember 테스트 import 경로 수정

Sequence Diagram(s)

sequenceDiagram
    participant Admin as 관리자
    participant Controller as RecruitCoreAdminController
    participant Service as RecruitCoreAdminService
    participant Repo as RecruitCoreApplicationRepository
    participant UserRepo as UserRepository

    Admin->>Controller: POST /api/v1/admin/recruit/core/applications/{id}/accept (body)
    Controller->>Service: accept(applicationId, reviewerId, request)
    Service->>Repo: findById(applicationId)
    Repo-->>Service: RecruitCoreApplication
    Service->>Service: ensureDecidable(application)
    Service->>Service: application.accept(...)
    Service->>UserRepo: findById(application.userId)
    UserRepo-->>Service: User
    Service->>Service: update user role/team if needed
    Service->>UserRepo: save(user)
    Service->>Repo: save(application)
    Service-->>Controller: RecruitCoreApplicationDecisionResponse
    Controller-->>Admin: 200 OK
Loading
sequenceDiagram
    participant User as 사용자
    participant Controller as RecruitCoreController
    participant Service as RecruitCoreApplicationService
    participant SessionResolver as RecruitCoreSessionResolver
    participant Repo as RecruitCoreApplicationRepository

    User->>Controller: POST /api/v1/recruit/core/applications (body)
    Controller->>Service: submit(userId, request)
    Service->>SessionResolver: currentSession()
    SessionResolver-->>Service: "YYYY-1" or "YYYY-2"
    Service->>Repo: findByUser_IdAndSession(userId, session)
    alt 기존 지원 존재
        Repo-->>Service: Optional[application]
        Service-->>Controller: throw RecruitCoreAlreadyAppliedException
        Controller-->>User: 409 Conflict (error body)
    else 신규 지원
        Repo-->>Service: Optional.empty()
        Service->>Repo: save(new RecruitCoreApplication)
        Service-->>Controller: RecruitCoreApplicationCreateResponse
        Controller-->>User: 201 Created
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

refactor, enhancement

🐰 새로운 계절의 지원서, 당근 한 움큼!
패키지 탈바꿈에 깡충댔네,
AccessGuard가 지켜주니 안심,
마이그레이션 깔끔히 끝났네,
텃밭엔 새싹, 코드엔 새 기능!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경 사항(운영진 리크루팅 기능 이관, 권한/학기 공통 컴포넌트 적용)을 정확하게 요약하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 10

Caution

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

⚠️ Outside diff range comments (3)
src/main/java/inha/gdgoc/domain/recruit/member/dto/request/CheckPhoneNumberRequest.java (1)

1-14: 패키지 이동은 완료되었지만, CheckPhoneNumberRequest가 실제로 사용되지 않습니다.

패키지 마이그레이션 자체는 올바르게 수행되었습니다. 다만 CheckPhoneNumberRequest는 코드베이스에서 어디에도 import되거나 사용되지 않습니다. 현재 /check/phone-number 엔드포인트 (RecruitMemberController 82-93줄)는 CheckPhoneNumberRequest 대신 @RequestParam과 인라인 유효성 검증 애노테이션을 사용하고 있습니다.

선택사항: 이 클래스를 사용하지 않으면 제거하거나, 일관성을 위해 컨트롤러를 리팩토링하여 request DTO를 사용하도록 변경하세요.

src/main/java/inha/gdgoc/domain/recruit/member/dto/request/RecruitMemberRequest.java (1)

31-46: RecruitMemberRequest에서 사용되지 않는 isPayed 필드 제거 필요.

RecruitMemberRequestisPayed 필드(Line 29)가 toEntity() 메서드에서 무시되고 항상 false로 하드코딩되고 있습니다(Line 44). 결제 상태 업데이트는 별도의 PaymentUpdateRequestupdatePayment() 메서드로 처리되므로 하드코딩 자체는 의도된 설계인 것으로 보이지만, 요청 객체에 정의된 미사용 필드는 혼동을 야기합니다. RecruitMemberRequest에서 isPayed 필드를 제거하거나 필드의 목적을 명확히 문서화해야 합니다.

src/test/java/inha/gdgoc/domain/recruit/service/RecruitMemberServiceTest.java (1)

18-64: 테스트가 불완전합니다 - 실제 검증 로직이 없습니다.

테스트 메서드 addMember_ShouldSaveRecruitMemberAndAnswers()가 테스트 데이터를 생성하지만:

  • 서비스 호출이 없음
  • 어서션(assertion)이 없음
  • recruitMemberRequest, answers, savedMember 변수가 생성만 되고 사용되지 않음

Mockito 임포트 제거 후 실제 테스트 로직도 함께 제거된 것으로 보입니다.

테스트 로직을 완성하거나, 이 테스트가 더 이상 필요하지 않다면 제거하는 것을 권장합니다. 테스트 구현을 도와드릴까요?

🤖 Fix all issues with AI agents
In
`@src/main/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminService.java`:
- Around line 46-73: The accept(...) flow has a race where ensureDecidable() can
pass concurrently; fix by adding concurrency control: either add an optimistic
lock field (`@Version`) to the RecruitCoreApplication entity and persist it so
concurrent commits will fail fast, or load the application with a pessimistic
lock in getApplication() by adding a repository method annotated with
`@Lock`(LockModeType.PESSIMISTIC_WRITE) (e.g.,
RecruitCoreApplicationRepository.findByIdWithLock(...) used by
getApplication()). Keep the `@Transactional` on accept(), handle
OptimisticLockException / LockTimeoutException to return an appropriate error,
and ensure ensureDecidable() runs after acquiring the lock so only one thread
proceeds to application.accept(...).

In
`@src/main/java/inha/gdgoc/domain/recruit/core/controller/message/RecruitCoreApplicationMessage.java`:
- Around line 4-5: Fix the typo in the two message constants by replacing "리쿠르트"
with the correct "리크루트": update the string values of
APPLICANT_LIST_RETRIEVED_SUCCESS and APPLICANT_RETRIEVED_SUCCESS so they read
"성공적으로 코어 리크루트 지원자 목록을 조회했습니다." and "성공적으로 코어 리크루트 지원자 상세를 조회했습니다."
respectively.

In
`@src/main/java/inha/gdgoc/domain/recruit/core/dto/request/RecruitCoreApplicationCreateRequest.java`:
- Line 17: 필드 fileUrls의 불필요한 제약조건 `@Size`(min = 0)을 제거하세요:
RecruitCoreApplicationCreateRequest 클래스에서 선언된 "@NotNull `@Size`(min = 0)
List<@NotBlank String> fileUrls"에서 `@Size`(min = 0) 어노테이션을 삭제하고 `@NotNull` 및 요소 검증용
`@NotBlank는` 그대로 유지하여 null 체크는 보장하되 빈 리스트는 허용되도록 변경하세요.

In
`@src/main/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationService.java`:
- Around line 94-111: getApplicantDetailForViewer currently throws
RecruitCoreApplicationNotFoundException while the helper getApplication throws
BusinessException(GlobalErrorCode.RESOURCE_NOT_FOUND), causing inconsistent
"application not found" behavior; fix by unifying the handling — either make
getApplication throw RecruitCoreApplicationNotFoundException (replace
BusinessException with new RecruitCoreApplicationNotFoundException) or, simpler,
have getApplicantDetailForViewer call the existing getApplication(applicationId)
and rely on its exception, ensuring both flows use the same exception type.

In
`@src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java`:
- Around line 51-57: The SpEL in LEAD_OR_HR_RULE calls
AccessCondition.atLeast(...) with two params which causes a runtime SpEL method
error; replace the second atLeast(...) invocation with
AccessCondition.of(UserRole.CORE, TeamType.HR) so the role+team pair is passed
correctly. Update the LEAD_OR_HR_RULE constant in RecruitMemberController (and
the identical constant in UserAdminController) to use
T(inha.gdgoc.global.security.AccessGuard.AccessCondition).of(...) with
T(inha.gdgoc.domain.user.enums.UserRole).CORE and
T(inha.gdgoc.domain.user.enums.TeamType).HR as the two arguments.

In `@src/main/resources/application-local.yml`:
- Around line 5-11: The application-local.yml AWS credentials are unused because
S3Config.java instantiates DefaultCredentialsProvider.create(), so
spring.cloud.aws.credentials (access-key/secret-key) is ignored; either remove
those keys from application-local.yml or change S3Config to load credentials
from Spring properties. To fix in code, update S3Config (the class that creates
the S3 client and currently calls DefaultCredentialsProvider.create()) to
instead construct a credentials provider from the Spring properties (bind
spring.cloud.aws.credentials -> e.g., use access-key and secret-key injected or
a AwsCredentialsProvider built from those values) or delegate to
DefaultCredentialsProvider only when the properties are absent; keep using the
existing awsRegion bean (spring.cloud.aws.region.static) as before. Ensure
configuration and behavior are consistent by removing unused
spring.cloud.aws.credentials entries from application-local.yml if you choose
the provider-chain approach.

In `@src/main/resources/application-prod.yml`:
- Around line 71-74: Change the production logging level for Hibernate SQL to
avoid excessive query logging: update the logging.level key for
org.hibernate.SQL from debug to a non-verbose level (e.g., warn or error) in the
production config, keep org.hibernate.type as off, and if you need debug for
development move the debug setting into a dev-specific profile (e.g.,
application-dev.yml) or guard it by active profile so that org.hibernate.SQL is
only debug when running in non-production environments.
- Around line 5-11: The YAML defines AWS credentials that are never used because
S3Config.java injects spring.cloud.aws.region.static but uses
DefaultCredentialsProvider.create() for auth; either remove the unused
spring.cloud.aws.credentials.access-key and secret-key entries from
application-prod.yml to rely on environment variables/SDK credential chain, or
modify S3Config (the class using DefaultCredentialsProvider.create()) to
`@Value-inject` the YAML properties (spring.cloud.aws.credentials.access-key and
spring.cloud.aws.credentials.secret-key) and construct the credentials provider
from those values so the YAML settings are actually applied.

In
`@src/test/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationServiceTest.java`:
- Around line 138-147: Rename the test method
getApplicantDetailForViewer_whenOwnerAlllowed to correct the typo
(getApplicantDetailForViewer_whenOwnerAllowed) so the name has only two 'l's in
"Allowed"; update the method declaration and any references (e.g., IDE/run
configurations or test annotations) that refer to
getApplicantDetailForViewer_whenOwnerAlllowed to the new name to keep tests
discoverable and runnable.
🧹 Nitpick comments (18)
src/test/resources/application-test.yml (1)

60-63: JWT secretKey는 테스트용 더미 값으로 안전합니다 (Gitleaks 경고는 false positive).

정적 분석 도구가 secretKey를 잠재적 API 키로 플래그했지만, 해당 값(MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=)은 Base64 디코딩 시 0123456789abcdef0123456789abcdef로, 명백한 더미/테스트 값입니다. 테스트 설정 파일에서 이러한 더미 자격 증명 사용은 적절합니다.

향후 혼란 방지를 위해 주석을 추가하는 것도 고려해볼 수 있습니다.

💡 선택적 개선: 테스트 값임을 명시하는 주석 추가
 jwt:
   googleIssuer: test-google-issuer
+  # Base64 encoded dummy key for testing: "0123456789abcdef0123456789abcdef"
   secretKey: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
   selfIssuer: test-self-issuer
src/main/java/inha/gdgoc/domain/recruit/member/dto/request/PaymentUpdateRequest.java (1)

3-5: isPayed 필드명 전체적으로 검토 필요: 다수의 파일에서 일관된 오타

"Payed"는 항해 용어(로프를 풀어주다)에서만 사용되며, "pay"의 올바른 과거형은 "paid"입니다. 이 오타는 PaymentUpdateRequest뿐만 아니라 RecruitMember 엔티티, RecruitMemberService, 컨트롤러, 여러 DTO(Response), 테스트까지 일관되게 나타나 있습니다.

마이그레이션 중에 수정한다면, 다음 영향도 함께 검토해야 합니다:

  • PaymentUpdateRequest 레코드에 @JsonProperty 같은 매핑 어노테이션이 없으므로, JSON 직렬화 시 필드명이 isPayed로 그대로 노출됨
  • 엔티티, 서비스, 컨트롤러, 테스트 등 최소 6개 이상의 파일 수정 필요
  • 외부 API 클라이언트가 이미 isPayed 필드명에 의존하고 있다면 breaking change 발생

따라서 단순 오타 수정이 아닌 전사적 리팩토링으로 계획하고, 클라이언트 호환성을 사전에 확인하거나 API 버전 관리 전략을 수립한 후 진행하시기 바랍니다.

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

5-11: 불필요한 spring.cloud.aws.credentials 속성 제거 또는 S3Config 리팩토링 필요

S3Config.java에서 DefaultCredentialsProvider.create()를 사용하고 있어 AWS SDK 기본 credential 체인(환경 변수 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)을 직접 읽습니다. application-dev.ymlspring.cloud.aws.credentials.access-keyspring.cloud.aws.credentials.secret-key 속성(lines 8-9)은 Java 코드 어디서도 참조되지 않으므로 불필요합니다.

다음 중 하나를 수행하세요:

  1. 미사용 spring.cloud.aws.credentials 속성을 application-dev.yml에서 제거
  2. 또는 Spring Cloud AWS 자동 구성을 활용하도록 S3Config 리팩토링
src/main/java/inha/gdgoc/domain/recruit/member/dto/response/RecruitMemberSummaryResponse.java (1)

16-20: 학기 변환 로직이 enum 명명 규칙에 의존합니다.

substring(1)replace('_', '-') 로직은 enum 이름이 S2024_1 형식임을 가정합니다. 이 변환 로직이 AdmissionSemester enum의 책임으로 이동되면 캡슐화와 재사용성이 향상될 수 있습니다.

♻️ 제안: enum에 변환 메서드 추가

AdmissionSemester enum에 toDisplayString() 메서드를 추가하는 것을 고려해 보세요:

// AdmissionSemester enum 내부
public String toDisplayString() {
    return name().substring(1).replace('_', '-');
}

그러면 이 코드는 다음과 같이 단순화됩니다:

-        String semester = null;
-        if (recruitMember.getAdmissionSemester() != null) {
-            String enumName = recruitMember.getAdmissionSemester().name();
-            semester = enumName.substring(1).replace('_', '-');
-        }
+        String semester = recruitMember.getAdmissionSemester() != null
+            ? recruitMember.getAdmissionSemester().toDisplayString()
+            : null;
src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/request/RecruitCoreApplicationRejectRequest.java (1)

5-7: 거절 사유 필드에 길이 제한 추가를 고려해 보세요.

@NotBlank는 빈 값을 방지하지만, 길이 제한이 없으면 비정상적으로 긴 입력이 가능합니다.

♻️ 제안: 길이 제한 추가
 public record RecruitCoreApplicationRejectRequest(
-    `@NotBlank` String resultNote
+    `@NotBlank` `@Size`(max = 500) String resultNote
 ) {
 }

jakarta.validation.constraints.Size import가 필요합니다.

src/main/java/inha/gdgoc/domain/recruit/member/entity/RecruitMember.java (1)

64-66: @JsonFormat을 엔티티에서 DTO로 이동하는 것을 고려해보세요.

엔티티 클래스에 직접 @JsonFormat을 사용하는 것은 프레젠테이션 계층과 도메인 계층의 결합을 유발할 수 있습니다. 일반적으로 JSON 직렬화 형식은 응답 DTO에서 정의하는 것이 더 적절합니다.

src/main/java/inha/gdgoc/global/util/SemesterCalculator.java (1)

49-56: Exception 대신 IllegalArgumentException을 catch하는 것을 권장합니다.

Enum.valueOf()는 해당 상수가 없을 때 IllegalArgumentException을 발생시킵니다. 더 넓은 범위의 Exception을 catch하면 예상치 못한 다른 예외까지 숨길 수 있습니다.

♻️ 제안하는 수정
         try {
             return AdmissionSemester.valueOf(enumName);
-        } catch (Exception ex) {
+        } catch (IllegalArgumentException ex) {
             throw new RuntimeException(
                     "AdmissionSemester enum에 상수 " + enumName + " 이(가) 정의되어 있지 않습니다. " +
                             "해당 연도/학기 상수를 추가하세요.", ex
             );
         }
src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreApplicationReviewResponse.java (1)

14-25: 중복된 null 체크 제거 가능

15-18줄의 조건부 early return은 불필요합니다. 모든 필드가 null인 경우에도 20-24줄이 동일한 결과를 반환합니다.

♻️ 단순화 제안
     public static RecruitCoreApplicationReviewResponse from(RecruitCoreApplication application) {
-        if (application.getReviewedAt() == null
-            && application.getReviewedBy() == null
-            && application.getResultNote() == null) {
-            return new RecruitCoreApplicationReviewResponse(null, null, null);
-        }
         return new RecruitCoreApplicationReviewResponse(
             application.getReviewedAt(),
             application.getReviewedBy(),
             application.getResultNote()
         );
     }
src/test/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminServiceTest.java (1)

150-172: 리플렉션 기반 테스트 헬퍼의 취약성

리플렉션을 사용한 ID 및 타임스탬프 설정은 테스트 목적으로 허용되나, 필드명 변경 시 런타임에 실패할 수 있습니다. 엔티티 클래스에 테스트용 패키지-프라이빗 생성자나 빌더에 ID 설정 옵션을 추가하는 것을 고려해 볼 수 있습니다.

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

6-19: 코드베이스의 ErrorCode 인터페이스 패턴과 불일치

다른 에러 코드 enum들(UserErrorCode, RecruitMemberErrorCode, StudyErrorCode, AuthErrorCode)은 ErrorCode 인터페이스를 구현하지만, 이 enum은 구현하지 않습니다. 또한 다른 enum들은 (HttpStatus status, String message) 두 개의 필드만 사용하는 반면, 이 enum은 code 필드를 추가로 포함합니다.

일관성을 위해 ErrorCode 인터페이스 구현을 고려하세요:

♻️ ErrorCode 인터페이스 구현 제안
 `@Getter`
-public enum RecruitCoreApplicationErrorCode {
-    ALREADY_APPLIED("ALREADY_APPLIED", "이미 지원이 완료되었습니다.", HttpStatus.CONFLICT),
-    APPLICATION_NOT_FOUND("APPLICATION_NOT_FOUND", "제출된 운영진 지원서가 없습니다.", HttpStatus.NOT_FOUND);
+public enum RecruitCoreApplicationErrorCode implements ErrorCode {
+    ALREADY_APPLIED(HttpStatus.CONFLICT, "이미 지원이 완료되었습니다."),
+    APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "제출된 운영진 지원서가 없습니다.");

-    private final String code;
-    private final String message;
     private final HttpStatus status;
+    private final String message;

-    RecruitCoreApplicationErrorCode(String code, String message, HttpStatus status) {
-        this.code = code;
-        this.message = message;
+    RecruitCoreApplicationErrorCode(HttpStatus status, String message) {
         this.status = status;
+        this.message = message;
+    }
+
+    `@Override`
+    public HttpStatus getStatus() {
+        return status;
+    }
+
+    `@Override`
+    public String getMessage() {
+        return message;
     }
 }
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreAlreadyAppliedException.java (1)

5-17: RecruitCoreControllerExceptionHandler에서 명시적으로 처리되고 있으므로 글로벌 핸들러 부재는 문제가 아닙니다

다른 도메인 예외들(RecruitMemberException)과 달리 이 예외가 BusinessException을 상속하지 않는 것은 아키텍처 일관성 측면에서 검토할 사항입니다. 다만, RecruitCoreControllerExceptionHandler에서 명시적으로 처리되므로 기능상 문제는 없습니다.

sessionapplicationId 필드를 활용한 커스텀 응답(RecruitCoreApplicationErrorResponse)이 필요하다면 현재 설계가 합리적입니다. 일관성을 위해 BusinessException으로 통일하거나, 명시적인 이유를 문서화하는 것을 고려하세요.

src/main/java/inha/gdgoc/domain/recruit/core/entity/RecruitCoreApplication.java (2)

95-97: getId() 메서드가 Lombok @Getter와 중복됩니다.

@Getter 어노테이션이 이미 클래스에 적용되어 있으므로, getId() 메서드는 자동 생성됩니다. 명시적인 메서드는 불필요합니다.

♻️ 제안된 수정
-    public Long getId() {
-        return id;
-    }
-

103-121: 상태 전이 메서드에서 현재 상태 검증이 누락되었습니다.

accept(), reject(), moveToReview() 메서드들이 현재 resultStatus를 검증하지 않고 상태를 변경합니다. 예를 들어, 이미 REJECTED 상태인 지원서를 다시 accept() 할 수 있습니다.

비즈니스 규칙에 따라 유효하지 않은 상태 전이를 방어적으로 차단하는 것이 좋습니다.

♻️ 상태 검증 추가 제안
 public void accept(Long reviewerId, String note, Instant reviewedAt) {
+    if (this.resultStatus == RecruitCoreResultStatus.ACCEPTED
+        || this.resultStatus == RecruitCoreResultStatus.REJECTED) {
+        throw new IllegalStateException("Cannot accept application in " + this.resultStatus + " status");
+    }
     this.resultStatus = RecruitCoreResultStatus.ACCEPTED;
     this.reviewedAt = reviewedAt;
     this.reviewedBy = reviewerId;
     this.resultNote = note;
 }

 public void reject(Long reviewerId, String note, Instant reviewedAt) {
+    if (this.resultStatus == RecruitCoreResultStatus.ACCEPTED
+        || this.resultStatus == RecruitCoreResultStatus.REJECTED) {
+        throw new IllegalStateException("Cannot reject application in " + this.resultStatus + " status");
+    }
     this.resultStatus = RecruitCoreResultStatus.REJECTED;
     this.reviewedAt = reviewedAt;
     this.reviewedBy = reviewerId;
     this.resultNote = note;
 }

 public void moveToReview(Long reviewerId, Instant reviewedAt) {
+    if (this.resultStatus != RecruitCoreResultStatus.SUBMITTED) {
+        throw new IllegalStateException("Cannot move to review from " + this.resultStatus + " status");
+    }
     this.resultStatus = RecruitCoreResultStatus.IN_REVIEW;
     this.reviewedAt = reviewedAt;
     this.reviewedBy = reviewerId;
 }
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreControllerExceptionHandler.java (1)

19-19: 로그 레벨을 debug에서 warn 또는 info로 변경 검토가 필요합니다.

비즈니스 예외를 debug 레벨로 로깅하면 프로덕션 환경에서 기본 로그 레벨 설정 시 이러한 예외들이 기록되지 않을 수 있습니다. GlobalExceptionHandler에서는 유사한 예외에 대해 더 높은 로그 레벨을 사용합니다.

운영 모니터링을 위해 info 또는 warn 레벨을 권장합니다.

♻️ 제안된 수정
-        log.debug("RecruitCoreAlreadyAppliedException: {}", ex.getMessage());
+        log.info("RecruitCoreAlreadyAppliedException: {}", ex.getMessage());
-        log.debug("RecruitCoreApplicationNotFoundException: {}", ex.getMessage());
+        log.info("RecruitCoreApplicationNotFoundException: {}", ex.getMessage());

Also applies to: 34-34

src/main/java/inha/gdgoc/domain/recruit/core/repository/RecruitCoreApplicationRepository.java (1)

14-16: java.util.Optional import 누락

Optional이 완전한 클래스 이름(FQN)으로 사용되고 있습니다. 다른 클래스들처럼 상단에 import 문을 추가하여 일관성을 유지하세요.

♻️ 제안된 수정
 package inha.gdgoc.domain.recruit.core.repository;
 
 import inha.gdgoc.domain.recruit.core.entity.RecruitCoreApplication;
+import java.util.Optional;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
-
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
 public interface RecruitCoreApplicationRepository extends JpaRepository<RecruitCoreApplication, Long>,
     JpaSpecificationExecutor<RecruitCoreApplication> {
     Page<RecruitCoreApplication> findByNameContainingIgnoreCase(String name, Pageable pageable);
 
-    java.util.Optional<RecruitCoreApplication> findByUser_IdAndSession(Long userId, String session);
+    Optional<RecruitCoreApplication> findByUser_IdAndSession(Long userId, String session);
 
-    java.util.Optional<RecruitCoreApplication> findByIdAndUser_Id(Long id, Long userId);
+    Optional<RecruitCoreApplication> findByIdAndUser_Id(Long id, Long userId);
 }
src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/response/RecruitCoreApplicationPageResponse.java (1)

30-30: 내부 Pageable 레코드 이름 충돌 가능성

내부 레코드 Pageable이 Spring Data의 org.springframework.data.domain.Pageable 인터페이스와 이름이 동일합니다. 이 DTO를 사용하는 코드에서 import 충돌이나 혼란이 발생할 수 있습니다.

♻️ 제안된 수정
-    public record Pageable(int pageNumber, int pageSize) {}
+    public record PageInfo(int pageNumber, int pageSize) {}

from() 메서드 내에서도 함께 변경 필요:

-            new Pageable(pageNumber, pageSize),
+            new PageInfo(pageNumber, pageSize),
src/main/java/inha/gdgoc/domain/admin/recruit/core/controller/RecruitCoreAdminController.java (1)

34-40: SpEL 표현식의 복잡도가 높습니다.

ORGANIZER_OR_HR_LEAD_RULE 상수가 여러 줄에 걸친 복잡한 SpEL 표현식을 포함하고 있습니다. 현재 구현은 동작하지만, 유지보수성을 고려해 AccessGuard에 전용 메서드를 추가하거나, 커스텀 어노테이션으로 분리하는 것을 검토해 볼 수 있습니다.

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

39-53: JSON 변환 실패 시 예외 처리 개선 고려

Line 47에서 RuntimeException을 직접 throw하고 있습니다. 도메인 특화 예외(예: RecruitMemberException)로 래핑하면 예외 핸들링 일관성과 클라이언트 응답 처리가 개선될 수 있습니다.

♻️ 도메인 예외로 래핑 제안
                 } catch (Exception e) {
-                    throw new RuntimeException("JSON 변환 오류", e);
+                    throw new RecruitMemberException(RecruitMemberErrorCode.JSON_CONVERSION_ERROR, e);
                 }

RecruitMemberErrorCode에 해당 에러 코드를 추가해야 합니다.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd5e5c3 and d476cc5.

📒 Files selected for processing (77)
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/controller/RecruitCoreAdminController.java
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/request/RecruitCoreApplicationAcceptRequest.java
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/request/RecruitCoreApplicationRejectRequest.java
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/response/RecruitCoreApplicantSummaryResponse.java
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/response/RecruitCoreApplicationDecisionResponse.java
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/response/RecruitCoreApplicationPageResponse.java
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminService.java
  • src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java
  • src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java
  • src/main/java/inha/gdgoc/domain/core/recruit/controller/CoreRecruitController.java
  • src/main/java/inha/gdgoc/domain/core/recruit/dto/request/CoreRecruitApplicationRequest.java
  • src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantDetailResponse.java
  • src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantSummaryResponse.java
  • src/main/java/inha/gdgoc/domain/core/recruit/entity/CoreRecruitApplication.java
  • src/main/java/inha/gdgoc/domain/core/recruit/repository/CoreRecruitApplicationRepository.java
  • src/main/java/inha/gdgoc/domain/core/recruit/service/CoreRecruitApplicationService.java
  • src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java
  • src/main/java/inha/gdgoc/domain/recruit/core/config/RecruitCoreSessionResolver.java
  • src/main/java/inha/gdgoc/domain/recruit/core/controller/RecruitCoreController.java
  • src/main/java/inha/gdgoc/domain/recruit/core/controller/message/RecruitCoreApplicationMessage.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/request/RecruitCoreApplicationCreateRequest.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreApplicantDetailResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreApplicationCreateResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreApplicationErrorResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreApplicationReviewResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreApplicationSnapshotResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreEligibilityResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreMyApplicationResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCorePrefillResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/core/entity/RecruitCoreApplication.java
  • src/main/java/inha/gdgoc/domain/recruit/core/enums/RecruitCoreResultStatus.java
  • src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreAlreadyAppliedException.java
  • src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreApplicationErrorCode.java
  • src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreApplicationNotFoundException.java
  • src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreControllerExceptionHandler.java
  • src/main/java/inha/gdgoc/domain/recruit/core/repository/RecruitCoreApplicationRepository.java
  • src/main/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationService.java
  • src/main/java/inha/gdgoc/domain/recruit/enums/AdmissionSemester.java
  • src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java
  • src/main/java/inha/gdgoc/domain/recruit/member/controller/message/RecruitMemberMessage.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/request/ApplicationRequest.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/request/CheckPhoneNumberRequest.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/request/PaymentUpdateRequest.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/request/RecruitMemberRequest.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/response/AnswerResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/response/AnswersResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/response/CheckPhoneNumberResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/response/CheckStudentIdResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/response/RecruitMemberSummaryResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/member/dto/response/SpecifiedMemberResponse.java
  • src/main/java/inha/gdgoc/domain/recruit/member/entity/Answer.java
  • src/main/java/inha/gdgoc/domain/recruit/member/entity/RecruitMember.java
  • src/main/java/inha/gdgoc/domain/recruit/member/enums/AdmissionSemester.java
  • src/main/java/inha/gdgoc/domain/recruit/member/enums/EnrolledClassification.java
  • src/main/java/inha/gdgoc/domain/recruit/member/enums/Gender.java
  • src/main/java/inha/gdgoc/domain/recruit/member/enums/InputType.java
  • src/main/java/inha/gdgoc/domain/recruit/member/enums/SurveyType.java
  • src/main/java/inha/gdgoc/domain/recruit/member/exception/RecruitMemberErrorCode.java
  • src/main/java/inha/gdgoc/domain/recruit/member/exception/RecruitMemberException.java
  • src/main/java/inha/gdgoc/domain/recruit/member/repository/AnswerRepository.java
  • src/main/java/inha/gdgoc/domain/recruit/member/repository/RecruitMemberRepository.java
  • src/main/java/inha/gdgoc/domain/recruit/member/service/RecruitMemberService.java
  • src/main/java/inha/gdgoc/domain/test/controller/TestController.java
  • src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java
  • src/main/java/inha/gdgoc/global/security/AccessGuard.java
  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java
  • src/main/java/inha/gdgoc/global/util/SemesterCalculator.java
  • src/main/resources/application-dev.yml
  • src/main/resources/application-local.yml
  • src/main/resources/application-prod.yml
  • src/main/resources/db/migration/V20260114__core_recruit_applications_session_and_status.sql
  • src/test/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminServiceTest.java
  • src/test/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationServiceTest.java
  • src/test/java/inha/gdgoc/domain/recruit/service/RecruitMemberServiceTest.java
  • src/test/java/inha/gdgoc/domain/study/service/StudyAttendeeServiceTest.java
  • src/test/java/inha/gdgoc/domain/study/service/StudyServiceTest.java
  • src/test/resources/application-test.yml
💤 Files with no reviewable changes (12)
  • src/main/java/inha/gdgoc/domain/recruit/enums/AdmissionSemester.java
  • src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantDetailResponse.java
  • src/main/java/inha/gdgoc/domain/test/controller/TestController.java
  • src/test/java/inha/gdgoc/domain/study/service/StudyServiceTest.java
  • src/test/java/inha/gdgoc/domain/study/service/StudyAttendeeServiceTest.java
  • src/main/java/inha/gdgoc/domain/core/recruit/repository/CoreRecruitApplicationRepository.java
  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java
  • src/main/java/inha/gdgoc/domain/core/recruit/service/CoreRecruitApplicationService.java
  • src/main/java/inha/gdgoc/domain/core/recruit/dto/request/CoreRecruitApplicationRequest.java
  • src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantSummaryResponse.java
  • src/main/java/inha/gdgoc/domain/core/recruit/entity/CoreRecruitApplication.java
  • src/main/java/inha/gdgoc/domain/core/recruit/controller/CoreRecruitController.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-23T03:40:02.759Z
Learnt from: kaswhy
Repo: GDGoCINHA/24-2_GDGoC_Server PR: 180
File: gdgoc/build.gradle:52-54
Timestamp: 2025-08-23T03:40:02.759Z
Learning: In the gdgoc project using Spring Boot 3, the user kaswhy confirmed that spring-cloud-aws 2.2.6.RELEASE dependency is currently working for them despite known compatibility concerns with Jakarta EE migration.

Applied to files:

  • src/main/resources/application-dev.yml
  • src/main/resources/application-prod.yml
  • src/test/resources/application-test.yml
📚 Learning: 2025-08-30T10:43:25.889Z
Learnt from: kaswhy
Repo: GDGoCINHA/24-2_GDGoC_Server PR: 207
File: src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java:31-39
Timestamp: 2025-08-30T10:43:25.889Z
Learning: The GDGoCINHA project has a consistent security architecture where public endpoints are handled in two layers: 1) SecurityConfig uses .permitAll() for paths like /api/v1/auth/**, /api/v1/game/**, /api/v1/apply/**, /api/v1/check/** and 2) TokenAuthenticationFilter excludes the same paths in shouldNotFilter(). This dual-layer approach is correct and intentional.

Applied to files:

  • src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java
🧬 Code graph analysis (21)
src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/request/RecruitCoreApplicationAcceptRequest.java (3)
src/main/java/inha/gdgoc/domain/user/dto/request/UpdateRoleRequest.java (1)
  • UpdateRoleRequest (6-8)
src/main/java/inha/gdgoc/domain/recruit/dto/request/PaymentUpdateRequest.java (1)
  • PaymentUpdateRequest (3-7)
src/main/java/inha/gdgoc/domain/core/recruit/dto/request/CoreRecruitApplicationRequest.java (2)
  • Getter (10-63)
  • Builder (47-62)
src/main/java/inha/gdgoc/domain/recruit/core/enums/RecruitCoreResultStatus.java (3)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (2)
  • Override (16-19)
  • RequiredArgsConstructor (7-25)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantDetailResponse.java (1)
  • CoreRecruitApplicantDetailResponse (7-42)
src/main/java/inha/gdgoc/domain/recruit/enums/AdmissionSemester.java (1)
  • AdmissionSemester (3-5)
src/main/java/inha/gdgoc/domain/recruit/member/controller/message/RecruitMemberMessage.java (1)
src/main/java/inha/gdgoc/domain/recruit/controller/message/RecruitMemberMessage.java (1)
  • RecruitMemberMessage (3-11)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreControllerExceptionHandler.java (3)
src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java (3)
  • ExceptionHandler (141-152)
  • ExceptionHandler (128-139)
  • ExceptionHandler (27-39)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1)
  • RecruitMemberException (6-11)
src/main/java/inha/gdgoc/domain/core/recruit/controller/message/CoreRecruitApplicationMessage.java (1)
  • CoreRecruitApplicationMessage (3-6)
src/main/resources/application-dev.yml (2)
src/main/java/inha/gdgoc/global/config/s3/S3Config.java (2)
  • Configuration (11-28)
  • Bean (19-22)
src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java (1)
  • Setter (8-16)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreApplicationErrorCode.java (7)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreAlreadyAppliedException.java (1)
  • Getter (5-18)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreApplicationNotFoundException.java (1)
  • Getter (5-13)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (3)
  • RequiredArgsConstructor (7-25)
  • Override (16-19)
  • Override (21-24)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (2)
  • AuthErrorCode (6-35)
  • AuthErrorCode (21-24)
src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1)
  • RequiredArgsConstructor (7-25)
src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (1)
  • StudyAttendeeErrorCode (6-34)
src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (1)
  • StudyErrorCode (6-34)
src/main/resources/application-local.yml (4)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)
  • Configuration (15-82)
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (3)
  • EnableMethodSecurity (25-120)
  • Bean (33-91)
  • auth (40-57)
src/main/java/inha/gdgoc/global/config/s3/S3Config.java (1)
  • Configuration (11-28)
src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java (1)
  • Setter (8-16)
src/main/java/inha/gdgoc/domain/recruit/core/controller/RecruitCoreController.java (5)
src/main/java/inha/gdgoc/domain/core/recruit/controller/CoreRecruitController.java (4)
  • Tag (32-102)
  • Operation (50-86)
  • Operation (88-101)
  • PostMapping (42-48)
src/main/java/inha/gdgoc/domain/core/recruit/service/CoreRecruitApplicationService.java (1)
  • Service (16-55)
src/main/java/inha/gdgoc/domain/core/recruit/entity/CoreRecruitApplication.java (1)
  • Entity (19-69)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantDetailResponse.java (1)
  • CoreRecruitApplicantDetailResponse (7-42)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantSummaryResponse.java (1)
  • CoreRecruitApplicantSummaryResponse (6-29)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreAlreadyAppliedException.java (4)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreApplicationNotFoundException.java (1)
  • Getter (5-13)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (2)
  • RecruitMemberException (6-11)
  • RecruitMemberException (8-10)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (1)
  • RequiredArgsConstructor (7-25)
src/main/java/inha/gdgoc/global/exception/BusinessException.java (1)
  • Getter (5-19)
src/main/resources/db/migration/V20260114__core_recruit_applications_session_and_status.sql (2)
src/main/java/inha/gdgoc/domain/core/recruit/entity/CoreRecruitApplication.java (1)
  • Entity (19-69)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantSummaryResponse.java (1)
  • CoreRecruitApplicantSummaryResponse (6-29)
src/main/java/inha/gdgoc/domain/recruit/member/dto/response/CheckStudentIdResponse.java (2)
src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java (1)
  • CheckStudentIdResponse (3-5)
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)
  • isRegisteredStudentId (52-56)
src/main/java/inha/gdgoc/domain/recruit/core/repository/RecruitCoreApplicationRepository.java (2)
src/main/java/inha/gdgoc/domain/core/recruit/repository/CoreRecruitApplicationRepository.java (2)
  • CoreRecruitApplicationRepository (8-10)
  • findByNameContainingIgnoreCase (9-9)
src/main/java/inha/gdgoc/domain/core/recruit/service/CoreRecruitApplicationService.java (1)
  • Service (16-55)
src/main/java/inha/gdgoc/domain/admin/recruit/core/dto/response/RecruitCoreApplicantSummaryResponse.java (2)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantSummaryResponse.java (2)
  • CoreRecruitApplicantSummaryResponse (6-29)
  • from (17-28)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantDetailResponse.java (2)
  • CoreRecruitApplicantDetailResponse (7-42)
  • from (24-41)
src/main/resources/application-prod.yml (1)
src/main/java/inha/gdgoc/global/config/s3/S3Config.java (2)
  • Configuration (11-28)
  • Bean (19-22)
src/main/java/inha/gdgoc/domain/recruit/member/dto/response/CheckPhoneNumberResponse.java (2)
src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java (1)
  • CheckPhoneNumberResponse (3-5)
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)
  • isRegisteredPhoneNumber (58-62)
src/main/java/inha/gdgoc/domain/recruit/core/dto/response/RecruitCoreApplicationCreateResponse.java (3)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantSummaryResponse.java (2)
  • CoreRecruitApplicantSummaryResponse (6-29)
  • from (17-28)
src/main/java/inha/gdgoc/domain/recruit/dto/response/RecruitMemberSummaryResponse.java (2)
  • RecruitMemberSummaryResponse (5-32)
  • from (15-31)
src/main/java/inha/gdgoc/domain/core/recruit/dto/response/CoreRecruitApplicantDetailResponse.java (2)
  • from (24-41)
  • CoreRecruitApplicantDetailResponse (7-42)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreApplicationNotFoundException.java (1)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreAlreadyAppliedException.java (1)
  • Getter (5-18)
src/main/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminService.java (1)
src/main/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationService.java (1)
  • Service (25-119)
src/main/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationService.java (1)
src/main/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminService.java (1)
  • Service (23-114)
src/main/java/inha/gdgoc/domain/recruit/core/entity/RecruitCoreApplication.java (2)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreAlreadyAppliedException.java (1)
  • Getter (5-18)
src/main/java/inha/gdgoc/domain/recruit/core/exception/RecruitCoreApplicationNotFoundException.java (1)
  • Getter (5-13)
src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java (1)
src/main/java/inha/gdgoc/domain/recruit/member/controller/message/RecruitMemberMessage.java (1)
  • RecruitMemberMessage (3-11)
🪛 Gitleaks (8.30.0)
src/test/resources/application-test.yml

[high] 62-62: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 62-63: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +46 to +73
@Transactional
public RecruitCoreApplicationDecisionResponse accept(
Long applicationId,
Long reviewerId,
RecruitCoreApplicationAcceptRequest request
) {
RecruitCoreApplication application = getApplication(applicationId);
ensureDecidable(application);
Instant now = Instant.now();
application.accept(reviewerId, request.resultNote(), now);

User applicant = application.getUser();
if (!UserRole.hasAtLeast(applicant.getUserRole(), UserRole.CORE)) {
applicant.changeRole(UserRole.CORE);
}
TeamType applicantTeam = applicant.getTeam();
TeamType applicationTeam = teamTypeOf(application.getTeam());
if (applicationTeam != null && (Boolean.TRUE.equals(request.overwriteTeamIfExists()) || applicantTeam == null)) {
applicant.changeTeam(applicationTeam);
applicantTeam = applicationTeam;
}

return RecruitCoreApplicationDecisionResponse.accepted(
application,
applicant.getUserRole(),
applicantTeam
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.java" | xargs grep -l "class RecruitCoreApplication" | head -5

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 539


🏁 Script executed:

# Find the RecruitCoreApplication entity
fd -e java -type f | xargs grep -l "RecruitCoreApplication" | grep -E "(entity|domain)" | head -10

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 241


🏁 Script executed:

# Search for RecruitCoreApplication class definition
rg "class RecruitCoreApplication" -A 50 --max-count=1

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 20848


🏁 Script executed:

# Check for `@Version` annotation in the project
rg "@Version" --type java -B 2 -A 2

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

cat src/main/java/inha/gdgoc/domain/recruit/core/entity/RecruitCoreApplication.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 3832


🏁 Script executed:

# Check ensureDecidable method in the service file
cat src/main/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminService.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 4612


🏁 Script executed:

# Check if repository uses pessimistic locking
cat src/main/java/inha/gdgoc/domain/recruit/core/repository/RecruitCoreApplicationRepository.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 871


🏁 Script executed:

# Check BaseEntity to see if it has `@Version`
cat src/main/java/inha/gdgoc/global/entity/BaseEntity.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 804


동시 요청 시 race condition 발생 가능 - 락 메커니즘 필수

동일한 지원서에 대해 여러 관리자가 동시에 accept()/reject()를 호출할 경우, ensureDecidable() 검사는 원자적(atomic)이지 않아 둘 다 통과한 후 중복 처리될 수 있습니다. 현재 RecruitCoreApplication 엔티티에는 @Version 어노테이션이 없고, 저장소 메서드도 @Lock을 사용하지 않으므로 낙관적/비관적 잠금이 적용되지 않아 있습니다.

이 경우 두 스레드가 동시에 상태 업데이트를 시도할 때, 첫 번째 스레드의 reviewedBy, reviewedAt, resultNote 값이 두 번째 스레드에 의해 덮어씌워져 데이터 손실이 발생할 수 있습니다. 낙관적 잠금(@Version)이나 비관적 잠금(@Lock(LockModeType.PESSIMISTIC_WRITE))을 추가하여 동시성 제어를 해야 합니다.

🤖 Prompt for AI Agents
In
`@src/main/java/inha/gdgoc/domain/admin/recruit/core/service/RecruitCoreAdminService.java`
around lines 46 - 73, The accept(...) flow has a race where ensureDecidable()
can pass concurrently; fix by adding concurrency control: either add an
optimistic lock field (`@Version`) to the RecruitCoreApplication entity and
persist it so concurrent commits will fail fast, or load the application with a
pessimistic lock in getApplication() by adding a repository method annotated
with `@Lock`(LockModeType.PESSIMISTIC_WRITE) (e.g.,
RecruitCoreApplicationRepository.findByIdWithLock(...) used by
getApplication()). Keep the `@Transactional` on accept(), handle
OptimisticLockException / LockTimeoutException to return an appropriate error,
and ensure ensureDecidable() runs after acquiring the lock so only one thread
proceeds to application.accept(...).

Comment on lines 4 to 5
public static final String APPLICANT_LIST_RETRIEVED_SUCCESS = "성공적으로 코어 리쿠르트 지원자 목록을 조회했습니다.";
public static final String APPLICANT_RETRIEVED_SUCCESS = "성공적으로 코어 리쿠르트 지원자 상세를 조회했습니다.";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

오타 수정 필요: "리쿠르트" → "리크루트"

"Recruit"의 한국어 표기는 "리크루트"입니다. 현재 "리쿠르트"로 되어 있어 수정이 필요합니다.

✏️ 수정 제안
-    public static final String APPLICANT_LIST_RETRIEVED_SUCCESS = "성공적으로 코어 리쿠르트 지원자 목록을 조회했습니다.";
-    public static final String APPLICANT_RETRIEVED_SUCCESS = "성공적으로 코어 리쿠르트 지원자 상세를 조회했습니다.";
+    public static final String APPLICANT_LIST_RETRIEVED_SUCCESS = "성공적으로 코어 리크루트 지원자 목록을 조회했습니다.";
+    public static final String APPLICANT_RETRIEVED_SUCCESS = "성공적으로 코어 리크루트 지원자 상세를 조회했습니다.";
📝 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
public static final String APPLICANT_LIST_RETRIEVED_SUCCESS = "성공적으로 코어 리쿠르트 지원자 목록을 조회했습니다.";
public static final String APPLICANT_RETRIEVED_SUCCESS = "성공적으로 코어 리쿠르트 지원자 상세를 조회했습니다.";
public static final String APPLICANT_LIST_RETRIEVED_SUCCESS = "성공적으로 코어 리크루트 지원자 목록을 조회했습니다.";
public static final String APPLICANT_RETRIEVED_SUCCESS = "성공적으로 코어 리크루트 지원자 상세를 조회했습니다.";
🤖 Prompt for AI Agents
In
`@src/main/java/inha/gdgoc/domain/recruit/core/controller/message/RecruitCoreApplicationMessage.java`
around lines 4 - 5, Fix the typo in the two message constants by replacing
"리쿠르트" with the correct "리크루트": update the string values of
APPLICANT_LIST_RETRIEVED_SUCCESS and APPLICANT_RETRIEVED_SUCCESS so they read
"성공적으로 코어 리크루트 지원자 목록을 조회했습니다." and "성공적으로 코어 리크루트 지원자 상세를 조회했습니다."
respectively.

@NotBlank String wish,
@NotBlank String strengths,
@NotBlank String pledge,
@NotNull @Size(min = 0) List<@NotBlank String> fileUrls
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

@Size(min = 0)는 불필요한 제약조건입니다.

@Size(min = 0)은 실질적인 검증을 수행하지 않습니다. 모든 리스트의 크기는 이미 0 이상이기 때문입니다. @NotNull만으로 null 체크가 충분하며, 빈 리스트는 유효한 입력으로 허용됩니다.

🔧 제안된 수정
-    `@NotNull` `@Size`(min = 0) List<@NotBlank String> fileUrls
+    `@NotNull` List<@NotBlank String> fileUrls
📝 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
@NotNull @Size(min = 0) List<@NotBlank String> fileUrls
`@NotNull` List<@NotBlank String> fileUrls
🤖 Prompt for AI Agents
In
`@src/main/java/inha/gdgoc/domain/recruit/core/dto/request/RecruitCoreApplicationCreateRequest.java`
at line 17, 필드 fileUrls의 불필요한 제약조건 `@Size`(min = 0)을 제거하세요:
RecruitCoreApplicationCreateRequest 클래스에서 선언된 "@NotNull `@Size`(min = 0)
List<@NotBlank String> fileUrls"에서 `@Size`(min = 0) 어노테이션을 삭제하고 `@NotNull` 및 요소 검증용
`@NotBlank는` 그대로 유지하여 null 체크는 보장하되 빈 리스트는 허용되도록 변경하세요.

Comment on lines +94 to +111
@Transactional(readOnly = true)
public RecruitCoreApplicantDetailResponse getApplicantDetailForViewer(
Long applicationId,
Long viewerId,
UserRole viewerRole
) {
RecruitCoreApplication application = repository.findById(applicationId)
.orElseThrow(RecruitCoreApplicationNotFoundException::new);
boolean privileged = UserRole.hasAtLeast(viewerRole, UserRole.LEAD);
if (!privileged && !application.isOwnedBy(viewerId)) {
throw new BusinessException(GlobalErrorCode.FORBIDDEN_USER);
}
return RecruitCoreApplicantDetailResponse.from(application);
}

private RecruitCoreApplication getApplication(Long id) {
return repository.findById(id)
.orElseThrow(() -> new BusinessException(GlobalErrorCode.RESOURCE_NOT_FOUND));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

예외 처리 일관성 검토 필요

동일한 "application not found" 상황에서 다른 예외가 사용되고 있습니다:

  • Line 100-101: RecruitCoreApplicationNotFoundException 사용
  • Line 109-111 (getApplication 헬퍼): BusinessException(GlobalErrorCode.RESOURCE_NOT_FOUND) 사용

이로 인해 동일한 시나리오에서 다른 HTTP 응답 코드나 메시지가 반환될 수 있습니다. 의도된 설계인지 확인하고, 아니라면 통일하는 것을 권장합니다.

♻️ 일관된 예외 처리 제안

옵션 1: RecruitCoreApplicationNotFoundException으로 통일

 private RecruitCoreApplication getApplication(Long id) {
     return repository.findById(id)
-        .orElseThrow(() -> new BusinessException(GlobalErrorCode.RESOURCE_NOT_FOUND));
+        .orElseThrow(RecruitCoreApplicationNotFoundException::new);
 }

옵션 2: 헬퍼 메서드 재사용

 public RecruitCoreApplicantDetailResponse getApplicantDetailForViewer(
     Long applicationId,
     Long viewerId,
     UserRole viewerRole
 ) {
-    RecruitCoreApplication application = repository.findById(applicationId)
-        .orElseThrow(RecruitCoreApplicationNotFoundException::new);
+    RecruitCoreApplication application = getApplication(applicationId);
     boolean privileged = UserRole.hasAtLeast(viewerRole, UserRole.LEAD);
🤖 Prompt for AI Agents
In
`@src/main/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationService.java`
around lines 94 - 111, getApplicantDetailForViewer currently throws
RecruitCoreApplicationNotFoundException while the helper getApplication throws
BusinessException(GlobalErrorCode.RESOURCE_NOT_FOUND), causing inconsistent
"application not found" behavior; fix by unifying the handling — either make
getApplication throw RecruitCoreApplicationNotFoundException (replace
BusinessException with new RecruitCoreApplicationNotFoundException) or, simpler,
have getApplicantDetailForViewer call the existing getApplication(applicationId)
and rely on its exception, ensuring both flows use the same exception type.

Comment on lines 51 to 57
private static final String LEAD_OR_HR_RULE =
"@accessGuard.check(authentication,"
+ " T(inha.gdgoc.global.security.AccessGuard.AccessCondition).atLeast("
+ "T(inha.gdgoc.domain.user.enums.UserRole).LEAD),"
+ " T(inha.gdgoc.global.security.AccessGuard.AccessCondition).atLeast("
+ "T(inha.gdgoc.domain.user.enums.UserRole).CORE,"
+ " T(inha.gdgoc.domain.user.enums.TeamType).HR))";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find and examine the AccessGuard class
find . -type f -name "AccessGuard.java" | head -20

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 130


🏁 Script executed:

# Search for AccessGuard class definition
rg -n "class AccessGuard|interface AccessGuard" --type java -A 15

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1573


🏁 Script executed:

# Search for AccessCondition enum/class
rg -n "class AccessCondition|enum AccessCondition" --type java -A 10

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

# Look for UserRole enum
rg -n "enum UserRole|class UserRole" --type java -A 15

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1476


🏁 Script executed:

# Look for TeamType enum
rg -n "enum TeamType|class TeamType" --type java -A 10

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 924


🏁 Script executed:

# Check the actual RecruitMemberController file for context around the constant
rg -n "LEAD_OR_HR_RULE" --type java -B 5 -A 10

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 14639


🏁 Script executed:

# Get the full AccessGuard.java file to check for inner class/enum AccessCondition
cat src/main/java/inha/gdgoc/global/security/AccessGuard.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 3118


🏁 Script executed:

# Search for atLeast method in the codebase
rg -n "atLeast" --type java -B 2 -A 5

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 9625


🏁 Script executed:

# Check if there are any overloaded atLeast() methods in AccessCondition record
rg -n "public static AccessCondition atLeast" --type java -A 5

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 704


🏁 Script executed:

# Check if there's an 'of()' method that takes two parameters for TeamType
rg -n "public static AccessCondition of" --type java -A 8

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1743


SpEL 표현식에 메서드 호출 오류가 있습니다. atLeast()는 하나의 파라미터만 받지만 두 개의 파라미터로 호출되고 있습니다.

LEAD_OR_HR_RULE 상수의 SpEL 표현식에 심각한 버그가 있습니다. 두 번째 조건에서 atLeast(CORE, HR)을 호출하려고 하는데, AccessCondition.atLeast()UserRole 하나의 파라미터만 받습니다. 런타임 시 SpEL 평가 단계에서 메서드 호출 오류가 발생합니다.

대신 of(UserRole.CORE, TeamType.HR) 메서드를 사용해야 합니다. 이 메서드는 역할과 팀 타입을 모두 받도록 설계되었습니다.

수정이 필요한 부분

현재:

+ " T(inha.gdgoc.global.security.AccessGuard.AccessCondition).atLeast("
+ "T(inha.gdgoc.domain.user.enums.UserRole).CORE,"
+ " T(inha.gdgoc.domain.user.enums.TeamType).HR))"

수정 필요:

+ " T(inha.gdgoc.global.security.AccessGuard.AccessCondition).of("
+ "T(inha.gdgoc.domain.user.enums.UserRole).CORE,"
+ " T(inha.gdgoc.domain.user.enums.TeamType).HR))"

이 오류는 RecruitMemberControllerUserAdminController 모두에서 동일하게 나타납니다.

🤖 Prompt for AI Agents
In
`@src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java`
around lines 51 - 57, The SpEL in LEAD_OR_HR_RULE calls
AccessCondition.atLeast(...) with two params which causes a runtime SpEL method
error; replace the second atLeast(...) invocation with
AccessCondition.of(UserRole.CORE, TeamType.HR) so the role+team pair is passed
correctly. Update the LEAD_OR_HR_RULE constant in RecruitMemberController (and
the identical constant in UserAdminController) to use
T(inha.gdgoc.global.security.AccessGuard.AccessCondition).of(...) with
T(inha.gdgoc.domain.user.enums.UserRole).CORE and
T(inha.gdgoc.domain.user.enums.TeamType).HR as the two arguments.

Comment on lines +5 to +11
cloud:
aws:
credentials:
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
region:
static: ${AWS_REGION}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check for Spring Cloud AWS dependency in pom.xml or build.gradle
fd -e xml -e gradle | head -20

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 99


🏁 Script executed:

# Find S3Config.java and examine its implementation
fd -t f "S3Config.java" -x cat {}

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 990


🏁 Script executed:

# Check for any CredentialsProvider or AWS configuration beans
rg "DefaultCredentialsProvider|CredentialsProvider" -t java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 680


🏁 Script executed:

# Check build.gradle for Spring Cloud AWS dependency
cat build.gradle | grep -A 5 -B 5 "spring-cloud-aws" || echo "Spring Cloud AWS dependency not found"

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 737


🏁 Script executed:

# Search for any reference to spring.cloud.aws.credentials properties
rg "spring\.cloud\.aws\.credentials" --type yaml --type java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


AWS 자격 증명 설정은 사용되지 않고 있습니다.

S3Config.java에서 명시적으로 DefaultCredentialsProvider.create()를 사용하고 있어, application-local.ymlspring.cloud.aws.credentials 설정(access-key, secret-key)은 무시됩니다. 대신 AWS SDK의 기본 자격 증명 체인(환경 변수, 시스템 프로퍼티 등)을 따릅니다.

문제는 spring.cloud.aws.region.static은 사용되지만 (awsRegion 빈에서 주입됨), 같은 방식으로 정의된 credentials는 사용되지 않는다는 점입니다. 혼동을 피하기 위해 spring.cloud.aws.credentials 설정을 제거하거나, S3Config가 이를 직접 활용하도록 수정하는 것이 좋습니다.

🤖 Prompt for AI Agents
In `@src/main/resources/application-local.yml` around lines 5 - 11, The
application-local.yml AWS credentials are unused because S3Config.java
instantiates DefaultCredentialsProvider.create(), so
spring.cloud.aws.credentials (access-key/secret-key) is ignored; either remove
those keys from application-local.yml or change S3Config to load credentials
from Spring properties. To fix in code, update S3Config (the class that creates
the S3 client and currently calls DefaultCredentialsProvider.create()) to
instead construct a credentials provider from the Spring properties (bind
spring.cloud.aws.credentials -> e.g., use access-key and secret-key injected or
a AwsCredentialsProvider built from those values) or delegate to
DefaultCredentialsProvider only when the properties are absent; keep using the
existing awsRegion bean (spring.cloud.aws.region.static) as before. Ensure
configuration and behavior are consistent by removing unused
spring.cloud.aws.credentials entries from application-local.yml if you choose
the provider-chain approach.

Comment on lines +5 to +11
cloud:
aws:
credentials:
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
region:
static: ${AWS_REGION}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find S3Config file location
fd -e java -i s3config

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 126


🏁 Script executed:

# Check for DefaultCredentialsProvider usage
rg "DefaultCredentialsProvider" -A 5 -B 5 --type=java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 2086


🏁 Script executed:

# Search for spring.cloud.aws.credentials usage
rg "spring\.cloud\.aws\.credentials" --type=yaml --type=java --type=properties

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 105


🏁 Script executed:

# Check for AWS credential configuration in the codebase
rg "@ConfigurationProperties.*aws|AwsCredentialsProvider|AwsCredentials" --type=java -A 3

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1271


🏁 Script executed:

# Look for Spring Cloud AWS auto-configuration or AwsProperties binding
rg "AwsProperties|@EnableAwsServices|@EnableSpringDataAwsDynamodb" --type=java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

cat -n src/main/java/inha/gdgoc/global/config/s3/S3Config.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1186


🏁 Script executed:

# Check if spring-cloud-aws is in dependencies
fd pom.xml && cat pom.xml | grep -A 2 -B 2 "spring-cloud-aws\|aws-java-sdk"

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 110


🏁 Script executed:

# Search for any YAML property binding with spring.cloud.aws
rg "spring\.cloud\.aws" --type=yaml

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

# Find pom.xml
fd pom.xml

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 53


🏁 Script executed:

# Check if there's a build.gradle instead
fd build.gradle

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 83


🏁 Script executed:

# Double-check if spring.cloud.aws credentials are referenced anywhere
rg "access-key|secret-key" --type=yaml -B 2 -A 2

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1636


AWS 자격 증명 YAML 설정이 실제로 사용되지 않습니다.

S3Config.java에서 AWS 리전은 YAML 설정(spring.cloud.aws.region.static)을 @Value로 주입받지만, 자격 증명은 DefaultCredentialsProvider.create()를 사용합니다. 따라서 YAML에 정의된 spring.cloud.aws.credentials.access-keysecret-key는 코드 어디에서도 참조되지 않습니다.

DefaultCredentialsProvider는 환경 변수(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), 시스템 프로퍼티, IAM 역할 등 AWS SDK의 기본 자격 증명 체인을 따르므로, YAML의 명시적 설정은 무시됩니다.

다음 중 하나로 정리해 주세요:

  1. YAML의 불필요한 credentials 설정 제거 (환경 변수만 사용)
  2. 또는 S3Config에서 YAML 프로퍼티를 직접 주입받도록 수정
🤖 Prompt for AI Agents
In `@src/main/resources/application-prod.yml` around lines 5 - 11, The YAML
defines AWS credentials that are never used because S3Config.java injects
spring.cloud.aws.region.static but uses DefaultCredentialsProvider.create() for
auth; either remove the unused spring.cloud.aws.credentials.access-key and
secret-key entries from application-prod.yml to rely on environment
variables/SDK credential chain, or modify S3Config (the class using
DefaultCredentialsProvider.create()) to `@Value-inject` the YAML properties
(spring.cloud.aws.credentials.access-key and
spring.cloud.aws.credentials.secret-key) and construct the credentials provider
from those values so the YAML settings are actually applied.

Comment on lines +71 to +74
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: off
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

프로덕션 환경에서 Hibernate SQL 디버그 로깅 활성화 검토 필요

org.hibernate.SQL: debug 설정은 모든 SQL 쿼리를 로깅하여 프로덕션 환경에서 성능 저하와 과도한 로그 볼륨을 유발할 수 있습니다. 일반적으로 프로덕션에서는 warn 또는 error 레벨을 권장합니다.

🔧 프로덕션 환경에 적합한 로깅 레벨 제안
 logging:
   level:
-    org.hibernate.SQL: debug
+    org.hibernate.SQL: warn
     org.hibernate.type: off
🤖 Prompt for AI Agents
In `@src/main/resources/application-prod.yml` around lines 71 - 74, Change the
production logging level for Hibernate SQL to avoid excessive query logging:
update the logging.level key for org.hibernate.SQL from debug to a non-verbose
level (e.g., warn or error) in the production config, keep org.hibernate.type as
off, and if you need debug for development move the debug setting into a
dev-specific profile (e.g., application-dev.yml) or guard it by active profile
so that org.hibernate.SQL is only debug when running in non-production
environments.

Comment on lines +18 to +25
update core_recruit_applications cra
set user_id = u.id
from users u
where cra.user_id is null
and u.email = cra.email;

alter table core_recruit_applications
alter column user_id set not null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

user_id 백필 후 NOT NULL 설정 시 마이그레이션 실패 위험

users 테이블에 매칭되는 이메일이 없는 기존 core_recruit_applications 레코드가 있을 경우, user_id가 NULL로 남아 24-25줄의 ALTER COLUMN user_id SET NOT NULL이 실패합니다.

마이그레이션 전 검증 쿼리를 추가하거나, 고아 레코드 처리 전략을 명시하는 것이 안전합니다.

🛡️ 안전한 마이그레이션을 위한 검증 추가 제안
 update core_recruit_applications cra
 set user_id = u.id
 from users u
 where cra.user_id is null
   and u.email = cra.email;

+-- 매칭되지 않는 레코드 확인 (프로덕션 배포 전 수동 검증 권장)
+-- SELECT id, email FROM core_recruit_applications WHERE user_id IS NULL;
+
+-- 옵션 1: 고아 레코드 삭제 (데이터 손실 감수 시)
+-- DELETE FROM core_recruit_applications WHERE user_id IS NULL;
+
+-- 옵션 2: 마이그레이션 실패 시 명확한 에러 메시지
+DO $$
+BEGIN
+    IF EXISTS (SELECT 1 FROM core_recruit_applications WHERE user_id IS NULL) THEN
+        RAISE EXCEPTION 'Migration blocked: orphan applications exist without matching user email';
+    END IF;
+END $$;
+
 alter table core_recruit_applications
     alter column user_id set not null;
📝 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
update core_recruit_applications cra
set user_id = u.id
from users u
where cra.user_id is null
and u.email = cra.email;
alter table core_recruit_applications
alter column user_id set not null;
update core_recruit_applications cra
set user_id = u.id
from users u
where cra.user_id is null
and u.email = cra.email;
-- 매칭되지 않는 레코드 확인 (프로덕션 배포 전 수동 검증 권장)
-- SELECT id, email FROM core_recruit_applications WHERE user_id IS NULL;
-- 옵션 1: 고아 레코드 삭제 (데이터 손실 감수 시)
-- DELETE FROM core_recruit_applications WHERE user_id IS NULL;
-- 옵션 2: 마이그레이션 실패 시 명확한 에러 메시지
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM core_recruit_applications WHERE user_id IS NULL) THEN
RAISE EXCEPTION 'Migration blocked: orphan applications exist without matching user email';
END IF;
END $$;
alter table core_recruit_applications
alter column user_id set not null;

Comment on lines +138 to +147
@Test
void getApplicantDetailForViewer_whenOwnerAlllowed() {
RecruitCoreApplication application = createApplication(99L, createUser(1L), SESSION);
when(repository.findById(99L)).thenReturn(Optional.of(application));

RecruitCoreApplicantDetailResponse detail =
service.getApplicantDetailForViewer(99L, 1L, UserRole.MEMBER);

assertThat(detail.applicationId()).isEqualTo(99L);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

메서드명 오타 수정 필요

whenOwnerAlllowed에 'l'이 3개 있습니다. whenOwnerAllowed로 수정해 주세요.

✏️ 수정 제안
     `@Test`
-    void getApplicantDetailForViewer_whenOwnerAlllowed() {
+    void getApplicantDetailForViewer_whenOwnerAllowed() {
         RecruitCoreApplication application = createApplication(99L, createUser(1L), SESSION);
🤖 Prompt for AI Agents
In
`@src/test/java/inha/gdgoc/domain/recruit/core/service/RecruitCoreApplicationServiceTest.java`
around lines 138 - 147, Rename the test method
getApplicantDetailForViewer_whenOwnerAlllowed to correct the typo
(getApplicantDetailForViewer_whenOwnerAllowed) so the name has only two 'l's in
"Allowed"; update the method declaration and any references (e.g., IDE/run
configurations or test annotations) that refer to
getApplicantDetailForViewer_whenOwnerAlllowed to the new name to keep tests
discoverable and runnable.

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

🤖 Fix all issues with AI agents
In
`@src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java`:
- Around line 22-24: The PreAuthorize expression in GuestbookController uses
atLeast(UserRole.LEAD) which expands access to LEAD and above (LEAD, ORGANIZER,
ADMIN) instead of limiting to ORGANIZER+; update the annotation on
GuestbookController to use atLeast(UserRole.ORGANIZER) (change the atLeast
argument from UserRole.LEAD to UserRole.ORGANIZER) so only ORGANIZER and higher
roles are allowed; verify the expression references AccessGuard.check(...) and
the enum type T(inha.gdgoc.domain.user.enums.UserRole).ORGANIZER is used
consistently.

In `@src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java`:
- Around line 27-37: Extract the duplicated SpEL constants LEAD_OR_HR_RULE and
LEAD_OR_HIGHER_RULE into a single shared class (e.g., SecurityRules) as public
static final String fields, then replace the local constants in
UserAdminController, RecruitMemberController, and CoreAttendanceController with
references to SecurityRules.LEAD_OR_HR_RULE and
SecurityRules.LEAD_OR_HIGHER_RULE; ensure the new class has the exact same
string values, update imports/usages in the controllers, and verify access
modifiers so the constants are visible where referenced and the project
compiles.
♻️ Duplicate comments (1)
src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java (1)

51-57: SpEL 메서드 호출 오류: atLeast()는 하나의 파라미터만 받습니다.

두 번째 atLeast() 호출에서 UserRole.CORETeamType.HR 두 개의 파라미터를 전달하고 있지만, atLeast() 메서드는 UserRole 하나만 받습니다. 런타임 시 SpEL 평가 오류가 발생합니다.

역할과 팀을 함께 지정하려면 of() 메서드를 사용해야 합니다:

수정 제안
 private static final String LEAD_OR_HR_RULE =
         "@accessGuard.check(authentication,"
                 + " T(inha.gdgoc.global.security.AccessGuard$AccessCondition).atLeast("
                 + "T(inha.gdgoc.domain.user.enums.UserRole).LEAD),"
-                + " T(inha.gdgoc.global.security.AccessGuard$AccessCondition).atLeast("
+                + " T(inha.gdgoc.global.security.AccessGuard$AccessCondition).of("
                 + "T(inha.gdgoc.domain.user.enums.UserRole).CORE,"
                 + " T(inha.gdgoc.domain.user.enums.TeamType).HR))";
🧹 Nitpick comments (3)
src/main/java/inha/gdgoc/domain/admin/recruit/core/controller/RecruitCoreAdminController.java (3)

44-66: 응답 래핑 방식이 다른 컨트롤러와 일관성이 없습니다.

관련 스니펫의 UserAdminController, GuestbookController, CoreAttendanceController에서는 ResponseEntity<ApiResponse<T, M>> 패턴을 사용합니다. 이 엔드포인트는 RecruitCoreApplicationPageResponse를 직접 반환하여 일관성이 떨어집니다.

또한 다른 컨트롤러에서 사용하는 @Operation(summary = "...", security = {@securityrequirement(name = "BearerAuth")}) Swagger 어노테이션이 누락되어 있습니다.

♻️ 권장 수정
+    `@Operation`(summary = "운영진 지원서 목록 조회", security = {`@SecurityRequirement`(name = "BearerAuth")})
     `@PreAuthorize`("hasAnyRole('ADMIN','ORGANIZER')")
     `@GetMapping`
-    public RecruitCoreApplicationPageResponse list(
+    public ResponseEntity<ApiResponse<RecruitCoreApplicationPageResponse, PageMeta>> list(
         `@RequestParam` String session,
         `@RequestParam`(required = false) RecruitCoreResultStatus status,
         `@RequestParam`(required = false) TeamType team,
         `@RequestParam`(defaultValue = "0") int page,
         `@RequestParam`(defaultValue = "20") int size
     ) {
         Pageable pageable = PageRequest.of(page, size);
         Page<RecruitCoreApplication> result = adminService.searchApplications(session, status, team, pageable);
-        java.util.List<RecruitCoreApplicantSummaryResponse> content = result
+        List<RecruitCoreApplicantSummaryResponse> content = result
             .map(RecruitCoreApplicantSummaryResponse::from)
             .getContent();
-        return RecruitCoreApplicationPageResponse.from(
+        RecruitCoreApplicationPageResponse response = RecruitCoreApplicationPageResponse.from(
             content,
             result.getNumber(),
             result.getSize(),
             result.getTotalElements(),
             result.getTotalPages(),
             result.isLast()
         );
+        return ResponseEntity.ok(ApiResponse.ok("RECRUIT_CORE_APPLICATION_LIST_RETRIEVED", response, PageMeta.of(result)));
     }

import 추가:

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import inha.gdgoc.global.dto.ApiResponse;
import inha.gdgoc.global.dto.PageMeta;
import java.util.List;

68-78: accept 엔드포인트에 Swagger 어노테이션 및 ApiResponse 래핑 누락

list 메서드와 동일하게 @Operation 어노테이션과 ApiResponse 래핑이 필요합니다.

♻️ 권장 수정
+    `@Operation`(summary = "운영진 지원 승인", security = {`@SecurityRequirement`(name = "BearerAuth")})
     `@PreAuthorize`(ORGANIZER_OR_HR_LEAD_RULE)
     `@PostMapping`("/{applicationId}/accept")
-    public ResponseEntity<RecruitCoreApplicationDecisionResponse> accept(
+    public ResponseEntity<ApiResponse<RecruitCoreApplicationDecisionResponse, Void>> accept(
         `@AuthenticationPrincipal` CustomUserDetails reviewer,
         `@PathVariable` Long applicationId,
         `@Valid` `@RequestBody` RecruitCoreApplicationAcceptRequest request
     ) {
         RecruitCoreApplicationDecisionResponse response =
             adminService.accept(applicationId, reviewer.getUserId(), request);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(ApiResponse.ok("RECRUIT_CORE_APPLICATION_ACCEPTED", response));
     }

80-91: reject 엔드포인트에 Swagger 어노테이션 및 ApiResponse 래핑 누락

accept 메서드와 동일하게 수정이 필요합니다.

♻️ 권장 수정
+    `@Operation`(summary = "운영진 지원 거절", security = {`@SecurityRequirement`(name = "BearerAuth")})
     `@PreAuthorize`(ORGANIZER_OR_HR_LEAD_RULE)
     `@PostMapping`("/{applicationId}/reject")
-    public ResponseEntity<RecruitCoreApplicationDecisionResponse> reject(
+    public ResponseEntity<ApiResponse<RecruitCoreApplicationDecisionResponse, Void>> reject(
         `@AuthenticationPrincipal` CustomUserDetails reviewer,
         `@PathVariable` Long applicationId,
         `@Valid` `@RequestBody` RecruitCoreApplicationRejectRequest request
     ) {
         RecruitCoreApplicationDecisionResponse response =
             adminService.reject(applicationId, reviewer.getUserId(), request);
-        return ResponseEntity.ok(response);
+        return ResponseEntity.ok(ApiResponse.ok("RECRUIT_CORE_APPLICATION_REJECTED", response));
     }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d476cc5 and 16698f6.

📒 Files selected for processing (5)
  • src/main/java/inha/gdgoc/domain/admin/recruit/core/controller/RecruitCoreAdminController.java
  • src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java
  • src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java
  • src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java
  • src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/inha/gdgoc/domain/admin/recruit/core/controller/RecruitCoreAdminController.java (3)
src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java (1)
  • RequiredArgsConstructor (22-92)
src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java (1)
  • RestController (30-152)
src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java (1)
  • RestController (19-82)
src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java (1)
src/main/java/inha/gdgoc/domain/recruit/member/controller/message/RecruitMemberMessage.java (1)
  • RecruitMemberMessage (3-11)
src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java (1)
src/main/java/inha/gdgoc/domain/core/attendance/controller/message/CoreAttendanceMessage.java (1)
  • CoreAttendanceMessage (3-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (7)
src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java (3)

1-9: LGTM!

패키지 및 정적 임포트가 새로운 recruit.member 구조에 맞게 일관성 있게 업데이트되었습니다.


11-18: LGTM!

DTO, 엔티티, 서비스 임포트가 새로운 recruit.member 패키지 경로로 올바르게 업데이트되었습니다.


96-96: 접근 제어 중앙화 패턴 적용 승인.

LEAD_OR_HR_RULE 상수를 통한 접근 제어 중앙화는 유지보수성을 높이는 좋은 접근 방식입니다. SpEL 표현식 버그가 수정되면 정상 동작할 것입니다.

Also applies to: 111-111, 133-133

src/main/java/inha/gdgoc/domain/admin/recruit/core/controller/RecruitCoreAdminController.java (1)

1-42: 클래스 구조와 접근 규칙이 적절합니다.

ORGANIZER_OR_HR_LEAD_RULE 상수가 다른 컨트롤러(UserAdminController, CoreAttendanceController)에서 사용하는 accessGuard.check 패턴과 일관성 있게 정의되어 있습니다.

src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java (1)

43-43: 권한 분리 로직 적절함.

각 엔드포인트별 권한 분리가 명확합니다:

  • list, updateUserRole: HR 팀도 접근 가능 (사용자 조회/역할 수정은 HR 업무)
  • updateRoleTeam, deleteUser: LEAD 이상만 접근 가능 (팀 변경/삭제는 더 민감한 작업)

updateUserRole(역할만 수정)과 updateRoleTeam(역할+팀 수정)의 권한 차이가 의도된 것이라면, 이 설계는 합리적입니다.

Also applies to: 59-59, 71-71, 83-83

src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java (2)

63-75: 메서드 레벨 권한 제어가 적절합니다.

날짜 생성/삭제 API에 대해 클래스 레벨(LEAD_OR_HIGHER)보다 더 높은 권한(ORGANIZER_OR_HIGHER)을 요구하는 것은 적절한 설계입니다. 조회는 리드 이상, 생성/삭제는 운영진 이상으로 권한을 분리하여 최소 권한 원칙을 잘 따르고 있습니다.


33-43: AccessGuard 기반 권한 규칙 상수화 - 리팩토링이 적절하게 구현되었습니다.

권한 검사 로직을 상수로 추출하여 재사용성과 유지보수성을 높인 점이 좋습니다. AccessGuard.AccessCondition.atLeast() 메서드가 UserRole.hasAtLeast()를 통해 역할 계층을 올바르게 처리하고 있으며, 기존 hasAnyRole('ORGANIZER','ADMIN') 방식과 동등한 동작을 보장합니다. 역할 계층(GUEST < MEMBER < CORE < LEAD < ORGANIZER < ADMIN)이 명확하게 정의되어 있어 유지보수도 용이합니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +22 to +24
@PreAuthorize("@accessGuard.check(authentication,"
+ " T(inha.gdgoc.global.security.AccessGuard$AccessCondition).atLeast("
+ "T(inha.gdgoc.domain.user.enums.UserRole).LEAD))")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find UserRole enum definition
echo "=== Locating UserRole.java ==="
fd -t f "UserRole.java"

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Find AccessGuard class
echo "=== Locating AccessGuard.java ==="
fd -t f "AccessGuard.java"

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 162


🏁 Script executed:

#!/bin/bash
# Search for role usage patterns in the codebase
echo "=== Searching for ORGANIZER/ADMIN role usage ==="
rg "ORGANIZER|ADMIN" --type java -n | head -30

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 4388


🏁 Script executed:

cat -n src/main/java/inha/gdgoc/domain/user/enums/UserRole.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1155


🏁 Script executed:

cat -n src/main/java/inha/gdgoc/global/security/AccessGuard.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 3748


권한 검사 로직 변경 시 의도하지 않은 접근 범위 확대

기존 hasAnyRole('ORGANIZER','ADMIN') 에서 atLeast(UserRole.LEAD) 로 변경되었습니다.

역할 계층 구조상 LEAD 는 rank 3, ORGANIZER 는 rank 4 입니다. atLeast(UserRole.LEAD) 는 rank 3 이상 모든 사용자(LEAD, ORGANIZER, ADMIN)에게 접근을 허용합니다. 따라서 기존에 접근 불가능했던 LEAD 역할의 사용자가 이 API에 접근할 수 있게 되어 접근 범위가 의도하지 않게 확대됩니다.

Guestbook 컨트롤러의 작업이 ORGANIZER 이상 권한만 허용되어야 한다면, atLeast(UserRole.ORGANIZER) 로 수정하세요.

🤖 Prompt for AI Agents
In
`@src/main/java/inha/gdgoc/domain/guestbook/controller/GuestbookController.java`
around lines 22 - 24, The PreAuthorize expression in GuestbookController uses
atLeast(UserRole.LEAD) which expands access to LEAD and above (LEAD, ORGANIZER,
ADMIN) instead of limiting to ORGANIZER+; update the annotation on
GuestbookController to use atLeast(UserRole.ORGANIZER) (change the atLeast
argument from UserRole.LEAD to UserRole.ORGANIZER) so only ORGANIZER and higher
roles are allowed; verify the expression references AccessGuard.check(...) and
the enum type T(inha.gdgoc.domain.user.enums.UserRole).ORGANIZER is used
consistently.

Comment on lines +27 to +37
private static final String LEAD_OR_HR_RULE =
"@accessGuard.check(authentication,"
+ " T(inha.gdgoc.global.security.AccessGuard$AccessCondition).atLeast("
+ "T(inha.gdgoc.domain.user.enums.UserRole).LEAD),"
+ " T(inha.gdgoc.global.security.AccessGuard$AccessCondition).atLeast("
+ "T(inha.gdgoc.domain.user.enums.UserRole).CORE,"
+ " T(inha.gdgoc.domain.user.enums.TeamType).HR))";
private static final String LEAD_OR_HIGHER_RULE =
"@accessGuard.check(authentication,"
+ " T(inha.gdgoc.global.security.AccessGuard$AccessCondition).atLeast("
+ "T(inha.gdgoc.domain.user.enums.UserRole).LEAD))";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "LEAD_OR_HR_RULE|LEAD_OR_HIGHER_RULE" --type=java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1592


🏁 Script executed:

# Check the exact definition of LEAD_OR_HR_RULE in UserAdminController
sed -n '27,32p' src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java

# Check the exact definition of LEAD_OR_HR_RULE in RecruitMemberController
sed -n '51,56p' src/main/java/inha/gdgoc/domain/recruit/member/controller/RecruitMemberController.java

# Check the exact definition of LEAD_OR_HIGHER_RULE in UserAdminController
sed -n '34,37p' src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java

# Check the exact definition of LEAD_OR_HIGHER_RULE in CoreAttendanceController
sed -n '36,39p' src/main/java/inha/gdgoc/domain/core/attendance/controller/CoreAttendanceController.java

Repository: GDGoCINHA/24-2_GDGoC_Server

Length of output: 1457


보안 규칙 상수를 공통 클래스로 분리해야 합니다.

인라인 SpEL 표현식을 상수로 추출한 것은 좋으나, LEAD_OR_HR_RULELEAD_OR_HIGHER_RULERecruitMemberController, CoreAttendanceController에서도 동일하게 정의되어 있습니다. 이는 중복 코드로, SecurityRules와 같은 공통 클래스로 통합하여 단일 정의점(Single Source of Truth)을 유지하세요.

찾아진 중복 위치
  • LEAD_OR_HR_RULE: UserAdminController, RecruitMemberController
  • LEAD_OR_HIGHER_RULE: UserAdminController, CoreAttendanceController
🤖 Prompt for AI Agents
In `@src/main/java/inha/gdgoc/domain/user/controller/UserAdminController.java`
around lines 27 - 37, Extract the duplicated SpEL constants LEAD_OR_HR_RULE and
LEAD_OR_HIGHER_RULE into a single shared class (e.g., SecurityRules) as public
static final String fields, then replace the local constants in
UserAdminController, RecruitMemberController, and CoreAttendanceController with
references to SecurityRules.LEAD_OR_HR_RULE and
SecurityRules.LEAD_OR_HIGHER_RULE; ensure the new class has the exact same
string values, update imports/usages in the controllers, and verify access
modifiers so the constants are visible where referenced and the project
compiles.

@CSE-Shaco CSE-Shaco merged commit 182eb2b into GDGoCINHA:develop Jan 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant