Skip to content

Commit c5ecba7

Browse files
authored
Fix/#82 S3 사진 관련 버그 수정 - 조직 사진 관련
Fix/#82 S3 사진 관련 버그 수정 - 조직 사진 관련
2 parents 9e7e002 + 91130cc commit c5ecba7

9 files changed

Lines changed: 127 additions & 30 deletions

File tree

src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ public class OrgRequest {
1111
public record Create (
1212
@NotBlank(message = "조직 이름은 필수입니다.")
1313
String name,
14-
String description,
15-
String logoUrl
14+
String description
1615
) {}
1716

1817
public record Read (
@@ -23,7 +22,7 @@ public record Update (
2322
@NotBlank(message = "조직 이름은 필수입니다.")
2423
String name,
2524
String description,
26-
String logoUrl
25+
boolean isImageDeleted
2726
) {}
2827

2928
public record UpdateRole (

src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ public static OrgResponse.SimpleInfo toOrgSimpleInfo(OrgMember orgMember) {
5656
}
5757

5858
//DTO -> Entity
59-
public static Organization toOrganization(Long userId, OrgRequest.Create request) {
59+
public static Organization toOrganization(Long userId, OrgRequest.Create request, String imageUrl) {
6060
return Organization.builder()
6161
.name(request.name())
6262
.description(request.description())
63-
.logoUrl(request.logoUrl())
63+
.logoUrl(imageUrl)
6464
.ownerUserId(userId)
6565
.status(OrgStatus.ACTIVE)
6666
.build();

src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22

33
import com.whereyouad.WhereYouAd.domains.organization.application.dto.request.OrgRequest;
44
import com.whereyouad.WhereYouAd.domains.organization.application.dto.response.OrgResponse;
5+
import org.springframework.web.multipart.MultipartFile;
56

67
public interface OrgService {
78

8-
OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request);
9+
OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request, MultipartFile imageFile);
910

1011
OrgResponse.MyOrganizations getMyOrganizations(Long userId);
1112

1213
OrgResponse.OrgDetail getOrganizationDetail(Long orgId);
1314

1415
OrgResponse.MyOrganizations getSoftDeletedOrgs(Long userId);
1516

16-
OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest.Update request);
17+
OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest.Update request, MultipartFile imageFile);
1718

1819
void removeOrganization(Long userId, Long orgId);
1920

src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@
1818
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
1919
import com.whereyouad.WhereYouAd.domains.user.persistence.repository.UserRepository;
2020
import com.whereyouad.WhereYouAd.global.utils.RedisUtil;
21+
import com.whereyouad.WhereYouAd.infrastructure.client.aws.s3.S3UploadService;
2122
import lombok.RequiredArgsConstructor;
23+
import lombok.extern.slf4j.Slf4j;
2224
import org.springframework.stereotype.Service;
2325
import org.springframework.transaction.annotation.Transactional;
26+
import org.springframework.web.multipart.MultipartFile;
2427

2528
import java.util.*;
2629

2730
@Service
2831
@Transactional
2932
@RequiredArgsConstructor
33+
@Slf4j
3034
public class OrgServiceImpl implements OrgService {
3135

3236
private final OrgRepository orgRepository;
@@ -35,9 +39,10 @@ public class OrgServiceImpl implements OrgService {
3539

3640
private final RedisUtil redisUtil;
3741
private final EmailService emailService;
42+
private final S3UploadService s3UploadService;
3843

3944
// 조직(워크스페이스) 생성 메서드
40-
public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request) {
45+
public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request, MultipartFile imageFile) {
4146

4247
// 유저 정보 추출
4348
User user = userRepository.findById(userId)
@@ -53,16 +58,35 @@ public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create requ
5358
}
5459
}
5560

56-
// 조직 생성
57-
Organization organization = OrgConverter.toOrganization(userId, request);
58-
59-
// OrgMember 생성
60-
OrgMember orgMember = OrgMemberConverter.toOrgMemberADMIN(user, organization);
61+
//추가 : 로고 이미지 처리
62+
String imageUrl = null;
63+
if (imageFile != null && !imageFile.isEmpty()) {
64+
imageUrl = s3UploadService.uploadImage(imageFile);
65+
}
6166

62-
orgRepository.save(organization);
63-
orgMemberRepository.save(orgMember);
67+
try {
68+
// 조직 생성
69+
Organization organization = OrgConverter.toOrganization(userId, request, imageUrl);
70+
71+
// OrgMember 생성
72+
OrgMember orgMember = OrgMemberConverter.toOrgMemberADMIN(user, organization);
73+
74+
orgRepository.save(organization);
75+
orgMemberRepository.save(orgMember);
76+
77+
return OrgConverter.toCreatedResponse(organization);
78+
} catch (Exception e) { //조직 생성 중 오류 발생 시
79+
log.error("조직 생성 실패: {}", e.getMessage(), e);
80+
if (imageUrl != null) { //로고 이미지 S3 에서 삭제 진행 (orphan 방지)
81+
try {
82+
s3UploadService.deleteImageFromUrl(imageUrl);
83+
} catch (Exception deleteException) {
84+
log.warn("조직 생성 실패 후 S3 이미지 삭제 실패: {}", imageUrl, deleteException);
85+
}
86+
}
6487

65-
return OrgConverter.toCreatedResponse(organization);
88+
throw new OrgHandler(OrgErrorCode.ORG_CREATE_FAILED);
89+
}
6690
}
6791

6892
//로그인한 회원이 속한 조직 모두 조회 메서드
@@ -115,7 +139,7 @@ public OrgResponse.MyOrganizations getSoftDeletedOrgs(Long userId) {
115139
}
116140

117141
// 조직 정보 수정 메서드
118-
public OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest.Update request) {
142+
public OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest.Update request, MultipartFile imageFile) {
119143
Organization organization = orgRepository.findById(orgId)
120144
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));
121145

@@ -124,8 +148,43 @@ public OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest
124148
throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리
125149
}
126150

151+
//조직 로고 이미지 처리 추가
152+
String oldLogoUrl = organization.getLogoUrl();
153+
//기본적으로는 기존 이미지 유지
154+
String finalLogoImageUrl = oldLogoUrl;
155+
156+
//로고 이미지를 기본 공백 이미지로 하는 거라면
157+
if (request.isImageDeleted()) {
158+
finalLogoImageUrl = null; //URL 을 null 처리
159+
160+
//기존 로고 이미지 존재 시 삭제
161+
if (oldLogoUrl != null) {
162+
try {
163+
s3UploadService.deleteImageFromUrl(oldLogoUrl);
164+
} catch (Exception e) {
165+
log.warn("조직 정보 수정 진행간에 S3 이미지 삭제 실패: {}", oldLogoUrl, e);
166+
}
167+
}
168+
169+
} else if (imageFile != null && !imageFile.isEmpty()) { //이미지 변경이라면,
170+
//이미지 업로드
171+
finalLogoImageUrl = s3UploadService.uploadImage(imageFile);
172+
173+
//기존 로고 이미지 존재 시 삭제
174+
if (oldLogoUrl != null) {
175+
try {
176+
s3UploadService.deleteImageFromUrl(oldLogoUrl);
177+
} catch (Exception e) {
178+
log.warn("조직 정보 수정 진행간에 S3 이미지 삭제 실패: {}", oldLogoUrl, e);
179+
}
180+
181+
}
182+
183+
}
184+
127185
// 조직 정보 수정
128186
organization.modifyInfo(request);
187+
organization.modifyLogoImage(finalLogoImageUrl);
129188

130189
// 변환 된 필드값과 해당 조직의 Id, updatedAt 가 포함된 DTO 로 반환
131190
return OrgConverter.toUpdatedResponse(organization);
@@ -160,13 +219,26 @@ public void removeOrganization(Long userId, Long orgId) {
160219
throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리
161220
}
162221

222+
String logoUrl = organization.getLogoUrl();
223+
163224
// 해당 조직에 가입된 모든 회원들의 가입 정보 삭제
164225
List<OrgMember> orgMembers = orgMemberRepository.findOrgMemberByOrg(organization);
165226

166227
orgMemberRepository.deleteAll(orgMembers);
167228

168229
// 조직 실제 삭제
169230
orgRepository.delete(organization);
231+
232+
//추가 : 조직 로고 이미지 존재 시, 이미지를 S3 에서 삭제하는 로직 추가
233+
//조직 삭제 이후 이미지 삭제하여 이미지 삭제 실패 시 조직 삭제 실패 방지
234+
if (logoUrl != null) {
235+
try {
236+
s3UploadService.deleteImageFromUrl(logoUrl);
237+
} catch (Exception e) {
238+
log.warn("조직 삭제 간에 S3 이미지 삭제 실패: {}", logoUrl, e);
239+
}
240+
241+
}
170242
}
171243

172244
// 조직 삭제 메서드 -> Soft Delete (status 만 DELETED 로 변경)

src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public enum OrgErrorCode implements BaseErrorCode {
3131
//410
3232
ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"),
3333
ORG_INVITATION_INVALID(HttpStatus.BAD_REQUEST, "ORG_INVITATION_400", "조직 초대 토큰이 만료되었거나 유효하지 않습니다."),
34+
35+
//500
36+
ORG_CREATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ORG_500_1", "조직 생성 중 서버 오류가 발생했습니다."),
3437
;
3538

3639
private final HttpStatus httpStatus;

src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/Organization.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ public class Organization extends BaseEntity {
4040
public void modifyInfo(OrgRequest.Update request) {
4141
this.name = request.name();
4242
this.description = request.description();
43-
this.logoUrl = request.logoUrl();
43+
}
44+
45+
public void modifyLogoImage(String imageUrl) {
46+
this.logoUrl = imageUrl;
4447
}
4548

4649
public void softDelete() {

src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.springframework.http.ResponseEntity;
1212
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1313
import org.springframework.web.bind.annotation.*;
14+
import org.springframework.web.multipart.MultipartFile;
1415

1516
@RestController
1617
@RequiredArgsConstructor
@@ -23,9 +24,11 @@ public class OrgController implements OrgControllerDocs {
2324
@PostMapping("/create")
2425
public ResponseEntity<DataResponse<OrgResponse.Create>> createOrganization(
2526
@AuthenticationPrincipal(expression = "userId") Long userId,
26-
@RequestBody @Valid OrgRequest.Create request
27-
) {
28-
OrgResponse.Create response = orgService.createOrganization(userId, request);
27+
@RequestPart(value = "request") @Valid OrgRequest.Create request,
28+
@RequestPart(value = "image", required = false) MultipartFile image
29+
)
30+
{
31+
OrgResponse.Create response = orgService.createOrganization(userId, request, image);
2932
return ResponseEntity.ok(
3033
DataResponse.created(response)
3134
);
@@ -70,10 +73,11 @@ public ResponseEntity<DataResponse<OrgResponse.MyOrganizations>> getSoftDeletedO
7073
public ResponseEntity<DataResponse<OrgResponse.Update>> modifyOrganization(
7174
@AuthenticationPrincipal(expression = "userId") Long userId,
7275
@PathVariable Long orgId,
73-
@RequestBody @Valid OrgRequest.Update request
76+
@RequestPart(value = "request") @Valid OrgRequest.Update request,
77+
@RequestPart(value = "image", required = false) MultipartFile imageFile
7478
)
7579
{
76-
OrgResponse.Update response = orgService.modifyOrganization(userId, orgId, request);
80+
OrgResponse.Update response = orgService.modifyOrganization(userId, orgId, request, imageFile);
7781
return ResponseEntity.ok(
7882
DataResponse.from(response)
7983
);

src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,26 @@
1010
import org.springframework.http.ResponseEntity;
1111
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1212
import org.springframework.web.bind.annotation.*;
13+
import org.springframework.web.multipart.MultipartFile;
1314

1415
public interface OrgControllerDocs {
1516
@Operation(
1617
summary = "조직 생성 API",
17-
description = "조직 이름, 설명, 로고 이미지 URL 을 받아 저장(로그인이 진행된 회원만 가능)"
18+
description = "조직 이름, 설명, 로고 이미지 파일을 받아 저장(로그인이 진행된 회원만 가능)\n\n"
19+
+ "🚨 **[프론트엔드 연동 주의사항]** 🚨\n"
20+
+ "- 요청 시 반드시 `multipart/form-data` 형식으로 전송해야 합니다.\n"
21+
+ "- `request` 파트는 단순 문자열이나 객체가 아닌, **`application/json` 타입의 Blob 객체**로 변환하여 append 해야 합니다.\n"
22+
+ "- `image` 파트는 파일 객체를 그대로 append 합니다. (변경하지 않을 경우 생략 가능)"
1823
)
1924
@ApiResponses({
2025
@ApiResponse(responseCode = "200", description = "성공"),
2126
@ApiResponse(responseCode = "400_1", description = "조직 이름 중복")
2227
})
23-
public ResponseEntity<DataResponse<OrgResponse.Create>> createOrganization(@AuthenticationPrincipal(expression = "userId") Long userId,
24-
@RequestBody @Valid OrgRequest.Create request);
28+
public ResponseEntity<DataResponse<OrgResponse.Create>> createOrganization(
29+
@AuthenticationPrincipal(expression = "userId") Long userId,
30+
@RequestPart(value = "request") @Valid OrgRequest.Create request,
31+
@RequestPart(value = "image", required = false) MultipartFile image
32+
);
2533

2634
@Operation(
2735
summary = "내가 속한 조직 전체 조회 API",
@@ -59,7 +67,12 @@ public ResponseEntity<DataResponse<OrgResponse.MyOrganizations>> getSoftDeletedO
5967

6068
@Operation(
6169
summary = "조직 정보 수정 API",
62-
description = "새로운 조직 이름, 설명, 로고 이미지 URL 을 받아 저장(해당 조직을 생성한 회원만 정보 변경 가능)"
70+
description = "새로운 조직 이름, 설명, 로고 이미지 파일을 받아 저장(해당 조직을 생성한 회원만 정보 변경 가능)\n\n"
71+
+ "request 에서 boolean 값인 isImageDeleted 를 true 로 하고 image 파일에 null 값을 담아 전송하면 조직 로고 이미지를 null 값으로 지정하고, isImageDeleted 를 true 로 하고 image 파일에 null 값을 담아 전송하면 기존 조직 로고 이미지를 유지합니다.\n\n"
72+
+ "🚨 **[프론트엔드 연동 주의사항]** 🚨\n"
73+
+ "- 요청 시 반드시 `multipart/form-data` 형식으로 전송해야 합니다.\n"
74+
+ "- `request` 파트는 단순 문자열이나 객체가 아닌, **`application/json` 타입의 Blob 객체**로 변환하여 append 해야 합니다.\n"
75+
+ "- `image` 파트는 파일 객체를 그대로 append 합니다. (변경하지 않을 경우 생략 가능)"
6376
)
6477
@ApiResponses({
6578
@ApiResponse(responseCode = "200", description = "성공(변경된 필드 값들과 조직Id, 변경 시각 반환)"),
@@ -69,7 +82,8 @@ public ResponseEntity<DataResponse<OrgResponse.MyOrganizations>> getSoftDeletedO
6982
public ResponseEntity<DataResponse<OrgResponse.Update>> modifyOrganization(
7083
@AuthenticationPrincipal(expression = "userId") Long userId,
7184
@PathVariable Long orgId,
72-
@RequestBody @Valid OrgRequest.Update request
85+
@RequestPart(value = "request") @Valid OrgRequest.Update request,
86+
@RequestPart(value = "image", required = false) MultipartFile imageFile
7387
);
7488

7589
@Operation(
@@ -91,7 +105,8 @@ public ResponseEntity<DataResponse<OrgResponse.Delete>> restoreOrganization(
91105
@Operation(
92106
summary = "조직 삭제 API",
93107
description = "조직 Id 를 PathVariable 로 받아 해당 조직 삭제(해당 조직을 생성한 회원만 삭제 가능) \n\n" +
94-
"param 인 isHard = true 이면 Hard Delete (DB에서 삭제), isHard = false 이면 Soft Delete (status 만 DELETED 로 변경)"
108+
"param 인 isHard = true 이면 Hard Delete (DB에서 삭제), isHard = false 이면 Soft Delete (status 만 DELETED 로 변경)\n\n"
109+
+ "*추가* Hard Delete 시 조직 로고 이미지가 S3 에서 자동 삭제됩니다. Soft Delete 시에는 삭제되지 않습니다."
95110
)
96111
@ApiResponses({
97112
@ApiResponse(responseCode = "200", description = "성공"),

src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public ResponseEntity<DataResponse<SmsResponse.SmsVerifiedResponse>> verifySms(
100100
@Operation(
101101
summary = "회원 정보 수정 API",
102102
description = "회원이 수정하려는 이름, 프로필 이미지 파일, 기존 비밀번호와 새로운 비밀번호 값을 입력받아 정보 수정을 진행합니다.\n\n" +
103-
"request 에서 boolean 값인 isImageDeleted 를 true 로 하고 image 파일에 null 값을 담아 전송하면 회원 프로필 이미지를 null 값으로 지정하고, isImageDeleted 를 true 로 하고 image 파일에 null 값을 담아 전송하면 기존 프로필 이미지를 유지합니다.\n\n"
103+
"request 에서 boolean 값인 isImageDeleted 를 true 로 하고 image 파일에 null 값을 담아 전송하면 회원 프로필 이미지를 null 값으로 지정하고, isImageDeleted 를 false 로 하고 image 파일에 null 값을 담아 전송하면 기존 프로필 이미지를 유지합니다.\n\n"
104104
+ "🚨 **[프론트엔드 연동 주의사항]** 🚨\n"
105105
+ "- 요청 시 반드시 `multipart/form-data` 형식으로 전송해야 합니다.\n"
106106
+ "- `request` 파트는 단순 문자열이나 객체가 아닌, **`application/json` 타입의 Blob 객체**로 변환하여 append 해야 합니다.\n"

0 commit comments

Comments
 (0)