포토그램

[Chapter 5] 프로필 페이지

yukuda 2024. 2. 22. 21:19
728x90

43. 프로필 페이지-Image 모델 만들기

  1. 포토 이미지 등록
  • multipart/form-data
  • UUID

 

  1. 포토 이미지 렌더링

 

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;
	} // 더티체킹이 일어나서 업데이트가 완료됨.
}