Skip to content

V2 Group API

DAEILLIM edited this page Dec 29, 2025 · 17 revisions

1. 모임 이미지 사전 업로드 (Pre-upload)

POST /api/v2/groups/images/upload

모임 생성 또는 수정 전에 이미지를 사전 업로드하는 API입니다.
업로드된 파일은 즉시 저장소(S3)에 업로드되며, 서버는 업로드 결과로 imageKey + 리사이즈된 URL 세트(440x240, 100x100) 를 반환합니다.

이 API에서 발급된 imageKey는 이후 모임 생성 / 모임 수정에서만 1회 사용 가능(consumed) 합니다.

📌 역할 분리(중요)

  • Pre-upload 단계의 sortOrder는 “업로드 요청 배열의 순서”를 그대로 반영한 값입니다.
  • 최종 정렬/대표 이미지 확정은 Create/Update 요청의 images.sortOrder 규칙이 책임집니다.

📥 Request

Headers

이름
Authorization Bearer {JWT}
Content-Type multipart/form-data

Body (multipart/form-data)

필드명 타입 필수 설명
images File[] O 업로드할 이미지 파일 목록 (1~3장)

📌 업로드 정책

  • 이미지 개수는 최소 1장, 최대 3장
  • images 필드가 없거나 빈 배열이면 오류
  • 업로드된 이미지는 서버에서 WEBP 포맷으로 변환됩니다.
  • 서버는 업로드된 이미지 1장당 아래 2가지 변형(Variant)을 생성합니다.
    • CARD: 440x240 (WEBP)
    • THUMBNAIL: 100x100 (WEBP)
  • 업로드 요청 내 이미지 순서가 pre-upload 응답의 sortOrder(0부터) 로 고정됩니다.

📌 업로드 제한 (추가)

  • 허용 MIME 타입: image/jpeg, image/png, image/webp
  • 파일 최대 크기: 각 파일 5MB 이하
  • 요청 총합 크기: 최대 15MB
  • 이미지 최소 해상도(권장): 가로 440px 이상
    • 최소 해상도 미만 파일은 오류로 처리될 수 있습니다.

📥 Example Request

POST /api/v2/groups/images/upload
Authorization: Bearer {JWT}
Content-Type: multipart/form-data

images: file1
images: file2

📤 Response (201 Created)

{
  "status": 201,
  "success": true,
  "data": {
    "images": [
      {
        "imageKey": "8f982cf4-42a3-4bad-b1d8-e870026bd013",
        "sortOrder": 0,
        "imageUrl440x240": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251222141747_0_8f982cf4-42a3-4bad-b1d8-e870026bd013_440x240.webp",
        "imageUrl100x100": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251222141747_0_8f982cf4-42a3-4bad-b1d8-e870026bd013_100x100.webp"
      },
      {
        "imageKey": "e7e36318-b34d-41d4-aaed-c200e0db6fc4",
        "sortOrder": 1,
        "imageUrl440x240": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251222141748_1_e7e36318-b34d-41d4-aaed-c200e0db6fc4_440x240.webp",
        "imageUrl100x100": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251222141748_1_e7e36318-b34d-41d4-aaed-c200e0db6fc4_100x100.webp"
      }
    ]
  }
}

📌 Response 모델

PreUploadGroupImageV2Response

필드 타입 설명
images PreUploadGroupImageV2Item[] 업로드 결과 이미지 목록

PreUploadGroupImageV2Item

필드 타입 설명
imageKey String 이후 create/update에서 사용하는 키
sortOrder Integer 업로드 요청 내 이미지 순서(0부터)
imageUrl440x240 String 카드용 이미지 URL
imageUrl100x100 String 썸네일 이미지 URL

🧾 정책 및 주의사항 (필독)

  1. imageKey는 “사전 업로드 결과 키”입니다.
  • 모임 생성/수정 시 서버는 imageKey를 통해 서버가 보증한 이미지 URL 세트를 확정 등록합니다.
  1. 유효기간(TTL)은 2시간입니다.
  • 2시간이 지나면 해당 imageKey는 서버에서 찾을 수 없어 사용 불가합니다.
  1. 업로더만 사용할 수 있습니다.
  • imageKey에는 uploaderId가 함께 저장됩니다.
  • 모임 생성/수정 요청자와 uploaderId가 다르면 오류가 발생합니다.
  1. imageKey는 1회성(consumed)입니다.
  • 모임 생성/수정에서 사용되는 순간 Redis에서 조회 후 즉시 삭제(getAndDelete) 됩니다.
  • 한 번 사용되었거나 만료된 imageKey는 다시 사용할 수 없습니다.
  1. 최대 업로드 3장 제한
  • 4장 이상 업로드 요청 시 오류로 처리됩니다.

❗ 오류 케이스 (대표)

에러 응답 포맷은 프로젝트 공통 규격을 따릅니다.

HTTP ErrorCode 조건
401 UNAUTHORIZED JWT 없음 / 만료 / 위조
400 INVALID_MULTIPART_REQUEST multipart 형식 오류 / images 파트 누락
400 INVALID_IMAGE_COUNT images가 빈 배열(0장 업로드)
400 IMAGE_UPLOAD_EXCEED 이미지 3장 초과
400 IMAGE_UPLOAD_FAILED 이미지 변환, 리사이즈, S3 업로드 실패
415 UNSUPPORTED_IMAGE_TYPE 허용되지 않은 MIME 타입
413 IMAGE_FILE_TOO_LARGE 파일 크기/요청 총합 크기 제한 초과

2. 모임 생성

POST /api/v2/groups/create

사전 업로드된 이미지(imageKey)를 사용하여 새로운 모임을 생성하는 API입니다.
모임 생성에 성공하면, 요청자는 자동으로 HOST 역할 + ATTEND 상태로 모임에 참여합니다.

또한 운영 안정성을 위해, 같은 사용자가 연속으로 모임을 생성하지 못하도록 생성 쿨다운 정책이 적용됩니다.


📥 Request

Headers

이름
Authorization Bearer {JWT}
Content-Type application/json

📥 Body – CreateGroupV2Request (Example)

⚠️ 반드시 사전 업로드 응답의 images[].imageKey를 전역 변수로 저장한 뒤, 아래 images 배열을 구성합니다. ⚠️ images: [] 를 “빈 배열”로 보내지 마세요. (이미지를 쓰지 않을 거면 필드를 생략하거나 null 로 처리)

{
  "title": "강남에서 하는 자바 스터디",
  "location": "서울 강남구",
  "joinPolicy": "FREE",
  "locationDetail": "강남역 2번 출구 근처 카페",
  "startTime": "2026-12-10T19:00:00",
  "endTime": "2026-12-10T21:00:00",
  "tags": [
    "자바",
    "백엔드",
    "스터디"
  ],
  "description": "사전 업로드 imageKey를 사용하여 모임을 생성합니다.",
  "maxParticipants": 12,
  "images": [
    {
      "sortOrder": 0,
      "imageKey": "{{img0_key}}"
    },
    {
      "sortOrder": 1,
      "imageKey": "{{img1_key}}"
    }
  ]
}

✅ 요청 필드 설명

필드 타입 필수 설명
title String O 모임 제목 (1~50자, 공백 불가)
location String O 모임 위치
joinPolicy String O 모임 참여 정책
locationDetail String X 상세 위치
startTime LocalDateTime O 시작 시간(현재 이후)
endTime LocalDateTime X 종료 시간(startTime 이후)
tags String[] X 태그(최대 10개, 중복 불가)
description String O 모임 설명 (1~300자)
maxParticipants Integer O 최대 인원 (2~12)
images CreateGroupImageV2Request[] X 사전 업로드 이미지 목록 (최대 3장)

📌 CreateGroupImageV2Request

필드 타입 필수 설명
imageKey String O 사전 업로드로 발급된 imageKey
sortOrder Integer X 이미지 순서(0 이상). 없으면 서버가 자동 부여

🧾 이미지 등록 정책 (생성 시)

  • images선택 항목입니다.
    • 생략 또는 null → 이미지 없음
    • images = []허용하지 않음(오류)
      • “이미지 없음”은 필드 생략 또는 null로 통일합니다.
  • 이미지가 있다면 최대 3장까지 가능합니다.
  • 각 이미지 항목에는 반드시 imageKey가 필요합니다.
  • sortOrder 정책
    • 모든 항목의 sortOrder가 없으면 → 요청 배열 순서대로 0, 1, 2 … 자동 부여
    • 하나라도 sortOrder가 존재하면 → sortOrder 오름차순으로 최종 정렬
    • 요청 내 sortOrder 중복은 허용되지 않습니다.
  • imageKey는 반드시
    • 사전 업로드(pre-upload) API에서 발급된 값이어야 하며
    • 업로더(uploaderId)와 요청자가 동일해야 하고
    • 생성 과정에서 1회 사용 후 즉시 consume 됩니다.
  • imageKey요청 내 중복 불가입니다.

🧾 태그 정책 (생성 시)

  • tags는 선택 항목입니다.
  • 전달된 태그는 서버에서 findOrCreate 방식으로 처리됩니다.
  • 정책
    • 최대 10개
    • 중복 불가
    • 공백/빈 문자열 제거 후 처리

📤 Response (201 Created)

{
  "status": 201,
  "success": true,
  "data": {
    "id": 2,
    "title": "강남에서 하는 자바 스터디",
    "joinPolicy": "FREE",
    "status": "RECRUITING",
    "address": {
      "location": "서울 강남구",
      "locationDetail": "강남역 2번 출구 근처 카페"
    },
    "startTime": "2026-12-10T19:00:00",
    "endTime": "2026-12-10T21:00:00",
    "tags": [
      "자바",
      "백엔드",
      "스터디"
    ],
    "description": "사전 업로드 imageKey를 사용하여 모임을 생성합니다.",
    "participantCount": 1,
    "maxParticipants": 12,
    "createdBy": {
      "userId": 102,
      "nickName": "Beemo",
      "profileImage": null,
      "profileMessage": null
    },
    "myMembership": {
      "groupUserId": 2,
      "userId": 102,
      "role": "HOST",
      "status": "ATTEND",
      "joinedAt": "2025-12-23T17:07:35.269796",
      "leftAt": null
    },
    "createdAt": "2025-12-23T17:07:35.2783251",
    "updatedAt": "2025-12-23T17:07:35.2783251",
    "images": [
      {
        "groupImageId": 5,
        "imageKey": "1624e5ec-843d-477d-afb0-9bea75a4f9d3",
        "sortOrder": 0,
        "variants": [
          {
            "variantId": 9,
            "type": "CARD_440_240",
            "width": 440,
            "height": 240,
            "format": "WEBP",
            "imageUrl": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251223170731_0_1624e5ec-843d-477d-afb0-9bea75a4f9d3_440x240.webp"
          },
          {
            "variantId": 10,
            "type": "THUMBNAIL_100_100",
            "width": 100,
            "height": 100,
            "format": "WEBP",
            "imageUrl": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251223170731_0_1624e5ec-843d-477d-afb0-9bea75a4f9d3_100x100.webp"
          }
        ]
      }
    ]
  }
}

🧾 특징 및 주의사항

  1. 생성 쿨다운 정책
  • 같은 사용자는 연속으로 모임을 생성할 수 없습니다.
  • 쿨다운 중 재시도하면 오류가 발생합니다.
  • 생성 트랜잭션이 실패(롤백)되면 쿨다운도 적용되지 않습니다.
  1. 호스트 자동 참여
  • 모임 생성자는 자동으로 HOST + ATTEND 상태로 참여합니다.
  1. 이미지 pre-upload key는 생성 시 consume
  • 사용된 imageKey는 즉시 삭제되며 재사용할 수 없습니다.

❗ 오류 케이스 (대표)

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
400 VALIDATION_FAILED 필드 유효성 검증 실패
404 USER_NOT_FOUND 생성자(호스트) 조회 실패
409 GROUP_CREATE_COOLDOWN 생성 쿨다운 활성화
400 IMAGE_KEY_NOT_FOUND pre-upload key 만료/이미 사용됨
403 IMAGE_KEY_UPLOADER_MISMATCH 업로더와 요청자 불일치
400 IMAGE_UPLOAD_EXCEED 이미지 3장 초과
400 DUPLICATED_IMAGE_SORT_ORDER_IN_REQUEST sortOrder 중복
400 DUPLICATED_IMAGE_KEY_IN_REQUEST imageKey 중복
400 INVALID_GROUP_IMAGE_ITEM images 항목 구조 오류
400 INVALID_GROUP_IMAGE_KEY imageKey null/blank
400 INVALID_GROUP_IMAGE_EMPTY_LIST images가 빈 배열로 전달됨

3. 모임 상세 조회

GET /api/v2/groups/{groupId}

특정 모임의 상세 정보를 조회하는 API입니다.
비로그인 사용자도 조회할 수 있으며, 로그인 여부 및 조회자의 역할(HOST 여부)에 따라 응답에 포함되는 정보 범위가 달라집니다.

  • 비로그인 사용자: 모임 기본 정보 + 이미지 + 태그 + 참여자 요약 정보 조회 가능
  • 로그인 사용자: myMembership(내 참여 정보) 제공
  • 호스트(HOST): joinedMembers모든 멤버 상태 노출
  • 호스트가 아닌 사용자: joinedMembersATTEND 상태만 노출

📥 Request

GET /api/v2/groups/{groupId}

Headers (선택)

비로그인 조회가 가능하므로 Authorization 헤더는 선택입니다.

이름 필수
Authorization Bearer {JWT} X

Path Variables

이름 타입 필수 설명
groupId Long O 조회할 모임 ID

📥 Example Request

GET /api/v2/groups/10
Authorization: Bearer {JWT}   // 로그인한 경우에만 전달

📤 Response (200 OK)

⚠️ 응답의 images항상 sortOrder 오름차순으로 정렬되며, sortOrder=0이 대표 이미지입니다. ⚠️ 비로그인인 경우 myMembership반드시 null 입니다.

{
  "status": 200,
  "success": true,
  "data": {
    "id": 1,
    "title": "참여 취소 테스트용 자바 스터디",
    "joinPolicy": "FREE",
    "status": "RECRUITING",
    "address": {
      "location": "서울 서초구",
      "locationDetail": "교대역 1번 출구 근처 카페"
    },
    "startTime": "2026-12-20T19:00:00",
    "endTime": "2026-12-20T21:00:00",
    "images": [
      {
        "groupImageId": 1,
        "imageKey": "12a420df-708e-43d3-8689-8b92b5b99419",
        "sortOrder": 0,
        "variants": [
          {
            "variantId": 1,
            "type": "CARD_440_240",
            "width": 440,
            "height": 240,
            "format": "WEBP",
            "imageUrl": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251223172948_0_12a420df-708e-43d3-8689-8b92b5b99419_440x240.webp"
          },
          {
            "variantId": 2,
            "type": "THUMBNAIL_100_100",
            "width": 100,
            "height": 100,
            "format": "WEBP",
            "imageUrl": "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251223172948_0_12a420df-708e-43d3-8689-8b92b5b99419_100x100.webp"
          }
        ]
      }
    ],
    "tags": ["참여취소테스트", "자바"],
    "description": "참여 취소/중복 취소/HOST 취소 예외를 테스트하는 모임입니다.",
    "participantCount": 3,
    "maxParticipants": 5,
    "createdBy": {
      "userId": 102,
      "nickName": "HostTwo",
      "profileImage": null,
      "profileMessage": null
    },
    "createdAt": "2025-12-23T17:29:52.273873",
    "updatedAt": "2025-12-23T17:29:52.273873",
    "myMembership": {
      "groupUserId": 3,
      "role": "MEMBER",
      "status": "ATTEND",
      "joinedAt": "2025-12-23T17:30:09.180863",
      "leftAt": null
    },
    "joinedMembers": [
      {
        "userId": 102,
        "groupUserId": 1,
        "role": "HOST",
        "status": "ATTEND",
        "nickName": "HostTwo",
        "profileImage": null,
        "joinedAt": "2025-12-23T17:29:52.255956",
        "leftAt": null
      }
    ]
  }
}

✅ 응답 필드 설명 (핵심)

필드 타입 설명
id Long 모임 ID
title String 모임 제목
joinPolicy String 모임 참여 정책
status GroupV2Status 모임 상태
address Address 위치 정보
startTime LocalDateTime 시작 시간
endTime LocalDateTime 종료 시간
images GroupImageItem[] 이미지 목록(variants 포함)
tags String[] 태그 목록
description String 모임 설명
participantCount long 현재 ATTEND 인원 수
maxParticipants int 최대 인원
createdBy CreatedBy 호스트 정보
createdAt LocalDateTime 생성 시각
updatedAt LocalDateTime 수정 시각
myMembership MyMembership 내 참여 정보(로그인 시)
joinedMembers JoinedMember[] 참여자 목록(권한별 노출)

🧾 정책 및 주의사항 (필독)

1) myMembership 노출 정책

  • 비로그인: myMembership = null
  • 로그인
    • 참여 이력이 있으면 해당 멤버십 정보 반환
    • 참여 이력이 없으면 null

2) joinedMembers 노출 정책

  • HOST가 조회한 경우
    • 모든 멤버 노출: ATTEND, LEFT, KICKED, BANNED
    • status, leftAt 포함
  • HOST가 아닌 사용자 / 비로그인
    • ATTEND 상태 멤버만 노출
    • 이미 나간 사용자 정보는 노출되지 않음

3) participantCount 계산 기준

  • participantCountATTEND 상태만 집계합니다.

4) 이미지 노출 정책

  • 상세 조회에서는 모든 이미지에 대해 variants 정보를 포함합니다.
  • 정렬 기준은 sortOrder 오름차순이며, 0번 이미지가 대표입니다.
  • 이미지가 없는 경우 images: [] 로 응답됩니다.

❗ 오류 케이스 (대표)

HTTP ErrorCode 조건
404 GROUP_NOT_FOUND 존재하지 않는 groupId
401 UNAUTHORIZED 토큰 형식은 있으나 유효하지 않음

⚠️ 토큰이 없는 경우는 비로그인 조회로 처리하며 오류가 아닙니다. ⚠️ 토큰이 존재하지만 유효하지 않은 경우만 401을 반환합니다.

4. 모임 목록 조회/검색

GET /api/v2/groups

사용자에게 노출되는 모임 목록을 조회하는 API입니다.
키워드 검색과 커서 기반 페이징을 지원하며, filter / includeStatuses / excludeStatuses 조합으로 노출할 모임 상태 범위를 제어할 수 있습니다.

목록 응답에는 프론트엔드에서 별도 계산이 필요 없도록,

  • remainingSeats (남은 자리 수)
  • joinable (참여 가능 여부)

를 서버에서 함께 계산하여 내려줍니다.


📥 Request

GET /api/v2/groups

Headers

이름 필수
Authorization Bearer {JWT} X

목록 조회는 비로그인도 가능합니다. 토큰이 없으면 비로그인 사용자로 처리합니다.


📥 Query Parameters

이름 타입 필수 기본값 설명
keyword String X - 검색 키워드(제목/위치/상세위치/설명)
cursor Long X - 커서 기준 ID (id < cursor)
size int X 20 페이지 크기(최대 50)
filter GroupListFilter X ACTIVE 상태 필터 프리셋
includeStatuses GroupV2Status[] X - 포함할 상태 목록
excludeStatuses GroupV2Status[] X - 제외할 상태 목록

🧾 상태 필터 동작 규칙 (중요)

1) filter 기본값

  • filter가 없으면 기본값은 ACTIVE

2) filter 프리셋 의미

  • ACTIVE
    • 기본 포함 상태: RECRUITING, FULL, CLOSED
  • ARCHIVED
    • 기본 포함 상태: CANCELLED, FINISHED
  • ALL
    • 상태 제한 없이 전체 조회

3) includeStatuses 우선 규칙

  • includeStatuses비어있지 않게 전달되면
    • filter의 기본 포함 상태는 무시됩니다.
    • includeStatuses에 명시된 상태만 포함됩니다.

4) excludeStatuses 적용 규칙

  • excludeStatuses에 포함된 상태는 최종 결과에서 제외됩니다.

5) 충돌 시 우선순위

  • includeStatusesexcludeStatuses가 충돌하면 → excludeStatuses가 우선 적용됩니다.

🔎 검색 범위 (keyword)

keyword가 전달되면 다음 필드에 대해 부분 검색(대소문자 무시) 을 수행합니다.

  • title
  • address.location
  • address.locationDetail
  • description

📌 페이징 방식 (cursor)

  • 기본 정렬: groupId desc
  • cursor가 있으면: groupId < cursor
  • 응답의 nextCursor는 다음 요청에 사용할 cursor 값입니다.
  • 더 이상 데이터가 없으면 nextCursor = null

📥 Example Request

GET /api/v2/groups
GET /api/v2/groups?keyword=강남&size=10
GET /api/v2/groups?filter=ARCHIVED
GET /api/v2/groups?includeStatuses=RECRUITING
GET /api/v2/groups?cursor=120&size=20

📤 Response (200 OK)

{
  "status": 200,
  "success": true,
  "data": {
    "items": [
      {
        "id": 6,
        "title": "참여 취소 테스트용 자바 스터디",
        "joinPolicy": "FREE",
        "status": "RECRUITING",
        "location": "서울 서초구",
        "locationDetail": "교대역 1번 출구 근처 카페",
        "startTime": "2026-12-20T19:00:00",
        "endTime": "2026-12-20T21:00:00",
        "images": [
          "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/sample_440x240.webp"
        ],
        "tags": ["자바", "스터디"],
        "description": "참여 취소 테스트용 모임입니다.",
        "participantCount": 1,
        "maxParticipants": 5,
        "remainingSeats": 4,
        "joinable": true,
        "createdBy": {
          "userId": 102,
          "nickName": "HostTwo",
          "profileImage": null
        },
        "createdAt": "2025-12-22T14:27:50.696442",
        "updatedAt": "2025-12-22T14:27:50.696442"
      }
    ],
    "nextCursor": 6
  }
}

🧩 GroupListItemV2Response 필드

필드 타입 설명
id Long 모임 ID
title String 제목
joinPolicy String 모임 정책
status GroupV2Status 모임 상태
location String 위치
locationDetail String 상세 위치
startTime LocalDateTime 시작 시간
endTime LocalDateTime 종료 시간
images String[] CARD_440_240 이미지 URL 목록
tags String[] 태그
description String 설명
participantCount int ATTEND 인원 수
maxParticipants int 최대 인원
remainingSeats int 남은 자리 수
joinable boolean 참여 가능 여부
createdBy CreatedByV2Response 작성자 정보
createdAt LocalDateTime 생성 시각
updatedAt LocalDateTime 수정 시각

📝 joinable 계산 규칙

  • status == RECRUITING AND remainingSeats > 0true
  • 그 외(FULL, CLOSED, CANCELLED, FINISHED) → false

📝 이미지 노출 정책 (목록)

  • 목록 조회에서는 CARD_440_240 이미지 URL만 내려줍니다.
  • 모임당 최대 3개까지 내려갑니다.
  • 이미지가 없는 경우 images: [] 로 응답됩니다.

❗ 오류 케이스

HTTP ErrorCode 조건
400 INVALID_QUERY_PARAMETER 파라미터 타입 오류
400 INVALID_PAGE_SIZE size 범위 초과
401 UNAUTHORIZED 토큰이 존재하지만 유효하지 않음

토큰이 없는 경우는 비로그인 조회로 정상 처리됩니다.

5. 내 모임 목록 조회 (내가 참여한 모임 / 내가 만든 모임)

GET /api/v2/groups/me

로그인한 사용자가 내 모임 목록을 조회하는 API입니다. type 파라미터로 “현재 참여 / 지난 모임 / 내가 만든 모임” 탭을 구분합니다.

  • current : 현재 참여 중인 모임(기본)
  • past : 종료/취소된 모임
  • myPost : 내가 생성한 모임(호스트)

이 API는 반드시 로그인 필요합니다.


📥 Request

GET /api/v2/groups/me

Headers

이름 필수
Authorization Bearer {JWT} O

📥 Query Parameters

이름 타입 필수 기본값 설명
type String X current 탭 구분(current / past / myPost)
cursor Long X - 커서 기준 ID (id < cursor)
size int X 20 페이지 크기(최대 50)
filter GroupListFilter X type별 자동 상태 필터 프리셋(ACTIVE / ARCHIVED / ALL)
includeStatuses GroupV2Status[] X - 포함할 모임 상태
excludeStatuses GroupV2Status[] X - 제외할 모임 상태
myStatuses GroupUserV2Status[] X type별 상이 내 멤버십 상태 필터

🧾 type(탭) 정책

1) 기본값

  • type이 없으면 current

2) 허용 값

  • current : 현재 참여 중인 모임
  • past : 종료/취소된 모임
  • myPost : 내가 생성한 모임(호스트)

그 외 값은 오류 처리합니다.


🧾 filter 기본값(type 연동)

요청에 filter가 없으면 서버가 type에 따라 자동 설정합니다.

type 적용 filter
current ACTIVE
myPost ACTIVE
past ARCHIVED

filter 의미

  • ACTIVERECRUITING, FULL, CLOSED
  • ARCHIVEDCANCELLED, FINISHED
  • ALL → 전체(= include 조건 미적용)

🧾 myStatuses 정책 (코드 기준)

  • current, past에서는 내 멤버십 상태(myGu.status) 기준으로 필터링합니다.
  • myPost에서는 “내가 만든 모임”을 조회하지만, 코드상 myStatuses 파라미터는 쿼리에서 사용되지 않습니다. (즉, 전달해도 결과에 영향 없음)

기본 동작

  • type=current 또는 type=past에서 myStatuses가 없으면 기본값은 ATTEND
  • type=myPost에서는 myStatuses를 사용하지 않음

참고: type=past에서 myStatusesLEFT, KICKED 등으로 확장할지 여부는 프로젝트 정책에 따라 허용 범위를 결정하면 됩니다.


🧾 상태 필터 동작 규칙 (서비스 로직 기준)

  • filter는 기본 include 세트를 제공합니다.
  • includeStatuses가 오면 include가 우선 적용되며, 이때 기본 include는 사용하지 않습니다.
  • excludeStatuses는 최종적으로 제외 조건으로 적용됩니다.
  • include/exclude 충돌 시 exclude가 우선됩니다. (include에서 충돌 항목 제거)

참고: filter=ACTIVE인데 include/exclude도 없고 include가 비어있으면, 서버는 안전장치로 RECRUITING, FULL을 include로 설정합니다. (현재 enum 기본 include가 이미 ACTIVE에 포함을 주므로 일반적으로는 이 케이스가 잘 발생하지 않습니다.)


📌 페이징 방식 (cursor)

  • 기본 정렬: id desc
  • cursor가 있으면: id < cursor
  • 응답의 nextCursor는 다음 요청의 cursor로 그대로 사용
  • 더 이상 데이터가 없으면 null

📥 Example Request (내 모임 조회 시나리오별)

1. 내 모임 조회 (기본: CURRENT + ACTIVE + myStatuses=ATTEND)

GET /api/v2/groups/me
Authorization: Bearer {JWT}
  • 포함 상태: RECRUITING, FULL, CLOSED

2. 내 모임 조회 (CURRENT 명시)

GET /api/v2/groups/me?type=current
Authorization: Bearer {JWT}
  • 포함 상태: RECRUITING, FULL, CLOSED

3. 내 모임 조회 (PAST: 지난 모임 탭, filter=ARCHIVED 자동 적용)

GET /api/v2/groups/me?type=past
Authorization: Bearer {JWT}
  • 포함 상태: CANCELLED, FINISHED

4. 내 모임 조회 (ALL: 상태 제한 없이 전부)

GET /api/v2/groups/me?filter=ALL
Authorization: Bearer {JWT}
  • 포함 상태: 전체 (RECRUITING, FULL, CLOSED, CANCELLED, FINISHED)

5. 내 모임 조회 (PAST + ALL: past 탭이지만 상태 제한 해제)

GET /api/v2/groups/me?type=past&filter=ALL
Authorization: Bearer {JWT}
  • 포함 상태: 전체

6. 내 모임 조회 (내가 만든 모임: MY_POST, 기본 filter=ACTIVE)

GET /api/v2/groups/me?type=myPost
Authorization: Bearer {JWT}
  • 포함 상태: RECRUITING, FULL, CLOSED

7. 내가 만든 모임 조회 (MY_POST + 지난 모임)

GET /api/v2/groups/me?type=myPost&filter=ARCHIVED
Authorization: Bearer {JWT}
  • 포함 상태: CANCELLED, FINISHED

8. 내가 만든 모임 조회 (MY_POST + ALL: 내가 만든 모임 전부)

GET /api/v2/groups/me?type=myPost&filter=ALL
Authorization: Bearer {JWT}
  • 포함 상태: 전체

9. 내 모임 조회 (참여 상태 필터: myStatuses 지정)

GET /api/v2/groups/me?myStatuses=ATTEND
Authorization: Bearer {JWT}

GET /api/v2/groups/me?myStatuses=PENDING
Authorization: Bearer {JWT}

GET /api/v2/groups/me?myStatuses=ATTEND&myStatuses=PENDING
Authorization: Bearer {JWT}
  • 의미: 내 membership 상태(GroupUserV2Status) 기준으로 필터링

10. 내 모임 조회 (페이징: cursor 기반 다음 페이지)

GET /api/v2/groups/me?cursor={nextCursor}
Authorization: Bearer {JWT}

GET /api/v2/groups/me?type=past&cursor={nextCursor}
Authorization: Bearer {JWT}

GET /api/v2/groups/me?type=myPost&filter=ARCHIVED&cursor={nextCursor}
Authorization: Bearer {JWT}
  • nextCursor는 이전 응답의 data.nextCursor 값 사용

📤 Response (200 OK)

⚠️ 목록 응답의 imagesCARD_440_240 URL만 내려가며, 이미지가 없으면 [] 입니다. ⚠️ nextCursor는 다음 요청의 cursor로 그대로 사용합니다.

GET http://localhost:8080/api/v2/groups/me?type=past&filter=ALL

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: SAMEORIGIN
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 29 Dec 2025 22:37:19 GMT
{
  "status": 200,
  "success": true,
  "data": {
    "items": [
      {
        "id": 1,
        "title": "내 모임(current) 테스트용 - ACTIVE",
        "joinPolicy": "FREE",
        "status": "FINISHED",
        "location": "서울 서초구",
        "locationDetail": "교대역 1번 출구 근처 카페",
        "startTime": "2025-12-29T22:18:00",
        "endTime": "2026-12-29T22:20:00",
        "images": [
          "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251229221640_0_075c8d34-e857-4588-a58e-7364c464be15_440x240.webp",
          "https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/20251229221641_1_54ea517a-b2ae-4145-a770-f6b65c14a2f3_440x240.webp"
        ],
        "tags": [
          "ME_HTTP",
          "ACTIVE"
        ],
        "description": "me.http current/active 테스트용 모임입니다.",
        "participantCount": 2,
        "maxParticipants": 5,
        "remainingSeats": 3,
        "joinable": false,
        "createdBy": {
          "userId": 102,
          "nickName": "HostTwo",
          "profileImage": null,
          "profileMessage": null
        },
        "createdAt": "2025-12-29T22:17:06.12904",
        "updatedAt": "2025-12-29T22:17:06.12904",
        "myMembership": {
          "groupUserId": 2,
          "role": "MEMBER",
          "status": "ATTEND",
          "joinedAt": "2025-12-29T22:17:12.74574",
          "leftAt": null
        }
      }
    ],
    "nextCursor": null
  }
}

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
400 INVALID_MY_GROUP_TYPE type 값이 허용되지 않음
400 INVALID_QUERY_PARAMETER 파라미터 타입 오류
400 INVALID_PAGE_SIZE size 범위 초과

6. 모임 수정

PATCH /api/v2/groups/{groupId}

모임 정보를 부분 수정(Partial Update) 하는 API입니다.
요청에 포함된 필드만 변경되며, 호스트(HOST)만 수정 가능합니다.

다음 상태의 모임은 수정할 수 없습니다.

  • CANCELLED (취소)
  • FINISHED (종료)
  • deletedAt이 존재하는 경우

📥 Request

PATCH /api/v2/groups/{groupId}

Headers

이름 필수
Authorization Bearer {JWT} O
Content-Type application/json O

Path Variables

이름 타입 필수 설명
groupId Long O 수정할 모임 ID

📥 Request Body – UpdateGroupV2Request

images 필드는 모임 생성(create)과 동일한 구조를 사용합니다.

  • images = null → 이미지 변경 없음
  • images = [] → 이미지 전체 삭제
  • images = [{ sortOrder, imageKey }, ...] → 최종 상태로 교체
{
  "title": "모임 제목 수정",
  "description": "설명 수정 - 일정과 장소를 변경했습니다.",
  "joinPolicy": "FREE",
  "location": "서울 강남구",
  "locationDetail": "역삼역 2번 출구",
  "startTime": "2026-12-30T19:00:00",
  "endTime": "2026-12-30T21:00:00",
  "maxParticipants": 10,
  "status": "CLOSED",
  "tags": ["러닝", "주말"],
  "images": [
    { "sortOrder": 0, "imageKey": "{{img0_key}}" },
    { "sortOrder": 1, "imageKey": "{{img1_key}}" }
  ]
}

🧾 수정 정책 (핵심)

1) 권한 정책

  • 요청자는 해당 모임의 HOST여야 합니다.
  • HOST가 아니면 수정 불가합니다.

2) 수정 불가 상태

다음 조건 중 하나라도 만족하면 모든 수정이 거부됩니다.

  • status = CANCELLED
  • status = FINISHED
  • deletedAt != null

3) 부분 수정(null 처리)

  • 요청 필드가 null이면 해당 필드는 변경되지 않습니다.
필드 null 처리
title 변경 없음
description 변경 없음
joinPolicy 변경 없음
location / locationDetail 부분 수정
startTime / endTime 부분 수정
maxParticipants 변경 없음
status 변경 없음
tags 변경 없음
images 변경 없음

4) 제목(title) 정책

  • 공백/빈 문자열 불가
  • 최대 50자
  • trim 후 저장

5) 설명(description) 정책

  • 공백/빈 문자열 불가
  • 최대 300자
  • trim 후 저장

6) 주소(location / locationDetail) 정책

  • 둘 중 하나라도 전달되면 “주소 변경 시도”로 판단
  • location = null → 기존 값 유지
  • locationDetail = null → 기존 값 유지
  • 최종적으로 location은 필수(빈 값 불가)

7) 시간(startTime / endTime) 정책

  • 하나만 수정하더라도 최종 상태 기준으로 유효해야 합니다
  • 규칙
    • startTime < endTime
    • endTime이 존재한다면 반드시 startTime 이후

8) 정원(maxParticipants) 정책

  • 0 이하 불가
  • 현재 ATTEND 인원보다 작게 줄일 수 없습니다

9) 상태(status) 전이 정책

허용되는 상태 전이

  • RECRUITING → FULL / CLOSED / CANCELLED / FINISHED
  • FULL → RECRUITING / CLOSED / CANCELLED / FINISHED
  • CLOSED → CANCELLED / FINISHED

허용되지 않는 전이

  • CANCELLED 또는 FINISHED → 다른 상태

10) 태그(tags) 수정 정책

  • tags = null → 변경 없음
  • tags 전달 시 전체 교체
  • 최대 10개
  • 중복/빈 문자열 불가

11) 이미지(images) 수정 정책 (최종 상태 방식)

11-1) images = null

  • 이미지 변경 없음

11-2) images = []

  • 이미지 전체 삭제

11-3) images = 1~3개 (수정)

  • 리스트에 포함된 imageKey만 최종적으로 유지
  • 기존 이미지 중 리스트에 없는 항목은 삭제
  • 신규 imageKey는 반드시
    • 사전 업로드(pre-upload) key
    • 요청자와 업로더 일치
    • 사용 시 consume(1회성)

정렬 규칙

  • 모든 항목의 sortOrder가 null → 요청 배열 순서 적용
  • 하나라도 존재 → sortOrder 오름차순
  • 중복 sortOrder 불가
  • 허용 범위: 0 ~ 2

✅ 주의: images: []를 비워두지 말고, 반드시 pre-upload 응답의 imageKey로 채워서 요청합니다.


📤 Response (200 OK)

응답 구조는 모임 생성 응답과 동일합니다.

{
  "status": 200,
  "success": true,
  "data": {
    "id": 2,
    "title": "모임 제목 수정",
    "joinPolicy": "FREE",
    "status": "CLOSED",
    "address": {
      "location": "서울 강남구",
      "locationDetail": "역삼역 2번 출구"
    },
    "startTime": "2026-12-30T19:00:00",
    "endTime": "2026-12-30T21:00:00",
    "tags": ["러닝", "주말"],
    "description": "설명 수정 - 일정과 장소를 변경했습니다.",
    "maxParticipants": 10,
    "images": [],
    "updatedAt": "2025-12-22T14:40:07.41714"
  }
}

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 NO_PERMISSION_TO_UPDATE_GROUP HOST 아님
404 GROUP_NOT_FOUND 모임 없음
400 INVALID_GROUP_STATUS 수정 불가 상태
400 INVALID_TIME_RANGE start/end 시간 오류
400 INVALID_MAX_PARTICIPANTS 정원 축소 불가
400 DUPLICATED_IMAGE_SORT_ORDER_IN_REQUEST sortOrder 중복
400 DUPLICATED_IMAGE_KEY_IN_REQUEST imageKey 중복
400 INVALID_GROUP_IMAGE_ITEM images 구조 오류
400 IMAGE_KEY_NOT_FOUND imageKey 만료/이미 사용

7. 모임 삭제 (Hard Delete)

DELETE /api/v2/groups/{groupId}

특정 모임을 완전히 삭제(하드 삭제) 하는 API입니다.
삭제는 HOST만 가능하며, 모임과 관련된 데이터(참가자, 태그 연결, 이미지/이미지 변형)도 함께 정리됩니다.

또한 이미지 파일(S3)은 DB 삭제가 커밋으로 확정된 이후(afterCommit) 에 삭제됩니다.
즉, “DB가 먼저 삭제 확정 → 그 다음 파일 삭제” 순서를 지켜 데이터 정합성을 우선합니다.


📥 Request

DELETE /api/v2/groups/{groupId}

Headers

이름 필수 설명
Authorization Bearer {JWT} O 로그인 필수(호스트 권한 확인)

Path Variables

이름 타입 필수 설명
groupId Long O 삭제할 모임 ID

Example Request

DELETE /api/v2/groups/10
Authorization: Bearer {JWT}

✅ 모임 삭제 정책(핵심)

1) 삭제는 HOST만 가능합니다.

  • 요청한 사용자가 해당 모임의 hostId와 다르면 삭제할 수 없습니다.
  • 즉, “내가 만든 모임만 삭제 가능”합니다.

2) 삭제는 Hard Delete 입니다.

이 API는 soft delete가 아니라 hard delete로 동작합니다.

  • groups(v2_groups) 레코드가 DB에서 제거됩니다.
  • 연결된 관계 데이터도 함께 정리됩니다.

3) 삭제 순서(명시적 정리)

연관관계가 여러 개 얽혀 있기 때문에, FK 충돌 없이 안정적으로 삭제되도록 순서를 지킵니다.

  1. 모임 참가자(v2_group_users) 삭제
  2. 모임-태그 연결(v2_group_tags) 삭제 (Tag 테이블 자체는 삭제하지 않음)
  3. 이미지 변형(v2_group_image_variants) 삭제
  4. 이미지(v2_group_images) 삭제
  5. 모임(v2_groups) 삭제

4) S3 파일 삭제는 afterCommit에서 수행됩니다.

  • 삭제할 URL 목록(variants의 imageUrl들)을 DB 삭제 이전에 조회하여 확보합니다.
  • DB 삭제 트랜잭션이 커밋 확정된 이후(afterCommit) 에 확보한 URL 기반으로 S3 파일을 삭제합니다.

📌 이 방식의 장점

  • DB 삭제가 롤백되면 파일도 삭제되지 않습니다.
  • 즉, “DB는 남아있는데 파일만 없어지는 사고”를 예방합니다.

5) afterCommit 파일 삭제 실패 처리(운영 관점)

afterCommit에서 파일 삭제를 수행하기 때문에, 파일 삭제 단계에서 장애가 나면 DB는 이미 삭제된 상태일 수 있습니다.

따라서 운영 확장 포인트를 고려할 수 있습니다.

  1. 파일 삭제 실패 URL 로깅
  2. 재시도(outbox, 배치 정리)
  3. 장애 발생 시 알림/모니터링

📤 Response

✅ Response (204 No Content)

삭제가 성공하면 응답 바디 없이 204 No Content를 반환합니다.

HTTP/1.1 204 No Content

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 NO_PERMISSION_TO_DELETE_GROUP HOST가 아님
404 GROUP_NOT_FOUND groupId에 해당하는 모임이 없음
400 INVALID_GROUP_STATUS 정책상 삭제 불가 상태(프로젝트에서 제한한다면)

✅ 참고: 현재 문서에서는 “삭제 불가 상태”를 별도로 제한하지 않고 HOST면 삭제 가능으로 읽힙니다. 만약 “FINISHED만 삭제 가능” 같은 제한을 둘 거면 INVALID_GROUP_STATUS를 활성화하면 됩니다.

8. 모임 참여

POST /api/v2/groups/{groupId}/attend

특정 모임에 참여(ATTEND) 하는 API입니다.
참여는 모집중(RECRUITING) 상태에서만 가능하며, 참여 처리 후 정원이 가득 차면 자동으로 FULL(정원마감) 으로 전환됩니다.

과거에 나갔거나(LEFT) 강퇴(KICKED)된 사용자는 재참여(reAttend) 가 가능하지만, 차단(BANNED) 된 사용자는 참여할 수 없습니다.

승인제에서는 message를 함께 보내면 가입 신청 메시지(joinRequestMessage) 로 저장됩니다.


📥 Request

POST /api/v2/groups/{groupId}/attend

Headers

이름 필수
Authorization Bearer {JWT} O

Path Variables

이름 타입 필수 설명
groupId Long O 참여할 모임 ID

Body

  • 선택(Optional)
  • 승인제(APPROVAL_REQUIRED)에서만 의미가 있으며, FREE 모임에서는 무시될 수 있습니다.
{
  "message": "참여 신청합니다!"
}
필드 타입 필수 설명
message String X 가입 신청 메시지(최대 300자)

✅ 참여 정책(핵심)

1) 로그인 사용자만 참여 가능

  • Authorization(JWT)이 필수입니다.
  • userId가 없으면 참여 처리 불가입니다.

2) HOST는 자신의 모임에 참여할 수 없습니다

  • 모임 생성 시 HOST는 이미 HOST + ATTEND 상태로 등록됩니다.
  • HOST가 attend API를 호출하면 오류 처리됩니다.

3) RECRUITING 상태에서만 참여 가능

다음 상태에서는 참여할 수 없습니다.

  • FULL
  • CLOSED
  • CANCELLED
  • FINISHED

4) joinPolicy에 따른 참여 결과

  • FREE : 즉시 ATTEND
  • APPROVAL_REQUIRED : 즉시 PENDING (승인 대기)

5) 기존 멤버십 상태에 따른 처리

서버는 (groupId, userId) 기준으로 기존 멤버십을 조회합니다.

기존 상태 처리 결과 (FREE) 처리 결과 (APPROVAL_REQUIRED)
멤버십 없음 신규 생성 (role=MEMBER, status=ATTEND) 신규 생성 (role=MEMBER, status=PENDING)
ATTEND 중복 참여 → 오류 중복 참여 → 오류
LEFT 재참여 허용 → ATTEND로 전환 재참여 허용 → PENDING로 전환(재신청)
KICKED 재참여 허용 → ATTEND로 전환 재참여 허용 → PENDING로 전환(재신청)
BANNED 참여 불가 → 오류 참여 불가 → 오류
PENDING (정책상 발생 X) 중복 요청 → 오류
REJECTED 재요청 불가 → 오류 재요청 불가 → 오류

6) 정원(maxParticipants) 초과 불가 (FREE / APPROVE 공통)

  • 최종적으로 ATTEND가 되는 시점(즉시 참여 or 승인 처리)에서 ATTEND 인원을 다시 카운트합니다.
  • attendCount > maxParticipants가 되면 전체 처리를 롤백하고 오류를 반환합니다.

7) 정원 도달 시 FULL 자동 전환 (ATTEND 확정 시점)

  • attendCount == maxParticipants
  • 그리고 모임 상태가 RECRUITING이면 → 모임 상태를 FULL로 자동 변경합니다.

승인제에서는 “승인(approve)” 시점에 FULL 전환이 일어날 수 있습니다.


📤 Response (200 OK)

응답 구조는 기존 AttendanceGroupV2Response 그대로 사용 단, 승인제 모임에서는 myMembership.statusPENDING으로 내려올 수 있습니다.

{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 10,
    "groupStatus": "RECRUITING",
    "participantCount": 5,
    "maxParticipants": 8,
    "myMembership": {
      "groupUserId": 44,
      "role": "MEMBER",
      "status": "PENDING",
      "joinedAt": "2025-12-19T16:10:20.123456",
      "leftAt": null
    },
    "serverTime": "2025-12-19T16:10:20.999999"
  }
}

❗ 오류 케이스 (수정)

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
404 GROUP_NOT_FOUND_BY_ID 모임 없음
400 GROUP_NOT_RECRUITING 모집 상태 아님
400 ALREADY_ATTEND_GROUP 이미 참여 중
400 GROUP_BANNED_USER 차단된 사용자
400 GROUP_HOST_CANNOT_ATTEND HOST 재참여 시도
400 GROUP_IS_FULL 정원 초과
409 GROUP_ALREADY_PENDING 이미 승인 대기중

9. 모임 나가기

POST /api/v2/groups/{groupId}/left

특정 모임에서 나가기(LEFT) 처리하는 API입니다.


✅ 나가기 정책(핵심)

1) 로그인 사용자만 가능

  • JWT 필수

2) HOST는 나가기 불가

  • HOST는 leave 호출 시 오류

3) ATTEND 상태에서만 가능

  • 멤버십 없거나 ATTEND가 아니면 오류

4) 나가기 처리 결과

  • 상태를 LEFT로 변경
  • leftAt에 서버 현재시간 기록
  • joinedAt 유지

5) FULL → RECRUITING 자동 복귀

  • FULL 상태에서 나가기 후 자리가 생기면 RECRUITING으로 복귀

📤 Response (200 OK)

응답 구조는 참여(attend)와 동일


❗ 오류 케이스 (수정)

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
404 GROUP_NOT_FOUND_BY_ID 모임 없음
400 GROUP_HOST_CANNOT_LEFT HOST 나가기 시도
400 GROUP_MEMBERSHIP_NOT_FOUND 참여 기록 없음
400 GROUP_NOT_ATTEND_STATUS ATTEND 아님

10. 승인

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/approve

APPROVAL_REQUIRED 모임 전용 승인 API입니다.


✅ 승인 정책(핵심)

1) HOST만 가능


2) 승인제 모임에서만 가능

  • FREE 모임 호출 시 오류

3) 대상은 반드시 PENDING


4) 승인 처리 결과

  • PENDING → ATTEND
  • joinedAt 유지
  • ATTEND 인원 증가

5) 정원/상태 전이

  • 승인 후 정원 도달 시 FULL 전환 가능

📤 Response (200 OK)

(기존 형식 유지)


❗ 오류 케이스 (수정)

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY HOST 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음
404 GROUP_USER_NOT_FOUND 대상 멤버십 없음
409 GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED 승인제 아님
409 GROUP_TARGET_STATUS_NOT_PENDING 대상이 PENDING 아님
409 GROUP_IS_FULL 승인 시 정원 초과

11. 거절

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/reject

APPROVAL_REQUIRED 모임 전용 거절 API입니다.


✅ 거절 정책(핵심)

1) HOST만 가능


2) 승인제 모임에서만 가능


3) 대상은 반드시 PENDING


4) 거절 처리 결과

  • PENDING → REJECTED
  • ATTEND 인원 변화 없음
  • leftAt 기록하지 않음 (이탈 아님)

📤 Response (200 OK)

(기존 형식 유지)


❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY HOST 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음
404 GROUP_USER_NOT_FOUND 대상 멤버십 없음
409 GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED 승인제 아님
409 GROUP_TARGET_STATUS_NOT_PENDING 대상이 PENDING 아님

✅ 전체 정리 (내부 일관성)

  • ATTEND / LEFT / KICKED / BANNED → leftAt 사용
  • PENDING / REJECTED → leftAt 사용 안 함
  • 참여 / 승인 / 강퇴 / 차단 / 나가기 모두 ATTEND 기준으로 상태 전이 일관
  • 조회 API(1517)와 실행 API(814)의 대상 상태 완전 일치

12. 강퇴

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/kick

호스트가 모임 참여자를 강퇴하여 KICKED로 전환합니다.

  • 일반적으로 ATTEND → KICKED
  • HOST만 가능
  • 강퇴로 인해 participantCount(ATTEND)가 감소할 수 있으며,
    • 모임이 FULL이었다면 자리 발생 시 RECRUITING으로 자동 복귀할 수 있습니다(정책 적용)

📥 Request

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/kick

Headers

이름 필수
Authorization Bearer {JWT} O
Content-Type application/json X (바디 없음)

Path Variables

이름 타입 필수 설명
groupId Long O 모임 ID
targetUserId Long O 강퇴 대상 유저 ID

Body

  • 없음 ({} 보내도 무방)

Example Request

POST /api/v2/groups/1/attendance/103/kick
Authorization: Bearer {JWT}

✅ 강퇴 정책(핵심)

1) 인증/권한

  • 로그인 필수(JWT 필수)
  • 요청자는 해당 모임의 HOST여야 합니다. (아니면 오류)

2) 강퇴 대상 제한

  • 호스트 자신(targetUserId=hostId)을 강퇴할 수 없습니다.
  • 대상 멤버십이 존재해야 합니다. (없으면 오류)

3) 대상 상태 제한(강퇴 가능 상태) (수정: 일관성 강화)

서버는 (groupId, targetUserId)로 대상 멤버십을 조회합니다.

  • 강퇴는 ATTEND 상태에서만 허용합니다.
  • 아래 상태들은 강퇴 대상이 될 수 없습니다(정책 오류):
    • PENDING, REJECTED, LEFT, KICKED, BANNED

4) 강퇴 처리 결과 (수정: leftAt 기록 정책 통일)

  • 대상 멤버십 상태를 KICKED로 변경합니다.
  • leftAt강퇴 시각(서버 현재시간) 을 기록합니다. (LEFT/KICKED/BANNED의 “이탈 시각”을 leftAt으로 통일)

5) FULL → RECRUITING 자동 복귀(선택 정책)

강퇴 후 서버는 ATTEND 인원을 다시 카운트합니다.

  • 강퇴 이전 모임 상태가 FULL 이고
  • 강퇴 후 attendCount < maxParticipants 라면 → 모임 상태를 RECRUITING으로 자동 변경할 수 있습니다.

📤 Response (200 OK)

강퇴 성공 시, 서버는 다음 정보를 반환합니다.

  • groupStatus (강퇴 후 모임 상태: FULL→RECRUITING 복귀 가능)
  • participantCount (ATTEND 인원 수)
  • targetMembership (강퇴된 대상의 최소 정보)
  • serverTime
{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 1,
    "groupStatus": "RECRUITING",
    "joinPolicy": "FREE",
    "participantCount": 2,
    "maxParticipants": 5,
    "targetMembership": {
      "userId": 103,
      "groupUserId": 3,
      "status": "KICKED"
    },
    "serverTime": "2025-12-23T16:06:47.8129076"
  }
}

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY 호스트가 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음
404 GROUP_USER_NOT_FOUND 대상 멤버십 없음
409 GROUP_CANNOT_KICK_HOST 호스트 자신 강퇴 시도
409 GROUP_TARGET_STATUS_NOT_KICKABLE 대상이 강퇴 불가 상태(ATTEND 아님 등)

13. 차단

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/ban

호스트가 특정 유저를 차단(BAN) 하여 BANNED 상태로 전환합니다.

  • 일반적으로 ATTEND → BANNED
  • HOST만 가능
  • 차단된 유저는 이후 해당 모임에 참여(attend) 불가
  • 차단 시점에 대상이 ATTEND였다면 participantCount(ATTEND)가 감소할 수 있으며,
    • 모임이 FULL이었다면 자리 발생 시 RECRUITING으로 자동 복귀할 수 있습니다(정책 적용)

📥 Request

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/ban

Headers

이름 필수
Authorization Bearer {JWT} O
Content-Type application/json X (바디 없음)

Path Variables

이름 타입 필수 설명
groupId Long O 모임 ID
targetUserId Long O 차단 대상 유저 ID

Body

  • 없음 ({} 보내도 무방)

Example Request

POST /api/v2/groups/1/attendance/103/ban
Authorization: Bearer {JWT}

✅ 차단 정책(핵심)

1) 인증/권한

  • 로그인 필수(JWT 필수)
  • 요청자는 해당 모임의 HOST여야 합니다. (아니면 오류)

2) 차단 대상 제한

  • 호스트 자신(targetUserId=hostId)을 차단할 수 없습니다.
  • 대상 멤버십이 존재해야 합니다. (없으면 오류)

3) 대상 상태 제한(차단 가능 상태) (수정: 일관성 강화)

서버는 (groupId, targetUserId)로 대상 멤버십을 조회합니다.

  • 차단은 ATTEND 상태에서만 허용합니다.
  • 아래 상태들은 차단 대상이 될 수 없습니다(정책 오류):
    • PENDING, REJECTED, LEFT, KICKED, BANNED

4) 차단 처리 결과 (수정: leftAt 기록 정책 통일)

  • 대상 멤버십 상태를 BANNED로 변경합니다.
  • leftAt차단 시각(서버 현재시간) 을 기록합니다. (LEFT/KICKED/BANNED의 “이탈 시각”을 leftAt으로 통일)

5) FULL → RECRUITING 자동 복귀(선택 정책)

차단 후 서버는 ATTEND 인원을 다시 카운트합니다.

  • 차단 이전 모임 상태가 FULL 이고
  • 차단 후 attendCount < maxParticipants 라면 → 모임 상태를 RECRUITING으로 자동 변경할 수 있습니다.

📤 Response (200 OK)

차단 성공 시, 서버는 다음 정보를 반환합니다.

  • groupStatus (차단 후 모임 상태: FULL→RECRUITING 복귀 가능)
  • participantCount (ATTEND 인원 수)
  • targetMembership (차단된 대상의 최소 정보)
  • serverTime
{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 1,
    "groupStatus": "RECRUITING",
    "joinPolicy": "FREE",
    "participantCount": 2,
    "maxParticipants": 5,
    "targetMembership": {
      "userId": 103,
      "groupUserId": 2,
      "status": "BANNED"
    },
    "serverTime": "2025-12-23T16:08:07.5338729"
  }
}

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY 호스트가 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음
404 GROUP_USER_NOT_FOUND 대상 멤버십 없음
409 GROUP_CANNOT_BAN_HOST 호스트 자신 차단 시도
409 GROUP_TARGET_STATUS_NOT_BANNABLE 대상이 차단 불가 상태(ATTEND 아님, 이미 BANNED 등)

14. 차단 해제

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/unban

호스트가 차단된 유저를 차단 해제(Unban) 합니다.

  • BANNED → KICKED
  • HOST만 가능
  • unban은 “차단만 해제”이며, 재참여는 /attend에서 처리합니다.

왜 KICKED로 복귀? 차단 해제 후 바로 ATTEND로 복구시키면 “호스트 승인 없이 자동 재참여”가 되어 운영상 혼선이 생길 수 있어요. 그래서 차단 해제 = 참여는 다시 가능하지만, 즉시 참여 상태는 아님 으로 정의합니다.


📥 Request

POST /api/v2/groups/{groupId}/attendance/{targetUserId}/unban

Headers

이름 필수
Authorization Bearer {JWT} O
Content-Type application/json X (바디 없음)

Path Variables

이름 타입 필수 설명
groupId Long O 모임 ID
targetUserId Long O 차단 해제 대상 유저 ID

Body

  • 없음 ({} 보내도 무방)

Example Request

POST /api/v2/groups/1/attendance/103/unban
Authorization: Bearer {JWT}

✅ 차단 해제 정책(핵심)

1) 인증/권한

  • 로그인 필수(JWT 필수)
  • 요청자는 해당 모임의 HOST여야 합니다.

2) 대상 상태 제한

서버는 (groupId, targetUserId)로 대상 멤버십을 조회합니다.

  • 대상이 BANNED 상태가 아니면 unban 불가입니다.
    • 예: ATTEND / LEFT / KICKED / PENDING / REJECTED 인데 unban 호출 → 오류

3) 차단 해제 처리 결과(복귀 상태) (수정: 명확화)

  • BANNED → KICKED 로 변경합니다.
  • leftAt변경하지 않습니다.
    • leftAt은 “이탈(강퇴/차단/나가기) 시각”으로 사용하며,
    • unban은 “상태 복귀”이므로 이탈 시각을 새로 찍지 않습니다.

📤 Response (200 OK)

차단 해제 성공 시, 서버는 다음 정보를 반환합니다.

  • groupStatus (차단 해제는 ATTEND 인원에 영향이 없으므로 보통 상태 변화 없음)
  • participantCount (ATTEND 인원 수)
  • targetMembership (차단 해제된 대상의 최소 정보)
  • serverTime
{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 1,
    "groupStatus": "RECRUITING",
    "joinPolicy": "FREE",
    "participantCount": 2,
    "maxParticipants": 5,
    "targetMembership": {
      "userId": 103,
      "groupUserId": 2,
      "status": "KICKED"
    },
    "serverTime": "2025-12-23T16:08:43.0092063"
  }
}

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY 호스트가 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음
404 GROUP_USER_NOT_FOUND 대상 멤버십 없음
409 GROUP_TARGET_NOT_BANNED 대상이 BANNED가 아닌데 unban 시도

15. 강퇴 대상 조회

GET /api/v2/groups/{groupId}/attendance/kick-targets

호스트가 현재 강퇴 가능한 대상 목록을 조회합니다.

  • HOST만 가능
  • 기본 정책: ATTEND 상태 중 HOST 본인을 제외한 사용자가 대상
  • 정책 확장 가능:
    • MANAGER 제외
    • PENDING 제외(승인제 모임에서)
    • 이미 KICKED/LEFT/BANNED/REJECTED 등 제외

📥 Request

GET /api/v2/groups/{groupId}/attendance/kick-targets

Headers

이름 필수
Authorization Bearer {JWT} O

Path Variables

이름 타입 필수 설명
groupId Long O 모임 ID

Body

  • 없음

Example Request

GET /api/v2/groups/1/attendance/kick-targets
Authorization: Bearer {JWT}

✅ 조회 정책(핵심)

1) 인증/권한

  • 로그인 필수(JWT 필수)
  • 요청자는 해당 모임의 HOST여야 합니다. (아니면 오류)

2) 조회 대상 필터링 규칙 (수정: “강퇴 가능 상태” 일관성 강화)

서버는 groupId의 멤버십 중 아래 조건을 만족하는 유저만 반환합니다.

  • status = ATTEND
  • role != HOST (호스트 본인 제외)

문서의 강퇴 API(12번) 기준이 ATTEND만 강퇴 가능이므로, 조회도 동일하게 ATTEND만 노출되도록 고정합니다.


3) 응답 목록 정렬(권장) (수정: 기준 명확화)

  • joinedAt asc (먼저 참여한 사람부터 위)

📤 Response (200 OK)

{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 1,
    "targets": [
      {
        "userId": 103,
        "nickName": "KickMember1",
        "profileImage": null,
        "groupUserId": 3,
        "status": "ATTEND",
        "joinedAt": "2025-12-23T16:06:20.216063"
      },
      {
        "userId": 104,
        "nickName": "KickMember2",
        "profileImage": null,
        "groupUserId": 2,
        "status": "ATTEND",
        "joinedAt": "2025-12-23T16:07:49.398200"
      }
    ],
    "serverTime": "2025-12-23T16:06:26.3672819"
  }
}

🧩 응답 필드 설명 (KickTargetsV2Response)

필드 타입 설명
groupId Long 모임 ID
targets KickTargetItem[] 강퇴 가능한 대상 목록
serverTime LocalDateTime 서버 기준 응답 시간

KickTargetItem

필드 타입 설명
userId Long 대상 유저 ID
nickName String 닉네임
profileImage String 프로필 이미지 URL(없으면 null)
groupUserId Long 대상 멤버십 ID
status GroupUserV2Status 대상 상태(항상 ATTEND)
joinedAt LocalDateTime 참여 시간

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY 호스트가 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음

📝 특징 및 주의사항

  • HOST만 조회 가능
  • 대상은 ATTEND 상태 중 HOST 제외
  • targets가 비어있으면 targets: [] 로 내려갑니다.
  • 강퇴 실행(/kick) 시에는 대상 상태가 여전히 ATTEND인지를 다시 검증해야 합니다(조회 시점과 실행 시점 사이 변경 가능)

16. 차단 대상 조회

GET /api/v2/groups/{groupId}/attendance/ban-targets

호스트가 차단 가능한 대상 목록을 조회합니다.

  • HOST만 가능
  • 기본 정책: ATTEND 상태 중 HOST 본인을 제외한 사용자가 대상
  • 정책 확장 가능:
    • MANAGER 제외
    • PENDING/REJECTED 등 포함 여부(운영 정책에 따라)

📥 Request

GET /api/v2/groups/{groupId}/attendance/ban-targets

Headers

이름 필수
Authorization Bearer {JWT} O

Path Variables

이름 타입 필수 설명
groupId Long O 모임 ID

Body

  • 없음

Example Request

GET /api/v2/groups/1/attendance/ban-targets
Authorization: Bearer {JWT}

✅ 조회 정책(핵심)

1) 인증/권한

  • 로그인 필수(JWT 필수)
  • 요청자는 해당 모임의 HOST여야 합니다. (아니면 오류)

2) 조회 대상 필터링 규칙 (수정: 차단 API(13번)와 일관성 유지)

서버는 groupId의 멤버십 중 아래 조건을 만족하는 유저만 반환합니다.

  • status = ATTEND
  • role != HOST (호스트 본인 제외)

문서의 차단 API(13번) 기준이 ATTEND만 차단 가능이므로, 조회도 동일하게 ATTEND만 노출되도록 고정합니다.


3) 응답 목록 정렬(권장) (수정: 운영 UX 기준 명확화)

  • joinedAt desc (최근 참여한 사람부터 위)

📤 Response (200 OK)

{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 1,
    "targets": [
      {
        "userId": 103,
        "nickName": "BanMembers1",
        "profileImage": null,
        "groupUserId": 2,
        "status": "ATTEND",
        "joinedAt": "2025-12-23T16:09:32.244210"
      },
      {
        "userId": 104,
        "nickName": "BanMembers2",
        "profileImage": null,
        "groupUserId": 3,
        "status": "ATTEND",
        "joinedAt": "2025-12-23T16:07:49.398200"
      }
    ],
    "serverTime": "2025-12-23T16:10:10.6443403"
  }
}

🧩 응답 필드 설명 (BanTargetsV2Response)

필드 타입 설명
groupId Long 모임 ID
targets BanTargetItem[] 차단 가능한 대상 목록
serverTime LocalDateTime 서버 기준 응답 시간

BanTargetItem

필드 타입 설명
userId Long 대상 유저 ID
nickName String 닉네임
profileImage String 프로필 이미지 URL(없으면 null)
groupUserId Long 대상 멤버십 ID
status GroupUserV2Status 대상 상태(항상 ATTEND)
joinedAt LocalDateTime 참여 시간

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY 호스트가 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음

📝 특징 및 주의사항

  • HOST만 조회 가능
  • 대상은 ATTEND 상태 중 HOST 제외
  • targets가 비어있으면 targets: [] 로 내려갑니다.
  • 실제 ban(/ban) 실행 시에는 대상이 여전히 ATTEND인지를 다시 검증해야 합니다(조회 시점과 실행 시점 사이 변경 가능)

17. 차단된 유저 목록 조회

GET /api/v2/groups/{groupId}/attendance/banned-targets

호스트가 현재 BANNED 상태인 유저 목록을 조회합니다.

  • HOST만 가능
  • “현재 차단 중인 유저를 관리(해제/unban 등)”하기 위한 목록 API입니다.
  • 반환 대상은 해당 모임의 멤버십 중 status = BANNED 인 항목입니다.

📥 Request

GET /api/v2/groups/{groupId}/attendance/banned-targets

Headers

이름 필수
Authorization Bearer {JWT} O

Path Variables

이름 타입 필수 설명
groupId Long O 모임 ID

Body

  • 없음

Example Request

GET /api/v2/groups/1/attendance/banned-targets
Authorization: Bearer {JWT}

✅ 조회 정책(핵심)

1) 인증/권한

  • 로그인 필수(JWT 필수)
  • 요청자는 해당 모임의 HOST여야 합니다.

2) 조회 대상 필터링 규칙

서버는 groupId의 멤버십 중 아래 조건만 반환합니다.

  • status = BANNED

3) 응답 목록 정렬(권장) (수정: “차단 관리” 관점에 맞게)

  • leftAt desc (최근 차단된 사람부터 위)

13번에서 BANNED 전환 시 leftAt 기록으로 통일했기 때문에, 차단 관리 목록은 leftAt desc 정렬이 가장 직관적입니다.


📤 Response (200 OK)

{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 1,
    "targets": [
      {
        "userId": 103,
        "nickName": "BanMembers1",
        "profileImage": null,
        "groupUserId": 2,
        "status": "BANNED",
        "joinedAt": "2025-12-23T16:09:32.244210"
      }
    ],
    "serverTime": "2025-12-23T16:11:00.8673094"
  }
}

🧩 응답 필드 설명 (BannedTargetsV2Response)

필드 타입 설명
groupId Long 모임 ID
targets BannedTargetItem[] 차단된 유저 목록
serverTime LocalDateTime 서버 기준 응답 시간

BannedTargetItem

필드 타입 설명
userId Long 유저 ID
nickName String 닉네임
profileImage String 프로필 이미지 URL(없으면 null)
groupUserId Long 멤버십 ID
status GroupUserV2Status 항상 BANNED
joinedAt LocalDateTime 참여 시간(또는 최초 관여 시간)

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
403 GROUP_HOST_ONLY 호스트가 아님
404 GROUP_NOT_FOUND_BY_ID 모임 없음

📝 특징 및 주의사항 (수정: 일관성 반영)

  • HOST만 조회 가능
  • 결과가 없으면 targets: []
  • 실제 unban(/unban) 실행 시에는 대상이 여전히 BANNED인지 재검증해야 합니다(조회 시점과 실행 시점 사이 변경 가능)
  • 운영에서 “차단 시각”을 보여주려면,
    • 13번 정책대로 leftAt을 차단 시각으로 사용하고,
    • 17번 목록은 정렬을 leftAt desc로 두는 구성이 가장 자연스럽습니다.

18. 가입 신청 목록 조회

GET /api/v2/groups/{groupId}/attendance

호스트가 해당 모임의 가입 신청/참여 관련 멤버십 목록status로 필터링하여 조회합니다.
기본값은 PENDING이며, 승인제 모임에서는 가입 신청자 목록 조회로 사용됩니다.


📥 Request

GET /api/v2/groups/{groupId}/attendance?status=PENDING

Headers

이름 필수
Authorization Bearer {JWT} O

Path Variables

이름 타입 필수 설명
groupId Long O 모임 ID

Query Parameters

이름 타입 필수 기본값 설명
status GroupUserV2Status X PENDING 조회할 멤버십 상태 필터

예: PENDING, REJECTED, ATTEND, LEFT, KICKED, BANNED


✅ 조회 정책(핵심)

1) HOST만 조회 가능

  • 로그인 필수(JWT 필수)
  • 요청자는 해당 모임의 HOST여야 합니다. (아니면 오류)

2) 상태(status)로 필터링

  • 서버는 (groupId, status) 조건으로 멤버십을 조회합니다.
  • HOST 멤버십은 기본적으로 제외됩니다.

3) 정렬(권장)

  • status = PENDING : joinedAt desc (최근 신청이 위)

📤 Response (200 OK)

{
  "status": 200,
  "success": true,
  "data": {
    "groupId": 1,
    "status": "PENDING",
    "count": 2,
    "items": [
      {
        "userId": 104,
        "nickName": "Member2",
        "profileImage": null,
        "groupUserId": 3,
        "status": "PENDING",
        "joinedAt": "2025-12-29T17:43:41.480751",
        "joinRequestMessage": null
      },
      {
        "userId": 103,
        "nickName": "Member1",
        "profileImage": null,
        "groupUserId": 2,
        "status": "PENDING",
        "joinedAt": "2025-12-29T17:43:37.570959",
        "joinRequestMessage": "안녕하세요! 승인제 모임 참가 신청합니다. 메시지 저장 테스트입니다."
      }
    ],
    "serverTime": "2025-12-29T17:43:45.974586"
  }
}

🧩 응답 필드 설명

필드 타입 설명
groupId Long 모임 ID
status GroupUserV2Status 조회 대상 status
items JoinRequestItem[] 가입/참여 관련 목록
serverTime LocalDateTime 서버 기준 응답 시간

JoinRequestItem

필드 타입 설명
userId Long 유저 ID
nickName String 닉네임
profileImage String 프로필 이미지 URL(없으면 null)
groupUserId Long 멤버십 ID
status GroupUserV2Status 멤버십 상태
joinedAt LocalDateTime 신청/참여 시간(정책상 joinedAt)
joinRequestMessage String 가입 신청 메시지(없으면 null)

❗ 오류 케이스

HTTP ErrorCode 조건
401 UNAUTHORIZED 인증 실패
404 GROUP_NOT_FOUND_BY_ID 모임 없음
403 NO_PERMISSION_TO_VIEW_JOIN_REQUESTS HOST가 아닌데 조회 시도

Clone this wiki locally