diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d4e4f4d 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..6612985 100644 --- a/build.gradle +++ b/build.gradle @@ -19,11 +19,46 @@ 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' + + //BCrypt + implementation 'org.springframework.boot:spring-boot-starter-security' + + //jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //validation + implementation 'javax.validation:validation-api:2.0.1.Final' + implementation 'org.hibernate:hibernate-validator:6.0.13.Final' + implementation 'org.glassfish:javax.el:3.0.0' +} +test{ + useJUnitPlatform() } +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} tasks.named('test') { useJUnitPlatform() } diff --git a/src/main/java/com/example/devSns/config/WebConfig.java b/src/main/java/com/example/devSns/config/WebConfig.java new file mode 100644 index 0000000..79ccdbd --- /dev/null +++ b/src/main/java/com/example/devSns/config/WebConfig.java @@ -0,0 +1,22 @@ +package com.example.devSns.config; + +import com.example.devSns.interceptor.JwtInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Autowired + private JwtInterceptor JwtInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry Registry) { + Registry.addInterceptor(JwtInterceptor) + .addPathPatterns("/api/**") // 인증이 필요한 경로 + .excludePathPatterns("/","/api/auth/signup", + "/api/auth/login", + "/error"); + } +} diff --git a/src/main/java/com/example/devSns/config/WebSecurityConfig.java b/src/main/java/com/example/devSns/config/WebSecurityConfig.java new file mode 100644 index 0000000..34643b2 --- /dev/null +++ b/src/main/java/com/example/devSns/config/WebSecurityConfig.java @@ -0,0 +1,36 @@ +package com.example.devSns.config; + +import com.example.devSns.interceptor.JwtInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +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.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/signup", "/api/auth/login").permitAll() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/devSns/controller/AuthController.java b/src/main/java/com/example/devSns/controller/AuthController.java new file mode 100644 index 0000000..e878115 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/AuthController.java @@ -0,0 +1,40 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.LoginRequest; +import com.example.devSns.dto.LoginResponse; +import com.example.devSns.dto.SignUpRequest; +import com.example.devSns.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired + private MemberService MemberService; + + // 회원 가입 + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody SignUpRequest signUpRequest) { + // 이메일 중복 체크 + if (MemberService.isEmailExists(signUpRequest.getEmail())) { + return ResponseEntity.status(400).body("Email already exists"); + } + + // 회원가입 처리 + MemberService.register(signUpRequest); + return ResponseEntity.status(201).body("Sign up successful"); + } + + // 로그인(JWT 발급) + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + String token = MemberService.login(loginRequest); + if (token == null) { + return ResponseEntity.status(401).body(new LoginResponse("Login failed")); // 로그인 실패 시 + } + return ResponseEntity.ok(new LoginResponse(token)); + } +} 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..dcde566 --- /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..16095a5 --- /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..e0056f6 --- /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..4f4f13e --- /dev/null +++ b/src/main/java/com/example/devSns/controller/MemberController.java @@ -0,0 +1,48 @@ +package com.example.devSns.controller; + +import com.example.devSns.dto.*; +import com.example.devSns.entity.Member; +import com.example.devSns.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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); + } +} 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..bb96bf8 --- /dev/null +++ b/src/main/java/com/example/devSns/controller/PostController.java @@ -0,0 +1,50 @@ +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 org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/posts") +public class PostController { + private final PostService PostService; + + public PostController(PostService postService) { + this.PostService = postService; + } + + @GetMapping + public Page getPosts(@RequestParam int page, @RequestParam int size){ + return PostService.FindAll(page, size); + } + + @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/controller/UserController.java b/src/main/java/com/example/devSns/controller/UserController.java new file mode 100644 index 0000000..4e9bc8a --- /dev/null +++ b/src/main/java/com/example/devSns/controller/UserController.java @@ -0,0 +1,14 @@ +package com.example.devSns.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UserController { + + @GetMapping("/api/user") + public String getUserInfo(@RequestAttribute("memberId") Long memberId, @RequestAttribute("email") String email) { + return "Member ID: " + memberId + ", Email: " + email; + } +} 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..dd06f1e --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentCreateRequest.java @@ -0,0 +1,21 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Getter +@NoArgsConstructor +public class CommentCreateRequest { + @NotEmpty(message= "Content cannot be empty") + @Size(max = 500, message="Content cannot exceed 500 characters") + private String content; + + @NotEmpty(message="Username cannot be empty") + private String username; + + @NotNull + 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..2ead883 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentResponse.java @@ -0,0 +1,30 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Comment; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +public record CommentResponse( + @NotNull(message="id cannot be null") + Long id, + + @NotEmpty(message= "Content cannot be empty") + String content, + + @NotEmpty(message= "Username cannot be empty") + String username, + + @NotNull + 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..95ee6b1 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/CommentUpdateRequest.java @@ -0,0 +1,13 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Getter +@NoArgsConstructor +public class CommentUpdateRequest { + @NotNull(message="Content cannot be null") + 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..f6a31e4 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/LikeResponse.java @@ -0,0 +1,15 @@ +package com.example.devSns.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.validation.constraints.NotNull; + +@Getter +@AllArgsConstructor +public class LikeResponse { + @NotNull + 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..c56e3e3 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/LikeToggleRequest.java @@ -0,0 +1,17 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Getter +@NoArgsConstructor +public class LikeToggleRequest { + + @NotNull + private Long memberId; + + @NotNull + private Long postId; +} diff --git a/src/main/java/com/example/devSns/dto/LoginRequest.java b/src/main/java/com/example/devSns/dto/LoginRequest.java new file mode 100644 index 0000000..7ffab39 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/LoginRequest.java @@ -0,0 +1,19 @@ +package com.example.devSns.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotEmpty; + +@Getter +public class LoginRequest { + + @NotEmpty(message="email cannot be empty") + private final String email; + @NotEmpty(message="password cannot be empty") + private final String password; + + public LoginRequest(String email, String password) { + this.email = email; + this.password = password; + } +} diff --git a/src/main/java/com/example/devSns/dto/LoginResponse.java b/src/main/java/com/example/devSns/dto/LoginResponse.java new file mode 100644 index 0000000..c3e6a9e --- /dev/null +++ b/src/main/java/com/example/devSns/dto/LoginResponse.java @@ -0,0 +1,12 @@ +package com.example.devSns.dto; + +import lombok.Getter; + +@Getter +public class LoginResponse { + private final String token; + + public LoginResponse(String token) { + this.token = token; + } +} 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..9cd1132 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/MemberJoinRequest.java @@ -0,0 +1,25 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Getter +@NoArgsConstructor +public class MemberJoinRequest { + + @NotEmpty(message="username cannot be empty") + private String username; + + @NotEmpty(message="email cannot be empty") + private String email; + + @NotEmpty(message="password cannot be empty") + 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..b728ab0 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/MemberResponse.java @@ -0,0 +1,25 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Member; +import lombok.Getter; + +import javax.validation.constraints.NotEmpty; + +@Getter +public class MemberResponse { + + @NotEmpty(message="id cannot be empty") + private Long id; + + @NotEmpty(message="username cannot be empty") + private String username; + + @NotEmpty(message="email cannot be empty") + 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..c156d85 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/PostCreateRequest.java @@ -0,0 +1,16 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Getter +@NoArgsConstructor +public class PostCreateRequest { + @NotEmpty(message="content cannot be empty") + private String content; + + @NotEmpty(message="memberId cannot be empty") + 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..5a7cc44 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/PostResponse.java @@ -0,0 +1,42 @@ +package com.example.devSns.dto; + +import com.example.devSns.entity.Post; +import lombok.Getter; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Getter +public class PostResponse { + @NotNull + private Long id; + + @NotNull + private Long memberId; + + @NotEmpty(message="content cannot be empty") + private String content; + + @NotEmpty(message="username cannot be empty") + private String username; + + @NotNull + private LocalDateTime createdAt; + @NotNull + 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..74f21ad --- /dev/null +++ b/src/main/java/com/example/devSns/dto/PostUpdateRequest.java @@ -0,0 +1,13 @@ +package com.example.devSns.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Getter +@NoArgsConstructor +public class PostUpdateRequest { + @NotEmpty(message="content cannot be empty") + private String content; +} diff --git a/src/main/java/com/example/devSns/dto/SignUpRequest.java b/src/main/java/com/example/devSns/dto/SignUpRequest.java new file mode 100644 index 0000000..4fd97d7 --- /dev/null +++ b/src/main/java/com/example/devSns/dto/SignUpRequest.java @@ -0,0 +1,23 @@ +package com.example.devSns.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotEmpty; + +@Getter +public class SignUpRequest { + @NotEmpty(message="username cannot be empty") + private final String username; + + @NotEmpty(message="email cannot be empty") + private final String email; + + @NotEmpty(message="password cannot be empty") + private final String password; + + public SignUpRequest(String username, String email, String password) { + this.username = username; + this.email = email; + this.password = password; + } +} 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..e2de1ae --- /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..c1a6f68 --- /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(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/EntityNotFoundException.java b/src/main/java/com/example/devSns/exception/EntityNotFoundException.java new file mode 100644 index 0000000..ffbe3ab --- /dev/null +++ b/src/main/java/com/example/devSns/exception/EntityNotFoundException.java @@ -0,0 +1,11 @@ +package com.example.devSns.exception; + +public class EntityNotFoundException extends RuntimeException { + public EntityNotFoundException(String entity, Long id) { + super(entity +" with id " + id + " not found"); + } + + public EntityNotFoundException(String message){ + super(message); + } +} 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/interceptor/JwtInterceptor.java b/src/main/java/com/example/devSns/interceptor/JwtInterceptor.java new file mode 100644 index 0000000..ea5f904 --- /dev/null +++ b/src/main/java/com/example/devSns/interceptor/JwtInterceptor.java @@ -0,0 +1,55 @@ +package com.example.devSns.interceptor; + +import com.example.devSns.jwt.JwtUtil; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class JwtInterceptor implements HandlerInterceptor { + + @Autowired + private JwtUtil jwtUtil; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + + String token = request.getHeader("Authorization"); + + if (token == null || !token.startsWith("Bearer ")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("{\"error\": \"Missing or invalid token\"}"); + return false; + } + + token = token.substring(7); + + try { + // 토큰 파싱 및 검증 + Claims claims = jwtUtil.parseClaims(token); + Long memberId = Long.valueOf(claims.getSubject()); + String email = claims.get("email", String.class); + + // 인증 정보를 request에 저장 + request.setAttribute("memberId", memberId); + request.setAttribute("email", email); + + // 파싱된 토큰 정보 로그 출력 + System.out.println("Token parsed successfully: memberId=" + memberId + ", email=" + email); + return true; + } catch (Exception e) { + // 토큰 파싱 실패 시 + System.out.println("Token parsing failed: " + e.getMessage()); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("{\"error\": \"Invalid token\"}"); + return false; + } + } + +} diff --git a/src/main/java/com/example/devSns/jwt/JwtUtil.java b/src/main/java/com/example/devSns/jwt/JwtUtil.java new file mode 100644 index 0000000..950adef --- /dev/null +++ b/src/main/java/com/example/devSns/jwt/JwtUtil.java @@ -0,0 +1,47 @@ +package com.example.devSns.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtil { + + // 적절한 비밀키 생성 (HS256에 적합한 256비트 비밀키) + private Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); // HS256에 적합한 비밀키 생성 + + // 토큰 생성 + public String generateToken(Long memberId, String email) { + return Jwts.builder() + .setSubject(String.valueOf(memberId)) // 사용자 ID + .claim("email", email) // 이메일을 추가 + .setIssuedAt(new Date()) // 발급 시간 + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 만료 시간 (1시간) + .signWith(secretKey) // 서명 (secretKey 사용) + .compact(); + } + + // 토큰 파싱 + public Claims parseClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) // 동일한 비밀키를 사용하여 파싱 + .build() + .parseClaimsJws(token) + .getBody(); + } + + // 토큰 유효성 검사 + public boolean validateToken(String token) { + try { + parseClaims(token); + return true; + } catch (Exception e) { + return false; + } + } +} 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..643e6dc --- /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 FindByPostId(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..42e3b5e --- /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..6310256 --- /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); + Optional FindByEmail(String email); +} 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..3a8bcc7 --- /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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +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.FindByPostId(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..36d8d45 --- /dev/null +++ b/src/main/java/com/example/devSns/service/LikeService.java @@ -0,0 +1,45 @@ +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.exception.EntityNotFoundException; +import com.example.devSns.repository.LikeRepository; +import com.example.devSns.repository.MemberRepository; +import com.example.devSns.repository.PostRepository; +import org.springframework.transaction.annotation.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 EntityNotFoundException("Member",memberId)); + Post post = PostRepository.findById(postId) + .orElseThrow(() -> new EntityNotFoundException("Post",postId)); + + 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..e55ccc7 --- /dev/null +++ b/src/main/java/com/example/devSns/service/MemberService.java @@ -0,0 +1,121 @@ +package com.example.devSns.service; + +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.dto.LoginRequest; +import com.example.devSns.dto.PostResponse; +import com.example.devSns.dto.SignUpRequest; +import com.example.devSns.entity.Like; +import com.example.devSns.entity.Member; +import com.example.devSns.jwt.JwtUtil; +import com.example.devSns.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +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; + private final BCryptPasswordEncoder PasswordEncoder; + private final JwtUtil JwtUtil; + + @Transactional + public Member join(SignUpRequest signUpRequest) { + validateDuplicateMember(signUpRequest.getUsername(), signUpRequest.getEmail()); // 이메일 중복 체크 + String encodedPassword = PasswordEncoder.encode(signUpRequest.getPassword()); + Member member = Member.create( + signUpRequest.getUsername(), + signUpRequest.getEmail(), + encodedPassword + ); + return MemberRepository.save(member); + } + + + private void validateDuplicateMember(String username, String email) { + boolean usernameExists = MemberRepository.FindByUsername(username).isPresent(); + boolean emailExists = MemberRepository.FindByEmail(email).isPresent(); + + if (usernameExists) { + throw new IllegalStateException("Already exists member with username " + username); + } + + if (emailExists) { + throw new IllegalStateException("Already exists member with email " + email); + } + } + 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(); + } + public boolean isEmailExists(String email) { + return MemberRepository.FindByEmail(email).isPresent(); + } + + @Transactional(readOnly = false) + public Member register(SignUpRequest signUpRequest) { + validateDuplicateMember(signUpRequest.getUsername(), signUpRequest.getEmail()); + String encodedPassword = PasswordEncoder.encode(signUpRequest.getPassword()); + Member member = Member.create(signUpRequest.getUsername(), signUpRequest.getEmail(), encodedPassword); + return MemberRepository.save(member); + } + @Transactional(readOnly = true) + public List getLikesByMember(Long memberId) { + Member member = MemberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("member not found")); + + return member.getLikes(); + } + + @Transactional(readOnly = true) + public String login(LoginRequest loginRequest) { + Member member = MemberRepository.FindByEmail(loginRequest.getEmail()) + .orElseThrow(()-> new RuntimeException("Invalid email or password")); + + boolean passwordMatch = PasswordEncoder.matches(loginRequest.getPassword(), member.getPassword()); + System.out.println("Password Match: " + passwordMatch); + if (!passwordMatch) { + throw new RuntimeException("Invalid email or password"); + } + + String token = JwtUtil.generateToken(member.getId(), member.getEmail()); + System.out.println("Generated Token: " + token); // 토큰 출력하여 확인 + return token; + } + + +} + 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..5a81ffb --- /dev/null +++ b/src/main/java/com/example/devSns/service/PostService.java @@ -0,0 +1,60 @@ +package com.example.devSns.service; + +import com.example.devSns.dto.PostCreateRequest; +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.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@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 Page FindAll(int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return PostRepository.findAll(pageable); + } + + 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..fb571c8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,19 @@ 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 + +spring.jpa.properties.hibernate.format_sql=true + +spring.datasource.hikari.read-only=false + +logging.level.org.springframework.security=DEBUG \ 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..2047c11 --- /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/LoginTest.java b/src/test/java/com/example/devSns/LoginTest.java new file mode 100644 index 0000000..9a28aa1 --- /dev/null +++ b/src/test/java/com/example/devSns/LoginTest.java @@ -0,0 +1,44 @@ +package com.example.devSns; + +import com.example.devSns.dto.LoginRequest; +import com.example.devSns.dto.SignUpRequest; +import com.example.devSns.service.MemberService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +public class LoginTest { + + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + @Autowired + private MemberService memberService; + + @Test + void testPasswordEncoderMatches() { + String rawPassword = "password123"; + String encodedPassword = passwordEncoder.encode(rawPassword); + + boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword); + assertTrue(isMatch); + } + + @Test + void testSignUpAndLogin() { + + SignUpRequest signUpRequest = new SignUpRequest("user2", "user2@example.com", "password123"); + memberService.join(signUpRequest); // 회원가입 호출 + + LoginRequest loginRequest = new LoginRequest("user2@example.com", "password123"); + + String token = memberService.login(loginRequest); + + assertNotNull(token); + } +} 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..711109a --- /dev/null +++ b/src/test/java/com/example/devSns/MemberServiceTest.java @@ -0,0 +1,140 @@ +package com.example.devSns; + +import com.example.devSns.dto.CommentResponse; +import com.example.devSns.dto.PostResponse; +import com.example.devSns.dto.SignUpRequest; +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 회원가입_성공() { + SignUpRequest request = new SignUpRequest("강지원", "test@test.com", "1234"); + + memberService.join(request); + + Member found = memberRepository.FindByUsername("강지원") + .orElseThrow(); + + assertThat(found.getEmail()).isEqualTo("test@test.com"); + } + + @Test + @DisplayName("중복 아이디 가입 예외") + void 중복회원_예외() { + SignUpRequest request1 = new SignUpRequest("강지원", "a@test.com", "1111"); + SignUpRequest request2 = new SignUpRequest("강지원", "b@test.com", "2222"); + + memberService.join(request1); + + assertThatThrownBy(() -> memberService.join(request2)) + .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()); + } +}