Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/main/java/org/sopt/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/sopt/common/config/JpaAuditingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
23 changes: 23 additions & 0 deletions src/main/java/org/sopt/common/entity/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.sopt.common.entity;

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;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;
}
19 changes: 19 additions & 0 deletions src/main/java/org/sopt/common/entity/BaseSoftDeleteEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.sopt.common.entity;

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
public abstract class BaseSoftDeleteEntity extends BaseEntity {

private LocalDateTime deletedAt;

@Column(nullable = false)
private Boolean isDeleted = false;

public void delete() {
this.deletedAt = LocalDateTime.now();
this.isDeleted = true;
}
}
10 changes: 10 additions & 0 deletions src/main/java/org/sopt/common/exception/CommentException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.common.exception;

import org.sopt.common.response.ErrorCode;

public class CommentException extends GeneralException {

public CommentException(ErrorCode errorCode) {
super(errorCode);
}
}
6 changes: 5 additions & 1 deletion src/main/java/org/sopt/common/response/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ public enum ErrorCode {
// Article
INVALID_TAG(3001, HttpStatus.BAD_REQUEST, "❌ 유효하지 않은 태그값입니다."),
ARTICLE_NOT_FOUND(3002, HttpStatus.NOT_FOUND, "❌ 존재하지 않는 아티클입니다."),
DUPLICATE_ARTICLE_TITLE(3003, HttpStatus.BAD_REQUEST, "⚠️ 이미 존재하는 아티클 제목입니다.")
DUPLICATE_ARTICLE_TITLE(3003, HttpStatus.BAD_REQUEST, "⚠️ 이미 존재하는 아티클 제목입니다."),

// Comment
COMMENT_NOT_FOUND(4001, HttpStatus.NOT_FOUND, "❌ 존재하지 않는 댓글입니다."),
COMMENT_EDIT_NOT_ALLOWED(4002, HttpStatus.FORBIDDEN, "❌댓글 작성자만 수정할 수 있습니다.")
;

private final int code;
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/org/sopt/common/response/SuccessCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ public enum SuccessCode {

// Article
ARTICLE_CREATED(3001, HttpStatus.CREATED, "✅ 아티클이 성공적으로 등록되었습니다."),
ARTICLE_FOUND(3002, HttpStatus.OK, "✅ 아티클이 성공적으로 조회되었습니다.")
ARTICLE_FOUND(3002, HttpStatus.OK, "✅ 아티클이 성공적으로 조회되었습니다."),

// Comment
COMMENT_CREATED(4001, HttpStatus.CREATED, "✅ 댓글이 성공적으로 등록되었습니다."),
COMMENT_FOUND(4002, HttpStatus.OK, "✅ 댓글이 성공적으로 조회되었습니다."),
COMMENT_DELETED(4003, HttpStatus.OK, "✅ 댓글이 성공적으로 삭제되었습니다.")
;

private final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import lombok.RequiredArgsConstructor;
import org.sopt.common.response.ApiResponse;
import org.sopt.common.response.SuccessCode;
import org.sopt.domain.article.dto.request.ArticleCreateRequestDTO;
import org.sopt.domain.article.dto.response.ArticleResponseDTO;
import org.sopt.domain.article.dto.request.ArticleCreateRequest;
import org.sopt.domain.article.dto.request.CommentCreateRequest;
import org.sopt.domain.article.dto.response.ArticleResponse;
import org.sopt.domain.article.dto.response.CommentListResponse;
import org.sopt.domain.article.dto.response.CommentResponse;
import org.sopt.domain.article.service.ArticleService;
import org.sopt.domain.article.service.CommentService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand All @@ -18,30 +22,55 @@
public class ArticleController {

private final ArticleService articleService;
private final CommentService commentService;

// ⚪ 아티클 생성 API
@PostMapping
public ResponseEntity<ApiResponse<ArticleResponseDTO>> createArticle(
@Valid @RequestBody ArticleCreateRequestDTO articleCreateRequestDTO
public ResponseEntity<ApiResponse<ArticleResponse>> createArticle(
@Valid @RequestBody ArticleCreateRequest articleCreateRequest
) {
ArticleResponseDTO responseDTO = articleService.createArticle(articleCreateRequestDTO);
ArticleResponse response = articleService.createArticle(articleCreateRequest);
return ResponseEntity.status(SuccessCode.ARTICLE_CREATED.getStatus())
.body(ApiResponse.ok(SuccessCode.ARTICLE_CREATED, responseDTO));
.body(ApiResponse.ok(SuccessCode.ARTICLE_CREATED, response));
}

// ⚪ 아티클 단일 조회 API
@GetMapping("/{articleId}")
public ResponseEntity<ApiResponse<ArticleResponseDTO>> getArticle(@PathVariable Long articleId) {
ArticleResponseDTO responseDTO = articleService.getArticleById(articleId);
public ResponseEntity<ApiResponse<ArticleResponse>> getArticle(@PathVariable Long articleId) {
ArticleResponse response = articleService.getArticleById(articleId);
return ResponseEntity.status(SuccessCode.ARTICLE_FOUND.getStatus())
.body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, responseDTO));
.body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, response));
}

// ⚪ 아티클 전체 조회 API
@GetMapping
public ResponseEntity<ApiResponse<List<ArticleResponseDTO>>> getAllArticles() {
List<ArticleResponseDTO> responseDTOs = articleService.getArticles();
public ResponseEntity<ApiResponse<List<ArticleResponse>>> getAllArticles() {
List<ArticleResponse> response = articleService.getArticles();
return ResponseEntity.status(SuccessCode.ARTICLE_FOUND.getStatus())
.body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, responseDTOs));
.body(ApiResponse.ok(SuccessCode.ARTICLE_FOUND, response));
}

// ⚪ 댓글 작성 API
@PostMapping("{articleId}/comments")
public ResponseEntity<ApiResponse<CommentResponse>> createComment(
@RequestHeader("Member-Id") Long memberId,
@PathVariable Long articleId,
@Valid @RequestBody CommentCreateRequest request
) {
CommentResponse response = commentService.createComment(memberId, articleId, request);
return ResponseEntity
.status(SuccessCode.COMMENT_CREATED.getStatus())
.body(ApiResponse.ok(SuccessCode.COMMENT_CREATED, response));
}

// ⚪ 특정 아티클의 댓글 전체 조회 API
@GetMapping("{articleId}/comments")
public ResponseEntity<ApiResponse<CommentListResponse>> readComments(
@PathVariable Long articleId
) {
CommentListResponse response = commentService.readComments(articleId);
return ResponseEntity
.status(SuccessCode.COMMENT_FOUND.getStatus())
.body(ApiResponse.ok(SuccessCode.COMMENT_FOUND, response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.sopt.domain.article.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.sopt.common.response.ApiResponse;
import org.sopt.common.response.SuccessCode;
import org.sopt.domain.article.dto.request.CommentUpdateRequest;
import org.sopt.domain.article.dto.response.CommentResponse;
import org.sopt.domain.article.service.CommentService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/comments")
@RequiredArgsConstructor
public class CommentController {

private final CommentService commentService;

// ⚪ 댓글 수정 API
@PatchMapping("/{commentId}")
public ResponseEntity<ApiResponse<CommentResponse>> updateComment(
@RequestHeader("Member-Id") Long memberId,
@PathVariable Long commentId,
@Valid @RequestBody CommentUpdateRequest request
) {
CommentResponse response = commentService.updateComment(memberId, commentId, request);
return ResponseEntity
.status(SuccessCode.COMMENT_CREATED.getStatus())
.body(ApiResponse.ok(SuccessCode.COMMENT_CREATED, response));
}

// ⚪ 댓글 삭제 API
@DeleteMapping("/{commentId}")
public ResponseEntity<ApiResponse<Void>> deleteComment(
@RequestHeader("Member-Id") Long memberId,
@PathVariable Long commentId
) {
commentService.deleteComment(memberId, commentId);
return ResponseEntity
.status(SuccessCode.COMMENT_DELETED.getStatus())
.body(ApiResponse.ok(SuccessCode.COMMENT_DELETED, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.sopt.domain.article.entity.Article;
import org.sopt.domain.article.entity.ArticleTag;

public record ArticleCreateRequestDTO(
public record ArticleCreateRequest(
@NotNull(message = "회원 ID는 필수입니다.")
Long memberId,

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.sopt.domain.article.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record CommentCreateRequest(
@NotBlank(message = "내용은 필수입니다.")
@Size(max = 300, message = "내용은 300자 이내여야 합니다.")
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.sopt.domain.article.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record CommentUpdateRequest(
@NotBlank(message = "내용은 필수입니다.")
@Size(max = 300, message = "내용은 300자 이내여야 합니다.")
String content
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
import org.sopt.domain.article.entity.Article;
import org.sopt.domain.article.entity.ArticleTag;

public record ArticleResponseDTO(
public record ArticleResponse(
Long articleId,
String memberName,
String title,
String content,
ArticleTag tag
) {
public static ArticleResponseDTO of(Article article) {
return new ArticleResponseDTO(
public static ArticleResponse of(Article article) {
return new ArticleResponse(
article.getId(),
article.getMember().getName(),
article.getAuthor().getName(),
article.getTitle(),
article.getContent(),
article.getTag()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.sopt.domain.article.dto.response;

import org.sopt.domain.article.entity.Comment;
import java.util.List;

public record CommentListResponse(
List<CommentResponse> comments
) {
public static CommentListResponse of(List<Comment> comments) {
return new CommentListResponse(
comments.stream()
.map(CommentResponse::of)
.toList()
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.sopt.domain.article.dto.response;

import org.sopt.domain.article.entity.Comment;
import java.time.LocalDateTime;

public record CommentResponse(
Long commentId,
String content,
Long authorId,
String authorName,
LocalDateTime createdAt
) {
public static CommentResponse of(Comment comment) {
return new CommentResponse(
comment.getId(),
comment.getContent(),
comment.getAuthor().getId(),
comment.getAuthor().getName(),
comment.getCreatedAt()
);
}
}
31 changes: 9 additions & 22 deletions src/main/java/org/sopt/domain/article/entity/Article.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,15 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sopt.common.entity.BaseEntity;
import org.sopt.domain.member.entity.Member;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Table(
name = "article",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"title"})
}
)
public class Article {
public class Article extends BaseEntity {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "article_id")
Expand All @@ -36,13 +28,12 @@ public class Article {
@Column(name = "tag", nullable = false)
private ArticleTag tag;

@CreatedDate
@Column(name = "created_date", nullable = false)
private LocalDate createdDate;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "memberId")
private Member member;
@JoinColumn(name = "member_id")
private Member author;

@OneToMany(mappedBy = "article")
private List<Comment> comments = new ArrayList<>();

public Article(String title, String content, ArticleTag tag) {
this.title = title;
Expand All @@ -53,8 +44,4 @@ public Article(String title, String content, ArticleTag tag) {
public static Article create(String title, String content, ArticleTag tag) {
return new Article(title, content, tag);
}

public void connectMember(Member member) {
this.member = member;
}
}
Loading