Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/apidoc/04-shared-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

## 2. GROUP-01 내 공유 그룹 목록 조회

활성 멤버십의 참여일시 내림차순으로 조회하며 동일 시각은 공유 그룹 ID로 순서를 고정한다.
활성 멤버십의 참여일시 내림차순으로 조회하며 동일 시각은 공유 그룹 ID로 순서를 고정한다. 각 항목의 `memberNames`는 삭제되지 않은 멤버의 이름을 참여일시 오름차순, 멤버십 ID 오름차순으로 반환한다.

### Request

Expand All @@ -42,6 +42,7 @@
"name": "우리 집",
"myRole": "HOST",
"memberCount": 4,
"memberNames": ["집집이", "홍길동", "김집집", "이사진"],
"sharedAlbumCount": 3,
"photoCount": 128,
"joinedAt": "2026-07-03T10:15:30Z",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
$kind: http-request
url: "{{baseUrl}}/api/v1/shared-groups?size={{pageSize}}"
method: GET
description: 각 공유 그룹 항목의 memberNames에서 활성 멤버 이름을 참여 순서대로 확인한다.
headers:
Authorization: "Bearer {{accessToken}}"
scripts:
Expand All @@ -9,5 +10,19 @@ scripts:
pm.test('공유 그룹 목록 조회 응답 상태가 200이다', function () {
pm.response.to.have.status(200);
});

pm.test('각 공유 그룹에 활성 멤버 이름 목록이 포함된다', function () {
const response = pm.response.json();
const items = response?.data?.items ?? [];

items.forEach(function (item) {
pm.expect(item).to.have.property('memberNames');
pm.expect(item.memberNames).to.be.an('array');
pm.expect(item.memberNames).to.have.lengthOf(item.memberCount);
item.memberNames.forEach(function (memberName) {
pm.expect(memberName).to.be.a('string').and.not.empty;
});
});
});
language: text/javascript
order: 1000
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ public class SharedGroupController {
@Operation(
summary = "내 공유 그룹 목록 조회",
description =
"현재 사용자의 활성 멤버십만 참여일시 내림차순으로 조회합니다. 다음 페이지 요청에는 이전"
+ " 응답의 nextCursor를 수정하지 않고 그대로 전달합니다.")
"현재 사용자의 활성 멤버십만 참여일시 내림차순으로 조회합니다. 각 항목의 memberNames에는"
+ " 삭제되지 않은 멤버의 이름을 참여일시·멤버십 ID 오름차순으로 반환합니다. 다음 페이지"
+ " 요청에는 이전 응답의 nextCursor를 수정하지 않고 그대로 전달합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "400", description = "유효하지 않은 커서 또는 size"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import org.zipzip.zipzipserver.domain.sharedgroup.entity.SharedGroupRole;

Expand All @@ -12,6 +13,8 @@ public record SharedGroupSummaryResponse(
@Schema(description = "공유 그룹 이름", example = "우리 집") String name,
@Schema(description = "요청자의 현재 공유 그룹 역할", example = "HOST") SharedGroupRole myRole,
@Schema(description = "활성 멤버 수", example = "4") long memberCount,
@Schema(description = "활성 멤버 이름 목록. 참여일시 오름차순, 멤버십 ID 오름차순", example = "[\"집집이\", \"홍길동\"]")
List<String> memberNames,
@Schema(description = "활성 공유집 수", example = "3") long sharedAlbumCount,
@Schema(description = "활성 사진 수", example = "128") long photoCount,
@Schema(description = "요청자의 그룹 참여 시각(UTC ISO-8601)", example = "2026-07-03T10:15:30Z")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -48,11 +49,8 @@ public List<SharedGroupListRow> findMySharedGroups(
sgm.role as my_role,
sgm.created_at as joined_at,
sg.updated_at,
(
select count(*)
from shared_group_membership member_sgm
where member_sgm.shared_group_id = sg.id
) as member_count,
coalesce(members.member_count, 0) as member_count,
coalesce(members.member_names, array[]::varchar[]) as member_names,
(
select count(*)
from shared_album sa
Expand All @@ -70,6 +68,18 @@ select count(distinct sap.photo_id)
) as photo_count
from shared_group_membership sgm
join shared_group sg on sg.id = sgm.shared_group_id
left join lateral (
select
count(*) as member_count,
array_agg(
member_user.display_name
order by member_sgm.created_at asc, member_sgm.id asc
) as member_names
from shared_group_membership member_sgm
join app_user member_user on member_user.id = member_sgm.app_user_id
where member_sgm.shared_group_id = sg.id
and member_user.deleted_at is null
) members on true
where sgm.app_user_id = :appUserId
and sg.deleted_at is null
"""
Expand Down Expand Up @@ -133,6 +143,7 @@ private SharedGroupListRow mapListRow(ResultSet resultSet, int rowNumber) throws
resultSet.getString("name"),
SharedGroupRole.valueOf(resultSet.getString("my_role")),
resultSet.getLong("member_count"),
Arrays.asList((String[]) resultSet.getArray("member_names").getArray()),
resultSet.getLong("shared_album_count"),
resultSet.getLong("photo_count"),
resultSet.getTimestamp("joined_at").toInstant(),
Expand All @@ -159,6 +170,7 @@ public record SharedGroupListRow(
String name,
SharedGroupRole myRole,
long memberCount,
List<String> memberNames,
long sharedAlbumCount,
long photoCount,
Instant joinedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ private SharedGroupSummaryResponse toSummaryResponse(
row.name(),
row.myRole(),
row.memberCount(),
row.memberNames(),
row.sharedAlbumCount(),
row.photoCount(),
row.joinedAt(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.zipzip.zipzipserver.domain.sharedgroup.code.SharedGroupSuccessCode;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.response.CreateSharedGroupResponse;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.response.SharedGroupListResponse;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.response.SharedGroupSummaryResponse;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.response.SharedGroupUpdateResponse;
import org.zipzip.zipzipserver.domain.sharedgroup.entity.SharedGroupRole;
import org.zipzip.zipzipserver.domain.sharedgroup.service.SharedGroupService;
Expand Down Expand Up @@ -62,12 +63,29 @@ class SharedGroupControllerTest {
@Test
void 내_공유_그룹_목록을_조회한다() throws Exception {
givenAuthenticatedUser();
UUID sharedGroupId = UUID.fromString("22222222-2222-2222-2222-222222222222");
when(sharedGroupService.findMySharedGroups(APP_USER_ID, null, null))
.thenReturn(new SharedGroupListResponse(List.of(), null, false));
.thenReturn(
new SharedGroupListResponse(
List.of(
new SharedGroupSummaryResponse(
sharedGroupId,
"우리 집",
SharedGroupRole.HOST,
2,
List.of("집집이", "홍길동"),
1,
10,
Instant.parse("2026-07-10T00:00:00Z"),
Instant.parse("2026-07-11T00:00:00Z"))),
null,
false));

mockMvc.perform(get("/api/v1/shared-groups").header("Authorization", bearerToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("SHARED_GROUP_LIST_FOUND"))
.andExpect(jsonPath("$.data.items[0].memberNames[0]").value("집집이"))
.andExpect(jsonPath("$.data.items[0].memberNames[1]").value("홍길동"))
.andExpect(jsonPath("$.data.hasNext").value(false));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class SharedGroupQueryRepositoryPostgresTest {
UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
private static final UUID DELETED_GROUP_MEMBERSHIP_ID =
UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
private static final UUID OTHER_MEMBERSHIP_ID =
UUID.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee");
private static final UUID FIRST_ALBUM_PHOTO_ID =
UUID.fromString("cccccccc-cccc-cccc-cccc-cccccccccccc");
private static final UUID SECOND_ALBUM_PHOTO_ID =
Expand Down Expand Up @@ -84,6 +86,8 @@ static void registerProperties(DynamicPropertyRegistry registry) {
assertThat(rows)
.extracting(SharedGroupQueryRepository.SharedGroupListRow::id)
.containsExactly(FIRST_GROUP_ID, SECOND_GROUP_ID);
assertThat(rows.get(0).memberNames()).containsExactly("집집이", "다른 사용자");
assertThat(rows.get(0).memberCount()).isEqualTo(2);
}

@Test
Expand All @@ -93,7 +97,7 @@ static void registerProperties(DynamicPropertyRegistry registry) {
SharedGroupQueryRepository.SharedGroupDetailRow row =
sharedGroupQueryRepository.findDetail(APP_USER_ID, FIRST_GROUP_ID).orElseThrow();

assertThat(row.memberCount()).isEqualTo(1);
assertThat(row.memberCount()).isEqualTo(2);
assertThat(row.sharedAlbumCount()).isEqualTo(2);
assertThat(row.photoCount()).isEqualTo(1);
}
Expand Down Expand Up @@ -140,6 +144,7 @@ insert into shared_group_membership (
values
(?, ?, ?, 'HOST', ?, now()),
(?, ?, ?, 'MEMBER', ?, now()),
(?, ?, ?, 'MEMBER', ?, now()),
(?, ?, ?, 'MEMBER', ?, now())
on conflict (shared_group_id, app_user_id) do nothing
""",
Expand All @@ -151,6 +156,10 @@ on conflict (shared_group_id, app_user_id) do nothing
FIRST_GROUP_ID,
APP_USER_ID,
Timestamp.from(Instant.parse("2026-07-10T00:00:00Z")),
OTHER_MEMBERSHIP_ID,
FIRST_GROUP_ID,
OTHER_USER_ID,
Timestamp.from(Instant.parse("2026-07-10T00:00:00Z")),
DELETED_GROUP_MEMBERSHIP_ID,
DELETED_GROUP_ID,
APP_USER_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
Expand All @@ -25,6 +26,7 @@
import org.zipzip.zipzipserver.domain.sharedgroup.code.SharedGroupErrorCode;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.request.CreateSharedGroupRequest;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.response.CreateSharedGroupResponse;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.response.SharedGroupListResponse;
import org.zipzip.zipzipserver.domain.sharedgroup.dto.response.SharedGroupUpdateResponse;
import org.zipzip.zipzipserver.domain.sharedgroup.entity.InviteCodeReservation;
import org.zipzip.zipzipserver.domain.sharedgroup.entity.SharedGroup;
Expand Down Expand Up @@ -57,6 +59,36 @@ class SharedGroupServiceTest {

@InjectMocks private SharedGroupService sharedGroupService;

@Test
void 공유_그룹_목록에_활성_멤버_이름을_반환한다() {
AppUser appUser = AppUser.create("apple-subject", "집집이");
UUID sharedGroupId = UUID.fromString("22222222-2222-2222-2222-222222222222");
Instant joinedAt = Instant.parse("2026-07-10T00:00:00Z");
Instant updatedAt = Instant.parse("2026-07-11T00:00:00Z");
SharedGroupQueryRepository.SharedGroupListRow row =
new SharedGroupQueryRepository.SharedGroupListRow(
sharedGroupId,
"우리 집",
SharedGroupRole.HOST,
2,
List.of("집집이", "홍길동"),
1,
10,
joinedAt,
updatedAt);
when(appUserRepository.findByIdAndDeletedAtIsNull(APP_USER_ID))
.thenReturn(Optional.of(appUser));
when(sharedGroupQueryRepository.findMySharedGroups(APP_USER_ID, null, null, 21))
.thenReturn(List.of(row));

SharedGroupListResponse response =
sharedGroupService.findMySharedGroups(APP_USER_ID, null, null);

assertThat(response.items()).hasSize(1);
assertThat(response.items().get(0).memberNames()).containsExactly("집집이", "홍길동");
assertThat(response.items().get(0).memberCount()).isEqualTo(2);
}

@Test
void 공유_그룹_생성_시_초대_코드와_HOST_멤버십을_생성한다() {
AppUser appUser = AppUser.create("apple-subject", "집집이");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,43 @@ class SwaggerOpenApiIntegrationTest {
assertThat(photoOperations.has("delete")).isFalse();
}

@Test
void 공유_그룹_목록_Swagger_스키마는_멤버_이름_목록을_노출한다() throws Exception {
String body =
mockMvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
JsonNode document = objectMapper.readTree(body);

JsonNode memberNamesSchema =
document.path("components")
.path("schemas")
.path("SharedGroupSummaryResponse")
.path("properties")
.path("memberNames");
JsonNode memberNamesExample =
document.path("paths")
.path("/api/v1/shared-groups")
.path("get")
.path("responses")
.path("200")
.path("content")
.path("application/json")
.path("example")
.path("data")
.path("items")
.path(0)
.path("memberNames");

assertThat(memberNamesSchema.path("type").asText()).isEqualTo("array");
assertThat(memberNamesSchema.path("items").path("type").asText()).isEqualTo("string");
assertThat(memberNamesSchema.path("description").asText()).contains("참여일시");
assertThat(memberNamesExample.isArray()).isTrue();
assertThat(memberNamesExample).isNotEmpty();
}

private void assertNoDefaultStatusExample(JsonNode node) {
if (node.isObject()) {
Iterator<Entry<String, JsonNode>> fields = node.fields();
Expand Down
Loading