Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ public record ModifyUserInfoRequest(
@Schema(description = "블로그 주소", example = "https://blog.example.com")
String blogUrl,

@Schema(description = "프로필 이미지 URL", example = "https://cdn.example.com/images/profile.jpg")
String profileImageUrl,

@Schema(description = "자기소개", example = "안녕하세요, 백엔드 개발자입니다.")
String introduction,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ public class UserGitubAutoPushResponse {
private final String message;
@Schema(description = "현재 상태", example = "true")
private final boolean gitPushStatus;
@Schema(description = "선택된 레포", example = "myRepo")
private final String githubRepoName;
@Schema(description = "선택된 브랜치", example = "dev")
private final String branch;
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,24 @@ public UserInfoResponse getUserInfo(AuthUser authUser) {
}

@Transactional
public UserInfoResponse modifyUserInfo(AuthUser authUser, ModifyUserInfoRequest modifyUserInfoRequest) {
public UserInfoResponse modifyUserInfo(AuthUser authUser, ModifyUserInfoRequest request, MultipartFile image) {
User user = userDomainService.getUserById(authUser.getId());

user.modifyUserInfo(
modifyUserInfoRequest.nickname(),
modifyUserInfoRequest.githubUrl(),
modifyUserInfoRequest.blogUrl(),
modifyUserInfoRequest.profileImageUrl(),
modifyUserInfoRequest.introduction(),
modifyUserInfoRequest.age());
request.nickname(),
request.githubUrl(),
request.blogUrl(),
request.introduction(),
request.age());

if (image != null && !image.isEmpty()) {
if (user.getProfileImageUrl()!=null) {
s3Uploader.delete(user.getProfileImageUrl(), "profile");
}
String profileImageUrl = uploadProfileImage(image);

user.modifyProfileImage(profileImageUrl);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

S3 삭제 시 디렉터리 상수 사용 및 업로드-삭제 순서 개선 제안

  • 하드코딩된 "profile" 대신 S3Directory.PROFILE.getDir()을 사용해 일관성을 유지하세요.
  • 안전한 교체 순서: 새 이미지 업로드 성공 후에 기존 이미지를 삭제하면 실패 시 사용자 이미지가 사라지는 리스크를 줄일 수 있습니다.

다음과 같이 수정 제안 드립니다.

- if (image != null && !image.isEmpty()) {
-   if (user.getProfileImageUrl()!=null) {
-     s3Uploader.delete(user.getProfileImageUrl(), "profile");
-   }
-   String profileImageUrl = uploadProfileImage(image);
-   user.modifyProfileImage(profileImageUrl);
- }
+ if (image != null && !image.isEmpty()) {
+   // 1) 새 이미지 업로드
+   String profileImageUrl = uploadProfileImage(image);
+   String oldImageUrl = user.getProfileImageUrl();
+   // 2) DB 갱신
+   user.modifyProfileImage(profileImageUrl);
+   // 3) 기존 이미지 삭제(실패해도 서비스 흐름 유지)
+   if (oldImageUrl != null) {
+     try {
+       s3Uploader.delete(oldImageUrl, S3Directory.PROFILE.getDir());
+     } catch (Exception e) {
+       log.warn("기존 프로필 이미지 삭제 실패 - url: {}", oldImageUrl, e);
+     }
+   }
+ }

검증이 필요하시면, s3Uploader.delete의 시그니처와 디렉터리 인자를 다음 스크립트로 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# S3Uploader.delete 시그니처 및 사용처 확인
fd -t f -g '*S3Uploader*.java' | while read -r f; do
  echo "=== $f ==="
  rg -n -A2 -B4 -g "$f" -P '\bdelete\s*\('
done

# "profile" 하드코딩 사용 여부 점검
rg -n -C2 '"profile"' --type=java

Length of output: 2630


아래 스크립트로 S3Uploader.delete 메서드 정의(시그니처)와 호출부를 확인해주세요.


🏁 Script executed:

#!/bin/bash
# 1) delete 메서드 시그니처(오버로드 포함) 확인
echo "=== delete 메서드 정의 확인 ==="
rg -nE 'public\s+\S+\s+delete\s*\(' src/main/java/org/ezcode/codetest/infrastructure/s3/S3Uploader.java -C2 || echo ">> delete 메서드 정의를 찾을 수 없습니다."

# 2) delete 호출부 확인
echo
echo "=== delete 호출부 확인 ==="
rg -n -C1 'delete\s*\(' src/main/java/org/ezcode/codetest/infrastructure/s3/S3Uploader.java

Length of output: 536


S3 삭제 리팩토링: 디렉터리 상수 사용 및 안전한 업로드-삭제 순서 적용

  • s3Uploader.delete(String fileUrl, String dirName) 시그니처 확인됨.
  • 하드코딩된 "profile" 대신 S3Directory.PROFILE.getDir() 사용으로 일관성 확보.
  • 새 이미지 업로드→DB 갱신 후 기존 이미지 삭제(try-catch) 순으로 변경해 삭제 실패 시에도 사용자 이미지를 보존.

검토 후 아래와 같이 반영해주세요.

- if (image != null && !image.isEmpty()) {
-   if (user.getProfileImageUrl() != null) {
-     s3Uploader.delete(user.getProfileImageUrl(), "profile");
-   }
-   String profileImageUrl = uploadProfileImage(image);
-   user.modifyProfileImage(profileImageUrl);
- }
+ if (image != null && !image.isEmpty()) {
+   // 1) 새 이미지 업로드
+   String profileImageUrl = uploadProfileImage(image);
+   String oldImageUrl = user.getProfileImageUrl();
+   // 2) DB에 새 URL 저장
+   user.modifyProfileImage(profileImageUrl);
+   // 3) 기존 이미지 삭제(실패해도 서비스 흐름 유지)
+   if (oldImageUrl != null) {
+     try {
+       s3Uploader.delete(oldImageUrl, S3Directory.PROFILE.getDir());
+     } catch (Exception e) {
+       log.warn("기존 프로필 이미지 삭제 실패 - url: {}", oldImageUrl, e);
+     }
+   }
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (image != null && !image.isEmpty()) {
if (user.getProfileImageUrl()!=null) {
s3Uploader.delete(user.getProfileImageUrl(), "profile");
}
String profileImageUrl = uploadProfileImage(image);
user.modifyProfileImage(profileImageUrl);
}
if (image != null && !image.isEmpty()) {
// 1) 새 이미지 업로드
String profileImageUrl = uploadProfileImage(image);
String oldImageUrl = user.getProfileImageUrl();
// 2) DB에 새 URL 저장
user.modifyProfileImage(profileImageUrl);
// 3) 기존 이미지 삭제(실패해도 서비스 흐름 유지)
if (oldImageUrl != null) {
try {
s3Uploader.delete(oldImageUrl, S3Directory.PROFILE.getDir());
} catch (Exception e) {
log.warn("기존 프로필 이미지 삭제 실패 - url: {}", oldImageUrl, e);
}
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java
around lines 86 to 93, replace the current logic to (1) call
uploadProfileImage(image) first and get the new profileImageUrl, (2) update the
user with user.modifyProfileImage(profileImageUrl) so the new image is preserved
even if deletion fails, and (3) if an old user.getProfileImageUrl() exists
attempt to delete it inside a try-catch using s3Uploader.delete(oldUrl,
S3Directory.PROFILE.getDir()) (matching the s3Uploader.delete(String, String)
signature) and log or ignore delete errors so they don’t roll back the update.


return UserInfoResponse.builder()
.username(user.getUsername())
Expand Down Expand Up @@ -142,18 +150,6 @@ public void resetAllUsersTokensWeekly(LocalDateTime startDateTime, LocalDateTime
userDomainService.resetReviewTokensForUsers(UsersByWeek.from(counts, weekLength));
}

@Transactional
public UserProfileImageResponse uploadUserProfileImage(AuthUser authUser, MultipartFile image) {
User user = userDomainService.getUserById(authUser.getId());
if (user.getProfileImageUrl()!=null) {
s3Uploader.delete(user.getProfileImageUrl(), "profile");
}
String profileImageUrl = uploadProfileImage(image);

user.modifyProfileImage(profileImageUrl);
return new UserProfileImageResponse(profileImageUrl);
}

private String uploadProfileImage(MultipartFile image) {
try {
return s3Uploader.upload(image, S3Directory.PROFILE.getDir());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,10 @@ public User(String email, String password, String username, String nickname,
유저 정보 업데이트
- 만약 입력 값이 없다면, 기존 값 유지
*/
public void modifyUserInfo(String nickname, String githubUrl, String blogUrl, String profileImageUrl, String introduction, Integer age){
public void modifyUserInfo(String nickname, String githubUrl, String blogUrl, String introduction, Integer age){
this.nickname = (nickname == null || nickname.isBlank()) ? this.nickname : nickname;
this.githubUrl = (githubUrl == null || githubUrl.isBlank()) ? this.githubUrl : githubUrl;
this.blogUrl = (blogUrl == null || blogUrl.isBlank()) ? this.blogUrl : blogUrl;
this.profileImageUrl = (profileImageUrl == null || profileImageUrl.isBlank()) ? this.profileImageUrl : profileImageUrl;
this.introduction = (introduction == null || introduction.isBlank()) ? this.introduction : introduction;
this.age = (age == null) ? this.age : age;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public UserGitubAutoPushResponse changeAutoPushSetting(AuthUser authUser) {
user.setGitPushStatus(!userGitPushStatus);
log.info("기존 status: {} || 변경 status : {}", userGitPushStatus, user.getGitPushStatus());

return new UserGitubAutoPushResponse("변경되었습니다", user.getGitPushStatus());
return new UserGitubAutoPushResponse("변경되었습니다", user.getGitPushStatus(), userGithubInfo.getRepo(), userGithubInfo.getBranch());
}

public UserGitubAutoPushResponse getAutoPushStatus(AuthUser authUser) {
Expand All @@ -133,6 +133,6 @@ public UserGitubAutoPushResponse getAutoPushStatus(AuthUser authUser) {
throw new UserException(UserExceptionCode.NO_GITHUB_INFO);
}
User user = userGithubInfo.getUser();
return new UserGitubAutoPushResponse("현재 githubAutoPush 설정", user.getGitPushStatus());
return new UserGitubAutoPushResponse("현재 githubAutoPush 설정", user.getGitPushStatus(),userGithubInfo.getRepo(), userGithubInfo.getBranch());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,10 @@ public ResponseEntity<UserInfoResponse> getUserInfo(@AuthenticationPrincipal Aut
@PutMapping("/users")
public ResponseEntity<UserInfoResponse> modifyUserInfo(
@AuthenticationPrincipal AuthUser authUser,
@RequestBody ModifyUserInfoRequest modifyUserInfoRequest
){
return ResponseEntity.status(HttpStatus.OK).body(userService.modifyUserInfo(authUser, modifyUserInfoRequest));
}

//유저 프로필 이미지 등록
@Operation(
summary = "프로필 이미지 등록",
description = "유저의 프로필 이미지를 등록합니다. 기존의 이미지가 있는 경우, 기존 이미지가 삭제되고 새로운 이미지로 교체됩니다.")
@PutMapping("/users/profile")
public ResponseEntity<UserProfileImageResponse> uploadUserProfileImage(
@AuthenticationPrincipal AuthUser authUser,
@RequestPart("request") ModifyUserInfoRequest request,
@RequestPart(value = "image", required = false) MultipartFile image
){
return ResponseEntity.status(HttpStatus.OK).body(userService.uploadUserProfileImage(authUser, image));
return ResponseEntity.status(HttpStatus.OK).body(userService.modifyUserInfo(authUser, request, image));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

컨텐츠 타입 매핑 명시 및 JSON/Multipart 동시 지원 제안

현재 시그니처는 멀티파트만 처리 가능하게 보입니다. 기존 클라이언트가 application/json으로 전송하면 415가 날 수 있습니다. 소비 타입을 명시하고, JSON 전용 오버로드를 추가하면 하위 호환을 유지할 수 있습니다.

멀티파트 메서드에 consumes를 명시:

- @PutMapping("/users")
+ @PutMapping(value = "/users", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
 public ResponseEntity<UserInfoResponse> modifyUserInfo(
   @AuthenticationPrincipal AuthUser authUser,
-  @RequestPart("request") ModifyUserInfoRequest request,
+  @Valid @RequestPart("request") ModifyUserInfoRequest request,
   @RequestPart(value = "image", required = false) MultipartFile image
 ){
   return ResponseEntity.status(HttpStatus.OK).body(userService.modifyUserInfo(authUser, request, image));
 }

JSON 전용 오버로드 추가(동일 경로, 다른 consumes):

@PutMapping(value = "/users", consumes = org.springframework.http.MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserInfoResponse> modifyUserInfoJson(
    @AuthenticationPrincipal AuthUser authUser,
    @Valid @RequestBody ModifyUserInfoRequest request
) {
    return ResponseEntity.status(HttpStatus.OK).body(userService.modifyUserInfo(authUser, request, null));
}

구현 후 기존 프런트가 JSON 또는 멀티파트 모두 동작하는지 확인을 권장합니다.

다음 쿼리로 기존 @RequestBody ModifyUserInfoRequest 사용 흔적이 남아있는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
rg -nP '@RequestBody\s+ModifyUserInfoRequest' --type=java -C2

Length of output: 63


멀티파트 및 JSON 요청 동시 지원을 위한 consumes 명시 및 오버로드 추가 제안

아래와 같이 Multipart 전용 핸들러에 consumes 를 명시하고, JSON 전용 오버로드를 추가해 하위 호환을 유지하세요.
스크립트 검증 결과(rg -nP '@RequestBody\s+ModifyUserInfoRequest' --type=java) JSON 전송 전용 핸들러는 아직 존재하지 않습니다.

  • 파일: src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java
  • 변경 대상 라인: 52–59
- @PutMapping("/users")
+ @PutMapping(
+   value = "/users",
+   consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE
+ )
public ResponseEntity<UserInfoResponse> modifyUserInfo(
    @AuthenticationPrincipal AuthUser authUser,
-   @RequestPart("request") ModifyUserInfoRequest request,
+   @Valid @RequestPart("request") ModifyUserInfoRequest request,
    @RequestPart(value = "image", required = false) MultipartFile image
) {
    return ResponseEntity.status(HttpStatus.OK)
        .body(userService.modifyUserInfo(authUser, request, image));
}
@PutMapping(
  value = "/users",
  consumes = org.springframework.http.MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<UserInfoResponse> modifyUserInfoJson(
    @AuthenticationPrincipal AuthUser authUser,
    @Valid @RequestBody ModifyUserInfoRequest request
) {
    return ResponseEntity.status(HttpStatus.OK)
        .body(userService.modifyUserInfo(authUser, request, null));
}

구현 후 기존 프런트엔드에서 JSON 및 Multipart 요청 모두 정상 처리되는지 꼭 확인해 주세요.

🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java
around lines 52–59, the current single @PutMapping handler accepts multipart but
does not declare consumes and there is no JSON-only overload; modify the
existing multipart handler to declare consumes =
MediaType.MULTIPART_FORM_DATA_VALUE and then add a new JSON-only overloaded
handler annotated with @PutMapping(value = "/users", consumes =
MediaType.APPLICATION_JSON_VALUE) that accepts @Valid @RequestBody
ModifyUserInfoRequest and calls userService.modifyUserInfo(authUser, request,
null); ensure annotations (@AuthenticationPrincipal, @Valid) match and both
endpoints return the same ResponseEntity<UserInfoResponse> so both multipart and
JSON clients work.


@Operation(
Expand Down