211108 아티클 도메인 개발중
This commit is contained in:
@@ -1,9 +1,40 @@
|
||||
package myblog.blog.article.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.dto.NewArticleDto;
|
||||
import myblog.blog.article.service.ArticleService;
|
||||
import myblog.blog.member.auth.PrincipalDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
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;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class ArticleController {
|
||||
|
||||
private final ArticleService articleService;
|
||||
|
||||
@GetMapping("article/write")
|
||||
public String writeArticleForm(NewArticleDto newArticleDto, Model model){
|
||||
|
||||
model.addAttribute(newArticleDto);
|
||||
|
||||
return "articleWriteForm";
|
||||
}
|
||||
|
||||
@PostMapping("article/write")
|
||||
public String WriteArticle(@ModelAttribute NewArticleDto newArticleDto, Authentication authentication){
|
||||
|
||||
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
|
||||
newArticleDto.setMemberId(principal.getMemberId());
|
||||
|
||||
Long articleId = articleService.writeArticle(newArticleDto);
|
||||
|
||||
return "redirect:/";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package myblog.blog.article.domain;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import myblog.blog.base.domain.BasicEntity;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
|
||||
import javax.persistence.*;
|
||||
@@ -8,26 +10,37 @@ import javax.persistence.*;
|
||||
@Entity
|
||||
@Getter
|
||||
@SequenceGenerator(
|
||||
name = "article_seq_generator",
|
||||
sequenceName = "article_seq",
|
||||
name = "ARTICLE_SEQ_GENERATOR",
|
||||
sequenceName = "ARTICLE_SEQ",
|
||||
initialValue = 1, allocationSize = 50)
|
||||
public class Article {
|
||||
public class Article extends BasicEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "article_generator")
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ARTICLE_SEQ_GENERATOR")
|
||||
@Column(name = "article_id")
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
@Column(nullable = false)
|
||||
@Column(nullable = false, length = 10000)
|
||||
private String content;
|
||||
@Column(columnDefinition = "bigint default 0",nullable = false)
|
||||
private Long hit;
|
||||
private String toc;
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id")
|
||||
private Member member;
|
||||
private String thumbnailUrl;
|
||||
|
||||
protected Article() {
|
||||
}
|
||||
|
||||
|
||||
@Builder
|
||||
public Article(String title, String content, String toc, Member member) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.toc = toc;
|
||||
this.member = member;
|
||||
this.hit = 0L;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package myblog.blog.article.service;
|
||||
package myblog.blog.article.dto;
|
||||
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class NewArticleDto {
|
||||
|
||||
@NotBlank
|
||||
@@ -15,4 +17,7 @@ public class NewArticleDto {
|
||||
@NotBlank
|
||||
private Long memberId;
|
||||
|
||||
private String thumbnailUrl;
|
||||
|
||||
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package myblog.blog.article.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.article.dto.NewArticleDto;
|
||||
import myblog.blog.article.repository.ArticleRepository;
|
||||
import org.modelmapper.ModelMapper;
|
||||
import myblog.blog.member.doamin.Member;
|
||||
import myblog.blog.member.repository.MemberRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -11,19 +13,27 @@ import org.springframework.stereotype.Service;
|
||||
public class ArticleService {
|
||||
|
||||
private final ArticleRepository articleRepository;
|
||||
private final ModelMapper modelMapper;
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
public Long writeArticle(NewArticleDto newArticleDto){
|
||||
public Long writeArticle(NewArticleDto articleDto) {
|
||||
|
||||
Article article = modelMapper.map(newArticleDto, Article.class);
|
||||
|
||||
articleRepository.save(article);
|
||||
|
||||
return article.getId();
|
||||
Article newArticle = createNewArticleFrom(articleDto);
|
||||
articleRepository.save(newArticle);
|
||||
return newArticle.getId();
|
||||
|
||||
}
|
||||
|
||||
private Article createNewArticleFrom(NewArticleDto articleDto) {
|
||||
Member member =
|
||||
memberRepository.findById(articleDto.getMemberId()).orElseThrow(() -> {
|
||||
throw new IllegalArgumentException("작성자를 확인할 수 없습니다");
|
||||
});
|
||||
|
||||
|
||||
|
||||
return Article.builder()
|
||||
.title(articleDto.getTitle())
|
||||
.content(articleDto.getContent())
|
||||
.toc(articleDto.getToc())
|
||||
.member(member)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.antMatchers("/admin").hasRole(Role.ADMIN.name())
|
||||
.antMatchers("/article/write").hasRole(Role.ADMIN.name())
|
||||
.anyRequest().permitAll()
|
||||
|
||||
.and()
|
||||
|
||||
19
src/main/java/myblog/blog/img/domain/UploadedImg.java
Normal file
19
src/main/java/myblog/blog/img/domain/UploadedImg.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package myblog.blog.img.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
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;
|
||||
}
|
||||
}
|
||||
59
src/main/java/myblog/blog/img/service/ImgService.java
Normal file
59
src/main/java/myblog/blog/img/service/ImgService.java
Normal file
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImgService {
|
||||
|
||||
@Value("${git.gitToken}")
|
||||
private String gitToken;
|
||||
@Value("${git.imgRepo}")
|
||||
|
||||
private String gitRepo;
|
||||
@Value("${git.imgUrl}")
|
||||
private String imgUrl;
|
||||
|
||||
private final ModelMapper modelMapper;
|
||||
|
||||
public UploadedImg storeImg(MultipartFile multipartFile) throws IOException {
|
||||
if (multipartFile.isEmpty()) {
|
||||
throw new IllegalArgumentException("이미지가 존재하지 않습니다.");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return new UploadedImg(originalFilename, storeFileName, 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,10 @@ public class PrincipalDetails implements OAuth2User {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public Long getMemberId(){
|
||||
return member.getId();
|
||||
}
|
||||
|
||||
// Oauth2
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
|
||||
@@ -17,12 +17,15 @@ button img{
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
line-height: 1em;
|
||||
font-family: 'Noto Sans C JK KR';
|
||||
font-weight: bold;
|
||||
padding: 0 2em 0 4em;
|
||||
text-decoration: none;
|
||||
transition: all 0.5s;
|
||||
height: 50px;
|
||||
width: 200px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
}
|
||||
|
||||
.btn-google {
|
||||
|
||||
145
src/main/resources/static/js/editor.js
Normal file
145
src/main/resources/static/js/editor.js
Normal file
@@ -0,0 +1,145 @@
|
||||
|
||||
const contents = document.getElementById("content");
|
||||
|
||||
toastui.Editor.setLanguage(['ko', 'ko-KR'], {
|
||||
Markdown: '마크다운',
|
||||
WYSIWYG: '일반',
|
||||
Write: '편집하기',
|
||||
Preview: '미리보기',
|
||||
Headings: '제목크기',
|
||||
Paragraph: '본문',
|
||||
Bold: '굵게',
|
||||
Italic: '기울임꼴',
|
||||
Strike: '취소선',
|
||||
Code: '인라인 코드',
|
||||
Line: '문단나눔',
|
||||
Blockquote: '인용구',
|
||||
'Unordered list': '글머리 기호',
|
||||
'Ordered list': '번호 매기기',
|
||||
Task: '체크박스',
|
||||
Indent: '들여쓰기',
|
||||
Outdent: '내어쓰기',
|
||||
'Insert link': '링크 삽입',
|
||||
'Insert CodeBlock': '코드블럭 삽입',
|
||||
'Insert table': '표 삽입',
|
||||
'Insert image': '이미지 삽입',
|
||||
Heading: '제목',
|
||||
'Image URL': '이미지 주소',
|
||||
'Select image file': '이미지 파일을 선택하세요.',
|
||||
'Choose a file': '파일 선택',
|
||||
'No file': '선택된 파일 없음',
|
||||
Description: '설명',
|
||||
OK: '확인',
|
||||
More: '더 보기',
|
||||
Cancel: '취소',
|
||||
File: '파일',
|
||||
URL: '주소',
|
||||
'Link text': '링크 텍스트',
|
||||
'Add row to up': '위에 행 추가',
|
||||
'Add row to down': '아래에 행 추가',
|
||||
'Add column to left': '왼쪽에 열 추가',
|
||||
'Add column to right': '오른쪽에 열 추가',
|
||||
'Remove row': '행 삭제',
|
||||
'Remove column': '열 삭제',
|
||||
'Align column to left': '열 왼쪽 정렬',
|
||||
'Align column to center': '열 가운데 정렬',
|
||||
'Align column to right': '열 오른쪽 정렬',
|
||||
'Remove table': '표 삭제',
|
||||
'Would you like to paste as table?': '표형태로 붙여 넣겠습니까?',
|
||||
'Text color': '글자 색상',
|
||||
'Auto scroll enabled': '자동 스크롤 켜짐',
|
||||
'Auto scroll disabled': '자동 스크롤 꺼짐',
|
||||
'Choose language': '언어 선택',
|
||||
})
|
||||
|
||||
const editor = new toastui.Editor({
|
||||
el: document.querySelector('#editor'),
|
||||
height: '500px',
|
||||
initialEditType: 'markdown',
|
||||
previewStyle: 'vertical',
|
||||
language: 'ko',
|
||||
toolbarItems: [
|
||||
['heading', 'bold', 'italic', 'strike'],
|
||||
['hr'],
|
||||
['ul', 'ol'],
|
||||
['code'],
|
||||
['table', 'image', 'link']
|
||||
],
|
||||
hooks: {
|
||||
addImageBlobHook: (blob, callback) => {
|
||||
let imgurl = uploadImage(blob);
|
||||
callback(imgurl, "첨부 이미지")
|
||||
}
|
||||
}
|
||||
});
|
||||
const editorMobile = new toastui.Editor({
|
||||
el: document.querySelector('#editorMobile'),
|
||||
height: 'auto',
|
||||
language: 'ko',
|
||||
initialEditType: 'wysiwyg',
|
||||
previewStyle: 'tab',
|
||||
toolbarItems: [
|
||||
['heading', 'bold', 'italic', 'strike'],
|
||||
['hr'],
|
||||
['ul', 'ol'],
|
||||
['code']
|
||||
]
|
||||
});
|
||||
|
||||
editor.setMarkdown(contents.value);
|
||||
editorMobile.setMarkdown(contents.value);
|
||||
|
||||
|
||||
function uploadImage(blob) {
|
||||
let token = getCsrfToken();
|
||||
let formData = new FormData();
|
||||
formData.append('img', blob);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/board/uploadImg", false);
|
||||
xhr.setRequestHeader("contentType", "multipart/form-data");
|
||||
xhr.setRequestHeader("X-XSRF-TOKEN", token);
|
||||
xhr.send(formData);
|
||||
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
return xhr.response;
|
||||
} else {
|
||||
alert("이미지가 정상적으로 업로드되지 못했습니다.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function checkTitle() {
|
||||
|
||||
let title = document.getElementById("title");
|
||||
|
||||
if (title.value === "") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function post() {
|
||||
|
||||
if (!checkTitle()) {
|
||||
alert("제목을 입력해주세요")
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
contents.value = editor.getMarkdown();
|
||||
document.getElementById("writeArticleForm").submit();
|
||||
|
||||
}
|
||||
|
||||
function postMobile() {
|
||||
|
||||
if (!checkTitle()) {
|
||||
alert("제목을 입력해주세요")
|
||||
return;
|
||||
}
|
||||
|
||||
contents.value = editorMobile.getMarkdown();
|
||||
document.getElementById("writeArticleForm").submit();
|
||||
|
||||
}
|
||||
120
src/main/resources/static/package-lock.json
generated
120
src/main/resources/static/package-lock.json
generated
@@ -1,27 +1,121 @@
|
||||
{
|
||||
"name": "static",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.1.3"
|
||||
"@toast-ui/editor": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-3.1.1.tgz",
|
||||
"integrity": "sha512-9Js9+qpg6rMlPZjJLf96sFd87Wm+rc3N+CqQ4RWuqNvpYQLE/YifXHlJkaLIZTpYx40L08gkv205xKq6Ifa9TQ==",
|
||||
"requires": {
|
||||
"dompurify": "^2.3.3",
|
||||
"prosemirror-commands": "^1.1.9",
|
||||
"prosemirror-history": "^1.1.3",
|
||||
"prosemirror-inputrules": "^1.1.3",
|
||||
"prosemirror-keymap": "^1.1.4",
|
||||
"prosemirror-model": "^1.14.1",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-view": "^1.18.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
||||
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
||||
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q=="
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz",
|
||||
"integrity": "sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg=="
|
||||
},
|
||||
"orderedmap": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz",
|
||||
"integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ=="
|
||||
},
|
||||
"prosemirror-commands": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.12.tgz",
|
||||
"integrity": "sha512-+CrMs3w/ZVPSkR+REg8KL/clyFLv/1+SgY/OMN+CB22Z24j9TZDje72vL36lOZ/E4NeRXuiCcmENcW/vAcG67A==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-history": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.2.0.tgz",
|
||||
"integrity": "sha512-B9v9xtf4fYbKxQwIr+3wtTDNLDZcmMMmGiI3TAPShnUzvo+Rmv1GiUrsQChY1meetHl7rhML2cppF3FTs7f7UQ==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-inputrules": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz",
|
||||
"integrity": "sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-keymap": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.5.tgz",
|
||||
"integrity": "sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-model": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.15.0.tgz",
|
||||
"integrity": "sha512-hQJv7SnIhlAy9ga3lhPPgaufhvCbQB9tHwscJ9E1H1pPHmN8w5V/lURueoYv9Kc3/bpNWoyHa8r3g//m7N0ChQ==",
|
||||
"requires": {
|
||||
"orderedmap": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-state": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.4.tgz",
|
||||
"integrity": "sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-transform": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz",
|
||||
"integrity": "sha512-9NLVXy1Sfa2G6qPqhWMkEvwQQMTw7OyTqOZbJaGQWsCeH3hH5Cw+c5eNaLM1Uu75EyKLsEZhJ93XpHJBa6RX8A==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-view": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.21.0.tgz",
|
||||
"integrity": "sha512-Pjh0vXw/l2Ifc3QcBVKJ/qHej33MX4sdq4/uAVqkSmHLf//bVWTYSZibihMya3lsIxTbDzNl/Z03KrhXWMHaoA==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.14.3",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"rope-sequence": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz",
|
||||
"integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg=="
|
||||
},
|
||||
"w3c-keyname": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@toast-ui/editor": "^3.1.1",
|
||||
"bootstrap": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
95
src/main/resources/templates/articleWriteForm.html
Normal file
95
src/main/resources/templates/articleWriteForm.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:replace="~{layout/layout.html :: layout(~{::head}, ~{::section})}"
|
||||
lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" xmlns="http://www.w3.org/1999/html">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Jinia's Log - 글 작성</title>
|
||||
<!-- SEO -->
|
||||
<meta name="description" content=""/>
|
||||
<meta name="keyword" content=""/>
|
||||
<meta name="author" content="jinia"/>
|
||||
<meta name="viewport" content="width=device-width, user-scalable = no, initial-scale=1.0"/>
|
||||
<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=""/>
|
||||
<link rel="apple-touch-icon" href=""/>
|
||||
<link rel="short icon" type="image/x-icon" href=""/>
|
||||
|
||||
<!-- CSS RESET -->
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"/>
|
||||
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/css/mainCss.css"/>
|
||||
|
||||
<script src="https://kit.fontawesome.com/233840a552.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section>
|
||||
|
||||
<div style="margin-bottom: 150px"></div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mt-5 mb-3 g-0">
|
||||
|
||||
<form class="" method="post" enctype="multipart/form-data" th:object="${newArticleDto}"
|
||||
th:action="@{/article/write}" id="writeArticleForm">
|
||||
|
||||
<div class="form-group">
|
||||
<input id="title" name="title" type="text" class="form-control"
|
||||
placeholder="제목을 입력해주세요" required max="30">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<textarea type="text" name="content" id="content" hidden></textarea>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row justify-content-center mb-3 g-0">
|
||||
<label for="editor"></label>
|
||||
<div class=" d-none d-sm-block">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div class=" d-block d-sm-none">
|
||||
<div id="editorMobile"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-evenly mb-5 g-0">
|
||||
<button class="btn btn-secondary col-4 d-none d-sm-block" onclick="post()">등 록</button>
|
||||
<button class="btn btn-secondary col-4 d-block d-sm-none" onclick="postMobile()">등 록</button>
|
||||
<button class="btn btn-secondary col-4" onclick="javascript:history.back()">취 소</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--scripts-->
|
||||
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:fragment="layout (title, content, style)"
|
||||
<html th:fragment="layout (head, content)"
|
||||
lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:replace="${title}">Jinia's Log</title>
|
||||
<!-- SEO -->
|
||||
<meta name="description" content=""/>
|
||||
<meta name="keyword" content=""/>
|
||||
<meta name="author" content="jinia"/>
|
||||
<meta name="viewport" content="width=device-width, user-scalable = no, initial-scale=1.0"/>
|
||||
<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=""/>
|
||||
<link rel="apple-touch-icon" href=""/>
|
||||
<link rel="short icon" type="image/x-icon" href=""/>
|
||||
|
||||
<!-- CSS RESET -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"/>
|
||||
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/css/mainCss.css"/>
|
||||
<link th:replace="${style}">
|
||||
|
||||
<script src="https://kit.fontawesome.com/233840a552.js" crossorigin="anonymous"></script>
|
||||
<head th:replace="${head}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="fixed-top d-xxl-none p-0">
|
||||
<nav class="navbar navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid p-0">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasMenu"
|
||||
aria-controls="offcanvasMenu">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div id="nav-brand">
|
||||
<a href="#">
|
||||
<a th:href="@{/}">
|
||||
<h4>Blog</h4>
|
||||
</a>
|
||||
</div>
|
||||
@@ -60,7 +23,8 @@
|
||||
class="far fa-address-card"></i></a></div>
|
||||
<div sec:authorize="isAuthenticated()">
|
||||
<span sec:authorize="isAuthenticated()" th:text="${#authentication.name} + '님'"></span>
|
||||
<a th:href="@{/article/write}"><span sec:authorize="hasRole('ADMIN')" style="font-size: 21px"><i class="fas fa-edit"></i></span></a>
|
||||
<a th:href="@{/article/write}"><span sec:authorize="hasRole('ADMIN')" style="font-size: 21px"><i
|
||||
class="fas fa-edit"></i></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -106,6 +70,7 @@
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
@@ -118,6 +83,7 @@
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
@@ -130,6 +96,7 @@
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="border-top my-3"></li>
|
||||
@@ -206,6 +173,7 @@
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
@@ -218,6 +186,7 @@
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
@@ -230,6 +199,7 @@
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="border-top my-3"></li>
|
||||
@@ -270,6 +240,14 @@
|
||||
<div class="col-xxl-10 p-0 sections-container">
|
||||
|
||||
<section th:replace="${content}"></section>
|
||||
|
||||
<button class="arrow-up">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
</button>
|
||||
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- scripts e -->
|
||||
|
||||
<!-- sections e -->
|
||||
|
||||
<footer class="footer bg-light">
|
||||
@@ -277,18 +255,11 @@
|
||||
<h5><span class="text-muted">Copyright ©Jinia</span></h5>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<!-- body e -->
|
||||
|
||||
|
||||
<button class="arrow-up">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
</button>
|
||||
|
||||
<!-- js -->
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:replace="~{layout/layout.html :: layout(~{::title}, ~{::section}, ~{::link})}"
|
||||
<html th:replace="~{layout/layout.html :: layout(~{::head}, ~{::section})}"
|
||||
lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" xmlns="http://www.w3.org/1999/html">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Jinia's Log - 로그인</title>
|
||||
<!-- SEO -->
|
||||
<meta name="description" content=""/>
|
||||
<meta name="keyword" content=""/>
|
||||
<meta name="author" content="jinia"/>
|
||||
<meta name="viewport" content="width=device-width, user-scalable = no, initial-scale=1.0"/>
|
||||
<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=""/>
|
||||
<link rel="apple-touch-icon" href=""/>
|
||||
<link rel="short icon" type="image/x-icon" href=""/>
|
||||
|
||||
<!-- CSS RESET -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"/>
|
||||
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/css/mainCss.css"/>
|
||||
<link rel="stylesheet" href="/css/login.css"/>
|
||||
|
||||
<script src="https://kit.fontawesome.com/233840a552.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section>
|
||||
|
||||
<div style="margin-bottom: 300px"></div>
|
||||
<div style="margin-bottom: 150px"></div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<P class="text-center" id="capsAlert" th:text="${errMsg}">로그인 오류</P>
|
||||
<span class="d-flex flex-column align-items-center justify-content-center h-100">
|
||||
|
||||
<P class="text-center field-error mt-3" id="capsAlert" th:text="${errMsg}">로그인 오류</P>
|
||||
|
||||
<div class="col">
|
||||
<a href="/oauth2/authorization/google">
|
||||
@@ -42,10 +78,10 @@
|
||||
<button class="btn-naver"><img src="/img/naver.png"></button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 300px"></div>
|
||||
<div style="margin-bottom: 100px"></div>
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
@@ -1,12 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:replace="~{layout/layout.html :: layout(~{::title}, ~{::section}, ~{::link})}"
|
||||
<html th:replace="~{layout/layout.html :: layout(~{::head}, ~{::section})}"
|
||||
lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Jinia's Log</title>
|
||||
<link rel="stylesheet" href=""/>
|
||||
<!-- SEO -->
|
||||
<meta name="description" content=""/>
|
||||
<meta name="keyword" content=""/>
|
||||
<meta name="author" content="jinia"/>
|
||||
<meta name="viewport" content="width=device-width, user-scalable = no, initial-scale=1.0"/>
|
||||
<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=""/>
|
||||
<link rel="apple-touch-icon" href=""/>
|
||||
<link rel="short icon" type="image/x-icon" href=""/>
|
||||
|
||||
<!-- CSS RESET -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"/>
|
||||
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/css/mainCss.css"/>
|
||||
<link rel="stylesheet" href="/css/login.css"/>
|
||||
|
||||
<script src="https://kit.fontawesome.com/233840a552.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package myblog.blog.article.service;
|
||||
|
||||
import myblog.blog.article.dto.NewArticleDto;
|
||||
import myblog.blog.article.repository.ArticleRepository;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.Rollback;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
|
||||
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
@Rollback(value = false)
|
||||
class ArticleServiceTest {
|
||||
|
||||
@Autowired
|
||||
ArticleService articleService;
|
||||
@Autowired
|
||||
ArticleRepository articleRepository;
|
||||
@Autowired
|
||||
EntityManager entityManager;
|
||||
|
||||
@Test
|
||||
public void 게시글테스트() throws Exception {
|
||||
// given
|
||||
NewArticleDto newArticleDto = new NewArticleDto();
|
||||
newArticleDto.setTitle("abs");
|
||||
newArticleDto.setMemberId(1L);
|
||||
newArticleDto.setToc("df");
|
||||
newArticleDto.setContent("sdfsf");
|
||||
|
||||
// when
|
||||
Long articleId = articleService.writeArticle(newArticleDto);
|
||||
|
||||
// then
|
||||
|
||||
|
||||
System.out.println(articleRepository.findById(articleId).get().getContent());
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user