241108/241111 파일 업로드/다운로드

241108/241111 에이콘 아카데미 수업을 기반하여 작성되었음을 알립니다.

파일 업로드 / 다운로드

파일 업로드 / 다운로드는 스프링에서 내부적으로 지원!

웹 프로젝트 내에 업로드 폴더를 만들고 해당 폴더에 업로드 / 다운로드를 진행하면 된다!

방법 1 (가장 일반적인 방법)
업로드 폴더 (외장 하드)에 담아두고 DB에는 해당 파일의 경로명 및 파일명만 저장하고, 경로명 및 파일명을 읽어와 show 해주면 된다.

방법 2
업로드 폴더말고 DB에 직접 저장할 수 있다. 장점으로는 DB만 들고다니면 된다. 단점으로는 수가 많아지면, 용량이 커지만 DB 용량도 커질 수 있다.

SPA 구현 시 지능형 웹까지 쓸 수 있다면 거의 배울거는 다 배웠다고 볼 수 있다. 자바스크립트 라이브러리도 써보자~

웹표준, UX/UI에 관한 질문 대비!

업로드는 반드시 post 방식으로 넘겨줘야함 바디에 넣어줘야 한다.

BindingResult 입력 자료 검사
https://velog.io/@imcool2551/Spring-%EA%B2%80%EC%A6%9D1-BindingResult-MessageCodesResolver

System.exit(0); // 응용프로그램의 무조건 종료, 프로그래머가 의도한 종료, 0의외의 숫자는 프로그래머가 의도하지 않은 종료

return, break, System.exit(0) 각각의 기능들을 알아보자!

업로드 / 다운로드 실습 1

dependencies

Spring Web / Spring Boot DevTools / Lombok / Thymeleaf

업로드 폴더 생성

src/main/resources/static/upload 폴더 생성

application.properties

spring.application.name=sprweb40fileupload

server.port=80
spring.thymeleaf.cache=false

spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

DTO

package pack.controller;

import org.springframework.web.multipart.MultipartFile;

import lombok.Data;

@Data
public class UploadDto {
	private String myName;
	private MultipartFile myFile;
}

UploadFile

package pack.controller;

import org.springframework.web.multipart.MultipartFile;

import lombok.Data;

@Data
public class UploadFile {
	private MultipartFile myFile;
}

UploadController

package pack.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class UploadController {
	@GetMapping("/upload")
	public String abc() {
		return "uploadform";
	}
	
	@PostMapping("/upload")
	public String submit(UploadFile uploadFile, Model model, BindingResult result) { // BindingResult 입력 자료 검사
		InputStream inputStream = null;
		OutputStream outputStream = null;
		
		// 업로드 파일 검사
		MultipartFile mfile = uploadFile.getMyFile();
		String fileName = mfile.getOriginalFilename(); // getOriginalFilename() 파일명 잡을 수 있음
		if(result.hasErrors()) {
			return "err";
		}
		
		try {
			inputStream = mfile.getInputStream();
			File newFile = new File("C:/work/sprsou/sprweb40fileupload/src/main/resources/static/upload/" + fileName); // 업로드 폴더 지정 (절대경로)
			if(!newFile.exists()) { // 파일이 없다면
				newFile.createNewFile(); // 해당 경로에 파일을 만들어 줌
			}
			outputStream = new FileOutputStream(newFile);
			int read = 0;
			byte[] bytes = new byte[1024]; // 파일은 기본이 1KB
			while((read = inputStream.read(bytes)) != -1) {
				outputStream.write(bytes, 0, read); // 1KB부터 계속 읽음 outputStream에 담음
			}
		} catch (Exception e) {
			System.out.println("submit err : " + e);
			return "err";
		} finally {
			try {
				inputStream.close();
				outputStream.close(); // close 시 가비지 컬렉터를 만나 사용하지 않은 힙영역의 데이터를 지움
			} catch (Exception e2) {
				// TODO: handle exception
			}
		}
		
		model.addAttribute("filename", fileName);
		return "uploadfile";
	}
}
BindingResult 입력 자료 검사
getOriginalFilename() 파일명 잡을 수 있음
close 시 가비지 컬렉터를 만나 사용하지 않은 힙영역의 데이터를 지움
경로명에 한글들어가면 안됨!

Downloadcontroller

package pack.controller;

import java.io.File;

import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import jakarta.servlet.http.HttpServletResponse;

@Controller
public class DownloadController {
	@PostMapping("download")
	@ResponseBody // 리턴값을 그대로 줌!
	public byte[] downProcess(HttpServletResponse response,
			@RequestParam("filename")String filename) throws Exception {
		System.out.println("filename : " + filename);
		
		File file = new File("C:/work/sprsou/sprweb40fileupload/src/main/resources/static/upload/" + filename); // 다운로드할 파일을 읽어옴
		byte[] bytes = FileCopyUtils.copyToByteArray(file); // 다운로드 파일을 직렬화
		
		String fn = new String(file.getName().getBytes(), "iso_8859_1");
		
		response.setHeader("Content-Disposition", "attachment;filename=\"" + fn + "\"");
		// 내보내기!, 브라우저에 다운로드 명령을 내리면 해당 파일을 파싱하지 않음, 다운로드 함!
		// 브라우저는 파싱할 수 없으면 다운로드 한다
		response.setContentLength(bytes.length);
		return bytes;
	}
}
다운로드 MIME 타입에 없는 파일이라면 그 책임을 브라우저(클라이언트)에게 넘김

index

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="upload">파일 업로드</a>
</body>
</html>

uploadform

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
* 파일 업로드/다운로드 *<br/> <!-- 파일 업로드 시 enctype="multipart/form-data" 반드시 해주어야함, 안해주면 파일명만 넘어감 -->
<form th:action="@{upload}" method="post" enctype="multipart/form-data">
업로드할 파일 선택 : <input type="file" name="myFile" ><br/>
<!-- 여러개 선택하려면 multiple="multiple", name은 DTO와 똑같이 맞춰줘야함! -->
<input type="submit" value="업로드 확인">
</form>
</body>
</html>
파일 업로드 시 enctype="multipart/form-data" 반드시 해주어야함, 안해주면 파일명만 넘어감
파일을 여러 개 선택하려면 multiple="multiple"
name은 DTO와 똑같이 맞춰줘야함!
파일선택 창은 일반적으로 모달 사용!, 모달리스를 사용할 때도 있을 수 있다.

uploadfile

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>업로드된 파일 정보</h3>
파일명 : [[${filename}]]
<br/><br/>
<form th:action="@{download}" method="post">
	<input type="hidden" name="filename" th:value="${filename}">
	<input type="submit" value="다운로드">
</form>
</body>
</html>

Refesh using native hooks or polling 체크해주기!!!

결과


업로드 / 다운로드 실습 2 (사진첩, DB 연동)

테이블 2개 이용

create table users(
id bigint auto_increment primary key,
username varchar(20) not null,
password varchar(255) not null,
email varchar(50) not null unique
);

insert into users values(1,'한국인','1111','han@abc.com');
insert into users values(1,'미국인','1111','mi@abc.com');

create table photos(
id bigint auto_increment primary key,
userid bigint not null,
albumname varchar(100),
title varchar(100),
description text,
filepath varchar(255) not null,
uploadat timestamp default current_timestamp,
foreign key(userid) references users(id) on delete cascade);
photos : users N : 1 관계로만 연관 관계 매핑 예정이다.
on delete cascade 유저를 지우면 해당 유저가 등록한 사진까지 한번에 지워줌
위험함, 별로 권장하는 방법은 아님, 별도 코드를 짜는게 더 좋다!

dependencies

application.properties

spring.application.name=sprweb41album

server.port=80
spring.thymeleaf.cache=false

# mariadb server connect
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/memberdb
spring.datasource.username=root
spring.datasource.password=1111
# jpa
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect

# upload directory
file.upload-dir=./uploads

PhotoDto

package pack.model;

import java.time.LocalDateTime;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PhotoDto {
	private Long id;
	private Long userid;
	private String albumname;
	private String title;
	private String description;
	private String filepath;
	private LocalDateTime uploadat;
}

UserDto

package pack.model;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserDto {
	private Long id;
	private String username;
	private String password;
	private String email;
}

Photo (Entity)

package pack.entity;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import pack.model.PhotoDto;

@Entity
@Table(name = "photos")
@Getter
@Setter
public class Photo {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	// user와 다대일 관계
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "userid", nullable = false)
	private User user;
	
	@Column(length = 100)
	private String albumname;
	
	@Column(length = 100)
	private String title;
	
	@Column(columnDefinition = "TEXT") // 테이블의 text type으로 mapping
	private String description;
	
	@Column(nullable = false, length = 255)
	private String filepath;
	
	@Column(nullable = false, updatable = false)
	private LocalDateTime uploadat;
	
	@PrePersist // @PrePersist 때문에 자동 호출 uploadat 저장되기 전에 먼저 실행되어 시스템의 날짜와 시간이 저장됨
	protected void onCreate() {
		uploadat = LocalDateTime.now();
	}
	
	// toDto
	public PhotoDto toDto() {
		PhotoDto dto = new PhotoDto();
		dto.setId(this.id);
		dto.setUserid(user.getId());
		dto.setAlbumname(this.albumname);
		dto.setTitle(this.title);
		dto.setDescription(this.description);
		dto.setFilepath(this.filepath);
		dto.setUploadat(this.uploadat);
		return dto;
	}
	
	// toEntity
	public static Photo toEntity(PhotoDto dto, User user) {
		Photo photo = new Photo();
		photo.setId(dto.getId());
		photo.setUser(user);
		photo.setAlbumname(dto.getAlbumname());
		photo.setTitle(dto.getTitle());
		photo.setDescription(dto.getDescription());
		photo.setFilepath(dto.getFilepath());
		photo.setUploadat(dto.getUploadat());
		return photo;	
	}
}

User (Entity)

package pack.entity;

import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import pack.model.UserDto;

@Entity
@Table(name = "users")
@Getter
@Setter
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@Column(nullable = false, length = 20)
	private String username;
	
	@Column(nullable = false, length = 255)
	private String password;
	
	@Column(nullable = false, length = 50, unique = true)
	private String email;
	
	@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Photo> photos;
	
	// references users(id) on delete cascade 따라가줌!
	// orphanRemoval = true : 연관된 자식 엔티티가 부모 엔티티와의 관계에서 제외될 경우 자식 엔티티 자동 삭제
	// cascade = CascadeType.ALL와 항상 같이 써줌!

	// toDto
	public UserDto toDto() {
		UserDto dto = new UserDto();
		dto.setId(this.id);
		dto.setUsername(this.username);
		dto.setPassword(this.password);
		dto.setEmail(this.email);
		return dto;
	}
	
	// toEntity
	public static User toEntity(UserDto dto) {
		User user = new User();
		user.setId(dto.getId());
		user.setUsername(dto.getUsername());
		user.setPassword(dto.getPassword());
		user.setEmail(dto.getEmail());
		return user;
	}
}

PhotoRepository

package pack.model;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import pack.entity.Photo;

public interface PhotoRepository extends JpaRepository<Photo, Long> {
	List<Photo> findByUserId(Long userid);
}

UserRepository

package pack.model;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import pack.entity.User;

public interface UserRepository extends JpaRepository<User, Long> {
	// JPA는 기본적으로 Optinal을 리턴
	Optional<User> findByUsername(String username);
	Optional<User> findByEmail(String email);
}

PhotoService

package pack.service;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import pack.entity.Photo;
import pack.entity.User;
import pack.model.PhotoDto;
import pack.model.PhotoRepository;
import pack.model.UserRepository;

@Service
public class PhotoService {
	@Autowired
	private PhotoRepository photoRepository;
	
	@Autowired
	private UserRepository userRepository;
	
	// 저장
	public PhotoDto savePhoto(PhotoDto photoBean) { // PhotoDto를 폼빈 대신 사용
		User user = userRepository
				.findById(photoBean.getUserid())
				.orElseThrow(() -> new RuntimeException("User not found"));
		Photo photo = Photo.toEntity(photoBean, user);
		Photo savedPhoto = photoRepository.save(photo);
		return savedPhoto.toDto(); // 리턴값인 PhotoDto로 바꿈
	}
	
	// 특정 userid를 가진 사용자의 사진 목록을 가져와 PhotoDto 객체 리스트로 반환하기
	public List<PhotoDto> getPhotoByUserid(Long userid) {
		return photoRepository.findByUserId(userid)
				.stream()
				.map(Photo :: toDto)
				.collect(Collectors.toList());
	}
	
	// 사진 업데이트, 특정 앨범의 사진 조회, 특정 날짜 범위의 사진 조회, ...
}

UserService

package pack.service;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import pack.entity.User;
import pack.model.UserDto;
import pack.model.UserRepository;

@Service
public class UserService { // user 저장, user 읽기
	@Autowired
	private UserRepository userRepository;
	
	// 유저 생성
	public UserDto createUser(UserDto userDto) {
		User user = User.toEntity(userDto);
		User savedUser = userRepository.save(user);
		return savedUser.toDto();
	}
	
	// 유저 읽어오기
	public Optional<UserDto> getUserById(Long id) {
		return userRepository.findById(id).map(User :: toDto);
	}
	
	// 사용자 관련 여러 추가 작업들이 있을 수 있다...
	// 이메일로 사용자 검색, 삭제, .. 등등
}

WebConfig

package pack.config;

import java.nio.file.Path; // import 주의!
import java.nio.file.Paths;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration // 환경 설정!, pack 패키지 밑에 있어야 스캔 됨!
public class WebConfig implements WebMvcConfigurer {
	// WebMvcConfigurer : MVC 환경 설정을 해주는 인터페이스
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) { // 일반 메소드임, 추상 메소드를 일반화 시켜놓은 메소드!
		// 정적 리소스 (이미지, CSS, JS 등) 경로 추가 설정 담당. upload 경로 지정이 우리 목적
		Path uploadDir = Paths.get("./uploads");
		// uploads 절대 경로 얻기
		String uploadPath = uploadDir.toFile().getAbsolutePath();
		
		// 예를 들어 /uploads/test.png 라는 url이 들어오면 uploads 디렉토리 내에 test.png를 반환
		registry.addResourceHandler("/uploads/**")
			.addResourceLocations("file:" + uploadPath + "/");
		// "file:" + uploadPath + "/" : 파일 시스템의 uploads 디렉토리 경로를 나타냄.
		// "file:" 접두사를 붙임으로 해서 이 경로가 파일 시스템의 경로임을 지정한다.
		// "file:" 해당 접두사는 WebMvcConfigurer 인터페이스가 원하는 구조이다!
	}
}

Controller

package pack.controller;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource; // import wndml
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import jakarta.servlet.http.HttpSession;
import pack.entity.Photo;
import pack.entity.User;
import pack.model.PhotoDto;
import pack.model.PhotoRepository;
import pack.model.UserRepository;
import pack.service.PhotoService;
import pack.service.UserService;

@Controller
public class PhotoController {
	@Autowired
	private HttpSession session;
	
	@Value("${file.upload-dir}")
	private String uploadDir;
	
	@Autowired
	private PhotoService photoService;
	
	@Autowired
	private UserService userService;
	
	@Autowired
	private PhotoRepository photoRepository;
	
	@Autowired
	private UserRepository userRepository;
	
	@GetMapping("/")
	public String sijak() {
		return "login";
	}
	
	@GetMapping("/login")
	public String showLoginForm() {
		return "login";
	}
	
	@PostMapping("/login")
	public String login(@RequestParam("userid")Long userid,
			@RequestParam("password")String password,
			Model model) {
		Optional<User> user = userRepository.findById(userid); // 유저 읽기
		if(user.isPresent() && user.get().getPassword().equals(password)) { // 클라이언트 입력 비밀번호와 DB에 저장된 비밀번호 비교
			session.setAttribute("useridsession", userid); // 세션 생성
			return "index";
		} else {
			model.addAttribute("error", "로그인 정보를 정확히 입력하세요");
			return "login";
		}
	}
	
	// 사진첩 보기
	@GetMapping("/show")
	public String viewPhoto(Model model, HttpSession session) {
		Long userid = (Long) session.getAttribute("useridsession"); // userid 얻기
		
		if(userid == null) {
			return "redirect:/login";
		}
		List<PhotoDto> photos = photoService.getPhotoByUserid(userid);
		if(photos.isEmpty()) {
			model.addAttribute("message", "현재 등록된 사진이 없습니다. 사진을 업로드 하세요");
			return "upload";
		}
		model.addAttribute("photos", photos);
		return "show";
	}
	
	@GetMapping("/upload")
	public String uploadForm() {
		return "upload";
	}
	
	// 사진 업로드 처리
	@PostMapping("/upload")
	public String uploadPhoto(@RequestParam("file")MultipartFile file,
			@RequestParam(value = "albumname", required = false)String albumname,
			@RequestParam(value = "title", required = false)String title,
			@RequestParam(value = "description", required = false)String description,
			Model model) {
		try {
			Long userid = (Long)session.getAttribute("useridsession");
			if(userid == null) {
				return "redirect:/login";
			}
			// 로그인이 인증된 경우 작업 계속
			User user = userRepository.findById(userid)
					.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없어")); // 조회 결과가 없을 때 예외 던지기
			
			String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();  // 파일명이 같은 경우 파일명에 시간을 넣어준다.
			System.out.println("fileName : " + fileName);
			
			Path uploadPath = Paths.get(uploadDir);
			
			if(Files.notExists(uploadPath)) {
				Files.createDirectories(uploadPath); // 업로드 디렉토리가 있을 경우 생성
			}
			// resolve : fileName을 결합하여 최종 파일경로를 생성하는 메소드
			Path filePath = uploadPath.resolve(fileName);
			
			// 파일을 특정 경로로 복사하기, 이미 동일 파일 존재 시 덮어쓰기
			// Files.copy() : InputStream으로부터 파일을 읽어 filePath 위치에 파일을 저장함
			Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
			
			// 사진 저장용 객체 생성
			PhotoDto photoDto = new PhotoDto();
			photoDto.setUserid(userid);
			photoDto.setAlbumname(albumname);
			photoDto.setTitle(title);
			photoDto.setDescription(description);
			photoDto.setFilepath(filePath.toString());

			photoService.savePhoto(photoDto);
			
			return "redirect:/show"; // 사진 저장 후 사진첩 보기
		} catch (Exception e) {
			System.out.println("uploadPhoto err : " + e);
			model.addAttribute("message", "파일 업로드 실패 : " + e.getMessage());
			return "upload";
		}
	}
	
	// 이미지 다운로드
	// ResponseEntity : Spring에서 파일(이미지) 등의 데이터 파일을 HTTP 응답으로 보내기 위한 객체
	// Resource 타입을 사용해 다운로드할 파일을 응답 본문에 포함시키는 방식으로 HTTP 응답을 구성
	@GetMapping("/download/{photoId}")
	public ResponseEntity<Resource> downloadPhoto(@PathVariable(name = "photoId")Long photoId) {
		Photo photo = photoRepository.findById(photoId)
				.orElseThrow(() -> new RuntimeException("사진이 없어"));
		try {
			// normalize() : abc/./test.png -> abc/test.png 경로 중간에 불필요한 값을 제거해줌
			Path filePath = Paths.get(photo.getFilepath()).normalize();
			System.out.println("파일 실제 경로 : " + filePath);
			Resource resource = new UrlResource(filePath.toUri()); // URI 형식으로 변환 file:///
			System.out.println(filePath.getFileName());
			
			// 다운로드할 파일 존재 및 읽기 가능 여부 판단 후 작업 계속
			if(resource.exists() && resource.isReadable()) {
				String originalFileName = filePath.getFileName().toString();
				// Mime type을 파악하고 타입을 결정
				String contentType = Files.probeContentType(filePath);
				System.out.println("contentType : " + contentType);
				
				if(contentType == null) {
					contentType = "application/octet-stream"; // 해당 타입은 브라우저가 해석 불가하여 다운로드함!
				}
				return ResponseEntity.ok()
						.contentType(MediaType.parseMediaType(contentType))
						.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + originalFileName + "\"")
						.body(resource);
					
			} else {
				throw new RuntimeException("파일을 찾을 수 없거나 읽기 불가");
			}
			
		} catch (Exception e) {
			System.out.println("downloadPhoto err : " + e);
			throw new RuntimeException("다운로드 중 에러 발생 : " + e.getMessage());
		}
	}
}

login

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>로그인</h1>
<form th:action="@{/login}" method="post">
사용자 id : <input type="number" id="userid" name="userid" required="required"><br/>
비밀  번호 : <input type="text" id="password" name="password" required="required"><br/>
<button type="submit">로그인</button>
</form>

<div th:if="${error}">
  <p style="color: red;" th:text="${error}"></p>
</div>
</body>
</html>

index

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>사진첩 작업을 진행합니다.</h2>
<div>
  <a th:href="@{/show}">사진첩 보기 (자료가 없으면 업로드 창으로 이동함)</a>
  <br/><br/>
  <a th:href="@{/upload}">사진 업로드</a>
</div>
</body>
</html>

upload

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>사진 업로드</h2>
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<table>
  <tr>
    <td>사용자 id</td>
    <td><input type="number" min="1" id="userid" name="userid" required="required"></td>
  </tr>
  <tr>
    <td>사진 파일</td>
    <td><input type="file" id="file" name="file" accept="image/*" required></td>
  </tr>
  <tr>
    <td>앨범 이름</td>
    <td><input type="text" id="albumname" name="albumname"></td>
  </tr>
  <tr>
    <td>제  목</td>
    <td><input type="text" id="title" name="title" style="width: 100%;"></td>
  </tr>
  <tr>
    <td>설  명</td>
    <td><textarea id="description" name="description" rows="5" style="width: 100%;"></textarea></td>
  </tr>
  <tr>
    <td colspan="2" style="text-align: center;">
      <button type="submit">업로드 확인</button>
    </td>
  </tr>
</table>
</form>
<!-- 에러 메시지 표시 -->
<div th:if="${message}">
  <p style="color: red;" th:text="${message}"></p>
</div>
</body>
</html>

show

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style type="text/css">
.gallery{
	display: flex;
	flex-wrap: wrap;
}

.photo{
	margin: 10px;
	border: 1px solid #ccc;
	padding: 10px;
	width: 200px;
}

.photo img {
	max-width: 100%;
	height: auto;
}
</style>
</head>
<body>
<h1>사진첩</h1>
<div>
  <a th:href="@{/upload}">사진 업로드</a>
</div>
<div class="gallery" th:if="${photos != null && !photos.isEmpty()}">
  <div class="photo" th:each="photo : ${photos}">
  <!-- download 속성은 브라우저가 새창에서 열지 않고 자동으로 다운로드 하도록 지시함 -->
    <a th:href="@{/download/{photoId}(photoId=${photo.id})}" download>
      <img th:src="@{${photo.filepath}}" alt="Photo">
    </a>
    <h3 th:text="${photo.title}">title</h3>
    <p th:text="${photo.description}">description</p>
    <p th:text="'앨범 : ' + ${photo.albumname}">albumname</p>
    <p th:text="${photo.uploadat}">upload at</p>
  </div>
</div>
<p th:if="${photos == null || photos.isEmpty()}">등록된 사진이 없습니다.</p>
</body>
</html>

 

결과

업로드 확인

사진첩 보기!

다운로드 확인

'Study > Acorn' 카테고리의 다른 글

241113 WebSocket / 리액트 설치  (1) 2024.11.13
241112 파일 업로드 (DB) / Security  (0) 2024.11.12
241107 RESTful / AOP  (3) 2024.11.07
241106 RESTful (CRUD/문제)  (1) 2024.11.07
241105 RESTful (PUT/DELETE/문제)  (0) 2024.11.05