-
Notifications
You must be signed in to change notification settings - Fork 3
주요 기능 소개
jwt 토큰
로그인 요청이 들어온 후 정보가 맞으면 jwtTokenProvider에서 createToken 함수를 이용해서 jwt 토큰을 생성한다.
jwtAuthenticationFilter를 만들어서 UsernamePasswordAuthenticationFilter보다 먼저 실행되게 해서 jwt 토큰이 유효한지 판단한다. (인증이 필요한 api에서 jwt 토큰을 보낼때 확인하는 용도)
jwt 생성할때 토큰 정보에 email을 넣었기 때문에 jwt 정보를 꺼내서 loadUserByUsername으로 보낼때 email값이 넘어가게 된다.
jwtTokenProvider에서 loadUserByUsername을 호출하면 CustomUserDetailsService은 UserDetailsService interface를 구현했기 때문에 @Override한 CustomUserDetailsService의 loadUserByUsername이 호출된다.
CustomUserDetailsService의 loadUserByUsername는 받은 email 값으로 userRepository에서 findByEmail을 찾아 User를 리턴해준다.
그래서 @AuthenticationPrincipal을 사용하면 토큰에서 User 정보를 받을 수 있는 것 같다.
이메일, 비밀번호가 user테이블에 등록되어 있으면 jwtTokenProvider에서 createToken 함수에 user.getEmail을 넣어서 토큰을 생성한다.
Dto에서 @Valid를 통한 유효성 검사
비밀번호와 비밀번호 체크가 맞는지 검사
닉네임 & 이메일 중복확인
가입하려는 이메일이 이메일 인증이 된 상태인지 확인 email_check 테이블에 이메일 값이 존재하고 authCode 값이 "Y"일 경우 인증되었다고 판단
-
닉네임중복체크
- 닉네임이 user 테이블에 존재하면 false 반환, 존재하지않으면 true 반환
-
이메일 중복체크 + 인증번호 발송
- 이메일이 user 테이블에 존재하면 false 반환, 존재하지않으면 입력한 이메일로 메일 발송하고 true값 반환 email_check 테이블에 같은 이메일로 메일발송 요청이 들어오면 authCode만 업데이트 시켜준다.
-
이메일 인증 확인
- 인증번호를 받은 이메일이 아니면 false 출력, 인증번호가 다르면 false 출력, 인증번호를 받은 이메일이고 authCode가 "Y"일 경우 true 출력
-
이메일 찾기
- 입력한 nickname이 user 테이블에 존재하면 그 user에 email을 반환해주고 없으면 null을 반환한다.
-
비밀번호 찾기
- 입력한 email이 user테이블에 존재하면 email로 authCode메일을 발송하고 pwd_check 테이블에 이메일과 authCode를 저장한다, user 테이블이 null이라면 false를 반환한다. pwd_check 테이블에 같은 이메일로 메일발송 요청이 들어오면 authCode만 업데이트 시켜준다.
-
비밀번호 인증 확인
- email이 pwd_chech 테이블에 없으면 false 반환, 테이블에 저장된 authCode와 입력한 코드가 같다면 "Y"로 변경해주고 true를 리턴한다.
-
비밀번호 수정
- user 테이블에 입력한 email이 존재하지 않는다면 에러를 보내주고, user테이블에 존재하고 테이블에 존재하는 auth코드가 "Y"일경우 비밀번호를 수정할 수 있게 해준다.
-
관리자 회원가입
- 기존 회원가입 방식에서 adminToken을 추가해서 회원가입을 하게되면 ADMIN role을 추가해서 관리자로 회원가입 시킨다.
-
OAuth2 로그인 흐름
-
사용자 측의 브라우저에서 엔드포인트
http://{도메인}/oauth2/authorize{provider}?redirect_uri={프론트엔드에서 소셜로그인 후 돌아갈 uri}
로 접속하는 것으로 프론트엔드 클라이언트에서 시작된다. -
provider 경로 매개변수는 naver, google, kakao중 하나이다.
-
OAuth2 콜백으로 인해 오류가 발생하면 스프링 시큐리티는 설정해놓은 oAuth2AuthenticationFailureHandler를 호출한다.
-
OAuth2 콜백이 성공하고 인증 코드가 포함 된 경우 Spring Security는 access_token에 대한 authorization_code를 교환하고 Security에 지정된 customOAuth2UserService를 호출한다.
-
customOAuth2UserService는 인증된 사용자의 세부 정보를 검색하고 데이터베이스에 새 항목을 작성하거나 동일한 이메일의 정보를 찾아 기존 항목을 업데이트 한다.
-
마지막으로 oAuth2AuthenticationSuccessHandler가 호출된다. 사용자에 대한 JWT 인증 토큰을 만들고 쿼리 문자열로 JWT 토큰과 함께 사용자를 redirect_uri로 보낸다.
-
-
security 설정
-
authoriztionEndpoint()를
/oauth2/authorize
로 지정한다. -
redirectionEndpoint()를
/login/oauth2/code/*
로 지정한다. -
성공했을경우 succesHandler로 보낸다.
-
실패했을경우 failureHandler로 보낸다.
-
-
customOAuth2UserService
-
oauth2 를 통해 로그인한 사용자 정보를 받아서 저장하는 역할을 한다.
-
-
OAuth2UserInfoFactory
-
customOAuth2UserService에서 받은 provider가 google, naver, kakao중에 어떤것인지 판단해 맞는 객체를 생성한다.
-
-
oAuth2AuthenticationSuccessHandler
-
jwt 토큰을 생성하고 사용자가 지정한 redirect_uri에 queryParam으로 token을 담아서 보내준다.
-
-
UserPrincipal
-
OAuth2로 로그인한 사용자도 담아주기 위해서 UserPrincipal에서 OAuth2User도 implements한다.
-
-
oauth2.yml
- oauth2에 대한 설정을 yml에 다해준다. 구글, 페이스북, 깃허브 같이 oauth2에 provider들은 provider를 따로 써줄필요 없는데 국내 소셜로그인 네이버, 카카오 같은 경우는 oauth2에 provider로 등록이 안되어 있기 때문에 yml에 provider에 대한 설정도 같이 넣어줘야한다.
- 프로필 정보
/profile/{userId}
- url의
userId
로 유저를 찾아 ProfileResponseDto를 리턴
@Getter
@NoArgsConstructor
public class ProfileResponseDto {
private Long userId;
private String nickname;
private String imgUrl;
private String introduceMsg;
public ProfileResponseDto(User editUser){
this.userId = editUser.getId();
this.nickname = editUser.getNickname();
this.imgUrl = editUser.getImgUrl();
this.introduceMsg = editUser.getIntroduceMsg();
}
}
-
유저가 업로드 한 게시물
/story/{userId}/board
- 무한 스크롤 방식 적용
- 유저가
null
일 경우는 비로그인 회원이 다른 사람의 페이지를 방문했을 경우이므로, 좋아요의 체크 여부를false
로 하여 반환
-
유저가 좋아요 한 게시물
/story/{userId}/likeboard
- 무한 스크롤 방식 적용
- 좋아요 한 게시물 중 유저가 업로드한 게시물은 제외
- 유저가
null
일 경우는 비로그인 회원이 다른 사람의 페이지를 방문했을 경우이므로, 좋아요의 체크 여부를false
로 하여 반환
@Getter
@NoArgsConstructor
public class MypageResponseDto {
//board
private Long boardId;
private double latitude;
private double longitude;
private String spotName;
private String category;
private List<BoardImgCommonRequestDto> boardImgResponseDtoList = new ArrayList<>();
//heart
private boolean liked;
private int likeCount;
@Builder
public MypageResponseDto(Board boardEntity, boolean likeCheck, int likeCount, List<BoardImgCommonRequestDto> responseDto) {
//board 정보
this.boardId = boardEntity.getId();
this.category = boardEntity.getCategory();
this.latitude = boardEntity.getLatitude();
this.longitude = boardEntity.getLongitude();
this.spotName = boardEntity.getSpotName();
//이미지
this.boardImgResponseDtoList = responseDto;
//좋아요
this.liked = likeCheck;
this.likeCount = likeCount;
}
}
- 프로필 편집
- 프로필 이미지, 소개 메시지
/editmyprofile/{userId}
-
userId
와 token 속 user를 비교하여 본인만 편집 가능 - 프로필 이미지를 변경하지 않는 경우에는 imgUrl에 유저의 기존 imgUrl로 설정
- 프로필 이미지 파일을 받은 경우에는
- 기존 파일 이름을 변경. 공백 제거,
.확장자
앞의 문자 제거 ---> 고유식별자 + 날짜 - S3에 업로드
- 기존 파일 이름을 변경. 공백 제거,
- 프로필 이미지, 소개 메시지
public String profileUpload(MultipartFile file, String dirName) throws IOException {
return changeProfileFileName(file, dirName);
}
private String changeProfileFileName(MultipartFile uploadFile, String dirName) throws IOException {
String replace = uploadFile.getOriginalFilename().replace(" ", ""); //공백 다 없애기
log.info("changeFileName1: " + uploadFile.getOriginalFilename());
String fileName = replace.substring(uploadFile.getOriginalFilename().lastIndexOf('.')); //.png 즉, 확장자와 . 앞에 문자 다 없애기
log.info("=======새로운 fileName : " + fileName);
log.info("changeFileName2: " + fileName);
Date date_now = new Date(System.currentTimeMillis()); // 현재시간을 가져와 Date형으로 저장한다
//파일 이름을 다르게 한다. 날짜로만헀는데 for문이 너무 빠르게 돌아서 mmss까지 커버가 안되서 교체!
UUID uuid = UUID.randomUUID();
String subUUID = uuid.toString().substring(0, 8); //16자리로 생성되는데 너무 길어서 8자리로 짜름!
SimpleDateFormat fourteen_format = new SimpleDateFormat("yyyyMMddHHmmss");
String dateUuidFileName = subUUID + fourteen_format.format(date_now) + fileName;
String resultFileName = dirName + "/" + dateUuidFileName;
log.info("파일 이름 나타내기 2번째 : " + uploadFile.getName() + " ," + resultFileName);
String uploadImgUrl = putS3Aws(uploadFile, resultFileName);
return uploadImgUrl;
}
private String putS3Aws(MultipartFile uploadFile, String fileName) throws IOException {
ObjectMetadata metadata = new ObjectMetadata();
amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile.getInputStream(), metadata).withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3.getUrl(bucket, fileName).toString();
}
-
닉네임 변경
/editnickname/{userId}
- 닉네임 중복확인을 먼저 거치기
- 본인만 편집 가능
-
비밀번호 변경
/editpwd/{userId}
- 본인만 변경 가능
- PasswordRequestDto에서
@NotBlank
,@Pattern
어노테이션으로 validation체크. 회원가입 시 비밀번호 세팅과 동일하게 맞춰줌 -
BCryptPasswordEncoder.matches
를 이용하여 원래 비밀번호와 입력 비밀번호가 같은 지 확인
@Getter
@NoArgsConstructor
public class PasswordRequestDto {
@NotBlank(message = "비밀번호를 비워둘 수 없습니다.")
private String pwd;
@NotBlank(message = "비밀번호를 비워둘 수 없습니다.")
@Pattern(regexp = "^(?=.*[a-zA-Z])((?=.*\\d)|(?=.*\\W)).{10,}$",
message = "비밀번호 형식을 지켜주세요")
private String newPwd;
@NotBlank(message = "비밀번호 체크를 비워둘 수 없습니다.")
private String pwdChk;
}
게시글
- 게시글 리스트
/qna
- 페이지네이션 적용
- QuestionResponseDto에 content제거(본인만 상세페이지 확인 가능하므로)
- 전체 데이터 수와 필요한 페이지 수 함께 리턴
public QuestionResponseDto(Question question, Long qnaSize, int pageSize) {
this.id = question.getId();
this.title = question.getTitle();
this.writer = question.getUser().getNickname();
this.modified = question.getModified();
this.qnaSize = qnaSize;
this.pageSize = pageSize;
}
-
게시글 상세보기
/qna/{qnaId}/detail
- 게시글에 연관된 댓글도 함께 리턴
-
게시글 작성
/qna
- QuestionRequestDto에
@NotBlank
어노테이션을 이용하여 validation 체크. 비어있을 경우 message리턴
@Getter
@NoArgsConstructor
public class QuestionRequestDto {
private Long id;
@NotBlank(message = "제목을 입력해주세요.")
private String title;
@NotBlank(message = "내용을 입력해주세요.")
private String content;
private Long userId;
}
-
게시글 수정
/qna/{qnaId}
- 본인만 수정 가능
-
@NotBlank
validation체크
-
게시글 삭제
/qna/{qnaId}
- 본인만 삭제 가능
-
cascade = CascadeType.REMOVE
으로 게시글에 연관된 댓글 함께 삭제
- 게시글과
@ManyToOne
mapping - configure에 다음 조건 추가하여 관리자만 접근 가능
.antMatchers(HttpMethod.POST,"/qcomment/**").hasRole("ADMIN") .antMatchers(HttpMethod.PUT,"/qcomment/**").hasRole("ADMIN") .antMatchers(HttpMethod.DELETE,"/qcomment/**").hasRole("ADMIN")
- service에서 role 한번 더 검증
- 댓글 작성 :
/qcomment/{qnaId}
- 댓글 수정 :
/qcomment/{qcommentId}/qna/{qnaId}
- 댓글 삭제 :
/qcomment/{qcommentId}/qna/{qnaId}
//게시물 작성
@PostMapping("/board")
public ResponseEntity save(@RequestParam(value = "file", required = false) List<MultipartFile> files, @RequestParam("title") String title,
@RequestParam("content") String content, @RequestParam("category") String category, @RequestParam("latitude") double latitude,
@RequestParam("longitude") double longitude, @RequestParam("spotName") String spotName, @AuthenticationPrincipal UserPrincipal user) throws IOException {
List<String> imgUrls = s3Service.boardUpload(files, "board");
User findUser = findUserMethod(user);
BoardSaveRequestDto boardSaveRequestDto = new BoardSaveRequestDto(title,content,category,latitude,longitude, spotName, findUser);
BoardSaveResponseDto boardSaveResponseDto = boardService.save(boardSaveRequestDto, imgUrls);
return customExceptionController.ok("게시물을 저장하였습니다.", boardSaveResponseDto);
}
-
게시글 작성에는 중요 포인트 가 2가지 정도 있다.
-
다중 사진 업로드를 위한 MultipartFile을 List로 받는다.
for (String imgUrl : imgUrls) { BoardImgSaveRequestDto boardImgSaveRequestDto = new BoardImgSaveRequestDto(boardEntity, imgUrl); boardImgUrlsRepository.save(boardImgSaveRequestDto.toEntity()); boardImgReponseDtoList.add(boardImgSaveRequestDto); }
-
Builder를 사용하여 Dto에서 메서드를 만들어 바로 변수에 삽입해준다는 점이다.
public Board toEntity() { return Board.builder() .title(title) .content(content) .category(category) .latitude(latitude) .longitude(longitude) .spotName(spotName) .user(user) .build(); }
-
//지도페이지 로딩 될 때
@GetMapping("/map")
public ResponseEntity loadingMapBoard(@AuthenticationPrincipal UserPrincipal user) {
// User findUser = findUserMethod(user);
List<LoadingBoardMapResponseDto> loadingBoardMapResponseDtos = boardService.loadingMapBoard(user);
return customExceptionController.ok("모든 게시물 데이터 정보입니다." ,loadingBoardMapResponseDtos);
}
//커뮤니티 게시글 조회
public List<BoardsGetResponseDto> getBoards(UserPrincipal loginUser) {
List<BoardsGetResponseDto> boardGetResponseDtoList = new ArrayList<>();
List<Board> boardAll = boardRepository.findAllByOrderByModifiedDesc();
boolean likeCheck = true;
for (int i=0; i<boardAll.size(); i++) {
Set<BoardImgUrls> allBoardImgUrls = boardAll.get(i).getBoardImgUrls();
Set<Comment> allComments = boardAll.get(i).getComments();
List<BoardImgCommonRequestDto> boardImgCommonRequestDtos = new ArrayList<>();
List<BoardDetailCommentsDto> boardDetailCommentsDtos = new ArrayList<>();
for (int j=0; j<allBoardImgUrls.size(); j++) {
boardImgCommonRequestDtos.add(new BoardImgCommonRequestDto(allBoardImgUrls.iterator().next()));
}
for (int k=0; k<allComments.size(); k++) {
boardDetailCommentsDtos.add(new BoardDetailCommentsDto(allComments.iterator().next()));
}
//로그인 사용자가 게시물을 좋아요 했는지 안했는지 체크!
if (loginUser == null) {//로그인이 되어있지 않은 사용자일 때
likeCheck = false;
} else //로그인이 되어있는 사용자 일 때
likeCheck = heartRepository.existsByBoardIdAndUserId(boardAll.get(i).getId(), loginUser.getId());
//게시물에 대한 좋아요 개수
List<Heart> allByBoardIdHearts = boardAll.get(i).getHearts();
BoardsGetResponseDto brdto = new BoardsGetResponseDto(boardAll.get(i).getUser(), boardAll.get(i), likeCheck, allByBoardIdHearts.size(), boardDetailCommentsDtos,boardImgCommonRequestDtos);
boardGetResponseDtoList.add(brdto);
}
return boardGetResponseDtoList;
}
//게시글(커뮤니티)페이지입니다.
@GetMapping("/board")
public ResponseEntity getBoards(@AuthenticationPrincipal UserPrincipal user) {
List<BoardsGetResponseDto> boards = boardService.getBoards(user);
return customExceptionController.ok("게시글 정보 입니다아!!!.", boards);
}
List<LoadingBoardMapResponseDto> responseDtos = new ArrayList<>();
List<Board> boards = boardRepository.findAllFetchJoin();
boolean likeCheck = true;
for (Board board : boards) {
Set<BoardImgCommonRequestDto> imgCommonRequestDtos = BoardImgUrls.toDtoList(board.getBoardImgUrls());
if (loginUser == null) {
likeCheck = false;
} else {
likeCheck = heartRepository.existsByBoardIdAndUserId(board.getId(), loginUser.getId());
}
responseDtos.add(new LoadingBoardMapResponseDto(board, likeCheck, imgCommonRequestDtos));
}
return responseDtos;
-
이미지를 사용자가 추가 or 삭제를 할 수 있게 기능을 설계하였는데, 추가할 이미지는 Multipart로 List로 받고 삭제할 사진들은 Long 형태로 이미지의 id를 받는다.
-
추가할 사진을 다시 Multipart로 받아 s3에 저장 및 DB에 URL을 저장한다.
// 게시글 수정 @PutMapping("/board/{boardId}") public ResponseEntity update(@PathVariable Long boardId, @RequestParam(value = "file", required = false) List<MultipartFile> files, @RequestParam("title") String title, @RequestParam("content") String content, @RequestParam(value = "deleteImages", required = false) List<Long> deleteImages, @AuthenticationPrincipal UserPrincipal user) throws IOException { User findUser = findUserMethod(user); //s3에 이미지를 삭제하는 메서드 if (deleteImages != null) s3Service.findImgUrls(deleteImages); else { deleteImages = new ArrayList<>(); } //s3에 이미지 업로드하고 업로드된 이미지 배열 List<String> imgUrls = new ArrayList<>(); if (files.size() != 0) { imgUrls = s3Service.boardUpload(files, "board"); //새로 추가된 이미지 s3에 저장. } BoardUpdateRequestDto boardUpdateRequestDto = new BoardUpdateRequestDto(boardId,title,content); BoardDetailResponseDto updateBoard = boardService.update(boardUpdateRequestDto, findUser, deleteImages, imgUrls); if (updateBoard == null) return customExceptionController.error("사용자가 옳바르지 않습니다."); else return customExceptionController.ok("게시글이 수정되었습니다.", updateBoard); }
-
주의할 점은 추가할 사진과 삭제할 사진이 항상 존재하는게 아니기 때문에 어노테이션 옵션에 required = false 추가 해줘야한다.!!! 그리고 만약 데이터가 오지 않았다면 지금 해당 변수는 NULL로 받아지기 때문에 Null exception을 잘 처리해줘야한다.
-
삭제할 데이터도 Null exception을 잘 처리해줘야한다!
-
public BoardDetailResponseDto update(BoardUpdateRequestDto boardUpdateRequestDto, User loginUser, List<Long> deleteImgUrlId, List<String> imgUrls) { Board board = boardRepository.findById(boardUpdateRequestDto.getBoardId()).orElseThrow(() -> new IllegalArgumentException("해당 게시물은 없습니다.")); log.info("해당 게시물의 아이디는 : " + boardUpdateRequestDto.getBoardId()); if (board.getUser().getId().equals(loginUser.getId())) { board.update(boardUpdateRequestDto); } else { return null; } boardRepository.save(board); // if (deleteImgUrlId.size() != 0) // deleteImgUrl(deleteImgUrlId); if (deleteImgUrlId.size() != 0) { for (Long id : deleteImgUrlId) { BoardImgUrls boardImgUrls = boardImgUrlsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 이미지가 없습니다.")); boardImgUrlsRepository.deleteById(boardImgUrls.getId()); } } //boardImgUrls 의 테이블에 새로운 imgUrls 를 저장입니다. if (imgUrls.size() != 0) { for (String imgUrl : imgUrls) { BoardImgSaveRequestDto boardImgSaveRequestDto = new BoardImgSaveRequestDto(board, imgUrl); boardImgUrlsRepository.save(boardImgSaveRequestDto.toEntity()); } } List<BoardImgCommonRequestDto> boardImgCommonRequestDtoList = new ArrayList<>(); List<BoardDetailCommentsDto> boardDetailCommentsDtoList = new ArrayList<>(); boolean likeCheck = true; //해당 board에 대한 imgUrl들을 가져오기. Set<BoardImgUrls> allBoardImgs = board.getBoardImgUrls(); //반목문을 돌려서 리스트에 imgUrl들을 담기. for (BoardImgUrls boardImgUrl : allBoardImgs) { boardImgCommonRequestDtoList.add(new BoardImgCommonRequestDto(boardImgUrl)); } //해당 board에 대한 comment 들을 가져오기. Set<Comment> allComments = board.getComments(); for (Comment comment : allComments) { boardDetailCommentsDtoList.add(new BoardDetailCommentsDto(comment)); } if (loginUser == null) { likeCheck = false; } else { likeCheck = heartRepository.existsByBoardIdAndUserId(board.getId(), loginUser.getId()); } //게시물에 대한 좋아요 개수 List<Heart> allBoardHeartCount = board.getHearts(); return new BoardDetailResponseDto(board,likeCheck,allBoardHeartCount.size(),boardDetailCommentsDtoList,boardImgCommonRequestDtoList); }
-
위 코드에서 @Transaction을 주석으로 처리를 해놓았는데 이유가 해당 board를 꺼내서 영속성 컨텍스트가 활성화가 되어서 인지는 몰라도 @Transaction을 넣어버리면 제대로 수정이 처리가 되지않아서 주석으로 해주었다.
-
-
삭제할 이미지를 DB에서 조회하고 S3에 삭제를 한 뒤에 DB에서도 해당 URL를 삭제한다.
- 1번에서도 말했다시피 NULL exception처리와 @Transaction 처리만 잘 하면 이상없이 동작한다.
-
-
로컬환경에서는 이미지 파일을 프로젝트 root단에 저장했다가 지우는 형식으로 설계
private Optional<File[]> convert(List<MultipartFile> file) throws IOException { multiparts = new MultipartFile[file.size()]; File[] convertFiles = new File[file.size()]; for (int i=0; i<file.size(); i++) { multiparts[i] = file.get(i); convertFiles[i] = new File((file.get(i).getOriginalFilename())); if (!convertFiles[i].exists()) { log.info("파일이 존재하는지? : " + convertFiles[i].exists()+" ,"+convertFiles[i]); convertFiles[i].mkdirs(); Runtime.getRuntime().exec("chmod 777 " + file.get(i).getOriginalFilename()); convertFiles[i].setExecutable(true, false); convertFiles[i].setReadable(true, false); convertFiles[i].setWritable(true, false); if (!convertFiles[i].createNewFile()) { log.debug("===========Optional.emty 가 나옴!!==========="); return Optional.empty(); }else { try(FileOutputStream fos = new FileOutputStream(convertFiles[i])) { fos.write(file.get(i).getBytes()); } } } } return Optional.of(convertFiles); }
-
하지만 배포를 Linux 가상환경에 하기 때문에 File를 만들 때 권환을 줘야하는 경우가 발생한다. 위에 코드처럼 만약 권한을 주지 않을경우 권한에러가 나버린다... 진짜 이것땜에 엄청 고생했다.
-
하지만 이렇게 해도 근본적으로 해결되지 않는다. 계속해서 File exception이 나기 때문에 더 이상 위와 같은 설계로는 진행이 안되었다. 찾아본 결과 MultiPart를 굳이 File로 다시 안만들어도 되는거였다..!! 처음에는 어디에서 Multipart는 못 읽기 때문에 File로 변경해줘야한다고 했는데!!!! 그래서 최종 코드는 다음과 같다.
public List<String> boardUpload(List<MultipartFile> multipartFile, String dirName) throws IOException { String[] result = changeUploadFileName(multipartFile, dirName); return Arrays.asList(result); }
private String[] changeUploadFileName(List<MultipartFile> uploadFile, String dirName) throws IOException { List<String> uploadImgUrl = new ArrayList<>(); for (int i = 0; i < uploadFile.size(); i++) { String replace = uploadFile.get(i).getOriginalFilename().replace(" ", ""); //공백 다 없애기 log.info("changeFileName1: " + uploadFile.get(i).getOriginalFilename()); String fileName = replace.substring(uploadFile.get(i).getOriginalFilename().lastIndexOf('.')); //.png 즉, 확장자와 . 앞에 문자 다 없애기 // fileName = fileName.substring(0,fileName.lastIndexOf('.')+1); //todo 다시 테스트하기 log.info("=======새로운 fileName : " + fileName); log.info("changeFileName2: " + fileName); Date date_now = new Date(System.currentTimeMillis()); // 현재시간을 가져와 Date형으로 저장한다 //파일 이름을 다르게 한다. 날짜로만헀는데 for문이 너무 빠르게 돌아서 mmss까지 커버가 안되서 교체! UUID uuid = UUID.randomUUID(); String subUUID = uuid.toString().substring(0, 8); //16자리로 생성되는데 너무 길어서 8자리로 짜름! SimpleDateFormat fourteen_format = new SimpleDateFormat("yyyyMMddHHmmss"); String dateUuidFileName = subUUID + fourteen_format.format(date_now) + fileName; String resultFileName = dirName + "/" + dateUuidFileName; log.info("파일 이름 나타내기 2번째 : " + uploadFile.get(i).getName() + " ," + resultFileName); uploadImgUrl.add(putS3Aws(uploadFile.get(i), resultFileName)); } return uploadImgUrl.toArray(new String[uploadImgUrl.size()]); }
- 파일이름이 한글인게 있기 때문에 다시 UUID와 날짜로 바꿔서 S3에 올려주었다.
-
- 이번 프로젝트에서는 대댓글을 설계하지 않았기 때문에 댓글에서는 큰 문제없이 구현하였다. 그럼 바로 코드를 살펴보자
-
Service.java
@Transactional public CommentSaveResponseDto addComment(Long boardId, User user, CommentSaveRequestDto commentSaveRequestDto) { Board findBoard = boardRepository.findById(boardId).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다..")); commentSaveRequestDto.addBoardInComment(findBoard); commentSaveRequestDto.addUserInComment(user); Comment saveComment = commentRepository.save(commentSaveRequestDto.toEntity()); return new CommentSaveResponseDto(saveComment); }
-
Service.java
@Transactional public CommentUpdateResponseDto updateComment(Long commentId, CommentUpdateRequestDto requestDto, User user) { Comment findComment = commentRepository.findById(commentId).orElseThrow(() -> new IllegalArgumentException("해당 댓글이 없습니다.")); if (user.getId().equals(findComment.getUser().getId())) { Comment updateComment = findComment.update(requestDto); return new CommentUpdateResponseDto(updateComment); } else return null; }
-
Service.java
@Transactional public void deleteComment(Long commentId, User user) { Comment findComment = commentRepository.findById(commentId).orElseThrow(() -> new IllegalArgumentException("해당 댓글이 없습니다...")); if (user.getId().equals(findComment.getUser().getId())) { commentRepository.deleteById(commentId); } }
-
좋아요는 설계 자체는 막상해보면 쉬웠지만 생각할 때는 어렵다고 느껴서 조금 헤메었다.
-
좋아요 엔티티를 살펴보자
@NoArgsConstructor @Getter @Entity public class Heart { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "HEART_ID") private Long id; //@ManyToOne(cascade = CascadeType.ALL) @ManyToOne @JoinColumn(name = "USER_ID") private User user; @ManyToOne @JoinColumn(name = "BOARD_ID") private Board board; @Builder public Heart(User user, Board board) { this.user = user; this.board = board; } }
- 좋아요 id와 사용자, 게시글 3개 밖에 없다. 만약 어떤 게시글에 좋아요를 조회를 하고 싶으면 좋아요 DB에서 해당 게시글의 갯수만 조회하면 끝이 나버린다. 그래서 좋아요 엔티티에는 딱히 들어갈게 없다. 그럼 코드로. 살펴보자
@Transactional public boolean addHeart(Long boardId, Long userId) { Board findBoard = boardRepository.findById(boardId).orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); User findUser = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")); //좋아요 중복 방지 if (!isNotAlreadyLike(findBoard, findUser)) { heartRepository.save(new Heart(findUser, findBoard)); return true; } //이미 좋아요를 했던 게시물이 있으면 false를 return return false; }
/사용자가 이미 좋아요 한 게시물인지 체크(boolean 값 받기) private boolean isNotAlreadyLike(Board findBoard, User findUser) { return heartRepository.existsByBoardIdAndUserId(findBoard.getId(), findUser.getId()); }
//좋아요 취소하기 @Transactional public boolean deleteHeart(Long boardId, Long userId) { Board findBoard = boardRepository.findById(boardId).orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); User findUser = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")); Heart heart = checkAlreadyLike(findBoard, findUser); //만약 좋아요를 한 객체가 있으면 그 객체를 Heart테이블에서 삭제를 시키고 true 리턴. 그렇지 않으면 false 리턴. if (heart != null) { heartRepository.deleteById(heart.getId()); return true; }else return false; }
//사용자가 이미 좋아요 한 게시물인지 체크(Heart 객체 받기) private Heart checkAlreadyLike(Board findBoard, User findUser) { return heartRepository.findByBoardIdAndUserId(findBoard.getId(), findUser.getId()); }