Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[댓글 기능] 제이 미션 제출합니다. #107

Merged
merged 72 commits into from
Aug 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
dd87430
docs: Readme 업데이트
MrKwon Jul 9, 2019
a2287b4
feat: 게시글 생성, 목록 조회 기능 구현
MrKwon Jul 9, 2019
940525c
feat: 게시글 조회 구현
MrKwon Jul 9, 2019
5430d29
feat: 공통 Head 분리
MrKwon Jul 9, 2019
1f9ef73
feat: html 중복제거
JunHoPark93 Jul 10, 2019
8dced8c
feat: 게시글 수정
JunHoPark93 Jul 10, 2019
9a3475c
feat: 게시글 삭제 구현
JunHoPark93 Jul 10, 2019
8731d42
feat: devtools 적용
JunHoPark93 Jul 10, 2019
66751cd
refactor: Js 분리
JunHoPark93 Jul 10, 2019
8901bf4
refactor: 컨트롤러 메서드 이름 수정, autowired 삭제
JunHoPark93 Jul 10, 2019
07ebbc1
refactor: 불필요한 메서드 제거
JunHoPark93 Jul 10, 2019
f2e700e
feat: Article 제목 검증 추가
JunHoPark93 Jul 11, 2019
1961b76
feat: error 페이지 추가
JunHoPark93 Jul 11, 2019
f44d9ff
refactor: 피드백 반영
JunHoPark93 Jul 15, 2019
8ef6ba5
feat: Jpa repository 적용
JunHoPark93 Jul 16, 2019
dc2a199
Merge branch 'temp' into JunHoPark93
JunHoPark93 Jul 16, 2019
087e5e0
docs: README 업데이트
JunHoPark93 Jul 16, 2019
15e570a
feat: 회원 등록 기능
JunHoPark93 Jul 16, 2019
47ae91d
feat: 유저 조회 기능 구현
JunHoPark93 Jul 16, 2019
6e9726b
feat: 로그인 및 로그아웃 기능 구현
JunHoPark93 Jul 17, 2019
c2ea962
feat: 회원 수정 및 탈퇴기능 구현
JunHoPark93 Jul 17, 2019
e6f5579
refactor: html 중복제거 및 세션적용
JunHoPark93 Jul 18, 2019
3e2bd3e
fix: Interceptor에서 /users post요청은 통과
JunHoPark93 Jul 18, 2019
63235c6
feat: Error 객체의 정의
JunHoPark93 Jul 18, 2019
924cee2
test: 세션관련 인수테스트 작성
JunHoPark93 Jul 18, 2019
7c83e30
test: 회원관련 인수테스트
JunHoPark93 Jul 18, 2019
bb280ba
refactor: 들여쓰기 및 사용하지 않는 import 제거
JunHoPark93 Jul 18, 2019
e00061f
feat: 회원가입시 이메일 중복 오류 처리
JunHoPark93 Jul 18, 2019
614121c
fix: 회원 가입시 email 중복 테스트 수정
JunHoPark93 Jul 18, 2019
8d0188b
refactor: UserService 분리
JunHoPark93 Jul 18, 2019
7a76891
feat: 회원 수정시 이름 형식 오류 처리
JunHoPark93 Jul 18, 2019
2553842
feat: 회원 가입시 유효하지 않은 입력 처리
JunHoPark93 Jul 18, 2019
c2f89df
refactor: Article 서비스 패키지 분리
JunHoPark93 Jul 18, 2019
dbe40f5
fix: 유저 리스트에서 다른 유저 정보에 접근하는 오류 처리
JunHoPark93 Jul 19, 2019
8985502
feat: 데이터베이스 구축
JunHoPark93 Jul 19, 2019
e537451
refactor: 삼항 연산자로 변경
JunHoPark93 Jul 19, 2019
c48f2f6
refactor: 삼항 연산자로 변경
JunHoPark93 Jul 19, 2019
e2fec3b
fix: 게시글 id 접근하는 로직 수정
JunHoPark93 Jul 19, 2019
6f21dfa
fix: 절대 경로로 변경
JunHoPark93 Jul 19, 2019
0ed6e1b
refactor: 삼항 연산자로 변경, url 변경
JunHoPark93 Jul 19, 2019
615e6fd
refactor: fragments 패키지로 분리
JunHoPark93 Jul 19, 2019
0e6e088
refactor: 피드백 반영
JunHoPark93 Jul 19, 2019
5447551
feat: 비밀번호 암호화 구현
JunHoPark93 Jul 20, 2019
cefea32
refactor: implements 삭제
JunHoPark93 Jul 20, 2019
b4d7ea5
feat: 비밀번호, 이메일 검증 로직 개선
JunHoPark93 Jul 22, 2019
e42ed91
feat: 검증 로직 추가
JunHoPark93 Jul 22, 2019
b90115e
fix: 없는 게시물 접근 처리
JunHoPark93 Jul 22, 2019
9f8190b
test: 테스트 파라미터 추가
JunHoPark93 Jul 22, 2019
59eaeb7
fix: email validation 추가
JunHoPark93 Jul 22, 2019
95ae854
test: cookie를 이용한 테스트
JunHoPark93 Jul 23, 2019
09b41c6
refactor: 피드백 반영
JunHoPark93 Jul 23, 2019
840253b
refactor: localport 제거
JunHoPark93 Jul 23, 2019
49fbaa5
refactor: 패키지 이름 변경
JunHoPark93 Jul 23, 2019
52a6d6c
refactor: 클래스 이름 변경
JunHoPark93 Jul 23, 2019
f4537c5
Merge remote-tracking branch 'upstream/JunHoPark93' into JunHoPark93
JunHoPark93 Jul 25, 2019
ed42da0
feat: paging 적용
JunHoPark93 Jul 26, 2019
cabf579
feat: article, user 관계 매핑
JunHoPark93 Jul 26, 2019
ccb502d
feat: article 작성자 매핑 기능 추가
JunHoPark93 Jul 26, 2019
8eb91d1
feat: Comment 작성, 조회
JunHoPark93 Jul 29, 2019
cc93b1a
feat: Comment 수정, 삭제
JunHoPark93 Jul 29, 2019
5304a84
refactor: comment redirect
JunHoPark93 Jul 30, 2019
0c28a01
feat: Comment 시간 계산
JunHoPark93 Jul 30, 2019
b4ef0a2
fix: 타인이 게시글 삭제 막기
JunHoPark93 Jul 30, 2019
85ff44c
test: 삭제 테스트 수정 및 정리
JunHoPark93 Jul 30, 2019
c915849
feat: JPA auditing 적용
JunHoPark93 Jul 31, 2019
fc5aa5c
feat: Comment 응답 dto에 대한 Custom Mapper의 구현
JunHoPark93 Jul 31, 2019
ed0e739
refactor: test 중복제거
JunHoPark93 Jul 31, 2019
e743f49
feat: JPA auditing, custom Context holder 적용
JunHoPark93 Aug 1, 2019
411388d
fix: session holder 코드 원복
JunHoPark93 Aug 2, 2019
6be9e90
refactor: Comment 검증 로직
JunHoPark93 Aug 2, 2019
c1b252e
feat: CoverUrl resource의 존재 확인
JunHoPark93 Aug 2, 2019
8454227
test: coverUrl 경로 수정
JunHoPark93 Aug 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@

## 회원조회
* GET /users 로 요청하여 회원목록페이지(user-list.html) 이동
* DB에 저장된 회원 정보 노출
* DB에 저장된 회원 정보 노출

## 댓글
* 댓글 작성 시 작성자와 게시글 정보가 같이 저장
* 댓글 생성/조회/수정/삭제조회 기능 구현
* 수정/삭제는 댓글 작성자만 가능
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
implementation 'com.h2database:h2'
implementation 'mysql:mysql-connector-java:8.0.16'
implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.3m'
implementation 'org.modelmapper:modelmapper:0.7.8'
runtimeOnly 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testCompile("org.junit.jupiter:junit-jupiter-params:5.4.2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@
import techcourse.myblog.service.dto.UserRequest;

@Component
public class AppRunner implements ApplicationRunner {
public class Runner implements ApplicationRunner {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오! ApplicationRunner를 사용하셨군요!
이 외에도 스프링 구동되는 시점에 Component를 주입하는 여러가지 방법들이 있으니 참고하세요

https://yuien.tistory.com/entry/스프링-시작시점에서-프로그램-동작할-수-있게-하는-방법

private UserService userService;

public AppRunner(UserService userService) {
public Runner(UserService userService) {
this.userService = userService;
}

@Override
public void run(ApplicationArguments args) {
public void run(ApplicationArguments args) throws Exception {
UserRequest userRequest = new UserRequest();
userRequest.setName("CU");
userRequest.setEmail("[email protected]");
userRequest.setPassword("PassWord!1");
userRequest.setReconfirmPassword("PassWord!1");

userRequest.setPassword("Password!1");
userRequest.setReconfirmPassword("Password!1");
userService.saveUser(userRequest);
}
}
50 changes: 41 additions & 9 deletions src/main/java/techcourse/myblog/domain/Article.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,53 @@
package techcourse.myblog.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import techcourse.myblog.domain.common.ContentsAudit;
import techcourse.myblog.service.exception.ResourceNotFoundException;

@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
import javax.persistence.*;
import java.net.HttpURLConnection;
import java.net.URL;

@Entity
@EntityListeners(value = AuditingEntityListener.class)
public class Article extends ContentsAudit{
@Column(nullable = false)
private String title;

@Column(nullable = false)
private String coverUrl;

@Column(nullable = false)
@Lob
private String contents;

@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(name = "FK_article_to_user"))
private User author;

private Article() {
}

public Article(String title, String coverUrl, String contents) {
checkCoverImageResource(coverUrl);
this.title = title;
this.coverUrl = coverUrl;
this.contents = contents;
}

private void checkCoverImageResource(String coverUrl) {
try {
HttpURLConnection.setFollowRedirects(false);
HttpURLConnection con = (HttpURLConnection) new URL(coverUrl).openConnection();
con.setRequestMethod("HEAD");
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new ResourceNotFoundException("리소스가 존재하지 않습니다");
}
} catch (Exception e) {
throw new ResourceNotFoundException("리소스 경로가 올바르지 않습니다");
}
}

public Long getId() {
return id;
}
Expand All @@ -45,4 +69,12 @@ public void updateArticle(Article article) {
this.coverUrl = article.coverUrl;
this.contents = article.contents;
}

public void setAuthor(User user) {
this.author = user;
}

public boolean isAuthor(User author) {
return this.author.equals(author);
}
}
67 changes: 67 additions & 0 deletions src/main/java/techcourse/myblog/domain/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package techcourse.myblog.domain;

import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import techcourse.myblog.domain.common.ContentsAudit;

import javax.persistence.*;

@Entity
@EntityListeners(value = AuditingEntityListener.class)
public class Comment extends ContentsAudit {
@Lob
@Column(nullable = false)
private String contents;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 구조에서는 contents는 varchar(255)로 세팅될텐데요. 더 긴 문자열을 받을 수 있도록 데이터타입을 바꿔주는 것은 어떨까요
지금과 같이 데이터 크기를 결정하기 힘든 큰 크기의 데이터는 @lob을 쓰는 것도 한번 고려해보세요.


@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(name = "FK_comment_to_user"), nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private User commenter;

@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(name = "FK_comment_to_article"), nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Article article;

private Comment() {
}

public Comment(String contents, Article article, User user) {
checkContents(contents);
this.contents = contents;
this.article = article;
this.commenter = user;
}

private void checkContents(String contents) {
if (contents == null) {
throw new IllegalArgumentException("본문이 없습니다");
}
}

public Long getId() {
return id;
}

public String getContents() {
return contents;
}

public User getCommenter() {
return commenter;
}

public Article getArticle() {
return article;
}

public Comment updateContents(String contents) {
this.contents = contents;
return this;
}

public boolean isCommentor(User commenter) {
return this.commenter.equals(commenter);
}
}
12 changes: 12 additions & 0 deletions src/main/java/techcourse/myblog/domain/CommentRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package techcourse.myblog.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByArticle(Article article);

Optional<Comment> findByCommenterAndId(User commenter, Long id);
}
28 changes: 22 additions & 6 deletions src/main/java/techcourse/myblog/domain/User.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package techcourse.myblog.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.*;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -16,8 +14,13 @@ public class User {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50)
private String name;

@Column(nullable = false)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java bean validator를 사용하여 email 등 포맷의 유효성 검증을 할 수 있어요

https://www.baeldung.com/javax-validation

private String email;

@Column(nullable = false)
private String password;

private User() {
Expand Down Expand Up @@ -46,8 +49,8 @@ public void changeName(String name) {
this.name = name;
}

public boolean matchPassword(String password) {
return this.password.equals(password);
public boolean matchEmail(User user) {
return this.email.equals(user.email);
}

public Long getId() {
Expand All @@ -65,4 +68,17 @@ public String getEmail() {
public String getPassword() {
return password;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}
46 changes: 46 additions & 0 deletions src/main/java/techcourse/myblog/domain/common/ContentsAudit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package techcourse.myblog.domain.common;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class ContentsAudit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;

@CreatedDate
private LocalDateTime createdDate;

@LastModifiedDate
private LocalDateTime lastModifiedDate;

public Long getId() {
return id;
}

public LocalDateTime getCreatedDate() {
return createdDate;
}

public LocalDateTime getLastModifiedDate() {
return lastModifiedDate;
}

public void setId(Long id) {
this.id = id;
}

public void setCreatedDate(LocalDateTime createdDate) {
this.createdDate = createdDate;
}

public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
46 changes: 38 additions & 8 deletions src/main/java/techcourse/myblog/service/ArticleService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package techcourse.myblog.service;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import techcourse.myblog.domain.Article;
import techcourse.myblog.domain.ArticleRepository;
import techcourse.myblog.domain.User;
import techcourse.myblog.service.dto.ArticleRequest;
import techcourse.myblog.service.exception.InvalidAuthorException;
import techcourse.myblog.service.exception.NoArticleException;
import techcourse.myblog.service.exception.ResourceNotFoundException;

import javax.transaction.Transactional;
import java.util.List;

@Service
public class ArticleService {
Expand All @@ -17,29 +22,54 @@ public ArticleService(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}

public List<Article> findAll() {
return articleRepository.findAll();
public Page<Article> findAll(int page) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return articleRepository.findAll(
PageRequest.of(page - 1, 10, Sort.by("id").descending()));
}

public Article save(ArticleRequest articleRequest) {
Article article = new Article(articleRequest.getTitle(),
articleRequest.getCoverUrl(), articleRequest.getContents());
public Article save(ArticleRequest articleRequest, User user) {
Article article = createArticle(articleRequest);
article.setAuthor(user);
return articleRepository.save(article);
}

private Article createArticle(ArticleRequest articleRequest) {
try {
return new Article(articleRequest.getTitle(), articleRequest.getCoverUrl(), articleRequest.getContents());
} catch (Exception e) {
throw new ResourceNotFoundException(e.getMessage());
}
}

public Article findById(long articleId) {
return articleRepository.findById(articleId)
.orElseThrow(() -> new NoArticleException("게시글이 존재하지 않습니다"));
}

public Article findByIdWithUser(long articleId, User user) {
Article article = articleRepository.findById(articleId)
.orElseThrow(() -> new NoArticleException("게시글이 존재하지 않습니다"));
checkAuthor(user, article);
return article;
}

@Transactional
public Article editArticle(ArticleRequest articleRequest, long articleId) {
public Article editArticle(ArticleRequest articleRequest, long articleId, User user) {
Article article = articleRepository.findById(articleId).orElseThrow(IllegalArgumentException::new);
checkAuthor(user, article);
article.updateArticle(new Article(articleRequest.getTitle(), articleRequest.getCoverUrl(), articleRequest.getContents()));
return article;
}

public void deleteById(long articleId) {
private void checkAuthor(User user, Article article) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (!article.isAuthor(user)) {
throw new InvalidAuthorException("작성자가 아닙니다");
}
}

public void deleteById(long articleId, User user) {
Article article = articleRepository.findById(articleId).orElseThrow(IllegalArgumentException::new);
checkAuthor(user, article);
articleRepository.deleteById(articleId);
}
}
Loading