21.12.07 백엔드 코드 리팩토링 1차 완료
This commit is contained in:
@@ -8,7 +8,7 @@ import myblog.blog.article.service.TempArticleService;
|
||||
import myblog.blog.category.dto.CategoryForView;
|
||||
import myblog.blog.category.dto.CategoryNormalDto;
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.comment.dto.CommentDtoForSide;
|
||||
import myblog.blog.comment.dto.CommentDtoForLayout;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import myblog.blog.member.auth.PrincipalDetails;
|
||||
import myblog.blog.member.dto.MemberDto;
|
||||
@@ -311,10 +311,10 @@ public class ArticleController {
|
||||
CategoryForView categoryForView = CategoryForView.createCategory(categoryService.getCategoryForView());
|
||||
model.addAttribute("category", categoryForView);
|
||||
|
||||
List<CommentDtoForSide> comments = commentService.recentCommentList()
|
||||
List<CommentDtoForLayout> comments = commentService.recentCommentList()
|
||||
.stream()
|
||||
.map(comment ->
|
||||
new CommentDtoForSide(comment.getId(), comment.getArticle().getId(), comment.getContent(), comment.isSecret()))
|
||||
new CommentDtoForLayout(comment.getId(), comment.getArticle().getId(), comment.getContent(), comment.isSecret()))
|
||||
.collect(Collectors.toList());
|
||||
model.addAttribute("commentsList", comments);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public class Article extends BasicEntity {
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(nullable = false, length = 10000)
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(columnDefinition = "bigint default 0",nullable = false)
|
||||
|
||||
@@ -43,6 +43,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
// 인가
|
||||
.authorizeRequests()
|
||||
.antMatchers("/article/write", "/article/edit","/article/delete","/edit/category", "/category/edit").hasRole(Role.ADMIN.name())
|
||||
.antMatchers("/comment/write","/comment/delete").authenticated()
|
||||
.anyRequest().permitAll()
|
||||
|
||||
// 로그아웃
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
package myblog.blog.category.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.exception.CustomFormException;
|
||||
import myblog.blog.exception.ListValidator;
|
||||
import myblog.blog.category.dto.CategoryForView;
|
||||
import myblog.blog.category.dto.CategoryNormalDto;
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.comment.dto.CommentDtoForSide;
|
||||
import myblog.blog.comment.dto.CommentDtoForLayout;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
@@ -24,6 +28,7 @@ public class CategoryController {
|
||||
private final CategoryService categoryService;
|
||||
private final CommentService commentService;
|
||||
private final ModelMapper modelMapper;
|
||||
private final ListValidator listValidator;
|
||||
|
||||
/*
|
||||
- 카테고리 수정폼 조회
|
||||
@@ -39,10 +44,10 @@ public class CategoryController {
|
||||
|
||||
CategoryForView categoryForView = CategoryForView.createCategory(categoryList);
|
||||
|
||||
List<CommentDtoForSide> comments = commentService.recentCommentList()
|
||||
List<CommentDtoForLayout> comments = commentService.recentCommentList()
|
||||
.stream()
|
||||
.map(comment ->
|
||||
new CommentDtoForSide(comment.getId(), comment.getArticle().getId(), comment.getContent(),comment.isSecret()))
|
||||
new CommentDtoForLayout(comment.getId(), comment.getArticle().getId(), comment.getContent(), comment.isSecret()))
|
||||
.collect(Collectors.toList());
|
||||
//
|
||||
|
||||
@@ -58,11 +63,18 @@ public class CategoryController {
|
||||
- 카테고리 수정 요청
|
||||
*/
|
||||
@PostMapping("/category/edit")
|
||||
public @ResponseBody String editCategory(@RequestBody List<CategoryNormalDto> categoryList){
|
||||
public @ResponseBody
|
||||
String editCategory(@RequestBody List<CategoryNormalDto> categoryList, Errors errors) {
|
||||
// List DTO 검증을 위한 커스텀 validator
|
||||
listValidator.validate(categoryList, errors);
|
||||
|
||||
if (errors.hasErrors()) {
|
||||
throw new CustomFormException(Objects.requireNonNull(errors.getFieldError()).getDefaultMessage());
|
||||
}
|
||||
|
||||
categoryService.changeCategory(categoryList);
|
||||
return "변경 성공";
|
||||
}
|
||||
|
||||
private List<CategoryNormalDto> cloneList(List<CategoryNormalDto> categoryList) {
|
||||
return categoryList
|
||||
.stream()
|
||||
|
||||
@@ -3,6 +3,9 @@ package myblog.blog.category.dto;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/*
|
||||
- 범용 카테고리 DTO
|
||||
*/
|
||||
@@ -12,6 +15,7 @@ import lombok.ToString;
|
||||
public class CategoryNormalDto {
|
||||
|
||||
private Long id;
|
||||
@NotBlank(message = "카테고리명은 공백일 수 없습니다.")
|
||||
private String title;
|
||||
private int tier;
|
||||
private int count;
|
||||
|
||||
@@ -6,12 +6,17 @@ import myblog.blog.article.service.ArticleService;
|
||||
import myblog.blog.comment.dto.CommentDto;
|
||||
import myblog.blog.comment.dto.CommentForm;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import myblog.blog.exception.CustomFormException;
|
||||
import myblog.blog.member.auth.PrincipalDetails;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@@ -20,25 +25,31 @@ public class CommentController {
|
||||
private final CommentService commentService;
|
||||
private final ArticleService articleService;
|
||||
|
||||
/*
|
||||
- 아티클 조회시 아티클에 달린 댓글들 전체 조회
|
||||
*/
|
||||
@GetMapping("/comment/list/{articleId}")
|
||||
public List<CommentDto> getCommentList(@PathVariable Long articleId){
|
||||
|
||||
List<CommentDto> commentList = commentService.getCommentList(articleId);
|
||||
|
||||
return commentList;
|
||||
|
||||
return CommentDto.listCreateFrom(commentService.getCommentList(articleId),0);
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 작성 요청
|
||||
*/
|
||||
@PostMapping("/comment/write")
|
||||
public List<CommentDto> getCommentList(@RequestParam Long articleId,
|
||||
@RequestParam(required = false) Long parentId,
|
||||
@RequestBody CommentForm commentForm,
|
||||
Authentication authentication){
|
||||
@Validated @RequestBody CommentForm commentForm, Errors errors,
|
||||
@AuthenticationPrincipal PrincipalDetails principal){
|
||||
|
||||
if (errors.hasErrors()) {
|
||||
throw new CustomFormException(Objects.requireNonNull(errors.getFieldError()).getDefaultMessage());
|
||||
}
|
||||
|
||||
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
|
||||
Member member = principal.getMember();
|
||||
Article article = articleService.readArticle(articleId);
|
||||
|
||||
// 부모 댓글인지 자식댓글인지 분기로 저장
|
||||
if(parentId != null){
|
||||
commentService.saveCComment(commentForm, member, article, parentId);
|
||||
}
|
||||
@@ -46,20 +57,16 @@ public class CommentController {
|
||||
commentService.savePComment(commentForm, member, article);
|
||||
}
|
||||
|
||||
List<CommentDto> commentList = commentService.getCommentList(articleId);
|
||||
return commentList;
|
||||
|
||||
|
||||
return CommentDto.listCreateFrom(commentService.getCommentList(articleId),0);
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 삭제 요청
|
||||
*/
|
||||
@PostMapping("/comment/delete")
|
||||
public List<CommentDto> deleteComment(@RequestParam Long articleId,
|
||||
@RequestParam Long commentId,
|
||||
Authentication authentication) {
|
||||
|
||||
@RequestParam Long commentId) {
|
||||
commentService.deleteComment(commentId);
|
||||
List<CommentDto> commentList = commentService.getCommentList(articleId);
|
||||
return commentList;
|
||||
return CommentDto.listCreateFrom(commentService.getCommentList(articleId),0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package myblog.blog.comment.domain;
|
||||
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import myblog.blog.article.domain.Article;
|
||||
@@ -35,7 +34,7 @@ public class Comment extends BasicEntity {
|
||||
|
||||
private int tier;
|
||||
|
||||
// 댓글 표시 순서
|
||||
// 댓글 표시 순서 - 자식댓글은 ID순으로 처리
|
||||
private int pOrder;
|
||||
|
||||
// 셀프조인
|
||||
@@ -47,7 +46,6 @@ public class Comment extends BasicEntity {
|
||||
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||
private List<Comment> child = new ArrayList<>();
|
||||
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id")
|
||||
private Member member;
|
||||
@@ -57,8 +55,6 @@ public class Comment extends BasicEntity {
|
||||
@Column(columnDefinition = "boolean default false")
|
||||
private boolean secret;
|
||||
|
||||
|
||||
|
||||
@Builder
|
||||
public Comment(Article article, int tier, Comment parents,int pOrder, Member member, String content, boolean secret) {
|
||||
|
||||
@@ -71,7 +67,5 @@ public class Comment extends BasicEntity {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
protected Comment() {
|
||||
|
||||
}
|
||||
protected Comment() {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package myblog.blog.comment.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
@@ -10,6 +9,10 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/*
|
||||
- 트리구조 댓글 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class CommentDto {
|
||||
@@ -22,26 +25,33 @@ public class CommentDto {
|
||||
private String picUrl;
|
||||
private String content;
|
||||
private boolean secret;
|
||||
private List<CommentDto> commentDtoList = new ArrayList<>();
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
public static List<CommentDto> createFrom(List<Comment> commentList, int dept) {
|
||||
// 트리 구조를 갖기위한 리스트
|
||||
private List<CommentDto> commentDtoList = new ArrayList<>();
|
||||
|
||||
/*
|
||||
- 재귀 호출용 스태틱 생성 메서드
|
||||
1. DTO객체 생성후 소스를 큐처리로 순차적 매핑
|
||||
2. Depth 변화시 재귀 호출 / 재귀 탈출
|
||||
3. 탈출시 상위 카테고리 list로 삽입하여 트리구조 작성
|
||||
*/
|
||||
public static List<CommentDto> listCreateFrom(List<Comment> commentSource, int dept) {
|
||||
|
||||
ArrayList<CommentDto> commentDtoList = new ArrayList<>();
|
||||
|
||||
while (true) {
|
||||
|
||||
if (commentList.isEmpty()) {
|
||||
if (commentSource.isEmpty()) {
|
||||
return commentDtoList;
|
||||
}
|
||||
|
||||
Comment comment = commentList.get(0);
|
||||
Comment comment = commentSource.get(0);
|
||||
|
||||
if (comment.getTier() == dept) {
|
||||
commentDtoList.add(new CommentDto(comment));
|
||||
commentList.remove(0);
|
||||
commentSource.remove(0);
|
||||
} else if (comment.getTier() > dept) {
|
||||
List<CommentDto> childList = createFrom(commentList, dept + 1);
|
||||
List<CommentDto> childList = listCreateFrom(commentSource, dept + 1);
|
||||
commentDtoList.get(commentDtoList.size() - 1)
|
||||
.setCommentDtoList(childList);
|
||||
} else {
|
||||
@@ -50,7 +60,7 @@ public class CommentDto {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 매핑 생성
|
||||
public CommentDto(Comment comment) {
|
||||
this.id = comment.getId();
|
||||
this.tier = comment.getTier();
|
||||
@@ -63,5 +73,4 @@ public class CommentDto {
|
||||
this.memberId = comment.getMember().getId();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,14 +4,18 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import myblog.blog.article.domain.Article;
|
||||
|
||||
|
||||
/*
|
||||
- 레이아웃 노출용 댓글 DTO
|
||||
*/
|
||||
@Getter @Setter
|
||||
public class CommentDtoForSide {
|
||||
public class CommentDtoForLayout {
|
||||
private Long id;
|
||||
private Long articleId;
|
||||
private String content;
|
||||
private boolean secret;
|
||||
|
||||
public CommentDtoForSide(Long id, Long articleId, String content, boolean secret) {
|
||||
public CommentDtoForLayout(Long id, Long articleId, String content, boolean secret) {
|
||||
this.id = id;
|
||||
this.secret = secret;
|
||||
this.articleId = articleId;
|
||||
@@ -2,10 +2,18 @@ package myblog.blog.comment.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/*
|
||||
- 댓글 작성 폼
|
||||
*/
|
||||
@Data
|
||||
public class CommentForm {
|
||||
|
||||
private String content;
|
||||
private boolean secret;
|
||||
@NotBlank(message = "댓글 내용을 작성해주세요")
|
||||
@Size(min = 1,max = 250, message = "댓글은 255자 이내로 작성해주세요")
|
||||
private String content;
|
||||
private boolean secret;
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import java.util.List;
|
||||
|
||||
public interface CommentRepository extends JpaRepository<Comment, Long> {
|
||||
|
||||
/*
|
||||
- 특정 아티클에 해당하는 댓글 리스트 가져오기
|
||||
*/
|
||||
@Query("select c " +
|
||||
"from Comment c " +
|
||||
"left join fetch c.member " +
|
||||
@@ -19,7 +22,13 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
|
||||
"order by c.pOrder, c.id asc")
|
||||
List<Comment> findCommentsByArticleId(@Param("articleId") Long articleId);
|
||||
|
||||
/*
|
||||
- 특정 아티클의 부모 댓글 총 갯수
|
||||
*/
|
||||
int countCommentsByArticleAndTier(Article article,int tier);
|
||||
|
||||
/*
|
||||
- 전체 댓글중 최신 댓글 5개
|
||||
*/
|
||||
List<Comment> findTop5ByOrderByIdDesc();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper
|
||||
public interface NaCommentRepository {
|
||||
|
||||
|
||||
/*
|
||||
- cascade 삭제처리
|
||||
*/
|
||||
@Delete("delete from comment " +
|
||||
"where comment_id = #{commentId} ")
|
||||
void deleteComment(Long commentId);
|
||||
|
||||
@@ -3,38 +3,36 @@ package myblog.blog.comment.service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
import myblog.blog.comment.dto.CommentDto;
|
||||
import myblog.blog.comment.dto.CommentForm;
|
||||
import myblog.blog.comment.repository.CommentRepository;
|
||||
import myblog.blog.comment.repository.NaCommentRepository;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CommentService {
|
||||
|
||||
private final CommentRepository commentRepository;
|
||||
private final ModelMapper modelMapper;
|
||||
private final NaCommentRepository naCommentRepository;
|
||||
|
||||
public List<CommentDto> getCommentList(Long articleId){
|
||||
|
||||
List<Comment> commentsByArticleId = commentRepository.findCommentsByArticleId(articleId);
|
||||
|
||||
return CommentDto.createFrom(commentsByArticleId,0);
|
||||
|
||||
/*
|
||||
- 아티클에 달린 댓글 전체 가져오기
|
||||
*/
|
||||
public List<Comment> getCommentList(Long articleId){
|
||||
return commentRepository.findCommentsByArticleId(articleId);
|
||||
}
|
||||
|
||||
/*
|
||||
- 부모 댓글 저장
|
||||
*/
|
||||
public void savePComment(CommentForm commentForm, Member member, Article article){
|
||||
|
||||
Comment comment = Comment.builder()
|
||||
.article(article)
|
||||
.content(commentForm.getContent())
|
||||
.content(removeDuplicatedEnter(commentForm))
|
||||
.tier(0)
|
||||
.pOrder(commentRepository.countCommentsByArticleAndTier(article,0)+1)
|
||||
.member(member)
|
||||
@@ -45,19 +43,16 @@ public class CommentService {
|
||||
|
||||
}
|
||||
|
||||
public void deleteComment(Long commentId){
|
||||
|
||||
naCommentRepository.deleteComment(commentId);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 자식 댓글 저장
|
||||
*/
|
||||
public void saveCComment(CommentForm commentForm, Member member, Article article, Long parentId) {
|
||||
|
||||
Comment pComment = commentRepository.findById(parentId).get();
|
||||
|
||||
Comment comment = Comment.builder()
|
||||
.article(article)
|
||||
.content(commentForm.getContent())
|
||||
.content(removeDuplicatedEnter(commentForm))
|
||||
.tier(1)
|
||||
.pOrder(pComment.getPOrder())
|
||||
.member(member)
|
||||
@@ -69,7 +64,53 @@ public class CommentService {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 댓글 삭제
|
||||
*/
|
||||
public void deleteComment(Long commentId){
|
||||
naCommentRepository.deleteComment(commentId);
|
||||
}
|
||||
|
||||
/*
|
||||
- 최신 댓글 5개 가져오기
|
||||
*/
|
||||
public List<Comment> recentCommentList(){
|
||||
return commentRepository.findTop5ByOrderByIdDesc();
|
||||
}
|
||||
|
||||
/*
|
||||
- 중복 개행 개행 하나로 압축 알고리즘
|
||||
*/
|
||||
private String removeDuplicatedEnter(CommentForm commentForm) {
|
||||
|
||||
char[] contentBox = new char[commentForm.getContent().length()];
|
||||
int idx = 0;
|
||||
String zipWord = "\n\n";
|
||||
|
||||
for(int i = 0; i< commentForm.getContent().length(); i++){
|
||||
|
||||
contentBox[idx] = commentForm.getContent().charAt(i);
|
||||
|
||||
if(contentBox[idx] == '\n'&&idx >= 1){
|
||||
|
||||
int tempIdx = idx;
|
||||
int length = 1;
|
||||
boolean isRemove = true;
|
||||
|
||||
for(int j = 0; j<2; j++){
|
||||
|
||||
if(contentBox[tempIdx--] != zipWord.charAt(length--)){
|
||||
|
||||
isRemove = false;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
if(isRemove) idx -= 1;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
return String.valueOf(contentBox).trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package myblog.blog.exception;
|
||||
/*
|
||||
- REST 컨트롤러 상태 메세지 전송용 커스텀 에러
|
||||
*/
|
||||
public class CustomFormException extends RuntimeException {
|
||||
public CustomFormException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package myblog.blog.exception;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
|
||||
@ControllerAdvice
|
||||
@Slf4j
|
||||
public class ExceptionController {
|
||||
|
||||
// @ExceptionHandler
|
||||
// public String handleRuntimeException(Principal principal, HttpServletRequest req, RuntimeException e) {
|
||||
// if (principal != null) {
|
||||
// log.info("'{}' requested '{}' ", principal.getName(), req.getRequestURI());
|
||||
// } else {
|
||||
// log.info("requested '{}'", req.getRequestURI());
|
||||
// }
|
||||
//
|
||||
// log.error("bad request", e);
|
||||
// return "";
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package myblog.blog.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/*
|
||||
- REST 컨트롤러 요청 에러 제어
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class ExceptionRestController {
|
||||
@ExceptionHandler(CustomFormException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public String handleCategoryControllerException(RuntimeException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
32
src/main/java/myblog/blog/exception/ListValidator.java
Normal file
32
src/main/java/myblog/blog/exception/ListValidator.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package myblog.blog.exception;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/*
|
||||
- List 순환으로 유효성 검사하게하는 커스텀 Validator
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ListValidator implements Validator {
|
||||
|
||||
private final SpringValidatorAdapter springValidatorAdapter;
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return List.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Object target, Errors errors) {
|
||||
for(Object object : (List)target){
|
||||
springValidatorAdapter.validate(object,errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,19 +13,21 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/*
|
||||
- 회원가입 실패 핸들러
|
||||
이메일 중복시 간편 회원가입 실패
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||
|
||||
String errMsg = "error";
|
||||
String errMsg = "";
|
||||
|
||||
if(exception instanceof OAuth2AuthenticationException){
|
||||
errMsg = "duplicatedEmail";
|
||||
request.setAttribute("errMsg", errMsg);
|
||||
|
||||
}
|
||||
|
||||
setDefaultFailureUrl("/login?error="+errMsg);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package myblog.blog.img.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.img.domain.UploadedImg;
|
||||
import myblog.blog.img.dto.UploadImgDto;
|
||||
import myblog.blog.img.service.UploadImgService;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -17,12 +16,12 @@ public class UploadImgController {
|
||||
|
||||
private final UploadImgService uploadImgService;
|
||||
|
||||
/*
|
||||
- 썸네일 업로드 요청
|
||||
*/
|
||||
@PostMapping("/article/uploadImg")
|
||||
public @ResponseBody
|
||||
String imgUpload(@ModelAttribute UploadImgDto uploadImgDto) throws IOException {
|
||||
|
||||
return uploadImgService.storeImg(uploadImgDto.getImg());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package myblog.blog.img.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
public class UploadedImg {
|
||||
|
||||
private String uploadFileName;
|
||||
private String storeFileName;
|
||||
private String uploadUrl;
|
||||
|
||||
public UploadedImg(String uploadFileName, String storeFileName, String uploadUrl) {
|
||||
this.uploadFileName = uploadFileName;
|
||||
this.storeFileName = storeFileName;
|
||||
this.uploadUrl = uploadUrl;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/*
|
||||
- 멀티파트 파일 래핑용 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class UploadImgDto {
|
||||
|
||||
private MultipartFile img;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package myblog.blog.img.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.img.domain.UploadedImg;
|
||||
import org.kohsuke.github.GHRepository;
|
||||
import org.kohsuke.github.GitHub;
|
||||
import org.kohsuke.github.GitHubBuilder;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -17,6 +15,9 @@ import java.util.UUID;
|
||||
@RequiredArgsConstructor
|
||||
public class UploadImgService {
|
||||
|
||||
/*
|
||||
- 설정 파일로 잡아놓은 깃헙 이미지 레포지토리와 토큰
|
||||
*/
|
||||
@Value("${git.gitToken}")
|
||||
private String gitToken;
|
||||
|
||||
@@ -26,32 +27,38 @@ public class UploadImgService {
|
||||
@Value("${git.imgUrl}")
|
||||
private String imgUrl;
|
||||
|
||||
/*
|
||||
- 이미지 저장 로직
|
||||
1. 깃허브 Repo에 이미지 업로드
|
||||
2. 업로드된 Url 반환
|
||||
*/
|
||||
public String storeImg(MultipartFile multipartFile) throws IOException {
|
||||
if (multipartFile.isEmpty()) {
|
||||
throw new IllegalArgumentException("이미지가 존재하지 않습니다.");
|
||||
}
|
||||
|
||||
String storeFileName = createStoreFileName(multipartFile.getOriginalFilename());
|
||||
|
||||
GitHub gitHub = new GitHubBuilder().withOAuthToken(gitToken).build();
|
||||
GHRepository repository = gitHub.getRepository(gitRepo);
|
||||
|
||||
String originalFilename = multipartFile.getOriginalFilename();
|
||||
String storeFileName = createStoreFileName(originalFilename);
|
||||
|
||||
repository.createContent().path("img/"+storeFileName)
|
||||
.content(multipartFile.getBytes()).message("test").branch("main").commit();
|
||||
|
||||
UploadedImg uploadedImg = new UploadedImg(originalFilename, storeFileName, imgUrl + storeFileName + "?raw=true");
|
||||
|
||||
return uploadedImg.getUploadUrl();
|
||||
.content(multipartFile.getBytes()).message("thumbnail").branch("main").commit();
|
||||
|
||||
return imgUrl + storeFileName + "?raw=true";
|
||||
}
|
||||
|
||||
/*
|
||||
- 이미지 중복 방지용 무작위 파일 이름 생성기
|
||||
*/
|
||||
private String createStoreFileName(String originalFilename) {
|
||||
String ext = extractExt(originalFilename);
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
return uuid + "." + ext;
|
||||
}
|
||||
|
||||
/*
|
||||
- 파일 이름 추출
|
||||
*/
|
||||
private String extractExt(String originalFilename) {
|
||||
int pos = originalFilename.lastIndexOf(".");
|
||||
return originalFilename.substring(pos + 1);
|
||||
|
||||
@@ -6,7 +6,7 @@ import myblog.blog.article.dto.ArticleDtoForMain;
|
||||
import myblog.blog.article.service.ArticleService;
|
||||
import myblog.blog.category.dto.CategoryForView;
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.comment.dto.CommentDtoForSide;
|
||||
import myblog.blog.comment.dto.CommentDtoForLayout;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.data.domain.Slice;
|
||||
@@ -29,33 +29,41 @@ public class MainController {
|
||||
private final CommentService commentService;
|
||||
private final ModelMapper modelMapper;
|
||||
|
||||
/*
|
||||
- 메인 화면 제어용 컨트롤러
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String main(Model model) {
|
||||
|
||||
// Dto 전처리
|
||||
CategoryForView categoryForView = CategoryForView.createCategory(categoryService.getCategoryForView());
|
||||
|
||||
model.addAttribute("category",categoryForView);
|
||||
List<CommentDtoForSide> comments = commentService.recentCommentList()
|
||||
List<CommentDtoForLayout> comments = commentService.recentCommentList()
|
||||
.stream()
|
||||
.map(comment ->
|
||||
new CommentDtoForSide(comment.getId(), comment.getArticle().getId(), comment.getContent(),comment.isSecret()))
|
||||
new CommentDtoForLayout(comment.getId(), comment.getArticle().getId(), comment.getContent(),comment.isSecret()))
|
||||
.collect(Collectors.toList());
|
||||
model.addAttribute("commentsList", comments);
|
||||
|
||||
List<ArticleDtoForMain> popularArticles = articleService.getPopularArticles()
|
||||
.stream()
|
||||
.map(article -> modelMapper.map(article, ArticleDtoForMain.class))
|
||||
.collect(Collectors.toList());
|
||||
model.addAttribute("popularArticles", popularArticles);
|
||||
|
||||
Slice<ArticleDtoForMain> recentArticles = articleService.getRecentArticles(0)
|
||||
.map(article -> modelMapper.map(article, ArticleDtoForMain.class));
|
||||
//
|
||||
|
||||
model.addAttribute("category",categoryForView);
|
||||
model.addAttribute("commentsList", comments);
|
||||
model.addAttribute("popularArticles", popularArticles);
|
||||
model.addAttribute("recentArticles",recentArticles);
|
||||
|
||||
return "index";
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 최신 아티클 무한스크롤로 조회
|
||||
*/
|
||||
@GetMapping("/main/article/{pageNum}")
|
||||
public @ResponseBody
|
||||
List<ArticleDtoForMain> mainNextPage(@PathVariable int pageNum) {
|
||||
@@ -66,5 +74,4 @@ public class MainController {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package myblog.blog.member.auth;
|
||||
|
||||
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import myblog.blog.member.doamin.Role;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -12,27 +11,28 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/*
|
||||
- 멤버 객체를 래핑한 커스텀 Principal 클래스
|
||||
*/
|
||||
public class PrincipalDetails implements OAuth2User {
|
||||
|
||||
private final Map<String, Object> attributes;
|
||||
private final Member member;
|
||||
|
||||
|
||||
public PrincipalDetails(Member member, Map<String, Object> attributes) {
|
||||
this.member = member;
|
||||
this.attributes = attributes;
|
||||
}
|
||||
public Member getMember(){return member;}
|
||||
|
||||
// 타임리프 뷰단처리를 위한 편의 메소드
|
||||
public Long getMemberId(){
|
||||
return member.getId();
|
||||
}
|
||||
|
||||
public String getMemberPicUrl(){
|
||||
return member.getPicUrl();
|
||||
}
|
||||
|
||||
public Member getMember(){return member;}
|
||||
|
||||
// Oauth2
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
|
||||
@@ -10,27 +10,33 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/*
|
||||
- 로그인 루트에 따른 UserInfo 생성 클래스
|
||||
팩터리 메서드 패턴으로 하위타입 반환
|
||||
*/
|
||||
@Component
|
||||
public class UserInfoFactory {
|
||||
|
||||
// 하위타입 생성 메소드와 열거타입 매핑
|
||||
private final static Map<ProviderType, Function<OAuth2User, Oauth2UserInfo>> userInfoFactoryMap;
|
||||
|
||||
static {
|
||||
userInfoFactoryMap = new EnumMap<>(ProviderType.class);
|
||||
userInfoFactoryMap.put(ProviderType.GOOGLE, GoogleUserInfo::new);
|
||||
userInfoFactoryMap.put(ProviderType.FACEBOOK, FacebookUserInfo::new);
|
||||
userInfoFactoryMap.put(ProviderType.KAKAO, KakaoUserInfo::new);
|
||||
userInfoFactoryMap.put(ProviderType.NAVER, NaverUserInfo::new);
|
||||
}
|
||||
|
||||
// String 파라미터를 열거타입으로 컨버팅하기위한 매핑
|
||||
private static final Map<String, ProviderType> stringToEnum;
|
||||
|
||||
static {
|
||||
stringToEnum =
|
||||
Stream.of(ProviderType.values())
|
||||
.collect(Collectors.toMap(ProviderType::getValue, providerType->providerType));
|
||||
}
|
||||
|
||||
/*
|
||||
- 팩토리 메소드
|
||||
*/
|
||||
public Oauth2UserInfo makeOauth2UserinfoOf(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
|
||||
|
||||
Optional<ProviderType> providerTypeOptional = fromString(oAuth2UserRequest.getClientRegistration().getRegistrationId());
|
||||
@@ -41,6 +47,10 @@ public class UserInfoFactory {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- String을 열거타입으로 컨버팅 로직
|
||||
존재하지 않는 요청위험 고려해서 Optional처리
|
||||
*/
|
||||
private Optional<ProviderType> fromString(String provider){
|
||||
return Optional.ofNullable(stringToEnum.get(provider));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.Map;
|
||||
|
||||
public class FacebookUserInfo implements Oauth2UserInfo {
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
public FacebookUserInfo(OAuth2User oAuth2User) {
|
||||
this.attributes = oAuth2User.getAttributes();
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package myblog.blog.member.auth.userinfo;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class KakaoUserInfo implements Oauth2UserInfo {
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
public KakaoUserInfo(OAuth2User oAuth2User) {
|
||||
this.attributes = oAuth2User.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return attributes.get("id").toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProvider() {
|
||||
return ProviderType.KAKAO.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
Map<String,Object> kakao_account = (Map)attributes.get("kakao_account");
|
||||
return kakao_account.get("email").toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserName() {
|
||||
Map<String,Object> kakao_account = (Map)attributes.get("kakao_account");
|
||||
Map<String,Object> profile = (Map) kakao_account.get("profile");
|
||||
return profile.get("nickname").toString()+ "#"+ ((String) attributes.get("id")).substring(0,5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPicture() {
|
||||
Map<String,Object> kakao_account = (Map)attributes.get("kakao_account");
|
||||
Map<String,Object> profile = (Map) kakao_account.get("profile");
|
||||
return profile.get("profile_image_url").toString();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import java.util.Map;
|
||||
|
||||
public class NaverUserInfo implements Oauth2UserInfo {
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
public NaverUserInfo(OAuth2User oAuth2User) {
|
||||
this.attributes = oAuth2User.getAttribute("response");
|
||||
|
||||
@@ -2,6 +2,9 @@ package myblog.blog.member.auth.userinfo;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/*
|
||||
- 팩토리 메서드 패턴을 위한 상위타입 인터페이스
|
||||
*/
|
||||
public interface Oauth2UserInfo {
|
||||
|
||||
String getProviderId();
|
||||
|
||||
@@ -3,13 +3,16 @@ package myblog.blog.member.auth.userinfo;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
|
||||
/*
|
||||
- 소셜 로그인 API 열거타입 정의
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum ProviderType {
|
||||
|
||||
FACEBOOK("facebook"),
|
||||
GOOGLE("google"),
|
||||
KAKAO("kakao"),
|
||||
NAVER("naver");
|
||||
|
||||
private final String value;
|
||||
|
||||
@@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import myblog.blog.category.dto.CategoryForView;
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.comment.dto.CommentDtoForSide;
|
||||
import myblog.blog.comment.dto.CommentDtoForLayout;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -22,26 +22,30 @@ public class MemberController {
|
||||
private final CategoryService categoryService;
|
||||
private final CommentService commentService;
|
||||
|
||||
|
||||
/*
|
||||
- 회원 로그인 폼 조회
|
||||
*/
|
||||
@GetMapping("/login")
|
||||
public String loginFrom(@RequestParam(value = "error",required = false) String error, Model model){
|
||||
|
||||
// 가입 실패시 에러 메시지 처리
|
||||
if(error!=null&&error.equals("duplicatedEmail")){
|
||||
model.addAttribute("errMsg","이미 가입된 이메일입니다.");
|
||||
}
|
||||
|
||||
// 레이아웃 DTO 전처리
|
||||
CategoryForView categoryForView = CategoryForView.createCategory(categoryService.getCategoryForView());
|
||||
model.addAttribute("category",categoryForView);
|
||||
List<CommentDtoForSide> comments = commentService.recentCommentList()
|
||||
List<CommentDtoForLayout> comments = commentService.recentCommentList()
|
||||
.stream()
|
||||
.map(comment ->
|
||||
new CommentDtoForSide(comment.getId(), comment.getArticle().getId(), comment.getContent(),comment.isSecret()))
|
||||
new CommentDtoForLayout(comment.getId(), comment.getArticle().getId(), comment.getContent(),comment.isSecret()))
|
||||
.collect(Collectors.toList());
|
||||
//
|
||||
|
||||
model.addAttribute("category",categoryForView);
|
||||
model.addAttribute("commentsList", comments);
|
||||
|
||||
|
||||
return "login";
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import javax.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/*
|
||||
- 회원 엔티티
|
||||
*/
|
||||
@Entity
|
||||
@SequenceGenerator(
|
||||
name = "MEMBER_SEQ_GENERATOR",
|
||||
@@ -47,8 +51,7 @@ public class Member extends BasicEntity {
|
||||
@OneToMany(mappedBy = "member")
|
||||
private List<Comment> commentList = new ArrayList<>();
|
||||
|
||||
protected Member() {
|
||||
}
|
||||
protected Member() {}
|
||||
|
||||
@Builder
|
||||
public Member(String username, String email, String picUrl, Role role,String userId, String provider, String providerId) {
|
||||
@@ -61,6 +64,11 @@ public class Member extends BasicEntity {
|
||||
this.providerId = providerId;
|
||||
}
|
||||
|
||||
//비지니스로직
|
||||
|
||||
/*
|
||||
- 유저명 변경 더티체킹 로직
|
||||
*/
|
||||
public void changeUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ package myblog.blog.member.doamin;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/*
|
||||
- 멤버 권한 열거
|
||||
- ADMIN 계정은 하나만
|
||||
- 나머지는 모두 USER계정
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum Role {
|
||||
|
||||
@@ -5,6 +5,9 @@ import lombok.Setter;
|
||||
|
||||
import javax.persistence.Column;
|
||||
|
||||
/*
|
||||
- 뷰단에 사용할 멈버 DTO
|
||||
*/
|
||||
@Getter @Setter
|
||||
public class MemberDto {
|
||||
|
||||
@@ -18,5 +21,4 @@ public class MemberDto {
|
||||
|
||||
private String picUrl;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,13 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
public interface MemberRepository extends JpaRepository<Member, Long> {
|
||||
|
||||
/*
|
||||
- Id로 유저 찾기
|
||||
*/
|
||||
Member findByUserId(String userId);
|
||||
/*
|
||||
- Email로 유저 찾기
|
||||
*/
|
||||
Member findByEmail(String email);
|
||||
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ public class Oauth2MemberService extends DefaultOAuth2UserService {
|
||||
@Value("${admin.provider}")
|
||||
private String adminProvider;
|
||||
|
||||
|
||||
/*
|
||||
- OAuth2 인증 로그인
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
@@ -49,12 +53,17 @@ public class Oauth2MemberService extends DefaultOAuth2UserService {
|
||||
return new PrincipalDetails(member, userInfo.getAttributes());
|
||||
}
|
||||
|
||||
/*
|
||||
- 회원가입 or 로그인 로직
|
||||
*/
|
||||
private Member getOrJoinMember(Oauth2UserInfo userInfo) {
|
||||
|
||||
//DB에서 조회해서 존재시 로그인처리, 미존재시 가입처리
|
||||
Member member = memberRepository.findByUserId(userInfo.getProviderId());
|
||||
|
||||
if(member == null) {
|
||||
|
||||
//Email 중복검증
|
||||
if(memberRepository.findByEmail(userInfo.getEmail()) != null)
|
||||
throw new OAuth2AuthenticationException("duplicateEmail");
|
||||
|
||||
@@ -71,6 +80,7 @@ public class Oauth2MemberService extends DefaultOAuth2UserService {
|
||||
memberRepository.save(member);
|
||||
}
|
||||
|
||||
// 유저 네임 변경시 더티체킹으로 유저네임 변경
|
||||
if(!member.getUsername().equals(userInfo.getUserName())){
|
||||
member.changeUsername(userInfo.getUserName());
|
||||
}
|
||||
@@ -78,6 +88,9 @@ public class Oauth2MemberService extends DefaultOAuth2UserService {
|
||||
return member;
|
||||
}
|
||||
|
||||
/*
|
||||
- 앱 구동시 ADMIN 계정 INSERT
|
||||
*/
|
||||
@PostConstruct
|
||||
public void insertAdmin(){
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import myblog.blog.base.domain.BasicEntity;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
/*
|
||||
- 다 대 다 연관관계 해소 엔티티
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@SequenceGenerator(
|
||||
@@ -14,6 +17,7 @@ import javax.persistence.*;
|
||||
sequenceName = "ARTICLE_TAG_LIST_SEQ",
|
||||
initialValue = 1, allocationSize = 50)
|
||||
public class ArticleTagList extends BasicEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "article_tag_list_id")
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ARTICLE_TAG_LIST_SEQ_GENERATOR")
|
||||
@@ -33,7 +37,5 @@ public class ArticleTagList extends BasicEntity {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
protected ArticleTagList() {
|
||||
|
||||
}
|
||||
protected ArticleTagList() {}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,5 @@ public class Tags extends BasicEntity {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
protected Tags() {
|
||||
|
||||
}
|
||||
protected Tags() {}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package myblog.blog.tags.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/*
|
||||
- 뷰단 사용을 위한 DTO
|
||||
*/
|
||||
@Data
|
||||
public class TagsDto {
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
public interface ArticleTagListsRepository extends JpaRepository<ArticleTagList, Long> {
|
||||
|
||||
|
||||
/*
|
||||
- 아티클 연관 태그 삭제 쿼리
|
||||
- cascade 필요시에는 아티클 삭제로 일괄 삭제하므로 해당쿼리는 연관태그 수정용
|
||||
*/
|
||||
@Transactional
|
||||
@Modifying
|
||||
@Query("delete from ArticleTagList t " +
|
||||
"where t.article =:article")
|
||||
void deleteByArticle(@Param("article") Article article);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package myblog.blog.tags.repository;
|
||||
import myblog.blog.tags.domain.Tags;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TagsRepository extends JpaRepository<Tags, Long> {
|
||||
|
||||
Tags findByName(String name);
|
||||
Optional<Tags> findByName(String name);
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@@ -22,31 +23,42 @@ public class TagsService {
|
||||
private final TagsRepository tagsRepository;
|
||||
private final ArticleTagListsRepository articleTagListsRepository;
|
||||
|
||||
/*
|
||||
- Json 객체로 넘어온 태그들을 파싱해서 신규 태그인경우 저장
|
||||
*/
|
||||
public void createNewTagsAndArticleTagList(String names, Article article) {
|
||||
|
||||
Gson gson = new Gson();
|
||||
ArrayList<Map> tagsDtoArrayList = gson.fromJson(names, ArrayList.class);
|
||||
ArrayList<Map<String,String>> tagsDtoArrayList = gson.fromJson(names, ArrayList.class);
|
||||
|
||||
// JsonString -> tag
|
||||
for (Map tags : tagsDtoArrayList) {
|
||||
for (Map<String,String> tags : tagsDtoArrayList) {
|
||||
|
||||
Tags tag = tagsRepository.findByName(tags.get("value").toString());
|
||||
if (tag == null) {
|
||||
tag = tagsRepository.save(Tags.builder().name(tags.get("value").toString()).build());
|
||||
// 신규태그인경우 저장 아닌경우 그대로 조회
|
||||
Tags tag =
|
||||
tagsRepository
|
||||
.findByName(tags.get("value"))
|
||||
.orElseGet(() ->
|
||||
tagsRepository
|
||||
.save(Tags.builder().name(tags.get("value")).build()));
|
||||
|
||||
}
|
||||
// 아티클 연관 태그로 저장
|
||||
articleTagListsRepository.save(ArticleTagList.builder()
|
||||
.article(article)
|
||||
.tags(tag)
|
||||
.build());
|
||||
.tags(tag).build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
- 전체 태그 조회
|
||||
*/
|
||||
public List<Tags> findAllTags(){
|
||||
return tagsRepository.findAll();
|
||||
}
|
||||
|
||||
/*
|
||||
- 아티클 연관 태그 모두 삭제
|
||||
*/
|
||||
public void deleteArticleTags(Article article){
|
||||
articleTagListsRepository.deleteByArticle(article);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
font-family: 'RIDIBatang','Open Sans', 'Helvetica Neue', 'Helvetica', 'Arial', '나눔바른고딕',
|
||||
'Nanum Barun Gothic', '맑은고딕', 'Malgun Gothic', sans-serif;
|
||||
z-index: 20;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.toastui-editor-contents *:not(table) {
|
||||
|
||||
@@ -49,11 +49,6 @@ button img{
|
||||
background-color: #2d4373;
|
||||
}
|
||||
|
||||
.btn-kakao{
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-naver{
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -231,13 +231,16 @@ body::-webkit-scrollbar-track {
|
||||
.arrow-up {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 50px;
|
||||
font-size: 50px;
|
||||
border-radius: 100%;
|
||||
border: white;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 30px;
|
||||
line-height: 50px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
|
||||
color: white;
|
||||
background-color: rgb(241, 226, 89);
|
||||
z-index: 5;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
const input = document.querySelector('input[name="tags"]');
|
||||
|
||||
const tagify = new Tagify(input, {
|
||||
whitelist:whitelist,
|
||||
maxTags: 10,
|
||||
dropdown: {
|
||||
maxItems: 20,
|
||||
classname: "tags-look",
|
||||
enabled: 0,
|
||||
closeOnSelect: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -92,12 +92,17 @@
|
||||
// DTO 카테고리 셀렉터
|
||||
let category;
|
||||
|
||||
// 카테고리명 중복 검사
|
||||
function isDuplicatedCategoryName(){
|
||||
// 카테고리명 유효성 검사
|
||||
function checkCategoryName(){
|
||||
const categoryList = document.getElementById("categoryBox").childNodes;
|
||||
for (let i = 0; i<categoryList.length; i++) {
|
||||
const curNode = categoryList[i];
|
||||
|
||||
if(curNode.textContent === ""||curNode.textContent === " "){
|
||||
alert("카테고리 명이 공백이면 안됩니다.");
|
||||
return true;
|
||||
}
|
||||
|
||||
for(let j = i+1; j<categoryList.length; j++){
|
||||
if(curNode.textContent === categoryList[j].textContent){
|
||||
alert("카테고리 명이 중복되면 안됩니다.");
|
||||
@@ -110,7 +115,7 @@
|
||||
|
||||
function clickCategory(div) {
|
||||
|
||||
if(isDuplicatedCategoryName()){
|
||||
if(checkCategoryName()){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,7 +193,6 @@
|
||||
function tierDown(){
|
||||
category.tier = 2;
|
||||
selector.classList.remove("fw-bold");
|
||||
|
||||
}
|
||||
|
||||
function addCategory(){
|
||||
@@ -204,7 +208,6 @@
|
||||
newCategory.corder = 0;
|
||||
|
||||
categorysList.push(newCategory);
|
||||
|
||||
}
|
||||
|
||||
function deleteCategory(){
|
||||
@@ -214,7 +217,7 @@
|
||||
}
|
||||
|
||||
function changeCategory(){
|
||||
if(isDuplicatedCategoryName()){
|
||||
if(checkCategoryName()){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,7 +233,7 @@
|
||||
location.href='/';
|
||||
}
|
||||
else{
|
||||
alert("에러발생");
|
||||
alert(xhr.response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,6 @@
|
||||
<script th:replace="layout/fragments.html :: tag"></script>
|
||||
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
|
||||
<script src="/js/getCsrf.js"></script>
|
||||
<script src="/js/tags.js"></script>
|
||||
<script src="/js/thumbnail.js"></script>
|
||||
<script th:replace="layout/fragments.html :: articleEdit"></script>
|
||||
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
</div>
|
||||
<!-- -->
|
||||
|
||||
|
||||
<div class="row justify-content-evenly mt-4 mb-5 g-0">
|
||||
<button class="btn btn-secondary col-4" onclick="post()">등 록</button>
|
||||
<button class="btn btn-secondary col-4" onclick="javascript:history.back()">취 소</button>
|
||||
@@ -113,20 +112,14 @@
|
||||
|
||||
<!--scripts-->
|
||||
|
||||
<script th:inline="javascript">
|
||||
const tags = [[${tagsInput}]];
|
||||
let whitelist = [];
|
||||
for (let tag of tags) {
|
||||
whitelist.push(tag.name)
|
||||
}
|
||||
</script>
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/node_modules/@yaireo/tagify/dist/tagify.min.js"></script>
|
||||
<script th:replace="layout/fragments.html :: tag"></script>
|
||||
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
|
||||
<script src="/js/getCsrf.js"></script>
|
||||
<script th:replace="layout/fragments.html :: articleWrite"></script>
|
||||
<script src="/js/tags.js"></script>
|
||||
<script src="/js/thumbnail.js"></script>
|
||||
<script th:replace="layout/fragments.html :: articleWrite"></script>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
@@ -43,16 +43,18 @@
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
function commentWrite() {
|
||||
let token = getCsrfToken();
|
||||
let content = document.getElementById("commentContent").value;
|
||||
let secret = document.getElementById("isCommentSecret")
|
||||
let isSecret = secret.checked;
|
||||
let isSecret = document.getElementById("isCommentSecret").checked;
|
||||
|
||||
console.log(isSecret);
|
||||
// 댓글 내용 유효성 검사
|
||||
if(content === ""|| content ===" "){
|
||||
alert("댓글을 작성해주세요");
|
||||
return;
|
||||
}
|
||||
|
||||
let commentForm = new Object();
|
||||
let commentForm = {};
|
||||
commentForm.content = content;
|
||||
commentForm.secret = isSecret;
|
||||
|
||||
@@ -63,8 +65,13 @@
|
||||
xhr.send(JSON.stringify(commentForm));
|
||||
|
||||
xhr.onload = () => {
|
||||
makeCommentBox(xhr, replyBox);
|
||||
document.getElementById("commentContent").value = '';
|
||||
if (xhr.status === 200 || xhr.status === 201 || xhr.status === 202) {
|
||||
makeCommentBox(xhr, replyBox);
|
||||
document.getElementById("commentContent").value = '';
|
||||
}
|
||||
else{
|
||||
alert(xhr.response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +81,7 @@
|
||||
let isSecret = document.getElementById("isCommentSecret-"+ parentCommentId).checked;
|
||||
|
||||
|
||||
let commentForm = new Object();
|
||||
let commentForm = {};
|
||||
commentForm.content = content;
|
||||
commentForm.secret = isSecret;
|
||||
|
||||
@@ -92,7 +99,7 @@
|
||||
|
||||
function deleteCommentConfirm(commentId) {
|
||||
|
||||
if (confirm("댓글을 정말 삭제하시겠습니까?") == true) {
|
||||
if (confirm("댓글을 정말 삭제하시겠습니까?") === true) {
|
||||
deleteComment(commentId);
|
||||
} else {
|
||||
return false;
|
||||
@@ -141,7 +148,7 @@
|
||||
<div class="d-flex flex-row p-2">
|
||||
<div>
|
||||
<img src="${parentComment.picUrl}" width="40" height="40"
|
||||
class="rounded-circle me-2">
|
||||
class="rounded-circle me-2" alt="">
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@@ -159,20 +166,20 @@
|
||||
`</div>
|
||||
</div>`
|
||||
|
||||
// 비밀댓글 처리
|
||||
// 부모 댓글 비밀댓글 처리
|
||||
if(parentComment.secret === true){
|
||||
if(roleArr[0].authority === "ROLE_ADMIN") {
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">${parentComment.content}(비밀댓글입니다)</p>`
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">${parentComment.content}(비밀댓글입니다)</p>`
|
||||
}
|
||||
else if(member != null &&member.id === parentComment.memberId){
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">${parentComment.content}(내가 쓴 비밀댓글입니다)</p>`;
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">${parentComment.content}(내가 쓴 비밀댓글입니다)</p>`;
|
||||
}
|
||||
else{
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">비밀댓글입니다.</p>`;
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">비밀댓글입니다.</p>`;
|
||||
}
|
||||
}
|
||||
else{
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">${parentComment.content}</p>`
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">${parentComment.content}</p>`
|
||||
}
|
||||
|
||||
<!-- comment-reply-post -->
|
||||
@@ -194,7 +201,7 @@
|
||||
<div class="mt-3 d-flex flex-row align-items-center p-2 form-color">
|
||||
<div>
|
||||
</div>
|
||||
<textarea type="text" id="child-content-${parentComment.id}" name="child-content-${parentComment.id}" class="form-control" placeholder="대댓글을 작성해주세요..."></textarea>
|
||||
<textarea id="child-content-${parentComment.id}" name="child-content-${parentComment.id}" class="form-control" placeholder="대댓글을 작성해주세요..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
@@ -225,7 +232,7 @@
|
||||
`<div class="d-flex flex-row p-2">
|
||||
<div>
|
||||
<img src="${childComment.picUrl}" width="40"
|
||||
height="40" class="rounded-circle me-2">
|
||||
height="40" class="rounded-circle me-2" alt="">
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@@ -243,20 +250,20 @@
|
||||
`</div>
|
||||
</div>`
|
||||
|
||||
// 비밀댓글 처리
|
||||
// 자식댓글 비밀댓글 처리
|
||||
if(childComment.secret === true){
|
||||
if(roleArr[0].authority === "ROLE_ADMIN") {
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">${childComment.content}(비밀댓글입니다)</p>`
|
||||
if(roleArr[0].authority === "ROLE_ADMIN"||member != null &&member.id === parentComment.memberId) {
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">${childComment.content}(비밀댓글입니다)</p>`
|
||||
}
|
||||
else if(member != null &&member.id === childComment.memberId){
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">${childComment.content}(내가 쓴 비밀댓글입니다)</p>`;
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">${childComment.content}(내가 쓴 비밀댓글입니다)</p>`;
|
||||
}
|
||||
else{
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">비밀댓글입니다.</p>`;
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">비밀댓글입니다.</p>`;
|
||||
}
|
||||
}
|
||||
else{
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0">${childComment.content}</p>`
|
||||
replyHtmlSource +=`<p class="text-justify comment-text mb-0" style="white-space:pre-wrap;">${childComment.content}</p>`
|
||||
}
|
||||
|
||||
replyHtmlSource +=
|
||||
@@ -402,11 +409,26 @@
|
||||
|
||||
|
||||
<script th:fragment="tag" th:inline="javascript" type="application/javascript">
|
||||
|
||||
const tags = [[${tagsInput}]];
|
||||
let whitelist = [];
|
||||
for (let tag of tags) {
|
||||
whitelist.push(tag.name)
|
||||
}
|
||||
|
||||
const input = document.querySelector('input[name="tags"]');
|
||||
|
||||
const tagify = new Tagify(input, {
|
||||
whitelist:whitelist,
|
||||
maxTags: 10,
|
||||
dropdown: {
|
||||
maxItems: 100,
|
||||
classname: "tags-look",
|
||||
enabled: 0,
|
||||
closeOnSelect: true
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -549,7 +571,7 @@
|
||||
function autoSave(){
|
||||
|
||||
let token = getCsrfToken();
|
||||
let tempDto = new Object();
|
||||
let tempDto = {};
|
||||
tempDto.content = editor.getMarkdown();
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
@@ -13,22 +13,6 @@
|
||||
<meta name="author" content="Jinia"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
|
||||
<!-- OPEN GRAPH(FACEBOOK, LINKEDIN) -->
|
||||
<meta property="og:type" content=""/>
|
||||
<meta property="og:description" content=""/>
|
||||
<meta property="og:title" content=""/>
|
||||
<meta property="og:image" content=""/>
|
||||
<meta property="og:url" content=""/>
|
||||
<meta property="og:site_name" content=""/>
|
||||
|
||||
<!-- twitter -->
|
||||
<meta property="twitter:card" content=""/>
|
||||
<meta property="twitter:title" content=""/>
|
||||
<meta property="twitter:description" content=""/>
|
||||
<meta property="twitter:image" content=""/>
|
||||
<meta property="twitter:url" content=""/>
|
||||
<meta property="twitter:creator" content=""/>
|
||||
|
||||
<link rel="icon" href="/img/favicon-16x16.png"/>
|
||||
<link rel="apple-touch-icon" href="/img/favicon-16x16.png"/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png">
|
||||
@@ -67,12 +51,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a href="/oauth2/authorization/kakao">
|
||||
<button class="btn-kakao"><img src="/img/kakao.png"></button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a href="/oauth2/authorization/naver">
|
||||
<button class="btn-naver"><img src="/img/naver.png"></button>
|
||||
|
||||
Reference in New Issue
Block a user