diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3746b402..498b94d1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,6 +29,13 @@ jobs: - name: Grant Execute Permission For Gradlew run: chmod +x backend/gradlew + - name: Make properties + run: | + cd backend/src/main/resources + touch ./application-local.yml + echo "${{ secrets.TEST_PROPERTIES }}" > ./application-local.yml + shell: bash + - name: Test with Gradle run: | cd backend diff --git a/.gitignore b/.gitignore index 06486ade..0be7018d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ .DS_Store .env -backend/src/test/resources/application-test.yml -backend/src/main/resources/application-local.yml backend/src/main/resources/application-docker.yml backend/src/main/resources/application-dbconfig.yml backend/gradle.properties @@ -14,3 +12,6 @@ logs/ *.log logs/ + +backend/src/main/resources/application-local.yml + diff --git a/backend/build.gradle b/backend/build.gradle index 40b29900..11bd1599 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -60,6 +60,7 @@ dependencies { // test testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.h2database:h2' //rest-docs testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' @@ -133,11 +134,12 @@ jacocoTestReport { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: [ "**/Q*", - "**/*Dto*", + '**/dto/**', "**/*Application*", "**/mapper/**", - "**/util/**", "**/global/**", + "**/com/graphy/backend/domain/job/domain/**", + "**/com/graphy/backend/domain/job/service/**" ]) })) } diff --git a/backend/src/main/java/com/graphy/backend/domain/job/domain/Job.java b/backend/src/main/java/com/graphy/backend/domain/job/domain/Job.java index 53971c2d..f855901e 100644 --- a/backend/src/main/java/com/graphy/backend/domain/job/domain/Job.java +++ b/backend/src/main/java/com/graphy/backend/domain/job/domain/Job.java @@ -25,6 +25,7 @@ public class Job { @Column(nullable = false) private LocalDateTime expirationDate; + @Builder public Job(Long id, String companyName, String title, diff --git a/backend/src/main/java/com/graphy/backend/domain/job/dto/JobDto.java b/backend/src/main/java/com/graphy/backend/domain/job/dto/JobDto.java index d6a9e0df..852c8782 100644 --- a/backend/src/main/java/com/graphy/backend/domain/job/dto/JobDto.java +++ b/backend/src/main/java/com/graphy/backend/domain/job/dto/JobDto.java @@ -3,6 +3,7 @@ import com.graphy.backend.domain.job.domain.Job; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; @@ -20,12 +21,14 @@ public static class CreateJobInfoRequest { private LocalDateTime expirationDate; - public static Job toJob(Long id, - String companyName, - String title, - String url, - LocalDateTime expirationDate) { - return new Job(id, companyName, title, url, expirationDate); + public Job toEntity() { + return Job.builder() + .id(id) + .companyName(companyName) + .title(title) + .url(url) + .expirationDate(expirationDate) + .build(); } } } diff --git a/backend/src/main/java/com/graphy/backend/domain/job/service/JobService.java b/backend/src/main/java/com/graphy/backend/domain/job/service/JobService.java index 07e1bf42..6b7e45f5 100644 --- a/backend/src/main/java/com/graphy/backend/domain/job/service/JobService.java +++ b/backend/src/main/java/com/graphy/backend/domain/job/service/JobService.java @@ -1,6 +1,7 @@ package com.graphy.backend.domain.job.service; import com.graphy.backend.domain.job.domain.Job; +import com.graphy.backend.domain.job.dto.JobDto; import com.graphy.backend.domain.job.repository.JobRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -18,7 +19,6 @@ import java.time.LocalDateTime; import java.time.ZoneId; -import static com.graphy.backend.domain.job.dto.JobDto.CreateJobInfoRequest.*; @Service @Transactional @@ -59,6 +59,7 @@ public void save() { } saveJobInfo(response.toString()); + System.out.println(response); } private void saveJobInfo(String response) { @@ -72,7 +73,6 @@ private void saveJobInfo(String response) { // 공고 ID Long jobId = jobObject.getLong("id"); -// if (jobRepository.findById(jobId) != null) break; // 회사 이름 String companyName = jobObject.getJSONObject("company") @@ -94,8 +94,8 @@ private void saveJobInfo(String response) { Instant.ofEpochSecond(expirationTimestampLong), ZoneId.systemDefault()); - Job job = toJob(jobId, companyName, jobTitle, companyInfoURL, expirationTimestamp); - jobRepository.save(job); + JobDto.CreateJobInfoRequest dto = new JobDto.CreateJobInfoRequest(jobId, companyName, jobTitle, companyInfoURL, expirationTimestamp); + jobRepository.save(dto.toEntity()); } } diff --git a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java index a76860af..22174fd2 100644 --- a/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java +++ b/backend/src/main/java/com/graphy/backend/domain/project/service/ProjectService.java @@ -50,20 +50,20 @@ public class ProjectService { private final GPTChatRestService gptChatRestService; - @PostConstruct - public void initTag() throws IOException { - if (tagRepository.existsById(1L)) - return; - ClassPathResource resource = new ClassPathResource("tag.txt"); - BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream())); - String s; - - while ((s = br.readLine()) != null) { - Tag tag = Tag.builder().tech(s).build(); - tagRepository.save(tag); - } - br.close(); - } +// @PostConstruct +// public void initTag() throws IOException { +// if (tagRepository.existsById(1L)) +// return; +// ClassPathResource resource = new ClassPathResource("tag.txt"); +// BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream())); +// String s; +// +// while ((s = br.readLine()) != null) { +// Tag tag = Tag.builder().tech(s).build(); +// tagRepository.save(tag); +// } +// br.close(); +// } public CreateProjectResponse createProject(CreateProjectRequest dto) { Project entity = mapper.toEntity(dto); @@ -96,12 +96,6 @@ public UpdateProjectResponse updateProject(Long projectId, UpdateProjectRequest return mapper.toUpdateProjectDto(project); } - public List getProjects(Pageable pageable) { - - Page projects = projectRepository.findAll(pageable); - return mapper.toDtoList(projects).getContent(); - } - public GetProjectDetailResponse getProjectById(Long projectId) { Project project = projectRepository.findById(projectId) .orElseThrow(() -> new EmptyResultException(ErrorCode.PROJECT_DELETED_OR_NOT_EXIST)); @@ -112,7 +106,7 @@ public GetProjectDetailResponse getProjectById(Long projectId) { } public Tags getTagsWithName(List techStacks) { - List foundTags = techStacks.stream().map(tagRepository::findTagByTech) + List foundTags = techStacks.stream().map(tagRepository::findTagByTech) .collect(Collectors.toList()); return new Tags(foundTags); } @@ -124,7 +118,7 @@ public List getProjects(GetProjectsRequest dto, Pageable pag @Async public CompletableFuture getProjectPlanAsync(String prompt) { - GptCompletionRequest dto = new GptCompletionDto.GptCompletionRequest(); + GptCompletionRequest dto = new GptCompletionRequest(); CompletableFuture response = new CompletableFuture<>(); dto.setPrompt(prompt); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e60dc72d..95d151c7 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -7,8 +7,8 @@ spring: matching-strategy: ant_path_matcher async: request-timeout: 60000 - profiles: - active: local + profiles: + active: local jpa: database: mysql diff --git a/backend/src/test/java/com/graphy/backend/domain/job/service/JobServiceTest.java b/backend/src/test/java/com/graphy/backend/domain/job/service/JobServiceTest.java index 6c7df0f4..70e12870 100644 --- a/backend/src/test/java/com/graphy/backend/domain/job/service/JobServiceTest.java +++ b/backend/src/test/java/com/graphy/backend/domain/job/service/JobServiceTest.java @@ -11,7 +11,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.time.LocalDateTime; @@ -36,5 +38,4 @@ public void saveTest() throws Exception { verify(jobRepository, times(1)).deleteAllExpiredSince(any(LocalDateTime.class)); } - } diff --git a/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectControllerTest.java b/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectControllerTest.java index 20bd7351..9a812935 100644 --- a/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectControllerTest.java +++ b/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectControllerTest.java @@ -2,7 +2,6 @@ import com.graphy.backend.domain.comment.dto.CommentWithMaskingDto; import com.graphy.backend.domain.comment.service.CommentService; -import com.graphy.backend.domain.project.dto.ProjectDto; import com.graphy.backend.domain.project.service.ProjectService; import com.graphy.backend.global.common.PageRequest; import com.graphy.backend.test.MockApiTest; @@ -23,14 +22,21 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; +import static com.graphy.backend.domain.project.dto.ProjectDto.*; import static com.graphy.backend.domain.project.dto.ProjectDto.GetProjectDetailResponse; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(ProjectController.class) @@ -94,6 +100,47 @@ public LocalDateTime getCreatedAt() { result.andExpect(status().isOk()); } + @Test + @DisplayName("프로젝트 생성 테스트") + public void createProject() throws Exception { + //given + CreateProjectRequest request = CreateProjectRequest.builder() + .projectName("projectName") + .description("description") + .content("content") + .build(); + + CreateProjectResponse response = CreateProjectResponse.builder().projectId(1L).build(); + + //when + when(projectService.createProject(request)).thenReturn(response); + + //then + mvc.perform(post(baseUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(document("project-create", + preprocessResponse(prettyPrint())) + ); + } + + @Test + @DisplayName("프로젝트 삭제 테스트") + public void deleteProject() throws Exception { + //given + Long projectId = 1L; + + doNothing().when(projectService).deleteProject(anyLong()); + + mvc.perform(delete(baseUrl + "/{projectId}", 1L) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("project-delete", + preprocessResponse(prettyPrint())) + ); + } + @Test @DisplayName("프로젝트 이름/내용/전체 검색한다") void searchProjectsWithName() throws Exception { @@ -101,15 +148,15 @@ void searchProjectsWithName() throws Exception { // given String projectName = "검색이름"; - ProjectDto.GetProjectsRequest request = ProjectDto.GetProjectsRequest.builder() + GetProjectsRequest request = GetProjectsRequest.builder() .projectName(projectName).build(); - com.graphy.backend.global.common.PageRequest pageRequest = new PageRequest(); - List result = new ArrayList(); + PageRequest pageRequest = new PageRequest(); + List result = new ArrayList(); for (int i = 0; i < 5; i++) { - ProjectDto.GetProjectResponse response = - ProjectDto.GetProjectResponse.builder().id((long) i).projectName("검색이름" + i) + GetProjectResponse response = + GetProjectResponse.builder().id((long) i).projectName("검색이름" + i) .description("프로젝트 설명" + i).createdAt(LocalDateTime.now()).build(); result.add(response); } @@ -135,7 +182,7 @@ void getProjectPlan() throws Exception { List plans = new ArrayList<>(Arrays.asList("Spring Security", "Docker")); String topic = "간단한 게시판"; - ProjectDto.GetPlanRequest request = new ProjectDto.GetPlanRequest(topic, features, techStacks, plans); + GetPlanRequest request = new GetPlanRequest(topic, features, techStacks, plans); String apiResult = "API 결과"; CompletableFuture result = CompletableFuture.completedFuture(apiResult); @@ -149,4 +196,26 @@ void getProjectPlan() throws Exception { // then resultActions.andExpect((status().isOk())); } + + @Test + @DisplayName("프로젝트 조회 시 프로젝트가 존재하지 않으면 예외가 발생한다") + public void EmptyResultTest() throws Exception { + + // given + GetProjectsRequest request = GetProjectsRequest.builder().build(); + PageRequest pageRequest = new PageRequest(); + Pageable pageable = pageRequest.of(); + + // when + when(projectService.getProjects(any(GetProjectsRequest.class), any(Pageable.class))) + .thenReturn(Collections.emptyList()); + + // then + mvc.perform(get(baseUrl + "/search") // "/project/search" should be replaced with the actual URL + .param("page", String.valueOf(pageable.getPageNumber())) + .param("size", String.valueOf(pageable.getPageSize())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().is4xxClientError()); // adjust based on the actual error code you're using + } } diff --git a/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectIntegrationTest.java b/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectIntegrationTest.java index 8f7b53ed..2f4bc78b 100644 --- a/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectIntegrationTest.java +++ b/backend/src/test/java/com/graphy/backend/domain/project/controller/ProjectIntegrationTest.java @@ -1,26 +1,64 @@ package com.graphy.backend.domain.project.controller; import com.graphy.backend.domain.project.domain.Project; -import com.graphy.backend.domain.project.domain.ProjectTags; import com.graphy.backend.domain.project.repository.ProjectRepository; import com.graphy.backend.domain.project.service.ProjectService; +import com.graphy.backend.global.common.PageRequest; +import com.graphy.backend.test.config.TestProfile; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; -import java.util.Optional; +import java.util.List; +import java.util.stream.Collectors; -import static com.graphy.backend.domain.project.dto.ProjectDto.UpdateProjectRequest; -import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; @SpringBootTest +@ActiveProfiles(TestProfile.TEST) @Transactional class ProjectIntegrationTest { @Autowired private ProjectRepository projectRepository; @Autowired private ProjectService projectService; + @Autowired private ProjectController projectController; + + @Test + @DisplayName("프로젝트 전체 조회 통합 테스트") + public void getProjectsTest() throws Exception { + //given + Project project1 = Project.builder().id(1L).projectName("test").content("con").comments(new ArrayList<>()).build(); + Project project2 = Project.builder().id(2L).projectName("test").content("con").comments(new ArrayList<>()).build(); + Project project3 = Project.builder().id(3L).projectName("test").content("con").comments(new ArrayList<>()).build(); + + projectRepository.save(project1); + projectRepository.save(project2); + projectRepository.save(project3); + + + PageRequest pageRequest = new PageRequest(); + pageRequest.setPage(0); + pageRequest.setSize(3); + Pageable pageable = pageRequest.of(); + + List result = projectRepository.searchProjectsWith(pageable, "test", "con") + .stream() + .collect(Collectors.toList()); + + assertAll( + () -> Assertions.assertThat(result.get(0).getProjectName()).isEqualTo(project1.getProjectName()), + () -> Assertions.assertThat(result.get(0).getContent()).isEqualTo(project1.getContent()), + () -> Assertions.assertThat(result.get(1).getProjectName()).isEqualTo(project2.getProjectName()), + () -> Assertions.assertThat(result.get(1).getContent()).isEqualTo(project2.getContent()), + () -> Assertions.assertThat(result.get(2).getProjectName()).isEqualTo(project3.getProjectName()), + () -> Assertions.assertThat(result.get(2).getContent()).isEqualTo(project3.getContent()) + ); + } // @Test // @DisplayName("프로젝트 soft delete 테스트") diff --git a/backend/src/test/java/com/graphy/backend/domain/project/domain/ProjectTagsTest.java b/backend/src/test/java/com/graphy/backend/domain/project/domain/ProjectTagsTest.java new file mode 100644 index 00000000..8d565ffb --- /dev/null +++ b/backend/src/test/java/com/graphy/backend/domain/project/domain/ProjectTagsTest.java @@ -0,0 +1,82 @@ +package com.graphy.backend.domain.project.domain; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProjectTagsTest { + + @Test + @DisplayName("프로젝트 태그 생성자 테스터") + public void projectTagsInit() throws Exception { + //when + ProjectTags projectTags = new ProjectTags(); + + //then + Assertions.assertAll( + () -> assertNotNull(projectTags.getValue()), + () -> assertTrue(projectTags.getValue().isEmpty()) + ); + } + + @Test + @DisplayName("태그 삭제 테스트") + void removeTagsTest() { + Tag tag1 = Tag.builder().tech("Spring").build(); + Tag tag2 = Tag.builder().tech("Spring Boot").build(); + + ProjectTags projectTags = new ProjectTags(); + + Project project = new Project(); + + Tags tags = new Tags(Arrays.asList(tag1, tag2)); + + projectTags.add(project, tags); + + assertFalse(projectTags.getValue().isEmpty()); + + projectTags.clear(); + + assertTrue(projectTags.getValue().isEmpty()); + } + + @Test + @DisplayName("태그 조회 테스트") + void getTagNamesTest() { + ProjectTags projectTags = new ProjectTags(); + Project project = new Project(); + + Tag tag1 = Tag.builder().tech("Spring").build(); + Tag tag2 = Tag.builder().tech("Spring Boot").build(); + + Tags tags = new Tags(Arrays.asList(tag1, tag2)); + + projectTags.add(project, tags); + + List tagNames = projectTags.getTagNames(); + + assertTrue(tagNames.containsAll(Arrays.asList("Spring", "Spring Boot"))); + } + + @Test + @DisplayName("태그 추가 테스트") + void addTagTest() { + ProjectTags projectTags = new ProjectTags(); + Project project = new Project(); + + Tag tag1 = Tag.builder().tech("Spring").build(); + Tag tag2 = Tag.builder().tech("Spring Boot").build(); + + Tags tags = new Tags(Arrays.asList(tag1, tag2)); + + projectTags.add(project, tags); + + assertEquals(2, projectTags.getValue().size()); + assertTrue(projectTags.getTagNames().containsAll(Arrays.asList("Spring", "Spring Boot"))); // 확인: 추가된 태그들이 정상적으로 존재 + } +} diff --git a/backend/src/test/java/com/graphy/backend/domain/project/domain/ProjectTest.java b/backend/src/test/java/com/graphy/backend/domain/project/domain/ProjectTest.java new file mode 100644 index 00000000..81c7cf24 --- /dev/null +++ b/backend/src/test/java/com/graphy/backend/domain/project/domain/ProjectTest.java @@ -0,0 +1,55 @@ +package com.graphy.backend.domain.project.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class ProjectTest { + + @Test + @DisplayName("프로젝트 태그 이름 조회") + public void getTagNameTest() throws Exception { + //given + Tag tag = Tag.builder().tech("Spring").build(); + ProjectTag projectTag = ProjectTag.builder().tag(tag).build(); + ProjectTags projectTags = new ProjectTags(Arrays.asList(projectTag)); + + Project project = Project.builder().projectTags(projectTags).build(); + + + //when + List result = project.getTagNames(); + + //then + assertAll( + () -> assertThat(result.size()).isEqualTo(1), + () -> assertThat(result.get(0)).isEqualTo("Spring") + ); + } + + @Test + @DisplayName("프로젝트 태그 id 조회") + public void getTagIdTest() throws Exception { + //given + Tag tag = Tag.builder().id(1L).tech("Spring").build(); + ProjectTag projectTag = ProjectTag.builder().tag(tag).build(); + ProjectTags projectTags = new ProjectTags(Arrays.asList(projectTag)); + + Project project = Project.builder().projectTags(projectTags).build(); + + + //when + List result = project.getTagIds(); + + //then + assertAll( + () -> assertThat(result.size()).isEqualTo(1), + () -> assertThat(result.get(0)).isEqualTo(1L) + ); + } +} diff --git a/backend/src/test/java/com/graphy/backend/domain/project/repository/ProjectCustomRepositoryImplTest.java b/backend/src/test/java/com/graphy/backend/domain/project/repository/ProjectCustomRepositoryImplTest.java new file mode 100644 index 00000000..c6902f72 --- /dev/null +++ b/backend/src/test/java/com/graphy/backend/domain/project/repository/ProjectCustomRepositoryImplTest.java @@ -0,0 +1,20 @@ +package com.graphy.backend.domain.project.repository; + +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.transaction.annotation.Transactional; + +@DataJpaTest +@Transactional +public class ProjectCustomRepositoryImplTest { +// @Test +// @DisplayName("") +// public void () throws Exception { +// //given +// +// +// //when +// +// +// //then +// } +} diff --git a/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java b/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java index decce2d2..26c25fd4 100644 --- a/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java +++ b/backend/src/test/java/com/graphy/backend/domain/project/service/ProjectServiceTest.java @@ -1,13 +1,42 @@ package com.graphy.backend.domain.project.service; +import com.graphy.backend.domain.comment.dto.CommentWithMaskingDto; import com.graphy.backend.domain.comment.repository.CommentRepository; +import com.graphy.backend.domain.project.domain.*; +import com.graphy.backend.domain.project.dto.ProjectDto; import com.graphy.backend.domain.project.mapper.ProjectMapper; import com.graphy.backend.domain.project.repository.ProjectRepository; +import com.graphy.backend.domain.project.repository.ProjectTagRepository; +import com.graphy.backend.domain.project.repository.TagRepository; +import com.graphy.backend.global.common.PageRequest; +import com.graphy.backend.global.error.ErrorCode; +import com.graphy.backend.global.error.exception.EmptyResultException; import com.graphy.backend.test.MockTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static com.graphy.backend.domain.project.dto.ProjectDto.*; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -15,13 +44,208 @@ public class ProjectServiceTest extends MockTest { @Mock private ProjectRepository projectRepository; + @Mock + private ProjectTagRepository projectTagRepository; @Mock private CommentRepository commentRepository; + @Mock + private TagRepository tagRepository; @Mock private ProjectMapper mapper; @InjectMocks private ProjectService projectService; + + @Test + @DisplayName("프로젝트 수정 테스트") + public void updateProject() throws Exception { + //given + Project project = Project.builder() + .id(1L) + .projectTags(new ProjectTags()) + .projectName("beforeUpdate") + .description("des") + .thumbNail("thumb") + .content("content") + .build(); + + UpdateProjectRequest request = UpdateProjectRequest.builder() + .projectName("afterUpdate") + .description("des") + .thumbNail("thumb") + .content("content") + .techTags(new ArrayList<>(Arrays.asList("Spring", "Django"))) + .build(); + + UpdateProjectResponse response = UpdateProjectResponse.builder() + .projectName(request.getProjectName()) + .description(request.getDescription()) + .thumbNail(request.getThumbNail()) + .content(request.getContent()) + .techTags(request.getTechTags()) + .build(); + + Tag tag1 = Tag.builder().tech("Spring").build(); + Tag tag2 = Tag.builder().tech("Django").build(); + Tags tags = new Tags(Arrays.asList(tag1, tag2)); + + project.updateProject(request.getProjectName(), request.getContent(), request.getDescription(), + tags, request.getThumbNail()); + + //when + when(projectRepository.findById(project.getId())).thenReturn(Optional.of(project)); + when(tagRepository.findTagByTech(anyString())).thenReturn(tag1, tag2); + when(mapper.toUpdateProjectDto(project)).thenReturn(response); + + UpdateProjectResponse result = projectService.updateProject(project.getId(), request); + + assertAll( + () -> assertThat(result.getProjectName()).isEqualTo(project.getProjectName()), + () -> assertThat(result.getDescription()).isEqualTo(project.getDescription()), + () -> assertThat(result.getThumbNail()).isEqualTo(project.getThumbNail()), + () -> assertThat(result.getTechTags()).isEqualTo(new ArrayList<>(Arrays.asList("Spring", "Django"))) + ); + } + + @Test + @DisplayName("프로젝트 생성 테스트") + public void createProject() throws Exception { + //given + List techTags = new ArrayList<>(Arrays.asList("Spring", "Django")); + + Project project = Project.builder() + .id(1L) + .projectTags(new ProjectTags()) + .projectName("testProject") + .build(); + + CreateProjectRequest request = CreateProjectRequest.builder() + .projectName("testProject") + .techTags(techTags). + build(); + + CreateProjectResponse response = CreateProjectResponse.builder() + .projectId(project.getId()) + .build(); + + Tag tag1 = Tag.builder().tech("Spring").build(); + Tag tag2 = Tag.builder().tech("Django").build(); + + //when + when(mapper.toEntity(any(CreateProjectRequest.class))).thenReturn(project); + when(projectRepository.save(project)).thenReturn(project); + when(tagRepository.findTagByTech(anyString())).thenReturn(tag1, tag2); + when(mapper.toCreateProjectDto(project.getId())).thenReturn(response); + + CreateProjectResponse result = projectService.createProject(request); + + //then + assertThat(result.getProjectId()).isEqualTo(1L); + + } + + @Test + @DisplayName("프로젝트 리스트 조회") + public void getProjects() throws Exception { + //given + GetProjectsRequest request = GetProjectsRequest.builder().projectName("name").build(); + + Project project1 = Project.builder().projectName("test1").build(); + Project project2 = Project.builder().projectName("test2").build(); + + GetProjectResponse response1 = GetProjectResponse.builder().projectName(project1.getProjectName()).build(); + GetProjectResponse response2 = GetProjectResponse.builder().projectName(project2.getProjectName()).build(); + + PageRequest pageRequest = new PageRequest(); + pageRequest.setSize(2); + pageRequest.setPage(0); + Pageable pageable = pageRequest.of(); + + List projectList = new ArrayList<>(Arrays.asList(project1, project2)); + Page projects = new PageImpl<>(projectList); + + List responseList = new ArrayList<>(Arrays.asList(response1, response2)); + Page responses = new PageImpl<>(responseList); + + + //when + when(projectRepository.searchProjectsWith(pageable, request.getProjectName(), request.getContent())) + .thenReturn(projects); + + when(mapper.toDtoList(projects)).thenReturn(responses); + + List result = projectService.getProjects(request, pageable); + + //then + assertThat(result.size()).isEqualTo(2); + assertThat(result.get(0).getProjectName()).isEqualTo("test1"); + assertThat(result.get(1).getProjectName()).isEqualTo("test2"); + assertThat(result).isEqualTo(responseList); + } + + @Test + @DisplayName("프로젝트 상세 조회") + public void getProjectById() throws Exception { + //given + Project project = Project.builder() + .id(1L) + .projectName("project").build(); + + List comments = new ArrayList<>(); + GetProjectDetailResponse response = GetProjectDetailResponse.builder() + .id(project.getId()) + .projectName(project.getProjectName()) + .build(); + + + //when + when(projectRepository.findById(project.getId())).thenReturn(Optional.of(project)); + when(commentRepository.findCommentsWithMasking(project.getId())).thenReturn(comments); + when(mapper.toGetProjectDetailDto(project, comments)).thenReturn(response); + + GetProjectDetailResponse result = projectService.getProjectById(1L); + + //then + assertThat(response.getId()).isEqualTo(result.getId()); + assertThat(response.getProjectName()).isEqualTo(result.getProjectName()); + } + + @Test + @DisplayName("프로젝트 삭제") + public void deleteProject() throws Exception { + //when + projectService.deleteProject(1L); + + //then + verify(projectRepository).deleteById(1L); + } + + @Test + @DisplayName("프로젝트 조회 시 존재하지 않는 프로젝트 예외 처리") + public void ProjectNotExistError() { + // given + Long projectId = 1L; + + // when + when(projectRepository.findById(projectId)).thenReturn(Optional.empty()); + + // then + assertThrows(EmptyResultException.class, () -> { + projectService.getProjectById(projectId); + }); + } + + @Test + @DisplayName("프로젝트 삭제 시 존재하지 않는 프로젝트 예외 처리") + public void EmptyResultDataAccessException() throws Exception { + // given + doThrow(EmptyResultDataAccessException.class).when(projectRepository).deleteById(anyLong()); + + // then + assertThrows(EmptyResultException.class, () -> { + projectService.deleteProject(1L); + }, ErrorCode.PROJECT_DELETED_OR_NOT_EXIST.getMessage()); + } } \ No newline at end of file diff --git a/backend/src/test/resources/application-test-example.yml b/backend/src/test/resources/application-test.yml similarity index 77% rename from backend/src/test/resources/application-test-example.yml rename to backend/src/test/resources/application-test.yml index 7efad9a3..c1cd0dc7 100644 --- a/backend/src/test/resources/application-test-example.yml +++ b/backend/src/test/resources/application-test.yml @@ -11,14 +11,7 @@ spring: dialect: org.hibernate.dialect.H2Dialect datasource: - url: + url: jdbc:h2:mem:testdb;MODE=MySQL + driver-class-name: org.h2.Driver username: password: - driver-class-name: org.h2.Driver - -logging: - level: - root: info - org: - hibernate: - type: trace \ No newline at end of file diff --git a/frontend/src/assets/image/arrow-left.svg b/frontend/src/assets/image/arrow-left.svg new file mode 100644 index 00000000..9d885017 --- /dev/null +++ b/frontend/src/assets/image/arrow-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/image/arrow-right.svg b/frontend/src/assets/image/arrow-right.svg new file mode 100644 index 00000000..d4b878b3 --- /dev/null +++ b/frontend/src/assets/image/arrow-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/assets/image/balloon.png b/frontend/src/assets/image/balloon.png new file mode 100644 index 00000000..7d5ad83e Binary files /dev/null and b/frontend/src/assets/image/balloon.png differ diff --git a/frontend/src/assets/image/desktop.png b/frontend/src/assets/image/desktop.png new file mode 100644 index 00000000..09eb82a8 Binary files /dev/null and b/frontend/src/assets/image/desktop.png differ diff --git a/frontend/src/assets/image/eyes.png b/frontend/src/assets/image/eyes.png new file mode 100644 index 00000000..fcc4dd99 Binary files /dev/null and b/frontend/src/assets/image/eyes.png differ diff --git a/frontend/src/assets/image/gptIcon.svg b/frontend/src/assets/image/gptIcon.svg new file mode 100644 index 00000000..41a6ab7c --- /dev/null +++ b/frontend/src/assets/image/gptIcon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/image/monster.png b/frontend/src/assets/image/monster.png new file mode 100644 index 00000000..fa7d652a Binary files /dev/null and b/frontend/src/assets/image/monster.png differ diff --git a/frontend/src/assets/image/pick.png b/frontend/src/assets/image/pick.png new file mode 100644 index 00000000..c61ffbe9 Binary files /dev/null and b/frontend/src/assets/image/pick.png differ diff --git a/frontend/src/assets/image/plus-circle.svg b/frontend/src/assets/image/plus-circle.svg new file mode 100644 index 00000000..66308ef1 --- /dev/null +++ b/frontend/src/assets/image/plus-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/src/components/ProjectCard.tsx b/frontend/src/components/ProjectCard.tsx index 29c157a8..a3d7e2b8 100644 --- a/frontend/src/components/ProjectCard.tsx +++ b/frontend/src/components/ProjectCard.tsx @@ -11,8 +11,6 @@ function ProjectCard(props: any) { const [, setProjectId] = useRecoilState(projectIdState); function findImage(tag: string) { - // AllStacks.map(x => x.name).findIndex(x => x == tag) - // AllStacks.map(x => x.image)[AllStacks.map(x => x.name).findIndex(x => x == tag)] return AllStacks.map((x) => x.image)[ AllStacks.map((x) => x.name).findIndex((x) => x === tag) ]; diff --git a/frontend/src/components/RenderModal.tsx b/frontend/src/components/RenderModal.tsx new file mode 100644 index 00000000..8fd2bfa5 --- /dev/null +++ b/frontend/src/components/RenderModal.tsx @@ -0,0 +1,466 @@ +import React, { useState, PropsWithChildren } from 'react'; + +import arrowLeftIcon from '../assets/image/arrow-left.svg'; +import arrowRightIcon from '../assets/image/arrow-right.svg'; +import desktop from '../assets/image/desktop.png'; +import eyes from '../assets/image/eyes.png'; +import monster from '../assets/image/monster.png'; +import pick from '../assets/image/pick.png'; +import plus from '../assets/image/plus-circle.svg'; +import AllStacks from '../Stack'; + +function findImage(tag: string) { + return AllStacks.map((x) => x.image)[ + AllStacks.map((x) => x.name).findIndex((x) => x === tag) + ]; +} + +// 화면 컴포넌트 +interface Screen1Props { + onNext: () => void; +} + +const Screen1: React.FC = ({ onNext }) => { + return ( +
+ {/* 이전/다음 */} + + + + {/* 1단계 */} +
    +
  1. + + monster + +
  2. +
  3. + + desktop + +
  4. +
  5. + + pick + +
  6. +
  7. + + eyes + +
  8. +
+ + {/* screen1 */} + {/* 질문 */} +
+ monster +   어떤 기술을 사용했나요? +
+ {/* 기술 선택 버튼 */} + {/* 배열에 요소를 처음부터 하나씩 읽는 게 map, 하나씩 읽을 때 요소를 가리키는 게 x, y는 요소의 인덱스 */} +
+ {AllStacks.map((x) => ( + + ))} +
+ + {/* 하단 버튼 */} + +
+ ); +}; + +interface Screen2Props { + onPrev: () => void; + onNext: () => void; +} + +const Screen2: React.FC = ({ onPrev, onNext }) => { + return ( +
+ {/* 이전/다음 */} +
+
+ + + +
+ + {/* 2단계 */} +
    +
  1. + + monster + +
  2. +
  3. + + desktop + +
  4. +
  5. + + pick + +
  6. +
  7. + + eyes + +
  8. +
+ + {/* screen2 */} + {/* 질문 */} +
+ desktop +   어떤 프로젝트를 구현했나요? +
+ {/* 프로젝트 설명 입력창 */} +
+ +
+ {/* 하단 버튼 */} + +
+ {/* {children} */} +
+ ); +}; + +interface Screen3Props { + onPrev: () => void; + onNext: () => void; +} + +const Screen3: React.FC = ({ onPrev, onNext }) => { + const [Arr, setArr] = useState([]); + + function Plus() { + const Arr2 = [...Arr]; + if (Arr.length < 4) { + Arr2.push(0); + setArr(Arr2); + } + } + return ( +
+ {/* 이전/다음 */} +
+
+ + + +
+ {/* 3단계 */} +
    +
  1. + + monster + +
  2. +
  3. + + desktop + +
  4. +
  5. + + pick + +
  6. +
  7. + + eyes + +
  8. +
+ + {/* screen3 */} + {/* 질문 */} +
+ pick +   어떤 기능을 구현했나요? +
+ {/* 기능 구현 입력창 */} +
+ + {Arr.map((x) => ( + + ))} + {Arr.length < 4 ? ( + + ) : null} +
+ {/* 하단버튼 */} + +
+ {/* {children} */} +
+ ); +}; + +interface Screen4Props { + onPrev: () => void; +} + +const Screen4: React.FC = ({ onPrev }) => { + const [Arr, setArr] = useState([]); + + function Plus() { + const Arr2 = [...Arr]; + if (Arr.length < 4) { + Arr2.push(0); + setArr(Arr2); + } + } + return ( +
+ {/* 이전/다음 */} + + + {/* 4단계 */} +
    +
  1. + + monster + +
  2. +
  3. + + desktop + +
  4. +
  5. + + pick + +
  6. +
  7. + + eyes + +
  8. +
+ + {/* screen4 */} + {/* 질문 */} +
+ eyes +   관심있는 고도화 계획이 있나요? +
+ {/* 고도화 기술 입력창 */} +
+ {' '} + {Arr.map((x) => ( + + ))} + {Arr.length < 4 ? ( + + ) : null} + {/* */} +
+ {/* 하단버튼 */} + + + {/* {children} */} +
+ ); +}; + +interface ModalDefaultType { + onClickToggleModal: () => void; +} + +function renderModal({ + onClickToggleModal, +}: PropsWithChildren) { + const [currentScreen, setCurrentScreen] = useState(1); + + const handleNext = () => { + setCurrentScreen(currentScreen + 1); + }; + + const handlePrev = () => { + setCurrentScreen(currentScreen - 1); + }; + + const renderScreen = () => { + switch (currentScreen) { + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + default: + return null; + } + }; + + return ( +
+
{renderScreen()}
+ {/* 모달 영역 외의 배경을 클릭하면 모달이 닫히게 하는 컨테이너, 이벤트핸들러를 사용하여 클릭 이벤트 발생 시 onClickToggleModal 함수 호출하여 모달 닫음 */} +
+ ); +} + +export default renderModal; diff --git a/frontend/src/pages/ReadingPage.tsx b/frontend/src/pages/ReadingPage.tsx index 7b6cb22a..ed682db2 100644 --- a/frontend/src/pages/ReadingPage.tsx +++ b/frontend/src/pages/ReadingPage.tsx @@ -1,11 +1,13 @@ import { act } from '@testing-library/react'; import axios from 'axios'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useRecoilState, useRecoilValue } from 'recoil'; +import gptIcon from '../assets/image/gptIcon.svg'; import NavBar from '../components/NavBar'; import QuillWrtten from '../components/QuillWritten'; +import RenderModal from '../components/RenderModal'; import Reply from '../components/Reply'; import { contentsState, @@ -26,6 +28,13 @@ function ReadingPage() { const navigate = useNavigate(); const params = useParams(); + const [isOpenModal, setOpenModal] = useState(false); + + const onClickToggleModal = useCallback(() => { + setOpenModal(!isOpenModal); + console.log(isOpenModal); + }, [isOpenModal]); + function toWrite() { // react-router-dom을 이용한 글 쓰기 페이지로 이동 함수 navigate('/write'); @@ -122,6 +131,21 @@ function ReadingPage() { {/** 전체 컨텐츠 영역* */}
+ {/* AI 고도화 버튼 */} + + + {isOpenModal ? ( + + ) : null} + {/** 텍스트 영역* */}
{/** 제목* */} diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index 7f879b23..55388408 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -3,12 +3,24 @@ module.exports = { content: ['./src/**/*.{html,js,ts,tsx}'], theme: { extend: { + spacing: { + 660: '660px', + 630: '630px', + 555: '555px', + 460: '460px', + 355: '350px', + 504: '54px', + }, + colors: { graphybg: '#F9F8F8', graphyblue: '#505F9A', + graphypink: '#CA92C7', mainbannerleft: '#678EF4', mainbannerright: '#FF93AE', subbanner: '#C1D0EF', + gptbutton: '#7082CA', + button: '#364A9A', }, width: { 284: '17.75rem', @@ -37,7 +49,6 @@ module.exports = { lef: ['LeferiBaseRegular', 'sans-serif'], 'lef-b': ['LeferiBaseBold', 'sans-serif'], lato: ['LatoBold', 'sans-serif'], - }, }, },