feat: 채팅 메시지 검색 결과에서 특정 메시지 시점으로 페이지 이동 기능 추가#533
Conversation
- 검색 응답(ChatMessageMatchResult)에 matchedMessageId 필드를 추가하여 프론트엔드에서 검색된 메시지를 특정할 수 있도록 함 - ChatMessageRepository에 findByIdWithChatRoom, countNewerMessagesByChatRoomId 쿼리를 추가하고 findByChatRoomId의 ORDER BY에 id DESC tie-breaker를 적용하여 동일 createdAt 메시지 간 결정론적 정렬을 보장 - ChatService.getMessages에 messageId 오버로드를 추가하여, messageId가 제공되면 해당 메시지가 포함된 페이지를 자동 계산 (page = newerCount / limit + 1) - 권한 오라클 방지를 위해 messageId resolution 전에 ensureMessageLookupAccess로 방 접근 권한을 사전 검증하고, 권한 없음 시 NOT_FOUND_CHAT_ROOM으로 통일 - visibleMessageFrom 경계 조건(createdAt == visibleMessageFrom)에서 가드와 쿼리의 경계 규칙이 일치하도록 isBefore를 !isAfter로 수정 - ChatApi/ChatController에 messageId optional 파라미터를 추가하고 Swagger 문서화 - 8개 단위 테스트 추가 (null messageId 호환성, 미존재/타 방 메시지 거부, 페이지 계산, 가시성 범위 밖 거부, 최신 메시지 page=1, 비회원 오라클 방지, visibleMessageFrom 경계)
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 3 minutes and 31 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough선택적 Changes
Sequence DiagramsequenceDiagram
participant Client
participant ChatApi
participant ChatController
participant ChatService
participant ChatRepository
participant Database
Client->>ChatApi: GET /chats/rooms/{roomId}?messageId=123
ChatApi->>ChatController: getChatRoomMessages(userId, roomId, page, limit, messageId)
ChatController->>ChatService: getMessages(userId, roomId, page, limit, messageId)
alt messageId 제공
ChatService->>ChatRepository: findByIdWithChatRoom(messageId)
ChatRepository->>Database: SELECT message JOIN chatRoom WHERE id=123
Database-->>ChatRepository: ChatMessage
ChatRepository-->>ChatService: Optional<ChatMessage>
ChatService->>ChatService: 가시성/멤버십 검증 (resolveVisibleMessageFromPure)
ChatService->>ChatRepository: countNewerMessagesByChatRoomId(roomId, messageId, createdAt, visibleMessageFrom)
ChatRepository->>Database: COUNT newer messages (createdAt,id cursor)
Database-->>ChatRepository: long count
ChatRepository-->>ChatService: count
ChatService->>ChatService: 페이지 계산 및 PageRequest 생성
else messageId 없음
ChatService->>ChatService: 제공된 page 사용
end
ChatService->>ChatRepository: findByChatRoomId(roomId, visibleMessageFrom, pageable)
ChatRepository->>Database: SELECT messages (ORDER BY createdAt DESC, id DESC)
Database-->>ChatRepository: Page<ChatMessage>
ChatRepository-->>ChatService: messages
ChatService-->>ChatController: ChatMessagePageResponse
ChatController-->>ChatApi: ResponseEntity.ok(response)
ChatApi-->>Client: 200 OK with messages
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
🧪 JaCoCo Coverage Report (Changed Files)Summary
Coverage by File
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java`:
- Around line 142-147: Update the Swagger doc in ChatApi for the endpoint that
accepts messageId (the controller method in ChatApi.java handling message
pagination/search) to state that when messageId is provided the service returns
NOT_FOUND_CHAT_ROOM (404) not only for invalid IDs but also when the caller
lacks access or the message is hidden by visibility boundaries; modify the error
description lines for NOT_FOUND_CHAT_ROOM and FORBIDDEN_CHAT_ROOM so they
explicitly note that access-denied or visibility-boundary violations for
messageId requests are mapped to 404, and ensure the documentation text around
the messageId behavior reflects this unified 404 response.
In `@src/main/java/gg/agit/konect/domain/chat/service/ChatService.java`:
- Around line 1457-1462: The current catch in the club room branch blindly
converts any CustomException from clubMemberRepository.getByClubIdAndUserId(...)
into NOT_FOUND_CHAT_ROOM; change it to only translate the expected
"membership-miss" error code (inspect the thrown CustomException's error/code
field) into throw CustomException.of(NOT_FOUND_CHAT_ROOM) and rethrow any other
CustomException unchanged so non-membership errors are propagated; keep the
surrounding logic in the room.isClubGroupRoom() branch and use the exact symbols
clubMemberRepository.getByClubIdAndUserId, CustomException, and
NOT_FOUND_CHAT_ROOM when implementing the conditional mapping.
- Around line 1491-1496: The page-calculation uses
chatMessageRepository.countNewerMessagesByChatRoomId(...) and then a separate
fetch, so concurrent inserts can make the computed page miss the target message;
change ChatService to perform the count+fetch in a single DB snapshot (single
SQL or inside one transactional/select-for-update style query) or, if you cannot
atomically compute both, detect after fetching the page whether the target
messageId is present and then recompute the page (re-run
countNewerMessagesByChatRoomId with the same parameters: roomId, messageId,
targetMessage.getCreatedAt(), visibleMessageFrom, limit) until the target is
included; ensure this logic lives alongside the existing code that uses limit
and newerCount to compute page so callers (e.g., methods referencing messageId
and targetMessage.getCreatedAt()) always receive a page that contains the target
message.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 57db7759-1ce1-47ae-b2dc-861fc8881e4b
📒 Files selected for processing (6)
src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.javasrc/main/java/gg/agit/konect/domain/chat/controller/ChatController.javasrc/main/java/gg/agit/konect/domain/chat/dto/ChatMessageMatchResult.javasrc/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.javasrc/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java
📜 Review details
⏰ 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). (2)
- GitHub Check: coverage
- GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageMatchResult.javasrc/main/java/gg/agit/konect/domain/chat/controller/ChatController.javasrc/main/java/gg/agit/konect/domain/chat/controller/ChatApi.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.javasrc/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageMatchResult.javasrc/main/java/gg/agit/konect/domain/chat/controller/ChatController.javasrc/main/java/gg/agit/konect/domain/chat/controller/ChatApi.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.javasrc/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.javasrc/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java
- getMessagesWithMessageIdRejectsNonMemberWithNotFound 테스트에서 ChatMessage message 변수가 생성 후 참조되지 않아 제거 - message 제거로 함께 미사용이 된 sender, memberId 변수도 정리
- 기존에는 messageId가 유효하지 않은 경우만 404로 설명했으나, 실제로는 접근 권한 없음과 가시성 경계 위반도 404로 통일 응답됨 - FORBIDDEN_CHAT_ROOM_ACCESS 설명에 messageId 없는 일반 조회 조건 명시 - NOT_FOUND_CHAT_ROOM 설명에 404 통일 응답 사유 구체화
- 기존에는 clubMemberRepository에서 발생하는 모든 CustomException을 NOT_FOUND_CHAT_ROOM으로 변환했음 - 추후 다른 비즈니스 예외가 추가될 때 원인이 404로 덮이는 것을 방지하기 위해 NOT_FOUND_CLUB_MEMBER 에러 코드만 선별 변환하고 나머지는 그대로 전파
- 기존에는 count와 fetch 사이 새 메시지 삽입 시 타겟 메시지가 응답 페이지에서 빠질 수 있었음 - 방 타입별 dispatch를 fetchMessagesByRoomType으로 추출하고, getMessages에서 응답에 타겟 메시지가 없으면 1회 재계산 후 재시도 - updateLastReadAt, recordPresenceSafely는 멱등성이 있어 재시도 시에도 안전
- 기존 테스트에서 mock 페이지 응답에 타겟 메시지(id=50)가 빠져 있어 재시도 로직이 트리거되어 countNewerMessagesByChatRoomId가 2회 호출됨 - mock 응답에 targetMessage를 포함하여 재시도 없이 정상 흐름 검증
- REPEATABLE READ 격리 수준에서 같은 트랜잭션 내 count 재실행은 동일한 스냅샷을 보므로 재시도가 의도한 효과를 내지 못함 - fetchMessagesByRoomType 추출도 함께 되돌려 원래 구조로 복원 - 검색 메시지 이동 UX에서 1페이지 오차는 허용 가능함을 기존 NOTE에 명시
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/gg/agit/konect/domain/chat/service/ChatService.java`:
- Around line 408-418: The retry uses the original readAt timestamp so
subsequent fetchMessagesByRoomType call still uses the old time; update readAt
before the retry (recompute LocalDateTime readAt = LocalDateTime.now()) or
alternatively change the flow so you only perform the read/mark-as-read logic
once after page is resolved (i.e., call resolvePageForMessage(...) then compute
a fresh readAt and call fetchMessagesByRoomType(...) and the read-update/isRead
logic), referencing the variables/methods messageId, readAt,
fetchMessagesByRoomType, resolvePageForMessage and response to locate and adjust
the logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 238204f9-4c3a-4b96-9a91-14d6ece48cc6
📒 Files selected for processing (3)
src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.javasrc/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java
📜 Review details
⏰ 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). (2)
- GitHub Check: coverage
- GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.java
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.javasrc/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.java
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/gg/agit/konect/domain/chat/service/ChatService.java`:
- Around line 1478-1500: Validate limit > 0 at the start of
resolvePageForMessage (or in getMessages before calling it) to avoid dividing by
zero; specifically, in resolvePageForMessage(Integer roomId, Integer messageId,
ChatRoom room, User user, int limit) check that limit is positive and throw the
same controlled validation exception used elsewhere for invalid paging
parameters (so callers see the same error flow), before any computation that
uses limit (notably before computing (int)(newerCount / limit) + 1).
In `@src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java`:
- Around line 1166-1203: The tests
getMessagesWithMessageIdCalculatesCorrectPageInGroupRoom() and
getMessagesWithMessageIdReturnsPage1ForNewestMessage() only assert
response.currentPage() and do not verify that the returned page actually
contains the targetMessage; update these tests to include the targetMessage in
the stubbed Page returned by chatMessageRepository.findByChatRoomId(...) and add
an assertion on response.messages() (or response.messages().stream() check) to
ensure the message with id 50 (targetMessage) is present; reference the existing
targetMessage variable, chatService.getMessages(...) call, and
response.messages() when making the assertions so the test validates the
contract that the calculated page includes the requested messageId.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5118a504-4163-4f0f-8b08-671dd5c34713
📒 Files selected for processing (2)
src/main/java/gg/agit/konect/domain/chat/service/ChatService.javasrc/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java
📜 Review details
⏰ 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). (2)
- GitHub Check: coverage
- GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (2)
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.java
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/domain/chat/service/ChatService.java
🧠 Learnings (1)
📚 Learning: 2026-04-13T00:26:21.345Z
Learnt from: dh2906
Repo: BCSDLab/KONECT_BACK_END PR: 533
File: src/main/java/gg/agit/konect/domain/chat/service/ChatService.java:1511-1516
Timestamp: 2026-04-13T00:26:21.345Z
Learning: In ChatService.java (Spring Boot + JPA, MySQL InnoDB), within a `Transactional(readOnly = true)` method, retrying a repository count query (e.g., `countNewerMessagesByChatRoomId`) to handle concurrent inserts is ineffective under REPEATABLE READ isolation: the same DB snapshot is used throughout the transaction, so the retry always returns the same result. A new transaction (`Propagation.REQUIRES_NEW`) would be required for a true retry, but accepting a 1-page offset as a UX tradeoff is preferred for search navigation in this codebase.
Applied to files:
src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.javasrc/main/java/gg/agit/konect/domain/chat/service/ChatService.java
- resolvePageForMessage() 초입에 limit <= 0 검증을 추가하여 ArithmeticException(500) 대신 IllegalArgumentException으로 제어 - getMessagesWithMessageIdCalculatesCorrectPageInGroupRoom 테스트의 mock 응답에 targetMessage를 포함하고 messageId 존재 단언 추가 - getMessagesWithMessageIdReturnsPage1ForNewestMessage 테스트에 응답에 타겟 메시지가 포함되었는지 검증하는 단언 추가
- ChatApi의 @min(1) 검증이 limit=0을 400으로 차단하므로 resolvePageForMessage의 중복 검증을 제거 - 예외 타입 불일치(IllegalArgumentException vs CustomException) 문제도 해결
* fix: Sheet API 요청자 접근 권한 검증 추가 * fix: integrated 시트 요청자 접근 권한 검증 추가 * fix: integrated 시트 권한 검증 보완 * fix: integrated 시트 서비스 계정 접근 재검증 추가 * fix: integrated 시트 Drive OAuth 미연결 우회 차단 * test: integrated 시트 OAuth 미연결 중단 검증 추가 * test: integrated 시트 검증 테스트 중복 정리 * test: 그룹 채팅방 생성 및 멤버 강퇴 테스트 추가 (#482) * test: 그룹 채팅방 생성 및 멤버 강퇴 테스트 추가 * test: 그룹 채팅방 메시지 전송 및 중복 생성 테스트 추가 * chore: 코드 포맷팅 * refactor: 그룹 채팅방 생성 테스트 개선 * test: 강퇴된 멤버 메시지 전송 테스트 변수 정리 * test: 강퇴된 멤버 메시지 조회 테스트 추가 * test: 채팅 조회 권한 케이스를 커밋된 상태 기준으로 검증 * fix: getChatRooms에서 GROUP 채팅방 누락 버그 수정 (#497) * fix: 어드민 유저가 채팅방을 나가도 목록에 조회되는 문제 해결 (#498) * fix: 채팅방 목록 조회에서 채팅방을 나간 사용자 제외 조건 추가 * fix: 관리자 채팅방 나가기 시 목록에서 필터링 안되는 버그 수정 * fix: 어드민이 문의 채팅방 나가고 다시 메시지가 오면 못보는 문제 해결 (#499) * fix: findAdminChatRoomsOptimized 쿼리의 가시성 조건을 수정 * test: 테스트 추가 (ChatApiTest.java) - `@BeforeEach` 수정: System Admin(ID=1)을 먼저 생성 - `createAdminChatRoomAndGetRoomsSuccess` 테스트 수정 - `adminLeftInquiryRoomReappearsWhenUserSendsNewMessage` 테스트 추가: 어드민이 나간 문의 채팅방에 사용자가 새 메시지를 보내 어드민 목록에 다시 노출되는 시나리오 검증 * chore: 코드 포맷팅 * test: System Admin ID 상수화 및 관련 케이스 수정 * fix: 동아리 지원 거절 알림을 AFTER_COMMIT 이벤트 기반으로 변경 (#500) * fix: 동아리 지원 거절 알림을 AFTER_COMMIT 이벤트 기반으로 변경 거절 알림이 트랜잭션 커밋 전에 비동기로 발송되어 롤백 시에도 알림이 전송될 수 있는 문제 해결. - ClubApplicationRejectedEvent 신규 생성 - ClubApplicationNotificationListener에 AFTER_COMMIT 핸들러 추가 - ClubApplicationService에서 직접 호출 대신 이벤트 발행으로 변경 * chore: 코드 포맷팅 * fix: 어드민 계정으로도 부원 직책 변경할 수 있도록 수정 * test: 관리자 멤버 권한 변경 회귀 테스트 추가 * fix: 관리자 계정의 부원 권한 변경 허용 * refactor: 관리자 권한 변경 요청의 사용자 조회 재사용 * refactor: 동아리 채팅방 멤버십 Lazy 생성으로 변경 (#502) * refactor: 동아리 채팅방 멤버십 Lazy 생성으로 변경 ensureClubRoomMemberships()의 REQUIRES_NEW 트랜잭션이 매 API 호출마다 실행되어 커넥션 풀을 오래 점유하는 문제 해결 - 동아리 가입 시점에 이미 addClubMember()로 멤버십 생성됨 - 채팅방 목록 조회 시 ensureClubRoomMemberships() 호출 제거 - 사용하지 않는 ensureClubRoomMemberships() 및 resolveOrCreateClubRooms() 제거 * chore: 누락된 chat_room_member 데이터 마이그레이션 추가 과거 버그로 인해 club_member는 있지만 chat_room_member가 없는 데이터를 복구하는 Flyway 마이그레이션 추가 - club_member.created_at을 last_read_at으로 설정 * refactor: 시트 등록 시 권한 조건 제거 * refactor: 시트 접근 권한 제거 * fix: InOrder 검증 추가 * fix: 문의하기에서 기존 문의 채팅방이 아닌 새 채팅방이 생성되는 문제를 해결 (#504) * fix: 어드민이 문의 채팅방에 멤버로 추가되어 중복 생성되는 문제 해결 - ensureDirectRoomMemberExists에서 어드민이 SYSTEM_ADMIN 방에 멤버로 추가되지 않도록 수정 - findByTwoUsers는 채팅방 멤버가 정확히 2명인 경우만 찾음 - 어드민이 멤버로 추가되면 멤버가 3명이 되어 기존 방을 찾지 못하고 새 방이 생성됨 - 어드민은 메시지 조회/전송 권한만 허용하고 SYSTEM_ADMIN의 lastReadAt을 업데이트 - toggleMute에서도 어드민이 SYSTEM_ADMIN 방에 접근 가능하도록 수정 - 기존 데이터 정리용 마이그레이션 추가 (V68) * chore: 코드 포맷팅 * fix: SYSTEM_ADMIN 문의방 재사용을 위해 관리자 멤버 재추가를 차단 * fix: 문의방 관리자 답변 알림이 실제 사용자에게 전달되도록 수정 * fix: 관리자 문의방 조회가 SYSTEM_ADMIN 멤버 기준으로 동작하도록 분리 * fix: 관리자 문의 채팅방 조회와 읽음 처리를 SYSTEM_ADMIN 기준으로 분리 - direct 메시지 조회에서 관리자+SYSTEM_ADMIN 방이면 전용 조회 경로로 분기 - 관리자 문의방 조회 시 관리자 개인 멤버십을 만들지 않고 SYSTEM_ADMIN 멤버 기준으로 visibleMessageFrom과 read baseline 계산 - direct lastReadAt 갱신이 read-only 조회 트랜잭션에 합류하지 않도록 REQUIRES_NEW로 분리 - 관리자 문의방 조회 시 SYSTEM_ADMIN의 lastReadAt만 갱신하고 일반 direct 방은 기존 멤버십 검사 흐름 유지 - 읽기 권한과 멤버십 생성 정책이 충돌하지 않도록 조회 경로와 수정 경로의 책임을 분리 * chore: 코드 포맷팅 * fix: ADMIN 멤버 중복 생성 방지 SQL 쿼리 수정 * fix: Auth 어노테이션 인식 문제 해결 (#507) * fix: 인터페이스 @Auth 어노테이션 인식 문제 해결 * chore: 코드 포맷팅 * refactor: 채팅 서비스 코드 중복 제거 및 단순화 (#508) * refactor: user.isAdmin() 메서드 활용하여 어드민 체크 로직 단순화 - User.getRole() == UserRole.ADMIN 패턴을 user.isAdmin()으로 일괄 변경 - ChatService.java 16개 위치, ChatRoomMembershipService.java 2개 위치 수정 - 불필요한 UserRole import 제거 * refactor: SYSTEM_ADMIN_ID 상수 중복 제거 - ChatRoomMembershipService.SYSTEM_ADMIN_ID를 public으로 변경 - ChatService의 중복 상수 정의 제거 - ChatService에서 static import로 참조하도록 변경 * refactor: toSortedReadBaselines 메서드 단순화 - 불필요한 지역 변수 제거하고 바로 반환하도록 변경 * refactor: findNonAdminUser를 findNonAdminUserFromMemberInfo로 위임 - findNonAdminUser에서 MemberInfo로 변환 후 통합된 메서드 호출 - 중복된 필터링 로직 제거 * refactor: getDirectChatRoomMessages 메서드 통합 - buildDirectChatRoomMessages 공통 메서드 추출 - getDirectChatRoomMessages와 getAdminSystemDirectChatRoomMessages가 공통 메서드를 호출하도록 변경 - 중복된 메시지 조회 및 매핑 로직 제거 * refactor: resolveDirectMessageReceiver 통합 - resolveDirectMessageReceiver가 MemberInfo로 변환 후 resolveMessageReceiverFromMemberInfo 호출하도록 변경 - 중복된 수신자 결정 로직 제거 * chore: SYSTEM_ADMIN_ID 중복 import 제거 * refactor: buildDirectChatRoomMessages 미사용 파라미터 제거 - 사용하지 않는 ChatRoom chatRoom 파라미터 제거 - 관련 호출 지점 업데이트 * feat: 시트 부원 미리보기 API 추가 (#506) * feat: 인명부 미리보기 확정 등록 API 추가 * fix: 시트 미리보기 리뷰 반영 * fix: 요청 검증 메시지 중복 제거 * test: 채팅 API 엣지 케이스 통합 테스트 추가 (#509) * test: 채팅 API 엣지 케이스 통합 테스트 추가 * fix: 채팅방 나간 멤버 새 메시지 표시 안 됨 해결 * test: 트랜잭션 경계 내 테스트 픽스처 실행 로직 추가 * refactor: DB 타임스탬프 정밀도 상수 도입 및 적용 * test: 채팅 API 통합테스트의 컨텍스트 재로딩 대신 커밋 데이터 정리로 격리 * test: 한 글자 채팅 검색 테스트 이름을 실제 검증 범위에 맞게 정리 * fix: 채팅방 나간 멤버의 마지막 메시지 표시 오류 해결 * test: 채팅방 나가기 테스트에서 중복 상대 유저 생성을 제거 * feat: 지원 응답에 userId 필드 추가 및 매핑 작성 (#510) * feat: 동아리 사전 등록 회원 배치 등록 API 추가 (#512) * feat: 동아리 사전 등록 회원 배치 등록 API 추가 한 번에 여러 명의 회원을 사전 등록할 수 있는 배치 API를 추가합니다. - POST /clubs/{clubId}/pre-members/batch 엔드포인트 추가 - 1~50명까지 한 번에 등록 가능 (부분 성공 지원) - 개별 트랜잭션 처리로 일부 실패 시에도 나머지는 등록됨 - 등록 결과는 성공/실패 통계와 개별 상세 정보를 포함 주요 변경사항: - ClubPreMemberBatchAddRequest: 배치 요청 DTO - ClubPreMemberBatchAddResponse: 배치 응답 DTO - ClubPreMemberBatchResultItem: 개별 결과 DTO - ClubMemberManagementService.addPreMembersBatch(): 배치 등록 메서드 관련 테스트: - 통합 테스트 5개 케이스 추가 - 단위 테스트 2개 케이스 추가 * refactor: 배치 등록 코드 개선 - TransactionTemplate을 루프 외부에서 한 번만 생성하도록 변경 - processSinglePreMember() 중복 메서드 제거하고 기존 addPreMember() 재사용 - 테스트 파일에서 불필요한 print 임포트 제거 * fix: 배치 요청 DTO에 @NotNull 추가 members 필드에 @NotNull 어노테이션을 추가하여 null이나 필드 생략 시 400 에러를 반환하도록 수정 * fix: 배치 요청에 @Valid 및 요소 수준 @NotNull 추가 - @Valid 추가로 개별 회원 항목의 제약조건 검증 활성화 - List<@NotNull ClubPreMemberAddRequest>로 null 요소 방지 * test: 배치 등록 API 검증 테스트 추가 - null 리스트 요청 시 400 반환 테스트 - null 요소 포함 리스트 400 반환 테스트 - MediaType 임포트 및 mockMvc 직접 사용 방식으로 테스트 개선 * chore: 코드 포맷팅 * chore: 배치 등록 최대 인원을 50명에서 300명으로 확대 대규모 동아리의 일괄 회원 등록 요구사항을 반영하여 배치 등록 가능 인원을 확대함. * refactor: 불필요한 변수 제거 * feat: 누락된 `@Valid` 어노테이션 추가 * refactor: 배치 등록 로직 중복 제거 및 함수 분리 * fix: 중복되는 채팅 방 메시지 병합 (#511) * fix: DIRECT 채팅방 중복 병합 처리 SQL 추가 * fix: DIRECT 채팅방 중복 병합 처리 SQL 추가 * fix: V69 마이그레이션에서 하드코딩된 schema 선택 제거 - USE konect; 문장 삭제하여 runtime datasource 기반 동작하도록 수정 - 다양한 환경 및 권한 설정에서 정상 작동하도록 개선 * fix: DIRECT 채팅방 병합 시 메시지 존재하는 방 우선 선택 및 메타데이터 갱신 - ROW_NUMBER() 정렬 로직 수정: (last_message_at IS NOT NULL) DESC를 첫 번째 정렬 기준으로 추가하여 메시지가 있는 방을 우선 선택 - 병합 후 keep_room의 last_message_content와 last_message_sent_at을 실제 메시지 기반으로 재계산하는 단계 추가 - 빈 방이 winner로 선택되어 last_message_* 컬럼이 stale 상태로 남는 문제 방지 * fix: 채팅 메시지 이동 시 updated_at 타임스탬프 보존 - chat_message 테이블 UPDATE 시 ON UPDATE CURRENT_TIMESTAMP로 인해 updated_at이 자동 갱신되는 문제 방지 - updated_at = updated_at 명시적 설정으로 원본 타임스탬프 유지 * fix: 채팅방 병합 시 멤버십 상태 보존 로직 추가 - 삭제 대상 방 멤버십 삭제 전, visible_message_from과 left_at 값을 keep 방으로 병합하는 UPDATE 단계 추가 (step 4) - visible_message_from: 더 이른 값(더 많은 메시지 조회 가능) 선택 - left_at: 둘 중 하나라도 나간 경우 나간 것으로 처리 (더 이른 값 선택) - 기존 멤버십 데이터 손실 방지 및 step 번호 재정렬 (5-8) * fix: 채팅방 병합 시 누락된 사용자 상태 병합 추가 - last_read_at: GREATEST로 병합하여 더 나중 읽음 시점 보존 - custom_room_name: COALESCE로 사용자 설정 방 이름 보존 - notification_mute_setting: target_id를 keep_room_id로 업데이트 (이미 keep 방에 설정 있으면 UNIQUE 제약으로 자동 삭제됨) - step 번호 재정렬 (4-9) * fix: last_message_content 재계산 시 타임스탬프 동일 문제 해결 - 기존: MAX(created_at)만 사용하여 타임스탬프 동일 시 여러 행 반환 가능 - 개선: MAX(id)로 최신 메시지를 결정론적으로 선택 - 서브쿼리에서 chat_room_id별 MAX(id)를 먼저 계산 후 해당 id를 가진 메시지와 조인하여 단일 행 보장 * fix: V69 마이그레이션 재시도 가능하도록 개선 - temp_duplicate_room_map을 CREATE TABLE IF NOT EXISTS로 생성하여 재시도 시 기존 매핑 테이블 재사용 - 후보 방 선정 시 매핑 테이블에 이미 있는 방도 포함 (재시도 시 멤버 0명이 된 방도 처리 가능) - INSERT ... ON DUPLICATE KEY UPDATE로 매핑 idempotent 처리 - temp_direct_room_pairs는 매 실행 새로 생성하여 최신 상태 반영 * fix: 다중 loser 방 매핑 시 멤버십 상태 병합 충돌 해결 - 여러 from_room_id가 같은 keep_room_id로 매핑될 때 동일한 타겟 행에 대한 다중 업데이트 충돌 방지 - loser 멤버십을 (keep_room_id, user_id) 기준으로 집계하여 단일 UPDATE로 처리하도록 개선 * fix: V69 마이그레이션 재시도 시 매핑 테이블 보호 재시도 시 ON DUPLICATE KEY UPDATE로 인해 keep/from 방향이 뒤바뀔 수 있는 문제를 해결하기 위해: - 매핑 테이블이 비어있을 때만 INSERT 실행 (NOT EXISTS 조건 추가) - ON DUPLICATE KEY UPDATE 절 제거 이제 재시도 시 기존 매핑이 유지되어 병합 방향이 일관되게 유지됨. * fix: V69 마이그레이션 뮤트 설정 UNIQUE 충돌 해결 notification_mute_setting UPDATE 시 (user_id, target_type, target_id) UNIQUE 제약조건 위반 문제를 해결하기 위해: - UPDATE 전에 keep_room에 이미 존재하는 뮤트 설정을 먼저 삭제 - 동일 사용자가 from_room과 keep_room 모두 뮤트 설정 시 from_room 설정이 우선하도록 처리 이제 중복 키 충돌 없이 알림 뮤트 설정 병합 가능. * fix: V69 마이그레이션 모든 엣지케이스 처리 발견된 잠재적 문제 및 해결: 1. 연산자 우선순위 버그 (치명적) - WHERE a AND b OR c → WHERE a AND (b OR c) 로 괄호 추가 2. chat_room_member 데이터 유실 (치명적) - loser 방에만 있는 멤버 INSERT 추가 (4b단계) - is_owner 컬럼 병합 로직 추가 (누락되었었음) 3. notification_mute_setting UNIQUE 충돌 - UPDATE 전 keep 방의 충돌하는 row 먼저 삭제 4. 재시도 안정성 - NOT EXISTS + ON DUPLICATE KEY UPDATE 제거로 매핑 보호 5. 주석 및 문서화 - 엣지케이스 설명 추가 - 각 단계 목적 명확화 * fix: V69 마이그레이션 다중 loser 방 PK/UNIQUE 충돌 해결 Codex adversarial review에서 지적된 문제 해결: 1. chat_room_member orphan INSERT PK 충돌 - GROUP BY (keep_room_id, user_id)로 집계하여 중복 삽입 방지 - MAX/MIN으로 각 컬럼 병합 규칙 적용 2. notification_mute_setting UNIQUE 충돌 - GROUP BY로 loser 설정 집계 (temp_mute_setting_agg) - MAX(is_muted)로 동일 사용자의 여러 설정 병합 - DELETE-INSERT 패턴으로 충돌 없이 마이그레이션 이제 다중 loser 방 → 단일 keep 방 시나리오에서도 안전하게 마이그레이션 실행 가능. * fix: V69 마이그레이션 LEAST/GREATEST NULL 처리 버그 수정 MySQL의 LEAST/GREATEST는 인자 중 하나라도 NULL이면 결과도 NULL을 반환하는 문제 해결: - visible_message_from: CASE로 NULL 처리 후 LEAST - last_read_at: CASE로 NULL 처리 후 GREATEST - left_at: 기존 CASE 패턴 유지 이제 loser/keep 중 하나라도 NULL이어도 유효한 값이 보존되고, 둘 다 NULL이면 NULL 유지. * fix: V69 마이그레이션 최신 메시지 선택 기준 개선 last_message_content 갱신 시 MAX(id) 대신 MAX(created_at)을 사용하여 실제 최신 메시지를 선택하도록 수정: - MAX(created_at)으로 최신 메시지 우선 선택 - 동일 타임스탬프 시 MAX(id)로 타임브레이커 적용 - 이전: id 기준 (시간 역순 불가능한 엣지케이스 존재) - 이후: 시간 기준 (결정론적이고 정확한 최신 메시지) * fix: V69 마이그레이션 0명 방 처리 개선 재시도 시 이미 처리된 방(from_room_id/keep_room_id)이 0명이 될 수 있는 경우를 처리하기 위해: - JOIN → LEFT JOIN 변경 - 0명 방도 EXISTS 조건으로 포함되도록 보존 - c1.user_id < c2.user_id는 ON 절에 유지 이제 이전 실행에서 멤버가 모두 삭제된 방도 재시도 시 매핑 테이블에서 찾을 수 있음. * fix: V69 마이그레이션 MySQL zero date 처리 MySQL의 zero date('0000-00-00 00:00:00')가 LEAST/GREATEST 함수에 들어가면 Data truncation 오류가 발생하는 문제 해결: - @ZERO_DATE 변수 정의 - CASE 문에서 NULL 체크와 함께 zero date도 동일하게 처리 - visible_message_from, left_at, last_read_at 모두 적용 이제 zero date 값이 있는 경우에도 안전하게 병합됨. * fix: V69 마이그레이션 MySQL zero date 서브쿼리 처리 Data truncation 오류를 해결하기 위해 서브쿼리에서 NULLIF로 zero date를 NULL로 변환: - Step 4a: MIN/MAX 집계 시 NULLIF 적용 - Step 4b: INSERT 시 NULLIF 적용 - UPDATE의 CASE 문은 기본 NULL 체크로 단순화 이제 서브쿼리 결과에 zero date가 없어 LEAST/GREATEST 함수에서 오류 발생 방지. * fix: V69 실행 중에만 zero datetime 관련 sql_mode를 완화 * fix: 테스트 계약과 구조를 정리해 전체 테스트를 안정화 (#514) * fix: 공통 테스트 mock 충돌과 fluent stub 누락을 정리한다 - UploadApiTest의 중복 GoogleCredentials mock 선언 제거 - KonectApplicationTests에 ServiceAccountCredentials mock 추가 - GoogleDrivePermissionHelperTest의 fluent request stub 공통화 - 테스트 기반 계층에서 발생하던 bootstrap/null-chain 실패를 먼저 줄임 * fix: 통합 테스트 계약 드리프트와 검증 기준 불일치를 정리해 전체 테스트를 안정화 - StudyTime, ClubMemberApplications, UserSignup 테스트를 현재 API 계약과 인증 흐름에 맞게 갱신 - ClubSettings와 UserWithdraw 테스트의 상태 캡처/soft delete 검증 방식을 현재 repository 동작에 맞게 정리 - AdminSchedule 롤백 검증은 REQUIRES_NEW 조회로 분리해 테스트 트랜잭션 영향 없이 확인 - UploadService에 maxUploadBytes 검증을 추가하고 ClubMember batch fixture를 현재 validation 규칙에 맞춰 전체 테스트를 green 상태로 복구 * perf: 테스트 로그와 스케줄링 비용을 줄여 CI 실행 시간을 낮춘다 - 테스트 프로필에서 SQL 및 스케줄러 로그를 줄이고 show-sql을 꺼서 GitHub Actions의 출력 비용을 낮춤 - SchedulingConfig를 조건부로 바꿔 테스트에서는 주기 작업이 돌지 않도록 정리 - Redis repository 스캔을 테스트에서 비활성화해 부팅 오버헤드를 줄임 - CI 환경에서는 HTML 테스트 리포트를 생략하고 JUnit XML만 남겨 불필요한 리포트 생성 비용을 줄임 * chore: 코드 포맷팅 * fix: 동아리 설정 조회 테스트의 권한별 응답 계약 누락을 막기 위해 검증을 통일 - 부회장/운영진 성공 케이스도 회장 케이스와 동일한 응답 계약 검증을 재사용한다. - 상태 코드만 확인하던 테스트를 공통 payload assertion으로 묶어 회귀 누락을 줄인다. - ID 타입을 long으로 바꾸라는 리뷰는 실제 컨트롤러 계약이 Integer여서 반영하지 않았다. Constraint: ClubSettings API의 clubId 경로 변수는 현재 Integer 계약이다 Rejected: NON_EXISTENT_ID를 long으로 변경 | 경로 바인딩 단계에서 404 대신 실패해 테스트 의미가 깨짐 Confidence: high Scope-risk: narrow Reversibility: clean Directive: settings 조회 성공 케이스가 늘어나면 assertPresidentSettingsPayload와 동일한 계약 검증을 재사용할 것 Tested: ./gradlew test --tests "*ClubSettingsControllerTest" --tests "*UserSignupApiTest" Not-tested: 전체 테스트 스위트 재실행 * fix: 회원가입 테스트 요청 구성을 한 경로로 유지해 검증 누락을 막음 - raw JSON 요청도 performSignup 헬퍼를 재사용하도록 오버로드를 추가했다. - 회원가입 요청 조립 로직을 한 곳으로 모아 쿠키/Content-Type 변경 시 테스트 드리프트를 줄인다. - null 마케팅 동의 케이스도 동일한 요청 경로를 타게 해 테스트 의도를 더 분명히 유지한다. Constraint: 일부 검증 케이스는 null 필드를 표현하기 위해 DTO 대신 raw JSON 요청이 필요하다 Rejected: 각 테스트에서 mockMvc.perform을 유지 | 요청 설정 변경 시 중복 수정과 누락 위험이 큼 Confidence: high Scope-risk: narrow Reversibility: clean Directive: 회원가입 요청 형태가 바뀌면 개별 테스트보다 performSignup 헬퍼를 먼저 갱신할 것 Tested: ./gradlew test --tests "*ClubSettingsControllerTest" --tests "*UserSignupApiTest" Not-tested: 전체 테스트 스위트 재실행 * chore: .gitignore에 .omx/ 폴더 추가 * test: ClubSettings 조회 테스트의 중복 검증 경로를 줄여 유지보수를 쉽게 한다 - 권한별 설정 조회 테스트가 동일한 로그인, 요청, 기본 성공 검증을 반복하고 있어 공통 helper로 묶어 수정 지점을 한 곳으로 모았다. - 테스트마다 다른 관심사만 남기고 공통 조회 경로를 추출해 시나리오 의도를 더 읽기 쉽게 유지했다. - 공통 성공 조건이 바뀔 때 여러 테스트를 각각 수정하다가 일부만 반영되는 불일치를 막기 위한 선택이다. * test: 회원가입 테스트가 실제 토큰 계약을 따르도록 맞춘다 - 프로덕션 RefreshTokenService 계약은 30일 TTL을 반환하므로, 테스트 stub도 같은 만료 기간을 사용하도록 정렬했다. - signup_token mock이 같은 토큰에 대해 반복 성공하지 않도록 첫 소비 이후 INVALID_SIGNUP_TOKEN 예외를 던지게 바꿨다. - 이 변경은 테스트가 실제 인증 흐름의 일회성 토큰 계약을 더 가깝게 검증하도록 만들어, 중복 소비 회귀를 숨기지 않게 한다. Constraint: 회원가입 컨트롤러는 signup token을 소비한 뒤 refresh token TTL을 응답 쿠키에 반영한다 Rejected: consumeOrThrow 호출 횟수만 verify | 일회성 계약 위반이 발생해도 stub이 계속 성공하면 흐름 회귀를 충분히 드러내지 못한다 Confidence: high Scope-risk: narrow Reversibility: clean Directive: 회원가입 토큰/리프레시 토큰 계약이 바뀌면 테스트 상수보다 서비스 계약과 호출 순서를 먼저 확인할 것 Tested: ./gradlew test --tests "*ClubSettingsControllerTest" --tests "*UserSignupApiTest", git diff --check Not-tested: 전체 Gradle 테스트 스위트 * test: 설정 조회 helper 이름이 검증 의도를 드러내도록 정리한다 - 설정 조회 helper가 요청만 수행하는 것처럼 보였지만 실제로는 성공 상태와 모집 활성화까지 함께 검증하고 있었다. - helper 이름을 검증 의도가 드러나는 형태로 바꿔, 호출부에서 부작용을 숨기지 않도록 정리했다. - 테스트 동작은 유지하면서 메서드 책임을 더 명확하게 읽히게 만들었다. Tested: ./gradlew test --tests "*ClubSettingsControllerTest", git diff --check * test: 회원가입 토큰 소비 검증을 테스트에 명시한다 - signup_token mock이 일회성으로 동작하더라도, 테스트에서 실제 호출 횟수를 확인하지 않으면 계약이 흐려질 수 있다. - 회원가입 흐름이 실제로 컨트롤러까지 진입하는 케이스들에 한해 토큰 소비가 정확히 1번 일어났는지 검증을 추가했다. - Bean Validation 단계에서 조기 실패하는 400 케이스는 토큰을 소비하지 않는 현재 흐름을 유지하도록 제외했다. Tested: ./gradlew test --tests "*UserSignupApiTest", git diff --check * fix: 구글 시트 preview API의 request 제거 (#513) * fix: 시트 preview는 등록된 시트만 사용하도록 변경 * test: 등록된 시트 기준 preview 흐름 테스트 추가 * refactor: 시트 preview 클럽 중복 조회 제거 * refactor: 시트 preview에 저장된 분석 매핑 우선 적용 * fix: 시트 preview 대학 지연 로딩 예외 방지 * test: 각종 테스트 케이스 보강 (#515) * test: 은행 API 통합 테스트를 보강 - 은행 목록 조회 API의 기본 성공 케이스와 빈 목록 응답을 검증한다 - 별도 추상화 없이 최소 테스트만 추가해 변경 범위를 은행 도메인에 한정한다 - 공용 조회 API 회귀 시 응답 구조가 조용히 깨지는 문제를 빠르게 잡을 수 있게 한다 * test: 대학 API 통합 테스트를 보강 - 대학 목록 조회 API의 정렬 순서와 빈 목록 응답을 통합 테스트로 고정한다 - 서비스의 이름 오름차순 계약을 테스트에서 바로 드러내도록 응답 순서를 검증한다 - 대학 선택 화면에 영향을 주는 정렬 회귀를 조기에 막기 위한 커버리지를 추가한다 * test: 문의 API 통합 테스트를 보강 - 문의 전송 성공 케이스와 빈 내용 검증 실패 케이스를 함께 추가한다 - Public API 특성상 입력값 검증 누락이 바로 외부 요청 오류로 이어질 수 있어 최소 실패 경로를 함께 묶었다 - 단순 이벤트 발행 엔드포인트라도 요청 본문 계약이 깨지지 않도록 안전망을 만든다 * fix: 버전 도메인 스키마와 API 테스트를 함께 정리 - version 엔티티의 복합 unique constraint 선언 오류를 바로잡아 H2 스키마 생성이 실패하던 문제를 수정한다 - 일반 버전 조회와 관리자 버전 등록 API에 성공, 중복, 권한, 잘못된 파라미터 케이스를 통합 테스트로 추가한다 - 버전 도메인 회귀가 테스트 환경에서 500으로 숨겨지지 않도록 스키마 결함과 커버리지 공백을 한 번에 메웠다 * fix: 공지 읽음 이력 제약조건 선언 오류를 수정 - council notice 읽음 이력 엔티티의 복합 unique constraint 컬럼 선언을 올바르게 분리한다 - 버전 도메인 검증 중 드러난 같은 유형의 스키마 오류를 함께 정리해 테스트 환경 불안정 원인을 줄인다 - 이후 notice 관련 통합 테스트를 추가할 때 스키마 생성 단계에서 막히지 않도록 선제적으로 정리한다 * chore: 코드 포맷팅 * test: 총동아리연합회 API 통합 테스트를 보강 - 총동아리연합회 조회, 생성, 수정, 삭제 흐름을 전용 통합 테스트로 고정한다 - 중복 생성과 잘못된 전화번호 형식 같은 실패 경로도 함께 검증해 입력 계약을 명확히 한다 - 대학 단위 대표 조직 정보가 깨질 때 주요 화면과 설정 흐름이 함께 흔들리는 문제를 조기에 잡을 수 있게 한다 * test: 공지사항 API 통합 테스트를 보강 - 공지 목록, 상세, 생성, 수정, 삭제 흐름을 읽음 여부와 권한 검증까지 포함해 통합 테스트로 추가한다 - 다른 대학 공지 접근 금지와 잘못된 페이지 파라미터 같은 엣지 케이스를 함께 고정해 회귀 여지를 줄인다 - 공지 생성 로직이 총동아리연합회 데이터에 의존하는 현재 구조를 테스트로 드러내 안정적으로 보호한다 * test: 알림 SSE 구독 API 통합 테스트를 보강 - 알림 inbox SSE 구독 엔드포인트가 비동기 응답으로 시작되는지 통합 테스트로 고정한다 - 최초 connect 이벤트와 text/event-stream 콘텐츠 타입을 함께 검증해 구독 초기 계약이 조용히 깨지는 문제를 막는다 - 기존 알림 inbox CRUD 테스트가 다루지 못하던 실시간 구독 경로를 별도 테스트로 보강한다 * test: 어드민 광고 API 통합 테스트를 보강 - 어드민 광고 목록, 단건 조회, 생성, 수정, 삭제 흐름을 전용 통합 테스트로 고정한다 - 존재하지 않는 광고 조회, 잘못된 요청 본문, 권한 부족 같은 실패 경로를 함께 검증해 회귀 범위를 넓힌다 - 일반 광고 조회 API와 분리된 어드민 전용 관리 경로가 조용히 깨지지 않도록 CRUD 계약을 명확히 보호한다 * fix: 총동아리연합회 수정 계약과 엣지 케이스를 보강 - 총동아리연합회 수정 시 phoneNumber와 email이 실제 반영되도록 누락된 필드 업데이트를 복구한다 - 생성, 수정, 삭제, 검증 실패, 미존재 대상 같은 엣지 케이스를 통합 테스트로 추가한다 - 설정 화면에서 변경이 일부만 저장되는 숨은 회귀를 테스트와 함께 바로 잡는다 * fix: 공지사항 입력 검증과 엣지 케이스를 보강 - 공지 제목과 내용의 공백 문자열이 통과하지 않도록 NotBlank 검증으로 강화한다 - 생성, 조회, 수정, 삭제 전반에 404, 400, 권한, 읽음 이력 중복 방지 케이스를 추가한다 - 공지 작성과 열람 흐름에서 사용자 입력과 읽음 상태가 조용히 어긋나는 문제를 테스트로 고정한다 * test: 대학 API 엣지 케이스를 보강 - 같은 이름이라도 캠퍼스가 다르면 각각 조회되는 계약을 통합 테스트로 추가한다 - 대학 선택 화면에서 이름 중복 케이스가 누락되어도 목록 응답이 의도대로 유지되도록 보호한다 * test: 문의 API 입력 엣지 케이스를 보강 - 요청 본문이 완전히 없을 때 INVALID_JSON_FORMAT이 반환되는 실제 계약을 테스트로 고정한다 - 빈 문자열 검증 케이스와 함께 본문 누락까지 포함해 외부 호출 실패 경로를 더 촘촘히 보호한다 * test: 버전 API 엣지 케이스를 보강 - releaseNotes가 비어 있는 최신 버전 조회와 플랫폼 누락, 동일 버전의 플랫폼별 등록 케이스를 추가한다 - 버전 조회와 관리자 등록 경로의 경계값을 함께 검증해 플랫폼별 계약이 흐트러지지 않도록 보호한다 * test: 알림 SSE 구독 엣지 케이스를 보강 - 동일 사용자의 재구독이 새 연결로 정상 시작되는지 추가로 검증한다 - 실시간 구독 경로에서 connect 이벤트 계약이 중복 구독 상황에서도 유지되도록 보호한다 * test: 어드민 광고 API 엣지 케이스를 보강 - 빈 목록, 수정 대상 없음, 삭제 대상 없음 케이스를 추가해 어드민 광고 CRUD의 경계값을 넓힌다 - 관리 화면에서 데이터가 없거나 이미 삭제된 상태에서도 응답 계약이 일관되게 유지되도록 보호한다 * test: 버전 최신 판단 규칙을 통합 테스트로 고정 - 최신 버전 선택 기준이 버전 문자열 크기가 아니라 createdAt임을 통합 테스트로 명시한다 - 운영자가 더 작은 버전 문자열을 나중에 등록해도 최신 배포 기준이 흔들리지 않도록 계약을 고정한다 * test: 총동아리연합회 대학 범위 규칙을 보강 - 다른 대학 council은 조회 대상이 아니고, 다른 대학에 이미 있어도 현재 대학에는 생성 가능하다는 규칙을 추가로 검증한다 - council 조회와 생성이 대학 단위로 격리된다는 핵심 도메인 규칙을 테스트로 고정한다 * test: 공지 읽음 상태와 대학 범위 규칙을 보강 - 다른 대학 공지가 목록에 섞이지 않는지, 한 사용자의 읽음 처리가 다른 사용자에게 전파되지 않는지를 검증한다 - 공지 목록과 읽음 상태가 대학/사용자 단위로 격리된다는 도메인 규칙을 테스트로 보호한다 * test: 어드민 광고 상태 보존 규칙을 보강 - 광고 생성 시 clickCount가 0으로 시작하고, 수정 시 기존 clickCount를 유지하는지를 검증한다 - 관리용 수정 작업이 통계성 상태를 의도치 않게 초기화하지 않는다는 계약을 테스트로 고정한다 * test: 네이티브 세션 브리지 상태 전이 규칙을 보강 - 브릿지 성공 시 기존 세션이 무효화되는지 확인해 네이티브 로그인 전환 과정의 세션 격리를 고정한다 - https 프록시 환경에서 refresh_token 쿠키가 Secure와 SameSite=None 속성을 갖는지 검증해 실제 배포 환경의 쿠키 보안 계약을 보호한다 * test: Slack 이벤트 처리 규칙을 보강 - url_verification 우선 처리, event_id 중복 무시, 스레드 app_mention 컨텍스트 전달, subtype message 무시 규칙을 컨트롤러 테스트로 고정한다 - Slack 이벤트 재전송과 스레드 멘션 같은 실제 운영 상황에서 중복 응답이나 잘못된 AI 호출이 발생하지 않도록 핵심 처리 규칙을 보호한다 * test: 알림 전달 서비스 규칙을 보강 - 같은 사용자의 SSE 재구독 시 이전 emitter 완료가 현재 구독을 지우지 않는 규칙을 서비스 테스트로 고정한다 - 배치 SSE 전송이 일부 실패에도 나머지 사용자 전송을 계속하고, saveAll이 실제 조회된 사용자에게만 알림을 생성하는 규칙을 검증한다 * chore: 코드 포맷팅 * test: 각종 단위 테스트 추가 (#516) * test: 회원가입 토큰 서비스의 직렬화/소비 경계 조건을 고정 - Redis 저장 포맷과 TTL이 의도한 규칙대로 유지되도록 발급 경로를 테스트로 고정했다 - providerId, name 이 비어 있는 경우 null 로 복원되는 현재 역직렬화 정책을 명시적으로 검증했다 - 잘못된 저장값, 빈 토큰, consume 시 단일 사용 같은 실패/경계 흐름을 테스트로 묶어 회귀를 빠르게 드러내도록 했다 - 외부 Redis 인프라 없이 Mockito 기반 단위 테스트로 구성해 병렬 실행에서도 테스트 간 간섭이 없도록 했다 * test: 리프레시 토큰 서비스의 서명/만료/설정 오류 검증을 보강 - 토큰 발급과 사용자 ID 추출의 정상 왕복 흐름을 먼저 고정해 기본 동작 회귀를 막았다 - 다른 secret 으로 서명된 토큰, 만료 토큰, 잘못된 token_type, 빈 입력을 각각 분리 검증해 인증 오류를 세밀하게 감지하도록 했다 - issuer 누락과 secret 길이 부족처럼 운영 설정 실수도 단위 테스트에서 바로 드러나게 했다 - 실시간 환경이나 외부 저장소에 의존하지 않는 순수 서비스 테스트로 작성해 병렬 실행 안정성을 유지했다 * test: 공부시간 랭킹 서비스의 정렬/가공/누락 처리 회귀를 방지 - DAILY 와 MONTHLY 분기, 페이지 기준 rank 계산, type trim 처리 같은 조회 분기를 테스트로 고정했다 - 내 랭킹 조회에서 랭킹이 없는 동아리를 제외하고 실제 rank 순으로 정렬되는 흐름을 검증했다 - 학번 표시, 개인 이름 가공, daily 우선 순위 계산처럼 응답 스펙에 숨어 있는 규칙을 명시적으로 보호했다 - mock 기반 단위 테스트로 상태 공유 없이 구성해 병렬 실행 중에도 순서 의존이나 자원 경합이 없도록 했다 * test: 리팩토링 중인 공부시간 랭킹 테스트를 작업 범위에서 제외 - 공부시간 랭킹 서비스는 현재 리팩토링 중이라 테스트가 구현 세부사항을 과하게 고정하지 않도록 이번 단위 테스트 보강 범위에서 제외했다 - 이미 추가했던 StudyTimeRankingServiceTest 를 제거해 리팩토링 중인 코드와 테스트가 서로 발목을 잡지 않게 했다 - 이번 변경 이후 검증 대상은 회원가입 토큰과 리프레시 토큰 서비스 테스트 두 개로 다시 고정했다 * omx(team): auto-checkpoint worker-3 [unknown] * omx(team): auto-checkpoint worker-1 [unknown] * omx(team): auto-checkpoint worker-2 [unknown] * omx(team): auto-checkpoint worker-2 [unknown] * omx(team): auto-checkpoint worker-1 [unknown] * Unblock service-layer unit test verification by clearing stale lint debt The user OAuth account service tests already covered the intended edge cases, but an unused Mockito import kept strict test checkstyle red in this lane. Removing the stale import restores clean compilation for the touched test file and keeps the remaining verification focused on pre-existing unrelated lint violations. Constraint: Worker scope is limited to the service-layer test lane Rejected: Fix unrelated ChatApiTest and SheetImportServiceTest lint failures | outside assigned scope Confidence: high Scope-risk: narrow Tested: ./gradlew compileTestJava Tested: ./gradlew test --tests 'gg.agit.konect.domain.user.service.UserActivityServiceTest' --tests 'gg.agit.konect.domain.user.service.UserOAuthAccountServiceTest' --tests 'gg.agit.konect.global.auth.web.AuthCookieServiceTest' Tested: ./gradlew checkstyleTest (fails only on pre-existing ChatApiTest and SheetImportServiceTest violations) Not-tested: Full repository test suite * omx(team): auto-checkpoint worker-1 [unknown] * test: 서비스 계층 회귀를 막기 위해 병렬 친화 단위 테스트를 보강 - 서비스 레이어에서 비어 있거나 약한 단위 테스트 구간을 보강해 OAuth 연동, 활동 시각 갱신, 쿠키 처리, 알림 전송, 동아리 멤버 관리, 공부시간 누적 경계 케이스를 고정 - Spring context 없이 Mockito 기반 구조를 유지해 병렬 실행 시 공유 상태 충돌 가능성을 줄임 - 실패하던 신규 테스트와 체크스타일 이슈는 최소 수정으로 정리하고, 서비스 테스트 작성 기준은 별도 가이드 문서로 남겨 이후 확장 비용을 낮춤 - 전체 checkstyleTest는 기존 무관 이슈 2건이 남아 있어 이번 변경 범위에서는 건드리지 않음 * refactor: 코드 가독성을 위해 createClub 메서드에 개행 추가 - return 문 전에 개행을 추가하여 코드 블록 구분 명확화 - 테스트 코드의 일관된 스타일 유지 * refactor: UserFixture에 createUserWithId 메서드 추가하여 테스트 코드 중복 제거 - UserFixture에 ID를 지정하여 사용자를 생성하는 createUserWithId 메서드 2개 추가 - createUserWithId(University, Integer, String, UserRole) - createUserWithId(Integer, String, UserRole) - ClubMemberManagementServiceTest의 중복 createUser 헬퍼 메서드 제거 - 테스트 코드에서 UserFixture.createUserWithId()를 사용하도록 변경 * refactor: ClubFixture와 UniversityFixture에 createWithId 메서드 추가 - UniversityFixture에 ID를 지정하여 생성하는 createWithId 메서드 2개 추가 - ClubFixture에 ID를 지정하여 생성하는 createWithId 메서드 2개 추가 - ClubMemberManagementServiceTest의 createClub 메서드 간소화 - ReflectionTestUtils 직접 호출 제거 - Fixture의 createWithId 메서드 사용하도록 변경 * chore: 코드 포맷팅 * chore: 서비스 테스트 가이드 문서 제거 * test: 공부 시간 타이머 테스트 코드 제거 - StudyTimerServiceTest.java 제거 - StudyTimeApiTest.java 제거 * refactor: 테스트 패키지 구조 개선 단위 테스트를 unit/ 패키지로 이동하여 main 소스와 테스트 구조의 일관성 확보 이동된 테스트: - domain/club/service/*Test (6개) - domain/notification/service/*Test (3개) - domain/user/service/*Test (4개) - infrastructure/slack/*Test (2개) 원래 위치 유지 (package-private 클래스 테스트): - ClubSheetIntegratedServiceTest - GoogleDrivePermissionHelperTest - GoogleSheetApiExceptionHelperTest - GoogleApiTestUtils (public 접근자로 변경) 테스트 컴파일 정상 확인 완료 * chore: 코드 포맷팅 * refactor: AuthCookieServiceTest를 unit 패키지로 이동 global/auth/web/AuthCookieServiceTest → unit/global/auth/web/AuthCookieServiceTest 패키지 선언 및 import 수정 * refactor: UserActivityServiceTest에서 로컬 createUser 헬퍼를 Fixture로 대체 - UserFixture에 createUserWithId(Integer id, String studentNumber) 메서드 추가 - UserActivityServiceTest의 중복된 로컬 createUser 메서드 제거 - Fixture 재사용으로 유지보수성 개선 * refactor: UserOAuthAccountServiceTest에서 로컬 createUser 헬퍼를 Fixture로 대체 - UserFixture.createUserWithId() 재사용으로 중복 제거 - createWithdrawnUser() 내부에서도 Fixture 메서드 사용 * test: AuthCookieServiceTest에 누락된 검증 추가 - X-Forwarded-Proto 테스트: refresh_token 값과 Max-Age 검증 추가 - clearSignupToken 테스트: Domain 검증 추가 * refactor: ClubFixture.createWithId가 create를 재사용하도록 개선 - 중복된 Club.builder() 호출 제거 - create() 메서드 재사용으로 유지보수성 향상 * refactor: UniversityFixture.createWithId가 create를 재사용하도록 개선 - 중복된 University.builder() 호출 제거 - create() 메서드 재사용으로 유지보수성 향상 * test: ClubMemberManagementServiceTest의 학번 검증을 matchedUser 기준으로 변경 - request.studentNumber() 대신 matchedUser.getStudentNumber() 사용 - 테스트 준비 객체와 assertion 간 결합 강화 * chore: 코드 포맷팅 * refactor: UserActivityServiceTest 개선 - 중복된 UserFixture import 제거 - 활동 시각 갱신 검증을 isAfterOrEqualTo에서 isAfter로 변경하여 실제 갱신 보장 * refactor: UserFixture.createUserWithId 오버로드 간 중복 제거 - createUserWithId(Integer, String)가 기존 오버로드를 재사용하도록 변경 - 필드 초기화 로직 중복 제거 * test: UserActivityServiceTest no-op 검증 개선 - verify(..., never()) 대신 verifyNoInteractions 사용 - null 케이스와 사용자 없음 케이스를 별도 테스트로 분리 - verifyNoMoreInteractions으로 추가 호출 방지 검증 * refactor: AuthCookieServiceTest Set-Cookie 검증 중복 제거 - assertCommonCookieAttributes 헬퍼 메서드 추가 - Domain, Path, HttpOnly 검증을 공통 헬퍼로 추출 * chore: 코드 포맷팅 * fix: UserFixture.createUserWithId가 studentNumber를 올바르게 사용하도록 수정 - 기본 오버로드에 studentNumber 파라미터 추가 - 모든 오버로드가 studentNumber를 올바르게 전달하도록 개선 * chore: 코드 포맷팅 * test: forwarded https 쿠키 공통 속성 회귀를 방지 X-Forwarded-Proto=https 분기에서도 Domain, Path, HttpOnly를 함께 검증해 공통 쿠키 속성 누락 회귀를 막는다. * test: UserOAuthAccountServiceTest 사용자 픽스처 누락을 복구 누락된 createUser 헬퍼를 복구해 관련 단위 테스트가 다시 컴파일되고 실행되도록 한다. * test: UserActivityService 영/음수 userId 엣지 케이스를 검증 - updateLastLoginAt userId=0 및 음수 입력 시 동작 확인 - updateLastActivityAt userId=0 및 음수 입력 시 동작 확인 * test: UserOAuthAccountService 빈 계정/복구/탈퇴 엣지 케이스를 보강 - getLinkStatus OAuth 계정이 없는 경우 - linkOAuthAccount null providerId, providerId+email 조합, 빈 email - restoreOrCleanupWithdrawnByLinkedProvider/OauthEmail 복구 로직 - getPrimaryOAuthAccount 계정이 없는 경우 * test: RefreshTokenService 토큰 파싱/클레임 검증 엣지 케이스를 보강 - issue userId=0 처리 - extractUserId 빈 문자열, null, 탭/개행, 손상된 토큰 거부 - token_type/id 클레임 누락 및 비정상 타입 거부 - issuer 클레임 null 거부 * test: SignupTokenService 직렬화/역직렬화 경계 조건을 보강 - issue 빈 이메일 문자열 거부 - deserialize 빈 파트, 초과 파트, 빈 필드, 유효하지 않은 Provider 거부 - readOrThrow/consumeOrThrow Redis 빈 문자열 반환 시 예외 * test: AuthCookieService null duration/비보안 요청 엣지 케이스를 보강 - setRefreshToken null duration 예외 처리 - getCookieValue 쿠키가 없는 경우 null 반환 - isSecureRequest 대소문자 혼합 HTTPS, 비보안 요청 처리 * test: ClubMemberManagementService 권한/이전/제거 엣지 케이스를 보강 - changeMemberPosition canManage 검증 실패 시나리오 - getPreMembers, removePreMember 정상 동작 - transferPresident 자기 이전 방지 및 정상 이전 - changeVicePresident 부회장 교체/신규 지정 - removeMember 회장/비회원 제거 방지 및 정상 제거 * test: ClubMemberSheetService updateSheetId/누락 sheetId 엣지 케이스를 보강 - updateSheetId 정상 동작 - syncMembersToSheet sheetId null/blank 시 NOT_FOUND_CLUB_SHEET_ID 예외 * test: NotificationService 토큰/음소거/미리보기 엣지 케이스를 보강 - registerToken null/빈 토큰, deleteToken 미존재 토큰 - sendChatNotification 음소거 사용자 알림 미발송 - sendGroupChatNotification 빈 수신자/일부 토큰 누락 - 동아리 신청 알림 세 가지 정상 동작 - buildPreview null/빈/최대 길이 메시지 - validateExpoToken null/빈 토큰 * test: NotificationInboxService 저장/발송/읽음 엣지 케이스를 보강 - save 단일 알림 생성 - sendSse SSE 발송 - getMyInboxes 빈 결과 - getUnreadCount 카운트 0 - markAsRead/markAllAsRead 정상 동작 및 알림 없음 * test: NotificationInboxSseService 미구독/emitter 교체 엣지 케이스를 보강 - send null userId 처리 - send emitter가 없는 경우 (미구독 사용자) - subscribe 기존 emitter 교체 * test: UserOAuthAccountService Stage 복구 차이/충돌/경계값 엣지 케이스를 심화 - Stage profile에서 탈퇴 계정 복구하지 않고 삭제 - providerId NULL→값 충돌 시 OAUTH_ACCOUNT_ALREADY_LINKED - 복구 기간 정확히 7일 경계값에서 삭제 - providerId와 oauthEmail이 각각 다른 탈퇴 사용자를 가리키는 경우 - 기존 계정 providerId 업데이트 충돌 - Apple provider appleRefreshToken 업데이트 검증 * test: RefreshTokenService 빈 issuer/만료 rotate/claim 라운드트립을 검증 - issuer 빈 문자열 토큰 거부 - 만료된 토큰으로 rotate 시 INVALID_REFRESH_TOKEN - issue-extractUserId 라운드트립 claim 검증 * test: SignupTokenService 파이프 문자 직렬화 불일치/name 오버로드를 검증 - 이메일에 파이프 문자 포함 시 파트 분리 오류로 거부 - 이름에 파이프 문자 포함 시 5개 파트로 거부 - 4파라미터 issue 오버로드 name 포함 확인 - consumeOrThrow로 name 복원 검증 * test: ClubMemberManagementService VP 강등/회장 이전/자기 제거 엣지 케이스를 심화 - 같은 부회장 재지정 시 변화 없음 - 기존 VP 강등 + 새 VP 지정 - 회장 이전 후 기존 회장 MEMBER 검증 - 자기 자신 제거 시 CANNOT_REMOVE_SELF - VP→VP 변경 시 validatePositionLimit 통과 - 기존 멤버 포함 배치 스킵 - 관리자가 관리자 제거 시 canManage 검증 실패 * test: ClubMemberSheetService 빈 동아리/null 매핑 엣지 케이스를 심화 - syncMembersToSheet memberCount=0, preMemberCount=0 정상 동작 - updateSheetId null memberListMapping 처리 * test: NotificationService 토큰 없음/예외 삼킴/중복 수신자 엣지 케이스를 심화 - getMyToken 토큰 없는 사용자 예외 처리 - sendChatNotification chatPresenceService 예외 시 정상 종료 - sendGroupChatNotification 중복 수신자 처리 - 동아리 신청 알림 inbox 저장 및 SSE 전송 검증 * test: NotificationInboxService 일부 사용자/다른 사용자 알림 엣지 케이스를 심화 - saveAll 일부 사용자만 존재 시 해당 사용자만 알림 생성 - markAsRead 다른 사용자 알림 접근 시 예외 * test: NotificationInboxSseService emitter 교체 엣지 케이스를 심화 - subscribe 기존 emitter complete 후 새 emitter로 교체 * test: AuthCookieService signupToken 설정/제거/복수 프로토콜 엣지 케이스를 심화 - setSignupToken 정상 동작 검증 - setSignupToken + clearSignupToken 연동 (maxAge=0) - X-Forwarded-Proto 복수 값 "https,http" 비보안 처리 * chore: 코드 포맷팅 * refactor: 테스트 코드 미사용 변수 제거 - ClubMemberManagementServiceTest: 미사용 president 변수 제거 (2개) - NotificationInboxServiceTest: 미사용 user2, inbox 변수 제거 - NotificationInboxSseServiceTest: 미사용 failingEmitter 변수 제거 - NotificationServiceTest: 미사용 user 변수 제거 (3개) * refactor: UserOAuthAccountServiceTest Fixture 중복 제거 및 UserFixture 확장 - UserFixture에 createWithdrawnUser() 메서드 추가 (탈퇴 사용자 생성) - UserOAuthAccountServiceTest의 로컬 createUser() 제거 → UserFixture.createUserWithId() 직접 사용 - UserOAuthAccountServiceTest의 로컬 createWithdrawnUser() 제거 → UserFixture.createWithdrawnUser() 직접 사용 - Fixture 중복 제거로 유지보수성 향상 (필드 기본값 변경 시 일관성 유지) * chore: 코드 포맷팅 * refactor: NotificationInboxServiceTest 미사용 user1 변수 제거 - markAsReadThrowsExceptionForOtherUsersNotification()에서 미사용 user1 변수 제거 * chore: 런타임에서 OTel javaagent 주입을 제외 (#519) - Docker 실행 시 JAVA_TOOL_OPTIONS 주입을 제거해 OTel agent로 인한 런타임 메모리 오버헤드를 줄이기 위함 * refactor: 순공 시간 리펙토링 (#520) * feat: StudyTimeDailyRepository에 SUM 집계 쿼리 추가 monthly/total 테이블 제거를 위한 대체 쿼리를 선제적으로 추가합니다. - 단일 유저 월별/전체 합산 쿼리 - 다수 유저 일별/월별 합산 쿼리 (랭킹 집계용) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: 이벤트 기반 랭킹 업데이트 서비스 및 리스너 추가 - StudyTimeAccumulatedEvent: 공부 시간 누적 시 발행되는 이벤트 - StudyTimeRankingUpdateService: 단일 유저 기준 개인/동아리/학번 랭킹 업데이트 - StudyTimeRankingUpdateListener: AFTER_COMMIT 시점에 랭킹 업데이트 실행 - UserRepository에 학번 연도 기반 검색 쿼리 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: StudyTimerService에서 monthly/total 누적 제거 및 이벤트 발행 - monthly/total 테이블 저장 로직 제거 (daily만 누적) - stop(), sync() 에서 StudyTimeAccumulatedEvent 발행 - accumulateDailyAndMonthlySeconds → accumulateDailySeconds 로 단순화 - addMonthlySegment, updateTotalSecondsIfNeeded, addTotalSeconds 메서드 삭제 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: StudyTimeQueryService를 daily SUM 쿼리 기반으로 전환 - getMonthlyStudyTime: monthly 테이블 → daily SUM 쿼리로 변경 - getTotalStudyTime: total 테이블 → daily SUM 쿼리로 변경 - StudyTimeMonthlyRepository, StudyTimeTotalRepository 의존성 제거 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: 스케줄러에서 랭킹 업데이트 제거, 리셋만 유지 - 5초 간격 랭킹 업데이트 스케줄러 3개 제거 (이벤트 기반으로 전환 완료) - StudyTimeSchedulerService에서 랭킹 업데이트 메서드 및 관련 의존성 제거 - 자정 일간 리셋, 월초 월간 리셋 스케줄러만 유지 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: 미사용 StudyTimeMonthly, StudyTimeTotal 모델 및 레포지토리 삭제 daily SUM 쿼리로 대체되어 더 이상 사용되지 않는 파일들을 제거합니다. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: StudyTimeApiTest에서 삭제된 monthly/total 참조 제거 - StudyTimeMonthlyRepository, StudyTimeTotalRepository 의존성 제거 - stopTimerAccumulatesTime 테스트: summary API 응답으로 월별/전체 검증 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: study_time_monthly, study_time_total 테이블 DROP 마이그레이션 daily SUM 쿼리로 대체되어 더 이상 사용되지 않는 테이블을 삭제합니다. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: 리스너 트랜잭션 전파 방식 수정 및 checkstyle 위반 해결 - @transactional → @transactional(propagation = REQUIRES_NEW) 변경 (Spring 6.2+ 에서 AFTER_COMMIT 리스너에 기본 전파 방식 사용 불가) - StudyTimeRankingUpdateService 라인 길이 120자 제한 준수 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: flyway 마이그레이션 버전 충돌 해결 (V69 → V70) V69가 이미 존재하여 V70으로 변경합니다. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: flyway V70 삭제 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: UploadService 단위 테스트 추가 및 버그 수정 (#525) * test: UploadService 단위 테스트 추가 업로드 도메인 핵심 비즈니스 로직을 검증하는 단위 테스트를 작성한다. * fix: UploadService content-type 대소문자/공백 정규화 및 trailing slash 제거 - content-type을 trim + lowercase로 정규화하여 대소문자/공백 불일치 해결 - CDN baseUrl의 모든 trailing slash를 제거하도록 수정 - normalizeContentType 헬퍼 추가로 검증과 확장자 추출의 일관성 확보 - 41개 단위 테스트로 모든 분기 및 엣지 케이스 커버 * chore: 코드 포맷팅 * chore: 코드 포맷팅 * chore: 코드 포맷팅 * chore: 코드 포맷팅 * fix: MIME 타입 정규화 로케일 고정 터키어 기본 로케일에서도 content-type 정규화가 깨지지 않도록 Locale.ROOT를 사용한다. 관련 회귀를 막기 위해 터키어 로케일 단위 테스트를 추가한다. * fix: 잘못된 CDN base-url 설정 차단 CDN base-url이 슬래시만 포함한 경우 빈 경로가 되기 전에 ILLEGAL_STATE로 실패시킨다. 해당 설정 오류에 대한 단위 테스트를 추가해 상대 경로 URL 생성을 방지한다. * test: UploadServiceTest 로케일 테스트 순차 실행 전역 Locale 변경을 사용하는 테스트가 병렬 실행과 충돌하지 않도록 SAME_THREAD를 적용한다. 터키어 로케일 회귀 테스트의 안정성을 높인다. * test: ClubService 단위 테스트 추가 (#527) * test: ClubService 단위 테스트 추가 동아리 도메인 핵심 비즈니스 로직을 검증하는 단위 테스트를 작성한다. * test: ClubService 단위 테스트 케이스 보강 - getClubDetail의 회원/지원 상태 반환 시나리오 추가 - getClubs의 pendingApproval 계산 및 null id 엣지 케이스 추가 - getClubMembers의 학번 마스킹, position 필터, 빈 목록 시나리오 추가 - updateInfo, updateBasicInfo 권한 검증 및 필드 수정 테스트 추가 - getJoinedClubs, getManagedClubs, getManagedClubDetail 시나리오 추가 - 회장 미존재 시 NOT_FOUND_CLUB_PRESIDENT 예외 테스트 추가 * fix: updateBasicInfo 매니저 권한 검증 추가 및 테스트 수정 - TODO로 남겨둔 권한 체크를 validateManagerAccess로 구현 - 테스트가 미인가 접근을 정상 동작으로 lock-in하던 문제 수정 - 비매니저 접근 거부 테스트 케이스 추가 * chore: 코드 포맷팅 * chore: 코드 포맷팅 * fix: address ClubService review comments * refactor: reuse loaded user for manager validation * chore: 코드 포맷팅 * test: ClubApplicationService 단위 테스트 추가 (#526) * test: ClubApplicationService 단위 테스트 추가 동아리 가입 신청 관련 핵심 비즈니스 로직을 검증하는 단위 테스트를 작성한다. * test: ClubApplicationService 단위 테스트 엣지케이스 보강 - applyClub: 공백/null 답변 검증, 중복 질문 ID, 운영진 없을 때 이벤트 미발행, 다중 운영진 이벤트 포함 - approveClubApplication: 재승인 방지, 거절 후 승인, 승인 후 거절 상태 전이 - replaceApplyQuestions: isRequired 기본값 true, displayOrder만 변경 시 soft delete 방지 - 질문 가시성 경계 테스트: createdAt == appliedAt, deletedAt == appliedAt, 단일 지원서 - 미테스트 메서드 커버: getApprovedMemberApplicationAnswers, getClubApplications, getFeeInfo, replaceFeeInfo * fix: 이미 처리된 지원서의 상태 전이를 방지하는 가드 추가 - ALREADY_PROCESSED_CLUB_APPLY 에러 코드 추가 - approveClubApplication, rejectClubApplication에서 PENDING이 아닌 지원서 처리 시 예외 발생 - 승인→거절, 거절→승인 등 터미널 상태 전이 테스트를 가드 검증 테스트로 교체 * chore: 코드 포맷팅 * chore: 코드 포맷팅 * fix: 동아리 지원서 처리 동시성 가드 추가 승인/거절 시 지원서를 비관적 락으로 조회해 중복 처리 경쟁 조건을 막는다. * test: align application answers fixture with pending query * docs: document processed application conflict responses * fix: narrow pessimistic lock scope for club apply lookup * chore: 코드 포맷팅 * refactor: reuse pending club apply fixture in tests * test: ChatService 단위 테스트 추가 (#528) * test: ChatService 단위 테스트 추가 ChatService의 핵심 비즈니스 로직을 검증하는 단위 테스트를 작성한다. * test: ChatService 단위 테스트 엣지케이스 보강 - createOrGetChatRoom: 새 direct room 생성, admin→admin 일반 direct 경로, admin system admin room 재사용, 나간 요청자 재오픈 - createOrGetAdminChatRoom: admin 미존재 에러 - leaveChatRoom: room not found, 멤버 아님 - kickMember: room not found, club group room 거부, requester/target 멤버 아님 - getMessages: room not found, admin system room 전용 경로, 나간 멤버 가시성 복구/거부 - sendMessage: direct/group/club 전송, room not found, 나간 멤버 복구/거부, admin system admin room bypass - toggleMute: room not found, group/direct room 비멤버 거부, club room 멤버십 검증 - updateChatRoomName: 정상 업데이트, 비멤버 거부, null/blank 정규화, room not found * test: Codex 리뷰 기반 취약점 보강 - admin이 system admin room에 전송 시 멤버십 바이패스/lastReadAt 미갱신/알림 대상 검증 테스트 추가 - toggleMute 기존 단일 테스트를 mute/unmute/신규생성 3개로 분리하여 unmute 회귀 방지 * chore: 코드 포맷팅 * chore: 코드 포맷팅 * test: 리뷰 코멘트 반영 * test: 테스트 픽스처 중복 제거 * test: 리뷰 코멘트 반영 * ci: 테스트 커버리치 체크 워크플로우 추가 (#529) * ci: PR JaCoCo 커버리지 체크 워크플로우 추가 - build.gradle에 JaCoCo 플러그인 및 리포트 설정 추가 - PR에서 변경된 파일만 커버리지 측정하는 GitHub Actions 워크플로우 생성 - 롬복 생성 코드 JaCoCo 제외 설정 (lombok.config) - DTO, Entity, Request/Response, Config, Exception 클래스 커버리지 제외 * ci: PR 커버리지 워크플로우 결과 처리를 안정화 기본 출력값과 안전한 문자열 전달 방식을 적용해 변경 파일이 없거나 테이블 내용에 특수문자가 있어도 PR 커버리지 코멘트가 깨지지 않게 한다. * build: 내부 클래스 로직이 커버리지에 반영되도록 JaCoCo 제외 범위를 축소 과도한 `$` 클래스 제외 규칙을 제거해 내부·익명 클래스에 담긴 실제 비즈니스 로직이 PR 커버리지에서 누락되지 않게 한다. * fix: JaCoCo 제외 범위를 dto 패키지로 한정해 RequestLoggingFilter 등 누락 방지 substring 기반 제외가 RequestLoggingFilter, CustomRequestEntityConverter 같은 실제 로직까지 제외하던 문제를 수정한다. * ci: 변경 파일 커버리지 50% 미만 시 워크플로우가 실패하도록 강제 게이트 추가 기존에는 코멘트만 남기고 통과시켰으나, 이제 커버리지가 임계값 미만이면 워크플로우가 실패한다. * fix: JaCoCo 커버리지 내장 클래스 매칭 정규식 수정 정규식 패턴에서 `\\$`를 `\$`로 수정하여 Java 내부 클래스 구분자($)를 올바르게 매칭하도록 함 * fix: JaCoCo 제외 패턴을 entity에서 model로 수정 실제 엔티티 클래스 경로는 domain/*/model/이므로 커버리지 제외 패턴을 **/domain/**/entity/*.class에서 **/domain/**/model/*.class로 변경 * fix: PR 커버리지 워크플로우의 권한 및 페이지네이션 문제 해결 - pull_request에서 pull_request_target으로 변경하여 forked PR에서도 코멘트 작성 권한을 획득하도록 수정 - github.paginate를 사용하여 모든 코멘트를 조회하도록 변경 (기존 listComments는 기본 30개만 반환하여 중복 코멘트 발생 가능) * fix: PR 커버리지 워크플로우가 변경 파일을 감지하지 못하는 문제 수정 (#531) - pull_request_target에서 ref 미지정 시 base 브랜치를 체크아웃하여 git diff 시 변경 파일이 0개로 나오는 현상 방지 - github.event.pull_request.head.sha를 명시적으로 체크아웃하도록 수정 * refactor: Lua 스크립트를 Redis GETDEL 네이티브 명령어로 교체 (#530) * refactor: Lua 스크립트를 Redis GETDEL 네이티브 명령어로 교체 - SignupTokenService, NativeSessionBridgeService, GoogleDriveOAuthService에서 GET+DEL Lua 스크립트를 StringRedisTemplate.getAndDelete()로 대체 - getAndDelete()는 Redis 6.2+ GETDEL 명령어에 매핑되어 동일한 원자성 보장 - 불필요해진 DefaultRedisScript import와 인라인 스크립트 정의 제거 - SignupTokenServiceTest의 mock 검증도 getAndDelete 기반으로 업데이트 * chore: 코드 포맷팅 * test: consumeOrThrow getAndDelete 호출 검증 추가 - getAndDelete가 정확히 1회 호출되었는지 확인하기 위해 verify 검증을 추가하여 원자적 소비 동작 보장 * test: 빈 토큰 검증 테스트의 Redis 미접근 assertion 강화 - opsForValue() 미호출만 확인하던 것을 verifyNoInteractions으로 교체 - 다른 Redis 접근이 추가되어도 테스트가 올바르게 실패하도록 개선 * fix: 테스트 미실행 시 JaCoCo 오류 메시지 및 가드 추가 (#532) * fix: 테스트 미실행 시 JaCoCo 오류 메시지 및 가드 추가 - no-report 상태 처리 및 관련 PR 댓글 내용 추가 - always 조건으로 오류 발생 시에도 후속 스텝 실행 보장 * fix: PR 커버리지 워크플로우 코멘트 갱신 및 오류 진단 개선 - 선행 스텝(tests, changed-files) outcome을 파서에 전달하여 실제 원인(테스트 실패 vs 리포트 누락 vs 변경파일 없음)을 구분 - parse-coverage, comment 스텝에 if: always() 추가로 이전 스텝 실패 시에도 코멘트가 항상 갱신되도록 수정 - workflow-error 분기 추가로 선행 스텝 실패 메시지 명확화 - Actions 실행 링크를 분기 밖으로 이동하여 모든 결과에 포함 * feat: 채팅 메시지 검색 결과에서 특정 메시지 시점으로 페이지 이동 기능 추가 (#533) * feat: 채팅 메시지 검색 결과에서 특정 메시지 시점으로 페이지 이동 기능 추가 - 검색 응답(ChatMessageMatchResult)에 matchedMessageId 필드를 추가하여 프론트엔드에서 검색된 메시지를 특정할 수 있도록 함 - ChatMessageRepository에 findByIdWithChatRoom, countNewerMessagesByChatRoomId 쿼리를 추가하고 findByChatRoomId의 ORDER BY에 id DESC tie-breaker를 적용하여 동일 createdAt 메시지 간 결정론적 정렬을 보장 - ChatService.getMessages에 messageId 오버로드를 추가하여, messageId가 제공되면 해당 메시지가 포함된 페이지를 자동 계산 (page = newerCount / limit + 1) - 권한 오라클 방지를 위해 messageId resolution 전에 ensureMessageLookupAccess로 방 접근 권한을 사전 검증하고, 권한 없음 시 NOT_FOUND_CHAT_ROOM으로 통일 - visibleMessageFrom 경계 조건(createdAt == visibleMessageFrom)에서 가드와 쿼리의 경계 규칙이 일치하도록 isBefore를 !isAfter로 수정 - ChatApi/ChatController에 messageId optional 파라미터를 추가하고 Swagger 문서화 - 8개 단위 테스트 추가 (null messageId 호환성, 미존재/타 방 메시지 거부, 페이지 계산, 가시성 범위 밖 거부, 최신 메시지 page=1, 비회원 오라클 방지, visibleMessageFrom 경계) * chore: 코드 포맷팅 * fix: 사용하지 않는 message 변수 및 관련 미사용 변수 제거 - getMessagesWithMessageIdRejectsNonMemberWithNotFound 테스트에서 ChatMessage message 변수가 생성 후 참조되지 않아 제거 - message 제거로 함께 미사용이 된 sender, memberId 변수도 정리 * docs: messageId 포…
🔍 개요
채팅 메시지 검색 결과에서 특정 메시지를 클릭하면 해당 메시지가 포함된 페이지로 바로 이동하는 기능을 추가
검색 응답에
matchedMessageId필드를 추가하고, 메시지 조회 API에messageId옵션 파라미터를 추가하여 클라에서 검색 결과 → 메시지 위치로 직접 이동 가능🚀 주요 변경 내용
ChatMessageMatchResult:
matchedMessageId필드 추가,from()에서message.getId()매핑ChatMessageRepository:
findByIdWithChatRoom(messageId)— 메시지 + 채팅방 JOIN FETCH 조회countNewerMessagesByChatRoomId(...)— 타겟 메시지 이후 메시지 개수 카운트 (페이지 계산용)findByChatRoomIdORDER BY에cm.id DESCtie-breaker 추가 (동일 createdAt 메시지 결정론적 정렬)ChatService:
getMessages(userId, roomId, page, limit, messageId)오버로드 추가messageId제공 시page = newerCount / limit + 1으로 자동 페이지 계산resolvePageForMessage— 페이지 계산 로직을 별도 메서드로 추출resolveVisibleMessageFromPure— 부수효과 없는 가시성 조회 (기존resolveAdminSystemRoomVisibleMessageFrom에 위임)ensureMessageLookupAccess— messageId resolution 전 접근 권한 사전 검증💬 참고 사항
GET /chats/rooms/{chatRoomId}?messageId=42호출 시 해당 메시지가 포함된 페이지 번호를 자동 계산하여 반환messageId미제공 시 기존 동작과 완전 동일 (하위 호환)존재하지 않는 messageId, 다른 채팅방의 messageId →
404 NOT_FOUND_CHAT_ROOM✅ Checklist (완료 조건)