요구사항
상품관리
- 상품이름
- 첨부파일 하나
- 이미지 파일 여러개
첨부파일을 업로드 다운로드 할 수 있어야 한다.
업로드 한 이미지를 웹 브라우저에서 확인할 수 있다.
@Data
public class Item {
private Long id;
private String itemName;
private UploadFile attachFile;
private List<UploadFile> imageFiles;
}
먼저 Item을 구현했다.
그 다음 간이로 저장해보기 위해
@Repository
public class ItemRepository {
private final Map<Long, Item> store = new HashMap<>();
private long sequence = 0L;
public Item save(Item item){
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id){
return store.get(id);
}
}
그냥 메모리에 저장되는 Repository를 만들었다(서비스를 끄면 휘발된다.)
Repository가 Item 접근을 위한 Id를 부여한다.
@Data
public class UploadFile {
private String uploadFileName;
private String storeFileName;
public UploadFile(String uploadFileName, String storeFileName) {
this.uploadFileName = uploadFileName;
this.storeFileName = storeFileName;
}
}
UploadFile을 정의했다.
이거는 파일 이름만 저장시켜 놓는거다.
보통 DB에는 이렇게 경로나 파일 이름정도만 저장시켜 놓고, 이미지 서버는 따로 두거나 한다.
파일 이름만 남겨두는 경우는, 또 그 위의 경로를 애플리케이션에다 static final 해서 저장시켜 놓기도 한다.
uploadFileName은 사용자가 업로드 했을 때 올렸던 파일의 이름을 넣을 것이고,
storeFileName은 서버에 저장 시킬 때의 파일 이름이다. 이건 나중에 겹치는 것을 방지하기 위해 UUID로 넣을 것이다.
@Component
public class FileStore {
@Value("${file.dir}")
String fileDir;
public String getFullPath(String fileName){
return fileDir + fileName;
}
public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException {
List<UploadFile> files = new ArrayList<>();
for (MultipartFile multipartFile : multipartFiles) {
if(!multipartFile.isEmpty()){
files.add(storeFile(multipartFile));
}
}
return files;
}
public UploadFile storeFile(MultipartFile multipartFile) throws IOException {
if(multipartFile.isEmpty()){
return null;
}
String originalFileName = multipartFile.getOriginalFilename();
String serverFileName = createStoreFileName(originalFileName);
multipartFile.transferTo(new File(getFullPath(serverFileName)));
return new UploadFile(originalFileName, serverFileName);
}
private String createStoreFileName(String originalFileName){
String extension = extracted(originalFileName);
String serverFileName = UUID.randomUUID().toString() + "." + extension;
return serverFileName;
}
private String extracted(String originalFilename){
int pos = originalFilename.lastIndexOf(".");
String extension = originalFilename.substring(pos + 1);
return extension;
}
}
파일을 관리하는 것이다. 여기서 저장시키고 서버에 저장 될 이름(UUID)도 준다.
@Value("file.dir")로
file.dir=G:/spring/files/
이걸 가져왔다.
getFullPath는 저 경로 + 파일이름이다.
먼저 storeFile()부터 보면,
Request로부터 올 수 있는 MultipartFile이 비어 있으면 그냥 null을 반환하고,
있으면 파일의 원래 이름을 originalFileName해서 넣고,
서버에 들어갈 파일 이름은 UUID로 생성을 하는데, 그 전에 원래 파일이름의 맨 끝의 "." 이 있는 것의 인덱스를 찾고,
substring()으로 그것의 다음것들을 모두 반환시켜 확장자를 얻는다.
그렇게 UUID + "." + 확장자
해서 서버에 저장 될 파일 이름이 완성된다.
그거를 이제 메모리에 있던 것을 transferTo(new File(경로))
해서 경로로 옮겨준다.
그리고, UploadFile(원래이름, 서버로들어갈이름) 해서 반환해 주면 UploadFile이 된다.
storeFiles는 그걸 여러 번 해준거다.
@Slf4j
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemRepository itemRepository;
private final FileStore fileStore;
@GetMapping("/items/new")
public String newItem(@ModelAttribute ItemForm form){
return "item-form";
}
@PostMapping("/items/new")
public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes) throws IOException {
Item item = new Item();
item.setItemName(form.getItemName());
item.setAttachFile(fileStore.storeFile(form.getAttachFile()));
item.setImageFiles(fileStore.storeFiles(form.getImageFiles()));
itemRepository.save(item);
redirectAttributes.addAttribute("itemId", item.getId());
return "redirect:/items/{itemId}";
}
@GetMapping("/items/{itemId}")
public String viewItem(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item",item);
return "item-view";
}
@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws IOException {
UrlResource urlResource = new UrlResource(("file:" + fileStore.getFullPath(filename)));
return urlResource;
}
@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
Item item = itemRepository.findById(itemId);
String fileName = item.getAttachFile().getStoreFileName();
String fullPath = fileStore.getFullPath(fileName);
String userFileName = item.getAttachFile().getUploadFileName();
UrlResource urlResource = new UrlResource("file:" + fullPath);
String encodedUserFileName = UriUtils.encode(userFileName, StandardCharsets.UTF_8);
String contentDisposition = "attachment; filename=\"" + encodedUserFileName + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(urlResource);
}
}
아이템 컨트롤러이다.
Get메소드의 newItem() 컨트롤러는 그냥 form이 있는 템플릿을 주는거다.
Post메소드의 saveItem()은 위와 같은 form이지만, 클라이언트가 위에서 받은 form에 정보를 입력하고 submit을 눌러 전송했기에 그 form의 메소드를 post로 설정했기 때문에 Post 요청으로 온다.
그럼 그냥 그 정보를 토대로 아이템을 생성하고(아이템이름, 파일, 사진들),
Repository에 저장하면 id는 알아서 부여 해 준다.
그 다음 아이템 저장소에 중복 저장되는것을 막기 위해 PRG 패턴을 적용했다.
참고로 RedirectAttributes는 Redirect를 도와주는 객체인데, return과 같이 redirect 경로를 지정하는데 도움을 줄 수도 있다.
굳이 redirect:/items/ + getId() 해도 되지만 저렇게 한 이유는,
만약 저게 한글이나 그런거면 인코딩문제가 있을수도 있기 때문에,
저 RedirectAttributes는 그걸 알아서 해결 해 준다.
@ModelAttribute ItemForm form
이거는,
@Data
public class ItemForm {
private Long itemId;
private String itemName;
private List<MultipartFile> imageFiles;
private MultipartFile attachFile;
}
이렇게 Form 전용 클래스를 따로 만든 것이다.
이게 html의 name
<form th:action method="post" enctype="multipart/form-data">
<ul>
<li>상품명 <input type="text" name="itemName"></li>
<li>첨부파일<input type="file" name="attachFile" ></li>
<li>이미지 파일들<input type="file" multiple="multiple" name="imageFiles" ></li>
</ul>
<input type="submit"/>
</form>
덕분에 알아서 잘 맞춰서 들어가 진다.
input 파일 태그에서 multiple="multiple" 이렇게 하면 여러 개 받을 수 있다. 이메일과 파일만 적용되는 속성이다.
viewItem()은 그냥 id 받고 그거 찾아서 모델에 넣어서 뷰에 전해준 다음에 보여주는 것이다.
downloadImage() 저게 뭐냐면, 먼저 템플릿 먼저 보자면
<div class="container">
<div class="py-5 text-center">
<h2>상품 조회</h2>
</div>
상품명: <span th:text="${item.itemName}">상품명</span><br/>
첨부파일: <a th:if="${item.attachFile}" th:href="|/attach/${item.id}|" th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
<img th:each="imageFile : ${item.imageFiles}" th:src="|/images/${imageFile.getStoreFileName()}|" width="300" height="300"/>
</div>
이미지의 src를 /image/서버이름
으로 해놨다.
@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws IOException {
UrlResource urlResource = new UrlResource(("file:" + fileStore.getFullPath(filename)));
return urlResource;
}
UrlResource(경로+파일이름) 하면 그걸 실제로 찾아와서 Stream형태로 반환이 된다고 한다.
@ResponseBody니 응답 메시지 바디에 넣는 것이다.
"file:"이렇게 하면 로컬파일을 뒤지는 것인데, 저기에 서버url을 넣어서 쓰면 될 것 같다.
여튼, 태그의 src는 위와 같은 Stream 형태로 반환 받을 수 있다. img태그는 그것을 이미지 형태로 표현할 것이다.
다음은 파일 다운로드이다.
@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
Item item = itemRepository.findById(itemId);
String fileName = item.getAttachFile().getStoreFileName();
String fullPath = fileStore.getFullPath(fileName);
String userFileName = item.getAttachFile().getUploadFileName();
UrlResource urlResource = new UrlResource("file:" + fullPath);
String encodedUserFileName = UriUtils.encode(userFileName, StandardCharsets.UTF_8);
String contentDisposition = "attachment; filename=\"" + encodedUserFileName + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(urlResource);
}
먼저 Id를 받아와 item을 찾고 거기서 파일이름을 찾아오고(업로드할 때 파일이름을 가져오는 것은 그 파일이름으로 그냥 보여주기 위해.)
마찬가지로 그 파일을 Stream 형태로 가져온다.
그리고, 파일 이름이 한글이나 그런 거 일수도 있어서, UriUtils 중에 encode() 기능을 이용해서 인코더를 UTF-8로 설정해 준 다음 넣어줬다.
그 다음 ResponseEntity를 이용해 바디에 넣어주는 것인데,
<a th:if="${item.attachFile}" th:href="|/attach/${item.id}|" th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
이렇게 링크를 클릭하면 다운로드 하게끔 하고 싶은데,
그냥 클릭하면 그냥 브라우저 자체에서 ResponseBody를 읽는 것이다. 우리는 다운로드 받게끔 하고 싶다.
그럴 때 필요한 게 헤더의 Content-disposition (컨텐츠의 성향)을 attachment(첨부, 첨부파일)로 해 줘야 한다.
그리고 거기에 추가로 filename="파일이름" 해 주면 저장할 때 그 파일 이름으로 미리 써 넣어준다.
filename 해 놓는 게 좋은 게, 따로 안 써넣으면 .htm등 이런 파일로 자동으로 써 넣어진다. 업로드 될 때 파일 이름이 확장자도 포함하고 있는 이름이라, 헤더의 Content-disposition 속성에 추가해 주는 게 좋다.
반환 타입은 ResponseEntity<Resouce(UrlResouce의 조상 인터페이스)>이고,
바디에 그 UrlResouce를 넣어 반환했다.
그러면,
<a th:if="${item.attachFile}" th:href="|/attach/${item.id}|" th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
저 경로 /attach/id와 맞춰 놓았으므로,
클릭하면 downloadAttach()가 호출되고,
거기에 링크로 Stream타입의 Resouce가 들어가고, attachment해놨으므로 첨부 형태이므로 다운로드 되는 형태이다.
'스프링 > 4. 스프링 MVC-2' 카테고리의 다른 글
86. 스프링의 파일 업로드 (0) | 2023.09.25 |
---|---|
85. 서블릿으로 실제 서버에 파일 업로드 (0) | 2023.09.25 |
84. 파일 업로드 (0) | 2023.09.24 |
83. 스프링이 제공하는 기본 포맷터 (0) | 2023.09.24 |
82. 포맷터 웹 어플리케이션(스프링, 컨버전서비스)에 등록 (0) | 2023.09.24 |