Skip to content

Commit 463e242

Browse files
committed
2 parents c761302 + e6e0b72 commit 463e242

File tree

10 files changed

+585
-5
lines changed

10 files changed

+585
-5
lines changed

src/main/java/com/back/BackApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
56

67
@SpringBootApplication
8+
@EnableScheduling
79
public class BackApplication {
810

911
public static void main(String[] args) {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.back.domain.funding.controller;
2+
3+
import com.back.domain.funding.service.FundingStatusService;
4+
import com.back.global.rsData.RsData;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.tags.Tag;
7+
import jakarta.validation.constraints.Positive;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.access.prepost.PreAuthorize;
11+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
@RequestMapping("/api/fundings")
17+
@Tag(name = "펀딩 상태", description = "펀딩 상태 관리 API")
18+
public class FundingStatusController {
19+
20+
private final FundingStatusService fundingStatusService;
21+
22+
@PutMapping("/{id}/close")
23+
@PreAuthorize("isAuthenticated()")
24+
@Operation(summary = "펀딩 종료", description = "펀딩 수동 종료")
25+
public ResponseEntity<RsData<?>> closeFunding(
26+
@PathVariable @Positive Long id,
27+
@AuthenticationPrincipal(expression = "username") String userEmail) {
28+
fundingStatusService.closeFunding(id, userEmail);
29+
return ResponseEntity.ok(RsData.of("200", "펀딩이 종료되었습니다."));
30+
}
31+
32+
@PutMapping("/{id}/cancel")
33+
@PreAuthorize("isAuthenticated()")
34+
@Operation(summary = "펀딩 취소", description = "펀딩 취소")
35+
public ResponseEntity<RsData<?>> cancelFunding(
36+
@PathVariable @Positive Long id,
37+
@AuthenticationPrincipal(expression = "username") String userEmail) {
38+
fundingStatusService.cancelFunding(id, userEmail);
39+
return ResponseEntity.ok(RsData.of("200", "펀딩이 취소되었습니다."));
40+
}
41+
42+
@PutMapping("/{id}/finalize")
43+
@PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_ROOT')")
44+
@Operation(summary = "단일 펀딩 최종 처리", description = "펀딩 성공/실패 확정 (관리자 전용) ")
45+
public ResponseEntity<RsData<?>> finalizeFunding(
46+
@PathVariable @Positive Long id) {
47+
fundingStatusService.finalizeFunding(id);
48+
return ResponseEntity.ok(RsData.of("200", "펀딩이 최종 처리되었습니다."));
49+
}
50+
51+
@PutMapping("/finalize/all")
52+
@PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_ROOT')")
53+
@Operation(summary = "모든 펀딩 최종 처리", description = "모든 펀딩 성공/실패 확정 (관리자 전용) ")
54+
public ResponseEntity<RsData<?>> finalizeAllFundings() {
55+
fundingStatusService.finalizeAllClosedFundings();
56+
return ResponseEntity.ok(RsData.of("200", "모든 펀딩이 최종 처리되었습니다."));
57+
}
58+
59+
@PutMapping("/process/all")
60+
@PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_ROOT')")
61+
@Operation(summary = "모든 펀딩 통합 처리",
62+
description = "만료된 펀딩을 종료하고, 종료된 펀딩을 최종 처리합니다. (종료 + 최종 처리 한 번에)")
63+
public ResponseEntity<RsData<?>> processAllFundings() {
64+
var result = fundingStatusService.processAllFundings();
65+
66+
String message = String.format(
67+
"펀딩 통합 처리 완료 - 종료: %d건, 성공: %d건, 실패: %d건",
68+
result.closedCount(), result.successCount(), result.failedCount()
69+
);
70+
71+
return ResponseEntity.ok(
72+
RsData.of("200", message)
73+
);
74+
}
75+
}

src/main/java/com/back/domain/funding/entity/Funding.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1515
@AllArgsConstructor
1616
@Builder
17-
@Table(name = "fundings")
17+
@Table(name = "fundings", indexes = {
18+
@Index(name = "idx_funding_status_enddate",
19+
columnList = "status, end_date")})
1820
public class Funding extends BaseEntity {
1921

2022
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@@ -128,4 +130,40 @@ public static Funding create(
128130
}
129131
return f;
130132
}
133+
134+
// 펀딩 상태 관련 메서드
135+
public void close() {
136+
if (this.status != FundingStatus.OPEN) {
137+
throw new IllegalStateException("진행 중인 펀딩만 종료할 수 있습니다.");
138+
}
139+
this.status = FundingStatus.CLOSED;
140+
}
141+
142+
public void markAsSuccess() {
143+
if (this.status != FundingStatus.CLOSED) {
144+
throw new IllegalStateException("종료된 펀딩만 성공 처리할 수 있습니다.");
145+
}
146+
if (this.collectedAmount < this.targetAmount) {
147+
throw new IllegalStateException("목표 금액을 달성하지 못했습니다.");
148+
}
149+
this.status = FundingStatus.SUCCESS;
150+
}
151+
152+
public void markAsFailed() {
153+
if (this.status != FundingStatus.CLOSED) {
154+
throw new IllegalStateException("종료된 펀딩만 실패 처리할 수 있습니다.");
155+
}
156+
this.status = FundingStatus.FAILED;
157+
}
158+
159+
public void cancel() {
160+
if (this.status == FundingStatus.SUCCESS || this.status == FundingStatus.FAILED) {
161+
throw new IllegalStateException("이미 완료된 펀딩은 취소할 수 없습니다.");
162+
}
163+
this.status = FundingStatus.CANCELED;
164+
}
165+
166+
public boolean isEndDatePassed() {
167+
return LocalDateTime.now().isAfter(this.endDate);
168+
}
131169
}

src/main/java/com/back/domain/funding/repository/FundingRepository.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import com.back.domain.funding.entity.FundingStatus;
55
import org.springframework.data.domain.Page;
66
import org.springframework.data.domain.Pageable;
7-
import org.springframework.data.jpa.repository.EntityGraph;
8-
import org.springframework.data.jpa.repository.JpaRepository;
9-
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
10-
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.jpa.repository.*;
118
import org.springframework.data.repository.query.Param;
9+
import org.springframework.transaction.annotation.Transactional;
1210

11+
import java.time.LocalDateTime;
12+
import java.util.List;
1313
import java.util.Optional;
1414
import java.util.Set;
1515

@@ -71,5 +71,29 @@ Page<Funding> findFundingsByArtist(
7171
@Param("order") String order,
7272
Pageable pageable
7373
);
74+
75+
// 종료일이 지난 펀딩 조회
76+
@Query("SELECT COUNT(f) FROM Funding f " +
77+
"WHERE f.status = :status AND f.endDate < :now")
78+
long countByStatusAndEndDateBefore(
79+
@Param("status") FundingStatus status,
80+
@Param("now") LocalDateTime now
81+
);
82+
83+
// 만료된 펀딩 일괄 종료
84+
@Modifying
85+
@Transactional
86+
@Query("UPDATE Funding f SET f.status = 'CLOSED' " +
87+
"WHERE f.status = 'OPEN' AND f.endDate < :now")
88+
int bulkCloseExpiredFundings(@Param("now") LocalDateTime now);
89+
90+
// 특정 상태의 펀딩 전체 조회
91+
List<Funding> findByStatus(FundingStatus status);
92+
93+
// 종료된 펀딩 조회 (정합성 체크용)
94+
List<Funding> findByStatusAndEndDateBefore(
95+
FundingStatus status,
96+
LocalDateTime endDate
97+
);
7498
}
7599

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.back.domain.funding.scheduler;
2+
3+
import com.back.domain.funding.service.FundingStatusService;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.scheduling.annotation.Scheduled;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
@RequiredArgsConstructor
11+
@Slf4j
12+
public class FundingStatusScheduler {
13+
14+
private final FundingStatusService fundingStatusService;
15+
16+
// 매시간 정각에 종료일이 지난 펀딩을 CLOSED로 변경
17+
@Scheduled(cron = "0 0 * * * *")
18+
public void closeExpiredFundings() {
19+
long startTime = System.currentTimeMillis();
20+
21+
int closed = fundingStatusService.closeExpiredFundings();
22+
23+
long duration = System.currentTimeMillis() - startTime;
24+
25+
if (closed > 0) {
26+
log.info("[스케줄러] 펀딩 자동 종료 완료 - 처리: {}건, 소요: {}ms", closed, duration);
27+
28+
// 성능 모니터링: 5초 이상 걸리면 경고
29+
if (duration > 5000) {
30+
log.warn("[성능 경고] 펀딩 종료 처리 시간 초과: {}ms", duration);
31+
}
32+
}
33+
}
34+
35+
// 매시간 5분에 CLOSED 펀딩을 SUCCESS/FAILED로 최종 처리
36+
@Scheduled(cron = "0 5 * * * *")
37+
public void finalizeFundings() {
38+
long startTime = System.currentTimeMillis();
39+
40+
FundingStatusService.FinalizeResult result = fundingStatusService.finalizeAllClosedFundings();
41+
42+
long duration = System.currentTimeMillis() - startTime;
43+
44+
if (result.totalProcessed() > 0) {
45+
log.info("[스케줄러] 펀딩 최종 처리 완료 - 성공 펀딩: {}건, 실패 펀딩: {}건, 오류: {}건, 소요: {}ms",
46+
result.successCount(), result.failedCount(), result.errorCount(), duration);
47+
}
48+
}
49+
50+
/**
51+
* 매일 새벽 1시에 정합성 체크 (안전장치)
52+
* 혹시 놓친 펀딩이 있는지 확인
53+
*/
54+
@Scheduled(cron = "0 0 1 * * *")
55+
public void dailyStatusCheck() {
56+
int closed = fundingStatusService.checkAndCloseMissedFundings();
57+
58+
if (closed > 0) {
59+
log.warn("[스케줄러] 정합성 체크 - 누락 펀딩 {}건 처리 완료", closed);
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)