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 @@ -3,6 +3,7 @@
import java.time.LocalDateTime;

import backend.chat.entity.ChatRoomMember;
import backend.spot.entity.SpotMemberRole;
import backend.user.entity.UserEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
Expand All @@ -27,14 +28,22 @@ public class ChatMemberResponse {
@Schema(description = "프로필 이미지 URL")
private String avatarUrl;

@Schema(description = "스팟/피드 그룹 내 표시용 역할 (OWNER / SUPPORTER / PARTNER)", nullable = true, example = "SUPPORTER")
private SpotMemberRole role;

@Schema(description = "입장 일시")
private LocalDateTime joinedAt;

public static ChatMemberResponse from(ChatRoomMember member, UserEntity user) {
return from(member, user, null);
}

public static ChatMemberResponse from(ChatRoomMember member, UserEntity user, SpotMemberRole role) {
return ChatMemberResponse.builder()
.userId(member.getUserId())
.nickname(user == null ? null : user.getNickname())
.avatarUrl(user == null ? null : user.getAvatarUrl())
.role(role)
.joinedAt(member.getJoinedAt())
.build();
}
Expand Down
46 changes: 45 additions & 1 deletion capstone-api/src/main/java/backend/chat/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -37,9 +38,15 @@
import backend.chat.repository.ChatMessageRepository;
import backend.chat.repository.ChatRoomMemberRepository;
import backend.chat.repository.ChatRoomRepository;
import backend.feed.entity.FeedApplicationStatus;
import backend.feed.entity.FeedItem;
import backend.feed.repository.FeedApplicationRepository;
import backend.feed.repository.FeedItemRepository;
import backend.global.error.exception.BusinessException;
import backend.global.error.exception.ErrorCode;
import backend.spot.entity.Spot;
import backend.spot.entity.SpotMemberRole;
import backend.spot.repository.SpotParticipantRepository;
import backend.spot.repository.SpotRepository;
import backend.user.entity.UserEntity;
import backend.user.repository.UserRepository;
Expand All @@ -56,6 +63,9 @@ public class ChatService {
private final ChatBlockRepository chatBlockRepository;
private final SseEmitterService sseEmitterService;
private final SpotRepository spotRepository;
private final SpotParticipantRepository spotParticipantRepository;
private final FeedItemRepository feedItemRepository;
private final FeedApplicationRepository feedApplicationRepository;
private final UserRepository userRepository;

// ─────────────────────────────────────────────
Expand Down Expand Up @@ -227,16 +237,50 @@ public List<ChatRoomResponse> getRoomsBySpot(String spotId, String currentUserId

@Transactional(readOnly = true)
public List<ChatMemberResponse> getMembers(Long roomId, String currentUserId) {
ChatRoom room = findRoomOrThrow(roomId);
assertMembership(roomId, currentUserId);
List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(roomId);
List<String> userIds = members.stream().map(ChatRoomMember::getUserId).toList();
Map<String, UserEntity> usersById = userRepository.findAllById(userIds).stream()
.collect(Collectors.toMap(UserEntity::getId, Function.identity()));
Map<String, SpotMemberRole> rolesByUserId = resolveRoomMemberRoles(room);
return members.stream()
.map(m -> ChatMemberResponse.from(m, usersById.get(m.getUserId())))
.map(m -> ChatMemberResponse.from(m, usersById.get(m.getUserId()), rolesByUserId.get(m.getUserId())))
.toList();
}

private Map<String, SpotMemberRole> resolveRoomMemberRoles(ChatRoom room) {
if (room.getType() != ChatRoomType.GROUP) {
return Map.of();
}
Long spotId = parseSpotId(room.getSpotId());
if (spotId != null) {
Map<String, SpotMemberRole> roles = new HashMap<>();
spotParticipantRepository.findBySpotId(spotId)
.forEach(participant -> {
SpotMemberRole role = SpotMemberRole.fromParticipant(participant);
if (role != null) {
roles.put(participant.getUserId(), role);
}
});
return roles;
}
Long feedId = parseSpotId(room.getPostId());
if (feedId == null) {
return Map.of();
}
Map<String, SpotMemberRole> roles = new HashMap<>();
feedItemRepository.findById(feedId)
.map(FeedItem::getAuthorId)
.ifPresent(authorId -> roles.put(authorId, SpotMemberRole.OWNER));
feedApplicationRepository.findAllByFeedItemIdAndStatus(feedId, FeedApplicationStatus.ACCEPTED)
.stream()
.filter(application -> application.getAppliedRole() != null)
.forEach(application -> roles.put(
application.getUserId(), SpotMemberRole.fromApplicationRole(application.getAppliedRole())));
return roles;
}

// ─────────────────────────────────────────────
// 멤버십 (Membership) — 외부 도메인에서도 호출 가능
// ─────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ public class FeedApplicationResponse {
private FeedApplicationStatus status;
private FeedApplicationRole appliedRole;
private Integer deposit;
private boolean spotConverted;
private Long spotId;
private LocalDateTime createdAt;

public static FeedApplicationResponse from(FeedApplication application) {
return from(application, null);
}

public static FeedApplicationResponse from(FeedApplication application, Long spotId) {
return FeedApplicationResponse.builder()
.id(application.getId())
.feedId(application.getFeedItemId())
Expand All @@ -39,6 +45,8 @@ public static FeedApplicationResponse from(FeedApplication application) {
.status(application.getStatus())
.appliedRole(application.getAppliedRole())
.deposit(application.getDeposit())
.spotConverted(spotId != null)
.spotId(spotId)
.createdAt(application.getCreatedAt())
.build();
}
Expand Down
37 changes: 37 additions & 0 deletions capstone-api/src/main/java/backend/feed/dto/FeedItemResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ public class FeedItemResponse {
@Schema(description = "펀딩 진행률(OFFER 전용)", example = "80")
private Integer progressPercent;

@Schema(description = "펀딩 목표 금액(OFFER/REQUEST)", example = "25000")
private Integer fundingGoal;

@Schema(description = "현재 확정/적립 금액(OFFER/REQUEST)", example = "20000")
private Integer fundedAmount;

@Schema(description = "스팟 전환까지 남은 금액", example = "5000")
private Integer remainingAmount;

@Schema(description = "현재 1인 가격 기준 스팟 전환까지 추가로 필요한 인원 수", example = "1")
private Integer remainingParticipantCount;

@Schema(description = "신청자 수(REQUEST 전용)", example = "3")
private Long applicantCount;

Expand Down Expand Up @@ -155,6 +167,10 @@ public static FeedItemResponse from(FeedItem feedItem, Long applicantCount, Bool
.deadline(feedItem.getDeadline())
.partnerCount(feedItem.getType() == FeedType.OFFER ? feedItem.getConfirmedPartnerCount() : null)
.progressPercent(feedItem.getType() == FeedType.OFFER ? calculateProgressPercent(feedItem) : null)
.fundingGoal(feedItem.getFundingGoal())
.fundedAmount(feedItem.getFundedAmount())
.remainingAmount(calculateRemainingAmount(feedItem))
.remainingParticipantCount(calculateRemainingParticipantCount(feedItem))
.applicantCount(feedItem.getType() == FeedType.REQUEST ? applicantCount : null)
.isBookmarked(isBookmarked)
.myApplicationStatus(myApplication != null ? myApplication.getStatus() : null)
Expand Down Expand Up @@ -211,6 +227,27 @@ private static Integer calculateProgressPercent(FeedItem feedItem) {
return (int) ((long) fundedAmount * 100L / fundingGoal);
}

private static Integer calculateRemainingAmount(FeedItem feedItem) {
Integer fundingGoal = feedItem.getFundingGoal();
if (fundingGoal == null || fundingGoal <= 0) {
return null;
}
Integer fundedAmount = feedItem.getFundedAmount() == null ? 0 : feedItem.getFundedAmount();
return Math.max(fundingGoal - fundedAmount, 0);
}

private static Integer calculateRemainingParticipantCount(FeedItem feedItem) {
Integer remainingAmount = calculateRemainingAmount(feedItem);
Integer price = feedItem.getPrice();
if (remainingAmount == null || price == null || price <= 0) {
return null;
}
long remaining = remainingAmount;
long unitPrice = price;
long participantCount = (remaining + unitPrice - 1) / unitPrice;
return participantCount > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) participantCount;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

static List<String> parseJsonList(String json) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,11 @@ public FeedApplicationResponse acceptApplication(Long feedId, String application
chatService.ensureGroupRoomForPost(String.valueOf(feedId), feedItem.getTitle(), Set.of(application.getUserId()));
feedItem.accumulateFunding(feedItem.getPrice());

Long convertedSpotId = null;
if (feedItem.isFundingGoalMet()) {
Spot spot = spotRepository.save(Spot.fromFeedItem(feedItem));
feedItem.softDelete(); // 피드는 소프트 딜리트 (스팟으로 전환됨)
convertedSpotId = spot.getId();
feedItem.convertToSpot(convertedSpotId); // 피드는 소프트 딜리트 (스팟으로 전환됨)
Set<String> participantIds = registerSpotParticipants(spot, feedItem);
chatService.linkGroupRoomToSpot(String.valueOf(feedId), String.valueOf(spot.getId()), spot.getTitle(), participantIds);
// Spot 전환은 시스템 자동 처리 — 작성자 포함 모든 참여자에게 알림
Expand All @@ -333,7 +335,7 @@ public FeedApplicationResponse acceptApplication(Long feedId, String application
"'" + feedItem.getTitle() + "' 신청이 수락됐어요");
}

return FeedApplicationResponse.from(application);
return FeedApplicationResponse.from(application, convertedSpotId);
}

@Transactional
Expand Down Expand Up @@ -392,6 +394,7 @@ private Set<String> registerSpotParticipants(Spot spot, FeedItem feedItem) {
.spotId(spot.getId())
.userId(uid)
.role(ParticipantRole.PARTICIPANT)
.applicationRole(app.getAppliedRole())
.state(ParticipantState.ACTIVE)
.build());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,17 @@ public ResponseEntity<ApiResponse<SpotSettlementResponse>> requestSettlement(
));
}

@Operation(summary = "스팟 정산 조회", description = "최근 정산 요청 상태를 조회합니다. 정산 요청이 없으면 data=null 입니다.")
@GetMapping("/{spotId}/settlement")
public ResponseEntity<ApiResponse<SpotSettlementResponse>> getSettlement(
@PathVariable Long spotId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
return ResponseEntity.ok(ApiResponse.success(
spotService.getSettlement(spotId, requireAuth(userDetails))
));
}

@Operation(summary = "스팟 정산 승인", description = "승인 대기 중인 정산을 참여자가 승인 처리합니다.")
@PostMapping("/{spotId}/settlement/approve")
public ResponseEntity<ApiResponse<SpotSettlementResponse>> approveSettlement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ public class SpotDetailResponse {
@Schema(description = "스팟 활동 타임라인 (오래된 순)")
private List<TimelineEventResponse> timeline;

@Schema(description = "최근 정산 요청 상태", nullable = true)
private SpotSettlementResponse settlement;

public static SpotDetailResponse of(
Spot spot, int participantCount, boolean isOwner, List<TimelineEventResponse> timeline
Spot spot, int participantCount, boolean isOwner, List<TimelineEventResponse> timeline,
SpotSettlementResponse settlement
) {
return SpotDetailResponse.builder()
.id(spot.getId())
Expand All @@ -84,6 +88,7 @@ public static SpotDetailResponse of(
.updatedAt(spot.getUpdatedAt())
.isOwner(isOwner)
.timeline(timeline)
.settlement(settlement)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import backend.spot.entity.ParticipantRole;
import backend.spot.entity.ParticipantState;
import backend.spot.entity.SpotMemberRole;
import backend.spot.entity.SpotParticipant;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
Expand Down Expand Up @@ -31,6 +32,9 @@ public class SpotParticipantResponse {
@Schema(description = "역할 (AUTHOR: 작성자, PARTICIPANT: 참여자)", example = "PARTICIPANT")
private ParticipantRole role;

@Schema(description = "표시용 구성원 역할 (OWNER / SUPPORTER / PARTNER)", nullable = true, example = "PARTNER")
private SpotMemberRole memberRole;

@Schema(description = "참여 상태 (ACTIVE / LEFT / EXPELLED)", example = "ACTIVE")
private ParticipantState state;

Expand All @@ -43,6 +47,7 @@ public static SpotParticipantResponse from(SpotParticipant participant) {
.userId(participant.getUserId())
.nickname(null)
.role(participant.getRole())
.memberRole(SpotMemberRole.fromParticipant(participant))
.state(participant.getState())
.joinedAt(participant.getJoinedAt())
.build();
Expand All @@ -54,6 +59,7 @@ public static SpotParticipantResponse of(SpotParticipant participant, String nic
.userId(participant.getUserId())
.nickname(nickname)
.role(participant.getRole())
.memberRole(SpotMemberRole.fromParticipant(participant))
.state(participant.getState())
.joinedAt(participant.getJoinedAt())
.build();
Expand Down
13 changes: 12 additions & 1 deletion capstone-api/src/main/java/backend/spot/service/SpotService.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,10 @@ private static <E extends Enum<E>> E parseEnum(Class<E> enumType, String value)
public SpotDetailResponse getSpot(Long spotId, String currentUserId) {
Spot spot = findSpotOrThrow(spotId);
long participantCount = spotParticipantRepository.countBySpotIdAndState(spotId, ParticipantState.ACTIVE);
boolean participant = isOwner(spotId, currentUserId);
SpotSettlementResponse settlement = participant ? getSettlement(spotId, currentUserId) : null;
return SpotDetailResponse.of(
spot, Math.toIntExact(participantCount), isOwner(spotId, currentUserId), loadTimeline(spotId));
spot, Math.toIntExact(participantCount), participant, loadTimeline(spotId), settlement);
}

private List<TimelineEventResponse> loadTimeline(Long spotId) {
Expand Down Expand Up @@ -799,6 +801,15 @@ public SpotSettlementResponse approveSettlement(Long spotId, String currentUserI
return buildSettlementResponse(settlement);
}

@Transactional(readOnly = true)
public SpotSettlementResponse getSettlement(Long spotId, String currentUserId) {
validateSpotExists(spotId);
validateParticipant(spotId, currentUserId, ErrorCode.NOT_SPOT_PARTICIPANT);
return spotSettlementRepository.findFirstBySpotIdOrderByCreatedAtDescIdDesc(spotId)
.map(this::buildSettlementResponse)
.orElse(null);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private SpotSettlementResponse buildSettlementResponse(SpotSettlement settlement) {
List<SettlementLineItemDto> lineItems = spotSettlementLineItemRepository
.findBySettlementId(settlement.getId()).stream()
Expand Down
Loading