Skip to content

[Feature] 지원자의 지원서 상태를 실시간으로 공유한다.#791

Merged
yw6938 merged 15 commits intodevelop/befrom
feature/#785-sse-applicant-status-MOA-289
Nov 9, 2025
Merged

[Feature] 지원자의 지원서 상태를 실시간으로 공유한다.#791
yw6938 merged 15 commits intodevelop/befrom
feature/#785-sse-applicant-status-MOA-289

Conversation

@yw6938
Copy link
Collaborator

@yw6938 yw6938 commented Oct 12, 2025

#️⃣연관된 이슈

#785

📝작업 내용

실시간으로 지원서 상태를 확인하기 위해 SSE기능을 추가했습니다.

동기보다는 비동기가 실시간으로 데이터를 전송하는 것과
동시 연결 처리에 더 적합할 것 같아서 비동기로 했습니다.

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 새로운 기능
    • 지원자 상태 변경 실시간 스트리밍(SSE)용 엔드포인트 추가
    • 관리자가 지원자 정보를 수정하면 수정 후 즉시 상태 변경 이벤트를 생성해 관련 구독자에게 브로드캐스트
    • 클라이언트가 구독하면 초기 연결 핸드셰이크 전송 및 연결 유지·정리(타임아웃/오류 처리) 지원
    • 전송 이벤트에 사용하는 불변 페이로드 타입 추가(지원자 상태, 메모, 타임스탬프 등)
    • 비동기 처리 설정으로 SSE 기본 타임아웃 30초 적용

@vercel
Copy link

vercel bot commented Oct 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Nov 6, 2025 1:20pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 12, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

SSE 기반 실시간 알림을 위해 불변 이벤트 모델 ApplicantStatusEvent를 추가하고, WebConfig 비동기 설정을 확장했으며, 컨트롤러에 SSE 엔드포인트와 ClubApplyAdminService에 SSE 연결 관리·발송 로직을 도입했습니다.

Changes

Cohort / File(s) 변경 요약
SSE 이벤트 데이터 모델
backend/src/main/java/moadong/club/payload/dto/ApplicantStatusEvent.java
지원자 상태 변경을 전달하는 불변 Java record 타입 추가 (applicantId, status, memo, timestamp, clubId, applicationFormId).
글로벌 비동기 설정
backend/src/main/java/moadong/global/config/WebConfig.java
SSE 처리를 위한 configureAsyncSupport(AsyncSupportConfigurer) 오버라이드 추가 (기본 타임아웃 30000ms).
컨트롤러: SSE 엔드포인트
backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java
GET /api/club/applicant/{applicationFormId}/events 엔드포인트 추가(Produces text/event-stream, 반환형 SseEmitter), 인증·문서 주석 포함.
서비스: SSE 연결관리 및 이벤트 브로드캐스트
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
ClubRepository 주입·SSE 연결 저장소(ConcurrentHashMap) 및 타임아웃 상수 추가, createSseConnection(...), validateClubOwner(...), sendStatusChangeEvent(...) 등 SSE 관련 메서드 추가. editApplicantDetail에서 트랜잭션 커밋 후 ApplicantStatusEvent 생성·방출 로직 등록 및 연결 정리(완료/타임아웃/오류 처리) 추가.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as 클라이언트
    participant Controller as ClubApplyAdminController
    participant Service as ClubApplyAdminService
    participant SSEStore as SSE 저장소
    participant TxnSync as TransactionSynchronization

    Client->>Controller: GET /api/club/applicant/{formId}/events
    Controller->>Service: createSseConnection(formId, user)
    Service->>Service: validateClubOwner(clubId, user)
    Service->>SSEStore: SseEmitter 생성·등록
    Service->>Client: 초기 "connected" 이벤트 전송
    Controller-->>Client: SseEmitter 반환 (스트림 유지)

    Admin->>Controller: 지원자 상태 편집 요청
    Controller->>Service: editApplicantDetail(...)
    Service->>Service: validateClubOwner(clubId, user)
    Service->>Service: DB 업데이트 (상태/메모)
    Service->>TxnSync: afterCommit 콜백 등록
    TxnSync-->>Service: afterCommit 실행
    Service->>Service: ApplicantStatusEvent 생성
    Service->>SSEStore: 관련 SseEmitter들로 "applicant-status-changed" 이벤트 전송
    SSEStore-->>Client: 클라이언트가 이벤트 수신
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

검토 시 주의할 점:

  • SseEmitter 생성·초기화와 완료/타임아웃/오류 콜백의 정확성 및 자원 정리
  • ConcurrentHashMap 기반 연결 관리의 동시성 이슈 및 메모리 누수 가능성
  • validateClubOwner의 권한 검사 시점과 예외 처리 일관성
  • TransactionSynchronization 사용 위치와 이벤트 발송 시점 (트랜잭션 커밋 후 보장)

Possibly related issues

Possibly related PRs

  • PR #621: editApplicantDetail 경로와 DTO/엔티티 변경을 다루며, 본 PR이 해당 경로에서 ApplicantStatusEvent를 생성·발송하도록 연계합니다.
  • PR #620: memo 지원 및 editApplicantDetail 구현 관련 변경과 겹치며, 본 PR은 그 편집 후 SSE 발송을 추가합니다.
  • PR #622: 신청서 편집 흐름 변경과 연관되며, 상태 변경 시점 처리와 연계됩니다.

Suggested labels

📬 API

Suggested reviewers

  • Zepelown
  • lepitaaar
  • seongwon030

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 SSE 기능을 통한 지원자 지원서 상태 실시간 공유라는 주요 변경사항을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경사항(SSE 엔드포인트, 이벤트 구조, 비동기 처리 설정)이 MOA-289의 실시간 지원서 상태 공유 목표를 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 SSE 기반 실시간 상태 공유 기능 구현과 직접 관련되어 있으며, 범위를 벗어난 변경은 없습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#785-sse-applicant-status-MOA-289

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
Contributor

@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

🧹 Nitpick comments (5)
backend/src/main/java/moadong/global/config/WebConfig.java (1)

42-46: 매직 넘버를 상수로 추출하는 것을 고려하세요.

타임아웃 값 30000LClubApplyServiceSseEmitter 생성 시에도 동일하게 사용됩니다. 이 값을 공통 상수로 추출하면 유지보수성이 향상됩니다.

예시:

public class SseConstants {
    public static final long DEFAULT_TIMEOUT = 30000L;
}

그 후 양쪽에서 이 상수를 참조하도록 수정할 수 있습니다.

backend/src/main/java/moadong/club/service/ClubApplyService.java (4)

480-494: 연결 성공 시 초기 이벤트를 전송하는 것을 권장합니다.

SSE 연결이 성공적으로 수립되었을 때 클라이언트에게 초기 이벤트(예: "connected" 메시지)를 전송하면 연결 상태를 즉시 확인할 수 있습니다. 이는 SSE의 모범 사례입니다.

예시:

         sseConnections.put(connectionKey, emitter);
         
+        try {
+            emitter.send(SseEmitter.event()
+                .name("connected")
+                .data("SSE connection established"));
+        } catch (Exception e) {
+            log.warn("초기 SSE 이벤트 전송 실패", e);
+            sseConnections.remove(connectionKey);
+            throw new RestApiException(ErrorCode.SSE_CONNECTION_ERROR);
+        }
+        
         emitter.onCompletion(() -> sseConnections.remove(connectionKey));

485-485: 타임아웃 값을 상수로 추출하는 것을 고려하세요.

30000L 값이 WebConfig에도 사용되고 있습니다. 공통 상수로 추출하여 일관성을 유지하세요.


497-510: SSE 이벤트에 이름을 지정하는 것을 권장합니다.

라인 504에서 send(event)를 사용하고 있는데, 이벤트명을 지정하지 않으면 클라이언트에서 이벤트 타입을 구분하기 어렵습니다. SseEmitter.event() 빌더를 사용하여 이벤트명을 명시하는 것이 좋습니다.

다음과 같이 수정할 수 있습니다:

                 try {
-                    entry.getValue().send(event);
+                    entry.getValue().send(SseEmitter.event()
+                        .name("status-change")
+                        .data(event));
                 } catch (Exception e) {

이렇게 하면 클라이언트에서 addEventListener('status-change', ...)로 특정 이벤트를 구독할 수 있습니다.


480-510: SSE 관리 로직을 별도 클래스로 분리하는 것을 고려하세요.

SSE 연결 관리 로직이 ClubApplyService에 추가되면서 서비스 클래스가 비대해지고 있습니다. 단일 책임 원칙(SRP)을 준수하기 위해 SseConnectionManager와 같은 별도 클래스로 SSE 관련 로직을 추출하는 것을 권장합니다.

예시 구조:

@Component
public class ApplicantStatusSseManager {
    private final Map<String, SseEmitter> connections = new ConcurrentHashMap<>();
    
    public SseEmitter createConnection(String clubId, String applicationFormId, String userId) { ... }
    public void broadcast(String clubId, String applicationFormId, ApplicantStatusEvent event) { ... }
    private void removeConnection(String key) { ... }
}

그 후 ClubApplyService에서 이 매니저를 주입받아 사용하면 됩니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fe1316b and 1aefd3d.

📒 Files selected for processing (4)
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java (2 hunks)
  • backend/src/main/java/moadong/club/payload/dto/ApplicantStatusEvent.java (1 hunks)
  • backend/src/main/java/moadong/club/service/ClubApplyService.java (4 hunks)
  • backend/src/main/java/moadong/global/config/WebConfig.java (2 hunks)
🔇 Additional comments (4)
backend/src/main/java/moadong/club/payload/dto/ApplicantStatusEvent.java (1)

6-13: LGTM!

불변 레코드를 사용한 이벤트 DTO 설계가 적절합니다. 필드 구성도 SSE 이벤트 페이로드로 충분합니다.

backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)

139-148: LGTM!

SSE 엔드포인트 구현이 올바릅니다. 인증 및 권한 검증도 적절하게 적용되어 있습니다.

backend/src/main/java/moadong/club/service/ClubApplyService.java (2)

52-53: LGTM!

ConcurrentHashMap을 사용하여 SSE 연결을 관리하는 것은 스레드 안전성을 보장하므로 적절한 선택입니다.


322-332: LGTM!

지원자 상태 변경 후 SSE 이벤트를 생성하고 전송하는 로직이 잘 구현되어 있습니다.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

수고하셨습니다! WebSocket,SSE, Http Polling 이 두개 차이점과 왜 SSE를 사용했는지 생각해보면 좋을꺼같습니다!

@yw6938 yw6938 added ✨ Feature 기능 개발 💾 BE Backend labels Oct 16, 2025
Copy link
Contributor

@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: 3

🧹 Nitpick comments (2)
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)

497-511: SSE 이벤트 포맷을 명시하고 로깅을 보강하세요

클라이언트 측 구독의 일관성을 위해 이벤트명을 명시하는 것을 권장합니다. 실패 로그에 key 컨텍스트를 포함하면 운영에 유용합니다.

-                    entry.getValue().send(event);
+                    entry.getValue().send(
+                        SseEmitter.event()
+                            .name("applicant-status-changed")
+                            .data(event)
+                    );
-                } catch (Exception e) {
-                    log.warn("SSE 이벤트 발송 실패: {}", e.getMessage());
+                } catch (Exception e) {
+                    log.warn("SSE 이벤트 발송 실패 clubId={}, formId={}, key={}: {}",
+                        clubId, applicationFormId, entry.getKey(), e.getMessage());
                     sseConnections.remove(entry.getKey());
                 }

52-53: 문자열 기반 키와 prefix 매칭은 취약 — 타입 안전한 키 구조 권장

clubId_applicationFormId_userId 문자열 결합 + startsWith는 구분자 충돌·가독성·오타 검출 측면에서 취약합니다. record ConnectionKey(String clubId, String applicationFormId, String userId) 같은 타입 안전 키 또는 중첩 맵(Map<clubId, Map<formId, Map<userId, List>>>) 구조로의 전환을 권장합니다. 최소한 고유 구분자 사용 또는 String.join(delimiter, ...)로 일관성을 확보하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1aefd3d and 94fe308.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/club/service/ClubApplyService.java (4 hunks)
🔇 Additional comments (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)

484-486: ID 접근자 일관성 유지된 점 좋습니다

user.getId() 사용으로 과거 불일치(getUserId) 이슈가 해소되었습니다.

Copy link
Contributor

@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

♻️ Duplicate comments (2)
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)

323-334: 트랜잭션 커밋 전 이벤트 발송 — 롤백 시 잘못된 알림 위험. AFTER_COMMIT으로 이동하세요.

현재 @transactional 범위 내에서 즉시 비동기 발송됩니다. 커밋 이후에만 발송하도록 afterCommit 훅을 사용하세요. 타임스탬프도 KST로 고정 권장.

적용 diff 예시:

-            ApplicantStatusEvent event = new ApplicantStatusEvent(
+            ApplicantStatusEvent event = new ApplicantStatusEvent(
                 app.getId(),
                 editRequest.status(),
                 editRequest.memo(),
-                LocalDateTime.now(),
+                ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(),
                 clubId,
                 applicationFormId
             );
-            CompletableFuture.runAsync(() -> sendStatusChangeEvent(clubId, applicationFormId, event));
+            // 커밋 이후에만 SSE 발송
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    CompletableFuture.runAsync(() -> sendStatusChangeEvent(clubId, applicationFormId, event));
+                }
+            });

추가 import:

import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionSynchronization;

481-495: 동일 key 재구독 처리 및 초기 플러시 이벤트, 타임아웃 값 개선.

  • 동일 key에서 기존 emitter 덮어쓰며 기존 연결을 방치합니다 → prev.complete()로 정리.
  • 초기 “connected” 이벤트를 한 번 보내 프록시 버퍼링을 깨주세요.
  • 30초 타임아웃은 짧습니다(불필요한 재연결 유발) → 5분 등으로 상향 또는 설정값화.

적용 diff 예시:

-        String connectionKey = clubId + "_" + applicationFormId + "_" + user.getId();
-        SseEmitter emitter = new SseEmitter(30000L); // 30초 타임아웃
-        
-        sseConnections.put(connectionKey, emitter);
+        String connectionKey = clubId + "_" + applicationFormId + "_" + user.getId();
+        SseEmitter emitter = new SseEmitter(300_000L); // 5분 타임아웃(필요 시 설정값으로)
+
+        SseEmitter prev = sseConnections.put(connectionKey, emitter);
+        if (prev != null) {
+            try { prev.complete(); } catch (Exception ignore) {}
+        }
         
         emitter.onCompletion(() -> sseConnections.remove(connectionKey));
         emitter.onTimeout(() -> sseConnections.remove(connectionKey));
         emitter.onError((ex) -> sseConnections.remove(connectionKey));
-        
-        return emitter;
+        // 초기 핸드셰이크 이벤트로 프록시 버퍼 방지
+        try {
+            emitter.send(SseEmitter.event().name("connected").data("ok"));
+        } catch (Exception e) {
+            sseConnections.remove(connectionKey);
+            emitter.completeWithError(e);
+        }
+        return emitter;
🧹 Nitpick comments (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)

52-54: SSE 연결 레지스트리 구조 제안(중복 탭/브라우저 대비).

단일 SseEmitter만 보관하면 동일 key 재연결 시 덮어쓰기/누수 우려가 있습니다. 선택지:

  • 다중 연결 지원: Map<String, Set>로 확장.
  • 단일 연결 유지: 신규 등록 시 이전 emitter를 complete()로 명시 종료. 아래 createSseConnection 코멘트의 diff 참고.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 94fe308 and 4954614.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/club/service/ClubApplyService.java (4 hunks)
🔇 Additional comments (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)

39-41: LGTM: 필요한 import가 적절히 추가되었습니다.

이전 컴파일 이슈(CompletableFuture, SseEmitter) 해소 확인했습니다.

@yw6938 yw6938 requested a review from alsdddk November 2, 2025 07:22
Copy link
Contributor

@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

♻️ Duplicate comments (2)
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)

482-495: 동일 키로 재연결 시 기존 Emitter가 정리되지 않아 리소스 누수가 발생합니다.

같은 사용자가 동일한 connectionKey로 재구독하면 기존 emitter가 Map에서 덮어씌워지면서 참조만 사라지고 실제 연결은 정리되지 않습니다. 이로 인해 리소스 누수가 발생할 수 있습니다.

다음과 같이 수정하세요:

         String connectionKey = clubId + "_" + applicationFormId + "_" + user.getId();
         SseEmitter emitter = new SseEmitter(300000L); // 5분 타임아웃
         
-        sseConnections.put(connectionKey, emitter);
+        SseEmitter prev = sseConnections.put(connectionKey, emitter);
+        if (prev != null) {
+            try { 
+                prev.complete(); 
+            } catch (Exception ignored) {}
+        }
         
         emitter.onCompletion(() -> sseConnections.remove(connectionKey));
         emitter.onTimeout(() -> sseConnections.remove(connectionKey));
         emitter.onError((ex) -> sseConnections.remove(connectionKey));
         
+        // 초기 연결 확인 이벤트 전송 (프록시 버퍼링 방지)
+        try {
+            emitter.send(SseEmitter.event().name("connected").data("ok"));
+        } catch (Exception e) {
+            sseConnections.remove(connectionKey);
+            emitter.completeWithError(e);
+        }
+        
         return emitter;

323-334: 트랜잭션 커밋 전 이벤트 전송으로 데이터 불일치 가능성이 있습니다.

현재 코드는 @Transactional 범위 내에서 즉시 비동기로 이벤트를 전송하므로, 트랜잭션이 롤백될 경우에도 클라이언트에게 잘못된 이벤트가 전달될 수 있습니다. 트랜잭션이 성공적으로 커밋된 이후에만 이벤트를 발송하도록 수정해야 합니다.

다음과 같이 수정하세요:

+import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.transaction.support.TransactionSynchronization;
             app.updateMemo(editRequest.memo());
             app.updateStatus(editRequest.status());
             
             // SSE 이벤트 발송
             ApplicantStatusEvent event = new ApplicantStatusEvent(
                 app.getId(),
                 editRequest.status(),
                 editRequest.memo(),
-                LocalDateTime.now(),
+                ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(),
                 clubId,
                 applicationFormId
             );
-            CompletableFuture.runAsync(() -> sendStatusChangeEvent(clubId, applicationFormId, event));
+            
+            // 트랜잭션 커밋 후에만 이벤트 전송
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    CompletableFuture.runAsync(() -> sendStatusChangeEvent(clubId, applicationFormId, event));
+                }
+            });
         });
🧹 Nitpick comments (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)

498-517: 이벤트 발송 로직이 잘 구현되었습니다.

접두어 매칭, 이벤트 메타데이터, 에러 처리가 모두 적절하게 구현되어 있습니다.

성능 개선을 위해 CompletableFuture.runAsync()의 기본 ForkJoinPool.commonPool() 대신 전용 Executor를 주입하여 사용하는 것을 고려해보세요. 이렇게 하면 SSE 이벤트 전송이 다른 작업과 격리되어 더 안정적으로 동작할 수 있습니다.

@Async("sseTaskExecutor")
public void sendStatusChangeEventAsync(String clubId, String applicationFormId, ApplicantStatusEvent event) {
    sendStatusChangeEvent(clubId, applicationFormId, event);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4954614 and 60ef845.

📒 Files selected for processing (2)
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java (2 hunks)
  • backend/src/main/java/moadong/club/service/ClubApplyService.java (4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyService.java
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java
📚 Learning: 2025-09-30T05:26:41.788Z
Learnt from: alsdddk
Repo: Moadong/moadong PR: 765
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:431-435
Timestamp: 2025-09-30T05:26:41.788Z
Learning: In the Moadong codebase's club application feature (backend/src/main/java/moadong/club/), multiple ClubApplicationForm entities can have ACTIVE status for the same clubId, semesterYear, and semesterTerm simultaneously. There is no uniqueness constraint requiring only one ACTIVE form per semester.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyService.java
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyService.java
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyService.java
🔇 Additional comments (4)
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)

39-41: Imports가 올바르게 추가되었습니다.

SSE 기능 구현에 필요한 모든 import가 정확하게 추가되었습니다.


53-54: SSE 연결 저장소가 적절하게 선언되었습니다.

ConcurrentHashMap을 사용하여 동시성 환경에서 안전하게 SSE 연결을 관리할 수 있습니다.

backend/src/main/java/moadong/club/controller/ClubApplyController.java (2)

31-31: SseEmitter import가 올바르게 추가되었습니다.

SSE 엔드포인트 구현에 필요한 import가 정확하게 추가되었습니다.


139-148: SSE 엔드포인트가 올바르게 구현되었습니다.

produces="text/event-stream" 속성이 명시되어 있고, 인증 및 권한 검증이 적절하게 설정되어 있으며, 서비스 계층으로의 위임도 정확합니다.

다만, lepitaaar님의 이전 코멘트에서 언급하신 것처럼 SSE 연결 수가 많아질 경우 이벤트 전송 시간이 증가할 수 있습니다. 실제 운영 환경에서의 동시 연결 수를 고려하여 부하 테스트를 수행해보시는 것을 권장합니다.

Based on learnings

@yw6938 yw6938 requested a review from Zepelown November 5, 2025 06:08
Copy link
Member

@Zepelown Zepelown left a comment

Choose a reason for hiding this comment

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

고생하셨습니다. 리뷰단 내용 반영 부탁드립니다.

@github-actions
Copy link

github-actions bot commented Nov 6, 2025

Test Results

78 tests   75 ✅  16s ⏱️
13 suites   3 💤
13 files     0 ❌

Results for commit 32f0d5a.

♻️ This comment has been updated with latest results.

@yw6938 yw6938 requested a review from Zepelown November 6, 2025 10:02
Copy link
Contributor

@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

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9e77c25 and df49913.

📒 Files selected for processing (2)
  • backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java (2 hunks)
  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (5 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-30T05:26:41.788Z
Learnt from: alsdddk
Repo: Moadong/moadong PR: 765
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:431-435
Timestamp: 2025-09-30T05:26:41.788Z
Learning: In the Moadong codebase's club application feature (backend/src/main/java/moadong/club/), multiple ClubApplicationForm entities can have ACTIVE status for the same clubId, semesterYear, and semesterTerm simultaneously. There is no uniqueness constraint requiring only one ACTIVE form per semester.

Applied to files:

  • backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java
  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java
  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
🔇 Additional comments (1)
backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java (1)

110-118: SSE 엔드포인트 추가 잘 반영되었습니다.
인증 검증과 서비스 위임이 간결하게 정리되어 있어 클라이언트 연동이 명확합니다.

Copy link
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)

237-276: CompletableFuture.runAsync() 예외 처리를 추가하세요.

현재 코드의 CompletableFuture.runAsync() (라인 270)는 명시적인 예외 처리가 없어서, sendStatusChangeEvent 메서드 내에서 예외가 발생하거나 비동기 작업 자체에서 문제가 발생하면 조용히 실패할 수 있습니다. 메서드 내부의 try-catch만으로는 충분하지 않습니다.

-                    CompletableFuture.runAsync(() -> sendStatusChangeEvent(clubId, applicationFormId, event));
+                    CompletableFuture.runAsync(() -> sendStatusChangeEvent(clubId, applicationFormId, event))
+                        .exceptionally(ex -> {
+                            log.error("SSE 이벤트 발송 중 예외 발생: applicantId={}, error={}", app.getId(), ex.getMessage(), ex);
+                            return null;
+                        });

또한 forEach 루프에서 각 지원자마다 TransactionSynchronization을 개별 등록하는 것이 의도된 동작인지 재검토하세요. 모든 이벤트를 단일 동기화로 묶어서 처리하는 것이 더 효율적일 수 있습니다.

🧹 Nitpick comments (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)

365-371: 코드 중복: validateClubOwner 메서드를 공통 유틸리티로 추출하는 것을 권장합니다.

ClubApplyServiceV1에도 동일한 validateClubOwner 메서드가 존재합니다. 두 서비스 클래스에서 동일한 검증 로직을 사용하고 있으므로, 공통 유틸리티 클래스나 별도의 validator 컴포넌트로 추출하는 것을 고려해 보세요.

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between df49913 and ce1b49d.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (5 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-09-30T05:26:41.788Z
Learnt from: alsdddk
Repo: Moadong/moadong PR: 765
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:431-435
Timestamp: 2025-09-30T05:26:41.788Z
Learning: In the Moadong codebase's club application feature (backend/src/main/java/moadong/club/), multiple ClubApplicationForm entities can have ACTIVE status for the same clubId, semesterYear, and semesterTerm simultaneously. There is no uniqueness constraint requiring only one ACTIVE form per semester.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
🧬 Code graph analysis (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)
backend/src/main/java/moadong/club/service/ClubApplyServiceV1.java (1)
  • Service (31-202)
🔇 Additional comments (3)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (3)

14-14: SSE 인프라가 적절히 추가되었습니다.

필요한 의존성과 필드들이 올바르게 추가되었으며, ConcurrentHashMap을 사용하여 동시성 안전성을 확보한 점이 좋습니다.

Also applies to: 35-39, 45-45, 52-55


373-404: SSE 연결 관리가 올바르게 구현되었습니다.

과거 리뷰에서 지적된 race condition 문제가 적절히 수정되었습니다. 기존 연결을 먼저 제거한 후 새 연결을 등록하고, remove(key, emitter) 메서드를 사용하여 동일 인스턴스일 때만 제거하도록 구현한 점이 좋습니다.


407-433: 이벤트 발송 메서드가 안전하게 구현되었습니다.

스냅샷을 만들어 순회하여 동시성 문제를 방지하고, 실패 시 적절한 로그와 함께 연결을 정리하는 로직이 잘 구현되었습니다.

Copy link
Collaborator

@alsdddk alsdddk left a comment

Choose a reason for hiding this comment

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

수고하셨어요!! 비동기로 알림 구현에 대해서 저도 많이 배웠습니다

@Transactional
public void editApplicantDetail(String applicationFormId, List<ClubApplicantEditRequest> request, CustomUserDetails user) {
String clubId = user.getClubId();
validateClubOwner(clubId, user);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이미 userDetails로 받은 유저 정보에서 clubId를 가져왔기에 validateClubOwner()은 필요없긴 해용

@yw6938 yw6938 requested review from Zepelown and removed request for Zepelown November 6, 2025 12:59
Copy link
Contributor

@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

🧹 Nitpick comments (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)

44-44: 사용되지 않는 필드를 제거하는 것을 고려해보세요.

ClubRepository 필드가 코드 내에서 사용되지 않는 것으로 보입니다. 이전 리뷰 코멘트에 따르면 validateClubOwner가 제거되었는데, 해당 필드가 남아있는 것 같습니다.

사용하지 않는다면 다음과 같이 제거할 수 있습니다:

-    private final ClubRepository clubRepository;
     private final ClubApplicationFormsRepository clubApplicationFormsRepository;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ce1b49d and 64237ba.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-09-30T05:26:41.788Z
Learnt from: alsdddk
Repo: Moadong/moadong PR: 765
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:431-435
Timestamp: 2025-09-30T05:26:41.788Z
Learning: In the Moadong codebase's club application feature (backend/src/main/java/moadong/club/), multiple ClubApplicationForm entities can have ACTIVE status for the same clubId, semesterYear, and semesterTerm simultaneously. There is no uniqueness constraint requiring only one ACTIVE form per semester.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
🧬 Code graph analysis (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)
backend/src/main/java/moadong/club/service/ClubApplyServiceV1.java (1)
  • Service (31-202)
🔇 Additional comments (4)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (4)

34-38: SSE 구현을 위한 적절한 import입니다.

SSE 연결 관리, 동시성 처리, 트랜잭션 커밋 후 이벤트 발송을 위한 import가 모두 적절하게 추가되었습니다.


50-54: SSE 연결 관리를 위한 적절한 필드 선언입니다.

ConcurrentHashMap을 사용한 thread-safe한 연결 관리와 5분 타임아웃 설정이 적절합니다.


236-274: 트랜잭션 커밋 후 SSE 이벤트 발송 로직이 잘 구현되었습니다.

TransactionSynchronization을 사용하여 DB 커밋이 성공한 후에만 SSE 이벤트를 발송하도록 구현한 것이 적절합니다. 각 지원자별로 개별 이벤트가 생성되어 세밀한 상태 업데이트가 가능합니다.


396-423: 이벤트 브로드캐스팅 로직이 안전하게 구현되었습니다.

스냅샷을 사용하여 동시성 문제를 방지하고, 실패한 emitter를 적절히 정리하며, atomic removal로 race condition을 방지하고 있습니다. 에러 핸들링도 포괄적으로 구현되었습니다.

Copy link
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)

236-274: 치명적인 권한 검증 누락 - applicationFormId 소유권 미확인

editApplicantDetail 메서드가 applicationFormId가 사용자의 클럽에 속하는지 검증하지 않습니다. 244번 줄의 findAllByIdInAndFormIdformId만 확인하고 clubId는 확인하지 않습니다.

악의적인 사용자가 다른 클럽의 applicationFormId를 알고 있다면, 해당 클럽의 지원자 상태와 메모를 임의로 수정할 수 있는 권한 상승 취약점이 존재합니다.

동일 서비스의 getClubApplyInfo (192번 줄)처럼 findByClubIdAndId를 사용하여 소유권을 검증해야 합니다.

다음과 같이 메서드 시작 부분에 검증 로직을 추가하세요:

 public void editApplicantDetail(String applicationFormId, List<ClubApplicantEditRequest> request, CustomUserDetails user) {
     String clubId = user.getClubId();
+    
+    // applicationFormId가 user의 clubId에 속하는지 검증
+    clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId)
+        .orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));

     Map<String, ClubApplicantEditRequest> requestMap = request.stream()
🧹 Nitpick comments (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)

265-270: CompletableFuture의 기본 스레드 풀 사용 개선 고려

268번 줄에서 CompletableFuture.runAsync()가 기본 ForkJoinPool.commonPool()을 사용합니다. 이 풀은 CPU 집약적 작업에 최적화되어 있어, I/O 작업인 SSE 이벤트 발송에는 최적이 아닐 수 있습니다.

이벤트 발송 빈도가 높아질 경우 전용 Executor를 주입받아 사용하는 것을 고려해보세요.

예시:

@Service
@AllArgsConstructor
public class ClubApplyAdminService {
    private final Executor sseEventExecutor; // 전용 스레드 풀 주입
    
    // ...
    
    CompletableFuture.runAsync(
        () -> sendStatusChangeEvent(clubId, applicationFormId, event),
        sseEventExecutor
    );
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 64237ba and 32f0d5a.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-09-30T05:26:41.788Z
Learnt from: alsdddk
Repo: Moadong/moadong PR: 765
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:431-435
Timestamp: 2025-09-30T05:26:41.788Z
Learning: In the Moadong codebase's club application feature (backend/src/main/java/moadong/club/), multiple ClubApplicationForm entities can have ACTIVE status for the same clubId, semesterYear, and semesterTerm simultaneously. There is no uniqueness constraint requiring only one ACTIVE form per semester.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.

Applied to files:

  • backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
🧬 Code graph analysis (1)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)
backend/src/main/java/moadong/club/service/ClubApplyServiceV1.java (1)
  • Service (31-202)
🔇 Additional comments (2)
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (2)

364-398: SSE 연결 생성 로직이 안전하게 구현되었습니다

이전 리뷰에서 지적된 race condition 문제가 해결되었습니다:

  • 376-381번 줄: 기존 emitter를 맵에서 먼저 제거한 후 정리
  • 385-387번 줄: atomic removal로 동시성 문제 방지
  • 390-395번 줄: 초기 핸드셰이크 이벤트로 프록시 버퍼링 문제 해결

권한 검증(368-369번 줄)도 적절히 추가되어 보안 취약점이 해결되었습니다.


400-427: 이벤트 발송 로직이 견고하게 구현되었습니다

동시성 안전성:

  • 406-408번 줄: 스냅샷 생성으로 ConcurrentModificationException 방지
  • 421번 줄: atomic removal로 race condition 방지

에러 처리:

  • 419번 줄: 발송 실패 시 로깅
  • 421-424번 줄: 실패한 emitter 정리 및 완료 처리

전체적으로 프로덕션 환경에 적합한 견고한 구현입니다.

Copy link
Member

@Zepelown Zepelown left a comment

Choose a reason for hiding this comment

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

고생하셨습니다.

@yw6938 yw6938 merged commit 0be5bb6 into develop/be Nov 9, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💾 BE Backend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants