21.12.07 백엔드 코드 리팩토링 1차 완료

This commit is contained in:
jinia91
2021-12-07 11:59:01 +09:00
parent 96b3fa4eee
commit 6da34c0fb8
51 changed files with 442 additions and 318 deletions

View File

@@ -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);

View File

@@ -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)

View File

@@ -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()
// 로그아웃

View File

@@ -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()

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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() {}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -0,0 +1,9 @@
package myblog.blog.exception;
/*
- REST 컨트롤러 상태 메세지 전송용 커스텀 에러
*/
public class CustomFormException extends RuntimeException {
public CustomFormException(String message) {
super(message);
}
}

View File

@@ -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 "";
// }
}

View File

@@ -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();
}
}

View 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);
}
}
}

View File

@@ -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);

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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());
}
}

View File

@@ -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() {

View File

@@ -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));
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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");

View File

@@ -2,6 +2,9 @@ package myblog.blog.member.auth.userinfo;
import java.util.Map;
/*
- 팩토리 메서드 패턴을 위한 상위타입 인터페이스
*/
public interface Oauth2UserInfo {
String getProviderId();

View File

@@ -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;

View File

@@ -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";
}
}

View File

@@ -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;
}

View File

@@ -3,6 +3,11 @@ package myblog.blog.member.doamin;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/*
- 멤버 권한 열거
- ADMIN 계정은 하나만
- 나머지는 모두 USER계정
*/
@RequiredArgsConstructor
@Getter
public enum Role {

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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(){

View File

@@ -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() {}
}

View File

@@ -30,7 +30,5 @@ public class Tags extends BasicEntity {
this.name = name;
}
protected Tags() {
}
protected Tags() {}
}

View File

@@ -2,6 +2,9 @@ package myblog.blog.tags.dto;
import lombok.Data;
/*
- 뷰단 사용을 위한 DTO
*/
@Data
public class TagsDto {

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -49,11 +49,6 @@ button img{
background-color: #2d4373;
}
.btn-kakao{
background: none;
border: none;
}
.btn-naver{
background: none;
border: none;

View File

@@ -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;

View File

@@ -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
}
});

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>