Skip to content

Commit 5c3f26e

Browse files
authored
Feat : 파티 추가 기능(파티 목록 페이지네이션, 파티탈퇴, 파티 모집 완료)
1 parent 87d2080 commit 5c3f26e

File tree

5 files changed

+191
-5
lines changed

5 files changed

+191
-5
lines changed

src/main/java/ita/tinybite/domain/party/controller/PartyController.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.swagger.v3.oas.annotations.Operation;
44
import io.swagger.v3.oas.annotations.Parameter;
55
import io.swagger.v3.oas.annotations.media.Content;
6+
import io.swagger.v3.oas.annotations.media.ExampleObject;
67
import io.swagger.v3.oas.annotations.media.Schema;
78
import io.swagger.v3.oas.annotations.responses.ApiResponse;
89
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -73,6 +74,103 @@ public APIResponse<Long> joinParty(
7374
return success(partyService.joinParty(partyId, userId));
7475
}
7576

77+
78+
/**
79+
* 파티 탈퇴
80+
*/
81+
@Operation(
82+
summary = "파티 탈퇴",
83+
description = "현재 참가 중인 파티에서 탈퇴합니다. 탈퇴 시 인원이 줄어들면 모집 완료 상태가 다시 모집 중으로 변경될 수 있습니다."
84+
)
85+
@ApiResponses({
86+
@ApiResponse(
87+
responseCode = "200",
88+
description = "파티 탈퇴 성공",
89+
content = @Content(
90+
mediaType = "application/json",
91+
schema = @Schema(implementation = String.class),
92+
examples = @ExampleObject(value = "\"파티에서 탈퇴했습니다.\"")
93+
)
94+
),
95+
@ApiResponse(
96+
responseCode = "400",
97+
description = "잘못된 요청 (파티에 참가하지 않은 사용자)",
98+
content = @Content(
99+
mediaType = "application/json",
100+
examples = @ExampleObject(value = "{\"message\": \"파티에 참가하지 않은 사용자입니다.\"}")
101+
)
102+
),
103+
@ApiResponse(
104+
responseCode = "404",
105+
description = "파티를 찾을 수 없음",
106+
content = @Content(
107+
mediaType = "application/json",
108+
examples = @ExampleObject(value = "{\"message\": \"파티를 찾을 수 없습니다.\"}")
109+
)
110+
)
111+
})
112+
@DeleteMapping("/{partyId}/leave")
113+
public ResponseEntity<?> leaveParty(
114+
@PathVariable Long partyId,
115+
@AuthenticationPrincipal Long userId) {
116+
117+
partyService.leaveParty(partyId, userId);
118+
return ResponseEntity.ok("파티에서 탈퇴했습니다.");
119+
}
120+
121+
122+
/**
123+
* 파티 모집 완료
124+
*/
125+
126+
@Operation(
127+
summary = "파티 모집 완료 처리",
128+
description = "파티 관리자가 정원 미달이어도 수동으로 모집을 완료 처리합니다. 모집 중 상태의 파티만 완료 처리할 수 있습니다."
129+
)
130+
@ApiResponses({
131+
@ApiResponse(
132+
responseCode = "200",
133+
description = "모집 완료 처리 성공",
134+
content = @Content(
135+
mediaType = "application/json",
136+
schema = @Schema(implementation = String.class),
137+
examples = @ExampleObject(value = "\"모집이 완료되었습니다.\"")
138+
)
139+
),
140+
@ApiResponse(
141+
responseCode = "400",
142+
description = "잘못된 요청 (모집 중 상태가 아님)",
143+
content = @Content(
144+
mediaType = "application/json",
145+
examples = @ExampleObject(value = "{\"message\": \"모집 중인 파티만 완료 처리할 수 있습니다.\"}")
146+
)
147+
),
148+
@ApiResponse(
149+
responseCode = "403",
150+
description = "권한 없음 (관리자가 아님)",
151+
content = @Content(
152+
mediaType = "application/json",
153+
examples = @ExampleObject(value = "{\"message\": \"파티 관리자만 모집을 완료할 수 있습니다.\"}")
154+
)
155+
),
156+
@ApiResponse(
157+
responseCode = "404",
158+
description = "파티를 찾을 수 없음",
159+
content = @Content(
160+
mediaType = "application/json",
161+
examples = @ExampleObject(value = "{\"message\": \"파티를 찾을 수 없습니다.\"}")
162+
)
163+
)
164+
})
165+
@PatchMapping("/{partyId}/complete")
166+
public ResponseEntity<?> completeRecruitment(
167+
@PathVariable Long partyId,
168+
@AuthenticationPrincipal Long userId) {
169+
170+
partyService.completeRecruitment(partyId, userId);
171+
return ResponseEntity.ok("모집이 완료되었습니다.");
172+
}
173+
76174
@Operation(summary = "참여 승인", description = "파티장이 참여를 승인하면 단체 채팅방에 자동 입장됩니다")
77175
@ApiResponses({
78176
@ApiResponse(

src/main/java/ita/tinybite/domain/party/dto/request/PartyListRequest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public class PartyListRequest {
1111
private PartyCategory category; // 필터: 카테고리
1212
private PartySortType sortType; // 정렬: 최신순/거리순
1313

14+
private Integer page;
15+
private Integer size;
16+
1417
// 거리순 정렬을 위한 현재 위치 (선택)
1518
private Double userLat;
1619
private Double userLon;

src/main/java/ita/tinybite/domain/party/entity/Party.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ public void incrementParticipants() {
9494
this.currentParticipants++;
9595
}
9696

97+
public void decrementParticipants() {
98+
this.currentParticipants--;
99+
}
100+
101+
102+
public void changePartyStatus(PartyStatus partyStatus) {
103+
this.status = partyStatus;
104+
}
105+
97106
public String getTimeAgo() {
98107
LocalDateTime now = LocalDateTime.now();
99108

src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.springframework.stereotype.Repository;
1212

1313
import java.util.List;
14+
import java.util.Optional;
1415

1516
@Repository
1617
public interface PartyParticipantRepository extends JpaRepository<PartyParticipant, Long> {
@@ -88,4 +89,7 @@ boolean existsByParty_IdAndUser_UserIdAndStatus(
8889
Long userId,
8990
ParticipantStatus status
9091
);
92+
93+
@Query("SELECT pp FROM PartyParticipant pp WHERE pp.party.id = :partyId AND pp.user.id = :userId")
94+
Optional<PartyParticipant> findByPartyIdAndUserId(@Param("partyId") Long partyId, @Param("userId") Long userId);
9195
}

src/main/java/ita/tinybite/domain/party/service/PartyService.java

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.transaction.annotation.Transactional;
3131

3232
import java.time.LocalDateTime;
33+
import java.util.ArrayList;
3334
import java.util.Comparator;
3435
import java.util.List;
3536
import java.util.stream.Collectors;
@@ -135,13 +136,16 @@ public PartyListResponse getPartyList(Long userId, PartyListRequest request) {
135136
user = userRepository.findById(userId).orElse(null);
136137
}
137138

139+
// 페이지네이션 파라미터 (기본값: page=0, size=20)
140+
int page = request.getPage() != null ? request.getPage() : 0;
141+
int size = request.getSize() != null ? request.getSize() : 20;
142+
138143
// 동네 기준으로 파티 조회
139144
List<Party> parties = fetchPartiesByLocation(user, request);
140145

141146
// PartyCardResponse로 변환
142147
List<PartyCardResponse> cardResponses = parties.stream()
143148
.map(party -> {
144-
// 거리순 정렬인 경우 거리 계산
145149
if (request.getSortType() == PartySortType.DISTANCE) {
146150
double distance = DistanceCalculator.calculateDistance(
147151
request.getUserLat(),
@@ -167,11 +171,37 @@ public PartyListResponse getPartyList(Long userId, PartyListRequest request) {
167171
.sorted(getComparator(request.getSortType()))
168172
.collect(Collectors.toList());
169173

174+
// 진행 중 + 마감된 파티 합치기 (진행 중이 먼저)
175+
List<PartyCardResponse> allParties = new ArrayList<>();
176+
allParties.addAll(activeParties);
177+
allParties.addAll(closedParties);
178+
179+
// 페이지네이션 적용
180+
int startIndex = page * size;
181+
int endIndex = Math.min(startIndex + size, allParties.size());
182+
183+
List<PartyCardResponse> paginatedParties = allParties.subList(
184+
Math.min(startIndex, allParties.size()),
185+
endIndex
186+
);
187+
188+
// hasNext 계산
189+
boolean hasNext = endIndex < allParties.size();
190+
191+
// 페이지네이션된 결과를 다시 진행 중/마감으로 분리
192+
List<PartyCardResponse> paginatedActiveParties = paginatedParties.stream()
193+
.filter(p -> !p.getIsClosed())
194+
.collect(Collectors.toList());
195+
196+
List<PartyCardResponse> paginatedClosedParties = paginatedParties.stream()
197+
.filter(PartyCardResponse::getIsClosed)
198+
.collect(Collectors.toList());
199+
170200
return PartyListResponse.builder()
171-
.activeParties(activeParties)
172-
.closedParties(closedParties)
173-
.totalCount(parties.size())
174-
.hasNext(false)
201+
.activeParties(paginatedActiveParties)
202+
.closedParties(paginatedClosedParties)
203+
.totalCount(allParties.size())
204+
.hasNext(hasNext)
175205
.build();
176206
}
177207

@@ -260,6 +290,48 @@ public Long joinParty(Long partyId, Long userId) {
260290
return oneToOneChatRoom.getId();
261291
}
262292

293+
/**
294+
* 파티 탈퇴 - 인원 감소 시 다시 모집 중으로 변경
295+
*/
296+
public void leaveParty(Long partyId, Long userId) {
297+
Party party = partyRepository.findById(partyId)
298+
.orElseThrow(() -> new IllegalArgumentException("파티를 찾을 수 없습니다."));
299+
300+
PartyParticipant member = partyParticipantRepository
301+
.findByPartyIdAndUserId(partyId, userId)
302+
.orElseThrow(() -> new IllegalArgumentException("파티에 참가하지 않은 사용자입니다."));
303+
304+
partyParticipantRepository.delete(member);
305+
306+
// 파티 현재 참여자 수 감소
307+
party.decrementParticipants();
308+
309+
// 모집 완료 상태였다면 다시 모집 중으로 변경
310+
if (party.getStatus() == PartyStatus.COMPLETED) {
311+
party.changePartyStatus(PartyStatus.RECRUITING);
312+
partyRepository.save(party);
313+
}
314+
}
315+
316+
317+
public void completeRecruitment(Long partyId, Long userId) {
318+
Party party = partyRepository.findById(partyId)
319+
.orElseThrow(() -> new IllegalArgumentException("파티를 찾을 수 없습니다."));
320+
321+
// 파티장 권한 확인
322+
if (!party.getHost().getUserId().equals(userId)) {
323+
throw new IllegalStateException("파티장만 승인할 수 있습니다");
324+
}
325+
326+
if (party.getStatus() != PartyStatus.RECRUITING) {
327+
throw new IllegalStateException("모집 중인 파티만 완료 처리할 수 있습니다.");
328+
}
329+
330+
party.changePartyStatus(PartyStatus.COMPLETED);
331+
partyRepository.save(party);
332+
}
333+
334+
263335
private void validateProductLink(PartyCategory category, String productLink) {
264336
// 배달은 링크 불가
265337
if (category == PartyCategory.DELIVERY && productLink != null) {

0 commit comments

Comments
 (0)