21.11.22
#### 요구사항 - 기능 요구사항 __기본기능__ v 권한 구분 관리자 권한 구현 v 깃헙아이디, 구글아이디로 로그인 가능 v 블로그 기본 CRUD v 연관글 등록 v 이미지 삽입시 이미지서버에 선업로드 후 URL반환 v 댓글과 대댓글 구현 v 랜드화면은 무한스크롤 구현 v 카테고리 화면에서는 일반 페이징 v 토스트 에디터 사용 v 게시물 조회수 순위별 v 최근 게시물 v 최근 코멘트 노출 v 블로그 태그별 검색과 태그 보이기 v 일반 검색기능 v 썸네일 링크로 추가 -----11.18 - 카테고리 목록 설정 __추가기능__ - TOC - 글 1분단위 자동저장 - 새로운글 알람 보내기 - RSS피드 - 공유하기 기능 - 글 포스팅시 자동 커밋 푸시
This commit is contained in:
@@ -8,8 +8,6 @@ import myblog.blog.article.service.ArticleService;
|
||||
import myblog.blog.category.dto.CategoryForView;
|
||||
import myblog.blog.category.dto.CategoryNormalDto;
|
||||
import myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.comment.domain.Comment;
|
||||
import myblog.blog.comment.dto.CommentDto;
|
||||
import myblog.blog.comment.dto.CommentDtoForSide;
|
||||
import myblog.blog.comment.service.CommentService;
|
||||
import myblog.blog.member.auth.PrincipalDetails;
|
||||
|
||||
@@ -80,8 +80,4 @@ public class Article extends BasicEntity {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public void deleteArticle(){
|
||||
this.articleTagLists = null;
|
||||
this.parentCommentList = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.Objects;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
|
||||
@@ -7,24 +7,27 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Component
|
||||
public class UserInfoFactory {
|
||||
|
||||
private final static Map<String, Function<OAuth2User, Oauth2UserInfo>> userInfoFactoryMap;
|
||||
|
||||
static {
|
||||
userInfoFactoryMap = new HashMap<>();
|
||||
userInfoFactoryMap.put("google", GoogleUserInfo::new);
|
||||
userInfoFactoryMap.put("facebook", FacebookUserInfo::new);
|
||||
userInfoFactoryMap.put("kakao", FacebookUserInfo::new);
|
||||
userInfoFactoryMap.put("naver", FacebookUserInfo::new);
|
||||
}
|
||||
|
||||
public Oauth2UserInfo makeOauth2UserinfoOf(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
|
||||
|
||||
if (oAuth2UserRequest.getClientRegistration().getRegistrationId().equals("google")) {
|
||||
return new GoogleUserInfo(oAuth2User.getAttributes());
|
||||
} else if (oAuth2UserRequest.getClientRegistration().getRegistrationId().equals("facebook")) {
|
||||
return new FacebookUserInfo(oAuth2User.getAttributes());
|
||||
} else if (oAuth2UserRequest.getClientRegistration().getRegistrationId().equals("kakao")) {
|
||||
return new KakaoUserInfo(oAuth2User.getAttributes());
|
||||
} else if (oAuth2UserRequest.getClientRegistration().getRegistrationId().equals("naver")) {
|
||||
return new NaverUserInfo(oAuth2User.getAttribute("response"));
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("지원하지 않는 Oauth 인증 시도입니다");}
|
||||
return userInfoFactoryMap
|
||||
.get(oAuth2UserRequest.getClientRegistration().getRegistrationId())
|
||||
.apply(oAuth2User);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package myblog.blog.member.auth.userinfo;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -9,8 +10,8 @@ public class FacebookUserInfo implements Oauth2UserInfo {
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
public FacebookUserInfo(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
public FacebookUserInfo(OAuth2User oAuth2User) {
|
||||
this.attributes = oAuth2User.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package myblog.blog.member.auth.userinfo;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -9,8 +10,8 @@ public class GoogleUserInfo implements Oauth2UserInfo{
|
||||
|
||||
private Map<String,Object> attributes;
|
||||
|
||||
public GoogleUserInfo(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
public GoogleUserInfo(OAuth2User oAuth2User) {
|
||||
this.attributes = oAuth2User.getAttributes();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package myblog.blog.member.auth.userinfo;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -9,8 +10,8 @@ public class KakaoUserInfo implements Oauth2UserInfo {
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
public KakaoUserInfo(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
public KakaoUserInfo(OAuth2User oAuth2User) {
|
||||
this.attributes = oAuth2User.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package myblog.blog.member.auth.userinfo;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -9,8 +10,8 @@ public class NaverUserInfo implements Oauth2UserInfo {
|
||||
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
public NaverUserInfo(Map<String, Object> attributes) {
|
||||
this.attributes = attributes;
|
||||
public NaverUserInfo(OAuth2User oAuth2User) {
|
||||
this.attributes = oAuth2User.getAttribute("response");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,4 +16,5 @@ public interface Oauth2UserInfo {
|
||||
|
||||
Map<String, Object> getAttributes();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package myblog.blog.member.doamin;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Table(name = "persistent_logins")
|
||||
@Entity
|
||||
@Getter
|
||||
public class PersistentLogins {
|
||||
|
||||
@Id
|
||||
@Column(length = 64)
|
||||
private String series;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String token;
|
||||
|
||||
@Column(name = "last_used", nullable = false, length = 64)
|
||||
private LocalDateTime lastUsed;
|
||||
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
|
||||
|
||||
|
||||
const myModal = new bootstrap.Modal(document.getElementById('thumbnailModal'), {keyboard: false});
|
||||
|
||||
const thumbBox = document.getElementById("thumbBox");
|
||||
const uploadThumbBtn = document.getElementById("thumbnail");
|
||||
const thumbDel = document.getElementById("thumbDelBtn");
|
||||
const previewThumb = document.getElementById("thumbnailPreView");
|
||||
const thumbUrl = document.getElementById("thumbnailUrl")
|
||||
const thumbUrl = document.getElementById("thumbnailUrl");
|
||||
const thumbUrlUploadBtn = document.getElementById("thumbnail-url-upload-btn");
|
||||
|
||||
function uploadImg(input) {
|
||||
|
||||
@@ -21,13 +27,17 @@ function uploadImg(input) {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
|
||||
thumbUrl.value = xhr.response;
|
||||
previewThumb.src = thumbUrl.value;
|
||||
|
||||
// 썸네일 등록은 서버에서 하도록 리팩토링할것
|
||||
// const reader = new FileReader();
|
||||
// reader.onload = e => {
|
||||
// previewThumb.src = e.target.result;
|
||||
// }
|
||||
// reader.readAsDataURL(input.files[0])
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
previewThumb.src = e.target.result;
|
||||
}
|
||||
reader.readAsDataURL(input.files[0])
|
||||
thumbBox.style.display = ''
|
||||
myModal.hide();
|
||||
|
||||
} else {
|
||||
alert("이미지가 정상적으로 업로드되지 못했습니다.")
|
||||
@@ -37,6 +47,16 @@ function uploadImg(input) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
thumbUrlUploadBtn.addEventListener("click", () =>{
|
||||
const thumbUrlUploadInput = document.getElementById("thumbnail-url-upload-input");
|
||||
const url = thumbUrlUploadInput.value;
|
||||
previewThumb.src = url;
|
||||
thumbUrl.value = url;
|
||||
thumbBox.style.display = ''
|
||||
myModal.hide();
|
||||
})
|
||||
|
||||
uploadThumbBtn.addEventListener("change", e => {
|
||||
uploadImg(e.target);
|
||||
})
|
||||
|
||||
@@ -62,15 +62,35 @@
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mt-5 mb-3 g-0">
|
||||
|
||||
<div class="d-flex">
|
||||
<label class="btn btn-secondary me-auto" for="thumbnail">썸네일 파일</label>
|
||||
<input type="file" id="thumbnail" name="thumbnail" accept="image/*" class="d-none">
|
||||
<!-- <div>-->
|
||||
<!-- <input type="text" id="thumbnailUrlSource" name="thumbnail" accept="text">-->
|
||||
<!-- <label class="btn btn-secondary" for="thumbnailUrlSource">썸네일 URL</label>-->
|
||||
<!-- </div>-->
|
||||
<div class="d-flex flex-row-reverse">
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#thumbnailModal">
|
||||
썸네일 등록
|
||||
</button>
|
||||
|
||||
<div class="modal fade" id="thumbnailModal" tabindex="-1" aria-labelledby="thumbnailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="thumbnailModalLabel">썸네일 등록</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<input type="text" id="thumbnail-url-upload-input" class="form-control">
|
||||
<button class="btn btn-secondary" id="thumbnail-url-upload-btn">URL 업로드</button>
|
||||
<label class="btn btn-secondary" for="thumbnail">파일 업로드</label>
|
||||
<input type="file" id="thumbnail" name="thumbnail" accept="image/*" class="d-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<form class="" method="post" enctype="multipart/form-data" th:object="${articleDto}"
|
||||
th:action="|@{/article/edit(articleId=${articleDto.getId()})}|" id="writeArticleForm">
|
||||
|
||||
@@ -119,6 +139,7 @@
|
||||
</div>
|
||||
|
||||
<!--scripts-->
|
||||
<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>
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
</div>
|
||||
<!--page e-->
|
||||
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
</div>
|
||||
<!--page e-->
|
||||
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -134,6 +134,9 @@
|
||||
</div>
|
||||
<!--page e-->
|
||||
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<div class="main ">
|
||||
<div class="carousel-inner ">
|
||||
<div class="carousel-item active">
|
||||
<div class="carousel-item active" style="cursor: default">
|
||||
<img th:src="${article.getThumbnailUrl()}" class="w-100 vh-100 cover"
|
||||
alt="...">
|
||||
<div class="card-img-overlay text-shadow text-white text-center row justify-content-center align-content-center p-0">
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea name="contents" id="contents" th:text="${article.getContent()}" hidden></textarea>
|
||||
<textarea name="contents" id="contents" hidden></textarea>
|
||||
|
||||
<div class="mt-5 ms-2 me-2 ms-sm-5 me-sm-5 mt-sm-5 d-flex justify-content-center">
|
||||
|
||||
@@ -140,11 +140,11 @@
|
||||
|
||||
|
||||
<!-- comment e -->
|
||||
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
|
||||
<script src="/js/getCsrf.js"></script>
|
||||
<script src="/js/articleView.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.0/moment.min.js"></script>
|
||||
<script th:replace="layout/fragments.html :: view"></script>
|
||||
<script th:replace="layout/fragments.html :: comment"></script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -62,10 +62,32 @@
|
||||
<div class="row justify-content-center mt-5 mb-3 g-0">
|
||||
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<label class="btn btn-secondary" for="thumbnail">썸네일 등록</label>
|
||||
<input type="file" id="thumbnail" name="thumbnail" accept="image/*" class="d-none">
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#thumbnailModal">
|
||||
썸네일 등록
|
||||
</button>
|
||||
|
||||
<div class="modal fade" id="thumbnailModal" tabindex="-1" aria-labelledby="thumbnailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="thumbnailModalLabel">썸네일 등록</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<input type="text" id="thumbnail-url-upload-input" class="form-control">
|
||||
<button class="btn btn-secondary" id="thumbnail-url-upload-btn">URL 업로드</button>
|
||||
<label class="btn btn-secondary" for="thumbnail">파일 업로드</label>
|
||||
<input type="file" id="thumbnail" name="thumbnail" accept="image/*" class="d-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<form class="" method="post" enctype="multipart/form-data" th:object="${articleDto}"
|
||||
th:action="@{/article/write}" id="writeArticleForm">
|
||||
|
||||
@@ -115,6 +137,7 @@
|
||||
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 src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
|
||||
<script src="/js/getCsrf.js"></script>
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
<script src="/node_modules/wow.js/dist/wow.js"></script>
|
||||
<script src="/js/infinityScroll.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.0/moment.min.js"></script>
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
|
||||
|
||||
|
||||
<script th:fragment="view" th:inline="javascript" type="application/javascript">
|
||||
const viewer = toastui.Editor.factory({
|
||||
el: document.querySelector('#viewer'),
|
||||
viewer: true,
|
||||
height: '500px',
|
||||
initialValue: ''
|
||||
});
|
||||
|
||||
|
||||
viewer.setMarkdown([[${article.getContent()}]]);
|
||||
|
||||
|
||||
|
||||
function deleteArticle() {
|
||||
|
||||
|
||||
if (confirm("글을 정말 삭제하시겠습니까?") == true) {
|
||||
document.getElementById("deleteArticle").submit();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<script th:fragment="comment" th:inline="javascript" type="application/javascript">
|
||||
|
||||
const replyBox = document.getElementById("commentBox");
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
</button>
|
||||
|
||||
<section th:replace="${content}"></section>
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/arrow.js"></script>
|
||||
<script src="/js/search.js"></script>
|
||||
<!-- scripts e -->
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
<div style="margin-bottom: 100px"></div>
|
||||
|
||||
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user