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

첨부파일 엔티티 생성 및 업로드 기능 구현 #44

Merged
merged 12 commits into from
Mar 27, 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
10 changes: 10 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## ☑️ Describe your changes
- 작업 내용을 적어주세요
> ex. - CORS 허용 범위를 (/**) URL 전체로 변경한다.

## 📷 Screenshot
- 관련 스크린샷

## 🔗 Issue number and link
- 이슈 번호를 등록해주세요
> closed {#이슈번호}
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ dependencies {
/* JSON 객체 역직렬화에 필요한 ObjectMapper를 사용하기 위해 jackson 라이브러리 의존성 추가 */
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2'

/* Thumbnail 파일 처리를 위한 의존성 */
implementation 'net.coobird:thumbnailator:0.4.17'


}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.hallym.festival.domain.booth.controller;

import com.hallym.festival.domain.booth.dto.BoothDTO;
import com.hallym.festival.domain.booth.dto.PageRequestDTO;
import com.hallym.festival.domain.booth.entity.Booth;
import com.hallym.festival.domain.booth.service.BoothService;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -39,36 +38,31 @@ public BoothDTO read(@PathVariable("bno") Long bno){
}

@PostMapping("register")
public Map<String, String> registerPost(@Valid @RequestBody BoothDTO boothDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
public Map<String, String> registerPost(@Valid @RequestBody BoothDTO boothDTO, BindingResult bindingResult){

if(bindingResult.hasErrors()) { //검증에 문제가 있다면 입력 화면으로 리다이렉트
log.info("has errors.......");
log.info("-----register----알맞지 않은 입력 값입니다-----");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() ); //잘못된 결과는 addFlashAttribute()로 전달
return Map.of("result","failed");
}

log.info(boothDTO);

boothService.register(boothDTO);

redirectAttributes.addFlashAttribute("result");

return Map.of("result","register success");
}

@PutMapping ("/modify/{bno}")
public Map<String, String> modify( @PathVariable("bno") Long bno, @Valid @RequestBody BoothDTO boothDTO ,
BindingResult bindingResult,
PageRequestDTO pageRequestDTO,
RedirectAttributes redirectAttributes){

log.info("board modify post......." + boothDTO);

if(bindingResult.hasErrors()) {
log.info("has errors.......");
log.info("-----modify----알맞지 않은 입력 값입니다-----");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors() );

return Map.of("result","failed");
}
Expand All @@ -84,14 +78,12 @@ public Map<String, String> modify( @PathVariable("bno") Long bno, @Valid @Reques


@DeleteMapping ("/{bno}")
public Map<String, String> remove(@PathVariable("bno") Long bno, RedirectAttributes redirectAttributes) {
public Map<String, String> remove(@PathVariable("bno") Long bno) {

log.info("remove post.. " + bno);

boothService.remove(bno);

redirectAttributes.addFlashAttribute("result", "removed");

return Map.of("result","remove success");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.hallym.festival.domain.booth.controller;

import com.hallym.festival.domain.booth.dto.upload.UploadFileDTO;
import com.hallym.festival.domain.booth.dto.upload.UploadResultDTO;
import lombok.extern.log4j.Log4j2;
import net.coobird.thumbnailator.Thumbnailator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;


import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

@RestController
@Log4j2
public class UploadController {

@Value("${com.hallym.festival.upload.path}")// import 시에 springframework으로 시작하는 Value
private String uploadPath;

//post 방식으로 파일 등록
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public List<UploadResultDTO> upload(UploadFileDTO uploadFileDTO) {

log.info(uploadFileDTO);

if(uploadFileDTO.getFiles() != null){

final List<UploadResultDTO> list = new ArrayList<>();

uploadFileDTO.getFiles().forEach(multipartFile -> {

String originalName = multipartFile.getOriginalFilename();
log.info(originalName);

String uuid = UUID.randomUUID().toString();

Path savePath = Paths.get(uploadPath, uuid+"_"+ originalName);

boolean image = false;

try {
multipartFile.transferTo(savePath);

//이미지 파일의 종류라면
if(Files.probeContentType(savePath).startsWith("image")){

image = true;

File thumbFile = new File(uploadPath, "s_" + uuid+"_"+ originalName);

Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200,200);
}

} catch (IOException e) {
e.printStackTrace();
}

list.add(UploadResultDTO.builder()
.uuid(uuid)
.fileName(originalName)
.img(image).build()
);

});//end each

return list;
}//end if

return null;
}

@GetMapping("/view/{fileName}")
public ResponseEntity<Resource> viewFileGET(@PathVariable String fileName){

Resource resource = new FileSystemResource(uploadPath+File.separator + fileName);

String resourceName = resource.getFilename();
HttpHeaders headers = new HttpHeaders();

try{
headers.add("Content-Type", Files.probeContentType( resource.getFile().toPath() ));
} catch(Exception e){
return ResponseEntity.internalServerError().build();
}
return ResponseEntity.ok().headers(headers).body(resource);
}

@DeleteMapping("/remove/{fileName}")
public Map<String,Boolean> removeFile(@PathVariable String fileName){

Resource resource = new FileSystemResource(uploadPath+File.separator + fileName);
String resourceName = resource.getFilename();

Map<String, Boolean> resultMap = new HashMap<>();
boolean removed = false;

try {
String contentType = Files.probeContentType(resource.getFile().toPath());
removed = resource.getFile().delete();

//섬네일이 존재한다면
if(contentType.startsWith("image")){
File thumbnailFile = new File(uploadPath+File.separator +"s_" + fileName);
thumbnailFile.delete();
}

} catch (Exception e) {
log.error(e.getMessage());
}

resultMap.put("result", removed);

return resultMap;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hallym.festival.domain.booth.dto.upload;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Data
public class UploadFileDTO {

private List<MultipartFile> files;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.hallym.festival.domain.booth.dto.upload;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UploadResultDTO { //파일이 여러 개인 경우 여러 정보를 반환해야하기 때문에 별도의 DTO 생성

private String uuid; //업로드 된 파일의 UUID

private String fileName; //파일 이름

private boolean img; //이미지 여부

public String getLink(){ //JSON 처리 시 첨부 파일 경로 처리를 위한 link 속성으로 자동 처리

if(img){
return "s_"+ uuid +"_"+fileName; //이미지인 경우 섬네일
}else {
return uuid+"_"+fileName;
}
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/hallym/festival/domain/booth/entity/Booth.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Getter
@AllArgsConstructor
Expand Down Expand Up @@ -51,6 +53,32 @@ public class Booth extends BaseTimeEntity {
@OneToMany(mappedBy = "booth", fetch = FetchType.LAZY)
private final List<Comment> comments = new ArrayList<>();

@OneToMany(mappedBy = "booth",
cascade = {CascadeType.ALL}, //booth 엔티티의 모든 상태 변화에 BoothImage 객체도 같이 변경
fetch = FetchType.LAZY,
orphanRemoval = true) //수정 기능에서 하위 엔티티의 참조가 없는 상태가 되면 삭제되기 위해 orphanRemoval 속성 true)
@Builder.Default //imageSet 인스턴스 생성 시 BoothImage 값으로 초기화하기 위함
private Set<BoothImage> imageSet = new HashSet<>();

// Booth 객체 자체에서 BoothImage 객체들을 관리하기 위한 addImage, clearImages
public void addImage(String uuid, String fileName){

BoothImage boothImage = BoothImage.builder()
.uuid(uuid)
.fileName(fileName)
.booth(this) //양방향의 경우 참조 관계가 서로 일치하도록
.ord(imageSet.size())
.build();
imageSet.add(boothImage);
}

public void clearImages() {

imageSet.forEach(boothImage -> boothImage.changeBoard(null));

this.imageSet.clear();
}


// @JsonManagedReference
// @OneToMany(mappedBy = "booth")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.hallym.festival.domain.booth.entity;


import lombok.*;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "booth") //Booth 필드는 toString에서 제외
public class BoothImage implements Comparable<BoothImage>{ //순번에 맞게 정렬하기 위해 Comparable 인터페이스 적용

@Id
private String uuid;

private String fileName;

private int ord;

@ManyToOne
private Booth booth;


@Override
public int compareTo(BoothImage other) {
return this.ord - other.ord;
}

public void changeBoard(Booth booth ){ //Booth 엔티티 삭제 시 BoothImage 객체의 참조도 변경하기 위함
this.booth = booth;
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.hallym.festival.domain.booth.repository;

import com.hallym.festival.domain.booth.entity.Booth;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface BoothRepository extends JpaRepository<Booth, Long> {
//EntityGraph를 이용해서 lazy 로딩이여도 한 번에 join 처리 후, select 실행
@EntityGraph(attributePaths = {"imageSet"}) //imageSet 속성을 같이 로딩
@Query("select b from Booth b where b.bno =:bno")
Optional<Booth> findByIdWithImages(Long bno);

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class Comment extends BaseTimeEntity {

@JsonManagedReference
@OneToMany(mappedBy = "comment", fetch = FetchType.LAZY)
@Builder.Default //reportList 인스턴스 생성 시 Report 값으로 초기화
private List<Report> reportList = new ArrayList<Report>();

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import com.hallym.festival.domain.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> {

// @Query("select r from Comment r where r.booth.bno = :bno") //게시물 삭제 시 댓글들 삭제를 위한 쿼리
List<Comment> findByBooth_BnoAndActiveOrderByRegDateDesc(Long bno, Boolean active);

void deleteByBooth_Bno(Long bno);

}
Loading