Skip to content

Commit

Permalink
[댓글 기능] 제이 미션 제출합니다. (#107)
Browse files Browse the repository at this point in the history
* docs: Readme 업데이트

* feat: 게시글 생성, 목록 조회 기능 구현

* feat: 게시글 조회 구현

* feat: 공통 Head 분리

* feat: html 중복제거

* feat: 게시글 수정

* feat: 게시글 삭제 구현

* feat: devtools 적용

* refactor: Js 분리

* refactor: 컨트롤러 메서드 이름 수정, autowired 삭제

* refactor: 불필요한 메서드 제거

* feat: Article 제목 검증 추가

* feat: error 페이지 추가

* refactor: 피드백 반영

리다이렉트, 이름 잘못 수정된것 원복, header location 검증 추가

* feat: Jpa repository 적용

* docs: README 업데이트

* feat: 회원 등록 기능

* feat: 유저 조회 기능 구현

Squashed commit of the following:

commit 3026e5c7eb726b65b992756d9ec17a73bec140d4
Author: JunHoPark <[email protected]>
Date:   Tue Jul 16 17:54:01 2019 +0900

    feat: 유저 조회 기능 구현

commit c82f723cc643fd9aed7686d0231ed18cd35924b7
Author: JunHoPark <[email protected]>
Date:   Tue Jul 16 16:53:26 2019 +0900

    fix: Valid 애노테이션 추가

    Validation 에러 수정, @Valid가 없으면 검증이 발동되지 않는다

* feat: 로그인 및 로그아웃 기능 구현

Squashed commit of the following:

commit 3af160545dc0fcdce403342652fd1c9497e60982
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 15:57:01 2019 +0900

    refactor: html 패키지화 원복

commit abaa172ae7507edcc0a8a21852291f5b216d967a
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 15:53:36 2019 +0900

    feat: 로그아웃 기능 구현

commit a523c371383247a05158711cdc7a1d1c6cd1c2ff
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 15:26:37 2019 +0900

    feat: 로그인 기능 구현

commit 010778b87f6995e0d38a29d15b3041529cb43b20
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 15:25:59 2019 +0900

    chore: JPA 실행 쿼리 보기 설정

commit fcf46a795cfdb3dcdd14a683d93dd1c1ea0817b7
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 14:48:54 2019 +0900

    feat: 세션 검증 Interceptor 구현

commit 261149e005613cc1fe6ab4ead0485842ff80780c
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 13:12:06 2019 +0900

    refactor: html 패키지 구조 변경

* feat: 회원 수정 및 탈퇴기능 구현

Squashed commit of the following:

commit d36b93b9f56d5a631168ea59425740e3852b4c23
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 17:29:53 2019 +0900

    feat: 회원 탈퇴 기능 구현

commit 0a0adfd710c88ca96f77b0a14b71812556c0643d
Author: JunHoPark <[email protected]>
Date:   Wed Jul 17 17:07:59 2019 +0900

    feat: 회원 수정 기능 구현

* refactor: html 중복제거 및 세션적용

* fix: Interceptor에서 /users post요청은 통과

* feat: Error 객체의 정의

Login 관련 에러 처리 객체

* test: 세션관련 인수테스트 작성

* test: 회원관련 인수테스트

* refactor: 들여쓰기 및 사용하지 않는 import 제거

* feat: 회원가입시 이메일 중복 오류 처리

* fix: 회원 가입시 email 중복 테스트 수정

* refactor: UserService 분리

* feat: 회원 수정시 이름 형식 오류 처리

* feat: 회원 가입시 유효하지 않은 입력 처리

* refactor: Article 서비스 패키지 분리

* fix: 유저 리스트에서 다른 유저 정보에 접근하는 오류 처리

* feat: 데이터베이스 구축

* 프로덕션 환경은 mysql, 테스트 환경은 h2를 사용하였다.

* refactor: 삼항 연산자로 변경

* login, logout을 구분하는 if-else문을 삼항 연산자로 변경

* article의 기본 그림을 구분하는 if-else문을 삼항 연산자로 변경

* refactor: 삼항 연산자로 변경

* fix: 게시글 id 접근하는 로직 수정

* fix: 절대 경로로 변경

* refactor: 삼항 연산자로 변경, url 변경

* refactor: fragments 패키지로 분리

* refactor: 피드백 반영

메서드 이름 수정
패키지 구조 전체적으로 변경 (root 기준으로 좀 더 깔끔해 지게 변경, 레이어드 아키텍쳐를 고려하여 depth 설정)
JPA 실행 쿼리 보여주기
JPA repository의 사용
Domain을 생성하는 곳 통일 (dto에서 domain 생성 vs 서비스에서 domain 생성 -> 서비스로 통일)

* feat: 비밀번호 암호화 구현

BCrypt 라이브러리를 이용한다. 내 프로젝트에 맞게 wrapping 작업을 한다. 그 이유는 크게 두가지이다.
1. BCrypt 클래스의 이름과 내부 메서드 철자의 어려움
2. 프로젝트의 특성에 맞게 새로 클래스를 구현할 수 있음 (인터페이스로 분리하여 다른 암호화를 쉽게 Bean을 통해 주입받아 변경 가능)

마지막으로 비밀번호 검증은 Web Config 와 연관있다고 생각하여 interceptor를 구현한 클래스를 수정하여 Web 설정과 관련된 @bean들의 집합 클래스로 변경

* refactor: implements 삭제

* feat: 비밀번호, 이메일 검증 로직 개선

* feat: 검증 로직 추가

비밀번호, 비밀번호 확인 필드 검증 기능
회원 수정 dto 추가

* fix: 없는 게시물 접근 처리

* test: 테스트 파라미터 추가

* fix: email validation 추가

* test: cookie를 이용한 테스트

* refactor: 피드백 반영

생성자 private, location 검증 수정

* refactor: localport 제거

* refactor: 패키지 이름 변경

* refactor: 클래스 이름 변경

Impl 네이밍은 불필요하다. 조금 더 암호화 구체 클래스의 기능을 나타내는 이름으로 변경하였다.

* feat: paging 적용

* feat: article, user 관계 매핑

* feat: article 작성자 매핑 기능 추가

* feat: Comment 작성, 조회

* feat: Comment 수정, 삭제

* refactor: comment redirect

* feat: Comment 시간 계산

* fix: 타인이 게시글 삭제 막기

* test: 삭제 테스트 수정 및 정리

interface 안쓰는것 삭제

* feat: JPA auditing 적용

Article과 Comment는 User의 행위가 연관되어 있기 때문에 그 둘을 연관시켜 저장하는 auditing기능을 구현한다.

* feat: Comment 응답 dto에 대한 Custom Mapper의 구현

custom Converter를 구현한다.

* refactor: test 중복제거

* feat: JPA auditing, custom Context holder 적용

수정자에 대한 업데이트가 이루어지도록 추가, 이 부분을 추가하려면 authenticated된 유저정보가 필요한데 security를 적용하지 않을 것이므로 static으로 유저정보들을 thread-safe하게 공유할 수 있는 contextholder를 직접 만들어본다.

* fix: session holder 코드 원복

미션 기한을 맞추기 위해서 추후 기능으로 한다

* refactor: Comment 검증 로직

User 체크를 조금 더 명시적이게 변경

* feat: CoverUrl resource의 존재 확인

head 요청으로 리소스의 유무를 확인하여 없다면 article 저장이 되지 않게 함

* test: coverUrl 경로 수정
  • Loading branch information
JunHoPark93 authored and 이동규 committed Aug 3, 2019
1 parent 1af9479 commit 827275b
Show file tree
Hide file tree
Showing 45 changed files with 1,124 additions and 376 deletions.
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 {
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;

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

0 comments on commit 827275b

Please sign in to comment.