diff --git a/.github/workflows/makefile.yaml b/.github/workflows/makefile.yaml index 754a7065..43c597ab 100644 --- a/.github/workflows/makefile.yaml +++ b/.github/workflows/makefile.yaml @@ -40,9 +40,9 @@ jobs: - name: Create .env file run: | - jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]' <<< "$SECRETS_CONTEXT" > .env - env: - SECRETS_CONTEXT: ${{ toJson(secrets) }} + touch ./env + echo "${{ secrets.ENV }}" > ./.env + shell: bash - name: Set up Gradle uses: gradle/actions/setup-gradle@v3 diff --git a/build.gradle b/build.gradle index 97a9fcc7..8cd35c83 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,10 @@ dependencies { // MySQL runtimeOnly 'com.mysql:mysql-connector-j' + + // S3 + implementation platform('software.amazon.awssdk:bom:2.21.1') + implementation 'software.amazon.awssdk:s3' } checkstyle { @@ -70,5 +74,5 @@ test { useJUnitPlatform() dependsOn 'checkstyleMain' dependsOn 'checkstyleTest' - systemProperty 'spring.profiles.active', 'local,h2' + systemProperty 'spring.profiles.active', 'test' } diff --git a/src/main/java/taco/klkl/domain/category/controller/TagController.java b/src/main/java/taco/klkl/domain/category/controller/TagController.java index f747b1cf..075ae221 100644 --- a/src/main/java/taco/klkl/domain/category/controller/TagController.java +++ b/src/main/java/taco/klkl/domain/category/controller/TagController.java @@ -18,7 +18,7 @@ import taco.klkl.domain.category.service.SubcategoryTagService; @Slf4j -@Tag(name = "7. 태그", description = "태그 관련 API") +@Tag(name = "6. 카테고리", description = "카테고리 관련 API") @RestController @RequestMapping("/v1/tags") @RequiredArgsConstructor diff --git a/src/main/java/taco/klkl/domain/image/controller/ImageController.java b/src/main/java/taco/klkl/domain/image/controller/ImageController.java new file mode 100644 index 00000000..ddca6a7e --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/controller/ImageController.java @@ -0,0 +1,70 @@ +package taco.klkl.domain.image.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import taco.klkl.domain.image.dto.request.ImageUploadRequest; +import taco.klkl.domain.image.dto.response.PresignedUrlResponse; +import taco.klkl.domain.image.service.ImageService; + +@Slf4j +@RestController +@Tag(name = "0. 이미지", description = "이미지 관련 API") +@RequiredArgsConstructor +public class ImageController { + + private final ImageService imageService; + + @Operation( + summary = "유저 이미지 업로드 Presigned URL 생성", + description = "유저 이미지 업로드를 위한 Presigned URL를 생성합니다." + ) + @PostMapping("/v1/users/me/upload-url") + public PresignedUrlResponse createUserImageUploadUrl( + @Valid @RequestBody final ImageUploadRequest request + ) { + return imageService.createUserImageUploadUrl(request); + } + + @Operation( + summary = "유저 이미지 업로드 완료 처리", + description = "유저 이미지 업로드를 완료 처리합니다." + ) + @PostMapping("/v1/users/me/upload-complete") + public ResponseEntity uploadCompleteUserImage() { + imageService.uploadCompleteUserImage(); + return ResponseEntity.ok().build(); + } + + @Operation( + summary = "상품 이미지 업로드 Presigned URL 생성", + description = "상품 이미지 업로드를 위한 Presigned URL를 생성합니다." + ) + @PostMapping("/v1/products/{productId}/upload-url") + public PresignedUrlResponse createProductImageUploadUrl( + @PathVariable final Long productId, + @Valid @RequestBody final ImageUploadRequest request + ) { + return imageService.createProductImageUploadUrl(productId, request); + } + + @Operation( + summary = "상품 이미지 업로드 완료 처리", + description = "상품 이미지 업로드를 완료 처리합니다." + ) + @PostMapping("/v1/products/{productId}/upload-complete") + public ResponseEntity uploadCompleteProductImage( + @PathVariable final Long productId + ) { + imageService.uploadCompleteProductImage(productId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/taco/klkl/domain/image/dao/ImageRepository.java b/src/main/java/taco/klkl/domain/image/dao/ImageRepository.java new file mode 100644 index 00000000..22481656 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/dao/ImageRepository.java @@ -0,0 +1,15 @@ +package taco.klkl.domain.image.dao; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import taco.klkl.domain.image.domain.Image; +import taco.klkl.domain.image.domain.ImageType; + +public interface ImageRepository extends JpaRepository { + Optional findByImageTypeAndTargetId(final ImageType imageType, final Long targetId); + + List findAllByImageTypeAndTargetId(final ImageType imageType, final Long targetId); +} diff --git a/src/main/java/taco/klkl/domain/image/domain/FileExtension.java b/src/main/java/taco/klkl/domain/image/domain/FileExtension.java new file mode 100644 index 00000000..15ba7ff9 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/domain/FileExtension.java @@ -0,0 +1,26 @@ +package taco.klkl.domain.image.domain; + +import java.util.Arrays; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import taco.klkl.domain.image.exception.FileExtensionNotFoundException; + +@Getter +@RequiredArgsConstructor +public enum FileExtension { + JPG("jpeg"), + JPEG("jpeg"), + PNG("png"), + WEBP("webp"), + ; + + private final String value; + + public static FileExtension from(final String fileExtension) throws FileExtensionNotFoundException { + return Arrays.stream(FileExtension.values()) + .filter(extension -> extension.getValue().equals(fileExtension)) + .findFirst() + .orElseThrow(FileExtensionNotFoundException::new); + } +} diff --git a/src/main/java/taco/klkl/domain/image/domain/Image.java b/src/main/java/taco/klkl/domain/image/domain/Image.java new file mode 100644 index 00000000..0a0e4338 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/domain/Image.java @@ -0,0 +1,105 @@ +package taco.klkl.domain.image.domain; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity(name = "image") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Image { + + @Id + @Column(name = "image_id", + nullable = false + ) + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Enumerated(EnumType.STRING) + @Column( + name = "image_type", + nullable = false + ) + private ImageType imageType; + + @Column( + name = "target_id", + nullable = false + ) + private Long targetId; + + @Column( + name = "image_key", + nullable = false + ) + private String imageKey; + + @Enumerated(EnumType.STRING) + @Column( + name = "file_extension", + nullable = false + ) + private FileExtension fileExtension; + + @Enumerated(EnumType.STRING) + @Column( + name = "upload_state", + nullable = false + ) + private UploadState uploadState; + + @Column( + name = "created_at", + nullable = false, + updatable = false + ) + private LocalDateTime createdAt; + + private Image( + final ImageType imageType, + final Long targetId, + final String imageKey, + final FileExtension fileExtension + ) { + this.imageType = imageType; + this.targetId = targetId; + this.imageKey = imageKey; + this.fileExtension = fileExtension; + this.uploadState = UploadState.PENDING; + this.createdAt = LocalDateTime.now(); + } + + public static Image of( + final ImageType imageType, + final Long targetId, + final String imageUuid, + final FileExtension fileExtension + ) { + return new Image(imageType, targetId, imageUuid, fileExtension); + } + + public void uploadComplete() { + this.uploadState = UploadState.COMPLETE; + } + + public void markAsOutdated() { + this.uploadState = UploadState.OUTDATED; + } + + public String createFileName() { + return imageType.getValue() + "/" + + targetId + "/" + + imageKey + "." + + fileExtension.getValue(); + } +} diff --git a/src/main/java/taco/klkl/domain/image/domain/ImageType.java b/src/main/java/taco/klkl/domain/image/domain/ImageType.java new file mode 100644 index 00000000..c6144f41 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/domain/ImageType.java @@ -0,0 +1,24 @@ +package taco.klkl.domain.image.domain; + +import java.util.Arrays; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import taco.klkl.domain.image.exception.ImageTypeNotFoundException; + +@Getter +@RequiredArgsConstructor +public enum ImageType { + USER_IMAGE("user_image"), + PRODUCT_IMAGE("product_image"), + ; + + private final String value; + + public static ImageType from(final String value) throws ImageTypeNotFoundException { + return Arrays.stream(ImageType.values()) + .filter(type -> type.getValue().equals(value)) + .findFirst() + .orElseThrow(ImageTypeNotFoundException::new); + } +} diff --git a/src/main/java/taco/klkl/domain/image/domain/UploadState.java b/src/main/java/taco/klkl/domain/image/domain/UploadState.java new file mode 100644 index 00000000..ccc9a1ce --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/domain/UploadState.java @@ -0,0 +1,25 @@ +package taco.klkl.domain.image.domain; + +import java.util.Arrays; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import taco.klkl.domain.image.exception.UploadStateNotFoundException; + +@Getter +@RequiredArgsConstructor +public enum UploadState { + PENDING("대기중"), + COMPLETE("완료"), + OUTDATED("폐기예정"), + ; + + private final String value; + + public static UploadState from(final String value) throws UploadStateNotFoundException { + return Arrays.stream(UploadState.values()) + .filter(state -> state.getValue().equals(value)) + .findFirst() + .orElseThrow(UploadStateNotFoundException::new); + } +} diff --git a/src/main/java/taco/klkl/domain/image/dto/request/ImageUploadRequest.java b/src/main/java/taco/klkl/domain/image/dto/request/ImageUploadRequest.java new file mode 100644 index 00000000..c3fd5c79 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/dto/request/ImageUploadRequest.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.dto.request; + +import jakarta.validation.constraints.NotBlank; +import taco.klkl.global.common.constants.ImageValidationMessages; + +public record ImageUploadRequest( + @NotBlank(message = ImageValidationMessages.FILE_EXTENSION_NOT_BLANK) + String fileExtension +) { +} diff --git a/src/main/java/taco/klkl/domain/image/dto/response/PresignedUrlResponse.java b/src/main/java/taco/klkl/domain/image/dto/response/PresignedUrlResponse.java new file mode 100644 index 00000000..4d883ea7 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/dto/response/PresignedUrlResponse.java @@ -0,0 +1,9 @@ +package taco.klkl.domain.image.dto.response; + +public record PresignedUrlResponse( + String presignedUrl +) { + public static PresignedUrlResponse from(final String presignedUrl) { + return new PresignedUrlResponse(presignedUrl); + } +} diff --git a/src/main/java/taco/klkl/domain/image/exception/FileExtensionNotFoundException.java b/src/main/java/taco/klkl/domain/image/exception/FileExtensionNotFoundException.java new file mode 100644 index 00000000..cec97ec1 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/exception/FileExtensionNotFoundException.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.exception; + +import taco.klkl.global.error.exception.CustomException; +import taco.klkl.global.error.exception.ErrorCode; + +public class FileExtensionNotFoundException extends CustomException { + public FileExtensionNotFoundException() { + super(ErrorCode.FILE_EXTENSION_NOT_FOUND); + } +} diff --git a/src/main/java/taco/klkl/domain/image/exception/ImageNotFoundException.java b/src/main/java/taco/klkl/domain/image/exception/ImageNotFoundException.java new file mode 100644 index 00000000..e82b3f66 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/exception/ImageNotFoundException.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.exception; + +import taco.klkl.global.error.exception.CustomException; +import taco.klkl.global.error.exception.ErrorCode; + +public class ImageNotFoundException extends CustomException { + public ImageNotFoundException() { + super(ErrorCode.IMAGE_NOT_FOUND); + } +} diff --git a/src/main/java/taco/klkl/domain/image/exception/ImageTypeNotFoundException.java b/src/main/java/taco/klkl/domain/image/exception/ImageTypeNotFoundException.java new file mode 100644 index 00000000..a0324d97 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/exception/ImageTypeNotFoundException.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.exception; + +import taco.klkl.global.error.exception.CustomException; +import taco.klkl.global.error.exception.ErrorCode; + +public class ImageTypeNotFoundException extends CustomException { + public ImageTypeNotFoundException() { + super(ErrorCode.IMAGE_TYPE_NOT_FOUND); + } +} diff --git a/src/main/java/taco/klkl/domain/image/exception/ImageUploadNotCompleteException.java b/src/main/java/taco/klkl/domain/image/exception/ImageUploadNotCompleteException.java new file mode 100644 index 00000000..def934c3 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/exception/ImageUploadNotCompleteException.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.exception; + +import taco.klkl.global.error.exception.CustomException; +import taco.klkl.global.error.exception.ErrorCode; + +public class ImageUploadNotCompleteException extends CustomException { + public ImageUploadNotCompleteException() { + super(ErrorCode.IMAGE_UPLOAD_NOT_COMPLETE); + } +} diff --git a/src/main/java/taco/klkl/domain/image/exception/ImageUrlInvalidException.java b/src/main/java/taco/klkl/domain/image/exception/ImageUrlInvalidException.java new file mode 100644 index 00000000..f047b0d2 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/exception/ImageUrlInvalidException.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.exception; + +import taco.klkl.global.error.exception.CustomException; +import taco.klkl.global.error.exception.ErrorCode; + +public class ImageUrlInvalidException extends CustomException { + public ImageUrlInvalidException() { + super(ErrorCode.IMAGE_URL_INVALID); + } +} diff --git a/src/main/java/taco/klkl/domain/image/exception/UploadStateNotFoundException.java b/src/main/java/taco/klkl/domain/image/exception/UploadStateNotFoundException.java new file mode 100644 index 00000000..8f7b9792 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/exception/UploadStateNotFoundException.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.exception; + +import taco.klkl.global.error.exception.CustomException; +import taco.klkl.global.error.exception.ErrorCode; + +public class UploadStateNotFoundException extends CustomException { + public UploadStateNotFoundException() { + super(ErrorCode.UPLOAD_STATE_NOT_FOUND); + } +} diff --git a/src/main/java/taco/klkl/domain/image/service/ImageKeyGenerator.java b/src/main/java/taco/klkl/domain/image/service/ImageKeyGenerator.java new file mode 100644 index 00000000..1432d2a8 --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/service/ImageKeyGenerator.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.image.service; + +import java.util.UUID; + +public final class ImageKeyGenerator { + + public static String generate() { + return UUID.randomUUID().toString(); + } +} diff --git a/src/main/java/taco/klkl/domain/image/service/ImageService.java b/src/main/java/taco/klkl/domain/image/service/ImageService.java new file mode 100644 index 00000000..7acd6ead --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/service/ImageService.java @@ -0,0 +1,22 @@ +package taco.klkl.domain.image.service; + +import org.springframework.stereotype.Service; + +import taco.klkl.domain.image.dto.request.ImageUploadRequest; +import taco.klkl.domain.image.dto.response.PresignedUrlResponse; + +@Service +public interface ImageService { + + PresignedUrlResponse createUserImageUploadUrl(final ImageUploadRequest createRequest); + + void uploadCompleteUserImage(); + + PresignedUrlResponse createProductImageUploadUrl( + final Long productId, + final ImageUploadRequest uploadRequest + ); + + void uploadCompleteProductImage(final Long productId); + +} diff --git a/src/main/java/taco/klkl/domain/image/service/ImageServiceImpl.java b/src/main/java/taco/klkl/domain/image/service/ImageServiceImpl.java new file mode 100644 index 00000000..cb3feb2b --- /dev/null +++ b/src/main/java/taco/klkl/domain/image/service/ImageServiceImpl.java @@ -0,0 +1,162 @@ +package taco.klkl.domain.image.service; + +import java.time.Duration; +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.ObjectCannedACL; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; +import taco.klkl.domain.image.dao.ImageRepository; +import taco.klkl.domain.image.domain.FileExtension; +import taco.klkl.domain.image.domain.Image; +import taco.klkl.domain.image.domain.ImageType; +import taco.klkl.domain.image.domain.UploadState; +import taco.klkl.domain.image.dto.request.ImageUploadRequest; +import taco.klkl.domain.image.dto.response.PresignedUrlResponse; +import taco.klkl.domain.image.exception.ImageNotFoundException; +import taco.klkl.domain.product.domain.Product; +import taco.klkl.domain.user.domain.User; +import taco.klkl.global.util.ProductUtil; +import taco.klkl.global.util.UserUtil; + +@Slf4j +@Primary +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ImageServiceImpl implements ImageService { + + private static final Duration SIGNATURE_DURATION = Duration.ofMinutes(5); + private static final ObjectCannedACL REQUEST_ACL = ObjectCannedACL.PRIVATE; + + private final S3Client s3Client; + private final S3Presigner s3Presigner; + private final ImageRepository imageRepository; + private final UserUtil userUtil; + private final ProductUtil productUtil; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + @Value("${cloud.aws.cloudfront.domain}") + private String cloudFrontDomain; + + @Override + @Transactional + public PresignedUrlResponse createUserImageUploadUrl(final ImageUploadRequest uploadRequest) { + final User currentUser = userUtil.findCurrentUser(); + return createImageUploadUrl(ImageType.USER_IMAGE, currentUser.getId(), uploadRequest.fileExtension()); + } + + @Override + @Transactional + public PresignedUrlResponse createProductImageUploadUrl( + final Long productId, + final ImageUploadRequest uploadRequest + ) { + return createImageUploadUrl(ImageType.PRODUCT_IMAGE, productId, uploadRequest.fileExtension()); + } + + @Override + @Transactional + public void uploadCompleteUserImage() { + final User currentUser = userUtil.findCurrentUser(); + final List images = uploadCompleteImage(ImageType.USER_IMAGE, currentUser.getId()); + + final Image newImage = images.stream() + .filter(image -> image.getUploadState() == UploadState.COMPLETE) + .findFirst() + .orElseThrow(ImageNotFoundException::new); + + final String imageUrl = createImageUrl(newImage); + currentUser.updateProfileImageUrl(imageUrl); + } + + @Override + @Transactional + public void uploadCompleteProductImage(final Long productId) { + final List newImages = uploadCompleteImage(ImageType.PRODUCT_IMAGE, productId); + + final List imageUrls = newImages.stream() + .map(this::createImageUrl) + .toList(); + + Product product = productUtil.findProductEntityById(productId); + product.updateImages(imageUrls); + } + + private PresignedUrlResponse createImageUploadUrl( + final ImageType imageType, + final Long targetId, + final String fileExtensionStr + ) { + final String imageKey = ImageKeyGenerator.generate(); + final FileExtension fileExtension = FileExtension.from(fileExtensionStr); + + final Image image = createAndSaveImageEntity(imageType, targetId, imageKey, fileExtension); + + final PutObjectRequest putObjectRequest + = createPutObjectRequest(image.createFileName(), image.getFileExtension()); + final PutObjectPresignRequest putObjectPresignRequest = createPutObjectPresignRequest(putObjectRequest); + + final PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(putObjectPresignRequest); + final String presignedUrl = presignedRequest.url().toString(); + + return PresignedUrlResponse.from(presignedUrl); + } + + private Image createAndSaveImageEntity( + final ImageType imageType, + final Long targetId, + final String imageKey, + final FileExtension fileExtension + ) { + final Image image = Image.of(imageType, targetId, imageKey, fileExtension); + return imageRepository.save(image); + } + + private List uploadCompleteImage(final ImageType imageType, Long targetId) { + final List images = imageRepository.findAllByImageTypeAndTargetId(imageType, targetId); + + images.stream() + .filter(image -> image.getUploadState() == UploadState.COMPLETE) + .forEach(Image::markAsOutdated); + + final List newImages = images.stream() + .filter(image -> image.getUploadState() == UploadState.PENDING) + .toList(); + newImages.forEach(Image::uploadComplete); + + return newImages; + } + + private PutObjectRequest createPutObjectRequest(final String fileName, final FileExtension fileExtension) { + return PutObjectRequest.builder() + .bucket(bucketName) + .key(fileName) + .contentType("image/" + fileExtension.getValue()) + .acl(REQUEST_ACL) + .build(); + } + + private PutObjectPresignRequest createPutObjectPresignRequest(final PutObjectRequest putObjectRequest) { + return PutObjectPresignRequest.builder() + .signatureDuration(SIGNATURE_DURATION) + .putObjectRequest(putObjectRequest) + .build(); + } + + private String createImageUrl(final Image image) { + return "https://" + cloudFrontDomain + "/" + image.createFileName(); + } +} diff --git a/src/main/java/taco/klkl/domain/oauth/service/OauthKakaoLoginServiceImpl.java b/src/main/java/taco/klkl/domain/oauth/service/OauthKakaoLoginServiceImpl.java index 054f45fe..a46bf50a 100644 --- a/src/main/java/taco/klkl/domain/oauth/service/OauthKakaoLoginServiceImpl.java +++ b/src/main/java/taco/klkl/domain/oauth/service/OauthKakaoLoginServiceImpl.java @@ -58,9 +58,8 @@ private User registerUser(final KakaoUserInfoRequest userInfoRequest) { // TODO: 성별, 나이는 기본값으로 넣고 있습니다. final UserCreateRequest userCreateRequest = UserCreateRequest.of( name, - Gender.MALE.getDescription(), + Gender.MALE.getValue(), 0, - userInfoRequest.profileImage(), "" ); diff --git a/src/main/java/taco/klkl/domain/product/domain/Product.java b/src/main/java/taco/klkl/domain/product/domain/Product.java index 59920545..8d82888a 100644 --- a/src/main/java/taco/klkl/domain/product/domain/Product.java +++ b/src/main/java/taco/klkl/domain/product/domain/Product.java @@ -5,6 +5,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.IntStream; import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; @@ -20,6 +21,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; import jakarta.persistence.PrePersist; import lombok.AccessLevel; import lombok.Getter; @@ -47,6 +49,14 @@ public class Product { @Column(name = "product_id") private Long id; + @OneToMany( + mappedBy = "product", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + @OrderBy("orderIndex ASC") + private List images = new ArrayList<>(); + @Column( name = "name", length = ProductConstants.NAME_MAX_LENGTH, @@ -163,30 +173,6 @@ protected void prePersist() { } } - private Product( - final String name, - final String description, - final String address, - final Integer price, - final Rating rating, - final User user, - final City city, - final Subcategory subcategory, - final Currency currency - ) { - this.name = name; - this.description = description; - this.address = address; - this.price = price; - this.rating = rating; - this.user = user; - this.city = city; - this.subcategory = subcategory; - this.currency = currency; - this.likeCount = DefaultConstants.DEFAULT_INT_VALUE; - this.createdAt = LocalDateTime.now(); - } - public static Product of( final String name, final String description, @@ -252,4 +238,37 @@ public int decreaseLikeCount() throws LikeCountBelowMinimumException { return this.likeCount; } + + public void updateImages(final List imageUrls) { + this.images.clear(); + IntStream.range(0, imageUrls.size()) + .forEach(i -> { + ProductImage newImage = ProductImage.of(this, imageUrls.get(i), i); + this.images.add(newImage); + }); + } + + private Product( + final String name, + final String description, + final String address, + final Integer price, + final Rating rating, + final User user, + final City city, + final Subcategory subcategory, + final Currency currency + ) { + this.name = name; + this.description = description; + this.address = address; + this.price = price; + this.rating = rating; + this.user = user; + this.city = city; + this.subcategory = subcategory; + this.currency = currency; + this.likeCount = DefaultConstants.DEFAULT_INT_VALUE; + this.createdAt = LocalDateTime.now(); + } } diff --git a/src/main/java/taco/klkl/domain/product/domain/ProductImage.java b/src/main/java/taco/klkl/domain/product/domain/ProductImage.java new file mode 100644 index 00000000..0a0e5222 --- /dev/null +++ b/src/main/java/taco/klkl/domain/product/domain/ProductImage.java @@ -0,0 +1,63 @@ +package taco.klkl.domain.product.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity(name = "product_image") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne( + fetch = FetchType.LAZY, + optional = false + ) + @JoinColumn( + name = "product_id", + nullable = false + ) + private Product product; + + @Column( + name = "image_url", + nullable = false + ) + private String imageUrl; + + @Column( + name = "order_index", + nullable = false + ) + private Integer orderIndex; + + public static ProductImage of( + final Product product, + final String imageUrl, + final Integer orderIndex + ) { + return new ProductImage(product, imageUrl, orderIndex); + } + + private ProductImage( + final Product product, + final String imageUrl, + final Integer orderIndex + ) { + this.product = product; + this.imageUrl = imageUrl; + this.orderIndex = orderIndex; + } +} diff --git a/src/main/java/taco/klkl/domain/product/dto/response/ProductDetailResponse.java b/src/main/java/taco/klkl/domain/product/dto/response/ProductDetailResponse.java index d8648e92..a8236ca5 100644 --- a/src/main/java/taco/klkl/domain/product/dto/response/ProductDetailResponse.java +++ b/src/main/java/taco/klkl/domain/product/dto/response/ProductDetailResponse.java @@ -1,6 +1,7 @@ package taco.klkl.domain.product.dto.response; import java.time.LocalDateTime; +import java.util.List; import java.util.Set; import taco.klkl.domain.category.dto.response.SubcategoryResponse; @@ -11,24 +12,9 @@ import taco.klkl.domain.user.dto.response.UserDetailResponse; import taco.klkl.global.util.ProductUtil; -/** - * TODO: 상품필터속성 추가 해야함 (상품필터속성 테이블 개발 후) - * TODO: 상품 컨트롤러에서 필터 서비스를 이용해서 조합 하는게 괜찮아 보입니다. - * @param id - * @param name - * @param description - * @param address - * @param price - * @param likeCount - * @param rating - * @param user - * @param city - * @param subcategory - * @param currency - * @param createdAt - */ public record ProductDetailResponse( Long id, + List images, String name, String description, String address, @@ -42,10 +28,14 @@ public record ProductDetailResponse( Set tags, LocalDateTime createdAt ) { - public static ProductDetailResponse from(final Product product) { + List images = product.getImages().stream() + .map(ProductImageResponse::from) + .toList(); + return new ProductDetailResponse( product.getId(), + images, product.getName(), product.getDescription(), product.getAddress(), diff --git a/src/main/java/taco/klkl/domain/product/dto/response/ProductImageResponse.java b/src/main/java/taco/klkl/domain/product/dto/response/ProductImageResponse.java new file mode 100644 index 00000000..e682af1c --- /dev/null +++ b/src/main/java/taco/klkl/domain/product/dto/response/ProductImageResponse.java @@ -0,0 +1,12 @@ +package taco.klkl.domain.product.dto.response; + +import taco.klkl.domain.product.domain.ProductImage; + +public record ProductImageResponse( + String url, + Integer orderIndex +) { + public static ProductImageResponse from(final ProductImage productImage) { + return new ProductImageResponse(productImage.getImageUrl(), productImage.getOrderIndex()); + } +} diff --git a/src/main/java/taco/klkl/domain/product/dto/response/ProductSimpleResponse.java b/src/main/java/taco/klkl/domain/product/dto/response/ProductSimpleResponse.java index 8cd992e4..4ac17980 100644 --- a/src/main/java/taco/klkl/domain/product/dto/response/ProductSimpleResponse.java +++ b/src/main/java/taco/klkl/domain/product/dto/response/ProductSimpleResponse.java @@ -1,23 +1,15 @@ package taco.klkl.domain.product.dto.response; +import java.util.List; import java.util.Set; import taco.klkl.domain.category.dto.response.TagResponse; import taco.klkl.domain.product.domain.Product; import taco.klkl.global.util.ProductUtil; -/** - * TODO: 상품필터속성 추가 해야함 (상품필터속성 테이블 개발 후) - * TODO: 상품필터속성 추가 해야함 (상품필터속성 테이블 개발 후) - * @param id - * @param name - * @param likeCount - * @param rating - * @param countryName - * @param categoryName - */ public record ProductSimpleResponse( Long id, + List images, String name, Integer likeCount, Double rating, @@ -25,10 +17,14 @@ public record ProductSimpleResponse( String categoryName, Set tags ) { - public static ProductSimpleResponse from(final Product product) { + List images = product.getImages().stream() + .map(ProductImageResponse::from) + .toList(); + return new ProductSimpleResponse( product.getId(), + images, product.getName(), product.getLikeCount(), product.getRating().getValue(), diff --git a/src/main/java/taco/klkl/domain/product/service/ProductServiceImpl.java b/src/main/java/taco/klkl/domain/product/service/ProductServiceImpl.java index cf7c26f2..36dde24f 100644 --- a/src/main/java/taco/klkl/domain/product/service/ProductServiceImpl.java +++ b/src/main/java/taco/klkl/domain/product/service/ProductServiceImpl.java @@ -127,10 +127,7 @@ public ProductDetailResponse updateProduct(final Long id, final ProductCreateUpd final Product product = productRepository.findById(id) .orElseThrow(ProductNotFoundException::new); updateProductEntity(product, updateRequest); - if (updateRequest.tagIds() != null) { - Set updatedTags = createTagsByTagIds(updateRequest.tagIds()); - product.updateTags(updatedTags); - } + updateProductEntityTags(product, updateRequest.tagIds()); return ProductDetailResponse.from(product); } @@ -287,6 +284,13 @@ private void updateProductEntity(final Product product, final ProductCreateUpdat ); } + private void updateProductEntityTags(final Product product, final Set tagIds) { + if (tagIds != null) { + final Set updatedTags = createTagsByTagIds(tagIds); + product.updateTags(updatedTags); + } + } + private Sort.Direction createSortDirectionByQuery(final String query) throws SortDirectionNotFoundException { try { return Sort.Direction.fromString(query); diff --git a/src/main/java/taco/klkl/domain/search/controller/SearchController.java b/src/main/java/taco/klkl/domain/search/controller/SearchController.java index f9115164..de824d9c 100644 --- a/src/main/java/taco/klkl/domain/search/controller/SearchController.java +++ b/src/main/java/taco/klkl/domain/search/controller/SearchController.java @@ -17,7 +17,7 @@ @RestController @RequestMapping("/v1/search") @RequiredArgsConstructor -@Tag(name = "6. 검색", description = "검색 관련 API") +@Tag(name = "8. 검색", description = "검색 관련 API") public class SearchController { private final SearchService searchService; diff --git a/src/main/java/taco/klkl/domain/user/controller/UserController.java b/src/main/java/taco/klkl/domain/user/controller/UserController.java index d61bbcd3..696131d1 100644 --- a/src/main/java/taco/klkl/domain/user/controller/UserController.java +++ b/src/main/java/taco/klkl/domain/user/controller/UserController.java @@ -1,13 +1,17 @@ package taco.klkl.domain.user.controller; -import org.springframework.http.ResponseEntity; 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; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import taco.klkl.domain.user.dto.request.UserUpdateRequest; import taco.klkl.domain.user.dto.response.UserDetailResponse; import taco.klkl.domain.user.service.UserService; @@ -15,17 +19,20 @@ @RestController @Tag(name = "1. 유저", description = "유저 관련 API") @RequestMapping("/v1/users") +@RequiredArgsConstructor public class UserController { - final UserService userService; - public UserController(UserService userService) { - this.userService = userService; - } + private final UserService userService; @Operation(summary = "내 정보 조회", description = "내 정보를 조회합니다. (테스트용)") @GetMapping("/me") - public ResponseEntity getMe() { - UserDetailResponse userDto = userService.getMyInfo(); - return ResponseEntity.ok().body(userDto); + public UserDetailResponse getMe() { + return userService.getCurrentUser(); + } + + @Operation(summary = "내 정보 수정", description = "내 정보를 수정합니다.") + @PutMapping("/me") + public UserDetailResponse updateMe(@Valid @RequestBody UserUpdateRequest request) { + return userService.updateUser(request); } } diff --git a/src/main/java/taco/klkl/domain/user/converter/GenderConverter.java b/src/main/java/taco/klkl/domain/user/converter/GenderConverter.java index 319738c5..c721491e 100644 --- a/src/main/java/taco/klkl/domain/user/converter/GenderConverter.java +++ b/src/main/java/taco/klkl/domain/user/converter/GenderConverter.java @@ -8,20 +8,18 @@ public class GenderConverter implements AttributeConverter { @Override - public String convertToDatabaseColumn(Gender gender) { + public String convertToDatabaseColumn(final Gender gender) { if (gender == null) { return null; } - - return gender.getDescription(); + return gender.getValue(); } @Override - public Gender convertToEntityAttribute(String dbData) { + public Gender convertToEntityAttribute(final String dbData) { if (dbData == null) { return null; } - - return Gender.getGenderByDescription(dbData); + return Gender.from(dbData); } } diff --git a/src/main/java/taco/klkl/domain/user/domain/Gender.java b/src/main/java/taco/klkl/domain/user/domain/Gender.java index 02d86861..cc8d9298 100644 --- a/src/main/java/taco/klkl/domain/user/domain/Gender.java +++ b/src/main/java/taco/klkl/domain/user/domain/Gender.java @@ -4,22 +4,21 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import taco.klkl.domain.user.exception.GenderNotFoundException; @Getter @AllArgsConstructor public enum Gender { MALE("남"), FEMALE("여"), - NONE(""), ; - private final String description; + private final String value; - public static Gender getGenderByDescription(String description) { + public static Gender from(final String value) { return Arrays.stream(Gender.values()) - .filter(g -> g.getDescription().equals(description)) + .filter(gender -> gender.getValue().equals(value)) .findFirst() - .orElse(NONE); + .orElseThrow(GenderNotFoundException::new); } - } diff --git a/src/main/java/taco/klkl/domain/user/domain/User.java b/src/main/java/taco/klkl/domain/user/domain/User.java index 5987828c..62b0abb9 100644 --- a/src/main/java/taco/klkl/domain/user/domain/User.java +++ b/src/main/java/taco/klkl/domain/user/domain/User.java @@ -22,42 +22,88 @@ public class User { @Column(name = "user_id") private Long id; - @Column(length = 500, nullable = false) - private String profile = UserConstants.DEFAULT_PROFILE; + @Column( + name = "profile_image_url", + length = 500, + nullable = false + ) + private String profileImageUrl; - @Column(unique = true, length = 30, nullable = false) + @Column( + name = "name", + unique = true, + length = 30, + nullable = false + ) private String name; - @Column(length = 1, nullable = false) + @Column( + name = "gender", + length = 1, + nullable = false + ) private Gender gender; - @Column(nullable = false) + @Column( + name = "age", + nullable = false + ) private Integer age; - @Column(length = 100) + @Column( + name = "description", + length = 100 + ) private String description; // TODO: created_at 이름으로 json나가야 함 - @Column(name = "created_at", nullable = false, updatable = false) + @Column( + name = "created_at", + nullable = false, + updatable = false + ) private LocalDateTime createdAt; @PrePersist protected void prePersist() { - if (this.profile == null) { - this.profile = UserConstants.DEFAULT_PROFILE; - } this.createdAt = LocalDateTime.now(); } - private User(String profile, String name, Gender gender, Integer age, String description) { - this.profile = profile; + private User( + final String name, + final Gender gender, + final Integer age, + final String description + ) { + this.profileImageUrl = UserConstants.DEFAULT_PROFILE; this.name = name; this.gender = gender; this.age = age; this.description = description; } - public static User of(String profile, String name, Gender gender, Integer age, String description) { - return new User(profile, name, gender, age, description); + public static User of( + final String name, + final Gender gender, + final Integer age, + final String description + ) { + return new User(name, gender, age, description); + } + + public void update( + final String name, + final Gender gender, + final Integer age, + final String description + ) { + this.name = name; + this.gender = gender; + this.age = age; + this.description = description; + } + + public void updateProfileImageUrl(final String profileImageUrl) { + this.profileImageUrl = profileImageUrl; } } diff --git a/src/main/java/taco/klkl/domain/user/dto/request/UserCreateRequest.java b/src/main/java/taco/klkl/domain/user/dto/request/UserCreateRequest.java index cfd0f72f..072467a8 100644 --- a/src/main/java/taco/klkl/domain/user/dto/request/UserCreateRequest.java +++ b/src/main/java/taco/klkl/domain/user/dto/request/UserCreateRequest.java @@ -7,16 +7,14 @@ public record UserCreateRequest( @NotNull(message = "이름은 필수 항목입니다.") String name, @NotNull(message = "성별은 필수 항목입니다.") String gender, @PositiveOrZero(message = "나이는 0 이상이어야 합니다.") Integer age, - String profile, String description ) { public static UserCreateRequest of( final String name, final String gender, final Integer age, - final String profile, final String description ) { - return new UserCreateRequest(name, gender, age, profile, description); + return new UserCreateRequest(name, gender, age, description); } } diff --git a/src/main/java/taco/klkl/domain/user/dto/request/UserUpdateRequest.java b/src/main/java/taco/klkl/domain/user/dto/request/UserUpdateRequest.java new file mode 100644 index 00000000..42bede10 --- /dev/null +++ b/src/main/java/taco/klkl/domain/user/dto/request/UserUpdateRequest.java @@ -0,0 +1,12 @@ +package taco.klkl.domain.user.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; + +public record UserUpdateRequest( + @NotNull(message = "이름은 필수 항목입니다.") String name, + @NotNull(message = "성별은 필수 항목입니다.") String gender, + @PositiveOrZero(message = "나이는 0 이상이어야 합니다.") Integer age, + String description +) { +} diff --git a/src/main/java/taco/klkl/domain/user/dto/response/UserDetailResponse.java b/src/main/java/taco/klkl/domain/user/dto/response/UserDetailResponse.java index 52948eab..925907dc 100644 --- a/src/main/java/taco/klkl/domain/user/dto/response/UserDetailResponse.java +++ b/src/main/java/taco/klkl/domain/user/dto/response/UserDetailResponse.java @@ -4,15 +4,15 @@ public record UserDetailResponse( Long id, - String profile, + String profileImageUrl, String name, String description, int totalLikeCount ) { - public static UserDetailResponse from(User user) { + public static UserDetailResponse from(final User user) { return new UserDetailResponse( user.getId(), - user.getProfile(), + user.getProfileImageUrl(), user.getName(), user.getDescription(), 0 diff --git a/src/main/java/taco/klkl/domain/user/dto/response/UserSimpleResponse.java b/src/main/java/taco/klkl/domain/user/dto/response/UserSimpleResponse.java index 9867174f..66cdb01d 100644 --- a/src/main/java/taco/klkl/domain/user/dto/response/UserSimpleResponse.java +++ b/src/main/java/taco/klkl/domain/user/dto/response/UserSimpleResponse.java @@ -4,13 +4,13 @@ public record UserSimpleResponse( Long id, - String profile, + String profileImageUrl, String name ) { - public static UserSimpleResponse from(User user) { + public static UserSimpleResponse from(final User user) { return new UserSimpleResponse( user.getId(), - user.getProfile(), + user.getProfileImageUrl(), user.getName() ); } diff --git a/src/main/java/taco/klkl/domain/user/exception/GenderNotFoundException.java b/src/main/java/taco/klkl/domain/user/exception/GenderNotFoundException.java new file mode 100644 index 00000000..d3ef7b65 --- /dev/null +++ b/src/main/java/taco/klkl/domain/user/exception/GenderNotFoundException.java @@ -0,0 +1,10 @@ +package taco.klkl.domain.user.exception; + +import taco.klkl.global.error.exception.CustomException; +import taco.klkl.global.error.exception.ErrorCode; + +public class GenderNotFoundException extends CustomException { + public GenderNotFoundException() { + super(ErrorCode.GENDER_NOT_FOUND); + } +} diff --git a/src/main/java/taco/klkl/domain/user/service/UserService.java b/src/main/java/taco/klkl/domain/user/service/UserService.java index 74f53269..25017e81 100644 --- a/src/main/java/taco/klkl/domain/user/service/UserService.java +++ b/src/main/java/taco/klkl/domain/user/service/UserService.java @@ -1,48 +1,17 @@ package taco.klkl.domain.user.service; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import taco.klkl.domain.user.dao.UserRepository; -import taco.klkl.domain.user.domain.Gender; import taco.klkl.domain.user.domain.User; import taco.klkl.domain.user.dto.request.UserCreateRequest; +import taco.klkl.domain.user.dto.request.UserUpdateRequest; import taco.klkl.domain.user.dto.response.UserDetailResponse; -import taco.klkl.global.common.constants.UserConstants; -@Slf4j @Service -@Transactional -@RequiredArgsConstructor -public class UserService { - private final UserRepository userRepository; +public interface UserService { + UserDetailResponse getCurrentUser(); - /** - * 임시 나의 정보 조회 - * name 속성이 "testUser"인 유저를 반환합니다. - */ - public UserDetailResponse getMyInfo() { - User me = userRepository.findFirstByName(UserConstants.TEST_USER_NAME); - return UserDetailResponse.from(me); - } - - /** - * - * @param userDto - * @return User - */ - public User createUser(UserCreateRequest userDto) { - final User user = User.of( - userDto.profile(), - userDto.name(), - Gender.getGenderByDescription(userDto.description()), - userDto.age(), - userDto.description() - ); - - return userRepository.save(user); - } + User createUser(final UserCreateRequest createRequest); + UserDetailResponse updateUser(final UserUpdateRequest updateRequest); } diff --git a/src/main/java/taco/klkl/domain/user/service/UserServiceImpl.java b/src/main/java/taco/klkl/domain/user/service/UserServiceImpl.java new file mode 100644 index 00000000..537732fb --- /dev/null +++ b/src/main/java/taco/klkl/domain/user/service/UserServiceImpl.java @@ -0,0 +1,80 @@ +package taco.klkl.domain.user.service; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import taco.klkl.domain.user.dao.UserRepository; +import taco.klkl.domain.user.domain.Gender; +import taco.klkl.domain.user.domain.User; +import taco.klkl.domain.user.dto.request.UserCreateRequest; +import taco.klkl.domain.user.dto.request.UserUpdateRequest; +import taco.klkl.domain.user.dto.response.UserDetailResponse; +import taco.klkl.global.util.UserUtil; + +@Slf4j +@Service +@Primary +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + + private final UserUtil userUtil; + + /** + * 임시 나의 정보 조회 + * name 속성이 "testUser"인 유저를 반환합니다. + */ + @Override + public UserDetailResponse getCurrentUser() { + final User currentUser = userUtil.findCurrentUser(); + return UserDetailResponse.from(currentUser); + } + + @Override + @Transactional + public User createUser(final UserCreateRequest createRequest) { + final User user = createUserEntity(createRequest); + return userRepository.save(user); + } + + @Override + @Transactional + public UserDetailResponse updateUser(final UserUpdateRequest updateRequest) { + User user = userUtil.findCurrentUser(); + updateUserEntity(user, updateRequest); + return UserDetailResponse.from(user); + } + + private User createUserEntity(final UserCreateRequest createRequest) { + final String name = createRequest.name(); + final Gender gender = Gender.from(createRequest.gender()); + final Integer age = createRequest.age(); + final String description = createRequest.description(); + + return User.of( + name, + gender, + age, + description + ); + } + + private void updateUserEntity(final User user, final UserUpdateRequest updateRequest) { + final String name = updateRequest.name(); + final Gender gender = Gender.from(updateRequest.gender()); + final Integer age = updateRequest.age(); + final String description = updateRequest.description(); + + user.update( + name, + gender, + age, + description + ); + } +} diff --git a/src/main/java/taco/klkl/global/common/constants/ImageValidationMessages.java b/src/main/java/taco/klkl/global/common/constants/ImageValidationMessages.java new file mode 100644 index 00000000..8927161f --- /dev/null +++ b/src/main/java/taco/klkl/global/common/constants/ImageValidationMessages.java @@ -0,0 +1,8 @@ +package taco.klkl.global.common.constants; + +public final class ImageValidationMessages { + public static final String FILE_EXTENSION_NOT_BLANK = "파일 확장자는 비어있을 수 없습니다."; + + private ImageValidationMessages() { + } +} diff --git a/src/main/java/taco/klkl/global/common/constants/NotificationConstants.java b/src/main/java/taco/klkl/global/common/constants/NotificationConstants.java index 402d8e92..6efd285f 100644 --- a/src/main/java/taco/klkl/global/common/constants/NotificationConstants.java +++ b/src/main/java/taco/klkl/global/common/constants/NotificationConstants.java @@ -1,9 +1,12 @@ package taco.klkl.global.common.constants; -public class NotificationConstants { +public final class NotificationConstants { public static final String DEFAULT_IS_READ_STRING = "false"; public static final boolean DEFAULT_IS_READ_VALUE = false; public static final int DEFAULT_PAGE_SIZE = 5; public static final boolean UPDATED_IS_READ_VALUE = true; + + private NotificationConstants() { + } } diff --git a/src/main/java/taco/klkl/global/common/constants/UserConstants.java b/src/main/java/taco/klkl/global/common/constants/UserConstants.java index cb27c0a2..1396d909 100644 --- a/src/main/java/taco/klkl/global/common/constants/UserConstants.java +++ b/src/main/java/taco/klkl/global/common/constants/UserConstants.java @@ -7,7 +7,6 @@ public final class UserConstants { public static final String TEST_USER_NAME = "testUser"; public static final User TEST_USER = User.of( - "image/test.jpg", TEST_USER_NAME, Gender.MALE, 20, diff --git a/src/main/java/taco/klkl/global/config/s3/S3Config.java b/src/main/java/taco/klkl/global/config/s3/S3Config.java new file mode 100644 index 00000000..c6cb0dde --- /dev/null +++ b/src/main/java/taco/klkl/global/config/s3/S3Config.java @@ -0,0 +1,48 @@ +package taco.klkl.global.config.s3; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AwsCredentialsProvider awsCredentialsProvider() { + AwsCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey); + return StaticCredentialsProvider.create(awsCredentials); + } + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(awsCredentialsProvider()) + .build(); + } + + @Bean + public S3Presigner s3Presigner() { + return S3Presigner.builder() + .region(Region.of(region)) + .credentialsProvider(awsCredentialsProvider()) + .build(); + } +} diff --git a/src/main/java/taco/klkl/global/error/exception/ErrorCode.java b/src/main/java/taco/klkl/global/error/exception/ErrorCode.java index 37e75921..aac9f011 100644 --- a/src/main/java/taco/klkl/global/error/exception/ErrorCode.java +++ b/src/main/java/taco/klkl/global/error/exception/ErrorCode.java @@ -18,7 +18,8 @@ public enum ErrorCode { QUERY_PARAM_NOT_FOUND(HttpStatus.BAD_REQUEST, "쿼리 파라미터가 존재하지 않습니다."), // User - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다."), + GENDER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 성별입니다."), // Product PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 상품입니다."), @@ -58,6 +59,14 @@ public enum ErrorCode { // Search + // Image + FILE_EXTENSION_NOT_FOUND(HttpStatus.NOT_FOUND, "유효하지 않은 파일 확장자입니다."), + IMAGE_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "유효하지 않은 이미지 타입입니다."), + UPLOAD_STATE_NOT_FOUND(HttpStatus.NOT_FOUND, "유효하지 않은 업로드 상태입니다."), + IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 이미지입니다."), + IMAGE_UPLOAD_NOT_COMPLETE(HttpStatus.BAD_REQUEST, "이미지 업로드가 완료되지 않았습니다."), + IMAGE_URL_INVALID(HttpStatus.BAD_REQUEST, "유효하지 않은 이미지 url 형식입니다."), + // Sample SAMPLE_ERROR(HttpStatus.BAD_REQUEST, "샘플 에러입니다."), ; diff --git a/src/main/java/taco/klkl/global/util/UserUtil.java b/src/main/java/taco/klkl/global/util/UserUtil.java index b4b4c039..78384255 100644 --- a/src/main/java/taco/klkl/global/util/UserUtil.java +++ b/src/main/java/taco/klkl/global/util/UserUtil.java @@ -20,7 +20,8 @@ public class UserUtil { private final UserRepository userRepository; public User findTestUser() { - return userRepository.findFirstByName(UserConstants.TEST_USER_NAME); + return userRepository.findById(1L) + .orElseThrow(UserNotFoundException::new); } /** @@ -29,16 +30,7 @@ public User findTestUser() { * @return */ public User findCurrentUser() { - return userRepository.findFirstByName(UserConstants.TEST_USER_NAME); - } - - public User findUserById(final Long id) { - return userRepository.findById(id) - .orElseThrow(UserNotFoundException::new); - } - - public User findUserByName(final String name) { - return userRepository.findFirstByName(name); + return findTestUser(); } public String createUsername(final String name, final Long oauthMemberId) { diff --git a/src/main/resources/application-storage.yaml b/src/main/resources/application-storage.yaml new file mode 100644 index 00000000..e9ddb373 --- /dev/null +++ b/src/main/resources/application-storage.yaml @@ -0,0 +1,20 @@ +spring: + config: + activate: + on-profile: "storage" + servlet: + multipart: + max-file-size: 5MB + max-request-size: 10MB + resolve-lazily: true +cloud: + aws: + credentials: + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} + region: + static: ${S3_REGION} + s3: + bucket: ${S3_BUCKET_NAME} + cloudfront: + domain: ${S3_CLOUDFRONT_DOMAIN} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index cbaed709..153f776b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -8,6 +8,7 @@ spring: dev: "dev, mysql" include: - swagger + - storage - oauth application: diff --git a/src/main/resources/database/data.sql b/src/main/resources/database/data.sql index 60819d87..7f18bf14 100644 --- a/src/main/resources/database/data.sql +++ b/src/main/resources/database/data.sql @@ -1,5 +1,5 @@ /* User */ -INSERT INTO klkl_user(user_id, profile, name, gender, age, description, created_at) +INSERT INTO klkl_user(user_id, profile_image_url, name, gender, age, description, created_at) VALUES (1, 'image/test.jpg', 'testUser', '남', 20, '테스트입니다.', now()); /* Like */ diff --git a/src/test/java/taco/klkl/domain/comment/controller/CommentControllerTest.java b/src/test/java/taco/klkl/domain/comment/controller/CommentControllerTest.java index b25d6ac8..39d3dc5a 100644 --- a/src/test/java/taco/klkl/domain/comment/controller/CommentControllerTest.java +++ b/src/test/java/taco/klkl/domain/comment/controller/CommentControllerTest.java @@ -68,14 +68,11 @@ public class CommentControllerTest { "이상화", "남", 19, - "image/ideal-flower.jpg", "저는 이상화입니다." ); - private final User user = User.of( - requestDto.profile(), requestDto.name(), - Gender.getGenderByDescription(requestDto.description()), + Gender.from(requestDto.gender()), requestDto.age(), requestDto.description() ); diff --git a/src/test/java/taco/klkl/domain/comment/service/CommentServiceTest.java b/src/test/java/taco/klkl/domain/comment/service/CommentServiceTest.java index 397af029..19d63559 100644 --- a/src/test/java/taco/klkl/domain/comment/service/CommentServiceTest.java +++ b/src/test/java/taco/klkl/domain/comment/service/CommentServiceTest.java @@ -52,14 +52,12 @@ public class CommentServiceTest { "이상화", "남", 19, - "image/ideal-flower.jpg", "저는 이상화입니다." ); private final User user = User.of( - userRequestDto.profile(), userRequestDto.name(), - Gender.getGenderByDescription(userRequestDto.description()), + Gender.from(userRequestDto.gender()), userRequestDto.age(), userRequestDto.description() ); diff --git a/src/test/java/taco/klkl/domain/notification/service/NotificationServiceTest.java b/src/test/java/taco/klkl/domain/notification/service/NotificationServiceTest.java index 5c0817fd..fffc0895 100644 --- a/src/test/java/taco/klkl/domain/notification/service/NotificationServiceTest.java +++ b/src/test/java/taco/klkl/domain/notification/service/NotificationServiceTest.java @@ -76,7 +76,7 @@ public class NotificationServiceTest { @BeforeEach public void setUp() { - commentUser = User.of("profile", + commentUser = User.of( "윤상정", Gender.FEMALE, 26, diff --git a/src/test/java/taco/klkl/domain/product/controller/ProductControllerTest.java b/src/test/java/taco/klkl/domain/product/controller/ProductControllerTest.java index 27194bb1..e880440c 100644 --- a/src/test/java/taco/klkl/domain/product/controller/ProductControllerTest.java +++ b/src/test/java/taco/klkl/domain/product/controller/ProductControllerTest.java @@ -32,6 +32,7 @@ import taco.klkl.domain.product.dto.request.ProductFilterOptions; import taco.klkl.domain.product.dto.request.ProductSortOptions; import taco.klkl.domain.product.dto.response.ProductDetailResponse; +import taco.klkl.domain.product.dto.response.ProductImageResponse; import taco.klkl.domain.product.dto.response.ProductSimpleResponse; import taco.klkl.domain.product.service.ProductService; import taco.klkl.domain.region.domain.CountryType; @@ -60,7 +61,7 @@ public class ProductControllerTest { void setUp() { UserDetailResponse userDetailResponse = new UserDetailResponse( 1L, - "image/profile.jpg", + "image/profileImageUrl.jpg", "userName", "userDescription", 100 @@ -87,8 +88,14 @@ void setUp() { "tagName2" ); + List imageResponses = List.of( + new ProductImageResponse("image/product1.jpg", 0), + new ProductImageResponse("image/product2.jpg", 1) + ); + productSimpleResponse = new ProductSimpleResponse( 1L, + imageResponses, "productName", 10, Rating.FIVE.getValue(), @@ -98,6 +105,7 @@ void setUp() { ); productDetailResponse = new ProductDetailResponse( 1L, + imageResponses, "productName", "Description", "123 Street", @@ -268,7 +276,8 @@ void testFindProductById_ShouldReturnProduct() throws Exception { .andExpect(jsonPath("$.data.likeCount", is(productDetailResponse.likeCount()))) .andExpect(jsonPath("$.data.rating", is(productSimpleResponse.rating()))) .andExpect(jsonPath("$.data.user.id", is(productDetailResponse.user().id().intValue()))) - .andExpect(jsonPath("$.data.user.profile", is(productDetailResponse.user().profile()))) + .andExpect(jsonPath("$.data.user.profileImageUrl", + is(productDetailResponse.user().profileImageUrl()))) .andExpect(jsonPath("$.data.user.name", is(productDetailResponse.user().name()))) .andExpect(jsonPath("$.data.user.description", is(productDetailResponse.user().description()))) @@ -312,7 +321,8 @@ void testCreateProduct_ShouldReturnCreatedProduct() throws Exception { .andExpect(jsonPath("$.data.likeCount", is(productDetailResponse.likeCount()))) .andExpect(jsonPath("$.data.rating", is(productSimpleResponse.rating()))) .andExpect(jsonPath("$.data.user.id", is(productDetailResponse.user().id().intValue()))) - .andExpect(jsonPath("$.data.user.profile", is(productDetailResponse.user().profile()))) + .andExpect(jsonPath("$.data.user.profileImageUrl", + is(productDetailResponse.user().profileImageUrl()))) .andExpect(jsonPath("$.data.user.name", is(productDetailResponse.user().name()))) .andExpect(jsonPath("$.data.user.description", is(productDetailResponse.user().description()))) @@ -355,7 +365,8 @@ void testUpdateProduct_ShouldReturnUpdatedProduct() throws Exception { .andExpect(jsonPath("$.data.likeCount", is(productDetailResponse.likeCount()))) .andExpect(jsonPath("$.data.rating", is(productSimpleResponse.rating()))) .andExpect(jsonPath("$.data.user.id", is(productDetailResponse.user().id().intValue()))) - .andExpect(jsonPath("$.data.user.profile", is(productDetailResponse.user().profile()))) + .andExpect(jsonPath("$.data.user.profileImageUrl", + is(productDetailResponse.user().profileImageUrl()))) .andExpect(jsonPath("$.data.user.name", is(productDetailResponse.user().name()))) .andExpect(jsonPath("$.data.user.description", is(productDetailResponse.user().description()))) diff --git a/src/test/java/taco/klkl/domain/product/dto/response/ProductSimpleResponseTest.java b/src/test/java/taco/klkl/domain/product/dto/response/ProductSimpleResponseTest.java index 77418cc1..95e55a1d 100644 --- a/src/test/java/taco/klkl/domain/product/dto/response/ProductSimpleResponseTest.java +++ b/src/test/java/taco/klkl/domain/product/dto/response/ProductSimpleResponseTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -30,6 +31,7 @@ import taco.klkl.domain.region.domain.Region; import taco.klkl.domain.region.domain.RegionType; import taco.klkl.domain.user.domain.User; +import taco.klkl.global.util.ProductUtil; class ProductSimpleResponseTest { @@ -109,28 +111,33 @@ void testFromProduct() { @Test @DisplayName("생성자를 통해 ProductSimpleResponse 생성 테스트") void testConstructor() { - // when - Set filters = product.getProductTags().stream() - .map(ProductTag::getTag) - .map(TagResponse::from) - .collect(Collectors.toSet()); + // given + List images = product.getImages().stream() + .map(ProductImageResponse::from) + .toList(); + + Set tags = ProductUtil.createTagsByProduct(product); + // when ProductSimpleResponse dto = new ProductSimpleResponse( product.getId(), + images, product.getName(), product.getLikeCount(), product.getRating().getValue(), - city.getCountry().getName().getKoreanName(), + product.getCity().getCountry().getName().getKoreanName(), product.getSubcategory().getCategory().getName().getKoreanName(), - filters + tags ); // then assertThat(dto.id()).isEqualTo(product.getId()); + assertThat(dto.images()).isEqualTo(images); assertThat(dto.name()).isEqualTo(product.getName()); assertThat(dto.likeCount()).isEqualTo(product.getLikeCount()); assertThat(dto.rating()).isEqualTo(product.getRating().getValue()); - assertThat(dto.countryName()).isEqualTo(city.getCountry().getName().getKoreanName()); + assertThat(dto.countryName()).isEqualTo(product.getCity().getCountry().getName().getKoreanName()); assertThat(dto.categoryName()).isEqualTo(product.getSubcategory().getCategory().getName().getKoreanName()); + assertThat(dto.tags()).isEqualTo(tags); } } diff --git a/src/test/java/taco/klkl/domain/user/controller/UserControllerTest.java b/src/test/java/taco/klkl/domain/user/controller/UserControllerTest.java index d51393e2..8e09dd39 100644 --- a/src/test/java/taco/klkl/domain/user/controller/UserControllerTest.java +++ b/src/test/java/taco/klkl/domain/user/controller/UserControllerTest.java @@ -40,7 +40,7 @@ public void setUp() { @DisplayName("내 정보 조회 API 테스트") public void testGetMe() throws Exception { // given - Mockito.when(userService.getMyInfo()).thenReturn(responseDto); + Mockito.when(userService.getCurrentUser()).thenReturn(responseDto); // when & then mockMvc.perform(get("/v1/users/me") @@ -48,7 +48,7 @@ public void testGetMe() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.isSuccess", is(true))) .andExpect(jsonPath("$.data.id", is(nullValue()))) - .andExpect(jsonPath("$.data.profile", is(notNullValue()))) + .andExpect(jsonPath("$.data.profileImageUrl", is(notNullValue()))) .andExpect(jsonPath("$.data.name", is(responseDto.name()))) .andExpect(jsonPath("$.data.description", is(responseDto.description()))) .andExpect(jsonPath("$.data.totalLikeCount", is(UserConstants.DEFAULT_TOTAL_LIKE_COUNT))) diff --git a/src/test/java/taco/klkl/domain/user/dto/request/UserRequestDtoTest.java b/src/test/java/taco/klkl/domain/user/dto/request/UserRequestDtoTest.java index 4bc801e3..599db180 100644 --- a/src/test/java/taco/klkl/domain/user/dto/request/UserRequestDtoTest.java +++ b/src/test/java/taco/klkl/domain/user/dto/request/UserRequestDtoTest.java @@ -24,7 +24,7 @@ public UserRequestDtoTest() { @DisplayName("유효한 UserCreateRequestDto에 대한 유효성 검사") public void testValidUserCreateRequestDto() { // given - UserCreateRequest requestDto = new UserCreateRequest("이름", "남", 20, "image/profile.png", "자기소개"); + UserCreateRequest requestDto = new UserCreateRequest("이름", "남", 20, "자기소개"); // when Set> violations = validator.validate(requestDto); @@ -37,7 +37,7 @@ public void testValidUserCreateRequestDto() { @DisplayName("이름이 null인 UserCreateRequest 유효성 검사") public void testInvalidUserCreateRequestDto_NameRequired() { // given - UserCreateRequest requestDto = new UserCreateRequest(null, "남", 20, "image/profile.png", "자기소개"); + UserCreateRequest requestDto = new UserCreateRequest(null, "남", 20, "자기소개"); // when Set> violations = validator.validate(requestDto); @@ -50,7 +50,7 @@ public void testInvalidUserCreateRequestDto_NameRequired() { @DisplayName("나이가 음수인 UserCreateRequest 유효성 검사") public void testInvalidUserCreateRequestDto_AgeNegative() { // given - UserCreateRequest requestDto = new UserCreateRequest("이름", "남", -1, "image/profile.png", "자기소개"); + UserCreateRequest requestDto = new UserCreateRequest("이름", "남", -1, "자기소개"); // when Set> violations = validator.validate(requestDto); diff --git a/src/test/java/taco/klkl/domain/user/dto/response/UserDetailResponseTest.java b/src/test/java/taco/klkl/domain/user/dto/response/UserDetailResponseTest.java index e16c78a4..957864c9 100644 --- a/src/test/java/taco/klkl/domain/user/dto/response/UserDetailResponseTest.java +++ b/src/test/java/taco/klkl/domain/user/dto/response/UserDetailResponseTest.java @@ -15,7 +15,7 @@ public class UserDetailResponseTest { public void testUserDetailResponseDto() { // given Long id = 1L; - String profile = "image/profile.png"; + String profile = "image/profileImageUrl.png"; String name = "이름"; String description = "자기소개"; int totalLikeCount = 100; @@ -25,7 +25,7 @@ public void testUserDetailResponseDto() { // then assertThat(userDetail.id()).isEqualTo(id); - assertThat(userDetail.profile()).isEqualTo(profile); + assertThat(userDetail.profileImageUrl()).isEqualTo(profile); assertThat(userDetail.name()).isEqualTo(name); assertThat(userDetail.description()).isEqualTo(description); assertThat(userDetail.totalLikeCount()).isEqualTo(totalLikeCount); @@ -35,18 +35,16 @@ public void testUserDetailResponseDto() { @DisplayName("User 객체로부터 UserDetailResponse 생성 테스트") public void testFrom() { // given - String profile = "image/profile.png"; String name = "이름"; Gender gender = Gender.MALE; int age = 20; String description = "자기소개"; // when - User user = User.of(profile, name, gender, age, description); + User user = User.of(name, gender, age, description); UserDetailResponse userDetail = UserDetailResponse.from(user); // then - assertThat(userDetail.profile()).isEqualTo(profile); assertThat(userDetail.name()).isEqualTo(name); assertThat(userDetail.description()).isEqualTo(description); assertThat(userDetail.totalLikeCount()).isEqualTo(UserConstants.DEFAULT_TOTAL_LIKE_COUNT); diff --git a/src/test/java/taco/klkl/domain/user/dto/response/UserSimpleResponseTest.java b/src/test/java/taco/klkl/domain/user/dto/response/UserSimpleResponseTest.java index 7b006736..547f3509 100644 --- a/src/test/java/taco/klkl/domain/user/dto/response/UserSimpleResponseTest.java +++ b/src/test/java/taco/klkl/domain/user/dto/response/UserSimpleResponseTest.java @@ -14,7 +14,7 @@ class UserSimpleResponseTest { public void testUserSimpleResponseDto() { // given Long id = 1L; - String profile = "image/profile.png"; + String profile = "image/profileImageUrl.png"; String name = "이름"; // when @@ -22,7 +22,7 @@ public void testUserSimpleResponseDto() { // then assertThat(userSimple.id()).isEqualTo(id); - assertThat(userSimple.profile()).isEqualTo(profile); + assertThat(userSimple.profileImageUrl()).isEqualTo(profile); assertThat(userSimple.name()).isEqualTo(name); } @@ -30,18 +30,16 @@ public void testUserSimpleResponseDto() { @DisplayName("User 객체로부터 UserSimpleResponse 생성 테스트") public void testFrom() { // given - String profile = "image/profile.png"; String name = "이름"; Gender gender = Gender.MALE; int age = 20; String description = "자기소개"; // when - User user = User.of(profile, name, gender, age, description); + User user = User.of(name, gender, age, description); UserSimpleResponse userSimple = UserSimpleResponse.from(user); // then - assertThat(userSimple.profile()).isEqualTo(profile); assertThat(userSimple.name()).isEqualTo(name); } } diff --git a/src/test/java/taco/klkl/domain/user/integration/UserIntegrationTest.java b/src/test/java/taco/klkl/domain/user/integration/UserIntegrationTest.java index 6266cf74..f09cb68e 100644 --- a/src/test/java/taco/klkl/domain/user/integration/UserIntegrationTest.java +++ b/src/test/java/taco/klkl/domain/user/integration/UserIntegrationTest.java @@ -31,14 +31,14 @@ public class UserIntegrationTest { @Test public void testUserMe() throws Exception { // given, when - UserDetailResponse userDto = userService.getMyInfo(); + UserDetailResponse userDto = userService.getCurrentUser(); // then mockMvc.perform(get("/v1/users/me")) .andExpect(status().isOk()) .andExpect(jsonPath("$.isSuccess", is(true))) .andExpect(jsonPath("$.data.id", is(userDto.id().intValue()))) - .andExpect(jsonPath("$.data.profile", is(userDto.profile()))) + .andExpect(jsonPath("$.data.profileImageUrl", is(userDto.profileImageUrl()))) .andExpect(jsonPath("$.data.name", is(userDto.name()))) .andExpect(jsonPath("$.data.description", is(userDto.description()))) .andExpect(jsonPath("$.timestamp", notNullValue())) diff --git a/src/test/java/taco/klkl/domain/user/service/UserServiceTest.java b/src/test/java/taco/klkl/domain/user/service/UserServiceImplTest.java similarity index 71% rename from src/test/java/taco/klkl/domain/user/service/UserServiceTest.java rename to src/test/java/taco/klkl/domain/user/service/UserServiceImplTest.java index 0e8e41d2..d28ba5af 100644 --- a/src/test/java/taco/klkl/domain/user/service/UserServiceTest.java +++ b/src/test/java/taco/klkl/domain/user/service/UserServiceImplTest.java @@ -18,30 +18,40 @@ import taco.klkl.domain.user.dto.request.UserCreateRequest; import taco.klkl.domain.user.dto.response.UserDetailResponse; import taco.klkl.global.common.constants.UserConstants; +import taco.klkl.global.util.UserUtil; @ExtendWith(MockitoExtension.class) -class UserServiceTest { - private static final Logger log = LoggerFactory.getLogger(UserServiceTest.class); +class UserServiceImplTest { + private static final Logger log = LoggerFactory.getLogger(UserServiceImplTest.class); @Mock UserRepository userRepository; + @Mock + UserUtil userUtil; + @InjectMocks - UserService userService; + UserServiceImpl userServiceImpl; @Test @DisplayName("내 정보 조회 서비스 테스트") - public void testGetMyInfo() { + public void testGetCurrentUser() { // given - User user = UserConstants.TEST_USER; - when(userRepository.findFirstByName(UserConstants.TEST_USER_NAME)).thenReturn(user); + + User user = mock(User.class); + when(userUtil.findCurrentUser()).thenReturn(user); + when(user.getId()).thenReturn(1L); + when(user.getName()).thenReturn("testUser"); + when(user.getProfileImageUrl()).thenReturn("image/test.jpg"); + when(user.getDescription()).thenReturn("테스트입니다."); // when - UserDetailResponse userDto = userService.getMyInfo(); + UserDetailResponse userDto = userServiceImpl.getCurrentUser(); // then - assertThat(userDto.profile()).isEqualTo(user.getProfile()); + assertThat(userDto.id()).isEqualTo(user.getId()); assertThat(userDto.name()).isEqualTo(user.getName()); + assertThat(userDto.profileImageUrl()).isEqualTo(user.getProfileImageUrl()); assertThat(userDto.description()).isEqualTo(user.getDescription()); assertThat(userDto.totalLikeCount()).isEqualTo(UserConstants.DEFAULT_TOTAL_LIKE_COUNT); } @@ -54,25 +64,22 @@ public void testCreateUser() { "이상화", "남", 19, - "image/ideal-flower.jpg", "저는 이상화입니다." ); User user = User.of( - requestDto.profile(), requestDto.name(), - Gender.getGenderByDescription(requestDto.description()), + Gender.from(requestDto.gender()), requestDto.age(), requestDto.description() ); when(userRepository.save(any(User.class))).thenReturn(user); // when - User user1 = userService.createUser(requestDto); + User user1 = userServiceImpl.createUser(requestDto); UserDetailResponse responseDto = UserDetailResponse.from(user1); // then assertThat(responseDto.name()).isEqualTo(requestDto.name()); - assertThat(responseDto.profile()).isEqualTo(requestDto.profile()); assertThat(responseDto.description()).isEqualTo(requestDto.description()); assertThat(responseDto.totalLikeCount()).isEqualTo(UserConstants.DEFAULT_TOTAL_LIKE_COUNT); } diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml new file mode 100644 index 00000000..6ff4108c --- /dev/null +++ b/src/test/resources/application-test.yaml @@ -0,0 +1,22 @@ +spring: + config: + activate: + on-profile: "test" + jpa: + properties: + hibernate: + show_sql: true + format_sql: true +api: + main-url: ${LOCAL_URL} + +logging: + level: + root: INFO + org.springframework.web: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" \ No newline at end of file