diff --git a/src/main/java/com/eactive/apim/portal/approval/statemachine/ProcessingState.java b/src/main/java/com/eactive/apim/portal/approval/statemachine/ProcessingState.java
index cc08b9e..2b8d0b0 100644
--- a/src/main/java/com/eactive/apim/portal/approval/statemachine/ProcessingState.java
+++ b/src/main/java/com/eactive/apim/portal/approval/statemachine/ProcessingState.java
@@ -23,6 +23,9 @@ public class ProcessingState implements ApprovalState {
case DENY:
handleDeny(approval, options);
break;
+ case CANCEL:
+ handleCancel(approval, options);
+ break;
default:
logger.warn("Invalid event {} for ProcessingState in approval {}", event, approval.getId());
throw new UnsupportedOperationException("Invalid event for Processing state");
@@ -91,6 +94,38 @@ public class ProcessingState implements ApprovalState {
}
}
+ /**
+ * 신청자 취소. 심사중(PROCESSING)이라도 아직 아무도 승인하지 않은 경우에 한해 허용한다.
+ *
+ *
관리자가 결재 상세를 열어보기만 해도 REQUESTED → PROCESSING 으로 바뀌므로(admin
+ * {@code PortalApprovalManService.selectDetail}), 이 상태의 상당수는 실제로 결재가 진행된 것이
+ * 아니라 열람만 된 건이다. 그런 건이 취소 불가로 남으면 해당 인증키의 후속 신청(변경/해지)이
+ * 무기한 막힌다. 승인 이력이 하나라도 있으면 기존 정책대로 거절한다.
+ */
+ private void handleCancel(Approval approval, Map options) {
+ boolean anyApproved = approval.getApprovers().stream()
+ .anyMatch(approver -> ApproverStatus.APPROVED.equals(approver.getApproverStatus()));
+ if (anyApproved) {
+ logger.warn("Cancel rejected for approval {} - already approved by at least one approver", approval.getId());
+ throw new UnsupportedOperationException("Cannot cancel approval already approved by an approver");
+ }
+
+ String approverId = (String) options.getOrDefault(ApprovalConstants.APPROVER, "");
+ if (!isAuthorizedApprover(approval, approverId, options)) {
+ logger.warn("Unauthorized cancel attempt for approval {} by {}", approval.getId(), approverId);
+ throw new UnauthorizedApprovalException("Unauthorized approver");
+ }
+
+ approval.getApprovers().forEach(approver -> {
+ approver.setApproverStatus(ApproverStatus.CANCELED);
+ approver.setApprovedDate(LocalDateTime.now());
+ });
+ approval.setApprovalDate(LocalDateTime.now());
+ getApprovalListener(options).rollback(approval);
+ approval.setApprovalStatus(new CanceledState());
+ logger.info("Approval {} cancelled in Processing state by {}", approval.getId(), approverId);
+ }
+
@Override
public String toString() {
return ApprovalStatus.PROCESSING.toString();