diff --git a/.gitignore b/.gitignore index a920cd2..00a92f7 100644 --- a/.gitignore +++ b/.gitignore @@ -260,4 +260,9 @@ gradle-app.setting # Java heap dump *.hprof -# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij,java,gradle,windows,macos,linux \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij,java,gradle,windows,macos,linux +.idea/modules/blog.test.iml +.idea/modules/blog.main.iml +.idea/modules/blog.iml +.idea/misc.xml +.idea/Blog-API.iml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..0d50744 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +blog \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..7853a20 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..fdc392f --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jpa-buddy.xml b/.idea/jpa-buddy.xml new file mode 100644 index 0000000..898e07a --- /dev/null +++ b/.idea/jpa-buddy.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3f7765a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 82beb60..b6fa823 100644 --- a/build.gradle +++ b/build.gradle @@ -22,19 +22,30 @@ repositories { } dependencies { + // Spring Boot implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' - compileOnly 'org.projectlombok:lombok' + + implementation group: 'org.mapstruct', name: 'mapstruct', version: '1.5.5.Final' + implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + implementation 'org.projectlombok:lombok' + + annotationProcessor group: 'org.mapstruct', name: 'mapstruct-processor', version: '1.5.5.Final' + annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0" annotationProcessor 'org.projectlombok:lombok' + + // Database + implementation 'org.mariadb.jdbc:mariadb-java-client:3.1.4' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' - implementation 'org.mariadb.jdbc:mariadb-java-client:3.1.4' + testAnnotationProcessor 'org.projectlombok:lombok' implementation 'com.h2database:h2:2.1.214' // TODO : testimplementation으로 변경 - implementation 'io.jsonwebtoken:jjwt-api:0.11.2' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' + // JWT implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' diff --git a/src/main/generated/com/gdsc/blog/mapper/ArticleMapperImpl.java b/src/main/generated/com/gdsc/blog/mapper/ArticleMapperImpl.java new file mode 100644 index 0000000..f3c4714 --- /dev/null +++ b/src/main/generated/com/gdsc/blog/mapper/ArticleMapperImpl.java @@ -0,0 +1,41 @@ +package com.gdsc.blog.mapper; + +import com.gdsc.blog.article.dto.ArticleDto; +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.comment.entity.Comment; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; +import org.springframework.stereotype.Component; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2023-07-17T01:17:13+0900", + comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.7 (Oracle Corporation)" +) +@Component +public class ArticleMapperImpl implements ArticleMapper { + + @Override + public ArticleDto toDto(Article article) { + if ( article == null ) { + return null; + } + + ArticleDto.ArticleDtoBuilder articleDto = ArticleDto.builder(); + + articleDto.idx( article.getIdx() ); + articleDto.title( article.getTitle() ); + articleDto.content( article.getContent() ); + List list = article.getComments(); + if ( list != null ) { + articleDto.comments( new ArrayList( list ) ); + } + + articleDto.username( article.getUser().getUsername() ); + articleDto.createdAt( article.getCreateDate().toLocalDateTime() ); + articleDto.modifiedAt( article.getModifyDate().toLocalDateTime() ); + + return articleDto.build(); + } +} diff --git a/src/main/java/com/gdsc/blog/BlogApplication.java b/src/main/java/com/gdsc/blog/BlogApplication.java index 56b0143..42bc01c 100644 --- a/src/main/java/com/gdsc/blog/BlogApplication.java +++ b/src/main/java/com/gdsc/blog/BlogApplication.java @@ -5,9 +5,7 @@ @SpringBootApplication public class BlogApplication { - - public static void main(String[] args) { - SpringApplication.run(BlogApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(BlogApplication.class, args); + } } diff --git a/src/main/java/com/gdsc/blog/article/controller/ArticleController.java b/src/main/java/com/gdsc/blog/article/controller/ArticleController.java new file mode 100644 index 0000000..ca1e35f --- /dev/null +++ b/src/main/java/com/gdsc/blog/article/controller/ArticleController.java @@ -0,0 +1,123 @@ +package com.gdsc.blog.article.controller; + +import com.gdsc.blog.article.dto.ArticleCreateDto; +import com.gdsc.blog.article.dto.ArticleDto; +import com.gdsc.blog.article.dto.ArticleUpdateDto; +import com.gdsc.blog.article.service.ArticleService; +import com.gdsc.blog.security.jwt.JwtTokenProvider; +import com.gdsc.blog.user.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Controller +@RequestMapping("/api/article") +@RestController +@RequiredArgsConstructor +@Tag(name = "게시글 Controller", description = "Article controller API") +@Slf4j +public class ArticleController { + + private final ArticleService articleService; + private final UserService userService; + private final JwtTokenProvider jwtTokenProvider; + + /** + * 게시글 생성 API + * + * @param articleCreateDto 게시글 생성 DTO + * @return 생성된 게시글 + */ + @PostMapping //컨트롤러 메핑 + @Operation(summary = "게시글 생성") //swagger 설명 + public ArticleDto createArticle( + @Parameter(name = "게시글 생성 DTO") @RequestBody ArticleCreateDto articleCreateDto, + @Parameter(hidden = true) HttpServletRequest req) { + + String username = jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req)); + return articleService.createArticle(articleCreateDto, username); + } + + /** + * 최근 게시글 조회 API + * + * @return 게시글 목록 + */ + @GetMapping + @Operation(summary = "최근 게시글 조회") + public List getUserArticle() { + return articleService.getRecentArticle(); + } + + /** + * id 로 게시글 조회 + * + * @param id 게시글 id + * @param req HTTP 파싱 객체 + * @return 게시글 + */ + @GetMapping("/{id}") + @Operation(summary = "id로 게시글 조회") + public ArticleDto getArticleById( + @Parameter(description = "게시글 id") @PathVariable("id") Long id) { + return articleService.getArticleById(id); + } + + /** + * 제목으로 게시글 조회 + * + * @param title 게시글 제목 + * @param req HTTP 파싱 객체 + * @return 게시글 + */ + @GetMapping("/search") + @Operation(summary = "제목으로 게시글 조회") + public List getArticleByTitle( + @Parameter(description = "게시글 제목") @RequestParam String title, + @Parameter(hidden = true) HttpServletRequest req) { + return articleService.findArticleByTitle(title); + } + + /** + * 게시글 수정 + * + * @param id 게시글 id + * @param dto 게시글 수정 정보 + * @param req HTTP 파싱 객체 + */ + @PatchMapping("/{id}") + @Operation(summary = "게시글 수정") + public ArticleDto updateArticle( + @Parameter(description = "게시글 id") @PathVariable(value = "id") Long id, + @Parameter(name = "게시글 수정 DTO") @RequestBody ArticleUpdateDto dto, + @Parameter(hidden = true) HttpServletRequest req) { + + String username = jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req)); + return articleService.updateArticle(id, dto, username); + } + + /** + * 게시글 삭제 API + * + * @param id 게시글 id + * @param req HTTP 파싱 객체 + */ + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + @Operation(summary = "게시글 삭제") + public void deleteArticle( + @Parameter(description = "게시글 id") @PathVariable(value = "id") Long id, + @Parameter(hidden = true) HttpServletRequest req) { + + String username = jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req)); + articleService.deleteArticle(id, username); + } +} diff --git a/src/main/java/com/gdsc/blog/article/dto/ArticleCreateDto.java b/src/main/java/com/gdsc/blog/article/dto/ArticleCreateDto.java new file mode 100644 index 0000000..ab34246 --- /dev/null +++ b/src/main/java/com/gdsc/blog/article/dto/ArticleCreateDto.java @@ -0,0 +1,17 @@ +package com.gdsc.blog.article.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class ArticleCreateDto { + @Schema(description = "제목", example = "게시글 제목") + public String title; + + @Schema(description = "내용", example = "게시글 내용") + public String content; +} diff --git a/src/main/java/com/gdsc/blog/article/dto/ArticleDto.java b/src/main/java/com/gdsc/blog/article/dto/ArticleDto.java new file mode 100644 index 0000000..4497e16 --- /dev/null +++ b/src/main/java/com/gdsc/blog/article/dto/ArticleDto.java @@ -0,0 +1,23 @@ +package com.gdsc.blog.article.dto; + +import com.gdsc.blog.comment.entity.Comment; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@ToString +@Builder +@Getter +@Setter +public class ArticleDto { + private Long idx; + private String title; + private String content; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private String username; + private List comments; +} diff --git a/src/main/java/com/gdsc/blog/article/dto/ArticleUpdateDto.java b/src/main/java/com/gdsc/blog/article/dto/ArticleUpdateDto.java new file mode 100644 index 0000000..cd7ebc9 --- /dev/null +++ b/src/main/java/com/gdsc/blog/article/dto/ArticleUpdateDto.java @@ -0,0 +1,17 @@ +package com.gdsc.blog.article.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class ArticleUpdateDto { + @Schema(description = "수정할 제목", example = "수정된 게시글 제목") + public String title; + + @Schema(description = "수정할 내용", example = "수정된 게시글 내용") + public String content; +} diff --git a/src/main/java/com/gdsc/blog/article/entity/Article.java b/src/main/java/com/gdsc/blog/article/entity/Article.java new file mode 100644 index 0000000..4453cdb --- /dev/null +++ b/src/main/java/com/gdsc/blog/article/entity/Article.java @@ -0,0 +1,40 @@ +package com.gdsc.blog.article.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.gdsc.blog.comment.entity.Comment; +import com.gdsc.blog.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.sql.Timestamp; +import java.util.List; + +@Entity +@NoArgsConstructor +@AllArgsConstructor //생성자 자동 생성 +@Getter +@Setter +@Builder +@Table(name = "articles") +public class Article { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idx; //id + + @Column(length = 200) + private String title; + + @Column(columnDefinition = "TEXT") //no limit length of text + private String content; + + private Timestamp createDate; //생성 날짜 + private Timestamp modifyDate; //수정된 날짜 + + //set relationship between article and user + @ManyToOne + @JsonBackReference + private User user; + + @OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE, fetch = FetchType.EAGER) + private List comments; +} diff --git a/src/main/java/com/gdsc/blog/article/repository/ArticleRepository.java b/src/main/java/com/gdsc/blog/article/repository/ArticleRepository.java new file mode 100644 index 0000000..018ac19 --- /dev/null +++ b/src/main/java/com/gdsc/blog/article/repository/ArticleRepository.java @@ -0,0 +1,15 @@ +package com.gdsc.blog.article.repository; + +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ArticleRepository extends JpaRepository { + List
findByTitleLike(String title); + + List
findByUser(User user); +} diff --git a/src/main/java/com/gdsc/blog/article/service/ArticleService.java b/src/main/java/com/gdsc/blog/article/service/ArticleService.java new file mode 100644 index 0000000..0aadd9f --- /dev/null +++ b/src/main/java/com/gdsc/blog/article/service/ArticleService.java @@ -0,0 +1,159 @@ +package com.gdsc.blog.article.service; + +import com.gdsc.blog.article.dto.ArticleCreateDto; +import com.gdsc.blog.article.dto.ArticleDto; +import com.gdsc.blog.article.dto.ArticleUpdateDto; +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.article.repository.ArticleRepository; +import com.gdsc.blog.mapper.ArticleMapper; +import com.gdsc.blog.user.entity.User; +import com.gdsc.blog.user.entity.UserRole; +import com.gdsc.blog.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ArticleService { + + private final UserRepository userRepository; + private final ArticleRepository articleRepository; + private final ArticleMapper articleMapper; + + /** + * 게시글 생성 + * + * @param articleCreateDto 게시글 생성 DTO + * @param username 작성자 + * @throws IllegalArgumentException 유저 정보가 없을 경우 + */ + public ArticleDto createArticle(ArticleCreateDto articleCreateDto, String username) { + + // 현재 시간 + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + + // 유저 정보 + Optional user = userRepository.findByUsername(username); + if (user.isEmpty()) { + throw new IllegalArgumentException("Invalid username"); + } + + // 게시글 생성 + Article article = Article.builder() + .title(articleCreateDto.getTitle()) + .content(articleCreateDto.getContent()) + .user(user.get()) + .createDate(timestamp) + .modifyDate(timestamp) + .build(); + + return articleMapper.toDto(articleRepository.save(article)); + } + + /** + * 최근 게시글 목록 조회 + * + * @return 게시글 목록 + */ + public List getRecentArticle() { + // get recent article list + List
articles = articleRepository.findAll(Sort.by(Sort.Direction.DESC, "createDate")); // 최신순으로 정렬 + + // article list to article dto list + List articleList = new ArrayList<>(); + articles.forEach(article -> articleList.add(articleMapper.toDto(article))); + + return articleList; + } + + /** + * 유저 게시글 목록 조회 + * + * @return 게시글 목록 + */ + public List getUserArticle(User user) { + // get user article list + List
articles = articleRepository.findByUser(user); + log.debug("articles: {}", articles); + + // article list to article dto list + List articleList = new ArrayList<>(); + articles.forEach(article -> articleList.add(articleMapper.toDto(article))); + + return articleList; + } + + /** + * id로 게시글 조회 + * + * @param idx 게시글 id + * @return Article 객체 + */ + public ArticleDto getArticleById(Long idx) { //get article by id + Article article = articleRepository.findById(idx).orElseThrow( + () -> new IllegalArgumentException("Not found Article by id") + ); + return articleMapper.toDto(article); + } + + public List findArticleByTitle(String title) { + List
articles = articleRepository.findByTitleLike(title); + + List articleList = new ArrayList<>(); + articles.forEach(article -> articleList.add(articleMapper.toDto(article))); + + return articleList; + } + + /** + * 게시글 수정 + * + * @param id 게시글 id + * @param dto 게시글 수정 DTO + * @param user 작성자 + * @return Article 객체 + */ + public ArticleDto updateArticle(Long id, ArticleUpdateDto dto, String username) { + + Article article = articleRepository.findById(id).orElseThrow(); //게시글 id로 게시글 가져오기 + User user = userRepository.findByUsername(username).orElseThrow(); //유저 정보 가져오기 + + if (!article.getUser().getUsername().equals(user.getUsername())) { //게시글 작성자와 로그인한 유저가 같지 않을 경우 + throw new IllegalArgumentException("수정 권한이 없습니다."); + } + + article.setTitle(dto.getTitle()); + article.setContent(dto.getContent()); + article.setModifyDate(new Timestamp(System.currentTimeMillis())); + + return articleMapper.toDto(articleRepository.save(article)); + } + + /** + * 게시글 삭제 (관리자, 작성자만 가능) + * + * @param idx 게시글 id + * @param user 작성자 + */ + public void deleteArticle(Long idx, String username) { + Article article = articleRepository.findById(idx).orElseThrow(); //게시글 id로 게시글 가져오기 + + User user = userRepository.findByUsername(username).orElseThrow(); //유저 정보 가져오기 + + // 게시글 작성자와 로그인한 유저가 같거나, 로그인 한 유저가 관리자일 경우 + if (article.getUser().getUsername().equals(user.getUsername()) || user.getRoles().contains(UserRole.ROLE_ADMIN)) { + articleRepository.delete(article); + } else { + throw new NullPointerException("삭제 권한이 없습니다."); + } + + } +} diff --git a/src/main/java/com/gdsc/blog/comment/controller/CommentController.java b/src/main/java/com/gdsc/blog/comment/controller/CommentController.java new file mode 100644 index 0000000..52f142a --- /dev/null +++ b/src/main/java/com/gdsc/blog/comment/controller/CommentController.java @@ -0,0 +1,129 @@ +package com.gdsc.blog.comment.controller; + +import com.gdsc.blog.article.service.ArticleService; +import com.gdsc.blog.comment.dto.CommentCreateDto; +import com.gdsc.blog.comment.dto.CommentUpdateDto; +import com.gdsc.blog.comment.entity.Comment; +import com.gdsc.blog.comment.service.CommentService; +import com.gdsc.blog.user.entity.User; +import com.gdsc.blog.user.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequestMapping("api/comment") +@RestController +@RequiredArgsConstructor +@Slf4j +@Tag(name = "댓글", description = "Comment Controller API") +public class CommentController { + private final ArticleService articleService; + private final CommentService commentService; + private final UserService userService; + + /** + * 댓글 생성 + * + * @param id 게시글 id + * @param dto 댓글 생성 정보 + * @param req HTTP 파싱 객체 + */ + @PostMapping("/{postId}") //컨트롤러 메핑 + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") //권한 설정 + @Operation(summary = "댓글 생성") + public Comment createComment( + @Parameter(description = "게시글 id") @PathVariable("postId") Long id, + @Parameter(name = "댓글 생성 DTO") @RequestBody CommentCreateDto dto, + @Parameter(hidden = true) HttpServletRequest req) { + + Comment comment = Comment.builder() + .content(dto.getContent()) + .build(); + + User user = userService.whoami(req); //로그인 유저 정보 가져오기 + + return commentService.create(comment, id, user); //create comment + } + +// /** +// * 게시글에 대한 모든 댓글 조회 +// * @param id 게시글 id +// * @param req HTTP 파싱 객체 +// * @return 댓글 객체 List +// */ +// @GetMapping("/get/{postId}") +// @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") +// @Operation(summary = "게시글에 대한 모든 댓글 조회") +// public List getAllCommand( +// @Parameter(description = "게시글 id") @PathVariable("postId") Long id, +// @Parameter(hidden = true) HttpServletRequest req) { +// User user = userService.whoami(req); +// +// Article article = articleService.getArticleById(id); //get article object +// +// return commentService.getAllCommandByArticle(article); //return comment content +// } + + /** + * id로 댓글 조회 + * + * @param id 댓글 id + * @param req HTTP 파싱 객체 + * @return 댓글 객체 + */ + @GetMapping("/get/all/{commentId}") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + @Operation(summary = "id로 댓글 조회") + public Comment getCommentById( + @Parameter(description = "댓글 id") @PathVariable("commentId") Long id, + @Parameter(hidden = true) HttpServletRequest req) { + User user = userService.whoami(req); + + return commentService.getCommentById(id); + } + + /** + * 댓글 수정 + * + * @param id 댓글 id + * @param dto 댓글 수정 정보 + * @param req HTTP 파싱 객체 + */ + @PostMapping("/patch/{commentId}") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + @Operation(summary = "댓글 수정") + public Comment updateComment( + @Parameter(description = "댓글 id") @PathVariable("commentId") Long id, + @Parameter(name = "댓글 수정 DTO") @RequestBody CommentUpdateDto dto, + @Parameter(hidden = true) HttpServletRequest req) { + User user = userService.whoami(req); + + return commentService.updateComment(id, dto); + } + + /** + * 댓글 삭제 + * + * @param id 댓글 id + * @param req HTTP 파싱 객체 + */ + @GetMapping("/delete/{commentId}") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + @Operation(summary = "댓글 삭제") + public void deleteComment( + @Parameter(description = "댓글 id") @PathVariable("commentId") Long id, + @Parameter(hidden = true) HttpServletRequest req) { + User user = userService.whoami(req); + + Comment comment = commentService.getCommentById(id); //get comment object + + commentService.delete(comment); //delete comment + } +} diff --git a/src/main/java/com/gdsc/blog/comment/dto/CommentCreateDto.java b/src/main/java/com/gdsc/blog/comment/dto/CommentCreateDto.java new file mode 100644 index 0000000..b7fc3c2 --- /dev/null +++ b/src/main/java/com/gdsc/blog/comment/dto/CommentCreateDto.java @@ -0,0 +1,14 @@ +package com.gdsc.blog.comment.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class CommentCreateDto { + @Schema(description = "내용", example = "댓글 내용") + public String content; +} diff --git a/src/main/java/com/gdsc/blog/comment/dto/CommentUpdateDto.java b/src/main/java/com/gdsc/blog/comment/dto/CommentUpdateDto.java new file mode 100644 index 0000000..b64492c --- /dev/null +++ b/src/main/java/com/gdsc/blog/comment/dto/CommentUpdateDto.java @@ -0,0 +1,14 @@ +package com.gdsc.blog.comment.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class CommentUpdateDto { + @Schema(description = "수정할 내용", example = "수정된 댓글 내용") + public String content; +} diff --git a/src/main/java/com/gdsc/blog/comment/entity/Comment.java b/src/main/java/com/gdsc/blog/comment/entity/Comment.java new file mode 100644 index 0000000..2ef8c94 --- /dev/null +++ b/src/main/java/com/gdsc/blog/comment/entity/Comment.java @@ -0,0 +1,36 @@ +package com.gdsc.blog.comment.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.user.entity.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Builder +@Table(name = "comments") +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idx; //id + + @Column(length = 200) + private String content; //내용 + + private LocalDateTime createData; //create date + private LocalDateTime modifyData; //update date + + @ManyToOne + @JsonBackReference + private Article article; + + @ManyToOne + @JsonBackReference + private User user; +} diff --git a/src/main/java/com/gdsc/blog/comment/repository/CommentRepository.java b/src/main/java/com/gdsc/blog/comment/repository/CommentRepository.java new file mode 100644 index 0000000..0dc8482 --- /dev/null +++ b/src/main/java/com/gdsc/blog/comment/repository/CommentRepository.java @@ -0,0 +1,14 @@ +package com.gdsc.blog.comment.repository; + +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findByArticle(Article article); + + List findByArticle_Idx(Long idx); + +} diff --git a/src/main/java/com/gdsc/blog/comment/service/CommentService.java b/src/main/java/com/gdsc/blog/comment/service/CommentService.java new file mode 100644 index 0000000..9afcdff --- /dev/null +++ b/src/main/java/com/gdsc/blog/comment/service/CommentService.java @@ -0,0 +1,80 @@ +package com.gdsc.blog.comment.service; + +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.article.repository.ArticleRepository; +import com.gdsc.blog.comment.dto.CommentUpdateDto; +import com.gdsc.blog.comment.entity.Comment; +import com.gdsc.blog.comment.repository.CommentRepository; +import com.gdsc.blog.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + private final ArticleRepository articleRepository; + + /** + * 댓글 생성 + * + * @param comment 댓글 객체 + * @param article 개시글 객체 + * @param user 로그인 유저 객체 + */ + public Comment create(Comment comment, Long id, User user) { + comment.setArticle(articleRepository.findById(id).orElseThrow()); + comment.setUser(user); + comment.setCreateData(LocalDateTime.now()); + comment.setModifyData(LocalDateTime.now()); + + return commentRepository.save(comment); + } + + public List getAllCommandByArticle(Long articleId) { + return commentRepository.findByArticle_Idx(articleId); + } + + /** + * id로 댓글 조회 + * + * @param idx 댓글 id + * @return 댓글 객체 + */ + public Comment getCommentById(Long idx) { + Optional comment = commentRepository.findById(idx); + + return comment.orElseThrow(); + + } + + /** + * 댓글 수정 + * + * @param id 댓글 id + * @param commentUpdateDto 댓글 수정 DTO + */ + public Comment updateComment(Long id, CommentUpdateDto commentUpdateDto) { + + Comment comment = getCommentById(id); + comment.setContent(commentUpdateDto.getContent()); + comment.setModifyData(LocalDateTime.now()); + + return commentRepository.save(comment); + } + + + /** + * 댓글 삭제 + * + * @param comment 댓글 객체 + */ + public void delete(Comment comment) { + commentRepository.delete(comment); + } +} diff --git a/src/main/java/com/gdsc/blog/configuration/JwtTokenFilterConfigurer.java b/src/main/java/com/gdsc/blog/configuration/JwtTokenFilterConfigurer.java index 8876d3d..fd849b1 100644 --- a/src/main/java/com/gdsc/blog/configuration/JwtTokenFilterConfigurer.java +++ b/src/main/java/com/gdsc/blog/configuration/JwtTokenFilterConfigurer.java @@ -1,27 +1,27 @@ package com.gdsc.blog.configuration; +import com.gdsc.blog.security.jwt.JwtTokenFilter; +import com.gdsc.blog.security.jwt.JwtTokenProvider; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import com.gdsc.blog.security.jwt.JwtTokenFilter; -import com.gdsc.blog.security.jwt.JwtTokenProvider; @Configuration public class JwtTokenFilterConfigurer - extends SecurityConfigurerAdapter { + extends SecurityConfigurerAdapter { - private JwtTokenProvider jwtTokenProvider; + private JwtTokenProvider jwtTokenProvider; - public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) { - this.jwtTokenProvider = jwtTokenProvider; - } + public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } - @Override - public void configure(HttpSecurity http) throws Exception { - JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); - http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); - } + @Override + public void configure(HttpSecurity http) throws Exception { + JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } } diff --git a/src/main/java/com/gdsc/blog/configuration/SecurityConfig.java b/src/main/java/com/gdsc/blog/configuration/SecurityConfig.java index d194f30..a2e5331 100644 --- a/src/main/java/com/gdsc/blog/configuration/SecurityConfig.java +++ b/src/main/java/com/gdsc/blog/configuration/SecurityConfig.java @@ -1,40 +1,49 @@ package com.gdsc.blog.configuration; +import com.gdsc.blog.security.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import com.gdsc.blog.security.jwt.JwtTokenProvider; -import lombok.RequiredArgsConstructor; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - private final JwtTokenProvider jwtTokenProvider; - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - - // Disable CSRF (cross site request forgery) - http.authorizeRequests() - .requestMatchers("/h2-console/**", "/swagger-ui/**", "/swagger-resources/**", - "/v3/api-docs/**", "/api/user/signin", "/api/user/signup") - .permitAll().anyRequest().authenticated(); - - http.csrf().disable(); - - http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); - - return http.build(); - } + private final JwtTokenProvider jwtTokenProvider; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // 요청 권한 설정 + http.authorizeRequests() + .requestMatchers("/h2-console/**", + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/api/user/signin", + "/api/user/signup", + "/", "index.html") // 홈페이지 + .permitAll() + .anyRequest() + .authenticated(); + + // Disable CSRF (cross site request forgery) + http.csrf(AbstractHttpConfigurer::disable); + + // JWT Provider 등록 + http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider)); + + return http.build(); + } } diff --git a/src/main/java/com/gdsc/blog/configuration/SwaggerConfig.java b/src/main/java/com/gdsc/blog/configuration/SwaggerConfig.java index 6738e3e..2bcfe9e 100644 --- a/src/main/java/com/gdsc/blog/configuration/SwaggerConfig.java +++ b/src/main/java/com/gdsc/blog/configuration/SwaggerConfig.java @@ -1,12 +1,5 @@ package com.gdsc.blog.configuration; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Contact; import io.swagger.v3.oas.annotations.info.Info; @@ -14,6 +7,8 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; @OpenAPIDefinition(info = @Info(title = "JSON Web Token Authentication API", version = "1.0", diff --git a/src/main/java/com/gdsc/blog/mapper/ArticleMapper.java b/src/main/java/com/gdsc/blog/mapper/ArticleMapper.java new file mode 100644 index 0000000..b6a6c0d --- /dev/null +++ b/src/main/java/com/gdsc/blog/mapper/ArticleMapper.java @@ -0,0 +1,14 @@ +package com.gdsc.blog.mapper; + +import com.gdsc.blog.article.dto.ArticleDto; +import com.gdsc.blog.article.entity.Article; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface ArticleMapper { + @Mapping(target = "username", expression = "java(article.getUser().getUsername())") + @Mapping(target = "createdAt", expression = "java(article.getCreateDate().toLocalDateTime())") + @Mapping(target = "modifiedAt", expression = "java(article.getModifyDate().toLocalDateTime())") + ArticleDto toDto(Article article); +} diff --git a/src/main/java/com/gdsc/blog/security/MyUserDetails.java b/src/main/java/com/gdsc/blog/security/MyUserDetails.java index 4aa3ffb..2e64221 100644 --- a/src/main/java/com/gdsc/blog/security/MyUserDetails.java +++ b/src/main/java/com/gdsc/blog/security/MyUserDetails.java @@ -1,39 +1,35 @@ package com.gdsc.blog.security; +import com.gdsc.blog.user.entity.User; +import com.gdsc.blog.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import com.gdsc.blog.user.entity.User; -import com.gdsc.blog.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; - @Service @RequiredArgsConstructor public class MyUserDetails implements UserDetailsService { - private final UserRepository userRepository; - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - final User user = userRepository.findByUsername(username).get(); - - if (user == null) { - throw new UsernameNotFoundException("User '" + username + "' not found"); + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User '" + username + "' not found")); + + return org.springframework.security.core.userdetails.User// + .withUsername(username)// + .password(user.getPassword())// + .authorities(user.getRoles())// + .accountExpired(false)// + .accountLocked(false)// + .credentialsExpired(false)// + .disabled(false)// + .build(); } - return org.springframework.security.core.userdetails.User// - .withUsername(username)// - .password(user.getPassword())// - .authorities(user.getRoles())// - .accountExpired(false)// - .accountLocked(false)// - .credentialsExpired(false)// - .disabled(false)// - .build(); - } - } diff --git a/src/main/java/com/gdsc/blog/security/jwt/JwtAccessDeniedHandler.java b/src/main/java/com/gdsc/blog/security/jwt/JwtAccessDeniedHandler.java index 0482a31..5c69156 100644 --- a/src/main/java/com/gdsc/blog/security/jwt/JwtAccessDeniedHandler.java +++ b/src/main/java/com/gdsc/blog/security/jwt/JwtAccessDeniedHandler.java @@ -13,7 +13,7 @@ public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException { + AccessDeniedException accessDeniedException) throws IOException { // 필요한 권한이 없이 접근하려 할때 403 response.sendError(HttpServletResponse.SC_FORBIDDEN); } diff --git a/src/main/java/com/gdsc/blog/security/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/gdsc/blog/security/jwt/JwtAuthenticationEntryPoint.java index 8dbfebd..9fabf93 100644 --- a/src/main/java/com/gdsc/blog/security/jwt/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/gdsc/blog/security/jwt/JwtAuthenticationEntryPoint.java @@ -13,7 +13,7 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { + AuthenticationException authException) throws IOException { // 유효한 자격증명을 제공하지 않고 접근하려 할때 401 response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } diff --git a/src/main/java/com/gdsc/blog/security/jwt/JwtTokenFilter.java b/src/main/java/com/gdsc/blog/security/jwt/JwtTokenFilter.java index 31e0c3d..4808f7a 100644 --- a/src/main/java/com/gdsc/blog/security/jwt/JwtTokenFilter.java +++ b/src/main/java/com/gdsc/blog/security/jwt/JwtTokenFilter.java @@ -1,47 +1,44 @@ package com.gdsc.blog.security.jwt; -import java.io.IOException; - - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; - - -import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; // We should use OncePerRequestFilter since we are doing a database call, there is no point in doing // this more than once public class JwtTokenFilter extends OncePerRequestFilter { - private JwtTokenProvider jwtTokenProvider; - - public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { - this.jwtTokenProvider = jwtTokenProvider; - } - - @Override - protected void doFilterInternal(HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse, FilterChain filterChain) - throws ServletException, IOException { - String token = jwtTokenProvider.resolveToken(httpServletRequest); - try { - if (token != null && jwtTokenProvider.validateToken(token)) { - Authentication auth = jwtTokenProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(auth); - } - } catch (Exception ex) { - // this is very important, since it guarantees the user is not authenticated at all - SecurityContextHolder.clearContext(); - httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); - return; + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; } - filterChain.doFilter(httpServletRequest, httpServletResponse); - } + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, FilterChain filterChain) + throws ServletException, IOException { + String token = jwtTokenProvider.resolveToken(httpServletRequest); + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (Exception ex) { + // this is very important, since it guarantees the user is not authenticated at all + SecurityContextHolder.clearContext(); + httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, ex.getMessage()); + return; + } + + filterChain.doFilter(httpServletRequest, httpServletResponse); + } } diff --git a/src/main/java/com/gdsc/blog/security/jwt/JwtTokenProvider.java b/src/main/java/com/gdsc/blog/security/jwt/JwtTokenProvider.java index 16c43b7..68180c0 100644 --- a/src/main/java/com/gdsc/blog/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/gdsc/blog/security/jwt/JwtTokenProvider.java @@ -1,21 +1,5 @@ package com.gdsc.blog.security.jwt; -import java.util.Base64; -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - - - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; import com.gdsc.blog.security.MyUserDetails; import com.gdsc.blog.user.entity.UserRole; import io.jsonwebtoken.Claims; @@ -24,66 +8,81 @@ import io.jsonwebtoken.SignatureAlgorithm; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; @Component public class JwtTokenProvider { - @Value("${security.jwt.token.secret-key}") - private String secretKey; + @Value("${security.jwt.token.secret-key}") + private String secretKey; - @Value("${security.jwt.token.expire-length:3600000}") - private long validityInMilliseconds = 3600000; // 1h + @Value("${security.jwt.token.expire-length:3600000}") + private long validityInMilliseconds = 3600000; // 1h - @Autowired - private MyUserDetails myUserDetails; + @Autowired + private MyUserDetails myUserDetails; + + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } - @PostConstruct - protected void init() { - secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); - } + public String createToken(String username, List userRoles) { - public String createToken(String username, List userRoles) { + Claims claims = Jwts.claims().setSubject(username); + claims.put("auth", userRoles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())) + .filter(Objects::nonNull).collect(Collectors.toList())); - Claims claims = Jwts.claims().setSubject(username); - claims.put("auth", userRoles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())) - .filter(Objects::nonNull).collect(Collectors.toList())); + claims.put("UID", 1L); - Date now = new Date(); - Date validity = new Date(now.getTime() + validityInMilliseconds); + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); - return Jwts.builder()// - .setClaims(claims)// - .setIssuedAt(now)// - .setExpiration(validity)// - .signWith(SignatureAlgorithm.HS256, secretKey)// - .compact(); - } + return Jwts.builder()// + .setClaims(claims)// + .setIssuedAt(now)// + .setExpiration(validity)// + .signWith(SignatureAlgorithm.HS256, secretKey)// + .compact(); + } - public Authentication getAuthentication(String token) { - UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); - return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); - } + public Authentication getAuthentication(String token) { + UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } - public String getUsername(String token) { - return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); - } + public String getUsername(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } - public String resolveToken(HttpServletRequest req) { - String bearerToken = req.getHeader("Authorization"); - if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); + public String resolveToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; } - return null; - } - - public boolean validateToken(String token) { - try { - Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); - return true; - } catch (JwtException | IllegalArgumentException e) { - return false; + + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } } - } } diff --git a/src/main/java/com/gdsc/blog/user/controller/UserController.java b/src/main/java/com/gdsc/blog/user/controller/UserController.java index 463a5d9..a1e709d 100644 --- a/src/main/java/com/gdsc/blog/user/controller/UserController.java +++ b/src/main/java/com/gdsc/blog/user/controller/UserController.java @@ -1,67 +1,71 @@ package com.gdsc.blog.user.controller; -import java.util.HashSet; -import java.util.Set; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import com.gdsc.blog.user.service.UserService; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import io.swagger.v3.oas.annotations.Operation; + +import com.gdsc.blog.security.jwt.JwtTokenProvider; import com.gdsc.blog.user.dto.SignInDto; import com.gdsc.blog.user.dto.SignUpDto; import com.gdsc.blog.user.dto.Token; import com.gdsc.blog.user.entity.User; -import com.gdsc.blog.user.entity.UserRole; +import com.gdsc.blog.user.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; @Slf4j @Tag(name = "User", description = "User API") @RestController @RequestMapping("/api/user") +@RequiredArgsConstructor public class UserController { - UserService userService; + private final UserService userService; + private final JwtTokenProvider jwtTokenProvider; + - public UserController(UserService userService) { - this.userService = userService; - } + @PostMapping("/signup") + @Operation(summary = "회원가입") + public User signup(@RequestBody SignUpDto user) { - @PostMapping("/signup") - @Operation(summary = "회원가입") - public User signup(@RequestBody SignUpDto user) { + User user1 = User.builder().email(user.getEmail()).password(user.getPassword()) + .username(user.getUsername()).build(); - User user1 = User.builder().email(user.getEmail()).password(user.getPassword()) - .username(user.getUsername()).build(); + return userService.signup(user1); + } - return userService.signup(user1); - } + @PostMapping("/signin") + @Operation(summary = "로그인") + public Token signin(@RequestBody SignInDto signInDto) { + Token token = Token.builder() + .token(userService.singin(signInDto.getEmail(), signInDto.getPassword())).build(); + return token; - @PostMapping("/signin") - @Operation(summary = "로그인") - public Token signin(@RequestBody SignInDto signInDto) { - Token token = Token.builder() - .token(userService.singin(signInDto.getEmail(), signInDto.getPassword())).build(); - return token; + } - } + @PostMapping("/becomeAdmin") + public String addRole( + @Parameter(hidden = true) HttpServletRequest req + ) { + String username = jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req)); + userService.becomeAdmin(username); + return "success"; + } - @GetMapping("/getUser/{email}") - @PreAuthorize("hasRole('ROLE_ADMIN')") - public User getUser(@PathVariable("email") String email) { - return userService.getUser(email); - } + @GetMapping("/getUser/{email}") + @PreAuthorize("hasRole('ROLE_ADMIN')") + public User getUser(@PathVariable("email") String email) { + return userService.getUser(email); + } - @GetMapping("/whoisme") - @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") - public User whoisme(HttpServletRequest req) { - return userService.whoami(req); - } + @GetMapping("/whoisme") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + public User whoisme(HttpServletRequest req) { + return userService.whoami(req); + } } diff --git a/src/main/java/com/gdsc/blog/user/dto/SignInDto.java b/src/main/java/com/gdsc/blog/user/dto/SignInDto.java index 663c25c..705bc3e 100644 --- a/src/main/java/com/gdsc/blog/user/dto/SignInDto.java +++ b/src/main/java/com/gdsc/blog/user/dto/SignInDto.java @@ -1,16 +1,19 @@ package com.gdsc.blog.user.dto; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @Builder public class SignInDto { - @Schema(description = "이메일", example = "test@test.com") - public String email; + @Schema(description = "이메일", example = "test@test.com") + public String email; - @Schema(description = "비밀번호", example = "1234") - public String password; + @Schema(description = "비밀번호", example = "1234") + public String password; } diff --git a/src/main/java/com/gdsc/blog/user/dto/SignUpDto.java b/src/main/java/com/gdsc/blog/user/dto/SignUpDto.java index c540ccc..9d6f277 100644 --- a/src/main/java/com/gdsc/blog/user/dto/SignUpDto.java +++ b/src/main/java/com/gdsc/blog/user/dto/SignUpDto.java @@ -2,21 +2,24 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Pattern; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @Builder public class SignUpDto { - @Schema(description = "이메일", example = "test@test.com") - @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$", - message = "이메일 형식이 올바르지 않습니다.") - private String email; + @Schema(description = "이메일", example = "test@test.com") + @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$", + message = "이메일 형식이 올바르지 않습니다.") + private String email; - @Schema(description = "비밀번호", example = "1234") - private String password; + @Schema(description = "비밀번호", example = "1234") + private String password; - @Schema(description = "이름", example = "홍길동") - private String username; + @Schema(description = "이름", example = "홍길동") + private String username; } diff --git a/src/main/java/com/gdsc/blog/user/entity/User.java b/src/main/java/com/gdsc/blog/user/entity/User.java index 28cddab..18726dc 100644 --- a/src/main/java/com/gdsc/blog/user/entity/User.java +++ b/src/main/java/com/gdsc/blog/user/entity/User.java @@ -1,46 +1,36 @@ package com.gdsc.blog.user.entity; -import java.util.Collection; +import com.gdsc.blog.article.entity.Article; +import jakarta.persistence.*; +import lombok.*; + import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; @Entity @Data @Builder @AllArgsConstructor @NoArgsConstructor +@Getter +@Setter @Table(name = "users") public class User { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long idx; - @Column(unique = true) - private String email; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idx; + + @Column(unique = true) + private String email; - private String password; + private String password; - private String username; + private String username; - @ElementCollection(fetch = FetchType.EAGER) - List roles; + @ElementCollection(fetch = FetchType.EAGER) + List roles; + //set relationship between article and user + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List
articles; } diff --git a/src/main/java/com/gdsc/blog/user/entity/UserRole.java b/src/main/java/com/gdsc/blog/user/entity/UserRole.java index fb8496c..976963c 100644 --- a/src/main/java/com/gdsc/blog/user/entity/UserRole.java +++ b/src/main/java/com/gdsc/blog/user/entity/UserRole.java @@ -3,9 +3,10 @@ import org.springframework.security.core.GrantedAuthority; public enum UserRole implements GrantedAuthority { - ROLE_ADMIN, ROLE_CLIENT; + ROLE_ADMIN, ROLE_CLIENT; - public String getAuthority() { - return name(); - } + @Override + public String getAuthority() { + return name(); + } } diff --git a/src/main/java/com/gdsc/blog/user/repository/UserRepository.java b/src/main/java/com/gdsc/blog/user/repository/UserRepository.java index fb8ca10..042795e 100644 --- a/src/main/java/com/gdsc/blog/user/repository/UserRepository.java +++ b/src/main/java/com/gdsc/blog/user/repository/UserRepository.java @@ -1,27 +1,28 @@ package com.gdsc.blog.user.repository; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; import com.gdsc.blog.user.entity.User; import jakarta.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; public interface UserRepository extends JpaRepository { - // 아이디 중복 체크 - boolean existsByEmail(String email); + // 아이디 중복 체크 + boolean existsByEmail(String email); - // 로그인 메소드 - Optional findByEmailAndPassword(String email, String password); + // 로그인 메소드 + Optional findByEmailAndPassword(String email, String password); - // 회원 탈퇴 - @Transactional - void deleteById(Long id); + // 회원 탈퇴 + @Transactional + void deleteById(Long id); - // 유저 검색 - Optional findByEmail(String email); + // 유저 검색 + Optional findByEmail(String email); - Optional findByUsername(String username); + Optional findByUsername(String username); - // 회원가입 메소드 + // 회원가입 메소드 } diff --git a/src/main/java/com/gdsc/blog/user/service/UserService.java b/src/main/java/com/gdsc/blog/user/service/UserService.java index 3160448..cc3686c 100644 --- a/src/main/java/com/gdsc/blog/user/service/UserService.java +++ b/src/main/java/com/gdsc/blog/user/service/UserService.java @@ -1,14 +1,5 @@ package com.gdsc.blog.user.service; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; import com.gdsc.blog.security.jwt.JwtTokenProvider; import com.gdsc.blog.user.entity.User; import com.gdsc.blog.user.entity.UserRole; @@ -16,56 +7,82 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; @Service @Slf4j @RequiredArgsConstructor public class UserService { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtTokenProvider jwtTokenProvider; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + public User signup(User user) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + // Client 역할 부여 + if (user.getRoles() == null) { + List set = new ArrayList(); + set.add(UserRole.ROLE_CLIENT); + user.setRoles(set); + } + return userRepository.save(user); + } + public String singin(String email, String password) { + try { + User user = userRepository.findByEmail(email).get(); + log.info("user: {}", user); + log.info("email: {}", email); + log.info("password: {}", password); + if (!passwordEncoder.matches(password, user.getPassword())) { + log.error("Invalid password"); + throw new RuntimeException("Invalid password"); + } + return jwtTokenProvider.createToken(user.getUsername(), user.getRoles()); - public String singin(String email, String password) { - try { - User user = userRepository.findByEmail(email).get(); - log.info("user: {}", user); - log.info("email: {}", email); - log.info("password: {}", password); - if (!passwordEncoder.matches(password, user.getPassword())) { - log.error("Invalid password"); - throw new RuntimeException("Invalid password"); - } - return jwtTokenProvider.createToken(user.getUsername(), user.getRoles()); + } catch (Exception e) { + log.info(e.getMessage()); + log.error("Invalid username/password supplied"); + throw new RuntimeException("Invalid username/password supplied"); + } + } - } catch (Exception e) { - log.info(e.getMessage()); - log.error("Invalid username/password supplied"); - throw new RuntimeException("Invalid username/password supplied"); - } - } + public User getUser(String username) { + return userRepository.findByEmail(username).get(); + } - public User signup(User user) { - user.setPassword(passwordEncoder.encode(user.getPassword())); - // Client 역할 부여 - if (user.getRoles() == null) { - List set = new ArrayList(); - set.add(UserRole.ROLE_CLIENT); - user.setRoles(set); - } - return userRepository.save(user); - } + public User getUserByName(String username) { + try { + return userRepository.findByUsername(username).get(); + } catch (Exception e) { //예외 처리 + log.error("User not found"); + throw new RuntimeException("User not found"); + } + } - public User getUser(String username) { - return userRepository.findByEmail(username).get(); - } + public User becomeAdmin(String username) { + User user = userRepository.findByUsername(username).orElseThrow(); + List roles = user.getRoles(); + roles.add(UserRole.ROLE_ADMIN); + user.setRoles(roles); + return userRepository.save(user); + } - public User whoami(HttpServletRequest req) { - log.info(jwtTokenProvider.resolveToken(req)); - return userRepository - .findByUsername(jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req))) - .get(); - } + public User whoami(HttpServletRequest req) { + try { + log.info(jwtTokenProvider.resolveToken(req)); + return userRepository + .findByUsername(jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req))) + .get(); + } catch (Exception e) { //예외 처리 + log.error("User not found"); + throw new RuntimeException("User not found"); + } + } } diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..586f53c --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,22 @@ + + + + + + Blog API 서버 + + + + + +
+

Blog API 서버

+

Blog API 서버입니다.

+

API 문서는 Swagger Documentation을 참고하세요.

+
+ + + + + + diff --git a/src/test/java/com/gdsc/blog/BlogApplicationTests.java b/src/test/java/com/gdsc/blog/BlogApplicationTests.java index 533a031..26eb524 100644 --- a/src/test/java/com/gdsc/blog/BlogApplicationTests.java +++ b/src/test/java/com/gdsc/blog/BlogApplicationTests.java @@ -1,14 +1,29 @@ package com.gdsc.blog; +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.article.repository.ArticleRepository; +import com.gdsc.blog.article.service.ArticleService; +import com.gdsc.blog.comment.entity.Comment; +import com.gdsc.blog.comment.repository.CommentRepository; +import com.gdsc.blog.comment.service.CommentService; +import com.gdsc.blog.user.controller.UserController; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Profile; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + @SpringBootTest @Profile("test") class BlogApplicationTests { - @Test - void contextLoads() {} - + @Test + void contextLoads() { + assertTrue(true); + } } diff --git a/src/test/java/com/gdsc/blog/article/service/ArticleServiceTest.java b/src/test/java/com/gdsc/blog/article/service/ArticleServiceTest.java new file mode 100644 index 0000000..dda5054 --- /dev/null +++ b/src/test/java/com/gdsc/blog/article/service/ArticleServiceTest.java @@ -0,0 +1,260 @@ +package com.gdsc.blog.article.service; + +import com.gdsc.blog.article.dto.ArticleCreateDto; +import com.gdsc.blog.article.dto.ArticleDto; +import com.gdsc.blog.article.dto.ArticleUpdateDto; +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.user.entity.User; +import com.gdsc.blog.user.entity.UserRole; +import com.gdsc.blog.user.service.UserService; +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@SpringBootTest +@Profile("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) // @BeforeAll을 사용하기 위해 필요 +class ArticleServiceTest { + + @Autowired + private ArticleService articleService; + + @Autowired + private UserService userService; + + User user; + User admin; + + @BeforeAll + void setUp() { + // 회원 가입 + user = userService.signup(User.builder() + .username("user") + .email("user@test.com") + .password("password") + .build()); + admin = userService.signup(User.builder() + .username("admin") + .email("admin@admin.com") + .password("password") + .roles(Collections.singletonList(UserRole.ROLE_ADMIN)) + .build()); + userService.becomeAdmin(admin.getUsername()); + } + + @Test + @DisplayName("게시글 생성") + @Transactional + void createArticle() { + // given + ArticleCreateDto articleCreateDto = ArticleCreateDto.builder() + .title("제목") + .content("내용") + .build(); + // when + ArticleDto article = articleService.createArticle(articleCreateDto, user.getUsername()); + + // then + assertEquals(articleCreateDto.getTitle(), article.getTitle()); + assertEquals(articleCreateDto.getContent(), article.getContent()); + assertEquals(user.getUsername(), article.getUsername()); + assertNotNull(article.getIdx()); + } + + @Test + @DisplayName("유저별 게시글 조회") + @Transactional + void getUserArticle() { + // given + List articleCreateDtoList = Arrays.asList( + ArticleCreateDto.builder() + .title("제목1") + .content("내용1") + .build(), + ArticleCreateDto.builder() + .title("제목2") + .content("내용2") + .build(), + ArticleCreateDto.builder() + .title("제목3") + .content("내용3") + .build() + ); + for (ArticleCreateDto articleCreateDto : articleCreateDtoList) { + articleService.createArticle(articleCreateDto, user.getUsername()); + } + // when + List articleDtoList = articleService.getUserArticle(user); + + // then + assertEquals(articleCreateDtoList.size(), articleDtoList.size()); + for (int i = 0; i < articleCreateDtoList.size(); i++) { + assertEquals(articleCreateDtoList.get(i).getTitle(), articleDtoList.get(i).getTitle()); + assertEquals(articleCreateDtoList.get(i).getContent(), articleDtoList.get(i).getContent()); + assertEquals(user.getUsername(), articleDtoList.get(i).getUsername()); + } + + } + + @Test + @Transactional + @DisplayName("게시글 번호로 게시글 조회") + void getArticleById() { + // given + ArticleCreateDto articleCreateDto = ArticleCreateDto.builder() + .title("제목") + .content("내용") + .build(); + ArticleDto SavedArticle = articleService.createArticle(articleCreateDto, user.getUsername()); + + + // when + ArticleDto foundArticle = articleService.getArticleById(SavedArticle.getIdx()); + + // then + assertEquals(SavedArticle.getIdx(), foundArticle.getIdx()); + assertEquals(SavedArticle.getTitle(), foundArticle.getTitle()); + assertEquals(SavedArticle.getContent(), foundArticle.getContent()); + assertEquals(SavedArticle.getUsername(), foundArticle.getUsername()); + } + + @Test + @Transactional + @DisplayName("게시글 제목으로 검색") + void findArticleByTitle() { + // given + List articleCreateDtoList = Arrays.asList( + ArticleCreateDto.builder() + .title("제목1") + .content("내용1") + .build(), + ArticleCreateDto.builder() + .title("제목2") + .content("내용2") + .build(), + ArticleCreateDto.builder() + .title("제목3") + .content("내용3") + .build(), + ArticleCreateDto.builder() + .title("이상한 제목") + .content("이상한 내용") + .build(), + ArticleCreateDto.builder() + .title("NULL") + .content("NULL") + .build() + ); + + for (ArticleCreateDto articleCreateDto : articleCreateDtoList) { + articleService.createArticle(articleCreateDto, user.getUsername()); + } + + // when + List articleDtoList = articleService.findArticleByTitle("제목"); + + // then + for (ArticleDto articleDto : articleDtoList) { + assertTrue(articleDto.getTitle().contains("제목")); + } + + + } + + @Test + @DisplayName("게시글 수정") + void updateArticle() { + // given + ArticleCreateDto articleCreateDto = ArticleCreateDto.builder() + .title("제목") + .content("내용") + .build(); + ArticleDto SavedArticle = articleService.createArticle(articleCreateDto, user.getUsername()); + + ArticleUpdateDto articleUpdateDto = ArticleUpdateDto.builder() + .title("수정된 제목") + .content("수정된 내용") + .build(); + + // when + ArticleDto updatedArticle = articleService.updateArticle(SavedArticle.getIdx(), articleUpdateDto, user.getUsername()); + + // then + assertEquals(articleUpdateDto.getTitle(), updatedArticle.getTitle()); + assertEquals(articleUpdateDto.getContent(), updatedArticle.getContent()); + assertEquals(user.getUsername(), updatedArticle.getUsername()); + assertEquals(SavedArticle.getIdx(), updatedArticle.getIdx()); + } + + @Test + @DisplayName("게시글 수정 - 권한 없음") + void updateArticleNoPermission() { + // given + ArticleCreateDto articleCreateDto = ArticleCreateDto.builder() + .title("제목") + .content("내용") + .build(); + ArticleDto SavedArticle = articleService.createArticle(articleCreateDto, user.getUsername()); + + ArticleUpdateDto articleUpdateDto = ArticleUpdateDto.builder() + .title("수정된 제목") + .content("수정된 내용") + .build(); + + // when + User anotherUser = User.builder() + .username("anotherUser") + .email("anotherUser@test.com") + .password("anotherUser") + .build(); + + assertThrows(NoSuchElementException.class, () -> articleService.updateArticle(SavedArticle.getIdx(), articleUpdateDto, anotherUser.getUsername())); + } + + @Test + @DisplayName("작성자가 게시글을 삭제") + void deleteArticle() { + // given + ArticleCreateDto articleCreateDto = ArticleCreateDto.builder() + .title("제목") + .content("내용") + .build(); + ArticleDto SavedArticle = articleService.createArticle(articleCreateDto, user.getUsername()); + + // when + articleService.deleteArticle(SavedArticle.getIdx(), user.getUsername()); + + // then + assertThrows(IllegalArgumentException.class, () -> articleService.getArticleById(SavedArticle.getIdx())); + } + + @Test + @DisplayName("관리자가 게시글을 삭제") + void deleteArticleByAdmin() { + // given + ArticleCreateDto articleCreateDto = ArticleCreateDto.builder() + .title("제목") + .content("내용") + .build(); + ArticleDto SavedArticle = articleService.createArticle(articleCreateDto, user.getUsername()); + + // when + + articleService.deleteArticle(SavedArticle.getIdx(), admin.getUsername()); + + // then + assertThrows(IllegalArgumentException.class, () -> articleService.getArticleById(SavedArticle.getIdx())); + } + +} diff --git a/src/test/java/com/gdsc/blog/comment/service/CommentServiceTest.java b/src/test/java/com/gdsc/blog/comment/service/CommentServiceTest.java new file mode 100644 index 0000000..c1c609c --- /dev/null +++ b/src/test/java/com/gdsc/blog/comment/service/CommentServiceTest.java @@ -0,0 +1,163 @@ +package com.gdsc.blog.comment.service; + +import com.gdsc.blog.article.dto.ArticleCreateDto; +import com.gdsc.blog.article.dto.ArticleDto; +import com.gdsc.blog.article.service.ArticleService; +import com.gdsc.blog.comment.dto.CommentUpdateDto; +import com.gdsc.blog.comment.entity.Comment; +import com.gdsc.blog.user.entity.User; +import com.gdsc.blog.user.entity.UserRole; +import com.gdsc.blog.user.service.UserService; +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Slf4j +@SpringBootTest +@Profile("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CommentServiceTest { + + @Autowired + private UserService userService; + + @Autowired + private ArticleService articleService; + + @Autowired + private CommentService commentService; + + User user; + User admin; + ArticleDto articleDto; + + @BeforeAll + void setUp() { + // 회원 가입 + user = userService.signup(User.builder() + .username("user1") + .email("user1@test.com") + .password("password") + .build()); + admin = userService.signup(User.builder() + .username("admin1") + .email("admin1@admin.com") + .password("password") + .roles(Collections.singletonList(UserRole.ROLE_ADMIN)) + .build()); + userService.becomeAdmin(admin.getUsername()); + + // 게시글 생성 + articleDto = articleService.createArticle(ArticleCreateDto.builder() + .title("title") + .content("content") + .build(), user.getUsername()); + + } + + @Test + @Transactional + @DisplayName("댓글 생성") + void create() { + Comment comment = Comment.builder() + .content("content") + .build(); + Comment saved = commentService.create(comment, articleDto.getIdx(), user); + assertEquals(comment.getContent(), saved.getContent()); + } + + @Test + @Transactional + @DisplayName("게시글 별 댓글 조회") + void getAllCommandByArticle() { + // given + Comment comment1 = Comment.builder() + .content("content1") + .build(); + Comment comment2 = Comment.builder() + .content("content2") + .build(); + Comment comment3 = Comment.builder() + .content("content3") + .build(); + commentService.create(comment1, articleDto.getIdx(), user); + commentService.create(comment2, articleDto.getIdx(), user); + commentService.create(comment3, articleDto.getIdx(), user); + + // when + List comments = commentService.getAllCommandByArticle(articleDto.getIdx()); + + // then + assertEquals(3, comments.size()); + } + + @Test + @Transactional + @DisplayName("특정 댓글 조회") + void getCommentById() { + // given + Comment comment = Comment.builder() + .content("content") + .build(); + Comment saved = commentService.create(comment, articleDto.getIdx(), user); + + // when + Comment found = commentService.getCommentById(saved.getIdx()); + + // then + assertEquals(saved.getIdx(), found.getIdx()); + assertEquals(saved.getContent(), found.getContent()); + } + + @Test + @Transactional + @DisplayName("댓글 수정") + void updateComment() { + // given + Comment comment = Comment.builder() + .content("content") + .build(); + Comment saved = commentService.create(comment, articleDto.getIdx(), user); + + CommentUpdateDto commentUpdateDto = CommentUpdateDto.builder() + .content("updated") + .build(); + + // when + Comment updated = commentService.updateComment(saved.getIdx(), commentUpdateDto); + + // then + assertEquals(saved.getIdx(), updated.getIdx()); + assertEquals("updated", updated.getContent()); + } + + @Test + @DisplayName("댓글 삭제") + @Transactional + void delete() { + // given + Comment comment = Comment.builder() + .content("content") + .build(); + Comment saved = commentService.create(comment, articleDto.getIdx(), user); + + // when + commentService.delete(saved); + + // then + assertThrows(NoSuchElementException.class, () -> commentService.getCommentById(saved.getIdx())); + } +} diff --git a/src/test/java/com/gdsc/blog/mapper/ArticleMapperTest.java b/src/test/java/com/gdsc/blog/mapper/ArticleMapperTest.java new file mode 100644 index 0000000..b44558c --- /dev/null +++ b/src/test/java/com/gdsc/blog/mapper/ArticleMapperTest.java @@ -0,0 +1,50 @@ +package com.gdsc.blog.mapper; + +import com.gdsc.blog.article.dto.ArticleDto; +import com.gdsc.blog.article.entity.Article; +import com.gdsc.blog.user.entity.User; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +import java.sql.Timestamp; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +class ArticleMapperTest { + + ArticleMapper articleMapper = Mappers.getMapper(ArticleMapper.class); + + @Test + void toDto() { + + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + LocalDateTime localDateTime = timestamp.toLocalDateTime(); + + log.info("timestamp: {}", timestamp); + log.info("localDateTime: {}", localDateTime); + + // given + Article article = Article.builder() + .idx(1L) + .title("제목") + .content("내용") + .createDate(timestamp) + .modifyDate(timestamp) + .user(User.builder().username("user").build()) + .build(); + // when + ArticleDto articleDto = articleMapper.toDto(article); + + // then + assertEquals(article.getIdx(), articleDto.getIdx()); + assertEquals(article.getTitle(), articleDto.getTitle()); + assertEquals(article.getContent(), articleDto.getContent()); + assertEquals(article.getUser().getUsername(), articleDto.getUsername()); + assertEquals(localDateTime, articleDto.getCreatedAt()); + assertEquals(localDateTime, articleDto.getModifiedAt()); + assertEquals(article.getUser().getUsername(), articleDto.getUsername()); + } +}