From 527429d4eff6f039fe9a0bf46c150069afd6132a Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sat, 13 Dec 2025 01:36:30 +0900 Subject: [PATCH 1/4] feat(docs): add validation annotations to request parameters in admin and consultation controllers --- .../docs/AdminConsultationControllerDocs.java | 3 ++- .../admin/controller/docs/AdminControllerDocs.java | 10 ++++++---- .../controller/docs/AdminRedotAppControllerDocs.java | 3 ++- .../auth/controller/docs/AdminAuthControllerDocs.java | 7 ++++--- .../docs/AdminImpersonationControllerDocs.java | 3 ++- .../auth/controller/docs/CMSAuthControllerDocs.java | 5 +++-- .../docs/EmailVerificationControllerDocs.java | 5 +++-- .../controller/docs/RedotMemberAuthControllerDocs.java | 7 ++++--- .../controller/docs/RedotAppInquiryControllerDocs.java | 3 ++- .../controller/docs/CMSMemberControllerDocs.java | 10 ++++++---- .../controller/docs/CMSSitePageControllerDocs.java | 3 ++- .../controller/docs/SiteSettingControllerDocs.java | 6 ++++-- .../style/controller/docs/StyleInfoControllerDocs.java | 3 ++- .../app/controller/docs/RedotAppControllerDocs.java | 5 +++-- .../controller/docs/ConsultationControllerDocs.java | 3 ++- .../controller/docs/RedotMemberControllerDocs.java | 3 ++- .../domain/controller/docs/DomainControllerDocs.java | 3 ++- 17 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java index 1499d84..7899ef9 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springdoc.core.annotations.ParameterObject; @@ -32,6 +33,6 @@ ResponseEntity> getAllConsultations( @Operation(summary = "상담 정보 수정", description = "상담 상태나 담당자 정보를 수정합니다.") @ApiResponse(responseCode = "200", description = "수정 성공", content = @Content(schema = @Schema(implementation = ConsultationResponse.class))) - ResponseEntity updateConsultationInfo(ConsultationUpdateRequest request, + ResponseEntity updateConsultationInfo(@Valid ConsultationUpdateRequest request, @Parameter(description = "상담 ID", example = "1") Long consultationId); } diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminControllerDocs.java index bedda64..5d91ecc 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminControllerDocs.java @@ -7,6 +7,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; @@ -30,7 +32,7 @@ public interface AdminControllerDocs { @Operation(summary = "관리자 생성", description = "새로운 관리자를 생성합니다.") @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = AdminResponse.class))) - ResponseEntity createAdmin(AdminCreateRequest request); + ResponseEntity createAdmin(@Valid AdminCreateRequest request); @Operation(summary = "관리자 목록 조회", description = "`page`, `size`, `sort` 파라미터를 사용해 페이징/정렬하며 `sort=필드명,정렬방향` (예: `sort=createdAt,desc`) 형식을 따릅니다.") @ApiResponse(responseCode = "200", description = "조회 성공", @@ -42,12 +44,12 @@ ResponseEntity> getAdminInfoList(@Parameter(descript @ApiResponse(responseCode = "200", description = "수정 성공", content = @Content(schema = @Schema(implementation = AdminResponse.class))) ResponseEntity updateAdmin(@Parameter(description = "관리자 ID", example = "1") Long adminId, - AdminUpdateRequest request); + @Valid AdminUpdateRequest request); @Operation(summary = "관리자 비밀번호 초기화", description = "관리자의 비밀번호를 재설정합니다.") @ApiResponse(responseCode = "204", description = "재설정 완료") ResponseEntity resetAdminPassword(@Parameter(description = "관리자 ID", example = "1") Long adminId, - AdminResetPasswordRequest request); + @Valid AdminResetPasswordRequest request); @Operation(summary = "다른 관리자 삭제", description = "현재 로그인한 관리자가 다른 관리자를 삭제합니다.") @ApiResponse(responseCode = "204", description = "삭제 완료") @@ -63,5 +65,5 @@ ResponseEntity deleteCurrentAdmin(@Parameter(hidden = true) HttpServletReq @ApiResponse(responseCode = "200", description = "업로드 성공", content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) ResponseEntity uploadProfileImage(@Parameter(hidden = true) JwtPrincipal jwtPrincipal, - MultipartFile image); + @NotNull MultipartFile image); } diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java index 8956d33..722cc4b 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest; import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse; @@ -15,5 +16,5 @@ public interface AdminRedotAppControllerDocs { @Operation(summary = "Redot 앱 생성", description = "관리자 권한으로 신규 Redot 앱을 생성합니다.") @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class))) - ResponseEntity createRedotApp(RedotAppCreateRequest request); + ResponseEntity createRedotApp(@Valid RedotAppCreateRequest request); } diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminAuthControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminAuthControllerDocs.java index 3fcbac7..7ffffaa 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminAuthControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminAuthControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.admin.dto.request.AdminCreateRequest; import redot.redot_server.domain.admin.dto.response.AdminResponse; @@ -22,12 +23,12 @@ public interface AdminAuthControllerDocs { @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = TokenResponse.class))) ResponseEntity signIn(@Parameter(hidden = true) HttpServletRequest request, - SignInRequest signInRequest); + @Valid SignInRequest signInRequest); @Operation(summary = "관리자 회원가입", description = "새로운 Redot 관리자를 등록합니다.") @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = AdminResponse.class))) - ResponseEntity createAdmin(AdminCreateRequest request); + ResponseEntity createAdmin(@Valid AdminCreateRequest request); @Operation(summary = "관리자 토큰 재발급", description = "만료 직전의 토큰을 쿠키 기반으로 재발급합니다.") @ApiResponse(responseCode = "200", description = "재발급 성공", @@ -45,5 +46,5 @@ ResponseEntity signIn(@Parameter(hidden = true) HttpServletReques @Operation(summary = "관리자 비밀번호 재설정 확정", description = "비밀번호 재설정 코드를 확인하고 새 비밀번호를 저장합니다.") @ApiResponse(responseCode = "204", description = "재설정 완료") - ResponseEntity confirmPasswordReset(PasswordResetConfirmRequest request); + ResponseEntity confirmPasswordReset(@Valid PasswordResetConfirmRequest request); } diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java index 27bbebb..b568040 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.auth.dto.request.CMSAdminImpersonationRequest; import redot.redot_server.domain.auth.dto.response.TokenResponse; @@ -19,6 +20,6 @@ public interface AdminImpersonationControllerDocs { @ApiResponse(responseCode = "200", description = "발급 성공", content = @Content(schema = @Schema(implementation = TokenResponse.class))) ResponseEntity impersonateAsCMSAdmin(@Parameter(hidden = true) HttpServletRequest request, - CMSAdminImpersonationRequest cmsAdminImpersonationRequest, + @Valid CMSAdminImpersonationRequest cmsAdminImpersonationRequest, @Parameter(hidden = true) JwtPrincipal jwtPrincipal); } diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/CMSAuthControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/CMSAuthControllerDocs.java index 5d46c43..16a5bea 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/docs/CMSAuthControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/CMSAuthControllerDocs.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; import redot.redot_server.domain.auth.dto.request.SignInRequest; @@ -24,7 +25,7 @@ public interface CMSAuthControllerDocs { @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = TokenResponse.class))) ResponseEntity signIn(@Parameter(hidden = true) HttpServletRequest request, - SignInRequest signInRequest, + @Valid SignInRequest signInRequest, @Parameter(hidden = true) Long redotAppId); @Parameter(name = "X-App-Subdomain", in = ParameterIn.HEADER, required = true, @@ -52,5 +53,5 @@ ResponseEntity getCurrentCMSMemberInfo(@Parameter(hidden = tr @Operation(summary = "CMS 비밀번호 재설정 확정", description = "발급받은 인증 코드로 비밀번호 재설정을 확정합니다.") @ApiResponse(responseCode = "204", description = "재설정 완료") ResponseEntity confirmPasswordReset(@Parameter(hidden = true) Long redotAppId, - PasswordResetConfirmRequest request); + @Valid PasswordResetConfirmRequest request); } diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/EmailVerificationControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/EmailVerificationControllerDocs.java index 24b4f06..d09b07f 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/docs/EmailVerificationControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/EmailVerificationControllerDocs.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.auth.dto.request.EmailVerificationSendRequest; import redot.redot_server.domain.auth.dto.request.EmailVerificationVerifyRequest; @@ -17,10 +18,10 @@ public interface EmailVerificationControllerDocs { @Operation(summary = "이메일 인증 코드 발송", description = "입력한 이메일로 인증 코드를 발송합니다.") @ApiResponse(responseCode = "200", description = "발송 성공", content = @Content(schema = @Schema(implementation = EmailVerificationSendResponse.class))) - ResponseEntity send(EmailVerificationSendRequest request); + ResponseEntity send(@Valid EmailVerificationSendRequest request); @Operation(summary = "이메일 인증 코드 검증", description = "발송된 인증 코드가 유효한지 검증합니다.") @ApiResponse(responseCode = "200", description = "검증 성공", content = @Content(schema = @Schema(implementation = EmailVerificationVerifyResponse.class))) - ResponseEntity verify(EmailVerificationVerifyRequest request); + ResponseEntity verify(@Valid EmailVerificationVerifyRequest request); } diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/RedotMemberAuthControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/RedotMemberAuthControllerDocs.java index 35d9fc3..1712110 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/docs/RedotMemberAuthControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/RedotMemberAuthControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; import redot.redot_server.domain.auth.dto.request.RedotMemberSignInRequest; @@ -22,13 +23,13 @@ public interface RedotMemberAuthControllerDocs { @Operation(summary = "Redot 회원 회원가입", description = "새로운 Redot 회원 계정을 생성합니다.") @ApiResponse(responseCode = "200", description = "회원가입 성공", content = @Content(schema = @Schema(implementation = RedotMemberResponse.class))) - ResponseEntity signUp(RedotMemberCreateRequest request); + ResponseEntity signUp(@Valid RedotMemberCreateRequest request); @Operation(summary = "Redot 회원 로그인", description = "Redot 회원 자격 증명을 사용해 로그인합니다.") @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = TokenResponse.class))) ResponseEntity signIn(@Parameter(hidden = true) HttpServletRequest request, - RedotMemberSignInRequest signInRequest); + @Valid RedotMemberSignInRequest signInRequest); @Operation(summary = "Redot 회원 토큰 재발급", description = "로그인된 회원의 토큰을 재발급합니다.") @ApiResponse(responseCode = "200", description = "재발급 성공", @@ -46,7 +47,7 @@ ResponseEntity signIn(@Parameter(hidden = true) HttpServletReques @Operation(summary = "Redot 회원 비밀번호 재설정 확정", description = "비밀번호 재설정 토큰을 확인하고 비밀번호를 교체합니다.") @ApiResponse(responseCode = "204", description = "재설정 완료") - ResponseEntity confirmPasswordReset(PasswordResetConfirmRequest request); + ResponseEntity confirmPasswordReset(@Valid PasswordResetConfirmRequest request); @Operation(summary = "소셜 로그인 인가 URL 조회", description = "선택한 소셜 제공자의 OAuth2 인가 URL을 제공합니다.") @ApiResponse(responseCode = "200", description = "URL 생성 성공", diff --git a/src/main/java/redot/redot_server/domain/cms/inquiry/controller/docs/RedotAppInquiryControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/inquiry/controller/docs/RedotAppInquiryControllerDocs.java index 7295e1a..1e116e3 100644 --- a/src/main/java/redot/redot_server/domain/cms/inquiry/controller/docs/RedotAppInquiryControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/cms/inquiry/controller/docs/RedotAppInquiryControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springdoc.core.annotations.ParameterObject; @@ -25,7 +26,7 @@ public interface RedotAppInquiryControllerDocs { @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = RedotAppInquiryResponse.class))) ResponseEntity createInquiry(@Parameter(hidden = true) Long redotAppId, - RedotAppInquiryCreateRequest request); + @Valid RedotAppInquiryCreateRequest request); @Parameter(name = "X-App-Subdomain", in = ParameterIn.HEADER, required = true, description = "요청 대상 CMS 앱의 서브도메인") diff --git a/src/main/java/redot/redot_server/domain/cms/member/controller/docs/CMSMemberControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/member/controller/docs/CMSMemberControllerDocs.java index 6d60422..3d6a797 100644 --- a/src/main/java/redot/redot_server/domain/cms/member/controller/docs/CMSMemberControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/cms/member/controller/docs/CMSMemberControllerDocs.java @@ -8,6 +8,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; @@ -30,7 +32,7 @@ public interface CMSMemberControllerDocs { @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) ResponseEntity createCMSMember(@Parameter(hidden = true) Long redotAppId, - CMSMemberCreateRequest request); + @Valid CMSMemberCreateRequest request); @Parameter(name = "X-App-Subdomain", in = ParameterIn.HEADER, required = true, description = "요청 대상 CMS 앱의 서브도메인") @@ -57,7 +59,7 @@ ResponseEntity> getCMSMemberList(@Parameter(hidd content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) ResponseEntity changeCMSMemberRole(@Parameter(hidden = true) Long redotAppId, @Parameter(description = "멤버 ID", example = "1") Long memberId, - CMSMemberRoleRequest request); + @Valid CMSMemberRoleRequest request); @Parameter(name = "X-App-Subdomain", in = ParameterIn.HEADER, required = true, description = "요청 대상 CMS 앱의 서브도메인") @@ -66,7 +68,7 @@ ResponseEntity changeCMSMemberRole(@Parameter(hidden = true) content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) ResponseEntity updateCMSMember(@Parameter(hidden = true) Long redotAppId, @Parameter(hidden = true) JwtPrincipal jwtPrincipal, - CMSMemberUpdateRequest request); + @Valid CMSMemberUpdateRequest request); @Parameter(name = "X-App-Subdomain", in = ParameterIn.HEADER, required = true, description = "요청 대상 CMS 앱의 서브도메인") @@ -91,5 +93,5 @@ ResponseEntity deleteCurrentCMSMember(@Parameter(hidden = true) HttpServle content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) ResponseEntity uploadProfileImage(@Parameter(hidden = true) Long redotAppId, @Parameter(hidden = true) JwtPrincipal jwtPrincipal, - MultipartFile image); + @NotNull MultipartFile image); } diff --git a/src/main/java/redot/redot_server/domain/cms/site/page/controller/docs/CMSSitePageControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/site/page/controller/docs/CMSSitePageControllerDocs.java index d0798ed..f9b253e 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/page/controller/docs/CMSSitePageControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/cms/site/page/controller/docs/CMSSitePageControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; @@ -44,5 +45,5 @@ ResponseEntity getPage(@Parameter(hidden = true) Long redotAppI @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = AppVersionSummaryResponse.class))) ResponseEntity createVersion(@Parameter(hidden = true) Long redotAppId, - AppVersionCreateRequest request); + @Valid AppVersionCreateRequest request); } diff --git a/src/main/java/redot/redot_server/domain/cms/site/setting/controller/docs/SiteSettingControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/docs/SiteSettingControllerDocs.java index 3ce10fa..1badfbd 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/setting/controller/docs/SiteSettingControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/docs/SiteSettingControllerDocs.java @@ -7,6 +7,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; import redot.redot_server.domain.cms.site.setting.dto.request.SiteSettingUpdateRequest; @@ -22,7 +24,7 @@ public interface SiteSettingControllerDocs { @ApiResponse(responseCode = "200", description = "수정 성공", content = @Content(schema = @Schema(implementation = SiteSettingResponse.class))) ResponseEntity updateSiteSetting(@Parameter(hidden = true) Long redotAppId, - SiteSettingUpdateRequest request); + @Valid SiteSettingUpdateRequest request); @Parameter(name = "X-App-Subdomain", in = ParameterIn.HEADER, required = true, description = "요청 대상 CMS 앱의 서브도메인") @@ -30,7 +32,7 @@ ResponseEntity updateSiteSetting(@Parameter(hidden = true) @ApiResponse(responseCode = "200", description = "업로드 성공", content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) ResponseEntity uploadLogoImage(@Parameter(hidden = true) Long redotAppId, - MultipartFile logoFile); + @NotNull MultipartFile logoFile); @Parameter(name = "X-App-Subdomain", in = ParameterIn.HEADER, required = true, description = "요청 대상 CMS 앱의 서브도메인") diff --git a/src/main/java/redot/redot_server/domain/cms/site/style/controller/docs/StyleInfoControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/site/style/controller/docs/StyleInfoControllerDocs.java index 365d3cc..a11ec32 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/style/controller/docs/StyleInfoControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/cms/site/style/controller/docs/StyleInfoControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.cms.site.style.dto.request.StyleInfoUpdateRequest; import redot.redot_server.domain.cms.site.style.dto.response.StyleInfoResponse; @@ -27,5 +28,5 @@ public interface StyleInfoControllerDocs { @ApiResponse(responseCode = "200", description = "수정 성공", content = @Content(schema = @Schema(implementation = StyleInfoResponse.class))) ResponseEntity updateStyleInfo(@Parameter(hidden = true) Long redotAppId, - StyleInfoUpdateRequest request); + @Valid StyleInfoUpdateRequest request); } diff --git a/src/main/java/redot/redot_server/domain/redot/app/controller/docs/RedotAppControllerDocs.java b/src/main/java/redot/redot_server/domain/redot/app/controller/docs/RedotAppControllerDocs.java index 7887fae..69f00f6 100644 --- a/src/main/java/redot/redot_server/domain/redot/app/controller/docs/RedotAppControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/redot/app/controller/docs/RedotAppControllerDocs.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springdoc.core.annotations.ParameterObject; @@ -36,12 +37,12 @@ ResponseEntity> getRedotAppList(@Parameter(hi @Operation(summary = "앱 생성", description = "새로운 Redot 앱을 생성합니다.") @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class))) - ResponseEntity createRedotApp(RedotAppCreateRequest request, + ResponseEntity createRedotApp(@Valid RedotAppCreateRequest request, @Parameter(hidden = true) JwtPrincipal jwtPrincipal); @Operation(summary = "앱 매니저 생성", description = "Redot 앱에 추가 매니저를 초대합니다.") @ApiResponse(responseCode = "200", description = "생성 성공") ResponseEntity createManager(@Parameter(description = "Redot 앱 ID", example = "1") Long redotAppId, - RedotAppCreateManagerRequest request, + @Valid RedotAppCreateManagerRequest request, @Parameter(hidden = true) JwtPrincipal jwtPrincipal); } diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/controller/docs/ConsultationControllerDocs.java b/src/main/java/redot/redot_server/domain/redot/consultation/controller/docs/ConsultationControllerDocs.java index 0e98acb..8ffc7c0 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/controller/docs/ConsultationControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/controller/docs/ConsultationControllerDocs.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.redot.consultation.dto.request.ConsultationCreateRequest; import redot.redot_server.domain.redot.consultation.dto.response.ConsultationResponse; @@ -15,5 +16,5 @@ public interface ConsultationControllerDocs { @Operation(summary = "상담 신청 생성", description = "서비스 상담 신청을 등록합니다.") @ApiResponse(responseCode = "200", description = "생성 성공", content = @Content(schema = @Schema(implementation = ConsultationResponse.class))) - ResponseEntity createConsultation(ConsultationCreateRequest request); + ResponseEntity createConsultation(@Valid ConsultationCreateRequest request); } diff --git a/src/main/java/redot/redot_server/domain/redot/member/controller/docs/RedotMemberControllerDocs.java b/src/main/java/redot/redot_server/domain/redot/member/controller/docs/RedotMemberControllerDocs.java index 649b90f..12a65a3 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/controller/docs/RedotMemberControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/redot/member/controller/docs/RedotMemberControllerDocs.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; import redot.redot_server.domain.redot.member.dto.RedotMemberUpdateRequest; @@ -26,7 +27,7 @@ ResponseEntity updateRedotMemberInfo(@Parameter(hidden = tr @ApiResponse(responseCode = "200", description = "업로드 성공", content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) ResponseEntity uploadProfileImage(@Parameter(hidden = true) JwtPrincipal jwtPrincipal, - MultipartFile image); + @NotNull MultipartFile image); @Operation(summary = "Redot 회원 탈퇴", description = "회원 계정을 삭제합니다.") @ApiResponse(responseCode = "204", description = "탈퇴 완료") diff --git a/src/main/java/redot/redot_server/domain/site/domain/controller/docs/DomainControllerDocs.java b/src/main/java/redot/redot_server/domain/site/domain/controller/docs/DomainControllerDocs.java index 7852ea6..c396122 100644 --- a/src/main/java/redot/redot_server/domain/site/domain/controller/docs/DomainControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/site/domain/controller/docs/DomainControllerDocs.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import redot.redot_server.domain.site.domain.dto.request.SubdomainLookupRequest; import redot.redot_server.domain.site.domain.dto.response.SubdomainLookupResponse; @@ -15,5 +16,5 @@ public interface DomainControllerDocs { @Operation(summary = "서브도메인 조회", description = "사용 가능한 서브도메인인지 확인합니다.") @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = SubdomainLookupResponse.class))) - ResponseEntity getSubdomain(SubdomainLookupRequest request); + ResponseEntity getSubdomain(@Valid SubdomainLookupRequest request); } From 0be4958f554dfdd3ea28a5f6215cc305bfc7086a Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sat, 13 Dec 2025 01:52:42 +0900 Subject: [PATCH 2/4] feat(controllers): specify multipart/form-data content type for image upload endpoints --- .../redot_server/domain/admin/controller/AdminController.java | 3 ++- .../domain/cms/member/controller/CMSMemberController.java | 3 ++- .../cms/site/setting/controller/SiteSettingController.java | 3 ++- .../domain/redot/member/controller/RedotMemberController.java | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java b/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java index 3749351..8dc08c4 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -91,7 +92,7 @@ public ResponseEntity deleteCurrentAdmin(HttpServletRequest request, @Auth .build(); } - @PostMapping("/upload-profile-image") + @PostMapping(value = "/upload-profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Override public ResponseEntity uploadProfileImage( @AuthenticationPrincipal JwtPrincipal jwtPrincipal, diff --git a/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java b/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java index e466b15..524fcdb 100644 --- a/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java +++ b/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -115,7 +116,7 @@ public ResponseEntity deleteCurrentCMSMember(HttpServletRequest request, .build(); } - @PostMapping("/upload-profile-image") + @PostMapping(value = "/upload-profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Override public ResponseEntity uploadProfileImage( @CurrentRedotApp Long redotAppId, diff --git a/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java index 57d8162..7c59e69 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java +++ b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -38,7 +39,7 @@ public ResponseEntity updateSiteSetting( return ResponseEntity.ok(siteSettingResponse); } - @PostMapping("/upload-logo") + @PostMapping(value = "/upload-logo", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Override public ResponseEntity uploadLogoImage( @CurrentRedotApp Long redotAppId, diff --git a/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java b/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java index 26b07c5..ea9a52c 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java +++ b/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -38,7 +39,7 @@ public ResponseEntity updateRedotMemberInfo( } - @PostMapping("/upload-profile-image") + @PostMapping(value = "/upload-profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Override public ResponseEntity uploadProfileImage( @AuthenticationPrincipal JwtPrincipal jwtPrincipal, From 899b03cae0047240e029198f5f1fd95212d9d4c9 Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 11:13:38 +0900 Subject: [PATCH 3/4] feat(s3): add ImageUrlResolver for converting between CDN and S3 paths --- .../global/s3/util/ImageUrlResolver.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/main/java/redot/redot_server/global/s3/util/ImageUrlResolver.java diff --git a/src/main/java/redot/redot_server/global/s3/util/ImageUrlResolver.java b/src/main/java/redot/redot_server/global/s3/util/ImageUrlResolver.java new file mode 100644 index 0000000..59fbc0e --- /dev/null +++ b/src/main/java/redot/redot_server/global/s3/util/ImageUrlResolver.java @@ -0,0 +1,91 @@ +package redot.redot_server.global.s3.util; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * CDN 주소와 S3 경로 간 변환을 담당한다. + */ +@Component +public class ImageUrlResolver { + + private final String cdnBaseUrl; + private final boolean hasCdnBaseUrl; + + public ImageUrlResolver(@Value("${image.cdn-base-url:}") String cdnBaseUrl) { + if (StringUtils.hasText(cdnBaseUrl)) { + String trimmed = cdnBaseUrl.trim(); + this.cdnBaseUrl = trimmed.endsWith("/") ? trimmed.substring(0, trimmed.length() - 1) : trimmed; + this.hasCdnBaseUrl = true; + } else { + this.cdnBaseUrl = ""; + this.hasCdnBaseUrl = false; + } + } + + /** + * 저장된 경로(또는 외부 URL)를 CDN이 붙은 공개 URL로 변환한다. + */ + public String toPublicUrl(String pathOrUrl) { + if (!StringUtils.hasText(pathOrUrl)) { + return null; + } + + String value = pathOrUrl.trim(); + if (isExternalUrl(value) || !hasCdnBaseUrl) { + return normalizePath(value); + } + String normalizedPath = normalizePath(value); + if (isExternalUrl(normalizedPath)) { + return normalizedPath; + } + return cdnBaseUrl + normalizedPath; + } + + /** + * CDN URL 혹은 경로를 S3에 저장되는 내부 경로(/app/1/logo.png 형태)로 정규화한다. + */ + public String toStoredPath(String pathOrUrl) { + if (!StringUtils.hasText(pathOrUrl)) { + return null; + } + String value = stripCdnBase(pathOrUrl.trim()); + if (isExternalUrl(value)) { + return value; + } + return normalizePath(value); + } + + /** + * S3 API에서 사용하는 key(app/1/logo.png)로 변환한다. + */ + public String toS3Key(String pathOrUrl) { + String storedPath = toStoredPath(pathOrUrl); + if (!StringUtils.hasText(storedPath) || isExternalUrl(storedPath)) { + return null; + } + return storedPath.startsWith("/") ? storedPath.substring(1) : storedPath; + } + + private String stripCdnBase(String value) { + if (hasCdnBaseUrl && value.startsWith(cdnBaseUrl)) { + return value.substring(cdnBaseUrl.length()); + } + return value; + } + + private boolean isExternalUrl(String value) { + return value.startsWith("http://") || value.startsWith("https://"); + } + + private String normalizePath(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + if (isExternalUrl(value)) { + return value; + } + return value.startsWith("/") ? value : "/" + value; + } +} From d61d3993c622bafcd3ceb3ffc6e847d90d50947a Mon Sep 17 00:00:00 2001 From: Dohun Kim Date: Sun, 14 Dec 2025 11:13:44 +0900 Subject: [PATCH 4/4] feat(s3): integrate ImageUrlResolver for profile image URL handling in admin and CMS member responses --- .../admin/dto/response/AdminResponse.java | 5 +++-- .../domain/admin/service/AdminService.java | 22 +++++++++++-------- .../domain/auth/service/AdminAuthService.java | 5 +++-- .../domain/auth/service/CMSAuthService.java | 12 +++------- .../auth/service/RedotMemberAuthService.java | 6 +++-- .../dto/response/CMSMemberResponse.java | 6 +++-- .../cms/member/service/CMSMemberService.java | 22 +++++++++++-------- .../dto/response/SiteSettingResponse.java | 6 +++-- .../setting/service/SiteSettingService.java | 16 +++++++++----- .../app/service/RedotAppCreationService.java | 6 +++-- .../redot/app/service/RedotAppService.java | 10 +++++---- .../dto/response/RedotMemberResponse.java | 9 ++++---- .../member/service/RedotMemberService.java | 14 +++++++----- .../s3/service/ImageStorageService.java | 10 ++++++--- .../global/s3/util/S3Manager.java | 13 +++++++---- 15 files changed, 97 insertions(+), 65 deletions(-) diff --git a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminResponse.java b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminResponse.java index 181d0b1..45cf64a 100644 --- a/src/main/java/redot/redot_server/domain/admin/dto/response/AdminResponse.java +++ b/src/main/java/redot/redot_server/domain/admin/dto/response/AdminResponse.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import redot.redot_server.domain.admin.entity.Admin; +import redot.redot_server.global.s3.util.ImageUrlResolver; public record AdminResponse( Long id, @@ -10,11 +11,11 @@ public record AdminResponse( String email, LocalDateTime createdAt ) { - public static AdminResponse from(Admin admin) { + public static AdminResponse from(Admin admin, ImageUrlResolver imageUrlResolver) { return new AdminResponse( admin.getId(), admin.getName(), - admin.getProfileImageUrl(), + imageUrlResolver.toPublicUrl(admin.getProfileImageUrl()), admin.getEmail(), admin.getCreatedAt() ); diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminService.java index be3dfda..6c7a94f 100644 --- a/src/main/java/redot/redot_server/domain/admin/service/AdminService.java +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminService.java @@ -21,6 +21,7 @@ import redot.redot_server.global.s3.event.ImageDeletionEvent; import redot.redot_server.global.s3.service.ImageStorageService; import redot.redot_server.global.s3.util.ImageDirectory; +import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.util.EmailUtils; import redot.redot_server.global.util.dto.response.PageResponse; @@ -32,6 +33,7 @@ public class AdminService { private final PasswordEncoder passwordEncoder; private final ImageStorageService imageStorageService; private final ApplicationEventPublisher eventPublisher; + private final ImageUrlResolver imageUrlResolver; @Transactional public AdminResponse createAdmin(AdminCreateRequest request) { @@ -42,14 +44,15 @@ public AdminResponse createAdmin(AdminCreateRequest request) { } try { + String profileImageUrl = imageUrlResolver.toStoredPath(request.profileImageUrl()); Admin admin = adminRepository.save( Admin.create( request.name(), normalizedEmail, - request.profileImageUrl(), + profileImageUrl, passwordEncoder.encode(request.password()) )); - return AdminResponse.from(admin); + return AdminResponse.from(admin, imageUrlResolver); } catch (DataIntegrityViolationException ex) { throw new AuthException(AuthErrorCode.EMAIL_ALREADY_EXISTS, ex); } @@ -59,12 +62,12 @@ public AdminResponse getAdminInfo(Long adminId) { Admin admin = adminRepository.findById(adminId) .orElseThrow(() -> new AuthException(AuthErrorCode.ADMIN_NOT_FOUND)); - return AdminResponse.from(admin); + return AdminResponse.from(admin, imageUrlResolver); } public PageResponse getAdminInfoList(Pageable pageable) { Page admins = adminRepository.findAll(pageable); - return PageResponse.from(admins.map(AdminResponse::from)); + return PageResponse.from(admins.map(admin -> AdminResponse.from(admin, imageUrlResolver))); } @Transactional @@ -78,19 +81,20 @@ public AdminResponse updateAdmin(Long adminId, AdminUpdateRequest request) { throw new AuthException(AuthErrorCode.EMAIL_ALREADY_EXISTS); } - deleteOldProfileImageUrlIfChanged(request, admin); + String newProfileImageUrl = imageUrlResolver.toStoredPath(request.profileImageUrl()); + deleteOldProfileImageUrlIfChanged(newProfileImageUrl, admin); - admin.update(request.name(), normalizedEmail, request.profileImageUrl()); + admin.update(request.name(), normalizedEmail, newProfileImageUrl); - return AdminResponse.from(admin); + return AdminResponse.from(admin, imageUrlResolver); } catch (DataIntegrityViolationException ex) { throw new AuthException(AuthErrorCode.EMAIL_ALREADY_EXISTS, ex); } } - private void deleteOldProfileImageUrlIfChanged(AdminUpdateRequest request, Admin admin) { + private void deleteOldProfileImageUrlIfChanged(String newProfileImageUrl, Admin admin) { String oldProfileImageUrl = admin.getProfileImageUrl(); - if (oldProfileImageUrl != null && !oldProfileImageUrl.equals(request.profileImageUrl())) { + if (oldProfileImageUrl != null && !oldProfileImageUrl.equals(newProfileImageUrl)) { eventPublisher.publishEvent(new ImageDeletionEvent(oldProfileImageUrl)); } } diff --git a/src/main/java/redot/redot_server/domain/auth/service/AdminAuthService.java b/src/main/java/redot/redot_server/domain/auth/service/AdminAuthService.java index 578cb10..dd64048 100644 --- a/src/main/java/redot/redot_server/domain/auth/service/AdminAuthService.java +++ b/src/main/java/redot/redot_server/domain/auth/service/AdminAuthService.java @@ -14,6 +14,7 @@ import redot.redot_server.domain.admin.dto.response.AdminResponse; import redot.redot_server.domain.admin.entity.Admin; import redot.redot_server.domain.admin.repository.AdminRepository; +import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.jwt.token.TokenContext; import redot.redot_server.global.jwt.token.TokenType; import redot.redot_server.global.security.filter.jwt.refresh.RefreshTokenPayload; @@ -29,6 +30,7 @@ public class AdminAuthService { private final AuthTokenService authTokenService; private final PasswordEncoder passwordEncoder; private final EmailVerificationService emailVerificationService; + private final ImageUrlResolver imageUrlResolver; public AuthResult signIn(HttpServletRequest request, SignInRequest signInRequest) { Admin admin = adminRepository.findByEmailIgnoreCase(EmailUtils.normalize(signInRequest.email())) @@ -68,8 +70,7 @@ public AdminResponse getCurrentAdminInfo(Long id) { Admin admin = adminRepository.findById(id) .orElseThrow(() -> new AuthException(AuthErrorCode.ADMIN_NOT_FOUND)); - return new AdminResponse(admin.getId(), admin.getName(), admin.getProfileImageUrl(), admin.getEmail(), - admin.getCreatedAt()); + return AdminResponse.from(admin, imageUrlResolver); } @Transactional diff --git a/src/main/java/redot/redot_server/domain/auth/service/CMSAuthService.java b/src/main/java/redot/redot_server/domain/auth/service/CMSAuthService.java index e3cb402..fe32d55 100644 --- a/src/main/java/redot/redot_server/domain/auth/service/CMSAuthService.java +++ b/src/main/java/redot/redot_server/domain/auth/service/CMSAuthService.java @@ -20,6 +20,7 @@ import redot.redot_server.global.security.filter.jwt.refresh.RefreshTokenPayload; import redot.redot_server.global.security.filter.jwt.refresh.RefreshTokenPayloadHolder; import redot.redot_server.global.util.EmailUtils; +import redot.redot_server.global.s3.util.ImageUrlResolver; @Service @@ -31,6 +32,7 @@ public class CMSAuthService { private final AuthTokenService authTokenService; private final PasswordEncoder passwordEncoder; private final EmailVerificationService emailVerificationService; + private final ImageUrlResolver imageUrlResolver; public AuthResult signIn(HttpServletRequest request, SignInRequest signInRequest, Long redotAppId) { CMSMember cmsMember = cmsMemberRepository @@ -76,15 +78,7 @@ public CMSMemberResponse getCurrentCMSMemberInfo(Long redotAppId, Long id) { CMSMember cmsMember = cmsMemberRepository.findByIdAndRedotApp_Id(id, redotAppId) .orElseThrow(() -> new AuthException(AuthErrorCode.CMS_MEMBER_NOT_FOUND)); - return new CMSMemberResponse( - redotAppId, - cmsMember.getId(), - cmsMember.getName(), - cmsMember.getEmail(), - cmsMember.getProfileImageUrl(), - cmsMember.getRole(), - cmsMember.getCreatedAt() - ); + return CMSMemberResponse.fromEntity(redotAppId, cmsMember, imageUrlResolver); } @Transactional diff --git a/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java b/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java index fc4c953..07e679d 100644 --- a/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java +++ b/src/main/java/redot/redot_server/domain/auth/service/RedotMemberAuthService.java @@ -15,6 +15,7 @@ import redot.redot_server.domain.redot.member.dto.response.RedotMemberResponse; import redot.redot_server.domain.redot.member.entity.RedotMember; import redot.redot_server.domain.redot.member.repository.RedotMemberRepository; +import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.jwt.token.TokenContext; import redot.redot_server.global.jwt.token.TokenType; import redot.redot_server.global.security.filter.jwt.refresh.RefreshTokenPayload; @@ -30,6 +31,7 @@ public class RedotMemberAuthService { private final PasswordEncoder passwordEncoder; private final AuthTokenService authTokenService; private final EmailVerificationService emailVerificationService; + private final ImageUrlResolver imageUrlResolver; @Transactional public RedotMemberResponse signUp(RedotMemberCreateRequest request) { @@ -53,7 +55,7 @@ public RedotMemberResponse signUp(RedotMemberCreateRequest request) { ); RedotMember savedMember = redotMemberRepository.save(redotMember); - return RedotMemberResponse.fromEntity(savedMember); + return RedotMemberResponse.fromEntity(savedMember, imageUrlResolver); } public AuthResult signIn(HttpServletRequest request, RedotMemberSignInRequest signInRequest) { @@ -107,7 +109,7 @@ public RedotMemberResponse getCurrentMember(Long memberId) { RedotMember member = redotMemberRepository.findById(memberId) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); - return RedotMemberResponse.fromEntity(member); + return RedotMemberResponse.fromEntity(member, imageUrlResolver); } @Transactional diff --git a/src/main/java/redot/redot_server/domain/cms/member/dto/response/CMSMemberResponse.java b/src/main/java/redot/redot_server/domain/cms/member/dto/response/CMSMemberResponse.java index 6a2bed6..e9a4ed9 100644 --- a/src/main/java/redot/redot_server/domain/cms/member/dto/response/CMSMemberResponse.java +++ b/src/main/java/redot/redot_server/domain/cms/member/dto/response/CMSMemberResponse.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import redot.redot_server.domain.cms.member.entity.CMSMember; import redot.redot_server.domain.cms.member.entity.CMSMemberRole; +import redot.redot_server.global.s3.util.ImageUrlResolver; public record CMSMemberResponse( Long redotAppId, @@ -13,13 +14,14 @@ public record CMSMemberResponse( CMSMemberRole role, LocalDateTime createdAt ) { - public static CMSMemberResponse fromEntity(Long redotAppId, CMSMember cmsMember) { + public static CMSMemberResponse fromEntity(Long redotAppId, CMSMember cmsMember, + ImageUrlResolver imageUrlResolver) { return new CMSMemberResponse( redotAppId, cmsMember.getId(), cmsMember.getName(), cmsMember.getEmail(), - cmsMember.getProfileImageUrl(), + imageUrlResolver.toPublicUrl(cmsMember.getProfileImageUrl()), cmsMember.getRole(), cmsMember.getCreatedAt() ); diff --git a/src/main/java/redot/redot_server/domain/cms/member/service/CMSMemberService.java b/src/main/java/redot/redot_server/domain/cms/member/service/CMSMemberService.java index 3f72097..c1217d3 100644 --- a/src/main/java/redot/redot_server/domain/cms/member/service/CMSMemberService.java +++ b/src/main/java/redot/redot_server/domain/cms/member/service/CMSMemberService.java @@ -25,6 +25,7 @@ import redot.redot_server.global.s3.event.ImageDeletionEvent; import redot.redot_server.global.s3.service.ImageStorageService; import redot.redot_server.global.s3.util.ImageDirectory; +import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.util.dto.response.PageResponse; @@ -38,6 +39,7 @@ public class CMSMemberService { private final PasswordEncoder passwordEncoder; private final ImageStorageService imageStorageService; private final ApplicationEventPublisher eventPublisher; + private final ImageUrlResolver imageUrlResolver; @Transactional public CMSMemberResponse createCMSMember(Long redotAppId, CMSMemberCreateRequest request) { @@ -48,14 +50,14 @@ public CMSMemberResponse createCMSMember(Long redotAppId, CMSMemberCreateRequest CMSMember.join(redotApp, request.name(), request.email(), passwordEncoder.encode(request.password()), request.role())); - return CMSMemberResponse.fromEntity(redotAppId, cmsMember); + return CMSMemberResponse.fromEntity(redotAppId, cmsMember, imageUrlResolver); } public CMSMemberResponse getCMSMemberInfo(Long redotAppId, Long memberId) { CMSMember cmsMember = cmsMemberRepository.findByIdAndRedotApp_Id(memberId, redotAppId) .orElseThrow(() -> new CMSMemberException(CMSMemberErrorCode.CMS_MEMBER_NOT_FOUND)); - return CMSMemberResponse.fromEntity(redotAppId, cmsMember); + return CMSMemberResponse.fromEntity(redotAppId, cmsMember, imageUrlResolver); } @Transactional @@ -65,7 +67,7 @@ public CMSMemberResponse changeCMSMemberRole(Long redotAppId, Long memberId, CMS cmsMember.changeRole(request.role()); - return CMSMemberResponse.fromEntity(redotAppId, cmsMember); + return CMSMemberResponse.fromEntity(redotAppId, cmsMember, imageUrlResolver); } @Transactional @@ -73,16 +75,18 @@ public CMSMemberResponse updateCMSMember(Long redotAppId, Long memberId, CMSMemb CMSMember cmsMember = cmsMemberRepository.findByIdAndRedotApp_Id(memberId, redotAppId) .orElseThrow(() -> new CMSMemberException(CMSMemberErrorCode.CMS_MEMBER_NOT_FOUND)); - deleteOldProfileImageUrlIfChanged(request, cmsMember); + String newProfileImageUrl = imageUrlResolver.toStoredPath(request.profileImageUrl()); - cmsMember.updateProfile(request.name(), request.profileImageUrl()); + deleteOldProfileImageUrlIfChanged(newProfileImageUrl, cmsMember); - return CMSMemberResponse.fromEntity(redotAppId, cmsMember); + cmsMember.updateProfile(request.name(), newProfileImageUrl); + + return CMSMemberResponse.fromEntity(redotAppId, cmsMember, imageUrlResolver); } - private void deleteOldProfileImageUrlIfChanged(CMSMemberUpdateRequest request, CMSMember cmsMember) { + private void deleteOldProfileImageUrlIfChanged(String newProfileImageUrl, CMSMember cmsMember) { String oldProfileImageUrl = cmsMember.getProfileImageUrl(); - if (oldProfileImageUrl != null && !oldProfileImageUrl.equals(request.profileImageUrl())) { + if (oldProfileImageUrl != null && !oldProfileImageUrl.equals(newProfileImageUrl)) { eventPublisher.publishEvent(new ImageDeletionEvent(oldProfileImageUrl)); } } @@ -100,7 +104,7 @@ public PageResponse getCMSMemberListBySearchCondition(Long re Pageable pageable) { Page page = cmsMemberRepository .findAllBySearchCondition(redotAppId, searchCondition, pageable) - .map(cmsMember -> CMSMemberResponse.fromEntity(redotAppId, cmsMember)); + .map(cmsMember -> CMSMemberResponse.fromEntity(redotAppId, cmsMember, imageUrlResolver)); return PageResponse.from(page); } diff --git a/src/main/java/redot/redot_server/domain/cms/site/setting/dto/response/SiteSettingResponse.java b/src/main/java/redot/redot_server/domain/cms/site/setting/dto/response/SiteSettingResponse.java index d858248..e60f0de 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/setting/dto/response/SiteSettingResponse.java +++ b/src/main/java/redot/redot_server/domain/cms/site/setting/dto/response/SiteSettingResponse.java @@ -2,6 +2,7 @@ import redot.redot_server.domain.site.domain.entity.Domain; import redot.redot_server.domain.site.setting.entity.SiteSetting; +import redot.redot_server.global.s3.util.ImageUrlResolver; public record SiteSettingResponse( String logoUrl, @@ -10,9 +11,10 @@ public record SiteSettingResponse( String customDomain, String gaInfo ) { - public static SiteSettingResponse fromEntity(SiteSetting siteSetting, Domain domain) { + public static SiteSettingResponse fromEntity(SiteSetting siteSetting, Domain domain, + ImageUrlResolver imageUrlResolver) { return new SiteSettingResponse( - siteSetting.getLogoUrl(), + imageUrlResolver.toPublicUrl(siteSetting.getLogoUrl()), siteSetting.getSiteName(), domain.getSubdomain(), domain.getCustomDomain(), diff --git a/src/main/java/redot/redot_server/domain/cms/site/setting/service/SiteSettingService.java b/src/main/java/redot/redot_server/domain/cms/site/setting/service/SiteSettingService.java index ea9f372..b258032 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/setting/service/SiteSettingService.java +++ b/src/main/java/redot/redot_server/domain/cms/site/setting/service/SiteSettingService.java @@ -21,6 +21,7 @@ import redot.redot_server.global.s3.event.ImageDeletionEvent; import redot.redot_server.global.s3.service.ImageStorageService; import redot.redot_server.global.s3.util.ImageDirectory; +import redot.redot_server.global.s3.util.ImageUrlResolver; @Service @RequiredArgsConstructor @@ -31,6 +32,7 @@ public class SiteSettingService { private final DomainRepository domainRepository; private final ImageStorageService imageStorageService; private final ApplicationEventPublisher eventPublisher; + private final ImageUrlResolver imageUrlResolver; @Transactional public SiteSettingResponse updateSiteSetting(Long redotAppId, SiteSettingUpdateRequest request) { @@ -43,20 +45,22 @@ public SiteSettingResponse updateSiteSetting(Long redotAppId, SiteSettingUpdateR throw new DomainException(DomainErrorCode.CUSTOM_DOMAIN_ALREADY_EXISTS); } - deleteOldLogoUrlIfChanged(request, siteSetting); + String newLogoUrl = imageUrlResolver.toStoredPath(request.logoUrl()); + + deleteOldLogoUrlIfChanged(newLogoUrl, siteSetting); siteSetting.updateSiteName(request.siteName()); - siteSetting.updateLogoUrl(request.logoUrl()); + siteSetting.updateLogoUrl(newLogoUrl); siteSetting.updateGaInfo(request.gaInfo()); domain.updateCustomDomain(request.customDomain()); - return SiteSettingResponse.fromEntity(siteSetting, domain); + return SiteSettingResponse.fromEntity(siteSetting, domain, imageUrlResolver); } - private void deleteOldLogoUrlIfChanged(SiteSettingUpdateRequest request, SiteSetting siteSetting) { + private void deleteOldLogoUrlIfChanged(String newLogoUrl, SiteSetting siteSetting) { String oldLogoUrl = siteSetting.getLogoUrl(); - if (oldLogoUrl != null && !oldLogoUrl.equals(request.logoUrl())) { + if (oldLogoUrl != null && !oldLogoUrl.equals(newLogoUrl)) { eventPublisher.publishEvent(new ImageDeletionEvent(oldLogoUrl)); } } @@ -93,6 +97,6 @@ public SiteSettingResponse getSiteSetting(Long redotAppId) { Domain domain = domainRepository.findByRedotAppId(redotAppId) .orElseThrow(() -> new DomainException(DomainErrorCode.DOMAIN_NOT_FOUND)); - return SiteSettingResponse.fromEntity(siteSetting, domain); + return SiteSettingResponse.fromEntity(siteSetting, domain, imageUrlResolver); } } diff --git a/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppCreationService.java b/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppCreationService.java index ccea3ef..c648ba0 100644 --- a/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppCreationService.java +++ b/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppCreationService.java @@ -26,6 +26,7 @@ import redot.redot_server.domain.site.setting.repository.SiteSettingRepository; import redot.redot_server.domain.site.style.entity.StyleInfo; import redot.redot_server.domain.site.style.repository.StyleInfoRepository; +import redot.redot_server.global.s3.util.ImageUrlResolver; @Service @RequiredArgsConstructor @@ -37,6 +38,7 @@ public class RedotAppCreationService { private final DomainRepository domainRepository; private final SiteSettingRepository siteSettingRepository; private final StyleInfoRepository styleInfoRepository; + private final ImageUrlResolver imageUrlResolver; private static final int DOMAIN_ALLOCATION_RETRY_LIMIT = 5; private Domain createDomainWithRetry(RedotApp redotApp) { @@ -84,9 +86,9 @@ private RedotAppInfoResponse initializeAppAssets(RedotAppCreateRequest request, return new RedotAppInfoResponse( RedotAppResponse.fromEntity(redotApp), - SiteSettingResponse.fromEntity(siteSetting, domain), + SiteSettingResponse.fromEntity(siteSetting, domain, imageUrlResolver), StyleInfoResponse.fromEntity(styleInfo), - RedotMemberResponse.fromNullable(redotApp.getOwner()) + RedotMemberResponse.fromNullable(redotApp.getOwner(), imageUrlResolver) ); } } diff --git a/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppService.java b/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppService.java index e6f3038..d9daa46 100644 --- a/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppService.java +++ b/src/main/java/redot/redot_server/domain/redot/app/service/RedotAppService.java @@ -38,6 +38,7 @@ import redot.redot_server.domain.site.style.exception.StyleInfoErrorCode; import redot.redot_server.domain.site.style.exception.StyleInfoException; import redot.redot_server.domain.site.style.repository.StyleInfoRepository; +import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.util.dto.response.PageResponse; @Service @@ -54,6 +55,7 @@ public class RedotAppService { private final CMSMemberRepository cmsMemberRepository; private final PasswordEncoder passwordEncoder; private final RedotAppCreationService redotAppCreationService; + private final ImageUrlResolver imageUrlResolver; public RedotAppInfoResponse getRedotAppInfo(Long redotAppId) { RedotApp redotApp = redotAppRepository.findById(redotAppId).orElseThrow( @@ -73,9 +75,9 @@ public RedotAppInfoResponse getRedotAppInfo(Long redotAppId) { return new RedotAppInfoResponse( RedotAppResponse.fromEntity(redotApp), - SiteSettingResponse.fromEntity(siteSetting, domain), + SiteSettingResponse.fromEntity(siteSetting, domain, imageUrlResolver), StyleInfoResponse.fromEntity(styleInfo), - RedotMemberResponse.fromNullable(owner) + RedotMemberResponse.fromNullable(owner, imageUrlResolver) ); } @@ -94,9 +96,9 @@ public PageResponse getRedotAppList(Long ownerId, Pageable return new RedotAppInfoResponse( RedotAppResponse.fromEntity(redotApp), - SiteSettingResponse.fromEntity(siteSetting, domain), + SiteSettingResponse.fromEntity(siteSetting, domain, imageUrlResolver), StyleInfoResponse.fromEntity(styleInfo), - RedotMemberResponse.fromNullable(redotApp.getOwner()) + RedotMemberResponse.fromNullable(redotApp.getOwner(), imageUrlResolver) ); }); diff --git a/src/main/java/redot/redot_server/domain/redot/member/dto/response/RedotMemberResponse.java b/src/main/java/redot/redot_server/domain/redot/member/dto/response/RedotMemberResponse.java index befe407..c1d541c 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/dto/response/RedotMemberResponse.java +++ b/src/main/java/redot/redot_server/domain/redot/member/dto/response/RedotMemberResponse.java @@ -2,6 +2,7 @@ import redot.redot_server.domain.redot.member.entity.RedotMember; import redot.redot_server.domain.redot.member.entity.SocialProvider; +import redot.redot_server.global.s3.util.ImageUrlResolver; public record RedotMemberResponse( Long id, @@ -10,17 +11,17 @@ public record RedotMemberResponse( String profileImageUrl, SocialProvider socialProvider ) { - public static RedotMemberResponse fromEntity(RedotMember member) { + public static RedotMemberResponse fromEntity(RedotMember member, ImageUrlResolver imageUrlResolver) { return new RedotMemberResponse( member.getId(), member.getName(), member.getEmail(), - member.getProfileImageUrl(), + imageUrlResolver.toPublicUrl(member.getProfileImageUrl()), member.getSocialProvider() ); } - public static RedotMemberResponse fromNullable(RedotMember member) { - return member == null ? null : fromEntity(member); + public static RedotMemberResponse fromNullable(RedotMember member, ImageUrlResolver imageUrlResolver) { + return member == null ? null : fromEntity(member, imageUrlResolver); } } diff --git a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java index d3c7a1e..e9773fb 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java +++ b/src/main/java/redot/redot_server/domain/redot/member/service/RedotMemberService.java @@ -17,6 +17,7 @@ import redot.redot_server.global.s3.event.ImageDeletionEvent; import redot.redot_server.global.s3.service.ImageStorageService; import redot.redot_server.global.s3.util.ImageDirectory; +import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.security.social.model.SocialProfile; import redot.redot_server.global.util.EmailUtils; @@ -28,6 +29,7 @@ public class RedotMemberService { private final RedotMemberRepository redotMemberRepository; private final ImageStorageService imageStorageService; private final ApplicationEventPublisher eventPublisher; + private final ImageUrlResolver imageUrlResolver; @Transactional public RedotMember findOrCreateSocialMember(SocialProfile profile, SocialProvider provider) { @@ -70,16 +72,18 @@ public RedotMemberResponse updateRedotMemberInfo(Long id, RedotMemberUpdateReque RedotMember redotMember = redotMemberRepository.findById(id) .orElseThrow(() -> new AuthException(AuthErrorCode.REDOT_MEMBER_NOT_FOUND)); - deleteOldProfileImageUrlIfChanged(request, redotMember); + String newProfileImageUrl = imageUrlResolver.toStoredPath(request.profileImageUrl()); - redotMember.updateInfo(request.name(), request.profileImageUrl()); + deleteOldProfileImageUrlIfChanged(newProfileImageUrl, redotMember); - return RedotMemberResponse.fromEntity(redotMember); + redotMember.updateInfo(request.name(), newProfileImageUrl); + + return RedotMemberResponse.fromEntity(redotMember, imageUrlResolver); } - private void deleteOldProfileImageUrlIfChanged(RedotMemberUpdateRequest request, RedotMember redotMember) { + private void deleteOldProfileImageUrlIfChanged(String newProfileImageUrl, RedotMember redotMember) { String oldProfileImageUrl = redotMember.getProfileImageUrl(); - if (oldProfileImageUrl != null && !oldProfileImageUrl.equals(request.profileImageUrl())) { + if (oldProfileImageUrl != null && !oldProfileImageUrl.equals(newProfileImageUrl)) { eventPublisher.publishEvent(new ImageDeletionEvent(oldProfileImageUrl)); } } diff --git a/src/main/java/redot/redot_server/global/s3/service/ImageStorageService.java b/src/main/java/redot/redot_server/global/s3/service/ImageStorageService.java index 627cdc2..2a230b6 100644 --- a/src/main/java/redot/redot_server/global/s3/service/ImageStorageService.java +++ b/src/main/java/redot/redot_server/global/s3/service/ImageStorageService.java @@ -11,6 +11,7 @@ import redot.redot_server.global.s3.util.ImageDirectory; import redot.redot_server.global.s3.util.ImageMimeDetector; import redot.redot_server.global.s3.util.ImagePathGenerator; +import redot.redot_server.global.s3.util.ImageUrlResolver; import redot.redot_server.global.s3.util.S3Manager; @Service @@ -20,19 +21,22 @@ public class ImageStorageService { private final S3Manager s3Manager; private final ImageUploadProperties properties; private final ImageMimeDetector imageMimeDetector; + private final ImageUrlResolver imageUrlResolver; public String upload(ImageDirectory directory, Long ownerId, MultipartFile file) { validateFile(file); String path = ImagePathGenerator.generate(directory, ownerId, file.getOriginalFilename()); - return s3Manager.uploadFile(file, path); + String storedPath = s3Manager.uploadFile(file, path); + return imageUrlResolver.toPublicUrl(storedPath); } public void delete(String imageUrl) throws ImageUploadException { - if(!s3Manager.exists(imageUrl)){ + String key = imageUrlResolver.toS3Key(imageUrl); + if (!StringUtils.hasText(key) || !s3Manager.exists(key)) { return; } - s3Manager.deleteFile(imageUrl); + s3Manager.deleteFile(key); } private void validateFile(MultipartFile file) { diff --git a/src/main/java/redot/redot_server/global/s3/util/S3Manager.java b/src/main/java/redot/redot_server/global/s3/util/S3Manager.java index 6afe8f1..9136d02 100644 --- a/src/main/java/redot/redot_server/global/s3/util/S3Manager.java +++ b/src/main/java/redot/redot_server/global/s3/util/S3Manager.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import redot.redot_server.global.s3.exception.S3ErrorCode; import redot.redot_server.global.s3.exception.S3StorageException; @@ -27,6 +28,7 @@ public class S3Manager { @Value("${cloud.aws.s3.bucket}") private String bucketName; + /** * 지정된 경로로 파일 업로드 * @param file 업로드할 파일 @@ -54,10 +56,10 @@ public String uploadFile(MultipartFile file, String targetPath) { * 지정된 경로의 파일 삭제 * @param filePath 삭제할 파일 경로 (ex: /app/1/logo/image.png) */ - public void deleteFile(String filePath) { - if (filePath == null || filePath.isBlank()) return; - - String key = filePath.startsWith("/") ? filePath.substring(1) : filePath; + public void deleteFile(String key) { + if (!StringUtils.hasText(key)) { + return; + } try { s3Client.deleteObject(DeleteObjectRequest.builder() .bucket(bucketName) @@ -81,6 +83,9 @@ public void deleteFile(String filePath) { * @return 존재하면 true, 존재하지 않으면 false */ public boolean exists(String key) { + if (!StringUtils.hasText(key)) { + return false; + } try { s3Client.headObject(HeadObjectRequest.builder() .bucket(bucketName)