Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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|delivery-parties)/[^/]+/join")) {
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
5 changes: 3 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,8 @@ public SecurityFilterChain securityFilterChain(
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(PUBLIC_ENDPOINTS).permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/parties/**", "/api/v1/delivery-parties/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/parties", "/api/v1/delivery-parties").permitAll()
// 계정등록(회원가입 완료)은 아직 로그인 전 상태라 토큰이 없다. GET(마이페이지)은
// 인증이 필요하니 이 경로/메서드만 예외로 공개한다.
.requestMatchers(HttpMethod.POST, "/api/v1/users/me").permitAll()
Expand All @@ -62,4 +63,4 @@ public SecurityFilterChain securityFilterChain(
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
import com.leets.tdd.party.dto.request.UpdateDeliveryPartyRequest;
import com.leets.tdd.party.dto.response.CreateDeliveryPartyResponse;
import com.leets.tdd.party.dto.response.DeliveryPartyDetailResponse;
import com.leets.tdd.party.dto.response.JoinDeliveryPartyResponse;
import com.leets.tdd.party.service.DeliveryPartyService;
import com.leets.tdd.global.common.ApiResponse;
import com.leets.tdd.global.jwt.UserPrincipal;

import lombok.RequiredArgsConstructor;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -23,7 +27,7 @@


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

Expand Down Expand Up @@ -68,6 +72,21 @@ public ResponseEntity<DeliveryPartyDetailResponse> getDeliveryPartyDetail(
}


// 배달팟 참여 API
@PostMapping("/{partyId}/join")
public ResponseEntity<ApiResponse<JoinDeliveryPartyResponse>> joinDeliveryParty(
@PathVariable Long partyId,
@AuthenticationPrincipal UserPrincipal currentUser
) {
JoinDeliveryPartyResponse response = deliveryPartyService.joinDeliveryParty(
partyId,
currentUser.userId()
);

return ResponseEntity.ok(ApiResponse.success("배달팟에 참여했습니다.", response));
}


// 배달팟 수정 API
@PutMapping("/{partyId}")
public ResponseEntity<Void> updateDeliveryParty(
Expand All @@ -84,4 +103,21 @@ public ResponseEntity<Void> updateDeliveryParty(

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


// 배달팟 삭제(취소) API
@DeleteMapping("/{partyId}")
public ResponseEntity<Long> deleteDeliveryParty(
@PathVariable Long partyId,
@AuthenticationPrincipal Long currentUserId
) {

Long deletedPartyId =
deliveryPartyService.deleteDeliveryParty(
partyId,
currentUserId
);

return ResponseEntity.ok(deletedPartyId);
}
Comment on lines +143 to +159

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the actual principal type set by the JWT filter, and find other `@AuthenticationPrincipal` Long usages.
rg -n -B3 -A10 'UsernamePasswordAuthenticationToken' --type=java
rg -n '`@AuthenticationPrincipal`' --type=java

Repository: Leets-Official/TDD-BE

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -u

echo "Files:"
git ls-files | sed -n '1,120p'

echo
echo "Find DeliveryPartyController and likely security/filter files:"
fd -i 'DeliveryPartyController|.*Security|.*Filter|.*Auth' . || true

echo
echo "Search AuthenticationPrincipal usages:"
rg -n '`@AuthenticationPrincipal`|AuthenticationPrincipalArgumentResolver|errorOnInvalidType|setPrincipal|setAuthentication|getPrincipal|UserPrincipal|username|principal' --type=java || true

Repository: Leets-Official/TDD-BE

Length of output: 8200


Bind the JWT principal to the controller parameter consistently.

The JWT filter’s authenticated identity is built as UserPrincipal, while update/delete receive @AuthenticationPrincipal Long currentUserId. Spring’s principal resolver won’t automatically unbox that composite principal, so owner authorization can resolve to null and reject owners with NOT_OWNER. Use @AuthenticationPrincipal UserPrincipal currentUser here and read currentUser.userId() (and apply the same fix consistently if other controller endpoints need the current user ID).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/leets/tdd/party/controller/DeliveryPartyController.java`
around lines 106 - 122, Update deleteDeliveryParty to accept
`@AuthenticationPrincipal` UserPrincipal currentUser instead of Long
currentUserId, and pass currentUser.userId() to
deliveryPartyService.deleteDeliveryParty. Apply the same principal binding and
userId extraction to any nearby update/delete controller endpoints using
`@AuthenticationPrincipal` Long.

}
36 changes: 33 additions & 3 deletions src/main/java/com/leets/tdd/party/domain/DeliveryParty.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.leets.tdd.party.domain;

import com.leets.tdd.party.exception.PartyErrorCode;
import com.leets.tdd.party.exception.PartyException;
import com.leets.tdd.settlement.domain.SettlementStatus;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -19,7 +21,9 @@
@Getter
@Table(name = "delivery_parties")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
/** 배달팟의 공통 정보와 정산 진행 상태를 함께 저장하는 엔티티입니다. */
/**
* 배달팟의 공통 정보와 정산 진행 상태를 함께 저장하는 엔티티입니다.
*/
public class DeliveryParty {

@Id
Expand Down Expand Up @@ -77,6 +81,7 @@ public class DeliveryParty {
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;


public DeliveryParty(
Long creatorId,
Long foodCategoryId,
Expand Down Expand Up @@ -111,30 +116,43 @@ public DeliveryParty(
this.updatedAt = updatedAt;
}

public void requestSettlement(int totalAmount, Long bankAccountId, LocalDateTime requestedAt) {
if (settlementStatus != SettlementStatus.NONE && settlementStatus != SettlementStatus.CANCELED) {

public void requestSettlement(
int totalAmount,
Long bankAccountId,
LocalDateTime requestedAt
) {
if (settlementStatus != SettlementStatus.NONE
&& settlementStatus != SettlementStatus.CANCELED) {
throw new IllegalStateException("정산 요청을 시작할 수 없는 상태입니다.");
}

this.settlementStatus = SettlementStatus.REQUESTED;
this.settlementTotalAmount = totalAmount;
this.settlementBankAccountId = bankAccountId;
this.settlementRequestedAt = requestedAt;
}


public void completeSettlement() {
if (settlementStatus != SettlementStatus.REQUESTED) {
throw new IllegalStateException("진행 중인 정산만 완료할 수 있습니다.");
}

this.settlementStatus = SettlementStatus.COMPLETED;
}


public void cancelSettlement() {
if (settlementStatus != SettlementStatus.REQUESTED) {
throw new IllegalStateException("진행 중인 정산만 취소할 수 있습니다.");
}

this.settlementStatus = SettlementStatus.CANCELED;
}


// 배달팟 수정
public void update(
String title,
String description,
Expand All @@ -147,4 +165,16 @@ public void update(
this.orderExpectedAt = orderExpectedAt;
this.updatedAt = LocalDateTime.now();
}


// 배달팟 모집 취소
public void cancel() {

if (status != PartyStatus.RECRUITING) {
throw new PartyException(PartyErrorCode.INVALID_PARTY_STATUS);
}

this.status = PartyStatus.CANCELED;
this.updatedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.leets.tdd.party.dto.response;

public record JoinDeliveryPartyResponse(
Long partyId,
long currentParticipants,
Integer maxParticipants
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
public enum PartyErrorCode implements ErrorCode {

PARTY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 배달팟입니다."),
ALREADY_JOINED(HttpStatus.CONFLICT, "이미 참여한 배달팟입니다."),
RECRUITMENT_CLOSED(HttpStatus.BAD_REQUEST, "모집이 종료된 배달팟입니다."),
PARTY_FULL(HttpStatus.CONFLICT, "모집 인원이 모두 찼습니다."),
JOIN_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "배달팟 참여에 실패했습니다."),
NOT_OWNER(HttpStatus.FORBIDDEN, "배달팟을 수정할 권한이 없습니다."),
INVALID_PARTY_STATUS(
HttpStatus.BAD_REQUEST,
"현재 상태에서는 배달팟을 수정할 수 없습니다."
"현재 상태에서는 배달팟을 변경할 수 없습니다."
);

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface PartyParticipantRepository extends JpaRepository<PartyParticipa

boolean existsByPartyIdAndUserIdAndStatus(Long partyId, Long userId, PartyParticipantStatus status);

long countByPartyIdAndStatus(Long partyId, PartyParticipantStatus status);

List<PartyParticipant> findAllByPartyIdAndStatus(Long partyId, PartyParticipantStatus status);

Optional<PartyParticipant> findByPartyIdAndUserId(Long partyId, Long userId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package com.leets.tdd.party.service;

import com.leets.tdd.party.domain.DeliveryParty;
import com.leets.tdd.party.domain.PartyParticipant;
import com.leets.tdd.party.domain.PartyParticipantRole;
import com.leets.tdd.party.domain.PartyParticipantStatus;
import com.leets.tdd.party.domain.PartyStatus;
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.DeliveryPartyDetailResponse;
import com.leets.tdd.party.dto.response.JoinDeliveryPartyResponse;
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.party.repository.PartyParticipantRepository;
import com.leets.tdd.settlement.domain.SettlementStatus;
import com.leets.tdd.user.domain.User;
import com.leets.tdd.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
Expand All @@ -24,6 +31,7 @@
public class DeliveryPartyService {

private final DeliveryPartyRepository deliveryPartyRepository;
private final PartyParticipantRepository partyParticipantRepository;
private final UserRepository userRepository;


Expand Down Expand Up @@ -103,6 +111,53 @@ public DeliveryPartyDetailResponse getDeliveryPartyDetail(Long partyId) {
}


// 배달팟 참여 API
@Transactional
public JoinDeliveryPartyResponse joinDeliveryParty(Long partyId, Long currentUserId) {
DeliveryParty deliveryParty = deliveryPartyRepository.findWithLockById(partyId)
.orElseThrow(() -> new PartyException(PartyErrorCode.PARTY_NOT_FOUND));

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

// 방장은 생성 시점부터 참여 인원으로 간주한다.
if (deliveryParty.getCreatorId().equals(currentUserId)
|| partyParticipantRepository.existsByPartyIdAndUserIdAndStatus(
partyId, currentUserId, PartyParticipantStatus.JOINED)) {
throw new PartyException(PartyErrorCode.ALREADY_JOINED);
}

long currentParticipants = currentParticipants(partyId);
if (currentParticipants >= deliveryParty.getMaxParticipants()) {
throw new PartyException(PartyErrorCode.PARTY_FULL);
}

try {
partyParticipantRepository.saveAndFlush(new PartyParticipant(
partyId,
currentUserId,
PartyParticipantRole.MEMBER,
PartyParticipantStatus.JOINED,
LocalDateTime.now()
));
} catch (DataIntegrityViolationException exception) {
throw new PartyException(PartyErrorCode.JOIN_FAILED);
}

return new JoinDeliveryPartyResponse(
partyId,
currentParticipants + 1,
deliveryParty.getMaxParticipants()
);
}

private long currentParticipants(Long partyId) {
return 1 + partyParticipantRepository.countByPartyIdAndStatus(
partyId, PartyParticipantStatus.JOINED);
}


// 배달팟 수정 API
public void updateDeliveryParty(
Long partyId,
Expand All @@ -114,12 +169,11 @@ public void updateDeliveryParty(
.orElseThrow(() -> new PartyException(PartyErrorCode.PARTY_NOT_FOUND));


// 작성자(파티장)만 수정 가능
if (!deliveryParty.getCreatorId().equals(currentUserId)) {
throw new PartyException(PartyErrorCode.NOT_OWNER);
}

// 모집 중(RECRUITING) 상태에서만 수정 가능

if (deliveryParty.getStatus() != PartyStatus.RECRUITING) {
throw new PartyException(PartyErrorCode.INVALID_PARTY_STATUS);
}
Expand All @@ -131,6 +185,30 @@ public void updateDeliveryParty(
request.getMaxParticipants(),
request.getOrderExpectedAt()
);

deliveryPartyRepository.save(deliveryParty);
}


// 배달팟 삭제(취소) API
public Long deleteDeliveryParty(
Long partyId,
Long currentUserId
) {

DeliveryParty deliveryParty = deliveryPartyRepository.findById(partyId)
.orElseThrow(() -> new PartyException(PartyErrorCode.PARTY_NOT_FOUND));

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

deliveryParty.cancel();

System.out.println("서비스 내부 상태 = " + deliveryParty.getStatus());

deliveryPartyRepository.save(deliveryParty);

return partyId;
}
}
Loading
Loading