Skip to content

Commit e03acef

Browse files
authored
[FEAT] 모임 이미지 단 건 삭제 구현
[FEAT] 모임 이미지 단 건 삭제 구현
2 parents 03bbdfe + 5ca0052 commit e03acef

File tree

9 files changed

+320
-47
lines changed

9 files changed

+320
-47
lines changed

src/main/java/team/wego/wegobackend/group/application/service/v1/GroupService.java

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@ public class GroupService {
6464
private static final String MAIN_IMAGE_SIZE_TOKEN = "440x240";
6565
private static final String THUMB_IMAGE_SIZE_TOKEN = "100x100";
6666

67-
/* =========================
68-
* Create
69-
* ========================= */
70-
7167
private void validateCreateGroupRequest(CreateGroupRequest request) {
7268
if (request.endTime() != null && !request.endTime().isAfter(request.startTime())) {
7369
throw new GroupException(GroupErrorCode.INVALID_TIME_RANGE);
@@ -203,10 +199,6 @@ private int sortOrderOrZero(Integer sortOrder) {
203199
return sortOrder != null ? sortOrder : 0;
204200
}
205201

206-
/* =========================
207-
* Common Finders
208-
* ========================= */
209-
210202
private Group findActiveGroup(Long groupId) {
211203
return groupRepository.findByIdAndDeletedAtIsNull(groupId)
212204
.orElseThrow(
@@ -218,10 +210,6 @@ private User findMember(Long userId) {
218210
.orElseThrow(() -> new GroupException(GroupErrorCode.MEMBER_NOT_FOUND, userId));
219211
}
220212

221-
/* =========================
222-
* Attend / Cancel Attend
223-
* ========================= */
224-
225213
private void validateNotAlreadyAttend(GroupUser groupUser, Long groupId, Long userId) {
226214
if (groupUser != null && groupUser.getStatus() == ATTEND) {
227215
throw new GroupException(GroupErrorCode.ALREADY_ATTEND_GROUP, groupId, userId);
@@ -299,10 +287,6 @@ private void validateCurrentlyAttend(GroupUser groupUser, Long groupId, Long use
299287
}
300288
}
301289

302-
/* =========================
303-
* Group List
304-
* ========================= */
305-
306290
private int clampPageSize(int size) {
307291
return Math.max(1, Math.min(size, MAX_PAGE_SIZE));
308292
}
@@ -336,10 +320,6 @@ private GroupListItemResponse toGroupListItemResponse(Group group) {
336320
return GroupListItemResponse.of(group, imageUrls, tagNames, participantCount, createdBy);
337321
}
338322

339-
/* =========================
340-
* Get Group (Detail)
341-
* ========================= */
342-
343323
private GetGroupResponse buildGetGroupResponse(Group group, Long currentUserId) {
344324
List<GroupImageItemResponse> images = extractImageItems(group);
345325
List<String> tagNames = extractTagNames(group);
@@ -384,10 +364,6 @@ public GetGroupResponse getGroup(Long groupId) {
384364
return getGroupInternal(groupId, null);
385365
}
386366

387-
/* =========================
388-
* Image / Tag / User extractors
389-
* ========================= */
390-
391367
private Map<Integer, List<GroupImage>> groupImagesBySortOrder(Group group) {
392368
if (group.getImages() == null || group.getImages().isEmpty()) {
393369
return Map.of();
@@ -533,11 +509,7 @@ private List<String> extractAllImageUrls(Group group) {
533509
.filter(url -> url != null && !url.isBlank())
534510
.toList();
535511
}
536-
537-
/* =========================
538-
* Update / Delete
539-
* ========================= */
540-
512+
541513
private void updateGroupTags(Group group, List<String> tagNames) {
542514
if (tagNames == null) { // null이면 "태그는 건드리지 않는다"
543515
return;
@@ -662,4 +634,28 @@ private GetGroupListResponse toGroupListResponse(List<Group> fetched, int pageSi
662634

663635
return GetGroupListResponse.of(items, nextCursor);
664636
}
637+
638+
639+
@Transactional
640+
public void deleteOne(CustomUserDetails userDetails, Long groupId, String url) {
641+
Long userId = userDetails.getId();
642+
Group group = findActiveGroup(groupId);
643+
644+
if (!group.getHost().getId().equals(userId)) {
645+
throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_DELETE_GROUP, groupId, userId);
646+
}
647+
648+
if (url == null || url.isBlank()) {
649+
throw new GroupException(GroupErrorCode.IMAGE_URL_REQUIRED);
650+
}
651+
652+
boolean exists = groupImageRepository.existsByGroupAndImageUrl(group, url);
653+
if (!exists) {
654+
throw new GroupException(GroupErrorCode.GROUP_IMAGE_NOT_FOUND, groupId);
655+
}
656+
657+
groupImageRepository.deleteByGroupAndImageUrl(group, url);
658+
imageUploadService.deleteOne(null, url);
659+
}
660+
665661
}

src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ public enum GroupErrorCode implements ErrorCode {
2828
"모임: 현재 참여 인원 수(%s)보다 작은 값으로 최대 인원을 줄일 수 없습니다."),
2929
NO_PERMISSION_TO_DELETE_GROUP(HttpStatus.UNAUTHORIZED, "모임: 삭제할 수 있는 권한이 없습니다."),
3030
MY_GROUP_TYPE_NOT_NULL(HttpStatus.BAD_REQUEST, "모임: MyGroupType 값은 null일 수 없습니다."),
31-
INVALID_MY_GROUP_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 MyGroupType: %s");
31+
INVALID_MY_GROUP_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 MyGroupType: %s"),
32+
IMAGE_URL_REQUIRED(HttpStatus.BAD_REQUEST, "모임 이미지 삭제: url은 필수입니다."),
33+
GROUP_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "모임 이미지가 존재하지 않습니다. groupId=%d");
3234

3335
private final HttpStatus status;
3436
private final String message;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package team.wego.wegobackend.group.domain.repository.v1;
22

33

4+
import java.util.Optional;
45
import org.springframework.data.jpa.repository.JpaRepository;
6+
import team.wego.wegobackend.group.domain.entity.Group;
57
import team.wego.wegobackend.group.domain.entity.GroupImage;
68

79
public interface GroupImageRepository extends JpaRepository<GroupImage, Long> {
810

11+
Optional<GroupImage> findByGroupAndImageUrl(Group group, String imageUrl);
12+
13+
void deleteByGroupAndImageUrl(Group group, String imageUrl);
14+
15+
boolean existsByGroupAndImageUrl(Group group, String imageUrl);
916
}

src/main/java/team/wego/wegobackend/group/presentation/v1/GroupController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ public ResponseEntity<ApiResponse<Void>> deleteGroup(
124124
}
125125

126126

127+
@DeleteMapping("/{groupId}/images/one")
128+
public ResponseEntity<Void> deleteOne(
129+
@AuthenticationPrincipal CustomUserDetails userDetails,
130+
@PathVariable Long groupId,
131+
@RequestParam(value = "url") String url
132+
) {
133+
groupService.deleteOne(userDetails, groupId, url);
134+
return ResponseEntity.noContent().build();
135+
}
136+
137+
127138
@GetMapping("/me")
128139
public ResponseEntity<ApiResponse<GetGroupListResponse>> getMyGroups(
129140
@AuthenticationPrincipal CustomUserDetails userDetails,

src/main/java/team/wego/wegobackend/image/application/service/ImageUploadService.java

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.awt.image.BufferedImage;
44
import java.io.ByteArrayOutputStream;
55
import java.io.IOException;
6+
import java.net.URI;
67
import java.time.LocalDateTime;
78
import java.time.format.DateTimeFormatter;
89
import java.util.ArrayList;
@@ -384,27 +385,30 @@ private void putToS3(String key, byte[] bytes, String contentType) {
384385
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(bytes));
385386
}
386387

388+
387389
public String extractKeyFromUrl(String imageUrl) {
388-
if (imageUrl == null || imageUrl.isBlank()) {
389-
return null;
390-
}
390+
if (imageUrl == null || imageUrl.isBlank()) return null;
391+
392+
String clean = imageUrl;
393+
int qIdx = clean.indexOf('?');
394+
if (qIdx >= 0) clean = clean.substring(0, qIdx);
391395

392396
String endpoint = awsS3Properties.getPublicEndpoint();
393397
if (endpoint != null && !endpoint.isBlank()) {
394398
String prefix = endpoint.endsWith("/") ? endpoint : endpoint + "/";
395-
if (imageUrl.startsWith(prefix)) {
396-
return imageUrl.substring(prefix.length());
399+
if (clean.startsWith(prefix)) {
400+
return clean.substring(prefix.length());
397401
}
398402
}
399403

400-
// fallback: 마지막 "/" 이후를 key로 사용
401-
int idx = imageUrl.lastIndexOf('/');
402-
if (idx >= 0 && idx + 1 < imageUrl.length()) {
403-
return imageUrl.substring(idx + 1);
404+
try {
405+
URI uri = URI.create(clean);
406+
String path = uri.getPath(); // "/dir/xxx.webp"
407+
if (path == null || path.isBlank()) return null;
408+
return path.startsWith("/") ? path.substring(1) : path;
409+
} catch (IllegalArgumentException e) {
410+
return clean; // url이 아니면 key로 간주
404411
}
405-
406-
// 그래도 안 되면 전체를 key로 시도 (최악의 경우)
407-
return imageUrl;
408412
}
409413

410414
public void deleteByUrl(String imageUrl) {
@@ -433,4 +437,34 @@ public void deleteAllByUrls(List<String> imageUrls) {
433437
}
434438

435439

440+
public void deleteOne(String key, String url) {
441+
boolean hasKey = key != null && !key.isBlank();
442+
boolean hasUrl = url != null && !url.isBlank();
443+
444+
if (!hasKey && !hasUrl) {
445+
throw new ImageException(ImageExceptionCode.KEY_OR_URL_REQUIRED);
446+
}
447+
if (hasKey && hasUrl) {
448+
throw new ImageException(ImageExceptionCode.KEY_AND_URL_CONFLICT);
449+
}
450+
451+
String targetKey = hasUrl ? extractKeyFromUrl(url) : key;
452+
453+
validateKey(targetKey);
454+
delete(targetKey);
455+
}
456+
457+
private void validateKey(String key) {
458+
if (key == null || key.isBlank()) {
459+
throw new ImageException(ImageExceptionCode.KEY_OR_URL_REQUIRED);
460+
}
461+
if (key.contains("..") || key.startsWith("/")) {
462+
throw new ImageException(ImageExceptionCode.DIR_INVALID_TRAVERSAL);
463+
}
464+
if (!key.matches("[a-zA-Z0-9_\\-./]+")) {
465+
throw new ImageException(ImageExceptionCode.INVALID_IMAGE_KEY, key);
466+
}
467+
}
468+
469+
436470
}

src/main/java/team/wego/wegobackend/image/domain/exception/ImageExceptionCode.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,19 @@ public enum ImageExceptionCode implements ErrorCode {
4444
"이미지: dir은 /로 끝나면 안 됩니다."),
4545

4646
DIR_INVALID_PATTERN(HttpStatus.BAD_REQUEST,
47-
"이미지: dir에는 알파벳, 숫자, '-', '_', '/'만 사용할 수 있습니다.");
47+
"이미지: dir에는 알파벳, 숫자, '-', '_', '/'만 사용할 수 있습니다."),
48+
49+
KEY_OR_URL_REQUIRED(HttpStatus.BAD_REQUEST,
50+
"이미지: key 또는 url 중 하나는 필수입니다."),
51+
52+
KEY_AND_URL_CONFLICT(HttpStatus.BAD_REQUEST,
53+
"이미지: key와 url은 동시에 보낼 수 없습니다. 둘 중 하나만 보내주세요."),
54+
55+
INVALID_IMAGE_KEY(HttpStatus.BAD_REQUEST,
56+
"이미지: 잘못된 key 형식입니다. key=%s"),
57+
58+
KEY_REQUIRED(HttpStatus.BAD_REQUEST,
59+
"이미지: key가 존재하지 않습니다.");
4860

4961
private final HttpStatus httpStatus;
5062
private final String messageTemplate;
@@ -58,4 +70,4 @@ public HttpStatus getHttpStatus() {
5870
public String getMessageTemplate() {
5971
return messageTemplate;
6072
}
61-
}
73+
}

src/main/java/team/wego/wegobackend/image/presentation/ImageController.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,21 @@ public ResponseEntity<ApiResponse<List<ImageFileResponse>>> uploadAsWebpWithSize
103103
}
104104

105105
@DeleteMapping("/one")
106-
public ResponseEntity<Void> deleteOne(@RequestParam("key") String key) {
107-
imageUploadService.delete(key);
108-
106+
public ResponseEntity<Void> deleteOne(
107+
@RequestParam(value = "key", required = false) String key,
108+
@RequestParam(value = "url", required = false) String url
109+
) {
110+
imageUploadService.deleteOne(key, url);
109111
return ResponseEntity.noContent().build();
110112
}
111113

114+
112115
@DeleteMapping
113116
public ResponseEntity<Void> deleteMany(@RequestParam("keys") List<String> keys) {
114117
imageUploadService.deleteAll(keys);
115118

116119
return ResponseEntity.noContent().build();
117120
}
121+
122+
118123
}

0 commit comments

Comments
 (0)