반응형

profile.html
- 이전에 followState에 따라 버튼에 어떤 메시지가 보일지를 세팅했다.
- 버튼을 클릭했을때 팔로우, 혹은 언팔로우 기능을 실행할 자바스크립트 코드를 삽입했다. 
- toggleSubscribe 를 클릭했을때 this를 전달하는데 여기서 this란 이벤트 정보를 의미한다. 

<div th:if="${dto.followState == true}">
  <button class="cta blue" th:onclick="toggleSubscribe([[${dto.user.id}]], this)">팔로우 취소</button>
</div>
<div th:if="${dto.followState == false}">
  <button class="cta" th:onclick="toggleSubscribe([[${dto.user.id}]],this)">팔로우</button>
</div>

 

profile.js
- toggleClass: 클래스의 css이름을 넣었다 뺐대 함
- toggleSubscribe는 toUserId(현재 페이지 주인의 id)와 obj를 받는다. 

1. 버튼이 '언팔로우'일때
정상적으로 끝나면 "팔로우" 와 클래스의 색을 blue로 표시하게 한다. 
2. 버튼이 '팔로우' 일때 
정상적으로 끝나면 "팔로우" 와 클래스의 색을 blue로 표시하게 한다. 

function toggleSubscribe(toUserId, obj) {
   if ($(obj).text() === "팔로우 취소") {
       $.ajax({
        type:"delete",
        url: "/api/follow/"+toUserId,
        dataType: "json"
       }).done(res=>{
              $(obj).text("팔로우");
              $(obj).toggleClass("blue");
       }).fail(error=>{
            console.log("팔로우취소실패", error);
       });

   } else {
        $.ajax({
             type:"post",
             url: "/api/follow/"+toUserId,
             dataType: "json"
            }).done(res=>{
                $(obj).text("팔로우취소");
                $(obj).toggleClass("blue");
            }).fail(error=>{
                console.log("팔로우하기실패", error);
            });
   }
}
반응형

UserProfileDto

followState: 팔로우를 한 상태인지
followCount: 팔로우수

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserProfileDto {
    private boolean pageOwnerState;
    private int imageCount;
    private boolean followState;
    private int followCount;
    private User user;

}

 

FollowRepositoy

- select 문이기 때문에 @Modifying 어노테이션 필요 없음
- mFollowState:
로그인한 유저가 현재 접근한 페이지의 유저를 팔로우 했는지 여부를 구하는 쿼리다.
로그인한 계정id(principalId) 와 현재 접속한 페이지의 유저아이디(pageUserId)를 매개변수로 받는다.
- mFollowCount:
현재 접근한 페이지의 유저가 팔로우하는 수를 구하는 쿼리다. 
pageUserId를 매개변수로 받는다.

public interface FollowRepositoy extends JpaRepository<Follow,Integer>{

 ...
 
    //구독 여부(test1으로 로그인, test2 페이지로감 - 1번이 2번 팔로우 했는지 - 1나오면 구독한다는거 ), 1이면 팔로우한 상태
    @Query(value = "SELECT COUNT(*) FROM follow WHERE fromUserId = :principalId AND toUserId = :pageUserId", nativeQuery = true)
    int mFollowState(int principalId, int pageUserId);

    //해당 페이지의 유저가 팔로우 하는 수
    @Query(value = "SELECT COUNT(*) FROM follow WHERE fromUserId = :pageUserId", nativeQuery = true)
    int mFollowCount(int pageUserId);

}

 이 쿼리들은 UserController에서 "user/profile"로 갈때 쿼리로 얻어진 정보를 UserProfileDto에 담아서 가져갈거다. 

 

UserService

- FollowRepository 를 DI해서 가져온다. 
- 로그인한 유저의 페이지 주인 팔로우 여부(followState)와 페이지 주인의 팔로우 수(followCount)를 구해서
  dto에 담는다. 
- followState가 1이면 true, 아니면 false가 담겨진다.

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final FollowRepositoy followRepositoy;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Transactional(readOnly = true)
    public UserProfileDto userProfile(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);
        //pageUserId 와 principalId가 같은지 비교
        dto.setPageOwnerState(pageUserId == principalId);
        dto.setImageCount(userEntity.getImages().size());

        int followState = followRepositoy.mFollowState(principalId, pageUserId);
        int followCount = followRepositoy.mFollowCount(pageUserId);

        dto.setFollowState(followState == 1);
        dto.setFollowCount(followCount);

        return dto;
    }
    
    ...

}
반응형

유저의 프로필 화면에 유저가 등록한 이미지들을 보이게 할거다.

Userservice

-userProfile 메소드는 해당페이지 유저의 사진을 반환한다.
- findById로 매개변수로 넘어온 유저아이디로 유저를 찾는다. 
  만약 유저정보가 없다면 orElseThrow로 예외를 발동하고,  CustomException을 throw한다. 

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Transactional(readOnly = true)
    public UserProfileDto userProfile(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);
        //pageUserId 와 principalId가 같은지 비교
        dto.setPageOwnerState(pageUserId == pageUserId);
        dto.setImageCount(userEntity.getImages().size());


        return dto;
    }

  ...
}

 

CustomException
- 에러 메시지만 받도록 한다. 

public class CustomException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    public CustomException(String message){
        super(message);
    }

}

 

ControllerExceptionHandler

@RestController
@ControllerAdvice //모든 Exception들을 낚아챔
public class ControllerExceptionHandler {

...
    @ExceptionHandler(CustomException.class) //CustomValidationException 발동하는 모든 Exception을 이 함수가 가로챔
    public String exception(CustomException e){
       return  Script.back(e.getMessage());
    }

...
}

 

UserController

- user/profile 로 유저 정보 뿐만 아니라 유저와 연관된 이미지, 팔로우 정보도 가져가야 한다. 
- 그래서 user셀렉 할때 이미지 정보 등을 가져오게 해야하고, 이는 양방향 매핑으로 구현 가능하다.

@RequiredArgsConstructor
@Controller
public class UserController {

    private final UserService userService;

    @GetMapping({"/user/{pageUserId}"})
    public String profile(@PathVariable int pageUserId, Model model,
                          @AuthenticationPrincipal PrincipalDetails principalDetails){

        UserProfileDto dto = userService.userProfile(pageUserId, principalDetails.getUser().getId());

        model.addAttribute("dto", dto);
        return "user/profile";
    }

......
}

 

User
- mappedyBy:
1. 나는 연관관계의 주인이 아니다. 그러므로 테이블에 컬럼을 만들지마
2. User를 select할때 해당 유저 Id로 등록된 image들을 다 가져와

- OneToMany에서는 FetchType.LAZY이 기본 옵션
 1. Lazy = User를 SELECT할때 해당 유저 ID로 등록된 image들을 가져오지마
   대신 getImages()함수의 image들이호출될때 가져와!
 2. Eager = User를 SELECT할때 해당 유저 ID로 등록된 image들을 전부 Join해서 가져와!

@Builder
@AllArgsConstructor
@NoArgsConstructor  //Bean생성자
@Data
@Entity //DB에 테이블 생성
public class User {
    ...
 
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    @JsonIgnoreProperties({"user"}) //Image내부에 있는 user를 무시하고 파싱한다.
    private List<Image> images;

  ...
}
반응형

Image.class
- Image 도메인을 만든다.
- postImageUrl: 사진을 전송받아서 그 사진을 서버의 특정 폴더에 저장 -Db에 그 저장된 경로를 insert
- 한명의 유저는 여러 이미지를 업로드를 할 수 있고, 하나의 이미지는 하나의 유저만이 만들 수 있으므로 @ManyToOne
- user는 db에서 User객체가 아닌, FK로 저장되기 때문에 userId라는 이름으로 컬럼명을 설정해준다.

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String caption;
    private String postImageUrl;

    @JoinColumn(name = "userId") // DB컬럼명 설정
    @ManyToOne
    private User user;

    //이미지 좋아요 정보-> 추후추가

    //댓글-> 추후추가

    private LocalDateTime createDate;

    @PrePersist
    public void createDate(){
        this.createDate = LocalDateTime.now();
    }

}

 

ImageRepository

public interface ImageRepository extends JpaRepository<Image, Integer> {
}

 

ImageController

- imageUploadDto와 유저정보인 principalDetails를 받도록 한다.
- 업로드가 완료되면 유저의 프로필 화면으로 이동하도록 설정한다.
- MultipartFile은 @Notblank 어노테이션이 지원되지 않으므로 이미지 첨부 유효성 검사는 if문으로 처리해준다.

@RequiredArgsConstructor
@Controller
public class ImageController {

    private final ImageService imageService;

   ...
   
    @PostMapping("/image")
    public String imageUpload(ImageUploadDto imageUploadDto, 
    @AuthenticationPrincipal PrincipalDetails principalDetails){

        if(imageUploadDto.getFile().isEmpty()){
            throw new CustomValidationException("이미지가 첨부되지 않았습니다.", null);
        }
        imageService.upload(imageUploadDto, principalDetails);

        return "redirect:/user/"+principalDetails.getUser().getId();
    }


}

 

ControllerExceptionHandler

- 업로드 이미지가 없을때 CustomValidationException을 throw하고 그것을 ControllerExceptionHandler의 validationException메소드가 낚아챈다.
- 이 경우 errorMap 매개변수를null로 전달하기 때문에 errorMap이 null일 경우, null이 아닐 경우를 분기해야한다.

@RestController
@ControllerAdvice //모든 Exception들을 낚아챔
public class ControllerExceptionHandler {

   
    @ExceptionHandler(CustomValidationException.class) //CustomValidationException 발동하는 모든 Exception을 이 함수가 가로챔
    public String validationException(CustomValidationException e){
        if(e.getErrorMap() ==null){
            return Script.back(e.getMessage());
        }else{
            return Script.back(e.getErrorMap().toString());
        }
    }

....
}

 

CustomValidationException

public class CustomValidationException extends RuntimeException{

    //객체를 구분할때
    private static final long serialVersionUID = 1L;

    private Map<String, String> errorMap;

    public CustomValidationException(String message, Map<String, String> errorMap){
        super(message);
        this.errorMap = errorMap;
    }

    public Map<String, String > getErrorMap(){
        return errorMap;
    }
}

 

ImageUploadDto

- 파일을 받을거기 때문에 요청을 위한 dto(ImageUploadDto)를 만들어준다.
- ImageUploadDto는 file 과 caption을 받을 수 있도록 한다.
- toEntity 메소드를 통해 Image객체로 형변환할 수 있도록 한다. 

@Data
public class ImageUploadDto {

    //MultipartFile 에는 @Notblank가 지원안된다.
    private MultipartFile file;
    private String caption;

    public Image toEntity(String postImagUrl, User user){
        return Image.builder()
                .caption(caption)
                .postImageUrl(postImagUrl)
                .user(user)
                .build();
    }
}

 

ImageService

- upload메소드에는 imageUploadDto와 principalDetails를 받는다.
- imageFilePath : 실제 저장될 경로
- uploadFolder: 이미지가 저장될 로컬경로다, @Value 어노테이션으로 해당 값을 가져올 수 있다. 
- imageUploadDto의 toEntity() 메소드를 통해 db에 저장할Image객체로 변환한다.
  이때 매개변수로 uuid가 포함된 이미지명과 현재 로그인한 유저의 정보를 전달한다. 

@RequiredArgsConstructor
@Service
public class ImageService {

    @Value("${file.path}") //application.properties에서 가져옴
    private String uploadFolder;

    private final ImageRepository imageRepository;

    @Transactional
    public void upload(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails){

        UUID uuid = UUID.randomUUID();
        String imageFilename = uuid + "_"+ imageUploadDto.getFile().getOriginalFilename(); //db에 저장되는 파일명

        Path imageFilePath = Paths.get(uploadFolder+imageFilename);

        // 통신, I/O -> 예외가 발생할 수 있다.
        try {
            Files.write(imageFilePath, imageUploadDto.getFile().getBytes()); //(실제 저장될 경로, 실제 이미지 파일)
        } catch (Exception e){
            e.printStackTrace();
        }

        //Image 테이블에 저장
        Image image = imageUploadDto.toEntity(imageFilename, principalDetails.getUser());
        imageRepository.save(image);


    }
}

 

upload.html
- 파일은 전송하는것이므로 enctype 로 multipart/form-data 로 설정해준다.

<!--사진업로드 Form-->
<form class="upload-form" th:action="@{/image}" method="post" enctype="multipart/form-data">
    <input type="file" name="file"  onchange="imageChoose(this)"/>
    <div class="upload-img">
        <img src="/images/person.jpeg" alt="" id="imageUploadPreview" />
    </div>

    <!--사진설명 + 업로드버튼-->
    <div class="upload-form-detail">
        <input type="text" placeholder="사진설명" name="caption">
        <button class="cta blue">업로드</button>
    </div>
    <!--사진설명end-->

</form>
반응형

Follow.java
- Follow라는 모델을 만든다.
- @JoinColumn 어노테이션으로 DB의 컬럼명을 커스텀한다.
- @Table 컬럼을 통해 한 테이블에 동일한 관계 정보가 중복되지 않게 들어가도록 Uniqe 제약조건을 설정한다.
  (한 컬럼만 걸거면 그냥 @Column(unique = true)하면 되긴하다.)
- User 테이블이 1이고 Follow 테이블이 N 이니까 @ManytoOne 어노테이션을 적는다
- 데이터 생성 정보 컬럼도 넣어준다.

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(
        uniqueConstraints = {
                @UniqueConstraint(
                        name = "follow_uk",
                        columnNames = {"fromUserId", "toUserId"} //실제 db이 컬럼명이 들어가야한다.
                )
        }
)
public class Follow {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @JoinColumn(name = "fromUserId") // DB컬럼명 설정
    @ManyToOne
    private User fromUser; //팔로우 하는애

    @JoinColumn(name = "toUserId")
    @ManyToOne
    private User toUser; //팔로우 받는애

    private LocalDateTime createDate;

    @PrePersist
    public void createDate(){
        this.createDate = LocalDateTime.now();
    }

}

 

FollowApiController

- 팔로우 메소드, 언팔로우 메소드를 만든다.
- 팔로우(언팔라우) 주체를 알아야 하기 때문에 @AuthenticationPrincipal PrincipalDetails principalDetails 를 매개변수로 넣어준다.
- 또한 팔로우,언팔 대상(toUserId)를 매개변수로 넣어준다.

@RequiredArgsConstructor
@RestController
public class FollowApiController {

    private final FollowService followService;

    @PostMapping("/api/follow/{toUserId}")
    public ResponseEntity<?> follow(@AuthenticationPrincipal PrincipalDetails principalDetails,
    								@PathVariable int toUserId){

        followService.follow(principalDetails.getUser().getId(), toUserId);
        return new ResponseEntity<>(new CMRespDto<>(1, "팔로우성공", null), HttpStatus.OK);
    }

    @DeleteMapping("/api/follow/{toUserId}")
    public ResponseEntity<?> unfollow(@AuthenticationPrincipal PrincipalDetails principalDetails, 
    								  @PathVariable int toUserId){

        followService.unfollow(principalDetails.getUser().getId(), toUserId);

        return new ResponseEntity<>(new CMRespDto<>(1, "언팔로우 성공", null), HttpStatus.OK);
    }
}


FollowService

-실제 팔로우, 언팔로우 로직이 실행되는곳
- DB에 영향을 주는 메소드기 때문에 @Transactional 어노테이션 넣어준다.

@RequiredArgsConstructor
@Service
public class FollowService {

    private final FollowRepositoy followRepositoy;

    @Transactional
    public void follow(int fromUserId, int toUserId){
        try {
            followRepositoy.mFollow(fromUserId,toUserId);
        } catch (Exception e){
            throw new CustomApiException("이미 팔로우 했습니다.");
        }

    }

    @Transactional
    public void unfollow(int fromUserId, int toUserId){
        followRepositoy.mUnFollow(fromUserId,toUserId);
    }
}

 

CustomApiException

public class CustomApiException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    public CustomApiException(String message){
        super(message);
    }
}

 

ControllerExceptionHandler

- apiException 메소드를 만든다. 여기에선 CMRespDto 매개변수에 에러메시지만 받고, errorMap은 null로만 받는다.

@RestController
@ControllerAdvice //모든 Exception들을 낚아챔
public class ControllerExceptionHandler {

  ...

    @ExceptionHandler(CustomApiException.class)
    public ResponseEntity<?> apiException(CustomApiException e){
        return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), null), HttpStatus.BAD_REQUEST);
    }
}

 

FollowRepositoy
- 네이티브 쿼리로 작성
- ':' 은 매개변수에 들어온 값을 바인딩하겠다는 의미다. 

public interface FollowRepositoy extends JpaRepository<Follow,Integer>{

    @Modifying //INSERT, DELETE, UPDATE를 네이티브쿼리로 작성하려면 해당 어노테이션 필요
    @Query(value = "INSERT INTO follow(fromUserId, toUserId, createDate) Values(:fromUserId, :toUserId, now())", nativeQuery = true)
    void mFollow(int fromUserId, int toUserId);

    @Modifying
    @Query(value = "DELETE FROM follow WHERE fromUserId = :fromUserId AND toUserId = :toUserId", nativeQuery = true)
    void mUnFollow(int fromUserId, int toUserId);
}

 

+ Recent posts