Skip to content

Commit 0eb8228

Browse files
authored
Merge pull request #40 from Block-Guard/feat/#31/report-record-api
[Feat] 긴급대응 API
2 parents dc58a50 + 032cf95 commit 0eb8228

16 files changed

+630
-10
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.blockguard.server.domain.report.api;
2+
3+
import com.blockguard.server.domain.report.application.ReportRecordService;
4+
import com.blockguard.server.domain.report.dto.request.UpdateReportStepRequest;
5+
import com.blockguard.server.domain.report.dto.response.CurrentReportRecordResponse;
6+
import com.blockguard.server.domain.report.dto.response.ReportRecordResponse;
7+
import com.blockguard.server.domain.report.dto.response.ReportRecordStepResponse;
8+
import com.blockguard.server.domain.user.domain.User;
9+
import com.blockguard.server.global.common.codes.SuccessCode;
10+
import com.blockguard.server.global.common.response.BaseResponse;
11+
import com.blockguard.server.global.config.resolver.CurrentUser;
12+
import com.blockguard.server.global.config.swagger.CustomExceptionDescription;
13+
import com.blockguard.server.global.config.swagger.SwaggerResponseDescription;
14+
import io.swagger.v3.oas.annotations.Operation;
15+
import io.swagger.v3.oas.annotations.Parameter;
16+
import jakarta.validation.Valid;
17+
import lombok.RequiredArgsConstructor;
18+
import org.springframework.web.bind.annotation.*;
19+
20+
@RestController
21+
@RequiredArgsConstructor
22+
@RequestMapping("/api/report-records")
23+
public class ReportRecordApi {
24+
private final ReportRecordService reportRecordService;
25+
26+
@Operation(summary = "신고 현황 생성 API")
27+
@CustomExceptionDescription(SwaggerResponseDescription.CREATE_REPORT_FAIL)
28+
@PostMapping
29+
public BaseResponse<ReportRecordResponse> createReportRecord(@Parameter(hidden = true) @CurrentUser User user) {
30+
ReportRecordResponse reportRecordResponse = reportRecordService.createReportRecord(user);
31+
return BaseResponse.of(SuccessCode.CREATE_REPORT_RECORD_SUCCESS, reportRecordResponse);
32+
}
33+
34+
@Operation(summary = "진행중인 신고 현황 조회 API")
35+
@CustomExceptionDescription(SwaggerResponseDescription.INVALID_TOKEN)
36+
@GetMapping("/current")
37+
public BaseResponse<CurrentReportRecordResponse> getCurrentRecord(@Parameter(hidden = true) @CurrentUser User user) {
38+
CurrentReportRecordResponse currentReportRecordResponse = reportRecordService.getCurrentRecord(user);
39+
return BaseResponse.of(SuccessCode.CURRENT_REPORT_FOUND, currentReportRecordResponse);
40+
}
41+
42+
@Operation(summary = "신고 현황의 특정 단계 정보 조회 API")
43+
@CustomExceptionDescription(SwaggerResponseDescription.GET_STEP_INFO_FAIL)
44+
@GetMapping("/{reportId}/steps/{stepNumber}")
45+
public BaseResponse<ReportRecordStepResponse> getStepInfo(
46+
@Parameter(hidden = true) @CurrentUser User user,
47+
@PathVariable Long reportId,
48+
@PathVariable int stepNumber
49+
) {
50+
ReportRecordStepResponse reportRecordStepResponse = reportRecordService.getStepInfo(user, reportId, stepNumber);
51+
return BaseResponse.of(SuccessCode.STEP_INFO_FOUND, reportRecordStepResponse);
52+
}
53+
54+
@Operation(summary = "신고 현황 수정 API")
55+
@CustomExceptionDescription(SwaggerResponseDescription.UPDATE_STEP_INFO_FAIL)
56+
@PutMapping("/{reportId}/steps/{stepNumber}")
57+
public BaseResponse<ReportRecordStepResponse> updateStepInfo(
58+
@Parameter(hidden = true) @CurrentUser User user,
59+
@PathVariable Long reportId,
60+
@PathVariable int stepNumber,
61+
@Valid @RequestBody UpdateReportStepRequest updateReportStepRequest
62+
) {
63+
ReportRecordStepResponse reportRecordStepResponse = reportRecordService.updateStepInfo(user, reportId, stepNumber, updateReportStepRequest);
64+
return BaseResponse.of(SuccessCode.UPDATE_STEP_INFO_SUCCESS, reportRecordStepResponse);
65+
}
66+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package com.blockguard.server.domain.report.application;
2+
3+
import com.blockguard.server.domain.report.dao.UserReportRecordRepository;
4+
import com.blockguard.server.domain.report.domain.ReportStepCheckbox;
5+
import com.blockguard.server.domain.report.domain.ReportStepProgress;
6+
import com.blockguard.server.domain.report.domain.UserReportRecord;
7+
import com.blockguard.server.domain.report.domain.enums.CheckboxType;
8+
import com.blockguard.server.domain.report.domain.enums.ReportStep;
9+
import com.blockguard.server.domain.report.domain.enums.ReportStepCheckboxConfig;
10+
import com.blockguard.server.domain.report.dto.request.UpdateReportStepRequest;
11+
import com.blockguard.server.domain.report.dto.response.CurrentReportRecordResponse;
12+
import com.blockguard.server.domain.report.dto.response.ReportRecordResponse;
13+
import com.blockguard.server.domain.report.dto.response.ReportRecordStepResponse;
14+
import com.blockguard.server.domain.user.domain.User;
15+
import com.blockguard.server.global.common.codes.ErrorCode;
16+
import com.blockguard.server.global.exception.BusinessExceptionHandler;
17+
import lombok.RequiredArgsConstructor;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
import java.util.Comparator;
22+
import java.util.List;
23+
import java.util.Optional;
24+
25+
@Service
26+
@RequiredArgsConstructor
27+
public class ReportRecordService {
28+
private final UserReportRecordRepository userReportRecordRepository;
29+
30+
@Transactional
31+
public ReportRecordResponse createReportRecord(User user) {
32+
if (userReportRecordRepository.existsByUserAndIsCompletedFalse(user)) {
33+
throw new BusinessExceptionHandler(ErrorCode.REPORT_ALREADY_IN_PROGRESS);
34+
}
35+
36+
UserReportRecord userReportRecord = UserReportRecord.builder()
37+
.user(user)
38+
.build();
39+
40+
ReportStepProgress progress = ReportStepProgress.createWithDefaultCheckboxes(userReportRecord, ReportStep.STEP1);
41+
userReportRecord.addProgress(progress);
42+
userReportRecordRepository.save(userReportRecord);
43+
44+
return ReportRecordResponse.from(userReportRecord, progress);
45+
}
46+
47+
@Transactional
48+
public CurrentReportRecordResponse getCurrentRecord(User user) {
49+
Optional<CurrentReportRecordResponse> currentReportRecordResponse = userReportRecordRepository.findFirstByUserAndIsCompletedFalseOrderByCreatedAtDesc(user)
50+
.map(record -> {
51+
ReportStepProgress lastProgress = record.getProgressList().stream()
52+
.max(Comparator.comparing(ReportStepProgress::getStep))
53+
.orElseThrow();
54+
int stepNum = lastProgress.getStep().getStepNumber();
55+
return CurrentReportRecordResponse.builder()
56+
.reportId(record.getId())
57+
.createdAt(String.valueOf(record.getCreatedAt()))
58+
.step(stepNum)
59+
.build();
60+
61+
});
62+
return currentReportRecordResponse.orElse(null);
63+
}
64+
65+
@Transactional
66+
public ReportRecordStepResponse getStepInfo(User user, Long reportId, int stepNumber) {
67+
UserReportRecord record = getUserReportRecord(user, reportId);
68+
ReportStep step = getReportStep(stepNumber);
69+
ReportStepProgress progress = getReportStepProgress(record, step);
70+
71+
return buildReportRecordStepResponse(reportId, stepNumber, progress, record);
72+
}
73+
74+
@Transactional
75+
public ReportRecordStepResponse updateStepInfo(User user, Long reportId, int stepNumber, UpdateReportStepRequest updateReportStepRequest) {
76+
UserReportRecord record = getUserReportRecord(user, reportId);
77+
ReportStep step = getReportStep(stepNumber);
78+
ReportStepProgress progress = getReportStepProgress(record, step);
79+
80+
// 요청 유효성 검증
81+
validateUpdateStepInfo(updateReportStepRequest, progress, step);
82+
83+
// 체크박스 상태 업데이트
84+
updateCheckboxStates(updateReportStepRequest, progress);
85+
86+
// 현 단계가 완료되었을때 다음 단계 생성 또는 해당 신고 완료
87+
if (updateReportStepRequest.getIsCompleted()) {
88+
// step 완료 플레그 업데이트
89+
progress.setCompleted(true);
90+
createNextStepIfNeeded(stepNumber, record);
91+
}
92+
93+
userReportRecordRepository.save(record);
94+
95+
return buildReportRecordStepResponse(reportId, stepNumber, progress, record);
96+
}
97+
98+
private void validateUpdateStepInfo(UpdateReportStepRequest updateReportStepRequest, ReportStepProgress progress, ReportStep step) {
99+
// 이미 완료된 step 인지 검증
100+
validateProgressIsCompleted(progress);
101+
// 체스박스 수 검증
102+
validateCheckboxCounts(updateReportStepRequest, step);
103+
validateCompletionConsistency(updateReportStepRequest);
104+
}
105+
106+
private void validateProgressIsCompleted(ReportStepProgress progress) {
107+
if (progress.isCompleted()) {
108+
throw new BusinessExceptionHandler(ErrorCode.REPORT_STEP_ALREADY_COMPLETED);
109+
}
110+
}
111+
112+
private void createNextStepIfNeeded(int stepNumber, UserReportRecord record) {
113+
// 마지막 단계가 아니라면
114+
if (stepNumber < ReportStep.STEP4.getStepNumber()){
115+
116+
// 다음 스텝 존재 여부 검증
117+
ReportStep nextStep = ReportStep.from(stepNumber + 1).get();
118+
boolean exists = record.getProgressList().stream()
119+
.anyMatch(p -> p.getStep() == nextStep);
120+
121+
// 다음 스텝이 존재하지 않는다면 새롭게 생성
122+
if (!exists) {
123+
ReportStepProgress nextProgress = ReportStepProgress.createWithDefaultCheckboxes(record, nextStep);
124+
record.addProgress(nextProgress);
125+
}
126+
}
127+
// 마지막 STEP 이 완료된 것이라면
128+
else{
129+
record.setCompleted(true);
130+
}
131+
}
132+
133+
private void updateCheckboxStates(UpdateReportStepRequest updateReportStepRequest, ReportStepProgress progress) {
134+
progress.getCheckboxes().stream()
135+
.filter(cb -> cb.getType() == CheckboxType.REQUIRED)
136+
.sorted(Comparator.comparingInt(ReportStepCheckbox::getBoxIndex))
137+
.forEach((cb) ->
138+
cb.updateChecked(updateReportStepRequest.getCheckBoxes().get(cb.getBoxIndex()))
139+
);
140+
141+
if(updateReportStepRequest.getRecommendedCheckBoxes() != null){
142+
progress.getCheckboxes().stream()
143+
.filter(cb -> cb.getType() == CheckboxType.RECOMMENDED)
144+
.sorted(Comparator.comparingInt(ReportStepCheckbox::getBoxIndex))
145+
.forEach((cb) ->
146+
cb.updateChecked(updateReportStepRequest.getRecommendedCheckBoxes().get(cb.getBoxIndex()))
147+
);
148+
}
149+
}
150+
151+
private void validateCheckboxCounts(UpdateReportStepRequest request, ReportStep step) {
152+
ReportStepCheckboxConfig checkboxConfig = ReportStepCheckboxConfig.of(step);
153+
154+
// 필수 체크박스 검증
155+
boolean isRequiredCountInvalid = request.getCheckBoxes().size() != checkboxConfig.getRequiredCount();
156+
157+
// 권장 체크박스 개수 검증
158+
List<Boolean> rec = request.getRecommendedCheckBoxes();
159+
boolean isRecommendedCountInvalid;
160+
if (checkboxConfig.getRecommendedCount() == 0) {
161+
// 권장 개수가 0개면, rec는 null 또는 비어 있어야 함
162+
isRecommendedCountInvalid = rec != null && !rec.isEmpty();
163+
} else {
164+
// 권장 개수가 1개 이상이면, null 아니고 정확히 갯수 맞아야 함
165+
isRecommendedCountInvalid = (rec == null || rec.size() != checkboxConfig.getRecommendedCount());
166+
}
167+
if (isRequiredCountInvalid || isRecommendedCountInvalid) {
168+
throw new BusinessExceptionHandler(ErrorCode.INVALID_CHECKBOX_COUNT);
169+
}
170+
}
171+
172+
// 1) isCompleted=true 인데 하나라도 unchecked → 예외
173+
private void validateCompletionConsistency(UpdateReportStepRequest request) {
174+
boolean allRequiredChecked = request.getCheckBoxes().stream()
175+
.allMatch(Boolean::booleanValue); // 모두 true 인가
176+
if (!allRequiredChecked && request.getIsCompleted()) {
177+
throw new BusinessExceptionHandler(ErrorCode.INVALID_STEP_COMPLETION);
178+
}
179+
}
180+
181+
private ReportRecordStepResponse buildReportRecordStepResponse(Long reportId, int stepNumber, ReportStepProgress progress, UserReportRecord record) {
182+
List<Boolean> resultRequiredCheckboxes = getRequiredCheckboxes(progress, CheckboxType.REQUIRED);
183+
List<Boolean> resultRecommendedCheckboxes = getRequiredCheckboxes(progress, CheckboxType.RECOMMENDED);
184+
185+
resultRecommendedCheckboxes = resultRecommendedCheckboxes.isEmpty() ? null : resultRecommendedCheckboxes;
186+
187+
return ReportRecordStepResponse.builder()
188+
.reportId(reportId)
189+
.step(stepNumber)
190+
.checkBoxes(resultRequiredCheckboxes)
191+
.recommendedCheckBoxes(resultRecommendedCheckboxes)
192+
.createdAt(String.valueOf(record.getCreatedAt()))
193+
.isCompleted(progress.isCompleted())
194+
.build();
195+
}
196+
197+
private List<Boolean> getRequiredCheckboxes(ReportStepProgress progress, CheckboxType type) {
198+
return progress.getCheckboxes().stream()
199+
.filter(cb -> cb.getType() == type)
200+
.sorted(Comparator.comparingInt(ReportStepCheckbox::getBoxIndex))
201+
.map(ReportStepCheckbox::isChecked)
202+
.toList();
203+
}
204+
205+
private ReportStepProgress getReportStepProgress(UserReportRecord record, ReportStep step) {
206+
return record.getProgressList().stream()
207+
.filter(p -> p.getStep() == step)
208+
.findFirst()
209+
.orElseThrow(() -> new BusinessExceptionHandler(ErrorCode.INVALID_STEP));
210+
}
211+
212+
private ReportStep getReportStep(int stepNumber) {
213+
return ReportStep.from(stepNumber)
214+
.orElseThrow(() -> new BusinessExceptionHandler(ErrorCode.INVALID_STEP));
215+
}
216+
217+
private UserReportRecord getUserReportRecord(User user, Long reportId) {
218+
return userReportRecordRepository.findByIdAndUser(reportId, user)
219+
.orElseThrow(() -> new BusinessExceptionHandler(ErrorCode.REPORT_NOT_FOUND));
220+
}
221+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.blockguard.server.domain.report.dao;
2+
3+
import com.blockguard.server.domain.report.domain.UserReportRecord;
4+
import com.blockguard.server.domain.user.domain.User;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
import java.util.Optional;
8+
9+
public interface UserReportRecordRepository extends JpaRepository<UserReportRecord, Long> {
10+
boolean existsByUserAndIsCompletedFalse(User user);
11+
12+
Optional<UserReportRecord> findFirstByUserAndIsCompletedFalseOrderByCreatedAtDesc(User user);
13+
14+
Optional<UserReportRecord> findByIdAndUser(Long id, User user);
15+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.blockguard.server.domain.report.domain;
2+
3+
import com.blockguard.server.domain.report.domain.enums.CheckboxType;
4+
import com.blockguard.server.global.common.entity.BaseEntity;
5+
import jakarta.persistence.*;
6+
import lombok.*;
7+
8+
@Entity
9+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
10+
@AllArgsConstructor
11+
@Builder
12+
@Getter
13+
@Table(name = "report_step_checkboxes",
14+
uniqueConstraints = @UniqueConstraint(
15+
name = "uk_progress_type_index",
16+
columnNames = {"progress_id", "type", "box_index"}
17+
))
18+
public class ReportStepCheckbox extends BaseEntity {
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
private Long id; // 단일 Surrogate PK
23+
24+
@Enumerated(EnumType.STRING)
25+
@Column(name = "type", length = 20, nullable = false)
26+
private CheckboxType type;
27+
28+
@Column(name = "box_index", nullable = false)
29+
private int boxIndex; // 단계 내에서 몇 번째 체크박스인지
30+
31+
@Column(name = "is_checked", nullable = false)
32+
private boolean isChecked = false;
33+
34+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
35+
@JoinColumn(name = "progress_id", nullable = false, updatable = false)
36+
private ReportStepProgress stepProgress;
37+
38+
public static ReportStepCheckbox createRequiredCheckbox(ReportStepProgress reportStepProgress, int boxIndex) {
39+
return ReportStepCheckbox.builder()
40+
.stepProgress(reportStepProgress)
41+
.type(CheckboxType.REQUIRED)
42+
.boxIndex(boxIndex)
43+
.build();
44+
}
45+
46+
public static ReportStepCheckbox createRecommendedCheckbox(ReportStepProgress reportStepProgress, int boxIndex) {
47+
return ReportStepCheckbox.builder()
48+
.stepProgress(reportStepProgress)
49+
.type(CheckboxType.RECOMMENDED)
50+
.boxIndex(boxIndex)
51+
.build();
52+
}
53+
54+
public void updateChecked(boolean checked) {
55+
this.isChecked = checked;
56+
}
57+
}

0 commit comments

Comments
 (0)