Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POM-94] 가게 이미지 업로드 기능 추가 #38

Merged
merged 12 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ray.pominowner.global.config;

import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Getter
@Component
public class ImagePathProvider {

private final String storeImageRootPath;
private final String storeLogoImageSubPath;

public ImagePathProvider(@Value("${file.store.image.path}") String storeImageRootPath,
@Value("${file.store.logo.path}") String storeLogoImageSubPath) {
this.storeImageRootPath = storeImageRootPath;
this.storeLogoImageSubPath = storeLogoImageSubPath;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.ray.pominowner.store.controller.dto.CategoryRequest;
import com.ray.pominowner.store.controller.dto.StoreInformationRequest;
import com.ray.pominowner.store.controller.dto.PhoneNumberRequest;
import com.ray.pominowner.store.controller.dto.StoreInformationRequest;
import com.ray.pominowner.store.controller.dto.StoreRegisterRequest;
import com.ray.pominowner.store.service.StoreService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PatchMapping;
Expand All @@ -16,8 +17,10 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/api/v1/stores")
Expand Down Expand Up @@ -57,4 +60,9 @@ public void deleteInformation(@PathVariable Long storeId) {
storeService.deleteInformation(storeId);
}

@PostMapping(value = "/{storeId}/store-images", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public void saveStoreImage(@RequestBody List<MultipartFile> images, @PathVariable Long storeId) {
storeService.saveStoreImages(images, storeId);
}

}
23 changes: 22 additions & 1 deletion src/main/java/com/ray/pominowner/store/domain/Store.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.List;

import static jakarta.persistence.CascadeType.ALL;

@Entity
@EqualsAndHashCode(of = "id")
Copy link
Collaborator

Choose a reason for hiding this comment

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

BaseTimeEntity를 상속하고 있으니 callSuper = false 해당 부분을 추가하면 좋을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

callsuper = false는 default라 명시 안했어요

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Store extends BaseTimeEntity {

Expand All @@ -29,14 +37,18 @@ public class Store extends BaseTimeEntity {
@Embedded
private PhoneNumber tel = new PhoneNumber();

@OneToMany(mappedBy = "store", cascade = ALL, orphanRemoval = true)
private List<StoreImage> images = new ArrayList<>();

private Long ownerId; // 추후 연관관계 설정 예정

@Builder
private Store(Long id, RequiredStoreInfo requiredStoreInfo, Information info, PhoneNumber tel, Long ownerId) {
private Store(Long id, RequiredStoreInfo requiredStoreInfo, Information info, PhoneNumber tel, List<StoreImage> images, Long ownerId) {
this.id = id;
this.requiredStoreInfo = requiredStoreInfo;
this.info = info;
this.tel = tel;
this.images = images;
this.ownerId = ownerId;
}

Expand All @@ -60,6 +72,7 @@ public Store retrieveStoreAfterRegisteringPhoneNumber(String phoneNumber) {
.requiredStoreInfo(this.requiredStoreInfo)
.info(this.info)
.tel(new PhoneNumber(phoneNumber))
.images(this.images)
.ownerId(this.ownerId)
.build();
}
Expand All @@ -70,6 +83,7 @@ public Store retrieveStoreAfterDeletingPhoneNumber() {
.requiredStoreInfo(this.requiredStoreInfo)
.info(this.info)
.tel(new PhoneNumber())
.images(this.images)
.ownerId(this.ownerId)
.build();
}
Expand All @@ -80,6 +94,7 @@ public Store retrieveStoreAfterRegisteringInfo(String information) {
.requiredStoreInfo(this.requiredStoreInfo)
.info(new Information(information))
.tel(this.tel)
.images(this.images)
.ownerId(this.ownerId)
.build();
}
Expand All @@ -90,6 +105,7 @@ public Store retrieveStoreAfterDeletingInfo() {
.requiredStoreInfo(this.requiredStoreInfo)
.info(new Information())
.tel(this.tel)
.images(this.images)
.ownerId(this.ownerId)
.build();
}
Expand All @@ -105,4 +121,9 @@ public PhoneNumber getPhoneNumber() {
public Information getInformation() {
return info;
}

public List<StoreImage> getImages() {
return images;
}

}
38 changes: 36 additions & 2 deletions src/main/java/com/ray/pominowner/store/domain/StoreImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,51 @@
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Builder;
import lombok.NoArgsConstructor;
import org.springframework.util.Assert;

import static jakarta.persistence.FetchType.LAZY;
import static lombok.AccessLevel.PROTECTED;

@Entity
@NoArgsConstructor(access = PROTECTED)
public class StoreImage extends BaseTimeEntity {

@Id
@Column(name = "STORE_IAMGE_ID")
@GeneratedValue
private Long id;

private String image;
private String path;

private String uploadName;

private String fileName;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "STORE_ID")
private Store store;

@Builder
private StoreImage(String path, String uploadName, String fileName, Store store) {
validateImage(path, uploadName, fileName);
this.path = path;
this.uploadName = uploadName;
this.fileName = fileName;
this.store = store;
}

private void validateImage(String path, String uploadName, String fileName) {
Assert.hasText(path, "경로는 빈 값일 수 없습니다.");
Assert.hasText(uploadName, "파일 이름은 빈 값일 수 없습니다.");
Assert.hasText(fileName, "파일 이름은 빈 값일 수 없습니다.");
}

private Long storeId;
public Store getStore() {
return store;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ray.pominowner.store.repository;

import com.ray.pominowner.store.domain.StoreImage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StoreImageRepository extends JpaRepository<StoreImage, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.ray.pominowner.store.service;

import com.ray.pominowner.global.config.ImagePathProvider;
import com.ray.pominowner.store.domain.Store;
import com.ray.pominowner.store.domain.StoreImage;
import com.ray.pominowner.store.repository.StoreImageRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;

@Component
@RequiredArgsConstructor
public class StoreImageService {

private static final String DOT = ".";

private final ImagePathProvider imagePathProvider;

private final StoreImageRepository storeImageRepository;

public void saveImages(List<MultipartFile> images, Store store) {
Assert.noNullElements(images, "올바르지 못한 파일입니다.");
String rootPath = imagePathProvider.getStoreImageRootPath();
images.forEach(image -> saveEachFile(store, image, rootPath));
}

private void saveEachFile(Store store, MultipartFile image, String rootPath) {
String originalFilename = image.getOriginalFilename();
validateFileName(originalFilename);

String createdFileName = createFileName(originalFilename);
saveImageToPath(image, rootPath + createdFileName);

storeImageRepository.save(StoreImage.builder()
.path(rootPath + createdFileName)
.uploadName(originalFilename)
.fileName(createdFileName)
.store(store)
.build());
}

private void validateFileName(String originalFilename) {
Assert.notNull(originalFilename, "올바르지 못한 파일입니다.");

if (!originalFilename.contains(DOT)) {
throw new IllegalArgumentException("올바르지 못한 파일입니다.");
}
}

private void saveImageToPath(MultipartFile image, String path) {
try {
image.transferTo(new File(path));
} catch (IOException e) {
throw new RuntimeException("파일 저장에 실패했습니다.",e);
}
}

private String createFileName(String originalFilename) {
String fileExtension = extractFileExtension(originalFilename);
return UUID.randomUUID()
.toString()
.concat(fileExtension);
}

private String extractFileExtension(String originalFilename) {
int dotIndex = originalFilename.lastIndexOf(DOT);
return originalFilename.substring(dotIndex);
}

}
15 changes: 8 additions & 7 deletions src/main/java/com/ray/pominowner/store/service/StoreService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class StoreService {

private final StoreServiceValidator storeServiceValidator;
Expand All @@ -22,43 +23,43 @@ public class StoreService {

private final StoreCategoryService storeCategoryService;

@Transactional
private final StoreImageService storeImageService;

public Long registerStore(Store store) throws JsonProcessingException {
storeServiceValidator.validateBusinessNumber(store.getBusinessNumber());

return storeRepository.save(store).getId();
}

@Transactional
public void registerCategory(List<String> categories, Long storeId) {
storeServiceValidator.validateCategory(categories);
storeCategoryService.saveCategories(findStore(storeId), categories);
}

@Transactional
public void registerPhoneNumber(String phoneNumber, Long storeId) {
Store store = findStore(storeId).retrieveStoreAfterRegisteringPhoneNumber(phoneNumber);
storeRepository.save(store);
}

@Transactional
public void deletePhoneNumber(Long storeId) {
Store store = findStore(storeId).retrieveStoreAfterDeletingPhoneNumber();
storeRepository.save(store);
}

@Transactional
public void registerInformation(String information, Long storeId) {
Store store = findStore(storeId).retrieveStoreAfterRegisteringInfo(information);
storeRepository.save(store);
}

@Transactional
public void deleteInformation(Long storeId) {
Store store = findStore(storeId).retrieveStoreAfterDeletingInfo();
storeRepository.save(store);
}

public void saveStoreImages(List<MultipartFile> images, Long storeId) {
storeImageService.saveImages(images, findStore(storeId));
}

private Store findStore(Long storeId) {
return storeRepository.findById(storeId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 가게입니다."));
Expand Down
13 changes: 13 additions & 0 deletions src/main/resources/dev/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ spring:
highlight_sql: true
defer-datasource-initialization: true
open-in-view: false

servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB

file:
store:
image:
path: src/main/resources/store-image/
logo:
path: src/main/resources/store-logo-image/

Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
Expand Down Expand Up @@ -130,4 +133,21 @@ void successRemoveInformation() throws Exception {
.andExpect(status().isOk());
}

@Test
@WithMockUser
@DisplayName("가게 이미지 저장에 성공한다")
void successSaveImages() throws Exception {
// given
MockMultipartFile firstMultipartFile = new MockMultipartFile("TEST", "test1.png", MediaType.IMAGE_PNG_VALUE, UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
MockMultipartFile secondMultipartFile = new MockMultipartFile("TEST2", "test2.png", MediaType.IMAGE_PNG_VALUE, UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));

// when, then
mvc.perform(multipart("/api/v1/stores/1/store-images")
.file(firstMultipartFile)
.file(secondMultipartFile)
.contentType(MediaType.MULTIPART_FORM_DATA)
.with(csrf()))
.andExpect(status().isOk());
}

}
Loading