-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#74] 빌릴게 대여 신청·반납 신청 API 추가 #75
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
1dc518a
aa5b2b7
4be5982
508b16b
383db16
66f9281
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,28 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.rental.request; | ||
|
|
||
| import jakarta.validation.constraints.Max; | ||
| import jakarta.validation.constraints.Min; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Positive; | ||
|
|
||
| public record RentalApplyRequest( | ||
| @NotNull(message = "대여할 물품을 선택해 주세요.") | ||
| Long itemId, | ||
|
|
||
| @NotNull(message = "대여 수량을 입력해 주세요.") | ||
| @Positive(message = "대여 수량은 1개 이상이어야 합니다.") | ||
| Integer count, | ||
|
|
||
| @NotNull(message = "대여 시작 시(hour)를 입력해 주세요.") | ||
| @Min(value = 0, message = "대여 시작 시(hour)는 0~23 사이여야 합니다.") | ||
| @Max(value = 23, message = "대여 시작 시(hour)는 0~23 사이여야 합니다.") | ||
| Integer rentAtHour, | ||
|
|
||
| @NotNull(message = "대여 시작 분(minute)을 입력해 주세요.") | ||
| @Min(value = 0, message = "대여 시작 분(minute)은 0~59 사이여야 합니다.") | ||
| @Max(value = 59, message = "대여 시작 분(minute)은 0~59 사이여야 합니다.") | ||
| Integer rentAtMinute, | ||
|
|
||
| @NotNull(message = "중복 대여 진행 여부를 입력해 주세요.") | ||
| Boolean ignoreDuplicate | ||
| ) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package kr.ac.kookmin.stream.api.app.welfare.rental.usecase; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import kr.ac.kookmin.stream.common.BusinessException; | ||
| import kr.ac.kookmin.stream.common.DateUtil; | ||
| import kr.ac.kookmin.stream.welfare.domain.fee.service.PayerService; | ||
| import kr.ac.kookmin.stream.welfare.domain.rental.domain.Item; | ||
| import kr.ac.kookmin.stream.welfare.domain.rental.domain.RentalErrorCode; | ||
| import kr.ac.kookmin.stream.welfare.domain.rental.service.ItemService; | ||
| import kr.ac.kookmin.stream.welfare.domain.rental.service.RentalHistoryService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| /** | ||
| * 대여 신청. {@code fee}(회비 납부 확인)와 {@code rental}(재고 차감, 이력 생성) 두 도메인을 조합한다. | ||
| * 대여 시간(영업시간·점심시간) 검증은 DB에 닿지 않으므로 이 트랜잭션에 들어오기 전, 컨트롤러에서 끝낸다. | ||
| */ | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RentalApplyUseCase { | ||
|
|
||
| private final PayerService payerService; | ||
| private final ItemService itemService; | ||
| private final RentalHistoryService rentalHistoryService; | ||
|
|
||
| @Transactional | ||
| public void apply(Long memberId, Long itemId, int count, int rentAtHour, int rentAtMinute, boolean ignoreDuplicate) { | ||
| // 동시성 보호(락) 없이 진행한다 — 재고 경쟁, 중복 대여 경쟁 둘 다 이론적으로 남아있는 | ||
| // 경합이다. 필요해지면 별도로 다시 도입한다(billilge-rental-apply-review-fixes.md 참고). | ||
| payerService.validatePayer(memberId); | ||
|
|
||
| if (!ignoreDuplicate && rentalHistoryService.existsActiveRental(itemId, memberId)) { | ||
| throw new BusinessException(RentalErrorCode.RENTAL_ITEM_DUPLICATED); | ||
| } | ||
|
|
||
| Item item = itemService.decreaseStock(itemId, count); | ||
|
|
||
| LocalDateTime now = LocalDateTime.now(DateUtil.KST); | ||
| LocalDateTime rentAt = LocalDate.now(DateUtil.KST).atTime(rentAtHour, rentAtMinute); | ||
|
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '48,75p' api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/welfare/rental/AppRentalApi.java
sed -n '26,45p' api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/welfare/rental/usecase/RentalApplyUseCase.java
sed -n '1,95p' core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/RentalTimeValidator.javaRepository: billilge/stream-server Length of output: 3502 🏁 Script executed: #!/bin/bash
set -o pipefail
printf '%s\n' '--- candidate files ---'
rg -l --glob '*.java' 'class RentalApplyRequest|record RentalApplyRequest|RentalTimeValidator|rentAtHour|applyRental|RentalApplyUseCase' api core | head -80
printf '%s\n' '--- request and controller/usecase bindings ---'
rg -n -C 6 --glob '*.java' 'RentalApplyRequest|RentalTimeValidator\.validate|rentalApplyUseCase\.apply|rentAtHour|rentAtMinute|applyRental' api core | head -260
printf '%s\n' '--- tests and documentation references ---'
rg -n -C 4 --glob '*Test.java' --glob '*.md' --glob '*.java' 'rentAt|대여 시간|대여 시각|RentalTimeValidator|INVALID_RENTAL_TIME' api core docs README.md 2>/dev/null | head -260Repository: billilge/stream-server Length of output: 41601 🏁 Script executed: #!/bin/bash
set -o pipefail
for f in $(rg -l --glob '*.java' 'class RentalApplyRequest|record RentalApplyRequest|RentalTimeValidator\.validate|RentalApplyUseCase' api core | head -30); do
echo "--- $f"
rg -n -C 8 'RentalApplyRequest|RentalTimeValidator\.validate|RentalApplyUseCase|rentAtHour|rentAtMinute|apply\(' "$f" | head -160
done
echo '--- all relevant tests ---'
rg -n -C 5 --glob '*Test.java' 'RentalTimeValidator|RentalApply|rentAtHour|rentAtMinute|rentAt' api core | head -240Repository: billilge/stream-server Length of output: 6801 현재 시각 이전의
요청한 시각이 현재 시각보다 이전이면 명시적 오류로 거부하세요. 다음 날 예약을 허용하려면 요청에 날짜를 포함하고 해당 날짜로 🤖 Prompt for AI Agents |
||
| rentalHistoryService.create(item, memberId, count, now, rentAt); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,7 @@ | ||||||||||||||||
| package kr.ac.kookmin.stream.welfare.domain.fee.service.impl; | ||||||||||||||||
|
|
||||||||||||||||
| import kr.ac.kookmin.stream.common.BusinessException; | ||||||||||||||||
| import kr.ac.kookmin.stream.welfare.domain.fee.domain.FeeErrorCode; | ||||||||||||||||
| import kr.ac.kookmin.stream.welfare.domain.fee.domain.Payer; | ||||||||||||||||
| import kr.ac.kookmin.stream.welfare.domain.fee.domain.TransferStatus; | ||||||||||||||||
| import kr.ac.kookmin.stream.welfare.domain.fee.repository.PayerRepository; | ||||||||||||||||
|
|
@@ -36,4 +38,12 @@ public void sync(Long memberId, String name, String studentId, TransferStatus st | |||||||||||||||
| private String enrollmentYearOf(String studentId) { | ||||||||||||||||
| return studentId.substring(0, 4); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // 조회 쿼리가 1개라 트랜잭션을 걸지 않는다(PR#67/#69 원칙 — 조회니까 붙이는 게 아니라 실제 이득이 있을 때만 붙인다). | ||||||||||||||||
| @Override | ||||||||||||||||
| public void validatePayer(Long memberId) { | ||||||||||||||||
| if (payerRepository.findByMemberId(memberId).isEmpty()) { | ||||||||||||||||
|
Comment on lines
+43
to
+45
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: rg -n 'readOnly|트랜잭션 경계|REQUIRES_NEW|Service 메서드' .github docs AGENTS.md 2>/dev/null | head -100
sed -n '35,50p' core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/service/impl/PayerServiceImpl.java
sed -n '68,94p' core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/impl/RentalHistoryServiceImpl.javaRepository: billilge/stream-server Length of output: 2264 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- coding-style transaction guidance ---'
sed -n '260,325p' docs/conventions/coding-style.md
sed -n '350,395p' docs/conventions/coding-style.md
printf '%s\n' '--- architecture transaction guidance ---'
sed -n '320,355p' docs/conventions/architecture.md
printf '%s\n' '--- PayerServiceImpl declaration and method ---'
cat -n core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/service/impl/PayerServiceImpl.java | sed -n '1,75p'
printf '%s\n' '--- RentalHistoryServiceImpl declaration and methods ---'
cat -n core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/impl/RentalHistoryServiceImpl.java | sed -n '1,110p'
printf '%s\n' '--- relevant callers and transaction annotations ---'
rg -n -C 5 'validatePayer|existsActiveRental|class .*ApplyUseCase|@Transactional' core/domain/welfare/src/main/java
printf '%s\n' '--- PR changes for the two methods ---'
git diff --unified=12 d4bbe1a01a2c1f878392f42f48b043b73ff7c887 66f9281fdbe4bbb4ce45fc161304ccafc26d8c73 -- \
core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/fee/service/impl/PayerServiceImpl.java \
core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/impl/RentalHistoryServiceImpl.javaRepository: billilge/stream-server Length of output: 41469 🤖 get_repo_knowledge executed:
Length of output: 7452 조회 전용 Service 메서드에
Suggested fix @Override
+ @Transactional(readOnly = true)
public void validatePayer(Long memberId) { @Override
+ @Transactional(readOnly = true)
public boolean existsActiveRental(Long itemId, Long memberId) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| throw new BusinessException(FeeErrorCode.MEMBER_IS_NOT_PAYER); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
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.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 2265
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 22174
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 6695
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 6695
중복 확인 전에 물품 행 잠금을 획득하세요.
AppRentalApi는ignoreDuplicate=true일 때 중복 거부를 건너뛴다고 문서화합니다. 현재RentalApplyUseCase.apply는ignoreDuplicate=false인 경우 잠금 없는existsActiveRental을 먼저 실행하고, 그 다음에decreaseStock과 이력 생성을 실행합니다. 두 요청이 동시에 실행되면 두 요청이 모두 중복 없음으로 판단한 뒤 각각 이력을 생성할 수 있습니다.재고 차감 시점에만 물품 잠금을 추가해도 이미 완료된 중복 확인은 직렬화되지 않습니다. 동일한 물품 행의 비관적 잠금을 중복 확인보다 먼저 획득하고 이력 생성까지 유지하세요.
ignoreDuplicate=true에서는 같은 잠금을 사용하되 중복 확인만 건너뛰어 여러 활성 이력을 허용하세요. 이 경로와 충돌하는 활성 회원·물품 조합의 무조건적인 고유 제약은 추가하지 마세요.🤖 Prompt for AI Agents