211110 메인화면 개발중

1. 메인화면에 필요한 게시물 리스트 조회 로직과 화면 렌더링 구현
2. 조회수순으로 메인화면 노출과 최신 업로드 순 게시물 노출 로직 구분
3. 무한스크롤 구현
4. 스크롤 화살표 구현
5. 계층형 카테고리 개발과 화면 렌더링 완료
   - 롤업함수와 백트래킹으로 구현
This commit is contained in:
jinia91
2021-11-10 19:09:14 +09:00
parent f35a9dab0d
commit 8e0dc43370
58 changed files with 3443 additions and 490 deletions

View File

@@ -1,40 +1,62 @@
package myblog.blog.article.controller;
import lombok.RequiredArgsConstructor;
import myblog.blog.article.dto.ArticleForMainView;
import myblog.blog.article.dto.NewArticleDto;
import myblog.blog.article.service.ArticleService;
import myblog.blog.category.dto.CategoryForMainView;
import myblog.blog.category.service.CategoryService;
import myblog.blog.member.auth.PrincipalDetails;
import myblog.blog.tags.service.TagsService;
import org.springframework.data.domain.Slice;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class ArticleController {
private final ArticleService articleService;
private final TagsService tagsService;
private final CategoryService categoryService;
@GetMapping("article/write")
public String writeArticleForm(NewArticleDto newArticleDto, Model model){
CategoryForMainView categoryForView = categoryService.getCategoryForView();
model.addAttribute("category",categoryForView);
model.addAttribute(newArticleDto);
return "articleWriteForm";
return "article/articleWriteForm";
}
@PostMapping("article/write")
@Transactional
public String WriteArticle(@ModelAttribute NewArticleDto newArticleDto, Authentication authentication){
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
newArticleDto.setMemberId(principal.getMemberId());
Long articleId = articleService.writeArticle(newArticleDto);
articleService.writeArticle(newArticleDto);
return "redirect:/";
}
@GetMapping("/main/article/{pageNum}")
public @ResponseBody
List<ArticleForMainView> nextPage(@PathVariable int pageNum){
return articleService.getRecentArticles(pageNum).getContent();
}
}

View File

@@ -3,9 +3,13 @@ package myblog.blog.article.domain;
import lombok.Builder;
import lombok.Getter;
import myblog.blog.base.domain.BasicEntity;
import myblog.blog.category.domain.Category;
import myblog.blog.member.doamin.Member;
import myblog.blog.tags.domain.ArticleTagList;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@@ -27,20 +31,31 @@ public class Article extends BasicEntity {
@Column(columnDefinition = "bigint default 0",nullable = false)
private Long hit;
private String toc;
private String thumbnailUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
private String thumbnailUrl;
@OneToMany(mappedBy = "article")
private List<ArticleTagList> articleTagLists = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
protected Article() {
}
@Builder
public Article(String title, String content, String toc, Member member) {
public Article(String title, String content, String toc, Member member, String thumbnailUrl, Category category) {
this.title = title;
this.content = content;
this.toc = toc;
this.member = member;
this.thumbnailUrl = thumbnailUrl;
this.hit = 0L;
this.category = category;
}
}

View File

@@ -0,0 +1,19 @@
package myblog.blog.article.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Getter
@Setter
public class ArticleForMainView {
private Long id;
private String title;
private String content;
private String thumbnailUrl;
private LocalDateTime createdDate;
}

View File

@@ -1,9 +1,16 @@
package myblog.blog.article.dto;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import myblog.blog.tags.domain.Tags;
import myblog.blog.tags.dto.TagsDto;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Setter
@Getter
@@ -19,5 +26,8 @@ public class NewArticleDto {
private String thumbnailUrl;
private String category;
private String tags;
}

View File

@@ -0,0 +1,12 @@
package myblog.blog.article.repository;
import myblog.blog.article.domain.Article;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.repository.Repository;
public interface ArticlePagingRepository extends Repository<Article,Long> {
Slice<Article> findBy(Pageable pageable);
}

View File

@@ -1,11 +1,18 @@
package myblog.blog.article.repository;
import myblog.blog.article.domain.Article;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ArticleRepository extends JpaRepository<Article, Long> {
List<Article> findTop6ByOrderByHitDesc();
Slice<Article> findByOrderByCreatedDateDesc(Pageable pageable);
}

View File

@@ -2,27 +2,70 @@ package myblog.blog.article.service;
import lombok.RequiredArgsConstructor;
import myblog.blog.article.domain.Article;
import myblog.blog.article.dto.ArticleForMainView;
import myblog.blog.article.dto.NewArticleDto;
import myblog.blog.article.repository.ArticleRepository;
import myblog.blog.category.service.CategoryService;
import myblog.blog.member.doamin.Member;
import myblog.blog.member.repository.MemberRepository;
import myblog.blog.member.service.Oauth2MemberService;
import myblog.blog.tags.service.TagsService;
import org.modelmapper.ModelMapper;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
@Transactional
@RequiredArgsConstructor
public class ArticleService {
private final ArticleRepository articleRepository;
private final ArticleRepository
articleRepository;
private final MemberRepository memberRepository;
private final TagsService tagsService;
private final CategoryService categoryService;
private final Oauth2MemberService memberService;
private final ModelMapper modelMapper;
public Long writeArticle(NewArticleDto articleDto) {
Article newArticle = createNewArticleFrom(articleDto);
articleRepository.save(newArticle);
tagsService.createNewTagsAndArticleTagList(articleDto.getTags(), newArticle);
return newArticle.getId();
}
public List<ArticleForMainView> getPopularArticles() {
List<Article> top6ByOrderByHitDesc = articleRepository.findTop6ByOrderByHitDesc();
List<ArticleForMainView> articles = new ArrayList<>();
for (Article article : top6ByOrderByHitDesc) {
articles.add(modelMapper.map(article, ArticleForMainView.class));
}
return articles;
}
public Slice<ArticleForMainView> getRecentArticles(int page) {
return articleRepository.findByOrderByCreatedDateDesc(PageRequest.of(page, 5))
.map(article -> modelMapper.map(article, ArticleForMainView.class));
}
private Article createNewArticleFrom(NewArticleDto articleDto) {
Member member =
memberRepository.findById(articleDto.getMemberId()).orElseThrow(() -> {
@@ -33,7 +76,13 @@ public class ArticleService {
.title(articleDto.getTitle())
.content(articleDto.getContent())
.toc(articleDto.getToc())
.thumbnailUrl(articleDto.getThumbnailUrl())
.category(categoryService.findCategory(articleDto.getCategory()))
.member(member)
.build();
}
/*--------------------------------------------------------------------------------------------*/
}

View File

@@ -0,0 +1,13 @@
package myblog.blog.base.config;
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public ModelMapper modelMapper(){ return new ModelMapper();}
}

View File

@@ -10,6 +10,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
@@ -43,6 +44,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID","remember-me")
.and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.oauth2Login()
@@ -51,6 +55,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.userInfoEndpoint()
.userService(oauth2MemberService)
;
}
}

View File

@@ -0,0 +1,51 @@
package myblog.blog.category.domain;
import lombok.Builder;
import lombok.Getter;
import myblog.blog.article.domain.Article;
import myblog.blog.base.domain.BasicEntity;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@SequenceGenerator(
name = "CATEGORY_SEQ_GENERATOR",
sequenceName = "CATEGORY_SEQ",
initialValue = 1, allocationSize = 50)
public class Category extends BasicEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CATEGORY_SEQ_GENERATOR")
@Column(name = "category_id")
private Long id;
private String title;
@OneToMany(mappedBy = "category")
private List<Article> articles = new ArrayList<>();
@Column(nullable = false)
private int tier;
// 셀프조인
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parents_id")
private Category parents;
@OneToMany(mappedBy = "parents")
private List<Category> child = new ArrayList<>();
@Builder
public Category(String title, Category parents, int tier) {
this.title = title;
this.parents = parents;
this.tier = tier;
}
protected Category() {
}
}

View File

@@ -0,0 +1,14 @@
package myblog.blog.category.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CategoryCountForRepository {
private String title;
private int tier;
private int count;
}

View File

@@ -0,0 +1,53 @@
package myblog.blog.category.dto;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Getter
@Setter
public class CategoryForMainView {
private int count;
private String title;
private List<CategoryForMainView> categoryTCountList = new ArrayList<>();
public static CategoryForMainView createCategory(List<CategoryCountForRepository> crList) {
Collections.reverse(crList);
return recursBuilding(0, crList);
}
private static CategoryForMainView recursBuilding(int d, List<CategoryCountForRepository> crList) {
CategoryForMainView categoryForMainView = new CategoryForMainView();
while (!crList.isEmpty()) {
CategoryCountForRepository cSource = crList.get(0);
if (cSource.getTier() == d) {
if(categoryForMainView.getTitle() != null
&& categoryForMainView.getTitle() != cSource.getTitle()){
return categoryForMainView;
}
categoryForMainView.setTitle(cSource.getTitle());
categoryForMainView.setCount(cSource.getCount());
crList.remove(0);
} else if (cSource.getTier() > d) {
CategoryForMainView sub = recursBuilding(d + 1, crList);
categoryForMainView.getCategoryTCountList().add(sub);
} else {
return categoryForMainView;
}
}
return categoryForMainView;
}
}

View File

@@ -0,0 +1,11 @@
package myblog.blog.category.repository;
import myblog.blog.category.domain.Category;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CategoryRepository extends JpaRepository<Category, Long> {
Category findByTitle(String title);
}

View File

@@ -0,0 +1,27 @@
package myblog.blog.category.repository;
import myblog.blog.category.dto.CategoryCountForRepository;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface NaCategoryRepository {
@Select("select ifnull(f.title,'total') as title,ifnull(tier,0) as tier, count\n" +
"from \n" +
"(select ifnull(child,parent) as title, count\n" +
"from\n" +
"(select c.title 'parent', b.title as 'child' , count(*) as 'count'\n" +
"from article a\n" +
"join category b on (a.category_id = b.category_id)\n" +
"left join category c on (b.parents_id = c.category_id)\n" +
"group by parent, child with rollup) d\n" +
") e\n" +
"left join category f on (e.title = f.title)")
List<CategoryCountForRepository> getCategoryCount();
}

View File

@@ -0,0 +1,47 @@
package myblog.blog.category.service;
import lombok.RequiredArgsConstructor;
import myblog.blog.category.domain.Category;
import myblog.blog.category.dto.CategoryCountForRepository;
import myblog.blog.category.dto.CategoryForMainView;
import myblog.blog.category.repository.CategoryRepository;
import myblog.blog.category.repository.NaCategoryRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CategoryService {
private final CategoryRepository categoryRepository;
private final NaCategoryRepository naCategoryRepository;
public Long createNewCategory(String title, String parent) {
Category parentCategory = null;
if (parent != null) {
parentCategory = categoryRepository.findByTitle(parent);
}
Category category = Category.builder()
.title(title)
.parents(parentCategory)
.build();
return category.getId();
}
public Category findCategory(String title){
return categoryRepository.findByTitle(title);
}
public CategoryForMainView getCategoryForView(){
List<CategoryCountForRepository> categoryCount = naCategoryRepository.getCategoryCount();
return CategoryForMainView.createCategory(categoryCount);
}
}

View File

@@ -0,0 +1,28 @@
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;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
@RequiredArgsConstructor
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

@@ -4,7 +4,6 @@ import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UploadedImg {
private String uploadFileName;

View File

@@ -0,0 +1,13 @@
package myblog.blog.img.dto;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
@Getter
@Setter
public class UploadImgDto {
private MultipartFile img;
}

View File

@@ -15,19 +15,18 @@ import java.util.UUID;
@Service
@RequiredArgsConstructor
public class ImgService {
public class UploadImgService {
@Value("${git.gitToken}")
private String gitToken;
@Value("${git.imgRepo}")
@Value("${git.imgRepo}")
private String gitRepo;
@Value("${git.imgUrl}")
private String imgUrl;
private final ModelMapper modelMapper;
public UploadedImg storeImg(MultipartFile multipartFile) throws IOException {
public String storeImg(MultipartFile multipartFile) throws IOException {
if (multipartFile.isEmpty()) {
throw new IllegalArgumentException("이미지가 존재하지 않습니다.");
}
@@ -41,7 +40,9 @@ public class ImgService {
repository.createContent().path("img/"+storeFileName)
.content(multipartFile.getBytes()).message("test").branch("main").commit();
return new UploadedImg(originalFilename, storeFileName, imgUrl +storeFileName+"?raw=true");
UploadedImg uploadedImg = new UploadedImg(originalFilename, storeFileName, imgUrl + storeFileName + "?raw=true");
return uploadedImg.getUploadUrl();
}

View File

@@ -1,18 +1,41 @@
package myblog.blog.main;
import lombok.RequiredArgsConstructor;
import myblog.blog.article.domain.Article;
import myblog.blog.article.dto.ArticleForMainView;
import myblog.blog.article.service.ArticleService;
import myblog.blog.category.dto.CategoryForMainView;
import myblog.blog.category.service.CategoryService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class MainController {
private final ArticleService articleService;
private final CategoryService categoryService;
@GetMapping("/")
public String main() {
public String main(Model model) {
List<ArticleForMainView> popularArticles = articleService.getPopularArticles();
Slice<ArticleForMainView> recentArticles = articleService.getRecentArticles(0);
CategoryForMainView categoryForView = categoryService.getCategoryForView();
model.addAttribute("category",categoryForView);
model.addAttribute("popularArticles", popularArticles);
model.addAttribute("recentArticles",recentArticles);
return "main";
}
}

View File

@@ -1,13 +1,19 @@
package myblog.blog.member.controller;
import lombok.RequiredArgsConstructor;
import myblog.blog.category.dto.CategoryForMainView;
import myblog.blog.category.service.CategoryService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequiredArgsConstructor
public class MemberController {
private final CategoryService categoryService;
@GetMapping("/login")
public String loginFrom(@RequestParam(value = "error",required = false) String error, Model model){
@@ -15,6 +21,11 @@ public class MemberController {
model.addAttribute("errMsg","이미 가입된 이메일입니다.");
}
CategoryForMainView categoryForView = categoryService.getCategoryForView();
model.addAttribute("category",categoryForView);
return "login";
}

View File

@@ -4,7 +4,6 @@ import myblog.blog.member.doamin.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByUserId(String userId);

View File

@@ -77,7 +77,6 @@ public class Oauth2MemberService extends DefaultOAuth2UserService {
return member;
}
@PostConstruct
public void insertAdmin(){
Member admin = memberRepository.findByEmail(adminEmail);

View File

@@ -0,0 +1,37 @@
package myblog.blog.tags.domain;
import lombok.Builder;
import myblog.blog.article.domain.Article;
import myblog.blog.base.domain.BasicEntity;
import javax.persistence.*;
@Entity
@SequenceGenerator(
name = "ARTICLE_TAG_LIST_SEQ_GENERATOR",
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")
private Long id;
@ManyToOne
@JoinColumn(name = "article_id")
private Article article;
@ManyToOne
@JoinColumn(name = "tags_id")
private Tags tags;
@Builder
public ArticleTagList(Article article, Tags tags) {
this.article = article;
this.tags = tags;
}
protected ArticleTagList() {
}
}

View File

@@ -0,0 +1,36 @@
package myblog.blog.tags.domain;
import lombok.Builder;
import lombok.Getter;
import myblog.blog.base.domain.BasicEntity;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@SequenceGenerator(
name = "TAGS_SEQ_GENERATOR",
sequenceName = "TAGS_SEQ",
initialValue = 1, allocationSize = 50)
@Getter
public class Tags extends BasicEntity {
@Id
@Column(name = "tags_id")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "TAGS_SEQ_GENERATOR")
private Long id;
@Column(unique = true, nullable = false)
private String name;
@OneToMany(mappedBy = "tags")
private List<ArticleTagList> articleTagLists = new ArrayList<>();
@Builder
public Tags(String name) {
this.name = name;
}
protected Tags() {
}
}

View File

@@ -0,0 +1,14 @@
package myblog.blog.tags.dto;
import lombok.Data;
@Data
public class TagsDto {
private String value;
@Override
public String toString() {
return "{ value : " + value + "}";
}
}

View File

@@ -0,0 +1,7 @@
package myblog.blog.tags.repository;
import myblog.blog.tags.domain.ArticleTagList;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ArticleTagListsRepository extends JpaRepository<ArticleTagList, Long> {
}

View File

@@ -0,0 +1,10 @@
package myblog.blog.tags.repository;
import myblog.blog.tags.domain.Tags;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TagsRepository extends JpaRepository<Tags, Long> {
Tags findByName(String name);
}

View File

@@ -0,0 +1,49 @@
package myblog.blog.tags.service;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.RequiredArgsConstructor;
import myblog.blog.article.domain.Article;
import myblog.blog.article.repository.ArticleRepository;
import myblog.blog.tags.domain.ArticleTagList;
import myblog.blog.tags.domain.Tags;
import myblog.blog.tags.dto.TagsDto;
import myblog.blog.tags.repository.ArticleTagListsRepository;
import myblog.blog.tags.repository.TagsRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
@Transactional
@RequiredArgsConstructor
public class TagsService {
private final TagsRepository tagsRepository;
private final ArticleTagListsRepository articleTagListsRepository;
public void createNewTagsAndArticleTagList(String names, Article article) {
Gson gson = new Gson();
ArrayList<Map> tagsDtoArrayList = gson.fromJson(names, ArrayList.class);
for (Map tags : tagsDtoArrayList) {
Tags tag = tagsRepository.findByName(tags.get("value").toString());
if (tag == null) {
tag = tagsRepository.save(Tags.builder().name(tags.get("value").toString()).build());
}
articleTagListsRepository.save(ArticleTagList.builder()
.article(article)
.tags(tag)
.build());
}
}
}