Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ public void commence(
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
if (request.getRequestURI().matches("/api/v1/parties/[^/]+/close")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(
objectMapper.writeValueAsString(ApiResponse.fail("로그인이 필요합니다."))
);
return;
}

Object errorType = request.getAttribute(JwtAuthenticationFilter.JWT_ERROR_ATTRIBUTE);

UserErrorCode errorCode;
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/com/leets/tdd/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ public class SecurityConfig {
"/api/v1/auth/password-reset",
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/v1/delivery-parties/**",
"/ws/**"
};

Expand All @@ -46,6 +45,10 @@ public SecurityFilterChain securityFilterChain(
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(PUBLIC_ENDPOINTS).permitAll()
.requestMatchers(HttpMethod.GET,
"/api/v1/delivery-parties",
"/api/v1/delivery-parties/*").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/delivery-parties").permitAll()
// 계정등록(회원가입 완료)은 아직 로그인 전 상태라 토큰이 없다. GET(마이페이지)은
// 인증이 필요하니 이 경로/메서드만 예외로 공개한다.
.requestMatchers(HttpMethod.POST, "/api/v1/users/me").permitAll()
Expand All @@ -62,4 +65,4 @@ public SecurityFilterChain securityFilterChain(
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import com.leets.tdd.party.dto.request.CreateDeliveryPartyRequest;
import com.leets.tdd.party.dto.request.UpdateDeliveryPartyRequest;
import com.leets.tdd.party.dto.response.CreateDeliveryPartyResponse;
import com.leets.tdd.party.dto.response.CloseDeliveryPartyResponse;
import com.leets.tdd.party.dto.response.DeliveryPartyDetailResponse;
import com.leets.tdd.party.service.DeliveryPartyService;
import com.leets.tdd.global.common.ApiResponse;
import com.leets.tdd.global.jwt.UserPrincipal;

import lombok.RequiredArgsConstructor;

Expand All @@ -15,6 +18,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -23,7 +27,7 @@


@RestController
@RequestMapping("/api/v1/delivery-parties")
@RequestMapping({"/api/v1/delivery-parties", "/api/v1/parties"})
@RequiredArgsConstructor
public class DeliveryPartyController {

Expand Down Expand Up @@ -84,4 +88,17 @@ public ResponseEntity<Void> updateDeliveryParty(

return ResponseEntity.ok().build();
}

@PatchMapping("/{partyId}/close")
public ResponseEntity<ApiResponse<CloseDeliveryPartyResponse>> closeDeliveryParty(
@PathVariable Long partyId,
@AuthenticationPrincipal UserPrincipal currentUser
) {
CloseDeliveryPartyResponse response = deliveryPartyService.closeDeliveryParty(
partyId,
currentUser.userId()
);

return ResponseEntity.ok(ApiResponse.success("배달팟 모집이 마감되었습니다.", response));
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/leets/tdd/party/domain/DeliveryParty.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,10 @@ public void update(
this.orderExpectedAt = orderExpectedAt;
this.updatedAt = LocalDateTime.now();
}

public void close() {
this.status = PartyStatus.CLOSED;
this.closedAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.leets.tdd.party.dto.response;

public record CloseDeliveryPartyResponse(
Long partyId,
String status
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public enum PartyErrorCode implements ErrorCode {

PARTY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 배달팟입니다."),
NOT_OWNER(HttpStatus.FORBIDDEN, "배달팟을 수정할 권한이 없습니다."),
CLOSE_FORBIDDEN(HttpStatus.FORBIDDEN, "배달팟 모집을 마감할 권한이 없습니다."),
ALREADY_CLOSED(HttpStatus.BAD_REQUEST, "이미 모집이 마감된 배달팟입니다."),
CLOSE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "배달팟 모집 마감에 실패했습니다."),
INVALID_PARTY_STATUS(
HttpStatus.BAD_REQUEST,
"현재 상태에서는 배달팟을 수정할 수 없습니다."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.leets.tdd.party.dto.request.CreateDeliveryPartyRequest;
import com.leets.tdd.party.dto.request.UpdateDeliveryPartyRequest;
import com.leets.tdd.party.dto.response.CreateDeliveryPartyResponse;
import com.leets.tdd.party.dto.response.CloseDeliveryPartyResponse;
import com.leets.tdd.party.dto.response.DeliveryPartyDetailResponse;
import com.leets.tdd.party.exception.PartyErrorCode;
import com.leets.tdd.party.exception.PartyException;
Expand All @@ -14,6 +15,7 @@
import com.leets.tdd.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
Expand Down Expand Up @@ -133,4 +135,25 @@ public void updateDeliveryParty(
);
deliveryPartyRepository.save(deliveryParty);
}

@Transactional
public CloseDeliveryPartyResponse closeDeliveryParty(Long partyId, Long currentUserId) {
DeliveryParty deliveryParty = deliveryPartyRepository.findWithLockById(partyId)
.orElseThrow(() -> new PartyException(PartyErrorCode.PARTY_NOT_FOUND));

if (!deliveryParty.getCreatorId().equals(currentUserId)) {
throw new PartyException(PartyErrorCode.CLOSE_FORBIDDEN);
}

if (deliveryParty.getStatus() != PartyStatus.RECRUITING) {
throw new PartyException(PartyErrorCode.ALREADY_CLOSED);
}

deliveryParty.close();

return new CloseDeliveryPartyResponse(
deliveryParty.getId() == null ? partyId : deliveryParty.getId(),
deliveryParty.getStatus().name()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import com.leets.tdd.party.domain.PartyStatus;
import com.leets.tdd.party.dto.request.UpdateDeliveryPartyRequest;
import com.leets.tdd.party.dto.response.DeliveryPartyDetailResponse;
import com.leets.tdd.party.dto.response.CloseDeliveryPartyResponse;
import com.leets.tdd.party.exception.PartyErrorCode;
import com.leets.tdd.party.exception.PartyException;
import com.leets.tdd.party.repository.DeliveryPartyRepository;
import com.leets.tdd.settlement.domain.SettlementStatus;
Expand Down Expand Up @@ -146,4 +148,64 @@ class DeliveryPartyServiceTest {
assertThat(deliveryParty.getOrderExpectedAt())
.isEqualTo(LocalDateTime.of(2026, 7, 25, 20, 0));
}

@Test
void 파티장이_모집중인_배달팟을_마감한다() {
DeliveryParty deliveryParty = new DeliveryParty(
1L, 1L, "치킨 같이 시켜요", "오늘 저녁 배달팟", 2, 4,
LocalDateTime.of(2026, 7, 24, 19, 30), PartyStatus.RECRUITING,
null, SettlementStatus.NONE, null, null, null,
LocalDateTime.now(), LocalDateTime.now()
);
when(deliveryPartyRepository.findWithLockById(1L)).thenReturn(Optional.of(deliveryParty));

CloseDeliveryPartyResponse response = deliveryPartyService.closeDeliveryParty(1L, 1L);

assertThat(response.partyId()).isEqualTo(1L);
assertThat(response.status()).isEqualTo("CLOSED");
assertThat(deliveryParty.getStatus()).isEqualTo(PartyStatus.CLOSED);
assertThat(deliveryParty.getClosedAt()).isNotNull();
}

@Test
void 존재하지_않는_배달팟은_마감할_수_없다() {
when(deliveryPartyRepository.findWithLockById(999L)).thenReturn(Optional.empty());

assertThatThrownBy(() -> deliveryPartyService.closeDeliveryParty(999L, 1L))
.isInstanceOfSatisfying(PartyException.class,
exception -> assertThat(exception.getErrorCode())
.isEqualTo(PartyErrorCode.PARTY_NOT_FOUND));
}

@Test
void 파티장이_아니면_모집을_마감할_수_없다() {
DeliveryParty deliveryParty = new DeliveryParty(
1L, 1L, "치킨 같이 시켜요", "오늘 저녁 배달팟", 2, 4,
LocalDateTime.of(2026, 7, 24, 19, 30), PartyStatus.RECRUITING,
null, SettlementStatus.NONE, null, null, null,
LocalDateTime.now(), LocalDateTime.now()
);
when(deliveryPartyRepository.findWithLockById(1L)).thenReturn(Optional.of(deliveryParty));

assertThatThrownBy(() -> deliveryPartyService.closeDeliveryParty(1L, 2L))
.isInstanceOfSatisfying(PartyException.class,
exception -> assertThat(exception.getErrorCode())
.isEqualTo(PartyErrorCode.CLOSE_FORBIDDEN));
}

@Test
void 이미_마감된_배달팟은_다시_마감할_수_없다() {
DeliveryParty deliveryParty = new DeliveryParty(
1L, 1L, "치킨 같이 시켜요", "오늘 저녁 배달팟", 2, 4,
LocalDateTime.of(2026, 7, 24, 19, 30), PartyStatus.CLOSED,
LocalDateTime.now(), SettlementStatus.NONE, null, null, null,
LocalDateTime.now(), LocalDateTime.now()
);
when(deliveryPartyRepository.findWithLockById(1L)).thenReturn(Optional.of(deliveryParty));

assertThatThrownBy(() -> deliveryPartyService.closeDeliveryParty(1L, 1L))
.isInstanceOfSatisfying(PartyException.class,
exception -> assertThat(exception.getErrorCode())
.isEqualTo(PartyErrorCode.ALREADY_CLOSED));
}
}
Loading