-
Notifications
You must be signed in to change notification settings - Fork 0
Feature : 파티 기능 #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Feature : 파티 기능 #42
Conversation
partycontroller partyservice partyrepository
📝 Walkthrough개요이 변경 사항은 파티 기능을 구현합니다. 파티 생성, 목록/상세 조회, 수정, 삭제, 참여 기능을 포함하는 새로운 REST 컨트롤러와 서비스 계층을 도입합니다. Party, PartyParticipant 엔티티와 관련 DTO, 리포지토리를 추가하고, JwtTokenProvider의 토큰 전처리 기능을 강화하며, 거리 계산 유틸리티를 제공합니다. 시퀀스 다이어그램sequenceDiagram
actor Client
participant Controller as PartyController
participant Service as PartyService
participant UserRepo as UserRepository
participant PartyRepo as PartyRepository
participant PPRepo as PartyParticipantRepository
participant DB as Database
rect rgb(220, 240, 250)
note over Client,DB: 파티 생성 흐름
Client->>Controller: POST /api/parties<br/>(Authorization, PartyCreateRequest)
Controller->>Controller: JwtTokenProvider에서<br/>userId 추출
Controller->>Service: createParty(userId, request)
Service->>UserRepo: findById(userId)
UserRepo->>DB: User 조회
DB-->>UserRepo: User 반환
Service->>Service: 카테고리별 상품링크 검증<br/>(DELIVERY는 링크 불허)
Service->>Service: 썸네일 이미지 설정<br/>(첫 이미지 또는 기본값)
Service->>PartyRepo: save(Party)
PartyRepo->>DB: Party 저장
DB-->>PartyRepo: partyId 반환
Service-->>Controller: partyId
Controller-->>Client: 200 OK {partyId}
end
rect rgb(240, 220, 250)
note over Client,DB: 파티 참여 흐름
Client->>Controller: POST /api/parties/{partyId}/join<br/>(Authorization)
Controller->>Controller: JwtTokenProvider에서<br/>userId 추출
Controller->>Service: joinParty(partyId, userId)
Service->>PartyRepo: findById(partyId)
PartyRepo->>DB: Party 조회
DB-->>PartyRepo: Party 반환
Service->>Service: 파티 상태 검증<br/>(종료 여부, 정원 확인)
Service->>PPRepo: existsByPartyAndUser<br/>(party, user)
PPRepo->>DB: 중복 참여 확인
DB-->>PPRepo: 결과 반환
alt 검증 통과
Service->>PPRepo: save(PartyParticipant)<br/>(isApproved=false)
PPRepo->>DB: PartyParticipant 저장
DB-->>PPRepo: 저장 완료
Service-->>Controller: 성공
Controller-->>Client: 200 OK
else 검증 실패
Service-->>Controller: Exception
Controller-->>Client: 에러 응답
end
end
rect rgb(250, 240, 220)
note over Client,DB: 파티 목록 조회 흐름<br/>(위치 기반)
Client->>Controller: GET /api/parties<br/>(Authorization, category, latitude, longitude)
Controller->>Controller: userId 추출<br/>(선택사항)
Controller->>Service: getPartyList(userId, category,<br/>userLat, userLon)
Service->>PartyRepo: findByPickupLocation_Place<br/>AndCategory(place, category)
PartyRepo->>DB: 파티 목록 조회
DB-->>PartyRepo: Party 리스트 반환
Service->>Service: 각 파티별 거리 계산<br/>(DistanceCalculator)
Service->>PPRepo: 각 파티의 참여 상태 확인
PPRepo->>DB: 사용자 참여 여부 조회
DB-->>PPRepo: 결과 반환
Service->>Service: 활성/종료 파티 분류<br/>PartyCardResponse 변환
Service-->>Controller: PartyListResponse
Controller-->>Client: 200 OK {activeParties,<br/>closedParties, hasNext,<br/>totalCount}
end
rect rgb(240, 250, 220)
note over Client,DB: 파티 상세 조회 흐름
Client->>Controller: GET /api/parties/{partyId}<br/>(Authorization, latitude, longitude)
Controller->>Controller: userId 추출
Controller->>Service: getPartyDetail(partyId, userId,<br/>userLat, userLon)
Service->>PartyRepo: findById(partyId)
PartyRepo->>DB: Party 조회
DB-->>PartyRepo: Party 반환
Service->>Service: 거리 계산<br/>(제공된 좌표 사용)
Service->>PPRepo: existsByPartyAndUserAnd<br/>IsApprovedTrue(party, user)
PPRepo->>DB: 사용자 참여 여부 확인<br/>(승인된 상태)
DB-->>PPRepo: 결과 반환
Service->>Service: 승인된 참여자 수 계산<br/>(getApprovedParticipantCount)
Service->>Service: 생성 시간 변환<br/>(getTimeAgo)
Service->>Service: PartyDetailResponse 구성
Service-->>Controller: PartyDetailResponse
Controller-->>Client: 200 OK {partyDetail}
end
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
🧹 Nitpick comments (8)
src/main/java/ita/tinybite/domain/user/entity/User.java (1)
32-33: profileImage 필드에 길이 제약 추가 권장새로 추가된
profileImage필드에 길이 제약이 없습니다. 데이터베이스 컬럼 최적화 및 예상치 못한 대용량 데이터 방지를 위해 적절한 길이 제약을 추가하는 것이 좋습니다.🔎 제안하는 수정안
-@Column -private String profileImage; +@Column(length = 255) +private String profileImage;src/main/java/ita/tinybite/domain/party/entity/Neighborhood.java (1)
1-20:PartyRepository에서 사용되지 않는Neighborhood임포트 제거
Neighborhood엔티티가PartyRepository에 임포트되어 있지만 실제로 사용되지 않습니다. 사용되지 않는 임포트를 제거하세요.src/main/java/ita/tinybite/domain/party/entity/PartyParticipant.java (1)
9-15:(party, user)조합에 대한 유니크 제약 조건 누락동일한 사용자가 같은 파티에 중복으로 참여 신청되는 것을 방지하려면
@Table어노테이션에 유니크 제약 조건을 추가하는 것이 좋습니다.🔎 제안된 수정 사항
@Entity -@Table(name = "party_participant") +@Table(name = "party_participant", + uniqueConstraints = @UniqueConstraint(columnNames = {"party_id", "user_id"})) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class PartyParticipant {src/main/java/ita/tinybite/domain/party/repository/PartyRepository.java (1)
3-3: 사용하지 않는 import 제거 필요
Neighborhoodimport가 사용되지 않고 있습니다.🔎 제안된 수정 사항
package ita.tinybite.domain.party.repository; -import ita.tinybite.domain.party.entity.Neighborhood; import ita.tinybite.domain.party.entity.Party; import ita.tinybite.domain.party.enums.PartyCategory;src/main/java/ita/tinybite/domain/party/dto/response/PartyDetailResponse.java (1)
23-25: Entity를 Response DTO에서 직접 사용
PickupLocation이 JPA@Embeddable엔티티인데 응답 DTO에서 직접 사용하고 있습니다. API 계층과 영속성 계층 간의 결합도가 높아질 수 있습니다. 별도의 응답용 DTO를 만드는 것을 고려해 보세요.src/main/java/ita/tinybite/domain/party/dto/request/PartyCreateRequest.java (1)
43-44: 이미지 URL 유효성 검증 미적용
images리스트의 개별 요소에 대한 URL 형식 검증이 없습니다.productLink처럼 각 이미지 URL도 검증하려면 아래와 같이 수정할 수 있습니다.🔎 제안된 수정 사항
@Size(max = 5, message = "이미지는 최대 5장까지 업로드 가능합니다") - private List<String> images; + private List<@URL(message = "올바른 이미지 URL 형식이어야 합니다") String> images;src/main/java/ita/tinybite/global/util/DistanceCalculator.java (2)
4-5: 지구 반지름 상수의 정밀도 개선 권장
EARTH_RADIUS_KM이int타입으로 선언되어 있어 거리 계산 시 부동소수점 연산의 정밀도가 떨어질 수 있습니다.double타입으로 변경하는 것이 권장됩니다.🔎 수정 제안
// 지구의 평균 반지름 (단위: km) - private static final int EARTH_RADIUS_KM = 6371; + private static final double EARTH_RADIUS_KM = 6371.0;
12-15: 부동소수점 비교 개선 고려Line 13에서
double타입에 대해==연산자를 사용하고 있습니다. 부동소수점 연산의 정밀도 문제로 인해 동일한 좌표가 정확히 일치하지 않을 수 있습니다.실무에서는 동일한 좌표가 드물고, 현재 구현도 동작하지만, 더 엄격한 비교가 필요한 경우 epsilon 기반 비교를 고려할 수 있습니다.
🔎 개선 제안 (선택사항)
+ private static final double EPSILON = 1e-9; + public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) { // 입력값 검증 validateCoordinates(lat1, lon1); validateCoordinates(lat2, lon2); // 같은 위치인 경우 0 반환 - if (lat1 == lat2 && lon1 == lon2) { + if (Math.abs(lat1 - lat2) < EPSILON && Math.abs(lon1 - lon2) < EPSILON) { return 0.0; }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
src/main/java/ita/tinybite/domain/auth/entity/JwtTokenProvider.javasrc/main/java/ita/tinybite/domain/party/controller/PartyController.javasrc/main/java/ita/tinybite/domain/party/dto/request/PartyCreateRequest.javasrc/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.javasrc/main/java/ita/tinybite/domain/party/dto/response/HostInfo.javasrc/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.javasrc/main/java/ita/tinybite/domain/party/dto/response/PartyDetailResponse.javasrc/main/java/ita/tinybite/domain/party/dto/response/PartyListResponse.javasrc/main/java/ita/tinybite/domain/party/dto/response/ProductLink.javasrc/main/java/ita/tinybite/domain/party/entity/Neighborhood.javasrc/main/java/ita/tinybite/domain/party/entity/Party.javasrc/main/java/ita/tinybite/domain/party/entity/PartyParticipant.javasrc/main/java/ita/tinybite/domain/party/entity/PickupLocation.javasrc/main/java/ita/tinybite/domain/party/enums/PartyCategory.javasrc/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.javasrc/main/java/ita/tinybite/domain/party/repository/PartyRepository.javasrc/main/java/ita/tinybite/domain/party/service/PartyService.javasrc/main/java/ita/tinybite/domain/user/entity/User.javasrc/main/java/ita/tinybite/global/util/DistanceCalculator.java
🧰 Additional context used
🧬 Code graph analysis (10)
src/main/java/ita/tinybite/domain/party/dto/request/PartyCreateRequest.java (1)
src/main/java/ita/tinybite/domain/party/entity/PickupLocation.java (1)
Getter(9-24)
src/main/java/ita/tinybite/domain/party/entity/PickupLocation.java (1)
src/main/java/ita/tinybite/domain/party/dto/request/PartyCreateRequest.java (1)
Getter(12-51)
src/main/java/ita/tinybite/domain/party/dto/response/HostInfo.java (1)
src/main/java/ita/tinybite/domain/party/dto/response/PartyDetailResponse.java (1)
Getter(9-48)
src/main/java/ita/tinybite/domain/party/dto/response/PartyDetailResponse.java (2)
src/main/java/ita/tinybite/domain/party/dto/response/HostInfo.java (1)
Getter(5-13)src/main/java/ita/tinybite/domain/party/dto/response/ProductLink.java (1)
Getter(4-12)
src/main/java/ita/tinybite/domain/party/entity/Party.java (4)
src/main/java/ita/tinybite/domain/user/entity/User.java (1)
Entity(13-65)src/main/java/ita/tinybite/domain/party/entity/PartyParticipant.java (1)
Entity(9-54)src/main/java/ita/tinybite/domain/party/dto/request/PartyCreateRequest.java (1)
Getter(12-51)src/main/java/ita/tinybite/domain/party/entity/PickupLocation.java (1)
Getter(9-24)
src/main/java/ita/tinybite/domain/party/dto/response/ProductLink.java (2)
src/main/java/ita/tinybite/domain/party/dto/response/HostInfo.java (1)
Getter(5-13)src/main/java/ita/tinybite/domain/party/dto/response/PartyDetailResponse.java (1)
Getter(9-48)
src/main/java/ita/tinybite/domain/party/controller/PartyController.java (3)
src/main/java/ita/tinybite/domain/notification/service/PartyNotificationService.java (5)
Service(18-208)Transactional(29-44)Transactional(150-168)Transactional(170-187)Transactional(67-88)src/main/java/ita/tinybite/domain/notification/controller/FcmTokenController.java (2)
RestController(15-29)PostMapping(23-28)src/main/java/ita/tinybite/domain/notification/service/manager/PartyMessageManager.java (1)
createPartyCompleteRequest(74-80)
src/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.java (1)
src/main/java/ita/tinybite/domain/party/dto/request/PartyCreateRequest.java (1)
Getter(12-51)
src/main/java/ita/tinybite/domain/party/repository/PartyRepository.java (5)
src/main/java/ita/tinybite/domain/notification/repository/NotificationRepository.java (1)
NotificationRepository(7-8)src/main/java/ita/tinybite/domain/user/repository/UserRepository.java (1)
Repository(10-21)src/main/java/ita/tinybite/domain/auth/repository/TermRepository.java (1)
TermRepository(9-21)src/main/java/ita/tinybite/domain/notification/repository/FcmTokenRepository.java (1)
FcmTokenRepository(14-30)src/main/java/ita/tinybite/domain/auth/repository/RefreshTokenRepository.java (1)
RefreshTokenRepository(8-12)
src/main/java/ita/tinybite/domain/party/dto/response/PartyListResponse.java (1)
src/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.java (1)
Getter(7-23)
🔇 Additional comments (25)
src/main/java/ita/tinybite/domain/party/enums/PartyCategory.java (1)
1-17: 깔끔한 enum 구현입니다PartyCategory enum이 명확하고 간결하게 정의되어 있습니다. 각 카테고리에 대한 한글 설명이 포함되어 있어 가독성이 좋습니다.
src/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.java (1)
1-23: 잘 설계된 응답 DTO입니다PartyCardResponse가 UI 요구사항을 잘 반영하고 있습니다. 특히 화면 표시용
distance와 정렬용distanceKm을 분리한 점이 좋습니다.src/main/java/ita/tinybite/domain/party/dto/response/PartyListResponse.java (1)
1-19: 파티 목록 응답 구조가 잘 설계되었습니다활성 파티와 마감된 파티를 명확히 구분하고, 페이지네이션 지원을 위한
hasNext와totalCount를 포함한 점이 좋습니다.src/main/java/ita/tinybite/domain/party/dto/response/HostInfo.java (1)
1-13: 간결하고 명확한 DTO입니다호스트 정보를 담는 DTO가 필요한 필드만 포함하여 깔끔하게 구성되어 있습니다.
src/main/java/ita/tinybite/domain/party/dto/response/ProductLink.java (1)
4-12: LGTM!DTO 구조가 프로젝트 컨벤션(HostInfo.java 등)과 일관되며, 응답용 DTO로서 적절하게 구현되었습니다.
src/main/java/ita/tinybite/domain/party/entity/PartyParticipant.java (1)
42-53: LGTM!
approve()와reject()메서드가 상태 변경 로직을 명확하게 캡슐화하고 있습니다.src/main/java/ita/tinybite/domain/party/repository/PartyRepository.java (1)
11-16: LGTM!Spring Data JPA의 프로퍼티 표현식을 사용한 쿼리 메서드가 올바르게 정의되어 있습니다.
src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java (1)
9-15: LGTM!참여 여부 확인을 위한 존재성 검사 메서드가 적절하게 정의되어 있습니다.
existsByPartyAndUser와existsByPartyAndUserAndIsApprovedTrue의 구분이 명확합니다.src/main/java/ita/tinybite/domain/party/entity/PickupLocation.java (1)
9-24: LGTM!Embeddable 값 객체가 올바르게 정의되어 있습니다.
PartyCreateRequest에서@Valid어노테이션과 함께 사용되어 유효성 검증이 수행됩니다.src/main/java/ita/tinybite/domain/party/dto/response/PartyDetailResponse.java (1)
9-48: LGTM!파티 상세 정보를 위한 포괄적인 응답 DTO가 잘 구성되어 있습니다. 필드 그룹별 주석이 가독성을 높여줍니다.
src/main/java/ita/tinybite/domain/party/dto/request/PartyCreateRequest.java (1)
33-41: 좌표 필드 중복 가능성 확인 필요
latitude/longitude필드와pickupLocation내부의pickupLatitude/pickupLongitude가 모두 존재합니다. 두 세트의 좌표가 서로 다른 목적(예: 사용자 위치 vs 수령 위치)으로 사용되는지 확인이 필요합니다. 동일한 목적이라면 중복을 제거하는 것이 좋습니다.src/main/java/ita/tinybite/domain/party/entity/Party.java (2)
88-125: LGTM!상대 시간 계산 로직이 올바르게 구현되어 있습니다. 월 계산(
days / 30)은 근사치이지만 "X개월 전" 표시 용도로는 적절합니다.
82-86: LGTM!승인된 참여자 수 계산 로직이 명확합니다. 호스트를 포함하는 것이 비즈니스 요구사항과 일치하는지 확인해 주세요.
src/main/java/ita/tinybite/domain/party/controller/PartyController.java (3)
30-39: 참여 신청 엔드포인트가 올바르게 구현되었습니다.파티 참여 로직이 적절하게 구현되어 있으며, Authorization 헤더를 통한 인증과 서비스 계층으로의 위임이 올바릅니다.
63-73: 상세 조회 엔드포인트가 올바르게 구현되었습니다.인증 처리와 좌표 타입 선언이 적절하며, 서비스 계층으로의 위임이 올바릅니다.
78-87: 파티 생성 엔드포인트가 올바르게 구현되었습니다.요청 검증과 인증 처리가 적절하며, 서비스 계층으로의 위임이 올바릅니다.
src/main/java/ita/tinybite/global/util/DistanceCalculator.java (3)
43-51: 거리 포맷팅 로직이 올바르게 구현되었습니다.1km 미만은 미터 단위로, 1km 이상은 킬로미터 단위로 표시하는 로직이 적절합니다.
59-68: 좌표 유효성 검증 로직이 올바르게 구현되었습니다.위도와 경도의 유효 범위를 정확하게 검증하고 있으며, 적절한 오류 메시지를 제공합니다.
38-41: 거리 계산 유틸리티 메서드들이 올바르게 구현되었습니다.미터 단위 계산, 포맷팅된 거리 반환, 반경 내 위치 확인 기능이 모두 적절하게 구현되어 있습니다.
Also applies to: 53-57, 70-75
src/main/java/ita/tinybite/domain/party/service/PartyService.java (6)
37-70: 파티 생성 로직이 올바르게 구현되었습니다.사용자 조회, 카테고리별 링크 검증, 기본 이미지 처리, 엔티티 생성 및 저장이 모두 적절하게 구현되어 있습니다.
168-199: 파티 참여 로직이 올바르게 구현되었습니다.마감 상태, 인원 제한, 중복 참여 체크 등 필요한 모든 검증이 적절하게 수행되고 있습니다.
318-337: 파티 삭제 로직이 올바르게 구현되었습니다.호스트 권한 확인과 승인된 참여자가 있을 때 삭제를 방지하는 로직이 적절하게 구현되어 있습니다.
201-220: 유효성 검증 및 기본 이미지 처리 로직이 올바르게 구현되었습니다.카테고리별 상품 링크 제약 검증과 기본 이미지 할당 로직이 적절하며, 현대적인 switch 표현식을 사용하고 있습니다.
222-241: 파티 카드 응답 변환 로직이 올바르게 구현되었습니다.Line 224의
maxParticipants로 나누는 연산은 DTO 검증에서 최소값 2를 보장하므로 안전합니다. 거리 포맷팅과 참여 상태 표시가 적절합니다.
303-315: 이 리뷰 의견은 부정확합니다. 코드가 올바르게 작동합니다.
PickupLocation클래스의@AllArgsConstructor는 필드 선언 순서대로 생성자를 생성합니다:
place(String)pickupLatitude(Double)pickupLongitude(Double)호출되는 매개변수의 순서와 타입이 일치합니다:
request.getPickupLocation()→ Stringrequest.getLatitude()→ Doublerequest.getLongitude()→ Double따라서 타입 불일치나 필드 순서 불일치는 없으며, 현재 코드는 정상 작동합니다.
Likely an incorrect or invalid review comment.
| public Long getUserId(String token) { | ||
|
|
||
| token = token.trim(); | ||
| if (token.startsWith("Bearer ")) { | ||
| token = token.substring(7).trim(); | ||
| } | ||
|
|
||
| if (token.contains(" ")) { | ||
| log.error("⚠️ 정제 후에도 토큰에 공백이 있습니다!"); | ||
| log.error("공백 제거 후 토큰: [{}]", token.replaceAll(" ", "")); | ||
| token = token.replaceAll("\\s+", ""); // 모든 공백 제거 | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토큰 정제 로직의 로깅 레벨 및 오류 처리 개선 필요
현재 구현의 문제점:
- 공백이 포함된 토큰에 대해
log.error를 사용하는데, 이는 클라이언트 측 오류일 가능성이 높아 warn 레벨이 더 적합합니다. - 토큰에 공백이 있을 경우 자동으로 제거하고 진행하는데, 이는 잘못된 형식의 토큰이나 변조된 토큰을 숨길 수 있습니다.
- 토큰 형식이 잘못된 경우 예외를 발생시키는 것이 보안상 더 안전합니다.
🔎 제안하는 개선안
public Long getUserId(String token) {
token = token.trim();
if (token.startsWith("Bearer ")) {
token = token.substring(7).trim();
}
if (token.contains(" ")) {
- log.error("⚠️ 정제 후에도 토큰에 공백이 있습니다!");
- log.error("공백 제거 후 토큰: [{}]", token.replaceAll(" ", ""));
- token = token.replaceAll("\\s+", ""); // 모든 공백 제거
+ log.warn("유효하지 않은 토큰 형식: 토큰에 공백이 포함되어 있습니다");
+ throw new IllegalArgumentException("토큰 형식이 올바르지 않습니다");
}
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/auth/entity/JwtTokenProvider.java around
lines 77 to 89, the code currently logs token whitespace as an error and
silently strips all whitespace then continues; change this to log at warn level
and reject malformed tokens instead of auto-fixing: after trimming and removing
the "Bearer " prefix, if the token contains any whitespace, log a warning and
throw a suitable authentication/invalid-token exception (e.g., JwtException or a
custom InvalidTokenException) rather than calling replaceAll to remove spaces;
ensure the thrown exception yields a clear message about malformed token format
so upstream handlers return an appropriate 401/400 response.
| @RestController | ||
| @RequestMapping("/api/parties") | ||
| @RequiredArgsConstructor | ||
| public class PartyController { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
파티 수정 및 삭제 엔드포인트 누락
PR 목표와 PartyService에 구현된 updateParty, deleteParty 메서드가 있음에도 불구하고, 컨트롤러에 수정(PUT/PATCH) 및 삭제(DELETE) 엔드포인트가 누락되어 있습니다.
🔎 누락된 엔드포인트 구현 제안
+ /**
+ * 파티 수정
+ */
+ @PutMapping("/{partyId}")
+ public ResponseEntity<Void> updateParty(
+ @PathVariable Long partyId,
+ @RequestHeader("Authorization") String token,
+ @Valid @RequestBody PartyUpdateRequest request) {
+ Long userId = jwtTokenProvider.getUserId(token);
+ partyService.updateParty(partyId, userId, request);
+ return ResponseEntity.ok().build();
+ }
+
+ /**
+ * 파티 삭제
+ */
+ @DeleteMapping("/{partyId}")
+ public ResponseEntity<Void> deleteParty(
+ @PathVariable Long partyId,
+ @RequestHeader("Authorization") String token) {
+ Long userId = jwtTokenProvider.getUserId(token);
+ partyService.deleteParty(partyId, userId);
+ return ResponseEntity.ok().build();
+ }
}필요한 import 추가:
import ita.tinybite.domain.party.dto.request.PartyUpdateRequest;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;이 엔드포인트들을 추가하시겠습니까?
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/controller/PartyController.java
around lines 21-24, the controller is missing endpoints for updating and
deleting parties even though PartyService exposes updateParty and deleteParty;
add a PUT (or PATCH) mapping method that accepts PartyUpdateRequest as
@RequestBody and a @PathVariable Long partyId, calls
partyService.updateParty(partyId, request) and returns appropriate
ResponseEntity (e.g., 200 OK with updated DTO or no-content), and add a DELETE
mapping method with @PathVariable Long partyId that calls
partyService.deleteParty(partyId) and returns ResponseEntity.noContent(); also
add the required imports (PartyUpdateRequest, PutMapping, DeleteMapping,
RequestBody, PathVariable, ResponseEntity) and annotate methods with
@PutMapping("/{partyId}") and @DeleteMapping("/{partyId}").
| @GetMapping | ||
| public ResponseEntity<PartyListResponse> getPartyList( | ||
| @RequestHeader(value = "Authorization", required = false) String token, | ||
| @RequestParam(defaultValue = "ALL") PartyCategory category, | ||
| @RequestParam String latitude, | ||
| @RequestParam String longitude) { | ||
| Long userId = jwtTokenProvider.getUserId(token); | ||
|
|
||
| PartyListResponse response = partyService.getPartyList( | ||
| userId, category, latitude, longitude); | ||
|
|
||
| return ResponseEntity.ok(response); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null 토큰으로 인한 NPE 위험과 타입 불일치 문제 수정 필요
다음 문제들이 있습니다:
- Line 51:
token이 null일 수 있음에도 불구하고getUserId(token)을 호출하면JwtTokenProvider에서 NPE가 발생할 수 있습니다. - Lines 49-50:
latitude와longitude가String타입으로 선언되어 있지만, Line 67의getPartyDetail에서는Double타입으로 선언되어 있습니다. 일관성을 위해Double타입으로 통일해야 합니다.
🔎 수정 제안
@GetMapping
public ResponseEntity<PartyListResponse> getPartyList(
@RequestHeader(value = "Authorization", required = false) String token,
@RequestParam(defaultValue = "ALL") PartyCategory category,
- @RequestParam String latitude,
- @RequestParam String longitude) {
- Long userId = jwtTokenProvider.getUserId(token);
+ @RequestParam Double latitude,
+ @RequestParam Double longitude) {
+ Long userId = token != null ? jwtTokenProvider.getUserId(token) : null;
PartyListResponse response = partyService.getPartyList(
- userId, category, latitude, longitude);
+ userId, category, String.valueOf(latitude), String.valueOf(longitude));
return ResponseEntity.ok(response);
}참고: 서비스 레이어의 시그니처도 String 대신 Double을 받도록 변경하는 것이 더 바람직합니다.
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/controller/PartyController.java
around lines 45-57, avoid calling jwtTokenProvider.getUserId(token) when token
may be null by first checking if token is null/empty and handling it (e.g., set
Long userId = null or return 401 as appropriate for your auth flow) before
invoking getUserId; also change the controller method signatures and usages to
use Double for latitude and longitude (update the method parameters, parse/bind
as Double by the framework, and update partyService.getPartyList signature and
call site to accept Double latitude and Double longitude) so types are
consistent with getPartyDetail and the service layer.
| @Size(max = 30, message = "수령 장소는 최대 30자까지 입력 가능합니다") | ||
| private String pickupLocation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pickupLocation 타입 불일치 문제
PartyUpdateRequest의 pickupLocation이 String 타입인데, PartyCreateRequest에서는 PickupLocation 객체를 사용합니다. 이 타입 불일치는 서비스 레이어에서 일관되지 않은 처리를 야기하고, 위도/경도 정보를 함께 업데이트해야 하는 경우 문제가 될 수 있습니다.
PartyCreateRequest와 동일하게 PickupLocation 객체를 사용하는 것이 일관성 있는 설계입니다.
🔎 제안하는 수정안
+import ita.tinybite.domain.party.entity.PickupLocation;
+
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PartyUpdateRequest {
@Size(max = 30, message = "파티 제목은 최대 30자까지 입력 가능합니다")
private String title;
@Positive(message = "금액은 0보다 커야 합니다")
private Integer totalPrice;
@Min(value = 2, message = "최소 2명 이상이어야 합니다")
@Max(value = 10, message = "최대 10명까지 가능합니다")
private Integer maxParticipants;
- @Size(max = 30, message = "수령 장소는 최대 30자까지 입력 가능합니다")
- private String pickupLocation;
-
- private Double latitude;
- private Double longitude;
+ @Valid
+ private PickupLocation pickupLocation;
@Pattern(regexp = "^(https?://)?.*", message = "올바른 URL 형식으로 입력해주세요")
private String productLink;
// 항상 수정 가능한 필드
@Size(max = 60, message = "상세 설명은 최대 60자까지 입력 가능합니다")
private String description;
@Size(max = 5, message = "이미지는 최대 5장까지 업로드 가능합니다")
private List<String> images;
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.java
around lines 25-26, pickupLocation is declared as String which mismatches
PartyCreateRequest's PickupLocation type; change the field type to the same
PickupLocation class, add any necessary import and validation annotations (e.g.,
@Valid on the field and remove/adjust the @Size constraint that applied to
String), and update getters/setters/constructors to use PickupLocation so update
flows can include latitude/longitude consistently.
| @Pattern(regexp = "^(https?://)?.*", message = "올바른 URL 형식으로 입력해주세요") | ||
| private String productLink; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
URL 검증 방식 불일치
PartyUpdateRequest는 @Pattern을 사용하지만 PartyCreateRequest는 @URL을 사용합니다. 현재 패턴 ^(https?://)?.*는 사실상 모든 문자열을 허용하여 유효성 검증이 제대로 이루어지지 않습니다.
일관성과 적절한 검증을 위해 PartyCreateRequest와 동일하게 @URL 어노테이션을 사용하는 것이 좋습니다.
🔎 제안하는 수정안
-@Pattern(regexp = "^(https?://)?.*", message = "올바른 URL 형식으로 입력해주세요")
+@URL(message = "올바른 URL 형식으로 입력해주세요")
private String productLink;🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/dto/request/PartyUpdateRequest.java
around lines 31-32, the productLink field uses a permissive @Pattern that
effectively allows any string and differs from PartyCreateRequest; replace the
@Pattern with the same @URL annotation used in PartyCreateRequest (importing
org.hibernate.validator.constraints.URL if needed) and provide a consistent
validation message, ensuring the field now validates as a proper URL exactly
like the create request.
| if (images != null && !images.isEmpty()) { | ||
| this.image = Collections.singletonList(String.join(",", images)); | ||
| this.thumbnailImage = images.get(0); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미지 리스트 처리 로직 검토 필요
Collections.singletonList(String.join(",", images))는 모든 이미지 URL을 콤마로 합친 문자열 하나를 포함하는 리스트를 생성합니다. 이는 image 필드의 타입 정의와 일관성이 없습니다. 위에서 제안한 image 필드 수정과 함께 이 로직도 조정해야 합니다.
🔎 제안된 수정 사항 (image 필드가 String인 경우)
if (images != null && !images.isEmpty()) {
- this.image = Collections.singletonList(String.join(",", images));
+ this.image = String.join(",", images);
this.thumbnailImage = images.get(0);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/entity/Party.java around lines
147-150, the current code wraps a single comma-joined string in a list
(Collections.singletonList(String.join(",", images))) which mismatches the
intended image field type; adjust the assignment to match the field's actual
type: if image is declared as List<String> set this.image = new
ArrayList<>(images) (or Collections.unmodifiableList/new ArrayList copy) and set
this.thumbnailImage = images.get(0); if image is declared as String set
this.image = String.join(",", images) and set this.thumbnailImage =
images.get(0); ensure null/empty checks remain and avoid wrapping the joined
string inside a list.
| List<PartyCardResponse> cardResponses = parties.stream() | ||
| .map(party -> { | ||
| // DistanceCalculator 활용 | ||
| double distance = DistanceCalculator.calculateDistance( | ||
| Double.parseDouble(userLat), Double.parseDouble(userLon), | ||
| party.getLatitude(), party.getLongitude() | ||
| ); | ||
| return convertToCardResponse(party, distance, userId, party.getCreatedAt()); | ||
| }) | ||
| .collect(Collectors.toList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좌표 파싱 시 예외 처리 누락
Line 105에서 Double.parseDouble(userLat) 호출 시 NumberFormatException이 발생할 수 있으나 예외 처리가 없습니다. PartyController에서 좌표를 String이 아닌 Double 타입으로 받도록 수정하면 이 문제를 근본적으로 해결할 수 있습니다.
🔎 수정 제안
방법 1 (권장): 컨트롤러에서 Double 타입으로 변경
PartyController의 getPartyList 메서드에서 이미 지적한 대로 latitude, longitude를 Double 타입으로 받도록 수정하고, 서비스 메서드 시그니처도 변경:
- public PartyListResponse getPartyList(Long userId, PartyCategory category,
- String userLat, String userLon) {
+ public PartyListResponse getPartyList(Long userId, PartyCategory category,
+ Double userLat, Double userLon) {
User user = null;
if (userId != null) {
user = userRepository.findById(userId).orElse(null);
}
// 동네 기준으로 파티 조회
List<Party> parties;
if (user != null && user.getLocation() != null) {
if (category == PartyCategory.ALL) {
parties = partyRepository.findByPickupLocation_Place(user.getLocation());
} else {
parties = partyRepository.findByPickupLocation_PlaceAndCategory(
user.getLocation(), category);
}
} else {
// 비회원이거나 동네 미설정 시
- String location = locationService.getLocation(userLat,userLon);
+ String location = locationService.getLocation(String.valueOf(userLat), String.valueOf(userLon));
if (category == PartyCategory.ALL) {
parties = partyRepository.findByPickupLocation_Place(location);
} else {
parties = partyRepository.findByPickupLocation_PlaceAndCategory(
location, category);
}
}
List<PartyCardResponse> cardResponses = parties.stream()
.map(party -> {
// DistanceCalculator 활용
double distance = DistanceCalculator.calculateDistance(
- Double.parseDouble(userLat), Double.parseDouble(userLon),
+ userLat, userLon,
party.getLatitude(), party.getLongitude()
);
return convertToCardResponse(party, distance, userId, party.getCreatedAt());
})
.collect(Collectors.toList());방법 2: 예외 처리 추가 (차선책)
컨트롤러 변경이 어려운 경우:
+ try {
List<PartyCardResponse> cardResponses = parties.stream()
.map(party -> {
// DistanceCalculator 활용
double distance = DistanceCalculator.calculateDistance(
Double.parseDouble(userLat), Double.parseDouble(userLon),
party.getLatitude(), party.getLongitude()
);
return convertToCardResponse(party, distance, userId, party.getCreatedAt());
})
.collect(Collectors.toList());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("올바른 좌표 형식이 아닙니다: " + e.getMessage());
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| List<PartyCardResponse> cardResponses = parties.stream() | |
| .map(party -> { | |
| // DistanceCalculator 활용 | |
| double distance = DistanceCalculator.calculateDistance( | |
| Double.parseDouble(userLat), Double.parseDouble(userLon), | |
| party.getLatitude(), party.getLongitude() | |
| ); | |
| return convertToCardResponse(party, distance, userId, party.getCreatedAt()); | |
| }) | |
| .collect(Collectors.toList()); | |
| try { | |
| List<PartyCardResponse> cardResponses = parties.stream() | |
| .map(party -> { | |
| // DistanceCalculator 활용 | |
| double distance = DistanceCalculator.calculateDistance( | |
| Double.parseDouble(userLat), Double.parseDouble(userLon), | |
| party.getLatitude(), party.getLongitude() | |
| ); | |
| return convertToCardResponse(party, distance, userId, party.getCreatedAt()); | |
| }) | |
| .collect(Collectors.toList()); | |
| } catch (NumberFormatException e) { | |
| throw new IllegalArgumentException("올바른 좌표 형식이 아닙니다: " + e.getMessage()); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/service/PartyService.java around
lines 101-110, Double.parseDouble(userLat) / userLon can throw
NumberFormatException and currently lacks handling; update
PartyController.getPartyList to accept latitude and longitude as Double (change
controller method parameters and request binding) and then change the
PartyService method signature to accept Double userLat, Double userLon so you
can remove parseDouble calls and use the Double values directly; update all call
sites accordingly. If changing the controller is not possible, wrap the
parseDouble calls in a try-catch that catches NumberFormatException and either
throw a meaningful BadRequest/IllegalArgumentException or handle the case (log
and skip/return empty) so invalid coordinate strings are not propagated.
| return PartyListResponse.builder() | ||
| .activeParties(activeParties) | ||
| .closedParties(closedParties) | ||
| .totalCount(parties.size()) | ||
| .hasNext(false) | ||
| .build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
페이지네이션 미구현
PartyListResponse에 hasNext와 totalCount 필드가 있지만, Line 128에서 hasNext가 false로 하드코딩되어 있고 실제 페이지네이션 로직이 구현되어 있지 않습니다. 데이터가 많아질 경우 성능 문제가 발생할 수 있습니다.
향후 파티 데이터가 증가하면 페이지네이션을 구현해야 합니다. 페이지네이션 구현을 도와드릴까요?
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/service/PartyService.java around
lines 124 to 129, hasNext is hardcoded to false and totalCount is derived from
the returned list size, so pagination is not implemented; change the service to
accept pagination parameters (Pageable or page + size), query the repository
with a paginated call (or apply limit/offset) and run a separate count query (or
use Spring Data Page) to get the true totalCount, set hasNext = (offset + size)
< totalCount (or use Page.hasNext()), and return only the paged activeParties
and closedParties while populating totalCount from the count result instead of
parties.size().
| // 거리 계산 (사용자 위치 필요) | ||
| double distance = 0.0; | ||
| if (user != null) { | ||
| distance = DistanceCalculator.calculateDistance( | ||
| userLat, | ||
| userLon, | ||
| party.getLatitude(), | ||
| party.getLongitude() | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
거리 계산 시 좌표 null 체크 누락
Line 153에서 user != null 조건만 확인하고 있지만, userLat 또는 userLon이 null일 경우 Line 154-158의 DistanceCalculator.calculateDistance 호출 시 NPE가 발생할 수 있습니다.
🔎 수정 제안
// 거리 계산 (사용자 위치 필요)
double distance = 0.0;
- if (user != null) {
+ if (userLat != null && userLon != null) {
distance = DistanceCalculator.calculateDistance(
userLat,
userLon,
party.getLatitude(),
party.getLongitude()
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 거리 계산 (사용자 위치 필요) | |
| double distance = 0.0; | |
| if (user != null) { | |
| distance = DistanceCalculator.calculateDistance( | |
| userLat, | |
| userLon, | |
| party.getLatitude(), | |
| party.getLongitude() | |
| ); | |
| } | |
| // 거리 계산 (사용자 위치 필요) | |
| double distance = 0.0; | |
| if (userLat != null && userLon != null) { | |
| distance = DistanceCalculator.calculateDistance( | |
| userLat, | |
| userLon, | |
| party.getLatitude(), | |
| party.getLongitude() | |
| ); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/service/PartyService.java around
lines 151 to 160, the distance calculation only checks user != null but not
whether userLat or userLon are null, which can cause an NPE when calling
DistanceCalculator.calculateDistance; update the condition to ensure userLat and
userLon are non-null (e.g., check user != null && userLat != null && userLon !=
null) before invoking calculateDistance, and handle the else-case by leaving
distance at 0.0 or another appropriate default/optional value so no NPE occurs.
| // 이미지 파싱 | ||
| List<String> images = new ArrayList<>(); | ||
| if (party.getImage() != null && !party.getImage().isEmpty()) { | ||
| images = List.of(party.getImage().toString()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미지 파싱 로직 오류
Line 252에서 List.of(party.getImage().toString())는 party.getImage()를 문자열로 변환하여 단일 요소 리스트를 생성합니다. 하지만 Party 엔티티의 image 필드가 쉼표로 구분된 여러 이미지 URL 문자열인 경우, 이를 분리하지 않고 하나의 요소로 처리하는 것은 잘못된 것입니다.
🔎 수정 제안
// 이미지 파싱
List<String> images = new ArrayList<>();
if (party.getImage() != null && !party.getImage().isEmpty()) {
- images = List.of(party.getImage().toString());
+ String imageStr = party.getImage().toString();
+ if (!imageStr.isBlank()) {
+ images = List.of(imageStr.split(","));
+ }
}또는 Party 엔티티에 이미지 리스트를 반환하는 메서드가 있다면 그것을 활용:
// 이미지 파싱
- List<String> images = new ArrayList<>();
- if (party.getImage() != null && !party.getImage().isEmpty()) {
- images = List.of(party.getImage().toString());
- }
+ List<String> images = party.getImageList(); // Party 엔티티에 getImageList() 메서드 추가 필요📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 이미지 파싱 | |
| List<String> images = new ArrayList<>(); | |
| if (party.getImage() != null && !party.getImage().isEmpty()) { | |
| images = List.of(party.getImage().toString()); | |
| } | |
| // 이미지 파싱 | |
| List<String> images = new ArrayList<>(); | |
| if (party.getImage() != null && !party.getImage().isEmpty()) { | |
| String imageStr = party.getImage().toString(); | |
| if (!imageStr.isBlank()) { | |
| images = Arrays.asList(imageStr.split(",")); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/ita/tinybite/domain/party/service/PartyService.java around
lines 249 to 253, the current code converts party.getImage() to a single-element
list which fails when the image field contains multiple comma-separated URLs;
replace that with logic that checks for null/empty, splits the string on commas,
trims each part and filters out empties to build the List<String>, or if the
Party entity already exposes a List<String> getter for images, call that
instead; ensure the resulting list is immutable if needed or wrapped in a new
ArrayList to preserve mutability expectations.
📝 상세 내용
🔗 관련 이슈
💬리뷰 요구사항 (선택)
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.