Skip to content
Merged
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 @@ -32,7 +32,7 @@ public class ExerciseValidator {

public void validateCreateExercise(Long memberId, ExerciseCreateDTO.Request request, Party party) {
validatePartyIsActive(party);
validateMemberPermission(memberId, party);
validateSubManagerPermission(memberId, party);
validateExerciseTime(request);
}

Expand Down Expand Up @@ -62,19 +62,19 @@ public void validateCancelGuestInvitation(Exercise exercise, Guest guest, Member

public void validateCancelCommonParticipationByManager(Exercise exercise, Member manager) {
validateAlreadyStarted(exercise, ExerciseErrorCode.EXERCISE_ALREADY_STARTED_CANCEL);
validateMemberPermission(manager.getId(), exercise.getParty());
validateSubManagerPermission(manager.getId(), exercise.getParty());
}

public void validateCancelGuestParticipationByManager(Guest guest, Exercise exercise){
validateGuestBelongsToExercise(guest, exercise);
}

public void validateDeleteExercise(Exercise exercise, Long memberId) {
validateMemberPermission(memberId, exercise.getParty());
validateManagerPermission(memberId, exercise.getParty());
}

public void validateUpdateExercise(Exercise exercise, Member member, ExerciseUpdateDTO.Request request) {
validateMemberPermission(member.getId(), exercise.getParty());
validateManagerPermission(member.getId(), exercise.getParty());
validateAlreadyStarted(exercise, ExerciseErrorCode.EXERCISE_ALREADY_STARTED_UPDATE);
validateUpdateTime(request, exercise);
}
Expand All @@ -87,7 +87,7 @@ private void validatePartyIsActive(Party party) {
}
}

private void validateMemberPermission(Long memberId, Party party) {
private void validateManagerPermission(Long memberId, Party party) {
boolean isOwner = party.getOwnerId().equals(memberId);
boolean isManager = memberPartyRepository.existsByPartyIdAndMemberIdAndRole(
party.getId(), memberId, Role.party_MANAGER);
Expand All @@ -96,6 +96,17 @@ private void validateMemberPermission(Long memberId, Party party) {
throw new ExerciseException(ExerciseErrorCode.INSUFFICIENT_PERMISSION);
}

private void validateSubManagerPermission(Long memberId, Party party) {
boolean isOwner = party.getOwnerId().equals(memberId);
boolean isManager = memberPartyRepository.existsByPartyIdAndMemberIdAndRole(
party.getId(), memberId, Role.party_MANAGER);
boolean isSubManager = memberPartyRepository.existsByPartyIdAndMemberIdAndRole(
party.getId(), memberId, Role.party_SUBMANAGER);

if (!isOwner && !isManager && !isSubManager)
throw new ExerciseException(ExerciseErrorCode.INSUFFICIENT_PERMISSION);
}

private void validateExerciseTime(ExerciseCreateDTO.Request request) {
LocalDate date = request.toParsedDate();
LocalTime startTime = request.toParsedStartTime();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@ public boolean isLeader() {
if (this.role == Role.party_MANAGER) return true;
return false;
}

public void changeRole(Role newRole) {
this.role = newRole;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum MemberErrorCode implements BaseErrorCode {
ALREADY_WITHDRAW(HttpStatus.UNAUTHORIZED, "MEMBER301", "이미 탈퇴한 회원입니다."),

MANAGER_CANNOT_LEAVE(HttpStatus.BAD_REQUEST, "MEMBER401", "모임장은 탈퇴할 수 없습니다. 모임 삭제를 먼저 해주세요."),
SUBMANAGER_CANNOT_LEAVE(HttpStatus.BAD_REQUEST, "MEMBER402", "부모임장은 탈퇴할 수 없습니다. 모임 탈퇴를 먼저 해주세요."),

// 회원 주소 관련
ADDRESS_NOT_FOUND(HttpStatus.NOT_FOUND, "MEM_ADDR201", "해당 주소를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ List<Long> findAllPartyIdsByMemberAndPartyIds(@Param("memberId") Long memberId,
WHERE mp.party.id = :partyId
""")
List<MemberParty> findAllByPartyIdWithMember(@Param("partyId") Long partyId);

Optional<MemberParty> findByPartyIdAndRole(Long partyId, Role role);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import umc.cockple.demo.domain.member.repository.*;
import umc.cockple.demo.domain.member.enums.MemberStatus;
import umc.cockple.demo.domain.image.service.ImageService;
import umc.cockple.demo.global.enums.Role;

import java.util.List;
import umc.cockple.demo.global.oauth2.service.KakaoOauthService;
Expand Down Expand Up @@ -283,5 +284,16 @@ private void validateCanWithdraw(Member member) {
if (isLeader) {
throw new MemberException(MemberErrorCode.MANAGER_CANNOT_LEAVE);
}

// 활성화 된 모임의 부모임장인 경우 -> 탈퇴 불가
boolean isSubOwner = member.getMemberParties().stream()
.anyMatch(memberParty ->
memberParty.getRole() == Role.party_SUBMANAGER
&& memberParty.getStatus() == MemberPartyStatus.ACTIVE
);

if (isSubOwner) {
throw new MemberException(MemberErrorCode.SUBMANAGER_CANNOT_LEAVE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ public enum NotificationTarget {
PARTY_MODIFY(NotificationType.CHANGE),
PARTY_INVITE(NotificationType.INVITE),
PARTY_INVITE_APPROVED(NotificationType.SIMPLE),
PARTY_JOINREQUEST_APPROVED(NotificationType.CHANGE);
PARTY_JOINREQUEST_APPROVED(NotificationType.CHANGE),
PARTY_SUBOWNER_ASSIGNED(NotificationType.SIMPLE),
PARTY_SUBOWNER_RELEASED(NotificationType.SIMPLE);

private final NotificationType defaultType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public class NotificationCommandService {
private final NotificationMessageGenerator notificationMessageGenerator;
private final ObjectMapper objectMapper;


// 알림 타입 변경 (초대 수락, 거절에 사용)
public Response markAsReadNotification(Long memberId, Long notificationId, NotificationType type) {
Notification notification = findByNotificationId(notificationId);
Expand Down Expand Up @@ -96,7 +95,11 @@ public void createNotification(CreateNotificationRequestDTO dto) {
title = "새로운 모임";
} else if (dto.target() == NotificationTarget.PARTY_INVITE_APPROVED) {
content = notificationMessageGenerator.generateInviteApprovedMessage(dto.subjectName());
}else {
} else if (dto.target() == NotificationTarget.PARTY_SUBOWNER_ASSIGNED) {
content = notificationMessageGenerator.generateSubOwnerAssignedMessage(dto.subjectName());
} else if (dto.target() == NotificationTarget.PARTY_SUBOWNER_RELEASED) {
content = notificationMessageGenerator.generateSubOwnerReleasedMessage(dto.subjectName());
} else {
content = notificationMessageGenerator.generateJoinRequestApprovedMessage();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,12 @@ public String generateExerciseChangedMessage(String dateStr) {
public String generateExerciseAttendChangedMessage() {
return "운동 참석으로 변경되었어요!";
}

public String generateSubOwnerAssignedMessage(String nickname) {
return String.format("'%s'님이 부모임장으로 지정되었습니다.", nickname);
}

public String generateSubOwnerReleasedMessage(String nickname) {
return String.format("'%s'님의 부모임장 권한이 해제되었습니다.", nickname);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ public BaseResponse<Void> removeMember(
return BaseResponse.success(CommonSuccessCode.OK);
}

@PatchMapping("/parties/{partyId}/members/{memberId}/role")
@Operation(summary = "멤버 역할(부모임장) 설정", description = "모임장이 특정 멤버를 부모임장으로 지정하거나 해제합니다.")
@ApiResponse(responseCode = "200", description = "역할 변경 성공")
@ApiResponse(responseCode = "400", description = "유효하지 않은 역할 값")
@ApiResponse(responseCode = "403", description = "모임장 권한 없음 또는 모임장 역할 변경 시도")
@ApiResponse(responseCode = "404", description = "존재하지 않는 모임 또는 멤버")
public BaseResponse<Void> updateMemberRole(
@PathVariable Long partyId,
@PathVariable Long memberId,
@RequestBody @Valid PartyMemberRoleDTO.Request request) {
Long currentMemberId = SecurityUtil.getCurrentMemberId();

partyCommandService.updateMemberRole(partyId, memberId, currentMemberId, request);
return BaseResponse.success(CommonSuccessCode.OK);
}

@PostMapping("/parties/{partyId}/join-requests")
@Operation(summary = "모임 가입 신청",
description = "사용자가 특정 모임에 가입을 신청합니다")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package umc.cockple.demo.domain.party.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import umc.cockple.demo.global.enums.Role;

public class PartyMemberRoleDTO {

@Schema(name = "PartyMemberRoleRequest")
public record Request(
@NotNull(message = "역할 값은 필수입니다.") Role role) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum PartyErrorCode implements BaseErrorCode {
INVALID_ORDER_TYPE(HttpStatus.BAD_REQUEST, "PARTY106", "유효하지 않은 정렬 기준입니다. (최신순, 오래된 순, 운동 많은 순 중 하나여야 합니다.)"),
INVALID_KEYWORD(HttpStatus.BAD_REQUEST, "PARTY107", "유효하지 않은 키워드입니다."),
MALE_LEVEL_NOT_NEEDED(HttpStatus.BAD_REQUEST, "PARTY108", "여복 모임은 남자 급수를 설정할 수 없습니다."),
INVALID_ROLE_VALUE(HttpStatus.BAD_REQUEST, "PARTY411", "유효하지 않은 역할 값입니다. (party_SUBMANAGER 또는 party_MEMBER를 입력해주세요.)"),

PARTY_NOT_FOUND(HttpStatus.NOT_FOUND, "PARTY201", "존재하지 않는 모임입니다."),
JoinRequest_NOT_FOUND(HttpStatus.NOT_FOUND, "PARTY202", "존재하지 않는 가입신청입니다."),
Expand All @@ -34,6 +35,7 @@ public enum PartyErrorCode implements BaseErrorCode {
INSUFFICIENT_PERMISSION(HttpStatus.FORBIDDEN, "PARTY301", "해당 작업을 수행할 권한이 없습니다."),
INVALID_ACTION_FOR_OWNER(HttpStatus.FORBIDDEN, "PARTY302", "모임장은 수행할 수 없는 작업입니다."),
NOT_YOUR_INVITATION(HttpStatus.FORBIDDEN, "PARTY303", "해당 사용자의 초대가 아닙니다."),
INVALID_ACTION_FOR_SUBOWNER(HttpStatus.FORBIDDEN, "PARTY304", "부모임장은 수행할 수 없는 작업입니다."),

ALREADY_MEMBER(HttpStatus.CONFLICT, "PARTY401", "이미 가입된 모임입니다."),
JOIN_REQUEST_ALREADY_EXISTS(HttpStatus.CONFLICT, "PARTY402", "처리 대기중인 가입 신청이 존재합니다."),
Expand All @@ -44,8 +46,8 @@ public enum PartyErrorCode implements BaseErrorCode {
PARTY_IS_DELETED(HttpStatus.BAD_REQUEST, "PARTY407", "이미 삭제된 모임입니다."),
CANNOT_REMOVE_SELF(HttpStatus.BAD_REQUEST, "PARTY408", "자기 자신을 모임에서 삭제할 수 없습니다."),
INVITATION_ALREADY_EXISTS(HttpStatus.CONFLICT, "PARTY409", "처리 대기중인 초대가 존재합니다."),
INVITATION_ALREADY_ACTIONS(HttpStatus.CONFLICT, "PARTY410", "이미 처리된 모임 초대입니다.")
;
INVITATION_ALREADY_ACTIONS(HttpStatus.CONFLICT, "PARTY410", "이미 처리된 모임 초대입니다."),
CANNOT_ASSIGN_TO_OWNER(HttpStatus.FORBIDDEN, "PARTY411", "모임장의 역할은 변경할 수 없습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public interface PartyCommandService {
void actionInvitation(Long memberId, PartyInviteActionDTO.Request request, Long invitationId);
void updateParty(Long partyId, Long memberId, PartyUpdateDTO.Request request);
void addKeyword(Long partyId, Long memberID, PartyKeywordDTO.Request request);
void updateMemberRole(Long partyId, Long targetMemberId, Long currentMemberId, PartyMemberRoleDTO.Request request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ public void leaveParty(Long partyId, Long memberId) {
validatePartyIsActive(party);
//모임장인 경우, 탈퇴가 불가능하도록 검증
validateIsNotOwner(party, memberId);
//부모임장인 경우, 탈퇴가 불가능하도록 검증
validateIsNotSubOwner(party, memberId);
//해당 모임의 멤버인지 검증 및 조회
MemberParty memberParty = findMemberPartyOrThrow(party, member);

Expand Down Expand Up @@ -170,6 +172,53 @@ public void removeMember(Long partyId, Long memberIdToRemove, Long currentMember
log.info("모임 멤버 삭제 완료 - partyId: {}, removed: {}", partyId, memberIdToRemove);
}

@Override
public void updateMemberRole(Long partyId, Long targetMemberId, Long currentMemberId,
PartyMemberRoleDTO.Request request) {
log.info("멤버 역할 변경 시작 - partyId: {}, targetMemberId: {}, currentMemberId: {}", partyId, targetMemberId,
currentMemberId);

Role newRole = request.role();

// 모임, 사용자 조회
Party party = findPartyOrThrow(partyId);
Member targetMember = findMemberOrThrow(targetMemberId);
MemberParty targetMemberParty = findMemberPartyOrThrow(party, targetMember);

// 모임 활성화 검증
validatePartyIsActive(party);
// 모임장 권한 검증
validateOwnerPermission(party, currentMemberId);
// 대상이 모임장인 경우 변경 불가
if (targetMemberParty.getRole() == Role.party_MANAGER) {
throw new PartyException(PartyErrorCode.CANNOT_ASSIGN_TO_OWNER);
}
// 이미 같은 역할인 경우
if (targetMemberParty.getRole() == newRole) {
return;
}

// SUBOWNER 지정 시, 기존 부모임장 자동 해제
if (newRole == Role.party_SUBMANAGER) {
memberPartyRepository.findByPartyIdAndRole(partyId, Role.party_SUBMANAGER)
.ifPresent(mp -> {
mp.changeRole(Role.party_MEMBER);
createRoleNotification(partyId, NotificationTarget.PARTY_SUBOWNER_RELEASED,
mp.getMember().getNickname());
});
}

// 역할 변경
targetMemberParty.changeRole(newRole);

// 알림 발송 (전체 멤버 대상)
NotificationTarget notifTarget = (newRole == Role.party_SUBMANAGER)
? NotificationTarget.PARTY_SUBOWNER_ASSIGNED
: NotificationTarget.PARTY_SUBOWNER_RELEASED;
createRoleNotification(partyId, notifTarget, targetMember.getNickname());

log.info("멤버 역할 변경 완료 - partyId: {}, targetMemberId: {}, newRole: {}", partyId, targetMemberId, newRole);
}

@Override
public PartyJoinCreateDTO.Response createJoinRequest(Long partyId, Long memberId) {
Expand Down Expand Up @@ -336,6 +385,16 @@ private void validateIsNotOwner(Party party, Long memberId) {
}
}

// 부모임장은 권한이 없음을 검증
private void validateIsNotSubOwner(Party party, Long memberId) {
memberPartyRepository.findByPartyIdAndRole(party.getId(), Role.party_SUBMANAGER)
.ifPresent(mp -> {
if (mp.getMember().getId().equals(memberId)) {
throw new PartyException(PartyErrorCode.INVALID_ACTION_FOR_SUBOWNER);
}
});
}

//모임 생성 검증
private void validateCreateParty(Member owner, PartyCreateDTO.Command command) {
ParticipationType partyType = command.partyType();
Expand Down Expand Up @@ -530,7 +589,7 @@ private void createNotification(Member member, Long partyId, NotificationTarget
notificationCommandService.createNotification(dto);
}

//알림 생성 (INVITE 타입)
//알림 생성 (초대)
private void createInviteNotification(Member member, Long partyId, NotificationTarget notificationTarget, Long inviteId) {
CreateNotificationRequestDTO dto = CreateNotificationRequestDTO.builder()
.member(member)
Expand All @@ -541,6 +600,7 @@ private void createInviteNotification(Member member, Long partyId, NotificationT
notificationCommandService.createNotification(dto);
}

//알림 생성 (초대 수락)
private void createInviteApprovedNotification(Member member, Long partyId, NotificationTarget notificationTarget, Member invitee) {
CreateNotificationRequestDTO dto = CreateNotificationRequestDTO.builder()
.member(member)
Expand All @@ -550,4 +610,18 @@ private void createInviteApprovedNotification(Member member, Long partyId, Notif
.build();
notificationCommandService.createNotification(dto);
}

//전체 알림 생성 (역할)
private void createRoleNotification(Long partyId, NotificationTarget notificationTarget, String subjectNickname) {
List<MemberParty> allMembers = memberPartyRepository.findAllByPartyIdWithMember(partyId);
allMembers.forEach(mp -> {
CreateNotificationRequestDTO dto = CreateNotificationRequestDTO.builder()
.member(mp.getMember())
.partyId(partyId)
.target(notificationTarget)
.subjectName(subjectNickname)
.build();
notificationCommandService.createNotification(dto);
});
}
}