diff --git a/README.md b/README.md index c7584e0..eb2c927 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ # UMC Spring A팀 레포지토리입니다. +Json의 브랜치입니다 😀 + +## ERD +![image](https://github.com/UMC-CAU-6th/Spring-A/assets/54016683/a2b739bc-4e01-45ae-b57f-5de76c454c2a) + diff --git a/umc/build.gradle b/umc/build.gradle index 73dda60..417c92e 100644 --- a/umc/build.gradle +++ b/umc/build.gradle @@ -26,21 +26,29 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'junit:junit:4.13.1' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'junit:junit:4.13.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' implementation 'org.springframework.boot:spring-boot-starter-validation' - runtimeOnly 'com.mysql:mysql-connector-j' - implementation group: 'org.hibernate', name: 'hibernate-spatial', version: '5.6.15.Final' + + //runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'com.h2database:h2' + //implementation group: 'org.hibernate', name: 'hibernate-spatial', version: '5.6.15.Final' // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' - implementation 'org.springdoc:springdoc-openapi-ui:1.6.15' - implementation 'io.springfox:springfox-swagger2:2.9.2' - implementation 'io.springfox:springfox-swagger-ui:2.9.2' + + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' +// implementation 'org.springdoc:springdoc-openapi-ui:1.6.15' +// implementation 'io.springfox:springfox-swagger2:2.9.2' +// implementation 'io.springfox:springfox-swagger-ui:2.9.2' +// implementation 'io.springfox:springfox-boot-starter:3.0.0' +// implementation 'io.springfox:springfox-swagger-ui:3.0.0' } diff --git a/umc/src/main/java/com/umc/common/config/SecurityConfig.java b/umc/src/main/java/com/umc/common/config/SecurityConfig.java index 47758ec..fad1d67 100644 --- a/umc/src/main/java/com/umc/common/config/SecurityConfig.java +++ b/umc/src/main/java/com/umc/common/config/SecurityConfig.java @@ -38,8 +38,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/upload").permitAll() .requestMatchers("/api/**").permitAll() .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/swagger-ui.html").permitAll() .requestMatchers("/v3/api-docs/**").permitAll() - .anyRequest().authenticated() + .anyRequest().permitAll() ) .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); diff --git a/umc/src/main/java/com/umc/common/config/SwaggerConfig.java b/umc/src/main/java/com/umc/common/config/SwaggerConfig.java index c2a158a..7720770 100644 --- a/umc/src/main/java/com/umc/common/config/SwaggerConfig.java +++ b/umc/src/main/java/com/umc/common/config/SwaggerConfig.java @@ -15,8 +15,8 @@ public class SwaggerConfig { @Bean public OpenAPI SchrodingerApi() { Info info = new Info() - .title("BuddyU API") - .description("BuddyU API 명세서") + .title("제목") + .description("명세서 설명") .version("1.0.0"); String jwtSchemeName = "JWT TOKEN"; diff --git a/umc/src/main/java/com/umc/common/entity/BaseTimeEntity.java b/umc/src/main/java/com/umc/common/entity/BaseTimeEntity.java index 15bb6ab..d650141 100644 --- a/umc/src/main/java/com/umc/common/entity/BaseTimeEntity.java +++ b/umc/src/main/java/com/umc/common/entity/BaseTimeEntity.java @@ -2,9 +2,9 @@ import java.time.LocalDateTime; -import javax.persistence.Column; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; diff --git a/umc/src/main/java/com/umc/common/response/status/ErrorCode.java b/umc/src/main/java/com/umc/common/response/status/ErrorCode.java index d307c38..aa6da48 100644 --- a/umc/src/main/java/com/umc/common/response/status/ErrorCode.java +++ b/umc/src/main/java/com/umc/common/response/status/ErrorCode.java @@ -24,6 +24,8 @@ public enum ErrorCode implements BaseErrorCode { MEMBER_SIGNUP_ERROR(HttpStatus.BAD_REQUEST, "SIGNUP4001", "회원가입 유효성 검사 실패"), EMAIL_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "SIGNUP4002", "이미 존재하는 이메일입니다."), + // 게시글 관련 에러 + LIKE_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "POST4001", "이미 좋아요를 눌렀습니다."), ; diff --git a/umc/src/main/java/com/umc/domain/board/controller/BoardController.java b/umc/src/main/java/com/umc/domain/board/controller/BoardController.java new file mode 100644 index 0000000..6ecb828 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/board/controller/BoardController.java @@ -0,0 +1,48 @@ +package com.umc.domain.board.controller; + +import com.umc.common.response.ApiResponse; +import com.umc.domain.board.dto.BoardRequestDTO; +import com.umc.domain.board.service.BoardService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/board") +@RestController +public class BoardController { + private final BoardService boardService; + + @CrossOrigin + @Operation(summary = "게시판 생성 API") + @PostMapping("/") + public ApiResponse createBoard(@Valid @RequestBody BoardRequestDTO boardRequestDTO){ + return boardService.createBoard(boardRequestDTO); + } + + @CrossOrigin + @Operation(summary = "게시판 게시글 조회 API") + @GetMapping("/{boardId}") + public ApiResponse readBoard(@PathVariable String boardId){ + return boardService.readBoardPosts(boardId); + } + + @CrossOrigin + @Operation(summary = "게시판 삭제 API") + @DeleteMapping("/{boardId}") + public ApiResponse deleteBoard(@PathVariable String boardId){ + return boardService.delete(boardId); + } + + @CrossOrigin + @Operation(summary = "게시판 수정 API") + @PutMapping("/{boardId}") + public ApiResponse updateBoard(@PathVariable String boardId, + @Valid @RequestBody BoardRequestDTO boardUpdateDTO){ + return boardService.update(boardId, boardUpdateDTO); + } +} diff --git a/umc/src/main/java/com/umc/domain/board/dto/BoardRequestDTO.java b/umc/src/main/java/com/umc/domain/board/dto/BoardRequestDTO.java new file mode 100644 index 0000000..b262a68 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/board/dto/BoardRequestDTO.java @@ -0,0 +1,12 @@ +package com.umc.domain.board.dto; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BoardRequestDTO { + @NotEmpty(message = "제목은 필수 입력값입니다.") + private String title; +} diff --git a/umc/src/main/java/com/umc/domain/board/entity/Board.java b/umc/src/main/java/com/umc/domain/board/entity/Board.java new file mode 100644 index 0000000..0a09e10 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/board/entity/Board.java @@ -0,0 +1,23 @@ +package com.umc.domain.board.entity; + +import com.umc.common.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Builder +@Setter +@NoArgsConstructor +@Entity +@Table(name= "board") +@AllArgsConstructor +public class Board extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private String title; + +} diff --git a/umc/src/main/java/com/umc/domain/board/repository/BoardRepository.java b/umc/src/main/java/com/umc/domain/board/repository/BoardRepository.java new file mode 100644 index 0000000..7e785ad --- /dev/null +++ b/umc/src/main/java/com/umc/domain/board/repository/BoardRepository.java @@ -0,0 +1,9 @@ +package com.umc.domain.board.repository; + +import com.umc.domain.board.entity.Board; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BoardRepository extends JpaRepository { +} diff --git a/umc/src/main/java/com/umc/domain/board/service/BoardService.java b/umc/src/main/java/com/umc/domain/board/service/BoardService.java new file mode 100644 index 0000000..d9f17fc --- /dev/null +++ b/umc/src/main/java/com/umc/domain/board/service/BoardService.java @@ -0,0 +1,63 @@ +package com.umc.domain.board.service; + +import com.umc.common.response.ApiResponse; +import com.umc.common.response.status.SuccessCode; +import com.umc.domain.board.dto.BoardRequestDTO; +import com.umc.domain.board.entity.Board; +import com.umc.domain.board.repository.BoardRepository; +import com.umc.domain.post.dto.SimplePostResponseDTO; +import com.umc.domain.post.entity.Post; +import com.umc.domain.post.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BoardService { + private final BoardRepository boardRepository; + private final PostRepository postRepository; + + public ApiResponse createBoard(BoardRequestDTO boardRequestDTO) { + Board board = Board.builder() + .title(boardRequestDTO.getTitle()) + .build(); + + boardRepository.save(board); + return ApiResponse.of(SuccessCode._OK, "저장되었습니다"); + } + + @Transactional + public ApiResponse readBoardPosts(String boardId) { + List posts = postRepository.findAllByBoardId(Long.getLong(boardId)); + + List postDTOs = posts.stream().map(post -> + SimplePostResponseDTO.builder() + .boardId(Long.getLong(boardId)) + .writerNickname(post.getWriter().getNickname()) + .title(post.getTitle()) + .createdAt(post.getCreatedAt()) + .modifiedAt(post.getModifiedAt()) + .build() + ).toList(); + + return ApiResponse.of(SuccessCode._OK, postDTOs); + } + + public ApiResponse delete(String boardId) { + Board target = boardRepository.findById(Long.getLong(boardId)).orElseThrow(); + boardRepository.delete(target); + + return ApiResponse.of(SuccessCode._OK, "삭제되었습니다"); + } + + public ApiResponse update(String boardId, BoardRequestDTO boardUpdateDTO) { + Board target = boardRepository.findById(Long.getLong(boardId)).orElseThrow(); + target.setTitle(boardUpdateDTO.getTitle()); + boardRepository.save(target); + + return ApiResponse.of(SuccessCode._OK, "수정되었습니다"); + } +} diff --git a/umc/src/main/java/com/umc/domain/comment/controller/CommentController.java b/umc/src/main/java/com/umc/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..45adc69 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/comment/controller/CommentController.java @@ -0,0 +1,52 @@ +package com.umc.domain.comment.controller; + +import com.umc.common.response.ApiResponse; +import com.umc.domain.comment.dto.CommentRequestDTO; +import com.umc.domain.comment.entity.Comment; +import com.umc.domain.comment.service.CommentService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class CommentController { + private final CommentService commentService; + + @CrossOrigin + @Operation(summary = "댓글 조회 API") + @GetMapping("/posts/{postId}/comments") + public ApiResponse> findComments(@PathVariable String postId){ + return commentService.findByPostId(postId); + } + + @CrossOrigin + @Operation(summary = "댓글 작성 API") + @PostMapping("/posts/{postId}/comments") + public ApiResponse writeComment(@PathVariable String postId, + @RequestBody CommentRequestDTO commentRequestDTO){ + return commentService.save(postId, commentRequestDTO); + } + + @CrossOrigin + @Operation(summary = "댓글 삭제 API") + @DeleteMapping("/posts/{postId}/comments/{commentId}") + public ApiResponse deleteComment(@PathVariable String postId, + @PathVariable String commentId){ + return commentService.delete(commentId); + } + + @CrossOrigin + @Operation(summary = "댓글 수정 API") + @PutMapping("/posts/{postId}/comments/{commentId}") + public ApiResponse updateComment(@PathVariable String postId, + @PathVariable String commentId, + @RequestBody CommentRequestDTO commentUpdateRequestDTO){ + return commentService.update(commentId, commentUpdateRequestDTO); + } +} diff --git a/umc/src/main/java/com/umc/domain/comment/dto/CommentRequestDTO.java b/umc/src/main/java/com/umc/domain/comment/dto/CommentRequestDTO.java new file mode 100644 index 0000000..3987b41 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/comment/dto/CommentRequestDTO.java @@ -0,0 +1,10 @@ +package com.umc.domain.comment.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CommentRequestDTO { + private String content; +} diff --git a/umc/src/main/java/com/umc/domain/comment/dto/CommentResponseDTO.java b/umc/src/main/java/com/umc/domain/comment/dto/CommentResponseDTO.java new file mode 100644 index 0000000..f2848bf --- /dev/null +++ b/umc/src/main/java/com/umc/domain/comment/dto/CommentResponseDTO.java @@ -0,0 +1,12 @@ +package com.umc.domain.comment.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CommentResponseDTO { + private Long id; + private String writerNickname; + private String content; +} diff --git a/umc/src/main/java/com/umc/domain/comment/entity/Comment.java b/umc/src/main/java/com/umc/domain/comment/entity/Comment.java new file mode 100644 index 0000000..6ad0498 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/comment/entity/Comment.java @@ -0,0 +1,36 @@ +package com.umc.domain.comment.entity; + +import com.umc.common.entity.BaseTimeEntity; +import com.umc.domain.post.entity.Post; +import com.umc.domain.user.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Builder +@Setter +@NoArgsConstructor +@Entity +@Table(name= "comment") +@AllArgsConstructor +public class Comment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "writer_id") + private Member writer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + private String content; + + public void setPost(Post post){ + this.post = post; + post.getComments().add(this); + } +} diff --git a/umc/src/main/java/com/umc/domain/comment/repository/CommentRepository.java b/umc/src/main/java/com/umc/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..239329b --- /dev/null +++ b/umc/src/main/java/com/umc/domain/comment/repository/CommentRepository.java @@ -0,0 +1,10 @@ +package com.umc.domain.comment.repository; + +import com.umc.domain.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findAllByPostId(Long postId); +} diff --git a/umc/src/main/java/com/umc/domain/comment/service/CommentService.java b/umc/src/main/java/com/umc/domain/comment/service/CommentService.java new file mode 100644 index 0000000..22c6b9d --- /dev/null +++ b/umc/src/main/java/com/umc/domain/comment/service/CommentService.java @@ -0,0 +1,58 @@ +package com.umc.domain.comment.service; + +import com.umc.common.jwt.SecurityUtil; +import com.umc.common.response.ApiResponse; +import com.umc.common.response.status.SuccessCode; +import com.umc.domain.comment.dto.CommentRequestDTO; +import com.umc.domain.comment.entity.Comment; +import com.umc.domain.comment.repository.CommentRepository; +import com.umc.domain.post.entity.Post; +import com.umc.domain.post.repository.PostRepository; +import com.umc.domain.user.entity.Member; +import com.umc.domain.user.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class CommentService { + private final PostRepository postRepository; + private final CommentRepository commentRepository; + private final MemberRepository memberRepository; + + public ApiResponse> findByPostId(String postId) { + List comments = commentRepository.findAllByPostId(Long.getLong(postId)); + return ApiResponse.of(SuccessCode._OK, comments); + } + + public ApiResponse save(String postId, CommentRequestDTO commentRequest) { + Member writer = memberRepository.findByEmail( + SecurityUtil.getCurrentUserEmail() + ).orElseThrow(); + Post post = postRepository.findById(Long.getLong(postId)).orElseThrow(); + Comment comment = Comment.builder() + .content(commentRequest.getContent()) + .build(); + + comment.setWriter(writer); + comment.setPost(post); + + commentRepository.save(comment); + return ApiResponse.of(SuccessCode._OK, "저장되었습니다"); + } + + public ApiResponse delete(String commentId) { + Comment target = commentRepository.findById(Long.getLong(commentId)).orElseThrow(); + commentRepository.delete(target); + return ApiResponse.of(SuccessCode._OK, "삭제되었습니다"); + } + + public ApiResponse update(String commentId, CommentRequestDTO commentRequestDTO) { + Comment target = commentRepository.findById(Long.getLong(commentId)).orElseThrow(); + target.setContent(commentRequestDTO.getContent()); + commentRepository.save(target); + return ApiResponse.of(SuccessCode._OK, "수정되었습니다"); + } +} diff --git a/umc/src/main/java/com/umc/domain/post/controller/PostController.java b/umc/src/main/java/com/umc/domain/post/controller/PostController.java new file mode 100644 index 0000000..beb32f7 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/controller/PostController.java @@ -0,0 +1,90 @@ +package com.umc.domain.post.controller; + +import com.umc.common.response.ApiResponse; +import com.umc.domain.post.dto.PostRequestDTO; +import com.umc.domain.post.dto.PostResponseDTO; +import com.umc.domain.post.dto.SimplePostResponseDTO; +import com.umc.domain.post.service.PostService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/posts") +@RestController +public class PostController { + + private final PostService postService; + + @CrossOrigin + @Operation(summary = "게시글 조회 API") + @GetMapping + public ApiResponse> findPosts() { + return postService.findAll(); + } + + @CrossOrigin + @Operation(summary = "특정 게시글 조회 API") + @GetMapping("/{postId}") + public ApiResponse findPost(@PathVariable String postId){ + return postService.find(postId); + } + + @CrossOrigin + @Operation(summary = "특정 게시글 삭제 API") + @DeleteMapping("/{postId}") + public ApiResponse deleteById(@PathVariable String postId){ + return postService.delete(postId); + } + + @CrossOrigin + @Operation(summary = "게시글 생성 API") + @PostMapping + public ApiResponse postPost(@RequestParam(name = "boardId") String boardId, + @ModelAttribute PostRequestDTO postRequestDto, + @RequestParam(value = "images", required = false) MultipartFile[] images){ + return postService.save(boardId, postRequestDto, images); + } + + @CrossOrigin + @Operation(summary = "게시글 수정 API") + @PutMapping("/{postId}") + public ApiResponse updatePost(@PathVariable String postId, + @ModelAttribute PostRequestDTO postRequestDto, + @RequestParam(value = "images", required = false) MultipartFile[] images){ + return postService.update(postId, postRequestDto, images); + } + + @CrossOrigin + @Operation(summary = "게시글 좋아요 API") + @PostMapping("/{postId}/likes") + public ApiResponse likePost(@PathVariable String postId){ + return postService.hitLike(postId); + } + + @CrossOrigin + @Operation(summary = "게시글 좋아요 개수 조회 API") + @GetMapping("/{postId}/likes") + public ApiResponse getLikes(@PathVariable String postId){ + return postService.getLikes(postId); + } + + @CrossOrigin + @Operation(summary = "사용자가 좋아요 누른 게시글 조회 API") + @GetMapping("/liked") + public ApiResponse> getLikedPosts(){ + return postService.getLikedPostsByUser(); + } + + @CrossOrigin + @Operation(summary = "게시글 좋아요 누름 여부 조회 API") + @GetMapping("/{postId}/liked") + public ApiResponse checkLike(@PathVariable String postId){ + return postService.checkLike(postId); + } +} diff --git a/umc/src/main/java/com/umc/domain/post/dto/PostImageDTO.java b/umc/src/main/java/com/umc/domain/post/dto/PostImageDTO.java new file mode 100644 index 0000000..a5e00b5 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/dto/PostImageDTO.java @@ -0,0 +1,11 @@ +package com.umc.domain.post.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class PostImageDTO { + private String originalFilename; + private String storedFilename; +} diff --git a/umc/src/main/java/com/umc/domain/post/dto/PostRequestDTO.java b/umc/src/main/java/com/umc/domain/post/dto/PostRequestDTO.java new file mode 100644 index 0000000..ce0eb5c --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/dto/PostRequestDTO.java @@ -0,0 +1,11 @@ +package com.umc.domain.post.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PostRequestDTO { + private String title; + private String content; +} diff --git a/umc/src/main/java/com/umc/domain/post/dto/PostResponseDTO.java b/umc/src/main/java/com/umc/domain/post/dto/PostResponseDTO.java new file mode 100644 index 0000000..8410dcd --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/dto/PostResponseDTO.java @@ -0,0 +1,22 @@ +package com.umc.domain.post.dto; + +import com.umc.domain.board.entity.Board; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +@Getter +public class PostResponseDTO { + private Long id; + private String title; + private Long boardId; + private String writerNickname; + private String content; + private List images; + + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; +} diff --git a/umc/src/main/java/com/umc/domain/post/dto/SimplePostResponseDTO.java b/umc/src/main/java/com/umc/domain/post/dto/SimplePostResponseDTO.java new file mode 100644 index 0000000..9409378 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/dto/SimplePostResponseDTO.java @@ -0,0 +1,18 @@ +package com.umc.domain.post.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class SimplePostResponseDTO { + private Long id; + private String title; + private Long boardId; + private String writerNickname; + + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; +} diff --git a/umc/src/main/java/com/umc/domain/post/entity/LikePost.java b/umc/src/main/java/com/umc/domain/post/entity/LikePost.java new file mode 100644 index 0000000..052b24c --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/entity/LikePost.java @@ -0,0 +1,28 @@ +package com.umc.domain.post.entity; + +import com.umc.common.entity.BaseTimeEntity; +import com.umc.domain.user.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Builder +@Setter +@NoArgsConstructor +@Entity +@Table(name= "post") +@AllArgsConstructor +public class LikePost extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; +} diff --git a/umc/src/main/java/com/umc/domain/post/entity/Post.java b/umc/src/main/java/com/umc/domain/post/entity/Post.java new file mode 100644 index 0000000..47070bd --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/entity/Post.java @@ -0,0 +1,44 @@ +package com.umc.domain.post.entity; + +import com.umc.common.entity.BaseTimeEntity; +import com.umc.domain.board.entity.Board; +import com.umc.domain.comment.entity.Comment; +import com.umc.domain.user.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; + +@Getter +@Builder +@Setter +@NoArgsConstructor +@Entity +@Table(name= "post") +@AllArgsConstructor +public class Post extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "writer_id") + private Member writer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id") + private Board board; + + @Column + private String title; + + @Column + private String content; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private List images; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private List comments; +} diff --git a/umc/src/main/java/com/umc/domain/post/entity/PostImage.java b/umc/src/main/java/com/umc/domain/post/entity/PostImage.java new file mode 100644 index 0000000..cf1992b --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/entity/PostImage.java @@ -0,0 +1,31 @@ +package com.umc.domain.post.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Getter +@Builder +@Setter +@NoArgsConstructor +@Entity +@Table(name= "post_image") +@AllArgsConstructor +public class PostImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + private String originalFilename; + + private String storedFilename; + + public void setPost(Post post){ + post.getImages().add(this); + this.post = post; + } +} diff --git a/umc/src/main/java/com/umc/domain/post/repository/LikePostRepository.java b/umc/src/main/java/com/umc/domain/post/repository/LikePostRepository.java new file mode 100644 index 0000000..ad3d1d1 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/repository/LikePostRepository.java @@ -0,0 +1,15 @@ +package com.umc.domain.post.repository; + +import com.umc.domain.post.entity.LikePost; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface LikePostRepository extends JpaRepository { + + boolean existsByPostIdAndMemberId(Long postId, Long memberId); + + int countByPostId(Long postId); +} diff --git a/umc/src/main/java/com/umc/domain/post/repository/PostImageRepository.java b/umc/src/main/java/com/umc/domain/post/repository/PostImageRepository.java new file mode 100644 index 0000000..201a3d6 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/repository/PostImageRepository.java @@ -0,0 +1,10 @@ +package com.umc.domain.post.repository; + +import com.umc.domain.post.entity.PostImage; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PostImageRepository extends JpaRepository { + List findAllByPostId(Long id); +} diff --git a/umc/src/main/java/com/umc/domain/post/repository/PostRepository.java b/umc/src/main/java/com/umc/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..1c02237 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/repository/PostRepository.java @@ -0,0 +1,17 @@ +package com.umc.domain.post.repository; + +import com.umc.domain.post.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface PostRepository extends JpaRepository { + + List findAllByBoardId(Long aLong); + + @Query("SELECT p FROM Post p JOIN LikePost lp ON p.id = lp.post.id WHERE lp.member.id = :memberId ORDER BY lp.createdAt DESC") + List findAllLikedPostsByMemberIdOrderByLikedTime(Long memberId); +} diff --git a/umc/src/main/java/com/umc/domain/post/service/PostImageService.java b/umc/src/main/java/com/umc/domain/post/service/PostImageService.java new file mode 100644 index 0000000..109e383 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/service/PostImageService.java @@ -0,0 +1,66 @@ +package com.umc.domain.post.service; + +import com.umc.domain.post.entity.Post; +import com.umc.domain.post.entity.PostImage; +import com.umc.domain.post.repository.PostImageRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class PostImageService { + private final PostImageRepository postImageRepository; + private static String savePath = ""; // TODO + + public static String createRandomFilename(String original){ + // 저장용 파일명 생성 (랜덤 문자열을 앞에 붙임) + return UUID.randomUUID().toString() + "_" + original; + } + + public List save(MultipartFile[] imageFiles, Post post){ + List postImages = new ArrayList<>(); + Arrays.stream(imageFiles).forEach(image -> { + // 1. PostImage 엔티티 생성 + postImages.add(PostImage.builder() + .originalFilename(image.getOriginalFilename()) + .storedFilename(PostImageService.createRandomFilename(image.getOriginalFilename())) + .post(post) + .build()); + // 2. 파일 저장 + try { + saveToDevice(image); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + // 3. 엔티티 저장 + postImageRepository.saveAll(postImages); + return postImages; + } + + public static void saveToDevice(MultipartFile image) throws IOException { + image.transferTo(new File(PostImageService.savePath)); + } + + public static void saveToDevice(MultipartFile image, String path) throws IOException { + image.transferTo(new File(path)); + } + + public List update(MultipartFile[] imageFiles, Post post) { + deleteImagesOnPost(post); + return save(imageFiles, post); + } + + public void deleteImagesOnPost(Post post){ + List postImages = postImageRepository.findAllByPostId(post.getId()); + postImageRepository.deleteAll(postImages); + } +} diff --git a/umc/src/main/java/com/umc/domain/post/service/PostService.java b/umc/src/main/java/com/umc/domain/post/service/PostService.java new file mode 100644 index 0000000..6159221 --- /dev/null +++ b/umc/src/main/java/com/umc/domain/post/service/PostService.java @@ -0,0 +1,166 @@ +package com.umc.domain.post.service; + +import com.umc.common.jwt.SecurityUtil; +import com.umc.common.response.ApiResponse; +import com.umc.common.response.status.ErrorCode; +import com.umc.common.response.status.SuccessCode; +import com.umc.domain.board.entity.Board; +import com.umc.domain.board.repository.BoardRepository; +import com.umc.domain.post.dto.PostImageDTO; +import com.umc.domain.post.dto.PostRequestDTO; +import com.umc.domain.post.dto.PostResponseDTO; +import com.umc.domain.post.dto.SimplePostResponseDTO; +import com.umc.domain.post.entity.LikePost; +import com.umc.domain.post.entity.Post; +import com.umc.domain.post.entity.PostImage; +import com.umc.domain.post.repository.LikePostRepository; +import com.umc.domain.post.repository.PostRepository; +import com.umc.domain.user.entity.Member; +import com.umc.domain.user.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +@Service +public class PostService { + private final PostRepository postRepository; + private final MemberRepository memberRepository; + private final BoardRepository boardRepository; + private final LikePostRepository likePostRepository; + private final PostImageService postImageService; + + @Transactional + public ApiResponse> findAll() { + List posts = postRepository.findAll(); + + List postDTO = posts.stream().map( + post -> SimplePostResponseDTO.builder() + .id(post.getId()) + .title(post.getTitle()) + .boardId(post.getBoard().getId()) + .writerNickname(post.getWriter().getNickname()) + .createdAt(post.getCreatedAt()) + .modifiedAt(post.getModifiedAt()) + .build() + ).toList(); + return ApiResponse.of(SuccessCode._OK, postDTO); + } + + public ApiResponse find(String postId) { + Post post = postRepository.findById(Long.getLong(postId)).orElseThrow(); + + return ApiResponse.of(SuccessCode._OK, PostResponseDTO.builder() + .id(Long.getLong(postId)) + .title(post.getTitle()) + .content(post.getContent()) + .writerNickname(post.getWriter().getNickname()) + .boardId(post.getBoard().getId()) + .images( + post.getImages().stream().map( image-> + PostImageDTO.builder() + .originalFilename(image.getOriginalFilename()) + .storedFilename(image.getStoredFilename()) + .build() + ).toList() + ) + .createdAt(post.getCreatedAt()) + .modifiedAt(post.getModifiedAt()) + .build() + ); + } + + public ApiResponse delete(String postId) { + Post target = postRepository.findById(Long.getLong(postId)).orElseThrow(); + postRepository.delete(target); + + return ApiResponse.of(SuccessCode._OK, "삭제되었습니다."); + } + + public ApiResponse save(String boardId, PostRequestDTO postRequest, MultipartFile[] imageFiles) { + Member writer = memberRepository.findByEmail( + SecurityUtil.getCurrentUserEmail() + ).orElseThrow(); // 되나? + Board board = boardRepository.findById(Long.getLong(boardId)).orElseThrow(); + // Post 엔티티 생성 + Post post = Post.builder() + .title(postRequest.getTitle()) + .content(postRequest.getContent()) + .writer(writer) + .board(board) + .build(); + // 이미지 저장 + List images = postImageService.save(imageFiles, post); + post.setImages(images); + + postRepository.save(post); + return ApiResponse.of(SuccessCode._OK, "저장되었습니다"); + } + + public ApiResponse update(String postId, PostRequestDTO postRequest, MultipartFile[] imageFiles) { + Post target = postRepository.findById(Long.getLong(postId)).orElseThrow(); + target.setTitle(postRequest.getTitle()); + target.setContent(postRequest.getContent()); + + if(!Objects.isNull(imageFiles)) { + List images = postImageService.update(imageFiles, target); + target.setImages(images); + } + + postRepository.save(target); + return ApiResponse.of(SuccessCode._OK, "수정되었습니다"); + } + + public ApiResponse hitLike(String postId) { + Post post = postRepository.findById(Long.getLong(postId)).orElseThrow(); + Member member = memberRepository.findByEmail( + SecurityUtil.getCurrentUserEmail() + ).orElseThrow(); + + if(likePostRepository.existsByPostIdAndMemberId(post.getId(), member.getId())) + return ApiResponse.ofFailure(ErrorCode.LIKE_ALREADY_EXISTS, "이미 좋아요를 누른 게시글입니다"); + + LikePost likePost = LikePost.builder() + .post(post) + .member(member) + .build(); + likePostRepository.save(likePost); + return ApiResponse.of(SuccessCode._OK, post.getId() + " 게시물에 좋아요 완료 되었습니다"); + } + + + public ApiResponse getLikes(String postId) { + int likes = likePostRepository.countByPostId(Long.getLong(postId)); + return ApiResponse.of(SuccessCode._OK, likes); + } + + @Transactional + public ApiResponse> getLikedPostsByUser() { + Member member = memberRepository.findByEmail( + SecurityUtil.getCurrentUserEmail() + ).orElseThrow(); + List posts = postRepository.findAllLikedPostsByMemberIdOrderByLikedTime(member.getId()); + return ApiResponse.of(SuccessCode._OK, posts.stream() + .map(post -> SimplePostResponseDTO.builder() + .title(post.getTitle()) + .writerNickname(post.getWriter().getNickname()) + .createdAt(post.getCreatedAt()) + .modifiedAt(post.getModifiedAt()) + .build() + ).toList() + ); + } + + public ApiResponse checkLike(String postId) { + Member member = memberRepository.findByEmail( + SecurityUtil.getCurrentUserEmail() + ).orElseThrow(); + boolean liked = likePostRepository.existsByPostIdAndMemberId(Long.getLong(postId), member.getId()); + + return ApiResponse.of(SuccessCode._OK, liked); + } +} diff --git a/umc/src/main/java/com/umc/domain/user/dto/MemberLoginRequestDTO.java b/umc/src/main/java/com/umc/domain/user/dto/MemberLoginRequestDTO.java index af097dc..050743a 100644 --- a/umc/src/main/java/com/umc/domain/user/dto/MemberLoginRequestDTO.java +++ b/umc/src/main/java/com/umc/domain/user/dto/MemberLoginRequestDTO.java @@ -1,8 +1,7 @@ package com.umc.domain.user.dto; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.Pattern; - +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; import lombok.Builder; import lombok.Getter; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; diff --git a/umc/src/main/java/com/umc/domain/user/entity/Member.java b/umc/src/main/java/com/umc/domain/user/entity/Member.java index 271c7c1..9587743 100644 --- a/umc/src/main/java/com/umc/domain/user/entity/Member.java +++ b/umc/src/main/java/com/umc/domain/user/entity/Member.java @@ -5,7 +5,7 @@ import java.util.Collection; import java.util.List; import java.util.stream.Collectors; -import javax.persistence.*; +import jakarta.persistence.*; import com.umc.common.entity.BaseTimeEntity; import lombok.AllArgsConstructor;