Repository navigation
[FEAT] 프로필 수정 및 편집 정보 조회에 프로필 이미지 URL 추가 #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,15 @@ public record UpdateProfileRequest( | |
| @Size(max = 69) | ||
| String bio, | ||
|
|
||
| @Schema( | ||
| description = "프로필 이미지 URL. 최대 512자. 이미지 업로드 API(imageDomain=PROFILE)로 발급받은 URL 또는 현재 설정된 값만 허용합니다. null이면 기존 프로필 이미지를 삭제합니다.", | ||
| example = "https://example.com/profile.png", | ||
| requiredMode = Schema.RequiredMode.REQUIRED, | ||
| nullable = true | ||
| ) | ||
| @Size(max = 512) | ||
| String profileImageUrl, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 필드 누락과 명시적 기존 클라이언트가 🤖 Prompt for AI Agents |
||
|
|
||
| @Schema( | ||
| description = """ | ||
| 드래그앤드롭으로 정렬한 전체 선택 태그 ID 목록입니다. | ||
|
|
@@ -67,6 +76,7 @@ public UpdateProfileCommand toCommand() { | |
| gender, | ||
| birthDate, | ||
| bio, | ||
| profileImageUrl, | ||
| orderedTagIds | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package org.sopt.buddys.domain.user.service; | ||
|
|
||
| import java.util.regex.Pattern; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.sopt.buddys.domain.image.entity.ImageDomain; | ||
| import org.sopt.buddys.domain.user.code.UserErrorCode; | ||
| import org.sopt.buddys.global.aws.s3.S3PresignedUrlManager; | ||
| import org.sopt.buddys.global.exception.BaseException; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| /** | ||
| * 프로필 이미지 URL은 다른 사용자 화면에 로드되므로, 프로필 이미지 업로드 경로의 URL만 새로 설정할 수 있다. | ||
| * 소셜 로그인으로 받은 외부 이미지처럼 이미 설정된 값은 그대로 유지할 수 있다. | ||
| */ | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ProfileImageUrlValidator { | ||
|
|
||
| private static final Pattern UPLOADED_FILE_NAME = Pattern.compile("^[A-Za-z0-9-]+\\.[A-Za-z0-9]+$"); | ||
|
|
||
| private final S3PresignedUrlManager s3PresignedUrlManager; | ||
|
|
||
| public void validate(String requestedUrl, String currentUrl) { | ||
| if (requestedUrl == null || requestedUrl.equals(currentUrl)) { | ||
| return; | ||
| } | ||
|
|
||
| String uploadUrlPrefix = s3PresignedUrlManager.buildPublicUrl( | ||
| ImageDomain.PROFILE.getFolder() + "/" | ||
| ); | ||
| if (!requestedUrl.startsWith(uploadUrlPrefix) | ||
| || !UPLOADED_FILE_NAME.matcher(requestedUrl.substring(uploadUrlPrefix.length())).matches()) { | ||
| throw new BaseException(UserErrorCode.INVALID_PROFILE_IMAGE_URL); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package org.sopt.buddys.domain.user.service; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatCode; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
| import static org.mockito.BDDMockito.given; | ||
| import static org.mockito.BDDMockito.then; | ||
| import static org.mockito.Mockito.lenient; | ||
|
|
||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.extension.ExtendWith; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
| import org.mockito.InjectMocks; | ||
| import org.mockito.Mock; | ||
| import org.mockito.junit.jupiter.MockitoExtension; | ||
| import org.sopt.buddys.domain.user.code.UserErrorCode; | ||
| import org.sopt.buddys.global.aws.s3.S3PresignedUrlManager; | ||
| import org.sopt.buddys.global.exception.BaseException; | ||
|
|
||
| @ExtendWith(MockitoExtension.class) | ||
| class ProfileImageUrlValidatorTest { | ||
|
|
||
| private static final String UPLOAD_URL_PREFIX = | ||
| "https://buddys-test-bucket.s3.ap-northeast-2.amazonaws.com/profiles/"; | ||
| private static final String CURRENT_URL = "http://k.kakaocdn.net/profile/current.jpg"; | ||
|
|
||
| @InjectMocks | ||
| private ProfileImageUrlValidator profileImageUrlValidator; | ||
|
|
||
| @Mock | ||
| private S3PresignedUrlManager s3PresignedUrlManager; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| lenient().when(s3PresignedUrlManager.buildPublicUrl("profiles/")).thenReturn(UPLOAD_URL_PREFIX); | ||
| } | ||
|
|
||
| @DisplayName("프로필 이미지 업로드 경로의 URL은 허용한다") | ||
| @Test | ||
| void validate_uploadedProfileImageUrl_passes() { | ||
| // given | ||
| String requestedUrl = UPLOAD_URL_PREFIX + "0b6f2c1e-7a55-4a52-9d0c-3f1f6f2f9f10.png"; | ||
|
|
||
| // when & then | ||
| assertThatCode(() -> profileImageUrlValidator.validate(requestedUrl, CURRENT_URL)) | ||
| .doesNotThrowAnyException(); | ||
| } | ||
|
|
||
| @DisplayName("현재 설정된 프로필 이미지 URL은 외부 도메인이어도 그대로 허용한다") | ||
| @Test | ||
| void validate_sameAsCurrentUrl_passes() { | ||
| // when & then | ||
| assertThatCode(() -> profileImageUrlValidator.validate(CURRENT_URL, CURRENT_URL)) | ||
| .doesNotThrowAnyException(); | ||
| then(s3PresignedUrlManager).shouldHaveNoInteractions(); | ||
| } | ||
|
|
||
| @DisplayName("프로필 이미지를 삭제하는 null은 허용한다") | ||
| @Test | ||
| void validate_null_passes() { | ||
| // when & then | ||
| assertThatCode(() -> profileImageUrlValidator.validate(null, CURRENT_URL)) | ||
| .doesNotThrowAnyException(); | ||
| } | ||
|
|
||
| @DisplayName("업로드 경로가 아닌 URL은 거부한다") | ||
| @ParameterizedTest | ||
| @ValueSource(strings = { | ||
| "https://attacker.example/pixel.png", | ||
| "javascript:alert(1)", | ||
| "", | ||
| " ", | ||
| "http://buddys-test-bucket.s3.ap-northeast-2.amazonaws.com/profiles/a.png", | ||
| "https://buddys-test-bucket.s3.ap-northeast-2.amazonaws.com/posts/a.png", | ||
| "https://buddys-test-bucket.s3.ap-northeast-2.amazonaws.com/profiles/", | ||
| "https://buddys-test-bucket.s3.ap-northeast-2.amazonaws.com/profiles/../posts/a.png", | ||
| "https://buddys-test-bucket.s3.ap-northeast-2.amazonaws.com/profiles/a.png?x=1", | ||
| "https://buddys-test-bucket.s3.ap-northeast-2.amazonaws.com.attacker.example/profiles/a.png" | ||
| }) | ||
| void validate_urlOutsideUploadPath_throws(String requestedUrl) { | ||
| // when & then | ||
| assertThatThrownBy(() -> profileImageUrlValidator.validate(requestedUrl, CURRENT_URL)) | ||
| .isInstanceOf(BaseException.class) | ||
| .satisfies(exception -> assertThat(((BaseException) exception).getErrorCode()) | ||
| .isEqualTo(UserErrorCode.INVALID_PROFILE_IMAGE_URL)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
이미지 URL 예시를 새 검증 규칙에 맞추세요. 현재 URL이 예시와 같지 않으면 검증기는
https://example.com/profile.png를 거부합니다. 업로드 API가 발급하는 형태의 URL을 일관되게 사용하세요.src/main/java/org/sopt/buddys/domain/user/dto/request/OnboardingRequest.java#L56-L56: 필드 예시를 허용되는 업로드 URL 형태로 바꾸세요.src/main/java/org/sopt/buddys/domain/user/dto/request/UpdateProfileRequest.java#L52-L52: 새 이미지 설정 요청의 필드 예시를 바꾸세요.src/main/java/org/sopt/buddys/domain/user/controller/swagger/CompleteOnboardingSwagger.java#L42-L42: 새 검증에서 실패하는 온보딩 요청 예시를 바꾸세요.📍 Affects 3 files
src/main/java/org/sopt/buddys/domain/user/dto/request/OnboardingRequest.java#L56-L56(this comment)src/main/java/org/sopt/buddys/domain/user/dto/request/UpdateProfileRequest.java#L52-L52src/main/java/org/sopt/buddys/domain/user/controller/swagger/CompleteOnboardingSwagger.java#L42-L42🤖 Prompt for AI Agents