WebConfig.java
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${upload.image.location}")
private String location;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/image/**")
// url에 /image/ 경로로 접근을 하게 된다면
// uploadPath에 설정한 폴더 기준으로 파일을 읽어오게 한다
.addResourceLocations("file:" + location)
// FileSystem의 location 경로에서 파일 접근
//로컬 컴퓨터에 저장된 파일을 읽어올 root 경로를 설정합니다.
.setCacheControl(CacheControl.maxAge(Duration.ofHours(1L)).cachePublic())
// 업로드된 각 이미지는 고유 이름 가지며 & 수정 X 예정 따라서 => 1시간의 캐시 설정
// 자원 접근 시 새로 접근 x , 캐시 자원 이용 (1시간 유효)
}
FileService
(interface)
/**
* 파일 업로드 & 삭제 수행 인터페이스
*/
// 실제 파일 업로드, 삭제 로직 작동시킬 구현체 필요
@Primary //얘를 service 단에서 주입시키면 (required args) , 구현체들 너무 많다고 어떤 구현체 주입시킬지
//모른다고 에러가 난다 따라서 얘를 primary로
public interface FileService {
void upload(MultipartFile file, String filename);
void delete(String filename);
}
LocalFileService
(class)
@Service
@Slf4j
public class LocalFileService implements FileService {
/**
* 파일 업로드 위치
*/
//파일 업로드할 위치를 지정
@Value("${upload.image.location}")
private String location;
/**
* 파일 업로드 디렉토리 생성
*/
@PostConstruct
void postConstruct() {
File dir = new File(location);
if (!dir.exists()) {
dir.mkdir();
}
}
/**
* MultipartFile 을 실제 파일 지정 위치에 저장
* @param file
* @param filename
*/
@Override
public void upload(MultipartFile file, String filename) {
try {
///////여기서부터
LocalDateTime now = LocalDateTime.now();
//날짜별로 파일을 나누기 위해 시간 받아오기
//now로 디렉토리를 생성하고, 거기에다 파일 저장할 것
String targetDir = Path.of(
location,
now.format(DateTimeFormatter.ISO_DATE)
).toString();
//현재 시간을 어떤 형식으로 나타내줄지 정하는 것
File dirNow = new File(targetDir);
if(!dirNow.exists()) dirNow.mkdir();//디렉토리 없으면 만들기
//////여기까지는 단순히 파일이름 지정 과정 (현재 날짜, 시간) + 파일 이름
// MultipartFile을 실제 파일로 지정된 위치에 저장
file.transferTo(Path.of(
targetDir,
filename
));
} catch(IOException e) {
throw new FileUploadFailureException(e);
}
}
//파일을 삭제하는 것 => 해당 로케이션의 파일이름의 이미지 파일을 삭제
@Override
public void delete(String filename) {
new File(location+ filename).delete();
}
}
=> 요약
// MultipartFile을 실제 파일로 지정된 위치에 저장
file.transferTo(Path.of(
targetDir,
filename
));
(+) application.yml에 정의한 파일 업로드 로케이션
upload:
location: src/main/prodmedia/image/
url에 /image/ 접두 경로가 설정되어있으면,
파일 시스템의 location 경로에서 파일에 접근합니다.
ItemService.java
- 아이템에 이미지를 첨부해주는 과정
@Transactional
public ItemCreateResponse create(ItemCreateRequest req) {
Item item = itemRepository.save(
ItemCreateRequest.toEntity(
req,
memberRepository,
colorRepository,
materialRepository,
manufactureRepository
)
);
uploadImages(item.getThumbnail(), req.getThumbnail());
if (!(req.getTag().size() == 0)) {//TODO : 나중에 함수로 빼기 (Attachment 유무 판단)
//attachment가 존재할 땜나
uploadAttachments(item.getAttachments(), req.getAttachments());
}
return new ItemCreateResponse(item.getId());
}
/**
* 썸네일 존재 시에 File Upload로 이미지 업로드
*
* @param images
* @param fileImages
*/
private void uploadImages(List<Image> images, List<MultipartFile> fileImages) {
// 실제 이미지 파일을 가지고 있는 Multipart 파일을
// 이미지가 가지는 uniquename을 파일명으로 해서 파일저장소 업로드
IntStream.range(0, images.size())
.forEach(
i -> fileService.upload
(
fileImages.get(i),
images.get(i).getUniqueName()
)
);
}
=> 이미지를 생성해주는 메소드 : uploadImages 메소드
private void uploadImages(List<Image> images, List<MultipartFile> fileImages) {
// 실제 이미지 파일을 가지고 있는 Multipart 파일을
// 이미지가 가지는 uniquename을 파일명으로 해서 파일저장소 업로드
IntStream.range(0, images.size())
.forEach(
i -> fileService.upload
(
fileImages.get(i),
images.get(i).getUniqueName()
)
);
}
&& 그럼 DB에는 뭐가 저장되느냐?
=> DB에는
=> 이것은 ItemCreateRequest가 들어와서 얘를 entity로 바꿔주는 과정에서!
DB에 저장되는 것은 파일의 이름, 주소, 등이 된다.
또한 아이템과 이 썸네일(이미지) 관계는
하나의 아이템에 많은 이미지가 들어갈 수 있는 형태입니다!
그리고 관계가 CASCADE 라서 아이템이 저장될 때 Image가 생성이 되기 때문에 dto를 entity로 바꿔주는 과정에서 자연스레 생성이 되게 됩니다~!
ItemService.java
@Transactional
public ItemUpdateResponse update(Long id, ItemUpdateRequest req) {
Item item = itemRepository.findById(id).orElseThrow(ItemNotFoundException::new);
if (item.getTempsave() == false) { //true면 임시저장 상태, false면 찐 저장 상태
//찐 저장 상태라면 UPDATE 불가, 임시저장 일때만 가능
throw new ItemUpdateImpossibleException();
}
Item.FileUpdatedResult result = item.update(
req,
colorRepository,
memberRepository,
materialRepository,
manufactureRepository,
itemManufactureRepository,
itemMaterialRepository
);
uploadImages(
result.getImageUpdatedResult().getAddedImages(),
result.getImageUpdatedResult().getAddedImageFiles()
);
deleteImages(
result.getImageUpdatedResult().getDeletedImages()
);
uploadAttachments(
result.getAttachmentUpdatedResult().getAddedAttachments(),
result.getAttachmentUpdatedResult().getAddedAttachmentFiles()
);
deleteAttachments(
result.getAttachmentUpdatedResult().getDeletedAttachments()
);
return new ItemUpdateResponse(id);
}
Item.java
/**
* postupdaterequest 받아서 update 수행
*
* @param req
* @return 새로 수정된 이미지
*/
public FileUpdatedResult update(
ItemUpdateRequest req,
ColorRepository colorRepository,
MemberRepository memberRepository,
MaterialRepository materialRepository,
ManufactureRepository manufactureRepository,
ItemManufactureRepository itemManufactureRepository,
ItemMaterialRepository itemMaterialRepository
) {
//isBlank 랑 isNull로 판단해서 기존 값 / req 값 채워넣기
this.name = req.getName();
this.type = req.getType();
this.width = req.getWidth();
this.height = req.getHeight();
this.weight = req.getWeight();
ImageUpdatedResult resultImage =
findImageUpdatedResult(
req.getAddedImages(),
req.getDeletedImages()
);
addImages(resultImage.getAddedImages());
deleteImages(resultImage.getDeletedImages());
AttachmentUpdatedResult resultAttachment =
findAttachmentUpdatedResult(
req.getAddedAttachments(),
req.getDeletedAttachments()
);
addUpdatedAttachments(req, resultAttachment.getAddedAttachments());
deleteAttachments(resultAttachment.getDeletedAttachments());
FileUpdatedResult fileUpdatedResult = new FileUpdatedResult(resultAttachment,resultImage);
this.modifier =
memberRepository.findById(
req.getModifierId()
).orElseThrow(MemberNotFoundException::new);//05 -22 생성자 추가
return fileUpdatedResult;
}
업데이트 시에는 Transactional 에 readonly를 주지 않음 => 변경감지 옵션을 가능하게 해주기
이미지 추가, 삭제 구현
1) 추가된 이미지
=> Item 의 update 메소드 중에서 아래 수행
ImageUpdatedResult resultImage =
findImageUpdatedResult(
req.getAddedImages(),
req.getDeletedImages()
);
addImages(resultImage.getAddedImages());
/**
* 추가할 이미지
*
* @param added
*/
private void addImages(List<Image> added) {
added.stream().forEach(i -> {
thumbnail.add(i);
i.initItem(this);
});
}
2) 삭제된 이미지
/**
* 삭제될 이미지 제거 (고아 객체 이미지 제거)
*
* @param deleted
*/
private void deleteImages(List<Image> deleted) {
deleted.stream().
forEach(di ->
this.thumbnail.remove(di)
);
}
- 조회 조건이 복잡한 화면은 querydsl 을 이용해서 조건에 맞는 쿼리를 동적으로 쉽게 생성하는 것이 가능
- 비슷한 쿼리 재활용 가능
- 자바 코드로 작성하기 때문에 IDE의 도움을 받아서 문법 오류를 바로 수정 가능
ItemProjectCreateReadCondition.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ItemProjectCreateReadCondition {
@NotNull(message = "페이지 번호를 입력해주세요.")
@PositiveOrZero(message = "올바른 페이지 번호를 입력해주세요. (0 이상)")
private Integer page;
@NotNull(message = "페이지 크기를 입력해주세요.")
@Positive(message = "올바른 페이지 크기를 입력해주세요. (1 이상)")
private Integer size;
}
}
/**
* 쿼리를 구현하는 메소드
* 검색 조건에 대한 정보가 담긴
* ItemReadCondition 전달받음
* 이를 Page로 반환하여
* 페이징 결과에 대한 각종 정보 확인
*/
public interface CustomItemRepository {
Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond);
}
/**
* CustomItemRepository의 구현체
*/
@Transactional(readOnly = true)
public class CustomItemRepositoryImpl extends QuerydslRepositorySupport implements CustomItemRepository {
private final JPAQueryFactory jpaQueryFactory;
public CustomItemRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
super(Item.class);
this.jpaQueryFactory = jpaQueryFactory;
}
/**
* 전달받은 ItemReadCondition(검색)으로
* Predicate와 PageRequest를 생성 &
* 조회 쿼리와 카운트 쿼리를 수행한 결과를 Page의 구현체로 반환
* @param cond
* @return Page
*/
@Override
public Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond) {
Pageable pageable = PageRequest.of(cond.getPage(), cond.getSize());
Predicate predicate = createPredicate(cond);
System.out.println(fetchAll(predicate, pageable).toString());
return new PageImpl<>(fetchAll(predicate, pageable), pageable, fetchCount(predicate));
}
/**
* 아이템 목록을 ItemSimpleDto로 조회한 결과 반환환
* @param predicate
* @param pageable
* @return getQuerydsl().applyPagination (페이징 적용 쿼리)
*/
private List<ItemProjectCreateDto> fetchAll(Predicate predicate, Pageable pageable) {
List<ItemProjectCreateDto> itemProjectCreateDtos = getQuerydsl().applyPagination(
pageable,
jpaQueryFactory
.select(constructor
(
ItemProjectCreateDto.class,
item.id,
item.name,
item.type,
item.itemNumber,
item.revision,
routeOrdering.lifecycleStatus
)
)
.from(item)
//.join(itemMaterial).on(item.id.eq(itemMaterial.item.id))
//.join(itemMaterial).on(item.id.eq(itemMaterial.item.id))
//jqpl은 연관관계 없으면 직접 못하고 join on으로 해줘야 함
.join(routeOrdering).on(item.id.eq(routeOrdering.item.id))
.join(item.member) //아이템 작성자 닉네임 조회 위해 Member와 조인
.where(predicate)
.orderBy(item.id.desc())
).fetch(); //리스트 반환
return itemProjectCreateDtos;
}
private Long fetchCount(Predicate predicate) { // 7
return jpaQueryFactory.select(
item.count()
).from(item).
where(predicate).fetchOne();
}
private Predicate createPredicate(ItemProjectCreateReadCondition cond) { // 8
return new BooleanBuilder();
}
private <T> Predicate orConditions(List<T> values, Function<T, BooleanExpression> term) { // 11
return values.stream()
.map(term)
.reduce(BooleanExpression::or)
.orElse(null);
}
}
public class CustomItemRepositoryImpl extends QuerydslRepositorySupport implements CustomItemRepository {
private final JPAQueryFactory jpaQueryFactory;
public CustomItemRepositoryImpl(JPAQueryFactory jpaQueryFactory) { // 4
super(Item.class);
this.jpaQueryFactory = jpaQueryFactory;
}
private List<ItemProjectCreateDto> fetchAll(Predicate predicate, Pageable pageable) {
List<ItemProjectCreateDto> itemProjectCreateDtos = getQuerydsl().applyPagination(
pageable,
jpaQueryFactory
.select(constructor
(
ItemProjectCreateDto.class,
item.id,
item.name,
item.type,
item.itemNumber,
item.revision,
routeOrdering.lifecycleStatus
)
)
.from(item)
//jqpl은 연관관계 없으면 직접 못하고 join on으로 해줘야 함 .join(routeOrdering).on(item.id.eq(routeOrdering.item.id))
.join(item.member) //아이템 작성자 닉네임 조회 위해 Member와 조인
.where(predicate)
.orderBy(item.id.desc())
).fetch(); //리스트 반환
return itemProjectCreateDtos;
}
/**
* 전달받은 ItemReadCondition(검색)으로
* Predicate와 PageRequest를 생성 &
* 조회 쿼리와 카운트 쿼리를 수행한 결과를 Page의 구현체로 반환
* @param cond
* @return Page
*/
@Override
public Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond) {
Pageable pageable = PageRequest.of(cond.getPage(), cond.getSize());
Predicate predicate = createPredicate(cond);
System.out.println(fetchAll(predicate, pageable).toString());
return new PageImpl<>(fetchAll(predicate, pageable), pageable, fetchCount(predicate));
}
public interface ItemRepository extends JpaRepository<Item, Long>, CustomItemRepository {
=> 이렇게 되면 이제 itemRepo 에서 public Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond) {
를 사용하는 것이 가능해지지
public ItemProjectCreateDtoList readItemCandidatesAll(ItemProjectCreateReadCondition cond) {
return ItemProjectCreateDtoList.toDto(
itemRepository.findAllByCondition(cond)
);
}
@Transactional(readonly=Tre)
속성을 지저애주는 것이 효율적