diff --git a/src/main/java/com/koliving/api/location/application/dto/LocationResponse.java b/src/main/java/com/koliving/api/location/application/dto/LocationResponse.java index 89f65901..3d6a1884 100644 --- a/src/main/java/com/koliving/api/location/application/dto/LocationResponse.java +++ b/src/main/java/com/koliving/api/location/application/dto/LocationResponse.java @@ -2,11 +2,16 @@ import com.koliving.api.location.domain.Location; import com.koliving.api.location.domain.LocationType; +import com.querydsl.core.annotations.QueryProjection; public record LocationResponse(Long id, Long upperLocationId, String name, String displayName, LocationType locationType) { + @QueryProjection + public LocationResponse { + } + public static LocationResponse valueOf(Location entity) { return new LocationResponse( entity.getId(), diff --git a/src/main/java/com/koliving/api/my/application/dto/UserProfileUpdateRequest.java b/src/main/java/com/koliving/api/my/application/dto/UserProfileUpdateRequest.java new file mode 100644 index 00000000..af7de61c --- /dev/null +++ b/src/main/java/com/koliving/api/my/application/dto/UserProfileUpdateRequest.java @@ -0,0 +1,42 @@ +package com.koliving.api.my.application.dto; + +import com.koliving.api.file.domain.ImageFile; +import com.koliving.api.user.Gender; +import com.koliving.api.user.User; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.lang.Nullable; + +import javax.validation.constraints.NotNull; +import java.time.LocalDate; + +@Schema(description = "작성자 정보") +public record UserProfileUpdateRequest( + + @NotNull + @Schema(description = "이미지 URL 고유 Key") + Long profileId, + + @NotNull + @Schema(description = "성별") + Gender gender, + + @NotNull + @Schema(description = "이름") + String firstName, + + @NotNull + @Schema(description = "성") + String lastName, + + @NotNull + @Schema(description = "생년월일") + LocalDate birthDate, + + @Nullable + @Schema(description = "설명") + String description +) { + public User toUser(ImageFile imageFile) { + return User.of(imageFile, gender, firstName, lastName, birthDate, description); + } +} diff --git a/src/main/java/com/koliving/api/my/ui/MyController.java b/src/main/java/com/koliving/api/my/ui/MyController.java new file mode 100644 index 00000000..1d1ce149 --- /dev/null +++ b/src/main/java/com/koliving/api/my/ui/MyController.java @@ -0,0 +1,73 @@ +package com.koliving.api.my.ui; + +import com.koliving.api.base.ErrorResponse; +import com.koliving.api.my.application.dto.UserProfileUpdateRequest; +import com.koliving.api.user.User; +import com.koliving.api.user.application.UserService; +import com.koliving.api.user.application.dto.UserResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@Tag(name = "MY API", description = "MY API") +@RestController +@RequestMapping("api/v1/my") +@RequiredArgsConstructor +public class MyController { + private final UserService userService; + + @Operation( + summary = "프로필 수정", + description = "프로필을 수정합니다.", + responses = { + @ApiResponse( + responseCode = "204", + description = "프로필 수정 성공" + ), + @ApiResponse( + responseCode = "400", + description = "프로필 수정 실패", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + }) + @PutMapping("/profile") + public ResponseEntity updateProfile(@RequestBody UserProfileUpdateRequest request, @AuthenticationPrincipal User user) { + userService.updateProfile(request, user.getId()); + return ResponseEntity.noContent().build(); + } + + + @Operation( + summary = "프로필 조회", + description = "프로필 정보를 조회합니다", + responses = { + @ApiResponse( + responseCode = "200", + description = "프로필 조회 성공", + content = @Content(schema = @Schema(implementation = UserResponse.class)) + ), + @ApiResponse( + responseCode = "400", + description = "프로필 조회 실패", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + }) + @GetMapping + public ResponseEntity myProfile(@AuthenticationPrincipal User user) { + UserResponse response = userService.findById(user.getId()); + + return ResponseEntity.ok() + .body(response); + } +} diff --git a/src/main/java/com/koliving/api/room/application/RoomService.java b/src/main/java/com/koliving/api/room/application/RoomService.java index 1ec48fe8..bde949b0 100644 --- a/src/main/java/com/koliving/api/room/application/RoomService.java +++ b/src/main/java/com/koliving/api/room/application/RoomService.java @@ -29,7 +29,6 @@ import static com.koliving.api.base.ServiceError.FORBIDDEN; import static com.koliving.api.base.ServiceError.RECORD_NOT_EXIST; -import static com.koliving.api.base.ServiceError.UNAUTHORIZED; /** * author : haedoang date : 2023/08/26 description : @@ -97,7 +96,7 @@ private Location getLocationById(Long locationId) { return location; } - public Page search(Pageable pageable, RoomSearchCondition condition) { + public Page search(Pageable pageable, RoomSearchCondition condition) { return roomRepository.search(pageable, condition); } diff --git a/src/main/java/com/koliving/api/room/application/dto/RoomResponse.java b/src/main/java/com/koliving/api/room/application/dto/RoomResponse.java index b8296d0b..3e792cf4 100644 --- a/src/main/java/com/koliving/api/room/application/dto/RoomResponse.java +++ b/src/main/java/com/koliving/api/room/application/dto/RoomResponse.java @@ -8,11 +8,9 @@ import com.koliving.api.room.domain.Room; import com.koliving.api.room.domain.info.RoomInfo; import io.swagger.v3.oas.annotations.media.Schema; + import java.time.LocalDate; -import java.util.List; import java.util.Set; -import lombok.AllArgsConstructor; -import lombok.Getter; /** * author : haedoang date : 2023/08/26 description : @@ -48,11 +46,10 @@ public record RoomResponse( String description, @Schema(description = "유저 정보") - MockUser mockUser, + WriterResponse user, @Schema(description = "방 이미지 정보") Set images - ) { public static RoomResponse valueOf(Room entity) { @@ -66,21 +63,8 @@ public static RoomResponse valueOf(Room entity) { entity.getFurnishings(), entity.getAvailableDate(), entity.getDescription(), - new MockUser(), + WriterResponse.of(entity.getUser()), entity.getImageFiles() ); } - - @Getter - public static class MockUser { - private final String firstName = "NAMI"; - private final String lastName = "OH"; - private final String profileImage = "https://cdn-icons-png.flaticon.com/512/3135/3135823.png"; - private final int age = 30; - private final Gender gender = Gender.FEMALE; - } - - public enum Gender { - MALE, FEMALE - } } diff --git a/src/main/java/com/koliving/api/room/application/dto/WriterResponse.java b/src/main/java/com/koliving/api/room/application/dto/WriterResponse.java new file mode 100644 index 00000000..f6ff57e8 --- /dev/null +++ b/src/main/java/com/koliving/api/room/application/dto/WriterResponse.java @@ -0,0 +1,33 @@ +package com.koliving.api.room.application.dto; + +import com.koliving.api.user.Gender; +import com.koliving.api.user.User; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.util.Objects; + +@Schema(description = "작성자 정보") +public record WriterResponse( + @Schema(description = "작성자 이름") + String firstName, + @Schema(description = "작성자 성") + String lastName, + + @Schema(description = "작성자 성별") + Gender gender, + @Schema(description = "작성자 생년월일") + LocalDate birthDate, + + @Schema(description = "작성자 소개") + String description, + + @Schema(description = "작성자 프로필 URL") + String imageUrl +) { + + public static WriterResponse of(User entity) { + String imageUrl = Objects.isNull(entity.getImageFile()) ? null : entity.getImageFile().getPath(); + return new WriterResponse(entity.getFirstName(), entity.getLastName(), entity.getGender(), entity.getBirthDate(), entity.getDescription(), imageUrl); + } +} diff --git a/src/main/java/com/koliving/api/room/infra/RoomRepositoryImpl.java b/src/main/java/com/koliving/api/room/infra/RoomRepositoryImpl.java index 91d602ec..e1a9d4d0 100644 --- a/src/main/java/com/koliving/api/room/infra/RoomRepositoryImpl.java +++ b/src/main/java/com/koliving/api/room/infra/RoomRepositoryImpl.java @@ -1,5 +1,6 @@ package com.koliving.api.room.infra; +import com.koliving.api.room.application.dto.RoomResponse; import com.koliving.api.room.application.dto.RoomSearchCondition; import com.koliving.api.room.domain.FurnishingType; import com.koliving.api.room.domain.Room; @@ -15,6 +16,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import static com.koliving.api.room.domain.QRoom.room; @@ -23,7 +25,7 @@ public class RoomRepositoryImpl implements RoomRepositoryQueryDsl { private final JPAQueryFactory queryFactory; @Override - public Page search(Pageable pageable, RoomSearchCondition condition) { + public Page search(Pageable pageable, RoomSearchCondition condition) { List rooms = queryFactory.selectFrom(room) .where( filterByLocationIds(condition.locationIds()), @@ -50,8 +52,9 @@ public Page search(Pageable pageable, RoomSearchCondition condition) { .fetch() .size(); - - return PageableExecutionUtils.getPage(rooms, pageable, () -> count); + return PageableExecutionUtils.getPage(rooms.stream() + .map(RoomResponse::valueOf) + .collect(Collectors.toList()), pageable, () -> count); } private BooleanExpression filterByFurnishings(List furnishingTypes) { diff --git a/src/main/java/com/koliving/api/room/infra/RoomRepositoryQueryDsl.java b/src/main/java/com/koliving/api/room/infra/RoomRepositoryQueryDsl.java index fbe30126..be277bb2 100644 --- a/src/main/java/com/koliving/api/room/infra/RoomRepositoryQueryDsl.java +++ b/src/main/java/com/koliving/api/room/infra/RoomRepositoryQueryDsl.java @@ -1,10 +1,11 @@ package com.koliving.api.room.infra; +import com.koliving.api.room.application.dto.RoomResponse; import com.koliving.api.room.application.dto.RoomSearchCondition; import com.koliving.api.room.domain.Room; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface RoomRepositoryQueryDsl { - Page search(Pageable pageable, RoomSearchCondition condition); + Page search(Pageable pageable, RoomSearchCondition condition); } diff --git a/src/main/java/com/koliving/api/room/ui/RoomController.java b/src/main/java/com/koliving/api/room/ui/RoomController.java index 3c837fcd..20605130 100644 --- a/src/main/java/com/koliving/api/room/ui/RoomController.java +++ b/src/main/java/com/koliving/api/room/ui/RoomController.java @@ -77,7 +77,7 @@ public ResponseEntity save(@RequestBody RoomSaveRequest request, @Authenti ), }) @PostMapping("/search") - public ResponseEntity> search(@ParameterObject @PageableDefault Pageable pageable, @ParameterObject RoomSearchCondition condition) { + public ResponseEntity> search(@ParameterObject @PageableDefault Pageable pageable, @ParameterObject RoomSearchCondition condition) { return ResponseEntity.ok() .body(roomService.search(pageable, condition)); } diff --git a/src/main/java/com/koliving/api/user/User.java b/src/main/java/com/koliving/api/user/User.java index b7700666..f1d83820 100644 --- a/src/main/java/com/koliving/api/user/User.java +++ b/src/main/java/com/koliving/api/user/User.java @@ -1,6 +1,7 @@ package com.koliving.api.user; import com.koliving.api.base.exception.KolivingServiceException; +import com.koliving.api.file.domain.ImageFile; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -8,6 +9,9 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.OneToOne; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; import lombok.AccessLevel; @@ -25,7 +29,6 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; -import javax.swing.*; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Collection; @@ -61,10 +64,12 @@ public class User implements UserDetails { @Column(name = "BIRTH_DATE") private LocalDate birthDate; + @Lob private String description; - @Column - private String imageUrl; + @OneToOne + @JoinColumn(name = "IMAGE_FILE_ID") + private ImageFile imageFile; @Enumerated(EnumType.STRING) @Column(name = "USER_ROLE") @@ -95,16 +100,26 @@ public User(String email) { this.userRole = UserRole.USER; } - private User(String email, String password, UserRole userRole) { + private User(String email, String password, String firstName, String lastName, Gender gender, LocalDate birthDate, String description, ImageFile imageFile, UserRole userRole) { this.email = email; this.password = password; + this.firstName = firstName; + this.lastName = lastName; + this.gender = gender; + this.birthDate = birthDate; + this.description = description; + this.imageFile = imageFile; this.userRole = userRole; } + @Deprecated public static User valueOf(String email, String encodedPassword, UserRole role) { - return new User(email, encodedPassword, role); + return new User(email, encodedPassword, null, null, null, null, null,null, role); } + public static User of(ImageFile imageFile, Gender gender, String firstName, String lastName, LocalDate birthDate, String description) { + return new User(null, null, firstName, lastName, gender, birthDate,description, imageFile, null); + } public void setPassword(String password) { this.password = password; this.signUpStatus = SignUpStatus.PROFILE_INFORMATION_PENDING; @@ -159,4 +174,13 @@ public void checkPassword(PasswordEncoder passwordEncoder, String rawPassword) { throw new KolivingServiceException(UNAUTHORIZED); } } + + public void update(User updatable) { + this.imageFile = updatable.imageFile; + this.firstName = updatable.firstName; + this.lastName = updatable.lastName; + this.gender = updatable.gender; + this.birthDate = updatable.birthDate; + this.description = updatable.description; + } } diff --git a/src/main/java/com/koliving/api/user/application/UserService.java b/src/main/java/com/koliving/api/user/application/UserService.java index 3739c296..94e4bb53 100644 --- a/src/main/java/com/koliving/api/user/application/UserService.java +++ b/src/main/java/com/koliving/api/user/application/UserService.java @@ -1,10 +1,13 @@ package com.koliving.api.user.application; +import com.koliving.api.base.ServiceError; +import com.koliving.api.base.exception.KolivingServiceException; +import com.koliving.api.file.domain.ImageFile; +import com.koliving.api.file.infra.ImageFileRepository; +import com.koliving.api.my.application.dto.UserProfileUpdateRequest; import com.koliving.api.user.User; import com.koliving.api.user.UserRepository; import com.koliving.api.user.application.dto.UserResponse; -import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -13,22 +16,26 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.stream.Collectors; + @Service -@Transactional +@Transactional(readOnly = true) @RequiredArgsConstructor public class UserService implements IUserService, UserDetailsService { private final UserRepository userRepository; + private final ImageFileRepository imageFileRepository; private final PasswordEncoder passwordEncoder; @Override - @Transactional(readOnly = true) public UserDetails loadUserByUsername(String email) { return userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException(String.format("User with email %s not found", email))); } @Override + @Transactional public User save(User user) { return userRepository.save(user); } @@ -51,4 +58,23 @@ public List list() { .map(UserResponse::valueOf) .collect(Collectors.toList()); } + + @Transactional + public void updateProfile(UserProfileUpdateRequest request, Long userId) { + ImageFile imageFile = getImageFile(request); + User updatable = request.toUser(imageFile); + + User user = userRepository.findById(userId).orElseThrow(() -> new KolivingServiceException(ServiceError.RECORD_NOT_EXIST)); + user.update(updatable); + } + + private ImageFile getImageFile(UserProfileUpdateRequest request) { + return imageFileRepository.findById(request.profileId()) + .orElseThrow(() -> new KolivingServiceException(ServiceError.RECORD_NOT_EXIST)); + } + + public UserResponse findById(Long id) { + User user = userRepository.findById(id).orElseThrow(() -> new KolivingServiceException(ServiceError.RECORD_NOT_EXIST)); + return UserResponse.valueOf(user); + } } diff --git a/src/main/java/com/koliving/api/user/application/dto/UserResponse.java b/src/main/java/com/koliving/api/user/application/dto/UserResponse.java index 65c923aa..36c62fba 100644 --- a/src/main/java/com/koliving/api/user/application/dto/UserResponse.java +++ b/src/main/java/com/koliving/api/user/application/dto/UserResponse.java @@ -1,11 +1,29 @@ package com.koliving.api.user.application.dto; +import com.koliving.api.file.domain.ImageFile; +import com.koliving.api.user.Gender; +import com.koliving.api.user.SignUpStatus; import com.koliving.api.user.User; +import com.koliving.api.user.UserRole; -public record UserResponse(Long id, String email, String imageUrl) { +import java.time.LocalDate; + +public record UserResponse(Long id, String email, String firstName, String lastName, Gender gender, LocalDate birthDate, + String description, ImageFile imageFile, UserRole userRole, SignUpStatus signUpStatus) { public static UserResponse valueOf(User entity) { - return new UserResponse(entity.getId(), entity.getEmail(), entity.getImageUrl()); + return new UserResponse( + entity.getId(), + entity.getEmail(), + entity.getFirstName(), + entity.getLastName(), + entity.getGender(), + entity.getBirthDate(), + entity.getDescription(), + entity.getImageFile(), + entity.getUserRole(), + entity.getSignUpStatus() + ); } } diff --git a/src/main/java/com/koliving/api/user/ui/UserManageController.java b/src/main/java/com/koliving/api/user/ui/UserManageController.java index 30f49ac7..9ce94cc6 100644 --- a/src/main/java/com/koliving/api/user/ui/UserManageController.java +++ b/src/main/java/com/koliving/api/user/ui/UserManageController.java @@ -7,14 +7,14 @@ 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 java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -//TODO auth scope -> ROLE_MANAGER +import java.util.List; + @Tag(name = "[관리자] 회원 관리 API", description = "관리자 회원 관리 API") @RestController @RequestMapping("api/v1/management/users") diff --git a/src/test/java/com/koliving/api/user/UserServiceTest.java b/src/test/java/com/koliving/api/user/UserServiceTest.java index 6451fa1d..11a355bd 100644 --- a/src/test/java/com/koliving/api/user/UserServiceTest.java +++ b/src/test/java/com/koliving/api/user/UserServiceTest.java @@ -146,7 +146,7 @@ void list_success() { List expected = userList.stream() .map(UserResponse::valueOf) - .collect(Collectors.toList()); + .toList(); assertTrue(actual.equals(expected)); assertTrue(actual.containsAll(expected));