diff --git a/.github/ISSUE_TEMPLATE/pull_request_template.md b/.github/ISSUE_TEMPLATE/pull_request_template.md deleted file mode 100644 index 144f4c5..0000000 --- a/.github/ISSUE_TEMPLATE/pull_request_template.md +++ /dev/null @@ -1,20 +0,0 @@ -## ๐Ÿ”— ๊ด€๋ จ ์ด์Šˆ - -- Closes #์ด์Šˆ๋ฒˆํ˜ธ - -## ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - - -### ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ -- - -## ๐Ÿงช ํ…Œ์ŠคํŠธ - -- [ ] ๋กœ์ปฌ์—์„œ ์ •์ƒ ๋™์ž‘ ํ™•์ธ -- [ ] ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ/์ˆ˜์ • - - -## โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ -- [ ] ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์ค€๋น„ ์™„๋ฃŒ -- [ ] ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ํ™•์ธ -- [ ] ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ (ํ•„์š”์‹œ) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..660aa71 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +## ๊ด€๋ จ ์ด์Šˆ + +Closes # + +## ๋ณ€๊ฒฝ ๋ฐฐ๊ฒฝ + + +## ๋ณ€๊ฒฝ ๋‚ด์šฉ + + +## ๊ฒ€์ฆ + + +- [ ] ํ…Œ์ŠคํŠธ ํ†ต๊ณผ +- [ ] ๋ณ€๊ฒฝ ๋™์ž‘ ํ™•์ธ + +## ์˜ํ–ฅ ๋ฐ ์ฐธ๊ณ ์‚ฌํ•ญ + diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index 61bfa3a..99e5938 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -2,13 +2,14 @@ name: dev-cd.yml on: push: - branches: ["develop"] + branches: [ "develop" ] paths: - "**" + workflow_dispatch: jobs: build-and-push: - runs-on: [self-hosted, crew-wiki-dev] + runs-on: [ self-hosted, crew-wiki-dev ] env: IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki steps: @@ -50,7 +51,7 @@ jobs: deploy: needs: build-and-push - runs-on: [self-hosted, crew-wiki-dev] + runs-on: [ self-hosted, crew-wiki-dev ] env: IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:dev-latest steps: diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index aa41c91..ddca30d 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -112,7 +112,7 @@ jobs: PROD_DB_URL=${{ secrets.PROD_DB_URL }} PROD_DB_USERNAME=${{ secrets.PROD_DB_USERNAME }} PROD_DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }} - CORS_ALLOWED_ORIGINS=${{ secrets.PROD_CORS_ALLOWED_ORIGINS || 'https://crew-wiki.site,https://api.crew-wiki.site' }} + CORS_ALLOWED_ORIGINS=${{ secrets.PROD_CORS_ALLOWED_ORIGINS || 'https://crew-wiki.site,https://www.crew-wiki.site,https://api.crew-wiki.site' }} SWAGGER_SERVER_URL=${{ secrets.PROD_SWAGGER_SERVER_URL || 'https://api.crew-wiki.site' }} AWS_CREDENTIALS_ACCESS_KEY=${{ secrets.AWS_CREDENTIALS_ACCESS_KEY }} AWS_CREDENTIALS_SECRET_KEY=${{ secrets.AWS_CREDENTIALS_SECRET_KEY }} diff --git a/.gitignore b/.gitignore index 1242208..cccd28e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ application-*.yml !src/main/resources/application-prod.yml .env .env.* + +### AI ### +.omc +.omx diff --git a/src/main/java/com/wooteco/wiki/admin/domain/Admin.java b/src/main/java/com/wooteco/wiki/admin/domain/Admin.java index 456b7da..94f0e31 100644 --- a/src/main/java/com/wooteco/wiki/admin/domain/Admin.java +++ b/src/main/java/com/wooteco/wiki/admin/domain/Admin.java @@ -21,7 +21,10 @@ public class Admin { protected Admin() { } - public Admin(String loginId, String password) { + public Admin( + String loginId, + String password + ) { this.loginId = loginId; this.password = password; } diff --git a/src/main/java/com/wooteco/wiki/admin/domain/dto/LoginRequest.java b/src/main/java/com/wooteco/wiki/admin/domain/dto/LoginRequest.java index 11af192..88fcd68 100644 --- a/src/main/java/com/wooteco/wiki/admin/domain/dto/LoginRequest.java +++ b/src/main/java/com/wooteco/wiki/admin/domain/dto/LoginRequest.java @@ -1,4 +1,7 @@ package com.wooteco.wiki.admin.domain.dto; -public record LoginRequest(String loginId, String password) { +public record LoginRequest( + String loginId, + String password +) { } diff --git a/src/main/java/com/wooteco/wiki/admin/repository/AdminRepository.java b/src/main/java/com/wooteco/wiki/admin/repository/AdminRepository.java index 3493ae0..51f90b9 100644 --- a/src/main/java/com/wooteco/wiki/admin/repository/AdminRepository.java +++ b/src/main/java/com/wooteco/wiki/admin/repository/AdminRepository.java @@ -6,5 +6,8 @@ public interface AdminRepository extends JpaRepository { - Optional findOneByLoginIdAndPassword(String loginId, String password); + Optional findOneByLoginIdAndPassword( + String loginId, + String password + ); } diff --git a/src/main/java/com/wooteco/wiki/admin/service/CrewDocumentService.java b/src/main/java/com/wooteco/wiki/admin/service/CrewDocumentService.java index 1eea9eb..71d3569 100644 --- a/src/main/java/com/wooteco/wiki/admin/service/CrewDocumentService.java +++ b/src/main/java/com/wooteco/wiki/admin/service/CrewDocumentService.java @@ -81,7 +81,10 @@ public DocumentResponse getRandom() { } @Transactional - public DocumentResponse update(UUID uuid, DocumentUpdateRequest request) { + public DocumentResponse update( + UUID uuid, + DocumentUpdateRequest request + ) { CrewDocument crewDocument = crewDocumentRepository.findByUuid(uuid) .orElseThrow(() -> new WikiException(ErrorCode.DOCUMENT_NOT_FOUND)); diff --git a/src/main/java/com/wooteco/wiki/document/controller/CrewDocumentQueryController.java b/src/main/java/com/wooteco/wiki/document/controller/CrewDocumentQueryController.java new file mode 100644 index 0000000..d31ac48 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/controller/CrewDocumentQueryController.java @@ -0,0 +1,31 @@ +package com.wooteco.wiki.document.controller; + +import com.wooteco.wiki.document.dto.GenerationCrewResponse; +import com.wooteco.wiki.document.service.CrewDocumentQueryService; +import com.wooteco.wiki.global.common.ApiResponse; +import com.wooteco.wiki.global.common.ApiResponse.SuccessBody; +import com.wooteco.wiki.global.common.ApiResponseGenerator; +import io.swagger.v3.oas.annotations.Operation; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/document") +public class CrewDocumentQueryController { + + private final CrewDocumentQueryService crewDocumentQueryService; + + @Operation(summary = "๊ธฐ์ˆ˜๋ณ„ ํฌ๋ฃจ ๋ชฉ๋ก ์กฐํšŒ", description = "๊ธฐ์ˆ˜์— ์†ํ•œ ํฌ๋ฃจ์˜ ์ด๋ฆ„, ๋ฌธ์„œ UUID, ๋ถ„์•ผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/crews") + public ApiResponse>> findAllByGeneration( + @RequestParam String generation + ) { + List response = crewDocumentQueryService.findAllByGeneration(generation); + return ApiResponseGenerator.success(response); + } +} 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 b0fdf3f..726f318 100644 --- a/src/main/java/com/wooteco/wiki/document/controller/DocumentController.java +++ b/src/main/java/com/wooteco/wiki/document/controller/DocumentController.java @@ -2,7 +2,13 @@ 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.domain.dto.CrewDocumentCreateRequest; +import com.wooteco.wiki.document.domain.dto.DocumentListResponse; +import com.wooteco.wiki.document.domain.dto.DocumentResponse; +import com.wooteco.wiki.document.domain.dto.DocumentSearchResponse; +import com.wooteco.wiki.document.domain.dto.DocumentTitleListResponse; +import com.wooteco.wiki.document.domain.dto.DocumentUpdateRequest; +import com.wooteco.wiki.document.domain.dto.ViewFlushRequest; import com.wooteco.wiki.document.service.DocumentSearchService; import com.wooteco.wiki.document.service.DocumentService; import com.wooteco.wiki.document.service.DocumentServiceJava; @@ -21,11 +27,20 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +@RequiredArgsConstructor @RestController @RequestMapping("/document") -@RequiredArgsConstructor public class DocumentController { private final CrewDocumentService crewDocumentService; @@ -56,6 +71,13 @@ public ApiResponse>>> findA return ApiResponseGenerator.success(convertToResponse(responses)); } + @Operation(summary = "์œ„ํ‚ค ๊ธ€ ์ œ๋ชฉ ์ „์ฒด ์กฐํšŒ", description = "๋ชจ๋“  ์œ„ํ‚ค ๊ธ€์˜ ์ œ๋ชฉ๊ณผ UUID๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping("/titles") + public ApiResponse>> findAllTitles() { + List response = documentService.findAllTitles(); + return ApiResponseGenerator.success(response); + } + @Operation(summary = "์ œ๋ชฉ์œผ๋กœ ์œ„ํ‚ค ๊ธ€ ์กฐํšŒ", description = "์ œ๋ชฉ์„ ํ†ตํ•ด ์œ„ํ‚ค ๊ธ€์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") @GetMapping("title/{title}") public ApiResponse> get(@PathVariable String title) { diff --git a/src/main/java/com/wooteco/wiki/document/domain/CrewDocument.java b/src/main/java/com/wooteco/wiki/document/domain/CrewDocument.java index a515591..413d11b 100644 --- a/src/main/java/com/wooteco/wiki/document/domain/CrewDocument.java +++ b/src/main/java/com/wooteco/wiki/document/domain/CrewDocument.java @@ -13,8 +13,13 @@ @Getter public class CrewDocument extends Document { - public CrewDocument(final String title, final String contents, final String writer, - final Long documentBytes, final UUID uuid) { + public CrewDocument( + final String title, + final String contents, + final String writer, + final Long documentBytes, + final UUID uuid + ) { super(title, contents, writer, documentBytes, uuid); } diff --git a/src/main/java/com/wooteco/wiki/document/domain/CrewField.java b/src/main/java/com/wooteco/wiki/document/domain/CrewField.java new file mode 100644 index 0000000..58e05dd --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/domain/CrewField.java @@ -0,0 +1,8 @@ +package com.wooteco.wiki.document.domain; + +public enum CrewField { + + BACKEND, + FRONTEND, + ANDROID +} diff --git a/src/main/java/com/wooteco/wiki/document/domain/Document.java b/src/main/java/com/wooteco/wiki/document/domain/Document.java index 3220ccc..2d53b14 100644 --- a/src/main/java/com/wooteco/wiki/document/domain/Document.java +++ b/src/main/java/com/wooteco/wiki/document/domain/Document.java @@ -3,6 +3,8 @@ import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -47,8 +49,17 @@ public abstract class Document { @Column(name = "view_count", nullable = false, columnDefinition = "INT DEFAULT 0 NOT NULL") protected Integer viewCount = 0; - public Document(final String title, final String contents, final String writer, - final Long documentBytes, final UUID uuid) { + @Enumerated(EnumType.STRING) + @Column(name = "dtype", insertable = false, updatable = false) + private DocumentType dtype; + + public Document( + final String title, + final String contents, + final String writer, + final Long documentBytes, + final UUID uuid + ) { this.title = title; this.contents = contents; this.writer = writer; 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 da1b8d5..f350ddc 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 @@ -7,21 +7,21 @@ import java.util.UUID; public record DocumentResponse( - Long documentId, - UUID documentUUID, - String title, - String contents, - String writer, - LocalDateTime generateTime, - Integer viewCount, - Long latestVersion, - List organizationDocumentResponses + 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 + CrewDocument crewDocument, + Long latestVersion, + List organizationDocumentResponses ) { return new DocumentResponse( crewDocument.getId(), @@ -36,4 +36,3 @@ public static DocumentResponse toDocumentResponse( ); } } - diff --git a/src/main/java/com/wooteco/wiki/document/domain/dto/DocumentTitleListResponse.java b/src/main/java/com/wooteco/wiki/document/domain/dto/DocumentTitleListResponse.java new file mode 100644 index 0000000..3bf8d69 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/domain/dto/DocumentTitleListResponse.java @@ -0,0 +1,13 @@ +package com.wooteco.wiki.document.domain.dto; + +import com.wooteco.wiki.document.domain.DocumentType; +import java.time.LocalDateTime; +import java.util.UUID; + +public record DocumentTitleListResponse( + String title, + UUID uuid, + DocumentType documentType, + LocalDateTime generateTime +) { +} diff --git a/src/main/java/com/wooteco/wiki/document/dto/GenerationCrewResponse.java b/src/main/java/com/wooteco/wiki/document/dto/GenerationCrewResponse.java new file mode 100644 index 0000000..75b58cf --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/dto/GenerationCrewResponse.java @@ -0,0 +1,19 @@ +package com.wooteco.wiki.document.dto; + +import com.wooteco.wiki.document.domain.CrewField; +import java.util.UUID; + +public record GenerationCrewResponse( + String name, + UUID documentUuid, + CrewField field +) { + + public static GenerationCrewResponse of( + String name, + UUID documentUuid, + CrewField field + ) { + return new GenerationCrewResponse(name, documentUuid, field); + } +} diff --git a/src/main/java/com/wooteco/wiki/document/repository/DocumentRepository.java b/src/main/java/com/wooteco/wiki/document/repository/DocumentRepository.java index d1e1e1a..4b066ea 100644 --- a/src/main/java/com/wooteco/wiki/document/repository/DocumentRepository.java +++ b/src/main/java/com/wooteco/wiki/document/repository/DocumentRepository.java @@ -1,6 +1,7 @@ package com.wooteco.wiki.document.repository; import com.wooteco.wiki.document.domain.Document; +import com.wooteco.wiki.document.domain.dto.DocumentTitleListResponse; import java.util.List; import java.util.Optional; import java.util.Set; @@ -17,6 +18,18 @@ public interface DocumentRepository extends JpaRepository { List findAllByTitleStartingWithOrderByTitle(String keyWord); + @Query(""" + SELECT new com.wooteco.wiki.document.domain.dto.DocumentTitleListResponse( + d.title, + d.uuid, + d.dtype, + d.generateTime + ) + FROM Document d + ORDER BY d.title ASC + """) + List findAllTitles(); + Optional findByUuid(UUID uuid); @Query("SELECT d.uuid FROM Document d WHERE d.title = :title") diff --git a/src/main/java/com/wooteco/wiki/document/repository/GenerationCrewOrganizationReadModel.java b/src/main/java/com/wooteco/wiki/document/repository/GenerationCrewOrganizationReadModel.java new file mode 100644 index 0000000..1c0accc --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/repository/GenerationCrewOrganizationReadModel.java @@ -0,0 +1,10 @@ +package com.wooteco.wiki.document.repository; + +import java.util.UUID; + +public record GenerationCrewOrganizationReadModel( + String crewTitle, + UUID documentUuid, + String organizationTitle +) { +} diff --git a/src/main/java/com/wooteco/wiki/document/repository/GenerationCrewQueryRepository.java b/src/main/java/com/wooteco/wiki/document/repository/GenerationCrewQueryRepository.java new file mode 100644 index 0000000..4c9c762 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/repository/GenerationCrewQueryRepository.java @@ -0,0 +1,8 @@ +package com.wooteco.wiki.document.repository; + +import java.util.List; + +public interface GenerationCrewQueryRepository { + + List findAllByGenerationTitle(String generationTitle); +} diff --git a/src/main/java/com/wooteco/wiki/document/service/CrewDocumentQueryService.java b/src/main/java/com/wooteco/wiki/document/service/CrewDocumentQueryService.java new file mode 100644 index 0000000..8e02472 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/service/CrewDocumentQueryService.java @@ -0,0 +1,37 @@ +package com.wooteco.wiki.document.service; + +import com.wooteco.wiki.document.dto.GenerationCrewResponse; +import com.wooteco.wiki.document.repository.GenerationCrewOrganizationReadModel; +import com.wooteco.wiki.document.repository.GenerationCrewQueryRepository; +import com.wooteco.wiki.global.exception.ErrorCode; +import com.wooteco.wiki.global.exception.WikiException; +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class CrewDocumentQueryService { + + private final GenerationCrewQueryRepository generationCrewQueryRepository; + private final CrewProfileExtractor crewProfileExtractor; + + @Transactional(readOnly = true) + public List findAllByGeneration(String generation) { + validateGeneration(generation); + List readModels = generationCrewQueryRepository + .findAllByGenerationTitle(generation); + GenerationCrewCandidates candidates = GenerationCrewCandidates.from(readModels); + List responses = candidates.extractResponses(crewProfileExtractor); + responses.sort(Comparator.comparing(GenerationCrewResponse::name)); + return List.copyOf(responses); + } + + private void validateGeneration(String generation) { + if (generation == null || generation.isBlank()) { + throw new WikiException(ErrorCode.VALIDATION_ERROR); + } + } +} diff --git a/src/main/java/com/wooteco/wiki/document/service/CrewProfileExtractor.java b/src/main/java/com/wooteco/wiki/document/service/CrewProfileExtractor.java new file mode 100644 index 0000000..a66cd56 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/service/CrewProfileExtractor.java @@ -0,0 +1,55 @@ +package com.wooteco.wiki.document.service; + +import com.wooteco.wiki.document.domain.CrewField; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.springframework.stereotype.Component; + +@Component +public class CrewProfileExtractor { + + private static final String NAME_DELIMITER = "("; + private static final Map FIELD_BY_ORGANIZATION_TITLE = Map.of( + "๋ฐฑ์—”๋“œ", CrewField.BACKEND, + "ํ”„๋ก ํŠธ์—”๋“œ", CrewField.FRONTEND, + "์•ˆ๋“œ๋กœ์ด๋“œ", CrewField.ANDROID + ); + + public Optional extractName(String title) { + int delimiterIndex = title.indexOf(NAME_DELIMITER); + String extractedName = title; + if (delimiterIndex >= 0) { + extractedName = title.substring(0, delimiterIndex); + } + String name = extractedName.trim(); + if (name.isEmpty()) { + return Optional.empty(); + } + return Optional.of(name); + } + + public Optional extractField(List organizationTitles) { + Set fields = new HashSet<>(); + for (String organizationTitle : organizationTitles) { + addFieldIfKnown(fields, organizationTitle); + } + if (fields.size() != 1) { + return Optional.empty(); + } + return Optional.of(fields.iterator().next()); + } + + private void addFieldIfKnown( + Set fields, + String organizationTitle + ) { + CrewField field = FIELD_BY_ORGANIZATION_TITLE.get(organizationTitle); + if (field == null) { + return; + } + fields.add(field); + } +} 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 4767496..39dc4bd 100644 --- a/src/main/java/com/wooteco/wiki/document/service/DocumentService.java +++ b/src/main/java/com/wooteco/wiki/document/service/DocumentService.java @@ -1,6 +1,7 @@ package com.wooteco.wiki.document.service; import com.wooteco.wiki.document.domain.Document; +import com.wooteco.wiki.document.domain.dto.DocumentTitleListResponse; import com.wooteco.wiki.document.domain.dto.DocumentUuidResponse; import com.wooteco.wiki.document.repository.DocumentRepository; import com.wooteco.wiki.global.common.PagingRequest; @@ -26,6 +27,10 @@ public Page findAll(PagingRequest pagingRequest) { return documentRepository.findAll(pagingRequest.toPageable()); } + public List findAllTitles() { + return documentRepository.findAllTitles(); + } + public DocumentUuidResponse getUuidByTitle(String title) { return documentRepository.findUuidByTitle(title) .map(DocumentUuidResponse::new) @@ -47,4 +52,3 @@ public void flushViews(Map views) { documentRepository.saveAll(documents); } } - diff --git a/src/main/java/com/wooteco/wiki/document/service/DocumentServiceJava.java b/src/main/java/com/wooteco/wiki/document/service/DocumentServiceJava.java index bbe971b..60f73e6 100644 --- a/src/main/java/com/wooteco/wiki/document/service/DocumentServiceJava.java +++ b/src/main/java/com/wooteco/wiki/document/service/DocumentServiceJava.java @@ -31,8 +31,10 @@ public List searchOrganizationDocument(UUID return toOrganizationDocumentTitleAndUuidResponses(organizationDocumentResponsesByDocument); } - public void addOrganizationDocument(UUID documentUuid, - DocumentOrganizationMappingAddRequest documentOrganizationMappingAddRequest) { + public void addOrganizationDocument( + UUID documentUuid, + DocumentOrganizationMappingAddRequest documentOrganizationMappingAddRequest + ) { CrewDocument crewDocument = getCrewDocument(documentUuid); OrganizationDocument organizationDocument = organizationDocumentRepository.save( documentOrganizationMappingAddRequest.toOrganizationDocument()); @@ -40,7 +42,10 @@ public void addOrganizationDocument(UUID documentUuid, documentOrganizationLinkService.link(crewDocument, organizationDocument); } - public void deleteOrganizationDocument(UUID documentUuid, UUID organizationDocumentUuid) { + public void deleteOrganizationDocument( + UUID documentUuid, + UUID organizationDocumentUuid + ) { CrewDocument crewDocument = getCrewDocument(documentUuid); OrganizationDocument organizationDocument = getOrganizationDocument(organizationDocumentUuid); diff --git a/src/main/java/com/wooteco/wiki/document/service/GenerationCrewCandidates.java b/src/main/java/com/wooteco/wiki/document/service/GenerationCrewCandidates.java new file mode 100644 index 0000000..2a590da --- /dev/null +++ b/src/main/java/com/wooteco/wiki/document/service/GenerationCrewCandidates.java @@ -0,0 +1,104 @@ +package com.wooteco.wiki.document.service; + +import com.wooteco.wiki.document.domain.CrewField; +import com.wooteco.wiki.document.dto.GenerationCrewResponse; +import com.wooteco.wiki.document.repository.GenerationCrewOrganizationReadModel; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +final class GenerationCrewCandidates { + + private final List values; + + private GenerationCrewCandidates(List values) { + this.values = List.copyOf(values); + } + + static GenerationCrewCandidates from(List readModels) { + Map> organizationTitlesByCrew = new LinkedHashMap<>(); + for (GenerationCrewOrganizationReadModel readModel : readModels) { + addOrganizationTitle(organizationTitlesByCrew, readModel); + } + List candidates = createCandidates(organizationTitlesByCrew); + return new GenerationCrewCandidates(candidates); + } + + List extractResponses(CrewProfileExtractor crewProfileExtractor) { + List responses = new ArrayList<>(); + for (GenerationCrewCandidate candidate : values) { + Optional response = candidate.extractResponse(crewProfileExtractor); + response.ifPresent(responses::add); + } + return responses; + } + + private static void addOrganizationTitle( + Map> organizationTitlesByCrew, + GenerationCrewOrganizationReadModel readModel + ) { + CrewIdentity crewIdentity = CrewIdentity.from(readModel); + List organizationTitles = organizationTitlesByCrew.computeIfAbsent( + crewIdentity, + ignored -> new ArrayList<>() + ); + organizationTitles.add(readModel.organizationTitle()); + } + + private static List createCandidates( + Map> organizationTitlesByCrew + ) { + List candidates = new ArrayList<>(); + organizationTitlesByCrew.forEach((crewIdentity, organizationTitles) -> candidates.add( + GenerationCrewCandidate.of(crewIdentity, organizationTitles) + )); + return candidates; + } + + private record CrewIdentity( + String title, + UUID documentUuid + ) { + + private static CrewIdentity from(GenerationCrewOrganizationReadModel readModel) { + return new CrewIdentity(readModel.crewTitle(), readModel.documentUuid()); + } + } + + private record GenerationCrewCandidate( + CrewIdentity crewIdentity, + List organizationTitles + ) { + + private GenerationCrewCandidate { + organizationTitles = List.copyOf(organizationTitles); + } + + private static GenerationCrewCandidate of( + CrewIdentity crewIdentity, + List organizationTitles + ) { + return new GenerationCrewCandidate(crewIdentity, organizationTitles); + } + + private Optional extractResponse( + CrewProfileExtractor crewProfileExtractor + ) { + Optional name = crewProfileExtractor.extractName(crewIdentity.title()); + if (name.isEmpty()) { + return Optional.empty(); + } + CrewField field = crewProfileExtractor.extractField(organizationTitles) + .orElse(null); + GenerationCrewResponse response = GenerationCrewResponse.of( + name.get(), + crewIdentity.documentUuid(), + field + ); + return Optional.of(response); + } + } +} diff --git a/src/main/java/com/wooteco/wiki/global/auth/domain/dto/AuthTokensResponse.java b/src/main/java/com/wooteco/wiki/global/auth/domain/dto/AuthTokensResponse.java index 9bf42ae..21f37bc 100644 --- a/src/main/java/com/wooteco/wiki/global/auth/domain/dto/AuthTokensResponse.java +++ b/src/main/java/com/wooteco/wiki/global/auth/domain/dto/AuthTokensResponse.java @@ -1,4 +1,7 @@ package com.wooteco.wiki.global.auth.domain.dto; -public record AuthTokensResponse(String accessToken, String refreshToken) { +public record AuthTokensResponse( + String accessToken, + String refreshToken +) { } diff --git a/src/main/java/com/wooteco/wiki/global/auth/domain/dto/TokenInfoResponse.java b/src/main/java/com/wooteco/wiki/global/auth/domain/dto/TokenInfoResponse.java index 88a76e8..6952852 100644 --- a/src/main/java/com/wooteco/wiki/global/auth/domain/dto/TokenInfoResponse.java +++ b/src/main/java/com/wooteco/wiki/global/auth/domain/dto/TokenInfoResponse.java @@ -3,9 +3,15 @@ import com.wooteco.wiki.admin.domain.Admin; import com.wooteco.wiki.global.auth.Role; -public record TokenInfoResponse (Long id, Role role) { +public record TokenInfoResponse( + Long id, + Role role +) { - public static TokenInfoResponse of(Admin admin, Role role) { + public static TokenInfoResponse of( + Admin admin, + Role role + ) { return new TokenInfoResponse(admin.getId(), role); } } diff --git a/src/main/java/com/wooteco/wiki/global/auth/service/AuthService.java b/src/main/java/com/wooteco/wiki/global/auth/service/AuthService.java index e1eac58..78bf864 100644 --- a/src/main/java/com/wooteco/wiki/global/auth/service/AuthService.java +++ b/src/main/java/com/wooteco/wiki/global/auth/service/AuthService.java @@ -18,7 +18,10 @@ public class AuthService { private final JwtTokenProvider jwtTokenProvider; private final AdminRepository adminRepository; - public AuthService(JwtTokenProvider jwtTokenProvider, AdminRepository adminRepository) { + public AuthService( + JwtTokenProvider jwtTokenProvider, + AdminRepository adminRepository + ) { this.jwtTokenProvider = jwtTokenProvider; this.adminRepository = adminRepository; } diff --git a/src/main/java/com/wooteco/wiki/global/common/ApiResponse.java b/src/main/java/com/wooteco/wiki/global/common/ApiResponse.java index f6f0772..c9bfc1e 100644 --- a/src/main/java/com/wooteco/wiki/global/common/ApiResponse.java +++ b/src/main/java/com/wooteco/wiki/global/common/ApiResponse.java @@ -7,13 +7,22 @@ public class ApiResponse extends ResponseEntity { - public ApiResponse(B body, HttpStatus status) { + public ApiResponse( + B body, + HttpStatus status + ) { super(body, status); } - public static record SuccessBody(D data, SuccessCode code) { + public static record SuccessBody( + D data, + SuccessCode code + ) { } - public static record FailureBody(ErrorCode code, String message) { + public static record FailureBody( + ErrorCode code, + String message + ) { } } diff --git a/src/main/java/com/wooteco/wiki/global/common/ApiResponseGenerator.java b/src/main/java/com/wooteco/wiki/global/common/ApiResponseGenerator.java index e510176..dbe1c79 100644 --- a/src/main/java/com/wooteco/wiki/global/common/ApiResponseGenerator.java +++ b/src/main/java/com/wooteco/wiki/global/common/ApiResponseGenerator.java @@ -10,7 +10,10 @@ public static ApiResponse> success(D data) { return new ApiResponse<>(new ApiResponse.SuccessBody<>(data, SuccessCode.SUCCESS), HttpStatus.OK); } - public static ApiResponse> success(D data, HttpStatus status) { + public static ApiResponse> success( + D data, + HttpStatus status + ) { return new ApiResponse<>(new ApiResponse.SuccessBody<>(data, SuccessCode.SUCCESS), status); } @@ -18,7 +21,11 @@ public static ApiResponse> success(HttpStatus stat return new ApiResponse<>(new ApiResponse.SuccessBody<>(null, SuccessCode.SUCCESS), status); } - public static ApiResponse failure(ErrorCode code, String message, HttpStatus status) { + public static ApiResponse failure( + ErrorCode code, + String message, + HttpStatus status + ) { return new ApiResponse<>(new ApiResponse.FailureBody(code, message), status); } diff --git a/src/main/java/com/wooteco/wiki/global/common/PagedResponse.java b/src/main/java/com/wooteco/wiki/global/common/PagedResponse.java index 3b835cf..9367ceb 100644 --- a/src/main/java/com/wooteco/wiki/global/common/PagedResponse.java +++ b/src/main/java/com/wooteco/wiki/global/common/PagedResponse.java @@ -1,8 +1,16 @@ package com.wooteco.wiki.global.common; -public record PagedResponse (int page, int totalPage, T data){ +public record PagedResponse( + int page, + int totalPage, + T data +) { - public static PagedResponse of(int page, int totalPage, T data) { + public static PagedResponse of( + int page, + int totalPage, + T data + ) { return new PagedResponse<>(page, totalPage, data); } } diff --git a/src/main/java/com/wooteco/wiki/global/config/interceptor/LoginCheckInterceptor.java b/src/main/java/com/wooteco/wiki/global/config/interceptor/LoginCheckInterceptor.java index a7f7a16..1b3faaf 100644 --- a/src/main/java/com/wooteco/wiki/global/config/interceptor/LoginCheckInterceptor.java +++ b/src/main/java/com/wooteco/wiki/global/config/interceptor/LoginCheckInterceptor.java @@ -23,7 +23,11 @@ public LoginCheckInterceptor(JwtTokenProvider jwtTokenProvider) { } @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) { String token = extractTokenFromCookie(request); if (token == null) { diff --git a/src/main/java/com/wooteco/wiki/global/config/resolver/AuthArgumentResolver.java b/src/main/java/com/wooteco/wiki/global/config/resolver/AuthArgumentResolver.java index 47f6331..976b54b 100644 --- a/src/main/java/com/wooteco/wiki/global/config/resolver/AuthArgumentResolver.java +++ b/src/main/java/com/wooteco/wiki/global/config/resolver/AuthArgumentResolver.java @@ -16,8 +16,12 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) throws Exception { return 1L;// ํšŒ์›์˜ ์•„์ด๋”” ๋ฐ˜ํ™˜ } } diff --git a/src/main/java/com/wooteco/wiki/global/config/resolver/LoginMemberArgumentResolver.java b/src/main/java/com/wooteco/wiki/global/config/resolver/LoginMemberArgumentResolver.java index 4f60754..b011ee8 100644 --- a/src/main/java/com/wooteco/wiki/global/config/resolver/LoginMemberArgumentResolver.java +++ b/src/main/java/com/wooteco/wiki/global/config/resolver/LoginMemberArgumentResolver.java @@ -20,7 +20,10 @@ public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolve private final JwtTokenProvider jwtTokenProvider; private final AuthService authService; - public LoginMemberArgumentResolver(JwtTokenProvider jwtTokenProvider, AuthService authService) { + public LoginMemberArgumentResolver( + JwtTokenProvider jwtTokenProvider, + AuthService authService + ) { this.jwtTokenProvider = jwtTokenProvider; this.authService = authService; } @@ -31,10 +34,12 @@ public boolean supportsParameter(MethodParameter methodParameter) { } @Override - public Admin resolveArgument(MethodParameter methodParameter, - ModelAndViewContainer mavContainer, - NativeWebRequest nativeWebRequest, - WebDataBinderFactory binderFactory) { + public Admin resolveArgument( + MethodParameter methodParameter, + ModelAndViewContainer mavContainer, + NativeWebRequest nativeWebRequest, + WebDataBinderFactory binderFactory + ) { HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest(); String token = extractTokenFromCookie(request); diff --git a/src/main/java/com/wooteco/wiki/global/exception/ErrorCode.java b/src/main/java/com/wooteco/wiki/global/exception/ErrorCode.java index 13c1f97..512b99a 100644 --- a/src/main/java/com/wooteco/wiki/global/exception/ErrorCode.java +++ b/src/main/java/com/wooteco/wiki/global/exception/ErrorCode.java @@ -28,7 +28,10 @@ public enum ErrorCode { private final String message; private final HttpStatus httpStatus; - ErrorCode(String message, HttpStatus httpStatus) { + ErrorCode( + String message, + HttpStatus httpStatus + ) { this.message = message; this.httpStatus = httpStatus; } diff --git a/src/main/java/com/wooteco/wiki/global/exception/SuccessCode.java b/src/main/java/com/wooteco/wiki/global/exception/SuccessCode.java index 059f75e..a6942a4 100644 --- a/src/main/java/com/wooteco/wiki/global/exception/SuccessCode.java +++ b/src/main/java/com/wooteco/wiki/global/exception/SuccessCode.java @@ -11,7 +11,10 @@ public enum SuccessCode { private final String message; private final HttpStatus httpStatus; - SuccessCode(String message, HttpStatus httpStatus) { + SuccessCode( + String message, + HttpStatus httpStatus + ) { this.message = message; this.httpStatus = httpStatus; } diff --git a/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java b/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java index 76ce0a6..d01c5d9 100644 --- a/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java +++ b/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java @@ -4,12 +4,14 @@ import com.wooteco.wiki.global.common.ApiResponseGenerator; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -@ControllerAdvice @Slf4j +@ControllerAdvice public class WikiExceptionHandler { @ExceptionHandler(WikiException.class) @@ -31,4 +33,19 @@ public ApiResponse handle(MethodArgumentNotValidExcepti log.error(exception.getMessage(), exception); return ApiResponseGenerator.failure(ErrorCode.VALIDATION_ERROR); } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ApiResponse handle(MissingServletRequestParameterException exception) { + return handleRequestParameter(exception); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ApiResponse handle(MethodArgumentTypeMismatchException exception) { + return handleRequestParameter(exception); + } + + private ApiResponse handleRequestParameter(Exception exception) { + log.warn(exception.getMessage()); + return ApiResponseGenerator.failure(ErrorCode.VALIDATION_ERROR); + } } diff --git a/src/main/java/com/wooteco/wiki/graph/controller/CrewGraphController.java b/src/main/java/com/wooteco/wiki/graph/controller/CrewGraphController.java new file mode 100644 index 0000000..3fdebe2 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/controller/CrewGraphController.java @@ -0,0 +1,38 @@ +package com.wooteco.wiki.graph.controller; + +import com.wooteco.wiki.global.common.ApiResponse; +import com.wooteco.wiki.global.common.ApiResponse.SuccessBody; +import com.wooteco.wiki.global.common.ApiResponseGenerator; +import com.wooteco.wiki.graph.dto.CrewGraphResponse; +import com.wooteco.wiki.graph.service.CrewGraphQueryService; +import io.swagger.v3.oas.annotations.Operation; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/graph") +public class CrewGraphController { + + private final CrewGraphQueryService crewGraphQueryService; + + @Operation(summary = "ํฌ๋ฃจ ๊ด€๊ณ„ ๊ทธ๋ž˜ํ”„ ์กฐํšŒ", description = "๊ธฐ์ˆ˜์— ์†ํ•œ ํฌ๋ฃจ ๋ฌธ์„œ ๊ด€๊ณ„๋ฅผ ์กฐํšŒํ•˜๊ณ , ์กฐ์ง ์„ ํƒ ์‹œ ์กฐ์ง ๋…ธ๋“œ์™€ ์—ฐ๊ฒฐ ๊ฐ„์„ ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.") + @GetMapping + public ApiResponse> findByGeneration( + @RequestParam String generation, + @RequestParam( + name = "organizationDocumentUuid", + required = false + ) UUID selectedOrganizationDocumentUuid + ) { + CrewGraphResponse response = crewGraphQueryService.findByGeneration( + generation, + selectedOrganizationDocumentUuid + ); + return ApiResponseGenerator.success(response); + } +} diff --git a/src/main/java/com/wooteco/wiki/graph/dto/CrewGraphResponse.java b/src/main/java/com/wooteco/wiki/graph/dto/CrewGraphResponse.java new file mode 100644 index 0000000..f0418c1 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/dto/CrewGraphResponse.java @@ -0,0 +1,21 @@ +package com.wooteco.wiki.graph.dto; + +import java.util.List; + +public record CrewGraphResponse( + List nodes, + List edges +) { + + public CrewGraphResponse { + nodes = List.copyOf(nodes); + edges = List.copyOf(edges); + } + + public static CrewGraphResponse of( + List nodes, + List edges + ) { + return new CrewGraphResponse(nodes, edges); + } +} diff --git a/src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeResponse.java b/src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeResponse.java new file mode 100644 index 0000000..b6cd098 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeResponse.java @@ -0,0 +1,10 @@ +package com.wooteco.wiki.graph.dto; + +import java.util.UUID; + +public record GraphEdgeResponse( + UUID sourceDocumentUuid, + UUID targetDocumentUuid, + GraphEdgeType type +) { +} diff --git a/src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeType.java b/src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeType.java new file mode 100644 index 0000000..6277eb0 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeType.java @@ -0,0 +1,6 @@ +package com.wooteco.wiki.graph.dto; + +public enum GraphEdgeType { + REFERENCE, + ORGANIZATION_LINK +} diff --git a/src/main/java/com/wooteco/wiki/graph/dto/GraphNodeResponse.java b/src/main/java/com/wooteco/wiki/graph/dto/GraphNodeResponse.java new file mode 100644 index 0000000..2e33275 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/dto/GraphNodeResponse.java @@ -0,0 +1,32 @@ +package com.wooteco.wiki.graph.dto; + +import java.util.UUID; + +public record GraphNodeResponse( + UUID documentUuid, + String title, + GraphNodeType type +) { + + public static GraphNodeResponse fromCrewDocument( + UUID documentUuid, + String title + ) { + return new GraphNodeResponse( + documentUuid, + title, + GraphNodeType.CREW + ); + } + + public static GraphNodeResponse fromOrganizationDocument( + UUID documentUuid, + String title + ) { + return new GraphNodeResponse( + documentUuid, + title, + GraphNodeType.ORGANIZATION + ); + } +} diff --git a/src/main/java/com/wooteco/wiki/graph/dto/GraphNodeType.java b/src/main/java/com/wooteco/wiki/graph/dto/GraphNodeType.java new file mode 100644 index 0000000..3d6d26c --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/dto/GraphNodeType.java @@ -0,0 +1,6 @@ +package com.wooteco.wiki.graph.dto; + +public enum GraphNodeType { + CREW, + ORGANIZATION +} diff --git a/src/main/java/com/wooteco/wiki/graph/repository/CrewGraphQueryRepository.java b/src/main/java/com/wooteco/wiki/graph/repository/CrewGraphQueryRepository.java new file mode 100644 index 0000000..fd9b8c6 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/repository/CrewGraphQueryRepository.java @@ -0,0 +1,14 @@ +package com.wooteco.wiki.graph.repository; + +import java.util.List; +import java.util.UUID; + +public interface CrewGraphQueryRepository { + + List findAllCrewDocumentsByGenerationTitle(String generationTitle); + + List findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid( + String generationTitle, + UUID organizationDocumentUuid + ); +} diff --git a/src/main/java/com/wooteco/wiki/graph/repository/CrewGraphReadModel.java b/src/main/java/com/wooteco/wiki/graph/repository/CrewGraphReadModel.java new file mode 100644 index 0000000..37203d5 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/repository/CrewGraphReadModel.java @@ -0,0 +1,10 @@ +package com.wooteco.wiki.graph.repository; + +import java.util.UUID; + +public record CrewGraphReadModel( + UUID documentUuid, + String title, + String contents +) { +} diff --git a/src/main/java/com/wooteco/wiki/graph/service/CrewDocumentReferenceExtractor.java b/src/main/java/com/wooteco/wiki/graph/service/CrewDocumentReferenceExtractor.java new file mode 100644 index 0000000..6d3b075 --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/service/CrewDocumentReferenceExtractor.java @@ -0,0 +1,58 @@ +package com.wooteco.wiki.graph.service; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.springframework.stereotype.Component; + +@Component +public class CrewDocumentReferenceExtractor { + + private static final String UUID_PATTERN = + "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"; + private static final Pattern INTERNAL_DOCUMENT_LINK_PATTERN = Pattern.compile( + "(?" + UUID_PATTERN + ")" + + "(?=$|[^A-Za-z0-9._~-])" + ); + + public List extract(String contents) { + if (contents == null || contents.isBlank()) { + return List.of(); + } + Set references = new LinkedHashSet<>(); + Matcher matcher = INTERNAL_DOCUMENT_LINK_PATTERN.matcher(contents); + while (matcher.find()) { + addReferenceIfValid(contents, matcher, references); + } + return List.copyOf(references); + } + + private void addReferenceIfValid( + String contents, + Matcher matcher, + Set references + ) { + if (isMarkdownImage(contents, matcher.start())) { + return; + } + references.add(UUID.fromString(matcher.group("uuid"))); + } + + private boolean isMarkdownImage( + String contents, + int linkStart + ) { + boolean startsAfterMarkdownLabel = linkStart >= 3 + && contents.charAt(linkStart - 1) == '(' + && contents.charAt(linkStart - 2) == ']'; + if (!startsAfterMarkdownLabel) { + return false; + } + int altTextStart = contents.lastIndexOf('[', linkStart - 2); + return altTextStart > 0 && contents.charAt(altTextStart - 1) == '!'; + } +} diff --git a/src/main/java/com/wooteco/wiki/graph/service/CrewGraphQueryService.java b/src/main/java/com/wooteco/wiki/graph/service/CrewGraphQueryService.java new file mode 100644 index 0000000..ea9e30d --- /dev/null +++ b/src/main/java/com/wooteco/wiki/graph/service/CrewGraphQueryService.java @@ -0,0 +1,205 @@ +package com.wooteco.wiki.graph.service; + +import com.wooteco.wiki.global.exception.ErrorCode; +import com.wooteco.wiki.global.exception.WikiException; +import com.wooteco.wiki.graph.dto.CrewGraphResponse; +import com.wooteco.wiki.graph.dto.GraphEdgeResponse; +import com.wooteco.wiki.graph.dto.GraphEdgeType; +import com.wooteco.wiki.graph.dto.GraphNodeResponse; +import com.wooteco.wiki.graph.repository.CrewGraphQueryRepository; +import com.wooteco.wiki.graph.repository.CrewGraphReadModel; +import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; +import com.wooteco.wiki.organizationdocument.repository.OrganizationDocumentRepository; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class CrewGraphQueryService { + + private final CrewGraphQueryRepository crewGraphQueryRepository; + private final OrganizationDocumentRepository organizationDocumentRepository; + private final CrewDocumentReferenceExtractor crewDocumentReferenceExtractor; + + @Transactional(readOnly = true) + public CrewGraphResponse findByGeneration(String generation) { + return findByGeneration(generation, null); + } + + @Transactional(readOnly = true) + public CrewGraphResponse findByGeneration( + String generation, + UUID organizationDocumentUuid + ) { + validateGeneration(generation); + List readModels = crewGraphQueryRepository.findAllCrewDocumentsByGenerationTitle(generation); + List nodes = new ArrayList<>(createCrewNodes(readModels)); + List edges = new ArrayList<>(createReferenceEdges(readModels)); + addOrganizationGraphIfSelected( + generation, + organizationDocumentUuid, + nodes, + edges + ); + return CrewGraphResponse.of(nodes, edges); + } + + private void validateGeneration(String generation) { + if (generation == null || generation.isBlank()) { + throw new WikiException(ErrorCode.VALIDATION_ERROR); + } + } + + private List createCrewNodes(List readModels) { + List nodes = new ArrayList<>(); + for (CrewGraphReadModel readModel : readModels) { + GraphNodeResponse node = GraphNodeResponse.fromCrewDocument( + readModel.documentUuid(), + readModel.title() + ); + nodes.add(node); + } + return List.copyOf(nodes); + } + + private List createReferenceEdges(List readModels) { + Set nodeDocumentUuids = createCrewDocumentUuids(readModels); + Set edges = new HashSet<>(); + for (CrewGraphReadModel readModel : readModels) { + addReferenceEdges(readModel, nodeDocumentUuids, edges); + } + List sortedEdges = new ArrayList<>(edges); + sortedEdges.sort(Comparator + .comparing(GraphEdgeResponse::sourceDocumentUuid) + .thenComparing(GraphEdgeResponse::targetDocumentUuid)); + return List.copyOf(sortedEdges); + } + + private Set createCrewDocumentUuids(List readModels) { + Set documentUuids = new HashSet<>(); + for (CrewGraphReadModel readModel : readModels) { + documentUuids.add(readModel.documentUuid()); + } + return documentUuids; + } + + private void addReferenceEdges( + CrewGraphReadModel sourceDocument, + Set nodeDocumentUuids, + Set edges + ) { + List referencedDocumentUuids = crewDocumentReferenceExtractor.extract(sourceDocument.contents()); + for (UUID targetDocumentUuid : referencedDocumentUuids) { + addReferenceEdgeIfValid( + sourceDocument.documentUuid(), + targetDocumentUuid, + nodeDocumentUuids, + edges + ); + } + } + + private void addReferenceEdgeIfValid( + UUID sourceDocumentUuid, + UUID targetDocumentUuid, + Set nodeDocumentUuids, + Set edges + ) { + if (sourceDocumentUuid.equals(targetDocumentUuid)) { + return; + } + if (!nodeDocumentUuids.contains(targetDocumentUuid)) { + return; + } + GraphEdgeResponse edge = createReferenceEdge(sourceDocumentUuid, targetDocumentUuid); + edges.add(edge); + } + + private GraphEdgeResponse createReferenceEdge( + UUID sourceDocumentUuid, + UUID targetDocumentUuid + ) { + if (sourceDocumentUuid.compareTo(targetDocumentUuid) < 0) { + return new GraphEdgeResponse( + sourceDocumentUuid, + targetDocumentUuid, + GraphEdgeType.REFERENCE + ); + } + return new GraphEdgeResponse( + targetDocumentUuid, + sourceDocumentUuid, + GraphEdgeType.REFERENCE + ); + } + + private void addOrganizationGraphIfSelected( + String generation, + UUID organizationDocumentUuid, + List nodes, + List edges + ) { + if (organizationDocumentUuid == null) { + return; + } + OrganizationDocument organizationDocument = findOrganizationDocument(organizationDocumentUuid); + validateOrganizationIsNotGeneration(generation, organizationDocument); + if (nodes.isEmpty()) { + return; + } + nodes.add(GraphNodeResponse.fromOrganizationDocument( + organizationDocument.getUuid(), + organizationDocument.getTitle() + )); + List linkedCrewDocumentUuids = crewGraphQueryRepository + .findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid( + generation, + organizationDocumentUuid + ); + addOrganizationLinkEdges( + organizationDocumentUuid, + linkedCrewDocumentUuids, + edges + ); + } + + private OrganizationDocument findOrganizationDocument(UUID organizationDocumentUuid) { + Optional organizationDocument = organizationDocumentRepository.findByUuid( + organizationDocumentUuid + ); + return organizationDocument.orElseThrow( + () -> new WikiException(ErrorCode.ORGANIZATION_DOCUMENT_NOT_FOUND) + ); + } + + private void validateOrganizationIsNotGeneration( + String generation, + OrganizationDocument organizationDocument + ) { + if (generation.equals(organizationDocument.getTitle())) { + throw new WikiException(ErrorCode.VALIDATION_ERROR); + } + } + + private void addOrganizationLinkEdges( + UUID organizationDocumentUuid, + List linkedCrewDocumentUuids, + List edges + ) { + for (UUID crewDocumentUuid : linkedCrewDocumentUuids) { + edges.add(new GraphEdgeResponse( + organizationDocumentUuid, + crewDocumentUuid, + GraphEdgeType.ORGANIZATION_LINK + )); + } + } +} diff --git a/src/main/java/com/wooteco/wiki/history/domain/History.java b/src/main/java/com/wooteco/wiki/history/domain/History.java index b3b6228..957e324 100644 --- a/src/main/java/com/wooteco/wiki/history/domain/History.java +++ b/src/main/java/com/wooteco/wiki/history/domain/History.java @@ -48,8 +48,15 @@ public class History { protected History() { } - public History(String title, String contents, String writer, Long documentBytes, LocalDateTime generateTime, - Document document, Long version) { + public History( + String title, + String contents, + String writer, + Long documentBytes, + LocalDateTime generateTime, + Document document, + Long version + ) { this.title = title; this.contents = contents; this.writer = writer; diff --git a/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryDetailResponse.java b/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryDetailResponse.java index 44ec3d2..d02fc83 100644 --- a/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryDetailResponse.java +++ b/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryDetailResponse.java @@ -2,6 +2,11 @@ import java.time.LocalDateTime; -public record HistoryDetailResponse(Long logId, String title, String contents, String writer, - LocalDateTime generateTime) { +public record HistoryDetailResponse( + Long logId, + String title, + String contents, + String writer, + LocalDateTime generateTime +) { } diff --git a/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryResponse.java b/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryResponse.java index e1c0d6e..9ae9d2d 100644 --- a/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryResponse.java +++ b/src/main/java/com/wooteco/wiki/history/domain/dto/HistoryResponse.java @@ -3,8 +3,14 @@ import com.wooteco.wiki.history.domain.History; import java.time.LocalDateTime; -public record HistoryResponse(Long id, String title, Long version, String writer, Long documentBytes, - LocalDateTime generateTime) { +public record HistoryResponse( + Long id, + String title, + Long version, + String writer, + Long documentBytes, + LocalDateTime generateTime +) { public static HistoryResponse of(History history) { return new HistoryResponse( diff --git a/src/main/java/com/wooteco/wiki/history/repository/HistoryRepository.java b/src/main/java/com/wooteco/wiki/history/repository/HistoryRepository.java index 3e5925a..b86deca 100644 --- a/src/main/java/com/wooteco/wiki/history/repository/HistoryRepository.java +++ b/src/main/java/com/wooteco/wiki/history/repository/HistoryRepository.java @@ -11,7 +11,10 @@ public interface HistoryRepository extends JpaRepository { @Query("SELECT h FROM History h WHERE h.document.id = :documentId") - Page findAllByDocumentId(Long documentId, Pageable pageable); + Page findAllByDocumentId( + Long documentId, + Pageable pageable + ); @Query("SELECT MAX(h.version) FROM History h WHERE h.document.id = :documentId") Optional findMaxVersionByDocumentId(@Param("documentId") Long documentId); diff --git a/src/main/java/com/wooteco/wiki/history/service/HistoryService.java b/src/main/java/com/wooteco/wiki/history/service/HistoryService.java index 4799e71..e4ea07c 100644 --- a/src/main/java/com/wooteco/wiki/history/service/HistoryService.java +++ b/src/main/java/com/wooteco/wiki/history/service/HistoryService.java @@ -47,7 +47,10 @@ public HistoryDetailResponse getLogDetail(Long logId) { } @Transactional(readOnly = true) - public Page findAllByDocumentUuid(UUID documentUuid, PagingRequest pageRequestDto) { + public Page findAllByDocumentUuid( + UUID documentUuid, + PagingRequest pageRequestDto + ) { Long documentId = documentRepository.findIdByUuid(documentUuid) .orElseThrow(() -> new WikiException(DOCUMENT_NOT_FOUND)); diff --git a/src/main/java/com/wooteco/wiki/logging/HttpLogger.java b/src/main/java/com/wooteco/wiki/logging/HttpLogger.java index 20ce0fc..86cd585 100755 --- a/src/main/java/com/wooteco/wiki/logging/HttpLogger.java +++ b/src/main/java/com/wooteco/wiki/logging/HttpLogger.java @@ -21,8 +21,11 @@ public class HttpLogger extends OncePerRequestFilter { @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { String requestId = UUID.randomUUID().toString(); request.setAttribute("requestId", requestId); ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request); @@ -59,7 +62,10 @@ private String makeResponseBodyLog(ContentCachingResponseWrapper responseWrapper } } - private List makeResponseHeaderLogs(String requestId, HttpServletResponse response) { + private List makeResponseHeaderLogs( + String requestId, + HttpServletResponse response + ) { List responseHeaderLogs = new ArrayList<>(); responseHeaderLogs.add(String.format("%s = %s %s %s", "RequestId", requestId, "<>", "Headers")); for (String s : response.getHeaderNames()) { @@ -69,7 +75,11 @@ private List makeResponseHeaderLogs(String requestId, HttpServletRespons return responseHeaderLogs; } - private void printResponseLogs(String requestId, List responseHeaderLogs, String responseBodyLog) { + private void printResponseLogs( + String requestId, + List responseHeaderLogs, + String responseBodyLog + ) { for (String s : responseHeaderLogs) { log.info(s); } diff --git a/src/main/java/com/wooteco/wiki/member/domain/dto/JoinRequest.java b/src/main/java/com/wooteco/wiki/member/domain/dto/JoinRequest.java index 61a900b..d2973c6 100644 --- a/src/main/java/com/wooteco/wiki/member/domain/dto/JoinRequest.java +++ b/src/main/java/com/wooteco/wiki/member/domain/dto/JoinRequest.java @@ -1,4 +1,8 @@ package com.wooteco.wiki.member.domain.dto; -public record JoinRequest(String email, String nickname, String password) { +public record JoinRequest( + String email, + String nickname, + String password +) { } diff --git a/src/main/java/com/wooteco/wiki/member/repository/MemberRepository.java b/src/main/java/com/wooteco/wiki/member/repository/MemberRepository.java index 3ffc6d8..391506b 100644 --- a/src/main/java/com/wooteco/wiki/member/repository/MemberRepository.java +++ b/src/main/java/com/wooteco/wiki/member/repository/MemberRepository.java @@ -7,5 +7,8 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); - Optional findByEmailAndPassword(String email, String password); + Optional findByEmailAndPassword( + String email, + String password + ); } diff --git a/src/main/java/com/wooteco/wiki/organizationdocument/domain/DocumentOrganizationLink.java b/src/main/java/com/wooteco/wiki/organizationdocument/domain/DocumentOrganizationLink.java index 984b75a..5e48eed 100644 --- a/src/main/java/com/wooteco/wiki/organizationdocument/domain/DocumentOrganizationLink.java +++ b/src/main/java/com/wooteco/wiki/organizationdocument/domain/DocumentOrganizationLink.java @@ -43,7 +43,10 @@ public class DocumentOrganizationLink { foreignKey = @ForeignKey(name = "fk_link_orgdoc")) private OrganizationDocument organizationDocument; - public DocumentOrganizationLink(CrewDocument crewDocument, OrganizationDocument organizationDocument) { + public DocumentOrganizationLink( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { this.crewDocument = crewDocument; this.organizationDocument = organizationDocument; } diff --git a/src/main/java/com/wooteco/wiki/organizationdocument/domain/OrganizationDocument.java b/src/main/java/com/wooteco/wiki/organizationdocument/domain/OrganizationDocument.java index 1a08ac4..d9ae498 100644 --- a/src/main/java/com/wooteco/wiki/organizationdocument/domain/OrganizationDocument.java +++ b/src/main/java/com/wooteco/wiki/organizationdocument/domain/OrganizationDocument.java @@ -13,8 +13,13 @@ @DiscriminatorValue("ORGANIZATION") public class OrganizationDocument extends Document { - public OrganizationDocument(final String title, final String contents, final String writer, - final Long documentBytes, final UUID uuid) { + public OrganizationDocument( + final String title, + final String contents, + final String writer, + final Long documentBytes, + final UUID uuid + ) { super(title, contents, writer, documentBytes, uuid); } 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 9b4f16f..b1d34e0 100644 --- a/src/main/java/com/wooteco/wiki/organizationdocument/repository/DocumentOrganizationLinkRepository.java +++ b/src/main/java/com/wooteco/wiki/organizationdocument/repository/DocumentOrganizationLinkRepository.java @@ -1,23 +1,33 @@ package com.wooteco.wiki.organizationdocument.repository; import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.repository.GenerationCrewOrganizationReadModel; +import com.wooteco.wiki.document.repository.GenerationCrewQueryRepository; +import com.wooteco.wiki.graph.repository.CrewGraphQueryRepository; +import com.wooteco.wiki.graph.repository.CrewGraphReadModel; import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink; import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; import java.util.List; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface DocumentOrganizationLinkRepository extends JpaRepository { +public interface DocumentOrganizationLinkRepository extends + JpaRepository, + GenerationCrewQueryRepository, + CrewGraphQueryRepository { Optional findByCrewDocumentAndOrganizationDocument( CrewDocument crewDocument, OrganizationDocument organizationDocument ); - void deleteByCrewDocumentAndOrganizationDocument(CrewDocument crewDocument, - OrganizationDocument organizationDocument); + void deleteByCrewDocumentAndOrganizationDocument( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ); List findAllByCrewDocument(CrewDocument crewDocument); @@ -27,5 +37,58 @@ void deleteByCrewDocumentAndOrganizationDocument(CrewDocument crewDocument, List findAllByOrganizationDocumentWithCrewDocument( @Param("organizationDocument") OrganizationDocument organizationDocument); + @Override + @Query(""" + SELECT new com.wooteco.wiki.document.repository.GenerationCrewOrganizationReadModel( + crewDocument.title, + crewDocument.uuid, + organizationDocument.title + ) + FROM DocumentOrganizationLink documentOrganizationLink + JOIN documentOrganizationLink.crewDocument crewDocument + JOIN documentOrganizationLink.organizationDocument organizationDocument + WHERE crewDocument IN ( + SELECT generationLink.crewDocument + FROM DocumentOrganizationLink generationLink + WHERE generationLink.organizationDocument.title = :generationTitle + ) + """) + List findAllByGenerationTitle( + @Param("generationTitle") String generationTitle + ); + + @Override + @Query(""" + SELECT new com.wooteco.wiki.graph.repository.CrewGraphReadModel( + crewDocument.uuid, + crewDocument.title, + crewDocument.contents + ) + FROM DocumentOrganizationLink documentOrganizationLink + JOIN documentOrganizationLink.crewDocument crewDocument + WHERE documentOrganizationLink.organizationDocument.title = :generationTitle + ORDER BY crewDocument.title + """) + List findAllCrewDocumentsByGenerationTitle( + @Param("generationTitle") String generationTitle + ); + + @Override + @Query(""" + SELECT selectedOrganizationLink.crewDocument.uuid + FROM DocumentOrganizationLink selectedOrganizationLink + WHERE selectedOrganizationLink.organizationDocument.uuid = :organizationDocumentUuid + AND selectedOrganizationLink.crewDocument IN ( + SELECT generationLink.crewDocument + FROM DocumentOrganizationLink generationLink + WHERE generationLink.organizationDocument.title = :generationTitle + ) + ORDER BY selectedOrganizationLink.crewDocument.uuid + """) + List findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid( + @Param("generationTitle") String generationTitle, + @Param("organizationDocumentUuid") UUID organizationDocumentUuid + ); + 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 fcce58a..9e7dc3b 100644 --- a/src/main/java/com/wooteco/wiki/organizationdocument/service/DocumentOrganizationLinkService.java +++ b/src/main/java/com/wooteco/wiki/organizationdocument/service/DocumentOrganizationLinkService.java @@ -17,13 +17,19 @@ public class DocumentOrganizationLinkService { private final DocumentOrganizationLinkRepository documentOrgDocLinkRepository; - public void link(CrewDocument crewDocument, OrganizationDocument organizationDocument) { + public void link( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { DocumentOrganizationLink documentOrgDocLink = new DocumentOrganizationLink(crewDocument, organizationDocument); documentOrgDocLinkRepository.save(documentOrgDocLink); } - public void unlink(CrewDocument crewDocument, OrganizationDocument organizationDocument) { + public void unlink( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { documentOrgDocLinkRepository.deleteByCrewDocumentAndOrganizationDocument(crewDocument, organizationDocument); } diff --git a/src/main/java/com/wooteco/wiki/organizationevent/controller/OrganizationEventController.java b/src/main/java/com/wooteco/wiki/organizationevent/controller/OrganizationEventController.java index 415a5fa..33abb45 100644 --- a/src/main/java/com/wooteco/wiki/organizationevent/controller/OrganizationEventController.java +++ b/src/main/java/com/wooteco/wiki/organizationevent/controller/OrganizationEventController.java @@ -40,7 +40,8 @@ public ApiResponse> post( @PutMapping("/{organizationEventUuid}") public ApiResponse> put( @PathVariable UUID organizationEventUuid, - @RequestBody @Valid OrganizationEventUpdateRequest organizationEventUpdateRequest) { + @RequestBody @Valid OrganizationEventUpdateRequest organizationEventUpdateRequest + ) { OrganizationEventUpdateResponse response = organizationEventService.put(organizationEventUuid, organizationEventUpdateRequest); return ApiResponseGenerator.success(response); diff --git a/src/main/java/com/wooteco/wiki/organizationevent/domain/OrganizationEvent.java b/src/main/java/com/wooteco/wiki/organizationevent/domain/OrganizationEvent.java index 79cc808..1bc2d05 100644 --- a/src/main/java/com/wooteco/wiki/organizationevent/domain/OrganizationEvent.java +++ b/src/main/java/com/wooteco/wiki/organizationevent/domain/OrganizationEvent.java @@ -54,8 +54,13 @@ public class OrganizationEvent { foreignKey = @ForeignKey(name = "fk_event_orgdoc")) private OrganizationDocument organizationDocument; - public static OrganizationEvent create(String title, String contents, String writer, LocalDate occurredAt, - OrganizationDocument organizationDocument) { + public static OrganizationEvent create( + String title, + String contents, + String writer, + LocalDate occurredAt, + OrganizationDocument organizationDocument + ) { return OrganizationEvent.builder() .uuid(UUID.randomUUID()) .title(title.trim()) @@ -66,7 +71,12 @@ public static OrganizationEvent create(String title, String contents, String wri .build(); } - public void update(String title, String contents, String writer, LocalDate occurredAt) { + public void update( + String title, + String contents, + String writer, + LocalDate occurredAt + ) { this.title = title.trim(); this.contents = getContents(contents); this.writer = writer.trim(); diff --git a/src/main/java/com/wooteco/wiki/organizationevent/service/OrganizationEventService.java b/src/main/java/com/wooteco/wiki/organizationevent/service/OrganizationEventService.java index 6374464..373eb1d 100644 --- a/src/main/java/com/wooteco/wiki/organizationevent/service/OrganizationEventService.java +++ b/src/main/java/com/wooteco/wiki/organizationevent/service/OrganizationEventService.java @@ -32,8 +32,10 @@ public OrganizationEventCreateResponse post(OrganizationEventCreateRequest reque return OrganizationEventCreateResponse.from(savedEvent); } - public OrganizationEventUpdateResponse put(UUID organizationEventUuid, - OrganizationEventUpdateRequest request) { + public OrganizationEventUpdateResponse put( + UUID organizationEventUuid, + OrganizationEventUpdateRequest request + ) { OrganizationEvent organizationEvent = getOrganizationEvent(organizationEventUuid); organizationEvent.update( request.title(), diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index b00c30f..4606cc3 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -16,7 +16,7 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect cors: - allowed-origins: ${CORS_ALLOWED_ORIGINS:https://crew-wiki.site, https://api.crew-wiki.site} + allowed-origins: ${CORS_ALLOWED_ORIGINS:https://crew-wiki.site, https://www.crew-wiki.site, https://api.crew-wiki.site} swagger: server-url: ${SWAGGER_SERVER_URL:https://api.crew-wiki.site} diff --git a/src/test/java/com/wooteco/wiki/document/controller/CrewDocumentQueryControllerTest.java b/src/test/java/com/wooteco/wiki/document/controller/CrewDocumentQueryControllerTest.java new file mode 100644 index 0000000..f82924d --- /dev/null +++ b/src/test/java/com/wooteco/wiki/document/controller/CrewDocumentQueryControllerTest.java @@ -0,0 +1,149 @@ +package com.wooteco.wiki.document.controller; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.fixture.CrewDocumentFixture; +import com.wooteco.wiki.document.repository.CrewDocumentRepository; +import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink; +import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; +import com.wooteco.wiki.organizationdocument.fixture.DocumentOrganizationLinkFixture; +import com.wooteco.wiki.organizationdocument.fixture.OrganizationDocumentFixture; +import com.wooteco.wiki.organizationdocument.repository.DocumentOrganizationLinkRepository; +import com.wooteco.wiki.organizationdocument.repository.OrganizationDocumentRepository; +import io.restassured.RestAssured; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +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.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class CrewDocumentQueryControllerTest { + + @LocalServerPort + private int port; + + @Autowired + private CrewDocumentRepository crewDocumentRepository; + + @Autowired + private OrganizationDocumentRepository organizationDocumentRepository; + + @Autowired + private DocumentOrganizationLinkRepository documentOrganizationLinkRepository; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @Nested + @DisplayName("๊ธฐ์ˆ˜๋ณ„ ํฌ๋ฃจ ์กฐํšŒ API๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ") + class FindAllByGeneration { + + @Test + @DisplayName("์„ฑ๊ณต ์‘๋‹ต์— ํฌ๋ฃจ ๋ชฉ๋ก์„ ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byValidGeneration() { + // given + CrewDocument crewDocument = saveCrewDocument("๊ฐ€๋žŒ (8๊ธฐ)"); + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument field = saveOrganizationDocument("๋ฐฑ์—”๋“œ"); + saveLink(crewDocument, generation); + saveLink(crewDocument, field); + + // when & then + RestAssured.given().log().all() + .queryParam("generation", "8๊ธฐ") + .when() + .get("/document/crews") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("data", hasSize(1)) + .body("data[0].name", equalTo("๊ฐ€๋žŒ")) + .body("data[0].documentUuid", equalTo(crewDocument.getUuid().toString())) + .body("data[0].field", equalTo("BACKEND")); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๋ฅผ ๋ˆ„๋ฝํ•˜๋ฉด ๊ฒ€์ฆ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_fail_byMissingGeneration() { + // when & then + assertValidationError("/document/crews"); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ๊ฒ€์ฆ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_fail_byEmptyGeneration() { + // when & then + assertValidationError("/document/crews?generation="); + } + + @Test + @DisplayName("์ž…๋ ฅํ•œ ์กฐ์ง ์ œ๋ชฉ๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๊ธฐ์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byNoExactGenerationTitle() { + // given + CrewDocument crewDocument = saveCrewDocument("๊ฐ€๋žŒ (8๊ธฐ)"); + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + saveLink(crewDocument, generation); + + // when & then + RestAssured.given().log().all() + .queryParam("generation", "8") + .when() + .get("/document/crews") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("data", hasSize(0)); + } + } + + private void assertValidationError(String path) { + RestAssured.given().log().all() + .when() + .get(path) + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .body("code", equalTo("VALIDATION_ERROR")); + } + + private CrewDocument saveCrewDocument(String title) { + CrewDocument crewDocument = CrewDocumentFixture.createCrewDocument( + title, + "contents", + "writer", + 10L, + UUID.randomUUID() + ); + return crewDocumentRepository.save(crewDocument); + } + + private OrganizationDocument saveOrganizationDocument(String title) { + OrganizationDocument organizationDocument = OrganizationDocumentFixture.create( + title, + "contents", + "writer", + 10L, + UUID.randomUUID() + ); + return organizationDocumentRepository.save(organizationDocument); + } + + private void saveLink( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { + DocumentOrganizationLink link = DocumentOrganizationLinkFixture.create( + crewDocument, + organizationDocument + ); + documentOrganizationLinkRepository.save(link); + } +} diff --git a/src/test/java/com/wooteco/wiki/document/fixture/CrewDocumentFixture.java b/src/test/java/com/wooteco/wiki/document/fixture/CrewDocumentFixture.java index c80e1f4..df98cd2 100644 --- a/src/test/java/com/wooteco/wiki/document/fixture/CrewDocumentFixture.java +++ b/src/test/java/com/wooteco/wiki/document/fixture/CrewDocumentFixture.java @@ -6,8 +6,13 @@ public class CrewDocumentFixture { - public static CrewDocument createCrewDocument(String title, String content, String writer, Long documentBytes, - UUID uuid) { + public static CrewDocument createCrewDocument( + String title, + String content, + String writer, + Long documentBytes, + UUID uuid + ) { return new CrewDocument(title, content, writer, documentBytes, uuid); } @@ -15,8 +20,13 @@ public static CrewDocument createDefaultCrewDocument() { return createCrewDocument("defaultCrewTitle", "defaultContent", "defaultWriter", 10L, UUID.randomUUID()); } - public static CrewDocumentCreateRequest createDocumentCreateRequest(String title, String contents, String writer, - Long documentBytes, UUID uuid) { + public static CrewDocumentCreateRequest createDocumentCreateRequest( + String title, + String contents, + String writer, + Long documentBytes, + UUID uuid + ) { return new CrewDocumentCreateRequest(title, contents, writer, documentBytes, uuid); } diff --git a/src/test/java/com/wooteco/wiki/document/service/CrewDocumentQueryServiceTest.java b/src/test/java/com/wooteco/wiki/document/service/CrewDocumentQueryServiceTest.java new file mode 100644 index 0000000..01f0722 --- /dev/null +++ b/src/test/java/com/wooteco/wiki/document/service/CrewDocumentQueryServiceTest.java @@ -0,0 +1,252 @@ +package com.wooteco.wiki.document.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.domain.CrewField; +import com.wooteco.wiki.document.dto.GenerationCrewResponse; +import com.wooteco.wiki.document.fixture.CrewDocumentFixture; +import com.wooteco.wiki.document.repository.CrewDocumentRepository; +import com.wooteco.wiki.global.exception.ErrorCode; +import com.wooteco.wiki.global.exception.WikiException; +import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink; +import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; +import com.wooteco.wiki.organizationdocument.fixture.DocumentOrganizationLinkFixture; +import com.wooteco.wiki.organizationdocument.fixture.OrganizationDocumentFixture; +import com.wooteco.wiki.organizationdocument.repository.DocumentOrganizationLinkRepository; +import com.wooteco.wiki.organizationdocument.repository.OrganizationDocumentRepository; +import java.util.List; +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 CrewDocumentQueryServiceTest { + + @Autowired + private CrewDocumentQueryService crewDocumentQueryService; + + @Autowired + private CrewDocumentRepository crewDocumentRepository; + + @Autowired + private OrganizationDocumentRepository organizationDocumentRepository; + + @Autowired + private DocumentOrganizationLinkRepository documentOrganizationLinkRepository; + + @Nested + @DisplayName("๊ธฐ์ˆ˜๋ณ„ ํฌ๋ฃจ๋ฅผ ์กฐํšŒํ•  ๋•Œ") + class FindAllByGeneration { + + @Test + @DisplayName("์š”์ฒญํ•œ ์กฐ์ง ์ œ๋ชฉ๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๊ธฐ์ˆ˜์˜ ํฌ๋ฃจ๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byExactGenerationTitle() { + // given + OrganizationDocument eighthGeneration = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument spacedEighthGeneration = saveOrganizationDocument("8 ๊ธฐ"); + OrganizationDocument seventhGeneration = saveOrganizationDocument("7๊ธฐ"); + CrewDocument eighthCrew = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + CrewDocument spacedEighthCrew = saveCrewDocument("๋‚˜๋ž˜(8๊ธฐ)"); + CrewDocument seventhCrew = saveCrewDocument("๋‹ค์˜จ(7๊ธฐ)"); + saveLink(eighthCrew, eighthGeneration); + saveLink(spacedEighthCrew, spacedEighthGeneration); + saveLink(seventhCrew, seventhGeneration); + + // when + List responses = crewDocumentQueryService.findAllByGeneration("8๊ธฐ"); + + // then + assertSoftly(softly -> { + softly.assertThat(responses).hasSize(1); + softly.assertThat(responses.get(0).name()).isEqualTo("๊ฐ€๋žŒ"); + softly.assertThat(responses.get(0).documentUuid()).isEqualTo(eighthCrew.getUuid()); + }); + } + + @Test + @DisplayName("ํ—ˆ์šฉ๋œ ์†Œ์†์„ ํฌ๋ฃจ ๋ถ„์•ผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byAllowedFields() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument backend = saveOrganizationDocument("๋ฐฑ์—”๋“œ"); + OrganizationDocument frontend = saveOrganizationDocument("ํ”„๋ก ํŠธ์—”๋“œ"); + OrganizationDocument android = saveOrganizationDocument("์•ˆ๋“œ๋กœ์ด๋“œ"); + OrganizationDocument unknown = saveOrganizationDocument("๋ฐ๋ธŒ์˜ต์Šค"); + CrewDocument backendCrew = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + CrewDocument frontendCrew = saveCrewDocument("๋‚˜๋ž˜(8๊ธฐ)"); + CrewDocument androidCrew = saveCrewDocument("๋‹ค์˜จ(8๊ธฐ)"); + saveLinks(backendCrew, generation, backend, unknown); + saveLinks(frontendCrew, generation, frontend); + saveLinks(androidCrew, generation, android); + + // when + List responses = crewDocumentQueryService.findAllByGeneration("8๊ธฐ"); + + // then + assertThat(responses) + .extracting(GenerationCrewResponse::field) + .containsExactly(CrewField.BACKEND, CrewField.FRONTEND, CrewField.ANDROID); + } + + @Test + @DisplayName("๋ถ„์•ผ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์•Œ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๋ณต์ˆ˜์ด๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byIndeterminateFields() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument unknown = saveOrganizationDocument("๋ฐ๋ธŒ์˜ต์Šค"); + OrganizationDocument backend = saveOrganizationDocument("๋ฐฑ์—”๋“œ"); + OrganizationDocument frontend = saveOrganizationDocument("ํ”„๋ก ํŠธ์—”๋“œ"); + CrewDocument noFieldCrew = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + CrewDocument unknownFieldCrew = saveCrewDocument("๋‚˜๋ž˜(8๊ธฐ)"); + CrewDocument multipleFieldCrew = saveCrewDocument("๋‹ค์˜จ(8๊ธฐ)"); + saveLink(noFieldCrew, generation); + saveLinks(unknownFieldCrew, generation, unknown); + saveLinks(multipleFieldCrew, generation, backend, frontend); + + // when + List responses = crewDocumentQueryService.findAllByGeneration("8๊ธฐ"); + + // then + assertThat(responses) + .extracting(GenerationCrewResponse::field) + .containsExactly(null, null, null); + } + + @Test + @DisplayName("์ œ๋ชฉ์˜ ์ฒซ ๊ด„ํ˜ธ ์•ž ๋ฌธ์ž์—ด์„ ์ด๋ฆ„์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byCrewTitleFormats() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + CrewDocument spacedCrew = saveCrewDocument(" ๊ฐ€๋žŒ (8๊ธฐ)"); + CrewDocument unspacedCrew = saveCrewDocument("๋‚˜๋ž˜(8๊ธฐ)"); + CrewDocument plainCrew = saveCrewDocument(" ๋ผ์˜จ "); + saveLink(spacedCrew, generation); + saveLink(unspacedCrew, generation); + saveLink(plainCrew, generation); + + // when + List responses = crewDocumentQueryService.findAllByGeneration("8๊ธฐ"); + + // then + assertThat(responses) + .extracting(GenerationCrewResponse::name) + .containsExactly("๊ฐ€๋žŒ", "๋‚˜๋ž˜", "๋ผ์˜จ"); + } + + @Test + @DisplayName("์ œ๋ชฉ์—์„œ ์ถ”์ถœํ•œ ์ด๋ฆ„์ด ๋น„์–ด ์žˆ์œผ๋ฉด ์ œ์™ธํ•œ๋‹ค.") + void findAllByGeneration_success_byBlankExtractedName() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + CrewDocument blankNameCrew = saveCrewDocument(" (8๊ธฐ)"); + saveLink(blankNameCrew, generation); + + // when + List responses = crewDocumentQueryService.findAllByGeneration("8๊ธฐ"); + + // then + assertThat(responses).isEmpty(); + } + + @Test + @DisplayName("์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•ด ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byNameAscendingOrder() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + CrewDocument thirdCrew = saveCrewDocument("๋‹ค์˜จ(8๊ธฐ)"); + CrewDocument firstCrew = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + CrewDocument secondCrew = saveCrewDocument("๋‚˜๋ž˜(8๊ธฐ)"); + saveLink(thirdCrew, generation); + saveLink(firstCrew, generation); + saveLink(secondCrew, generation); + + // when + List responses = crewDocumentQueryService.findAllByGeneration("8๊ธฐ"); + + // then + assertThat(responses) + .extracting(GenerationCrewResponse::name) + .containsExactly("๊ฐ€๋žŒ", "๋‚˜๋ž˜", "๋‹ค์˜จ"); + } + + @Test + @DisplayName("ํ•ด๋‹น ๊ธฐ์ˆ˜์˜ ํฌ๋ฃจ๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllByGeneration_success_byNoCrew() { + // when + List responses = crewDocumentQueryService.findAllByGeneration("8๊ธฐ"); + + // then + assertThat(responses).isEmpty(); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ๊ฒ€์ฆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void findAllByGeneration_fail_byBlankGeneration() { + // when & then + assertThatThrownBy(() -> crewDocumentQueryService.findAllByGeneration(" ")) + .isInstanceOf(WikiException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๊ฐ€ null์ด๋ฉด ๊ฒ€์ฆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void findAllByGeneration_fail_byNullGeneration() { + // when & then + assertThatThrownBy(() -> crewDocumentQueryService.findAllByGeneration(null)) + .isInstanceOf(WikiException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + } + + private CrewDocument saveCrewDocument(String title) { + CrewDocument crewDocument = CrewDocumentFixture.createCrewDocument( + title, + "contents", + "writer", + 10L, + UUID.randomUUID() + ); + return crewDocumentRepository.save(crewDocument); + } + + private OrganizationDocument saveOrganizationDocument(String title) { + OrganizationDocument organizationDocument = OrganizationDocumentFixture.create( + title, + "contents", + "writer", + 10L, + UUID.randomUUID() + ); + return organizationDocumentRepository.save(organizationDocument); + } + + private void saveLink( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { + DocumentOrganizationLink link = DocumentOrganizationLinkFixture.create( + crewDocument, + organizationDocument + ); + documentOrganizationLinkRepository.save(link); + } + + private void saveLinks( + CrewDocument crewDocument, + OrganizationDocument... organizationDocuments + ) { + for (OrganizationDocument organizationDocument : organizationDocuments) { + saveLink(crewDocument, organizationDocument); + } + } +} diff --git a/src/test/java/com/wooteco/wiki/document/service/DocumentServiceTest.java b/src/test/java/com/wooteco/wiki/document/service/DocumentServiceTest.java index 45d252a..d91e9dc 100644 --- a/src/test/java/com/wooteco/wiki/document/service/DocumentServiceTest.java +++ b/src/test/java/com/wooteco/wiki/document/service/DocumentServiceTest.java @@ -2,13 +2,16 @@ import static com.wooteco.wiki.global.exception.ErrorCode.DOCUMENT_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; 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.Document; +import com.wooteco.wiki.document.domain.DocumentType; import com.wooteco.wiki.document.domain.dto.CrewDocumentCreateRequest; import com.wooteco.wiki.document.domain.dto.DocumentResponse; +import com.wooteco.wiki.document.domain.dto.DocumentTitleListResponse; import com.wooteco.wiki.document.domain.dto.DocumentUuidResponse; import com.wooteco.wiki.document.fixture.CrewDocumentFixture; import com.wooteco.wiki.document.repository.DocumentRepository; @@ -16,10 +19,11 @@ import com.wooteco.wiki.global.exception.ErrorCode; import com.wooteco.wiki.global.exception.WikiException; import com.wooteco.wiki.history.repository.HistoryRepository; +import com.wooteco.wiki.organizationdocument.fixture.OrganizationDocumentFixture; import java.util.List; +import java.util.Objects; import java.util.Map; import java.util.UUID; -import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; @@ -212,6 +216,57 @@ void findAll_fail_byNegativePageNumber() { } } + @Nested + @DisplayName("์ „์ฒด ๋ฌธ์„œ ์ œ๋ชฉ ์กฐํšŒ ๊ธฐ๋Šฅ") + class FindAllTitles { + + @DisplayName("๋ฌธ์„œ๊ฐ€ ์กด์žฌํ•˜๋ฉด ์ œ๋ชฉ, UUID, ๋ฌธ์„œ ํƒ€์ž…, ์ƒ์„ฑ์ผ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void findAllTitles_success_bySomeData() { + // given + UUID firstUuid = UUID.randomUUID(); + UUID secondUuid = UUID.randomUUID(); + UUID thirdUuid = UUID.randomUUID(); + + crewDocumentService.create( + CrewDocumentFixture.createDocumentCreateRequest("title1", "content1", "writer1", 10L, firstUuid)); + crewDocumentService.create( + CrewDocumentFixture.createDocumentCreateRequest("title2", "content2", "writer2", 11L, secondUuid)); + documentRepository.save( + OrganizationDocumentFixture.create("title3", "content3", "writer3", 12L, thirdUuid)); + + // when + List result = documentService.findAllTitles(); + + // then + assertSoftly(softly -> { + softly.assertThat(result).hasSize(3); + softly.assertThat(result) + .extracting(DocumentTitleListResponse::title) + .containsExactlyInAnyOrder("title1", "title2", "title3"); + softly.assertThat(result) + .extracting(DocumentTitleListResponse::uuid) + .containsExactlyInAnyOrder(firstUuid, secondUuid, thirdUuid); + softly.assertThat(result) + .extracting(DocumentTitleListResponse::documentType) + .containsExactlyInAnyOrder(DocumentType.CREW, DocumentType.CREW, DocumentType.ORGANIZATION); + softly.assertThat(result) + .extracting(DocumentTitleListResponse::generateTime) + .allMatch(Objects::nonNull); + }); + } + + @DisplayName("๋ฌธ์„œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void findAllTitles_success_byNoData() { + // when + List result = documentService.findAllTitles(); + + // then + assertThat(result).isEmpty(); + } + } + @Test @DisplayName("flushViews ํ˜ธ์ถœ ์‹œ uuid๋ณ„๋กœ ์กฐํšŒ์ˆ˜๊ฐ€ ์ฆ๊ฐ€๋œ๋‹ค") void flushViews_success_byAccumulatedViewCount() { @@ -235,7 +290,7 @@ void flushViews_success_byAccumulatedViewCount() { Document updated1 = documentRepository.findById(doc1.getId()).get(); Document updated2 = documentRepository.findById(doc2.getId()).get(); - Assertions.assertThat(updated1.getViewCount()).isEqualTo(5); - Assertions.assertThat(updated2.getViewCount()).isEqualTo(10); + assertThat(updated1.getViewCount()).isEqualTo(5); + assertThat(updated2.getViewCount()).isEqualTo(10); } } diff --git a/src/test/java/com/wooteco/wiki/graph/controller/CrewGraphControllerTest.java b/src/test/java/com/wooteco/wiki/graph/controller/CrewGraphControllerTest.java new file mode 100644 index 0000000..0f6689e --- /dev/null +++ b/src/test/java/com/wooteco/wiki/graph/controller/CrewGraphControllerTest.java @@ -0,0 +1,266 @@ +package com.wooteco.wiki.graph.controller; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.fixture.CrewDocumentFixture; +import com.wooteco.wiki.document.repository.CrewDocumentRepository; +import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink; +import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; +import com.wooteco.wiki.organizationdocument.fixture.DocumentOrganizationLinkFixture; +import com.wooteco.wiki.organizationdocument.fixture.OrganizationDocumentFixture; +import com.wooteco.wiki.organizationdocument.repository.DocumentOrganizationLinkRepository; +import com.wooteco.wiki.organizationdocument.repository.OrganizationDocumentRepository; +import io.restassured.RestAssured; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +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.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class CrewGraphControllerTest { + + @LocalServerPort + private int port; + + @Autowired + private CrewDocumentRepository crewDocumentRepository; + + @Autowired + private OrganizationDocumentRepository organizationDocumentRepository; + + @Autowired + private DocumentOrganizationLinkRepository documentOrganizationLinkRepository; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @Nested + @DisplayName("ํฌ๋ฃจ ๊ทธ๋ž˜ํ”„ ์กฐํšŒ API๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ") + class FindByGeneration { + + @Test + @DisplayName("์„ฑ๊ณต ์‘๋‹ต์— ๊ธฐ์ˆ˜์™€ ์ผ์น˜ํ•˜๋Š” ํฌ๋ฃจ ๋ฌธ์„œ ๋…ธ๋“œ์™€ ์ฐธ์กฐ ๊ฐ„์„ ์„ ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_success_byMatchingGenerationTitle() { + // given + UUID firstCrewUuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + UUID secondCrewUuid = UUID.fromString("22222222-2222-2222-2222-222222222222"); + CrewDocument firstCrew = saveCrewDocument( + "๊ฐ€๋žŒ(8๊ธฐ)", + "https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222", + firstCrewUuid + ); + CrewDocument secondCrew = saveCrewDocument( + "๋‚˜๋ž˜(8๊ธฐ)", + "contents", + secondCrewUuid + ); + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + saveLink(firstCrew, generation); + saveLink(secondCrew, generation); + + // when & then + RestAssured.given().log().all() + .queryParam("generation", "8๊ธฐ") + .when() + .get("/graph") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("data.nodes", hasSize(2)) + .body("data.nodes[0].documentUuid", equalTo(firstCrewUuid.toString())) + .body("data.nodes[0].title", equalTo("๊ฐ€๋žŒ(8๊ธฐ)")) + .body("data.nodes[0].type", equalTo("CREW")) + .body("data.nodes[1].documentUuid", equalTo(secondCrewUuid.toString())) + .body("data.nodes[1].title", equalTo("๋‚˜๋ž˜(8๊ธฐ)")) + .body("data.nodes[1].type", equalTo("CREW")) + .body("data.edges", hasSize(1)) + .body("data.edges[0].sourceDocumentUuid", equalTo(firstCrewUuid.toString())) + .body("data.edges[0].targetDocumentUuid", equalTo(secondCrewUuid.toString())) + .body("data.edges[0].type", equalTo("REFERENCE")); + } + + @Test + @DisplayName("์„ ํƒํ•œ ์กฐ์ง ๋…ธ๋“œ์™€ ํ˜„์žฌ ๊ธฐ์ˆ˜์—์„œ ์กฐ์ง์— ์—ฐ๊ฒฐ๋œ ํฌ๋ฃจ ๊ฐ„์„ ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_success_bySelectedOrganization() { + // given + UUID firstCrewUuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + UUID secondCrewUuid = UUID.fromString("22222222-2222-2222-2222-222222222222"); + UUID organizationDocumentUuid = UUID.fromString("33333333-3333-3333-3333-333333333333"); + CrewDocument firstCrew = saveCrewDocument( + "๊ฐ€๋žŒ(8๊ธฐ)", + "https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222", + firstCrewUuid + ); + CrewDocument secondCrew = saveCrewDocument( + "๋‚˜๋ž˜(8๊ธฐ)", + "contents", + secondCrewUuid + ); + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument backend = saveOrganizationDocument( + "๋ฐฑ์—”๋“œ", + organizationDocumentUuid + ); + saveLink(firstCrew, generation); + saveLink(firstCrew, backend); + saveLink(secondCrew, generation); + + // when & then + RestAssured.given().log().all() + .queryParam("generation", "8๊ธฐ") + .queryParam("organizationDocumentUuid", organizationDocumentUuid) + .when() + .get("/graph") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("data.nodes", hasSize(3)) + .body("data.nodes[0].documentUuid", equalTo(firstCrewUuid.toString())) + .body("data.nodes[0].type", equalTo("CREW")) + .body("data.nodes[1].documentUuid", equalTo(secondCrewUuid.toString())) + .body("data.nodes[1].type", equalTo("CREW")) + .body("data.nodes[2].documentUuid", equalTo(organizationDocumentUuid.toString())) + .body("data.nodes[2].title", equalTo("๋ฐฑ์—”๋“œ")) + .body("data.nodes[2].type", equalTo("ORGANIZATION")) + .body("data.edges", hasSize(2)) + .body("data.edges[0].sourceDocumentUuid", equalTo(firstCrewUuid.toString())) + .body("data.edges[0].targetDocumentUuid", equalTo(secondCrewUuid.toString())) + .body("data.edges[0].type", equalTo("REFERENCE")) + .body("data.edges[1].sourceDocumentUuid", equalTo(organizationDocumentUuid.toString())) + .body("data.edges[1].targetDocumentUuid", equalTo(firstCrewUuid.toString())) + .body("data.edges[1].type", equalTo("ORGANIZATION_LINK")); + } + + @Test + @DisplayName("์„ ํƒํ•œ ์กฐ์ง ๋ฌธ์„œ๊ฐ€ ์—†์œผ๋ฉด ์กฐํšŒ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_fail_byMissingOrganizationDocument() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + CrewDocument crewDocument = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + saveLink(crewDocument, generation); + + // when & then + RestAssured.given().log().all() + .queryParam("generation", "8๊ธฐ") + .queryParam("organizationDocumentUuid", UUID.randomUUID()) + .when() + .get("/graph") + .then().log().all() + .statusCode(HttpStatus.NOT_FOUND.value()) + .body("code", equalTo("ORGANIZATION_DOCUMENT_NOT_FOUND")); + } + + @Test + @DisplayName("์„ ํƒํ•œ ์กฐ์ง ๋ฌธ์„œ UUID ํ˜•์‹์ด ์ž˜๋ชป๋˜๋ฉด ๊ฒ€์ฆ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_fail_byInvalidOrganizationDocumentUuid() { + // when & then + RestAssured.given().log().all() + .queryParam("generation", "8๊ธฐ") + .queryParam("organizationDocumentUuid", "invalid-uuid") + .when() + .get("/graph") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .body("code", equalTo("VALIDATION_ERROR")); + } + + @Test + @DisplayName("์ž…๋ ฅํ•œ ์กฐ์ง ์ œ๋ชฉ๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๊ธฐ์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๊ทธ๋ž˜ํ”„๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_success_byNoExactGenerationTitle() { + // given + CrewDocument crewDocument = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + saveLink(crewDocument, generation); + + // when & then + RestAssured.given().log().all() + .queryParam("generation", "8") + .when() + .get("/graph") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .body("data.nodes", hasSize(0)) + .body("data.edges", hasSize(0)); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๋ฅผ ๋ˆ„๋ฝํ•˜๋ฉด ๊ฒ€์ฆ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_fail_byMissingGeneration() { + // when & then + assertValidationError("/graph"); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ๊ฒ€์ฆ ์‹คํŒจ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_fail_byEmptyGeneration() { + // when & then + assertValidationError("/graph?generation="); + } + } + + private void assertValidationError(String path) { + RestAssured.given().log().all() + .when() + .get(path) + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .body("code", equalTo("VALIDATION_ERROR")); + } + + private CrewDocument saveCrewDocument(String title) { + return saveCrewDocument(title, "contents", UUID.randomUUID()); + } + + private CrewDocument saveCrewDocument( + String title, + String contents, + UUID uuid + ) { + CrewDocument crewDocument = CrewDocumentFixture.createCrewDocument( + title, + contents, + "writer", + 10L, + uuid + ); + return crewDocumentRepository.save(crewDocument); + } + + private OrganizationDocument saveOrganizationDocument(String title) { + return saveOrganizationDocument(title, UUID.randomUUID()); + } + + private OrganizationDocument saveOrganizationDocument( + String title, + UUID uuid + ) { + OrganizationDocument organizationDocument = OrganizationDocumentFixture.create( + title, + "contents", + "writer", + 10L, + uuid + ); + return organizationDocumentRepository.save(organizationDocument); + } + + private void saveLink( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { + DocumentOrganizationLink link = DocumentOrganizationLinkFixture.create( + crewDocument, + organizationDocument + ); + documentOrganizationLinkRepository.save(link); + } +} diff --git a/src/test/java/com/wooteco/wiki/graph/repository/CrewGraphQueryRepositoryTest.java b/src/test/java/com/wooteco/wiki/graph/repository/CrewGraphQueryRepositoryTest.java new file mode 100644 index 0000000..0d2277b --- /dev/null +++ b/src/test/java/com/wooteco/wiki/graph/repository/CrewGraphQueryRepositoryTest.java @@ -0,0 +1,101 @@ +package com.wooteco.wiki.graph.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.fixture.CrewDocumentFixture; +import com.wooteco.wiki.document.repository.CrewDocumentRepository; +import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink; +import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; +import com.wooteco.wiki.organizationdocument.fixture.DocumentOrganizationLinkFixture; +import com.wooteco.wiki.organizationdocument.fixture.OrganizationDocumentFixture; +import com.wooteco.wiki.organizationdocument.repository.DocumentOrganizationLinkRepository; +import com.wooteco.wiki.organizationdocument.repository.OrganizationDocumentRepository; +import java.util.List; +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.autoconfigure.orm.jpa.DataJpaTest; + +@DataJpaTest +class CrewGraphQueryRepositoryTest { + + @Autowired + private CrewGraphQueryRepository crewGraphQueryRepository; + + @Autowired + private CrewDocumentRepository crewDocumentRepository; + + @Autowired + private OrganizationDocumentRepository organizationDocumentRepository; + + @Autowired + private DocumentOrganizationLinkRepository documentOrganizationLinkRepository; + + @Nested + @DisplayName("๊ธฐ์ˆ˜์™€ ์กฐ์ง์— ๋ชจ๋‘ ์—ฐ๊ฒฐ๋œ ํฌ๋ฃจ ๋ฌธ์„œ UUID๋ฅผ ์กฐํšŒํ•  ๋•Œ") + class FindAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid { + + @Test + @DisplayName("๋‘ ์กฐ์ง์— ๋ชจ๋‘ ์—ฐ๊ฒฐ๋œ ํฌ๋ฃจ ๋ฌธ์„œ UUID๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid_success_byMatchingLinks() { + // given + OrganizationDocument eighthGeneration = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument seventhGeneration = saveOrganizationDocument("7๊ธฐ"); + OrganizationDocument backend = saveOrganizationDocument("๋ฐฑ์—”๋“œ"); + CrewDocument eighthBackendCrew = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + CrewDocument eighthFrontendCrew = saveCrewDocument("๋‚˜๋ž˜(8๊ธฐ)"); + CrewDocument seventhBackendCrew = saveCrewDocument("๋‹ค์˜จ(7๊ธฐ)"); + saveLink(eighthBackendCrew, eighthGeneration); + saveLink(eighthBackendCrew, backend); + saveLink(eighthFrontendCrew, eighthGeneration); + saveLink(seventhBackendCrew, seventhGeneration); + saveLink(seventhBackendCrew, backend); + + // when + List crewDocumentUuids = crewGraphQueryRepository + .findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid( + "8๊ธฐ", + backend.getUuid() + ); + + // then + assertThat(crewDocumentUuids).containsExactly(eighthBackendCrew.getUuid()); + } + } + + private CrewDocument saveCrewDocument(String title) { + CrewDocument crewDocument = CrewDocumentFixture.createCrewDocument( + title, + "contents", + "writer", + 10L, + UUID.randomUUID() + ); + return crewDocumentRepository.save(crewDocument); + } + + private OrganizationDocument saveOrganizationDocument(String title) { + OrganizationDocument organizationDocument = OrganizationDocumentFixture.create( + title, + "contents", + "writer", + 10L, + UUID.randomUUID() + ); + return organizationDocumentRepository.save(organizationDocument); + } + + private void saveLink( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { + DocumentOrganizationLink link = DocumentOrganizationLinkFixture.create( + crewDocument, + organizationDocument + ); + documentOrganizationLinkRepository.save(link); + } +} diff --git a/src/test/java/com/wooteco/wiki/graph/service/CrewDocumentReferenceExtractorTest.java b/src/test/java/com/wooteco/wiki/graph/service/CrewDocumentReferenceExtractorTest.java new file mode 100644 index 0000000..a365e11 --- /dev/null +++ b/src/test/java/com/wooteco/wiki/graph/service/CrewDocumentReferenceExtractorTest.java @@ -0,0 +1,95 @@ +package com.wooteco.wiki.graph.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CrewDocumentReferenceExtractorTest { + + private static final UUID FIRST_DOCUMENT_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); + private static final UUID SECOND_DOCUMENT_UUID = UUID.fromString("22222222-2222-2222-2222-222222222222"); + + private final CrewDocumentReferenceExtractor crewDocumentReferenceExtractor = + new CrewDocumentReferenceExtractor(); + + @Nested + @DisplayName("ํฌ๋ฃจ ๋ฌธ์„œ ์ฐธ์กฐ UUID๋ฅผ ์ถ”์ถœํ•  ๋•Œ") + class Extract { + + @Test + @DisplayName("์ •์‹ ์ฃผ์†Œ๋กœ ์ž‘์„ฑํ•œ Markdown ๋งํฌ์™€ ์ผ๋ฐ˜ URL์—์„œ UUID๋ฅผ ์ถ”์ถœํ•œ๋‹ค.") + void extract_success_byCanonicalLinks() { + // given + String contents = """ + [์ฒซ ๋ฒˆ์งธ ํฌ๋ฃจ](https://crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111) + ๊ด€๋ จ ๋ฌธ์„œ: https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222 + """; + + // when + List references = crewDocumentReferenceExtractor.extract(contents); + + // then + assertThat(references).containsExactly(FIRST_DOCUMENT_UUID, SECOND_DOCUMENT_UUID); + } + + @Test + @DisplayName("๋™์ผํ•œ ๋ฌธ์„œ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฐธ์กฐํ•˜๋ฉด UUID๋ฅผ ํ•œ ๋ฒˆ๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void extract_success_byDuplicateLinks() { + // given + String contents = """ + https://crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111 + [๊ฐ™์€ ํฌ๋ฃจ](https://crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111) + """; + + // when + List references = crewDocumentReferenceExtractor.extract(contents); + + // then + assertThat(references).containsExactly(FIRST_DOCUMENT_UUID); + } + + @Test + @DisplayName("์™ธ๋ถ€ ์ฃผ์†Œ์™€ ์ด๋ฏธ์ง€ ๋ฐ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฌธ์„œ ๊ฒฝ๋กœ๋Š” ์ œ์™ธํ•œ๋‹ค.") + void extract_success_byInvalidLinks() { + // given + String contents = """ + https://example.com/wiki/11111111-1111-1111-1111-111111111111 + https://api.crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111 + https://www.crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111 + https://dev.crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111 + http://localhost:3000/wiki/11111111-1111-1111-1111-111111111111 + /wiki/11111111-1111-1111-1111-111111111111 + ![์ด๋ฏธ์ง€](https://crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111) + /wiki/not-a-uuid + /document/uuid/11111111-1111-1111-1111-111111111111 + 11111111-1111-1111-1111-111111111111 + https://crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111suffix + """; + + // when + List references = crewDocumentReferenceExtractor.extract(contents); + + // then + assertThat(references).isEmpty(); + } + + @Test + @DisplayName("๋ณธ๋ฌธ์ด ์—†๊ฑฐ๋‚˜ ๊ณต๋ฐฑ์ด๋ฉด ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void extract_success_byEmptyContents() { + // when + List nullContentsReferences = crewDocumentReferenceExtractor.extract(null); + List blankContentsReferences = crewDocumentReferenceExtractor.extract(" "); + + // then + assertSoftly(softly -> { + softly.assertThat(nullContentsReferences).isEmpty(); + softly.assertThat(blankContentsReferences).isEmpty(); + }); + } + } +} diff --git a/src/test/java/com/wooteco/wiki/graph/service/CrewGraphQueryServiceTest.java b/src/test/java/com/wooteco/wiki/graph/service/CrewGraphQueryServiceTest.java new file mode 100644 index 0000000..448ebb4 --- /dev/null +++ b/src/test/java/com/wooteco/wiki/graph/service/CrewGraphQueryServiceTest.java @@ -0,0 +1,356 @@ +package com.wooteco.wiki.graph.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.document.fixture.CrewDocumentFixture; +import com.wooteco.wiki.document.repository.CrewDocumentRepository; +import com.wooteco.wiki.global.exception.ErrorCode; +import com.wooteco.wiki.global.exception.WikiException; +import com.wooteco.wiki.graph.dto.CrewGraphResponse; +import com.wooteco.wiki.graph.dto.GraphEdgeResponse; +import com.wooteco.wiki.graph.dto.GraphEdgeType; +import com.wooteco.wiki.graph.dto.GraphNodeResponse; +import com.wooteco.wiki.graph.dto.GraphNodeType; +import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink; +import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; +import com.wooteco.wiki.organizationdocument.fixture.DocumentOrganizationLinkFixture; +import com.wooteco.wiki.organizationdocument.fixture.OrganizationDocumentFixture; +import com.wooteco.wiki.organizationdocument.repository.DocumentOrganizationLinkRepository; +import com.wooteco.wiki.organizationdocument.repository.OrganizationDocumentRepository; +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 CrewGraphQueryServiceTest { + + @Autowired + private CrewGraphQueryService crewGraphQueryService; + + @Autowired + private CrewDocumentRepository crewDocumentRepository; + + @Autowired + private OrganizationDocumentRepository organizationDocumentRepository; + + @Autowired + private DocumentOrganizationLinkRepository documentOrganizationLinkRepository; + + @Nested + @DisplayName("๊ธฐ์ˆ˜๋ณ„ ํฌ๋ฃจ ๊ทธ๋ž˜ํ”„๋ฅผ ์กฐํšŒํ•  ๋•Œ") + class FindByGeneration { + + @Test + @DisplayName("์š”์ฒญํ•œ ๊ธฐ์ˆ˜ ์กฐ์ง๊ณผ ์—ฐ๊ฒฐ๋œ ํฌ๋ฃจ ๋ฌธ์„œ๋งŒ ๋…ธ๋“œ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_success_byMatchingGenerationTitle() { + // given + OrganizationDocument eighthGeneration = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument seventhGeneration = saveOrganizationDocument("7๊ธฐ"); + CrewDocument eighthCrew = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + CrewDocument anotherEighthCrew = saveCrewDocument("๋‚˜๋ž˜(8๊ธฐ)"); + CrewDocument seventhCrew = saveCrewDocument("๋‹ค์˜จ(7๊ธฐ)"); + saveLink(eighthCrew, eighthGeneration); + saveLink(anotherEighthCrew, eighthGeneration); + saveLink(seventhCrew, seventhGeneration); + + // when + CrewGraphResponse response = crewGraphQueryService.findByGeneration("8๊ธฐ"); + + // then + assertSoftly(softly -> { + softly.assertThat(response.nodes()) + .extracting(GraphNodeResponse::documentUuid) + .containsExactly(eighthCrew.getUuid(), anotherEighthCrew.getUuid()); + softly.assertThat(response.nodes()) + .extracting(GraphNodeResponse::title) + .containsExactly("๊ฐ€๋žŒ(8๊ธฐ)", "๋‚˜๋ž˜(8๊ธฐ)"); + softly.assertThat(response.nodes()) + .extracting(GraphNodeResponse::type) + .containsOnly(GraphNodeType.CREW); + softly.assertThat(response.edges()).isEmpty(); + }); + } + + @Test + @DisplayName("์„ ํƒํ•œ ์กฐ์ง ๋…ธ๋“œ์™€ ํ˜„์žฌ ๊ธฐ์ˆ˜์—์„œ ์กฐ์ง์— ์—ฐ๊ฒฐ๋œ ํฌ๋ฃจ ๊ฐ„์„ ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.") + void findByGeneration_success_bySelectedOrganization() { + // given + UUID firstCrewUuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + UUID secondCrewUuid = UUID.fromString("22222222-2222-2222-2222-222222222222"); + UUID organizationDocumentUuid = UUID.fromString("33333333-3333-3333-3333-333333333333"); + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument backend = saveOrganizationDocument( + "๋ฐฑ์—”๋“œ", + organizationDocumentUuid + ); + CrewDocument firstCrew = saveCrewDocument( + "๊ฐ€๋žŒ(8๊ธฐ)", + "https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222", + firstCrewUuid + ); + CrewDocument secondCrew = saveCrewDocument( + "๋‚˜๋ž˜(8๊ธฐ)", + "contents", + secondCrewUuid + ); + saveLink(firstCrew, generation); + saveLink(firstCrew, backend); + saveLink(secondCrew, generation); + + // when + CrewGraphResponse response = crewGraphQueryService.findByGeneration( + "8๊ธฐ", + organizationDocumentUuid + ); + + // then + assertSoftly(softly -> { + softly.assertThat(response.nodes()) + .extracting(GraphNodeResponse::documentUuid) + .containsExactly( + firstCrewUuid, + secondCrewUuid, + organizationDocumentUuid + ); + softly.assertThat(response.nodes()) + .extracting(GraphNodeResponse::type) + .containsExactly( + GraphNodeType.CREW, + GraphNodeType.CREW, + GraphNodeType.ORGANIZATION + ); + softly.assertThat(response.edges()) + .containsExactly( + new GraphEdgeResponse( + firstCrewUuid, + secondCrewUuid, + GraphEdgeType.REFERENCE + ), + new GraphEdgeResponse( + organizationDocumentUuid, + firstCrewUuid, + GraphEdgeType.ORGANIZATION_LINK + ) + ); + }); + } + + @Test + @DisplayName("์„ ํƒํ•œ ์กฐ์ง ๋ฌธ์„œ๊ฐ€ ์—†์œผ๋ฉด ์กฐํšŒ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void findByGeneration_fail_byMissingOrganizationDocument() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + CrewDocument crewDocument = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + saveLink(crewDocument, generation); + + // when & then + assertThatThrownBy(() -> crewGraphQueryService.findByGeneration( + "8๊ธฐ", + UUID.randomUUID() + )) + .isInstanceOf(WikiException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.ORGANIZATION_DOCUMENT_NOT_FOUND); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜์— ์†ํ•œ ํฌ๋ฃจ๊ฐ€ ์—†์–ด๋„ ์„ ํƒํ•œ ์กฐ์ง ๋ฌธ์„œ๊ฐ€ ์—†์œผ๋ฉด ์กฐํšŒ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void findByGeneration_fail_byMissingOrganizationDocumentWithoutGenerationCrew() { + // when & then + assertThatThrownBy(() -> crewGraphQueryService.findByGeneration( + "8๊ธฐ", + UUID.randomUUID() + )) + .isInstanceOf(WikiException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.ORGANIZATION_DOCUMENT_NOT_FOUND); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜ ์กฐ์ง์„ ์†Œ์† ์กฐ์ง์œผ๋กœ ์„ ํƒํ•˜๋ฉด ๊ฒ€์ฆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void findByGeneration_fail_byGenerationOrganizationSelected() { + // given + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + CrewDocument crewDocument = saveCrewDocument("๊ฐ€๋žŒ(8๊ธฐ)"); + saveLink(crewDocument, generation); + + // when & then + assertThatThrownBy(() -> crewGraphQueryService.findByGeneration( + "8๊ธฐ", + generation.getUuid() + )) + .isInstanceOf(WikiException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + @DisplayName("๊ฐ™์€ ๊ธฐ์ˆ˜ ํฌ๋ฃจ ๋ฌธ์„œ์˜ ์ฐธ์กฐ๋ฅผ ์ค‘๋ณต ์—†๋Š” ๋ฌด๋ฐฉํ–ฅ ๊ฐ„์„ ์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_success_byCrewDocumentReferences() { + // given + UUID firstCrewUuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + UUID secondCrewUuid = UUID.fromString("22222222-2222-2222-2222-222222222222"); + OrganizationDocument generation = saveOrganizationDocument("8๊ธฐ"); + CrewDocument firstCrew = saveCrewDocument( + "๊ฐ€๋žŒ(8๊ธฐ)", + """ + [๋‚˜๋ž˜](https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222) + https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222 + """, + firstCrewUuid + ); + CrewDocument secondCrew = saveCrewDocument( + "๋‚˜๋ž˜(8๊ธฐ)", + "https://crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111", + secondCrewUuid + ); + saveLink(firstCrew, generation); + saveLink(secondCrew, generation); + + // when + CrewGraphResponse response = crewGraphQueryService.findByGeneration("8๊ธฐ"); + + // then + assertSoftly(softly -> { + softly.assertThat(response.nodes()).hasSize(2); + softly.assertThat(response.edges()) + .containsExactly(new GraphEdgeResponse( + firstCrewUuid, + secondCrewUuid, + GraphEdgeType.REFERENCE + )); + }); + } + + @Test + @DisplayName("ํ˜„์žฌ ๊ธฐ์ˆ˜์˜ ๋‹ค๋ฅธ ํฌ๋ฃจ ๋ฌธ์„œ๋ฅผ ๊ฐ€๋ฆฌํ‚ค์ง€ ์•Š๋Š” ์ฐธ์กฐ๋Š” ๊ฐ„์„ ์—์„œ ์ œ์™ธํ•œ๋‹ค.") + void findByGeneration_success_byInvalidReferenceTargets() { + // given + UUID sourceCrewUuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + UUID otherGenerationCrewUuid = UUID.fromString("22222222-2222-2222-2222-222222222222"); + UUID missingDocumentUuid = UUID.fromString("33333333-3333-3333-3333-333333333333"); + UUID organizationDocumentUuid = UUID.fromString("44444444-4444-4444-4444-444444444444"); + OrganizationDocument eighthGeneration = saveOrganizationDocument("8๊ธฐ"); + OrganizationDocument seventhGeneration = saveOrganizationDocument( + "7๊ธฐ", + organizationDocumentUuid + ); + CrewDocument sourceCrew = saveCrewDocument( + "๊ฐ€๋žŒ(8๊ธฐ)", + """ + https://crew-wiki.site/wiki/11111111-1111-1111-1111-111111111111 + https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222 + https://crew-wiki.site/wiki/33333333-3333-3333-3333-333333333333 + https://crew-wiki.site/wiki/44444444-4444-4444-4444-444444444444 + """, + sourceCrewUuid + ); + CrewDocument otherGenerationCrew = saveCrewDocument( + "๋‚˜๋ž˜(7๊ธฐ)", + "contents", + otherGenerationCrewUuid + ); + saveLink(sourceCrew, eighthGeneration); + saveLink(otherGenerationCrew, seventhGeneration); + + // when + CrewGraphResponse response = crewGraphQueryService.findByGeneration("8๊ธฐ"); + + // then + assertSoftly(softly -> { + softly.assertThat(response.nodes()) + .extracting(GraphNodeResponse::documentUuid) + .containsExactly(sourceCrewUuid); + softly.assertThat(response.edges()).isEmpty(); + }); + } + + @Test + @DisplayName("์š”์ฒญํ•œ ๊ธฐ์ˆ˜ ์กฐ์ง์ด ์—†์œผ๋ฉด ๋นˆ ๊ทธ๋ž˜ํ”„๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + void findByGeneration_success_byMissingGenerationOrganization() { + // when + CrewGraphResponse response = crewGraphQueryService.findByGeneration("8๊ธฐ"); + + // then + assertSoftly(softly -> { + softly.assertThat(response.nodes()).isEmpty(); + softly.assertThat(response.edges()).isEmpty(); + }); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๊ฐ€ ๊ณต๋ฐฑ์ด๋ฉด ๊ฒ€์ฆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void findByGeneration_fail_byBlankGeneration() { + // when & then + assertThatThrownBy(() -> crewGraphQueryService.findByGeneration(" ")) + .isInstanceOf(WikiException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + @DisplayName("๊ธฐ์ˆ˜๊ฐ€ null์ด๋ฉด ๊ฒ€์ฆ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void findByGeneration_fail_byNullGeneration() { + // when & then + assertThatThrownBy(() -> crewGraphQueryService.findByGeneration(null)) + .isInstanceOf(WikiException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + } + + private CrewDocument saveCrewDocument(String title) { + return saveCrewDocument(title, "contents", UUID.randomUUID()); + } + + private CrewDocument saveCrewDocument( + String title, + String contents, + UUID uuid + ) { + CrewDocument crewDocument = CrewDocumentFixture.createCrewDocument( + title, + contents, + "writer", + 10L, + uuid + ); + return crewDocumentRepository.save(crewDocument); + } + + private OrganizationDocument saveOrganizationDocument(String title) { + return saveOrganizationDocument(title, UUID.randomUUID()); + } + + private OrganizationDocument saveOrganizationDocument( + String title, + UUID uuid + ) { + OrganizationDocument organizationDocument = OrganizationDocumentFixture.create( + title, + "contents", + "writer", + 10L, + uuid + ); + return organizationDocumentRepository.save(organizationDocument); + } + + private void saveLink( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { + DocumentOrganizationLink link = DocumentOrganizationLinkFixture.create( + crewDocument, + organizationDocument + ); + documentOrganizationLinkRepository.save(link); + } +} diff --git a/src/test/java/com/wooteco/wiki/history/fixture/HistoryFixture.java b/src/test/java/com/wooteco/wiki/history/fixture/HistoryFixture.java index 9cb79a2..7f1380c 100644 --- a/src/test/java/com/wooteco/wiki/history/fixture/HistoryFixture.java +++ b/src/test/java/com/wooteco/wiki/history/fixture/HistoryFixture.java @@ -6,8 +6,15 @@ public class HistoryFixture { - public static History create(String title, String contents, String writer, long documentBytes, - LocalDateTime generateTime, CrewDocument crewDocument, Long version) { + public static History create( + String title, + String contents, + String writer, + long documentBytes, + LocalDateTime generateTime, + CrewDocument crewDocument, + Long version + ) { return new History(title, contents, writer, documentBytes, generateTime, crewDocument, version); } } diff --git a/src/test/java/com/wooteco/wiki/organizationdocument/fixture/DocumentOrganizationLinkFixture.java b/src/test/java/com/wooteco/wiki/organizationdocument/fixture/DocumentOrganizationLinkFixture.java new file mode 100644 index 0000000..ee09a32 --- /dev/null +++ b/src/test/java/com/wooteco/wiki/organizationdocument/fixture/DocumentOrganizationLinkFixture.java @@ -0,0 +1,15 @@ +package com.wooteco.wiki.organizationdocument.fixture; + +import com.wooteco.wiki.document.domain.CrewDocument; +import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink; +import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument; + +public class DocumentOrganizationLinkFixture { + + public static DocumentOrganizationLink create( + CrewDocument crewDocument, + OrganizationDocument organizationDocument + ) { + return new DocumentOrganizationLink(crewDocument, organizationDocument); + } +} diff --git a/src/test/java/com/wooteco/wiki/organizationdocument/fixture/OrganizationDocumentFixture.java b/src/test/java/com/wooteco/wiki/organizationdocument/fixture/OrganizationDocumentFixture.java index 8862712..e950efd 100644 --- a/src/test/java/com/wooteco/wiki/organizationdocument/fixture/OrganizationDocumentFixture.java +++ b/src/test/java/com/wooteco/wiki/organizationdocument/fixture/OrganizationDocumentFixture.java @@ -6,8 +6,13 @@ public class OrganizationDocumentFixture { - public static OrganizationDocument create(String title, String contents, String writer, Long documentBytes, - UUID uuid) { + public static OrganizationDocument create( + String title, + String contents, + String writer, + Long documentBytes, + UUID uuid + ) { return new OrganizationDocument(title, contents, writer, documentBytes, uuid); } @@ -15,11 +20,13 @@ public static OrganizationDocument createDefault() { return create("defaultOrganizationTitle", "defaultContent", "defaultWriter", 10L, UUID.randomUUID()); } - public static DocumentOrganizationMappingAddRequest createDocumentOrganizationDocumentCreateRequest(String title, - String contents, - String writer, - Long documentBytes, - UUID uuid) { + public static DocumentOrganizationMappingAddRequest createDocumentOrganizationDocumentCreateRequest( + String title, + String contents, + String writer, + Long documentBytes, + UUID uuid + ) { return new DocumentOrganizationMappingAddRequest(title, contents, writer, documentBytes, uuid); }