Skip to content
Closed
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
Expand Up @@ -9,9 +9,12 @@
import org.springframework.web.bind.annotation.RequestParam;

import in.koreatech.koin.admin.article.dto.AdminArticleAiSummariesResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryLogsResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryOverviewResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryResponse;
import in.koreatech.koin.domain.community.article.model.ArticleAiSummaryLogType;
import in.koreatech.koin.domain.community.article.model.ArticleAiSummaryStatus;
import in.koreatech.koin.domain.community.article.service.summary.ArticleSummaryFailureType;
import in.koreatech.koin.global.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -49,6 +52,23 @@ ResponseEntity<AdminArticleAiSummariesResponse> getSummaries(
@Auth(permit = {ADMIN}) Integer adminId
);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true)))
})
@Operation(summary = "게시글 AI 요약 작업 로그 조회")
@GetMapping("/admin/articles/ai-summaries/logs")
ResponseEntity<AdminArticleAiSummaryLogsResponse> getLogs(
@RequestParam(name = "page", defaultValue = "1") Integer page,
@RequestParam(name = "limit", defaultValue = "10", required = false) Integer limit,
@RequestParam(name = "summary_id", required = false) Integer summaryId,
@RequestParam(name = "article_id", required = false) Integer articleId,
@RequestParam(name = "event_type", required = false) ArticleAiSummaryLogType eventType,
@RequestParam(name = "failure_type", required = false) ArticleSummaryFailureType failureType,
@Auth(permit = {ADMIN}) Integer adminId
);

@ApiResponses(value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
import org.springframework.web.bind.annotation.RestController;

import in.koreatech.koin.admin.article.dto.AdminArticleAiSummariesResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryLogsResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryOverviewResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryResponse;
import in.koreatech.koin.admin.article.service.AdminArticleAiSummaryService;
import in.koreatech.koin.domain.community.article.model.ArticleAiSummaryLogType;
import in.koreatech.koin.domain.community.article.model.ArticleAiSummaryStatus;
import in.koreatech.koin.domain.community.article.service.summary.ArticleSummaryFailureType;
import in.koreatech.koin.global.auth.Auth;
import lombok.RequiredArgsConstructor;

Expand Down Expand Up @@ -41,6 +44,27 @@ public ResponseEntity<AdminArticleAiSummariesResponse> getSummaries(
return ResponseEntity.ok(adminArticleAiSummaryService.getSummaries(page, limit, status));
}

@Override
@GetMapping("/admin/articles/ai-summaries/logs")
public ResponseEntity<AdminArticleAiSummaryLogsResponse> getLogs(
@RequestParam(name = "page", defaultValue = "1") Integer page,
@RequestParam(name = "limit", defaultValue = "10", required = false) Integer limit,
@RequestParam(name = "summary_id", required = false) Integer summaryId,
@RequestParam(name = "article_id", required = false) Integer articleId,
@RequestParam(name = "event_type", required = false) ArticleAiSummaryLogType eventType,
@RequestParam(name = "failure_type", required = false) ArticleSummaryFailureType failureType,
@Auth(permit = {ADMIN}) Integer adminId
) {
return ResponseEntity.ok(adminArticleAiSummaryService.getLogs(
page,
limit,
summaryId,
articleId,
eventType,
failureType
));
}

@Override
@GetMapping("/admin/articles/ai-summaries/{summaryId}")
public ResponseEntity<AdminArticleAiSummaryResponse> getSummary(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package in.koreatech.koin.admin.article.dto;

import java.time.LocalDateTime;

public interface AdminArticleAiSummaryLogProjection {

Integer getLogId();

Integer getSummaryId();

Integer getArticleId();

Integer getBoardId();

String getArticleTitle();

String getEventType();

String getStatus();

String getFailureType();

String getMessage();

Integer getRetryCount();

LocalDateTime getNextAttemptAt();

String getWorkerId();

LocalDateTime getCreatedAt();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package in.koreatech.koin.admin.article.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.time.LocalDateTime;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(SnakeCaseStrategy.class)
public record AdminArticleAiSummaryLogResponse(
@Schema(description = "로그 ID", example = "1", requiredMode = REQUIRED)
Integer logId,

@Schema(description = "요약 작업 ID", example = "12")
Integer summaryId,

@Schema(description = "게시글 ID", example = "17291")
Integer articleId,

@Schema(description = "게시판 ID", example = "5")
Integer boardId,

@Schema(description = "게시글 제목")
String articleTitle,

@Schema(description = "로그 유형", example = "FAILED", requiredMode = REQUIRED)
String eventType,

@Schema(description = "로그 발생 시점의 요약 상태", example = "FAILED")
String status,

@Schema(description = "실패 유형", example = "RATE_LIMIT")
String failureType,

@Schema(description = "안전하게 마스킹된 메시지")
String message,

@Schema(description = "로그 발생 시점의 재시도 횟수", example = "2")
Integer retryCount,

@Schema(description = "다음 시도 가능 시각")
LocalDateTime nextAttemptAt,

@Schema(description = "작업자 ID")
String workerId,

@Schema(description = "로그 생성 시각", requiredMode = REQUIRED)
LocalDateTime createdAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package in.koreatech.koin.admin.article.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.util.List;

import org.springframework.data.domain.Page;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import in.koreatech.koin.common.model.Criteria;
import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(SnakeCaseStrategy.class)
public record AdminArticleAiSummaryLogsResponse(
@Schema(description = "게시글 AI 요약 로그 목록", requiredMode = REQUIRED)
List<AdminArticleAiSummaryLogResponse> logs,

@Schema(description = "총 로그 수", example = "57", requiredMode = REQUIRED)
Long totalCount,

@Schema(description = "현재 페이지에 포함된 로그 수", example = "10", requiredMode = REQUIRED)
Integer currentCount,

@Schema(description = "총 페이지 수", example = "6", requiredMode = REQUIRED)
Integer totalPage,

@Schema(description = "현재 페이지", example = "2", requiredMode = REQUIRED)
Integer currentPage
) {

public static AdminArticleAiSummaryLogsResponse of(
Page<AdminArticleAiSummaryLogResponse> pagedResult,
Criteria criteria
) {
return new AdminArticleAiSummaryLogsResponse(
pagedResult.getContent(),
pagedResult.getTotalElements(),
pagedResult.getContent().size(),
pagedResult.getTotalPages(),
criteria.getPage() + 1
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@
import org.springframework.transaction.annotation.Transactional;

import in.koreatech.koin.admin.article.dto.AdminArticleAiSummariesResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryLogProjection;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryLogResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryLogsResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryOverviewResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryProjection;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryQueueCountProjection;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryResponse;
import in.koreatech.koin.admin.article.exception.AdminArticleAiSummaryNotFoundException;
import in.koreatech.koin.common.model.Criteria;
import in.koreatech.koin.domain.community.article.model.ArticleAiSummaryLogType;
import in.koreatech.koin.domain.community.article.model.ArticleAiSummaryStatus;
import in.koreatech.koin.domain.community.article.repository.ArticleAiSummaryLogRepository;
import in.koreatech.koin.domain.community.article.repository.ArticleAiSummaryRepository;
import in.koreatech.koin.domain.community.article.service.summary.ArticleAiSummaryProperties;
import in.koreatech.koin.domain.community.article.service.summary.ArticleSummaryFailureType;
import in.koreatech.koin.domain.community.article.service.summary.ArticleSummaryFailureReasonSanitizer;
import lombok.RequiredArgsConstructor;

Expand All @@ -31,6 +37,7 @@
public class AdminArticleAiSummaryService {

private final ArticleAiSummaryRepository articleAiSummaryRepository;
private final ArticleAiSummaryLogRepository articleAiSummaryLogRepository;
private final ArticleAiSummaryProperties properties;
private final ArticleSummaryFailureReasonSanitizer failureReasonSanitizer;
private final Clock clock;
Expand Down Expand Up @@ -83,6 +90,28 @@ public AdminArticleAiSummariesResponse getSummaries(Integer page, Integer limit,
return AdminArticleAiSummariesResponse.of(summaries, criteria);
}

public AdminArticleAiSummaryLogsResponse getLogs(
Integer page,
Integer limit,
Integer summaryId,
Integer articleId,
ArticleAiSummaryLogType eventType,
ArticleSummaryFailureType failureType
) {
Criteria criteria = Criteria.of(page, limit);
PageRequest pageable = PageRequest.of(criteria.getPage(), criteria.getLimit());
Page<AdminArticleAiSummaryLogResponse> logs = articleAiSummaryLogRepository
.findAdminLogs(
summaryId,
articleId,
eventType == null ? null : eventType.name(),
failureType == null ? null : failureType.name(),
pageable
)
.map(this::toLogResponse);
return AdminArticleAiSummaryLogsResponse.of(logs, criteria);
}

public AdminArticleAiSummaryResponse getSummary(Integer summaryId) {
return articleAiSummaryRepository.findAdminSummaryById(summaryId)
.map(this::toResponse)
Expand Down Expand Up @@ -112,6 +141,25 @@ private AdminArticleAiSummaryResponse toResponse(AdminArticleAiSummaryProjection
);
}

private AdminArticleAiSummaryLogResponse toLogResponse(AdminArticleAiSummaryLogProjection log) {
String message = failureReasonSanitizer.sanitize(log.getMessage());
return new AdminArticleAiSummaryLogResponse(
log.getLogId(),
log.getSummaryId(),
log.getArticleId(),
log.getBoardId(),
log.getArticleTitle(),
log.getEventType(),
log.getStatus(),
log.getFailureType(),
message,
log.getRetryCount(),
log.getNextAttemptAt(),
log.getWorkerId(),
log.getCreatedAt()
);
}

private Long nullToZero(Long value) {
return value == null ? 0L : value;
}
Expand Down
Loading
Loading