-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] 부스 예약 : QR 발급 실패 시 승인 롤백 보상 처리 구현 #27
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
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,25 @@ | ||
| package com.festivalapp.api; | ||
|
|
||
| import com.festivalapp.dto.auth.AuthResponse; | ||
| import com.festivalapp.dto.auth.SignupRequest; | ||
| import com.festivalapp.service.auth.AuthService; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| 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; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/admin/auth") | ||
| @RequiredArgsConstructor | ||
| public class AdminAuthController { | ||
|
|
||
| private final AuthService authService; | ||
|
|
||
| @PostMapping("/signup") | ||
| ResponseEntity<AuthResponse> signup(@Valid @RequestBody SignupRequest request) { | ||
| return ResponseEntity.ok(authService.signupAdmin(request)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| import org.springframework.security.crypto.password.PasswordEncoder; | ||
| import org.springframework.security.web.SecurityFilterChain; | ||
| import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
| import org.springframework.web.cors.CorsConfigurationSource; | ||
|
|
||
| @Configuration | ||
| @EnableWebSecurity | ||
|
|
@@ -23,10 +24,12 @@ | |
| public class SecurityConfig { | ||
|
|
||
| private final JwtAuthenticationFilter jwtAuthenticationFilter; | ||
| private final CorsConfigurationSource corsConfigurationSource; | ||
|
|
||
| @Bean | ||
| SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
| return http.csrf(AbstractHttpConfigurer::disable) | ||
| return http.cors(cors -> cors.configurationSource(corsConfigurationSource)) | ||
| .csrf(AbstractHttpConfigurer::disable) | ||
| .httpBasic(AbstractHttpConfigurer::disable) | ||
| .formLogin(AbstractHttpConfigurer::disable) | ||
| .sessionManagement( | ||
|
|
@@ -44,6 +47,8 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | |
| .permitAll() | ||
| .requestMatchers("/api/auth/signup", "/api/auth/login", "/api/health") | ||
| .permitAll() | ||
| .requestMatchers("/api/admin/auth/signup") | ||
| .permitAll() | ||
| .requestMatchers( | ||
| HttpMethod.GET, | ||
| "/api/booths", | ||
|
|
@@ -53,6 +58,8 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | |
| "/api/map-locations", | ||
| "/api/map-locations/**") | ||
| .permitAll() | ||
| .requestMatchers(HttpMethod.GET, "/api/booth-reservations/*") | ||
|
Collaborator
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. PR 설명에는 예약 조회를 로그인 계정 기준으로 변경했다고 되어 있는데, 단건 예 QR 스캔 확인 화면 때문에 공개 조회가 의도라면 PR 설명과 API 책임을 명확히 분 |
||
| .permitAll() | ||
| .requestMatchers("/api/admin/**") | ||
| .hasRole("ADMIN") | ||
| .anyRequest() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.festivalapp.dto; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public record BoothReservationCompensationLogResponse( | ||
| String step, | ||
| String reason, | ||
| String fromStatus, | ||
| String toStatus, | ||
| LocalDateTime createdAt) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package com.festivalapp.service; | ||
|
|
||
| import com.festivalapp.domain.booth.reservation.BoothReservation; | ||
| import com.festivalapp.domain.booth.reservation.BoothReservationStatus; | ||
| import com.festivalapp.dto.BoothReservationCompensationLogResponse; | ||
| import com.festivalapp.dto.BoothReservationResponse; | ||
| import com.festivalapp.dto.BoothReservationSagaLogResponse; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class BoothReservationApprovalResultFactory { | ||
|
|
||
| public BoothReservationSagaLogResponse approvalLog(String approverName, LocalDateTime approvedAt) { | ||
| return new BoothReservationSagaLogResponse( | ||
| "APPROVED", | ||
| approverName + "가 예약 신청을 승인했습니다.", | ||
| approvedAt); | ||
| } | ||
|
|
||
| public BoothReservationResponse qrIssued( | ||
| BoothReservation reservation, | ||
| BoothReservationSagaLogResponse approvalLog, | ||
| LocalDateTime issuedAt) { | ||
| return BoothReservationResponse.from( | ||
| reservation, | ||
| List.of( | ||
| approvalLog, | ||
| new BoothReservationSagaLogResponse( | ||
| "QR_ISSUED", | ||
| "QR 발급에 성공해 예약 완료 상태로 전환했습니다.", | ||
| issuedAt))); | ||
| } | ||
|
|
||
| public BoothReservationResponse qrIssueFailed( | ||
| BoothReservation reservation, | ||
| BoothReservationSagaLogResponse approvalLog, | ||
| String failureReason, | ||
| LocalDateTime failedAt) { | ||
| return BoothReservationResponse.from( | ||
| reservation, | ||
| List.of( | ||
| approvalLog, | ||
| new BoothReservationSagaLogResponse( | ||
| "QR_ISSUE_FAILED", | ||
| failureReason, | ||
| failedAt), | ||
| new BoothReservationSagaLogResponse( | ||
| "APPROVAL_COMPENSATED", | ||
| "QR 발급 실패로 승인 상태를 보상 처리했습니다.", | ||
| failedAt)), | ||
| compensationLogs(failureReason, failedAt)); | ||
| } | ||
|
|
||
| private List<BoothReservationCompensationLogResponse> compensationLogs( | ||
| String failureReason, | ||
| LocalDateTime failedAt) { | ||
| return List.of( | ||
| new BoothReservationCompensationLogResponse( | ||
| "APPROVAL_ROLLBACK", | ||
| failureReason, | ||
| BoothReservationStatus.APPROVED.name(), | ||
| BoothReservationStatus.QR_FAILED.name(), | ||
| failedAt), | ||
| new BoothReservationCompensationLogResponse( | ||
| "RETRY_REQUIRED", | ||
| "관리자 재승인이 필요합니다.", | ||
| BoothReservationStatus.QR_FAILED.name(), | ||
| BoothReservationStatus.QR_FAILED.name(), | ||
| failedAt)); | ||
| } | ||
| } |
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.
/api/admin/auth/signup이permitAll()로 열려 있어서 URL만 알면 누구나ADMIN 계정을 만들 수 있습니다. 일반 회원가입에서 관리자 선택을 제거했더라도,
이 공개 API로 관리자 권한을 발급받을 수 있어
/api/admin/**보호가 무력화될수 있습니다.
관리자 가입은 기존 관리자 권한 필요, 초대 토큰 검증, 로컬/개발 프로필 전용
등 별도 제한을 두는 게 안전해 보입니다.