diff --git a/src/main/java/com/wooteco/wiki/admin/service/AdminService.java b/src/main/java/com/wooteco/wiki/admin/service/AdminService.java index 53d27e3..58b779b 100644 --- a/src/main/java/com/wooteco/wiki/admin/service/AdminService.java +++ b/src/main/java/com/wooteco/wiki/admin/service/AdminService.java @@ -1,19 +1,16 @@ package com.wooteco.wiki.admin.service; -import com.wooteco.wiki.document.service.DocumentService; import java.util.UUID; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +@RequiredArgsConstructor @Service public class AdminService { - private final DocumentService documentService; - - public AdminService(DocumentService documentService) { - this.documentService = documentService; - } + private final CrewDocumentService crewDocumentService; public void deleteDocumentByDocumentUuid(UUID documentUuid) { - documentService.deleteByUuid(documentUuid); + crewDocumentService.deleteByUuid(documentUuid); } } diff --git a/src/main/java/com/wooteco/wiki/admin/service/CrewDocumentService.java b/src/main/java/com/wooteco/wiki/admin/service/CrewDocumentService.java new file mode 100644 index 0000000..1eea9eb --- /dev/null +++ b/src/main/java/com/wooteco/wiki/admin/service/CrewDocumentService.java @@ -0,0 +1,106 @@ +package com.wooteco.wiki.admin.service; + +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.domain.Document; +import com.wooteco.wiki.document.domain.dto.CrewDocumentCreateRequest; +import com.wooteco.wiki.document.domain.dto.DocumentResponse; +import com.wooteco.wiki.document.domain.dto.DocumentUpdateRequest; +import com.wooteco.wiki.document.repository.CrewDocumentRepository; +import com.wooteco.wiki.document.repository.DocumentRepository; +import com.wooteco.wiki.global.exception.ErrorCode; +import com.wooteco.wiki.global.exception.WikiException; +import com.wooteco.wiki.history.service.HistoryService; +import com.wooteco.wiki.organizationdocument.dto.response.OrganizationDocumentResponse; +import com.wooteco.wiki.organizationdocument.service.DocumentOrganizationLinkService; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class CrewDocumentService { + + private final DocumentOrganizationLinkService documentOrganizationLinkService; + private final CrewDocumentRepository crewDocumentRepository; + private final DocumentRepository documentRepository; + private final HistoryService historyService; + private final Random random; + + @Transactional + public void deleteByUuid(UUID documentUuid) { + CrewDocument crewDocument = crewDocumentRepository.findByUuid(documentUuid) + .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); + + documentOrganizationLinkService.unlinkAll(crewDocument); + + documentRepository.deleteByUuid(documentUuid); + } + + @Transactional + public DocumentResponse create(CrewDocumentCreateRequest request) { + String title = request.title(); + if (documentRepository.existsByTitle(title)) { + throw new WikiException(ErrorCode.DOCUMENT_DUPLICATE); + } + + CrewDocument crewDocument = request.toCrewDocument(); + CrewDocument savedDocument = crewDocumentRepository.save(crewDocument); + historyService.save(savedDocument); + return mapToResponse(savedDocument); + } + + public DocumentResponse getByUuid(UUID uuid) { + CrewDocument crewDocument = crewDocumentRepository.findByUuid(uuid) + .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); + return mapToResponse(crewDocument); + } + + public DocumentResponse getByTitle(String title) { + Document document = documentRepository.findByTitle(title) + .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); + + if (!(document instanceof CrewDocument crewDocument)) { + throw new WikiException(ErrorCode.DOCUMENT_NOT_FOUND); + } + + return mapToResponse(crewDocument); + } + + public DocumentResponse getRandom() { + List documents = crewDocumentRepository.findAll(); + if (documents.isEmpty()) { + throw new WikiException(ErrorCode.DOCUMENT_NOT_FOUND); + } + CrewDocument document = documents.get(random.nextInt(documents.size())); + return mapToResponse(document); + } + + @Transactional + public DocumentResponse update(UUID uuid, DocumentUpdateRequest request) { + CrewDocument crewDocument = crewDocumentRepository.findByUuid(uuid) + .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); + + Document updateData = crewDocument.update( + request.title(), + request.contents(), + request.writer(), + request.documentBytes(), + LocalDateTime.now() + ); + historyService.save(updateData); + return mapToResponse(crewDocument); + } + + private DocumentResponse mapToResponse(CrewDocument crewDocument) { + long latestVersion = historyService.findLatestVersionByDocument(crewDocument); + List organizationDocumentResponses = + documentOrganizationLinkService.findOrganizationDocumentResponsesByDocument(crewDocument); + + return DocumentResponse.toDocumentResponse(crewDocument, latestVersion, organizationDocumentResponses); + } +} diff --git a/src/main/java/com/wooteco/wiki/document/controller/DocumentController.java b/src/main/java/com/wooteco/wiki/document/controller/DocumentController.java index dd80411..b0fdf3f 100644 --- a/src/main/java/com/wooteco/wiki/document/controller/DocumentController.java +++ b/src/main/java/com/wooteco/wiki/document/controller/DocumentController.java @@ -1,5 +1,6 @@ package com.wooteco.wiki.document.controller; +import com.wooteco.wiki.admin.service.CrewDocumentService; import com.wooteco.wiki.document.domain.Document; import com.wooteco.wiki.document.domain.dto.*; import com.wooteco.wiki.document.service.DocumentSearchService; @@ -27,6 +28,7 @@ @RequiredArgsConstructor public class DocumentController { + private final CrewDocumentService crewDocumentService; private final DocumentService documentService; private final HistoryService historyService; private final DocumentSearchService documentSearchService; @@ -35,14 +37,14 @@ public class DocumentController { @Operation(summary = "위키 글 작성", description = "위키 글을 작성합니다.") @PostMapping public ApiResponse> post(@RequestBody CrewDocumentCreateRequest crewDocumentCreateRequest) { - DocumentResponse response = documentService.postCrewDocument(crewDocumentCreateRequest); + DocumentResponse response = crewDocumentService.create(crewDocumentCreateRequest); return ApiResponseGenerator.success(response); } @Operation(summary = "랜덤 위키 글 조회", description = "랜덤으로 위키 글을 조회합니다.") @GetMapping("/random") public ApiResponse> getRandom() { - DocumentResponse response = documentService.getRandom(); + DocumentResponse response = crewDocumentService.getRandom(); return ApiResponseGenerator.success(response); } @@ -56,8 +58,8 @@ public ApiResponse>>> findA @Operation(summary = "제목으로 위키 글 조회", description = "제목을 통해 위키 글을 조회합니다.") @GetMapping("title/{title}") - public ApiResponse> get(@PathVariable String title) { - Object response = documentService.get(title); + public ApiResponse> get(@PathVariable String title) { + DocumentResponse response = crewDocumentService.getByTitle(title); return ApiResponseGenerator.success(response); } @@ -70,9 +72,9 @@ public ApiResponse> getUuidByTitle(@PathVariable String titl @Operation(summary = "UUID로 위키 글 조회", description = "UUID를 통해 위키 글을 조회합니다.") @GetMapping("uuid/{uuidText}") - public ApiResponse> getByUuid(@PathVariable String uuidText) { + public ApiResponse> getByUuid(@PathVariable String uuidText) { UUID uuid = UUID.fromString(uuidText); - Object response = documentService.getByUuid(uuid); + DocumentResponse response = crewDocumentService.getByUuid(uuid); return ApiResponseGenerator.success(response); } @@ -99,7 +101,7 @@ public ApiResponse> getDocumentLogs(@PathVari public ApiResponse> put( @RequestBody DocumentUpdateRequest documentUpdateRequest ) { - DocumentResponse response = documentService.put(documentUpdateRequest.uuid(), documentUpdateRequest); + DocumentResponse response = crewDocumentService.update(documentUpdateRequest.uuid(), documentUpdateRequest); return ApiResponseGenerator.success(response); } diff --git a/src/main/java/com/wooteco/wiki/document/domain/dto/DocumentResponse.java b/src/main/java/com/wooteco/wiki/document/domain/dto/DocumentResponse.java index 78b1469..da1b8d5 100644 --- a/src/main/java/com/wooteco/wiki/document/domain/dto/DocumentResponse.java +++ b/src/main/java/com/wooteco/wiki/document/domain/dto/DocumentResponse.java @@ -1,20 +1,39 @@ package com.wooteco.wiki.document.domain.dto; +import com.wooteco.wiki.document.domain.CrewDocument; import com.wooteco.wiki.organizationdocument.dto.response.OrganizationDocumentResponse; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; public record DocumentResponse( - Long documentId, - UUID documentUUID, - String title, - String contents, - String writer, - LocalDateTime generateTime, - Integer viewCount, + Long documentId, + UUID documentUUID, + String title, + String contents, + String writer, + LocalDateTime generateTime, + Integer viewCount, + Long latestVersion, + List organizationDocumentResponses +) { + + public static DocumentResponse toDocumentResponse( + CrewDocument crewDocument, Long latestVersion, List organizationDocumentResponses -) { + ) { + return new DocumentResponse( + crewDocument.getId(), + crewDocument.getUuid(), + crewDocument.getTitle(), + crewDocument.getContents(), + crewDocument.getWriter(), + crewDocument.getGenerateTime(), + crewDocument.getViewCount(), + latestVersion, + organizationDocumentResponses + ); + } } diff --git a/src/main/java/com/wooteco/wiki/document/service/DocumentService.java b/src/main/java/com/wooteco/wiki/document/service/DocumentService.java index 8eb706b..4767496 100644 --- a/src/main/java/com/wooteco/wiki/document/service/DocumentService.java +++ b/src/main/java/com/wooteco/wiki/document/service/DocumentService.java @@ -1,111 +1,38 @@ package com.wooteco.wiki.document.service; -import com.wooteco.wiki.document.domain.CrewDocument; import com.wooteco.wiki.document.domain.Document; -import com.wooteco.wiki.document.domain.dto.CrewDocumentCreateRequest; -import com.wooteco.wiki.document.domain.dto.DocumentResponse; -import com.wooteco.wiki.document.domain.dto.DocumentUpdateRequest; import com.wooteco.wiki.document.domain.dto.DocumentUuidResponse; import com.wooteco.wiki.document.repository.DocumentRepository; import com.wooteco.wiki.global.common.PagingRequest; import com.wooteco.wiki.global.exception.ErrorCode; import com.wooteco.wiki.global.exception.WikiException; -import com.wooteco.wiki.history.service.HistoryService; -import com.wooteco.wiki.organizationdocument.dto.response.OrganizationDocumentResponse; -import com.wooteco.wiki.organizationdocument.service.DocumentOrganizationLinkService; -import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.UUID; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@RequiredArgsConstructor +@Transactional(readOnly = true) @Service -@Transactional public class DocumentService { private final DocumentRepository documentRepository; - private final DocumentOrganizationLinkService organizationDocumentLinkService; - private final HistoryService historyService; - private final Random random; - - public DocumentService( - DocumentRepository documentRepository, - DocumentOrganizationLinkService organizationDocumentLinkService, - HistoryService historyService, - Random random - ) { - this.documentRepository = documentRepository; - this.organizationDocumentLinkService = organizationDocumentLinkService; - this.historyService = historyService; - this.random = random; - } - - public DocumentResponse postCrewDocument(CrewDocumentCreateRequest request) { - String title = request.title(); - if (documentRepository.existsByTitle(title)) { - throw new WikiException(ErrorCode.DOCUMENT_DUPLICATE); - } - - CrewDocument crewDocument = request.toCrewDocument(); - Document savedDocument = documentRepository.save(crewDocument); - historyService.save(savedDocument); - return mapToResponse(savedDocument); - } - - @Transactional(readOnly = true) - public DocumentResponse getRandom() { - List documents = documentRepository.findAll(); - if (documents.isEmpty()) { - throw new WikiException(ErrorCode.DOCUMENT_NOT_FOUND); - } - Document document = documents.get(random.nextInt(documents.size())); - return mapToResponse(document); - } @Transactional(readOnly = true) - public Page findAll(PagingRequest requestDto) { - return documentRepository.findAll(requestDto.toPageable()); + public Page findAll(PagingRequest pagingRequest) { + return documentRepository.findAll(pagingRequest.toPageable()); } - @Transactional(readOnly = true) - public DocumentResponse get(String title) { - Document document = documentRepository.findByTitle(title) - .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); - return mapToResponse(document); - } - - @Transactional(readOnly = true) public DocumentUuidResponse getUuidByTitle(String title) { return documentRepository.findUuidByTitle(title) - .map(DocumentUuidResponse::new) - .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); - } - - @Transactional(readOnly = true) - public DocumentResponse getByUuid(UUID uuid) { - Document document = documentRepository.findByUuid(uuid) - .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); - return mapToResponse(document); - } - - public DocumentResponse put(UUID uuid, DocumentUpdateRequest request) { - String title = request.title(); - String contents = request.contents(); - String writer = request.writer(); - Long documentBytes = request.documentBytes(); - - Document document = documentRepository.findByUuid(uuid) - .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); - - Document updateData = document.update(title, contents, writer, documentBytes, LocalDateTime.now()); - historyService.save(updateData); - return mapToResponse(document); + .map(DocumentUuidResponse::new) + .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); } + @Transactional public void flushViews(Map views) { List documents = documentRepository.findAllByUuidIn(views.keySet()); @@ -119,36 +46,5 @@ public void flushViews(Map views) { documentRepository.saveAll(documents); } - - public void deleteByUuid(UUID documentUuid) { - documentRepository.findByUuid(documentUuid) - .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); - documentRepository.deleteByUuid(documentUuid); - } - - private DocumentResponse mapToResponse(Document document) { - long latestVersion = historyService.findLatestVersionByDocument(document); - List organizationDocumentResponses = - (document instanceof CrewDocument crew) - ? organizationDocumentLinkService.findOrganizationDocumentResponsesByDocument(crew) - : Collections.emptyList(); - - return new DocumentResponse( - document.getId() != null ? document.getId() : - throwNotFound(), - document.getUuid(), - document.getTitle(), - document.getContents(), - document.getWriter(), - document.getGenerateTime(), - document.getViewCount(), - latestVersion, - organizationDocumentResponses - ); - } - - private Long throwNotFound() { - throw new WikiException(ErrorCode.DOCUMENT_NOT_FOUND); - } } diff --git a/src/main/java/com/wooteco/wiki/organizationdocument/repository/DocumentOrganizationLinkRepository.java b/src/main/java/com/wooteco/wiki/organizationdocument/repository/DocumentOrganizationLinkRepository.java index 50b350e..5d6b498 100644 --- a/src/main/java/com/wooteco/wiki/organizationdocument/repository/DocumentOrganizationLinkRepository.java +++ b/src/main/java/com/wooteco/wiki/organizationdocument/repository/DocumentOrganizationLinkRepository.java @@ -18,4 +18,6 @@ void deleteByCrewDocumentAndOrganizationDocument(CrewDocument crewDocument, OrganizationDocument organizationDocument); List findAllByCrewDocument(CrewDocument crewDocument); + + void deleteAllByCrewDocument(CrewDocument crewDocument); } diff --git a/src/main/java/com/wooteco/wiki/organizationdocument/service/DocumentOrganizationLinkService.java b/src/main/java/com/wooteco/wiki/organizationdocument/service/DocumentOrganizationLinkService.java index a219e1f..fcce58a 100644 --- a/src/main/java/com/wooteco/wiki/organizationdocument/service/DocumentOrganizationLinkService.java +++ b/src/main/java/com/wooteco/wiki/organizationdocument/service/DocumentOrganizationLinkService.java @@ -27,6 +27,10 @@ public void unlink(CrewDocument crewDocument, OrganizationDocument organizationD documentOrgDocLinkRepository.deleteByCrewDocumentAndOrganizationDocument(crewDocument, organizationDocument); } + public void unlinkAll(CrewDocument crewDocument) { + documentOrgDocLinkRepository.deleteAllByCrewDocument(crewDocument); + } + @Transactional(readOnly = true) public List findOrganizationDocumentResponsesByDocument(CrewDocument crewDocument) { List documentOrganizationLinks = documentOrgDocLinkRepository.findAllByCrewDocument( diff --git a/src/test/java/com/wooteco/wiki/document/service/CrewDocumentServiceTest.java b/src/test/java/com/wooteco/wiki/document/service/CrewDocumentServiceTest.java new file mode 100644 index 0000000..30457c5 --- /dev/null +++ b/src/test/java/com/wooteco/wiki/document/service/CrewDocumentServiceTest.java @@ -0,0 +1,98 @@ +package com.wooteco.wiki.document.service; + +import static com.wooteco.wiki.global.exception.ErrorCode.DOCUMENT_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.wooteco.wiki.admin.service.CrewDocumentService; +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.domain.dto.DocumentResponse; +import com.wooteco.wiki.document.domain.dto.DocumentUpdateRequest; +import com.wooteco.wiki.document.fixture.DocumentFixture; +import com.wooteco.wiki.document.repository.DocumentRepository; +import com.wooteco.wiki.global.exception.WikiException; +import com.wooteco.wiki.history.domain.History; +import com.wooteco.wiki.history.fixture.HistoryFixture; +import com.wooteco.wiki.history.repository.HistoryRepository; +import java.time.LocalDateTime; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class CrewDocumentServiceTest { + + @Autowired + private CrewDocumentService crewDocumentService; + @Autowired + private DocumentRepository documentRepository; + @Autowired + private HistoryRepository historyRepository; + + @DisplayName("문서 조회 기능") + @Nested + class Find { + + @DisplayName("문서 조회시, 해당 문서의 마지막 로그 번호를 가져온다.") + @Test + void getDocumentLatestVersion_success_byExistsDocument() { + // given + CrewDocument crewDocument = DocumentFixture.createDefaultCrewDocument(); + CrewDocument savedCrewDocument = documentRepository.save(crewDocument); + + History history = HistoryFixture.create("test", "test", "tesst", 150, + LocalDateTime.of(2025, 7, 15, 10, 0, 0), + savedCrewDocument, 20L); + historyRepository.save(history); + + // when + DocumentUpdateRequest documentUpdateRequest = new DocumentUpdateRequest("test", "test", "test", 150L, + savedCrewDocument.getUuid()); + + crewDocumentService.update(savedCrewDocument.getUuid(), documentUpdateRequest); + DocumentResponse documentResponse = crewDocumentService.getByUuid(savedCrewDocument.getUuid()); + + // then + assertThat(documentResponse.latestVersion()).isEqualTo(21L); + } + } + + @Nested + @DisplayName("문서 uuid로 삭제 기능") + class deleteByUuid { + + @DisplayName("존재하는 문서 id일 경우 문서가 로그들과 함께 삭제된다") + @Test + void deleteById_success_byExistsId() { + // given + DocumentResponse documentResponse = crewDocumentService.create( + DocumentFixture.createDocumentCreateRequest("title1", "content1", "writer1", 10L, + UUID.randomUUID())); + + // before then + assertThat(documentRepository.findAll()).hasSize(1); + assertThat(historyRepository.findAll()).hasSize(1); + + // when + crewDocumentService.deleteByUuid(documentResponse.documentUUID()); + + // after then + assertThat(documentRepository.findAll()).hasSize(0); + assertThat(historyRepository.findAll()).hasSize(0); + } + + @DisplayName("존재하지 않는 문서의 id일 경우 예외가 발생한다 : WikiException.DOCUMENT_NOT_FOUND") + @Test + void deleteById_throwsException_byNonExistsId() { + // when & then + WikiException ex = assertThrows(WikiException.class, + () -> crewDocumentService.deleteByUuid(UUID.randomUUID())); + assertThat(ex.getErrorCode()).isEqualTo(DOCUMENT_NOT_FOUND); + } + } +} diff --git a/src/test/java/com/wooteco/wiki/document/service/DocumentSearchServiceTest.java b/src/test/java/com/wooteco/wiki/document/service/DocumentSearchServiceTest.java index 5ae680b..0608e2a 100644 --- a/src/test/java/com/wooteco/wiki/document/service/DocumentSearchServiceTest.java +++ b/src/test/java/com/wooteco/wiki/document/service/DocumentSearchServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; +import com.wooteco.wiki.admin.service.CrewDocumentService; import com.wooteco.wiki.document.domain.DocumentType; import com.wooteco.wiki.document.domain.dto.DocumentSearchResponse; import com.wooteco.wiki.document.fixture.DocumentFixture; @@ -25,7 +26,7 @@ class DocumentSearchServiceTest { private DocumentSearchService documentSearchService; @Autowired - private DocumentService documentService; + private CrewDocumentService crewDocumentService; @Autowired private OrganizationDocumentRepository organizationDocumentRepository; @@ -34,9 +35,9 @@ class DocumentSearchServiceTest { @Test void search_success_byKeywordPrefix() { // given - documentService.postCrewDocument( + crewDocumentService.create( DocumentFixture.createDocumentCreateRequest("title1", "content1", "writer1", 10L, UUID.randomUUID())); - documentService.postCrewDocument( + crewDocumentService.create( DocumentFixture.createDocumentCreateRequest("title2", "content2", "writer2", 11L, UUID.randomUUID())); // when @@ -63,7 +64,7 @@ void search_success_byNoMatch() { @Test void search_success_byOrganizationDocumentType() { // given - documentService.postCrewDocument( + crewDocumentService.create( DocumentFixture.createDocumentCreateRequest("title1", "content1", "writer1", 10L, UUID.randomUUID())); OrganizationDocument organizationDocument = OrganizationDocumentFixture.create("title2", "content", "writer", 15L, UUID.randomUUID());