Skip to content

Commit 671a148

Browse files
committed
⚡ MarkSphere v1.0.7
특정 그룹에서 자신 권한 확인 API 추가 이미지 저장 시 압축 기능 구현 그룹 삭제 요청 가능 권한 상향 (관리자 -> 소유자)
1 parent 0f1950e commit 671a148

File tree

8 files changed

+103
-27
lines changed

8 files changed

+103
-27
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ dependencies {
7474
annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:7.0:jakarta'
7575
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
7676
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
77+
78+
// image 리사이징 및 변환
79+
implementation "com.sksamuel.scrimage:scrimage-core:4.3.5"
80+
implementation "com.sksamuel.scrimage:scrimage-webp:4.3.5"
7781
}
7882

7983
// Q-Class를 생성할 디렉토리 경로

src/main/java/com/sonkim/bookmarking/common/s3/service/S3Service.java

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
package com.sonkim.bookmarking.common.s3.service;
22

3+
import com.sksamuel.scrimage.ImmutableImage;
4+
import com.sksamuel.scrimage.webp.WebpWriter;
35
import com.sonkim.bookmarking.common.s3.dto.PresignedUrlDto;
46
import lombok.extern.slf4j.Slf4j;
57
import org.springframework.beans.factory.annotation.Value;
68
import org.springframework.stereotype.Service;
9+
import software.amazon.awssdk.core.ResponseBytes;
710
import software.amazon.awssdk.core.sync.RequestBody;
811
import software.amazon.awssdk.services.s3.S3Client;
12+
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
913
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
14+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
1015
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
1116
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
1217
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
1318
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
1419
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
1520
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
1621

22+
import java.io.IOException;
1723
import java.net.URL;
1824
import java.time.Duration;
1925
import java.util.UUID;
@@ -70,21 +76,50 @@ public PresignedUrlDto generatePresignedPutUrl(String fileName) {
7076
.build();
7177
}
7278

73-
public void moveFileToPermanentStorage(String prefix, String key) {
74-
String sourceKey = "temp/" + key;
75-
String destinationKey = prefix + key;
76-
77-
// 복사
78-
s3Client.copyObject(builder -> builder
79-
.sourceBucket(bucketName)
80-
.sourceKey(sourceKey)
81-
.destinationBucket(bucketName)
82-
.destinationKey(destinationKey));
83-
84-
// 원본 삭제
85-
s3Client.deleteObject(builder -> builder
86-
.bucket(bucketName)
87-
.key(sourceKey));
79+
public String moveFileToPermanentStorage(String prefix, String fileName) {
80+
String sourceKey = "temp/" + fileName;
81+
82+
try {
83+
// S3 임시 폴더에서 이미지 다운로드
84+
GetObjectRequest getRequest = GetObjectRequest.builder()
85+
.bucket(bucketName)
86+
.key(sourceKey)
87+
.build();
88+
ResponseBytes<GetObjectResponse> responseBytes = s3Client.getObjectAsBytes(getRequest);
89+
byte[] originalBytes = responseBytes.asByteArray();
90+
91+
// 리사이징 및 WebP로 변환
92+
ImmutableImage image = ImmutableImage.loader().fromBytes(originalBytes);
93+
if (image.width > 600) {
94+
image = image.scaleToWidth(600);
95+
}
96+
byte[] convertedBytes = image.bytes(WebpWriter.DEFAULT.withQ(80));
97+
98+
// 새로운 파일 이름 생성 (확장자를 .webp로 변경)
99+
String newFileName = getFileNameWithoutExtension(fileName) + ".webp";
100+
String destKey = prefix + newFileName;
101+
102+
// 영구 저장소에 이미지 업로드
103+
PutObjectRequest putRequest = PutObjectRequest.builder()
104+
.bucket(bucketName)
105+
.key(destKey)
106+
.contentType("image/webp")
107+
.contentDisposition("inline")
108+
.build();
109+
s3Client.putObject(putRequest, RequestBody.fromBytes(convertedBytes));
110+
111+
// 임시 저장소의 원본 파일 제거
112+
s3Client.deleteObject(DeleteObjectRequest.builder()
113+
.bucket(bucketName)
114+
.key(sourceKey)
115+
.build());
116+
117+
log.info("이미지 변환 및 이동 완료: {} -> {}", sourceKey, destKey);
118+
return newFileName;
119+
120+
} catch (IOException e) {
121+
throw new RuntimeException("이미지 처리 및 이동 중 오류 발생: " + fileName, e);
122+
}
88123
}
89124

90125
public void deleteFile(String prefix, String key) {
@@ -125,9 +160,17 @@ private String getContentType(String fileName) {
125160
case "jpg", "jpeg" -> "image/jpeg";
126161
case "png" -> "image/png";
127162
case "gif" -> "image/gif";
163+
case "webp" -> "image/webp";
128164
default ->
129165
// 알려지지 않은 확장자는 일반적인 바이너리 파일 타입으로 처리
130166
"application/octet-stream";
131167
};
132168
}
169+
170+
private String getFileNameWithoutExtension(String fileName) {
171+
if (fileName.contains(".")) {
172+
return fileName.substring(0, fileName.lastIndexOf('.'));
173+
}
174+
return fileName;
175+
}
133176
}

src/main/java/com/sonkim/bookmarking/domain/bookmark/service/BookmarkService.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ public BookmarkResponseDto createBookmark(Long userId, Long teamId, BookmarkRequ
8080
if (request.getImageKey() != null && !request.getImageKey().isEmpty()) {
8181
// 사용자가 직접 이미지를 업로드한 경우
8282
// 파일을 temp -> bookmarks로 이동
83-
s3Service.moveFileToPermanentStorage("bookmarks/", request.getImageKey());
84-
imageKey = request.getImageKey();
83+
imageKey = s3Service.moveFileToPermanentStorage("bookmarks/", request.getImageKey());
8584
} else if (request.getOriginalImageUrl() != null && !request.getOriginalImageUrl().isEmpty()) {
8685
// OG 이미지 그대로 사용하는 경우
8786
// 임시 저장 후 비동기 처리 요청
@@ -198,8 +197,8 @@ public void updateBookmark(Long userId, Long bookmarkId, BookmarkRequestDto dto)
198197
}
199198
} else {
200199
// 이미지를 변경하려는 경우
201-
s3Service.moveFileToPermanentStorage("bookmarks/", dto.getImageKey());
202-
bookmark.updateImageKey(dto.getImageKey());
200+
String newImageKey = s3Service.moveFileToPermanentStorage("bookmarks/", dto.getImageKey());
201+
bookmark.updateImageKey(newImageKey);
203202
if (oldImageKey != null) {
204203
s3Service.deleteFile("bookmarks/", oldImageKey);
205204
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ public void updateProfile(Long userId, MyProfileDto.UpdateRequestDto updateReque
8080
user.getProfile().updateImageKey(null);
8181
} else {
8282
// 이미지를 새로 변경하려는 경우
83-
s3Service.moveFileToPermanentStorage("profile-images/", newImageKey);
84-
user.getProfile().updateImageKey(newImageKey);
83+
String finalImageKey = s3Service.moveFileToPermanentStorage("profile-images/", newImageKey);
84+
user.getProfile().updateImageKey(finalImageKey);
8585
}
8686
if (oldImageKey != null) {
8787
s3Service.deleteFile("profile-images/", oldImageKey);

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.sonkim.bookmarking.auth.entity.UserDetailsImpl;
44
import com.sonkim.bookmarking.domain.team.dto.TeamMemberDto;
5+
import com.sonkim.bookmarking.domain.team.enums.Permission;
56
import com.sonkim.bookmarking.domain.team.service.TeamMemberService;
67
import io.swagger.v3.oas.annotations.Operation;
78
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -33,6 +34,19 @@ public ResponseEntity<List<TeamMemberDto.MemberResponseDto>> getMembers(@PathVar
3334
return ResponseEntity.ok(members);
3435
}
3536

37+
@Operation(summary = "그룹 내 자신의 권한 조회", description = "로그인한 사용자의 특정 그룹에서의 권한을 조회합니다.")
38+
@ApiResponses({
39+
@ApiResponse(responseCode = "200", description = "역할 조회 성공"),
40+
@ApiResponse(responseCode = "404", description = "그룹을 찾을 수 없음")
41+
})
42+
@GetMapping("/{groupId}/permission")
43+
public ResponseEntity<TeamMemberDto.GetPermissionResponseDto> getMemberPermission(@AuthenticationPrincipal UserDetailsImpl userDetails,
44+
@PathVariable("groupId") Long groupId) {
45+
Permission permission = teamMemberService.getUserPermissionInTeam(userDetails.getId(), groupId);
46+
return ResponseEntity.ok().body(TeamMemberDto.GetPermissionResponseDto.builder().permission(permission).build());
47+
}
48+
49+
3650
@Operation(summary = "멤버 역할 수정", description = "그룹 멤버의 역할을 변경합니다. ADMIN 권한이 필요합니다.")
3751
@ApiResponses({
3852
@ApiResponse(responseCode = "200", description = "역할 수정 성공"),

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,11 @@ public static class MemberResponseDto {
2121
public static class UpdatePermissionRequestDto {
2222
private Permission permission;
2323
}
24+
25+
// 멤버 역할 조회 응답용 DTO
26+
@Data
27+
@Builder
28+
public static class GetPermissionResponseDto {
29+
private Permission permission;
30+
}
2431
}

src/main/java/com/sonkim/bookmarking/domain/team/service/TeamMemberService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.sonkim.bookmarking.domain.team.repository.TeamMemberRepository;
99
import com.sonkim.bookmarking.domain.team.repository.TeamRepository;
1010
import com.sonkim.bookmarking.domain.user.entity.User;
11+
import com.sonkim.bookmarking.domain.user.enums.UserStatus;
1112
import jakarta.persistence.EntityNotFoundException;
1213
import lombok.RequiredArgsConstructor;
1314
import lombok.extern.slf4j.Slf4j;
@@ -113,6 +114,11 @@ public void updateMemberPermission(Long userId, Long teamId, Long memberId, Team
113114
TeamMember member = teamMemberRepository.getTeamMemberByUser_IdAndTeam_Id(memberId, teamId)
114115
.orElseThrow(() -> new EntityNotFoundException("해당 유저 혹은 그룹을 찾을 수 없습니다. memberId: " + memberId + ", teamId: " + teamId));
115116

117+
// 대상 멤버가 탈퇴한 사용자이면 변경 불가
118+
if (member.getUser().getUserStatus() != UserStatus.ACTIVE) {
119+
throw new IllegalStateException("탈퇴한 사용자의 권한은 변경할 수 없습니다.");
120+
}
121+
116122
// 역할 업데이트
117123
member.updatePermission(dto.getPermission());
118124
}

src/main/java/com/sonkim/bookmarking/domain/team/service/TeamService.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.sonkim.bookmarking.domain.team.entity.TeamMember;
66
import com.sonkim.bookmarking.domain.team.enums.Permission;
77
import com.sonkim.bookmarking.domain.team.enums.TeamStatus;
8-
import com.sonkim.bookmarking.domain.team.repository.TeamMemberRepository;
98
import com.sonkim.bookmarking.domain.team.repository.TeamRepository;
109
import com.sonkim.bookmarking.domain.user.entity.User;
1110
import com.sonkim.bookmarking.domain.user.enums.UserStatus;
@@ -201,25 +200,29 @@ public void joinTeam(Long userId, String inviteCode) {
201200
@Transactional
202201
public void scheduleTeamDeletion(Long userId, Long teamId) {
203202
log.info("userId: {}, teamId: {} 그룹 삭제 요청", userId, teamId);
203+
Team team = getTeamById(teamId);
204204

205-
// ADMIN 권한 확인
206-
teamMemberService.validateAdmin(userId, teamId);
205+
// 소유자 확인
206+
if (!team.getOwner().getId().equals(userId)) {
207+
throw new AuthorizationDeniedException("그룹 삭제는 소유자만 가능합니다.");
208+
}
207209

208210
// 그룹 삭제 요청
209-
Team team = getTeamById(teamId);
210211
team.scheduleDeletion();
211212
}
212213

213214
// 그룹 삭제 취소 요청
214215
@Transactional
215216
public void cancelTeamDeletion(Long userId, Long teamId) {
216217
log.info("userId: {}, teamId: {} 그룹 삭제 취소 요청", userId, teamId);
218+
Team team = getTeamById(teamId);
217219

218220
// ADMIN 권한 확인
219-
teamMemberService.validateAdmin(userId, teamId);
221+
if (!team.getOwner().getId().equals(userId)) {
222+
throw new AuthorizationDeniedException("그룹 삭제 취소는 소유자만 가능합니다.");
223+
}
220224

221225
// 그룹 삭제 취소 요청
222-
Team team = getTeamById(teamId);
223226
team.cancelDeletion();
224227
}
225228

0 commit comments

Comments
 (0)