본문 바로가기

카테고리 없음

24.01.05 과제 해설

오늘은 과제의 완벽한 견본이 올라와서, 그걸 알아보기로 했다.

package com.teamsparta.todo.todocards

import com.teamsparta.todo.replies.Reply
import jakarta.persistence.*
import org.hibernate.annotations.CreationTimestamp
import java.time.ZonedDateTime

@Entity
class TodoCard(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column
    val title: String,
    @Column
    val content: String,
    @Column
    val authorName: String,
    @OneToMany(mappedBy = "todoCard")
    val replies: List<Reply> = emptyList(),
) {
    init {
        if (this.title.isEmpty() || this.title.length > 200) {
            throw TodoCardException("title must be at least 1 character and not more than 200 characters long")
        }

        if (this.content.isEmpty() || this.content.length > 1000) {
            throw TodoCardException("content must be at least 1 character and not more than 1000 characters long")
        }
    }

    @CreationTimestamp
    @Column(updatable = false)
    val createdAt: ZonedDateTime = ZonedDateTime.now()

    @Column(name = "is_completed")
    private var _isCompleted: Boolean = false

    val isCompleted: Boolean
        get() = _isCompleted

    fun complete() {
        _isCompleted = true
    }
}

id: TodoCard의 고유 식별자로, Long 타입입니다.
title: TodoCard의 제목으로, String 타입입니다.
content: TodoCard의 내용으로, String 타입입니다.
authorName: TodoCard의 작성자 이름으로, String 타입입니다.
replies: TodoCard에 대한 댓글(Reply) 목록으로, List<Reply> 타입입니다. OneToMany 애노테이션을 사용하여 TodoCard와 Reply 간의 일대다 관계를 설정하고 있습니다.

 

 

@OneToMany(mappedBy = "todoCard")
    val replies: List<Reply> = emptyList(),
) { 이걸로 할일과 댓글을 연결하고 일대다 관계를 형성했다.

createdAt이건 시간을 나타내고,

val isCompleted: Boolean
    get() = _isCompleted

fun complete() {
    _isCompleted = true
}

_isCompleted: TodoCard의 완료 여부를 나타내는 내부 변수로, Boolean 타입입니다. isCompleted 프로퍼티를 통해 외부에서 접근할 수 있습니다.
isCompleted: get() 함수를 사용하여 _isCompleted 값을 반환하는 프로퍼티입니다.
complete(): TodoCard를 완료 상태로 변경하는 함수입니다. _isCompleted 값을 true로 설정

init {
if (this.title.isEmpty() || this.title.length > 200) {
throw TodoCardException("title must be at least 1 character and not more than 200 characters long")

이걸 이용해서 

제목은 1자 이상 200자 이하, 내용은 1자 이상 1000자 이하로 제한되어 있습니다. 유효하지 않은 제목 또는 내용이 주어진 경우 TodoCardException을 던집니다.

 

package com.teamsparta.todo.todocards

import com.teamsparta.todo.todocards.dtos.CreateTodoCardArguments
import com.teamsparta.todo.todocards.dtos.RetrieveTodoCardDto
import com.teamsparta.todo.todocards.dtos.TodoCardDto
import com.teamsparta.todo.todocards.dtos.UpdateTodoCardArguments
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RequestMapping("/api/v1/todo-cards")
@RestController
class TodoCardController(
    val todoCardService: TodoCardService,
) {
    @PostMapping
    fun createTodoCard(
        @RequestBody createTodoCardArguments: CreateTodoCardArguments,
    ): ResponseEntity<TodoCardDto> {
        val todoCard = todoCardService.createTodoCard(createTodoCardArguments)

        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(todoCard)
    }

    @GetMapping
    fun findAllTodoCard(
        @RequestParam authorName: String?,
        @RequestParam sort: String?,
    ): ResponseEntity<List<TodoCardDto>> {
        val todoCards = todoCardService.findAll(authorName, sort)

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(todoCards)
    }

    @GetMapping("/{todoCardId}")
    fun findTodoCard(
        @PathVariable todoCardId: Long,
    ): ResponseEntity<RetrieveTodoCardDto?> {
        val todoCard = todoCardService.findById(todoCardId)

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(todoCard)
    }

    @PutMapping("/{todoCardId}")
    fun updateTodoCard(
        @PathVariable todoCardId: Long,
        @RequestBody todoCardArguments: UpdateTodoCardArguments,
    ): ResponseEntity<TodoCardDto> {
        val arguments = UpdateTodoCardArguments(
            id = todoCardId,
            title = todoCardArguments.title,
            content = todoCardArguments.content,
            authorName = todoCardArguments.authorName,
        )

        val todoCard: TodoCardDto = todoCardService.updateTodoCard(arguments)

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(todoCard)
    }

    @PatchMapping("/{todoCardId}/complete")
    fun completeTodoCard(
        @PathVariable todoCardId: Long,
    ): ResponseEntity<Unit> {
        todoCardService.completeTodoCard(todoCardId)

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(null)
    }

    @DeleteMapping("/{todoCardId}")
    fun deleteTodoCard(
        @PathVariable todoCardId: Long,
    ): ResponseEntity<Unit> {
        todoCardService.deleteTodoCard(todoCardId)

        return ResponseEntity
            .status(HttpStatus.NO_CONTENT)
            .body(null)
    }

    @ExceptionHandler
    fun handle(exception: TodoCardException?): ResponseEntity<String> {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(exception?.message)
    }
}

 

@RequestMapping("/api/v1/todo-cards"): 이 클래스의 모든 엔드포인트는 "/api/v1/todo-cards" 경로 아래에서 작동합니다.


@RestController: 이 클래스는 RESTful API 컨트롤러임을 나타냅니다.
TodoCardController(val todoCardService: TodoCardService): TodoCardService 클래스의 인스턴스를 주입받아서 사용합니다.


@PostMapping: POST 메서드 요청을 처리하는 엔드포인트입니다. createTodoCard 메서드는 CreateTodoCardArguments 객체를 요청 본문에서 받아와서 todoCardService.createTodoCard를 호출하고, 생성된 TodoCardDto를 HTTP 응답 본문에 담아 반환합니다.


@GetMapping: GET 메서드 요청을 처리하는 엔드포인트입니다. findAllTodoCard 메서드는 authorName과 sort라는 두 개의 쿼리 매개변수를 받아서 todoCardService.findAll을 호출하고, 조회된 모든 TodoCardDto를 HTTP 응답 본문에 담아 반환합니다.(전부조회)


@GetMapping("/{todoCardId}"): GET 메서드 요청을 처리하는 엔드포인트입니다. 경로 변수인 todoCardId를 받아서 todoCardService.findById를 호출하고, 조회된 TodoCardDto를 HTTP 응답 본문에 담아 반환합니다.(하나만 조회)


@PutMapping("/{todoCardId}"): PUT 메서드 요청을 처리하는 엔드포인트입니다. 경로 변수인 todoCardId와 요청 본문의 todoCardArguments를 받아서 todoCardService.updateTodoCard를 호출하고, 업데이트된 TodoCardDto를 HTTP 응답 본문에 담아 반환합니다.(수정)


@PatchMapping("/{todoCardId}/complete"): PATCH 메서드 요청을 처리하는 엔드포인트입니다. 경로 변수인 todoCardId를 받아서 todoCardService.completeTodoCard를 호출합니다. 이 엔드포인트는 별도의 응답 본문을 반환하지 않습니다.


@DeleteMapping("/{todoCardId}"): DELETE 메서드 요청을 처리하는 엔드포인트입니다. 경로 변수인 todoCardId를 받아서 todoCardService.deleteTodoCard를 호출합니다. 이 엔드포인트는 별도의 응답 본문을 반환하지 않습니다.


@ExceptionHandler: 예외를 처리하는 핸들러 메서드입니다. TodoCardException 예외가 발생하면 HttpStatus.BAD_REQUEST 상태와 예외 메시지를 HTTP 응답 본문에 담아 반환합니다.
이 클래스는 TodoCardService와 함께 동작하여 Todo 카드와 관련된 생성, 조회, 수정, 완료, 삭제 등의 기능을 제공합니다.

 

class TodoCardException(override val message: String?): Exception(message)

 

이녀석은 예외를 처리하는녀석으로, 이것만 있으면 된다.

package com.teamsparta.todo.todocards

import org.springframework.data.jpa.repository.JpaRepository

interface TodoCardRepository: JpaRepository<TodoCard, Long> {
    fun findAllByOrderByCreatedAtDesc(): List<TodoCard>
    fun findAllByOrderByCreatedAtAsc(): List<TodoCard>
    fun findAllByAuthorName(authorName: String): List<TodoCard>
}

 JpaRepository 인터페이스를 상속받아 기본적인 CRUD(Create, Read, Update, Delete) 기능을 제공합니다.

이 리포지토리 인터페이스는 다음과 같은 메서드를 정의하고 있습니다:

findAllByOrderByCreatedAtDesc(): List<TodoCard>: 생성된 날짜 기준으로 내림차순으로 모든 TodoCard를 조회하는 메서드입니다.
findAllByOrderByCreatedAtAsc(): List<TodoCard>: 생성된 날짜 기준으로 오름차순으로 모든 TodoCard를 조회하는 메서드입니다.
findAllByAuthorName(authorName: String): List<TodoCard>: 작성자 이름으로 TodoCard를 조회하는 메서드입니다.

 

interface TodoCardService {
    fun createTodoCard(createTodoCardArguments: CreateTodoCardArguments): TodoCardDto
    fun findById(id: Long): RetrieveTodoCardDto?
    fun findAll(authorName: String?, sort: String?): List<TodoCardDto>
    fun updateTodoCard(todoCardArguments: UpdateTodoCardArguments): TodoCardDto
    fun deleteTodoCard(id: Long)
    fun completeTodoCard(id: Long)
}

 

TodoCardService는 TodoCard와 관련된 비즈니스 로직을 처리하기 위한 인터페이스입니다. 이 인터페이스는 TodoCard의 생성, 조회, 수정, 삭제 등의 기능을 정의하고 있습니다.

TodoCardService 인터페이스에는 다음과 같은 메서드들이 포함되어 있습니다:

createTodoCard(createTodoCardArguments: CreateTodoCardArguments): TodoCardDto: 새로운 TodoCard를 생성하는 메서드입니다. CreateTodoCardArguments 객체를 매개변수로 받아서 TodoCard를 생성하고, 생성된 TodoCard의 정보를 TodoCardDto 형태로 반환합니다.


findById(id: Long): RetrieveTodoCardDto?: 주어진 ID에 해당하는 TodoCard를 조회하는 메서드입니다. 조회된 TodoCard의 정보를 RetrieveTodoCardDto 형태로 반환합니다. 만약 ID에 해당하는 TodoCard가 없을 경우에는 null을 반환합니다.


findAll(authorName: String?, sort: String?): List<TodoCardDto>: 모든 TodoCard를 조회하는 메서드입니다. 작성자 이름과 정렬 기준을 선택적으로 받아서 해당 조건에 맞는 TodoCard를 조회하고, 조회된 TodoCard의 정보를 TodoCardDto 형태의 리스트로 반환합니다.


updateTodoCard(todoCardArguments: UpdateTodoCardArguments): TodoCardDto: 주어진 TodoCard의 정보를 업데이트하는 메서드입니다. UpdateTodoCardArguments 객체를 매개변수로 받아서 TodoCard를 업데이트하고, 업데이트된 TodoCard의 정보를 TodoCardDto 형태로 반환합니다.


deleteTodoCard(id: Long): 주어진 ID에 해당하는 TodoCard를 삭제하는 메서드입니다.


completeTodoCard(id: Long): 주어진 ID에 해당하는 TodoCard를 완료 상태로 변경하는 메서드입니다.

이게 있어야 Task에 있던게 완료

 

@Service
class TodoCardServiceImpl(
    val todoCardRepository: TodoCardRepository,
): TodoCardService {
    override fun createTodoCard(createTodoCardArguments: CreateTodoCardArguments): TodoCardDto {
        val savedTodoCard = todoCardRepository.save(createTodoCardArguments.to())

        return TodoCardDto.from(savedTodoCard)
    }

    override fun findById(id: Long): RetrieveTodoCardDto? {
        val foundTodoCard = todoCardRepository.findByIdOrNull(id)

        return foundTodoCard?.let { RetrieveTodoCardDto.from(it) }
    }

    override fun findAll(authorName: String?, sort: String?): List<TodoCardDto> {
        authorName?.let {
            return todoCardRepository.findAllByAuthorName(authorName)
                .map { TodoCardDto.from(it) }
        }

        return if (sort == "createdAt") {
            todoCardRepository.findAllByOrderByCreatedAtAsc()
        } else {
            todoCardRepository.findAllByOrderByCreatedAtDesc()
        }.map { TodoCardDto.from(it) }
    }

    override fun updateTodoCard(todoCardArguments: UpdateTodoCardArguments): TodoCardDto {
        val savedTodoCard = todoCardRepository.save(todoCardArguments.to())

        return TodoCardDto.from(savedTodoCard)
    }

    override fun deleteTodoCard(id: Long) {
        todoCardRepository.deleteById(id)
    }

    override fun completeTodoCard(id: Long) {
        val targetTodoCard = todoCardRepository.findByIdOrNull(id)

        targetTodoCard?.let {
            it.complete()
            todoCardRepository.save(it)
        }
    }
}

createTodoCard 함수는 CreateTodoCardArguments를 받아와 해당 인자를 사용하여 TodoCardDto를 생성하고 저장한 뒤 반환합니다.

findById 함수는 주어진 id를 사용하여 TodoCard를 찾아서 RetrieveTodoCardDto로 변환하여 반환합니다.

findAll 함수는 authorName과 sort를 인자로 받습니다. authorName이 주어지면 해당 작성자(authorName)의 모든 TodoCard를 찾아서 TodoCardDto 리스트로 반환합니다. authorName이 주어지지 않은 경우에는 sort 값에 따라 모든 TodoCard를 createdAt 기준으로 오름차순 또는 내림차순으로 정렬하여 TodoCardDto 리스트로 반환합니다.

updateTodoCard 함수는 UpdateTodoCardArguments를 받아와 해당 인자를 사용하여 TodoCard를 업데이트한 뒤 저장하고, 업데이트된 TodoCardDto를 반환합니다.

deleteTodoCard 함수는 주어진 id에 해당하는 TodoCard를 삭제합니다.

completeTodoCard 함수는 주어진 id에 해당하는 TodoCard를 찾아서 완료 상태로 변경한 뒤 저장합니다.

 

@Entity
class Reply(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column
    var content: String,
    @Column
    val authorName: String,
    @Column
    val password: String,
    @ManyToOne
    var todoCard: TodoCard,
) {
    fun changeContent(content: String) {
        this.content = content
    }

    fun checkAuthentication(authorName: String, password: String) {
        if (authorName != this.authorName) {
            throw Exception("wrong authentication for reply")
        }

        if (password != this.password) {
            throw Exception("wrong authentication for reply")
        }
    }
}

 

Reply 클래스는 다음과 같은 속성을 가지고 있습니다:

id: 답변의 고유 식별자입니다. 자동으로 생성되고, 기본값은 null입니다.
content: 답변의 내용을 나타냅니다.
authorName: 답변의 작성자 이름을 나타냅니다.
password: 답변의 작성자 인증을 위한 비밀번호입니다.
todoCard: 해당 답변이 속한 TodoCard를 나타냅니다. TodoCard와의 관계는 다대일 관계입니다.
Reply 클래스에는 다음과 같은 함수가 있습니다:

changeContent(content: String): 답변의 내용을 변경하는 함수입니다. 주어진 content로 내용을 변경합니다.
checkAuthentication(authorName: String, password: String): 작성자 이름과 비밀번호를 확인하여 인증을 수행하는 함수입니다. 주어진 작성자 이름과 비밀번호가 현재 답변의 작성자 이름과 비밀번호와 일치하지 않으면 예외를 발생시킵니다.

 

@RequestMapping("/api/v1/replies")
@RestController
class ReplyController(
    val replyService: ReplyService,
) {
    @PostMapping
    fun createReply(
        @RequestBody createReplyArguments: CreateReplyArguments,
    ): ResponseEntity<ReplyDto> {
        val result = replyService.createReply(createReplyArguments)

        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(result)
    }

    @PutMapping("/{replyId}")
    fun updateReply(
        @PathVariable replyId: Long,
        @RequestBody updateReplyArguments: UpdateReplyArguments,
    ): ResponseEntity<ReplyDto> {
        val arguments = UpdateReplyArguments(
            id = replyId,
            content = updateReplyArguments.content,
            authorName = updateReplyArguments.authorName,
            password = updateReplyArguments.password,
        )
        val reply = replyService.updateReply(arguments)

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(reply)
    }

    @DeleteMapping("/{replyId}")
    fun deleteReply(
        @PathVariable replyId: Long,
        @RequestBody deleteReplyArguments: DeleteReplyArguments,
    ): ResponseEntity<Unit> {
        val arguments = DeleteReplyArguments(
            id = replyId,
            authorName = deleteReplyArguments.authorName,
            password = deleteReplyArguments.password,
        )

        replyService.deleteReply(arguments)

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(null)
    }
}

 

  1. createReply 메서드: POST /api/v1/replies
    • 요청 본문(@RequestBody)에는 CreateReplyArguments 객체가 포함되어야 합니다.
    • replyService.createReply를 호출하여 새로운 댓글을 생성하고, 생성된 결과(ReplyDto)를 ResponseEntity에 담아 반환합니다.
    • HTTP 상태 코드는 201(CREATED)을 사용합니다.
  2. updateReply 메서드: PUT /api/v1/replies/{replyId}
    • {replyId}는 경로 변수로, 업데이트할 댓글의 고유 식별자입니다.
    • 요청 본문(@RequestBody)에는 UpdateReplyArguments 객체가 포함되어야 합니다.
    • replyService.updateReply를 호출하여 댓글을 업데이트하고, 업데이트된 결과(ReplyDto)를 ResponseEntity에 담아 반환합니다.
    • HTTP 상태 코드는 200(OK)을 사용합니다.
  3. deleteReply 메서드: DELETE /api/v1/replies/{replyId}
    • {replyId}는 경로 변수로, 삭제할 댓글의 고유 식별자입니다.
    • 요청 본문(@RequestBody)에는 DeleteReplyArguments 객체가 포함되어야 합니다.
    • replyService.deleteReply를 호출하여 댓글을 삭제합니다.
    • 삭제 후에는 ResponseEntity에 빈 본문(null)과 함께 200(OK) 상태 코드를 반환합니다.

 

interface ReplyRepository : JpaRepository<Reply, Long> {

 

JpaRepository는 Spring Data JPA에서 제공하는 인터페이스로, 데이터베이스와 상호작용하는 CRUD (Create, Read, Update, Delete) 작업을 수행할 수 있습니다.

ReplyRepository는 Reply 엔티티의 데이터베이스 작업을 위해 다양한 메서드를 상속받고 있습니다. 예를 들면:

findById(id: Long): Optional<Reply>: 주어진 id에 해당하는 Reply 엔티티를 조회합니다. Optional을 사용하여 결과가 null인 경우를 처리할 수 있습니다.
save(reply: Reply): Reply: 주어진 Reply 엔티티를 저장하거나 업데이트합니다.
deleteById(id: Long): Unit: 주어진 id에 해당하는 Reply 엔티티를 삭제합니다.

 

interface ReplyService {
    fun createReply(createReplyArguments: CreateReplyArguments): ReplyDto
    fun updateReply(updateReplyArguments: UpdateReplyArguments): ReplyDto
    fun deleteReply(deleteReplyArguments: DeleteReplyArguments)
}

ReplyService는 댓글(Reply)에 대한 생성, 업데이트, 삭제와 관련된 작업을 정의하고 있습니다. 각 메서드는 특정 인자를 받아와 해당 작업을 수행한 후, 결과를 ReplyDto 형태로 반환하거나 void로 처리합니다.

  1. createReply 메서드: 새로운 댓글 생성
    • createReplyArguments를 인자로 받아와서 새로운 댓글을 생성합니다.
    • 생성된 댓글을 ReplyDto 형태로 반환합니다.
  2. updateReply 메서드: 댓글 업데이트
    • updateReplyArguments를 인자로 받아와서 해당하는 댓글을 업데이트합니다.
    • 업데이트된 댓글을 ReplyDto 형태로 반환합니다.
  3. deleteReply 메서드: 댓글 삭제
    • deleteReplyArguments를 인자로 받아와서 해당하는 댓글을 삭제합니다.
    • 반환값이 없습니다.

ReplyService 인터페이스를 구현하는 클래스에서는 이러한 메서드들을 구현하여 댓글 관련 작업을 처리할 수 있습니다. 댓글 생성, 업데이트, 삭제와 관련된 비즈니스 로직을 구현하고, 필요한 데이터베이스 작업을 수행하여 ReplyDto 형태로 결과를 반환하거나 데이터를 삭제합니다.

 

@Service
class ReplyServiceImpl(
    val replyRepository: ReplyRepository,
    val todoCardRepository: TodoCardRepository,
) : ReplyService {
    override fun createReply(createReplyArguments: CreateReplyArguments): ReplyDto {
        val targetTodoCard = todoCardRepository.findByIdOrNull(createReplyArguments.todoCardId)
            ?: throw Exception("target todo card is not found")

        val reply = Reply(
            content = createReplyArguments.content,
            authorName = createReplyArguments.authorName,
            password = createReplyArguments.password,
            todoCard = targetTodoCard,
        )

        val result = replyRepository.save(reply)

        return ReplyDto.from(result)
    }

    override fun updateReply(updateReplyArguments: UpdateReplyArguments): ReplyDto {
        val foundReply = updateReplyArguments.id?.let {
            replyRepository.findByIdOrNull(it)
        } ?: throw Exception("target reply is not found")

        foundReply.checkAuthentication(updateReplyArguments.authorName, updateReplyArguments.password)

        foundReply.changeContent(updateReplyArguments.content)

        replyRepository.save(foundReply)

        return ReplyDto.from(foundReply)
    }

    override fun deleteReply(deleteReplyArguments: DeleteReplyArguments) {
        val foundReply = deleteReplyArguments.id?.let {
            replyRepository.findByIdOrNull(it)
        } ?: throw Exception("target reply is not found")

        foundReply.checkAuthentication(deleteReplyArguments.authorName, deleteReplyArguments.password)

        replyRepository.deleteById(deleteReplyArguments.id)
    }
}

createReply 메서드
createReply 메서드는 CreateReplyArguments 인자를 받아서 ReplyDto를 반환합니다. createReplyArguments에는 content, authorName, password, todoCardId가 포함되어 있습니다. 이 메서드는 todoCardRepository에서 주어진 todoCardId에 해당하는 TodoCard를 찾고, Reply 객체를 생성하여 replyRepository에 저장한 다음, ReplyDto로 변환하여 반환합니다.

updateReply 메서드
updateReply 메서드는 UpdateReplyArguments 인자를 받아서 ReplyDto를 반환합니다. updateReplyArguments에는 id, content, authorName, password가 포함되어 있습니다. 이 메서드는 주어진 id를 사용하여 replyRepository에서 해당하는 Reply를 찾은 다음, 인증을 확인하고 content를 변경한 후 replyRepository에 저장합니다. 마지막으로 변경된 Reply를 ReplyDto로 변환하여 반환합니다.

deleteReply 메서드
deleteReply 메서드는 DeleteReplyArguments 인자를 받습니다. deleteReplyArguments에는 id, authorName, password가 포함되어 있습니다. 이 메서드는 주어진 id를 사용하여 replyRepository에서 해당하는 Reply를 찾은 다음, 인증을 확인한 후 replyRepository에서 삭제합니다.