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
@@ -0,0 +1,64 @@
package in.koreatech.koin.admin.article.controller;

import static in.koreatech.koin.domain.user.model.UserType.ADMIN;
import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import in.koreatech.koin.admin.article.dto.AdminArticleAiSummariesResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryOverviewResponse;
import in.koreatech.koin.admin.article.dto.AdminArticleAiSummaryResponse;
import in.koreatech.koin.domain.community.article.model.ArticleAiSummaryStatus;
import in.koreatech.koin.global.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "(Admin) ArticleAiSummary: 게시글 AI 요약", description = "관리자 권한으로 게시글 AI 요약 작업 상태를 조회한다")
public interface AdminArticleAiSummaryApi {

@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/overview")
ResponseEntity<AdminArticleAiSummaryOverviewResponse> getOverview(
@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")
ResponseEntity<AdminArticleAiSummariesResponse> getSummaries(
@RequestParam(name = "page", defaultValue = "1") Integer page,
@RequestParam(name = "limit", defaultValue = "10", required = false) Integer limit,
@RequestParam(name = "status", required = false) ArticleAiSummaryStatus status,
@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))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true)))
})
@Operation(summary = "게시글 AI 요약 작업 단건 조회")
@GetMapping("/admin/articles/ai-summaries/{summaryId}")
ResponseEntity<AdminArticleAiSummaryResponse> getSummary(
@Parameter(in = PATH) @PathVariable Integer summaryId,
@Auth(permit = {ADMIN}) Integer adminId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package in.koreatech.koin.admin.article.controller;

import static in.koreatech.koin.domain.user.model.UserType.ADMIN;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import in.koreatech.koin.admin.article.dto.AdminArticleAiSummariesResponse;
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.ArticleAiSummaryStatus;
import in.koreatech.koin.global.auth.Auth;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class AdminArticleAiSummaryController implements AdminArticleAiSummaryApi {

private final AdminArticleAiSummaryService adminArticleAiSummaryService;

@Override
@GetMapping("/admin/articles/ai-summaries/overview")
public ResponseEntity<AdminArticleAiSummaryOverviewResponse> getOverview(
@Auth(permit = {ADMIN}) Integer adminId
) {
return ResponseEntity.ok(adminArticleAiSummaryService.getOverview());
}

@Override
@GetMapping("/admin/articles/ai-summaries")
public ResponseEntity<AdminArticleAiSummariesResponse> getSummaries(
@RequestParam(name = "page", defaultValue = "1") Integer page,
@RequestParam(name = "limit", defaultValue = "10", required = false) Integer limit,
@RequestParam(name = "status", required = false) ArticleAiSummaryStatus status,
@Auth(permit = {ADMIN}) Integer adminId
) {
return ResponseEntity.ok(adminArticleAiSummaryService.getSummaries(page, limit, status));
}

@Override
@GetMapping("/admin/articles/ai-summaries/{summaryId}")
public ResponseEntity<AdminArticleAiSummaryResponse> getSummary(
@PathVariable Integer summaryId,
@Auth(permit = {ADMIN}) Integer adminId
) {
return ResponseEntity.ok(adminArticleAiSummaryService.getSummary(summaryId));
}
}
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 AdminArticleAiSummariesResponse(
@Schema(description = "게시글 AI 요약 작업 목록", requiredMode = REQUIRED)
List<AdminArticleAiSummaryResponse> summaries,

@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 AdminArticleAiSummariesResponse of(
Page<AdminArticleAiSummaryResponse> pagedResult,
Criteria criteria
) {
return new AdminArticleAiSummariesResponse(
pagedResult.getContent(),
pagedResult.getTotalElements(),
pagedResult.getContent().size(),
pagedResult.getTotalPages(),
criteria.getPage() + 1
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package in.koreatech.koin.admin.article.dto;

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

import java.util.List;

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 AdminArticleAiSummaryOverviewResponse(
@Schema(description = "상태별 작업 수", requiredMode = REQUIRED)
List<StatusCountResponse> statusCounts,

@Schema(description = "큐 처리 상태", requiredMode = REQUIRED)
QueueResponse queue,

@Schema(description = "요약 설정", requiredMode = REQUIRED)
ConfigResponse config
) {

@JsonNaming(SnakeCaseStrategy.class)
public record StatusCountResponse(
@Schema(description = "요약 상태", example = "WAIT", requiredMode = REQUIRED)
String status,

@Schema(description = "작업 수", example = "12", requiredMode = REQUIRED)
Long count
) {
}

@JsonNaming(SnakeCaseStrategy.class)
public record QueueResponse(
@Schema(description = "즉시 처리 가능한 WAIT 수", example = "4", requiredMode = REQUIRED)
Long readyWaitCount,

@Schema(description = "next_attempt_at 대기 중인 WAIT 수", example = "2", requiredMode = REQUIRED)
Long delayedWaitCount,

@Schema(description = "처리 중인 수", example = "1", requiredMode = REQUIRED)
Long processingCount,

@Schema(description = "lock이 만료된 PROCESSING 수", example = "0", requiredMode = REQUIRED)
Long expiredProcessingCount,

@Schema(description = "재시도 가능한 FAILED 수", example = "3", requiredMode = REQUIRED)
Long retryableFailedCount
) {
}

@JsonNaming(SnakeCaseStrategy.class)
public record ConfigResponse(
@Schema(description = "스케줄러 실행 간격(ms)", example = "60000", requiredMode = REQUIRED)
Long schedulerFixedDelayMs,

@Schema(description = "스케줄러 batch size", example = "5", requiredMode = REQUIRED)
Integer batchSize,

@Schema(description = "최대 재시도 횟수", example = "5", requiredMode = REQUIRED)
Integer maxRetryCount,

@Schema(description = "FAILED 재처리 시작 시각", example = "0", requiredMode = REQUIRED)
Integer failedRetryWindowStartHour,

@Schema(description = "FAILED 재처리 종료 시각", example = "4", requiredMode = REQUIRED)
Integer failedRetryWindowEndHour
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package in.koreatech.koin.admin.article.dto;

import java.time.LocalDateTime;

public interface AdminArticleAiSummaryProjection {

Integer getSummaryId();

Integer getArticleId();

Integer getBoardId();

String getArticleTitle();

String getStatus();

String getFailureReason();

Integer getRetryCount();

LocalDateTime getNextAttemptAt();

LocalDateTime getLockedUntil();

String getWorkerId();

LocalDateTime getCreatedAt();

LocalDateTime getUpdatedAt();

LocalDateTime getSummarizedAt();

LocalDateTime getSourceUpdatedAt();

String getModel();

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

public interface AdminArticleAiSummaryQueueCountProjection {

Long getReadyWaitCount();

Long getDelayedWaitCount();

Long getProcessingCount();

Long getExpiredProcessingCount();

Long getRetryableFailedCount();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 in.koreatech.koin.domain.community.article.service.summary.ArticleSummaryFailureType;
import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(SnakeCaseStrategy.class)
public record AdminArticleAiSummaryResponse(
@Schema(description = "요약 작업 ID", example = "1", requiredMode = REQUIRED)
Integer summaryId,

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

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

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

@Schema(description = "요약 상태", example = "FAILED", requiredMode = REQUIRED)
String status,

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

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

@Schema(description = "재시도 횟수", example = "2", requiredMode = REQUIRED)
Integer retryCount,

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

@Schema(description = "작업 잠금 만료 시각")
LocalDateTime lockedUntil,

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

@Schema(description = "생성 시각", requiredMode = REQUIRED)
LocalDateTime createdAt,

@Schema(description = "수정 시각", requiredMode = REQUIRED)
LocalDateTime updatedAt,

@Schema(description = "요약 성공 시각")
LocalDateTime summarizedAt,

@Schema(description = "원본 수정 시각")
LocalDateTime sourceUpdatedAt,

@Schema(description = "모델명")
String model,

@Schema(description = "프롬프트 버전")
String promptVersion
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package in.koreatech.koin.admin.article.dto;

public interface AdminArticleAiSummaryStatusCountProjection {

String getStatus();

Long getSummaryCount();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package in.koreatech.koin.admin.article.exception;

import in.koreatech.koin.global.exception.custom.DataNotFoundException;

public class AdminArticleAiSummaryNotFoundException extends DataNotFoundException {

private static final String DEFAULT_MESSAGE = "해당 게시글 AI 요약 작업을 찾을 수 없습니다.";

public AdminArticleAiSummaryNotFoundException(String message, String detail) {
super(message, detail);
}

public static AdminArticleAiSummaryNotFoundException withDetail(String detail) {
return new AdminArticleAiSummaryNotFoundException(DEFAULT_MESSAGE, detail);
}
}
Loading
Loading