diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..66c495c Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index c2065bc..4c75e42 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +.env \ No newline at end of file diff --git a/README.md b/README.md index 395edc5..d8ca717 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # backend-study-sns +강지원 diff --git a/build.gradle b/build.gradle index 610d6a6..0527f19 100644 --- a/build.gradle +++ b/build.gradle @@ -19,11 +19,34 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'com.mysql:mysql-connector-j:9.0.0' + implementation 'me.paulschwarz:spring-dotenv:4.0.0' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + // test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + +} +test{ + useJUnitPlatform() } +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} tasks.named('test') { useJUnitPlatform() } diff --git a/src/main/java/com/example/devSns/controller/CommentController.java b/src/main/java/com/example/devSns/controller/CommentController.java new file mode 100644 index 0000000..71ed3ae --- /dev/null +++ b/src/main/java/com/example/devSns/controller/CommentController.java @@ -0,0 +1,43 @@ +package com.example.devSns.controller; + +import com.example.devSns.entity.Comment; +import com.example.devSns.service.CommentService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/post/{postId}/comment") +public class CommentController { + private CommentService commentService; + + public CommentController(CommentService commentService) { + this.commentService = commentService; + } + + @GetMapping + public List getComments(@PathVariable Long postId) { + return commentService.getCommentByPost(postId); + } + + @PostMapping + public Comment createComment(@PathVariable Long postId, @RequestBody Comment comment) { + return commentService.addComment(postId, comment); + } + + @DeleteMapping("/{commentId}") + public void deleteComment(@PathVariable Long commentId) { + commentService.deleteComment(commentId); + } + + @PatchMapping("/{commentId}") + public ResponseEntity updateComment( + @PathVariable Long commentId, + @RequestBody Map request + ) { + String newContent = request.get("content"); + Comment updated = commentService.updateComment(commentId, newContent); + return ResponseEntity.ok(updated); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/controller/PostController.java b/src/main/java/com/example/devSns/controller/PostController.java new file mode 100644 index 0000000..d279035 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -0,0 +1,47 @@ +package com.example.devSns.controller; + +import com.example.devSns.entity.Post; +import com.example.devSns.service.PostService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/post") +public class PostController { + private final PostService postService; + + public PostController(PostService postService) { + this.postService = postService; + } + + @GetMapping + public List getAllPosts(){ + return postService.findAll(); + } + + @GetMapping("/{id}") + public ResponseEntity getPostById(@PathVariable Long id){ + Post post = postService.findById(id); + return ResponseEntity.ok(post); + } + + @PostMapping + public Post createPost(@RequestBody Post post){ + return postService.save(post); + } + + @PutMapping("/{id}") + public Post updatePost(@PathVariable Long id, @RequestBody Post updatedPost){ + + return postService.updatePost(id,updatedPost); + } + + @DeleteMapping("/{id}") + public void deletePost(@PathVariable Long id){ + postService.delete(id); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/entity/Comment.java b/src/main/java/com/example/devSns/entity/Comment.java new file mode 100644 index 0000000..5ead634 --- /dev/null +++ b/src/main/java/com/example/devSns/entity/Comment.java @@ -0,0 +1,41 @@ +package com.example.devSns.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Comment{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + private String username; + private LocalDateTime createdAt; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn( + name="post_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_diary_user_id_ref_user_id") + ) + @JsonBackReference + private Post post; + + @PrePersist + public void onCreate(){ + createdAt = LocalDateTime.now(); + } + public void update(String content){ + this.content = content; + } + public void assignTo(Post post){ + this.post = post; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/entity/Post.java b/src/main/java/com/example/devSns/entity/Post.java new file mode 100644 index 0000000..645b299 --- /dev/null +++ b/src/main/java/com/example/devSns/entity/Post.java @@ -0,0 +1,48 @@ +package com.example.devSns.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Post { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + private int likes; + private String username; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL,orphanRemoval = true) + @JsonManagedReference + private List comments = new ArrayList<>(); + + @PrePersist + public void onCreate(){ + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void onUpdate(){ + updatedAt = LocalDateTime.now(); + } + public void update(String content){ + this.content = content; + } + public void addComment(Comment comment){ + comments.add(comment); + comment.assignTo(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/repository/CommentRepository.java b/src/main/java/com/example/devSns/repository/CommentRepository.java new file mode 100644 index 0000000..c293959 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/CommentRepository.java @@ -0,0 +1,9 @@ +package com.example.devSns.repository; + +import com.example.devSns.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findByPost_Id(Long postId); +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/repository/PostRepository.java b/src/main/java/com/example/devSns/repository/PostRepository.java new file mode 100644 index 0000000..4fd8426 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/PostRepository.java @@ -0,0 +1,6 @@ +package com.example.devSns.repository; + +import com.example.devSns.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository {} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/service/CommentService.java b/src/main/java/com/example/devSns/service/CommentService.java new file mode 100644 index 0000000..4414e49 --- /dev/null +++ b/src/main/java/com/example/devSns/service/CommentService.java @@ -0,0 +1,43 @@ +package com.example.devSns.service; + +import com.example.devSns.entity.Comment; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.CommentRepository; +import com.example.devSns.repository.PostRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class CommentService { + private final CommentRepository commentRepository; + private final PostRepository postRepository; + + public CommentService(CommentRepository commentRepository, PostRepository postRepository) { + this.commentRepository = commentRepository; + this.postRepository = postRepository; + } + + @Transactional(readOnly = true) + public List getCommentByPost(Long postId){ + return commentRepository.findByPost_Id(postId); + } + + public Comment addComment(Long postId, Comment comment){ + Post post = postRepository.findById(postId).orElseThrow(()-> new IllegalArgumentException("post not found")); + + post.addComment(comment); + postRepository.save(post); + return commentRepository.save(comment); + } + public Comment updateComment(Long commentId, String newContent){ + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new IllegalArgumentException("comment not found")); + comment.update(newContent); + return commentRepository.save(comment); + } + + public void deleteComment(Long id){ + commentRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/service/PostService.java b/src/main/java/com/example/devSns/service/PostService.java new file mode 100644 index 0000000..ddedba3 --- /dev/null +++ b/src/main/java/com/example/devSns/service/PostService.java @@ -0,0 +1,39 @@ +package com.example.devSns.service; + +import com.example.devSns.entity.Post; +import com.example.devSns.repository.PostRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +public class PostService { + private final PostRepository postRepository; + + public PostService(PostRepository postRepository) { + this.postRepository = postRepository; + } + + public List findAll(){ + return postRepository.findAll(); + } + + public Post findById(Long id){ + return postRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("Post not found")); + } + + public Post save(Post post){ + return postRepository.save(post); + } + public Post updatePost(Long id, Post updatedPost) { + Post existingPost = postRepository.findById(id).orElseThrow(() -> new RuntimeException("Post not found")); + existingPost.update(updatedPost.getContent()); + return postRepository.save(existingPost); + } + + public void delete(Long id){ + postRepository.deleteById(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3f10af..f3fabed 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,13 @@ spring.application.name=devSns + +# DB ???? +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} +# DB ???? +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +spring.jpa.hibernate.ddl-auto=update +# true? ???? ?? ???? sql?? ???? ??? ? ????. +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect \ No newline at end of file diff --git a/src/test/java/com/example/devSns/CommentControllerTest.java b/src/test/java/com/example/devSns/CommentControllerTest.java new file mode 100644 index 0000000..b648b36 --- /dev/null +++ b/src/test/java/com/example/devSns/CommentControllerTest.java @@ -0,0 +1,107 @@ +package com.example.devSns; + +import com.example.devSns.entity.Comment; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.CommentRepository; +import com.example.devSns.repository.PostRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class CommentControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private PostRepository postRepository; + + @Autowired + private CommentRepository commentRepository; + + private Post savedPost; + + @BeforeEach + void setUp() { + commentRepository.deleteAll(); + postRepository.deleteAll(); + + savedPost = postRepository.save(Post.builder() + .username("dardar") + .content("댓글 테스트용 게시글") + .build()); + } + + @Test + void 댓글_작성_성공() throws Exception { + String json = """ + { + "username": "tester", + "content": "좋은 글이네요!" + } + """; + + mockMvc.perform(post("/post/" + savedPost.getId() + "/comment") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("tester")) + .andExpect(jsonPath("$.content").value("좋은 글이네요!")); + } + + @Test + void 댓글_조회_성공() throws Exception { + Comment comment = commentRepository.save(Comment.builder() + .username("tester") + .content("조회용 댓글") + .post(savedPost) + .build()); + + mockMvc.perform(get("/post/" + savedPost.getId() + "/comment")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].content").value("조회용 댓글")) + .andExpect(jsonPath("$[0].username").value("tester")); + } + + @Test + void 댓글_수정_성공() throws Exception { + Comment comment = commentRepository.save(Comment.builder() + .username("tester") + .content("원본 댓글") + .post(savedPost) + .build()); + + String updateJson = """ + { + "content": "수정된 댓글입니다." + } + """; + + mockMvc.perform(patch("/post/" + savedPost.getId() + "/comment/" + comment.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(updateJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("수정된 댓글입니다.")); + } + + @Test + void 댓글_삭제_성공() throws Exception { + Comment comment = commentRepository.save(Comment.builder() + .username("tester") + .content("삭제용 댓글") + .post(savedPost) + .build()); + + mockMvc.perform(delete("/post/" + savedPost.getId() + "/comment/" + comment.getId())) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/example/devSns/PostControllerTest.java b/src/test/java/com/example/devSns/PostControllerTest.java new file mode 100644 index 0000000..ffe8f6a --- /dev/null +++ b/src/test/java/com/example/devSns/PostControllerTest.java @@ -0,0 +1,92 @@ +package com.example.devSns; + +import com.example.devSns.entity.Post; +import com.example.devSns.repository.PostRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class PostControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private PostRepository postRepository; + + @BeforeEach + void cleanDB() { + postRepository.deleteAll(); + } + + @Test + void 게시글_생성_성공() throws Exception { + String json = """ + { + "username": "dardar", + "content": "테스트 게시글입니다." + } + """; + + mockMvc.perform(post("/post") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("dardar")) + .andExpect(jsonPath("$.content").value("테스트 게시글입니다.")); + } + + @Test + void 게시글_조회_성공() throws Exception { + Post post = Post.builder() + .username("tester") + .content("조회용 게시글") + .build(); + postRepository.save(post); + + mockMvc.perform(get("/post/" + post.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("tester")) + .andExpect(jsonPath("$.content").value("조회용 게시글")); + } + + @Test + void 게시글_수정_성공() throws Exception { + Post post = postRepository.save(Post.builder() + .username("tester") + .content("원본 내용") + .build()); + + String updateJson = """ + { + "content": "수정된 내용" + } + """; + + mockMvc.perform(put("/post/" + post.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(updateJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("수정된 내용")); + } + + @Test + void 게시글_삭제_성공() throws Exception { + Post post = postRepository.save(Post.builder() + .username("tester") + .content("삭제 대상 게시글") + .build()); + + mockMvc.perform(delete("/post/" + post.getId())) + .andExpect(status().isOk()); + } +}