-
Notifications
You must be signed in to change notification settings - Fork 0
게시판 CRUD API 구현 #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,111 @@ | ||||||||||||||||||||||||||||||||||||||||||||
package io.devchw.coderabbit | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.http.HttpStatus | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.http.ResponseEntity | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.DeleteMapping | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.GetMapping | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.PathVariable | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.PostMapping | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.PutMapping | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.RequestBody | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.RequestMapping | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.web.bind.annotation.RestController | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.concurrent.atomic.AtomicLong | ||||||||||||||||||||||||||||||||||||||||||||
import org.springframework.beans.factory.annotation.Autowired | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||
* @author DevCHW | ||||||||||||||||||||||||||||||||||||||||||||
* @since 2025-06-05 | ||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||
@RestController | ||||||||||||||||||||||||||||||||||||||||||||
@RequestMapping("/api/board") | ||||||||||||||||||||||||||||||||||||||||||||
class BoardController( | ||||||||||||||||||||||||||||||||||||||||||||
@Autowired private val commentController: CommentController | ||||||||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
private val posts = mutableMapOf<Long, Post>() | ||||||||||||||||||||||||||||||||||||||||||||
private val idGenerator = AtomicLong(1) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||
* Creates a new post, assigns it a unique ID, and stores it in memory. | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* @param post The post data to create. | ||||||||||||||||||||||||||||||||||||||||||||
* @return The created post with its assigned ID and HTTP status 201 (Created). | ||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||
@PostMapping | ||||||||||||||||||||||||||||||||||||||||||||
fun createPost(@RequestBody post: Post): ResponseEntity<Post> { | ||||||||||||||||||||||||||||||||||||||||||||
val id = idGenerator.getAndIncrement() | ||||||||||||||||||||||||||||||||||||||||||||
post.id = id | ||||||||||||||||||||||||||||||||||||||||||||
posts[id] = post | ||||||||||||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.CREATED).body(post) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+35
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add input validation to the create endpoint. The create logic is correct and follows REST conventions properly. Add validation to ensure data integrity: @PostMapping
-fun createPost(@RequestBody post: Post): ResponseEntity<Post> {
+fun createPost(@RequestBody @Valid post: Post): ResponseEntity<Post> {
+ if (post.title.isBlank() || post.content.isBlank()) {
+ return ResponseEntity.badRequest().build()
+ }
val id = idGenerator.getAndIncrement()
post.id = id
posts[id] = post
return ResponseEntity.status(HttpStatus.CREATED).body(post)
} Don't forget to add the import: +import javax.validation.Valid 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @DevCHW 1. 수동 검증 (Manual Validation)컨트롤러 메서드 내에서 직접 검증 로직을 구현하는 방법입니다: @PostMapping
fun createPost(@RequestBody post: Post): ResponseEntity<*> {
// 필수 필드 검증
if (post.title.isNullOrBlank()) {
return ResponseEntity.badRequest()
.body(mapOf("error" to "제목은 필수입니다"))
}
if (post.content.isNullOrBlank()) {
return ResponseEntity.badRequest()
.body(mapOf("error" to "내용은 필수입니다"))
}
// 길이 검증
if (post.title.length > 100) {
return ResponseEntity.badRequest()
.body(mapOf("error" to "제목은 100자를 초과할 수 없습니다"))
}
val id = idGenerator.getAndIncrement()
post.id = id
posts[id] = post
return ResponseEntity.status(HttpStatus.CREATED).body(post)
} 2. 검증 유틸리티 클래스 생성별도의 검증 클래스를 만들어 재사용 가능한 검증 로직을 구현하는 방법입니다: class PostValidator {
companion object {
fun validate(post: Post): List<String> {
val errors = mutableListOf<String>()
if (post.title.isNullOrBlank()) {
errors.add("제목은 필수입니다")
} else if (post.title.length > 100) {
errors.add("제목은 100자를 초과할 수 없습니다")
}
if (post.content.isNullOrBlank()) {
errors.add("내용은 필수입니다")
} else if (post.content.length > 1000) {
errors.add("내용은 1000자를 초과할 수 없습니다")
}
return errors
}
}
}
// 컨트롤러에서 사용
@PostMapping
fun createPost(@RequestBody post: Post): ResponseEntity<*> {
val validationErrors = PostValidator.validate(post)
if (validationErrors.isNotEmpty()) {
return ResponseEntity.badRequest()
.body(mapOf("errors" to validationErrors))
}
val id = idGenerator.getAndIncrement()
post.id = id
posts[id] = post
return ResponseEntity.status(HttpStatus.CREATED).body(post)
} 3. 확장 함수를 이용한 검증Kotlin의 확장 함수를 활용하여 Post 클래스에 검증 메서드를 추가하는 방법입니다: fun Post.validate(): List<String> {
val errors = mutableListOf<String>()
if (title.isBlank()) errors.add("제목은 필수입니다")
if (content.isBlank()) errors.add("내용은 필수입니다")
if (title.length > 100) errors.add("제목이 너무 깁니다")
return errors
}
// 컨트롤러에서 사용
@PostMapping
fun createPost(@RequestBody post: Post): ResponseEntity<*> {
val validationErrors = post.validate()
if (validationErrors.isNotEmpty()) {
return ResponseEntity.badRequest()
.body(mapOf("errors" to validationErrors))
}
val id = idGenerator.getAndIncrement()
post.id = id
posts[id] = post
return ResponseEntity.status(HttpStatus.CREATED).body(post)
} 4. 서비스 레이어에서 검증비즈니스 로직과 함께 서비스 계층에서 검증을 처리하는 방법입니다: @Service
class BoardService {
fun createPost(post: Post): Post {
validatePost(post)
// 비즈니스 로직 처리
return post
}
private fun validatePost(post: Post) {
require(post.title.isNotBlank()) { "제목은 필수입니다" }
require(post.content.isNotBlank()) { "내용은 필수입니다" }
require(post.title.length <= 100) { "제목이 너무 깁니다" }
}
} 각 방법의 장단점:
프로젝트 규모와 요구사항에 따라 적절한 방법을 선택하시면 됩니다! |
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||
* Retrieves a list of all posts. | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* @return HTTP 200 response containing all stored posts. | ||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||
@GetMapping | ||||||||||||||||||||||||||||||||||||||||||||
fun getPosts(): ResponseEntity<List<Post>> { | ||||||||||||||||||||||||||||||||||||||||||||
return ResponseEntity.ok(posts.values.toList()) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||
* Retrieves a post by its unique ID. | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* Returns the post with HTTP 200 if found, or HTTP 404 if no post exists with the given ID. | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* @param id The unique identifier of the post to retrieve. | ||||||||||||||||||||||||||||||||||||||||||||
* @return A ResponseEntity containing the post if found, or a 404 status if not. | ||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||
@GetMapping("/{id}") | ||||||||||||||||||||||||||||||||||||||||||||
fun getPost(@PathVariable id: Long): ResponseEntity<Post> { | ||||||||||||||||||||||||||||||||||||||||||||
val post = posts[id] | ||||||||||||||||||||||||||||||||||||||||||||
return if (post != null) { | ||||||||||||||||||||||||||||||||||||||||||||
ResponseEntity.ok(post) | ||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||
ResponseEntity.status(HttpStatus.NOT_FOUND).build() | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||
* Updates an existing post with new data by its ID. | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* If the post with the specified ID does not exist, returns HTTP 404 (Not Found). | ||||||||||||||||||||||||||||||||||||||||||||
* Otherwise, replaces the post's content and returns the updated post with HTTP 200 (OK). | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* @param id The ID of the post to update. | ||||||||||||||||||||||||||||||||||||||||||||
* @param updatedPost The new post data to replace the existing post. | ||||||||||||||||||||||||||||||||||||||||||||
* @return The updated post with HTTP 200, or HTTP 404 if the post does not exist. | ||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||
@PutMapping("/{id}") | ||||||||||||||||||||||||||||||||||||||||||||
fun updatePost(@PathVariable id: Long, @RequestBody updatedPost: Post): ResponseEntity<Post> { | ||||||||||||||||||||||||||||||||||||||||||||
if (!posts.containsKey(id)) { | ||||||||||||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build() | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
updatedPost.id = id | ||||||||||||||||||||||||||||||||||||||||||||
posts[id] = updatedPost | ||||||||||||||||||||||||||||||||||||||||||||
return ResponseEntity.ok(updatedPost) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+81
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add input validation to the update endpoint. The update logic correctly handles existence checks and maintains data consistency. Add validation similar to the create endpoint: @PutMapping("/{id}")
-fun updatePost(@PathVariable id: Long, @RequestBody updatedPost: Post): ResponseEntity<Post> {
+fun updatePost(@PathVariable id: Long, @RequestBody @Valid updatedPost: Post): ResponseEntity<Post> {
if (!posts.containsKey(id)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build()
}
+ if (updatedPost.title.isBlank() || updatedPost.content.isBlank()) {
+ return ResponseEntity.badRequest().build()
+ }
updatedPost.id = id
posts[id] = updatedPost
return ResponseEntity.ok(updatedPost)
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||
* Deletes a post by its ID. | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* Removes the post with the specified ID from the in-memory store. Returns HTTP 204 (No Content) if the post was deleted, or HTTP 404 (Not Found) if the post does not exist. | ||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||
* @param id The unique identifier of the post to delete. | ||||||||||||||||||||||||||||||||||||||||||||
* @return A ResponseEntity with HTTP status 204 if successful, or 404 if the post is not found. | ||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||
@DeleteMapping("/{id}") | ||||||||||||||||||||||||||||||||||||||||||||
fun deletePost(@PathVariable id: Long): ResponseEntity<Void> { | ||||||||||||||||||||||||||||||||||||||||||||
if (!posts.containsKey(id)) { | ||||||||||||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build() | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
// 게시물과 관련된 모든 댓글 삭제 | ||||||||||||||||||||||||||||||||||||||||||||
commentController.deleteAllCommentsForPost(id) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
posts.remove(id) | ||||||||||||||||||||||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build() | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package io.devchw.coderabbit | ||
|
||
/** | ||
* @author DevCHW | ||
* @since 2025-06-05 | ||
*/ | ||
data class Comment( | ||
var id: Long? = null, | ||
var postId: Long, | ||
var content: String | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package io.devchw.coderabbit | ||
|
||
import org.springframework.http.HttpStatus | ||
import org.springframework.http.ResponseEntity | ||
import org.springframework.web.bind.annotation.* | ||
import java.util.concurrent.atomic.AtomicLong | ||
|
||
/** | ||
* @author DevCHW | ||
* @since 2025-06-05 | ||
*/ | ||
@RestController | ||
@RequestMapping("/api/board") | ||
class CommentController { | ||
|
||
private val comments = mutableMapOf<Long, Comment>() | ||
private val idGenerator = AtomicLong(1) | ||
|
||
// 댓글 생성 | ||
@PostMapping("/{postId}/comments") | ||
fun createComment( | ||
@PathVariable postId: Long, | ||
@RequestBody comment: Comment | ||
): ResponseEntity<Comment> { | ||
val id = idGenerator.getAndIncrement() | ||
comment.id = id | ||
comment.postId = postId | ||
comments[id] = comment | ||
return ResponseEntity.status(HttpStatus.CREATED).body(comment) | ||
} | ||
|
||
// 게시물의 모든 댓글 조회 | ||
@GetMapping("/{postId}/comments") | ||
fun getCommentsByPost(@PathVariable postId: Long): ResponseEntity<List<Comment>> { | ||
val postComments = comments.values.filter { it.postId == postId } | ||
return ResponseEntity.ok(postComments) | ||
} | ||
|
||
// 댓글 상세 조회 | ||
@GetMapping("/comments/{commentId}") | ||
fun getComment(@PathVariable commentId: Long): ResponseEntity<Comment> { | ||
val comment = comments[commentId] | ||
return if (comment != null) { | ||
ResponseEntity.ok(comment) | ||
} else { | ||
ResponseEntity.status(HttpStatus.NOT_FOUND).build() | ||
} | ||
} | ||
|
||
// 댓글 수정 | ||
@PutMapping("/comments/{commentId}") | ||
fun updateComment( | ||
@PathVariable commentId: Long, | ||
@RequestBody updatedComment: Comment | ||
): ResponseEntity<Comment> { | ||
val existingComment = comments[commentId] | ||
if (existingComment == null) { | ||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build() | ||
} | ||
|
||
// 기존 postId 유지 | ||
updatedComment.id = commentId | ||
updatedComment.postId = existingComment.postId | ||
comments[commentId] = updatedComment | ||
return ResponseEntity.ok(updatedComment) | ||
} | ||
|
||
// 댓글 삭제 | ||
@DeleteMapping("/comments/{commentId}") | ||
fun deleteComment(@PathVariable commentId: Long): ResponseEntity<Void> { | ||
if (!comments.containsKey(commentId)) { | ||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build() | ||
} | ||
comments.remove(commentId) | ||
return ResponseEntity.status(HttpStatus.NO_CONTENT).build() | ||
} | ||
|
||
// 게시물의 모든 댓글 삭제 (게시물이 삭제될 때 호출) | ||
fun deleteAllCommentsForPost(postId: Long) { | ||
val commentIdsToRemove = comments.values | ||
.filter { it.postId == postId } | ||
.mapNotNull { it.id } | ||
|
||
commentIdsToRemove.forEach { comments.remove(it) } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package io.devchw.coderabbit | ||
|
||
/** | ||
* @author DevCHW | ||
* @since 2025-06-05 | ||
*/ | ||
data class Post( | ||
var id: Long? = null, | ||
var title: String, | ||
var content: String, | ||
var comments: MutableList<Comment> = mutableListOf() | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Address thread safety concerns for concurrent access.
While
AtomicLong
provides thread-safe ID generation,mutableMapOf
is not thread-safe for concurrent modifications. This could lead to data corruption or inconsistent state in a multi-threaded environment.Consider using
ConcurrentHashMap
for thread safety:Don't forget to add the import:
+import java.util.concurrent.ConcurrentHashMap
🤖 Prompt for AI Agents