Skip to content

20260226 #236 관리자 페이지 통계 대시보드 실시간 활동 로그 추가#249

Merged
discipline24 merged 6 commits intomainfrom
20260226_#236_관리자_페이지_통계_대시보드_실시간_활동_로그_추가
Feb 27, 2026

Hidden character warning

The head ref may contain hidden characters: "20260226_#236_\uad00\ub9ac\uc790_\ud398\uc774\uc9c0_\ud1b5\uacc4_\ub300\uc2dc\ubcf4\ub4dc_\uc2e4\uc2dc\uac04_\ud65c\ub3d9_\ub85c\uadf8_\ucd94\uac00"
Merged

20260226 #236 관리자 페이지 통계 대시보드 실시간 활동 로그 추가#249
discipline24 merged 6 commits intomainfrom
20260226_#236_관리자_페이지_통계_대시보드_실시간_활동_로그_추가

Conversation

@discipline24
Copy link
Contributor

@discipline24 discipline24 commented Feb 27, 2026

Summary by CodeRabbit

  • 새 기능

    • 사용자 활동 기록(출석, 게시물/댓글/좋아요, 베팅, 로그인/회원가입 등) 저장 및 조회 기능 추가
    • 관리자 대시보드로 실시간 활동 푸시(서버-센트 이벤트) 제공
  • 개선 사항

    • 다양한 활동 발생 시점에 이벤트를 발행해 실시간 반영 지원
    • 프론트엔드 URL/배포 설정 통합 및 배포 환경 단순화
  • 기타

    • 서버 상태 확인 메시지 문구 업데이트

회원가입, 로그인, 베팅 참여, 게시물 작성, 출석 체크인에 이벤트 추가 완료
백테스팅, 댓글, 좋아요 이벤트 추가 고려
QrStreamService에 SseService 코드 사용 고려
백테스팅, 퀀트봇에 이벤트 발생 고려
QrStreamService에 SseService 코드 사용 고려
yml 수정에 따른 securityConfig 수정
@discipline24 discipline24 requested a review from Kosw6 as a code owner February 27, 2026 17:05
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Walkthrough

새로운 액티비티 로깅 시스템을 도입해 서비스 동작(출석, 게시물/댓글/좋아요, 베팅, 로그인/가입)을 ActivityEvent로 발행하고 DB에 저장한 뒤 SSE로 실시간 대시보드에 전송하도록 통합하였습니다. 또한 일부 서비스 메서드 시그니처를 사용자명 인자를 받도록 확장하고 프론트엔드 URL 설정을 단일 프로퍼티로 통합했습니다.

Changes

Cohort / File(s) Summary
액티비티 도메인 및 저장/이벤트 처리
backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java, backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityType.java, backend/src/main/java/org/sejongisc/backend/activity/event/ActivityEvent.java, backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java, backend/src/main/java/org/sejongisc/backend/activity/listener/ActivityEventListener.java
ActivityLog JPA 엔티티, ActivityType enum, ActivityEvent 레코드, 리포지토리 쿼리(대시보드/유저 페이지/집계) 및 트랜잭션 커밋 후 비동기 리스너 추가.
실시간 전송(서버-센트 이벤트)
backend/src/main/java/org/sejongisc/backend/common/sse/SseService.java
채널 기반 SseEmitter 관리 및 이벤트 전송 로직 추가(구독, 전송, 제거, 구독 여부 확인).
서비스 통합 — 시그니처 변경 및 이벤트 발행
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java, backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java, backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java, backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java, backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java, backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java, backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java, backend/src/main/java/org/sejongisc/backend/user/service/UserService.java
checkIn, postUserBet 등에 사용자명 파라미터 추가. 여러 서비스에 ApplicationEventPublisher 주입 및 관련 ActivityEvent 발행 로직 삽입(출석, 베팅 참여, 게시물 작성, 댓글/좋아요, 로그인, 가입).
조회 최적화
backend/src/main/java/org/sejongisc/backend/board/repository/PostRepository.java
Post와 연관된 Board를 함께 페치하는 findByIdWithBoard 쿼리 메서드 추가(LEFT JOIN FETCH).
환경/보안 설정 및 URL 통합
backend/src/main/resources/application-prod.yml, backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java, backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java, .github/workflows/deploy.yml
dev/prod 프론트엔드 URL을 단일 app.frontend-url로 통합. Security CORS 설정과 워크플로우의 이미지/레지스트리/SSH/FRONTEND_URL 관련 환경 처리 변경.
주석·테스트 정리
backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java, backend/src/main/java/org/sejongisc/backend/common/config/db/StockDataSourceConfig.java, backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java
주석 수정 및 테스트 내 예외 어설션 주석 처리(검증 제거).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Service as 서비스 (Attendance/Betting/Board)
    participant EventPublisher as ApplicationEventPublisher
    participant Listener as ActivityEventListener
    participant Repository as ActivityLogRepository
    participant SseService as SseService

    User->>Service: 행위 수행 (출석/베팅/댓글/게시글/로그인/가입)
    Service->>Service: 비즈니스 로직 실행
    Service->>EventPublisher: publishEvent(ActivityEvent)
    EventPublisher->>Listener: handleActivityEvent() (`@TransactionalEventListener` AFTER_COMMIT)
    Listener->>Repository: save(ActivityLog)
    Repository-->>Listener: 저장 완료
    Listener->>SseService: send(ADMIN_DASHBOARD, eventName, ActivityLog)
    SseService->>SseService: 채널의 emitters 순회 및 전송
    SseService-->>User: SSE 이벤트 전송 (구독자에게)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

BE

Suggested reviewers

  • cksdid202

토끼의 시

🐰 이벤트가 튀어나온다, 깡총깡총
출석도, 댓글도, 베팅도 한줄기 빛
로그는 쌓이고 대시보드 반짝,
SSE로 전송하니 모두가 봐요 —
우후, 배포도 깔끔히! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 관리자 페이지 통계 대시보드에 실시간 활동 로그를 추가하는 것으로, 변경사항의 주요 내용을 명확하게 요약하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 20260226_#236_관리자_페이지_통계_대시보드_실시간_활동_로그_추가

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

Caution

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

⚠️ Outside diff range comments (1)
backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java (1)

70-77: ⚠️ Potential issue | 🔴 Critical

테스트가 postUserBet을 호출하지 않아 verify() 호출이 실패합니다.

assertThatThrownBy 전체가 주석 처리되어 있으므로 bettingService.postUserBet() 메서드 호출 자체가 실행되지 않습니다. 따라서 verify(pointHistoryRepository, times(1)).save(...)verify(userBetRepository, times(1)).save(...)는 "Wanted but not invoked" 오류로 실패할 것입니다.

또한 postUserBet() 메서드 시그니처가 3개 파라미터를 요구하지만(userId, username, userBetRequest), 테스트는 2개만 전달하고 있습니다.

제안된 수정사항
-        //assertThatThrownBy(() -> bettingService.postUserBet(userId, req)).isInstanceOf(RuntimeException.class).hasMessageContaining("외부 트랜잭션 롤백");
+        assertThatThrownBy(() -> bettingService.postUserBet(userId, "testUser", req))
+            .isInstanceOf(RuntimeException.class);

         verify(pointHistoryRepository, times(1)).save(any(PointHistory.class));
         verify(userBetRepository, times(1)).save(any());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java`
around lines 70 - 77, The test fails because postUserBet(...) is never invoked
and the wrong overload is used; uncomment and call bettingService.postUserBet
with the correct three-argument signature (userId, username, userBetRequest)
inside the assertThatThrownBy so the RuntimeException thrown by the mocked
userBetRepository.save triggers and the verifications run; ensure the
assertThatThrownBy wraps bettingService.postUserBet(userId, username, req) and
then verify pointHistoryRepository.save(...) and userBetRepository.save(...)
were each called once.
🧹 Nitpick comments (6)
backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java (1)

107-108: Line 107 URL 결합 시 슬래시 정규화를 권장합니다.

app.frontend-url에 trailing slash가 들어오면 //attendance가 생성될 수 있어 URL 포맷을 고정해두는 편이 안전합니다.

제안 코드
 public String createQrUrl(UUID roundId, String token) {
-    String baseUrl = frontendUrl;
+    String baseUrl = frontendUrl.endsWith("/")
+        ? frontendUrl.substring(0, frontendUrl.length() - 1)
+        : frontendUrl;
     return String.format("%s%s?roundId=%s&token=%s", baseUrl, ATTENDANCE_PATH, roundId, token);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java`
around lines 107 - 108, The URL builder in QrTokenStreamService that does return
String.format("%s%s?roundId=%s&token=%s", baseUrl, ATTENDANCE_PATH, roundId,
token) can produce a double slash if app.frontend-url has a trailing slash;
normalize the base and path before joining by trimming any trailing '/' from
frontendUrl (or ensure ATTENDANCE_PATH is normalized to start with a single
'/'), or use a standard join/resolve approach (e.g., URI.resolve) to ensure
exactly one separator between base and ATTENDANCE_PATH; update the code that
sets baseUrl and/or the ATTENDANCE_PATH constant and then keep the same
formatting for query parameters.
backend/src/main/java/org/sejongisc/backend/common/sse/SseService.java (2)

55-62: removeEmitter에서 경합 조건 가능성 (낮은 영향도)

list.isEmpty() 확인과 emitters.remove(channelId) 사이에 다른 스레드가 emitter를 추가할 수 있습니다. 실제로 SSE 클라이언트가 재연결하면 되므로 영향은 미미하지만, compute를 사용하면 원자적으로 처리할 수 있습니다.

🔧 원자적 제거 방식
     public void removeEmitter(String channelId, SseEmitter emitter) {
-        CopyOnWriteArrayList<SseEmitter> list = emitters.get(channelId);
-        if (list != null) {
-            list.remove(emitter);
-            if (list.isEmpty()) {
-                emitters.remove(channelId);
-            }
-        }
+        emitters.computeIfPresent(channelId, (key, list) -> {
+            list.remove(emitter);
+            return list.isEmpty() ? null : list;
+        });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/sejongisc/backend/common/sse/SseService.java`
around lines 55 - 62, The removeEmitter method has a race between list.isEmpty()
and emitters.remove(channelId); change it to perform the remove-and-maybe-delete
atomically using ConcurrentMap.compute/computeIfPresent on emitters: inside
removeEmitter use emitters.compute(channelId, (k, list) -> { if list==null
return null; remove the provided SseEmitter from list; return list.isEmpty() ?
null : list; }) so the removal and channel cleanup happen in one atomic
operation on the map.

43-45: 예외 발생 시 로깅 추가를 권장합니다.

send() 메서드에서 예외 발생 시 로깅 없이 emitter만 제거하고 있습니다. 디버깅을 위해 경고 레벨 로그를 추가하는 것이 좋습니다.

🔧 제안된 수정사항
             } catch (Exception e) {
+                log.warn("SSE 전송 실패: channelId={}, eventName={}", channelId, eventName, e);
                 removeEmitter(channelId, emitter);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/sejongisc/backend/common/sse/SseService.java`
around lines 43 - 45, send()의 catch (Exception e) 블록에서 단순히
removeEmitter(channelId, emitter)만 호출하고 예외를 기록하지 않으므로 디버깅이 어렵습니다; 해당 블록에
SseService의 로거(예: private static final Logger log 또는 log 변수)를 사용해 channelId와 관련
정보(가능하면 emitter 식별자)를 포함한 경고 수준 로그를 남기고, 예외 객체(e)를 함께 전달하여 스택트레이스가 기록되도록
변경하세요(즉, send()의 catch문에서 먼저 log.warn("...", channelId, ..., e) 호출 후
removeEmitter(channelId, emitter) 호출).
backend/src/main/java/org/sejongisc/backend/activity/listener/ActivityEventListener.java (1)

20-35: 비동기 이벤트 처리에 에러 핸들링 추가를 권장합니다.

@Async 실행 시 예외가 발생하면 호출자에게 전파되지 않고 조용히 실패합니다. 로그 저장이나 SSE 전송 실패 시 최소한 로깅을 통해 문제를 추적할 수 있도록 에러 핸들링을 추가하는 것이 좋습니다.

♻️ 에러 핸들링 추가 제안
+import lombok.extern.slf4j.Slf4j;
+
 `@Component`
 `@RequiredArgsConstructor`
+@Slf4j
 public class ActivityEventListener {

     private final ActivityLogRepository activityLogRepository;
     private final SseService sseService; // 실시간 전송용 서비스

     `@Async`
     `@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT)
     public void handleActivityEvent(ActivityEvent event) {
+        try {
             // DB 저장 (마이페이지 및 관리자 통계용)
             ActivityLog log = activityLogRepository.save(ActivityLog.builder()
                     .userId(event.userId())
                     .username(event.username())
                     .type(event.type())
                     .message(event.message())
                     .targetId(event.targetId())
                     .boardName(event.boardName())
                     .build());

             // 관리자 채널에 실시간 SSE 전송 (메인 대시보드 피드용)
             sseService.send("ADMIN_DASHBOARD", "newLog", log);
+        } catch (Exception e) {
+            log.error("활동 로그 처리 실패: userId={}, type={}", event.userId(), event.type(), e);
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/activity/listener/ActivityEventListener.java`
around lines 20 - 35, Wrap the body of ActivityEventListener.handleActivityEvent
in a try-catch that catches Exception, so failures during
activityLogRepository.save(...) or sseService.send(...) are not silently
swallowed by `@Async`; on catch, call the class logger (e.g., logger.error) with a
clear message including identifying event data (event.userId(), event.type(),
event.targetId(), event.boardName()) and the exception, and optionally record a
metric or increment a failure counter — do not change transaction semantics,
just ensure errors are logged for diagnosis.
backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java (1)

27-30: 게시판 활동 집계 쿼리에서도 동일한 enum 리터럴 이슈

'BOARD_POST', 'BOARD_COMMENT', 'BOARD_LIKE' 문자열 리터럴 사용에 동일한 주의가 필요합니다. 대용량 데이터에서 성능을 위해 createdAttype 컬럼에 복합 인덱스 추가를 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java`
around lines 27 - 30, The JPQL in ActivityLogRepository uses hard-coded enum
string literals ('BOARD_POST','BOARD_COMMENT','BOARD_LIKE') which is brittle;
change the query to use a parameterized enum list (e.g., a :types parameter of
List<ActivityType>) or use fully-qualified enum literals instead, updating the
repository method signature to accept List<ActivityType> and bind it to :types
in the `@Query` on ActivityLogRepository; additionally, add a composite DB index
on (type, created_at) (or createdAt/type depending on your DB) to improve
performance for large datasets when filtering by type and date.
backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java (1)

42-42: createdAt 필드에 @Column 어노테이션 추가 고려.

다른 필드들과 달리 createdAt에는 @Column 어노테이션이 없습니다. 일관성을 위해 추가하거나, Hibernate의 @CreationTimestamp를 사용하면 생성자에서 직접 설정하지 않아도 됩니다.

옵션 1: `@Column` 추가
+    `@Column`(nullable = false, updatable = false)
     private LocalDateTime createdAt;
옵션 2: `@CreationTimestamp` 사용
+import org.hibernate.annotations.CreationTimestamp;
+    `@CreationTimestamp`
+    `@Column`(nullable = false, updatable = false)
     private LocalDateTime createdAt;

이 경우 생성자에서 this.createdAt = LocalDateTime.now(); 라인을 제거할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java`
at line 42, The createdAt field in ActivityLog lacks column/configuration
annotations; update ActivityLog to either (A) add `@Column` on the private
LocalDateTime createdAt field to match other fields, or (B) prefer using
Hibernate's `@CreationTimestamp` on createdAt and remove the manual initialization
(this.createdAt = LocalDateTime.now()) from the ActivityLog constructor so
creation time is set automatically. Ensure imports for javax.persistence.Column
or org.hibernate.annotations.CreationTimestamp are added and that the field
remains private LocalDateTime createdAt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java`:
- Around line 35-36: The message field in ActivityLog is currently
`@Column`(nullable = false) which defaults to VARCHAR(255); decide whether
messages may exceed 255 chars and update the mapping accordingly: either set an
explicit length (e.g., add length=512 or another appropriate size) on the
message column or switch to a text-capable mapping (e.g., use `@Lob` or
columnDefinition="TEXT") to allow longer content; update the ActivityLog.message
annotation to the chosen option so the DB schema reflects the intended max
length.

In
`@backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java`:
- Around line 131-134: The call to config.setAllowedOriginPatterns(List.of(...))
can throw NPE if env.getProperty("app.frontend-url") returns null; change the
code that builds the allowed-origin list used by config.setAllowedOriginPatterns
to omit null values (e.g., collect only non-null/blank entries or provide a
default) so that the list contains no null elements; update the code where
env.getProperty("app.frontend-url") is referenced (the expression passed into
config.setAllowedOriginPatterns) to filter or conditionally add the property
value before calling config.setAllowedOriginPatterns (for example, build the
list via a stream or conditional add and then pass the resulting non-null list).

In `@backend/src/main/java/org/sejongisc/backend/user/service/UserService.java`:
- Around line 85-90: The ActivityEvent for user signup is being published with
ActivityType.ATTENDANCE which is semantically incorrect; update the event to use
a SIGNUP activity type (replace ActivityType.ATTENDANCE with ActivityType.SIGNUP
in the publishEvent call in UserService where ActivityEvent is constructed) and
if ActivityType.SIGNUP does not exist, add SIGNUP to the ActivityType enum so
the new type can be used for signup events and statistics.

---

Outside diff comments:
In
`@backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java`:
- Around line 70-77: The test fails because postUserBet(...) is never invoked
and the wrong overload is used; uncomment and call bettingService.postUserBet
with the correct three-argument signature (userId, username, userBetRequest)
inside the assertThatThrownBy so the RuntimeException thrown by the mocked
userBetRepository.save triggers and the verifications run; ensure the
assertThatThrownBy wraps bettingService.postUserBet(userId, username, req) and
then verify pointHistoryRepository.save(...) and userBetRepository.save(...)
were each called once.

---

Nitpick comments:
In
`@backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java`:
- Line 42: The createdAt field in ActivityLog lacks column/configuration
annotations; update ActivityLog to either (A) add `@Column` on the private
LocalDateTime createdAt field to match other fields, or (B) prefer using
Hibernate's `@CreationTimestamp` on createdAt and remove the manual initialization
(this.createdAt = LocalDateTime.now()) from the ActivityLog constructor so
creation time is set automatically. Ensure imports for javax.persistence.Column
or org.hibernate.annotations.CreationTimestamp are added and that the field
remains private LocalDateTime createdAt.

In
`@backend/src/main/java/org/sejongisc/backend/activity/listener/ActivityEventListener.java`:
- Around line 20-35: Wrap the body of ActivityEventListener.handleActivityEvent
in a try-catch that catches Exception, so failures during
activityLogRepository.save(...) or sseService.send(...) are not silently
swallowed by `@Async`; on catch, call the class logger (e.g., logger.error) with a
clear message including identifying event data (event.userId(), event.type(),
event.targetId(), event.boardName()) and the exception, and optionally record a
metric or increment a failure counter — do not change transaction semantics,
just ensure errors are logged for diagnosis.

In
`@backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java`:
- Around line 27-30: The JPQL in ActivityLogRepository uses hard-coded enum
string literals ('BOARD_POST','BOARD_COMMENT','BOARD_LIKE') which is brittle;
change the query to use a parameterized enum list (e.g., a :types parameter of
List<ActivityType>) or use fully-qualified enum literals instead, updating the
repository method signature to accept List<ActivityType> and bind it to :types
in the `@Query` on ActivityLogRepository; additionally, add a composite DB index
on (type, created_at) (or createdAt/type depending on your DB) to improve
performance for large datasets when filtering by type and date.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java`:
- Around line 107-108: The URL builder in QrTokenStreamService that does return
String.format("%s%s?roundId=%s&token=%s", baseUrl, ATTENDANCE_PATH, roundId,
token) can produce a double slash if app.frontend-url has a trailing slash;
normalize the base and path before joining by trimming any trailing '/' from
frontendUrl (or ensure ATTENDANCE_PATH is normalized to start with a single
'/'), or use a standard join/resolve approach (e.g., URI.resolve) to ensure
exactly one separator between base and ATTENDANCE_PATH; update the code that
sets baseUrl and/or the ATTENDANCE_PATH constant and then keep the same
formatting for query parameters.

In `@backend/src/main/java/org/sejongisc/backend/common/sse/SseService.java`:
- Around line 55-62: The removeEmitter method has a race between list.isEmpty()
and emitters.remove(channelId); change it to perform the remove-and-maybe-delete
atomically using ConcurrentMap.compute/computeIfPresent on emitters: inside
removeEmitter use emitters.compute(channelId, (k, list) -> { if list==null
return null; remove the provided SseEmitter from list; return list.isEmpty() ?
null : list; }) so the removal and channel cleanup happen in one atomic
operation on the map.
- Around line 43-45: send()의 catch (Exception e) 블록에서 단순히
removeEmitter(channelId, emitter)만 호출하고 예외를 기록하지 않으므로 디버깅이 어렵습니다; 해당 블록에
SseService의 로거(예: private static final Logger log 또는 log 변수)를 사용해 channelId와 관련
정보(가능하면 emitter 식별자)를 포함한 경고 수준 로그를 남기고, 예외 객체(e)를 함께 전달하여 스택트레이스가 기록되도록
변경하세요(즉, send()의 catch문에서 먼저 log.warn("...", channelId, ..., e) 호출 후
removeEmitter(channelId, emitter) 호출).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bacf07 and 343c592.

📒 Files selected for processing (22)
  • .github/workflows/deploy.yml
  • backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java
  • backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityType.java
  • backend/src/main/java/org/sejongisc/backend/activity/event/ActivityEvent.java
  • backend/src/main/java/org/sejongisc/backend/activity/listener/ActivityEventListener.java
  • backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java
  • backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java
  • backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java
  • backend/src/main/java/org/sejongisc/backend/attendance/service/QrTokenStreamService.java
  • backend/src/main/java/org/sejongisc/backend/betting/controller/BettingController.java
  • backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java
  • backend/src/main/java/org/sejongisc/backend/board/repository/PostRepository.java
  • backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java
  • backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java
  • backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java
  • backend/src/main/java/org/sejongisc/backend/common/config/db/PrimaryDataSourceConfig.java
  • backend/src/main/java/org/sejongisc/backend/common/config/db/StockDataSourceConfig.java
  • backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java
  • backend/src/main/java/org/sejongisc/backend/common/sse/SseService.java
  • backend/src/main/java/org/sejongisc/backend/user/service/UserService.java
  • backend/src/main/resources/application-prod.yml
  • backend/src/test/java/org/sejongisc/backend/betting/service/BettingServiceTransactionalTest.java

oauth2를 더이상 쓰지 않으므로 변경
targetId String -> UUID로 변경
회원가입 시 출석 type 사용 오류 수정
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

🧹 Nitpick comments (4)
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java (1)

44-44: checkInusername 인자는 제거해도 됩니다.

Line 53에서 userRef를 이미 확보하므로, 이벤트 메시지는 userRef.getName()으로 구성하면 중복 파라미터 없이 같은 결과를 얻고 호출부 결합도도 낮출 수 있습니다.

Also applies to: 53-53, 74-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java`
at line 44, Remove the redundant username parameter from
AttendanceService.checkIn and compose any event/message using the resolved user
reference instead: after obtaining userRef (userRef variable at the start of
checkIn), replace uses of the removed username with userRef.getName() when
building event payloads/messages; update the method signature (remove String
username), adjust all internal references inside checkIn (e.g., event creation
around line where userRef is used) and update all callers to pass only the UUID
(or to call the new signature), ensuring compile-time fixes across the codebase.
backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java (1)

75-84: 게시글 생성 이벤트는 첨부파일 처리 완료 후 발행하는 편이 의미적으로 더 정확합니다.

현재는 게시글 저장 직후 이벤트를 먼저 발행합니다. “게시글 작성 완료”를 첨부파일까지 포함한 단위로 본다면, 파일 저장 성공 이후로 이벤트 시점을 이동하는 구성이 더 일관적입니다.

Also applies to: 86-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java`
around lines 75 - 84, The ActivityEvent is being published immediately after
postRepository.save(post) in PostServiceImpl (via
eventPublisher.publishEvent(new ActivityEvent(...))), but it should be emitted
only after attachment processing completes; move the publishEvent call so it
runs after successful file/attachment save logic (the same change must be
applied to the other occurrence referenced around lines 86-103), using the saved
Post and board data (post.getPostId(), board.getBoardName(), user.getName()) to
populate the ActivityEvent and ensuring any attachment failures prevent or
rollback the event emission.
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (1)

147-147: username을 파라미터로 전달받기보다 서비스 내부에서 일관되게 결정하는 편이 안전합니다.

userId를 이미 받고 있으므로 활동 로그용 사용자명도 같은 소스(사용자 엔티티/인증 컨텍스트)에서 조회해 기록하면, 호출부 의존과 데이터 불일치 가능성을 줄일 수 있습니다.

Also applies to: 209-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java`
at line 147, Change postUserBet to stop accepting the external username and
instead resolve the username inside the service using the user source tied to
userId: update the method signature postUserBet(UUID userId, UserBetRequest
userBetRequest) (remove the String username param) and, inside postUserBet, load
the User entity (e.g., via userRepository.findById(userId) or your
authentication/context service) to obtain the canonical username for logging and
activity creation; also update any related internal calls (the other affected
block around lines 209-213) to pass only userId and UserBetRequest and to use
the same resolved username when creating activity logs or UserBetResponse so
there is no mismatch between caller-supplied and persisted usernames.
backend/src/main/java/org/sejongisc/backend/activity/event/ActivityEvent.java (1)

7-14: ActivityEvent 생성 실수를 줄이려면 타입별 팩토리 메서드가 더 안전합니다.

현재처럼 nullable 필드를 포함한 위치 기반 record 생성은 호출부에서 인자 순서/null 위치 실수가 나기 쉽습니다. forAttendance(...), forBoardLike(...) 같은 정적 팩토리를 두면 유지보수성이 좋아집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/org/sejongisc/backend/activity/event/ActivityEvent.java`
around lines 7 - 14, Add type-specific static factory methods to the
ActivityEvent record (e.g., forAttendance(...), forBoardLike(...),
forBoardComment(...)) that accept only the fields relevant to that ActivityType
and return a properly populated ActivityEvent (setting unused fields to null).
Use ActivityType enum values inside these methods and perform null checks on
required args (userId, username, type-specific ids) so callers cannot
accidentally swap positional parameters; keep the canonical record signature but
prefer callers use these factories like ActivityEvent.forAttendance(userId,
username, message) or ActivityEvent.forBoardLike(userId, username, targetId,
boardName).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java`:
- Around line 88-95: The `@Retryable` methods createComment and toggleLike call
eventPublisher.publishEvent which can throw non-retryable exceptions (e.g.,
EventPublicationFailedException) and Spring Retry will look for a matching
`@Recover` method; add either a `@Recover` method in PostInteractionService to
handle those non-retryable exceptions (e.g., a recover method signature
accepting Throwable or the specific exception and the same parameters as the
retried method) or wrap the eventPublisher.publishEvent call in a try-catch
inside createComment and toggleLike to log/handle publication failures
separately so a missing `@Recover` does not trigger "Cannot locate recovery
method" errors.

---

Nitpick comments:
In
`@backend/src/main/java/org/sejongisc/backend/activity/event/ActivityEvent.java`:
- Around line 7-14: Add type-specific static factory methods to the
ActivityEvent record (e.g., forAttendance(...), forBoardLike(...),
forBoardComment(...)) that accept only the fields relevant to that ActivityType
and return a properly populated ActivityEvent (setting unused fields to null).
Use ActivityType enum values inside these methods and perform null checks on
required args (userId, username, type-specific ids) so callers cannot
accidentally swap positional parameters; keep the canonical record signature but
prefer callers use these factories like ActivityEvent.forAttendance(userId,
username, message) or ActivityEvent.forBoardLike(userId, username, targetId,
boardName).

In
`@backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java`:
- Line 44: Remove the redundant username parameter from
AttendanceService.checkIn and compose any event/message using the resolved user
reference instead: after obtaining userRef (userRef variable at the start of
checkIn), replace uses of the removed username with userRef.getName() when
building event payloads/messages; update the method signature (remove String
username), adjust all internal references inside checkIn (e.g., event creation
around line where userRef is used) and update all callers to pass only the UUID
(or to call the new signature), ensuring compile-time fixes across the codebase.

In
`@backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java`:
- Line 147: Change postUserBet to stop accepting the external username and
instead resolve the username inside the service using the user source tied to
userId: update the method signature postUserBet(UUID userId, UserBetRequest
userBetRequest) (remove the String username param) and, inside postUserBet, load
the User entity (e.g., via userRepository.findById(userId) or your
authentication/context service) to obtain the canonical username for logging and
activity creation; also update any related internal calls (the other affected
block around lines 209-213) to pass only userId and UserBetRequest and to use
the same resolved username when creating activity logs or UserBetResponse so
there is no mismatch between caller-supplied and persisted usernames.

In
`@backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java`:
- Around line 75-84: The ActivityEvent is being published immediately after
postRepository.save(post) in PostServiceImpl (via
eventPublisher.publishEvent(new ActivityEvent(...))), but it should be emitted
only after attachment processing completes; move the publishEvent call so it
runs after successful file/attachment save logic (the same change must be
applied to the other occurrence referenced around lines 86-103), using the saved
Post and board data (post.getPostId(), board.getBoardName(), user.getName()) to
populate the ActivityEvent and ensuring any attachment failures prevent or
rollback the event emission.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 343c592 and ddded86.

📒 Files selected for processing (9)
  • backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java
  • backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityType.java
  • backend/src/main/java/org/sejongisc/backend/activity/event/ActivityEvent.java
  • backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java
  • backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java
  • backend/src/main/java/org/sejongisc/backend/board/service/PostInteractionService.java
  • backend/src/main/java/org/sejongisc/backend/board/service/PostServiceImpl.java
  • backend/src/main/java/org/sejongisc/backend/common/config/security/SecurityConfig.java
  • backend/src/main/java/org/sejongisc/backend/user/service/UserService.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityLog.java
  • backend/src/main/java/org/sejongisc/backend/activity/entity/ActivityType.java
  • backend/src/main/java/org/sejongisc/backend/user/service/UserService.java

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant