Skip to content

[FEAT] 모임 목록 조회에서 나의 정보 제공#185

Merged
LimdaeIl merged 3 commits intomainfrom
feat/get-group-list
Dec 31, 2025
Merged

[FEAT] 모임 목록 조회에서 나의 정보 제공#185
LimdaeIl merged 3 commits intomainfrom
feat/get-group-list

Conversation

@LimdaeIl
Copy link
Collaborator

@LimdaeIl LimdaeIl commented Dec 31, 2025

📝 Pull Request

📌 PR 종류

해당하는 항목에 체크해주세요.

  • 기능 추가 (Feature)
  • 버그 수정 (Fix)
  • 문서 수정 (Docs)
  • 코드 리팩터링 (Refactor)
  • 테스트 추가 (Test)
  • 기타 변경 (Chore)

✨ 변경 내용

사용자별 멤버십 정보를 추가하여 그룹 목록 응답을 더욱 풍부하게 만듭니다.
이를 통해 프런트엔드는 각 그룹의 멤버십 상태(예: "가입됨", "대기 중", "가입하지 않음")를 표시할 수 있으며,
더욱 개인화된 정보를 제공하여 사용자 경험을 향상시킵니다.
사용자가 로그인하지 않은 경우에도 원활하게 처리합니다.

🔍 관련 이슈

🧪 테스트

변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.

  • 유닛 테스트 추가 / 수정
  • 통합 테스트 검증
  • 수동 테스트 완료

🚨 확인해야 할 사항 (Checklist)

PR을 제출하기 전에 아래 항목들을 확인해주세요.

  • 코드 포매팅 완료
  • 불필요한 파일/코드 제거
  • 로직 검증 완료
  • 프로젝트 빌드 성공
  • 린트/정적 분석 통과 (해당 시)

🙋 기타 참고 사항

리뷰어가 참고하면 좋을 만한 추가 설명이 있다면 적어주세요.

Summary by CodeRabbit

Release Notes

  • New Features

    • 그룹 목록 조회 시 로그인한 사용자의 멤버십 정보를 표시합니다.
    • 미인증 사용자에 대해서는 멤버십 정보를 표시하지 않습니다.
    • 웹소켓 기반 실시간 채팅 테스트 페이지를 추가했습니다.
  • Tests

    • 그룹 목록 조회 API의 테스트 케이스를 추가했습니다.

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

사용자별 멤버십 정보를 추가하여 그룹 목록 응답을 더욱 풍부하게 만듭니다.
이를 통해 프런트엔드는 각 그룹의 멤버십 상태(예: "가입됨", "대기 중", "가입하지 않음")를 표시할 수 있으며,
더욱 개인화된 정보를 제공하여 사용자 경험을 향상시킵니다.
사용자가 로그인하지 않은 경우에도 원활하게 처리합니다.
@LimdaeIl LimdaeIl self-assigned this Dec 31, 2025
Copilot AI review requested due to automatic review settings December 31, 2025 08:16
@LimdaeIl LimdaeIl added the ✨enhancement New feature or request label Dec 31, 2025
@LimdaeIl LimdaeIl moved this from Backlog to In progress in WeGo-Together Backend Dec 31, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

Caution

Review failed

The pull request is closed.

요약

이 풀 리퀘스트는 그룹 목록 조회 API에 사용자별 멤버십 정보를 추가합니다. 컨트롤러에서 인증된 사용자의 ID를 추출하여 서비스로 전달하고, 서비스에서 저장소의 새로운 쿼리 메서드를 통해 사용자 멤버십 데이터를 조회한 후 응답 DTO에 포함시킵니다.

Changes

그룹 / 파일 요약
응답 DTO
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupListV2Response.java
MyMembership myMembership 필드 추가 및 of() 팩토리 메서드 시그니처 확장
서비스 계층
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java
getGroupListV2() 메서드에 userIdOrNull 매개변수 추가 및 멤버십 데이터 조회/매핑 로직 구현
저장소 계층
src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2Repository.java
findMyMembershipsByGroupIds() 쿼리 메서드 추가
컨트롤러 및 문서
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java, src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2ControllerDocs.java
@AuthenticationPrincipal 매개변수 추가 및 사용자 ID 추출 후 서비스 호출
테스트 파일
src/main/resources/static/chat-test.html, src/test/http/group/v2/v2-group-attend-approval-required.http
WebSocket 채팅 테스트 페이지 추가 및 그룹 목록 조회 테스트 스텝 추가

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Controller as GroupV2Controller
    participant Service as GroupV2Service
    participant UserRepo as GroupUserV2Repository
    participant Mapper as DTO Factory
    
    User->>Controller: GET /groups?keyword=...&size=20<br/>(with Auth Token)
    activate Controller
    Controller->>Controller: Extract userId from<br/>@AuthenticationPrincipal
    Controller->>Service: getGroupListV2(userId, keyword,<br/>cursor, size, filter, ...)
    deactivate Controller
    
    activate Service
    Service->>Service: Query groups with filters
    Service->>Service: Extract groupIds from results
    
    alt userId is not null
        Service->>UserRepo: findMyMembershipsByGroupIds<br/>(userId, groupIds)
        activate UserRepo
        UserRepo-->>Service: List<GroupUserV2><br/>(myMembershipMap)
        deactivate UserRepo
    else userId is null
        Service->>Service: myMembershipMap = empty
    end
    
    Service->>Mapper: For each group row,<br/>call GroupListItemV2Response.of()<br/>with myMembership from map
    activate Mapper
    Mapper-->>Service: GroupListItemV2Response<br/>(includes myMembership)
    deactivate Mapper
    
    Service-->>Service: Build GetGroupListV2Response
    deactivate Service
    
    Controller->>User: 200 OK<br/>GetGroupListV2Response<br/>(groups with myMembership)
Loading

예상 코드 리뷰 소요 시간

🎯 3 (Moderate) | ⏱️ ~20 minutes

관련 가능성 있는 PR

🐰 목록에 내 정보를 담아
서비스가 쿼리 뛰어다니며
멤버십 지도를 그려내고
응답에 담긴 나의 발자국
모임마다 함께 흐르네 ✨

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1f4a02 and 44c9fa1.

📒 Files selected for processing (7)
  • src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupListV2Response.java
  • src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java
  • src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2Repository.java
  • src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java
  • src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2ControllerDocs.java
  • src/main/resources/static/chat-test.html
  • src/test/http/group/v2/v2-group-attend-approval-required.http

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.

@LimdaeIl LimdaeIl merged commit 50e5e76 into main Dec 31, 2025
4 of 5 checks passed
@LimdaeIl LimdaeIl deleted the feat/get-group-list branch December 31, 2025 08:17
@github-project-automation github-project-automation bot moved this from In progress to Done in WeGo-Together Backend Dec 31, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the group list API (V2) to include user-specific membership information in the response. The primary change allows the frontend to display personalized membership status for each group (e.g., "joined", "pending", "not joined") by adding a myMembership field to each group item. The implementation gracefully handles both authenticated and unauthenticated users, returning null when users are not logged in or have no membership in a group.

Key changes:

  • Added optional CustomUserDetails parameter to the group list endpoint to support both authenticated and unauthenticated access
  • Implemented new repository query to fetch user memberships across multiple groups efficiently
  • Enhanced GroupListItemV2Response to include MyMembership data with status, role, and join timestamps

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
GroupV2Controller.java Added @AuthenticationPrincipal CustomUserDetails parameter and passes userId to service layer
GroupV2ControllerDocs.java Updated API documentation interface to include userDetails parameter
GroupUserV2Repository.java Added findMyMembershipsByGroupIds query method to fetch user memberships for multiple groups
GroupV2Service.java Implemented logic to fetch and map user memberships to group list items
GetGroupListV2Response.java Added myMembership field to GroupListItemV2Response
chat-test.html Added WebSocket/STOMP chat testing utility (unrelated to PR purpose)
v2-group-attend-approval-required.http Added test cases for second member and group list verification

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +221
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Chat WS Test (SockJS + STOMP)</title>

<!-- SockJS -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<!-- STOMP -->
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7/bundles/stomp.umd.min.js"></script>

<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, "Noto Sans KR", sans-serif; margin: 16px; }
.row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
input, button, textarea { padding: 8px; font-size: 14px; }
input, textarea { width: 100%; max-width: 680px; }
.box { border: 1px solid #ddd; padding: 12px; border-radius: 10px; max-width: 720px; }
.log { height: 320px; overflow: auto; background: #0b1020; color: #d7e2ff; padding: 10px; border-radius: 10px; white-space: pre-wrap; }
.tag { font-size: 12px; opacity: .8; }
button { cursor: pointer; }
.status { font-weight: 700; }
</style>
</head>

<body>
<h2>Chat WebSocket Test (SockJS + STOMP)</h2>

<div class="box">
<div class="row">
<div style="flex:1; min-width:260px;">
<div class="tag">SockJS URL</div>
<input id="wsUrl" value="http://localhost:8080/ws-chat" />
</div>
<div style="flex:1; min-width:260px;">
<div class="tag">Access Token (Bearer 제외)</div>
<input id="token" placeholder="eyJhbGciOiJIUzI1NiJ9..." />
</div>
</div>

<div class="row">
<div style="flex:1; min-width:180px;">
<div class="tag">roomId</div>
<input id="roomId" type="number" value="1" />
</div>
<div style="flex:1; min-width:180px;">
<div class="tag">userId (개인 알림용)</div>
<input id="userId" type="number" value="1" />
</div>
<div style="display:flex; align-items:flex-end; gap:8px;">
<button id="btnConnect">Connect</button>
<button id="btnDisconnect" disabled>Disconnect</button>
</div>
</div>

<div class="row">
<div class="status">Status: <span id="statusText">DISCONNECTED</span></div>
</div>

<hr/>

<div class="row">
<div style="flex:1; min-width:260px;">
<div class="tag">Message Content</div>
<input id="msg" placeholder="안녕하세요!" />
</div>
<div style="display:flex; align-items:flex-end; gap:8px;">
<button id="btnSend" disabled>Send</button>
<button id="btnClear">Clear Log</button>
</div>
</div>

<div class="tag">Log</div>
<div id="log" class="log"></div>
</div>

<script>
const $ = (id) => document.getElementById(id);

let stompClient = null;
let roomSub = null;
let userSub = null;

function log(...args) {
const line = args.map(a => typeof a === 'string' ? a : JSON.stringify(a, null, 2)).join(' ');
const el = $('log');
el.textContent += line + "\n";
el.scrollTop = el.scrollHeight;
console.log(...args);
}

function setStatus(s) {
$('statusText').textContent = s;
}

function setUiConnected(connected) {
$('btnConnect').disabled = connected;
$('btnDisconnect').disabled = !connected;
$('btnSend').disabled = !connected;
}

function connect() {
const wsUrl = $('wsUrl').value.trim();
const token = $('token').value.trim();
const roomId = Number($('roomId').value);
const userId = Number($('userId').value);

if (!wsUrl) return alert("wsUrl을 입력하세요.");
if (!token) return alert("accessToken을 입력하세요.");

setStatus("CONNECTING...");
log("[CONNECT] wsUrl =", wsUrl);

// SockJS 연결
const socket = new SockJS(wsUrl);

// STOMP client 생성
stompClient = new StompJs.Client({
webSocketFactory: () => socket,
connectHeaders: {
Authorization: `Bearer ${token}`,
},
debug: (str) => log("[STOMP]", str),
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});

stompClient.onConnect = (frame) => {
setStatus("CONNECTED");
setUiConnected(true);
log("[CONNECTED]", frame.headers);

// room 구독
const roomDest = `/sub/chat/room/${roomId}`;
roomSub = stompClient.subscribe(roomDest, (message) => {
try {
const payload = JSON.parse(message.body);
log(`[ROOM ${roomId}]`, payload);
} catch (e) {
log(`[ROOM ${roomId}] raw:`, message.body);
}
});
log("[SUBSCRIBE]", roomDest);

// user 알림 구독
const userDest = `/sub/user/${userId}`;
userSub = stompClient.subscribe(userDest, (message) => {
try {
const payload = JSON.parse(message.body);
log(`[USER ${userId}]`, payload);
} catch (e) {
log(`[USER ${userId}] raw:`, message.body);
}
});
log("[SUBSCRIBE]", userDest);
};

stompClient.onDisconnect = () => {
setStatus("DISCONNECTED");
setUiConnected(false);
log("[DISCONNECTED]");
};

stompClient.onStompError = (frame) => {
log("[STOMP ERROR]", frame.headers, frame.body);
};

stompClient.onWebSocketError = (evt) => {
log("[WS ERROR]", evt);
};

stompClient.onWebSocketClose = (evt) => {
log("[WS CLOSED]", evt.code, evt.reason);
setStatus("DISCONNECTED");
setUiConnected(false);
};

stompClient.activate();
}

function disconnect() {
if (roomSub) { roomSub.unsubscribe(); roomSub = null; }
if (userSub) { userSub.unsubscribe(); userSub = null; }

if (stompClient) {
log("[DISCONNECT] deactivating...");
stompClient.deactivate();
stompClient = null;
}
setStatus("DISCONNECTED");
setUiConnected(false);
}

function sendMessage() {
if (!stompClient || !stompClient.connected) {
alert("연결되어 있지 않습니다.");
return;
}
const roomId = Number($('roomId').value);
const content = $('msg').value;

const body = JSON.stringify({ chatRoomId: roomId, content });
stompClient.publish({
destination: "/pub/chat/message",
body,
});
log("[SEND]", body);
}

$('btnConnect').addEventListener('click', connect);
$('btnDisconnect').addEventListener('click', disconnect);
$('btnSend').addEventListener('click', sendMessage);
$('btnClear').addEventListener('click', () => $('log').textContent = "");

window.addEventListener('beforeunload', () => {
try { disconnect(); } catch (_) {}
});
</script>
</body>
</html>
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

This chat WebSocket test HTML file appears to be unrelated to the PR's stated purpose of adding membership information to group list responses. Consider moving this to a separate PR focused on chat/WebSocket functionality to maintain clearer change history and easier code review.

Copilot uses AI. Check for mistakes.
where gu.user.id = :userId
and g.id in :groupIds
""")
List<GroupUserV2> findMyMembershipsByGroupIds(Long userId, List<Long> groupIds);
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The query parameters are missing @param annotations. JPA requires @param annotations for named parameters in @query when using JPQL. Add @param("userId") and @param("groupIds") to the method parameters to ensure proper parameter binding.

Suggested change
List<GroupUserV2> findMyMembershipsByGroupIds(Long userId, List<Long> groupIds);
List<GroupUserV2> findMyMembershipsByGroupIds(@Param("userId") Long userId,
@Param("groupIds") List<Long> groupIds);

Copilot uses AI. Check for mistakes.
@Query("""
select gu
from GroupUserV2 gu
join fetch gu.group g
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The query should eager fetch the user entity to avoid potential LazyInitializationException. While the group is already being fetched, the user relationship might be accessed when creating MyMembership objects outside the transaction context. Consider adding "join fetch gu.user u" to ensure the user entity is loaded with the GroupUserV2.

Suggested change
join fetch gu.group g
join fetch gu.group g
join fetch gu.user u

Copilot uses AI. Check for mistakes.
.collect(java.util.stream.Collectors.toMap(
gu -> gu.getGroup().getId(),
MyMembership::from,
(a, b) -> a
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The merge function "(a, b) -> a" in the toMap collector silently discards duplicate entries. If a user has multiple memberships for the same group (which shouldn't happen but could occur due to data inconsistency), only the first one will be kept without any logging or warning. Consider adding validation or logging to detect such cases, or use a more explicit merge strategy.

Suggested change
(a, b) -> a
(a, b) -> {
log.warn("Duplicate group membership detected for userId={} in group list; keeping the first membership and discarding the others.", userIdOrNull);
return a;
}

Copilot uses AI. Check for mistakes.
{} No newline at end of file
{}

### 6-1. V2 목록 첫 페이지 (기본: filter=ACTIVE, keyword 없음)
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The test case numbering is inconsistent. Test "6-1" appears after tests "7" and "8", which breaks the logical sequence. Consider renumbering this to "9" or reorganizing the test cases to maintain sequential order.

Suggested change
### 6-1. V2 목록 첫 페이지 (기본: filter=ACTIVE, keyword 없음)
### 9. V2 목록 첫 페이지 (기본: filter=ACTIVE, keyword 없음)

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +154
final Map<Long, MyMembership> myMembershipMap =
(userIdOrNull == null)
? Map.of()
: groupUserV2Repository.findMyMembershipsByGroupIds(userIdOrNull, groupIds)
.stream()
.collect(java.util.stream.Collectors.toMap(
gu -> gu.getGroup().getId(),
MyMembership::from,
(a, b) -> a
));
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

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

The new functionality to include user membership information in the group list response lacks automated test coverage. Consider adding tests to verify: 1) membership information is correctly populated for authenticated users, 2) null is returned for unauthenticated users, 3) null is returned when user has no membership in a group, and 4) the correct membership details (status, role, joinedAt) are mapped from GroupUserV2 entities.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEAT] 모임 목록 조회에서 나의 정보 제공

1 participant