diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e21eb36 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/CommentActionController.java b/src/main/java/com/example/devSns/controller/CommentActionController.java new file mode 100644 index 0000000..dcf2f16 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/CommentActionController.java @@ -0,0 +1,31 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.dto.CommentUpdateRequest; +import com.example.devSns.entity.Comment; +import com.example.devSns.service.CommentService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/comments") +public class CommentActionController { + private final CommentService commentService; + + public CommentActionController(CommentService commentService){ + this.commentService = commentService; + } + @DeleteMapping("/{commentId}") + public void deleteComment(@PathVariable Long commentId) { + commentService.deleteComment(commentId); + } + + @PatchMapping("/{commentId}") + public ResponseEntity updateComment( + @PathVariable Long commentId, + @RequestBody CommentUpdateRequest request + ) { + Comment updated = commentService.updateComment(commentId, request.getContent()); + return ResponseEntity.ok(new CommentResponse(updated)); + } +} 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..0add7be --- /dev/null +++ b/src/main/java/com/example/devSns/controller/CommentController.java @@ -0,0 +1,36 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.CommentCreateRequest; +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.dto.CommentUpdateRequest; +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("/posts/{postId}/comments") +public class CommentController { + private final CommentService commentService; + + public CommentController(CommentService commentService) { + this.commentService = commentService; + } + + @GetMapping + public List getComments(@PathVariable Long postId) { + return commentService.getCommentByPost(postId).stream() + .map(CommentResponse::new) + .toList(); + } + + @PostMapping + public CommentResponse createComment(@PathVariable Long postId, @RequestBody CommentCreateRequest request) { + Comment created = commentService.addComment(postId, request); + return new CommentResponse(created); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/controller/LikeController.java b/src/main/java/com/example/devSns/controller/LikeController.java new file mode 100644 index 0000000..d12e813 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/LikeController.java @@ -0,0 +1,29 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.LikeResponse; +import com.example.devSns.dto.LikeToggleRequest; +import com.example.devSns.service.LikeService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/likes") +public class LikeController { + private final LikeService likeService; + + @PostMapping("/toggle") + public ResponseEntity toggleLike(@RequestBody LikeToggleRequest request){ + likeService.toggleLike(request.getMemberId(), request.getPostId()); + long count = likeService.getLikeCount(request.getPostId()); + return ResponseEntity.ok(new LikeResponse(request.getPostId(),count,true)); + } + + @GetMapping("/count/{postId}") + public ResponseEntity getLikeCount(@PathVariable Long postId){ + long count = likeService.getLikeCount(postId); + return ResponseEntity.ok(count); + } + +} diff --git a/src/main/java/com/example/devSns/controller/MemberController.java b/src/main/java/com/example/devSns/controller/MemberController.java new file mode 100644 index 0000000..d7be09f --- /dev/null +++ b/src/main/java/com/example/devSns/controller/MemberController.java @@ -0,0 +1,52 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.dto.MemberJoinRequest; +import com.example.devSns.dto.MemberResponse; +import com.example.devSns.dto.PostResponse; +import com.example.devSns.entity.Member; +import com.example.devSns.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/members") +public class MemberController { + private final MemberService memberService; + + @GetMapping("/{id}") + public MemberResponse getMember(@PathVariable Long id) { + Member member = memberService.findMemberById(id); + return new MemberResponse(member); + } + + @GetMapping("/search") + public List search(@RequestParam String keyword) { + return memberService.searchMembers(keyword).stream().map(MemberResponse::new).toList(); + } + + @GetMapping("/{id}/posts") + public List getMemberPosts(@PathVariable Long id) { + return memberService.getPostsByMember(id); + } + + @GetMapping("/{id}/comments") + public List getMemberComments(@PathVariable Long id) { + return memberService.getCommentsByMember(id); + } + + @GetMapping("/{id}/likes") + public List getMemberLikes(@PathVariable Long id) { + return memberService.getLikedPosts(id); + } + + @PostMapping + public MemberResponse createMember(@RequestBody MemberJoinRequest request){ + Member member = memberService.join(request.toEntity()); + return new MemberResponse(member); + } + +} 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..81eead8 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -0,0 +1,52 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.PostCreateRequest; +import com.example.devSns.dto.PostResponse; +import com.example.devSns.dto.PostUpdateRequest; +import com.example.devSns.entity.Post; +import com.example.devSns.service.PostService; +import jakarta.persistence.PostUpdate; +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().stream().map(PostResponse::new).toList(); + } + + @GetMapping("/{id}") + public ResponseEntity getPostById(@PathVariable Long id){ + Post post = postService.findById(id); + return ResponseEntity.ok(new PostResponse(post)); + } + + @PostMapping + public PostResponse createPost(@RequestBody PostCreateRequest request){ + Post created = postService.createPost(request); + return new PostResponse(created); + } + + @PatchMapping("/{id}") + public PostResponse updatePost(@PathVariable Long id, @RequestBody PostUpdateRequest request){ + Post updated = postService.updatePost(id, request); + return new PostResponse(updated); + } + + @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/dto/CommentCreateRequest.java b/src/main/java/com/example/devSns/dto/CommentCreateRequest.java new file mode 100644 index 0000000..085e7cc --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentCreateRequest.java @@ -0,0 +1,12 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CommentCreateRequest { + private String content; + private String username; + private Long memberId; +} diff --git a/src/main/java/com/example/devSns/dto/CommentResponse.java b/src/main/java/com/example/devSns/dto/CommentResponse.java new file mode 100644 index 0000000..c28db4a --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentResponse.java @@ -0,0 +1,21 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Comment; + +import java.time.LocalDateTime; + +public record CommentResponse( + Long id, + String content, + String username, + LocalDateTime createdAt +){ + public CommentResponse(Comment comment){ + this( + comment.getId(), + comment.getContent(), + comment.getUsername(), + comment.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/example/devSns/dto/CommentUpdateRequest.java b/src/main/java/com/example/devSns/dto/CommentUpdateRequest.java new file mode 100644 index 0000000..cf910c3 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentUpdateRequest.java @@ -0,0 +1,10 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CommentUpdateRequest { + private String content; +} diff --git a/src/main/java/com/example/devSns/dto/LikeResponse.java b/src/main/java/com/example/devSns/dto/LikeResponse.java new file mode 100644 index 0000000..00c62b4 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/LikeResponse.java @@ -0,0 +1,12 @@ +package com.example.devSns.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LikeResponse { + private Long PostId; + private long likeCount; + private boolean liked; +} diff --git a/src/main/java/com/example/devSns/dto/LikeToggleRequest.java b/src/main/java/com/example/devSns/dto/LikeToggleRequest.java new file mode 100644 index 0000000..234ca45 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/LikeToggleRequest.java @@ -0,0 +1,11 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class LikeToggleRequest { + private Long memberId; + private Long postId; +} diff --git a/src/main/java/com/example/devSns/dto/MemberJoinRequest.java b/src/main/java/com/example/devSns/dto/MemberJoinRequest.java new file mode 100644 index 0000000..83ff098 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/MemberJoinRequest.java @@ -0,0 +1,17 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class MemberJoinRequest { + private String username; + private String email; + private String password; + + public Member toEntity(){ + return Member.create(username, email, password); + } +} diff --git a/src/main/java/com/example/devSns/dto/MemberResponse.java b/src/main/java/com/example/devSns/dto/MemberResponse.java new file mode 100644 index 0000000..308147c --- /dev/null +++ b/src/main/java/com/example/devSns/dto/MemberResponse.java @@ -0,0 +1,17 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Member; +import lombok.Getter; + +@Getter +public class MemberResponse { + private Long id; + private String username; + private String email; + + public MemberResponse(Member member){ + this.id = member.getId(); + this.username = member.getUsername(); + this.email = member.getEmail(); + } +} diff --git a/src/main/java/com/example/devSns/dto/PostCreateRequest.java b/src/main/java/com/example/devSns/dto/PostCreateRequest.java new file mode 100644 index 0000000..be9ca00 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/PostCreateRequest.java @@ -0,0 +1,11 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PostCreateRequest { + private String content; + private Long memberId; +} diff --git a/src/main/java/com/example/devSns/dto/PostResponse.java b/src/main/java/com/example/devSns/dto/PostResponse.java new file mode 100644 index 0000000..5b6aec1 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/PostResponse.java @@ -0,0 +1,30 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Post; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class PostResponse { + + private Long id; + private Long memberId; + private String content; + private String username; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private int likeCount; + private int commentCount; + + public PostResponse(Post post) { + this.id = post.getId(); + this.memberId = post.getMember() != null ? post.getMember().getId() : null; + this.content = post.getContent(); + this.username = post.getUsername(); + this.createdAt = post.getCreatedAt(); + this.updatedAt = post.getUpdatedAt(); + this.likeCount = post.getLikes().size(); // 리스트 -> size() + this.commentCount = post.getComments().size(); + } +} diff --git a/src/main/java/com/example/devSns/dto/PostUpdateRequest.java b/src/main/java/com/example/devSns/dto/PostUpdateRequest.java new file mode 100644 index 0000000..e9bc144 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/PostUpdateRequest.java @@ -0,0 +1,10 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PostUpdateRequest { + private String content; +} 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..11f34d7 --- /dev/null +++ b/src/main/java/com/example/devSns/entity/Comment.java @@ -0,0 +1,55 @@ +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 + ) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @PrePersist + public void onCreate(){ + createdAt = LocalDateTime.now(); + } + public void update(String content){ + this.content = content; + } + public void assignTo(Post post){ + this.post = post; + } + public void assignMember(Member member){ + this.member = member; + member.addComment(this); + } + public static Comment create(String content, Member member, Post post) { + return Comment.builder() + .content(content) + .username(member.getUsername()) + .member(member) + .post(post) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/entity/Like.java b/src/main/java/com/example/devSns/entity/Like.java new file mode 100644 index 0000000..fa5a0dd --- /dev/null +++ b/src/main/java/com/example/devSns/entity/Like.java @@ -0,0 +1,55 @@ +package com.example.devSns.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Table( + name = "likes", + uniqueConstraints = @UniqueConstraint(columnNames ={"member_id","post_id"} ) +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Like { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id",nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id",nullable = false) + private Post post; + + private LocalDateTime CreatedAt; + + private Like(Member member, Post post){ + this.member = member; + this.post = post; + this.CreatedAt = LocalDateTime.now(); + } + + public static Like create(Member member, Post post){ + Like like = new Like(member, post); + member.addLike(like); + return like; + } + + public void assignMember(Member member) { + this.member = member; + member.addLike(this); + } + + public void assignPost(Post post) { + this.post = post; + post.addLike(this); + + } + +} diff --git a/src/main/java/com/example/devSns/entity/Member.java b/src/main/java/com/example/devSns/entity/Member.java new file mode 100644 index 0000000..37ed019 --- /dev/null +++ b/src/main/java/com/example/devSns/entity/Member.java @@ -0,0 +1,51 @@ +package com.example.devSns.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + private String email; + private String password; + + @OneToMany(mappedBy = "member") + private List posts = new ArrayList<>(); + + @OneToMany(mappedBy = "member") + private List comments = new ArrayList<>(); + + @OneToMany(mappedBy = "member") + private List likes = new ArrayList<>(); + + private Member (String username, String email, String password){ + this.username = username; + this.email = email; + this.password = password; + } + + public static Member create(String username, String email, String password){ + return new Member(username, email, password); + } + + public void addPost(Post post) { + posts.add(post); + } + public void addComment(Comment comment) { + comments.add(comment); + } + public void addLike(Like like) { + likes.add(like); + } +} 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..0be76de --- /dev/null +++ b/src/main/java/com/example/devSns/entity/Post.java @@ -0,0 +1,75 @@ +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 +public class Post { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + private String username; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private List likes = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL,orphanRemoval = true) + private List comments = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @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); + } + + private Post(String content, Member member){ + this.content = content; + this.member = member; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + + member.addPost(this); + } + + public static Post create(String content, Member member){ + return new Post(content,member); + } + + public void updateContent(String newContent){ + this.content = newContent; + this.updatedAt = LocalDateTime.now(); + } + + public void addLike(Like like) { + likes.add(like); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/exception/GlobalExceptionHandler.java b/src/main/java/com/example/devSns/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..134135b --- /dev/null +++ b/src/main/java/com/example/devSns/exception/GlobalExceptionHandler.java @@ -0,0 +1,16 @@ +package com.example.devSns.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(e.getMessage()); + } +} 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/LikeRepository.java b/src/main/java/com/example/devSns/repository/LikeRepository.java new file mode 100644 index 0000000..7b61102 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/LikeRepository.java @@ -0,0 +1,13 @@ +package com.example.devSns.repository; + +import com.example.devSns.entity.Like; +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface LikeRepository extends JpaRepository { + Optional findByMemberAndPost(Member member, Post post); + boolean existsByMemberAndPost(Member member, Post post); + long countByPost(Post post); +} diff --git a/src/main/java/com/example/devSns/repository/MemberRepository.java b/src/main/java/com/example/devSns/repository/MemberRepository.java new file mode 100644 index 0000000..430f7d2 --- /dev/null +++ b/src/main/java/com/example/devSns/repository/MemberRepository.java @@ -0,0 +1,13 @@ +package com.example.devSns.repository; + +import com.example.devSns.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByUsername(String username); + List findByUsernameContaining(String keyword); + +} 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..5ca0b84 --- /dev/null +++ b/src/main/java/com/example/devSns/service/CommentService.java @@ -0,0 +1,57 @@ +package com.example.devSns.service; + +import com.example.devSns.dto.CommentCreateRequest; +import com.example.devSns.entity.Comment; +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.CommentRepository; +import com.example.devSns.repository.MemberRepository; +import com.example.devSns.repository.PostRepository; +import org.springframework.beans.factory.annotation.Autowired; +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; + private final MemberRepository memberRepository; + + public CommentService(CommentRepository commentRepository, PostRepository postRepository, MemberRepository memberRepository) { + this.commentRepository = commentRepository; + this.postRepository = postRepository; + this.memberRepository = memberRepository; + } + + @Transactional(readOnly = true) + public List getCommentByPost(Long postId){ + return commentRepository.findByPost_Id(postId); + } + + @Transactional + public Comment addComment(Long postId, CommentCreateRequest request){ + Post post = postRepository.findById(postId).orElseThrow(()-> new IllegalArgumentException("post not found")); + Member member = memberRepository.findById(request.getMemberId()) + .orElseThrow(() -> new IllegalArgumentException("member not found")); + + Comment comment = Comment.builder() + .content(request.getContent()) + .username(request.getUsername()) + .post(post) + .build(); + post.addComment(comment); + comment.assignMember(member); + 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/LikeService.java b/src/main/java/com/example/devSns/service/LikeService.java new file mode 100644 index 0000000..f2fd498 --- /dev/null +++ b/src/main/java/com/example/devSns/service/LikeService.java @@ -0,0 +1,44 @@ +package com.example.devSns.service; + +import com.example.devSns.entity.Like; +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.LikeRepository; +import com.example.devSns.repository.MemberRepository; +import com.example.devSns.repository.PostRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class LikeService { + private final LikeRepository likeRepository; + private final PostRepository postRepository; + private final MemberRepository memberRepository; + + @Transactional + public void toggleLike(Long memberId, Long postId){ + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new RuntimeException("member not found")); + Post post = postRepository.findById(postId) + .orElseThrow(() -> new RuntimeException("post not found")); + + Optional existingLike = likeRepository.findByMemberAndPost(member, post); + + if(existingLike.isPresent()){ + likeRepository.delete(existingLike.get()); + } else { + Like like = Like.create(member, post); + likeRepository.save(like); + } + } + + public long getLikeCount(Long postId){ + Post post = postRepository.findById(postId) + .orElseThrow(() -> new RuntimeException("post not found")); + return likeRepository.countByPost(post); + } +} diff --git a/src/main/java/com/example/devSns/service/MemberService.java b/src/main/java/com/example/devSns/service/MemberService.java new file mode 100644 index 0000000..be4597b --- /dev/null +++ b/src/main/java/com/example/devSns/service/MemberService.java @@ -0,0 +1,78 @@ +package com.example.devSns.service; + +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.dto.PostResponse; +import com.example.devSns.entity.Comment; +import com.example.devSns.entity.Like; +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberService { + private final MemberRepository memberRepository; + + @Transactional + public Member join(Member member) { + validateDuplicateMember(member.getUsername()); + return memberRepository.save(member); + } + + private void validateDuplicateMember(String username) { + boolean exits = memberRepository.findByUsername(username).isPresent(); + if (exits) { + throw new IllegalStateException("Already exists member "+username); + } + } + public Member findMemberById(long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new RuntimeException("member not found")); + } + + public List searchMembers(String keyword){ + return memberRepository.findByUsernameContaining(keyword); + } + + public List getPostsByMember(Long memberId){ + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("member not found")); + + return member.getPosts().stream() + .map(PostResponse::new) + .toList(); + } + + public List getCommentsByMember(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("member not found")); + + return member.getComments().stream() + .map(CommentResponse::new) + .toList(); + } + public List getLikedPosts(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("member not found")); + + return member.getLikes().stream() + .map(like -> new PostResponse(like.getPost())) + .toList(); + } + @Transactional(readOnly = true) + public List getLikesByMember(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("member not found")); + + return member.getLikes(); + } + + +} + 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..1a4bb4f --- /dev/null +++ b/src/main/java/com/example/devSns/service/PostService.java @@ -0,0 +1,61 @@ +package com.example.devSns.service; + +import com.example.devSns.dto.PostCreateRequest; +import com.example.devSns.dto.PostResponse; +import com.example.devSns.dto.PostUpdateRequest; +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.MemberRepository; +import com.example.devSns.repository.PostRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +public class PostService { + private final PostRepository postRepository; + private final MemberRepository memberRepository; + + public PostService(PostRepository postRepository,MemberRepository memberRepository) { + this.postRepository = postRepository; + this.memberRepository = memberRepository; + } + + public List findAll(){ + return postRepository.findAll(); + } + + public Post findById(Long id){ + return postRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("Post not found")); + } + + @Transactional + public Post createPost(PostCreateRequest request){ + Member member = memberRepository.findById(request.getMemberId()) + .orElseThrow(() -> new IllegalArgumentException("member not found")); + + Post post = Post.create( + request.getContent(), + member // ✔ member 연결 + ); + + return postRepository.save(post); + } + + @Transactional + public Post updatePost(Long id, PostUpdateRequest request) { + Post existingPost = postRepository.findById(id) + .orElseThrow(()-> new IllegalArgumentException("post not found")); + existingPost.updateContent(request.getContent()); + return existingPost; + } + + @Transactional + 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..a17f7a5 --- /dev/null +++ b/src/test/java/com/example/devSns/CommentControllerTest.java @@ -0,0 +1,121 @@ +package com.example.devSns; + +import com.example.devSns.entity.Comment; +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.CommentRepository; +import com.example.devSns.repository.MemberRepository; +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.assertj.core.api.Assertions.assertThat; +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; + + @Autowired + private MemberRepository memberRepository; + + private Post savedPost; + private Member savedMember; + + @BeforeEach + void setUp() { + commentRepository.deleteAll(); + postRepository.deleteAll(); + memberRepository.deleteAll(); + + // 1) 멤버 생성 + savedMember = memberRepository.save( + Member.create("writer", "writer@test.com", "1234") + ); + + // 2) 게시글 생성 + savedPost = postRepository.save( + Post.create("댓글 테스트용 게시글", savedMember) + ); + } + + @Test + void 댓글_작성_성공() throws Exception { + + String json = """ + { + "memberId": %d, + "content": "좋은 글이네요!" + } + """.formatted(savedMember.getId()); + + mockMvc.perform(post("/posts/" + savedPost.getId() + "/comments") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("좋은 글이네요!")) + .andExpect(jsonPath("$.memberId").value(savedMember.getId())) + .andExpect(jsonPath("$.postId").value(savedPost.getId())); + } + + @Test + void 댓글_조회_성공() throws Exception { + + Comment comment = commentRepository.save( + Comment.create("조회용 댓글", savedMember, savedPost) + ); + + mockMvc.perform(get("/posts/" + savedPost.getId() + "/comments")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].content").value("조회용 댓글")) + .andExpect(jsonPath("$[0].memberId").value(savedMember.getId())); + } + + @Test + void 댓글_수정_성공() throws Exception { + + Comment comment = commentRepository.save( + Comment.create("원본 댓글", savedMember, savedPost) + ); + + String updateJson = """ + { + "content": "수정된 댓글입니다." + } + """; + + mockMvc.perform(patch("/comments/" + comment.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(updateJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("수정된 댓글입니다.")); + } + + @Test + void 댓글_삭제_성공() throws Exception { + + Comment comment = commentRepository.save( + Comment.create("삭제용 댓글", savedMember, savedPost) + ); + + mockMvc.perform(delete("/comments/" + comment.getId())) + .andExpect(status().isOk()); + + assertThat(commentRepository.existsById(comment.getId())).isFalse(); + } +} diff --git a/src/test/java/com/example/devSns/LikeServiceTest.java b/src/test/java/com/example/devSns/LikeServiceTest.java new file mode 100644 index 0000000..3ae596b --- /dev/null +++ b/src/test/java/com/example/devSns/LikeServiceTest.java @@ -0,0 +1,70 @@ +package com.example.devSns; + +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.LikeRepository; +import com.example.devSns.repository.MemberRepository; +import com.example.devSns.repository.PostRepository; +import com.example.devSns.service.LikeService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +public class LikeServiceTest { + @Autowired + private LikeService likeService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private PostRepository postRepository; + + @Autowired + private LikeRepository likeRepository; + + private Member member; + private Post post; + + @BeforeEach + public void setup() { + member = Member.create("user1","user1@test.com","1234"); + memberRepository.save(member); + + post = Post.create("테스트 게시글",member); + postRepository.save(post); + } + + @Test + @DisplayName("멤버 게시글에 좋아요 가능") + void memberCanLikePost(){ + likeService.toggleLike(member.getId(),post.getId()); + boolean exists = likeRepository.existsByMemberAndPost(member,post); + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("좋아요 두번 누르면 취소 가능") + void likeIsToggled(){ + likeService.toggleLike(member.getId(),post.getId()); + assertThat(likeRepository.countByPost(post)).isEqualTo(1); + + likeService.toggleLike(member.getId(),post.getId()); + assertThat(likeRepository.countByPost(post)).isEqualTo(0); + } + + @Test + @DisplayName("게시글의 좋아요 수 조회 가능") + void canCountLikes(){ + likeService.toggleLike(member.getId(),post.getId()); + long count = likeService.getLikeCount(post.getId()); + assertThat(count).isEqualTo(1); + } +} diff --git a/src/test/java/com/example/devSns/MemberServiceTest.java b/src/test/java/com/example/devSns/MemberServiceTest.java new file mode 100644 index 0000000..de12264 --- /dev/null +++ b/src/test/java/com/example/devSns/MemberServiceTest.java @@ -0,0 +1,139 @@ +package com.example.devSns; + +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.dto.PostResponse; +import com.example.devSns.entity.*; +import com.example.devSns.repository.CommentRepository; +import com.example.devSns.repository.MemberRepository; +import com.example.devSns.repository.PostRepository; +import com.example.devSns.repository.LikeRepository; +import com.example.devSns.service.MemberService; +import com.example.devSns.service.LikeService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@Transactional +class MemberServiceTest { + + @Autowired MemberService memberService; + @Autowired MemberRepository memberRepository; + + @Autowired PostRepository postRepository; + @Autowired CommentRepository commentRepository; + @Autowired LikeRepository likeRepository; + + @Autowired LikeService likeService; + + @Test + @DisplayName("회원 가입 성공") + void 회원가입_성공() { + Member member = Member.create("강지원", "test@test.com", "1234"); + + memberService.join(member); + + Member found = memberRepository.findByUsername("강지원") + .orElseThrow(); + + assertThat(found.getEmail()).isEqualTo("test@test.com"); + } + + @Test + @DisplayName("중복 아이디 가입 예외") + void 중복회원_예외() { + Member m1 = Member.create("강지원", "a@test.com", "1111"); + Member m2 = Member.create("강지원", "b@test.com", "2222"); + + memberService.join(m1); + + assertThatThrownBy(() -> memberService.join(m2)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Already exists member"); + } + + @Test + @DisplayName("이름으로 회원 검색 성공") + void 회원검색_성공() { + Member m1 = Member.create("강지원", "a@test.com", "1111"); + Member m2 = Member.create("먼지", "b@test.com", "2222"); + + memberRepository.saveAll(List.of(m1, m2)); + + List result = memberService.searchMembers("강"); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getUsername()).isEqualTo("강지원"); + } + + @Test + @DisplayName("회원이 작성한 게시글 조회") + void 회원게시글조회() { + + Member member = memberRepository.save(Member.create("강지원", "t@t.com", "1234")); + + Post p1 = postRepository.save(Post.create("post1", member)); + Post p2 = postRepository.save(Post.create("post2", member)); + + List posts = memberService.getPostsByMember(member.getId()); + + assertThat(posts).hasSize(2); + assertThat(posts.get(0).getContent()).isEqualTo("post1"); + assertThat(posts.get(1).getContent()).isEqualTo("post2"); + } + + @Test + @DisplayName("회원이 작성한 댓글 조회") + void 회원댓글조회() { + + Member member = memberRepository.save(Member.create("tester", "t@t.com", "1111")); + Post post = postRepository.save(Post.create("게시글", member)); + + Comment c1 = commentRepository.save(Comment.builder() + .content("댓글1") + .username("tester") + .post(post) + .member(member) + .build()); + c1.assignMember(member); + commentRepository.save(c1); + + Comment c2 = commentRepository.save(Comment.builder() + .content("댓글2") + .username("tester") + .post(post) + .member(member) + .build()); + c1.assignMember(member); + commentRepository.save(c2); + + List comments = memberService.getCommentsByMember(member.getId()); + + assertThat(comments).hasSize(2); + assertThat(comments.get(0).content()).isIn("댓글1", "댓글2"); + } + + @Test + @DisplayName("회원이 좋아요한 게시글 조회") + void 회원좋아요조회() { + + Member member = memberRepository.save(Member.create("user", "u@u.com", "1234")); + Post post1 = postRepository.save(Post.create("좋아요 게시글1", member)); + Post post2 = postRepository.save(Post.create("좋아요 게시글2", member)); + + likeRepository.save(Like.create(member, post1)); + likeRepository.save(Like.create(member, post2)); + + List likedPosts = memberService.getLikedPosts(member.getId()); + + assertThat(likedPosts).hasSize(2); + assertThat(likedPosts.get(0).getContent()).isIn("좋아요 게시글1", "좋아요 게시글2"); + } + +} 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..90e6b9b --- /dev/null +++ b/src/test/java/com/example/devSns/PostControllerTest.java @@ -0,0 +1,109 @@ +package com.example.devSns; + +import com.example.devSns.entity.Member; +import com.example.devSns.entity.Post; +import com.example.devSns.repository.MemberRepository; +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 MockMvc mockMvc; + @Autowired PostRepository postRepository; + @Autowired MemberRepository memberRepository; + + @BeforeEach + void cleanDB() { + postRepository.deleteAll(); + memberRepository.deleteAll(); + } + + @Test + void 게시글_생성_성공() throws Exception { + + Member member = memberRepository.save( + Member.create("강지원", "test@test.com", "1234") + ); + + String json = """ + { + "memberId": %d, + "content": "테스트 게시글입니다." + } + """.formatted(member.getId()); + + mockMvc.perform(post("/post") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("테스트 게시글입니다.")) + .andExpect(jsonPath("$.memberId").value(member.getId())); + } + + @Test + void 게시글_조회_성공() throws Exception { + + Member member = memberRepository.save( + Member.create("tester", "tester@test.com", "1234") + ); + + Post post = postRepository.save( + Post.create("조회용 게시글", member) + ); + + mockMvc.perform(get("/post/" + post.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("조회용 게시글")) + .andExpect(jsonPath("$.memberId").value(member.getId())); + } + + @Test + void 게시글_수정_성공() throws Exception { + + Member member = memberRepository.save( + Member.create("tester", "tester@test.com", "1234") + ); + + Post post = postRepository.save( + Post.create("원본 내용", member) + ); + + String updateJson = """ + { + "content": "수정된 내용" + } + """; + + mockMvc.perform(patch("/post/" + post.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(updateJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("수정된 내용")); + } + + @Test + void 게시글_삭제_성공() throws Exception { + + Member member = memberRepository.save( + Member.create("tester", "tester@test.com", "1234") + ); + + Post post = postRepository.save( + Post.create("삭제 대상 게시글", member) + ); + + mockMvc.perform(delete("/post/" + post.getId())) + .andExpect(status().isOk()); + } +}