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
111 changes: 111 additions & 0 deletions src/main/kotlin/io/devchw/coderabbit/BoardController.kt
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)
Comment on lines +26 to +27
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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:

-private val posts = mutableMapOf<Long, Post>()
+private val posts = ConcurrentHashMap<Long, Post>()

Don't forget to add the import:

+import java.util.concurrent.ConcurrentHashMap
🤖 Prompt for AI Agents
In src/main/kotlin/io/devchw/coderabbit/BoardController.kt around lines 23 to
24, the mutableMapOf used for posts is not thread-safe, which can cause issues
during concurrent access. Replace mutableMapOf with a thread-safe alternative
like ConcurrentHashMap to ensure safe concurrent modifications. Also, add the
necessary import for ConcurrentHashMap at the top of the file.


/**
* 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
Copy link

@coderabbitai coderabbitai bot Jun 5, 2025

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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)
}
import javax.validation.Valid
@PostMapping
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)
}
🤖 Prompt for AI Agents
In src/main/kotlin/io/devchw/coderabbit/BoardController.kt around lines 27 to
33, the createPost function lacks input validation which is necessary to ensure
data integrity. Add validation logic to check the incoming Post object's
required fields before processing, returning an appropriate error response if
validation fails. Also, include the necessary import statements for validation
annotations or utilities used.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai @Valid를 사용하지 않고 처리하려면 어떤 식으로 할 수 있나요?

Copy link

Choose a reason for hiding this comment

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

@DevCHW @Valid 어노테이션을 사용하지 않고 검증을 처리할 수 있는 여러 방법이 있습니다:

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) { "제목이 너무 깁니다" }
    }
}

각 방법의 장단점:

  • 수동 검증: 간단하지만 코드 중복 가능성
  • 검증 유틸리티: 재사용 가능하고 테스트하기 쉬움
  • 확장 함수: Kotlin다운 방식, 객체 지향적
  • 서비스 레이어: 비즈니스 로직과 분리, 복잡한 검증에 적합

프로젝트 규모와 요구사항에 따라 적절한 방법을 선택하시면 됩니다!


/**
* 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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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)
}
@PutMapping("/{id}")
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)
}
🤖 Prompt for AI Agents
In src/main/kotlin/io/devchw/coderabbit/BoardController.kt around lines 53 to
61, the updatePost method lacks input validation for the updatedPost object. Add
validation checks similar to those in the create endpoint to ensure the
updatedPost data meets required constraints before updating. This may include
checking for null or empty fields and validating field formats. Return
appropriate error responses if validation fails.


/**
* 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()
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/io/devchw/coderabbit/Comment.kt
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
)
86 changes: 86 additions & 0 deletions src/main/kotlin/io/devchw/coderabbit/CommentController.kt
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) }
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/io/devchw/coderabbit/Post.kt
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()
)