화면에서 파일을 클릭하면 다운로드를 받고 싶을 때

view 화면

      <div id="downloadList" class="mt-1 text-center">
          <span class="attachment" th:each="a : *{attachments}">
            <input type="hidden" name="attachments" th:value="${a.id}" th:field="*{attachments}"/>
            <a class="text-black" th:href="|@{/attachment/}${a.id}|">
              <div class="file-name" th:text="${a.fileName}"></div>
            </a>
          </span>
      </div>

 

연결이 /attachment/{id}로 되어 있으니 controller를 생성

AttachmentController

@RestController
@RequestMapping("/attachment")
@RequiredArgsConstructor
public class AttachmentController {

  @NonNull
  private final AttachmentService attachmentService;

  @GetMapping(value = "/{id}", produces = "application/octet-stream")
  public ResponseEntity attachment(@PathVariable("id") Long id) throws IOException {
    Attachment attachment = attachmentService.findById(id).orElseThrow(DataNotFoundException::new);
    File file = new File(attachment.getFilePath());

    if (file.exists() && file.length() > 0) {
      byte[] bytes = FileUtils.readFileToByteArray(file);

      HttpHeaders httpHeaders = new HttpHeaders();
      httpHeaders.setContentLength(bytes.length);
      httpHeaders.set("Content-Disposition",
          "attachment; filename=\"" + attachment.getEncodedFileName() + "\";");
      httpHeaders.set("Content-Transfer-Encoding", "binary");
      httpHeaders.setContentType(MediaType.valueOf(Files.probeContentType(file.toPath())));
      return new ResponseEntity(bytes, httpHeaders, HttpStatus.OK);
    }

    return ResponseEntity.notFound().build();
  }
}

 

해당 controller 함수에 맞게 repository랑 service 구현

AttachmentRepository

@Repository
public interface AttachmentRepository extends JpaRepository<Attachment, Long> {

}

 

AttachmentService

@Service
@RequiredArgsConstructor
public class AttachmentService {

  @NonNull
  private final AttachmentRepository attachmentRepository;

  @Cacheable(value = "attachment", key = "#id")
  public Optional<Attachment> findById(Long id) {
    return attachmentRepository.findById(id);
  }

  @CacheEvict(value = "attachment", key = "#attachment.id")
  public Attachment save(Attachment attachment) {
    return attachmentRepository.save(attachment);
  }
}

 

위의 과정을 거치면 파일 클릭시 다운로드 가능!!

domain 추가

(첨부파일: Attachment)

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "attachment")
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Attachment {

  public Attachment(Long id) {
    this.id = id;
  }

  public Attachment(String fileName, String filePath, long fileSize, String fileContentType) {
    this.fileName = fileName;
    this.filePath = filePath;
    this.fileSize = fileSize;
    this.fileContentType = fileContentType;
  }

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @EqualsAndHashCode.Include
  private Long id;

  @Column(updatable = false, nullable = false)
  private String fileName; // 원본 파일명

  @Column(updatable = false, nullable = false, unique = true)
  private String filePath; // 실제 저장된 파일명

  @Column(updatable = false)
  private String fileContentType;

  @Column(updatable = false)
  private long fileSize;

  @PreRemove
  public void onRemove() {
    File file = new File(filePath);
    FileUtils.deleteQuietly(file);
  }

  @JsonIgnore
  public String getEncodedFileName() throws UnsupportedEncodingException {
    return URLEncoder.encode(this.fileName, "UTF-8").replace("+", "%20");
  }

  public String   getFileSizeText() {
    float size = fileSize;
    String unit = "Byte";
    if (size >= 1024) {
      unit = "KB";
      size /= 1024;
      if (size >= 1024) {
        unit = "MB";
        size /= 1024;
      }
    }
    return String.format("%d%s", (int) size, unit);
  }

  public String getIconPath() {
    String type = "file";
    if(fileContentType.toLowerCase().startsWith("image")) {
      type = "img";
    } else {
      String lowerCaseName = fileName.toLowerCase();
      if(lowerCaseName.endsWith("doc") || lowerCaseName.endsWith("docx")) {
        type = "doc";
      } else if(lowerCaseName.endsWith("hwp")) {
        type = "hwp";
      } else if(lowerCaseName.endsWith("pdf")) {
        type = "pdf";
      } else if(lowerCaseName.endsWith("xls") || lowerCaseName.endsWith("xlsx")) {
        type = "xl";
      } else if(lowerCaseName.endsWith("zip")) {
        type = "zip";
      } else if(lowerCaseName.endsWith("ppt") || lowerCaseName.endsWith("pptx")) {
        type = "ppt";
      }
    }
    return "static/img/files/" + String.format("icon-%s.png", type);
  }
}

 

service추가

(파일 업로드 관련)

@Service
@RequiredArgsConstructor
public class FileUploadService {

  public static final String PATH_LOGOS = "logos/";

  public static final String PATH_ATTACHMENTS = "attachments/";

  @Value("${spring.servlet.multipart.location}")
  private String rootPath;

  public Attachment upload(MultipartFile file, String destinationPath) throws IOException {
    if (file == null || file.isEmpty()) {
      return null;
    }

    String fileName = file.getOriginalFilename();
    String fileExtension = FilenameUtils.getExtension(fileName);
    if (StringUtils.isBlank(fileExtension)) {
      return null;
    }

    File destinationFile;
    String destinationFileName;
    do {
      destinationFileName = destinationPath + FileUtil.getRandomFilename(fileExtension);
      destinationFile = new File(rootPath + destinationFileName);
    } while (destinationFile.exists());

    //noinspection ResultOfMethodCallIgnored
    destinationFile.getParentFile().mkdirs();
    file.transferTo(destinationFile);

    Attachment attachment = new Attachment();

    //이미지인 경우 리사이징
    if (file.getContentType() != null && file.getContentType().contains("image")) {
      BufferedImage image = ImageIO.read(destinationFile);

      double maxThumbnailWidth = 1280.0;
      double thumbnailRatio =
          maxThumbnailWidth > image.getWidth() ? 1 : maxThumbnailWidth / image.getWidth();

      //이미지 사이즈 변경
      BufferedImage resizedImage = resize(image, thumbnailRatio, Image.SCALE_FAST);
      ImageIO.write(resizedImage, "JPEG", destinationFile);
    }

    attachment.setFileName(file.getOriginalFilename());
    attachment.setFilePath(FilenameUtils.normalize(destinationFile.getAbsolutePath(), true));
    attachment.setFileContentType(file.getContentType());
    attachment.setFileSize(file.getSize());
    return attachment;
  }

  public Attachment uploadFile(MultipartFile file) throws IOException {
    return upload(file, PATH_ATTACHMENTS);
  }

  public Attachment uploadLogo(MultipartFile file) throws IOException {
    return upload(file, PATH_LOGOS);
  }

  private BufferedImage resize(BufferedImage beforeImage, double ratio, int imageScale) {
    int newW = (int) (beforeImage.getWidth() * ratio);
    int newH = (int) (beforeImage.getHeight() * ratio);
    Image tmp = beforeImage.getScaledInstance(newW, newH, imageScale);
    BufferedImage dimg = new BufferedImage(newW, newH, beforeImage.getType());

    Graphics2D g2d = dimg.createGraphics();
    g2d.drawImage(tmp, 0, 0, null);
    g2d.dispose();

    return dimg;
  }
}

 

파일을 첨부할 controller 수정

 

  @PostMapping("/save")
  public String save(@ModelAttribute("project") Project project, BindingResult result,
      MultipartFile[] files) {

    if (!result.hasErrors()) {
      if (files != null) {
        for (MultipartFile file : files) {
          try {
            project.getAttachments().add(fileUploadService.uploadFile(file));
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
      projectService.save(project);
      return "project/edit";
    }
    return null;
  }

 

화면에 폼 추가하기

(project/edit)

<form method="post" th:action="|@{/project/save}|" th:object="${project}" enctype="multipart/form-data">
 <!--        첨부파일-->
 <div class="d-flex mt-3 mb-2 justify-content-between">
 	<label class="form-label m-0 mr-2">Attachment</label>
 	<div>
 		<label for="input-file" id="addFile" class="input-file-button">Upload</label>
 		<input type="file" id="input-file" name="files" style="display: none;" multiple/>
 	</div>
 </div>
 </form>

enctype="multipart/form-data" 이게 중요하다! 이걸 표시해줘야 파일이란 것을 알 수 있음!!

 

본인의 프로젝트에 맞게 설정을 잘하고 저장버튼을 누르면 파일이 업로드 됨을 볼 수 있다.

DB에 추가됨

'Web > Spring' 카테고리의 다른 글

[Login구현] login-logout구현(setting)  (0) 2021.08.10
[MultipartFile] 첨부파일 다운로드  (0) 2021.08.06
[properties] server.port vs server.http.port  (0) 2021.08.06
[ERROR] 유의해야 할 점  (0) 2021.08.06
[Annotation] @PathVariable  (0) 2021.08.06

운영에 있어서 http와 https 포트 두 개를 모두 열기 위함!

"멀티 포트"

 

 

/config/ServletConfig 파일 설정

@Configuration
public class ServletConfig {

  @Value("${server.http.port}")
  private int httpPort;
  @Value("${server.port}")
  private int httpsPort;
  
  @Bean
  public ServletWebServerFactory servletWebServerFactory() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
      @Override
      protected void postProcessContext(Context context) {
        SecurityConstraint securityConstraint = new SecurityConstraint();
        securityConstraint.setUserConstraint("CONFIDENTIAL");
        SecurityCollection collection = new SecurityCollection();
        collection.addPattern("/*");
        securityConstraint.addCollection(collection);
        context.addConstraint(securityConstraint);
      }
    };

    tomcat.addAdditionalTomcatConnectors(createStandardConnector());
    return tomcat;
  }

  private Connector createStandardConnector() {
    Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
    connector.setPort(httpPort);
    connector.setScheme("http");
    connector.setSecure(false);
    connector.setRedirectPort(httpsPort);
    return connector;
  }
}

 

properties 설정

server.port=443
server.http.port=80

#SSL 설정
server.ssl.enabled=true
server.ssl.key-store=
server.ssl.key-store-type=
server.ssl.ciphers=
server.ssl.key-store-password=
server.ssl.key-alias= 도메인

'Web > Spring' 카테고리의 다른 글

[MultipartFile] 첨부파일 다운로드  (0) 2021.08.06
[MultipartFile] 첨부파일 업로드  (0) 2021.08.06
[ERROR] 유의해야 할 점  (0) 2021.08.06
[Annotation] @PathVariable  (0) 2021.08.06
[Pageable] 페이징 처리 - 기본  (0) 2021.08.06
  • filter를 사용할 땐 검색할 때만!!!(페이지 리스트 찾을 때) → 시도 때도 없이 찾으면 속도가 느려진다..
  • html은 포맷팅(ctrl+d)을 피하자!! 가독성이 떨어진다

"URL 경로에 변수를 넣어주는 것" "RESTful 서비스의 URI 형태"

 

사용법

@GetMapping({"/edit", "/edit/{id}"})
  public String edit(@PathVariable(required = false) Integer id, Model model) {

위와 같이 GetMapping에서 id가 필요할 경우에만 사용할 땐 (required = false)를 붙인다.

그럼 id가 들어오면 /edit/{id}를 주고 id가 없으면 /edit을 준다.

 

 @GetMapping("/read/{id}")
  public String read(@PathVariable Integer id, Model model) {

위는 id가 필수적으로 필요하다.

 

❗ id가 없을 때도 잘 받기 위해선 view 쪽에 hidden input으로 id를 설정해주면 알아서 처리해줌... ❗

<form method="post" th:action="|@{/project/save}|" th:object="${project}">
        <input th:field="*{id}" type="hidden"/>

/repository/support안에 impl 파일들

public class ProjectRepositoryImpl extends QuerydslRepositorySupport implements
    CustomProjectRepository {

위와 같이 QuerydslRepositorySupport를 extends 한다. 안에 필요한 함수들을 구현해주면 된다.

 

 @Override
  public Page<Project> findAllByFilter(Pageable pageable, ProjectFilter filter) {
    BooleanBuilder builder = new BooleanBuilder();

    if (!StringUtils.isBlank(filter.getDate())) {
      builder.and(project.date.containsIgnoreCase(filter.getDate()));
    }
    if (!StringUtils.isBlank(filter.getTitle())) {
      builder.and(project.title.containsIgnoreCase(filter.getTitle()));
    }
    if(!StringUtils.isBlank(filter.getAddress())){
      builder.and(project.address.containsIgnoreCase(filter.getAddress()));
    }
    if(!StringUtils.isBlank(filter.getTechnology())){
      builder.and(project.technology.containsIgnoreCase(filter.getTechnology()));
    }
    if(!StringUtils.isBlank(filter.getAddress())){
      builder.and(project.purpose.containsIgnoreCase(filter.getPurpose()));
    }

    final JPQLQuery<Project> query = from(project).where(builder);

    List<Project> result = getQuerydsl().applyPagination(pageable, query).fetch();

    return new PageImpl<>(result,pageable,query.fetchCount());

  }

위와 같이 원하는 정보들을 모아서 paging 해준다.

 

물론 /repository/custom 과 /repository 설정은 기본적으로 해준 상태에서 진행해야한다!!!

 

그 후 controller에 pageable 활용

  @GetMapping("")
  public String list(Model model, Pageable pageable, ProjectFilter filter) {

    model.addAttribute("projectList", projectService.findAllByFilter(pageable, filter));
    model.addAttribute("filter", filter);
    return "project/list";
  }

 

❗ paging 원리에 대해선 나중에 다뤄봐야겠다.. ❗

'Web > Spring' 카테고리의 다른 글

[ERROR] 유의해야 할 점  (0) 2021.08.06
[Annotation] @PathVariable  (0) 2021.08.06
[배경] JPA vs JDBC, JPA vs Mybatis, JPA vs Spring Data JPA  (0) 2021.08.06
[설정] 자동망치 기능  (0) 2021.08.06
[설정] application.properties  (0) 2021.08.04

+ Recent posts