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 @@ -6,7 +6,9 @@
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -17,6 +19,8 @@
import project.flipnote.group.model.GroupCreateRequest;
import project.flipnote.group.model.GroupCreateResponse;
import project.flipnote.group.model.GroupDetailResponse;
import project.flipnote.group.model.GroupPutRequest;
import project.flipnote.group.model.GroupPutResponse;
import project.flipnote.group.service.GroupService;

@RequiredArgsConstructor
Expand All @@ -34,6 +38,16 @@ public ResponseEntity<GroupCreateResponse> create(
return ResponseEntity.status(HttpStatus.CREATED).body(res);
}

//그룹 수정
@PutMapping("/{groupId}")
public ResponseEntity<GroupPutResponse> changeGroup(
@AuthenticationPrincipal AuthPrinciple authPrinciple,
@Valid @RequestBody GroupPutRequest req,
@PathVariable("groupId") Long groupId) {
GroupPutResponse res = groupService.changeGroup(authPrinciple, req, groupId);
return ResponseEntity.ok(res);
}

//그룹 상세 API
@GetMapping("/{groupId}")
public ResponseEntity<GroupDetailResponse> findGroupDetail(
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/project/flipnote/group/entity/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
Expand All @@ -24,6 +25,7 @@
import project.flipnote.common.entity.BaseEntity;
import project.flipnote.common.exception.BizException;
import project.flipnote.group.exception.GroupErrorCode;
import project.flipnote.group.model.GroupPutRequest;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down Expand Up @@ -94,8 +96,24 @@ public void validateJoinable() {
}
}

public void validateMaxMemberUpdatable(int changeNumber) {
if (memberCount > changeNumber) {
throw new BizException(GroupErrorCode.INVALID_MEMBER_COUNT);
}
}

public void increaseMemberCount() {
validateJoinable();
memberCount++;
}

public void changeGroup(GroupPutRequest req) {
this.name = req.name();
this.category = req.category();
this.description = req.description();
this.applicationRequired = req.applicationRequired();
this.publicVisible = req.publicVisible();
this.maxMember = req.maxMember();
this.imageUrl = req.image();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum GroupErrorCode implements ErrorCode {
USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_004", "그룹에 유저가 존재하지 않습니다."),
OTHER_USER_EXIST_IN_GROUP(HttpStatus.CONFLICT, "GROUP_005", "그룹내 오너 제외 유저가 존재합니다."),
GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_006", "그룹 정원이 가득 찼습니다."),
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_007", "이미 그룹 회원입니다.");
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_007", "이미 그룹 회원입니다."),
INVALID_MEMBER_COUNT(HttpStatus.BAD_REQUEST, "GROUP_008", "그룹 내에 인원수보다 많게 수정해야합니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/project/flipnote/group/model/GroupPutRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package project.flipnote.group.model;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import project.flipnote.group.entity.Category;

public record GroupPutRequest(
@NotBlank
@Size(max = 50)
String name,

@NotNull
Category category,

@NotBlank
@Size(max = 150)
String description,

@NotNull
Boolean applicationRequired,

@NotNull
Boolean publicVisible,

@NotNull
Integer maxMember,

String image
) {
}
40 changes: 40 additions & 0 deletions src/main/java/project/flipnote/group/model/GroupPutResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package project.flipnote.group.model;

import java.time.LocalDateTime;

import project.flipnote.group.entity.Category;
import project.flipnote.group.entity.Group;

public record GroupPutResponse(
String name,

Category category,

String description,

Boolean applicationRequired,

Boolean publicVisible,

Integer maxMember,

String imageUrl,

LocalDateTime createdAt,

LocalDateTime modifiedAt
) {
public static GroupPutResponse from(Group group) {
return new GroupPutResponse(
group.getName(),
group.getCategory(),
group.getDescription(),
group.getApplicationRequired(),
group.getPublicVisible(),
group.getMaxMember(),
group.getImageUrl(),
group.getCreatedAt(),
group.getModifiedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package project.flipnote.group.service;

import java.util.concurrent.TimeUnit;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import project.flipnote.common.exception.BizException;
import project.flipnote.common.exception.CommonErrorCode;
import project.flipnote.group.entity.Group;
import project.flipnote.group.exception.GroupErrorCode;
import project.flipnote.group.model.GroupPutRequest;
import project.flipnote.group.repository.GroupRepository;

@Service
@RequiredArgsConstructor
public class GroupPolicyService {
private final GroupRepository groupRepository;
private final RedissonClient redissonClient;

@Transactional
public Group changeGroup(Long groupId, GroupPutRequest req) {
String lockKey = "group_lock:" + groupId;
RLock lock = redissonClient.getLock(lockKey);

boolean isLocked = false;
try {
isLocked = lock.tryLock(2, 3, TimeUnit.SECONDS);
if (!isLocked) {
throw new BizException(CommonErrorCode.SERVICE_TEMPORARILY_UNAVAILABLE);
}

Group lockedGroup = groupRepository.findByIdForUpdate(groupId)
.orElseThrow(() -> new BizException(GroupErrorCode.GROUP_NOT_FOUND));

lockedGroup.validateMaxMemberUpdatable(req.maxMember());

lockedGroup.changeGroup(req);

return lockedGroup;

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BizException(CommonErrorCode.SERVICE_TEMPORARILY_UNAVAILABLE);
} finally {
if (isLocked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
39 changes: 38 additions & 1 deletion src/main/java/project/flipnote/group/service/GroupService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import project.flipnote.common.exception.BizException;
Expand All @@ -20,6 +21,8 @@
import project.flipnote.group.model.GroupCreateRequest;
import project.flipnote.group.model.GroupCreateResponse;
import project.flipnote.group.model.GroupDetailResponse;
import project.flipnote.group.model.GroupPutRequest;
import project.flipnote.group.model.GroupPutResponse;
import project.flipnote.group.repository.GroupMemberRepository;
import project.flipnote.group.repository.GroupPermissionRepository;
import project.flipnote.group.repository.GroupRepository;
Expand All @@ -41,6 +44,7 @@ public class GroupService {
private final GroupPermissionRepository groupPermissionRepository;
private final GroupRolePermissionRepository groupRolePermissionRepository;
private final UserProfileRepository userProfileRepository;
private final GroupPolicyService groupPolicyService;

/*
유저 정보 조회
Expand Down Expand Up @@ -87,7 +91,7 @@ public GroupCreateResponse create(AuthPrinciple authPrinciple, GroupCreateReques
//4. 그룹 회원 정보 생성
saveGroupOwner(group, user);

//5. 그룹 내의 모든 권한 조회
//5. 그룹 내의 모든 권한 생성
initializeGroupPermissions(group);

return GroupCreateResponse.from(group.getId());
Expand Down Expand Up @@ -164,6 +168,39 @@ private void validateMaxMember(int maxMember) {
}
}

//유저수 검증
private void validateUserCount(Group group, int maxMember) {
if (group.getMemberCount() > maxMember) {
throw new BizException(GroupErrorCode.INVALID_MEMBER_COUNT);
}
}

//그룹 수정
@Transactional
public GroupPutResponse changeGroup(AuthPrinciple authPrinciple, GroupPutRequest req, Long groupId) {

//1. 유저 조회
UserProfile user = validateUser(authPrinciple);

//2. 인원수 검증
validateMaxMember(req.maxMember());

//3. 그룹 가져오기
Group group = validateGroup(groupId);

//4. 그룹 내 유저 조회
GroupMember groupMember = validateGroupInUser(user, groupId);

//5. 유저 권환 조회
if (!groupMember.getRole().equals(GroupMemberRole.OWNER)) {
throw new BizException(GroupErrorCode.USER_NOT_PERMISSION);
}

//6. 그룹 수정
Group changeGroup = groupPolicyService.changeGroup(groupId, req);

return GroupPutResponse.from(changeGroup);
}
/*
그룹 내 오너를 제외한 인원이 존재하는 경우 체크
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package project.flipnote.group.service;


import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.BDDMockito.*;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.test.util.ReflectionTestUtils;

import project.flipnote.common.exception.BizException;
import project.flipnote.group.entity.Category;
import project.flipnote.group.entity.Group;
import project.flipnote.group.exception.GroupErrorCode;
import project.flipnote.group.model.GroupPutRequest;
import project.flipnote.group.repository.GroupRepository;

@ExtendWith(MockitoExtension.class)
class GroupPolicyServiceTest {

@InjectMocks
GroupPolicyService groupPolicyService;

@Mock
GroupRepository groupRepository;

@Mock
RedissonClient redissonClient;

@Mock
RLock rLock;

@Test
void 실패_유저수보다_작게_변경() throws Exception {
Long groupId = 1L;
Group group = Group.builder()
.name("그룹1")
.category(Category.IT)
.description("설명1")
.publicVisible(true)
.applicationRequired(true)
.maxMember(100)
.imageUrl("www.~~~")
.build();

ReflectionTestUtils.setField(group, "id", 1L);
ReflectionTestUtils.setField(group, "memberCount", 100);

GroupPutRequest req = new GroupPutRequest("그룹1", Category.ENGLISH, "설명1", true, true, 50, "www.~~");

given(redissonClient.getLock(anyString())).willReturn(rLock);
given(rLock.tryLock(anyLong(), anyLong(), any())).willReturn(true);
given(rLock.isHeldByCurrentThread()).willReturn(true);
given(groupRepository.findByIdForUpdate(groupId)).willReturn(Optional.of(group));


//when & then
BizException exception =
assertThrows(BizException.class, () -> groupPolicyService.changeGroup(groupId, req));

assertEquals(GroupErrorCode.INVALID_MEMBER_COUNT, exception.getErrorCode());
then(rLock).should().unlock();
}

@Test
void 그룹_수정_성공() throws Exception {
Long groupId = 1L;
Group group = Group.builder()
.name("그룹1")
.category(Category.IT)
.description("설명1")
.publicVisible(true)
.applicationRequired(true)
.maxMember(100)
.imageUrl("www.~~~")
.build();

ReflectionTestUtils.setField(group, "id", 1L);
ReflectionTestUtils.setField(group, "memberCount", 3);

GroupPutRequest req = new GroupPutRequest("그룹1", Category.ENGLISH, "설명1", true, true, 50, "www.~~");

given(redissonClient.getLock(anyString())).willReturn(rLock);
given(rLock.tryLock(anyLong(), anyLong(), any())).willReturn(true);
given(rLock.isHeldByCurrentThread()).willReturn(true);
given(groupRepository.findByIdForUpdate(groupId)).willReturn(Optional.of(group));


//when
Group changeGroup = groupPolicyService.changeGroup(groupId, req);

assertEquals(req.name(), changeGroup.getName());
assertEquals(req.category(), changeGroup.getCategory());

}
}
Loading
Loading