Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.opensource.docgrid.domain.sync.controller;

import java.util.UUID;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.opensource.docgrid.domain.auth.annotation.CurrentUser;
import com.opensource.docgrid.domain.sync.dto.request.IgnoreSyncIssueRequest;
import com.opensource.docgrid.domain.sync.dto.request.RunSyncReconciliationRequest;
import com.opensource.docgrid.domain.sync.dto.response.SyncAdminActionResponse;
import com.opensource.docgrid.domain.sync.dto.response.SyncAdminSummaryResponse;
import com.opensource.docgrid.domain.sync.dto.response.SyncEventAdminResponse;
import com.opensource.docgrid.domain.sync.dto.response.SyncIssueAdminResponse;
import com.opensource.docgrid.domain.sync.dto.response.SyncReconciliationAdminResponse;
import com.opensource.docgrid.domain.sync.enums.SyncConsistencyIssueStatus;
import com.opensource.docgrid.domain.sync.enums.SyncConsistencyIssueType;
import com.opensource.docgrid.domain.sync.enums.SyncConsistencySeverity;
import com.opensource.docgrid.domain.sync.enums.SyncEventStatus;
import com.opensource.docgrid.domain.sync.enums.SyncEventType;
import com.opensource.docgrid.domain.sync.service.command.SyncAdminCommandService;
import com.opensource.docgrid.domain.sync.service.query.SyncAdminQueryService;
import com.opensource.docgrid.global.common.response.ApiResponse;
import com.opensource.docgrid.global.common.response.PageResponse;
import com.opensource.docgrid.global.common.response.ResponseUtils;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;

/**
* ADMIN 전용 Sync 운영 요약·Event·Issue 조회와 감사 가능한 재시도·복구·무시·검사 API를 제공한다.
*/
@Tag(name = "Admin - Sync", description = "관리자 전용 Outbox와 정합성 Reconciliation 운영 API")
@Validated
@RestController
@RequestMapping("/admin/sync")
@RequiredArgsConstructor
public class SyncAdminController {

private final SyncAdminQueryService syncAdminQueryService;
private final SyncAdminCommandService syncAdminCommandService;

@Operation(summary = "Sync 운영 요약 조회")
@GetMapping(value = "/summary", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<SyncAdminSummaryResponse>> getSummary() {
return ResponseUtils.ok(syncAdminQueryService.getSummary());
}

@Operation(summary = "Sync Event 목록 조회")
@GetMapping(value = "/events", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<PageResponse<SyncEventAdminResponse>>> getEvents(
@RequestParam(required = false) SyncEventStatus status,
@RequestParam(required = false) SyncEventType eventType,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
) {
return ResponseUtils.ok(syncAdminQueryService.getEvents(status, eventType, page, size));
}

@Operation(summary = "정합성 Issue 목록 조회")
@GetMapping(value = "/issues", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<PageResponse<SyncIssueAdminResponse>>> getIssues(
@RequestParam(required = false) SyncConsistencyIssueStatus status,
@RequestParam(required = false) SyncConsistencyIssueType issueType,
@RequestParam(required = false) SyncConsistencySeverity severity,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
) {
return ResponseUtils.ok(syncAdminQueryService.getIssues(status, issueType, severity, page, size));
}

@Operation(summary = "최종 실패 Sync Event 재시도")
@PostMapping(value = "/events/{eventId}/retry", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<SyncAdminActionResponse>> retryEvent(
@PathVariable UUID eventId,
@CurrentUser Long adminUserId
) {
return ResponseUtils.ok(syncAdminCommandService.retryEvent(eventId, adminUserId));
}

@Operation(summary = "정합성 Issue 안전 복구 요청")
@PostMapping(value = "/issues/{issueId}/repair", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<SyncAdminActionResponse>> repairIssue(
@PathVariable @Positive Long issueId,
@CurrentUser Long adminUserId
) {
return ResponseUtils.ok(syncAdminCommandService.repairIssue(issueId, adminUserId));
}

@Operation(summary = "정합성 Issue 무시")
@PostMapping(value = "/issues/{issueId}/ignore", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<SyncAdminActionResponse>> ignoreIssue(
@PathVariable @Positive Long issueId,
@CurrentUser Long adminUserId,
@RequestBody @Valid IgnoreSyncIssueRequest request
) {
return ResponseUtils.ok(syncAdminCommandService.ignoreIssue(
issueId,
adminUserId,
request.reason()
));
}

@Operation(summary = "Reconciliation Batch 수동 실행")
@PostMapping(value = "/reconcile", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<SyncReconciliationAdminResponse>> reconcile(
@CurrentUser Long adminUserId,
@RequestBody @Valid RunSyncReconciliationRequest request
) {
return ResponseUtils.ok(syncAdminCommandService.reconcile(
request.cursor(),
request.mode(),
adminUserId
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.opensource.docgrid.domain.sync.converter;

import org.springframework.stereotype.Component;

import com.opensource.docgrid.domain.sync.dto.response.SyncAdminActionResponse;
import com.opensource.docgrid.domain.sync.dto.response.SyncEventAdminResponse;
import com.opensource.docgrid.domain.sync.dto.response.SyncIssueAdminResponse;
import com.opensource.docgrid.domain.sync.entity.SyncAdminAction;
import com.opensource.docgrid.domain.sync.entity.SyncConsistencyIssue;
import com.opensource.docgrid.domain.sync.entity.SyncOutboxEvent;

/**
* Sync 운영 Entity를 내부 소유권·Payload·오류 메시지가 제거된 관리자 DTO로 변환한다.
*/
@Component
public class SyncAdminConverter {

public SyncEventAdminResponse toEventResponse(SyncOutboxEvent event) {
return new SyncEventAdminResponse(
event.getEventId(),
event.getIdempotencyKey(),
event.getAggregateType(),
event.getAggregateId(),
event.getAggregateVersion(),
event.getEventType(),
event.getStatus(),
event.getOccurredAt(),
event.getAvailableAt(),
event.getProcessedAt(),
event.getRetryCount(),
event.getMaxRetryCount(),
event.getLockedBy(),
event.getLockExpiresAt(),
event.getLastErrorCode()
);
}

public SyncIssueAdminResponse toIssueResponse(SyncConsistencyIssue issue) {
return new SyncIssueAdminResponse(
issue.getId(),
issue.getIssueKey(),
issue.getIssueType(),
issue.getSeverity(),
issue.getStatus(),
issue.getDocument() == null ? null : issue.getDocument().getId(),
issue.getDocumentVersion() == null ? null : issue.getDocumentVersion().getId(),
issue.getEmbeddingModel() == null ? null : issue.getEmbeddingModel().getId(),
issue.getExpectedJson(),
issue.getActualJson(),
issue.isRepairable(),
issue.getDetectedAt(),
issue.getLastDetectedAt(),
issue.getRepairEventId(),
issue.getRepairAttemptCount(),
issue.getResolvedAt(),
issue.getResolutionMessage()
);
}

public SyncAdminActionResponse toActionResponse(SyncAdminAction action) {
return new SyncAdminActionResponse(
action.getActionId(),
action.getActionType(),
action.getTargetType(),
action.getTargetId(),
action.getAdminUser().getId(),
action.getOccurredAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.opensource.docgrid.domain.sync.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/**
* 관리자가 자동 조치하지 않을 정합성 Issue에 남기는 감사 사유다.
*/
public record IgnoreSyncIssueRequest(
@NotBlank
@Size(max = 1000)
String reason
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.opensource.docgrid.domain.sync.dto.request;

import com.opensource.docgrid.domain.sync.enums.SyncReconciliationMode;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;

/**
* 관리자가 실행할 Reconciliation 모드와 시작 ID Cursor를 지정한다.
*/
public record RunSyncReconciliationRequest(
@NotNull
SyncReconciliationMode mode,

@PositiveOrZero
long cursor
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.opensource.docgrid.domain.sync.dto.response;

import java.time.LocalDateTime;
import java.util.UUID;

import com.opensource.docgrid.domain.sync.enums.SyncAdminActionType;
import com.opensource.docgrid.domain.sync.enums.SyncAdminTargetType;

/**
* 상태 변경 요청이 저장된 감사 Action 식별자와 실행자를 반환한다.
*/
public record SyncAdminActionResponse(
UUID actionId,
SyncAdminActionType actionType,
SyncAdminTargetType targetType,
String targetId,
Long adminUserId,
LocalDateTime occurredAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.opensource.docgrid.domain.sync.dto.response;

import java.time.LocalDateTime;

/**
* 관리자 Dashboard가 한 번에 조회하는 Outbox·Issue·Reconciliation 운영 Snapshot이다.
*/
public record SyncAdminSummaryResponse(
LocalDateTime capturedAt,
SyncEventSummaryResponse events,
SyncIssueSummaryResponse issues,
SyncReconciliationSummaryResponse reconciliation
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.opensource.docgrid.domain.sync.dto.response;

import java.time.LocalDateTime;
import java.util.UUID;

import com.opensource.docgrid.domain.sync.enums.SyncAggregateType;
import com.opensource.docgrid.domain.sync.enums.SyncEventStatus;
import com.opensource.docgrid.domain.sync.enums.SyncEventType;

/**
* 관리자에게 Payload와 오류 메시지를 제외하고 공개하는 Sync Event 운영 Snapshot이다.
*/
public record SyncEventAdminResponse(
UUID eventId,
String idempotencyKey,
SyncAggregateType aggregateType,
Long aggregateId,
Long aggregateVersion,
SyncEventType eventType,
SyncEventStatus status,
LocalDateTime occurredAt,
LocalDateTime availableAt,
LocalDateTime processedAt,
int retryCount,
int maxRetryCount,
String lockedBy,
LocalDateTime lockExpiresAt,
String lastErrorCode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.opensource.docgrid.domain.sync.dto.response;

import java.time.LocalDateTime;
import java.util.UUID;

/**
* Outbox Queue의 현재 적체와 최근 24시간 처리 품질 Snapshot이다.
*/
public record SyncEventSummaryResponse(
long pendingCount,
long processingCount,
long failedCount,
Long oldestPendingAgeSeconds,
long processedLast24hCount,
long failedLast24hCount,
long retriedLast24hCount,
double successRateLast24h,
UUID lastProcessedEventId,
LocalDateTime lastProcessedAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.opensource.docgrid.domain.sync.dto.response;

import java.time.LocalDateTime;
import java.util.UUID;

import com.opensource.docgrid.domain.sync.enums.SyncConsistencyIssueStatus;
import com.opensource.docgrid.domain.sync.enums.SyncConsistencyIssueType;
import com.opensource.docgrid.domain.sync.enums.SyncConsistencySeverity;

/**
* 관리자가 불일치 근거와 복구 생명주기를 판단하는 정합성 Issue 응답이다.
*/
public record SyncIssueAdminResponse(
Long issueId,
String issueKey,
SyncConsistencyIssueType issueType,
SyncConsistencySeverity severity,
SyncConsistencyIssueStatus status,
Long documentId,
Long documentVersionId,
Long embeddingModelId,
String expectedJson,
String actualJson,
boolean repairable,
LocalDateTime detectedAt,
LocalDateTime lastDetectedAt,
UUID repairEventId,
int repairAttemptCount,
LocalDateTime resolvedAt,
String resolutionMessage
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.opensource.docgrid.domain.sync.dto.response;

/**
* 정합성 Issue의 활성 상태와 최근 자동 복구 결과 Snapshot이다.
*/
public record SyncIssueSummaryResponse(
long openCount,
long repairingCount,
long autoResolvedLast24hCount,
long failedRepairCount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.opensource.docgrid.domain.sync.dto.response;

import java.util.UUID;

/**
* 수동 Reconciliation Batch 결과와 이를 추적할 감사 Action을 함께 반환한다.
*/
public record SyncReconciliationAdminResponse(
UUID runId,
long startCursor,
long endCursor,
int scannedCount,
int detectedCount,
int repairRequestedCount,
boolean hasMore,
SyncAdminActionResponse action
) {
}
Loading