From 285d5a9f088d33f9da4391192eeae4490a6a3bc0 Mon Sep 17 00:00:00 2001 From: chanyoung Date: Sat, 8 Aug 2026 22:22:14 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(#15):=20=EB=8C=80=EC=B6=9C=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/library/bookitem/domain/BookItem.kt | 5 + .../bookitem/domain/BookItemRepository.kt | 8 ++ .../loan/application/CreateLoanService.kt | 111 ++++++++++++++++++ .../library/loan/controller/LoanController.kt | 42 +++++++ .../library/loan/domain/error/LoanError.kt | 15 +++ 5 files changed, 181 insertions(+) create mode 100644 src/main/kotlin/org/library/loan/application/CreateLoanService.kt create mode 100644 src/main/kotlin/org/library/loan/controller/LoanController.kt create mode 100644 src/main/kotlin/org/library/loan/domain/error/LoanError.kt diff --git a/src/main/kotlin/org/library/bookitem/domain/BookItem.kt b/src/main/kotlin/org/library/bookitem/domain/BookItem.kt index d3c59fe..c92e3f6 100644 --- a/src/main/kotlin/org/library/bookitem/domain/BookItem.kt +++ b/src/main/kotlin/org/library/bookitem/domain/BookItem.kt @@ -40,6 +40,11 @@ class BookItem( this.status = status } + fun markOnLoan() { + require(status == BookItemStatus.AVAILABLE) { "대출 가능한 상태가 아닙니다." } + status = BookItemStatus.ON_LOAN + } + companion object { val MANAGEMENT_NUMBER_REGEX = Regex("^[^-\\s]+-\\d+$") diff --git a/src/main/kotlin/org/library/bookitem/domain/BookItemRepository.kt b/src/main/kotlin/org/library/bookitem/domain/BookItemRepository.kt index 3c561af..218077c 100644 --- a/src/main/kotlin/org/library/bookitem/domain/BookItemRepository.kt +++ b/src/main/kotlin/org/library/bookitem/domain/BookItemRepository.kt @@ -1,8 +1,16 @@ package org.library.bookitem.domain +import jakarta.persistence.LockModeType import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param interface BookItemRepository : JpaRepository { fun findByManagementNumber(managementNumber: String): BookItem? + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select bi from BookItem bi where bi.managementNumber = :managementNumber") + fun findByManagementNumberForUpdate(@Param("managementNumber") managementNumber: String): BookItem? } diff --git a/src/main/kotlin/org/library/loan/application/CreateLoanService.kt b/src/main/kotlin/org/library/loan/application/CreateLoanService.kt new file mode 100644 index 0000000..7b48a16 --- /dev/null +++ b/src/main/kotlin/org/library/loan/application/CreateLoanService.kt @@ -0,0 +1,111 @@ +package org.library.loan.application + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import org.library.book.domain.BookRepository +import org.library.bookitem.domain.BookItemRepository +import org.library.bookitem.domain.BookItemStatus +import org.library.core.application.Result +import org.library.core.application.err +import org.library.core.application.ok +import org.library.loan.domain.Loan +import org.library.loan.domain.LoanRepository +import org.library.loan.domain.LoanStatus +import org.library.loan.domain.error.LoanError +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +class CreateLoanService( + private val bookItemRepository: BookItemRepository, + private val bookRepository: BookRepository, + private val loanRepository: LoanRepository, +) { + + @Transactional + fun execute(request: Request): Result { + val bookItem = bookItemRepository.findByManagementNumberForUpdate(request.managementNumber) + ?: return LoanError.BOOK_ITEM_NOT_FOUND.err() + + if (bookItem.status != BookItemStatus.AVAILABLE) { + return LoanError.BOOK_ITEM_NOT_AVAILABLE.err() + } + + val book = bookRepository.findByIdAndDeletedAtIsNull(bookItem.bookId) + ?: error("BookItem(${bookItem.id})이 가리키는 도서를 찾을 수 없습니다.") + + bookItem.markOnLoan() + + val loan = loanRepository.save( + Loan( + bookItemId = bookItem.id, + managementNumber = bookItem.managementNumber, + bookTitle = book.title, + borrowerName = request.borrowerName, + department = request.department, + borrowerEmail = request.borrowerEmail, + loanDate = request.loanDate, + ), + ) + return Response.from(loan).ok() + } + + @Schema(name = "CreateLoanRequest") + data class Request( + @field:NotBlank + @field:Schema(description = "대출할 소장본 관리번호", example = "기술-0001") + val managementNumber: String, + @field:NotBlank + @field:Schema(description = "대출자 이름", example = "홍길동") + val borrowerName: String, + @field:NotBlank + @field:Schema(description = "부서명", example = "총무과") + val department: String, + @field:NotNull + @field:Schema(description = "대여일", example = "2026-07-31") + val loanDate: LocalDate, + @field:Schema(description = "대출자 이메일", example = "hong@example.com", nullable = true) + val borrowerEmail: String? = null, + ) + + @Schema(name = "CreateLoanResponse") + data class Response( + @field:Schema(description = "대출 ID", example = "100") + val loanId: Long, + @field:Schema(description = "소장본 ID", example = "10") + val bookItemId: Long, + @field:Schema(description = "관리번호", example = "기술-0001") + val managementNumber: String, + @field:Schema(description = "도서명", example = "클린 코드") + val bookTitle: String, + @field:Schema(description = "대출자 이름", example = "홍길동") + val borrowerName: String, + @field:Schema(description = "부서명", example = "총무과") + val department: String, + @field:Schema(description = "대출자 이메일", example = "hong@example.com", nullable = true) + val borrowerEmail: String?, + @field:Schema(description = "대여일", example = "2026-07-31") + val loanDate: LocalDate, + @field:Schema(description = "반납 예정일", example = "2026-08-14") + val dueDate: LocalDate, + @field:Schema(description = "대출 상태", example = "ON_LOAN") + val status: LoanStatus, + ) { + companion object { + fun from(loan: Loan): Response = Response( + loanId = loan.id, + bookItemId = loan.bookItemId, + managementNumber = loan.managementNumber, + bookTitle = loan.bookTitle, + borrowerName = loan.borrowerName, + department = loan.department, + borrowerEmail = loan.borrowerEmail, + loanDate = loan.loanDate, + dueDate = loan.dueDate, + status = loan.status, + ) + } + } +} diff --git a/src/main/kotlin/org/library/loan/controller/LoanController.kt b/src/main/kotlin/org/library/loan/controller/LoanController.kt new file mode 100644 index 0000000..1e3b6a5 --- /dev/null +++ b/src/main/kotlin/org/library/loan/controller/LoanController.kt @@ -0,0 +1,42 @@ +package org.library.loan.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.library.core.application.getOrThrow +import org.library.core.swagger.ApiErrorCode +import org.library.loan.application.CreateLoanService +import org.library.loan.domain.error.LoanError +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Loan", description = "대출 관리 API") +@RestController +@RequestMapping("/loans") +class LoanController( + private val createLoanService: CreateLoanService, +) { + + @Operation( + summary = "대출 등록", + description = "관리번호로 소장본을 조회해 대출을 등록한다. 반납 예정일은 대여일 + 14일로 자동 계산되며, 등록 성공 시 소장본 상태가 ON_LOAN으로 바뀐다.", + ) + @ApiResponse( + responseCode = "201", + description = "대출 등록 성공", + content = [Content(schema = Schema(implementation = CreateLoanService.Response::class))], + ) + @ApiErrorCode(errorCodes = [LoanError::class], only = ["BOOK_ITEM_NOT_FOUND", "BOOK_ITEM_NOT_AVAILABLE"]) + @PostMapping + fun create(@Valid @RequestBody request: CreateLoanService.Request): ResponseEntity { + val loan = createLoanService.execute(request).getOrThrow() + return ResponseEntity.status(HttpStatus.CREATED).body(loan) + } +} diff --git a/src/main/kotlin/org/library/loan/domain/error/LoanError.kt b/src/main/kotlin/org/library/loan/domain/error/LoanError.kt new file mode 100644 index 0000000..f90204a --- /dev/null +++ b/src/main/kotlin/org/library/loan/domain/error/LoanError.kt @@ -0,0 +1,15 @@ +package org.library.loan.domain.error + +import org.library.core.exception.ErrorCode +import org.springframework.http.HttpStatus + +enum class LoanError( + override val status: HttpStatus, + override val message: String, +) : ErrorCode { + BOOK_ITEM_NOT_FOUND(HttpStatus.NOT_FOUND, "소장본을 찾을 수 없습니다."), + BOOK_ITEM_NOT_AVAILABLE(HttpStatus.CONFLICT, "대출할 수 없는 소장본입니다."), + ; + + override val code: String get() = name +} From abce765c4e0f37a23ba072c3e88f7b0734b18447 Mon Sep 17 00:00:00 2001 From: chanyoung Date: Sat, 8 Aug 2026 22:22:26 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(#15):=20=EB=B0=98=EB=82=A9=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/library/bookitem/domain/BookItem.kt | 5 ++ .../loan/application/ReturnLoanService.kt | 75 +++++++++++++++++++ .../library/loan/controller/LoanController.kt | 16 ++++ .../org/library/loan/domain/LoanRepository.kt | 7 ++ .../library/loan/domain/error/LoanError.kt | 2 + 5 files changed, 105 insertions(+) create mode 100644 src/main/kotlin/org/library/loan/application/ReturnLoanService.kt diff --git a/src/main/kotlin/org/library/bookitem/domain/BookItem.kt b/src/main/kotlin/org/library/bookitem/domain/BookItem.kt index c92e3f6..4ea1cec 100644 --- a/src/main/kotlin/org/library/bookitem/domain/BookItem.kt +++ b/src/main/kotlin/org/library/bookitem/domain/BookItem.kt @@ -45,6 +45,11 @@ class BookItem( status = BookItemStatus.ON_LOAN } + fun markAvailable() { + require(status == BookItemStatus.ON_LOAN) { "대출 중인 상태가 아닙니다." } + status = BookItemStatus.AVAILABLE + } + companion object { val MANAGEMENT_NUMBER_REGEX = Regex("^[^-\\s]+-\\d+$") diff --git a/src/main/kotlin/org/library/loan/application/ReturnLoanService.kt b/src/main/kotlin/org/library/loan/application/ReturnLoanService.kt new file mode 100644 index 0000000..0b04490 --- /dev/null +++ b/src/main/kotlin/org/library/loan/application/ReturnLoanService.kt @@ -0,0 +1,75 @@ +package org.library.loan.application + +import io.swagger.v3.oas.annotations.media.Schema +import org.library.bookitem.domain.BookItemRepository +import org.library.core.application.Result +import org.library.core.application.err +import org.library.core.application.ok +import org.library.loan.domain.Loan +import org.library.loan.domain.LoanRepository +import org.library.loan.domain.LoanStatus +import org.library.loan.domain.error.LoanError +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +class ReturnLoanService( + private val loanRepository: LoanRepository, + private val bookItemRepository: BookItemRepository, +) { + + @Transactional + fun execute(loanId: Long, request: Request): Result { + val loan = loanRepository.findByIdForUpdate(loanId) + ?: return LoanError.LOAN_NOT_FOUND.err() + + if (loan.status != LoanStatus.ON_LOAN) { + return LoanError.LOAN_NOT_ON_LOAN.err() + } + + val bookItem = bookItemRepository.findById(loan.bookItemId).orElse(null) + ?: error("Loan(${loan.id})이 가리키는 소장본을 찾을 수 없습니다.") + + loan.markReturned(request.returnedAt ?: LocalDate.now()) + bookItem.markAvailable() + + return Response.from(loan).ok() + } + + @Schema(name = "ReturnLoanRequest") + data class Request( + @field:Schema(description = "실제 반납일 (미지정 시 오늘 날짜)", example = "2026-08-20", nullable = true) + val returnedAt: LocalDate? = null, + ) + + @Schema(name = "ReturnLoanResponse") + data class Response( + @field:Schema(description = "대출 ID", example = "100") + val loanId: Long, + @field:Schema(description = "관리번호", example = "기술-0001") + val managementNumber: String, + @field:Schema(description = "대여일", example = "2026-07-31") + val loanDate: LocalDate, + @field:Schema(description = "반납 예정일", example = "2026-08-14") + val dueDate: LocalDate, + @field:Schema(description = "실제 반납일", example = "2026-08-20") + val returnedAt: LocalDate?, + @field:Schema(description = "대출 상태", example = "RETURNED") + val status: LoanStatus, + @field:Schema(description = "반납 지연 일수 (지연 없으면 null)", example = "6", nullable = true) + val overdueDays: Long?, + ) { + companion object { + fun from(loan: Loan): Response = Response( + loanId = loan.id, + managementNumber = loan.managementNumber, + loanDate = loan.loanDate, + dueDate = loan.dueDate, + returnedAt = loan.returnedAt, + status = loan.status, + overdueDays = loan.overdueDays(), + ) + } + } +} diff --git a/src/main/kotlin/org/library/loan/controller/LoanController.kt b/src/main/kotlin/org/library/loan/controller/LoanController.kt index 1e3b6a5..d537fd6 100644 --- a/src/main/kotlin/org/library/loan/controller/LoanController.kt +++ b/src/main/kotlin/org/library/loan/controller/LoanController.kt @@ -5,13 +5,16 @@ import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag +import io.swagger.v3.oas.annotations.Parameter import jakarta.validation.Valid import org.library.core.application.getOrThrow import org.library.core.swagger.ApiErrorCode import org.library.loan.application.CreateLoanService +import org.library.loan.application.ReturnLoanService import org.library.loan.domain.error.LoanError import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -22,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/loans") class LoanController( private val createLoanService: CreateLoanService, + private val returnLoanService: ReturnLoanService, ) { @Operation( @@ -39,4 +43,16 @@ class LoanController( val loan = createLoanService.execute(request).getOrThrow() return ResponseEntity.status(HttpStatus.CREATED).body(loan) } + + @Operation( + summary = "반납 처리", + description = "대출 건을 반납 처리한다. 실제 반납일을 지정하지 않으면 오늘 날짜로 기록되며, 소장본 상태가 AVAILABLE로 돌아간다.", + ) + @ApiErrorCode(errorCodes = [LoanError::class], only = ["LOAN_NOT_FOUND", "LOAN_NOT_ON_LOAN"]) + @PostMapping("/{loanId}/return") + fun returnLoan( + @Parameter(description = "대출 ID") @PathVariable loanId: Long, + @RequestBody(required = false) request: ReturnLoanService.Request?, + ): ReturnLoanService.Response = + returnLoanService.execute(loanId, request ?: ReturnLoanService.Request()).getOrThrow() } diff --git a/src/main/kotlin/org/library/loan/domain/LoanRepository.kt b/src/main/kotlin/org/library/loan/domain/LoanRepository.kt index 6672900..c53f281 100644 --- a/src/main/kotlin/org/library/loan/domain/LoanRepository.kt +++ b/src/main/kotlin/org/library/loan/domain/LoanRepository.kt @@ -1,7 +1,14 @@ package org.library.loan.domain +import jakarta.persistence.LockModeType import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param interface LoanRepository : JpaRepository { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select l from Loan l where l.id = :id") + fun findByIdForUpdate(@Param("id") id: Long): Loan? } diff --git a/src/main/kotlin/org/library/loan/domain/error/LoanError.kt b/src/main/kotlin/org/library/loan/domain/error/LoanError.kt index f90204a..00064ca 100644 --- a/src/main/kotlin/org/library/loan/domain/error/LoanError.kt +++ b/src/main/kotlin/org/library/loan/domain/error/LoanError.kt @@ -9,6 +9,8 @@ enum class LoanError( ) : ErrorCode { BOOK_ITEM_NOT_FOUND(HttpStatus.NOT_FOUND, "소장본을 찾을 수 없습니다."), BOOK_ITEM_NOT_AVAILABLE(HttpStatus.CONFLICT, "대출할 수 없는 소장본입니다."), + LOAN_NOT_FOUND(HttpStatus.NOT_FOUND, "대출 내역을 찾을 수 없습니다."), + LOAN_NOT_ON_LOAN(HttpStatus.CONFLICT, "이미 반납된 대출 건입니다."), ; override val code: String get() = name From c95e2daa9457dd2ca34ffdbdcd76870a33270306 Mon Sep 17 00:00:00 2001 From: chanyoung Date: Sat, 8 Aug 2026 22:22:41 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(#15):=20=EB=8C=80=EC=B6=9C=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EA=B2=80=EC=83=89=C2=B7=EB=AA=A9=EB=A1=9D=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loan/application/SearchLoansService.kt | 80 +++++++++++++++++++ .../library/loan/controller/LoanController.kt | 23 +++++- .../org/library/loan/domain/LoanRepository.kt | 19 +++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/library/loan/application/SearchLoansService.kt diff --git a/src/main/kotlin/org/library/loan/application/SearchLoansService.kt b/src/main/kotlin/org/library/loan/application/SearchLoansService.kt new file mode 100644 index 0000000..24208ab --- /dev/null +++ b/src/main/kotlin/org/library/loan/application/SearchLoansService.kt @@ -0,0 +1,80 @@ +package org.library.loan.application + +import io.swagger.v3.oas.annotations.media.Schema +import org.library.core.presentation.PageRequestParams +import org.library.core.presentation.Pagination +import org.library.loan.domain.Loan +import org.library.loan.domain.LoanRepository +import org.library.loan.domain.LoanStatus +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +@Transactional(readOnly = true) +class SearchLoansService( + private val loanRepository: LoanRepository, +) { + + fun execute( + bookTitle: String?, + borrowerName: String?, + department: String?, + status: LoanStatus?, + params: PageRequestParams, + ): Response { + val pageRequest = params.toPageRequest(Sort.by(Sort.Direction.DESC, "loanDate")) + val page = loanRepository.search(bookTitle, borrowerName, department, status, pageRequest) + val today = LocalDate.now() + return Response( + loans = page.content.map { LoanSummary.from(it, today) }, + pagination = Pagination.from(page), + ) + } + + @Schema(name = "LoanSummary") + data class LoanSummary( + @field:Schema(description = "대출 ID", example = "100") + val loanId: Long, + @field:Schema(description = "관리번호", example = "기술-0001") + val managementNumber: String, + @field:Schema(description = "도서명", example = "클린 코드") + val bookTitle: String, + @field:Schema(description = "대출자 이름", example = "홍길동") + val borrowerName: String, + @field:Schema(description = "부서명", example = "총무과") + val department: String, + @field:Schema(description = "대여일", example = "2026-07-31") + val loanDate: LocalDate, + @field:Schema(description = "반납 예정일", example = "2026-08-14") + val dueDate: LocalDate, + @field:Schema(description = "실제 반납일", example = "2026-08-20", nullable = true) + val returnedAt: LocalDate?, + @field:Schema(description = "대출 상태", example = "ON_LOAN") + val status: LoanStatus, + @field:Schema(description = "연체 여부 (대출 중이면서 반납 예정일이 지남)", example = "true") + val overdue: Boolean, + ) { + companion object { + fun from(loan: Loan, today: LocalDate): LoanSummary = LoanSummary( + loanId = loan.id, + managementNumber = loan.managementNumber, + bookTitle = loan.bookTitle, + borrowerName = loan.borrowerName, + department = loan.department, + loanDate = loan.loanDate, + dueDate = loan.dueDate, + returnedAt = loan.returnedAt, + status = loan.status, + overdue = loan.isOverdue(today), + ) + } + } + + @Schema(name = "LoanSearchResponse", description = "대출 내역 검색·목록 응답") + data class Response( + val loans: List, + val pagination: Pagination, + ) +} diff --git a/src/main/kotlin/org/library/loan/controller/LoanController.kt b/src/main/kotlin/org/library/loan/controller/LoanController.kt index d537fd6..5f54af5 100644 --- a/src/main/kotlin/org/library/loan/controller/LoanController.kt +++ b/src/main/kotlin/org/library/loan/controller/LoanController.kt @@ -1,23 +1,29 @@ package org.library.loan.controller import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag -import io.swagger.v3.oas.annotations.Parameter import jakarta.validation.Valid import org.library.core.application.getOrThrow +import org.library.core.presentation.PageRequestParams import org.library.core.swagger.ApiErrorCode import org.library.loan.application.CreateLoanService import org.library.loan.application.ReturnLoanService +import org.library.loan.application.SearchLoansService +import org.library.loan.domain.LoanStatus import org.library.loan.domain.error.LoanError +import org.springdoc.core.annotations.ParameterObject import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity +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.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @Tag(name = "Loan", description = "대출 관리 API") @@ -26,6 +32,7 @@ import org.springframework.web.bind.annotation.RestController class LoanController( private val createLoanService: CreateLoanService, private val returnLoanService: ReturnLoanService, + private val searchLoansService: SearchLoansService, ) { @Operation( @@ -55,4 +62,18 @@ class LoanController( @RequestBody(required = false) request: ReturnLoanService.Request?, ): ReturnLoanService.Response = returnLoanService.execute(loanId, request ?: ReturnLoanService.Request()).getOrThrow() + + @Operation( + summary = "대출 내역 검색·목록", + description = "도서명·대출자 이름·부서·상태로 대출 내역을 검색한다. 기본 정렬은 대여일 내림차순.", + ) + @GetMapping + fun search( + @Parameter(description = "도서명 부분 일치") @RequestParam(required = false) bookTitle: String?, + @Parameter(description = "대출자 이름 부분 일치") @RequestParam(required = false) borrowerName: String?, + @Parameter(description = "부서명 부분 일치") @RequestParam(required = false) department: String?, + @Parameter(description = "대출 상태") @RequestParam(required = false) status: LoanStatus?, + @ParameterObject params: PageRequestParams, + ): SearchLoansService.Response = + searchLoansService.execute(bookTitle, borrowerName, department, status, params) } diff --git a/src/main/kotlin/org/library/loan/domain/LoanRepository.kt b/src/main/kotlin/org/library/loan/domain/LoanRepository.kt index c53f281..d03279c 100644 --- a/src/main/kotlin/org/library/loan/domain/LoanRepository.kt +++ b/src/main/kotlin/org/library/loan/domain/LoanRepository.kt @@ -1,6 +1,8 @@ package org.library.loan.domain import jakarta.persistence.LockModeType +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query @@ -11,4 +13,21 @@ interface LoanRepository : JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select l from Loan l where l.id = :id") fun findByIdForUpdate(@Param("id") id: Long): Loan? + + @Query( + """ + select l from Loan l + where (:bookTitle is null or lower(l.bookTitle) like lower(concat('%', :bookTitle, '%'))) + and (:borrowerName is null or lower(l.borrowerName) like lower(concat('%', :borrowerName, '%'))) + and (:department is null or lower(l.department) like lower(concat('%', :department, '%'))) + and (:status is null or l.status = :status) + """, + ) + fun search( + @Param("bookTitle") bookTitle: String?, + @Param("borrowerName") borrowerName: String?, + @Param("department") department: String?, + @Param("status") status: LoanStatus?, + pageable: Pageable, + ): Page }