Skip to content

Commit e71eb2d

Browse files
committed
⚡ MarkSphere v1.0.5
그룹 소유권 양도 기능 구현 그룹 탈퇴, 회원 탈퇴 시 소유 그룹 검사 QR 코드 생성 시 권한 검사
1 parent dd4a1c8 commit e71eb2d

File tree

15 files changed

+153
-7
lines changed

15 files changed

+153
-7
lines changed

src/main/java/com/sonkim/bookmarking/auth/service/AuthService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public User createAccount(RegisterRequestDto dto) {
6767

6868
// 기본 개인 그룹 생성
6969
Team personalTeam = Team.builder()
70-
.name(dto.getNickname() + "님의 개인 공간")
70+
.name("개인 공간")
7171
.owner(newUser)
7272
.build();
7373
teamService.saveTeam(personalTeam);

src/main/java/com/sonkim/bookmarking/common/exception/GlobalExceptionHandler.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ public ResponseEntity<?> handleUnprocessableEntityException(UnprocessableEntityE
8484
return buildResponse(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage());
8585
}
8686

87+
// 회원 탈퇴 시 소유주 변경이 필요한 그룹이 있을 경우 예외 처리
88+
@ExceptionHandler(OwnershipTransferRequiredException.class)
89+
public ResponseEntity<Map<String, Object>> handleOwnershipTransferRequiredException(OwnershipTransferRequiredException e) {
90+
log.warn("회원 탈퇴 실패: 소유권 이전 필요. {}", e.getMessage());
91+
92+
Map<String, Object> response = new HashMap<>();
93+
response.put("message", e.getMessage());
94+
response.put("requiredActionGroups", e.getGroups());
95+
96+
return new ResponseEntity<>(response, HttpStatus.CONFLICT);
97+
}
98+
8799
// 범용 예외 처리
88100
@ExceptionHandler(Exception.class)
89101
public ResponseEntity<?> handleException(Exception e) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.sonkim.bookmarking.common.exception;
2+
3+
import com.sonkim.bookmarking.domain.mypage.dto.OwnershipRequiredGroupDto;
4+
import lombok.Getter;
5+
6+
import java.util.List;
7+
8+
public class OwnershipTransferRequiredException extends RuntimeException {
9+
@Getter
10+
private final List<OwnershipRequiredGroupDto> groups;
11+
12+
public OwnershipTransferRequiredException(List<OwnershipRequiredGroupDto> groups) {
13+
super("소유권 이전이 필요한 그룹이 존재하여 탈퇴할 수 없습니다.");
14+
this.groups = groups;
15+
}
16+
}

src/main/java/com/sonkim/bookmarking/domain/category/service/CategoryService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public List<CategoryDto.CategoryResponseDto> updateCategory(Long userId, Long ca
9595
teamService.validateGroupIsActive(teamId);
9696

9797
// 요청자가 권한이 있는지 검사
98-
teamMemberService.validateAdmin(userId, teamId);
98+
teamMemberService.validateEditor(userId, teamId);
9999

100100
// 동일한 이름의 카테고리가 있는지 확인
101101
if(categoryRepository.existsByNameAndTeam_Id(request.getName(), teamId)) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.sonkim.bookmarking.domain.mypage.dto;
2+
3+
import lombok.Builder;
4+
import lombok.Data;
5+
6+
@Data
7+
@Builder
8+
public class OwnershipRequiredGroupDto {
9+
private Long groupId;
10+
private String groupName;
11+
}

src/main/java/com/sonkim/bookmarking/domain/mypage/service/MyPageService.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package com.sonkim.bookmarking.domain.mypage.service;
22

33
import com.sonkim.bookmarking.common.dto.PageResponseDto;
4+
import com.sonkim.bookmarking.common.exception.OwnershipTransferRequiredException;
45
import com.sonkim.bookmarking.common.s3.service.S3Service;
56
import com.sonkim.bookmarking.domain.bookmark.dto.BookmarkResponseDto;
67
import com.sonkim.bookmarking.domain.bookmark.entity.Bookmark;
78
import com.sonkim.bookmarking.domain.bookmark.repository.BookmarkRepository;
89
import com.sonkim.bookmarking.domain.bookmark.service.BookmarkService;
910
import com.sonkim.bookmarking.domain.mypage.dto.MyProfileDto;
11+
import com.sonkim.bookmarking.domain.mypage.dto.OwnershipRequiredGroupDto;
1012
import com.sonkim.bookmarking.domain.mypage.dto.PasswordDto;
1113
import com.sonkim.bookmarking.auth.token.service.TokenService;
1214
import com.sonkim.bookmarking.domain.profile.service.ProfileService;
15+
import com.sonkim.bookmarking.domain.team.entity.Team;
16+
import com.sonkim.bookmarking.domain.team.repository.TeamMemberRepository;
17+
import com.sonkim.bookmarking.domain.team.repository.TeamRepository;
1318
import com.sonkim.bookmarking.domain.user.entity.User;
1419
import com.sonkim.bookmarking.domain.user.repository.UserRepository;
1520
import com.sonkim.bookmarking.domain.user.service.UserService;
@@ -22,6 +27,8 @@
2227
import org.springframework.transaction.annotation.Transactional;
2328
import org.springframework.data.domain.Pageable;
2429

30+
import java.util.List;
31+
2532
@Slf4j
2633
@Service
2734
@RequiredArgsConstructor
@@ -35,6 +42,8 @@ public class MyPageService {
3542
private final UserService userService;
3643
private final S3Service s3Service;
3744
private final ProfileService profileService;
45+
private final TeamRepository teamRepository;
46+
private final TeamMemberRepository teamMemberRepository;
3847

3948
@Transactional(readOnly = true)
4049
public MyProfileDto.MyProfileResponseDto getMyProfile(Long userId) {
@@ -128,6 +137,24 @@ public PageResponseDto<BookmarkResponseDto> getMyLikedBookmarks(Long userId, Pag
128137
// 탈퇴 처리
129138
@Transactional
130139
public void deleteAccount(Long userId) {
140+
// 탈퇴하려는 사용자가 속한 그룹 목록 조회
141+
List<Team> ownedGroups = teamRepository.findAllByOwner_Id(userId);
142+
143+
// 소유한 그룹 중, 개인 그룹이 아닌 그룹만 필터링
144+
List<OwnershipRequiredGroupDto> groupsRequiringAction = ownedGroups.stream()
145+
.filter(team -> teamMemberRepository.countByTeam_Id(team.getId()) > 1)
146+
.map(team -> OwnershipRequiredGroupDto.builder()
147+
.groupId(team.getId())
148+
.groupName(team.getName())
149+
.build()
150+
)
151+
.toList();
152+
153+
// 조치가 필요한 그룹이 하나라도 있으면 예외 발생
154+
if (!groupsRequiringAction.isEmpty()) {
155+
throw new OwnershipTransferRequiredException(groupsRequiringAction);
156+
}
157+
131158
log.info("userId: {} 탈퇴 요청", userId);
132159

133160
// 사용자 정보 불러와서 탈퇴 처리

src/main/java/com/sonkim/bookmarking/domain/team/controller/TeamController.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.sonkim.bookmarking.domain.team.service.QrCodeService;
88
import com.sonkim.bookmarking.domain.team.service.TeamService;
99
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.Parameter;
1011
import io.swagger.v3.oas.annotations.media.Content;
1112
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1213
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -108,8 +109,9 @@ public ResponseEntity<Void> joinTeamMember(@AuthenticationPrincipal UserDetailsI
108109
return ResponseEntity.status(HttpStatus.CREATED).build();
109110
}
110111

111-
@Operation(summary = "그룹 초대 QR 코드 이미지 생성", description = "그룹 초대를 위한 QR 코드를 이미지(PNG) 형식으로 생성하여 반환합니다.")
112+
@Operation(summary = "그룹 초대 QR 코드 이미지 생성", description = "그룹 초대를 위한 QR 코드를 이미지(PNG) 형식으로 생성하여 반환합니다. ADMIN 이상의 권한을 가진 유저만 가능")
112113
@ApiResponse(responseCode = "200", description = "QR 코드 생성 성공", content = @Content(mediaType = "image/png"))
114+
@ApiResponse(responseCode = "403", description = "권한 부족")
113115
@GetMapping("/{groupId}/invite-qr")
114116
public ResponseEntity<byte[]> generateInviteCode(@AuthenticationPrincipal UserDetailsImpl userDetails,
115117
@PathVariable("groupId") Long groupId) {
@@ -118,10 +120,10 @@ public ResponseEntity<byte[]> generateInviteCode(@AuthenticationPrincipal UserDe
118120
String inviteCode = teamService.getInviteCodeByTeamId(groupId);
119121

120122
// QR 코드에 담을 URL 생성
121-
String joinUrl = "http://localhost:8080/api/groups/join?code=" + inviteCode; // 차후 도메인으로 수정
123+
String joinUrl = "https://marksphere.link/api/groups/join?code=" + inviteCode; // 차후 도메인으로 수정
122124

123125
// QR 코드 이미지 생성
124-
byte[] qrCodeImage = qrCodeService.generateQrCodeImage(userDetails.getId(), joinUrl);
126+
byte[] qrCodeImage = qrCodeService.generateQrCodeImage(userDetails.getId(), groupId, joinUrl);
125127

126128
return ResponseEntity.ok()
127129
.contentType(MediaType.IMAGE_PNG)
@@ -174,4 +176,20 @@ public ResponseEntity<TeamDto.CodeResponseDto> getInviteCode(
174176

175177
return ResponseEntity.status(HttpStatus.CREATED).body(response);
176178
}
179+
180+
@Operation(summary = "그룹 소유권 이전", description = "그룹의 소유권을 다른 멤버에게 이전합니다. 현재 소유주만 실행할 수 있습니다.")
181+
@ApiResponses({
182+
@ApiResponse(responseCode = "200", description = "소유권 이전 성공"),
183+
@ApiResponse(responseCode = "403", description = "권한 없음 (소유주가 아님)"),
184+
@ApiResponse(responseCode = "400", description = "잘못된 요청 (예: 멤버가 아닌 사용자에게 이전 시도)")
185+
})
186+
@PostMapping("/{groupId}/transfer-ownership")
187+
public ResponseEntity<Void> transferOwnership(
188+
@AuthenticationPrincipal UserDetailsImpl userDetails,
189+
@Parameter(description = "소유권을 이전할 그룹 ID") @PathVariable Long groupId,
190+
@RequestBody TeamDto.OwnerTransferDto transferDto)
191+
{
192+
teamService.transferOwnership(userDetails.getId(), transferDto.getNewOwnerId(), groupId);
193+
return ResponseEntity.ok().build();
194+
}
177195
}

src/main/java/com/sonkim/bookmarking/domain/team/controller/TeamMemberController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public ResponseEntity<Void> kickMember(@AuthenticationPrincipal UserDetailsImpl
6666
@Operation(summary = "그룹 탈퇴", description = "멤버 스스로 그룹에서 나갑니다.")
6767
@ApiResponses({
6868
@ApiResponse(responseCode = "200", description = "그룹 탈퇴 성공"),
69-
@ApiResponse(responseCode = "400", description = "마지막 남은 관리자는 탈퇴할 수 없음"),
69+
@ApiResponse(responseCode = "409", description = "마지막 남은 관리자는 탈퇴할 수 없음"),
7070
@ApiResponse(responseCode = "404", description = "그룹 또는 멤버를 찾을 수 없음")
7171
})
7272
@DeleteMapping("/{groupId}/leave")

src/main/java/com/sonkim/bookmarking/domain/team/dto/TeamDto.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,10 @@ public static class CreateResponseDto {
5555
public static class CodeResponseDto {
5656
private String code;
5757
}
58+
59+
// 그룹 소유주 이전
60+
@Data
61+
public static class OwnerTransferDto {
62+
private Long newOwnerId;
63+
}
5864
}

src/main/java/com/sonkim/bookmarking/domain/team/entity/Team.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,9 @@ public void cancelDeletion() {
7070
this.status = TeamStatus.ACTIVE;
7171
this.deletionScheduledAt = null;
7272
}
73+
74+
// 소유주 변경
75+
public void updateOwner(User owner) {
76+
this.owner = owner;
77+
}
7378
}

0 commit comments

Comments
 (0)