Skip to content

Conversation

@yuripbong
Copy link
Contributor

@yuripbong yuripbong commented Nov 12, 2025

요약



작업 내용



참고 사항



관련 이슈

BACKEND-166



Summary by CodeRabbit

  • 새로운 기능

    • 이력서 추출 실패 시 중복 방지된 Slack 알림 및 실패 복구 처리
    • 추출 시도/실패 메트릭 수집 및 이력서 작업 타임아웃·결과 누락 자동 감지(워치독)
    • Zoom 웹훅 검증·참여/퇴장 처리 개선으로 출석 기록 신뢰성 향상
    • 주간 TodayCS 자동 발행 스케줄러 추가
  • 버그 픽스 / 개선

    • 부트캠프 생성/수정에 권한 검증·입력 유효성 강화
    • 부트캠프 리스트 항목에 이름·프로젝트 설명 노출 추가
  • 잡무(Chores)

    • 도커 베이스 이미지 및 빌드·CI 설정 업데이트, 테스트 인프라 정비

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

@coderabbitai
Copy link

coderabbitai bot commented Nov 12, 2025

Walkthrough

레주메 추출 실패 감지·분류, Micrometer 메트릭 기록, Redis 기반 멱등성 슬랙 알림 발행, Redis 태스크 타임아웃 워치독 및 Zoom webhook/관련 인프라와 테스트 인프라 확장 등이 추가되었습니다.

Changes

응집군 / 파일(s) 변경 요약
레주메 알림·메트릭
techeerzip/src/main/java/backend/techeerzip/domain/resume/alert/ResumeAlertNotifier.java, .../monitoring/ResumeMetricsRecorder.java
Redis 멱등성 키로 슬랙 알림을 idempotent 하게 발행(이벤트 퍼블리시), 메트릭 카운터(markTotal, markFail) 추가. 발송 실패 시 Redis 키 롤백 처리.
레주메 처리 리스너·유틸
techeerzip/src/main/java/backend/techeerzip/domain/resume/event/ResumeStackExtractionSaveListener.java, .../support/ResumeFailureClassifier.java, .../support/ResumeTaskContextExtractor.java
리스너에 메트릭·알림 호출 추가, 실패 분류 로직(classify) 도입, taskId에서 resumeId 추출 유틸 추가 및 실패/맵핑 오류 시 조기 반환·메트릭/알림 호출.
스케줄러(워크독)
techeerzip/src/main/java/backend/techeerzip/domain/resume/scheduler/ResumeTaskWatchdog.java
Redis SCAN으로 resume_extraction-* 키 검사, 작업 생성 후 20분 초과 또는 처리되었으나 결과 누락 시 메트릭·알림 기록 및 키 삭제(checkTimeouts() 메서드).
슬랙 채널/설정 확장
techeerzip/src/main/java/backend/techeerzip/infra/slack/config/SlackProperties.java, .../util/SlackChannelType.java, src/main/resources/application.properties
resumeId 프로퍼티·SlackChannelType.RESUME 추가, application.properties에 slack.resume-id 설정 추가.
Zoom 웹훅·토큰·서비스 재구성
.../infra/zoom/* (다수: ZoomWebhookController, ZoomWebhookService, ZoomTokenProvider, DTO/예외/타입 추가/삭제 등)
기존 ZoomApiClient/ZoomOAuthTokenResponse 삭제, 웹훅 검증 어노테이션·AOP(ZoomWebhookAspect) 추가, URL validation 응답 DTO 및 토큰 암호화 로직 도입, 웹훅 컨트롤러/서비스 분리 및 예외 타입 추가.
테스트 인프라 및 CI/빌드
Dockerfile, .github/workflows/other.yml, build.gradle, src/test/... (IntegrationTestSupport, SharedTestContainer, TestExternalServiceConfig 등)
빌드 베이스 이미지 변경, Gradle wrapper 권한·빌드 단계 명시, GitHub Actions JDK/테스트/JaCoCo 구성 활성화, 테스트용 통합/리포지토리 지원 클래스·모킹 TestConfiguration 및 Testcontainers 통합 추가.
타임/프로파일 구성 및 DI 개선
global/config/TimeConfig.java, RedisConfig.java, PyroscopeConfig.java, WebConfig.java
Clock 빈 추가 및 주입, Redis/Pyroscope 설정에 test 프로파일 제외(@Profile("!test")) 적용, 일부 빈 필드 불변성 조정.
인증·사용자·부트캠프 도메인 변경
domain/auth/jwt/*, domain/bootcamp/*, domain/user/*, global/permission/*
CustomUserPrincipal에 bootcampYear 추가·JWT에 claim 포함, BootcampGeneration API 변경(연도 계산, validate 인자화), BootcampMapper 정적화·매핑 변경, BootcampService/Facade에 Clock·BootcampMemberRepository 도입, 권한 평가자(BootcampPermissionEvaluator) 추가 및 DelegatingPermissionEvaluator 안전성 개선.
여러 테스트·유틸·엔티티 변경 및 추가
src/test/java/..., domain/* 테스트 파일 다수
단위/통합 테스트 대폭 추가 및 리팩터(bootcamp, projectMember, studyMember 등), 테스트 유틸헬퍼와 컨테이너 지원 클래스 추가.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Listener as ResumeStackExtractionSaveListener
    participant Classifier as ResumeFailureClassifier
    participant Recorder as ResumeMetricsRecorder
    participant Notifier as ResumeAlertNotifier
    participant Redis as Redis
    participant Publisher as ApplicationEventPublisher

    Listener->>Recorder: markTotal()
    Listener->>Classifier: classify(resultJson)
    alt 분류 결과 있음 (실패)
        Classifier-->>Listener: FailureInfo(stage, reason)
        Listener->>Recorder: markFail(reason, stage)
        Listener->>Notifier: notify(reason, stage, taskId, userId, resumeId, detail)
        Notifier->>Redis: setIfAbsent("resume_alert_sent:" + resumeId)
        alt setIfAbsent == true (첫발송)
            Notifier->>Publisher: publish(SlackEvent{channel:RESUME, message})
            Publisher-->>Notifier: publish 완료
        else 이미 발송됨
            Notifier-->>Notifier: 멱등성으로 중단
        end
    else 정상/매핑 실패 없음
        Listener-->>Listener: 정상 처리 및 RedisTaskProcessedEvent 발행
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • printSANO
  • dlwhsk0
  • dhdid
  • HwangMinSeon
  • kimzini
  • dongwooooooo
  • jungeunyooon

Poem

깡충깡충 달려온 토끼가 말하네 🐇
실패를 분류하고 메트릭 톡톡 쌓이며,
Redis로 중복 막고 슬랙에 조용히 알려,
워치독은 20분을 지키고 깨어있네,
작은 알림으로 오늘도 레주메를 지켰다요 ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.79% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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: 2

🧹 Nitpick comments (1)
techeerzip/src/main/java/backend/techeerzip/domain/resume/monitoring/ResumeMetricsRecorder.java (1)

13-22: Counter 인스턴스를 캐싱하면 호출 부담을 더 줄일 수 있습니다.
매번 Counter.builder(...).register(...)를 타면 Micrometer가 내부적으로 동일 Meter를 찾아주긴 하지만, 고빈도 호출 시 lookup 비용이 반복됩니다. 필드에 Counter를 한번만 저장해 두고 counter.increment()로 재사용하면 조금 더 가볍고 명확해집니다.

 @RequiredArgsConstructor
 public class ResumeMetricsRecorder {
     private final MeterRegistry meterRegistry;
+    private final Counter totalCounter;
+
+    public ResumeMetricsRecorder(MeterRegistry meterRegistry) {
+        this.meterRegistry = meterRegistry;
+        this.totalCounter = Counter.builder("resume_extraction_total")
+                .register(meterRegistry);
+    }
 
     public void markTotal() {
-        Counter.builder("resume_extraction_total").register(meterRegistry).increment();
+        totalCounter.increment();
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0bf011 and cc08fab.

📒 Files selected for processing (9)
  • techeerzip/src/main/java/backend/techeerzip/domain/resume/alert/ResumeAlertNotifier.java (1 hunks)
  • techeerzip/src/main/java/backend/techeerzip/domain/resume/event/ResumeStackExtractionSaveListener.java (3 hunks)
  • techeerzip/src/main/java/backend/techeerzip/domain/resume/monitoring/ResumeMetricsRecorder.java (1 hunks)
  • techeerzip/src/main/java/backend/techeerzip/domain/resume/scheduler/ResumeTaskWatchdog.java (1 hunks)
  • techeerzip/src/main/java/backend/techeerzip/domain/resume/support/ResumeFailureClassifier.java (1 hunks)
  • techeerzip/src/main/java/backend/techeerzip/domain/resume/support/ResumeTaskContextExtractor.java (1 hunks)
  • techeerzip/src/main/java/backend/techeerzip/infra/slack/config/SlackProperties.java (3 hunks)
  • techeerzip/src/main/java/backend/techeerzip/infra/slack/util/SlackChannelType.java (1 hunks)
  • techeerzip/src/main/resources/application.properties (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
techeerzip/src/main/java/backend/techeerzip/domain/resume/scheduler/ResumeTaskWatchdog.java (1)
techeerzip/src/main/java/backend/techeerzip/domain/resume/support/ResumeTaskContextExtractor.java (1)
  • ResumeTaskContextExtractor (6-11)
techeerzip/src/main/java/backend/techeerzip/domain/resume/event/ResumeStackExtractionSaveListener.java (2)
techeerzip/src/main/java/backend/techeerzip/domain/resume/support/ResumeFailureClassifier.java (1)
  • ResumeFailureClassifier (7-44)
techeerzip/src/main/java/backend/techeerzip/domain/resume/support/ResumeTaskContextExtractor.java (1)
  • ResumeTaskContextExtractor (6-11)
🔇 Additional comments (5)
techeerzip/src/main/java/backend/techeerzip/domain/resume/support/ResumeTaskContextExtractor.java (1)

7-9: 간단하고 의도가 분명한 헬퍼입니다.
TaskIdHandler 로직을 재사용해 이력서 ID 추출을 캡슐화한 점이 명확하고 안전해 보입니다.

techeerzip/src/main/java/backend/techeerzip/infra/slack/config/SlackProperties.java (1)

24-46: 필요한 Slack 채널 ID를 명시적으로 받도록 확장한 점이 좋습니다.
새 필드를 @NotBlank로 강제해 누락 시점을 빠르게 발견할 수 있겠습니다.

techeerzip/src/main/resources/application.properties (1)

83-87: 새 Slack 채널 환경변수 반영 여부를 확인해 주세요.
SlackProperties에서 resumeId@NotBlank로 강제되므로 각 배포 환경에 SLACK_CHANNEL_RESUME_ID가 누락되어 있으면 부팅 단계에서 실패합니다. 운영·스테이징 등 모든 환경 설정에 값이 추가됐는지 꼭 점검 부탁드립니다.

techeerzip/src/main/java/backend/techeerzip/infra/slack/util/SlackChannelType.java (1)

8-12: Slack 채널 타입 확장은 자연스럽습니다.
기존 패턴과 동일하게 SlackProperties 접근자를 전달해 일관성을 지켰습니다.

techeerzip/src/main/java/backend/techeerzip/domain/resume/event/ResumeStackExtractionSaveListener.java (1)

39-62: 실패 경로에 알림과 메트릭을 바로 연결한 흐름이 명확합니다.
분류된 오류 및 매핑 실패를 즉시 계측·알림으로 플러시하는 구조가 관측성 측면에서 일관적입니다.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ddd007 and c2924e6.

📒 Files selected for processing (1)
  • Dockerfile (2 hunks)
🧰 Additional context used
🪛 GitHub Actions: Other CI
Dockerfile

[error] 1-3: Docker build failed. Base image openjdk:21-jdk-slim could not be resolved (not found) during 'docker buildx build'. Command failed: docker buildx build --platform linux/amd64 --push -t ***/techeerism-spring:$DOCKER_IMAGE_TAG .

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

🧹 Nitpick comments (1)
Dockerfile (1)

9-9: OpenTelemetry 에이전트 다운로드 시 체크섬 검증 추가 검토

라인 9에서 GitHub 릴리스로부터 OpenTelemetry Java Agent를 다운로드하고 있습니다. 보안 강화를 위해 다운로드한 파일의 SHA256 체크섬 검증을 추가하는 것을 권장합니다.

예시:

RUN wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.5.0/opentelemetry-javaagent.jar && \
    echo "expected_sha256_hash  opentelemetry-javaagent.jar" | sha256sum -c -
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd9fef0 and 9422ed3.

📒 Files selected for processing (1)
  • Dockerfile (2 hunks)
⏰ 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 (2)
Dockerfile (2)

1-1: ✅ 과거 critical 이슈 해결됨

베이스 이미지가 openjdk에서 eclipse-temurin으로 올바르게 변경되어 Docker 빌드 실패 문제가 해결되었습니다. eclipse-temurin은 OpenJDK의 공식 후속 제품으로서 적극적으로 유지보수되고 있습니다.

Also applies to: 11-11


5-6: 빌드 중 spotlessApply 실행으로 인한 빌드 실패 위험 검토

라인 6에서 ./gradlew clean spotlessApply bootJar --no-daemon -x test를 실행하는데, spotlessApply는 코드 포매팅을 자동으로 적용합니다. 만약 소스 코드의 포매팅이 규칙에 맞지 않으면 빌드가 실패할 수 있습니다.

권장사항: 포매팅 검증만 수행하는 spotlessCheck 또는 빌드 시스템에서 사전에 포매팅을 적용하고 커밋하는 방식으로 변경하는 것을 검토해주세요.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 7, 2026

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

Caution

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

⚠️ Outside diff range comments (5)
techeerzip/src/test/java/backend/techeerzip/domain/todayCs/service/TodayCsServiceIntegrationTest.java (1)

1-133: 주석 처리된 테스트 코드의 의도를 명확히 해주세요.

전체 테스트 파일이 주석 처리되어 있습니다. 다음 중 어떤 상황인지 확인이 필요합니다:

  • 향후 사용할 예정이라면: 주석을 해제하고 테스트를 활성화하거나, @Disabled 어노테이션을 사용하여 의도를 명확히 표시
  • 더 이상 필요하지 않다면: 파일 자체를 삭제하여 코드베이스를 깔끔하게 유지
  • 통합 테스트가 실패한다면: 실패 원인을 수정하거나 문제를 추적할 이슈를 생성

버전 관리 시스템에서 주석 처리된 코드를 유지하는 것은 일반적으로 권장되지 않는 패턴입니다.

통합 테스트 활성화나 제거 작업에 도움이 필요하시면 말씀해 주세요.

techeerzip/src/main/java/backend/techeerzip/global/config/RedisConfig.java (1)

126-131: 로그 메시지에 CS_GRADING 토픽이 누락되었습니다.

5개의 리스너가 등록되었지만 로그에는 4개 토픽만 출력됩니다. TaskType.CS_GRADING.getValue()를 로그 메시지에 추가해야 합니다.

🔎 수정 제안
         log.info(
-                "Redis Message Listeners registered for topics: {}, {}, {}, {}",
+                "Redis Message Listeners registered for topics: {}, {}, {}, {}, {}",
                 TaskType.SIGNUP_BLOG_FETCH.getValue(),
                 TaskType.DAILY_UPDATE.getValue(),
                 TaskType.SHARED_POST_FETCH.getValue(),
-                TaskType.RESUME_EXTRACTION.getValue());
+                TaskType.RESUME_EXTRACTION.getValue(),
+                TaskType.CS_GRADING.getValue());
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/entity/BootcampGeneration.java (1)

31-39: getCurrentBootcampYear() 메서드가 비부트캠프 기간에 잘못된 기수를 반환합니다.

getCurrentBootcampYear()는 검증 없이 calculateBootcampGeneration(LocalDate.now())을 호출하는데, getCurrentPeriod()가 3-5월, 9-11월을 모두 동계로 반환하여 부정확한 기수가 계산됩니다. 이 메서드는 BootcampController.getBootcampYear(), UserController 등 공개 API에서 호출되므로, 비부트캠프 기간에 엔드포인트가 호출되면 오류가 발생합니다.

validateCreationPeriod()를 호출하거나 getCurrentBootcampYear()에서 유효한 부트캠프 기간만 보장하도록 처리해야 합니다.

techeerzip/src/main/java/backend/techeerzip/domain/zoom/service/ZoomAttendanceService.java (2)

73-77: participantUuid.substring(0, 8) 호출 시 예외 가능성

participantUuidnull이거나 길이가 8자 미만인 경우 NullPointerException 또는 StringIndexOutOfBoundsException이 발생합니다. Line 129에서도 동일한 패턴이 사용됩니다.

🔎 안전한 로깅을 위한 수정 제안
             log.info(
                     "새로운 출석 기록 저장: {} (uuid: {}, 시간: {})",
                     userName,
-                    participantUuid.substring(0, 8) + "...",
+                    participantUuid != null && participantUuid.length() >= 8 
+                            ? participantUuid.substring(0, 8) + "..." 
+                            : participantUuid,
                     joinTime);

138-150: LocalDateTime.now() fallback이 잘못된 출석 기록을 생성할 수 있음

파싱 실패 시 현재 시간을 반환하면 실제 입장/퇴장 시간과 다른 값이 저장됩니다. 이로 인해 출석 시간 계산이 부정확해지고 디버깅이 어려워집니다.

예외를 상위로 전파하거나, 파싱 실패 시 해당 이벤트를 무시하는 것이 더 안전합니다.

🔎 예외 전파 방식으로 수정 제안
     /** Zoom DateTime 파싱 (UTC ISO 8601 → LocalDateTime) */
     private LocalDateTime parseZoomDateTime(String dateTimeStr) {
-        try {
-            // Zoom 형식: "2025-01-20T13:30:15Z"
-            if (dateTimeStr.endsWith("Z")) {
-                dateTimeStr = dateTimeStr.substring(0, dateTimeStr.length() - 1);
-            }
-            return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
-        } catch (Exception e) {
-            log.error("시간 파싱 오류: {}", dateTimeStr, e);
-            return LocalDateTime.now(); // fallback
+        // Zoom 형식: "2025-01-20T13:30:15Z"
+        if (dateTimeStr.endsWith("Z")) {
+            dateTimeStr = dateTimeStr.substring(0, dateTimeStr.length() - 1);
         }
+        return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
     }
🤖 Fix all issues with AI agents
In
@techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/JwtTokenProvider.java:
- Around line 92-94: JwtTokenProvider currently reads claims into userId,
roleName, bootcampYear and calls RoleType.valueOf(roleName) which will throw a
NullPointerException for older tokens missing these claims; update the parsing
logic in JwtTokenProvider (the method that contains Long userId =
claims.get(...), String roleName = claims.get(...), Integer bootcampYear =
claims.get(...)) to defensively handle absent claims by checking for null or
using defaults (e.g., default roleName to "USER", default userId to 0L or throw
a specific authentication exception, default bootcampYear to a safe value), then
use the safe roleName when calling RoleType.valueOf or map null to a default
RoleType; ensure any chosen fallback is consistent with downstream auth logic
and consider logging or forcing token refresh where appropriate.

In
@techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/controller/BootcampController.java:
- Around line 46-50: The PreAuthorize type string for updateBootcamp and
deleteBootcamp uses 'Bootcamp' (wrong case) so BootcampPermissionEvaluator's
strict check ("BOOTCAMP".equals(targetType)) rejects them; update the
@PreAuthorize annotations on the updateBootcamp and deleteBootcamp methods to
use the exact uppercase type "BOOTCAMP" so the permission evaluator matches and
authorization succeeds.

In
@techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/repository/BootcampMemberRepository.java:
- Around line 1-11: Move the BootcampMemberRepository interface into the
backend.techeerzip.domain.bootcampMember.repository package to match project
conventions: change the package declaration at the top of
BootcampMemberRepository from backend.techeerzip.domain.bootcamp to
backend.techeerzip.domain.bootcampMember.repository, relocate the file to that
package directory, and keep the existing interface signature (public interface
BootcampMemberRepository extends JpaRepository<BootcampMember, Long>) and
imports/annotations intact so existsByBootcampIdAndUserIdAndIsDeletedFalse
continues to work.

In
@techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/service/BootcampService.java:
- Around line 262-269: The end-of-method log in checkActiveMemberByTeamAndUser
incorrectly repeats the "부트캠프 멤버 검증 시작" message; change the second log (the one
that prints the exists boolean) to indicate completion/result (e.g., "부트캠프 멤버 검증
완료: 존재 여부={}") so the start and result logs are distinct and clear.

In
@techeerzip/src/main/java/backend/techeerzip/domain/user/controller/UserController.java:
- Around line 46-55: The controller currently has two loggers: the
Lombok-generated `log` from `@Slf4j` and the injected `CustomLogger logger`,
causing inconsistent usage (e.g., `log` used around lines 256-259 while `logger`
is used elsewhere). Pick one logger and remove the other: either delete the
`@Slf4j` annotation and replace all `log` references with `logger`, or remove
the `CustomLogger logger` field and replace all `logger` calls with `log`;
update the references in `UserController` (including the occurrences around
lines 256-259) so the class uses the single chosen logger consistently.

In
@techeerzip/src/main/java/backend/techeerzip/domain/zoom/repository/ZoomAttendanceRepository.java:
- Around line 40-44: The JPQL query in
ZoomAttendanceRepository.findActiveSessionByParticipantUuid is non-deterministic
when multiple ZoomAttendance rows have leaveTime IS NULL; update the query to
include an explicit ordering so the most recent active session is returned
(e.g., add "ORDER BY z.joinTime DESC" to the SELECT for ZoomAttendance) so the
method consistently returns the latest joinTime record for the given
participantUuid.

In
@techeerzip/src/main/java/backend/techeerzip/global/permission/DelegatingPermissionEvaluator.java:
- Around line 68-74: The current DelegatingPermissionEvaluator lambda calls
e.hasPermission(auth, id, type, permission) when id is not a Long, which leads
to ClassCastException in implementations that assume targetId is Long; modify
the lambda in DelegatingPermissionEvaluator so that it only calls
e.hasPermission(...) with a Long (casting the id when instance of Long) and
otherwise skips that evaluator (treats it as no-match/false) instead of passing
the non-Long id through; ensure the anyMatch returns false for non-Long ids so
StudyTeamPermissionEvaluator, ProjectTeamPermissionEvaluator, and
BootcampPermissionEvaluator never receive a non-Long targetId.

In @techeerzip/src/test/java/backend/techeerzip/config/SharedTestContainer.java:
- Around line 13-14: The test container instantiation POSTGRES uses
withReuse(true) which only works if Testcontainers reuse is enabled; update
project documentation (README or CONTRIBUTING) to instruct developers to enable
reuse by setting the environment variable TESTCONTAINERS_REUSE_ENABLE=true or
adding testcontainers.reuse.enable=true to ~/.testcontainers.properties, and
mention that without this setting withReuse(true) is ignored and will cause
containers to be recreated for each test.

In
@techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java:
- Around line 28-63: The @MockBean fields in TestExternalServiceConfig cause
unreliable behavior when the class is @Import'ed under Spring Boot 3.3; replace
each @MockBean field in TestExternalServiceConfig with a @Bean factory method
that returns Mockito.mock(...) for the same type (e.g., provide S3Service
s3Service() { return Mockito.mock(S3Service.class); }, similarly for
SlackService, SlackEventHandler, SlackProperties, HttpClient,
GoogleDriveService, RedisService, RedisConnectionFactory, RedisTemplate,
RedisMessageListenerContainer, ReactiveRedisConnectionFactory,
ReactiveRedisTemplate) so the test configuration supplies explicit mock
instances instead of relying on @MockBean processing.

In
@techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/controller/BootcampControllerTest.java:
- Around line 149-196: The test updateBootcampSuccess (and similarly
deleteBootcampSuccess) is missing mocks required by the @PreAuthorize permission
check; mock the BootcampService methods the BootcampPermissionEvaluator relies
on: stub getCurrentBootcampYear() to return the current bootcamp year and stub
checkActiveMemberByTeamAndUser(bootcampId, userId) to return true for the test
user, and ensure the test user identity (from @WithMockUser) matches the userId
used in the member request (or mock the Authentication principal accordingly) so
the permission check passes.

In
@techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/helper/BootcampTestHelper.java:
- Around line 80-104: The createUser helper always constructs a new Role
instance which isn’t persisted and causes FK constraint failures when saving
User; update tests to persist the Role first (e.g., call roleRepository.save(new
Role("ROLE_USER")) in test setup) and then pass the managed Role into user
creation (modify createUser to accept a Role parameter or add an overload
createUser(Role role)), or alternatively change createUser to look up and reuse
an existing persisted Role via RoleRepository before building the User; ensure
the Role used in User.role is a persisted entity.

In
@techeerzip/src/test/java/backend/techeerzip/domain/projectMember/repository/ProjectMemberRepositoryTest.java:
- Around line 99-124: The test is placed in the FindByProjectTeamIdTest nested
class but calls and asserts against findAllByProjectTeamId; move this test
method (findByProjectTeamIdExcludesDeleted) into the FindAllByProjectTeamIdTest
nested class OR change the test to actually exercise findByProjectTeamId
(replace the repository call with
projectMemberRepository.findByProjectTeamId(...) and adjust assertions for a
single Optional/entity result). Update the test name if you change the tested
method to keep names consistent (e.g., rename to
findAllByProjectTeamIdExcludesDeleted if you move it).

In
@techeerzip/src/test/java/backend/techeerzip/domain/studyMember/entity/StudyMemberTest.java:
- Around line 95-116: The test updateUpdatesTimestamp in StudyMemberTest is
flawed: remove the Thread.sleep and fix the assertion so it verifies a real
change (use isAfter(beforeUpdate) only, not isAfter || equals) or move this test
to an integration test using @DataJpaTest where JPA lifecycle callbacks run
(persist the entity, call studyMember.update(...), save via repository to
trigger @PreUpdate, then assert getUpdatedAt().isAfter(beforeUpdate));
alternatively, if keeping it as a pure unit test, limit the check to
assertNotNull(studyMember.getUpdatedAt()) since @PreUpdate may not run. Ensure
you reference the StudyMember.update(...) call and the updateUpdatesTimestamp
test when making the change.
🧹 Nitpick comments (37)
.rules/.cursorrules.mdc (6)

86-90: 메서드 라인 수 제한의 유연한 적용 검토 필요

"메서드는 20줄 이내로 작성"이라는 권장사항은 일반적으로 좋은 원칙이지만, 복잡한 비즈니스 로직(예: 데이터 변환, 검증 체인)에서는 경직된 제약이 될 수 있습니다. 가독성과 단일 책임을 우선으로 하되, 상황에 따라 유연하게 적용하는 것을 고려해보세요.


32-44: 에러 코드 정의 예시 확장 권장

에러 코드의 일반적인 카테고리만 제시되어 있습니다. 예를 들어, 프로젝트의 주요 도메인(예: Resume, User, Auth)별로 구체적인 에러 코드 예시를 추가하면 개발자가 새로운 도메인 기능 추가 시 일관성 있게 에러 코드를 정의할 수 있습니다.

🔎 제안: 도메인별 에러 코드 예시 추가
카테고리별 에러 코드:
- Common (C001~): 공통 에러 (잘못된 입력, 서버 오류 등)
- User (U001~): 사용자 관련
- Auth (A001~): 인증/인가 관련
- Resume (R001~): 이력서 관련 (예: R001="이력서를 찾을 수 없음", R002="이력서 크롤링 실패")

99-117: 패키지 구조 예시에 이력서(Resume) 도메인 추가 고려

현재 예시에는 user와 order 도메인만 포함되어 있습니다. PR의 주요 기능인 이력서 크롤링 실패 감지 및 Slack 알림을 처리하는 Resume 도메인을 예시에 추가하면 가이드의 실제 적용성이 높아질 것입니다.


129-152: 환경 변수 관리에 Redis 및 Slack 설정 추가

현재 예시에는 데이터베이스, API 키, JWT 정보만 포함되어 있습니다. 이번 PR에서 추가되는 Redis(멱등성 관리) 및 Slack(알림)과 같은 인프라 컴포넌트의 환경 변수 설정 예시를 추가하면 운영 관점에서 더 실용적일 것입니다.

🔎 제안: 인프라 환경 변수 예시 추가
# application.yml
spring:
  redis:
    host: ${REDIS_HOST}
    port: ${REDIS_PORT}
slack:
  webhook:
    url: ${SLACK_WEBHOOK_URL}
  token: ${SLACK_BOT_TOKEN}

154-175: 필수 체크리스트에 관찰성(Observability) 항목 추가

현재 체크리스트에는 예외 처리, 검증, 보안이 포함되어 있지만, 마이크로메터 메트릭, 로깅, 모니터링과 같은 관찰성 항목이 누락되어 있습니다. 특히 이번 PR에서 ResumeMetricsRecorder와 같은 메트릭 기록 기능이 추가되므로, 이를 반영한 체크리스트 항목이 필요합니다.

🔎 제안: 관찰성 체크리스트 항목 추가
### 관찰성 (Observability) 확인 사항
- [ ] 주요 비즈니스 로직에 메트릭 기록 (Micrometer)
- [ ] 중요 작업 흐름 로그 구조화 (structured logging)
- [ ] 예외 발생 시 적절한 로그 레벨 사용
- [ ] 성능에 영향을 미치는 작업 모니터링

186-198: 코드 생성 템플릿에 테스트 작성 순서 명확화

템플릿에서 "9. 테스트 코드"가 마지막 단계로 표시되어 있습니다. 현대적인 개발 관행(TDD 또는 BDD)을 고려하면, 테스트 작성 시점을 더 구체화하거나 선택적으로 마련하는 것이 좋습니다. 또한 통합 테스트와 단위 테스트를 구분하는 가이드가 도움이 될 것입니다.

techeerzip/src/test/resources/application-test.properties (1)

1-139: 테스트 설정 구조 및 완성도 평가

테스트 설정 파일이 포괄적으로 작성되었습니다. 다음 항목들이 확인됩니다:

  • ✓ 데이터베이스 및 Flyway 마이그레이션 설정 완료
  • ✓ 모든 외부 서비스(AWS, Google Drive, RabbitMQ, Zoom 등)에 대한 더미 값 설정
  • ✓ Slack 알림 설정 포함
  • ✓ 모든 값이 테스트용 더미 자격증명으로 설정됨 (보안 위험 없음)

Pyroscope 설정(라인 138-139)이 테스트 중 실제로 필요한지는 고려 대상입니다. 프로파일링 오버헤드를 줄이려면 테스트 환경에서 비활성화하는 것을 검토해보세요.

techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/request/BootcampRankUpdateRequest.java (1)

3-8: PR 범위와 무관한 변경사항으로 보입니다.

이 파일의 변경사항(BootcampRankUpdateRequest@AllArgsConstructor 추가)은 PR의 목적인 "이력서 크롤링 실패 슬랙봇 알림"과 직접적인 관련이 없어 보입니다.

또한 단일 필드만 가진 DTO의 경우, @AllArgsConstructor가 생성하는 생성자는 단일 인자만 받게 되므로 명시적인 생성자를 작성하는 것과 큰 차이가 없습니다.

이 변경사항이 의도적인 것인지, 아니면 다른 PR에 포함되어야 할 내용인지 확인이 필요합니다.

techeerzip/src/main/java/backend/techeerzip/global/config/SecurityConfig.java (1)

40-40: CSRF 설정 리팩토링이 올바르게 적용되었으나 PR 범위와 무관합니다.

람다 표현식을 메서드 레퍼런스로 변경한 것은 더 간결하고 관용적인 Spring Security 설정 스타일입니다. 두 SecurityFilterChain 모두에 일관되게 적용되었습니다.

다만, 이 변경사항은 PR의 주요 목적(이력서 크롤링 실패 슬랙 알림)과 무관한 스타일 개선입니다. 기능과 직접 관련 없는 리팩토링은 별도의 PR로 분리하는 것을 고려해보세요.

Also applies to: 114-114

techeerzip/src/test/java/backend/techeerzip/domain/studyMember/entity/StudyMemberTest.java (2)

23-49: "Mock" 네이밍이 실제 목(Mock) 객체가 아닙니다.

mockStudyTeammockUser는 Mockito 등의 목 프레임워크로 생성된 것이 아니라 빌더로 생성된 실제 객체입니다. 혼동을 피하기 위해 testStudyTeam, testUser 등으로 변경하는 것을 고려해보세요.

🔎 네이밍 개선 제안
-    private StudyTeam mockStudyTeam;
-    private User mockUser;
+    private StudyTeam testStudyTeam;
+    private User testUser;

     @BeforeEach
     void setUp() {
-        // Mock 엔티티 생성 (실제 의존성 없이 테스트)
-        mockStudyTeam =
+        // 테스트용 엔티티 생성
+        testStudyTeam =
                 StudyTeam.builder()
                         .name("테스트 스터디")
                         .studyExplain("테스트 설명")
                         .isRecruited(true)
                         .build();

-        mockUser =
+        testUser =
                 User.builder()
                         .name("홍길동")
                         .email("[email protected]")
                         .password("password")
                         .year(21)
                         .build();

         studyMember =
                 StudyMember.builder()
                         .isLeader(false)
                         .summary("스터디 멤버입니다.")
                         .status(StatusCategory.APPROVED)
-                        .studyTeam(mockStudyTeam)
-                        .user(mockUser)
+                        .studyTeam(testStudyTeam)
+                        .user(testUser)
                         .build();
     }

424-431: 최대 길이 테스트가 실제 검증 로직을 테스트하지 않습니다.

이 테스트는 3000자 문자열을 설정할 수 있는지 확인하지만, 실제로 길이 제한을 강제하는 로직(예: @Length 어노테이션이나 커스텀 검증)을 검증하지 않습니다. 3000자를 초과하는 경우의 동작을 테스트하는 것이 더 의미있습니다.

🔎 개선된 엣지 케이스 테스트
     @Test
-    @DisplayName("summary가 최대 길이(3000자)인 경우")
-    void maximumSummaryLength() {
+    @DisplayName("summary가 최대 길이를 초과하는 경우 예외 발생 또는 잘림")
+    void summaryExceedsMaximumLength() {
-        String longSummary = "a".repeat(3000);
+        String tooLongSummary = "a".repeat(3001);
-        studyMember.update(longSummary, StatusCategory.APPROVED);
-
-        assertEquals(3000, studyMember.getSummary().length());
+        // 엔티티에 검증 로직이 있다면:
+        // assertThrows(ValidationException.class, () -> 
+        //     studyMember.update(tooLongSummary, StatusCategory.APPROVED));
+        // 
+        // 또는 DB 제약에 의존한다면 통합 테스트에서 검증
     }
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/repository/ProjectMemberRepositoryTest.java (2)

52-73: deleteAll 순서가 FK 제약조건 위반을 일으킬 수 있습니다.

projectMemberRepository.deleteAll()이 먼저 실행되어야 합니다. 현재 순서는 올바르지만, 향후 유지보수 시 순서가 변경되면 FK 제약조건 위반이 발생할 수 있습니다. 삭제 순서에 대한 주석을 추가하거나, @Sql 어노테이션으로 정리하는 방식을 고려하세요.

또한 Line 68-69에서 createUser가 내부적으로 새 Role을 생성하지만, 이후 user.setRole(savedRole)로 덮어쓰고 있어 불필요한 객체 생성이 발생합니다.


164-170: containsExactly 대신 containsExactlyInAnyOrder 사용을 권장합니다.

findAllByProjectTeamId가 정렬 순서를 보장하지 않는다면, containsExactly는 테스트가 비결정적으로 실패할 수 있습니다.

🔎 수정 제안
             assertThat(result)
                     .extracting(ProjectMember::getUser)
                     .extracting(User::getId)
-                    .containsExactly(savedUser.getId(), savedUser2.getId());
+                    .containsExactlyInAnyOrder(savedUser.getId(), savedUser2.getId());
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/helper/ProjectMemberTestHelper.java (1)

112-135: createUser에서 생성하는 Role이 통합 테스트에서 문제가 될 수 있습니다.

createUser 메서드는 내부적으로 new Role("ROLE_USER")를 생성하지만, 이 Role은 영속화되지 않습니다. @DataJpaTest 환경에서 이 User를 저장하려고 하면 FK 제약조건 위반이 발생할 수 있습니다.

실제로 ProjectMemberRepositoryTest에서는 user.setRole(savedRole)로 별도 저장된 Role을 설정하고 있어 현재는 문제가 없지만, 향후 사용자가 이 헬퍼를 직접 사용할 때 혼란이 있을 수 있습니다. Role 파라미터를 받는 오버로드 메서드 추가를 고려하세요.

🔎 오버로드 메서드 추가 제안
/** 테스트용 User 엔티티를 지정된 Role로 생성합니다. */
public static User createUser(String suffix, Role role) {
    return User.builder()
            .name("테스터" + suffix)
            .email("tester" + suffix + "@example.com")
            .nickname("tester" + suffix)
            .year(2025)
            .password("password")
            .isLft(false)
            .githubUrl("https://github.com/tester" + suffix)
            .mainPosition("BE")
            .subPosition("FE")
            .school("Techeer")
            .profileImage(null)
            .isAuth(true)
            .role(role)
            .grade("A")
            .mediumUrl(null)
            .tistoryUrl(null)
            .velogUrl(null)
            .bootcampYear(2025)
            .feedbackNotes("notes")
            .build();
}
techeerzip/src/main/java/backend/techeerzip/domain/todayCs/scheduler/TodayCsScheduler.java (2)

25-39: 예외 처리 전략에 대한 문서화를 권장합니다.

현재 구현은 모든 예외를 catch하고 Slack 알림을 발송한 후 예외를 삼켜버립니다. 스케줄러의 경우 이는 일반적인 패턴이지만, 다음 사항을 고려해주세요:

  1. Slack 알림 실패 처리: eventPublisher.publishEvent 자체가 실패하는 경우에 대한 처리가 없습니다.
  2. 예외 전파 여부: 현재는 예외를 재발생시키지 않아 스케줄러가 계속 실행되지만, 이러한 동작이 의도적인지 코드나 Javadoc으로 명시하는 것이 좋습니다.
🔎 Slack 알림 실패에 대한 방어 코드 예시
 } catch (Exception e) {
     log.error("[{}] CS 문제 발행 중 예상치 못한 오류 발생: {}", CONTEXT, e.getMessage(), e);
-    final String errorMessage =
-            String.format(
-                    "[ERROR] CS 문제 발행 중 오류 발생!\n> 원인: %s\n> 시간: %s",
-                    e.getMessage(), java.time.LocalDateTime.now());
-    eventPublisher.publishEvent(new SlackEvent.Channel(errorMessage, SlackChannelType.EA));
+    try {
+        final String errorMessage =
+                String.format(
+                        "[ERROR] CS 문제 발행 중 오류 발생!\n> 원인: %s\n> 시간: %s",
+                        e.getMessage(), java.time.LocalDateTime.now());
+        eventPublisher.publishEvent(new SlackEvent.Channel(errorMessage, SlackChannelType.EA));
+    } catch (Exception slackError) {
+        log.error("[{}] Slack 알림 발송 실패: {}", CONTEXT, slackError.getMessage(), slackError);
+    }
 }

33-36: 코드 스타일 개선을 고려해보세요.

Line 36에서 java.time.LocalDateTime.now()를 인라인으로 사용하고 있습니다. 다음과 같이 개선할 수 있습니다:

  1. 가독성: LocalDateTime을 static import하여 더 간결하게 표현
  2. 타임존 일관성: 스케줄러는 Asia/Seoul 타임존을 사용하는데, 에러 메시지의 시간은 시스템 기본 타임존을 사용합니다. 일관성을 위해 ZonedDateTime.now(ZoneId.of("Asia/Seoul"))을 사용하는 것을 고려해보세요.
🔎 제안하는 개선 사항
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;

 public class TodayCsScheduler {
+    private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul");
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
     
     // ...
     
     } catch (Exception e) {
         log.error("[{}] CS 문제 발행 중 예상치 못한 오류 발생: {}", CONTEXT, e.getMessage(), e);
         final String errorMessage =
                 String.format(
                         "[ERROR] CS 문제 발행 중 오류 발생!\n> 원인: %s\n> 시간: %s",
-                        e.getMessage(), java.time.LocalDateTime.now());
+                        e.getMessage(), ZonedDateTime.now(SEOUL_ZONE).format(FORMATTER));
         eventPublisher.publishEvent(new SlackEvent.Channel(errorMessage, SlackChannelType.EA));
     }
techeerzip/src/test/java/backend/techeerzip/domain/todayCs/scheduler/TodayCsSchedulerUnitTest.java (1)

36-45: .thenReturn(null) 대신 mock 객체 반환 고려

publishCsProblem 메서드는 void가 아니라 TodayProblemDetailResponse를 반환합니다. 현재 스케줄러가 반환값을 사용하지 않으므로 기술적으로 null을 반환해도 테스트는 통과하지만, 더 나은 테스트 관행을 위해 mock TodayProblemDetailResponse 객체를 반환하는 것이 좋습니다.

techeerzip/src/main/java/backend/techeerzip/infra/zoom/type/ZoomWebhookEventType.java (1)

1-28: Zoom 웹훅 이벤트 타입 enum 구현이 깔끔합니다.

enum 기반 이벤트 타입 분류와 정적 헬퍼 메서드 구현이 적절합니다.

향후 확장성을 위해 fromEventName(String) 메서드를 추가하면 이벤트 이름으로 enum 상수를 직접 조회할 수 있어 유용할 수 있습니다:

🔎 선택적 개선 제안
public static Optional<ZoomWebhookEventType> fromEventName(String eventName) {
    return Arrays.stream(values())
            .filter(e -> e.getEventName().equals(eventName))
            .findFirst();
}
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/mapper/BootcampMapper.java (1)

69-85: 스트림 수집 방식 불일치

toListResponse에서는 Collectors.toList()를 사용하고 toListResponseWithUserGeneration에서는 toList()를 사용하고 있습니다. 일관성을 위해 하나로 통일하는 것이 좋습니다.

🔎 제안된 수정
     public static BootcampListResponse toListResponse(
             List<Bootcamp> bootcamps, Long nextCursor, Integer nextCursorRank, Boolean hasNext) {
         List<BootcampListResponse.BootcampListItem> items =
-                bootcamps.stream().map(BootcampMapper::toListItem).collect(Collectors.toList());
+                bootcamps.stream().map(BootcampMapper::toListItem).toList();
techeerzip/src/main/java/backend/techeerzip/infra/zoom/exception/ZoomWebhookProcessingException.java (1)

9-11: 생성자 오버로드 추가를 고려하세요.

현재 Throwable cause를 받는 생성자만 제공되어 있어, 원인 없이 예외를 발생시키려는 경우 유연성이 제한됩니다.

🔎 제안: 추가 생성자 오버로드
 public class ZoomWebhookProcessingException extends InfraException {
 
+    public ZoomWebhookProcessingException() {
+        super(ErrorCode.ZOOM_WEBHOOK_PROCESSING_FAILED);
+    }
+
+    public ZoomWebhookProcessingException(String message) {
+        super(ErrorCode.ZOOM_WEBHOOK_PROCESSING_FAILED, message);
+    }
+
     public ZoomWebhookProcessingException(Throwable cause) {
         super(ErrorCode.ZOOM_WEBHOOK_PROCESSING_FAILED, cause);
     }
 }
techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/CustomUserPrincipal.java (1)

25-32: 필드 할당 순서를 파라미터 순서와 일치시키세요.

생성자 파라미터 순서(userId, email, password, bootcampYear, role)와 필드 할당 순서가 일치하지 않습니다. Line 30에서 role을 먼저 할당하고 Line 31에서 bootcampYear를 할당하는데, 파라미터 순서와 반대입니다.

🔎 제안: 할당 순서 조정
     public CustomUserPrincipal(
             Long userId, String email, String password, Integer bootcampYear, RoleType role) {
         this.userId = userId;
         this.email = email;
         this.password = password;
-        this.role = role;
         this.bootcampYear = bootcampYear;
+        this.role = role;
         this.authorities = List.of(new SimpleGrantedAuthority(role.getRoleName().toUpperCase()));
     }
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/entity/BootcampGenerationTest.java (1)

74-79: 선택사항: AssertJ로 단언문을 통일하는 것을 고려해보세요.

테스트 전반에 걸쳐 AssertJ를 사용하고 있는데, 여기서만 JUnit의 Assertions.assertDoesNotThrow를 사용하고 있습니다. 일관성을 위해 AssertJ의 API를 사용하는 것이 좋습니다.

🔎 제안하는 수정사항
+import static org.assertj.core.api.Assertions.assertThatCode;
+
 @ParameterizedTest(name = "{0}은 생성 가능한 기간이어야 한다")
 @MethodSource("provideDatesForGeneration")
 void validateCreationPeriodSuccessTest(LocalDate date) {
-    // 예외가 발생하지 않아야 함
-    Assertions.assertDoesNotThrow(() -> BootcampGeneration.validateCreationPeriod(date));
+    assertThatCode(() -> BootcampGeneration.validateCreationPeriod(date))
+            .doesNotThrowAnyException();
 }
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/entity/BootcampTest.java (1)

68-79: 멤버 컬렉션 동기화가 올바르게 테스트되었습니다.

addMember/removeMember 동작이 적절히 검증되고 있습니다.

다만, 라인 3에서 import static org.junit.jupiter.api.Assertions.*;로 정적 임포트를 사용하고 있으므로, 라인 78에서 Assertions. 접두사 없이 assertDoesNotThrow를 직접 사용하는 것이 더 일관적입니다.

🔎 제안하는 수정사항
     @Test
     @DisplayName("멤버 추가 및 제거 시 Bootcamp.members 컬렉션이 동기화된다")
     void addAndRemoveMembers() {
         Bootcamp bootcamp = BootcampTestHelper.createBootcamp("FE", 2025, 1);
         var member = BootcampTestHelper.createBootcampMember(bootcamp, "user1", true);
 
         bootcamp.addMember(member);
         assertEquals(1, bootcamp.getMembers().size());
 
         bootcamp.removeMember(member);
         assertTrue(bootcamp.getMembers().isEmpty());
     }

참고: 라인 78의 assertDoesNotThrow 호출이 현재 코드에는 보이지 않지만, BootcampGenerationTest의 유사한 패턴을 참고하여 정적 임포트를 일관되게 사용하는 것을 권장합니다.

techeerzip/src/main/java/backend/techeerzip/infra/zoom/dto/ZoomWebhookValidationResponse.java (1)

15-19: @JsonProperty 어노테이션이 중복됩니다.

필드명(plainToken, encryptedToken)이 이미 JSON 속성명과 동일하므로 @JsonProperty 어노테이션은 생략 가능합니다. Jackson은 기본적으로 camelCase 필드명을 동일한 JSON 속성명으로 매핑합니다.

🔎 제안된 수정
-    @JsonProperty("plainToken")
     private String plainToken;

-    @JsonProperty("encryptedToken")
     private String encryptedToken;
techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomTokenProvider.java (2)

26-29: 공백만 있는 토큰 검증 누락

isEmpty() 대신 isBlank()를 사용하면 공백 문자만 포함된 토큰도 검증할 수 있습니다.

🔎 제안된 수정
-        if (secretToken == null || secretToken.isEmpty()) {
+        if (secretToken == null || secretToken.isBlank()) {

39-49: Java 17+ HexFormat 사용 고려

수동 hex 변환 대신 Java 17+의 HexFormat을 사용하면 코드가 더 간결해집니다.

🔎 제안된 수정
+import java.util.HexFormat;
-            // 16진수 문자열로 변환
-            StringBuilder hexString = new StringBuilder();
-            for (byte b : encryptedBytes) {
-                String hex = Integer.toHexString(0xff & b);
-                if (hex.length() == 1) {
-                    hexString.append('0');
-                }
-                hexString.append(hex);
-            }
-
-            return hexString.toString();
+            return HexFormat.of().formatHex(encryptedBytes);
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/repository/BootcampRepositoryTest.java (1)

147-154: 변수 이름이 저장 순서와 맞지 않아 혼란을 줄 수 있습니다.

rank1Laterrank1First보다 먼저 저장되어 더 낮은 ID를 가지게 됩니다. 변수명을 rank1Firstrank1Second로 변경하면 테스트 의도가 더 명확해집니다.

🔎 제안하는 수정
-            Bootcamp rank1Later = createBootcamp("Rank 1 Later", 2025, 1);
-            Bootcamp rank1First = createBootcamp("Rank 1 First", 2025, 1);
+            Bootcamp rank1First = createBootcamp("Rank 1 First", 2025, 1);
+            Bootcamp rank1Second = createBootcamp("Rank 1 Second", 2025, 1);

             // 저장 순서와 상관없이 정렬되는지 확인하기 위해 섞어서 저장
             bootcampRepository.save(rank2);
-            bootcampRepository.save(rank1Later);
-            bootcampRepository.save(rank1First);
+            bootcampRepository.save(rank1First);
+            bootcampRepository.save(rank1Second);
techeerzip/src/test/java/backend/techeerzip/TecheerzipApplicationTests.java (1)

11-14: 테스트에 실제 검증 로직을 추가하는 것을 권장합니다.

System.out.println으로 URL을 출력하는 것만으로는 컨텍스트 로딩 성공 여부를 검증하지 않습니다. 로거를 사용하거나 간단한 assertion을 추가하세요.

🔎 제안하는 수정
     @Test
     void contextLoads() {
-        System.out.println("연결된 DB URL: " + POSTGRES.getJdbcUrl());
+        // 컨텍스트 로딩 성공 시 POSTGRES 컨테이너가 실행 중이어야 함
+        assertThat(POSTGRES.isRunning()).isTrue();
     }
techeerzip/build.gradle (2)

117-122: 중복된 test task 설정이 존재합니다.

tasks.named('test', Test) (Line 117-122)과 tasks.test (Line 229-232) 블록이 중복되어 있습니다. 두 블록 모두 useJUnitPlatform()finalizedBy jacocoTestReport를 설정하고 있어 불필요한 중복이 발생합니다.

🔎 하나의 test task 블록으로 통합하는 것을 권장합니다
 tasks.named('test', Test) {
     systemProperty 'spring.profiles.active', 'test'
     systemProperty 'spring.config.location', "$projectDir/src/test/resources/application-test.properties"
     useJUnitPlatform()
     finalizedBy jacocoTestReport
 }
-
-// 테스트 task 마무리 설정
-tasks.test {
-    useJUnitPlatform()
-    finalizedBy tasks.jacocoTestReport
-}

Also applies to: 229-232


211-224: 커버리지 최소값이 0%로 설정되어 있습니다.

LINEBRANCH 커버리지 최소값이 0.00으로 설정되어 있어 실질적인 커버리지 검증이 비활성화되어 있습니다. 초기 설정 단계라면 의도된 것일 수 있으나, 추후 적절한 커버리지 목표값(예: 0.60 ~ 0.80)으로 상향 조정하는 것을 권장합니다.

techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomWebhookAspect.java (2)

41-44: event.getEventName()이 null일 경우 NPE 발생 가능성이 있습니다.

ZoomWebhookEventType.isUrlValidation(event.getEventName())에서 eventName이 null인 경우 isUrlValidation 메서드 내부에서 NPE가 발생할 수 있습니다. 방어적 null 체크를 추가하는 것을 권장합니다.

🔎 null 체크 추가 제안
+        String eventName = event.getEventName();
+        if (eventName == null) {
+            log.warn("Event name is null");
+            throw new ZoomWebhookInvalidAuthenticationException();
+        }
+
-        if (ZoomWebhookEventType.isUrlValidation(event.getEventName())) {
+        if (ZoomWebhookEventType.isUrlValidation(eventName)) {
             log.debug("URL 검증 요청 - 인증 검증 생략");
             return;
         }

47-50: Authorization 헤더 조회 로직을 간소화할 수 있습니다.

HttpServletRequest.getHeader()는 Servlet 스펙에 따라 대소문자를 구분하지 않습니다. 따라서 "authorization""Authorization"을 별도로 확인할 필요가 없습니다.

🔎 간소화 제안
-        String authHeader =
-                request.getHeader("authorization") != null
-                        ? request.getHeader("authorization")
-                        : request.getHeader("Authorization");
+        String authHeader = request.getHeader("Authorization");
techeerzip/src/main/java/backend/techeerzip/global/permission/BootcampPermissionEvaluator.java (1)

35-42: targetId가 null일 때 Long으로 캐스팅하면 NPE 위험이 있습니다.

Line 37에서 (Long) targetId로 캐스팅한 후 Line 40에서 bootcampId == null을 체크하고 있습니다. 그러나 targetId가 null이면 캐스팅 자체는 문제가 없지만, 로직의 가독성을 위해 순서를 조정하는 것이 좋습니다.

또한, targetIdLong 타입이 아닌 다른 Serializable일 경우 ClassCastException이 발생할 수 있습니다.

🔎 타입 안전성 개선 제안
         CustomUserPrincipal user = (CustomUserPrincipal) auth.getPrincipal();
         Long userId = user.getUserId();
-        Long bootcampId = (Long) targetId;
         Integer currentYear = bootcampService.getCurrentBootcampYear();
         boolean isPeriod = Objects.equals(user.getBootcampYear(), currentYear);
-        if (bootcampId == null && isCreatePermission(permission)) {
+
+        if (targetId == null && isCreatePermission(permission)) {
             log.info("Bootcamp CREATE 권한 확인 완료 - userId: {}, permission: {}", userId, permission);
             return isPeriod;
-
-        } else if (targetId != null && isMemberPermission(permission)) {
+        }
+
+        if (targetId != null && isMemberPermission(permission)) {
+            if (!(targetId instanceof Long)) {
+                log.warn("잘못된 targetId 타입: {}", targetId.getClass().getSimpleName());
+                throw new PermissionDeniedException();
+            }
+            Long bootcampId = (Long) targetId;
             boolean isMember = bootcampService.checkActiveMemberByTeamAndUser(bootcampId, userId);
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/service/BootcampServiceTest.java (1)

428-436: @BeforeEach에서 이미 설정된 Clock 설정이 중복되어 있습니다.

toggleBootcampParticipationSuccess() 테스트에서 Clock을 다시 설정하고 있지만, 이미 setUp() 메서드에서 동일한 설정이 되어 있습니다. lenient() 사용으로 인해 문제는 없지만, 중복 코드입니다.

🔎 중복 코드 제거 제안
     @Test
     @DisplayName("부트캠프 참여 여부 토글 기능은 유저 서비스로 위임되어야 한다")
     void toggleBootcampParticipationSuccess() {
-        Clock fixedClock =
-                Clock.fixed(
-                        FIXED_DATE.atStartOfDay(ZoneId.systemDefault()).toInstant(),
-                        ZoneId.systemDefault());
-
-        // 2. Mock Clock이 항상 위 시간을 반환하도록 설정
-        // (lenient()를 붙이면, clock을 쓰지 않는 테스트 메서드에서도 에러가 나지 않습니다)
-        lenient().when(clock.instant()).thenReturn(fixedClock.instant());
-        lenient().when(clock.getZone()).thenReturn(fixedClock.getZone());
-
         Integer currentGen = bootcampService.getCurrentBootcampYear();
         // Given
         Long userId = 1L;
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookService.java (2)

38-44: 중첩된 객체 접근 시 NPE 발생 가능성이 있습니다.

event.getPayload().getObject().getParticipant() 체인에서 중간 객체가 null인 경우 NPE가 발생할 수 있습니다. 방어적 null 체크를 추가하는 것을 권장합니다.

🔎 null 체크 추가 제안
     public ZoomWebhookValidationResponse handleWebhookEvents(ZoomWebhookEvent event) {
+        if (event.getPayload() == null 
+                || event.getPayload().getObject() == null 
+                || event.getPayload().getObject().getParticipant() == null) {
+            log.warn("Webhook 이벤트 payload가 불완전합니다");
+            return new ZoomWebhookValidationResponse(null, null);
+        }
+
         ZoomWebhookEvent.WebhookParticipant participant =
                 event.getPayload().getObject().getParticipant();

30-30: plainToken을 INFO 레벨로 로깅하는 것은 보안상 권장되지 않습니다.

plainToken은 Zoom 검증에 사용되는 민감한 토큰입니다. DEBUG 레벨로 변경하거나, 토큰 값을 마스킹하는 것을 권장합니다.

🔎 로그 레벨 변경 제안
-        log.info("Zoom Webhook URL 검증 요청 - plainToken: {}", plainToken);
+        log.debug("Zoom Webhook URL 검증 요청 - plainToken: {}", plainToken);
techeerzip/src/main/java/backend/techeerzip/domain/zoom/service/ZoomAttendanceService.java (1)

79-81: 포괄적 예외 처리가 버그를 은폐할 수 있음

모든 Exception을 catch하고 로그만 남기면, 데이터 정합성 문제나 프로그래밍 오류가 조용히 무시될 수 있습니다. 복구 가능한 특정 예외만 catch하거나, 최소한 메트릭/알림을 추가하는 것을 권장합니다.

Also applies to: 133-135

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 619a937 and b8ad0a8.

📒 Files selected for processing (74)
  • .github/workflows/other.yml
  • .rules/.cursorrules.mdc
  • techeerzip/build.gradle
  • techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/CustomUserPrincipal.java
  • techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/JwtTokenProvider.java
  • techeerzip/src/main/java/backend/techeerzip/domain/auth/service/CustomUserDetailsService.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/controller/BootcampController.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/request/BootcampCreateRequest.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/request/BootcampRankUpdateRequest.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/request/BootcampVisibilityUpdateRequest.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/response/BootcampListResponse.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/entity/BootcampGeneration.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/exception/BootcampNotParticipantException.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/mapper/BootcampMapper.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/repository/BootcampMemberRepository.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/service/BootcampFacadeServiceImpl.java
  • techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/service/BootcampService.java
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/controller/FeedbackController.java
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/repository/FeedbackRepositoryCustom.java
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/repository/FeedbackRepositoryImpl.java
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/service/FeedbackService.java
  • techeerzip/src/main/java/backend/techeerzip/domain/todayCs/scheduler/TodayCsScheduler.java
  • techeerzip/src/main/java/backend/techeerzip/domain/user/controller/UserController.java
  • techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java
  • techeerzip/src/main/java/backend/techeerzip/domain/zoom/repository/ZoomAttendanceRepository.java
  • techeerzip/src/main/java/backend/techeerzip/domain/zoom/service/ZoomAttendanceService.java
  • techeerzip/src/main/java/backend/techeerzip/global/config/PyroscopeConfig.java
  • techeerzip/src/main/java/backend/techeerzip/global/config/RedisConfig.java
  • techeerzip/src/main/java/backend/techeerzip/global/config/SecurityConfig.java
  • techeerzip/src/main/java/backend/techeerzip/global/config/TimeConfig.java
  • techeerzip/src/main/java/backend/techeerzip/global/config/WebConfig.java
  • techeerzip/src/main/java/backend/techeerzip/global/exception/ErrorCode.java
  • techeerzip/src/main/java/backend/techeerzip/global/exception/GlobalExceptionHandler.java
  • techeerzip/src/main/java/backend/techeerzip/global/permission/BootcampPermissionEvaluator.java
  • techeerzip/src/main/java/backend/techeerzip/global/permission/DelegatingPermissionEvaluator.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/client/ZoomApiClient.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomApiConfig.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomAuth.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomTokenProvider.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomWebhookAspect.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/dto/ZoomOAuthTokenResponse.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/dto/ZoomWebhookEvent.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/dto/ZoomWebhookValidationResponse.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/exception/ZoomException.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/exception/ZoomWebhookInvalidAuthenticationException.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/exception/ZoomWebhookPlainTokenException.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/exception/ZoomWebhookProcessingException.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/type/ZoomLeaveReason.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/type/ZoomWebhookEventType.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookController.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookHandler.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookService.java
  • techeerzip/src/test/java/backend/techeerzip/TecheerzipApplicationTests.java
  • techeerzip/src/test/java/backend/techeerzip/config/IntegrationTestSupport.java
  • techeerzip/src/test/java/backend/techeerzip/config/RepositoryTestSupport.java
  • techeerzip/src/test/java/backend/techeerzip/config/SharedTestContainer.java
  • techeerzip/src/test/java/backend/techeerzip/config/SyncTestConfig.java
  • techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/controller/BootcampControllerTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/entity/BootcampGenerationTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/entity/BootcampTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/helper/BootcampTestHelper.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/repository/BootcampRepositoryTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/service/BootcampFacadeServiceImplTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/service/BootcampServiceTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/bootcampMember/entity/BootcampMemberTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/projectMember/entity/ProjectMemberTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/projectMember/helper/ProjectMemberTestHelper.java
  • techeerzip/src/test/java/backend/techeerzip/domain/projectMember/repository/ProjectMemberRepositoryTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/projectMember/service/ProjectMemberServiceTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/studyMember/entity/StudyMemberTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/todayCs/scheduler/TodayCsSchedulerUnitTest.java
  • techeerzip/src/test/java/backend/techeerzip/domain/todayCs/service/TodayCsServiceIntegrationTest.java
  • techeerzip/src/test/resources/application-test.properties
💤 Files with no reviewable changes (4)
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/dto/ZoomOAuthTokenResponse.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/client/ZoomApiClient.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookHandler.java
  • techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomApiConfig.java
✅ Files skipped from review due to trivial changes (4)
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/repository/FeedbackRepositoryCustom.java
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/controller/FeedbackController.java
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/service/FeedbackService.java
  • techeerzip/src/main/java/backend/techeerzip/domain/feedback/repository/FeedbackRepositoryImpl.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-05-24T09:14:59.080Z
Learnt from: dongwooooooo
Repo: Techeer-Hogwarts/backend PR: 7
File: techeerzip/src/main/java/backend/techeerzip/domain/projectTeam/service/ProjectTeamService.java:680-680
Timestamp: 2025-05-24T09:14:59.080Z
Learning: 이 프로젝트에서는 이메일 필드가 데이터베이스 레벨에서 NOT NULL 제약조건을 가지며, 멤버 서비스에서 존재하지 않는 멤버에 대해서는 예외를 발생시키는 패턴을 사용한다. 따라서 이메일 관련 서비스 메서드들은 null을 반환하지 않으며, 별도의 null 체크가 불필요하다.

Applied to files:

  • techeerzip/src/test/java/backend/techeerzip/domain/projectMember/service/ProjectMemberServiceTest.java
📚 Learning: 2025-09-09T10:19:12.637Z
Learnt from: kimzini
Repo: Techeer-Hogwarts/backend PR: 117
File: techeerzip/src/main/java/backend/techeerzip/global/permission/DelegatingPermissionEvaluator.java:37-43
Timestamp: 2025-09-09T10:19:12.637Z
Learning: DelegatingPermissionEvaluator에서 target이 null일 때 어떤 evaluator도 매칭되지 않는 것이 의도된 설계입니다. 어드민 권한 검증은 hasPermission이 아닌 hasRole('ADMIN')을 사용해야 합니다.

Applied to files:

  • techeerzip/src/main/java/backend/techeerzip/global/permission/BootcampPermissionEvaluator.java
  • techeerzip/src/main/java/backend/techeerzip/global/permission/DelegatingPermissionEvaluator.java
📚 Learning: 2025-09-12T02:45:00.652Z
Learnt from: dlwhsk0
Repo: Techeer-Hogwarts/backend PR: 111
File: techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java:133-137
Timestamp: 2025-09-12T02:45:00.652Z
Learning: UserService class in this codebase uses declarative transaction management with Transactional annotations, not programmatic transaction management with TransactionTemplate, so PlatformTransactionManager field injection is not needed.

Applied to files:

  • techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java
🧬 Code graph analysis (20)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookController.java (3)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomWebhookAspect.java (1)
  • Slf4j (20-82)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookService.java (1)
  • Slf4j (14-79)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomTokenProvider.java (1)
  • Slf4j (15-56)
techeerzip/src/main/java/backend/techeerzip/domain/todayCs/scheduler/TodayCsScheduler.java (1)
techeerzip/src/main/java/backend/techeerzip/domain/todayCs/exception/CsProblemNotFoundException.java (1)
  • CsProblemNotFoundException (6-10)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/request/BootcampVisibilityUpdateRequest.java (4)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/response/BootcampListResponse.java (2)
  • Getter (8-29)
  • Getter (19-28)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/request/BootcampRankUpdateRequest.java (1)
  • Getter (7-12)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/response/BootcampVisibilityUpdateResponse.java (1)
  • Getter (6-10)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/response/BootcampResponse.java (1)
  • Getter (10-28)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/type/ZoomLeaveReason.java (1)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/dto/ZoomWebhookEvent.java (4)
  • Getter (11-111)
  • Getter (28-41)
  • Getter (43-74)
  • Getter (76-110)
techeerzip/src/test/java/backend/techeerzip/domain/bootcampMember/entity/BootcampMemberTest.java (1)
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/helper/BootcampTestHelper.java (1)
  • BootcampTestHelper (12-105)
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/entity/ProjectMemberTest.java (1)
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/helper/ProjectMemberTestHelper.java (1)
  • ProjectMemberTestHelper (13-143)
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/service/ProjectMemberServiceTest.java (3)
techeerzip/src/main/java/backend/techeerzip/domain/projectMember/exception/ProjectMemberNotFoundException.java (1)
  • ProjectMemberNotFoundException (6-11)
techeerzip/src/main/java/backend/techeerzip/domain/projectMember/exception/TeamInvalidActiveRequester.java (1)
  • TeamInvalidActiveRequester (6-15)
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/helper/ProjectMemberTestHelper.java (1)
  • ProjectMemberTestHelper (13-143)
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/entity/BootcampTest.java (1)
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/helper/BootcampTestHelper.java (1)
  • BootcampTestHelper (12-105)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookService.java (4)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/exception/ZoomWebhookPlainTokenException.java (1)
  • ZoomWebhookPlainTokenException (7-12)
techeerzip/src/main/java/backend/techeerzip/domain/zoom/service/ZoomAttendanceService.java (1)
  • Slf4j (18-151)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookController.java (1)
  • Slf4j (15-36)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomTokenProvider.java (1)
  • Slf4j (15-56)
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/repository/ProjectMemberRepositoryTest.java (2)
techeerzip/src/test/java/backend/techeerzip/domain/projectMember/helper/ProjectMemberTestHelper.java (1)
  • ProjectMemberTestHelper (13-143)
techeerzip/src/test/java/backend/techeerzip/config/RepositoryTestSupport.java (1)
  • ActiveProfiles (7-17)
techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/entity/BootcampGenerationTest.java (1)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/exception/InvalidBootcampCreationPeriodException.java (1)
  • InvalidBootcampCreationPeriodException (6-11)
techeerzip/src/test/java/backend/techeerzip/domain/todayCs/scheduler/TodayCsSchedulerUnitTest.java (1)
techeerzip/src/main/java/backend/techeerzip/domain/todayCs/exception/CsProblemNotFoundException.java (1)
  • CsProblemNotFoundException (6-10)
techeerzip/src/main/java/backend/techeerzip/global/permission/BootcampPermissionEvaluator.java (3)
techeerzip/src/main/java/backend/techeerzip/global/permission/DelegatingPermissionEvaluator.java (1)
  • Component (14-92)
techeerzip/src/main/java/backend/techeerzip/global/exception/PermissionDeniedException.java (1)
  • PermissionDeniedException (3-7)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/service/BootcampService.java (1)
  • Slf4j (35-270)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomWebhookAspect.java (2)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/exception/ZoomWebhookInvalidAuthenticationException.java (1)
  • ZoomWebhookInvalidAuthenticationException (10-21)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookController.java (1)
  • Slf4j (15-36)
techeerzip/src/test/java/backend/techeerzip/config/IntegrationTestSupport.java (2)
techeerzip/src/test/java/backend/techeerzip/config/RepositoryTestSupport.java (1)
  • ActiveProfiles (7-17)
techeerzip/src/test/java/backend/techeerzip/config/SharedTestContainer.java (1)
  • SharedTestContainer (5-23)
techeerzip/src/test/java/backend/techeerzip/config/RepositoryTestSupport.java (1)
techeerzip/src/test/java/backend/techeerzip/config/SharedTestContainer.java (1)
  • SharedTestContainer (5-23)
techeerzip/src/test/java/backend/techeerzip/TecheerzipApplicationTests.java (1)
techeerzip/src/test/java/backend/techeerzip/config/SharedTestContainer.java (1)
  • SharedTestContainer (5-23)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/entity/BootcampGeneration.java (1)
techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/dto/response/BootcampResponse.java (1)
  • Getter (10-28)
techeerzip/src/main/java/backend/techeerzip/domain/user/service/UserService.java (1)
techeerzip/src/main/java/backend/techeerzip/domain/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (6-10)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/dto/ZoomWebhookEvent.java (3)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookController.java (1)
  • Slf4j (15-36)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/webhook/ZoomWebhookService.java (1)
  • Slf4j (14-79)
techeerzip/src/main/java/backend/techeerzip/infra/zoom/config/ZoomTokenProvider.java (1)
  • Slf4j (15-56)
⏰ 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

Comment on lines +92 to +94
Long userId = claims.get(USER_ID, Long.class);
String roleName = claims.get(ROLE_KEY, String.class);
Integer bootcampYear = claims.get(BOOTCAMP_YEAR, Integer.class);
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:

#!/bin/bash
# Find the JwtTokenProvider.java file
find . -name "JwtTokenProvider.java" -type f

# Also find CustomUserPrincipal
find . -name "CustomUserPrincipal.java" -type f

Repository: Techeer-Hogwarts/backend

Length of output: 238


🏁 Script executed:

#!/bin/bash
# Check the content of JwtTokenProvider.java around lines 92-94
if [ -f "techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/JwtTokenProvider.java" ]; then
  cat -n "techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/JwtTokenProvider.java" | sed -n '85,110p'
fi

Repository: Techeer-Hogwarts/backend

Length of output: 1298


🏁 Script executed:

#!/bin/bash
# Find CustomUserPrincipal and check its constructor
find . -name "CustomUserPrincipal.java" -type f -exec cat -n {} \;

Repository: Techeer-Hogwarts/backend

Length of output: 2281


🏁 Script executed:

#!/bin/bash
# Check if there's any validation or null checking around claims extraction
cat -n "techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/JwtTokenProvider.java" | sed -n '1,110p' | grep -A5 -B5 "getAuthentication\|claims.get\|RoleType.valueOf"

Repository: Techeer-Hogwarts/backend

Length of output: 1028


🏁 Script executed:

#!/bin/bash
# Check if there are any token creation methods to see when claims are set
cat -n "techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/JwtTokenProvider.java" | head -90

Repository: Techeer-Hogwarts/backend

Length of output: 4217


기존 토큰에 대한 하위 호환성 문제로 인한 실패 위험이 있습니다.

이 변경 이전에 발급된 토큰에는 role, userId, bootcampYear 클레임이 없을 수 있습니다. 라인 96의 RoleType.valueOf(roleName)에서 roleNamenull이면 NullPointerException이 발생합니다.

배포 전 다음을 고려하세요:

  • 기존 토큰의 만료 또는 갱신 강제
  • 클레임 누락 시 기본값 처리 로직 추가 (예: roleName = claims.getOrDefault(ROLE_KEY, "USER"))
🤖 Prompt for AI Agents
In
@techeerzip/src/main/java/backend/techeerzip/domain/auth/jwt/JwtTokenProvider.java
around lines 92 - 94, JwtTokenProvider currently reads claims into userId,
roleName, bootcampYear and calls RoleType.valueOf(roleName) which will throw a
NullPointerException for older tokens missing these claims; update the parsing
logic in JwtTokenProvider (the method that contains Long userId =
claims.get(...), String roleName = claims.get(...), Integer bootcampYear =
claims.get(...)) to defensively handle absent claims by checking for null or
using defaults (e.g., default roleName to "USER", default userId to 0L or throw
a specific authentication exception, default bootcampYear to a safe value), then
use the safe roleName when calling RoleType.valueOf or map null to a default
RoleType; ensure any chosen fallback is consistent with downstream auth logic
and consider logging or forcing token refresh where appropriate.

Comment on lines +46 to +50
@PreAuthorize("hasPermission(null, 'BOOTCAMP', 'CREATE')")
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BootcampResponse> createBootcamp(
@RequestPart("image") @Valid MultipartFile imageFile,
@RequestPart("request") BootcampCreateRequest request) {
@RequestPart("request") @Valid BootcampCreateRequest request) {
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:

# First, locate and examine BootcampController.java
find . -name "BootcampController.java" -type f

Repository: Techeer-Hogwarts/backend

Length of output: 164


🏁 Script executed:

# Search for PermissionEvaluator implementation
find . -name "*PermissionEvaluator*" -type f

Repository: Techeer-Hogwarts/backend

Length of output: 556


🏁 Script executed:

# Also search for where PermissionEvaluator might be defined or implemented
rg -l "PermissionEvaluator" --type=java

Repository: Techeer-Hogwarts/backend

Length of output: 630


🏁 Script executed:

# Examine BootcampController.java around the mentioned lines
wc -l ./techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/controller/BootcampController.java

Repository: Techeer-Hogwarts/backend

Length of output: 168


🏁 Script executed:

# Read the relevant sections of BootcampController.java
sed -n '40,55p' ./techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/controller/BootcampController.java

Repository: Techeer-Hogwarts/backend

Length of output: 776


🏁 Script executed:

# Check lines 85-105 for updateBootcamp and deleteBootcamp
sed -n '85,105p' ./techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/controller/BootcampController.java

Repository: Techeer-Hogwarts/backend

Length of output: 993


🏁 Script executed:

# Examine BootcampPermissionEvaluator.java to understand how it checks permissions
cat -n ./techeerzip/src/main/java/backend/techeerzip/global/permission/BootcampPermissionEvaluator.java

Repository: Techeer-Hogwarts/backend

Length of output: 3034


UPDATE와 DELETE 메서드의 권한 타입을 수정하세요.

updateBootcampdeleteBootcamp에서 사용하는 타입이 'Bootcamp'이지만, BootcampPermissionEvaluator는 "BOOTCAMP".equals(targetType)으로 대소문자를 구분하여 비교합니다. 현재 구조에서 UPDATE와 DELETE 요청은 권한 검사를 통과하지 못하고 PermissionDeniedException이 발생합니다.

🔎 권한 타입 수정
     @Override
-    @PreAuthorize("hasPermission(#bootcampId, 'Bootcamp', 'UPDATE')")
+    @PreAuthorize("hasPermission(#bootcampId, 'BOOTCAMP', 'UPDATE')")
     @PutMapping(value = "/{bootcampId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public ResponseEntity<BootcampResponse> updateBootcamp(

     @Override
-    @PreAuthorize("hasPermission(#bootcampId, 'Bootcamp', 'DELETE')")
+    @PreAuthorize("hasPermission(#bootcampId, 'BOOTCAMP', 'DELETE')")
     @DeleteMapping("/{bootcampId}")
     public ResponseEntity<Void> deleteBootcamp(@PathVariable Long bootcampId) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/controller/BootcampController.java
around lines 46 - 50, The PreAuthorize type string for updateBootcamp and
deleteBootcamp uses 'Bootcamp' (wrong case) so BootcampPermissionEvaluator's
strict check ("BOOTCAMP".equals(targetType)) rejects them; update the
@PreAuthorize annotations on the updateBootcamp and deleteBootcamp methods to
use the exact uppercase type "BOOTCAMP" so the permission evaluator matches and
authorization succeeds.

Comment on lines +1 to +11
package backend.techeerzip.domain.bootcamp.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import backend.techeerzip.domain.bootcampMember.entity.BootcampMember;

@Repository
public interface BootcampMemberRepository extends JpaRepository<BootcampMember, Long> {

boolean existsByBootcampIdAndUserIdAndIsDeletedFalse(Long bootcampId, Long userId);
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:

#!/bin/bash
# Find all Repository files and examine their package structure
echo "=== All Repository files in the project ==="
fd -e 'java' -p 'Repository\.java$' 

echo -e "\n=== Examining repository locations and patterns ==="
fd -e 'java' -p 'Repository\.java$' | while read file; do
  echo "File: $file"
  grep -E "^package|^import.*domain\." "$file" | head -5
  echo "---"
done

Repository: Techeer-Hogwarts/backend

Length of output: 15416


🏁 Script executed:

#!/bin/bash
# Check if there's a bootcampMember repository directory and its contents
fd -p 'bootcampMember' -type d
echo "---"
find . -path '*/bootcampMember*' -type f -name '*.java' | head -20

Repository: Techeer-Hogwarts/backend

Length of output: 659


🏁 Script executed:

#!/bin/bash
# List all files in bootcampMember domain directory
find ./techeerzip/src/main/java/backend/techeerzip/domain/bootcampMember -type f -name '*.java'
echo "---"
# Check if there's a repository subdirectory
ls -la ./techeerzip/src/main/java/backend/techeerzip/domain/bootcampMember/

Repository: Techeer-Hogwarts/backend

Length of output: 592


리포지토리 패키지 위치를 프로젝트 컨벤션에 맞게 이동

BootcampMemberRepositorybackend.techeerzip.domain.bootcampMember.repository 패키지로 이동해주세요. 프로젝트의 모든 다른 리포지토리는 관리하는 엔티티와 동일한 도메인 패키지에 위치하며, 이 파일만 bootcamp 패키지에 있어 도메인 경계가 불명확합니다.

🤖 Prompt for AI Agents
In
@techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/repository/BootcampMemberRepository.java
around lines 1 - 11, Move the BootcampMemberRepository interface into the
backend.techeerzip.domain.bootcampMember.repository package to match project
conventions: change the package declaration at the top of
BootcampMemberRepository from backend.techeerzip.domain.bootcamp to
backend.techeerzip.domain.bootcampMember.repository, relocate the file to that
package directory, and keep the existing interface signature (public interface
BootcampMemberRepository extends JpaRepository<BootcampMember, Long>) and
imports/annotations intact so existsByBootcampIdAndUserIdAndIsDeletedFalse
continues to work.

Comment on lines +262 to +269
public boolean checkActiveMemberByTeamAndUser(Long bootcampId, Long userId) {
log.info("부트캠프 멤버 검증 시작: teamId={}, userId={}", bootcampId, userId);
final boolean exists =
bootcampMemberRepository.existsByBootcampIdAndUserIdAndIsDeletedFalse(
bootcampId, userId);
log.info("부트캠프 멤버 검증 시작: 존재 여부={}", exists);
return exists;
}
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

로그 메시지 오류: 결과 로그가 시작 로그와 동일함

Line 263과 Line 267의 로그 메시지가 모두 "부트캠프 멤버 검증 시작"으로 동일합니다. Line 267은 결과를 출력하는 로그이므로 메시지를 수정해야 합니다.

🔎 로그 메시지 수정 제안
     public boolean checkActiveMemberByTeamAndUser(Long bootcampId, Long userId) {
         log.info("부트캠프 멤버 검증 시작: teamId={}, userId={}", bootcampId, userId);
         final boolean exists =
                 bootcampMemberRepository.existsByBootcampIdAndUserIdAndIsDeletedFalse(
                         bootcampId, userId);
-        log.info("부트캠프 멤버 검증 시작: 존재 여부={}", exists);
+        log.info("부트캠프 멤버 검증 완료: 존재 여부={}", exists);
         return exists;
     }
📝 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 boolean checkActiveMemberByTeamAndUser(Long bootcampId, Long userId) {
log.info("부트캠프 멤버 검증 시작: teamId={}, userId={}", bootcampId, userId);
final boolean exists =
bootcampMemberRepository.existsByBootcampIdAndUserIdAndIsDeletedFalse(
bootcampId, userId);
log.info("부트캠프 멤버 검증 시작: 존재 여부={}", exists);
return exists;
}
public boolean checkActiveMemberByTeamAndUser(Long bootcampId, Long userId) {
log.info("부트캠프 멤버 검증 시작: teamId={}, userId={}", bootcampId, userId);
final boolean exists =
bootcampMemberRepository.existsByBootcampIdAndUserIdAndIsDeletedFalse(
bootcampId, userId);
log.info("부트캠프 멤버 검증 완료: 존재 여부={}", exists);
return exists;
}
🤖 Prompt for AI Agents
In
@techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/service/BootcampService.java
around lines 262 - 269, The end-of-method log in checkActiveMemberByTeamAndUser
incorrectly repeats the "부트캠프 멤버 검증 시작" message; change the second log (the one
that prints the exists boolean) to indicate completion/result (e.g., "부트캠프 멤버 검증
완료: 존재 여부={}") so the start and result logs are distinct and clear.

Comment on lines +46 to 55
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/api/v3/users")
@RequiredArgsConstructor
public class UserController implements UserSwagger {
private final UserService userService;
private final BootcampService bootcampService;
private final CustomLogger logger;
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

로거 사용의 일관성 문제가 있습니다.

라인 46에서 @Slf4j 어노테이션을 추가하여 log 필드가 생성되었지만, 라인 55의 기존 CustomLogger logger 필드도 여전히 존재합니다. 이로 인해 컨트롤러 내에 두 개의 로거가 공존하게 되었습니다.

대부분의 로깅에는 logger를 사용하지만, 라인 256-259에서는 log를 사용하고 있어 혼란스럽습니다. 컨트롤러 전체에서 하나의 로거를 일관되게 사용해야 합니다.

권장 사항:

  • 모든 로깅을 CustomLogger logger로 통일하고 @Slf4j를 제거하거나,
  • 모든 로깅을 @Slf4jlog로 통일하고 CustomLogger logger를 제거
🔎 CustomLogger 통일 시 수정사항
-import lombok.extern.slf4j.Slf4j;
 
-@Slf4j
 @RestController
 @RequestMapping("/api/v3/users")
 @RequiredArgsConstructor

그리고 라인 256-259를 다음과 같이 수정:

-        log.info(
-                "현재 부트캠프 기수 멤버 프로필 조회 요청 처리 완료 -  count: {}, context: {}",
+        logger.info(
+                "현재 부트캠프 기수 멤버 프로필 조회 요청 처리 완료 -  count: {}",
                 profiles.getProfiles().size(),
                 CONTEXT);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@techeerzip/src/main/java/backend/techeerzip/domain/user/controller/UserController.java
around lines 46 - 55, The controller currently has two loggers: the
Lombok-generated `log` from `@Slf4j` and the injected `CustomLogger logger`,
causing inconsistent usage (e.g., `log` used around lines 256-259 while `logger`
is used elsewhere). Pick one logger and remove the other: either delete the
`@Slf4j` annotation and replace all `log` references with `logger`, or remove
the `CustomLogger logger` field and replace all `logger` calls with `log`;
update the references in `UserController` (including the occurrences around
lines 256-259) so the class uses the single chosen logger consistently.

Comment on lines +28 to +63
@TestConfiguration
@Profile("test")
public class TestExternalServiceConfig {

@MockBean private S3Service s3Service;

@MockBean private SlackService slackService;

@MockBean private SlackEventHandler slackEventHandler;

@MockBean private SlackProperties slackProperties;

@MockBean private HttpClient httpClient;

@MockBean private GoogleDriveService googleDriveService;

@MockBean private RedisService redisService;

/**
* 테스트 환경에서 Redis 연결을 시도하지 않도록 MockBean으로 제공. RedisConfig가 @Profile("!test")로 비활성화되므로, 필요한 빈들을
* 여기서 제공합니다.
*/
@MockBean private RedisConnectionFactory redisConnectionFactory;

@MockBean private RedisTemplate<String, String> redisTemplate;

@MockBean private RedisMessageListenerContainer redisMessageListenerContainer;

/**
* Spring Boot의 RedisReactiveAutoConfiguration이 필요로 하는 빈들. ReactiveRedisTemplate을 생성하기 위해
* ReactiveRedisConnectionFactory가 필요합니다.
*/
@MockBean private ReactiveRedisConnectionFactory reactiveRedisConnectionFactory;

@MockBean private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
}
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 how TestExternalServiceConfig is imported/used
rg -n 'TestExternalServiceConfig' --type=java -B 2 -A 2

Repository: Techeer-Hogwarts/backend

Length of output: 1155


🏁 Script executed:

# Find Spring Boot version
fd -e pom.xml | head -5 | xargs grep -l "spring-boot" | head -1 | xargs cat | grep -A 5 "spring-boot-starter"

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

# Look for any test files that import or use this configuration
fd -e java -path "*/test/*" | xargs grep -l "@Import\|TestExternalServiceConfig" 2>/dev/null | head -10

Repository: Techeer-Hogwarts/backend

Length of output: 236


🏁 Script executed:

# Find pom.xml and check Spring Boot version
find . -name "pom.xml" -type f | head -1 | xargs cat | grep -A 2 "spring-boot-starter-parent\|spring.boot.version" | head -20

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

# Also check if there's a parent pom or build.gradle
ls -la | grep -E "pom|gradle"

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

# Look at the actual pom.xml in techeerzip directory
cat techeerzip/pom.xml 2>/dev/null | grep -i "spring-boot\|version" | head -30

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

# Check IntegrationTestSupport more thoroughly
cat techeerzip/src/test/java/backend/techeerzip/config/IntegrationTestSupport.java

Repository: Techeer-Hogwarts/backend

Length of output: 1322


🏁 Script executed:

# Find the root pom.xml
find . -name "pom.xml" -type f | xargs ls -la

Repository: Techeer-Hogwarts/backend

Length of output: 885


🏁 Script executed:

# Try to get Spring Boot version from root or parent pom
find . -maxdepth 3 -name "pom.xml" | xargs grep -h "spring-boot-starter-parent\|spring.boot.version" 2>/dev/null

Repository: Techeer-Hogwarts/backend

Length of output: 52


🏁 Script executed:

# Search for any test files that extend IntegrationTestSupport to see if tests are passing
rg -l 'extends IntegrationTestSupport' --type java | head -5

Repository: Techeer-Hogwarts/backend

Length of output: 143


🏁 Script executed:

# Check if there are any test failures or issues reported related to MockBean
rg -n 'MockBean\|@Bean' techeerzip/src/test/java/backend/techeerzip/config/ --type java

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

# Read the pom.xml directly
cat ./pom.xml | head -100

Repository: Techeer-Hogwarts/backend

Length of output: 109


🏁 Script executed:

# Read techeerzip/pom.xml if it exists
cat ./techeerzip/pom.xml | grep -i "parent\|spring" | head -40

Repository: Techeer-Hogwarts/backend

Length of output: 120


🏁 Script executed:

# Check the test that extends IntegrationTestSupport
cat ./techeerzip/src/test/java/backend/techeerzip/TecheerzipApplicationTests.java

Repository: Techeer-Hogwarts/backend

Length of output: 440


🏁 Script executed:

# Let's examine TestExternalServiceConfig more carefully with line numbers
cat -n ./techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java

Repository: Techeer-Hogwarts/backend

Length of output: 2925


🏁 Script executed:

# Let's find build files to determine Spring Boot version
find . -name "build.gradle*" -o -name "gradle.properties" | head -5

Repository: Techeer-Hogwarts/backend

Length of output: 93


🏁 Script executed:

# Search for spring-boot version in any configuration files
find . -type f \( -name "*.gradle" -o -name "*.xml" -o -name "*.properties" \) | xargs grep -l "spring" 2>/dev/null | head -5

Repository: Techeer-Hogwarts/backend

Length of output: 308


🏁 Script executed:

# Check if there's a gradle.properties or similar file
ls -la techeerzip/

Repository: Techeer-Hogwarts/backend

Length of output: 657


🏁 Script executed:

# Look for any gradle files or build configuration
find techeerzip -maxdepth 2 -type f \( -name "*.gradle" -o -name "*.gradle.kts" -o -name "gradle.properties" \)

Repository: Techeer-Hogwarts/backend

Length of output: 118


🏁 Script executed:

# Read the build.gradle file to check Spring Boot version
cat techeerzip/build.gradle | head -50

Repository: Techeer-Hogwarts/backend

Length of output: 1529


🏁 Script executed:

# Also check for spring-boot version in the file
cat techeerzip/build.gradle | grep -i "spring" | head -20

Repository: Techeer-Hogwarts/backend

Length of output: 1520


🏁 Script executed:

# Check if there are any test execution issues or errors in test resources
cat techeerzip/src/test/resources/application-test.properties

Repository: Techeer-Hogwarts/backend

Length of output: 4531


🏁 Script executed:

# Let's verify the pattern is correct by checking Spring Boot documentation in code comments
cat techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java -A 5

Repository: Techeer-Hogwarts/backend

Length of output: 4260


🏁 Script executed:

# Check if there are any other test configuration files or patterns used
find techeerzip/src/test/java -name "*Config*.java" -o -name "*Test*.java" | xargs grep -l "@TestConfiguration\|@MockBean" | head -10

Repository: Techeer-Hogwarts/backend

Length of output: 546


🏁 Script executed:

# Check other test configs to see if there's a different pattern used elsewhere
cat techeerzip/src/test/java/backend/techeerzip/config/SyncTestConfig.java

Repository: Techeer-Hogwarts/backend

Length of output: 481


🏁 Script executed:

# Search for any errors or issues with @MockBean in test files
rg "@MockBean" techeerzip/src/test/java --type java -B 3 -A 3 | head -100

Repository: Techeer-Hogwarts/backend

Length of output: 7703


🏁 Script executed:

# Check if there are other ways MockBeans are created in the codebase
rg "mock\|Mock" techeerzip/src/test/java --type java -l | head -10

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

# Let's verify that the test actually runs successfully and there are no known issues
grep -r "contextLoads\|@Test" techeerzip/src/test/java/backend/techeerzip/TecheerzipApplicationTests.java

Repository: Techeer-Hogwarts/backend

Length of output: 103


🌐 Web query:

@MockBean Spring Boot 3.3 @TestConfiguration @Import pattern

💡 Result:

Short answer: in Spring Boot 3.3 you should prefer @MockitoBean (replacement for the old @MockBean) and place those annotations on the test type/fields (or use explicit @bean factory methods). Declaring @MockitoBean/@MockBean inside a top-level @TestConfiguration that you @import can be unreliable because the Mockito/MockBean override processors don’t run for imported configuration the same way — several Spring issues/PRs describe this limitation. Use one of these patterns:

  1. Preferred — put mocks on the test class (or a test superclass / enclosing class):

    • @SpringBootTest
      class MyTest {
      @MockitoBean
      MyDependency mockDep;
      ...
      }
    • (This is the supported usage for @MockitoBean/@MockBean per the framework docs.) [1][3]
  2. If you want reusable mocked beans, expose them as @bean factory methods in your @TestConfiguration (returned value = Mockito.mock(...)) and @import that config:

    • @TestConfiguration
      class SharedTestConfig {
      @bean
      MyService myService() { return Mockito.mock(MyService.class); }
      }
    • @SpringBootTest @import(SharedTestConfig.class)
      class MyTest { ... }
    • This circumvents the limitation by creating mocks as normal beans rather than relying on the @MockitoBean/@MockBean processing. [1][3]
  3. Avoid declaring @MockitoBean/@MockBean on a top-level @TestConfiguration that you @import — there are open/closed issues showing that the annotation processors do not handle that scenario reliably (reports in Spring Boot / Spring Framework issue trackers). If you see missing mocks when importing a TestConfiguration, move the @MockitoBean to the test class or use the @bean-factory workaround above. [2][4]

References

  • Spring Boot testing docs (TestConfiguration/@import notes). [1]
  • Spring Boot issue showing MockitoBean/MockBean not processed in imported TestConfiguration. [2]
  • Spring Framework @MockitoBean javadoc / supported usage (fields/type-level in tests). [3]
  • Spring Framework issue discussion about supporting @MockitoBean in configuration classes. [4]

If you want, tell me which pattern you prefer and I’ll give the minimal example you should use in your project.


@TestConfiguration에서 @MockBean의 신뢰할 수 없는 동작 문제를 해결하세요.

Spring Boot 3.3에서 @import로 포함된 @TestConfiguration 클래스 내의 @MockBean은 Mock 빈 처리 프로세서가 신뢰할 수 없게 동작합니다. 대신 다음 중 하나를 선택하세요:

  1. 추천: @bean 팩토리 메서드로 변경하고 Mockito.mock()을 명시적으로 사용하기

    @TestConfiguration
    @Profile("test")
    public class TestExternalServiceConfig {
        @Bean
        S3Service s3Service() {
            return Mockito.mock(S3Service.class);
        }
        // ... 나머지 빈들
    }
  2. 대안: Mock 빈을 IntegrationTestSupport 테스트 클래스에 직접 선언하기

🤖 Prompt for AI Agents
In
@techeerzip/src/test/java/backend/techeerzip/config/TestExternalServiceConfig.java
around lines 28 - 63, The @MockBean fields in TestExternalServiceConfig cause
unreliable behavior when the class is @Import'ed under Spring Boot 3.3; replace
each @MockBean field in TestExternalServiceConfig with a @Bean factory method
that returns Mockito.mock(...) for the same type (e.g., provide S3Service
s3Service() { return Mockito.mock(S3Service.class); }, similarly for
SlackService, SlackEventHandler, SlackProperties, HttpClient,
GoogleDriveService, RedisService, RedisConnectionFactory, RedisTemplate,
RedisMessageListenerContainer, ReactiveRedisConnectionFactory,
ReactiveRedisTemplate) so the test configuration supplies explicit mock
instances instead of relying on @MockBean processing.

Comment on lines +149 to +196
@Test
@DisplayName("부트캠프 수정 성공 (200 OK)")
@WithMockUser
void updateBootcampSuccess() throws Exception {
// given
Long bootcampId = 1L;

BootcampCreateRequest.BootcampMemberRequest memberReq =
BootcampCreateRequest.BootcampMemberRequest.builder()
.userId(1L)
.position(
backend.techeerzip.domain.bootcampMember.entity.BootcampPosition.FE)
.isLeader(false)
.build();

BootcampCreateRequest updateRequest =
BootcampCreateRequest.builder()
.name("Updated")
.team("Updated Team")
.projectExplain("Updated Description")
.members(java.util.List.of(memberReq))
.build();

MockMultipartFile requestPart =
new MockMultipartFile(
"request",
"",
MediaType.APPLICATION_JSON_VALUE,
objectMapper
.writeValueAsString(updateRequest)
.getBytes(StandardCharsets.UTF_8));
MockMultipartFile imageFile =
new MockMultipartFile(
"image", "new.jpg", MediaType.IMAGE_JPEG_VALUE, "new content".getBytes());

given(bootcampFacadeService.updateBootcamp(eq(bootcampId), any(), any()))
.willReturn(BootcampResponse.builder().name("Updated").build());

// when & then
mockMvc.perform(
multipart(HttpMethod.PUT, BASE_URL + "/{bootcampId}", bootcampId)
.file(imageFile)
.file(requestPart)
.with(csrf()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Updated"))
.andDo(print());
}
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:

rg -n "@PreAuthorize|@Secured|hasPermission|hasRole" techeerzip/src/main/java/backend/techeerzip/domain/bootcamp/controller/BootcampController.java -B 2 -A 2

Repository: Techeer-Hogwarts/backend

Length of output: 1336


🏁 Script executed:

head -100 techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/controller/BootcampControllerTest.java

Repository: Techeer-Hogwarts/backend

Length of output: 4756


🏁 Script executed:

sed -n '1,250p' techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/controller/BootcampControllerTest.java | tail -150

Repository: Techeer-Hogwarts/backend

Length of output: 6307


🏁 Script executed:

rg -n "PermissionEvaluator|@Bean|@Configuration|@TestConfiguration" techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/controller/BootcampControllerTest.java

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

fd "TestConfiguration|SecurityConfiguration" techeerzip/src/test/java/ -type f

Repository: Techeer-Hogwarts/backend

Length of output: 238


🏁 Script executed:

fd "TestConfiguration|SecurityConfiguration" techeerzip/src/test/java/ --type f

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

rg -n "PermissionEvaluator|PermissionChecker|hasPermission" techeerzip/src/test/java/ -A 3 -B 3

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

rg -n "@WebMvcTest|@MockBean|security" techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/controller/BootcampControllerTest.java -B 2 -A 2

Repository: Techeer-Hogwarts/backend

Length of output: 1062


🏁 Script executed:

rg -n "PermissionEvaluator|implements PermissionEvaluator" techeerzip/src/main/java/ -A 5

Repository: Techeer-Hogwarts/backend

Length of output: 7422


🏁 Script executed:

cat techeerzip/src/main/java/backend/techeerzip/global/permission/BootcampPermissionEvaluator.java

Repository: Techeer-Hogwarts/backend

Length of output: 2558


부트캠프 수정 테스트에서 권한 검증 설정이 누락되었습니다.

컨트롤러의 updateBootcamp() 메서드는 @PreAuthorize("hasPermission(#bootcampId, 'Bootcamp', 'UPDATE')")로 보호되어 있습니다. BootcampPermissionEvaluator는 UPDATE 권한 부여를 위해 다음 두 가지를 모두 확인합니다:

  1. 사용자의 bootcampYear가 현재 bootcamp year와 일치
  2. 사용자가 해당 bootcamp의 활성 멤버

그러나 updateBootcampSuccess() 테스트는 @WithMockUser만 사용하고 있어, BootcampService의 다음 메서드들을 모킹하지 않습니다:

  • getCurrentBootcampYear()
  • checkActiveMemberByTeamAndUser(bootcampId, userId)

이로 인해 권한 검증이 실패하여 테스트가 403 Forbidden으로 실패할 가능성이 높습니다. deleteBootcampSuccess() 테스트도 동일한 문제를 가지고 있습니다.

필요한 모킹 설정을 추가하거나 테스트 시나리오를 재검토해 주세요.

🤖 Prompt for AI Agents
In
@techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/controller/BootcampControllerTest.java
around lines 149 - 196, The test updateBootcampSuccess (and similarly
deleteBootcampSuccess) is missing mocks required by the @PreAuthorize permission
check; mock the BootcampService methods the BootcampPermissionEvaluator relies
on: stub getCurrentBootcampYear() to return the current bootcamp year and stub
checkActiveMemberByTeamAndUser(bootcampId, userId) to return true for the test
user, and ensure the test user identity (from @WithMockUser) matches the userId
used in the member request (or mock the Authentication principal accordingly) so
the permission check passes.

Comment on lines +80 to +104
/** 테스트용 User 엔티티를 생성합니다. */
public static User createUser(String suffix) {
Role role = new Role("ROLE_USER");
return User.builder()
.name("테스터" + suffix)
.email("tester" + suffix + "@example.com")
.nickname("tester" + suffix)
.year(2025)
.password("password")
.isLft(false)
.githubUrl("https://github.com/tester" + suffix)
.mainPosition("BE")
.subPosition("FE")
.school("Techeer")
.profileImage(null)
.isAuth(true)
.role(role)
.grade("A")
.mediumUrl(null)
.tistoryUrl(null)
.velogUrl(null)
.bootcampYear(2025)
.feedbackNotes("notes")
.build();
}
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:

#!/bin/bash
# Find User entity
find . -type f -name "User.java" | head -5

Repository: Techeer-Hogwarts/backend

Length of output: 142


🏁 Script executed:

#!/bin/bash
# Search for User class definition and Role relationship
rg -n "class User" --type=java -A 30 | head -100

Repository: Techeer-Hogwarts/backend

Length of output: 11861


🏁 Script executed:

#!/bin/bash
# Search for Role field in User entity and its annotations
rg -n "@.*\nRole|Role\s+role" --type=java -B 2 | head -50

Repository: Techeer-Hogwarts/backend

Length of output: 259


🏁 Script executed:

#!/bin/bash
# Read the User entity file
cat -n techeerzip/src/main/java/backend/techeerzip/domain/user/entity/User.java | head -150

Repository: Techeer-Hogwarts/backend

Length of output: 5838


🏁 Script executed:

#!/bin/bash
# Search for Role field definition in User
rg -n "private.*Role|@.*Role" techeerzip/src/main/java/backend/techeerzip/domain/user/entity/User.java -A 2

Repository: Techeer-Hogwarts/backend

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Check how createUser is used in tests
rg -n "createUser" --type=java -B 2 -A 2 | head -100

Repository: Techeer-Hogwarts/backend

Length of output: 12067


🏁 Script executed:

#!/bin/bash
# Check how Role is handled in tests - look for roleRepository or Role setup
rg -n "roleRepository|new Role\(" --type=java -g '*Test*.java' -A 2 | head -80

Repository: Techeer-Hogwarts/backend

Length of output: 3124


🏁 Script executed:

#!/bin/bash
# Verify there are repository/integration tests that would save User
rg -n "@DataJpaTest|@SpringBootTest|repositoryTest" --type=java -l | head -10

Repository: Techeer-Hogwarts/backend

Length of output: 980


🏁 Script executed:

#!/bin/bash
# Check if there are any bootcamp repository tests
find . -type f -name "*BootcampRepositoryTest*.java" -o -name "*BootcampIntegrationTest*.java"

Repository: Techeer-Hogwarts/backend

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Check BootcampRepositoryTest to see if it uses createUser and saves to database
cat -n techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/repository/BootcampRepositoryTest.java

Repository: Techeer-Hogwarts/backend

Length of output: 10935


🏁 Script executed:

#!/bin/bash
# Check if BootcampRepositoryTest or other tests use createBootcampMember or createUser that would cause the issue
rg -n "createBootcampMember\|createUserWithId\|createUser" techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/ -A 2 | head -60

Repository: Techeer-Hogwarts/backend

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check BootcampTestHelper to see what createBootcamp creates
cat -n techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/helper/BootcampTestHelper.java

Repository: Techeer-Hogwarts/backend

Length of output: 4704


🏁 Script executed:

#!/bin/bash
# Check if any tests use BootcampTestHelper.createBootcampMember
rg -n "createBootcampMember" --type=java

Repository: Techeer-Hogwarts/backend

Length of output: 760


🏁 Script executed:

#!/bin/bash
# Check BootcampMember entity to confirm FK relationship
cat -n techeerzip/src/main/java/backend/techeerzip/domain/bootcampMember/entity/BootcampMember.java | head -100

Repository: Techeer-Hogwarts/backend

Length of output: 2251


🏁 Script executed:

#!/bin/bash
# Verify if BootcampMemberTest and BootcampTest actually persist to database
head -50 techeerzip/src/test/java/backend/techeerzip/domain/bootcampMember/entity/BootcampMemberTest.java

Repository: Techeer-Hogwarts/backend

Length of output: 1801


🏁 Script executed:

#!/bin/bash
# Check BootcampTest.java
head -50 techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/entity/BootcampTest.java

Repository: Techeer-Hogwarts/backend

Length of output: 2089


Repository 테스트에서 Role 엔티티 영속성 주의

createUser가 매번 새로운 Role 객체를 생성합니다. User 엔티티의 Role 필드는 @ManyToOne(fetch = FetchType.LAZY)이며 cascade 설정이 없고, @JoinColumn(nullable = false)입니다. Repository 테스트에서 User를 저장할 때 Role이 먼저 영속화되어 있지 않으면 FK 제약 조건 위반이 발생합니다.

Role을 먼저 저장하는 패턴을 고려하세요. (예: ProjectMemberRepositoryTestroleRepository.save(new Role("ROLE_USER")))

🤖 Prompt for AI Agents
In
@techeerzip/src/test/java/backend/techeerzip/domain/bootcamp/helper/BootcampTestHelper.java
around lines 80 - 104, The createUser helper always constructs a new Role
instance which isn’t persisted and causes FK constraint failures when saving
User; update tests to persist the Role first (e.g., call roleRepository.save(new
Role("ROLE_USER")) in test setup) and then pass the managed Role into user
creation (modify createUser to accept a Role parameter or add an overload
createUser(Role role)), or alternatively change createUser to look up and reuse
an existing persisted Role via RoleRepository before building the User; ensure
the Role used in User.role is a persisted entity.

Comment on lines +99 to +124
@Test
@DisplayName("성공: 삭제된 멤버는 조회되지 않는다")
@Transactional
void findByProjectTeamIdExcludesDeleted() {
// Given - findByProjectTeamId는 단일 결과를 반환하므로, active 멤버 1명만 생성
// 삭제된 멤버는 다른 팀에 생성하여 unique constraint 회피
ProjectTeam team2 = createProjectTeam("팀2");
ProjectTeam savedTeam2 = projectTeamRepository.save(team2);

ProjectMember active =
createProjectMember(savedTeam, savedUser, TeamRole.BACKEND, true);
ProjectMember deleted =
createDeletedProjectMember(savedTeam2, savedUser, TeamRole.FRONTEND, false);

projectMemberRepository.saveAll(List.of(active, deleted));
clearPersistenceContext();

// When
List<ProjectMember> result =
projectMemberRepository.findAllByProjectTeamId(savedTeam.getId());

// Then
assertThat(result).hasSize(1);
assertThat(result.getFirst().isDeleted()).isFalse();
assertThat(result.getFirst().getUser().getId()).isEqualTo(savedUser.getId());
}
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

테스트 위치 또는 메서드가 불일치합니다.

이 테스트는 FindByProjectTeamIdTest 중첩 클래스 내에 있지만, 실제로는 findAllByProjectTeamId (Line 117-118)를 테스트하고 있습니다. 테스트를 FindAllByProjectTeamIdTest 클래스로 이동하거나, findByProjectTeamId 메서드를 테스트하도록 수정해야 합니다.

🤖 Prompt for AI Agents
In
@techeerzip/src/test/java/backend/techeerzip/domain/projectMember/repository/ProjectMemberRepositoryTest.java
around lines 99 - 124, The test is placed in the FindByProjectTeamIdTest nested
class but calls and asserts against findAllByProjectTeamId; move this test
method (findByProjectTeamIdExcludesDeleted) into the FindAllByProjectTeamIdTest
nested class OR change the test to actually exercise findByProjectTeamId
(replace the repository call with
projectMemberRepository.findByProjectTeamId(...) and adjust assertions for a
single Optional/entity result). Update the test name if you change the tested
method to keep names consistent (e.g., rename to
findAllByProjectTeamIdExcludesDeleted if you move it).

Comment on lines +95 to +116
@Test
@DisplayName("update 호출 시 updatedAt이 갱신됨")
void updateUpdatesTimestamp() {
LocalDateTime beforeUpdate = studyMember.getUpdatedAt();

// 시간 차이를 만들기 위해 잠시 대기
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

studyMember.update("새 요약", StatusCategory.REJECT);

assertNotNull(studyMember.getUpdatedAt());
// updatedAt이 변경되었는지 확인 (null이 아닌 경우에만)
if (beforeUpdate != null) {
assertTrue(
studyMember.getUpdatedAt().isAfter(beforeUpdate)
|| studyMember.getUpdatedAt().equals(beforeUpdate));
}
}
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

타임스탬프 업데이트 검증 로직에 결함이 있습니다.

두 가지 문제가 있습니다:

  1. Thread.sleep() 사용: 테스트를 느리고 불안정하게 만듭니다.
  2. 잘못된 단언문: isAfter(beforeUpdate) || equals(beforeUpdate) 조건은 타임스탬프가 변경되지 않아도(equals) 테스트가 통과하므로, 실제로 업데이트를 검증하지 못합니다.

추가로, 순수 단위 테스트 환경에서는 JPA의 @PreUpdate가 동작하지 않을 수 있어 updatedAt이 실제로 갱신되지 않을 수 있습니다.

🔎 개선된 테스트 접근 방법

옵션 1: 타임스탬프 검증을 통합 테스트로 이동
JPA 라이프사이클 콜백(@PreUpdate)은 실제 영속성 컨텍스트에서만 동작하므로, 이 검증은 @DataJpaTest를 사용하는 통합 테스트에서 수행하는 것이 더 적절합니다.

옵션 2: 단위 테스트에서 명시적 검증
엔티티에 타임스탬프를 명시적으로 설정하는 메서드가 있다면 해당 메서드 호출 여부를 검증하거나, 단순히 assertNotNull(studyMember.getUpdatedAt())만 확인합니다.

     @Test
     @DisplayName("update 호출 시 updatedAt이 갱신됨")
     void updateUpdatesTimestamp() {
-        LocalDateTime beforeUpdate = studyMember.getUpdatedAt();
-
-        // 시간 차이를 만들기 위해 잠시 대기
-        try {
-            Thread.sleep(10);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        }
-
         studyMember.update("새 요약", StatusCategory.REJECT);
-
         assertNotNull(studyMember.getUpdatedAt());
-        // updatedAt이 변경되었는지 확인 (null이 아닌 경우에만)
-        if (beforeUpdate != null) {
-            assertTrue(
-                    studyMember.getUpdatedAt().isAfter(beforeUpdate)
-                            || studyMember.getUpdatedAt().equals(beforeUpdate));
-        }
+        // 참고: 실제 타임스탬프 갱신은 JPA 통합 테스트에서 검증
     }
🤖 Prompt for AI Agents
In
@techeerzip/src/test/java/backend/techeerzip/domain/studyMember/entity/StudyMemberTest.java
around lines 95 - 116, The test updateUpdatesTimestamp in StudyMemberTest is
flawed: remove the Thread.sleep and fix the assertion so it verifies a real
change (use isAfter(beforeUpdate) only, not isAfter || equals) or move this test
to an integration test using @DataJpaTest where JPA lifecycle callbacks run
(persist the entity, call studyMember.update(...), save via repository to
trigger @PreUpdate, then assert getUpdatedAt().isAfter(beforeUpdate));
alternatively, if keeping it as a pure unit test, limit the check to
assertNotNull(studyMember.getUpdatedAt()) since @PreUpdate may not run. Ensure
you reference the StudyMember.update(...) call and the updateUpdatesTimestamp
test when making the change.

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.

5 participants