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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.leets7th.domain.category.entity;

import com.example.leets7th.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "categories")
public class Category extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String name;

public static Category create(String name) {
Category category = new Category();
category.name = name;
return category;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.leets7th.domain.category.repository;

import com.example.leets7th.domain.category.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface CategoryRepository extends JpaRepository<Category, Long> {
Optional<Category> findByName(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.example.leets7th.domain.post.controller;

import com.example.leets7th.domain.post.dto.request.PostCreateRequest;
import com.example.leets7th.domain.post.dto.request.PostUpdateRequest;
import com.example.leets7th.domain.post.dto.response.PostDetailResponse;
import com.example.leets7th.domain.post.dto.response.PostListResponse;
import com.example.leets7th.domain.post.service.PostService;
import com.example.leets7th.global.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@Tag(name = "Post", description = "게시글 관련 API")
@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
@Validated
public class PostController {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swagger 문서관리해주셨네요~! 지금도 충분히 세세하게 잘 작성해주셨는데 Docs 파일로 따로 관리해서 ( PostControllerDocs, PostController )
controller 코드는 설명을 제외하고 기능적인 코드만 남겨서 가독성을 높일 수 있는 방법도 있습니다. 수정은 아니고 참고만 해주시면 감사하겠습니다~!


private final PostService postService;

// 게시글 목록 조회
@Operation(summary = "게시글 목록 조회", description = "페이지 단위로 게시글 목록을 조회합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공")
})
@GetMapping
public ResponseEntity<ApiResponse<PostListResponse>> getPosts(
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0")
@RequestParam(defaultValue = "0") @Min(value = 0, message = "페이지 번호는 0 이상이어야 합니다.") int page,
@Parameter(description = "페이지 크기", example = "10")
@RequestParam(defaultValue = "10") @Min(value = 1, message = "페이지 크기는 1 이상이어야 합니다.") int size
) {
PostListResponse response = postService.getPosts(page, size);
return ResponseEntity.ok(ApiResponse.success("POST_LIST_SUCCESS", "게시글 목록 조회 성공", response));
}

// 게시글 상세 조회
@Operation(summary = "게시글 상세 조회", description = "게시글 ID로 단건 조회합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 없음")
})
@GetMapping("/{postId}")
public ResponseEntity<ApiResponse<PostDetailResponse>> getPost(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId) {
PostDetailResponse response = postService.getPost(postId);
return ResponseEntity.ok(ApiResponse.success("POST_DETAIL_SUCCESS", "게시글 상세 조회 성공", response));
}

// 게시글 작성
// TODO: 실제 인증 구현 시 @AuthenticationPrincipal로 userId 주입
@Operation(summary = "게시글 작성", description = "새 게시글을 생성합니다. X-User-Id 헤더로 작성자를 지정합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "생성 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "입력값 오류")
})
@PostMapping
public ResponseEntity<ApiResponse<Map<String, Object>>> createPost(
@RequestBody @Valid PostCreateRequest request,
@Parameter(description = "작성자 ID (임시)", example = "1")
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
Long postId = postService.createPost(request, userId);
Map<String, Object> data = Map.of(
"postId", postId,
"message", "게시글이 생성되었습니다."
);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(data));
}

// 게시글 수정
@Operation(summary = "게시글 수정", description = "게시글 제목/내용을 수정합니다. 작성자만 수정 가능합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "수정 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "수정 권한 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 없음")
})
@PatchMapping("/{postId}")
public ResponseEntity<ApiResponse<Map<String, String>>> updatePost(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId,
@RequestBody PostUpdateRequest request,
@Parameter(description = "요청자 ID (임시)", example = "1")
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
postService.updatePost(postId, request, userId);
return ResponseEntity.ok(ApiResponse.success(Map.of("message", "게시글이 수정되었습니다.")));
}

// 게시글 삭제
@Operation(summary = "게시글 삭제", description = "게시글을 삭제합니다. 작성자만 삭제 가능합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "삭제 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "삭제 권한 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "게시글 없음")
})
@DeleteMapping("/{postId}")
public ResponseEntity<ApiResponse<Map<String, String>>> deletePost(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId,
@Parameter(description = "요청자 ID (임시)", example = "1")
@RequestHeader(value = "X-User-Id", defaultValue = "1") Long userId
) {
postService.deletePost(postId, userId);
return ResponseEntity.ok(ApiResponse.success(Map.of("message", "게시글이 삭제되었습니다.")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.leets7th.domain.post.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@Schema(description = "게시글 생성 요청")
public record PostCreateRequest(
@Schema(description = "카테고리 ID", example = "1")
@NotNull(message = "카테고리는 필수입니다.") Long categoryId,

@Schema(description = "제목", example = "스프링 공부 1일차")
@NotBlank(message = "제목은 필수입니다.") String title,

@Schema(description = "내용", example = "오늘은 JPA를 배웠다.")
@NotBlank(message = "내용은 필수입니다.") String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.leets7th.domain.post.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "게시글 수정 요청 (null인 필드는 수정하지 않음)")
public record PostUpdateRequest(
@Schema(description = "수정할 제목", example = "수정된 제목") String title,
@Schema(description = "수정할 내용", example = "수정된 내용입니다.") String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.example.leets7th.domain.post.dto.response;

import com.example.leets7th.domain.post.entity.Post;

import java.time.LocalDateTime;
import java.util.List;

public record PostDetailResponse(
Long postId,
String title,
String content,
String thumbnailImageUrl,
CategoryDto category,
AuthorDto author,
List<ImageDto> images,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public record CategoryDto(Long categoryId, String name) {
}

public record AuthorDto(Long userId, String nickname) {
}

public record ImageDto(Long imageId, String imageUrl) {
}

public static PostDetailResponse from(Post post) {
CategoryDto categoryDto = null;
if (post.getCategory() != null) {
categoryDto = new CategoryDto(post.getCategory().getId(), post.getCategory().getName());
}

String nickname = post.getUser().getNickname();
AuthorDto authorDto = new AuthorDto(
post.getUser().getId(),
nickname != null ? nickname : "Unknown"
);

List<ImageDto> images = post.getImages().stream()
.map(img -> new ImageDto(img.getId(), img.getImageUrl()))
.toList();

return new PostDetailResponse(
post.getId(),
post.getTitle(),
post.getContent(),
post.getThumbnailImageUrl(),
categoryDto,
authorDto,
images,
post.getCreatedAt(),
post.getUpdatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.example.leets7th.domain.post.dto.response;

import com.example.leets7th.domain.post.entity.Post;
import org.springframework.data.domain.Page;

import java.time.LocalDateTime;
import java.util.List;

public record PostListResponse(
List<PostSummaryDto> posts,
PageInfoDto pageInfo
) {
public record PostSummaryDto(
Long postId,
String title,
String content,
String thumbnailImageUrl,
String author,
LocalDateTime createdAt
) {
public static PostSummaryDto from(Post post) {
String nickname = post.getUser().getNickname();
return new PostSummaryDto(
post.getId(),
post.getTitle(),
post.getContent(),
post.getThumbnailImageUrl(),
nickname != null ? nickname : "Unknown",
post.getCreatedAt()
);
}
}

public record PageInfoDto(
int page,
int size,
long totalElements,
int totalPages,
boolean hasNext
) {
}

public static PostListResponse from(Page<Post> page) {
List<PostSummaryDto> posts = page.getContent().stream()
.map(PostSummaryDto::from)
.toList();

PageInfoDto pageInfo = new PageInfoDto(
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.hasNext()
);

return new PostListResponse(posts, pageInfo);
}
}
30 changes: 29 additions & 1 deletion src/main/java/com/example/leets7th/domain/post/entity/Post.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.leets7th.domain.post.entity;

import com.example.leets7th.domain.category.entity.Category;
import com.example.leets7th.domain.user.entity.User;
import com.example.leets7th.domain.comment.entity.Comment;
import com.example.leets7th.global.entity.BaseEntity;
Expand Down Expand Up @@ -29,6 +30,14 @@ public class Post extends BaseEntity {
@Column(columnDefinition = "TEXT", nullable = false)
private String content;

// 썸네일 이미지 URL
private String thumbnailImageUrl;

// 카테고리
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;

// 작성자
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
Expand All @@ -43,6 +52,25 @@ public class Post extends BaseEntity {
@OrderBy("imageOrder ASC")
private List<PostImage> images = new ArrayList<>();

public static Post create(String title, String content, String thumbnailImageUrl, User user, Category category) {
Post post = new Post();
post.title = title;
post.content = content;
post.thumbnailImageUrl = thumbnailImageUrl;
post.user = user;
post.category = category;
return post;
}

public void update(String title, String content) {
if (title != null && !title.isBlank()) {
this.title = title;
}
if (content != null && !content.isBlank()) {
this.content = content;
}
}

// 연관관계 편의 메서드

public void setUser(User user) {
Expand All @@ -69,4 +97,4 @@ public void removeImage(PostImage image) {
images.remove(image);
image.setPost(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.leets7th.domain.post.repository;

import com.example.leets7th.domain.post.entity.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Long> {
Page<Post> findAll(Pageable pageable);
}
Loading