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 @@ -73,6 +73,9 @@ public class CollectionDocument extends BaseEntity {
@Column(name = "added_at", nullable = false)
private LocalDateTime addedAt;

/**
* 문서를 컬렉션에 추가한 사용자와 시각을 포함한 유일한 N:M 연결을 생성한다.
*/
@Builder
public CollectionDocument(DocumentCollection collection, Document document, User addedBy, LocalDateTime addedAt) {
this.collection = collection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public class DocumentCollection extends BaseEntity {
@Column(name = "deleted_at")
private LocalDateTime deletedAt;

/**
* 소유자, 선택적 상위 컬렉션과 공개 범위를 가진 활성 문서 그룹을 생성한다.
*/
@Builder
public DocumentCollection(User owner, DocumentCollection parentCollection, String name, String description,
VisibilityType visibility, CollectionStatus status) {
Expand All @@ -93,11 +96,17 @@ public DocumentCollection(User owner, DocumentCollection parentCollection, Strin
this.status = status != null ? status : CollectionStatus.ACTIVE;
}

/**
* 컬렉션을 Soft-delete하고 삭제 시각을 기록한다.
*/
public void markDeleted(LocalDateTime deletedAt) {
this.status = CollectionStatus.DELETED;
this.deletedAt = deletedAt;
}

/**
* 컬렉션 공개 범위를 변경한다. 파생 권한 캐시 갱신은 호출 Service가 담당한다.
*/
public void updateVisibility(VisibilityType visibility) {
this.visibility = visibility;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
import lombok.Getter;
import lombok.Setter;

/**
* {@code document.upload} 설정에서 도메인 업로드 검증에 사용할 최대 파일 크기를 제공한다.
*
* <p>HTTP Multipart 수신 한도와 같은 기본값을 사용해 전송은 성공했지만 도메인 검증에서 예상과 다르게
* 거부되는 구성 차이를 줄인다.
*/
@Getter
@Setter
@Component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;

/**
* 로그인 사용자의 문서 목록·상세·버전·추출 본문·원본 파일·인덱싱 상태 조회 API를 제공한다.
*
* <p>HTTP 파라미터와 파일 응답 Header 조립만 담당하며, 문서 접근 권한과 조회 규칙은
* {@link DocumentQueryService} 및 {@link DocumentFileService}에 위임한다. Entity와 저장소 내부 위치는
* 외부에 노출하지 않는다.
*/
@Tag(name = "Document", description = "문서 관련 API")
@Validated
@RestController
Expand All @@ -48,6 +55,9 @@ public class DocumentQueryController {
private final DocumentQueryService documentQueryService;
private final DocumentFileService documentFileService;

/**
* 사용자가 읽을 수 있는 문서를 선택적 상태 조건과 페이지 단위로 조회한다.
*/
@Operation(
summary = "내 문서 목록 조회",
description = "로그인한 사용자가 읽을 수 있는 문서를 최신 등록순으로 조회합니다. "
Expand All @@ -65,6 +75,9 @@ public ResponseEntity<ApiResponse<PageResponse<DocumentSummaryResponse>>> getMyD
return ResponseUtils.ok(documentQueryService.getMyDocuments(userId, status, page, size));
}

/**
* 읽기 권한이 있는 문서의 Metadata와 현재 버전 요약을 조회한다.
*/
@Operation(
summary = "문서 상세 조회",
description = "문서 Metadata, 소유자와 현재 버전 정보를 조회합니다. 추출 본문과 원본 파일은 포함하지 않습니다. "
Expand All @@ -78,6 +91,9 @@ public ResponseEntity<ApiResponse<DocumentDetailResponse>> getDocumentDetail(
return ResponseUtils.ok(documentQueryService.getDocumentDetail(userId, documentId));
}

/**
* 읽기 권한이 있는 문서의 전체 버전과 각 인덱싱 상태를 최신 순으로 조회한다.
*/
@Operation(
summary = "문서 버전 전체 이력 조회",
description = "읽기 가능한 문서의 모든 버전을 최신 번호순으로 조회합니다. 현재 검색 버전 여부, "
Expand All @@ -91,6 +107,9 @@ public ResponseEntity<ApiResponse<List<DocumentVersionHistoryResponse>>> getDocu
return ResponseUtils.ok(documentQueryService.getDocumentVersions(userId, documentId));
}

/**
* 현재 검색 버전의 Chunk를 문서 순서로 복원한 추출 본문을 조회한다.
*/
@Operation(
summary = "문서 추출 본문 조회",
description = "현재 버전의 Chunk 중복을 제거하고 페이지·섹션 순서대로 복원한 정규화 Text 전체를 반환합니다. "
Expand All @@ -104,6 +123,9 @@ public ResponseEntity<ApiResponse<DocumentContentResponse>> getDocumentContent(
return ResponseUtils.ok(documentQueryService.getDocumentContent(userId, documentId));
}

/**
* 현재 문서 버전의 원본 파일을 브라우저 표시 또는 첨부 다운로드 응답으로 반환한다.
*/
@Operation(
summary = "문서 원본 파일 조회",
description = "현재 버전의 원본 PDF·DOCX·TXT 파일을 반환합니다. disposition은 inline 또는 attachment이며 "
Expand Down Expand Up @@ -131,6 +153,9 @@ public ResponseEntity<byte[]> getDocumentFile(
return ResponseEntity.ok().headers(headers).body(download.content());
}

/**
* 현재 검색 버전과 별도로 처리 중인 버전·Job 상태를 조회한다.
*/
@Operation(
summary = "문서 인덱싱 상태 조회",
description = "현재 검색 가능한 INDEXED 버전과 처리 중인 버전 및 임베딩 작업 상태를 함께 조회합니다. "
Expand All @@ -144,6 +169,9 @@ public ResponseEntity<ApiResponse<DocumentStatusResponse>> getDocumentStatus(
return ResponseUtils.ok(documentQueryService.getDocumentStatus(userId, documentId));
}

/**
* 저장된 Content-Type을 HTTP MediaType으로 변환하고 비어 있거나 잘못된 값은 안전한 Binary 형식으로 대체한다.
*/
private MediaType resolveMediaType(String contentType) {
if (!StringUtils.hasText(contentType)) {
return MediaType.APPLICATION_OCTET_STREAM;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

/**
* 최초 문서와 기존 문서의 새 버전 파일을 접수하는 Multipart HTTP API를 제공한다.
*
* <p>요청 Bean Validation과 HTTP 응답 변환만 담당하고, 파일 검증·저장소 작업·DB 트랜잭션 및
* 보상 삭제는 각 업로드 Facade에 위임한다. 파싱과 임베딩은 이 요청에서 실행하지 않는다.
*/
@Tag(name = "Document", description = "문서 관련 API")
@RestController
@RequestMapping("/api/documents")
Expand All @@ -33,6 +39,9 @@ public class DocumentUploadController {
private final DocumentUploadFacade documentUploadFacade;
private final DocumentVersionUploadFacade documentVersionUploadFacade;

/**
* 새 문서의 원본 파일과 Metadata를 접수하고 비동기 인덱싱 Job 생성 결과를 반환한다.
*/
@Operation(
summary = "문서 업로드 접수",
description = "TXT, Markdown, PDF 또는 DOCX 원본 파일을 저장하고 비동기 인덱싱 작업을 생성합니다. "
Expand All @@ -47,6 +56,9 @@ public ResponseEntity<ApiResponse<DocumentUploadResponse>> upload(
return ResponseUtils.created(documentUploadFacade.upload(userId, request));
}

/**
* 기존 문서에 새 원본 파일 버전을 접수하고 해당 버전의 인덱싱 Job 생성 결과를 반환한다.
*/
@Operation(
summary = "문서 새 버전 업로드 접수",
description = "기존 문서에 새 파일 버전을 등록하고 비동기 인덱싱 작업을 생성합니다. "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@
import com.opensource.docgrid.domain.document.enums.DocumentVersionStatus;
import com.opensource.docgrid.domain.document.repository.DocumentStatusProjection;

/**
* 문서 상태 Projection을 현재 검색 Version과 처리 중 Version이 구분된 API 응답으로 변환한다.
*
* <p>현재 Version은 실제 검색 가능한 INDEXED 상태일 때만 노출하고, 별도로 진행 중인 Version과 Job은
* processingVersion에 담아 기존 검색 가능 Version과 혼동하지 않게 한다.
*/
@Component
public class DocumentStatusConverter {

/** 문서 상태 조회 Projection을 공개 응답 계약으로 변환한다. */
public DocumentStatusResponse toResponse(DocumentStatusProjection projection) {
// 1. 현재 Version은 INDEXED가 확정된 경우에만 검색 기준 Version으로 응답한다.
CurrentVersionStatusResponse currentVersion = null;
if (projection.getCurrentVersionStatus() == DocumentVersionStatus.INDEXED) {
currentVersion = new CurrentVersionStatusResponse(
Expand All @@ -20,6 +28,7 @@ public DocumentStatusResponse toResponse(DocumentStatusProjection projection) {
);
}

// 2. 업로드·인덱싱 중인 별도 Version이 있으면 Job 상태와 함께 처리 상태로 구성한다.
ProcessingVersionStatusResponse processingVersion = null;
if (projection.getProcessingVersionNo() != null) {
processingVersion = new ProcessingVersionStatusResponse(
Expand All @@ -29,6 +38,7 @@ public DocumentStatusResponse toResponse(DocumentStatusProjection projection) {
);
}

// 3. 문서 원장 상태와 두 Version 관점을 하나의 안정적인 상태 응답으로 조합한다.
return new DocumentStatusResponse(
projection.getDocumentId(),
projection.getDocumentStatus(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@
import com.opensource.docgrid.domain.document.entity.Document;
import com.opensource.docgrid.domain.document.entity.DocumentVersion;

/**
* Document 원장과 선택적 현재 Version을 문서 목록용 요약 응답으로 변환한다.
*
* <p>Entity 자체를 Controller에 노출하지 않고 소유자와 현재 검색 Version의 필요한 필드만 평탄화한다.
*/
@Component
public class DocumentSummaryConverter {

/** 문서와 현재 INDEXED Version 정보를 목록 응답으로 조합한다. */
public DocumentSummaryResponse toResponse(Document document) {
// 1. 아직 최초 인덱싱이 끝나지 않은 문서에는 현재 Version이 없을 수 있다.
DocumentVersion currentVersion = document.getCurrentVersion();

// 2. Transaction 안에서 필요한 LAZY 관계 값을 읽어 Entity와 분리된 응답을 완성한다.
return new DocumentSummaryResponse(
document.getId(),
document.getTitle(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

/**
* 새 문서 업로드 API가 받는 파일과 문서 메타데이터 입력이다.
*
* <p>파일 내용·확장자·크기 검증은 저장 전 FileValidationService가 담당하고, 이 요청은 HTTP 필수값과
* 제목 길이 같은 형식 검증만 선언한다.
*/
public record DocumentUploadRequest(
@NotNull
@Schema(description = "업로드할 TXT, Markdown, PDF 또는 DOCX 파일", type = "string", format = "binary")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

import jakarta.validation.constraints.NotNull;

/**
* 기존 문서에 새 Version으로 추가할 필수 Multipart 파일 입력이다.
*/
public record DocumentVersionUploadRequest(
@NotNull MultipartFile file
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

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

/**
* 문서가 현재 검색 대상으로 제공하는 INDEXED Version의 번호와 상태를 나타낸다.
*/
@Schema(description = "현재 검색 가능한 문서 버전 상태")
public record CurrentVersionStatusResponse(
@Schema(description = "문서 버전 번호") int versionNo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

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

/**
* 문서 원장 상태와 현재 검색 가능 Version 및 별도 처리 중 Version을 구분해 제공하는 응답이다.
*/
@Schema(description = "문서 인덱싱 상태")
public record DocumentStatusResponse(
@Schema(description = "문서 ID") Long documentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@

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

/**
* 문서 목록에서 원장 상태와 현재 검색 가능한 Version을 함께 보여주는 요약 응답이다.
*
* <p>아직 인덱싱 완료 Version이 없는 문서는 currentVersion 필드가 {@code null}이며 처리 중 상태는 별도
* 문서 상태 API에서 조회한다.
*/
public record DocumentSummaryResponse(
@Schema(description = "문서 ID") Long documentId,
@Schema(description = "문서 제목") String title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

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

/**
* 최초 문서 업로드로 함께 생성·연결된 Document, Version, FileObject와 Embedding Job 결과를 반환한다.
*/
public record DocumentUploadResponse(
@Schema(description = "생성된 문서 ID")
Long documentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

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

/**
* 기존 문서에 새 Version을 업로드한 결과와 유지되는 현재 검색 Version 정보를 함께 반환한다.
*
* <p>새 Version이 인덱싱을 마칠 때까지 currentVersionId는 기존 검색 가능 Version을 계속 가리킬 수 있다.
*/
public record DocumentVersionUploadResponse(
@Schema(description = "문서 ID") Long documentId,
@Schema(description = "생성된 문서 버전 ID") Long documentVersionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

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

/**
* 아직 현재 검색 Version으로 승격되지 않은 처리 중 문서 Version과 Job 상태를 제공한다.
*/
@Schema(description = "현재 처리 중인 문서 버전과 임베딩 작업 상태")
public record ProcessingVersionStatusResponse(
@Schema(description = "문서 버전 번호") int versionNo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ public class Document extends BaseEntity {
@Column(name = "deleted_at")
private LocalDateTime deletedAt;

/**
* 문서 원장의 소유자·표시 Metadata·형식·공개 범위와 초기 상태를 생성한다.
*
* <p>최초 저장 전에는 순환 FK 때문에 currentVersion이 비어 있을 수 있으며, 버전 저장 후
* {@link #updateCurrentVersion(DocumentVersion)}으로 연결한다.
*/
@Builder
public Document(User owner, DocumentVersion currentVersion, String title, String description,
DocumentType documentType, DocumentSourceType sourceType, DocumentStatus status,
Expand All @@ -122,6 +128,12 @@ public Document(User owner, DocumentVersion currentVersion, String title, String
this.visibility = visibility;
}

/**
* 최초 업로드 중 생성한 버전 또는 내부 복구 흐름의 대상 버전을 현재 포인터로 설정한다.
*
* <p>검색 가능한 완료 승격에는 상태·소유 관계를 검증하는
* {@link #activateIndexedVersion(DocumentVersion)}을 사용한다.
*/
public void updateCurrentVersion(DocumentVersion currentVersion) {
this.currentVersion = currentVersion;
}
Expand All @@ -136,6 +148,9 @@ public void updateMetadata(String title, String description) {
this.description = description == null || description.isBlank() ? null : description.trim();
}

/**
* 문서의 공개 범위를 변경한다. 권한 검증과 접근 캐시 갱신은 호출 Service가 담당한다.
*/
public void updateVisibility(VisibilityType visibility) {
this.visibility = visibility;
}
Expand Down Expand Up @@ -168,22 +183,42 @@ public void activateIndexedVersion(DocumentVersion documentVersion) {
this.status = DocumentStatus.INDEXED;
}

/**
* 검색 가능한 버전이 아직 없는 문서를 비동기 인덱싱 진행 상태로 표시한다.
*/
public void markIndexing() {
this.status = DocumentStatus.INDEXING;
}

/**
* 실패 문서에 새 버전이 접수됐음을 나타내도록 업로드 상태로 되돌린다.
*/
public void markUploaded() {
this.status = DocumentStatus.UPLOADED;
}

/**
* 문서에 검색 가능한 버전이 있음을 표시한다.
*
* <p>새 currentVersion과 함께 변경해야 하는 완료 경로에서는
* {@link #activateIndexedVersion(DocumentVersion)}을 사용한다.
*/
public void markIndexed() {
this.status = DocumentStatus.INDEXED;
}

/**
* 검색 가능한 버전 없이 인덱싱이 최종 실패한 문서를 실패 상태로 표시한다.
*/
public void markFailed() {
this.status = DocumentStatus.FAILED;
}

/**
* 문서를 Soft-delete하고 삭제 시각을 함께 기록한다.
*
* <p>Vector 비활성화와 권한 캐시 정리는 호출 Transaction의 Outbox Event가 후속 처리한다.
*/
public void markDeleted(LocalDateTime deletedAt) {
this.status = DocumentStatus.DELETED;
this.deletedAt = deletedAt;
Expand Down
Loading