Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
bce7dbc
feat : 닉네임 중복 체크 (nickname unique), 인증코드 검사 예외처리 추가
yyytir777 Dec 8, 2025
3573613
fix : user_id IDENTITY strategy & dev redis host 이름변경 (localhost -> r…
yyytir777 Dec 8, 2025
08cfb63
test코드 생성 & swagger url 삭제 & 환경변수 중복 삭제
yyytir777 Dec 8, 2025
ac96fa0
fix : 엔드포인트 추가
yyytir777 Dec 9, 2025
43e75ed
Merge pull request #27 from tinybite-2025/feature/13-user
yyytir777 Dec 9, 2025
a70941b
Feature/26 notification (#29)
marshmallowing Dec 11, 2025
cbbf597
feat : google login 구현 완료
yyytir777 Dec 11, 2025
d03fd30
fix : main push 시에만 workflow trigger
yyytir777 Dec 11, 2025
00cc289
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-serv…
yyytir777 Dec 11, 2025
729ae0a
Feature/#28 google apple login
yyytir777 Dec 16, 2025
b5c29db
Merge branch 'main' into develop
yyytir777 Dec 16, 2025
2e5da55
Merge branch 'develop' of https://github.com/tinybite-2025/tinybite-s…
yyytir777 Dec 18, 2025
1fbd896
merge main into develop : main의 핫픽스 변경사항 develop에 반영
yyytir777 Dec 18, 2025
dd9771a
workflow 줄바꿈 에러 수정
yyytir777 Dec 18, 2025
38de611
hotifx : 에러 핸들링 수정 및 무중단 배포 삭제 (리소스 너무 많이 먹음)
yyytir777 Dec 19, 2025
91b8cba
main의 핫픽스 develop에 반영
yyytir777 Dec 22, 2025
fbc5e90
수정사항 반영 (API 인증 관련, db schema, 예외 처리 등..)
yyytir777 Dec 23, 2025
7b9fb7b
main브랜치 핫픽스 반영
yyytir777 Dec 24, 2025
bbb080f
Feature/35 term (#38)
yyytir777 Dec 24, 2025
4c352aa
fix : docker compose 명령어 수정
yyytir777 Dec 24, 2025
27dfcbb
Feature : 파티 기능 (#42)
milowon Dec 27, 2025
0de3a43
Merge branch 'main' into develop
milowon Dec 31, 2025
2f27dee
hotfix : url parser 경로 제거
milowon Dec 31, 2025
6eb0d8d
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-serv…
milowon Jan 1, 2026
3f69bc1
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-serv…
milowon Jan 2, 2026
45191a9
hotfix : 파티 거리 계산 로직 임시 주석 처리
milowon Jan 2, 2026
3a9a6e6
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-serv…
milowon Jan 2, 2026
b2ca1f4
hotfix : 파티 수정, 삭제 controller 추가
milowon Jan 2, 2026
38ab16c
hotfix : 선택 값들이 존재할때만 넣도록 수정
milowon Jan 2, 2026
037d2e4
hotfix : 위도, 경도 로직 삭제
milowon Jan 2, 2026
9d305f8
Feat : 마이페이지 참여중인 파티 조회 (#50)
milowon Jan 2, 2026
1cee657
hotfix : user service에 transactional 어노테이션 추가
milowon Jan 2, 2026
5213214
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-serv…
milowon Jan 2, 2026
ed3a399
hotfix : 참여중 파티 조회 반환 형식 통일
milowon Jan 2, 2026
374f720
hotfix : 파티 생성, 조회 시, 거리 계산 로직 반영
milowon Jan 2, 2026
4ddfa39
Hotfix: 유저 좌표 입력 requestParam 형식으로 변경
milowon Jan 2, 2026
42bc4d4
Merge branch 'main' into develop
milowon Jan 2, 2026
9ee078a
Merge branch 'main' into develop
milowon Jan 2, 2026
3be9d38
hotfix : 누락된 swagger 문서 수정사항 반영
milowon Jan 2, 2026
89bbef3
Merge branch 'main' into develop
milowon Jan 2, 2026
b281a29
feat : 회원 탈퇴 및 재가입 방지, 검증 (#65)
milowon Jan 2, 2026
35a62b7
fix : 파티 수정 버그 픽스 (#67)
milowon Jan 2, 2026
a4f3582
hotfix : 탈퇴 유저 마스킹 로직 변경
milowon Jan 3, 2026
9f05a6a
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-serv…
milowon Jan 3, 2026
d802e15
feat : 마이페이지에서 참여중,호스트인 파티 구분해서 조회 (#71)
milowon Jan 4, 2026
8715584
Feature/73 search party (#74)
yyytir777 Jan 4, 2026
78349d6
fix : 스웨거 description 추가
yyytir777 Jan 4, 2026
ba3d9d8
Merge branch 'main' into develop
yyytir777 Jan 4, 2026
c15e45b
Feature/73 search party (#76)
yyytir777 Jan 4, 2026
92bef29
Merge branch 'main' into develop
yyytir777 Jan 4, 2026
bd136de
Fix : 호스트만 있을때는 파티 수정할 수 있도록 변경 (#78)
milowon Jan 4, 2026
5d3e13d
fix : 파티 삭제시 호스트는 현재인원에서 제외하도록 수정 (#80)
milowon Jan 4, 2026
1e5b600
hotfix : jpa 네이밍 및 쿼리 수정
yyytir777 Jan 4, 2026
ba4ed25
hotfix : jpa 네이밍 및 쿼리 수정
yyytir777 Jan 4, 2026
c2d9465
Merge branch 'main' into develop
yyytir777 Jan 4, 2026
0b783c7
feat : 파티 카테고리, 최신순, 거리순 정렬 (#83)
milowon Jan 4, 2026
4474664
Merge branch 'main' of https://github.com/tinybite-2025/tinybite-serv…
milowon Jan 4, 2026
3353068
Feature/44 chat (#82)
yyytir777 Jan 6, 2026
6e2450c
hotfix : 파티 삭제 되지 않는 문제 수정 (#86)
milowon Jan 6, 2026
b9630cf
hotfix : 파티 수정사항이 db에 반영 되지 않는 문제 수정
milowon Jan 6, 2026
866bb84
feat : 유저 프로필 이미지 수정, 삭제 (#89)
milowon Jan 6, 2026
e1fc441
Fix : 유저 프로필 수정, 삭제 (#91)
milowon Jan 6, 2026
64449d4
Merge branch 'main' into develop
milowon Jan 6, 2026
85f9a6e
hotfix: 유저 정보 조회시 프로필 이미지 반환하도록 수정
milowon Jan 9, 2026
17485c9
Fix/party search (#94)
yyytir777 Jan 9, 2026
e77c560
feat: 파티 참여, 거절, 종료 알림 연결 (#97)
marshmallowing Jan 10, 2026
2826cae
fix : 마이페이지에서 참여중, 호스트 파티 조회시 최신순으로 조회되도록 수정 (#98)
milowon Jan 10, 2026
cf0f51d
Merge branch 'main' into develop
milowon Jan 10, 2026
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 @@ -26,62 +26,59 @@ public class PartyNotificationService {
private final NotificationLogService notificationLogService;
private final NotificationTransactionHelper notificationTransactionHelper;

//
@Transactional
public void sendApprovalNotification(Long targetUserId, Long partyId) {
String title = "🎉 파티 참여 승인";
String detail = "파티 참여가 승인되었습니다! 지금 확인하세요.";
notificationLogService.saveLog(targetUserId, NotificationType.PARTY_APPROVAL.name(), title, detail);
public void sendNewPartyRequestNotification(Long managerId, String requesterNickname, String partyTitle, Long partyId) {
String title = String.format("🍽️ [%s]님이 참여 요청했어요", requesterNickname);
String detail = String.format("‘[%s]’ 파티 승인 여부를 확인해 주세요", partyTitle);

List<String> tokens = fcmTokenService.getTokensAndLogIfEmpty(targetUserId);
notificationLogService.saveLog(managerId, NotificationType.PARTY_NEW_REQUEST.name(), title, detail);

List<String> tokens = fcmTokenService.getTokensAndLogIfEmpty(managerId);
if (tokens.isEmpty()) {
return;
}

NotificationMulticastRequest request =
partyMessageManager.createApprovalRequest(tokens, partyId, title, detail);
partyMessageManager.createNewPartyRequest(tokens, partyId, title, detail);

BatchResponse response = fcmNotificationSender.send(request);
notificationTransactionHelper.handleBatchResponse(response, tokens);
}

//
@Transactional
public void sendRejectionNotification(Long targetUserId, Long partyId) {
String title = "🚨 파티 참여 거절";
String detail = "죄송합니다. 파티 참여가 거절되었습니다.";
notificationLogService.saveLog(targetUserId, NotificationType.PARTY_REJECTION.name(), title, detail);
public void sendApprovalNotification(Long targetUserId, String partyTitle, Long partyId) {
// 참여 승인 (파티원에게 전송)
String title = String.format("🍽️ ‘[%s]’ 파티 승인 완료!", partyTitle);
String detail = "파티 채팅방에 입장했어요";

notificationLogService.saveLog(targetUserId, NotificationType.PARTY_APPROVAL.name(), title, detail);

List<String> tokens = fcmTokenService.getTokensAndLogIfEmpty(targetUserId);
if (tokens.isEmpty()) {
return;
}
if (tokens.isEmpty()) return;

NotificationMulticastRequest request =
partyMessageManager.createRejectionRequest(tokens, partyId, title, detail);
partyMessageManager.createApprovalRequest(tokens, partyId, title, detail);

BatchResponse response = fcmNotificationSender.send(request);
notificationTransactionHelper.handleBatchResponse(response, tokens);
}

/**
* 아래 메서드들 파티장,파티멤버의 알림 내용 다른지에 따라 추후 수정 필요
*/

//
@Transactional
public void sendAutoCloseNotification(List<Long> memberIds, Long partyId, Long managerId) {
String title = "🎉 파티 자동 마감";
String memberDetail = "참여 인원이 모두 차서 파티가 마감되었습니다.";
String managerDetail = "축하합니다! 목표 인원 달성으로 파티가 자동 마감되었습니다.";
public void sendRejectionNotification(Long targetUserId, String partyTitle, Long partyId) {
// 참여 거절 (파티원에게 전송)
String title = String.format("🍽️ ‘[%s]’ 😢 참여 거절", partyTitle);
String detail = "아쉽게도 이번 파티는 함께하지 못해요";

memberIds.forEach(userId -> {
String detail = userId.equals(managerId) ? managerDetail : memberDetail;
notificationLogService.saveLog(userId, NotificationType.PARTY_AUTO_CLOSE.name(), title, detail);
});
notificationLogService.saveLog(targetUserId, NotificationType.PARTY_REJECTION.name(), title, detail);

List<String> tokens = fcmTokenService.getMulticastTokensAndLogIfEmpty(memberIds);
if (tokens.isEmpty()) {
return;
}
List<String> tokens = fcmTokenService.getTokensAndLogIfEmpty(targetUserId);
if (tokens.isEmpty()) return;

NotificationMulticastRequest request =
partyMessageManager.createAutoCloseRequest(tokens, partyId, title, memberDetail);
partyMessageManager.createRejectionRequest(tokens, partyId, title, detail);

BatchResponse response = fcmNotificationSender.send(request);
notificationTransactionHelper.handleBatchResponse(response, tokens);
Expand Down Expand Up @@ -147,46 +144,46 @@ public void sendDeliveryReminderNotification(List<Long> memberIds, Long partyId,
}
}

//
@Transactional
public void sendPartyCompleteNotification(List<Long> memberIds, Long partyId) {
String title = "👋 파티 종료";
String detail = "파티장이 수령 완료 처리했습니다. 파티가 종료되었습니다.";
memberIds.forEach(userId ->
notificationLogService.saveLog(userId, NotificationType.PARTY_COMPLETE.name(), title, detail)
);
public void sendAutoCloseNotification(List<Long> memberIds, String partyTitle, Long partyId, Long managerId) {
String title = String.format("🎯 [%s] 인원 모집 완료 !", partyTitle);
String detail = "파티가 시작 되었어요";

memberIds.forEach(userId -> {
notificationLogService.saveLog(userId, NotificationType.PARTY_AUTO_CLOSE.name(), title, detail);
});

List<String> tokens = fcmTokenService.getMulticastTokensAndLogIfEmpty(memberIds);
if (tokens.isEmpty()) {
return;
}
if (tokens.isEmpty()) return;

NotificationMulticastRequest request =
partyMessageManager.createPartyCompleteRequest(tokens, partyId, title, detail);
partyMessageManager.createAutoCloseRequest(tokens, partyId, title, detail);

BatchResponse response = fcmNotificationSender.send(request);
notificationTransactionHelper.handleBatchResponse(response, tokens);
}

//
@Transactional
public void sendNewPartyRequestNotification(Long managerId, Long partyId) {
String title = "🔔 새 참여 요청";
String detail = "새로운 참여 요청이 도착했습니다. 지금 승인해 주세요.";
public void sendPartyCompleteNotification(List<Long> memberIds, String partyTitle, Long partyId) {
String title = String.format("✅ [%s] 파티 종료", partyTitle);
String detail = "참여해 주셔서 감사합니다";

notificationLogService.saveLog(managerId, NotificationType.PARTY_NEW_REQUEST.name(), title, detail);
memberIds.forEach(userId ->
notificationLogService.saveLog(userId, NotificationType.PARTY_COMPLETE.name(), title, detail)
);

List<String> tokens = fcmTokenService.getTokensAndLogIfEmpty(managerId);
if (tokens.isEmpty()) {
return;
}
List<String> tokens = fcmTokenService.getMulticastTokensAndLogIfEmpty(memberIds);
if (tokens.isEmpty()) return;

NotificationMulticastRequest request =
partyMessageManager.createNewPartyRequest(tokens, partyId, title, detail);
partyMessageManager.createPartyCompleteRequest(tokens, partyId, title, detail);

BatchResponse response = fcmNotificationSender.send(request);
notificationTransactionHelper.handleBatchResponse(response, tokens);
}


@Transactional
public void sendMemberLeaveNotification(Long managerId, Long partyId, String leaverName) {
String title = "⚠️ 파티원 이탈";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@

import ita.tinybite.domain.notification.service.ChatNotificationService;
import ita.tinybite.domain.notification.service.PartyNotificationService;
import ita.tinybite.domain.party.entity.Party;
import ita.tinybite.domain.party.repository.PartyRepository;
import ita.tinybite.domain.user.entity.User;
import ita.tinybite.domain.user.repository.UserRepository;
import ita.tinybite.global.exception.BusinessException;
import ita.tinybite.global.exception.errorcode.PartyErrorCode;
import ita.tinybite.global.exception.errorcode.UserErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -22,39 +29,65 @@ public class NotificationFacade {
private final PartyNotificationService partyNotificationService;
private final ChatNotificationService chatNotificationService;

private final PartyRepository partyRepository;
private final UserRepository userRepository;

@Transactional
public void notifyNewPartyRequest(Long managerId, Long requesterId, Long partyId) {
Party party = partyRepository.findById(partyId)
.orElseThrow(() -> new BusinessException(PartyErrorCode.PARTY_NOT_FOUND));

User requester = userRepository.findById(requesterId)
.orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_EXISTS));

partyNotificationService.sendNewPartyRequestNotification(
managerId,
requester.getNickname(),
party.getTitle(),
partyId
);
}

@Transactional
public void notifyApproval(Long targetUserId, Long partyId) {
partyNotificationService.sendApprovalNotification(targetUserId, partyId);
Party party = partyRepository.findById(partyId)
.orElseThrow(() -> new BusinessException(PartyErrorCode.PARTY_NOT_FOUND));
partyNotificationService.sendApprovalNotification(targetUserId, party.getTitle(), partyId);
}

@Transactional
public void notifyRejection(Long targetUserId, Long partyId) {
partyNotificationService.sendRejectionNotification(targetUserId, partyId);
Party party = partyRepository.findById(partyId)
.orElseThrow(() -> new BusinessException(PartyErrorCode.PARTY_NOT_FOUND));
partyNotificationService.sendRejectionNotification(targetUserId, party.getTitle(), partyId);
}

// 인원 모집 완료
@Transactional
public void notifyPartyAutoClose(List<Long> memberIds, Long partyId, Long managerId) {
partyNotificationService.sendAutoCloseNotification(memberIds, partyId, managerId);
}
Party party = partyRepository.findById(partyId)
.orElseThrow(() -> new BusinessException(PartyErrorCode.PARTY_NOT_FOUND));

@Transactional
public void notifyOrderComplete(List<Long> memberIds, Long partyId) {
partyNotificationService.sendOrderCompleteNotification(memberIds, partyId);
partyNotificationService.sendAutoCloseNotification(memberIds, party.getTitle(), partyId, managerId);
}

// 파티 종료
@Transactional
public void notifyDeliveryReminder(List<Long> memberIds, Long partyId, Long managerId) {
partyNotificationService.sendDeliveryReminderNotification(memberIds, partyId, managerId);
public void notifyPartyComplete(List<Long> memberIds, Long partyId) {
Party party = partyRepository.findById(partyId)
.orElseThrow(() -> new BusinessException(PartyErrorCode.PARTY_NOT_FOUND));

partyNotificationService.sendPartyCompleteNotification(memberIds, party.getTitle(), partyId);
}

@Transactional
public void notifyPartyComplete(List<Long> memberIds, Long partyId) {
partyNotificationService.sendPartyCompleteNotification(memberIds, partyId);
public void notifyOrderComplete(List<Long> memberIds, Long partyId) {
partyNotificationService.sendOrderCompleteNotification(memberIds, partyId);
}

@Transactional
public void notifyNewPartyRequest(Long managerId, Long partyId) {
partyNotificationService.sendNewPartyRequestNotification(managerId, partyId);
public void notifyDeliveryReminder(List<Long> memberIds, Long partyId, Long managerId) {
partyNotificationService.sendDeliveryReminderNotification(memberIds, partyId, managerId);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,6 @@ List<PartyParticipant> findActivePartiesByUserIdExcludingHost(
);

int countByPartyIdAndStatusAndUser_UserIdNot(Long partyId, ParticipantStatus participantStatus, Long userId);

List<PartyParticipant> findAllByPartyAndStatus(Party party, ParticipantStatus status);
}
28 changes: 28 additions & 0 deletions src/main/java/ita/tinybite/domain/party/service/PartyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ita.tinybite.domain.chat.entity.ChatRoom;
import ita.tinybite.domain.chat.enums.ChatRoomType;
import ita.tinybite.domain.chat.repository.ChatRoomRepository;
import ita.tinybite.domain.notification.service.facade.NotificationFacade;
import ita.tinybite.domain.party.dto.request.PartyCreateRequest;
import ita.tinybite.domain.party.dto.request.PartyListRequest;
import ita.tinybite.domain.party.dto.request.PartyUpdateRequest;
Expand Down Expand Up @@ -40,6 +41,7 @@ public class PartyService {
private final PartyParticipantRepository partyParticipantRepository;
private final ChatRoomRepository chatRoomRepository;
private final PartyParticipantRepository participantRepository;
private final NotificationFacade notificationFacade;

/**
* 파티 생성
Expand Down Expand Up @@ -218,6 +220,12 @@ public Long joinParty(Long partyId, Long userId) {

PartyParticipant saved = partyParticipantRepository.save(participant);

notificationFacade.notifyNewPartyRequest(
party.getHost().getUserId(), // 파티장 ID
userId, // 신청자 ID
partyId
);

return saved.getId();
}

Expand Down Expand Up @@ -428,6 +436,12 @@ public void approveParticipant(Long partyId, Long participantId, Long hostId) {
// 단체 채팅방에 참여자 추가
groupChatRoom.addMember(participant.getUser());

// 승인 알림
notificationFacade.notifyApproval(
participant.getUser().getUserId(),
partyId
);

// 목표 인원 달성 확인
checkAndCloseIfFull(party);
}
Expand Down Expand Up @@ -455,6 +469,10 @@ public void rejectParticipant(Long partyId, Long participantId, Long hostId) {
participant.getOneToOneChatRoom().deactivate();
}

notificationFacade.notifyRejection(
participant.getUser().getUserId(),
partyId
);
}

/**
Expand Down Expand Up @@ -546,6 +564,15 @@ public void settleParty(Long partyId, Long hostId) {

// 파티 마감
party.close();

// 알림 대상
List<Long> memberIds = partyParticipantRepository.findAllByPartyAndStatus(party, ParticipantStatus.APPROVED)
.stream()
.map(p -> p.getUser().getUserId())
.toList();

// 파티 종료 알림
notificationFacade.notifyPartyComplete(memberIds, party.getId());
}

// ========== Private Methods ==========
Expand Down Expand Up @@ -614,6 +641,7 @@ private void validateGroupChatRoomAccess(Party party, Long userId) {
}
}

// ??
private void checkAndCloseIfFull(Party party) {
if (party.getCurrentParticipants() >= party.getMaxParticipants()) {
party.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,6 @@ public ResponseEntity<List<PartyCardResponse>> getParticipatingParties(
return ResponseEntity.ok(response);
}

// @Operation(summary = "활성 파티 목록 조회", description = "사용자가 참여 중인 활성 파티 목록을 조회합니다.")
// @ApiResponses({
// @ApiResponse(responseCode = "200", description = "조회 성공",
// content = @Content(array = @ArraySchema(schema = @Schema(implementation = PartyCardResponse.class)))),
// @ApiResponse(responseCode = "401", description = "인증 실패")
// })
// @GetMapping("/parties/active")
// public ResponseEntity<List<PartyCardResponse>> getActiveParties(
// @AuthenticationPrincipal Long userId) {
// List<PartyCardResponse> response = userService.getActiveParties(userId);
// return ResponseEntity.ok(response);
// }

@Operation(summary = "닉네임 중복 확인", description = "닉네임 사용 가능 여부를 확인합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "사용 가능한 닉네임"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -181,6 +182,7 @@ public List<PartyCardResponse> getHostingParties(Long userId) {
);

return parties.stream()
.sorted(Comparator.comparing(Party::getCreatedAt).reversed())
.map(party -> {
int currentParticipants = participantRepository
.countByPartyIdAndStatus(party.getId(), ParticipantStatus.APPROVED);
Expand All @@ -198,6 +200,8 @@ public List<PartyCardResponse> getParticipatingParties(Long userId) {
);

return participants.stream()
.sorted(Comparator.comparing(pp -> pp.getParty().getCreatedAt(),
Comparator.reverseOrder()))
.map(pp -> {
Party party = pp.getParty();
int currentParticipants = participantRepository
Expand Down
Loading