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,68 @@
package com.opensource.docgrid.domain.collection.controller;

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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.opensource.docgrid.domain.auth.annotation.CurrentUser;
import com.opensource.docgrid.domain.collection.dto.request.AddDocumentRequest;
import com.opensource.docgrid.domain.collection.dto.request.CreateCollectionRequest;
import com.opensource.docgrid.domain.collection.dto.response.CollectionDocumentResponse;
import com.opensource.docgrid.domain.collection.dto.response.CollectionResponse;
import com.opensource.docgrid.domain.collection.service.command.CollectionCommandService;
import com.opensource.docgrid.domain.collection.service.query.CollectionQueryService;
import com.opensource.docgrid.global.common.response.ApiResponse;
import com.opensource.docgrid.global.common.response.ResponseUtils;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "Collection", description = "컬렉션 관련 API")
@RestController
@RequestMapping("/collections")
@RequiredArgsConstructor
public class CollectionController {

private final CollectionCommandService collectionCommandService;
private final CollectionQueryService collectionQueryService;

@Operation(
summary = "컬렉션 생성",
description = "새 컬렉션을 생성합니다. 생성자가 소유자(owner)로 설정됩니다. visibility 미입력 시 PRIVATE으로 생성됩니다."
)
@PostMapping
public ResponseEntity<ApiResponse<CollectionResponse>> createCollection(
@Parameter(hidden = true) @CurrentUser Long userId,
@RequestBody @Valid CreateCollectionRequest request) {
return ResponseUtils.created(collectionCommandService.createCollection(userId, request));
}

@Operation(
summary = "컬렉션 단건 조회",
description = "컬렉션 ID로 컬렉션 정보를 조회합니다."
)
@GetMapping("/{collectionId}")
public ResponseEntity<ApiResponse<CollectionResponse>> getCollection(
@PathVariable Long collectionId) {
return ResponseUtils.ok(collectionQueryService.getCollection(collectionId));
}

@Operation(
summary = "컬렉션에 문서 추가",
description = "컬렉션에 문서를 추가합니다. 컬렉션 소유자(owner)만 가능합니다. 이미 추가된 문서면 409를 반환합니다."
)
@PostMapping("/{collectionId}/documents")
public ResponseEntity<ApiResponse<CollectionDocumentResponse>> addDocument(
@PathVariable Long collectionId,
@Parameter(hidden = true) @CurrentUser Long userId,
@RequestBody @Valid AddDocumentRequest request) {
return ResponseUtils.created(collectionCommandService.addDocument(collectionId, userId, request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.opensource.docgrid.domain.collection.converter;

import org.springframework.stereotype.Component;

import com.opensource.docgrid.domain.collection.dto.response.CollectionDocumentResponse;
import com.opensource.docgrid.domain.collection.dto.response.CollectionResponse;
import com.opensource.docgrid.domain.collection.entity.CollectionDocument;
import com.opensource.docgrid.domain.collection.entity.DocumentCollection;

@Component
public class CollectionConverter {

public CollectionResponse toResponse(DocumentCollection collection) {
Long parentId = collection.getParentCollection() != null
? collection.getParentCollection().getId()
: null;

return new CollectionResponse(
collection.getId(),
collection.getName(),
collection.getDescription(),
collection.getOwner().getId(),
parentId,
collection.getVisibility(),
collection.getStatus(),
collection.getCreatedAt()
);
}

public CollectionDocumentResponse toDocumentResponse(CollectionDocument cd) {
Long addedById = cd.getAddedBy() != null ? cd.getAddedBy().getId() : null;

return new CollectionDocumentResponse(
cd.getCollection().getId(),
cd.getDocument().getId(),
addedById,
cd.getAddedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.opensource.docgrid.domain.collection.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

public record AddDocumentRequest(
@Schema(description = "추가할 문서 ID") @NotNull Long documentId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.opensource.docgrid.domain.collection.dto.request;

import com.opensource.docgrid.domain.document.enums.VisibilityType;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

public record CreateCollectionRequest(
@Schema(description = "컬렉션 이름") @NotBlank String name,
@Schema(description = "컬렉션 설명") String description,
@Schema(description = "상위 컬렉션 ID (최상위면 null)") Long parentCollectionId,
@Schema(description = "공개 범위 (기본값 PRIVATE)", example = "PRIVATE") VisibilityType visibility
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.opensource.docgrid.domain.collection.dto.response;

import java.time.LocalDateTime;

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

public record CollectionDocumentResponse(
@Schema(description = "컬렉션 ID") Long collectionId,
@Schema(description = "문서 ID") Long documentId,
@Schema(description = "추가한 사용자 ID") Long addedBy,
@Schema(description = "추가 시각") LocalDateTime addedAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.opensource.docgrid.domain.collection.dto.response;

import java.time.LocalDateTime;

import com.opensource.docgrid.domain.collection.enums.CollectionStatus;
import com.opensource.docgrid.domain.document.enums.VisibilityType;

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

public record CollectionResponse(
@Schema(description = "컬렉션 ID") Long collectionId,
@Schema(description = "컬렉션 이름") String name,
@Schema(description = "컬렉션 설명") String description,
@Schema(description = "소유자 사용자 ID") Long ownerUserId,
@Schema(description = "상위 컬렉션 ID") Long parentCollectionId,
@Schema(description = "공개 범위") VisibilityType visibility,
@Schema(description = "컬렉션 상태") CollectionStatus status,
@Schema(description = "생성 시각") LocalDateTime createdAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.opensource.docgrid.domain.collection.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.opensource.docgrid.domain.collection.entity.CollectionDocument;

public interface CollectionDocumentRepository extends JpaRepository<CollectionDocument, Long> {

boolean existsByCollectionIdAndDocumentId(Long collectionId, Long documentId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.opensource.docgrid.domain.collection.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.opensource.docgrid.domain.collection.entity.DocumentCollection;

public interface CollectionRepository extends JpaRepository<DocumentCollection, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.opensource.docgrid.domain.collection.service.command;

import java.time.LocalDateTime;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.opensource.docgrid.domain.collection.converter.CollectionConverter;
import com.opensource.docgrid.domain.collection.dto.request.AddDocumentRequest;
import com.opensource.docgrid.domain.collection.dto.request.CreateCollectionRequest;
import com.opensource.docgrid.domain.collection.dto.response.CollectionDocumentResponse;
import com.opensource.docgrid.domain.collection.dto.response.CollectionResponse;
import com.opensource.docgrid.domain.collection.entity.CollectionDocument;
import com.opensource.docgrid.domain.collection.entity.DocumentCollection;
import com.opensource.docgrid.domain.collection.enums.CollectionStatus;
import com.opensource.docgrid.domain.collection.repository.CollectionDocumentRepository;
import com.opensource.docgrid.domain.collection.repository.CollectionRepository;
import com.opensource.docgrid.domain.document.entity.Document;
import com.opensource.docgrid.domain.document.enums.VisibilityType;
import com.opensource.docgrid.domain.document.repository.DocumentRepository;
import com.opensource.docgrid.domain.user.entity.User;
import com.opensource.docgrid.domain.user.repository.UserRepository;
import com.opensource.docgrid.global.exception.DocGridException;
import com.opensource.docgrid.global.exception.ErrorCode;

import lombok.RequiredArgsConstructor;

@Transactional
@Service
@RequiredArgsConstructor
public class CollectionCommandService {

private final CollectionRepository collectionRepository;
private final CollectionDocumentRepository collectionDocumentRepository;
private final DocumentRepository documentRepository;
private final UserRepository userRepository;
private final CollectionConverter collectionConverter;

// 폴더 생성
public CollectionResponse createCollection(Long userId, CreateCollectionRequest request) {
User owner = userRepository.getReferenceById(userId);

DocumentCollection parentCollection = null; // 상위 폴더 지정은 선택 사항이라 null로 초기화
if (request.parentCollectionId() != null) {
parentCollection = collectionRepository.findById(request.parentCollectionId())
.orElseThrow(() -> new DocGridException(ErrorCode.COLLECTION_NOT_FOUND));
}

VisibilityType visibility = request.visibility() != null ? request.visibility() : VisibilityType.PRIVATE;

DocumentCollection collection = DocumentCollection.builder()
.owner(owner)
.parentCollection(parentCollection)
.name(request.name())
.description(request.description())
.visibility(visibility)
.status(CollectionStatus.ACTIVE)
.build();

collectionRepository.save(collection);
return collectionConverter.toResponse(collection);
}

// 폴더에 문서 추가
public CollectionDocumentResponse addDocument(Long collectionId, Long userId, AddDocumentRequest request) {
DocumentCollection collection = collectionRepository.findById(collectionId)
.orElseThrow(() -> new DocGridException(ErrorCode.COLLECTION_NOT_FOUND));

// TODO: Issue 3 PermissionService 완성 후 canWriteDocument() 로 대체
if (!collection.getOwner().getId().equals(userId)) {
throw new DocGridException(ErrorCode.PERMISSION_DENIED);
}

Document document = documentRepository.findById(request.documentId())
.orElseThrow(() -> new DocGridException(ErrorCode.DOCUMENT_NOT_FOUND));

// 이미 컬렉션에 문서가 존재하는지 확인
if (collectionDocumentRepository.existsByCollectionIdAndDocumentId(collectionId, request.documentId())) {
throw new DocGridException(ErrorCode.COLLECTION_DOCUMENT_ALREADY_EXISTS);
}

User addedBy = userRepository.getReferenceById(userId); // 문서를 추가한 사용자 정보 가져오기

CollectionDocument collectionDocument = CollectionDocument.builder()
.collection(collection)
.document(document)
.addedBy(addedBy)
.addedAt(LocalDateTime.now())
.build();

collectionDocumentRepository.save(collectionDocument);
return collectionConverter.toDocumentResponse(collectionDocument);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.opensource.docgrid.domain.collection.service.query;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.opensource.docgrid.domain.collection.converter.CollectionConverter;
import com.opensource.docgrid.domain.collection.dto.response.CollectionResponse;
import com.opensource.docgrid.domain.collection.entity.DocumentCollection;
import com.opensource.docgrid.domain.collection.repository.CollectionRepository;
import com.opensource.docgrid.global.exception.DocGridException;
import com.opensource.docgrid.global.exception.ErrorCode;

import lombok.RequiredArgsConstructor;

@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class CollectionQueryService {

private final CollectionRepository collectionRepository;
private final CollectionConverter collectionConverter;

// 컬렉션 단건 조회
public CollectionResponse getCollection(Long collectionId) {
DocumentCollection collection = collectionRepository.findById(collectionId)
.orElseThrow(() -> new DocGridException(ErrorCode.COLLECTION_NOT_FOUND));
return collectionConverter.toResponse(collection);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.opensource.docgrid.domain.document.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.opensource.docgrid.domain.document.entity.Document;

// A담당자 영역 — B담당자는 존재 확인 등 읽기 전용으로만 사용
public interface DocumentRepository extends JpaRepository<Document, Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ public enum ErrorCode {
PERMISSION_DENIED(HttpStatus.FORBIDDEN, "ROLE-002", "관리자만 접근할 수 있습니다."),
ROLE_ALREADY_ASSIGNED(HttpStatus.CONFLICT, "ROLE-003", "이미 부여된 역할입니다."),

// COLLECTION
COLLECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "COLLECTION-001", "컬렉션을 찾을 수 없습니다."),
COLLECTION_DOCUMENT_ALREADY_EXISTS(HttpStatus.CONFLICT, "COLLECTION-002", "이미 컬렉션에 추가된 문서입니다."),

// DOCUMENT
DOCUMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "DOCUMENT-001", "문서를 찾을 수 없습니다."),

// EMBEDDING MODEL
EMBEDDING_MODEL_NOT_CONFIGURED(
HttpStatus.INTERNAL_SERVER_ERROR,
Expand Down
Loading