포토그램
[Chapter 5] 프로필 페이지
yukuda
2024. 2. 22. 21:19
728x90
43. 프로필 페이지-Image 모델 만들기
- 포토 이미지 등록
- multipart/form-data
- UUID
- 포토 이미지 렌더링
com.cos.photogramstart.domain.image.Image.java
package com.cos.photogramstart.domain.image;
import java.time.LocalDateTime;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;
import com.cos.photogramstart.domain.subscribe.Subscribe;
import com.cos.photogramstart.domain.user.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Entity // DB에 테이블을 생성
@Data
@NoArgsConstructor // 빈 생성자
@AllArgsConstructor // 전체 생성자
public class Image { // N
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 번호 증가 전략이 데이터베이스를 따라간다.
private int id;
private String caption; // 오늘 나 너무 피곤해!!
private String postImageUrl; // 사진을 전송받아서 그 사진을 서버에 특정 폴더에 저장 - DB에 그 저장된 경로를 insert
@JoinColumn(name = "userId")
@ManyToOne
private User user; // 1
// 이미지 좋아요
// 댓글
private LocalDateTime createDate;
@PrePersist // DB에 Insert가 되기 직전에 실행
public void createDate() {
this.createDate = LocalDateTime.now();
}
}
com.cos.photogramstart.domain.image.ImageRepository.java
package com.cos.photogramstart.domain.image;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ImageRepository extends JpaRepository<Image,Integer> {
}
44. 프로필 페이지-Image 서버에 업로드하기
application.yml
file:
path: C:/workspace/springbootwork/upload/
ImageController.java
package com.cos.photogramstart.web;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.service.ImageService;
import com.cos.photogramstart.web.dto.image.ImageUploadDto;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class ImageController {
private final ImageService imageService;
@GetMapping({"/","/image/story"})
public String story() {
return "image/story";
}
@GetMapping({"/image/popular"})
public String popular() {
return "image/popular";
}
@GetMapping({"/image/upload"})
public String upload() {
return "image/upload";
}
// 사용자에게 데이터를 받고 service에게 호출 해주면 된다.
@PostMapping("/image")
public String imageUpload(ImageUploadDto imageUploadDto, @AuthenticationPrincipal PrincipalDatails principalDatails) {
// 서비스 호출
imageService.사진업로드(imageUploadDto, principalDatails);
return "redirect:/user/"+principalDatails.getUser().getId(); // 업로드를 딱 누르면 /user/{?}로 오게
}
}
ImageService.java
package com.cos.photogramstart.service;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.image.ImageRepository;
import com.cos.photogramstart.web.dto.image.ImageUploadDto;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ImageService {
private final ImageRepository imageRepository;
@Value("${file.path}")
private String uploadFolder; // application.yml의 값 가져오기
public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDatails principalDatails) {
UUID uuid = UUID.randomUUID(); // uuid
String imageFileName = uuid+"_"+imageUploadDto.getFile().getOriginalFilename(); // 실제 file 이름이 들어감 1.jpg
System.out.println("이미지 파일이름: "+imageFileName);
Path imageFilePath = Paths.get(uploadFolder+imageFileName); // 경로 + file명
// 통신, I/O -> 예외가 발생할 수 있다.
try {
Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
}
upload.jsp
<!--사진업로드 Form-->
<form class="upload-form" action="/image" method="post" enctype="multipart/form-data"> <!-- file과 key-value 데이터 모두 전송하고 싶을떄 multipart/form-data 사용 -->
<input type="file" name="file" onchange="imageChoose(this)"/>
<div class="upload-img">
<img src="/images/person.jpeg" alt="" id="imageUploadPreview" />
</div>
45. 프로필 페이지-upload폴더를 프로젝트 외부에 두는 이유
deploy 시간차 문제
46. 프로필 페이지-Image DB에 업로드하기
ImageService.java
package com.cos.photogramstart.service;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.image.Image;
import com.cos.photogramstart.domain.image.ImageRepository;
import com.cos.photogramstart.web.dto.image.ImageUploadDto;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ImageService {
private final ImageRepository imageRepository;
@Value("${file.path}")
private String uploadFolder; // application.yml의 값 가져오기
public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDatails principalDatails) {
UUID uuid = UUID.randomUUID(); // uuid
String imageFileName = uuid+"_"+imageUploadDto.getFile().getOriginalFilename(); // 실제 file 이름이 들어감 1.jpg
System.out.println("이미지 파일이름: "+imageFileName);
Path imageFilePath = Paths.get(uploadFolder+imageFileName); // 경로 + file명
// 통신, I/O -> 예외가 발생할 수 있다.
try {
Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
} catch (Exception e) {
e.printStackTrace();
}
// image 테이블에 저장
Image image = imageUploadDto.toEntity(imageFileName, principalDatails.getUser()); // 5cf6237d-c404-43e5-836b-e55413ed0e49_bag.jpeg
Image imageEntity = imageRepository.save(image);
System.out.println(imageEntity);
}
}
ImageUploadDto.java
package com.cos.photogramstart.web.dto.image;
import org.springframework.web.multipart.MultipartFile;
import com.cos.photogramstart.domain.image.Image;
import com.cos.photogramstart.domain.user.User;
import lombok.Data;
@Data
public class ImageUploadDto { // 이미지를 업로드 하기 위한 Dto
private MultipartFile file;
private String caption;
public Image toEntity(String postImageUrl, User user) {
return Image.builder()
.caption(caption)
.postImageUrl(postImageUrl)
.user(user)
.build();
}
}upload.jsp required 추가(프론트단에서 막기)
47. 프로필 페이지-Image 유효성 검사하기
ImageController.java
package com.cos.photogramstart.web;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.service.ImageService;
import com.cos.photogramstart.web.dto.image.ImageUploadDto;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class ImageController {
private final ImageService imageService;
@GetMapping({"/","/image/story"})
public String story() {
return "image/story";
}
@GetMapping({"/image/popular"})
public String popular() {
return "image/popular";
}
@GetMapping({"/image/upload"})
public String upload() {
return "image/upload";
}
// 사용자에게 데이터를 받고 service에게 호출 해주면 된다.
@PostMapping("/image")
public String imageUpload(ImageUploadDto imageUploadDto, @AuthenticationPrincipal PrincipalDatails principalDatails) {
if(imageUploadDto.getFile().isEmpty()) {
throw new CustomValidationException("이미지가 첨부되지 않았습니다.",null);
}
// 서비스 호출
imageService.사진업로드(imageUploadDto, principalDatails);
return "redirect:/user/"+principalDatails.getUser().getId(); // 업로드를 딱 누르면 /user/{?}로 오게
}
}
ControllerExceptionHandler.java
package com.cos.photogramstart.handler;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import com.cos.photogramstart.handler.ex.CustomApiException;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.util.Script;
import com.cos.photogramstart.web.dto.CMRespDto;
@RestController // 낚아챈 데이터
@ControllerAdvice // 모든 exception을 낚아챔
public class ControllerExceptionHandler {
// validation 오류는 이거나 밑에꺼 쓰고
@ExceptionHandler(CustomValidationException.class)
public String vaildationException(CustomValidationException e) {
// CMRespDto, Script 비교
// 1.클라이언트에게 응답할때는 Script 좋음
// 2.Ajax통신 - CMRespDto
// 3.Android통신 - CMRespDto
if(e.getErrorMap() == null) {
return Script.back(e.getMessage());
}else {
return Script.back(e.getErrorMap().toString());
}
//return new CMRespDto<Map<String,String>>(-1,e.getMessage(),e.getErrorMap());
}
@ExceptionHandler(CustomValidationApiException.class)
public ResponseEntity<?> vaildationApiException(CustomValidationApiException e) {
return new ResponseEntity<>(new CMRespDto<>(-1,e.getMessage(),e.getErrorMap()),HttpStatus.BAD_REQUEST);
}
// 그 외의 모든 오류는 이걸 쓰면 될듯
@ExceptionHandler(CustomApiException.class)
public ResponseEntity<?> apiException(CustomApiException e) {
return new ResponseEntity<>(new CMRespDto<>(-1,e.getMessage(),null),HttpStatus.BAD_REQUEST);
}
}
48. 프로필 페이지-양방향 매핑 이해하기
UserController.java
package com.cos.photogramstart.web;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
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 com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.service.UserService;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/user/{id}")
public String profile(@PathVariable int id, Model model) {
User userEntity = userService.회원프로필(id);
model.addAttribute("user",userEntity);
return "user/profile";
}
@GetMapping("/user/{id}/update")
public String update(@PathVariable int id, @AuthenticationPrincipal PrincipalDatails principalDatails) {
// 1. 추천
System.out.println("세션 정보 : "+principalDatails.getUser());
// 2. 극혐
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
PrincipalDatails mPrincipalDatails = (PrincipalDatails) auth.getPrincipal();
System.out.println("직접 찾은 세션 정보: " + mPrincipalDatails.getUser()); // 최악
return "user/update";
}
}
UserService.java
package com.cos.photogramstart.service;
import java.util.function.Supplier;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;
import com.cos.photogramstart.handler.ex.CustomException;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public User 회원프로필(int userId) {
// SELECT * FROM image WHERE userId = :userId;
User userEntity = userRepository.findById(userId).orElseThrow(()->{
throw new CustomException("해당 프로필 페이지는 없는 페이지입니다.");
});
return userEntity;
}
@Transactional
public User 회원수정(int id, User user) {
// 1. 영속화
// 1. 무조건 찾았다. 걱정마 get() // 2. 못찾았어 익섹션 발동시킬게 orElseThrow()
User userEntity = userRepository.findById(id).orElseThrow(()->{return new CustomValidationException("찾을 수 없는 ID 입니다.");});
// 2. 영속화된 오브젝트를 수정 - 더티체킹 (업데이트 완료)
userEntity.setName(user.getName());
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
userEntity.setPassword(encPassword);
userEntity.setBio(user.getBio());
userEntity.setWebsite(user.getWebsite());
userEntity.setPhone(user.getPhone());
userEntity.setGender(user.getGender());
return userEntity;
} // 더티체킹이 일어나서 업데이트가 완료됨.
}
User.java
// 나는 연관관계의 주인이 아니다. 그러므로 테이블에 컬럼을 만들지 마
// User를 Select할 때 해당 User id로 등록된 image들을 다 가져와
// Lazy: User를 Select할 때 해당 User id로 등록된 image들을 가져오지마 - 대신 getImages() 함수의 image들이 호출될 때 가져와!
// Eager: User를 Select할 때 해당 User id로 등록된 image들을 전부 Join해서 가져와!
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Image> images; // 양방향 매핑
49. 프로필 페이지-Image 뷰 렌더링하기
이미지를 못찾아서 렌더링
profile.jsp
${user.name}
${user.bio }
${user.website}
<!--아이템들-->
<c:forEach var="image" items="${user.images}"> <!-- EL표현식에서 변수명을 적으면 get함수가 자동 호출된다. -->
<div class="img-box">
<a href=""> <img src="/upload/${image.postImageUrl}" />
</a>
<div class="comment">
<a href="#" class=""> <i class="fas fa-heart"></i><span>0</span>
</a>
</div>
</div>
</c:forEach>
com.cos.photogramstart.config.WebMvcConfig.java
package com.cos.photogramstart.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer{ // web 설정 파일
@Value("${file.path}")
private String uploadFolder;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
// file:///C:/workspace/springbootwork/upload/
registry
.addResourceHandler("/upload/**") // jsp 페이지에서 /upload/** 이런 주소 패턴이 나오면 발동
.addResourceLocations("file:///"+uploadFolder)
.setCachePeriod(60*10*6) // 1시간 이미지 캐싱
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
}
50. 프로필 페이지-open in view (중요)
51. 프로필 페이지-회원정보 수정 오류 해결하기
무한참조 막기
User.java
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@JsonIgnoreProperties({"user"}) // 무한참조 막기
private List<Image> images; // 양방향 매
52. 프로필 페이지-게시물 개수 뷰 렌더링하기
profile.jsp
<li><a href=""> 게시물<span>${user.images.size()}</span>
53. 프로필 페이지-DTO로 페이지 완성하기
내가 아니면 사진등록 버튼이 안나오게
DTO: Data Transform Object
com.cos.photogramstart.web.dto.user.UserProfileDto.java
package com.cos.photogramstart.web.dto.user;
import com.cos.photogramstart.domain.user.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserProfileDto {
private boolean PageOwnerState;
private int imageCount;
private User user;
}
profile.jsp
${dto.isPageOwnerState}
${dto.imageCount}
${dto.user.bio }
${dto.user.website}
${dto.user.images}
<c:forEach var="image" items="${dto.user.images}"> <!-- EL표현식에서 변수명을 적으면 get함수가 자동 호출된다. -->
<div class="img-box">
<a href=""> <img src="/upload/${image.postImageUrl}" />
</a>
<div class="comment">
<a href="#" class=""> <i class="fas fa-heart"></i><span>0</span>
</a>
</div>
</div>
</c:forEach>
UserController.java
package com.cos.photogramstart.web;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
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 com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.service.UserService;
import com.cos.photogramstart.web.dto.user.UserProfileDto;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/user/{pageUserId}")
public String profile(@PathVariable int pageUserId, Model model,@AuthenticationPrincipal PrincipalDatails principalDatails) {
UserProfileDto dto = userService.회원프로필(pageUserId, principalDatails.getUser().getId());
model.addAttribute("dto",dto);
return "user/profile";
}
@GetMapping("/user/{id}/update")
public String update(@PathVariable int id, @AuthenticationPrincipal PrincipalDatails principalDatails) {
// 1. 추천
// System.out.println("세션 정보 : "+principalDatails.getUser());
// 2. 극혐
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
PrincipalDatails mPrincipalDatails = (PrincipalDatails) auth.getPrincipal();
// System.out.println("직접 찾은 세션 정보: " + mPrincipalDatails.getUser()); // 최악
return "user/update";
}
}
UserService.java
package com.cos.photogramstart.service;
import java.util.function.Supplier;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;
import com.cos.photogramstart.handler.ex.CustomException;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.web.dto.user.UserProfileDto;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Transactional(readOnly=true) // select는 readOnly = true
public UserProfileDto 회원프로필(int pageUserId, int principalId) {
UserProfileDto dto = new UserProfileDto();
// SELECT * FROM image WHERE userId = :userId;
User userEntity = userRepository.findById(pageUserId).orElseThrow(()->{
throw new CustomException("해당 프로필 페이지는 없는 페이지입니다.");
});
dto.setUser(userEntity);
dto.setPageOwnerState(pageUserId == principalId);
dto.setImageCount(userEntity.getImages().size());
return dto;
}
@Transactional
public User 회원수정(int id, User user) {
// 1. 영속화
// 1. 무조건 찾았다. 걱정마 get() // 2. 못찾았어 익섹션 발동시킬게 orElseThrow()
User userEntity = userRepository.findById(id).orElseThrow(()->{return new CustomValidationException("찾을 수 없는 ID 입니다.");});
// 2. 영속화된 오브젝트를 수정 - 더티체킹 (업데이트 완료)
userEntity.setName(user.getName());
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
userEntity.setPassword(encPassword);
userEntity.setBio(user.getBio());
userEntity.setWebsite(user.getWebsite());
userEntity.setPhone(user.getPhone());
userEntity.setGender(user.getGender());
return userEntity;
} // 더티체킹이 일어나서 업데이트가 완료됨.
}