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 @@ -13,6 +13,7 @@
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 Down Expand Up @@ -84,4 +85,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
Comment on lines +91 to +100

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -HI -t f '.*Security.*\.java' src/main/java src/test/java
rg -n -C3 '`@AuthenticationPrincipal`|authorizeHttpRequests|requestMatchers|SecurityFilterChain|setPrincipal|Authentication' \
  src/main/java src/test/java

Repository: Leets-Official/TDD-BE

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -HI -t f '.*(UserPrincipal|DeliveryPartyController|DeliveryPartyControllerTest|SecurityConfig).java' src/main/java src/test/java

echo "== DeliveryPartyController relevant section =="
cat -n src/main/java/com/leets/tdd/party/controller/DeliveryPartyController.java | sed -n '1,140p'

echo "== SecurityConfig =="
cat -n src/main/java/com/leets/tdd/global/config/SecurityConfig.java

echo "== JwtAuthenticationFilter relevant section =="
cat -n src/main/java/com/leets/tdd/global/config/JwtAuthenticationFilter.java | sed -n '1,115p'

echo "== UserPrincipal def/classpath search =="
rg -n -C5 'class UserPrincipal|record UserPrincipal|UserPrincipal' src/main/java src/test/java | head -n 200

echo "== DeliveryPartyControllerTest files =="
fd -HI -t f 'DeliveryPartyControllerTest.java' src/test/java -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {}

Repository: Leets-Official/TDD-BE

Length of output: 35002


🌐 Web query:

Spring Security @AuthenticationPrincipal Long principal needs SecurityContext AuthenticationPrincipalArgumentResolver converter Long

💡 Result:

In Spring Security, the @AuthenticationPrincipal annotation relies on the AuthenticationPrincipalArgumentResolver to resolve the principal from the SecurityContext to a method argument [1][2]. When attempting to use @AuthenticationPrincipal directly with a Long or primitive long, you may encounter issues because the resolver checks if the principal is assignable to the requested parameter type [3][4]. Important technical constraints: 1. Primitive Types Not Supported: You cannot use primitive types (e.g., long) as arguments for @AuthenticationPrincipal, as these are not considered assignable from the object stored in the SecurityContext, which is typically a wrapper type or custom object [3][4]. Even if the underlying principal is a Long, using a primitive long parameter often causes errors or results in a null value [4]. 2. Type Mismatch: If the principal stored in the SecurityContext is not directly assignable to the type requested in your controller (e.g., you are trying to inject a Long but the principal is a custom UserDetails object), the resolver will return null by default [1][5]. If you set errorOnInvalidType = true in the annotation, it will throw a ClassCastException [1][6]. Recommended Approaches: If your principal object is a custom user class and you simply need to extract a Long ID: 1. Use a SpEL Expression: You can use the expression attribute of @AuthenticationPrincipal to navigate to the desired field [6][2]. If your principal has a getId method, you can use: @AuthenticationPrincipal(expression = "id") Long userId 2. Use a Custom Meta-Annotation or Argument Resolver: If you need more complex conversion logic that SpEL cannot handle, you should create a custom argument resolver. You can implement the HandlerMethodArgumentResolver interface to inspect the security context and perform manual conversion of the principal to your desired Long type. 3. Controller Logic: Alternatively, inject the full principal object (e.g., @AuthenticationPrincipal CustomUser user) and retrieve the ID inside your method body using user.getId. This is often the cleanest and most readable approach [2]. If you are encountering specific errors with Long conversion, ensure that the object stored in your Authentication principal is indeed the type you expect (e.g., verify that principal is not a UserDetails wrapper when you are expecting a raw Long) [1][7].

Citations:


Require authentication and accept the configured principal type.

/api/v1/delivery-parties/** is permitted to all requests in SecurityConfig, so this delete endpoint and the matching update endpoint bypass the JWT checker. If a token is present, JwtAuthenticationFilter stores a UserPrincipal, not a Long, so @AuthenticationPrincipal Long will not resolve under the current config. Remove this from PUBLIC_ENDPOINTS unless these methods are genuinely public, and/or change these parameters to @AuthenticationPrincipal UserPrincipal. Add a failing @WebMvcTest for the unauthenticated endpoint.

🤖 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 91 - 100, Secure the delivery-party delete and matching update
endpoints by removing their route from SecurityConfig.PUBLIC_ENDPOINTS, and
change their `@AuthenticationPrincipal` parameters to the configured UserPrincipal
type, extracting the user ID as needed by deliveryPartyService. Add a failing
`@WebMvcTest` that verifies unauthenticated requests to the endpoint are rejected.

);

return ResponseEntity.ok(deletedPartyId);
}
Comment on lines +90 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the Swagger contract for this endpoint.

The PR explicitly leaves Swagger unfinished, so consumers will not see the owner-only cancellation behavior, CANCELED transition, response, or 400/403/404 errors.

🤖 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 90 - 104, The deleteDeliveryParty endpoint in
DeliveryPartyController is missing its Swagger documentation; add an operation
description documenting owner-only cancellation, the transition to CANCELED, the
successful deleted-party response, and 400, 403, and 404 error responses.

}
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
Expand Up @@ -13,7 +13,7 @@ public enum PartyErrorCode implements ErrorCode {
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 @@ -114,12 +114,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 +130,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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ class DeliveryPartyServiceTest {
LocalDateTime.now()
);


when(deliveryPartyRepository.findById(1L))
.thenReturn(Optional.of(deliveryParty));

Expand Down Expand Up @@ -109,7 +108,6 @@ class DeliveryPartyServiceTest {
LocalDateTime.now()
);


when(deliveryPartyRepository.findById(1L))
.thenReturn(Optional.of(deliveryParty));

Expand All @@ -134,6 +132,7 @@ class DeliveryPartyServiceTest {
// then
verify(deliveryPartyRepository)
.save(deliveryParty);

assertThat(deliveryParty.getTitle())
.isEqualTo("변경된 제목");

Expand All @@ -146,4 +145,47 @@ class DeliveryPartyServiceTest {
assertThat(deliveryParty.getOrderExpectedAt())
.isEqualTo(LocalDateTime.of(2026, 7, 25, 20, 0));
}


@Test
void 배달팟_삭제_성공() {

// given
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.findById(1L))
.thenReturn(Optional.of(deliveryParty));


// when
Long result = null;

try {
result = deliveryPartyService.deleteDeliveryParty(1L, 1L);
} catch (Exception e) {
e.printStackTrace();
throw e;
}


// then
assertThat(result)
.isEqualTo(1L);
}
}
Loading