Super Kawaii Cute Cat Kaoani [인스타그램 클론 코딩][Spring boot] 7. 프로필 페이지

[인스타그램 클론 코딩][Spring boot] 7. 프로필 페이지

2024. 2. 20. 03:19
728x90
SMALL
  • 포토 이미지 등록
    • 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 페이지로 가는 시간이 더 빠를 수도 있음
728x90

📌 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를 모두 받아와 서비스에 넘긴다.

 


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

SMALL

 

728x90
LIST

BELATED ARTICLES

more