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 |