[인스타그램 클론 코딩][Spring boot] 7. 프로필 페이지
- 포토 이미지 등록
- multipart/form-data
- UUID
- 포토 이미지 렌더링
- 엔티티에 파일이 아닌 url 저장
- 사진을 전송받으면 그 사진을 서버에 특정 폴더에 저장하게 될 것임(DB에 그 경로를 insert 할 것!)
📌 Image 엔티티 구현
package yerong.InstagramCloneCoding.domain.image;
import jakarta.persistence.*;
import lombok.*;
import yerong.InstagramCloneCoding.domain.user.User;
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
@Entity
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "image_id")
private Long id;
private String caption;
private String postImageUrl;
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
//좋아요
//댓글
}
- 사진의 경로와 캡션(글 내용)을 갖는다
- User와는 다대일 매핑으로 한다
- Lazy 설정
- Lazy : User를 Select 할 때 해당 UserId로 등록된 image들을 가져오지 않다가 getImages() 함수가 호출될 때 가져옴
- Eager : User를 Select 할 때 해당 UserId로 등록된 Image들을 전부 Join 해서 가져옴
⚡️ User 엔티티에도 양방향 연관관계 설정
package yerong.InstagramCloneCoding.domain.user;
import jakarta.persistence.*;
import lombok.*;
import yerong.InstagramCloneCoding.domain.BaseTimeEntity;
import yerong.InstagramCloneCoding.domain.image.Image;
import yerong.InstagramCloneCoding.domain.subs.Subscribe;
import java.util.ArrayList;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
@ToString(of = {"id", "username", "password", "email", "name", "role", "bio", "website", "phone", "gender"})
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(nullable = false, unique = true, length = 20)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
private String bio; //자기소개
private String website;
private String phone;
private String gender;
@OneToMany(mappedBy = "user")
@JsonIgnoreProperties({"user"})
private List<Image> images = new ArrayList<>();
public void setRole(Role role){
this.role = role;
}
public void updateName(String name){
this.name = name;
}
public void updatePassword(String password){
this.password = password;
}
public void updateBio(String bio){
this.bio = bio;
}
public void updateWebsite(String website){
this.website = website;
}
public void updatePhone(String phone){
this.phone = phone;
}
public void updateGender(String gender){
this.gender = gender;
}
public void addImage(Image image){
this.images.add(image);
}
}
- mappedBy : 연관관계의 주인이 아니라는 것
- @ToString을 사용할 때는 연관관계를 꼭 넣지 말아야 함!!!
- @JsonIgnoreProperties({"user"}) ➡️ 양방향 순환 참조를 막아줌 (JSON으로 응답할 때 getter가 발생하는 것을 얘는 제외시킴)
📌 Image 리포지토리 구현
package yerong.InstagramCloneCoding.repository.image;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import yerong.InstagramCloneCoding.domain.image.Image;
@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {
}
📌 ImageUpdateDto
package yerong.InstagramCloneCoding.web.dto.image;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
import yerong.InstagramCloneCoding.domain.image.Image;
import yerong.InstagramCloneCoding.domain.user.User;
@Getter
@Setter
public class ImageUploadDto {
private String caption;
private MultipartFile file;
public Image toEntity(String postImageUrl, User user){
return Image.builder()
.caption(caption)
.postImageUrl(postImageUrl)
.user(user)
.build();
}
}
- 사진은 MultipartFile을 이용해서 파일 받음
- MultipartFile은 @NotBlank가 지원되지 않음
- 예외처리는 따로 (밑에서 설명)
📌 appilication.yml
servlet:
multipart:
enabled: true
max-file-size: 2MB
- 이미지 처리
file:
path: /Users/...
- 업로드된 사진을 저장할 파일을 생성 후 그 파일의 경로를 application.yml에 설정해둔다
- 업로드된 사진을 저장하는 폴더는 무조건 프로젝트 외부에 두어야 한다
- deploy하는 시간 때문에 사진을 못 찾을 수도 있다
- redirect 페이지로 가는 시간이 더 빠를 수도 있음
📌 Image 서비스 구현
package yerong.InstagramCloneCoding.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import yerong.InstagramCloneCoding.domain.image.Image;
import yerong.InstagramCloneCoding.domain.user.User;
import yerong.InstagramCloneCoding.handler.exception.CustomValidationApiException;
import yerong.InstagramCloneCoding.repository.image.ImageRepository;
import yerong.InstagramCloneCoding.repository.user.UserRepository;
import yerong.InstagramCloneCoding.service.ImageService;
import yerong.InstagramCloneCoding.web.dto.image.ImageUploadDto;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class ImageServiceImpl implements ImageService {
private final ImageRepository imageRepository;
private final UserRepository userRepository;
@Value("${file.path}")
private String uploadFolder;
@Override
@Transactional
public void imageUpload(Long userId, ImageUploadDto imageUploadDto){
UUID uuid = UUID.randomUUID();
String imageFileName = uuid + "_" + imageUploadDto.getFile().getOriginalFilename();
Path imageFilePath = Paths.get(uploadFolder+imageFileName);
try{
Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
}
catch (Exception e){
e.printStackTrace();
}
User user = userRepository.findById(userId).orElseThrow(() -> new CustomValidationApiException("찾을 수 없는 Id 입니다."));
Image image = imageUploadDto.toEntity(imageFileName, user);
Image savedImage = imageRepository.save(image);
user.addImage(savedImage);
}
}
- getOriginalFilename()을 통해서 파일명을 가져올 수 있음
➡️ 하지만, 파일명을 그대로 서버에 저장하면 같은 파일명의 사진이 또 들어올 수가 없게 되므로 애초에 서버에서 파일명들이 중복되지 않도록 따로 파일 이름을 바꿔서 저장해줘야한다. → UUID- UUID란? : 네트워크 상에서 고유성이 보장되는 id를 만들기 위한 표준 규약
- “_”를 붙인 이유는 혹시나 몇억분의 일로 uuid가 겹치는 상황이 생겨날까봐 겹칠 확률을 더 낮춰준다.
📌 CustomException(예외 처리)
package yerong.InstagramCloneCoding.handler.exception;
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
📌 ControllerExceptionHandler에 해당 코드 추가
@ExceptionHandler(CustomException.class)
public String exception(CustomException e){
return Script.back(e.getMessage());
}
📌 Image 컨트롤러 구현
package yerong.InstagramCloneCoding.web.controller;
import lombok.RequiredArgsConstructor;
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 yerong.InstagramCloneCoding.config.auth.PrincipalDetails;
import yerong.InstagramCloneCoding.handler.exception.CustomValidationException;
import yerong.InstagramCloneCoding.service.ImageService;
import yerong.InstagramCloneCoding.web.dto.image.ImageUploadDto;
@Controller
@RequiredArgsConstructor
public class ImageController {
private final ImageService imageService;
@GetMapping({"/", "/image/story"})
public String story(){
return "views/image/story";
}
@GetMapping("/image/popular")
public String popular(){
return "views/image/popular";
}
@GetMapping("/image/upload")
public String upload(){
return "views/image/upload";
}
@PostMapping("/image")
public String imageUpload(
ImageUploadDto imageUploadDto,
@AuthenticationPrincipal PrincipalDetails principalDetails
){
if(imageUploadDto.getFile().isEmpty()){
throw new CustomValidationException("이미지가 첨부되지 않았습니다.", null);
}
imageService.imageUpload(principalDetails.getUser().getId(), imageUploadDto);
return "redirect:/user/" + principalDetails.getUser().getId();
}
}
- user의 프로필이 보이는 화면으로 redirect 해줌
📌 User에서 Image 정보들을 받아와서 model에 넘겨주게 수정
package yerong.InstagramCloneCoding.web.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.web.bind.annotation.PutMapping;
import yerong.InstagramCloneCoding.config.auth.PrincipalDetails;
import yerong.InstagramCloneCoding.domain.user.User;
import yerong.InstagramCloneCoding.service.UserService;
@Controller
@Slf4j
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/user/{id}")
public String profile(@PathVariable("id") Long id, Model model){
User user = userService.profile(id);
model.addAttribute("user", user);
return "views/user/profile";
}
@GetMapping("/user/{id}/update")
public String update(@PathVariable("id") Long id, @AuthenticationPrincipal PrincipalDetails principalDetails, Model model){
// principalDetails.getUser() 로 편리하게 세션 사용
/**
* 어려운 방법 (세션 정보 직접 찾기)
* Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
* PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
* principal.getUser();
*/
model.addAttribute("principal", principalDetails.getUser());
return "views/user/update";
}
}
📌 UserService에 프로필에 사용할 User를 가져오는 코드 구현
@Override
@Transactional
public User profile(Long userId){
return userRepository.findById(userId).orElseThrow(() -> new CustomException("해당 프로필 페이지는 없는 페이지입니다."));
}
📌 WebMvcConfig
package yerong.InstagramCloneCoding.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 {
@Value("${file.path}")
private String uploadFolder;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
registry
.addResourceHandler("/upload/**")
.addResourceLocations("file:///" + uploadFolder+"/")
.setCachePeriod(60 * 10 * 6)
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
}
- Web 설정 파일
- .addResourceHandler("/upload/**") : html 파일에서 /upload/** 유형의 주소들이 나오면 발동함
- 60 * 10 * 6 => 한시간
- 엔티티로는 파일 이름만 넘어가기 때문에 여기서 전체 경로 및 "file:///"도 추가하여 폴더에서 사진을 꺼내오게끔 설정하는 것임
📌 open-in-view
1. 클라이언트로부터 요청을 받으면
2. 디스패처에서 맞는 controller를 찾은 후 세션이 생성되며 해당 로직에 있는 service를 찾는다.
3. service에서는 repository로 이동하여 데이터를 찾는다.
4. repository에는 영속성 컨텍스트가 있으며 영속성 컨텍스트에 데이터가 있다면 이것을 사용하고 없다면 DB에서 데이터를 가져올 수 있다.(영속성 컨텍스트에 올라옴)
5. 순서대로 로직을 마치면 서비스 앞에서 세션이 종료된다.
6. controller에서 Lazy로딩으로 설정한 연관관계의 데이터를 불러오려면 세션이 종료되었기 때문에 불가능하다.
➡️ application.yml
spring:
jpa:
open-in-view: true
이렇게 설정한다면 세션이 Controller 앞에서 종료되므로 Lazy 로딩으로 데이터를 가져오는 것이 가능해진다.
📌 내 페이지에만 사진 등록 버튼이 보여야하고, 다른 사람들의 페이지는 보이면 안됨
⚡️ UserProfileDto
package yerong.InstagramCloneCoding.web.dto.user;
import lombok.*;
import yerong.InstagramCloneCoding.domain.user.User;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserProfileDto {
private boolean pageOwnerState;
private int imageCount;
private User user;
}
➡️ 나의 페이지인지 다른 사람의 페이지인지 구분할 Dto
⚡️ UserServiceImpl
@Override
@Transactional
public UserProfileDto profile(Long pageUserId, Long pageOwnerId){
UserProfileDto dto = new UserProfileDto();
User pageUser = userRepository.findById(pageUserId).orElseThrow(() -> new CustomException("해당 프로필 페이지는 없는 페이지입니다."));
dto.setUser(pageUser);
dto.setImageCount(pageUser.getImages().size());
dto.setPageOwnerState(pageOwnerId.equals(pageUserId));
return dto;
}
- 접속한 페이지의 userId와 로그인된 사용자의 userId를 모두 받아와서 둘을 비교하며 나의 페이지인지, 다른 사람의 page인지 구분한다.
- ImageCount도 여기서 해결(프론트에서 계산하지 말고 모든 것을 넘겨주는 것이 좋음)
⚡️ UserController
@GetMapping("/user/{pageUserId}")
public String profile(
@PathVariable("pageUserId") Long pageUserId,
Model model,
@AuthenticationPrincipal PrincipalDetails principalDetails){
UserProfileDto userDto = userService.profile(pageUserId, principalDetails.getUser().getId());
model.addAttribute("userDto", userDto);
return "views/user/profile";
}
- 요청된 page의 userId와 로그인된 사용자의 userId를 모두 받아와 서비스에 넘긴다.
![](https://t1.daumcdn.net/keditor/emoticon/friends1/large/002.gif)
PREV
[인스타그램 클론 코딩][Spring boot] 6. 구독하기
✅ 연관관계 N : 1 관계에서는 Fk는 Many 쪽에 둔다 N : N 의 관계에서는 중간 테이블이 필요하다 (N : 1로 나누어야 함) 📌 API 시큐리티 설정 .authorizeHttpRequests(authorizationRequest -> authorizationRequest .reques
nyeroni.tistory.com
NEXT
[인스타그램 클론 코딩][Spring boot] 8. 구독 정보 뷰 렌더링
✅ 구독 정보를 누르면 page User의 구독 정보를 볼 수 있음 ✅ 여기서 구독취소/구독하기 버튼은 로그인한 user를 기준으로 구분되어야 함. 📌 UserProfileDto package yerong.InstagramCloneCoding.web.dto.user; impor
nyeroni.tistory.com
'프로젝트 > 인스타그램 클론 코딩' 카테고리의 다른 글
[인스타그램 클론 코딩][Spring boot] 9. 스토리(메인) 페이지 (0) | 2024.02.22 |
---|---|
[인스타그램 클론 코딩][Spring boot] 8. 구독 정보 뷰 렌더링 (0) | 2024.02.21 |
[인스타그램 클론 코딩][Spring boot] 6. 구독하기 (0) | 2024.02.19 |
[인스타그램 클론 코딩][Spring boot] 5. 회원 정보 수정 (1) | 2024.02.19 |
[인스타그램 클론 코딩][Spring boot] 4. 로그인 구현 (1) | 2024.02.18 |