diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/CommentAcceptanceFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/CommentAcceptanceFixture.java new file mode 100644 index 000000000..11cddeefc --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/CommentAcceptanceFixture.java @@ -0,0 +1,24 @@ +package wooteco.prolog.fixtures; + +import wooteco.prolog.studylog.application.dto.CommentChangeRequest; +import wooteco.prolog.studylog.application.dto.CommentCreateRequest; + +public enum CommentAcceptanceFixture { + + COMMENT("스터디로그의 댓글 내용입니다."), + UPDATED_COMMENT("수정된 스터디로그의 댓글 내용입니다."); + + private final String content; + + CommentAcceptanceFixture(String content) { + this.content = content; + } + + public CommentCreateRequest getCreateRequest() { + return new CommentCreateRequest(COMMENT.content); + } + + public CommentChangeRequest getUpdateRequest() { + return new CommentChangeRequest(UPDATED_COMMENT.content); + } +} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/LevellogFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/LevellogFixture.java new file mode 100644 index 000000000..356aeeee6 --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/LevellogFixture.java @@ -0,0 +1,43 @@ +package wooteco.prolog.fixtures; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import wooteco.prolog.levellogs.application.dto.LevelLogRequest; +import wooteco.prolog.levellogs.application.dto.SelfDiscussionRequest; + +public class LevellogFixture { + + public static final List SELF_DISCUSSION_REQUESTS = Arrays.asList( + new SelfDiscussionRequest("Q1", "A1"), + new SelfDiscussionRequest("Q2", "A2")); + + public static final LevelLogRequest LEVEL_LOG_REQUEST = new LevelLogRequest("title1", + "content1", + SELF_DISCUSSION_REQUESTS); + + public static final List SELF_DISCUSSION_UPDATE_REQUESTS = Arrays.asList( + new SelfDiscussionRequest("Updated Q1", "Updated A1"), + new SelfDiscussionRequest("Updated Q2", "Updated A2")); + + public static final LevelLogRequest LEVEL_LOG_UPDATE_REQUEST = new LevelLogRequest( + "updated title", "updated content", SELF_DISCUSSION_UPDATE_REQUESTS + ); + + public static List levelLogRequests() { + List requests = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + requests.add( + new LevelLogRequest("title" + i, "content" + i, selfDiscussionRequests(3))); + } + return requests; + } + + private static List selfDiscussionRequests(int count) { + List requests = new ArrayList<>(); + for (int i = 0; i < count; i++) { + requests.add(new SelfDiscussionRequest("Q" + i, "A" + i)); + } + return requests; + } +} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/MemberFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/MemberFixture.java deleted file mode 100644 index fc1a26f6c..000000000 --- a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/MemberFixture.java +++ /dev/null @@ -1,5 +0,0 @@ -package wooteco.prolog.fixtures; - -public class MemberFixture { - -} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/PostAcceptanceFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/PostAcceptanceFixture.java index 180215a3c..b550fcb95 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/PostAcceptanceFixture.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/PostAcceptanceFixture.java @@ -44,6 +44,14 @@ public enum PostAcceptanceFixture { 2L, TAG5, TAG6 + ), + POST6( + "새로운 스터디로그", + "옵셔널은 NPE를 배제하기 위해 만들어진 자바8에 추가된 라이브러리입니다. \n " + + "다양한 메소드를 호출하여 원하는 대로 활용할 수 있습니다", + 1L, + TAG1, + TAG2 ); PostAcceptanceFixture( diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/SessionAcceptanceFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/SessionAcceptanceFixture.java index a1685cb8b..e49b725fa 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/SessionAcceptanceFixture.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/SessionAcceptanceFixture.java @@ -6,8 +6,8 @@ public class SessionAcceptanceFixture { - public static SessionRequest session1 = new SessionRequest("백엔드Java 레벨1 - 2022"); - public static SessionRequest session2 = new SessionRequest("프론트엔드JavaScript 레벨1 - 2022"); + public static SessionRequest session1 = new SessionRequest("세션1"); + public static SessionRequest session2 = new SessionRequest("세션2"); public static SessionRequest session3 = new SessionRequest("세션3"); public static SessionRequest session4 = new SessionRequest("세션4"); public static SessionRequest session5 = new SessionRequest("세션5"); diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java index dcb3fb72c..9284784e3 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java @@ -9,113 +9,133 @@ import static wooteco.prolog.fixtures.TagAcceptanceFixture.TAG6; import java.util.Arrays; +import java.util.Collections; import java.util.List; import wooteco.prolog.studylog.application.dto.StudylogRequest; import wooteco.prolog.studylog.application.dto.TagRequest; public enum StudylogAcceptanceFixture { - STUDYLOG1( - "[자바][옵셔널] 학습log 제출합니다.", - "옵셔널은 NPE를 배제하기 위해 만들어진 자바8에 추가된 라이브러리입니다. \n " + - "다양한 메소드를 호출하여 원하는 대로 활용할 수 있습니다", - 1L, - 1L, - TAG1, - TAG2 - ), - STUDYLOG2( - "[자바스크립트][비동기] 학습log 제출합니다.", - "모던 JS의 fetch문, ajax라이브러리인 axios등을 통해 비동기 요청을 \n " + - "편하게 할 수 있습니다. 자바 최고", - 2L, - 2L, - TAG3, - TAG4 - ), - STUDYLOG3( - "[자료구조] 자료구조는 어려워요", - "진짜 어려움", - 1L, - 1L, - TAG1, - TAG5 - ), - STUDYLOG4( - "[DOM] DOM DOM Dance", - "덤덤 댄스 아니고", - 2L, - 2L - ), - STUDYLOG5( - "[알고리즘] 자료구조의 big O에 관하여", - "big O는 small O보다 크다", - 2L, - 2L, - TAG5, - TAG6 - ), - STUDYLOG6( - "[DOM] DOM DOM Dance", - "덤덤 댄스 아니고", - 2L, - null - ), - STUDYLOG7( - "[알고리즘] 자료구조의 big O에 관하여", - "big O는 small O보다 크다", - 2L, - null, - TAG5, - TAG6 - ), - STUDYLOG8( - "[공지] 배지에 관하여", - "열정왕을 위해 달려라", - 10L, - 2L, - TAG5, - TAG6 - ), - STUDYLOG9( - "[공지] 배지에 관하여2", - "칭찬왕을 위해서 달려라", - 11L, - 2L, - TAG5, - TAG6 - ); + STUDYLOG1( + "[자바][옵셔널] 학습log 제출합니다.", + "옵셔널은 NPE를 배제하기 위해 만들어진 자바8에 추가된 라이브러리입니다. \n " + + "다양한 메소드를 호출하여 원하는 대로 활용할 수 있습니다", + 1L, + 1L, + Collections.emptyList(), + TAG1, + TAG2 + ), + STUDYLOG2( + "[자바스크립트][비동기] 학습log 제출합니다.", + "모던 JS의 fetch문, ajax라이브러리인 axios등을 통해 비동기 요청을 \n " + + "편하게 할 수 있습니다. 자바 최고", + 2L, + 2L, + Collections.emptyList(), + TAG3, + TAG4 + ), + STUDYLOG3( + "[자료구조] 자료구조는 어려워요", + "진짜 어려움", + 1L, + 1L, + Collections.emptyList(), + TAG1, + TAG5 + ), + STUDYLOG4( + "[DOM] DOM DOM Dance", + "덤덤 댄스 아니고", + 2L, + 2L, + Collections.emptyList() + ), + STUDYLOG5( + "[알고리즘] 자료구조의 big O에 관하여", + "big O는 small O보다 크다", + 2L, + 2L, + Collections.emptyList(), + TAG5, + TAG6 + ), + STUDYLOG6( + "[DOM] DOM DOM Dance", + "덤덤 댄스 아니고", + 2L, + null, + Collections.emptyList() + ), + STUDYLOG7( + "[알고리즘] 자료구조의 big O에 관하여", + "big O는 small O보다 크다", + 2L, + null, + Collections.emptyList(), + TAG5, + TAG6 + ), + STUDYLOG8( + "[공지] 배지에 관하여", + "열정왕을 위해 달려라", + 10L, + 2L, + Collections.emptyList(), + TAG5, + TAG6 + ), + STUDYLOG9( + "[공지] 배지에 관하여2", + "칭찬왕을 위해서 달려라", + 11L, + 2L, + Collections.emptyList(), + TAG5, + TAG6 + ), + STUDYLOG10( + "[알고리즘] 알고리즘은 어려워요", + "진짜 어려움", + 1L, + 1L, + Arrays.asList(4L) + ); - private final StudylogRequest studylogRequest; - private final List tags; + private final StudylogRequest studylogRequest; + private final List tags; - StudylogAcceptanceFixture( - String title, - String content, - Long sessionId, - Long missionId, - TagAcceptanceFixture... tags) { - this.tags = Arrays.asList(tags); - List tagRequests = Arrays.stream(tags) - .map(TagAcceptanceFixture::getTagRequest) - .collect(toList()); - this.studylogRequest = new StudylogRequest(title, content, sessionId, missionId, tagRequests); - } + StudylogAcceptanceFixture( + String title, + String content, + Long sessionId, + Long missionId, + List abilities, + TagAcceptanceFixture... tags) { + this.tags = Arrays.asList(tags); + List tagRequests = Arrays.stream(tags) + .map(TagAcceptanceFixture::getTagRequest) + .collect(toList()); + this.studylogRequest = new StudylogRequest(title, content, sessionId, missionId, + tagRequests, abilities + ); + } - public static List findByMissionNumber(Long missionId) { - return Arrays.stream(StudylogAcceptanceFixture.values()) - .map(StudylogAcceptanceFixture::getStudylogRequest) - .filter(it -> it.getMissionId() != null && it.getMissionId().equals(missionId)) - .collect(toList()); - } + public static List findByMissionNumber(Long missionId) { + return Arrays.stream(StudylogAcceptanceFixture.values()) + .map(StudylogAcceptanceFixture::getStudylogRequest) + .filter(it -> it.getMissionId() != null && it.getMissionId().equals(missionId)) + .collect(toList()); + } - public static List findByTagNumber(Long tagId) { - return Arrays.stream(StudylogAcceptanceFixture.values()) - .filter(it -> it.tags.stream().anyMatch(tag -> tag.getTagId().equals(tagId))) - .map(StudylogAcceptanceFixture::getStudylogRequest) - .collect(toList()); - } + public static List findByTagNumber(Long tagId) { + return Arrays.stream(StudylogAcceptanceFixture.values()) + .filter(it -> it.tags.stream().anyMatch(tag -> tag.getTagId().equals(tagId))) + .map(StudylogAcceptanceFixture::getStudylogRequest) + .collect(toList()); + } - public StudylogRequest getStudylogRequest() { - return studylogRequest; - } + public StudylogRequest getStudylogRequest() { + return studylogRequest; + } } diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/BadgesStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/BadgesStepDefinitions.java index f22201b7f..4e8f0286c 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/BadgesStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/BadgesStepDefinitions.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpStatus; import wooteco.prolog.AcceptanceSteps; import wooteco.prolog.common.exception.ExceptionDto; -import wooteco.prolog.studylog.domain.BadgeType; +import wooteco.prolog.badge.domain.BadgeType; public class BadgesStepDefinitions extends AcceptanceSteps { diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/CommentStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/CommentStepDefinitions.java new file mode 100644 index 000000000..87d3dcdbe --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/CommentStepDefinitions.java @@ -0,0 +1,81 @@ +package wooteco.prolog.steps; + +import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.prolog.fixtures.CommentAcceptanceFixture.COMMENT; +import static wooteco.prolog.fixtures.CommentAcceptanceFixture.UPDATED_COMMENT; + +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.elasticsearch.common.collect.List; +import org.springframework.http.HttpStatus; +import wooteco.prolog.AcceptanceSteps; +import wooteco.prolog.fixtures.GithubResponses; +import wooteco.prolog.studylog.application.dto.CommentMemberResponse; +import wooteco.prolog.studylog.application.dto.CommentResponse; +import wooteco.prolog.studylog.application.dto.CommentsResponse; + +public class CommentStepDefinitions extends AcceptanceSteps { + + @Given("{long}번 스터디로그에 대한 댓글을 작성하고") + @When("{long}번 스터디로그에 대한 댓글을 작성하면") + public void 스터디로그에_대한_댓글을_작성하면(Long studylogId) { + context.invokeHttpPostWithToken("/studylogs/" + studylogId + "/comments", + COMMENT.getCreateRequest()); + } + + @Then("댓글이 작성된다") + public void 댓글이_작성된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.CREATED.value()); + } + + @When("{long}번 스터디로그의 댓글을 조회하면") + public void 스터디로그의_댓글을_조회하면(Long studylogId) { + context.invokeHttpGetWithToken("/studylogs/" + studylogId + "/comments"); + } + + @Then("해당 스터디로그의 댓글 목록을 조회한다") + public void 해당_스터디로그의_댓글_목록을_조회한다() { + int statusCode = context.response.statusCode(); + CommentsResponse commentsResponse = context.response.as(CommentsResponse.class); + + assertThat(statusCode).isEqualTo(HttpStatus.OK.value()); + assertThat(commentsResponse.getData()) + .usingRecursiveComparison() + .ignoringFields("createAt").isEqualTo(List.of( + new CommentResponse(1L, new CommentMemberResponse(1L, GithubResponses.브라운.getLogin(), + GithubResponses.브라운.getName(), GithubResponses.브라운.getAvatarUrl(), "CREW"), + "스터디로그의 댓글 내용입니다.", null), + new CommentResponse(2L, new CommentMemberResponse(2L, GithubResponses.웨지.getLogin(), + GithubResponses.웨지.getName(), GithubResponses.웨지.getAvatarUrl(), "CREW"), + "스터디로그의 댓글 내용입니다.", null) + )); + } + + @When("{long}번 스터디로그에 대한 {long}번 댓글을 수정하면") + public void 스터디로그에_대한_댓글을_수정하면(Long studylogId, Long commentId) { + context.invokeHttpPutWithToken("/studylogs/" + studylogId + "/comments/" + commentId, + UPDATED_COMMENT.getUpdateRequest()); + } + + @When("{long}번 스터디로그에 대한 {long}번 댓글을 삭제하면") + public void 스터디로그에_대한_댓글을_삭제하면(Long studylogId, Long commentId) { + context.invokeHttpDeleteWithToken("/studylogs/" + studylogId + "/comments/" + commentId); + } + + @Then("댓글이 수정된다") + public void 댓글이_수정_된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Then("댓글이 삭제된다") + public void 댓글이_삭제_된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.NO_CONTENT.value()); + } +} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/GroupMemberStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/GroupMemberStepDefinitions.java new file mode 100644 index 000000000..902d6f09b --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/GroupMemberStepDefinitions.java @@ -0,0 +1,34 @@ +package wooteco.prolog.steps; + +import io.cucumber.java.en.Given; +import wooteco.prolog.AcceptanceSteps; +import wooteco.prolog.member.domain.GroupMember; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.MemberGroup; +import wooteco.prolog.member.domain.repository.GroupMemberRepository; +import wooteco.prolog.member.domain.repository.MemberGroupRepository; +import wooteco.prolog.member.domain.repository.MemberRepository; + +public class GroupMemberStepDefinitions extends AcceptanceSteps { + + private final MemberRepository memberRepository; + private final MemberGroupRepository memberGroupRepository; + private final GroupMemberRepository groupMemberRepository; + + public GroupMemberStepDefinitions(MemberRepository memberRepository, + MemberGroupRepository memberGroupRepository, + GroupMemberRepository groupMemberRepository) { + this.memberRepository = memberRepository; + this.memberGroupRepository = memberGroupRepository; + this.groupMemberRepository = groupMemberRepository; + } + + @Given("{string}을 멤버그룹과 그룹멤버에 등록하고") + public void 그룹멤버를_생성하고(String title) { + Member member = memberRepository.findById(1L).get(); + MemberGroup 프론트엔드 = memberGroupRepository.save(new MemberGroup(null, "4기 프론트엔드", "4기 프론트엔드 설명")); + MemberGroup 백엔드 = memberGroupRepository.save(new MemberGroup(null, "4기 백엔드", "4기 백엔드 설명")); + groupMemberRepository.save(new GroupMember(null, member, 백엔드)); + groupMemberRepository.save(new GroupMember(null, member, 프론트엔드)); + } +} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/LevellogsStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/LevellogsStepDefinitions.java new file mode 100644 index 000000000..bf8e28994 --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/LevellogsStepDefinitions.java @@ -0,0 +1,134 @@ +package wooteco.prolog.steps; + +import static org.apache.http.HttpHeaders.LOCATION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.api.Assertions.assertAll; +import static wooteco.prolog.fixtures.LevellogFixture.LEVEL_LOG_UPDATE_REQUEST; +import static wooteco.prolog.fixtures.LevellogFixture.levelLogRequests; + +import io.cucumber.java.en.And; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.util.ArrayList; +import java.util.List; +import org.springframework.http.HttpStatus; +import wooteco.prolog.AcceptanceSteps; +import wooteco.prolog.fixtures.LevellogFixture; +import wooteco.prolog.levellogs.application.dto.LevelLogRequest; +import wooteco.prolog.levellogs.application.dto.LevelLogResponse; +import wooteco.prolog.levellogs.application.dto.LevelLogSummariesResponse; +import wooteco.prolog.levellogs.application.dto.LevelLogSummaryResponse; +import wooteco.prolog.levellogs.application.dto.SelfDiscussionResponse; + +public class LevellogsStepDefinitions extends AcceptanceSteps { + + @When("레벨로그를 작성하(고)(면)") + public void 레벨로그를작성하면() { + context.invokeHttpPostWithToken("/levellogs", LevellogFixture.LEVEL_LOG_REQUEST); + + assertThat(context.response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + + String levelLogId = context.response.getHeader(LOCATION).replaceAll("/levellogs/", ""); + context.storage.put("levelLogId", Long.valueOf(levelLogId)); + } + + @Then("레벨로그가 조회된다") + public void 레벨로그가조회된다() { + Object levelLogId = context.storage.get("levelLogId"); + context.invokeHttpGet("/levellogs/" + levelLogId); + + LevelLogResponse response = context.response.as(LevelLogResponse.class); + + assertAll( + () -> assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.getTitle()).isEqualTo("title1"), + () -> assertThat(response.getContent()).isEqualTo("content1"), + () -> assertThat(response.getAuthor().getNickname()).isEqualTo("브라운"), + () -> assertThat(response.getLevelLogs()) + .extracting(SelfDiscussionResponse::getQuestion, SelfDiscussionResponse::getAnswer) + .containsExactlyInAnyOrder( + tuple("Q1", "A1"), + tuple("Q2", "A2") + ) + ); + } + + @And("레벨로그를 삭제하면") + public void 레벨로그를삭제하면() { + Object levelLogId = context.storage.get("levelLogId"); + context.invokeHttpDeleteWithToken("/levellogs/" + levelLogId); + + assertThat(context.response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Then("레벨로그가 삭제된다") + public void 레벨로그가삭제된다() { + Object levelLogId = context.storage.get("levelLogId"); + context.invokeHttpGet("/levellogs/" + levelLogId); + + assertThat(context.response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @And("레벨로그를 수정하면") + public void 레벨로그를수정하면() { + Object levelLogId = context.storage.get("levelLogId"); + context.invokeHttpPutWithToken("/levellogs/" + levelLogId, LEVEL_LOG_UPDATE_REQUEST); + + assertThat(context.response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Then("레벨로그가 수정된다") + public void 레벨로그가수정된다() { + Object levelLogId = context.storage.get("levelLogId"); + context.invokeHttpGet("/levellogs/" + levelLogId); + + LevelLogResponse response = context.response.as(LevelLogResponse.class); + + assertAll( + () -> assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.getTitle()).isEqualTo("updated title"), + () -> assertThat(response.getContent()).isEqualTo("updated content"), + () -> assertThat(response.getAuthor().getNickname()).isEqualTo("브라운"), + () -> assertThat(response.getLevelLogs()) + .extracting(SelfDiscussionResponse::getQuestion, SelfDiscussionResponse::getAnswer) + .containsExactlyInAnyOrder( + tuple("Updated Q1", "Updated A1"), + tuple("Updated Q2", "Updated A2") + ) + ); + } + + @When("레벨로그를 여러개 작성하면") + public void 레벨로그를여러개작성하면() { + List levelLogIds = new ArrayList<>(); + + for (LevelLogRequest request : levelLogRequests()) { + context.invokeHttpPostWithToken("/levellogs", request); + assertThat(context.response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + String levelLogId = context.response.getHeader(LOCATION).replaceAll("/levellogs/", ""); + levelLogIds.add(Long.valueOf(levelLogId)); + } + + context.storage.put("levelLogIds", levelLogIds); + } + + @Then("레벨로그가 여러개 조회된다") + public void 레벨로그가여러개조회된다() { + context.invokeHttpGet("/levellogs?page=0&size=3"); + + LevelLogSummariesResponse response = context.response.as(LevelLogSummariesResponse.class); + + List levelLogIds = (List) context.storage.get("levelLogIds"); + + assertAll( + () -> assertThat(context.response.getStatusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.getCurrPage()).isEqualTo(1), + () -> assertThat(response.getTotalPage()).isEqualTo(2), + () -> assertThat(response.getTotalSize()).isEqualTo(5), + () -> assertThat(response.getData()) + .extracting(LevelLogSummaryResponse::getId) + .containsExactlyInAnyOrderElementsOf(levelLogIds.subList(2, 5)) + ); + } +} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ReportStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ReportStepDefinitions.java index c3d1112ad..2b6b7d873 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ReportStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ReportStepDefinitions.java @@ -5,6 +5,7 @@ import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -12,12 +13,16 @@ import java.util.stream.Collectors; import org.springframework.http.HttpStatus; import wooteco.prolog.AcceptanceSteps; +import wooteco.prolog.ability.application.dto.AbilityStudylogResponse; import wooteco.prolog.common.PageableResponse; import wooteco.prolog.fixtures.GithubResponses; +import wooteco.prolog.fixtures.PostAcceptanceFixture; import wooteco.prolog.report.application.dto.ReportAbilityRequest; import wooteco.prolog.report.application.dto.ReportRequest; import wooteco.prolog.report.application.dto.ReportResponse; import wooteco.prolog.report.application.dto.ReportUpdateRequest; +import wooteco.prolog.report.application.dto.StudylogPeriodRequest; +import wooteco.prolog.studylog.application.dto.CalendarStudylogResponse; public class ReportStepDefinitions extends AcceptanceSteps { @@ -163,4 +168,26 @@ public class ReportStepDefinitions extends AcceptanceSteps { assertThat(reportIds).isNull(); } + + @When("지난 일주일부터 오늘까지의 학습로그를 조회하면") + public void 지난일주일부터오늘까지의학습로그를조회하면() { + LocalDate start = LocalDate.now().minusDays(7); + LocalDate end = LocalDate.now(); + + String path = String.format("studylogs/me/?startDate=%s&endDate=%s", start, end); + context.invokeHttpGetWithToken(path); + } + + @Then("해당 유저의 해당 기간 스터디로그 목록이 조회된다") + public void 해당유저의해당기간스터디로그목록이조회된다() { + final List data = context.response.then().extract() + .body() + .jsonPath() + .getList(".", AbilityStudylogResponse.class); + + assertThat(data).extracting(abilityStudylogResponse -> abilityStudylogResponse.getStudylog().getTitle()) + .containsExactlyInAnyOrder( + PostAcceptanceFixture.POST6.getPostRequest().getTitle() + ); + } } diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/SessionMemberStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/SessionMemberStepDefinitions.java index 90262d43d..4a182923a 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/SessionMemberStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/SessionMemberStepDefinitions.java @@ -32,7 +32,7 @@ public class SessionMemberStepDefinitions extends AcceptanceSteps { context.storage.put("session", sessionName); } - @When("{long} 강의에 자신을 등록하면") + @When("{long} 강의에 자신을 등록하(면/고)") public void 강의에자신을등록(Long sessionId) { context.invokeHttpPostWithToken("sessions/" + sessionId + "/members/me"); } @@ -41,4 +41,14 @@ public class SessionMemberStepDefinitions extends AcceptanceSteps { public void 강의에내가추가된다() { assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()); } + + @When("{long} 강의에서 자신을 제거하면") + public void 강의에서자신을제거하면(Long sessionId) { + context.invokeHttpDeleteWithToken("/sessions/" + sessionId + "/members/me"); + } + + @Then("강의에서 내가 제거된다.") + public void 강의에서내가제거된다() { + assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } } diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogAbilityStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogAbilityStepDefinitions.java index cb3a57442..ee33fcc09 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogAbilityStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogAbilityStepDefinitions.java @@ -86,6 +86,19 @@ public class StudylogAbilityStepDefinitions extends AcceptanceSteps { assertThat(abilityNames.contains(abilityName2)).isTrue(); } + @Then("{string} 역량 하나가 맵핑된 {string} 학습로그가 조회된다") + public void 역량하나가맵핑된학습로그가조회된다(String abilityName, String studylogName) { + List abilityStudylogResponses = context.response.jsonPath().getList("data", AbilityStudylogResponse.class); + Optional abilityStudylog = abilityStudylogResponses.stream() + .filter(it -> it.getStudylog().getTitle().equals(studylogName)) + .findAny(); + assertThat(abilityStudylogResponses.size()).isEqualTo(1); + assertThat(abilityStudylog.isPresent()).isTrue(); + + List abilityNames = context.response.jsonPath().getList("data.abilities[0].name"); + assertThat(abilityNames.contains(abilityName)).isTrue(); + } + @Then("{string}, {string} 역량만 맵핑된 {string} 학습로그가 조회된다") public void 역량만맵핑된학습로그가조회된다(String abilityName1, String abilityName2, String studylogName) { List abilityStudylogResponses = context.response.jsonPath().getList("data", AbilityStudylogResponse.class); diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java index 028bc7e43..bffc762f9 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static wooteco.prolog.fixtures.StudylogAcceptanceFixture.STUDYLOG1; +import static wooteco.prolog.fixtures.StudylogAcceptanceFixture.STUDYLOG10; import static wooteco.prolog.fixtures.StudylogAcceptanceFixture.STUDYLOG2; import static wooteco.prolog.fixtures.StudylogAcceptanceFixture.STUDYLOG3; import static wooteco.prolog.fixtures.StudylogAcceptanceFixture.STUDYLOG4; @@ -14,9 +15,12 @@ import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import io.cucumber.messages.internal.com.google.common.collect.Lists; - -import java.util.*; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.springframework.http.HttpStatus; import wooteco.prolog.AcceptanceSteps; import wooteco.prolog.fixtures.StudylogAcceptanceFixture; @@ -25,9 +29,7 @@ import wooteco.prolog.studylog.application.dto.StudylogRequest; import wooteco.prolog.studylog.application.dto.StudylogResponse; import wooteco.prolog.studylog.application.dto.StudylogSessionRequest; -import wooteco.prolog.studylog.application.dto.StudylogWithScrapedCountResponse; import wooteco.prolog.studylog.application.dto.StudylogsResponse; -import wooteco.prolog.studylog.application.dto.StudylogsWithScrapCountResponse; import wooteco.prolog.studylog.application.dto.TagRequest; public class StudylogStepDefinitions extends AcceptanceSteps { @@ -56,7 +58,8 @@ public class StudylogStepDefinitions extends AcceptanceSteps { Lists.newArrayList( new TagRequest(TAG1.getTagName()), new TagRequest(TAG2.getTagName()) - ) + ), + Collections.emptyList() ); context.invokeHttpPostWithToken("/studylogs", studylogRequest); @@ -76,7 +79,8 @@ public class StudylogStepDefinitions extends AcceptanceSteps { Lists.newArrayList( new TagRequest(TAG1.getTagName()), new TagRequest(TAG2.getTagName()) - ) + ), + Collections.emptyList() ); context.invokeHttpPostWithToken("/studylogs", studylogRequest); if (context.response.statusCode() == HttpStatus.CREATED.value()) { @@ -100,8 +104,8 @@ public class StudylogStepDefinitions extends AcceptanceSteps { context.invokeHttpGet("/studylogs/" + studylogResponse.getId()); - StudylogWithScrapedCountResponse response = context.response.as(StudylogWithScrapedCountResponse.class); - assertThat(response.getStudylogResponse().getId()).isNotNull(); + StudylogResponse response = context.response.as(StudylogResponse.class); + assertThat(response.getId()).isNotNull(); } @Given("{long}개의 스터디로그를 작성하고") @@ -277,37 +281,25 @@ public class StudylogStepDefinitions extends AcceptanceSteps { String path = "/studylogs/" + studylogId; context.invokeHttpGet(path); - StudylogWithScrapedCountResponse studylog = context.response.as(StudylogWithScrapedCountResponse.class); - - assertThat(studylog.getScrapedCount()).isNotNull(); - } - - @Then("{long}번째 스터디로그의 스크랩수가 조회된다.") - public void 스터디로그의스크랩수가조회된다(Long studylogId) { - assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()); - - String path = "/studylogs/" + studylogId; - context.invokeHttpGet(path); - StudylogWithScrapedCountResponse studylog = context.response.as(StudylogWithScrapedCountResponse.class); + StudylogResponse studylog = context.response.as(StudylogResponse.class); - assertThat(studylog.getScrapedCount()).isEqualTo(1); + assertThat(studylog).isNotNull(); } @Then("조회된 스터디로그의 조회수가 {int}로 증가된다") public void 조회된스터디로그의조회수가증가된다(int viewCount) { - StudylogWithScrapedCountResponse studylog = context.response.as(StudylogWithScrapedCountResponse.class); + StudylogResponse studylog = context.response.as(StudylogResponse.class); assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()); - assertThat(studylog.getStudylogResponse().getViewCount()).isEqualTo(viewCount); + assertThat(studylog.getViewCount()).isEqualTo(viewCount); } @Then("조회된 스터디로그의 조회수가 증가되지 않고 {int}이다") public void 조회된스터디로그의조회수가증가되지않는다(int viewCount) { - StudylogWithScrapedCountResponse studylog = context.response.as(StudylogWithScrapedCountResponse.class); + StudylogResponse studylog = context.response.as(StudylogResponse.class); assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()); - assertThat(studylog.getStudylogResponse().getViewCount()).isEqualTo(viewCount); - + assertThat(studylog.getViewCount()).isEqualTo(viewCount); } @When("{long}번째 스터디로그를 수정하면") @@ -316,15 +308,21 @@ public class StudylogStepDefinitions extends AcceptanceSteps { context.invokeHttpPutWithToken(path, STUDYLOG3.getStudylogRequest()); } + @When("{long}번째 스터디로그의 역량을 수정하면") + public void 스터디로그의역량을수정하면(Long studylogId) { + String path = "/studylogs/" + studylogId; + context.invokeHttpPutWithToken(path, STUDYLOG10.getStudylogRequest()); + } + @Then("{long}번째 스터디로그가 수정된다") public void 스터디로그가수정된다(Long studylogId) { assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()); String path = "/studylogs/" + studylogId; context.invokeHttpGet(path); - StudylogWithScrapedCountResponse studylog = context.response.as(StudylogWithScrapedCountResponse.class); + StudylogResponse studylog = context.response.as(StudylogResponse.class); - assertThat(studylog.getStudylogResponse().getContent()).isEqualTo(STUDYLOG3.getStudylogRequest().getContent()); + assertThat(studylog.getContent()).isEqualTo(STUDYLOG3.getStudylogRequest().getContent()); } @Then("에러 응답을 받는다") @@ -377,27 +375,26 @@ public class StudylogStepDefinitions extends AcceptanceSteps { @Then("조회된 스터디로그의 좋아요 수가 증가한다") public void 조회된스터디로그의좋아요수가증가한다() { - StudylogWithScrapedCountResponse response = context.response.as(StudylogWithScrapedCountResponse.class); - System.out.println("responseLikesCount = " + response.getStudylogResponse().getLikesCount()); - assertThat(response.getStudylogResponse().getLikesCount()).isEqualTo(1); + StudylogResponse response = context.response.as(StudylogResponse.class); + assertThat(response.getLikesCount()).isEqualTo(1); } @Then("조회된 스터디로그의 좋아요 수가 증가하지 않는다") public void 조회된스터디로그의좋아요수가증가하지않는다() { - StudylogWithScrapedCountResponse response = context.response.as(StudylogWithScrapedCountResponse.class); - assertThat(response.getStudylogResponse().getLikesCount()).isEqualTo(0); + StudylogResponse response = context.response.as(StudylogResponse.class); + assertThat(response.getLikesCount()).isEqualTo(0); } @Then("조회된 스터디로그의 좋아요 여부가 참이다") public void 조회된스터디로그의좋아요여부가참이다() { - StudylogWithScrapedCountResponse response = context.response.as(StudylogWithScrapedCountResponse.class); - assertThat(response.getStudylogResponse().isLiked()).isTrue(); + StudylogResponse response = context.response.as(StudylogResponse.class); + assertThat(response.isLiked()).isTrue(); } @Then("조회된 스터디로그의 좋아요 여부가 거짓이다") public void 조회된스터디로그의좋아요여부가거짓이다() { - StudylogWithScrapedCountResponse response = context.response.as(StudylogWithScrapedCountResponse.class); - assertThat(response.getStudylogResponse().isLiked()).isFalse(); + StudylogResponse response = context.response.as(StudylogResponse.class); + assertThat(response.isLiked()).isFalse(); } @When("인기 있는 스터디로그 목록을 {string}개만큼 갱신하고") @@ -410,13 +407,13 @@ public class StudylogStepDefinitions extends AcceptanceSteps { public void 스터디로그가Id순서로조회된다(String studylogIds) { context.invokeHttpGet("/studylogs/popular"); assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()); - PopularStudylogsResponse popularStudylogsResponse = context.response.as(PopularStudylogsResponse.class); + PopularStudylogsResponse studylogsResponse = context.response.as(PopularStudylogsResponse.class); List ids = Arrays.asList(studylogIds.split(", ")); for (int i = 0; i < ids.size(); i++) { - StudylogWithScrapedCountResponse response = popularStudylogsResponse.getAllResponse().getData().get(i); + StudylogResponse response = studylogsResponse.getAllResponse().getData().get(i); Long id = Long.parseLong(ids.get(i)); - assertThat(response.getStudylogResponse().getId()).isEqualTo(id);; + assertThat(response.getId()).isEqualTo(id); } } @@ -430,7 +427,7 @@ public class StudylogStepDefinitions extends AcceptanceSteps { public void 스터디로그세션이로수정된다(long sessionId) { StudylogResponse studylogResponse = (StudylogResponse) context.storage.get("studylog"); context.invokeHttpGet("/studylogs/" + studylogResponse.getId()); - assertThat(context.response.as(StudylogWithScrapedCountResponse.class).getStudylogResponse().getSession().getId()).isEqualTo(sessionId); + assertThat(context.response.as(StudylogResponse.class).getSession().getId()).isEqualTo(sessionId); } @When("스터디로그 미션을 {long}로 수정하면") @@ -443,6 +440,22 @@ public class StudylogStepDefinitions extends AcceptanceSteps { public void 스터디로그미션이로수정된다(long missionId) { StudylogResponse studylogResponse = (StudylogResponse) context.storage.get("studylog"); context.invokeHttpGet("/studylogs/" + studylogResponse.getId()); - assertThat(context.response.as(StudylogWithScrapedCountResponse.class).getStudylogResponse().getMission().getId()).isEqualTo(missionId); + assertThat(context.response.as(StudylogResponse.class).getMission().getId()).isEqualTo(missionId); + } + + @Given("{long}, {long} 역량을 맵핑한 {string} 스터디로그를 작성하고") + public void 역량을맵핑한스터디로그를작성하고(long abilityId1, long abilityId2, String studylogName) { + StudylogRequest studylogRequest = new StudylogRequest(studylogName, "content", null, 1L, + Collections.emptyList(), Arrays.asList(abilityId1, abilityId2)); + context.invokeHttpPostWithToken("/studylogs", studylogRequest); + context.storage.put(studylogName, context.response.as(StudylogResponse.class)); + } + + @Given("{long} 역량 한개를 맵핑한 {string} 스터디로그를 작성하고") + public void 역량한개를맵핑한스터디로그를작성하고(long abilityId1, String studylogName) { + StudylogRequest studylogRequest = new StudylogRequest(studylogName, "content", null, 1L, + Collections.emptyList(), Arrays.asList(abilityId1)); + context.invokeHttpPostWithToken("/studylogs", studylogRequest); + context.storage.put(studylogName, context.response.as(StudylogResponse.class)); } } diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/ability-create-retrieve.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/ability-create-retrieve.feature index 4af28673a..f8aae908c 100644 --- a/backend/src/acceptanceTest/resources/wooteco/prolog/ability-create-retrieve.feature +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/ability-create-retrieve.feature @@ -42,4 +42,4 @@ Feature: 역량 기능 And 부모역량 "프로그래밍"을 추가하고 And "프로그래밍"의 자식역량 "언어"를 추가하고 When "브라운"의 역량 목록을 조회하면 - Then 역량 목록을 받는다. \ No newline at end of file + Then 역량 목록을 받는다. diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/ability-studylog.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/ability-studylog.feature index 7d931b538..b1e4a73c7 100644 --- a/backend/src/acceptanceTest/resources/wooteco/prolog/ability-studylog.feature +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/ability-studylog.feature @@ -37,4 +37,15 @@ Feature: 역량 학습로그 맵핑 기능 Given "또다른 스터디로그" 스터디로그를 작성하고 And "새로운 스터디로그" 학습로그에 "프로그래밍", "TDD" 역량을 맵핑하고 When "브라운"이 작성한 역량이 맵핑된 학습로그를 조회하면 - Then "프로그래밍", "TDD" 역량이 맵핑된 "새로운 스터디로그" 학습로그가 조회된다 \ No newline at end of file + Then "프로그래밍", "TDD" 역량이 맵핑된 "새로운 스터디로그" 학습로그가 조회된다 + + Scenario: 스터디로그 작성시 역량 맵핑하기 + Given 2, 4 역량을 맵핑한 "더새로운 스터디로그" 스터디로그를 작성하고 + When "브라운"이 작성한 역량이 맵핑된 학습로그를 조회하면 + Then "언어", "TDD" 역량이 맵핑된 "더새로운 스터디로그" 학습로그가 조회된다 + + Scenario: 스터디로그 수정시 역량 맵핑하기 + Given 2 역량 한개를 맵핑한 "[알고리즘] 알고리즘은 어려워요" 스터디로그를 작성하고 + When 2번째 스터디로그의 역량을 수정하면 + When "브라운"이 작성한 역량이 맵핑된 학습로그를 조회하면 + Then "TDD" 역량 하나가 맵핑된 "[알고리즘] 알고리즘은 어려워요" 학습로그가 조회된다 diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/comment.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/comment.feature new file mode 100644 index 000000000..7425ea5e0 --- /dev/null +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/comment.feature @@ -0,0 +1,29 @@ +@api +Feature: 댓글 관련 기능 + + Background: 사전 작업 + Given "브라운"이 로그인을 하고 + And 세션 여러개를 생성하고 + And 미션 여러개를 생성하고 + And 스터디로그를 작성하면 + + Scenario: 댓글 작성하기 + When 1번 스터디로그에 대한 댓글을 작성하면 + Then 댓글이 작성된다 + + Scenario: 스터디로그의 댓글 목록 조회하기 + Given 1번 스터디로그에 대한 댓글을 작성하고 + And "웨지"가 로그인을 하고 + And 1번 스터디로그에 대한 댓글을 작성하고 + When 1번 스터디로그의 댓글을 조회하면 + Then 해당 스터디로그의 댓글 목록을 조회한다 + + Scenario: 댓글 수정하기 + Given 1번 스터디로그에 대한 댓글을 작성하고 + When 1번 스터디로그에 대한 1번 댓글을 수정하면 + Then 댓글이 수정된다 + + Scenario: 댓글 삭제하기 + Given 1번 스터디로그에 대한 댓글을 작성하고 + When 1번 스터디로그에 대한 1번 댓글을 삭제하면 + Then 댓글이 삭제된다 diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/levellogs.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/levellogs.feature new file mode 100644 index 000000000..6577ceee4 --- /dev/null +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/levellogs.feature @@ -0,0 +1,25 @@ +@api +Feature: 레벨 로그 관련 기능 + + Background: 사전 작업 + Given "브라운"이 로그인을 하고 + + Scenario: 레벨 로그 작성하기 + When 레벨로그를 작성하면 + Then 레벨로그가 조회된다 + + Scenario: 레벨 로그 삭제하기 + When 레벨로그를 작성하고 + And 레벨로그를 삭제하면 + Then 레벨로그가 삭제된다 + + Scenario: 레벨 로그 수정하기 + When 레벨로그를 작성하고 + And 레벨로그를 수정하면 + Then 레벨로그가 수정된다 + + + Scenario: 레벨 로그 목록 조회하기 + When 레벨로그를 여러개 작성하면 + Then 레벨로그가 여러개 조회된다 + diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/report.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/report.feature index d407f40da..eed8ebf99 100644 --- a/backend/src/acceptanceTest/resources/wooteco/prolog/report.feature +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/report.feature @@ -51,6 +51,10 @@ Feature: 리포트 기능 When 리포트 목록을 조회하면 Then 최신순으로 정렬되어 반환된다 + Scenario: 기간에 따라 학습로그 조회하기 + When 지난 일주일부터 오늘까지의 학습로그를 조회하면 + Then 해당 유저의 해당 기간 스터디로그 목록이 조회된다 + # Scenario: 단순 리포트 조회하기 # And 리포트를 등록하고 # When "브라운"의 단순 리포트 목록을 조회하면 diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/sessionmember.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/sessionmember.feature index 715941883..2e5131f9b 100644 --- a/backend/src/acceptanceTest/resources/wooteco/prolog/sessionmember.feature +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/sessionmember.feature @@ -5,3 +5,10 @@ Feature: 세션 멤버 기능 Given "강의1" 추가하면 When 1 강의에 자신을 등록하면 Then 강의에 내가 추가된다. + + Scenario: 강의에서 자신을 제거하는 기능 + Given "브라운" 이 로그인을 하공 + Given "강의1" 추가하면 + Given 1 강의에 자신을 등록하고 + When 1 강의에서 자신을 제거하면 + Then 강의에서 내가 제거된다. diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/studylog-popular.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/studylog-popular.feature new file mode 100644 index 000000000..bf4997a72 --- /dev/null +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/studylog-popular.feature @@ -0,0 +1,14 @@ +@api +Feature: 인기있는 학습로그 관련 기능 + + Background: 사전 작업 + Given 세션 여러개를 생성하고 + And 미션 여러개를 생성하고 + And "브라운"이 로그인을 하고 + And "브라운"을 멤버그룹과 그룹멤버에 등록하고 + + Scenario: 인기 있는 순서로 스터디로그 목록 조회하기 + Given 스터디로그 여러개를 작성하고 + When 로그인된 사용자가 2번째 스터디로그를 좋아요 하고 + When 인기 있는 스터디로그 목록을 "2"개만큼 갱신하고 + Then 인기있는 스터디로그 목록 요청시 id "2, 1" 순서로 조회된다 diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/studylog.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/studylog.feature index 8761f453d..e0d1d0056 100644 --- a/backend/src/acceptanceTest/resources/wooteco/prolog/studylog.feature +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/studylog.feature @@ -160,8 +160,8 @@ Feature: 스터디로그 관련 기능 When 로그인된 사용자가 1번째 스터디로그를 조회하면 Then 조회된 스터디로그의 좋아요 여부가 거짓이다 - Scenario: 인기 있는 순서로 스터디로그 목록 조회하기 - Given 스터디로그 여러개를 작성하고 - When 로그인된 사용자가 2번째 스터디로그를 좋아요 하고 - When 인기 있는 스터디로그 목록을 "2"개만큼 갱신하고 - Then 인기있는 스터디로그 목록 요청시 id "2, 1" 순서로 조회된다 +# Scenario: 인기 있는 순서로 스터디로그 목록 조회하기 +# Given 스터디로그 여러개를 작성하고 +# When 로그인된 사용자가 2번째 스터디로그를 좋아요 하고 +# When 인기 있는 스터디로그 목록을 "2"개만큼 갱신하고 +# Then 인기있는 스터디로그 목록 요청시 id "2, 1" 순서로 조회된다 diff --git a/backend/src/documentation/adoc/comment.adoc b/backend/src/documentation/adoc/comment.adoc new file mode 100644 index 000000000..942c8dc3a --- /dev/null +++ b/backend/src/documentation/adoc/comment.adoc @@ -0,0 +1,42 @@ +[[comment]] +== 댓글 + +=== 댓글 등록 + +==== Request + +include::{snippets}/comment/create/http-request.adoc[] + +==== Response + +include::{snippets}/comment/create/http-response.adoc[] + +=== 댓글 전체 조회 + +==== Request + +include::{snippets}/comment/showAll/http-request.adoc[] + +==== Response + +include::{snippets}/comment/showAll/http-response.adoc[] + +=== 댓글 수정 + +==== Request + +include::{snippets}/comment/update/http-request.adoc[] + +==== Response + +include::{snippets}/comment/update/http-response.adoc[] + +=== 댓글 삭제 + +==== Request + +include::{snippets}/comment/delete/http-request.adoc[] + +==== Response + +include::{snippets}/comment/delete/http-response.adoc[] diff --git a/backend/src/documentation/adoc/index.adoc b/backend/src/documentation/adoc/index.adoc index 33d32e577..48c415759 100644 --- a/backend/src/documentation/adoc/index.adoc +++ b/backend/src/documentation/adoc/index.adoc @@ -10,6 +10,7 @@ include::login.adoc[] include::profile.adoc[] include::session.adoc[] include::studylog.adoc[] +include::comment.adoc[] include::mission.adoc[] include::tag.adoc[] include::filter.adoc[] diff --git a/backend/src/documentation/java/wooteco/prolog/docu/CommentDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/CommentDocumentation.java new file mode 100644 index 000000000..4faac6525 --- /dev/null +++ b/backend/src/documentation/java/wooteco/prolog/docu/CommentDocumentation.java @@ -0,0 +1,186 @@ +package wooteco.prolog.docu; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import wooteco.prolog.Documentation; +import wooteco.prolog.GithubResponses; +import wooteco.prolog.session.application.dto.MissionRequest; +import wooteco.prolog.session.application.dto.MissionResponse; +import wooteco.prolog.session.application.dto.SessionRequest; +import wooteco.prolog.session.application.dto.SessionResponse; +import wooteco.prolog.studylog.application.dto.CommentChangeRequest; +import wooteco.prolog.studylog.application.dto.CommentCreateRequest; +import wooteco.prolog.studylog.application.dto.CommentMemberResponse; +import wooteco.prolog.studylog.application.dto.CommentResponse; +import wooteco.prolog.studylog.application.dto.CommentsResponse; +import wooteco.prolog.studylog.application.dto.StudylogRequest; +import wooteco.prolog.studylog.application.dto.TagRequest; + +public class CommentDocumentation extends Documentation { + + @Test + void 댓글을_등록한다() { + // given + Long studylogId = 스터디로그_등록함(createStudylogRequest()); + + // when + ExtractableResponse extract = given("comment/create") + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .body(createCommentRequest()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/studylogs/" + studylogId + "/comments") + .then().log().all().extract(); + + // then + assertThat(extract.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + assertThat(extract.header("Location")).isNotNull(); + } + + @Test + void 단일_스터디로그에_대한_댓글을_조회한다() { + // given + Long studylogId = 스터디로그_등록함(createStudylogRequest()); + 댓글_등록_성공되어_있음(studylogId, createCommentRequest()); + + // when + ExtractableResponse extract = given("comment/showAll") + .when().get("/studylogs/" + studylogId + "/comments") + .then().log().all().extract(); + + // then + CommentsResponse commentsResponse = extract.as(CommentsResponse.class); + assertThat(extract.statusCode()).isEqualTo(HttpStatus.OK.value()); + assertThat(commentsResponse.getData()) + .usingRecursiveComparison() + .ignoringFields("createAt").isEqualTo(org.elasticsearch.common.collect.List.of( + new CommentResponse(1L, + new CommentMemberResponse(1L, GithubResponses.소롱.getLogin(), + GithubResponses.소롱.getName(), + GithubResponses.소롱.getAvatarUrl(), "CREW"), "댓글의 내용입니다.", null) + )); + } + + @Test + void 댓글을_수정한다() { + // given + Long studylogId = 스터디로그_등록함(createStudylogRequest()); + Long commentId = 댓글_등록_성공되어_있음(studylogId, createCommentRequest()); + + // when + ExtractableResponse extract = given("comment/update") + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .body(updateCommentRequest()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().put("/studylogs/" + studylogId + "/comments/" + commentId) + .then().log().all().extract(); + + //then + assertThat(extract.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + + ExtractableResponse findExtract = 댓글_조회함(studylogId); + CommentsResponse commentsResponse = findExtract.as(CommentsResponse.class); + CommentResponse commentResponse = commentsResponse.getData().get(0); + + assertThat(commentResponse.getContent()).isEqualTo(updateCommentRequest().getContent()); + } + + @Test + void 댓글을_삭제한다() { + // given + Long studylogId = 스터디로그_등록함(createStudylogRequest()); + Long commentId = 댓글_등록_성공되어_있음(studylogId, createCommentRequest()); + + // when + ExtractableResponse extract = given("comment/delete") + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .when().delete("/studylogs/" + studylogId + "/comments/" + commentId) + .then().log().all().extract(); + + //then + assertThat(extract.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + private StudylogRequest createStudylogRequest() { + String title = "스터디로그 제목"; + String content = "스터디로그에 본문 내용임.\n" + "여기에 글을 작성할 수 있음\n"; + Long sessionId = 세션_등록함(new SessionRequest("백엔드Java 레벨1 - 2022")); + Long missionId = 미션_등록함(new MissionRequest("[BE] 레벨1 - 미션이름", sessionId)); + List tags = Arrays.asList(new TagRequest("tag")); + + return new StudylogRequest(title, content, sessionId, missionId, tags, + Collections.emptyList()); + } + + private CommentCreateRequest createCommentRequest() { + return new CommentCreateRequest("댓글의 내용입니다."); + } + + private CommentChangeRequest updateCommentRequest() { + return new CommentChangeRequest("수정된 댓글의 내용입니다."); + } + + private Long 스터디로그_등록함(StudylogRequest request) { + ExtractableResponse extract = RestAssured.given().log().all() + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .body(request) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().log().all() + .post("/studylogs") + .then().log().all().extract(); + + return Long.parseLong(extract.header("Location").split("/studylogs/")[1]); + } + + private ExtractableResponse 댓글_등록함(Long studylogId, CommentCreateRequest request) { + return RestAssured.given().log().all() + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .body(request) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().log().all() + .post("/studylogs/" + studylogId + "/comments") + .then().log().all().extract(); + } + + private Long 댓글_등록_성공되어_있음(Long studylogId, CommentCreateRequest request) { + ExtractableResponse response = 댓글_등록함(studylogId, request); + + String commentId = response.header("Location").split("/comments/")[1]; + assertThat(commentId).isNotNull(); + return Long.parseLong(commentId); + } + + private ExtractableResponse 댓글_조회함(Long studylogId) { + return RestAssured.given().log().all() + .when().get("/studylogs/" + studylogId + "/comments") + .then().log().all().extract(); + } + + private Long 세션_등록함(SessionRequest request) { + return RestAssured.given().log().all() + .body(request) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/sessions") + .then().log().all() + .extract().as(SessionResponse.class).getId(); + } + + private Long 미션_등록함(MissionRequest request) { + return RestAssured.given() + .body(request) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/missions") + .then().log().all() + .extract().as(MissionResponse.class).getId(); + } +} diff --git a/backend/src/documentation/java/wooteco/prolog/docu/MemberDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/MemberDocumentation.java index 84ae14892..9dc4bb599 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/MemberDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/MemberDocumentation.java @@ -16,10 +16,10 @@ import wooteco.prolog.GithubResponses; import wooteco.prolog.member.application.dto.MemberScrapRequest; import wooteco.prolog.member.application.dto.MemberUpdateRequest; -import wooteco.prolog.session.application.dto.SessionRequest; -import wooteco.prolog.session.application.dto.SessionResponse; import wooteco.prolog.session.application.dto.MissionRequest; import wooteco.prolog.session.application.dto.MissionResponse; +import wooteco.prolog.session.application.dto.SessionRequest; +import wooteco.prolog.session.application.dto.SessionResponse; import wooteco.prolog.studylog.application.dto.StudylogRequest; import wooteco.prolog.studylog.application.dto.StudylogsResponse; import wooteco.prolog.studylog.application.dto.TagRequest; @@ -81,7 +81,7 @@ public class MemberDocumentation extends Documentation { .when().get(scrapedLogLocation) .then().log().all() .assertThat() - .body("studylogResponse.id", equalTo(Integer.parseInt(logId))); + .body("id", equalTo(Integer.parseInt(logId))); } @Test diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ReportDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ReportDocumentation.java index fc0349ffe..c32ddc4b4 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/ReportDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/ReportDocumentation.java @@ -133,6 +133,7 @@ public class ReportDocumentation extends NewDocumentation { "제목", "내용내용내용내용내용", Lists.newArrayList(new TagResponse(1L, "태그1"), new TagResponse(2L, "태그2")), + Collections.emptyList(), false, false, 10, diff --git a/backend/src/documentation/java/wooteco/prolog/docu/SessionMemberDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/SessionMemberDocumentation.java index 91490e46a..2b4df1fc5 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/SessionMemberDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/SessionMemberDocumentation.java @@ -5,12 +5,12 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import java.util.Map; -import javax.persistence.DiscriminatorValue; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import wooteco.prolog.Documentation; import wooteco.prolog.session.application.dto.SessionRequest; +import wooteco.prolog.session.application.dto.SessionResponse; public class SessionMemberDocumentation extends Documentation { @@ -34,4 +34,42 @@ public class SessionMemberDocumentation extends Documentation { //then assertThat(createResponse2.statusCode()).isEqualTo(HttpStatus.OK.value()); } + + @Test + void 강의에서_자신을_제거한다() { + // given + SessionResponse sessionResponse = createSessionWithToken(); + createSessionMemberWithToken(sessionResponse.getId()); + + // when + ExtractableResponse response = given("session") + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().delete("/sessions/" + sessionResponse.getId() + "/members/me") + .then().log().all().extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + private SessionResponse createSessionWithToken() { + SessionRequest sessionRequest = new SessionRequest("새강의"); + + ExtractableResponse createResponse = given("session") + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(sessionRequest) + .when().post("/sessions") + .then().log().all().extract(); + + return createResponse.as(SessionResponse.class); + } + + private void createSessionMemberWithToken(Long sessionId) { + given("session") + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().post("/sessions/" + sessionId + "/members/me") + .then().log().all().extract(); + } } diff --git a/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java index 1f5fc08ac..bdca9cc35 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/StudylogDocumentation.java @@ -10,10 +10,20 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import wooteco.prolog.Documentation; +import wooteco.prolog.ability.application.dto.AbilityCreateRequest; +import wooteco.prolog.ability.application.dto.AbilityResponse; +import wooteco.prolog.member.domain.GroupMember; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.MemberGroup; +import wooteco.prolog.member.domain.repository.GroupMemberRepository; +import wooteco.prolog.member.domain.repository.MemberGroupRepository; +import wooteco.prolog.member.domain.repository.MemberRepository; import wooteco.prolog.session.application.dto.MissionRequest; import wooteco.prolog.session.application.dto.MissionResponse; import wooteco.prolog.session.application.dto.SessionRequest; @@ -26,6 +36,13 @@ class StudylogDocumentation extends Documentation { + @Autowired + private MemberRepository memberRepository; + @Autowired + private MemberGroupRepository memberGroupRepository; + @Autowired + private GroupMemberRepository groupMemberRepository; + @Test void 스터디로그를_생성한다() { // when @@ -100,6 +117,7 @@ class StudylogDocumentation extends Documentation { @Test void 인기있는_스터디로그_목록을_갱신하고_조회한다() { // given + 회원과_멤버그룹_그룹멤버를_등록함(); String studylogLocation1 = 스터디로그_등록함(createStudylogRequest1()).header("Location"); String studylogLocation2 = 스터디로그_등록함(createStudylogRequest2()).header("Location"); Long studylogId1 = Long.parseLong(studylogLocation1.split("/studylogs/")[1]); @@ -118,15 +136,12 @@ class StudylogDocumentation extends Documentation { .then().log().all().extract(); // then - PopularStudylogsResponse popularStudylogsResponse = response.as( - PopularStudylogsResponse.class); + PopularStudylogsResponse popularStudylogsResponse = response.as(PopularStudylogsResponse.class); assertThat(popularStudylogsResponse.getAllResponse().getData()).hasSize(2); - assertThat(popularStudylogsResponse.getAllResponse().getData().get(0).getStudylogResponse() - .getId()).isEqualTo(studylogId2); - assertThat(popularStudylogsResponse.getAllResponse().getData().get(1).getStudylogResponse() - .getId()).isEqualTo(studylogId1); - } + assertThat(popularStudylogsResponse.getFrontResponse().getData()).hasSize(2); + assertThat(popularStudylogsResponse.getBackResponse().getData()).hasSize(2); + } private void 인기있는_스터디로그_목록_갱신(int studylogCount) { RestAssured.given() @@ -209,8 +224,19 @@ private StudylogRequest createStudylogRequest1() { Long sessionId = 세션_등록함(new SessionRequest("프론트엔드JS 레벨1 - 2021")); Long missionId = 미션_등록함(new MissionRequest("세션1 - 지하철 노선도 미션", sessionId)); List tags = Arrays.asList(new TagRequest("spa"), new TagRequest("router")); - - return new StudylogRequest(title, content, sessionId, missionId, tags); + Long parentAbilityId = 역량_등록함(new AbilityCreateRequest( + "부모 역량1", + "부모 역량1입니다", + "#ffffff", + null)); + Long abilityId = 역량_등록함(new AbilityCreateRequest( + "자식 역량1", + "자식 역량1입니다", + "#ffffff", + parentAbilityId)); + + return new StudylogRequest(title, content, sessionId, missionId, tags, + Arrays.asList(abilityId)); } private StudylogRequest createStudylogRequest2() { @@ -219,8 +245,33 @@ private StudylogRequest createStudylogRequest2() { Long sessionId = 세션_등록함(new SessionRequest("백엔드Java 레벨1 - 2021")); Long missionId = 미션_등록함(new MissionRequest("세션3 - 프로젝트", sessionId)); List tags = Arrays.asList(new TagRequest("java"), new TagRequest("jpa")); + Long parentAbilityId = 역량_등록함(new AbilityCreateRequest( + "부모 역량2", + "부모 역량2입니다", + "#000000", + null)); + Long abilityId = 역량_등록함(new AbilityCreateRequest( + "자식 역량2", + "자식 역량2입니다", + "#000000", + parentAbilityId)); + + return new StudylogRequest(title, content, sessionId, missionId, tags, + Arrays.asList(abilityId)); + } - return new StudylogRequest(title, content, sessionId, missionId, tags); + private Long 역량_등록함(AbilityCreateRequest request) { + return RestAssured.given().log().all() + .header("Authorization", "Bearer " + 로그인_사용자.getAccessToken()) + .body(request) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/abilities") + .then() + .log().all() + .extract() + .as(AbilityResponse.class) + .getId(); } private ExtractableResponse 스터디로그_등록함(StudylogRequest request) { @@ -247,6 +298,15 @@ private StudylogRequest createStudylogRequest2() { .getId(); } + private void 회원과_멤버그룹_그룹멤버를_등록함() { + Member member = memberRepository.findById(1L).get(); + MemberGroup 프론트엔드 = memberGroupRepository.save( + new MemberGroup(null, "4기 프론트엔드", "4기 프론트엔드 설명")); + MemberGroup 백엔드 = memberGroupRepository.save(new MemberGroup(null, "4기 백엔드", "4기 백엔드 설명")); + groupMemberRepository.save(new GroupMember(null, member, 백엔드)); + groupMemberRepository.save(new GroupMember(null, member, 프론트엔드)); + } + private Long 미션_등록함(MissionRequest request) { return RestAssured.given() .body(request) diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ability/StudylogAbilityDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ability/StudylogAbilityDocumentation.java index 95f65adcf..b800cf76d 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/ability/StudylogAbilityDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/ability/StudylogAbilityDocumentation.java @@ -7,6 +7,7 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; @@ -85,6 +86,7 @@ public class StudylogAbilityDocumentation extends NewDocumentation { "제목", "내용내용내용내용내용", Lists.newArrayList(new TagResponse(1L, "태그1"), new TagResponse(2L, "태그2")), + Collections.emptyList(), false, false, 10, diff --git a/backend/src/main/java/wooteco/prolog/DataLoaderApplicationListener.java b/backend/src/main/java/wooteco/prolog/DataLoaderApplicationListener.java index e285e5da4..b0fc45b0b 100644 --- a/backend/src/main/java/wooteco/prolog/DataLoaderApplicationListener.java +++ b/backend/src/main/java/wooteco/prolog/DataLoaderApplicationListener.java @@ -1,30 +1,38 @@ package wooteco.prolog; +import static wooteco.prolog.DataLoaderApplicationListener.Members.BROWN; +import static wooteco.prolog.DataLoaderApplicationListener.Members.SUNNY; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; -import lombok.AllArgsConstructor; + import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.data.domain.PageRequest; import org.testcontainers.shaded.com.google.common.collect.Lists; + +import lombok.AllArgsConstructor; import wooteco.prolog.ability.application.AbilityService; import wooteco.prolog.ability.application.dto.DefaultAbilityCreateRequest; +import wooteco.prolog.levellogs.application.LevelLogService; +import wooteco.prolog.levellogs.application.dto.LevelLogRequest; +import wooteco.prolog.levellogs.application.dto.SelfDiscussionRequest; import wooteco.prolog.login.application.dto.GithubProfileResponse; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.domain.Member; +import wooteco.prolog.session.application.MissionService; import wooteco.prolog.session.application.SessionMemberService; import wooteco.prolog.session.application.SessionService; -import wooteco.prolog.session.application.MissionService; +import wooteco.prolog.session.application.dto.MissionRequest; +import wooteco.prolog.session.application.dto.MissionResponse; import wooteco.prolog.session.application.dto.SessionMemberRequest; import wooteco.prolog.session.application.dto.SessionRequest; import wooteco.prolog.session.application.dto.SessionResponse; -import wooteco.prolog.session.application.dto.MissionRequest; -import wooteco.prolog.session.application.dto.MissionResponse; import wooteco.prolog.studylog.application.DocumentService; import wooteco.prolog.studylog.application.PopularStudylogService; import wooteco.prolog.studylog.application.StudylogService; @@ -39,266 +47,333 @@ @AllArgsConstructor @Configuration public class DataLoaderApplicationListener implements - ApplicationListener { - - private SessionService sessionService; - private SessionMemberService sessionMemberService; - private MissionService missionService; - private TagService tagService; - private MemberService memberService; - private StudylogService studylogService; - private DocumentService studylogDocumentService; - private AbilityService abilityService; - private UpdatedContentsRepository updatedContentsRepository; - private PopularStudylogService popularStudylogService; - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - studylogDocumentService.deleteAll(); - - // session init - Sessions.init(sessionService); - - // mission init - Missions.init(missionService); - - // filter init - TagRequests.init(tagService); - - // member init - Members.init(memberService); - - sessionMemberService.registerMembers(1L, new SessionMemberRequest(Lists.newArrayList(Members.BROWN.value.getId(), Members.SUNNY.value.getId()))); - - // post init - studylogService.insertStudylogs(Members.BROWN.value.getId(), StudylogGenerator.generate(20)); - studylogService.insertStudylogs(Members.JOANNE.value.getId(), StudylogGenerator.generate(20)); - studylogService.insertStudylogs(Members.TYCHE.value.getId(), StudylogGenerator.generate(100)); - studylogService.insertStudylogs(Members.SUNNY.value.getId(), StudylogGenerator.generate(20)); - - // defaultAbility init - Long csId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("CS", "CS 입니다.", "#9100ff", "common")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Database", "Database 입니다.", "#9100ff", "common", csId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("네트워크", "네트워크 입니다.", "#9100ff", "common", csId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("OS", "OS 입니다.", "#9100ff", "common", csId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("알고리즘", "알고리즘 입니다.", "#9100ff", "common", csId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("자료구조", "자료구조 입니다.", "#9100ff", "common", csId)); - - Long programmingId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Programming", "Programming 입니다.", "#ff9100", "be")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Language", "Language 입니다.", "#ff9100", "be", programmingId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Framework", "Framework 입니다.", "#ff9100", "be", programmingId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Testing", "Testing 입니다.", "#ff9100", "be", programmingId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Web Programming", "Web Programming 입니다.", "#ff9100", "be", programmingId)); - - Long designId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Design", "Design 입니다.", "#00cccc", "be")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Development Driven", "Development Driven 입니다.", "#00cccc", "be", designId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Programming Principle", "Programming Principle 입니다.", "#00cccc", "be", designId)); - - Long infrastructureId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Infrastructure", "Infrastructure 입니다.", "#ccccff", "be")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Web Architecture Components", "Web Architecture Components 입니다.", "#ccccff", "be", infrastructureId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Service & Tools", "Service & Tools 입니다.", "#ccccff", "be", infrastructureId)); - - Long developmentId = abilityService.createDefaultAbility( - new DefaultAbilityCreateRequest("Software Development Process & Maintenance", "Software Development Process & Maintenance 입니다.", "#ffcce5", "be")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Development Process", "Development Process 입니다.", "#ffcce5", "be", developmentId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Maintenance", "Maintenance 입니다.", "#ffcce5", "be", developmentId)); - - Long jsHtmlId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("JavaScript & HTML", "JavaScript & HTML 입니다.", "#ff009a", "fe")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("기초 언어 시스템", "기초 언어 시스템 입니다.", "#ff009a", "fe", jsHtmlId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("고급 언어 시스템", "고급 언어 시스템 입니다.", "#ff009a", "fe", jsHtmlId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("브라우저", "브라우저 입니다.", "#ff009a", "fe", jsHtmlId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("비동기&데이터 처리", "비동기&데이터 처리 입니다.", "#ff009a", "fe", jsHtmlId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("성능", "성능 입니다.", "#ff009a", "fe", jsHtmlId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("HTML", "HTML 입니다.", "#ff009a", "fe", jsHtmlId)); - - Long graphicsId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Graphics", "Graphics 입니다.", "#2f8aff", "fe")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("CSS 스타일링: 레이아웃과 포지셔닝", "CSS 스타일링: 레이아웃과 포지셔닝 입니다.", "#2f8aff", "fe", graphicsId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("트랜지션, 애니메이션", "트랜지션, 애니메이션 입니다.", "#2f8aff", "fe", graphicsId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("2D 그래픽스", "2D 그래픽스 입니다.", "#2f8aff", "fe", graphicsId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("3D 그래픽스", "3D 그래픽스 입니다.", "#2f8aff", "fe", graphicsId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Vector 그래픽스", "Vector 그래픽스 입니다.", "#2f8aff", "fe", graphicsId)); - - Long architectureId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("Architecture", "Architecture 입니다.", "#e5ffcc", "fe")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("인터랙션 디자인 이론", "인터랙션 디자인 이론 입니다.", "#e5ffcc", "fe", architectureId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("UI/UX 설계", "UI/UX 설계 입니다.", "#e5ffcc", "fe", architectureId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("UX 일반 이론", "UX 일반 이론 입니다.", "#e5ffcc", "fe", architectureId)); - - Long uiuxId = abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("UI/UX", "UI/UX 입니다.", "#2fff6e", "fe")); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("웹애플리케이션 구현 시 고려해야 하는 설계", "웹애플리케이션 구현 시 고려해야 하는 설계 입니다.", "#2fff6e", "fe", uiuxId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("프로젝트 개발 환경 설계", "프로젝트 개발 환경 설계 입니다.", "#2fff6e", "fe", uiuxId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("테스트와 프로젝트 배포", "테스트와 프로젝트 배포 입니다.", "#2fff6e", "fe", uiuxId)); - abilityService.createDefaultAbility(new DefaultAbilityCreateRequest("프로그래밍 테크닉, 개발 프랙티스, 개발 방법론", "프로그래밍 테크닉, 개발 프랙티스, 개발 방법론 입니다.", "#2fff6e", "fe", uiuxId)); - - // ability init -// abilityService.addDefaultAbilities(Members.BROWN.value.getId(), "be"); - abilityService.applyDefaultAbilities(Members.JOANNE.value.getId(), "be"); - abilityService.applyDefaultAbilities(Members.SUNNY.value.getId(), "fe"); - - updatedContentsRepository.save(new UpdatedContents(null, UpdateContent.MEMBER_TAG_UPDATE, 1)); - PageRequest pageRequest = PageRequest.of(0, 10); - popularStudylogService.updatePopularStudylogs(pageRequest); + ApplicationListener { + + private SessionService sessionService; + private SessionMemberService sessionMemberService; + private MissionService missionService; + private TagService tagService; + private MemberService memberService; + private StudylogService studylogService; + private DocumentService studylogDocumentService; + private AbilityService abilityService; + private UpdatedContentsRepository updatedContentsRepository; + private PopularStudylogService popularStudylogService; + private LevelLogService levelLogService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + studylogDocumentService.deleteAll(); + + // session init + Sessions.init(sessionService); + + // mission init + Missions.init(missionService); + + // filter init + TagRequests.init(tagService); + + // member init + Members.init(memberService); + + sessionMemberService.registerMembers(1L, + new SessionMemberRequest(Lists.newArrayList(BROWN.value.getId(), Members.SUNNY.value.getId()))); + + // post init + studylogService.insertStudylogs(BROWN.value.getId(), StudylogGenerator.generate(20)); + studylogService.insertStudylogs(Members.JOANNE.value.getId(), StudylogGenerator.generate(20)); + studylogService.insertStudylogs(Members.TYCHE.value.getId(), StudylogGenerator.generate(100)); + studylogService.insertStudylogs(Members.SUNNY.value.getId(), StudylogGenerator.generate(20)); + + // defaultAbility init + Long csId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("CS", "CS 입니다.", "#9100ff", "common")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Database", "Database 입니다.", "#9100ff", "common", csId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("네트워크", "네트워크 입니다.", "#9100ff", "common", csId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("OS", "OS 입니다.", "#9100ff", "common", csId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("알고리즘", "알고리즘 입니다.", "#9100ff", "common", csId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("자료구조", "자료구조 입니다.", "#9100ff", "common", csId)); + + Long programmingId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Programming", "Programming 입니다.", "#ff9100", "be")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Language", "Language 입니다.", "#ff9100", "be", programmingId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Framework", "Framework 입니다.", "#ff9100", "be", programmingId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Testing", "Testing 입니다.", "#ff9100", "be", programmingId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Web Programming", "Web Programming 입니다.", "#ff9100", "be", programmingId)); + + Long designId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Design", "Design 입니다.", "#00cccc", "be")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Development Driven", "Development Driven 입니다.", "#00cccc", "be", + designId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Programming Principle", "Programming Principle 입니다.", "#00cccc", "be", + designId)); + + Long infrastructureId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Infrastructure", "Infrastructure 입니다.", "#ccccff", "be")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Web Architecture Components", "Web Architecture Components 입니다.", + "#ccccff", "be", infrastructureId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Service & Tools", "Service & Tools 입니다.", "#ccccff", "be", + infrastructureId)); + + Long developmentId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Software Development Process & Maintenance", + "Software Development Process & Maintenance 입니다.", "#ffcce5", "be")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Development Process", "Development Process 입니다.", "#ffcce5", "be", + developmentId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Maintenance", "Maintenance 입니다.", "#ffcce5", "be", developmentId)); + + Long jsHtmlId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("JavaScript & HTML", "JavaScript & HTML 입니다.", "#ff009a", "fe")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("기초 언어 시스템", "기초 언어 시스템 입니다.", "#ff009a", "fe", jsHtmlId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("고급 언어 시스템", "고급 언어 시스템 입니다.", "#ff009a", "fe", jsHtmlId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("브라우저", "브라우저 입니다.", "#ff009a", "fe", jsHtmlId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("비동기&데이터 처리", "비동기&데이터 처리 입니다.", "#ff009a", "fe", jsHtmlId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("성능", "성능 입니다.", "#ff009a", "fe", jsHtmlId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("HTML", "HTML 입니다.", "#ff009a", "fe", jsHtmlId)); + + Long graphicsId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Graphics", "Graphics 입니다.", "#2f8aff", "fe")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("CSS 스타일링: 레이아웃과 포지셔닝", "CSS 스타일링: 레이아웃과 포지셔닝 입니다.", "#2f8aff", "fe", + graphicsId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("트랜지션, 애니메이션", "트랜지션, 애니메이션 입니다.", "#2f8aff", "fe", graphicsId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("2D 그래픽스", "2D 그래픽스 입니다.", "#2f8aff", "fe", graphicsId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("3D 그래픽스", "3D 그래픽스 입니다.", "#2f8aff", "fe", graphicsId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Vector 그래픽스", "Vector 그래픽스 입니다.", "#2f8aff", "fe", graphicsId)); + + Long architectureId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("Architecture", "Architecture 입니다.", "#e5ffcc", "fe")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("인터랙션 디자인 이론", "인터랙션 디자인 이론 입니다.", "#e5ffcc", "fe", architectureId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("UI/UX 설계", "UI/UX 설계 입니다.", "#e5ffcc", "fe", architectureId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("UX 일반 이론", "UX 일반 이론 입니다.", "#e5ffcc", "fe", architectureId)); + + Long uiuxId = abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("UI/UX", "UI/UX 입니다.", "#2fff6e", "fe")); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("웹애플리케이션 구현 시 고려해야 하는 설계", "웹애플리케이션 구현 시 고려해야 하는 설계 입니다.", "#2fff6e", "fe", + uiuxId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("프로젝트 개발 환경 설계", "프로젝트 개발 환경 설계 입니다.", "#2fff6e", "fe", uiuxId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("테스트와 프로젝트 배포", "테스트와 프로젝트 배포 입니다.", "#2fff6e", "fe", uiuxId)); + abilityService.createDefaultAbility( + new DefaultAbilityCreateRequest("프로그래밍 테크닉, 개발 프랙티스, 개발 방법론", "프로그래밍 테크닉, 개발 프랙티스, 개발 방법론 입니다.", "#2fff6e", + "fe", uiuxId)); + + // ability init + // abilityService.addDefaultAbilities(Members.BROWN.value.getId(), "be"); + abilityService.applyDefaultAbilities(Members.JOANNE.value.getId(), "be"); + abilityService.applyDefaultAbilities(Members.SUNNY.value.getId(), "fe"); + + updatedContentsRepository + .save(new UpdatedContents(null, UpdateContent.MEMBER_TAG_UPDATE, 1)); + PageRequest pageRequest = PageRequest.of(0, 10); + popularStudylogService.updatePopularStudylogs(pageRequest); + + // levelLog init + levelLogService.insertLevellogs(BROWN.getValue().getId(), + new LevelLogRequest("더미데이터 넣는 방법", "DataLoderApplication 에서 넣으세요.", + Arrays.asList(new SelfDiscussionRequest("초기화 하는 방법", "후이에게 부탁하기")))); + + levelLogService.insertLevellogs(SUNNY.getValue().getId(), + new LevelLogRequest("수달이 서니 이름으로 작성한 레벨로그1", "서니처럼 멋진 개발자가 되는 방법에 대해서 고민해보았습니다.", + Arrays.asList(new SelfDiscussionRequest("멋진 사람이란", "우테코 수료한사람 ~")))); + + levelLogService.insertLevellogs(SUNNY.getValue().getId(), + new LevelLogRequest("수달이 서니 이름으로 작성한 레벨로그2", "서니는 언제부터 개발자가 되고 싶었나요?", + Arrays.asList(new SelfDiscussionRequest("백엔드 개발자란?", "우테코 수료한사람 ~")))); } - private enum Sessions { - LEVEL1(new SessionRequest("백엔드Java 세션1 - 2021")), - LEVEL3(new SessionRequest("백엔드Java 세션2 - 2021")), - LEVEL2(new SessionRequest("프론트엔드JS 세션1 - 2021")), - LEVEL4(new SessionRequest("프론트엔드JS 세션2 - 2021")); - - private final SessionRequest request; - private SessionResponse response; - - Sessions(SessionRequest sessionRequest) { - this.request = sessionRequest; - } - - public static void init(SessionService sessionService) { - for (Sessions session : values()) { - session.response = sessionService.create(session.request); - } - } - - public Long getId() { - return response.getId(); - } - } - - private enum Missions { - MISSION1(new MissionRequest("자동차경주", Sessions.LEVEL1.getId())), - MISSION2(new MissionRequest("로또", Sessions.LEVEL2.getId())), - MISSION3(new MissionRequest("장바구니", Sessions.LEVEL3.getId())), - MISSION4(new MissionRequest("지하철", Sessions.LEVEL4.getId())); - - private final MissionRequest request; - private MissionResponse response; - - Missions(MissionRequest request) { - this.request = request; - } - - public static void init(MissionService missionService) { - for (Missions mission : values()) { - mission.response = missionService.create(mission.request); - } - } - - public Long getId() { - return response.getId(); - } - - } - - private enum TagRequests { - TAG_REQUESTS_1234(Arrays.asList( - new TagRequest("자바"), - new TagRequest("자바스크립트"), - new TagRequest("스프링"), - new TagRequest("리액트") - )), - TAG_REQUESTS_EMPTY(Collections.emptyList()), - TAG_REQUESTS_12(Arrays.asList( - new TagRequest("자바"), - new TagRequest("자바스크립트") - )), - TAG_REQUESTS_3(Collections.singletonList( - new TagRequest("스프링") - )); - - private static final Random random = new Random(); - - private final List value; - - TagRequests(List tagRequests) { - this.value = tagRequests; - } - - public static void init(TagService tagService) { - Arrays.asList(values()) - .forEach(tagRequests -> tagService.findOrCreate(tagRequests.value)); - } - - public static List random() { - int i = random.nextInt(values().length); - return values()[i].value; - } - } - - public enum Members { - BROWN(new GithubProfileResponse( - "류성현", - "gracefulBrown", - "46308949", - "https://avatars.githubusercontent.com/u/46308949?v=4" - )), - JOANNE(new GithubProfileResponse( - "서민정", - "seovalue", - "123456", - "https://avatars.githubusercontent.com/u/48412963?v=4" - )), - TYCHE(new GithubProfileResponse( - "티케", - "devhyun637", - "59258239", - "https://avatars.githubusercontent.com/u/59258239?v=4" - )), - SUNNY(new GithubProfileResponse( - "박선희", - "서니", - "67677561", - "https://avatars.githubusercontent.com/u/67677561?v=4" - )), - HYEON9MAK(new GithubProfileResponse( - "최현구", - "hyeon9mak", - "37354145", - "https://avatars.githubusercontent.com/u/37354145?v=4" - )); - - private final GithubProfileResponse githubProfileResponse; - private Member value; - - Members(GithubProfileResponse githubProfileResponse) { - this.githubProfileResponse = githubProfileResponse; - } - - public Member getValue() { - return value; - } - - public static void init(MemberService memberService) { - for (Members member : values()) { - member.value = memberService - .findOrCreateMember(member.githubProfileResponse); - } - } - } - - private static class StudylogGenerator { - - private static int cnt = 0; - - private StudylogGenerator() { - } - - public static List generate(int size) { - List result = new ArrayList<>(); - - for (int i = 0; i < size; i++) { - result.add(create()); - } - - return result; - } - - private static StudylogRequest create() { - return new StudylogRequest( - "페이지네이션 데이터 " + cnt, - "좋은 내용" + cnt, - Sessions.values()[cnt++ % Missions.values().length].getId(), - Missions.values()[cnt++ % Missions.values().length].getId(), - TagRequests.random() - ); - } - } + private enum Sessions { + LEVEL1(new SessionRequest("백엔드Java 세션1 - 2021")), + LEVEL3(new SessionRequest("백엔드Java 세션2 - 2021")), + LEVEL2(new SessionRequest("프론트엔드JS 세션1 - 2021")), + LEVEL4(new SessionRequest("프론트엔드JS 세션2 - 2021")); + + private final SessionRequest request; + private SessionResponse response; + + Sessions(SessionRequest sessionRequest) { + this.request = sessionRequest; + } + + public static void init(SessionService sessionService) { + for (Sessions session : values()) { + session.response = sessionService.create(session.request); + } + } + + public Long getId() { + return response.getId(); + } + } + + private enum Missions { + MISSION1(new MissionRequest("자동차경주", Sessions.LEVEL1.getId())), + MISSION2(new MissionRequest("로또", Sessions.LEVEL2.getId())), + MISSION3(new MissionRequest("장바구니", Sessions.LEVEL3.getId())), + MISSION4(new MissionRequest("지하철", Sessions.LEVEL4.getId())); + + private final MissionRequest request; + private MissionResponse response; + + Missions(MissionRequest request) { + this.request = request; + } + + public static void init(MissionService missionService) { + for (Missions mission : values()) { + mission.response = missionService.create(mission.request); + } + } + + public Long getId() { + return response.getId(); + } + + } + + private enum TagRequests { + TAG_REQUESTS_1234(Arrays.asList( + new TagRequest("자바"), + new TagRequest("자바스크립트"), + new TagRequest("스프링"), + new TagRequest("리액트") + )), + TAG_REQUESTS_EMPTY(Collections.emptyList()), + TAG_REQUESTS_12(Arrays.asList( + new TagRequest("자바"), + new TagRequest("자바스크립트") + )), + TAG_REQUESTS_3(Collections.singletonList( + new TagRequest("스프링") + )); + + private static final Random random = new Random(); + + private final List value; + + TagRequests(List tagRequests) { + this.value = tagRequests; + } + + public static void init(TagService tagService) { + Arrays.asList(values()) + .forEach(tagRequests -> tagService.findOrCreate(tagRequests.value)); + } + + public static List random() { + int i = random.nextInt(values().length); + return values()[i].value; + } + } + + public enum Members { + BROWN(new GithubProfileResponse( + "류성현", + "gracefulBrown", + "46308949", + "https://avatars.githubusercontent.com/u/46308949?v=4" + )), + JOANNE(new GithubProfileResponse( + "서민정", + "seovalue", + "123456", + "https://avatars.githubusercontent.com/u/48412963?v=4" + )), + TYCHE(new GithubProfileResponse( + "티케", + "devhyun637", + "59258239", + "https://avatars.githubusercontent.com/u/59258239?v=4" + )), + SUNNY(new GithubProfileResponse( + "박선희", + "서니", + "67677561", + "https://avatars.githubusercontent.com/u/67677561?v=4" + )), + HYEON9MAK(new GithubProfileResponse( + "최현구", + "hyeon9mak", + "37354145", + "https://avatars.githubusercontent.com/u/37354145?v=4" + )); + + private final GithubProfileResponse githubProfileResponse; + private Member value; + + Members(GithubProfileResponse githubProfileResponse) { + this.githubProfileResponse = githubProfileResponse; + } + + public Member getValue() { + return value; + } + + public static void init(MemberService memberService) { + for (Members member : values()) { + member.value = memberService + .findOrCreateMember(member.githubProfileResponse); + } + } + } + + private static class StudylogGenerator { + + private static int cnt = 0; + + private StudylogGenerator() { + } + + public static List generate(int size) { + List result = new ArrayList<>(); + + for (int i = 0; i < size; i++) { + result.add(create()); + } + + return result; + } + + private static StudylogRequest create() { + return new StudylogRequest( + "페이지네이션 데이터 " + cnt, + "좋은 내용" + cnt, + Sessions.values()[cnt++ % Missions.values().length].getId(), + Missions.values()[cnt++ % Missions.values().length].getId(), + TagRequests.random(), + Collections.emptyList() + ); + } + } } diff --git a/backend/src/main/java/wooteco/prolog/ability/application/AbilityService.java b/backend/src/main/java/wooteco/prolog/ability/application/AbilityService.java index 2ba5fa1d4..392f7f56e 100644 --- a/backend/src/main/java/wooteco/prolog/ability/application/AbilityService.java +++ b/backend/src/main/java/wooteco/prolog/ability/application/AbilityService.java @@ -2,9 +2,11 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -57,7 +59,8 @@ private Ability extractAbility(Member member, AbilityCreateRequest request) { return extractChildAbility(member, abilities, name, description, color, parentId); } - private Ability extractParentAbility(Member member, List abilities, String name, String description, String color) { + private Ability extractParentAbility(Member member, List abilities, String name, + String description, String color) { Ability parentAbility = Ability.parent(name, description, color, member); parentAbility.validateDuplicateName(abilities); @@ -66,7 +69,8 @@ private Ability extractParentAbility(Member member, List abilities, Str return parentAbility; } - private Ability extractChildAbility(Member member, List abilities, String name, String description, String color, Long parentId) { + private Ability extractChildAbility(Member member, List abilities, String name, + String description, String color, Long parentId) { Ability parentAbility = findAbilityById(parentId); Ability childAbility = Ability.child(name, description, color, parentAbility, member); @@ -94,7 +98,8 @@ public List findParentAbilitiesByMemberId(Long memberI public List findParentAbilitiesByUsername(String username) { Member member = memberService.findByUsername(username); - List parentAbilities = abilityRepository.findByMemberIdAndParentIsNull(member.getId()); + List parentAbilities = abilityRepository.findByMemberIdAndParentIsNull( + member.getId()); return HierarchyAbilityResponse.listOf(parentAbilities); } @@ -151,7 +156,8 @@ private Ability findAbilityByIdAndMemberId(Long abilityId, Long memberId) { public Long createDefaultAbility(DefaultAbilityCreateRequest request) { if (request.hasParent()) { DefaultAbility parentDefaultAbility = findDefaultAbilityById(request.getParentId()); - DefaultAbility childDefaultAbility = createChildDefaultAbility(request, parentDefaultAbility); + DefaultAbility childDefaultAbility = createChildDefaultAbility(request, + parentDefaultAbility); return childDefaultAbility.getId(); } @@ -159,7 +165,8 @@ public Long createDefaultAbility(DefaultAbilityCreateRequest request) { return defaultAbility.getId(); } - private DefaultAbility createChildDefaultAbility(DefaultAbilityCreateRequest request, DefaultAbility parentDefaultAbility) { + private DefaultAbility createChildDefaultAbility(DefaultAbilityCreateRequest request, + DefaultAbility parentDefaultAbility) { return defaultAbilityRepository.save(new DefaultAbility( request.getName(), request.getDescription(), @@ -201,7 +208,8 @@ public void applyDefaultAbilities(Long memberId, String template) { } private List findDefaultAbilitiesByTemplate(String templateType) { - List defaultAbilities = defaultAbilityRepository.findByTemplateOrTemplate(COMMON_TYPE, templateType); + List defaultAbilities = defaultAbilityRepository.findByTemplateOrTemplate( + COMMON_TYPE, templateType); if (defaultAbilities.isEmpty()) { throw new DefaultAbilityNotFoundException(); @@ -222,7 +230,8 @@ private Ability mapToParentAbility(Member member, DefaultAbility defaultAbility) return abilityRepository.save(parentAbility); } - private Ability mapToChildAbility(Member member, DefaultAbility defaultAbility, Ability parentAbility) { + private Ability mapToChildAbility(Member member, DefaultAbility defaultAbility, + Ability parentAbility) { Ability childAbility = extractChildAbility( member, findByMemberId(member.getId()), @@ -239,8 +248,8 @@ public Ability findById(Long id) { return abilityRepository.findById(id).orElseThrow(IllegalArgumentException::new); } - public List findByIdIn(Long memberId, List ids) { - List abilities = abilityRepository.findAllById(ids); + public Set findByIdIn(Long memberId, List ids) { + Set abilities = new HashSet<>(abilityRepository.findAllById(ids)); abilities.stream() .filter(it -> !it.isBelongsTo(memberId)) .findAny() @@ -249,4 +258,4 @@ public List findByIdIn(Long memberId, List ids) { }); return abilities; } -} \ No newline at end of file +} diff --git a/backend/src/main/java/wooteco/prolog/ability/application/StudylogAbilityService.java b/backend/src/main/java/wooteco/prolog/ability/application/StudylogAbilityService.java index 0af2cf0c9..7192b689e 100644 --- a/backend/src/main/java/wooteco/prolog/ability/application/StudylogAbilityService.java +++ b/backend/src/main/java/wooteco/prolog/ability/application/StudylogAbilityService.java @@ -2,6 +2,7 @@ import java.time.LocalDate; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.context.event.EventListener; import org.springframework.data.domain.Page; @@ -17,6 +18,7 @@ import wooteco.prolog.common.PageableResponse; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.domain.Member; +import wooteco.prolog.report.application.dto.StudylogPeriodRequest; import wooteco.prolog.studylog.application.StudylogService; import wooteco.prolog.studylog.domain.Studylog; import wooteco.prolog.studylog.event.StudylogDeleteEvent; @@ -40,37 +42,46 @@ public StudylogAbilityService(StudylogAbilityRepository studylogAbilityRepositor } @Transactional - public List updateStudylogAbilities(Long memberId, Long studylogId, StudylogAbilityRequest studylogAbilityRequest) { - Studylog studylog = studylogService.findStudylogById(studylogId); - studylog.validateBelongTo(memberId); + public List updateStudylogAbilities(Long memberId, Long studylogId, + StudylogAbilityRequest studylogAbilityRequest) { + Set abilities = abilityService.findByIdIn(memberId, + studylogAbilityRequest.getAbilities()); - List abilities = abilityService.findByIdIn(memberId, studylogAbilityRequest.getAbilities()); // 자식 역량이 있는데 부모 역량이 있는 경우 예외처리 - abilities.stream() - .filter(it -> !it.isParent()) - .filter(it -> abilities.contains(it.getParent())) - .findFirst() - .ifPresent(it -> { - throw new IllegalArgumentException("자식 역량이 존재하는 경우 부모 역량을 선택할 수 없습니다."); - }); + if (hasChildAndParentAbility(abilities)) { + throw new IllegalArgumentException("자식 역량이 존재하는 경우 부모 역량을 선택할 수 없습니다."); + } + + Studylog studylog = studylogService.findStudylogById(studylogId); + studylog.validateBelongTo(memberId); List studylogAbilities = abilities.stream() .map(it -> new StudylogAbility(memberId, it, studylog)) .collect(Collectors.toList()); studylogAbilityRepository.deleteByStudylogId(studylogId); - List persistStudylogAbilities = studylogAbilityRepository.saveAll(studylogAbilities); + List persistStudylogAbilities = studylogAbilityRepository.saveAll( + studylogAbilities); return persistStudylogAbilities.stream() .map(it -> AbilityResponse.of(it.getAbility())) .collect(Collectors.toList()); } - public PageableResponse findAbilityStudylogsByAbilityIds(String username, List abilityIds, Pageable pageable) { + private boolean hasChildAndParentAbility(Set abilities) { + return abilities.stream() + .anyMatch(ability -> !ability.isParent() && + abilities.contains(ability.getParent())); + } + + public PageableResponse findAbilityStudylogsByAbilityIds( + String username, List abilityIds, Pageable pageable) { if (abilityIds != null && !abilityIds.isEmpty()) { - Page studylogAbilities = studylogAbilityRepository.findByAbilityIdIn(abilityIds, pageable); - List abilityStudylogResponses = AbilityStudylogResponse.listOf(studylogAbilities.getContent()); + Page studylogAbilities = studylogAbilityRepository.findByAbilityIdIn( + abilityIds, pageable); + List abilityStudylogResponses = AbilityStudylogResponse.listOf( + studylogAbilities.getContent()); return PageableResponse.of(abilityStudylogResponses, studylogAbilities); } @@ -79,34 +90,52 @@ public PageableResponse findAbilityStudylogsByAbilityId .map(Studylog::getId) .collect(Collectors.toList()); - List studylogAbilities = studylogAbilityRepository.findByStudylogIdIn(studylogIds); + List studylogAbilities = studylogAbilityRepository.findByStudylogIdIn( + studylogIds); - List abilityStudylogResponses = AbilityStudylogResponse.listOf(studylogs.getContent(), studylogAbilities); + List abilityStudylogResponses = AbilityStudylogResponse.listOf( + studylogs.getContent(), studylogAbilities); return PageableResponse.of(abilityStudylogResponses, studylogs); } - public PageableResponse findAbilityStudylogsMappingOnlyByAbilityIds(String username, List abilityIds, Pageable pageable) { + public PageableResponse findAbilityStudylogsMappingOnlyByAbilityIds( + String username, List abilityIds, Pageable pageable) { if (abilityIds != null && !abilityIds.isEmpty()) { - Page studylogAbilities = studylogAbilityRepository.findByAbilityIdIn(abilityIds, pageable); - List abilityStudylogResponses = AbilityStudylogResponse.listOf(studylogAbilities.getContent()); + Page studylogAbilities = studylogAbilityRepository.findByAbilityIdIn( + abilityIds, pageable); + List abilityStudylogResponses = AbilityStudylogResponse.listOf( + studylogAbilities.getContent()); return PageableResponse.of(abilityStudylogResponses, studylogAbilities); } Member member = memberService.findByUsername(username); - Page studylogAbilities = studylogAbilityRepository.findByMemberId(member.getId(), pageable); + Page studylogAbilities = studylogAbilityRepository.findByMemberId( + member.getId(), pageable); - List abilityStudylogResponses = AbilityStudylogResponse.listOf(studylogAbilities.getContent()); + List abilityStudylogResponses = AbilityStudylogResponse.listOf( + studylogAbilities.getContent()); return PageableResponse.of(abilityStudylogResponses, studylogAbilities); } - public List findStudylogAbilitiesInPeriod(Long memberId, LocalDate startDate, LocalDate endDate) { - List studylogs = studylogService.findStudylogsInPeriod(memberId, startDate, endDate); - return studylogAbilityRepository.findByStudylogIdIn(studylogs.stream().map(Studylog::getId).collect(Collectors.toList())); + public List findStudylogAbilitiesInPeriod(Long memberId, LocalDate startDate, + LocalDate endDate) { + List studylogs = studylogService.findStudylogsInPeriod(memberId, startDate, + endDate); + return studylogAbilityRepository.findByStudylogIdIn( + studylogs.stream().map(Studylog::getId).collect(Collectors.toList())); } @EventListener public void onStudylogDeleteEvent(StudylogDeleteEvent event) { studylogAbilityRepository.deleteByStudylogId(event.getStudylogId()); } + + public List getStudylogsByDate(Long memberId, + StudylogPeriodRequest studylogPeriodRequest) { + List studylogAbilities = findStudylogAbilitiesInPeriod(memberId, LocalDate.parse(studylogPeriodRequest.getStartDate()), + LocalDate.parse(studylogPeriodRequest.getEndDate())); + + return AbilityStudylogResponse.listOf(studylogAbilities); + } } diff --git a/backend/src/main/java/wooteco/prolog/ability/domain/Ability.java b/backend/src/main/java/wooteco/prolog/ability/domain/Ability.java index 1e0129299..d83bf4efe 100644 --- a/backend/src/main/java/wooteco/prolog/ability/domain/Ability.java +++ b/backend/src/main/java/wooteco/prolog/ability/domain/Ability.java @@ -185,6 +185,6 @@ public List getStudylogAbilities() { } public boolean isBelongsTo(Long memberId) { - return this.member.getId() == memberId; + return this.member.getId().equals(memberId); } } diff --git a/backend/src/main/java/wooteco/prolog/ability/domain/repository/StudylogAbilityRepository.java b/backend/src/main/java/wooteco/prolog/ability/domain/repository/StudylogAbilityRepository.java index f58a876e0..d46f612f7 100644 --- a/backend/src/main/java/wooteco/prolog/ability/domain/repository/StudylogAbilityRepository.java +++ b/backend/src/main/java/wooteco/prolog/ability/domain/repository/StudylogAbilityRepository.java @@ -17,4 +17,6 @@ public interface StudylogAbilityRepository extends JpaRepository findByStudylogIdIn(List studylogIds); void deleteByStudylogId(Long studylogId); + + List findAllByStudylogId(Long studylogId); } diff --git a/backend/src/main/java/wooteco/prolog/ability/ui/StudylogAbilityController.java b/backend/src/main/java/wooteco/prolog/ability/ui/StudylogAbilityController.java index 36ba1c016..151b7cda2 100644 --- a/backend/src/main/java/wooteco/prolog/ability/ui/StudylogAbilityController.java +++ b/backend/src/main/java/wooteco/prolog/ability/ui/StudylogAbilityController.java @@ -19,6 +19,7 @@ import wooteco.prolog.login.aop.MemberOnly; import wooteco.prolog.login.domain.AuthMemberPrincipal; import wooteco.prolog.login.ui.LoginMember; +import wooteco.prolog.report.application.dto.StudylogPeriodRequest; @RestController public class StudylogAbilityController { @@ -53,4 +54,13 @@ public ResponseEntity> findAbilityStud PageableResponse studylogs = studylogAbilityService.findAbilityStudylogsMappingOnlyByAbilityIds(username, abilityIds, pageable); return ResponseEntity.ok().body(studylogs); } + + @MemberOnly + @GetMapping("/studylogs/me") + public ResponseEntity> getStudylogsByDate(@AuthMemberPrincipal LoginMember member, + StudylogPeriodRequest studylogPeriodRequest) { + return (ResponseEntity.ok( + studylogAbilityService.getStudylogsByDate(member.getId(), studylogPeriodRequest)) + ); + } } diff --git a/backend/src/main/java/wooteco/prolog/badge/application/BadgeCreator.java b/backend/src/main/java/wooteco/prolog/badge/application/BadgeCreator.java new file mode 100644 index 000000000..23f30967e --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/application/BadgeCreator.java @@ -0,0 +1,9 @@ +package wooteco.prolog.badge.application; + +import java.util.Optional; +import wooteco.prolog.badge.domain.BadgeType; + +public interface BadgeCreator { + + Optional create(String username); +} diff --git a/backend/src/main/java/wooteco/prolog/badge/application/BadgeService.java b/backend/src/main/java/wooteco/prolog/badge/application/BadgeService.java new file mode 100644 index 000000000..45f08686c --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/application/BadgeService.java @@ -0,0 +1,31 @@ +package wooteco.prolog.badge.application; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import wooteco.prolog.badge.domain.BadgeType; +import wooteco.prolog.member.application.MemberService; +import wooteco.prolog.member.domain.Member; + +@Service +public class BadgeService { + + private final MemberService memberService; + private final List badgeCreators; + + public BadgeService(MemberService memberService, List badgeCreators) { + this.memberService = memberService; + this.badgeCreators = badgeCreators; + } + + public List getBadges(String username) { + Member member = memberService.findByUsername(username); + + return badgeCreators.stream() + .map(creator -> creator.create(member.getUsername())) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } +} diff --git a/backend/src/main/java/wooteco/prolog/badge/application/ComplimentKingBadgeCreator.java b/backend/src/main/java/wooteco/prolog/badge/application/ComplimentKingBadgeCreator.java new file mode 100644 index 000000000..fa5f06d5e --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/application/ComplimentKingBadgeCreator.java @@ -0,0 +1,29 @@ +package wooteco.prolog.badge.application; + +import java.util.List; +import java.util.Optional; +import wooteco.prolog.badge.domain.BadgeType; +import wooteco.prolog.studylog.domain.repository.BadgeRepository; + +public class ComplimentKingBadgeCreator implements BadgeCreator { + + private static final int COMPLIMENT_KING_CRITERIA = 15; + + private final BadgeRepository badgeRepository; + private final List sessionIds; + + public ComplimentKingBadgeCreator(BadgeRepository badgeRepository, List sessionIds) { + this.badgeRepository = badgeRepository; + this.sessionIds = sessionIds; + } + + @Override + public Optional create(String username) { + int likeCount = badgeRepository.countLikesByUsernameDuringSessions(username, sessionIds); + + if (likeCount >= COMPLIMENT_KING_CRITERIA) { + return Optional.of(BadgeType.COMPLIMENT_KING); + } + return Optional.empty(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/badge/application/PassionKingBadgeCreator.java b/backend/src/main/java/wooteco/prolog/badge/application/PassionKingBadgeCreator.java new file mode 100644 index 000000000..bb2412d7b --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/application/PassionKingBadgeCreator.java @@ -0,0 +1,30 @@ +package wooteco.prolog.badge.application; + +import java.util.List; +import java.util.Optional; +import wooteco.prolog.badge.domain.BadgeType; +import wooteco.prolog.studylog.domain.repository.BadgeRepository; + +public class PassionKingBadgeCreator implements BadgeCreator { + + private static final int PASSION_KING_CRITERIA = 7; + + private final BadgeRepository badgeRepository; + private final List sessions; + + + public PassionKingBadgeCreator(BadgeRepository badgeRepository, List sessions) { + this.badgeRepository = badgeRepository; + this.sessions = sessions; + } + + @Override + public Optional create(String username) { + int studylogCount = badgeRepository.countStudylogByUsernameDuringSessions(username, sessions); + + if (studylogCount >= PASSION_KING_CRITERIA) { + return Optional.of(BadgeType.PASSION_KING); + } + return Optional.empty(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/BadgeResponse.java b/backend/src/main/java/wooteco/prolog/badge/application/dto/BadgeResponse.java similarity index 69% rename from backend/src/main/java/wooteco/prolog/studylog/application/dto/BadgeResponse.java rename to backend/src/main/java/wooteco/prolog/badge/application/dto/BadgeResponse.java index 210a2b4e8..5234b4566 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/BadgeResponse.java +++ b/backend/src/main/java/wooteco/prolog/badge/application/dto/BadgeResponse.java @@ -1,4 +1,4 @@ -package wooteco.prolog.studylog.application.dto; +package wooteco.prolog.badge.application.dto; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -8,8 +8,7 @@ @NoArgsConstructor @AllArgsConstructor @Getter -@EqualsAndHashCode public class BadgeResponse { - private String name; + private String name; } diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/BadgesResponse.java b/backend/src/main/java/wooteco/prolog/badge/application/dto/BadgesResponse.java similarity index 56% rename from backend/src/main/java/wooteco/prolog/studylog/application/dto/BadgesResponse.java rename to backend/src/main/java/wooteco/prolog/badge/application/dto/BadgesResponse.java index 2163fc97a..e9cf7658b 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/BadgesResponse.java +++ b/backend/src/main/java/wooteco/prolog/badge/application/dto/BadgesResponse.java @@ -1,12 +1,9 @@ -package wooteco.prolog.studylog.application.dto; +package wooteco.prolog.badge.application.dto; import java.util.List; -import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.http.ResponseEntity; -import wooteco.prolog.studylog.domain.Badge; @NoArgsConstructor diff --git a/backend/src/main/java/wooteco/prolog/badge/config/BadgeCreatorConfig.java b/backend/src/main/java/wooteco/prolog/badge/config/BadgeCreatorConfig.java new file mode 100644 index 000000000..a380e54da --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/config/BadgeCreatorConfig.java @@ -0,0 +1,33 @@ +package wooteco.prolog.badge.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import wooteco.prolog.badge.application.BadgeCreator; +import wooteco.prolog.badge.application.ComplimentKingBadgeCreator; +import wooteco.prolog.badge.application.PassionKingBadgeCreator; +import wooteco.prolog.badge.domain.FourthCrewSessions; +import wooteco.prolog.studylog.domain.repository.BadgeRepository; + +@Configuration +public class BadgeCreatorConfig { + + @Bean + public BadgeCreator levelTwoPassionKingBadgeCreator(BadgeRepository badgeRepository) { + return new PassionKingBadgeCreator(badgeRepository, FourthCrewSessions.LEVEL_TWO.getSessionIds()); + } + + @Bean + public BadgeCreator levelTwoComplimentKingBadgeCreator(BadgeRepository badgeRepository) { + return new ComplimentKingBadgeCreator(badgeRepository, FourthCrewSessions.LEVEL_TWO.getSessionIds()); + } + + @Bean + public BadgeCreator levelThreePassionKingBadgeCreator(BadgeRepository badgeRepository) { + return new PassionKingBadgeCreator(badgeRepository, FourthCrewSessions.LEVEL_THREE.getSessionIds()); + } + + @Bean + public BadgeCreator levelThreeComplimentKingBadgeCreator(BadgeRepository badgeRepository) { + return new ComplimentKingBadgeCreator(badgeRepository, FourthCrewSessions.LEVEL_THREE.getSessionIds()); + } +} diff --git a/backend/src/main/java/wooteco/prolog/badge/domain/Badge.java b/backend/src/main/java/wooteco/prolog/badge/domain/Badge.java new file mode 100644 index 000000000..fd47b4784 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/domain/Badge.java @@ -0,0 +1,21 @@ +package wooteco.prolog.badge.domain; + +public class Badge { + + private final String name; + + public Badge(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "Badge{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/wooteco/prolog/badge/domain/BadgeType.java b/backend/src/main/java/wooteco/prolog/badge/domain/BadgeType.java new file mode 100644 index 000000000..091390532 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/domain/BadgeType.java @@ -0,0 +1,7 @@ +package wooteco.prolog.badge.domain; + +public enum BadgeType { + + PASSION_KING, + COMPLIMENT_KING +} diff --git a/backend/src/main/java/wooteco/prolog/badge/domain/FourthCrewSessions.java b/backend/src/main/java/wooteco/prolog/badge/domain/FourthCrewSessions.java new file mode 100644 index 000000000..56592aa56 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/domain/FourthCrewSessions.java @@ -0,0 +1,32 @@ +package wooteco.prolog.badge.domain; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public enum FourthCrewSessions { + + LEVEL_TWO(10L, 11L), + LEVEL_THREE(12L); + + private final List sessionIds; + + FourthCrewSessions(Long... sessionIds) { + validateSessionCount(sessionIds); + this.sessionIds = Arrays.asList(sessionIds); + } + + private void validateSessionCount(Long[] sessions) { + if (Objects.isNull(sessions) || isInvalidLength(sessions)) { + throw new IllegalArgumentException("세션은 최소 1이상 존재해야 합니다."); + } + } + + private boolean isInvalidLength(Long[] sessions) { + return sessions.length < 1; + } + + public List getSessionIds() { + return sessionIds; + } +} diff --git a/backend/src/main/java/wooteco/prolog/badge/ui/BadgeController.java b/backend/src/main/java/wooteco/prolog/badge/ui/BadgeController.java new file mode 100644 index 000000000..f3f85f840 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/badge/ui/BadgeController.java @@ -0,0 +1,33 @@ +package wooteco.prolog.badge.ui; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wooteco.prolog.badge.application.BadgeService; +import wooteco.prolog.badge.application.dto.BadgeResponse; +import wooteco.prolog.badge.application.dto.BadgesResponse; +import wooteco.prolog.badge.domain.BadgeType; + +@RestController +@AllArgsConstructor +@RequestMapping("/members") +public class BadgeController { + + private final BadgeService badgeService; + + @GetMapping(value = "/{username}/badges", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findMemberBadges(@PathVariable String username) { + List badges = badgeService.getBadges(username); + List badgeResponses = badges.stream() + .map(BadgeType::toString) + .map(BadgeResponse::new) + .collect(Collectors.toList()); + return ResponseEntity.ok(new BadgesResponse(badgeResponses)); + } +} diff --git a/backend/src/main/java/wooteco/prolog/common/AuditingEntity.java b/backend/src/main/java/wooteco/prolog/common/AuditingEntity.java index 3a06b2968..aba2022f6 100644 --- a/backend/src/main/java/wooteco/prolog/common/AuditingEntity.java +++ b/backend/src/main/java/wooteco/prolog/common/AuditingEntity.java @@ -20,5 +20,4 @@ public abstract class AuditingEntity { @LastModifiedDate private LocalDateTime updatedAt; - } diff --git a/backend/src/main/java/wooteco/prolog/common/ResourceSizeAdvice.java b/backend/src/main/java/wooteco/prolog/common/ResourceSizeAdvice.java new file mode 100644 index 000000000..18291586d --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/common/ResourceSizeAdvice.java @@ -0,0 +1,30 @@ +package wooteco.prolog.common; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.util.Collection; + +@ControllerAdvice +public class ResourceSizeAdvice implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return ResponseEntity.class.isAssignableFrom(returnType.getParameterType()); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { + if (body instanceof Collection) { + response.getHeaders().add("Access-Control-Expose-Headers", "X-Total-Count"); + response.getHeaders().add("X-Total-Count", String.valueOf(((Collection) body).size())); + } + return body; + } +} diff --git a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java index f18533d16..255e00acd 100644 --- a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java +++ b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java @@ -3,6 +3,8 @@ import java.util.Arrays; import lombok.AllArgsConstructor; import lombok.Getter; +import wooteco.prolog.levellogs.exception.InvalidLevelLogAuthorException; +import wooteco.prolog.levellogs.exception.LevelLogNotFoundException; import wooteco.prolog.login.excetpion.GithubApiFailException; import wooteco.prolog.login.excetpion.GithubConnectionException; import wooteco.prolog.login.excetpion.RoleNameNotFoundException; @@ -19,6 +21,8 @@ import wooteco.prolog.report.exception.ReportTitleLengthException; import wooteco.prolog.report.exception.ReportUpdateException; import wooteco.prolog.report.exception.UnRelatedAbilityExistenceException; +import wooteco.prolog.studylog.exception.CommentDeleteException; +import wooteco.prolog.studylog.exception.CommentNotFoundException; import wooteco.prolog.studylog.exception.DuplicateReportTitleException; import wooteco.prolog.studylog.exception.InvalidLikeRequestException; import wooteco.prolog.studylog.exception.InvalidUnlikeRequestException; @@ -102,7 +106,14 @@ public enum BadRequestCode { REPORT_TITLE_LENGTH_EXCEPTION(4013, "리포트 제목은 15자를 넘을 수 없습니다.", ReportTitleLengthException.class), INVALID_LIKE_REQUEST_EXCEPTION(5001, "스터디로그를 좋아요 할 수 없습니다.", InvalidLikeRequestException.class), - INVALID_UNLIKE_REQUEST_EXCEPTION(5002, "스터디로그를 좋아요 취소 할 수 없습니다.", InvalidUnlikeRequestException .class); + INVALID_UNLIKE_REQUEST_EXCEPTION(5002, "스터디로그를 좋아요 취소 할 수 없습니다.", InvalidUnlikeRequestException .class), + + + COMMENT_NOT_FOUND(6001, "존재하지 않는 댓글입니다.",CommentNotFoundException.class), + COMMENT_DELETE_EXCEPTION(6002, "댓글을 삭제할 수 없습니다.",CommentDeleteException.class), + + INVALID_LEVEL_LOG_AUTHOR_EXCEPTION(7001, "레벨 로그 작성자가 아닙니다.",InvalidLevelLogAuthorException.class), + LEVEL_LOG_NOT_FOUND_EXCEPTION(7002, "레벨 로그를 찾을 수 없습니다.",LevelLogNotFoundException.class); private int code; private String message; diff --git a/backend/src/main/java/wooteco/prolog/levellogs/application/LevelLogService.java b/backend/src/main/java/wooteco/prolog/levellogs/application/LevelLogService.java new file mode 100644 index 000000000..b404bf54d --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/application/LevelLogService.java @@ -0,0 +1,128 @@ +package wooteco.prolog.levellogs.application; + +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.levellogs.application.dto.LevelLogRequest; +import wooteco.prolog.levellogs.application.dto.LevelLogResponse; +import wooteco.prolog.levellogs.application.dto.LevelLogSummariesResponse; +import wooteco.prolog.levellogs.application.dto.LevelLogSummaryResponse; +import wooteco.prolog.levellogs.application.dto.SelfDiscussionRequest; +import wooteco.prolog.levellogs.domain.LevelLog; +import wooteco.prolog.levellogs.domain.SelfDiscussion; +import wooteco.prolog.levellogs.domain.repository.LevelLogRepository; +import wooteco.prolog.levellogs.domain.repository.SelfDiscussionRepository; +import wooteco.prolog.levellogs.exception.InvalidLevelLogAuthorException; +import wooteco.prolog.levellogs.exception.LevelLogNotFoundException; +import wooteco.prolog.levellogs.exception.SelfDiscussionNotFoundException; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.repository.MemberRepository; +import wooteco.prolog.member.exception.MemberNotFoundException; + +@Service +@Transactional(readOnly = true) +public class LevelLogService { + + private final MemberRepository memberRepository; + private final LevelLogRepository levelLogRepository; + private final SelfDiscussionRepository selfDiscussionRepository; + + public LevelLogService(MemberRepository memberRepository, + LevelLogRepository levelLogRepository, + SelfDiscussionRepository selfDiscussionRepository) { + this.memberRepository = memberRepository; + this.levelLogRepository = levelLogRepository; + this.selfDiscussionRepository = selfDiscussionRepository; + } + + @Transactional + public LevelLogResponse insertLevellogs(Long memberId, LevelLogRequest levelLogRequest) { + final Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + + final LevelLog levelLog = levelLogRepository.save( + new LevelLog(levelLogRequest.getTitle(), levelLogRequest.getContent(), member)); + + insertSelfDiscussions(levelLogRequest, levelLog); + + return findLevelLogResponseById(levelLog.getId()); + } + + private void insertSelfDiscussions(LevelLogRequest levelLogRequest, LevelLog levelLog) { + for (SelfDiscussionRequest selfDiscussionRequest : levelLogRequest.getLevelLogs()) { + insertSelfDiscussion(levelLog, selfDiscussionRequest); + } + } + + private void insertSelfDiscussion(LevelLog levelLog, + SelfDiscussionRequest selfDiscussionRequest) { + selfDiscussionRepository.save( + new SelfDiscussion(levelLog, selfDiscussionRequest.getQuestion(), + selfDiscussionRequest.getAnswer())); + } + + public LevelLogSummariesResponse findAll(Pageable pageable) { + Page page = levelLogRepository.findAll(pageable); + + List data = page.getContent() + .stream() + .map(LevelLogSummaryResponse::new) + .collect(Collectors.toList()); + + return new LevelLogSummariesResponse(data, page.getTotalElements(), page.getTotalPages(), + page.getNumber() + 1); + } + + @Transactional + public void deleteById(Long memberId, Long levelLogId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + LevelLog levelLog = findById(levelLogId); + + if (!levelLog.isAuthor(member)) { + throw new InvalidLevelLogAuthorException(); + } + + selfDiscussionRepository.deleteByLevelLog(levelLog); + levelLogRepository.delete(levelLog); + } + + @Transactional + public LevelLogResponse updateLevelLog(Long memberId, Long levelLogId, LevelLogRequest levelLogRequest) { + final LevelLog levelLog = findById(levelLogId); + levelLog.validateBelongTo(memberId); + + final List selfDiscussions = selfDiscussionRepository.findByLevelLog(levelLog); + + if (selfDiscussions.isEmpty()) { + throw new SelfDiscussionNotFoundException(); + } + + updateLevelLog(levelLog, levelLogRequest); + + return findLevelLogResponseById(levelLogId); + } + + private void updateLevelLog(LevelLog levelLog, LevelLogRequest levelLogRequest) { + selfDiscussionRepository.deleteByLevelLog(levelLog); + + levelLog.update(levelLogRequest.getTitle(), levelLogRequest.getContent()); + for (SelfDiscussionRequest request : levelLogRequest.getLevelLogs()) { + selfDiscussionRepository.save(new SelfDiscussion(levelLog, request.getQuestion(), request.getAnswer())); + } + } + + public LevelLogResponse findLevelLogResponseById(Long levelLogId) { + final LevelLog levelLog = findById(levelLogId); + final List discussions = selfDiscussionRepository.findByLevelLog(levelLog); + return new LevelLogResponse(levelLog, discussions); + } + + public LevelLog findById(Long levelLogId) { + return levelLogRepository.findById(levelLogId) + .orElseThrow(LevelLogNotFoundException::new); + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogRequest.java b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogRequest.java new file mode 100644 index 000000000..892c6070c --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogRequest.java @@ -0,0 +1,30 @@ +package wooteco.prolog.levellogs.application.dto; + +import java.util.List; + +import com.sun.istack.NotNull; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@ToString +public class LevelLogRequest { + + @NotNull + private String title; + @NotNull + private String content; + @NotNull + private List levelLogs; + + public LevelLogRequest(String title, String content, + List levelLogs) { + this.title = title; + this.content = content; + this.levelLogs = levelLogs; + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogResponse.java b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogResponse.java new file mode 100644 index 000000000..74813087a --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogResponse.java @@ -0,0 +1,42 @@ +package wooteco.prolog.levellogs.application.dto; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import wooteco.prolog.levellogs.domain.LevelLog; +import wooteco.prolog.levellogs.domain.SelfDiscussion; +import wooteco.prolog.member.application.dto.MemberResponse; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@ToString +public class LevelLogResponse { + + private Long id; + private String title; + private String content; + private MemberResponse author; + private List levelLogs; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public LevelLogResponse(LevelLog levelLog, List selfDiscussions) { + this.id = levelLog.getId(); + this.title = levelLog.getTitle(); + this.content = levelLog.getContent(); + this.author = MemberResponse.of(levelLog.getMember()); + this.levelLogs = selfDiscussions.stream() + .map(SelfDiscussionResponse::new) + .collect(Collectors.toList()); + this.createdAt = levelLog.getCreatedAt(); + this.updatedAt = levelLog.getUpdatedAt(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogSummariesResponse.java b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogSummariesResponse.java new file mode 100644 index 000000000..1900cd284 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogSummariesResponse.java @@ -0,0 +1,22 @@ +package wooteco.prolog.levellogs.application.dto; + +import java.util.List; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@ToString +public class LevelLogSummariesResponse { + + private List data; + private long totalSize; + private int totalPage; + private int currPage; +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogSummaryResponse.java b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogSummaryResponse.java new file mode 100644 index 000000000..1e0c2bb89 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/LevelLogSummaryResponse.java @@ -0,0 +1,30 @@ +package wooteco.prolog.levellogs.application.dto; + +import java.time.LocalDateTime; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import wooteco.prolog.levellogs.domain.LevelLog; +import wooteco.prolog.member.application.dto.MemberResponse; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@ToString +public class LevelLogSummaryResponse { + + private Long id; + private String title; + private MemberResponse author; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public LevelLogSummaryResponse(LevelLog levelLog) { + this(levelLog.getId(), levelLog.getTitle(), MemberResponse.of(levelLog.getMember()), + levelLog.getCreatedAt(), levelLog.getUpdatedAt()); + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/application/dto/SelfDiscussionRequest.java b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/SelfDiscussionRequest.java new file mode 100644 index 000000000..013f64f22 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/SelfDiscussionRequest.java @@ -0,0 +1,26 @@ +package wooteco.prolog.levellogs.application.dto; + +import com.sun.istack.NotNull; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@EqualsAndHashCode +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SelfDiscussionRequest { + + @NotNull + private String question; + @NotNull + private String answer; + + public SelfDiscussionRequest(String question, String answer) { + this.question = question; + this.answer = answer; + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/application/dto/SelfDiscussionResponse.java b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/SelfDiscussionResponse.java new file mode 100644 index 000000000..6ece504b4 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/application/dto/SelfDiscussionResponse.java @@ -0,0 +1,24 @@ +package wooteco.prolog.levellogs.application.dto; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import wooteco.prolog.levellogs.domain.SelfDiscussion; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@ToString +public class SelfDiscussionResponse { + + private Long id; + private String question; + private String answer; + + public SelfDiscussionResponse(SelfDiscussion selfDiscussion) { + this.id = selfDiscussion.getId(); + this.question = selfDiscussion.getQuestion(); + this.answer = selfDiscussion.getAnswer(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/domain/Content.java b/backend/src/main/java/wooteco/prolog/levellogs/domain/Content.java new file mode 100644 index 000000000..b40b80755 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/domain/Content.java @@ -0,0 +1,39 @@ +package wooteco.prolog.levellogs.domain; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import wooteco.prolog.studylog.exception.StudylogContentNullOrEmptyException; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +@Embeddable +public class Content { + + @Column(nullable = false, columnDefinition = "text") + private String content; + + public Content(String content) { + validateNullOrEmpty(content, length(content)); + this.content = content; + } + + private int length(String title) { + if (title != null) { + return title.trim().length(); + } + return 0; + } + + private void validateNullOrEmpty(String content, int trimedContentLength) { + if ((content == null) || trimedContentLength == 0) { + throw new StudylogContentNullOrEmptyException(); + } + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/domain/LevelLog.java b/backend/src/main/java/wooteco/prolog/levellogs/domain/LevelLog.java new file mode 100644 index 000000000..48f1ab461 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/domain/LevelLog.java @@ -0,0 +1,82 @@ +package wooteco.prolog.levellogs.domain; + +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import wooteco.prolog.common.AuditingEntity; +import wooteco.prolog.levellogs.exception.InvalidLevelLogAuthorException; +import wooteco.prolog.member.domain.Member; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +public class LevelLog extends AuditingEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private Title title; + + @Embedded + private Content content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + public LevelLog(final Long id, final String title, final String content, final Member member) { + this.id = id; + this.title = new Title(title); + this.content = new Content(content); + this.member = member; + } + + public LevelLog(final String title, final String content, final Member member) { + this(null, title, content, member); + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title.getTitle(); + } + + public String getContent() { + return content.getContent(); + } + + public Member getMember() { + return member; + } + + public void validateBelongTo(Long memberId) { + if (!isBelongsTo(memberId)) { + throw new InvalidLevelLogAuthorException(); + } + } + + public boolean isBelongsTo(Long memberId) { + return this.member.getId().equals(memberId); + } + + public boolean isAuthor(Member member) { + return this.member.equals(member); + } + + public void update(String title, String content) { + this.title = new Title(title); + this.content = new Content(content); + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/domain/SelfDiscussion.java b/backend/src/main/java/wooteco/prolog/levellogs/domain/SelfDiscussion.java new file mode 100644 index 000000000..ce451de15 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/domain/SelfDiscussion.java @@ -0,0 +1,57 @@ +package wooteco.prolog.levellogs.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SelfDiscussion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "level_log_id") + private LevelLog levelLog; + + @Column(nullable = false) + private String question; + + @Column(nullable = false, columnDefinition = "text") + private String answer; + + public SelfDiscussion(final LevelLog levelLog, final String question, final String answer) { + this.levelLog = levelLog; + this.question = question; + this.answer = answer; + } + + public void update(String question, String answer) { + this.question = question; + this.answer = answer; + } + + public Long getId() { + return id; + } + + public LevelLog getLevelLog() { + return levelLog; + } + + public String getQuestion() { + return question; + } + + public String getAnswer() { + return answer; + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/domain/Title.java b/backend/src/main/java/wooteco/prolog/levellogs/domain/Title.java new file mode 100644 index 000000000..6d921c391 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/domain/Title.java @@ -0,0 +1,63 @@ +package wooteco.prolog.levellogs.domain; + +import java.util.Objects; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import wooteco.prolog.login.excetpion.StudylogTitleNullOrEmptyException; +import wooteco.prolog.studylog.exception.TooLongTitleException; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +@ToString +@Embeddable +public class Title { + + public static final int MAX_LENGTH = 50; + + @Column(nullable = false, length = MAX_LENGTH) + private String title; + + public Title(String title) { + this.title = trim(title); + validateNull(title); + validateEmpty(title); + validateOnlyBlank(title); + validateMaxLength(title); + } + + private String trim(String name) { + return name.trim(); + } + + private void validateNull(String title) { + if (Objects.isNull(title)) { + throw new StudylogTitleNullOrEmptyException(); + } + } + + private void validateEmpty(String title) { + if (title.isEmpty()) { + throw new StudylogTitleNullOrEmptyException(); + } + } + + private void validateOnlyBlank(String title) { + if (title.trim().isEmpty()) { + throw new StudylogTitleNullOrEmptyException(); + } + } + + private void validateMaxLength(String title) { + if (title.length() > MAX_LENGTH) { + throw new TooLongTitleException(); + } + } +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/domain/repository/LevelLogRepository.java b/backend/src/main/java/wooteco/prolog/levellogs/domain/repository/LevelLogRepository.java new file mode 100644 index 000000000..9b3fcfadd --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/domain/repository/LevelLogRepository.java @@ -0,0 +1,8 @@ +package wooteco.prolog.levellogs.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.levellogs.domain.LevelLog; + +public interface LevelLogRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/domain/repository/SelfDiscussionRepository.java b/backend/src/main/java/wooteco/prolog/levellogs/domain/repository/SelfDiscussionRepository.java new file mode 100644 index 000000000..ffedb8ddc --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/domain/repository/SelfDiscussionRepository.java @@ -0,0 +1,15 @@ +package wooteco.prolog.levellogs.domain.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.levellogs.domain.LevelLog; +import wooteco.prolog.levellogs.domain.SelfDiscussion; + +public interface SelfDiscussionRepository extends JpaRepository { + + List findByLevelLog(LevelLog levelLog); + + List findAllByLevelLogIn(List levelLogs); + + void deleteByLevelLog(LevelLog levelLog); +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/exception/InvalidLevelLogAuthorException.java b/backend/src/main/java/wooteco/prolog/levellogs/exception/InvalidLevelLogAuthorException.java new file mode 100644 index 000000000..131e8e9da --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/exception/InvalidLevelLogAuthorException.java @@ -0,0 +1,7 @@ +package wooteco.prolog.levellogs.exception; + +import wooteco.prolog.common.exception.BadRequestException; + +public class InvalidLevelLogAuthorException extends BadRequestException { + +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/exception/LevelLogNotFoundException.java b/backend/src/main/java/wooteco/prolog/levellogs/exception/LevelLogNotFoundException.java new file mode 100644 index 000000000..d59d28dd3 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/exception/LevelLogNotFoundException.java @@ -0,0 +1,7 @@ +package wooteco.prolog.levellogs.exception; + +import wooteco.prolog.common.exception.BadRequestException; + +public class LevelLogNotFoundException extends BadRequestException { + +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/exception/SelfDiscussionNotFoundException.java b/backend/src/main/java/wooteco/prolog/levellogs/exception/SelfDiscussionNotFoundException.java new file mode 100644 index 000000000..e67f20bc2 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/exception/SelfDiscussionNotFoundException.java @@ -0,0 +1,7 @@ +package wooteco.prolog.levellogs.exception; + +import wooteco.prolog.common.exception.BadRequestException; + +public class SelfDiscussionNotFoundException extends BadRequestException { + +} diff --git a/backend/src/main/java/wooteco/prolog/levellogs/ui/LevelLogsController.java b/backend/src/main/java/wooteco/prolog/levellogs/ui/LevelLogsController.java new file mode 100644 index 000000000..3d642a61e --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/levellogs/ui/LevelLogsController.java @@ -0,0 +1,72 @@ +package wooteco.prolog.levellogs.ui; + +import java.net.URI; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wooteco.prolog.levellogs.application.LevelLogService; +import wooteco.prolog.levellogs.application.dto.LevelLogRequest; +import wooteco.prolog.levellogs.application.dto.LevelLogResponse; +import wooteco.prolog.levellogs.application.dto.LevelLogSummariesResponse; +import wooteco.prolog.login.aop.MemberOnly; +import wooteco.prolog.login.domain.AuthMemberPrincipal; +import wooteco.prolog.login.ui.LoginMember; + +@RestController +@RequestMapping("/levellogs") +public class LevelLogsController { + + private final LevelLogService levelLogService; + + public LevelLogsController(LevelLogService levelLogService) { + this.levelLogService = levelLogService; + } + + @PostMapping + @MemberOnly + public ResponseEntity create(@AuthMemberPrincipal LoginMember member, + @RequestBody LevelLogRequest levelLogRequest) { + final LevelLogResponse response = levelLogService.insertLevellogs( + member.getId(), levelLogRequest); + return ResponseEntity.created(URI.create("/levellogs/" + response.getId())).build(); + } + + @PutMapping("/{id}") + @MemberOnly + public ResponseEntity updateLovellog(@AuthMemberPrincipal LoginMember member, + @PathVariable Long id, + @RequestBody LevelLogRequest levelLogRequest) { + levelLogService.updateLevelLog(member.getId(), id, levelLogRequest); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable Long id) { + final LevelLogResponse response = levelLogService.findLevelLogResponseById(id); + return ResponseEntity.ok(response); + } + + @GetMapping + public ResponseEntity findAll( + @PageableDefault(sort = {"createdAt"}, direction = Direction.DESC) Pageable pageable) { + final LevelLogSummariesResponse response = levelLogService.findAll(pageable); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{id}") + @MemberOnly + public ResponseEntity deleteById(@AuthMemberPrincipal LoginMember member, + @PathVariable Long id) { + levelLogService.deleteById(member.getId(), id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/member/application/dto/MemberResponse.java b/backend/src/main/java/wooteco/prolog/member/application/dto/MemberResponse.java index 4ccfbf50f..fb6211d71 100644 --- a/backend/src/main/java/wooteco/prolog/member/application/dto/MemberResponse.java +++ b/backend/src/main/java/wooteco/prolog/member/application/dto/MemberResponse.java @@ -1,14 +1,18 @@ package wooteco.prolog.member.application.dto; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; import wooteco.prolog.member.domain.Member; import wooteco.prolog.member.domain.Role; @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode @Getter +@ToString public class MemberResponse { private Long id; diff --git a/backend/src/main/java/wooteco/prolog/member/domain/GroupMember.java b/backend/src/main/java/wooteco/prolog/member/domain/GroupMember.java index 62b4aa0db..3c6797a56 100644 --- a/backend/src/main/java/wooteco/prolog/member/domain/GroupMember.java +++ b/backend/src/main/java/wooteco/prolog/member/domain/GroupMember.java @@ -27,4 +27,10 @@ public class GroupMember { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "group_id", nullable = false) private MemberGroup group; + + public GroupMember(Long id, Member member, MemberGroup group) { + this.id = id; + this.member = member; + this.group = group; + } } diff --git a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroup.java b/backend/src/main/java/wooteco/prolog/member/domain/MemberGroup.java index ab65b4459..20834bde5 100644 --- a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroup.java +++ b/backend/src/main/java/wooteco/prolog/member/domain/MemberGroup.java @@ -13,6 +13,8 @@ @Getter public class MemberGroup { + private static final String BACKEND = "백엔드"; + private static final String FRONTEND = "프론트엔드"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -20,4 +22,21 @@ public class MemberGroup { private String name; private String description; + + public MemberGroup(Long id, String name, String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public String getGroupName() { + if (this.name.contains(BACKEND)) { + return BACKEND; + } + if (this.name.contains(FRONTEND)) { + return FRONTEND; + } + + return null; + } } diff --git a/backend/src/main/java/wooteco/prolog/member/domain/MemberGroups.java b/backend/src/main/java/wooteco/prolog/member/domain/MemberGroups.java new file mode 100644 index 000000000..9c9424779 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/MemberGroups.java @@ -0,0 +1,16 @@ +package wooteco.prolog.member.domain; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class MemberGroups { + + private List values; + + public boolean isContainsMemberGroups(GroupMember groupMember) { + return values.contains(groupMember.getGroup()); + } +} diff --git a/backend/src/main/java/wooteco/prolog/member/domain/repository/GroupMemberRepository.java b/backend/src/main/java/wooteco/prolog/member/domain/repository/GroupMemberRepository.java index 28afab7e8..5b2c76f1d 100644 --- a/backend/src/main/java/wooteco/prolog/member/domain/repository/GroupMemberRepository.java +++ b/backend/src/main/java/wooteco/prolog/member/domain/repository/GroupMemberRepository.java @@ -3,8 +3,12 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import wooteco.prolog.member.domain.GroupMember; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.MemberGroup; public interface GroupMemberRepository extends JpaRepository { List findByGroupId(Long groupId); + + boolean existsGroupMemberByMemberAndGroup(Member member, MemberGroup memberGroup); } diff --git a/backend/src/main/java/wooteco/prolog/member/domain/repository/MemberGroupRepository.java b/backend/src/main/java/wooteco/prolog/member/domain/repository/MemberGroupRepository.java new file mode 100644 index 000000000..c98165eb2 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/member/domain/repository/MemberGroupRepository.java @@ -0,0 +1,8 @@ +package wooteco.prolog.member.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.member.domain.MemberGroup; + +public interface MemberGroupRepository extends JpaRepository { + +} diff --git a/backend/src/main/java/wooteco/prolog/report/application/dto/StudylogPeriodRequest.java b/backend/src/main/java/wooteco/prolog/report/application/dto/StudylogPeriodRequest.java new file mode 100644 index 000000000..96c76060b --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/report/application/dto/StudylogPeriodRequest.java @@ -0,0 +1,16 @@ +package wooteco.prolog.report.application.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@NoArgsConstructor +public class StudylogPeriodRequest { + + // @DateTimeFormat(pattern = "yyyy-MM-dd") + private String startDate; + // @DateTimeFormat(pattern = "yyyy-MM-dd") + private String endDate; +} diff --git a/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java b/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java index b5f9f3dd3..ad1a50a6b 100644 --- a/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java +++ b/backend/src/main/java/wooteco/prolog/session/application/SessionMemberService.java @@ -71,6 +71,23 @@ public List findAllMembersBySessionId(Long sessionId) { } public List findByMemberId(Long memberId) { - return sessionMemberRepository.findByMemberId(memberId); + Member member = memberService.findById(memberId); + return sessionMemberRepository.findByMember(member); + } + + @Transactional + public void deleteRegistedSession(Long sessionId, Long memberId) { + Member member = memberService.findById(memberId); + SessionMember sessionMember = findSessionMemberBySessionIdAndMemberId( + sessionId, + member + ); + + sessionMemberRepository.delete(sessionMember); + } + + private SessionMember findSessionMemberBySessionIdAndMemberId(Long sessionId, Member member) { + return sessionMemberRepository.findBySessionIdAndMember(sessionId, member) + .orElseThrow(SessionNotFoundException::new); } } diff --git a/backend/src/main/java/wooteco/prolog/session/domain/repository/SessionMemberRepository.java b/backend/src/main/java/wooteco/prolog/session/domain/repository/SessionMemberRepository.java index 105b0733f..ba1629413 100644 --- a/backend/src/main/java/wooteco/prolog/session/domain/repository/SessionMemberRepository.java +++ b/backend/src/main/java/wooteco/prolog/session/domain/repository/SessionMemberRepository.java @@ -1,12 +1,16 @@ package wooteco.prolog.session.domain.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.member.domain.Member; import wooteco.prolog.session.domain.SessionMember; public interface SessionMemberRepository extends JpaRepository { List findAllBySessionId(Long sessionId); - List findByMemberId(Long memberId); + List findByMember(Member member); + + Optional findBySessionIdAndMember(Long sessionId, Member member); } diff --git a/backend/src/main/java/wooteco/prolog/session/ui/SessionMemberController.java b/backend/src/main/java/wooteco/prolog/session/ui/SessionMemberController.java index 5489bb4ee..bdb97d57d 100644 --- a/backend/src/main/java/wooteco/prolog/session/ui/SessionMemberController.java +++ b/backend/src/main/java/wooteco/prolog/session/ui/SessionMemberController.java @@ -3,6 +3,7 @@ import java.util.List; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -35,6 +36,15 @@ public ResponseEntity registerMe(@PathVariable Long sessionId, @AuthMember return ResponseEntity.ok().build(); } + @DeleteMapping("/me") + public ResponseEntity deleteRegistedSession( + @PathVariable Long sessionId, + @AuthMemberPrincipal LoginMember member + ){ + sessionMemberService.deleteRegistedSession(sessionId, member.getId()); + return ResponseEntity.ok().build(); + } + // admin only @PostMapping public ResponseEntity registerByGroupId(@PathVariable Long sessionId, @RequestBody SessionGroupMemberRequest sessionGroupMemberRequest) { diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/BadgeService.java b/backend/src/main/java/wooteco/prolog/studylog/application/BadgeService.java deleted file mode 100644 index bc0eb04cf..000000000 --- a/backend/src/main/java/wooteco/prolog/studylog/application/BadgeService.java +++ /dev/null @@ -1,56 +0,0 @@ -package wooteco.prolog.studylog.application; - -import java.util.ArrayList; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import wooteco.prolog.member.application.MemberService; -import wooteco.prolog.member.domain.Member; -import wooteco.prolog.studylog.domain.BadgeType; -import wooteco.prolog.studylog.domain.repository.BadgeRepository; - -@Service -@RequiredArgsConstructor -public class BadgeService { - - private static final int PASSION_KING_CRITERIA = 7; - private static final int COMPLIMENT_KING_CRITERIA = 15; - - private final MemberService memberService; - private final BadgeRepository badgeRepository; - - public List findBadges(String username, List sessions) { - Member member = memberService.findByUsername(username); - - int studylogCount = badgeRepository.countStudylogByUsernameDuringSessions( - member.getUsername(), - sessions); - - int likeCount = badgeRepository.countLikesByUsernameDuringSessions( - member.getUsername(), sessions); - - return createBadges(studylogCount, likeCount); - } - - private List createBadges(int studylogCount, int likeCount) { - List badges = new ArrayList<>(); - - if (isPassionKing(studylogCount)) { - badges.add(BadgeType.PASSION_KING); - } - - if (isComplimentKing(likeCount)) { - badges.add(BadgeType.COMPLIMENT_KING); - } - - return badges; - } - - private boolean isPassionKing(int studylogCount) { - return studylogCount >= PASSION_KING_CRITERIA; - } - - private boolean isComplimentKing(int likeCount) { - return likeCount >= COMPLIMENT_KING_CRITERIA; - } -} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/CommentService.java b/backend/src/main/java/wooteco/prolog/studylog/application/CommentService.java new file mode 100644 index 000000000..a5f253361 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/CommentService.java @@ -0,0 +1,87 @@ +package wooteco.prolog.studylog.application; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.repository.MemberRepository; +import wooteco.prolog.member.exception.MemberNotFoundException; +import wooteco.prolog.studylog.application.dto.CommentResponse; +import wooteco.prolog.studylog.application.dto.CommentSaveRequest; +import wooteco.prolog.studylog.application.dto.CommentUpdateRequest; +import wooteco.prolog.studylog.application.dto.CommentsResponse; +import wooteco.prolog.studylog.domain.Comment; +import wooteco.prolog.studylog.domain.Studylog; +import wooteco.prolog.studylog.domain.repository.CommentRepository; +import wooteco.prolog.studylog.domain.repository.StudylogRepository; +import wooteco.prolog.studylog.exception.CommentNotFoundException; +import wooteco.prolog.studylog.exception.StudylogNotFoundException; + +@Transactional +@AllArgsConstructor +@Service +public class CommentService { + + private final CommentRepository commentRepository; + private final MemberRepository memberRepository; + private final StudylogRepository studylogRepository; + + public Long insertComment(CommentSaveRequest request) { + Member findMember = memberRepository.findById(request.getMemberId()) + .orElseThrow(MemberNotFoundException::new); + Studylog findStudylog = studylogRepository.findById(request.getStudylogId()) + .orElseThrow(StudylogNotFoundException::new); + + Comment comment = request.toEntity(findMember, findStudylog); + + return commentRepository.save(comment).getId(); + } + + @Transactional(readOnly = true) + public CommentsResponse findComments(Long studylogId) { + Studylog findStudylog = studylogRepository.findById(studylogId) + .orElseThrow(StudylogNotFoundException::new); + + List commentResponses = commentRepository.findCommentByStudylog(findStudylog) + .stream() + .map(CommentResponse::of) + .collect(Collectors.toList()); + + return new CommentsResponse(commentResponses); + } + + public Long updateComment(CommentUpdateRequest request) { + validateExistsMember(request.getMemberId()); + validateExistsStudylog(request.getStudylogId()); + + Comment comment = commentRepository.findById(request.getCommentId()) + .orElseThrow(CommentNotFoundException::new); + comment.updateContent(request.getContent()); + + return comment.getId(); + } + + public void deleteComment(Long memberId, Long studylogId, Long commentId) { + validateExistsMember(memberId); + validateExistsStudylog(studylogId); + + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + comment.delete(); + } + + private void validateExistsMember(Long memberId) { + if (!memberRepository.existsById(memberId)) { + throw new MemberNotFoundException(); + } + } + + private void validateExistsStudylog(Long studylogId) { + if (!studylogRepository.existsById(studylogId)) { + throw new StudylogNotFoundException(); + } + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java b/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java index 94d5b10a9..742464324 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/PopularStudylogService.java @@ -6,45 +6,55 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.member.domain.GroupMember; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.MemberGroup; +import wooteco.prolog.member.domain.MemberGroups; +import wooteco.prolog.member.domain.repository.GroupMemberRepository; +import wooteco.prolog.member.domain.repository.MemberGroupRepository; import wooteco.prolog.studylog.application.dto.PopularStudylogsResponse; import wooteco.prolog.studylog.application.dto.StudylogResponse; -import wooteco.prolog.studylog.application.dto.StudylogWithScrapedCountResponse; import wooteco.prolog.studylog.application.dto.StudylogsResponse; -import wooteco.prolog.studylog.application.dto.StudylogsWithScrapCountResponse; -import wooteco.prolog.studylog.domain.Curriculum; import wooteco.prolog.studylog.domain.PopularStudylog; import wooteco.prolog.studylog.domain.Studylog; import wooteco.prolog.studylog.domain.repository.PopularStudylogRepository; import wooteco.prolog.studylog.domain.repository.StudylogRepository; -import wooteco.prolog.studylog.domain.repository.StudylogScrapRepository; @Service @AllArgsConstructor @Transactional(readOnly = true) public class PopularStudylogService { - private static final int ONE_INDEXED_PARAMETER = 1; private static final int A_WEEK = 7; + private static final String FRONTEND = "프론트엔드"; + private static final String BACKEND = "백엔드"; private final StudylogService studylogService; private final StudylogRepository studylogRepository; private final PopularStudylogRepository popularStudylogRepository; - private final StudylogScrapRepository studylogScrapRepository; + private final MemberGroupRepository memberGroupRepository; + private final GroupMemberRepository groupMemberRepository; @Transactional public void updatePopularStudylogs(Pageable pageable) { deleteAllLegacyPopularStudylogs(); + + List groupMembers = groupMemberRepository.findAll(); + Map> memberGroups = memberGroupRepository.findAll().stream() + .collect(Collectors.groupingBy(MemberGroup::getGroupName)); + List studylogs = new ArrayList<>(); - for (Curriculum curriculum : Curriculum.values()) { - List studylogsByDays = findStudylogsByDays(pageable, LocalDateTime.now(), - curriculum); - studylogs.addAll(studylogsByDays); - } + studylogs.addAll(findStudylogsByDays(pageable, LocalDateTime.now(), + new MemberGroups(memberGroups.get(FRONTEND)), groupMembers)); + studylogs.addAll(findStudylogsByDays(pageable, LocalDateTime.now(), + new MemberGroups(memberGroups.get(BACKEND)), groupMembers)); List popularStudylogs = studylogs.stream() .map(it -> new PopularStudylog(it.getId())) @@ -53,59 +63,65 @@ public void updatePopularStudylogs(Pageable pageable) { popularStudylogRepository.saveAll(popularStudylogs); } - public PopularStudylogsResponse findPopularStudylogs(Pageable pageable, Long memberId, - boolean isAnonymousMember) { - List allStudylogs = getSortedPopularStudyLogs(); - List frontStudylogs = getSortedCurriculumPopularStudylogs(allStudylogs, - Curriculum.FRONTEND); - List backStudylogs = getSortedCurriculumPopularStudylogs(allStudylogs, - Curriculum.BACKEND); - - StudylogsWithScrapCountResponse allResponse = getPageablePopularStudylogs(allStudylogs, pageable, - memberId); - StudylogsWithScrapCountResponse frontResponse = getPageablePopularStudylogs(frontStudylogs, pageable, - memberId); - StudylogsWithScrapCountResponse backResponse = getPageablePopularStudylogs(backStudylogs, pageable, - memberId); + public PopularStudylogsResponse findPopularStudylogs(Pageable pageable, + Long memberId, + boolean isAnonymousMember) { + + List groupMembers = groupMemberRepository.findAll(); + Map> memberGroups = memberGroupRepository.findAll().stream() + .collect(Collectors.groupingBy(MemberGroup::getGroupName)); + + List all = getSortedPopularStudyLogs(pageable); + List frontend = getSortedPopularStudyLogs(all, + new MemberGroups(memberGroups.get(FRONTEND)), groupMembers); + List backend = getSortedPopularStudyLogs(all, + new MemberGroups(memberGroups.get(BACKEND)), groupMembers); + + PageImpl allPage = new PageImpl<>(all, pageable, all.size()); + PageImpl frontendPage = new PageImpl<>(frontend, pageable, frontend.size()); + PageImpl backendPage = new PageImpl<>(backend, pageable, backend.size()); + + StudylogsResponse allStudylogsResponse = StudylogsResponse.of(allPage, memberId); + StudylogsResponse frontendStudylogsResponse = StudylogsResponse.of(frontendPage, memberId); + StudylogsResponse backendStudylogsResponse = StudylogsResponse.of(backendPage, memberId); if (isAnonymousMember) { - return new PopularStudylogsResponse(allResponse, frontResponse, backResponse); + return PopularStudylogsResponse.of( + allStudylogsResponse, frontendStudylogsResponse, backendStudylogsResponse); } - List allData = allResponse.getStudylogResponses(); - updateScrapAndRead(allData, memberId); - - List frontendData = frontResponse.getStudylogResponses(); - updateScrapAndRead(frontendData, memberId); + List allData = allStudylogsResponse.getData(); + List frontendData = frontendStudylogsResponse.getData(); + List backendData = backendStudylogsResponse.getData(); - List backendData = backResponse.getStudylogResponses(); - updateScrapAndRead(backendData, memberId); + checkStudylogScrapAndRead(allData, memberId); + checkStudylogScrapAndRead(frontendData, memberId); + checkStudylogScrapAndRead(backendData, memberId); - return new PopularStudylogsResponse(allResponse, frontResponse, backResponse); + return PopularStudylogsResponse.of( + allStudylogsResponse, frontendStudylogsResponse, backendStudylogsResponse); } - private List getSortedCurriculumPopularStudylogs(List allStudylogs, - Curriculum curriculum) { - - return allStudylogs.stream() - .filter(studylog -> studylog.isContainsCurriculum(curriculum)) + private List getSortedPopularStudyLogs(Pageable pageable) { + return studylogRepository.findAllByIdIn(getPopularStudylogIds(), pageable) + .stream() + .sorted(Comparator.comparing(Studylog::getPopularScore).reversed()) .collect(toList()); } - private StudylogsWithScrapCountResponse getPageablePopularStudylogs(List allStudylogs, - Pageable pageable, Long memberId) { - - final List studylogsResponse = allStudylogs.stream() - .map(studylog -> new StudylogWithScrapedCountResponse(StudylogResponse.of(studylog), - studylogScrapRepository.countByStudylogId(studylog.getId()))) + private List getSortedPopularStudyLogs(List studylogs, + MemberGroups memberGroups, + List groupMembers) { + return studylogs.stream() + .filter(it -> isContainsGroupMemberOfMemberGroups(memberGroups, it.getMember(), + groupMembers)) + .sorted(Comparator.comparing(Studylog::getPopularScore).reversed()) .collect(toList()); - - return new StudylogsWithScrapCountResponse(studylogsResponse, 0L, 0, 0); } - private void updateScrapAndRead(List studylogResponses, Long memberId) { - studylogService.updateScrap(studylogResponses, studylogService.findScrapIds(memberId)); - studylogService.updateRead(studylogResponses, studylogService.findReadIds(memberId)); + private void checkStudylogScrapAndRead(List data, Long memberId) { + studylogService.updateScrap(data, studylogService.findScrapIds(memberId)); + studylogService.updateRead(data, studylogService.findReadIds(memberId)); } private void deleteAllLegacyPopularStudylogs() { @@ -116,13 +132,6 @@ private void deleteAllLegacyPopularStudylogs() { } } - private List getSortedPopularStudyLogs() { - return studylogRepository.findAllById(getPopularStudylogIds()) - .stream() - .sorted(Comparator.comparing(Studylog::getPopularScore).reversed()) - .collect(toList()); - } - private List getPopularStudylogIds() { return popularStudylogRepository.findAllByDeletedFalse() .stream() @@ -130,8 +139,10 @@ private List getPopularStudylogIds() { .collect(toList()); } - private List findStudylogsByDays(Pageable pageable, LocalDateTime dateTime, - Curriculum curriculum) { + private List findStudylogsByDays(Pageable pageable, + LocalDateTime dateTime, + MemberGroups memberGroups, + List groupMembers) { int decreaseDays = 0; int searchFailedCount = 0; @@ -141,20 +152,23 @@ private List findStudylogsByDays(Pageable pageable, LocalDateTime date dateTime.minusDays(decreaseDays)); if (studylogs.size() >= pageable.getPageSize()) { - final List filteredStudylogs = studylogs.stream() - .filter(it -> it.getSession().isSameAs(curriculum)) + List filteringStudylogs = studylogs.stream() + .filter(it -> isContainsGroupMemberOfMemberGroups( + memberGroups, it.getMember(), groupMembers)) .sorted(Comparator.comparing(Studylog::getPopularScore).reversed()) .collect(toList()); - if (filteredStudylogs.size() < pageable.getPageSize()) { - return filteredStudylogs; + + if (filteringStudylogs.size() < pageable.getPageSize()) { + return filteringStudylogs; } - return filteredStudylogs + return filteringStudylogs .subList(0, pageable.getPageSize()); } if (searchFailedCount >= 2) { return studylogs.stream() - .filter(it -> it.getSession().isSameAs(curriculum)) + .filter(it -> isContainsGroupMemberOfMemberGroups( + memberGroups, it.getMember(), groupMembers)) .sorted(Comparator.comparing(Studylog::getPopularScore).reversed()) .collect(toList()); } @@ -162,4 +176,12 @@ private List findStudylogsByDays(Pageable pageable, LocalDateTime date searchFailedCount += 1; } } + + private boolean isContainsGroupMemberOfMemberGroups(MemberGroups memberGroups, + Member member, + List groupMembers) { + return groupMembers.stream() + .anyMatch( + it -> it.getMember().equals(member) && memberGroups.isContainsMemberGroups(it)); + } } diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/StudylogService.java b/backend/src/main/java/wooteco/prolog/studylog/application/StudylogService.java index 32c2ae3a4..bab6ce763 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/StudylogService.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/StudylogService.java @@ -9,6 +9,8 @@ import java.time.LocalTime; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; @@ -16,6 +18,11 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.ability.application.AbilityService; +import wooteco.prolog.ability.application.dto.AbilityResponse; +import wooteco.prolog.ability.domain.Ability; +import wooteco.prolog.ability.domain.StudylogAbility; +import wooteco.prolog.ability.domain.repository.StudylogAbilityRepository; import wooteco.prolog.login.ui.LoginMember; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.application.MemberTagService; @@ -62,14 +69,17 @@ public class StudylogService { private final TagService tagService; private final SessionService sessionService; private final MissionService missionService; + private final AbilityService abilityService; private final StudylogRepository studylogRepository; private final StudylogScrapRepository studylogScrapRepository; private final StudylogReadRepository studylogReadRepository; private final StudylogTempRepository studylogTempRepository; + private final StudylogAbilityRepository studylogAbilityRepository; private final ApplicationEventPublisher eventPublisher; @Transactional - public List insertStudylogs(Long memberId, List studylogRequests) { + public List insertStudylogs(Long memberId, + List studylogRequests) { if (studylogRequests.isEmpty()) { throw new StudylogArgumentException(); } @@ -81,10 +91,19 @@ public List insertStudylogs(Long memberId, List abilities = abilityService.findByIdIn(memberId, + studylogRequest.getAbilities()); + + if (hasChildAndParentAbility(abilities)) { + throw new IllegalArgumentException("자식 역량이 존재하는 경우 부모 역량을 선택할 수 없습니다."); + } + Member member = memberService.findById(memberId); Tags tags = tagService.findOrCreate(studylogRequest.getTags()); - Session session = sessionService.findSessionById(studylogRequest.getSessionId()).orElse(null); - Mission mission = missionService.findMissionById(studylogRequest.getMissionId()).orElse(null); + Session session = sessionService.findSessionById(studylogRequest.getSessionId()) + .orElse(null); + Mission mission = missionService.findMissionById(studylogRequest.getMissionId()) + .orElse(null); Studylog persistStudylog = studylogRepository.save(new Studylog(member, studylogRequest.getTitle(), @@ -94,25 +113,45 @@ public StudylogResponse insertStudylog(Long memberId, StudylogRequest studylogRe tags.getList()) ); + List studylogAbilities = abilities.stream() + .map(it -> new StudylogAbility(memberId, it, persistStudylog)) + .collect(Collectors.toList()); + + studylogAbilityRepository.deleteByStudylogId(persistStudylog.getId()); + List persistStudylogAbilities = studylogAbilityRepository.saveAll( + studylogAbilities); + + final List abilityResponses = persistStudylogAbilities.stream() + .map(it -> AbilityResponse.of(it.getAbility())) + .collect(toList()); + onStudylogCreatedEvent(member, tags, persistStudylog); deleteStudylogTemp(memberId); - return StudylogResponse.of(persistStudylog); + return StudylogResponse.of(persistStudylog, abilityResponses); + } + + private boolean hasChildAndParentAbility(Set abilities) { + return abilities.stream() + .anyMatch(ability -> !ability.isParent() && + abilities.contains(ability.getParent())); } @Transactional public StudylogTempResponse insertStudylogTemp(Long memberId, StudylogRequest studylogRequest) { Member member = memberService.findById(memberId); Tags tags = tagService.findOrCreate(studylogRequest.getTags()); - Session session = sessionService.findSessionById(studylogRequest.getSessionId()).orElse(null); - Mission mission = missionService.findMissionById(studylogRequest.getMissionId()).orElse(null); + Session session = sessionService.findSessionById(studylogRequest.getSessionId()) + .orElse(null); + Mission mission = missionService.findMissionById(studylogRequest.getMissionId()) + .orElse(null); StudylogTemp requestedStudylogTemp = new StudylogTemp(member, - studylogRequest.getTitle(), - studylogRequest.getContent(), - session, - mission, - tags.getList()); + studylogRequest.getTitle(), + studylogRequest.getContent(), + session, + mission, + tags.getList()); deleteStudylogTemp(memberId); StudylogTemp createdStudylogTemp = studylogTempRepository.save(requestedStudylogTemp); @@ -124,7 +163,8 @@ private void onStudylogCreatedEvent(Member foundMember, Tags tags, Studylog crea studylogDocumentService.save(createdStudylog.toStudylogDocument()); } - public StudylogsResponse findStudylogs(StudylogsSearchRequest request, Long memberId, boolean isAnonymousMember) { + public StudylogsResponse findStudylogs(StudylogsSearchRequest request, Long memberId, + boolean isAnonymousMember) { StudylogsResponse studylogs = findStudylogs(request, memberId); if (isAnonymousMember) { return studylogs; @@ -149,7 +189,8 @@ public StudylogsResponse findStudylogs(StudylogsSearchRequest request, Long memb Pageable pageable = request.getPageable(); List ids = request.getIds(); - Page studylogs = studylogRepository.findByIdInAndDeletedFalseOrderByIdAsc(ids, pageable); + Page studylogs = studylogRepository.findByIdInAndDeletedFalseOrderByIdAsc(ids, + pageable); return StudylogsResponse.of(studylogs, memberId); } @@ -173,7 +214,8 @@ public StudylogsResponse findStudylogs(StudylogsSearchRequest request, Long memb request.getPageable() ); - final List studylogs = studylogRepository.findByIdInAndDeletedFalseOrderByIdDesc(response.getStudylogIds()); + final List studylogs = studylogRepository.findByIdInAndDeletedFalseOrderByIdDesc( + response.getStudylogIds()); return StudylogsResponse.of( studylogs, response.getTotalSize(), @@ -228,7 +270,8 @@ private void deleteStudylogTemp(Long memberId) { } @Transactional - public StudylogResponse retrieveStudylogById(LoginMember loginMember, Long studylogId, boolean isViewed) { + public StudylogResponse retrieveStudylogById(LoginMember loginMember, Long studylogId, + boolean isViewed) { Studylog studylog = findStudylogById(studylogId); @@ -275,8 +318,17 @@ private StudylogResponse toStudylogResponse(LoginMember loginMember, Studylog st boolean liked = studylog.likedByMember(loginMember.getId()); boolean read = studylogReadRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId()).isPresent(); boolean scraped = studylogScrapRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId()).isPresent(); + List abilityResponses = findAbilityByStudylogId(studylog.getId()); + + return StudylogResponse.of(studylog, abilityResponses, scraped, read, liked); + } - return StudylogResponse.of(studylog, scraped, read, liked); + private List findAbilityByStudylogId(Long studylogId) { + List studylogAbilities = studylogAbilityRepository.findAllByStudylogId(studylogId); + List abilities = studylogAbilities.stream() + .map(StudylogAbility::getAbility) + .collect(Collectors.toList()); + return AbilityResponse.listOf(abilities); } public StudylogResponse findByIdAndReturnStudylogResponse(Long id) { @@ -306,6 +358,13 @@ private void increaseViewCount(LoginMember loginMember, Studylog studylog) { @Transactional public void updateStudylog(Long memberId, Long studylogId, StudylogRequest studylogRequest) { + Set abilities = abilityService.findByIdIn(memberId, + studylogRequest.getAbilities()); + + if (hasChildAndParentAbility(abilities)) { + throw new IllegalArgumentException("자식 역량이 존재하는 경우 부모 역량을 선택할 수 없습니다."); + } + Studylog studylog = studylogRepository.findById(studylogId).orElseThrow(StudylogNotFoundException::new); studylog.validateBelongTo(memberId); @@ -319,6 +378,13 @@ public void updateStudylog(Long memberId, Long studylogId, StudylogRequest study studylog.update(studylogRequest.getTitle(), studylogRequest.getContent(), session, mission, newTags); memberTagService.updateMemberTag(originalTags, newTags, foundMember); + List studylogAbilities = abilities.stream() + .map(it -> new StudylogAbility(memberId, it, studylog)) + .collect(Collectors.toList()); + + studylogAbilityRepository.deleteByStudylogId(studylog.getId()); + studylogAbilityRepository.saveAll(studylogAbilities); + studylogDocumentService.update(studylog.toStudylogDocument()); } diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentChangeRequest.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentChangeRequest.java new file mode 100644 index 000000000..16223c159 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentChangeRequest.java @@ -0,0 +1,20 @@ +package wooteco.prolog.studylog.application.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentChangeRequest { + + private String content; + + public CommentChangeRequest(String content) { + this.content = content; + } + + public CommentUpdateRequest toUpdateRequest(Long id, Long studylogId, Long commentId) { + return new CommentUpdateRequest(id, studylogId, commentId, content); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentCreateRequest.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentCreateRequest.java new file mode 100644 index 000000000..0977a1777 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentCreateRequest.java @@ -0,0 +1,20 @@ +package wooteco.prolog.studylog.application.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentCreateRequest { + + private String content; + + public CommentCreateRequest(String content) { + this.content = content; + } + + public CommentSaveRequest toSaveRequest(Long memberId, Long studylogId) { + return new CommentSaveRequest(memberId, studylogId, content); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentMemberResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentMemberResponse.java new file mode 100644 index 000000000..7c2015fb2 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentMemberResponse.java @@ -0,0 +1,25 @@ +package wooteco.prolog.studylog.application.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentMemberResponse { + + private Long id; + private String username; + private String nickname; + private String imageUrl; + private String role; + + public CommentMemberResponse(Long id, String username, String nickname, String imageUrl, + String role) { + this.id = id; + this.username = username; + this.nickname = nickname; + this.imageUrl = imageUrl; + this.role = role; + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentResponse.java new file mode 100644 index 000000000..b59f8205c --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentResponse.java @@ -0,0 +1,39 @@ +package wooteco.prolog.studylog.application.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.studylog.domain.Comment; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentResponse { + + private Long id; + private CommentMemberResponse author; + private String content; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime createAt; + + public CommentResponse(Long id, CommentMemberResponse author, String content, LocalDateTime localDateTime) { + this.id = id; + this.author = author; + this.content = content; + this.createAt = localDateTime; + } + + public static CommentResponse of(Comment comment) { + return new CommentResponse( + comment.getId(), + new CommentMemberResponse( + comment.getMemberId(), + comment.getMemberUsername(), + comment.getMemberNickName(), + comment.getMemberImageUrl(), + comment.getMemberRole()), + comment.getContent(), + comment.getCreatedAt()); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentSaveRequest.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentSaveRequest.java new file mode 100644 index 000000000..a24681e4a --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentSaveRequest.java @@ -0,0 +1,27 @@ +package wooteco.prolog.studylog.application.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.studylog.domain.Comment; +import wooteco.prolog.studylog.domain.Studylog; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentSaveRequest { + + private Long memberId; + private Long studylogId; + private String content; + + public CommentSaveRequest(Long memberId, Long studylogId, String content) { + this.memberId = memberId; + this.studylogId = studylogId; + this.content = content; + } + + public Comment toEntity(Member member, Studylog studylog) { + return new Comment(null, member, studylog, content); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentUpdateRequest.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentUpdateRequest.java new file mode 100644 index 000000000..c23d8e325 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentUpdateRequest.java @@ -0,0 +1,22 @@ +package wooteco.prolog.studylog.application.dto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentUpdateRequest { + + private Long memberId; + private Long studylogId; + private Long commentId; + private String content; + + public CommentUpdateRequest(Long memberId, Long studylogId, Long commentId, String content) { + this.memberId = memberId; + this.studylogId = studylogId; + this.commentId = commentId; + this.content = content; + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentsResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentsResponse.java new file mode 100644 index 000000000..4c3604e1a --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/CommentsResponse.java @@ -0,0 +1,17 @@ +package wooteco.prolog.studylog.application.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentsResponse { + + private List data; + + public CommentsResponse(List data) { + this.data = data; + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/PopularStudylogsResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/PopularStudylogsResponse.java index 79c4b832d..ccf850161 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/PopularStudylogsResponse.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/PopularStudylogsResponse.java @@ -9,7 +9,13 @@ @Getter public class PopularStudylogsResponse { - private StudylogsWithScrapCountResponse allResponse; - private StudylogsWithScrapCountResponse frontResponse; - private StudylogsWithScrapCountResponse backResponse; + private StudylogsResponse allResponse; + private StudylogsResponse frontResponse; + private StudylogsResponse backResponse; + + public static PopularStudylogsResponse of(StudylogsResponse all, + StudylogsResponse frontend, + StudylogsResponse backend) { + return new PopularStudylogsResponse(all, frontend, backend); + } } diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogRequest.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogRequest.java index 2c3f1f239..b571dc25d 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogRequest.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogRequest.java @@ -1,5 +1,6 @@ package wooteco.prolog.studylog.application.dto; +import java.util.ArrayList; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; @@ -15,11 +16,13 @@ public class StudylogRequest { private Long sessionId; private Long missionId; private List tags; + private List abilities; public StudylogRequest(String title, String content, Long missionId, List tags) { this.title = title; this.content = content; this.missionId = missionId; this.tags = tags; + this.abilities = new ArrayList<>(); } } diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogResponse.java index c626c00d3..7e3eab39c 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogResponse.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogResponse.java @@ -3,10 +3,13 @@ import static java.util.stream.Collectors.toList; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import wooteco.prolog.ability.application.dto.AbilityResponse; import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.session.application.dto.MissionResponse; import wooteco.prolog.session.application.dto.SessionResponse; @@ -29,6 +32,7 @@ public class StudylogResponse { private String title; private String content; private List tags; + private List abilities; private boolean scrap; private boolean read; private int viewCount; @@ -36,26 +40,53 @@ public class StudylogResponse { private int likesCount; public StudylogResponse( - Studylog studylog, - SessionResponse sessionResponse, - MissionResponse missionResponse, - List tagResponses, - boolean liked) { + Studylog studylog, + SessionResponse sessionResponse, + MissionResponse missionResponse, + List tagResponses, + boolean liked) { this( - studylog.getId(), - MemberResponse.of(studylog.getMember()), - studylog.getCreatedAt(), - studylog.getUpdatedAt(), - sessionResponse, - missionResponse, - studylog.getTitle(), - studylog.getContent(), - tagResponses, - false, - false, - studylog.getViewCount(), - liked, - studylog.getLikeCount() + studylog.getId(), + MemberResponse.of(studylog.getMember()), + studylog.getCreatedAt(), + studylog.getUpdatedAt(), + sessionResponse, + missionResponse, + studylog.getTitle(), + studylog.getContent(), + tagResponses, + Collections.emptyList(), + false, + false, + studylog.getViewCount(), + liked, + studylog.getLikeCount() + ); + } + + public StudylogResponse( + Studylog studylog, + SessionResponse sessionResponse, + MissionResponse missionResponse, + List tagResponses, + List abilityResponses, + boolean liked) { + this( + studylog.getId(), + MemberResponse.of(studylog.getMember()), + studylog.getCreatedAt(), + studylog.getUpdatedAt(), + sessionResponse, + missionResponse, + studylog.getTitle(), + studylog.getContent(), + tagResponses, + abilityResponses, + false, + false, + studylog.getViewCount(), + liked, + studylog.getLikeCount() ); } @@ -64,20 +95,44 @@ public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read List tagResponses = toTagResponses(studylogTags); return new StudylogResponse( - studylog.getId(), - MemberResponse.of(studylog.getMember()), - studylog.getCreatedAt(), - studylog.getUpdatedAt(), - SessionResponse.of(studylog.getSession()), - MissionResponse.of(studylog.getMission()), - studylog.getTitle(), - studylog.getContent(), - tagResponses, - scrap, - read, - studylog.getViewCount(), - liked, - studylog.getLikeCount() + studylog.getId(), + MemberResponse.of(studylog.getMember()), + studylog.getCreatedAt(), + studylog.getUpdatedAt(), + SessionResponse.of(studylog.getSession()), + MissionResponse.of(studylog.getMission()), + studylog.getTitle(), + studylog.getContent(), + tagResponses, + Collections.emptyList(), + scrap, + read, + studylog.getViewCount(), + liked, + studylog.getLikeCount() + ); + } + + public static StudylogResponse of(Studylog studylog, List abilityResponses, boolean scrap, boolean read, boolean liked) { + List studylogTags = studylog.getStudylogTags(); + List tagResponses = toTagResponses(studylogTags); + + return new StudylogResponse( + studylog.getId(), + MemberResponse.of(studylog.getMember()), + studylog.getCreatedAt(), + studylog.getUpdatedAt(), + SessionResponse.of(studylog.getSession()), + MissionResponse.of(studylog.getMission()), + studylog.getTitle(), + studylog.getContent(), + tagResponses, + abilityResponses, + scrap, + read, + studylog.getViewCount(), + liked, + studylog.getLikeCount() ); } @@ -86,40 +141,50 @@ public static StudylogResponse of(Studylog studylog) { return of(studylog, false, false, null); } + public static StudylogResponse of(Studylog studylog, List abilityResponses) { + return of(studylog, abilityResponses, false, false, null); + } + + public static StudylogResponse of(Studylog studylog, List abilityResponses, boolean scrap, boolean read, Long memberId) { + return StudylogResponse.of(studylog, abilityResponses, scrap, read, studylog.likedByMember(memberId)); + } + public static StudylogResponse of(Studylog studylog, Long memberId) { return of(studylog, false, false, memberId); } public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read, Long memberId) { - return StudylogResponse.of(studylog, scrap, read, studylog.likedByMember(memberId)); + return StudylogResponse.of(studylog, Collections.emptyList(), scrap, read, studylog.likedByMember(memberId)); } private static List toTagResponses(List studylogTags) { return studylogTags.stream() - .map(StudylogTag::getTag) - .map(TagResponse::of) - .collect(toList()); + .map(StudylogTag::getTag) + .map(TagResponse::of) + .collect(toList()); } - public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read, boolean liked, Session session, Mission mission) { + public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read, boolean liked, Session session, + Mission mission) { List studylogTags = studylog.getStudylogTags(); List tagResponses = toTagResponses(studylogTags); return new StudylogResponse( - studylog.getId(), - MemberResponse.of(studylog.getMember()), - studylog.getCreatedAt(), - studylog.getUpdatedAt(), - SessionResponse.of(session), - MissionResponse.of(mission), - studylog.getTitle(), - studylog.getContent(), - tagResponses, - scrap, - read, - studylog.getViewCount(), - liked, - studylog.getLikeCount() + studylog.getId(), + MemberResponse.of(studylog.getMember()), + studylog.getCreatedAt(), + studylog.getUpdatedAt(), + SessionResponse.of(session), + MissionResponse.of(mission), + studylog.getTitle(), + studylog.getContent(), + tagResponses, + Collections.emptyList(), + scrap, + read, + studylog.getViewCount(), + liked, + studylog.getLikeCount() ); } diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogsResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogsResponse.java index 97487cc52..a6af78cc6 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogsResponse.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogsResponse.java @@ -88,5 +88,4 @@ private static List toResponse(List tags) { .map(TagResponse::of) .collect(Collectors.toList()); } - } diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/Badge.java b/backend/src/main/java/wooteco/prolog/studylog/domain/Badge.java deleted file mode 100644 index 80cef1e22..000000000 --- a/backend/src/main/java/wooteco/prolog/studylog/domain/Badge.java +++ /dev/null @@ -1,21 +0,0 @@ -package wooteco.prolog.studylog.domain; - -public class Badge { - - private final String name; - - public Badge(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return "Badge{" + - "name='" + name + '\'' + - '}'; - } -} diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/BadgeType.java b/backend/src/main/java/wooteco/prolog/studylog/domain/BadgeType.java deleted file mode 100644 index 587b03440..000000000 --- a/backend/src/main/java/wooteco/prolog/studylog/domain/BadgeType.java +++ /dev/null @@ -1,7 +0,0 @@ -package wooteco.prolog.studylog.domain; - -public enum BadgeType { - - PASSION_KING, - COMPLIMENT_KING; -} diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/Comment.java b/backend/src/main/java/wooteco/prolog/studylog/domain/Comment.java new file mode 100644 index 000000000..6c3cac91e --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/domain/Comment.java @@ -0,0 +1,80 @@ +package wooteco.prolog.studylog.domain; + +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.common.AuditingEntity; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.studylog.exception.CommentDeleteException; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Comment extends AuditingEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "studylog_id") + private Studylog studylog; + + @Column(nullable = false, columnDefinition = "text") + private String content; + + @Column(nullable = false) + private boolean isDelete; + + public Comment(Long id, Member member, Studylog studylog, String content) { + this.id = id; + this.member = member; + this.studylog = studylog; + this.content = Objects.requireNonNull(content); + } + + public void updateContent(String content) { + this.content = content; + } + + public void delete() { + if (isDelete) { + throw new CommentDeleteException(); + } + + this.isDelete = true; + } + + public Long getMemberId() { + return member.getId(); + } + + public String getMemberUsername() { + return member.getUsername(); + } + + public String getMemberNickName() { + return member.getNickname(); + } + + public String getMemberImageUrl() { + return member.getImageUrl(); + } + + public String getMemberRole() { + return member.getRole().name(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/Studylog.java b/backend/src/main/java/wooteco/prolog/studylog/domain/Studylog.java index 9bbc20996..884fbf01a 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/domain/Studylog.java +++ b/backend/src/main/java/wooteco/prolog/studylog/domain/Studylog.java @@ -199,8 +199,4 @@ public void updateSession(Session session) { public void updateMission(Mission mission) { this.mission = mission; } - - public boolean isContainsCurriculum(Curriculum curriculum) { - return this.session.isSameAs(curriculum); - } } diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/repository/CommentRepository.java b/backend/src/main/java/wooteco/prolog/studylog/domain/repository/CommentRepository.java new file mode 100644 index 000000000..d33daa4fa --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/domain/repository/CommentRepository.java @@ -0,0 +1,17 @@ +package wooteco.prolog.studylog.domain.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import wooteco.prolog.studylog.domain.Comment; +import wooteco.prolog.studylog.domain.Studylog; + +public interface CommentRepository extends JpaRepository { + + @Query("SELECT c FROM Comment c" + + " JOIN FETCH c.studylog s" + + " JOIN FETCH c.member m" + + " WHERE s = :studylog" + + " AND c.isDelete = false") + List findCommentByStudylog(Studylog studylog); +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/domain/repository/StudylogRepository.java b/backend/src/main/java/wooteco/prolog/studylog/domain/repository/StudylogRepository.java index 94d6573ec..204925e7c 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/domain/repository/StudylogRepository.java +++ b/backend/src/main/java/wooteco/prolog/studylog/domain/repository/StudylogRepository.java @@ -32,4 +32,6 @@ public interface StudylogRepository extends JpaRepository, JpaSp List findByMemberIdAndCreatedAtBetween(Long memberId, LocalDateTime startDate, LocalDateTime endDate); Page findByIdInAndDeletedFalseOrderByIdAsc(List ids, Pageable pageable); + + Page findAllByIdIn(List ids, Pageable pageable); } diff --git a/backend/src/main/java/wooteco/prolog/studylog/exception/CommentDeleteException.java b/backend/src/main/java/wooteco/prolog/studylog/exception/CommentDeleteException.java new file mode 100644 index 000000000..ec89120f0 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/exception/CommentDeleteException.java @@ -0,0 +1,6 @@ +package wooteco.prolog.studylog.exception; + +import wooteco.prolog.common.exception.BadRequestException; + +public class CommentDeleteException extends BadRequestException { +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/exception/CommentNotFoundException.java b/backend/src/main/java/wooteco/prolog/studylog/exception/CommentNotFoundException.java new file mode 100644 index 000000000..8f25f55ed --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/exception/CommentNotFoundException.java @@ -0,0 +1,7 @@ +package wooteco.prolog.studylog.exception; + +import wooteco.prolog.common.exception.BadRequestException; + +public class CommentNotFoundException extends BadRequestException { + +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/ui/CommentController.java b/backend/src/main/java/wooteco/prolog/studylog/ui/CommentController.java new file mode 100644 index 000000000..052367ecf --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/ui/CommentController.java @@ -0,0 +1,67 @@ +package wooteco.prolog.studylog.ui; + +import java.net.URI; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wooteco.prolog.login.domain.AuthMemberPrincipal; +import wooteco.prolog.login.ui.LoginMember; +import wooteco.prolog.studylog.application.CommentService; +import wooteco.prolog.studylog.application.dto.CommentChangeRequest; +import wooteco.prolog.studylog.application.dto.CommentCreateRequest; +import wooteco.prolog.studylog.application.dto.CommentsResponse; + +@RestController +@RequestMapping("/studylogs") +public class CommentController { + + private final CommentService commentService; + + public CommentController(CommentService commentService) { + this.commentService = commentService; + } + + @PostMapping("/{studylogId}/comments") + public ResponseEntity createComment(@AuthMemberPrincipal LoginMember loginMember, + @PathVariable Long studylogId, + @RequestBody CommentCreateRequest request) { + Long commentId = commentService.insertComment( + request.toSaveRequest(loginMember.getId(), studylogId)); + + return ResponseEntity.created( + URI.create("/studylogs/" + studylogId + "/comments/" + commentId)).build(); + } + + @GetMapping("/{studylogId}/comments") + public ResponseEntity showComments(@PathVariable Long studylogId) { + CommentsResponse commentsResponse = commentService.findComments(studylogId); + + return ResponseEntity.ok(commentsResponse); + } + + @PutMapping("/{studylogId}/comments/{commentId}") + public ResponseEntity changeComment(@AuthMemberPrincipal LoginMember loginMember, + @PathVariable Long studylogId, + @PathVariable Long commentId, + @RequestBody CommentChangeRequest request) { + commentService.updateComment(request.toUpdateRequest(loginMember.getId(), studylogId, commentId)); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @DeleteMapping("/{studylogId}/comments/{commentId}") + public ResponseEntity deleteComment(@AuthMemberPrincipal LoginMember loginMember, + @PathVariable Long studylogId, + @PathVariable Long commentId) { + commentService.deleteComment(loginMember.getId(), studylogId, commentId); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/ui/PopularStudylogController.java b/backend/src/main/java/wooteco/prolog/studylog/ui/PopularStudylogController.java index b90f7146e..ed5346c83 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/ui/PopularStudylogController.java +++ b/backend/src/main/java/wooteco/prolog/studylog/ui/PopularStudylogController.java @@ -11,7 +11,6 @@ import wooteco.prolog.login.ui.LoginMember; import wooteco.prolog.studylog.application.PopularStudylogService; import wooteco.prolog.studylog.application.dto.PopularStudylogsResponse; -import wooteco.prolog.studylog.application.dto.StudylogsResponse; @RestController @RequestMapping("/studylogs") @@ -31,8 +30,7 @@ public ResponseEntity updatePopularStudylogs(@PageableDefault Pageable pag @GetMapping("/popular") public ResponseEntity showPopularStudylogs(@AuthMemberPrincipal LoginMember member, @PageableDefault Pageable pageable) { - final PopularStudylogsResponse popularStudylogs = popularStudylogService - .findPopularStudylogs(pageable, member.getId(), member.isAnonymous()); - return ResponseEntity.ok(popularStudylogs); + PopularStudylogsResponse studylogsResponse = popularStudylogService.findPopularStudylogs(pageable, member.getId(), member.isAnonymous()); + return ResponseEntity.ok(studylogsResponse); } } diff --git a/backend/src/main/java/wooteco/prolog/studylog/ui/ProfileStudylogController.java b/backend/src/main/java/wooteco/prolog/studylog/ui/ProfileStudylogController.java index 635ee4c43..ef94010f8 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/ui/ProfileStudylogController.java +++ b/backend/src/main/java/wooteco/prolog/studylog/ui/ProfileStudylogController.java @@ -2,10 +2,8 @@ import java.time.LocalDate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Data; import org.springframework.data.domain.Pageable; @@ -25,12 +23,8 @@ import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.member.application.dto.ProfileIntroRequest; import wooteco.prolog.member.application.dto.ProfileIntroResponse; -import wooteco.prolog.studylog.application.BadgeService; import wooteco.prolog.studylog.application.StudylogService; -import wooteco.prolog.studylog.application.dto.BadgeResponse; -import wooteco.prolog.studylog.application.dto.BadgesResponse; import wooteco.prolog.studylog.application.dto.StudylogsResponse; -import wooteco.prolog.studylog.domain.BadgeType; @RestController @AllArgsConstructor @@ -39,7 +33,6 @@ public class ProfileStudylogController { private StudylogService studylogService; private MemberService memberService; - private BadgeService badgeService; @Deprecated @GetMapping(value = "/{username}/posts", produces = MediaType.APPLICATION_JSON_VALUE) @@ -99,16 +92,6 @@ public ResponseEntity findMemberProfileIntro(@AuthMemberPrincipal LoginMem return ResponseEntity.ok().build(); } - @GetMapping(value = "/{username}/badges", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity findMemberBadges(@PathVariable String username) { - List badges = badgeService.findBadges(username, Arrays.asList(10L, 11L)); - List badgeResponses = badges.stream() - .map(BadgeType::toString) - .map(BadgeResponse::new) - .collect(Collectors.toList()); - return ResponseEntity.ok(new BadgesResponse(badgeResponses)); - } - @Data public static class StudylogFilterRequest { diff --git a/backend/src/main/java/wooteco/prolog/studylog/ui/StudylogController.java b/backend/src/main/java/wooteco/prolog/studylog/ui/StudylogController.java index fbe5549ba..f5e323349 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/ui/StudylogController.java +++ b/backend/src/main/java/wooteco/prolog/studylog/ui/StudylogController.java @@ -1,10 +1,17 @@ package wooteco.prolog.studylog.ui; import java.net.URI; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; +import javax.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import wooteco.prolog.login.aop.MemberOnly; import wooteco.prolog.login.domain.AuthMemberPrincipal; import wooteco.prolog.login.ui.LoginMember; @@ -13,15 +20,12 @@ import wooteco.prolog.studylog.application.dto.StudylogRequest; import wooteco.prolog.studylog.application.dto.StudylogResponse; import wooteco.prolog.studylog.application.dto.StudylogTempResponse; -import wooteco.prolog.studylog.application.dto.StudylogWithScrapedCountResponse; import wooteco.prolog.studylog.application.dto.StudylogsResponse; import wooteco.prolog.studylog.application.dto.search.SearchParams; import wooteco.prolog.studylog.application.dto.search.StudylogsSearchRequest; import wooteco.prolog.studylog.exception.StudylogNotFoundException; import wooteco.support.number.NumberUtils; -import javax.servlet.http.HttpServletResponse; - @RestController @RequestMapping("/studylogs") public class StudylogController { @@ -62,7 +66,7 @@ public ResponseEntity showAll(@AuthMemberPrincipal LoginMembe } @GetMapping("/{id}") - public ResponseEntity showStudylog(@PathVariable String id, @AuthMemberPrincipal LoginMember member, + public ResponseEntity showStudylog(@PathVariable String id, @AuthMemberPrincipal LoginMember member, @CookieValue(name = "viewed", required = false, defaultValue = "/") String viewedStudyLogs, HttpServletResponse httpServletResponse) { @@ -71,7 +75,7 @@ public ResponseEntity showStudylog(@PathVariab } viewedStudyLogCookieGenerator.setViewedStudyLogCookie(viewedStudyLogs, id, httpServletResponse); - return ResponseEntity.ok(studylogService.retrieveStudylogByIdWithScrapedCount(member, Long.parseLong(id), + return ResponseEntity.ok(studylogService.retrieveStudylogById(member, Long.parseLong(id), viewedStudyLogCookieGenerator.isViewed(viewedStudyLogs, id))); } diff --git a/backend/src/main/java/wooteco/prolog/update/UpdatedContentsRepository.java b/backend/src/main/java/wooteco/prolog/update/UpdatedContentsRepository.java index 38479c42f..58fe9bf15 100644 --- a/backend/src/main/java/wooteco/prolog/update/UpdatedContentsRepository.java +++ b/backend/src/main/java/wooteco/prolog/update/UpdatedContentsRepository.java @@ -3,9 +3,10 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface UpdatedContentsRepository extends JpaRepository { @Query("select uc from UpdatedContents uc where uc.updateContent = :content") - Optional findByContent(UpdateContent content); + Optional findByContent(@Param("content") UpdateContent content); } diff --git a/backend/src/main/resources/db/migration/prod/V26__create_comment_table.sql b/backend/src/main/resources/db/migration/prod/V26__create_comment_table.sql new file mode 100644 index 000000000..e8b22d6e3 --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V26__create_comment_table.sql @@ -0,0 +1,16 @@ +CREATE TABLE comment( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + studylog_id BIGINT NOT NULL, + content TEXT(255) NOT NULL, + is_delete TINYINT(1) DEFAULT 0 NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB; + +ALTER TABLE comment + ADD CONSTRAINT FK_COMMENT_MEMBER + FOREIGN KEY (member_id) REFERENCES member (id); + +ALTER TABLE comment + ADD CONSTRAINT FK_COMMENT_STUDYLOG + FOREIGN KEY (studylog_id) REFERENCES studylog (id); diff --git a/backend/src/main/resources/db/migration/prod/V27__alter_comment_table.sql b/backend/src/main/resources/db/migration/prod/V27__alter_comment_table.sql new file mode 100644 index 000000000..291bf144d --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V27__alter_comment_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE comment ADD COLUMN `created_at` datetime NOT NULL; +ALTER TABLE comment ADD COLUMN `updated_at` datetime; diff --git a/backend/src/main/resources/db/migration/prod/V28__create_levellog_table.sql b/backend/src/main/resources/db/migration/prod/V28__create_levellog_table.sql new file mode 100644 index 000000000..f0702239c --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V28__create_levellog_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE level_log( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + title VARCHAR(50) NOT NULL, + content TEXT(255) NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB; + +ALTER TABLE level_log + ADD CONSTRAINT FK_LEVEL_LOG_MEMBER + FOREIGN KEY (member_id) REFERENCES member (id); diff --git a/backend/src/main/resources/db/migration/prod/V29__alter_levellog_table.sql b/backend/src/main/resources/db/migration/prod/V29__alter_levellog_table.sql new file mode 100644 index 000000000..82c8dbb9b --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V29__alter_levellog_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE level_log ADD COLUMN `created_at` datetime NOT NULL; +ALTER TABLE level_log ADD COLUMN `updated_at` datetime; diff --git a/backend/src/main/resources/db/migration/prod/V30__create_self_discussion_table.sql b/backend/src/main/resources/db/migration/prod/V30__create_self_discussion_table.sql new file mode 100644 index 000000000..f038a6a57 --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V30__create_self_discussion_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE self_discussion +( + id BIGINT NOT NULL AUTO_INCREMENT, + level_log_id BIGINT NOT NULL, + question VARCHAR(255) NOT NULL, + answer TEXT NOT NULL, + CONSTRAINT pk_self_discussion PRIMARY KEY (id) +) ENGINE = InnoDB; + + +ALTER TABLE self_discussion + ADD CONSTRAINT FK_self_discussion_level_log + FOREIGN KEY (level_log_id) REFERENCES level_log (id); \ No newline at end of file diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html index 66c713193..65735db60 100644 --- a/backend/src/main/resources/static/index.html +++ b/backend/src/main/resources/static/index.html @@ -471,6 +471,14 @@

Prolog API Document

  • 스터디로그 삭제
  • +
  • 댓글 + +
  • 미션
    • 미션 생성 성공
    • @@ -558,13 +566,13 @@

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:50 GMT +Date: Sun, 21 Aug 2022 11:36:13 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 168 { - "accessToken" : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDExLCJleHAiOjE2NTc0NjU2MTEsInJvbGUiOiJDUkVXIn0.52VaLgNnZBOclcYJIZWwKJ88_pmkryaNjkPtUKos0vI" + "accessToken" : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzc0LCJleHAiOjE2NjEwODUzNzQsInJvbGUiOiJDUkVXIn0.SgjmnG2Cdl62iIYiOPOZUTUbI6bi6FXWZ-XzewCTIV8" } @@ -577,7 +585,7 @@

      Request

      GET /members/me HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDExLCJleHAiOjE2NTc0NjU2MTEsInJvbGUiOiJDUkVXIn0.52VaLgNnZBOclcYJIZWwKJ88_pmkryaNjkPtUKos0vI
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzczLCJleHAiOjE2NjEwODUzNzMsInJvbGUiOiJDUkVXIn0.jJRo5GQjiRBOqzTJIGiKq3rCaKcNOMpR5YEJlILzv5w
       Host: localhost:4000
      @@ -592,7 +600,7 @@

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:50 GMT +Date: Sun, 21 Aug 2022 11:36:12 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 153 @@ -615,7 +623,7 @@

      Request

      GET /members/soulG HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDExLCJleHAiOjE2NTc0NjU2MTEsInJvbGUiOiJDUkVXIn0.52VaLgNnZBOclcYJIZWwKJ88_pmkryaNjkPtUKos0vI
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzczLCJleHAiOjE2NjEwODUzNzMsInJvbGUiOiJDUkVXIn0.jJRo5GQjiRBOqzTJIGiKq3rCaKcNOMpR5YEJlILzv5w
       Host: localhost:4000
      @@ -630,7 +638,7 @@

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:50 GMT +Date: Sun, 21 Aug 2022 11:36:12 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 153 @@ -653,7 +661,7 @@

      Request

      PUT /members/soulG HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDEwLCJleHAiOjE2NTc0NjU2MTAsInJvbGUiOiJDUkVXIn0.Yre5XZ1efV_m0X9NNeahhRKRdsl2wwpegJO2Ffqooe8
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzcyLCJleHAiOjE2NjEwODUzNzIsInJvbGUiOiJDUkVXIn0.QSXQLyRfqaz29gbtmkD78SoLVwY6Fe27eHHCaKTScPg
       Content-Type: application/json; charset=UTF-8
       Host: localhost:4000
       Content-Length: 104
      @@ -673,7 +681,7 @@ 

      Response

      Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers -Date: Sun, 10 Jul 2022 14:06:50 GMT +Date: Sun, 21 Aug 2022 11:36:12 GMT Keep-Alive: timeout=60 Connection: keep-alive
      @@ -706,7 +714,7 @@

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:47 GMT +Date: Sun, 21 Aug 2022 11:36:02 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 153 @@ -743,7 +751,7 @@

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:47 GMT +Date: Sun, 21 Aug 2022 11:36:02 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 19 @@ -762,7 +770,7 @@

      Request

      PUT /members/soulG/profile-intro HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA4LCJleHAiOjE2NTc0NjU2MDgsInJvbGUiOiJDUkVXIn0.Kh9ZEChq5o689qJ--qw5bv0ycoR73EtnKBefrs9uP90
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzYyLCJleHAiOjE2NjEwODUzNjIsInJvbGUiOiJDUkVXIn0.6vMTWPeBDGhsPW2iANPJsy-lZGsKl7y970JhBEL6GYc
       Content-Type: application/json; charset=UTF-8
       Host: localhost:4000
       Content-Length: 47
      @@ -781,7 +789,7 @@ 

      Response

      Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers -Date: Sun, 10 Jul 2022 14:06:47 GMT +Date: Sun, 21 Aug 2022 11:36:02 GMT Keep-Alive: timeout=60 Connection: keep-alive
      @@ -810,10 +818,10 @@

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:47 GMT +Date: Sun, 21 Aug 2022 11:36:01 GMT Keep-Alive: timeout=60 Connection: keep-alive -Content-Length: 1684 +Content-Length: 1730 { "data" : [ { @@ -825,8 +833,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:47.892927", - "updatedAt" : "2022-07-10T23:06:47.892927", + "createdAt" : "2022-08-21T20:36:01.753888", + "updatedAt" : "2022-08-21T20:36:01.753888", "session" : null, "mission" : { "id" : 2, @@ -845,6 +853,7 @@

      Response

      "id" : 4, "name" : "jpa" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -859,8 +868,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:47.848131", - "updatedAt" : "2022-07-10T23:06:47.848131", + "createdAt" : "2022-08-21T20:36:01.481996", + "updatedAt" : "2022-08-21T20:36:01.481996", "session" : null, "mission" : { "id" : 1, @@ -879,6 +888,7 @@

      Response

      "id" : 2, "name" : "router" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -956,6 +966,8 @@

      Request

      POST /studylogs HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA1LCJleHAiOjE2NTc0NjU2MDUsInJvbGUiOiJDUkVXIn0.IgrgpkJHnhhW87DzIpQ3_n9rq5D5-HNQhmgqn1v_tR0
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzU1LCJleHAiOjE2NjEwODUzNTUsInJvbGUiOiJDUkVXIn0.wAT90N-llfRVWLlihou9jk95lH4XwSFK7XsKW5lw2zY
       Content-Type: application/json; charset=UTF-8
       Host: localhost:4000
      -Content-Length: 239
      +Content-Length: 262
       
       {
         "title" : "나는야 Joanne",
      @@ -999,7 +1011,8 @@ 

      Request

      "name" : "spa" }, { "name" : "router" - } ] + } ], + "abilities" : [ 2 ] }
      @@ -1015,10 +1028,10 @@

      Response

      Location: /studylogs/1 Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:45 GMT +Date: Sun, 21 Aug 2022 11:35:55 GMT Keep-Alive: timeout=60 Connection: keep-alive -Content-Length: 867 +Content-Length: 1036 { "id" : 1, @@ -1029,8 +1042,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:45.71584", - "updatedAt" : "2022-07-10T23:06:45.71584", + "createdAt" : "2022-08-21T20:35:55.583786", + "updatedAt" : "2022-08-21T20:35:55.583786", "session" : { "id" : 1, "name" : "프론트엔드JS 레벨1 - 2021" @@ -1052,6 +1065,13 @@

      Response

      "id" : 2, "name" : "router" } ], + "abilities" : [ { + "id" : 2, + "name" : "자식 역량1", + "description" : "자식 역량1입니다", + "color" : "#ffffff", + "isParent" : false + } ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -1069,7 +1089,7 @@

      Request

      GET /studylogs HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA1LCJleHAiOjE2NTc0NjU2MDUsInJvbGUiOiJDUkVXIn0.IgrgpkJHnhhW87DzIpQ3_n9rq5D5-HNQhmgqn1v_tR0
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzUzLCJleHAiOjE2NjEwODUzNTMsInJvbGUiOiJDUkVXIn0.-VU-cLBxh1MfePFnt4Ymi-2lzAgxiI8SsdR42wPBjLI
       Accept: application/json
       Host: localhost:4000
      @@ -1085,10 +1105,10 @@

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:45 GMT +Date: Sun, 21 Aug 2022 11:35:53 GMT Keep-Alive: timeout=60 Connection: keep-alive -Content-Length: 1877 +Content-Length: 1923 { "data" : [ { @@ -1100,8 +1120,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:45.395224", - "updatedAt" : "2022-07-10T23:06:45.395224", + "createdAt" : "2022-08-21T20:35:54.402948", + "updatedAt" : "2022-08-21T20:35:54.402948", "session" : { "id" : 2, "name" : "백엔드Java 레벨1 - 2021" @@ -1123,6 +1143,7 @@

      Response

      "id" : 4, "name" : "jpa" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -1137,8 +1158,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:45.318991", - "updatedAt" : "2022-07-10T23:06:45.318991", + "createdAt" : "2022-08-21T20:35:53.949548", + "updatedAt" : "2022-08-21T20:35:53.949548", "session" : { "id" : 1, "name" : "프론트엔드JS 레벨1 - 2021" @@ -1160,6 +1181,7 @@

      Response

      "id" : 2, "name" : "router" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -1181,7 +1203,7 @@

      Request

      GET /studylogs/1 HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA0LCJleHAiOjE2NTc0NjU2MDQsInJvbGUiOiJDUkVXIn0.Vq5Nfd2MxehYc_pI9tDn7x4rap-DJWwFr9we8PAA_SQ
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzUyLCJleHAiOjE2NjEwODUzNTIsInJvbGUiOiJDUkVXIn0.UtcQAetiRDK9604Oe4Ycj9eX0_WSyYjhulq9uK-IQ4U
       Host: localhost:4000
      @@ -1194,54 +1216,52 @@

      Response

      Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers -Set-Cookie: viewed=/1/; Max-Age=3194; Expires=Sun, 10 Jul 2022 14:59:59 GMT; Secure; HttpOnly; SameSite=None +Set-Cookie: viewed=/1/; Max-Age=12246; Expires=Sun, 21 Aug 2022 14:59:59 GMT; Secure; HttpOnly; SameSite=None Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:44 GMT +Date: Sun, 21 Aug 2022 11:35:53 GMT Keep-Alive: timeout=60 Connection: keep-alive -Content-Length: 991 +Content-Length: 889 { - "studylogResponse" : { + "id" : 1, + "author" : { "id" : 1, - "author" : { - "id" : 1, - "username" : "soulG", - "nickname" : "소롱", - "role" : "CREW", - "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt" : "2022-07-10T23:06:44.881723", - "updatedAt" : "2022-07-10T23:06:44.881723", + "username" : "soulG", + "nickname" : "소롱", + "role" : "CREW", + "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" + }, + "createdAt" : "2022-08-21T20:35:53.097346", + "updatedAt" : "2022-08-21T20:35:53.097346", + "session" : { + "id" : 1, + "name" : "프론트엔드JS 레벨1 - 2021" + }, + "mission" : { + "id" : 1, + "name" : "세션1 - 지하철 노선도 미션", "session" : { "id" : 1, "name" : "프론트엔드JS 레벨1 - 2021" - }, - "mission" : { - "id" : 1, - "name" : "세션1 - 지하철 노선도 미션", - "session" : { - "id" : 1, - "name" : "프론트엔드JS 레벨1 - 2021" - } - }, - "title" : "나는야 Joanne", - "content" : "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", - "tags" : [ { - "id" : 1, - "name" : "spa" - }, { - "id" : 2, - "name" : "router" - } ], - "scrap" : false, - "read" : true, - "viewCount" : 0, - "liked" : false, - "likesCount" : 0 + } }, - "scrapedCount" : 0 + "title" : "나는야 Joanne", + "content" : "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", + "tags" : [ { + "id" : 1, + "name" : "spa" + }, { + "id" : 2, + "name" : "router" + } ], + "abilities" : [ ], + "scrap" : false, + "read" : true, + "viewCount" : 0, + "liked" : false, + "likesCount" : 0 }
      @@ -1254,10 +1274,10 @@

      Request

      PUT /studylogs/1 HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA2LCJleHAiOjE2NTc0NjU2MDYsInJvbGUiOiJDUkVXIn0.Fd4QAud6Oj6RvzrUtOOgmOrZMFbKmiPlJjoKtj7DrMc
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzU2LCJleHAiOjE2NjEwODUzNTYsInJvbGUiOiJDUkVXIn0.wBsVhKhJXbhTHxLfI8ELX8sZg1QxaqjivEDHlHohdac
       Content-Type: application/json; charset=UTF-8
       Host: localhost:4000
      -Content-Length: 177
      +Content-Length: 198
       
       {
         "title" : "수정된 제목",
      @@ -1268,7 +1288,8 @@ 

      Request

      "name" : "spa" }, { "name" : "edit" - } ] + } ], + "abilities" : [ ] }
      @@ -1281,7 +1302,7 @@

      Response

      Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers -Date: Sun, 10 Jul 2022 14:06:45 GMT +Date: Sun, 21 Aug 2022 11:35:56 GMT Keep-Alive: timeout=60 Connection: keep-alive
      @@ -1295,7 +1316,7 @@

      Request

      DELETE /studylogs/1 HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA1LCJleHAiOjE2NTc0NjU2MDUsInJvbGUiOiJDUkVXIn0.IgrgpkJHnhhW87DzIpQ3_n9rq5D5-HNQhmgqn1v_tR0
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzU2LCJleHAiOjE2NjEwODUzNTYsInJvbGUiOiJDUkVXIn0.wBsVhKhJXbhTHxLfI8ELX8sZg1QxaqjivEDHlHohdac
       Accept: application/json
       Host: localhost:4000
      @@ -1309,7 +1330,150 @@

      Response

      Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers -Date: Sun, 10 Jul 2022 14:06:45 GMT +Date: Sun, 21 Aug 2022 11:35:56 GMT +Keep-Alive: timeout=60 +Connection: keep-alive +
      + + + + + +
      +

      댓글

      +
      +
      +

      댓글 등록

      +
      +

      Request

      +
      +
      +
      POST /studylogs/1/comments HTTP/1.1
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzc1LCJleHAiOjE2NjEwODUzNzUsInJvbGUiOiJDUkVXIn0.svRMBX_1cjCcCzrGJ5uLiDkaWKFPjENojD7bYNaYrHU
      +Content-Type: application/json; charset=UTF-8
      +Host: localhost:4000
      +Content-Length: 46
      +
      +{
      +  "content" : "댓글의 내용입니다."
      +}
      +
      +
      +
      +
      +

      Response

      +
      +
      +
      HTTP/1.1 201 Created
      +Vary: Origin
      +Vary: Access-Control-Request-Method
      +Vary: Access-Control-Request-Headers
      +Location: /studylogs/1/comments/1
      +Date: Sun, 21 Aug 2022 11:36:15 GMT
      +Keep-Alive: timeout=60
      +Connection: keep-alive
      +
      +
      +
      +
      +
      +

      댓글 전체 조회

      +
      +

      Request

      +
      +
      +
      GET /studylogs/1/comments HTTP/1.1
      +Host: localhost:4000
      +
      +
      +
      +
      +

      Response

      +
      +
      +
      HTTP/1.1 200 OK
      +Vary: Origin
      +Vary: Access-Control-Request-Method
      +Vary: Access-Control-Request-Headers
      +Content-Type: application/json
      +Transfer-Encoding: chunked
      +Date: Sun, 21 Aug 2022 11:36:15 GMT
      +Keep-Alive: timeout=60
      +Connection: keep-alive
      +Content-Length: 324
      +
      +{
      +  "data" : [ {
      +    "id" : 1,
      +    "author" : {
      +      "id" : 1,
      +      "username" : "soulG",
      +      "nickname" : "소롱",
      +      "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4",
      +      "role" : "CREW"
      +    },
      +    "content" : "댓글의 내용입니다.",
      +    "createAt" : "2022-08-21T20:36:15.916395"
      +  } ]
      +}
      +
      +
      +
      +
      +
      +

      댓글 수정

      +
      +

      Request

      +
      +
      +
      PUT /studylogs/1/comments/1 HTTP/1.1
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzc1LCJleHAiOjE2NjEwODUzNzUsInJvbGUiOiJDUkVXIn0.svRMBX_1cjCcCzrGJ5uLiDkaWKFPjENojD7bYNaYrHU
      +Content-Type: application/json; charset=UTF-8
      +Host: localhost:4000
      +Content-Length: 56
      +
      +{
      +  "content" : "수정된 댓글의 내용입니다."
      +}
      +
      +
      +
      +
      +

      Response

      +
      +
      +
      HTTP/1.1 204 No Content
      +Vary: Origin
      +Vary: Access-Control-Request-Method
      +Vary: Access-Control-Request-Headers
      +Date: Sun, 21 Aug 2022 11:36:15 GMT
      +Keep-Alive: timeout=60
      +Connection: keep-alive
      +
      +
      +
      +
      +
      +

      댓글 삭제

      +
      +

      Request

      +
      +
      +
      DELETE /studylogs/1/comments/1 HTTP/1.1
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzc1LCJleHAiOjE2NjEwODUzNzUsInJvbGUiOiJDUkVXIn0.svRMBX_1cjCcCzrGJ5uLiDkaWKFPjENojD7bYNaYrHU
      +Host: localhost:4000
      +
      +
      +
      +
      +

      Response

      +
      +
      +
      HTTP/1.1 204 No Content
      +Vary: Origin
      +Vary: Access-Control-Request-Method
      +Vary: Access-Control-Request-Headers
      +Date: Sun, 21 Aug 2022 11:36:15 GMT
       Keep-Alive: timeout=60
       Connection: keep-alive
      @@ -1324,7 +1488,7 @@

      미션

      미션 생성 성공

      -

      Request

      +

      Request

      POST /missions HTTP/1.1
      @@ -1340,7 +1504,7 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 200 OK
      @@ -1349,7 +1513,7 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:47 GMT +Date: Sun, 21 Aug 2022 11:36:00 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 113 @@ -1369,7 +1533,7 @@

      Response

      미션 중복 생성 시 실패

      -

      Request

      +

      Request

      POST /missions HTTP/1.1
      @@ -1385,7 +1549,7 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 400 Bad Request
      @@ -1394,7 +1558,7 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:46 GMT +Date: Sun, 21 Aug 2022 11:35:58 GMT Connection: close Content-Length: 63 @@ -1409,7 +1573,7 @@

      Response

      미션 목록 조회

      -

      Request

      +

      Request

      GET /missions HTTP/1.1
      @@ -1418,16 +1582,18 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 200 OK
       Vary: Origin
       Vary: Access-Control-Request-Method
       Vary: Access-Control-Request-Headers
      +Access-Control-Expose-Headers: X-Total-Count
      +X-Total-Count: 2
       Content-Type: application/json
       Transfer-Encoding: chunked
      -Date: Sun, 10 Jul 2022 14:06:46 GMT
      +Date: Sun, 21 Aug 2022 11:36:00 GMT
       Keep-Alive: timeout=60
       Connection: keep-alive
       Content-Length: 209
      @@ -1459,7 +1625,7 @@ 

      태그

      태그 목록 조회

      -

      Request

      +

      Request

      GET /tags HTTP/1.1
      @@ -1468,16 +1634,18 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 200 OK
       Vary: Origin
       Vary: Access-Control-Request-Method
       Vary: Access-Control-Request-Headers
      +Access-Control-Expose-Headers: X-Total-Count
      +X-Total-Count: 2
       Content-Type: application/json
       Transfer-Encoding: chunked
      -Date: Sun, 10 Jul 2022 14:06:47 GMT
      +Date: Sun, 21 Aug 2022 11:36:03 GMT
       Keep-Alive: timeout=60
       Connection: keep-alive
       Content-Length: 79
      @@ -1585,7 +1753,7 @@ 

      스터디로

      멤버 태그 조회

      -

      Request

      +

      Request

      GET /members/gracefulBrown/tags HTTP/1.1
      @@ -1595,7 +1763,7 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 400 Bad Request
      @@ -1604,7 +1772,7 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:47 GMT +Date: Sun, 21 Aug 2022 11:36:00 GMT Connection: close Content-Length: 80 @@ -1619,7 +1787,7 @@

      Response

      멤버 달력 스터디로그 조회

      -

      Request

      +

      Request

      GET /members/gracefulBrown/calendar-studylogs?year=2021&month=8 HTTP/1.1
      @@ -1629,7 +1797,7 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 400 Bad Request
      @@ -1638,7 +1806,7 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:47 GMT +Date: Sun, 21 Aug 2022 11:36:01 GMT Connection: close Content-Length: 80 @@ -1653,17 +1821,17 @@

      Response

      인기있는 스터디로그 조회

      -

      Request

      +

      Request

      GET /studylogs/popular HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA2LCJleHAiOjE2NTc0NjU2MDYsInJvbGUiOiJDUkVXIn0.Fd4QAud6Oj6RvzrUtOOgmOrZMFbKmiPlJjoKtj7DrMc
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzU3LCJleHAiOjE2NjEwODUzNTcsInJvbGUiOiJDUkVXIn0.SqpFnRMnaiNJbLyTkWXqhZABAPoVz2DeOAqfT2iTxmM
       Host: localhost:4000
      -

      Response

      +

      Response

      HTTP/1.1 200 OK
      @@ -1672,98 +1840,14 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:46 GMT +Date: Sun, 21 Aug 2022 11:35:58 GMT Keep-Alive: timeout=60 Connection: keep-alive -Content-Length: 8744 +Content-Length: 6311 { "allResponse" : { "data" : [ { - "studylogResponse" : { - "id" : 2, - "author" : { - "id" : 1, - "username" : "soulG", - "nickname" : "소롱", - "role" : "CREW", - "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt" : "2022-07-10T23:06:46.533399", - "updatedAt" : "2022-07-10T23:06:46.533399", - "session" : { - "id" : 2, - "name" : "백엔드Java 레벨1 - 2021" - }, - "mission" : { - "id" : 2, - "name" : "세션3 - 프로젝트", - "session" : { - "id" : 2, - "name" : "백엔드Java 레벨1 - 2021" - } - }, - "title" : "JAVA", - "content" : "Spring Data JPA를 학습함.", - "tags" : [ { - "id" : 3, - "name" : "java" - }, { - "id" : 4, - "name" : "jpa" - } ], - "scrap" : false, - "read" : true, - "viewCount" : 0, - "liked" : false, - "likesCount" : 1 - }, - "scrapedCount" : 0 - }, { - "studylogResponse" : { - "id" : 1, - "author" : { - "id" : 1, - "username" : "soulG", - "nickname" : "소롱", - "role" : "CREW", - "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt" : "2022-07-10T23:06:46.486704", - "updatedAt" : "2022-07-10T23:06:46.486704", - "session" : { - "id" : 1, - "name" : "프론트엔드JS 레벨1 - 2021" - }, - "mission" : { - "id" : 1, - "name" : "세션1 - 지하철 노선도 미션", - "session" : { - "id" : 1, - "name" : "프론트엔드JS 레벨1 - 2021" - } - }, - "title" : "나는야 Joanne", - "content" : "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", - "tags" : [ { - "id" : 1, - "name" : "spa" - }, { - "id" : 2, - "name" : "router" - } ], - "scrap" : false, - "read" : true, - "viewCount" : 0, - "liked" : false, - "likesCount" : 0 - }, - "scrapedCount" : 0 - } ], - "totalSize" : 0, - "totalPage" : 0, - "currPage" : 0, - "studylogResponses" : [ { "id" : 2, "author" : { "id" : 1, @@ -1772,8 +1856,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:46.533399", - "updatedAt" : "2022-07-10T23:06:46.533399", + "createdAt" : "2022-08-21T20:35:58.443709", + "updatedAt" : "2022-08-21T20:35:58.443709", "session" : { "id" : 2, "name" : "백엔드Java 레벨1 - 2021" @@ -1795,10 +1879,11 @@

      Response

      "id" : 4, "name" : "jpa" } ], + "abilities" : [ ], "scrap" : false, "read" : true, "viewCount" : 0, - "liked" : false, + "liked" : true, "likesCount" : 1 }, { "id" : 1, @@ -1809,8 +1894,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:46.486704", - "updatedAt" : "2022-07-10T23:06:46.486704", + "createdAt" : "2022-08-21T20:35:58.286017", + "updatedAt" : "2022-08-21T20:35:58.286017", "session" : { "id" : 1, "name" : "프론트엔드JS 레벨1 - 2021" @@ -1832,59 +1917,57 @@

      Response

      "id" : 2, "name" : "router" } ], + "abilities" : [ ], "scrap" : false, "read" : true, "viewCount" : 0, "liked" : false, "likesCount" : 0 - } ] + } ], + "totalSize" : 2, + "totalPage" : 1, + "currPage" : 1 }, "frontResponse" : { "data" : [ { - "studylogResponse" : { + "id" : 2, + "author" : { "id" : 1, - "author" : { - "id" : 1, - "username" : "soulG", - "nickname" : "소롱", - "role" : "CREW", - "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt" : "2022-07-10T23:06:46.486704", - "updatedAt" : "2022-07-10T23:06:46.486704", + "username" : "soulG", + "nickname" : "소롱", + "role" : "CREW", + "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" + }, + "createdAt" : "2022-08-21T20:35:58.443709", + "updatedAt" : "2022-08-21T20:35:58.443709", + "session" : { + "id" : 2, + "name" : "백엔드Java 레벨1 - 2021" + }, + "mission" : { + "id" : 2, + "name" : "세션3 - 프로젝트", "session" : { - "id" : 1, - "name" : "프론트엔드JS 레벨1 - 2021" - }, - "mission" : { - "id" : 1, - "name" : "세션1 - 지하철 노선도 미션", - "session" : { - "id" : 1, - "name" : "프론트엔드JS 레벨1 - 2021" - } - }, - "title" : "나는야 Joanne", - "content" : "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", - "tags" : [ { - "id" : 1, - "name" : "spa" - }, { "id" : 2, - "name" : "router" - } ], - "scrap" : false, - "read" : true, - "viewCount" : 0, - "liked" : false, - "likesCount" : 0 + "name" : "백엔드Java 레벨1 - 2021" + } }, - "scrapedCount" : 0 - } ], - "totalSize" : 0, - "totalPage" : 0, - "currPage" : 0, - "studylogResponses" : [ { + "title" : "JAVA", + "content" : "Spring Data JPA를 학습함.", + "tags" : [ { + "id" : 3, + "name" : "java" + }, { + "id" : 4, + "name" : "jpa" + } ], + "abilities" : [ ], + "scrap" : false, + "read" : true, + "viewCount" : 0, + "liked" : true, + "likesCount" : 1 + }, { "id" : 1, "author" : { "id" : 1, @@ -1893,8 +1976,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:46.486704", - "updatedAt" : "2022-07-10T23:06:46.486704", + "createdAt" : "2022-08-21T20:35:58.286017", + "updatedAt" : "2022-08-21T20:35:58.286017", "session" : { "id" : 1, "name" : "프론트엔드JS 레벨1 - 2021" @@ -1916,59 +1999,19 @@

      Response

      "id" : 2, "name" : "router" } ], + "abilities" : [ ], "scrap" : false, "read" : true, "viewCount" : 0, "liked" : false, "likesCount" : 0 - } ] + } ], + "totalSize" : 2, + "totalPage" : 1, + "currPage" : 1 }, "backResponse" : { "data" : [ { - "studylogResponse" : { - "id" : 2, - "author" : { - "id" : 1, - "username" : "soulG", - "nickname" : "소롱", - "role" : "CREW", - "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt" : "2022-07-10T23:06:46.533399", - "updatedAt" : "2022-07-10T23:06:46.533399", - "session" : { - "id" : 2, - "name" : "백엔드Java 레벨1 - 2021" - }, - "mission" : { - "id" : 2, - "name" : "세션3 - 프로젝트", - "session" : { - "id" : 2, - "name" : "백엔드Java 레벨1 - 2021" - } - }, - "title" : "JAVA", - "content" : "Spring Data JPA를 학습함.", - "tags" : [ { - "id" : 3, - "name" : "java" - }, { - "id" : 4, - "name" : "jpa" - } ], - "scrap" : false, - "read" : true, - "viewCount" : 0, - "liked" : false, - "likesCount" : 1 - }, - "scrapedCount" : 0 - } ], - "totalSize" : 0, - "totalPage" : 0, - "currPage" : 0, - "studylogResponses" : [ { "id" : 2, "author" : { "id" : 1, @@ -1977,8 +2020,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:46.533399", - "updatedAt" : "2022-07-10T23:06:46.533399", + "createdAt" : "2022-08-21T20:35:58.443709", + "updatedAt" : "2022-08-21T20:35:58.443709", "session" : { "id" : 2, "name" : "백엔드Java 레벨1 - 2021" @@ -2000,12 +2043,54 @@

      Response

      "id" : 4, "name" : "jpa" } ], + "abilities" : [ ], "scrap" : false, "read" : true, "viewCount" : 0, - "liked" : false, + "liked" : true, "likesCount" : 1 - } ] + }, { + "id" : 1, + "author" : { + "id" : 1, + "username" : "soulG", + "nickname" : "소롱", + "role" : "CREW", + "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" + }, + "createdAt" : "2022-08-21T20:35:58.286017", + "updatedAt" : "2022-08-21T20:35:58.286017", + "session" : { + "id" : 1, + "name" : "프론트엔드JS 레벨1 - 2021" + }, + "mission" : { + "id" : 1, + "name" : "세션1 - 지하철 노선도 미션", + "session" : { + "id" : 1, + "name" : "프론트엔드JS 레벨1 - 2021" + } + }, + "title" : "나는야 Joanne", + "content" : "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", + "tags" : [ { + "id" : 1, + "name" : "spa" + }, { + "id" : 2, + "name" : "router" + } ], + "abilities" : [ ], + "scrap" : false, + "read" : true, + "viewCount" : 0, + "liked" : false, + "likesCount" : 0 + } ], + "totalSize" : 2, + "totalPage" : 1, + "currPage" : 1 } }
      @@ -2033,8 +2118,8 @@

      사용자 리액

      사용자 스크랩 등록

      -

      Request

      +

      Request

      POST /members/scrap HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDExLCJleHAiOjE2NTc0NjU2MTEsInJvbGUiOiJDUkVXIn0.52VaLgNnZBOclcYJIZWwKJ88_pmkryaNjkPtUKos0vI
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzczLCJleHAiOjE2NjEwODUzNzMsInJvbGUiOiJDUkVXIn0.jJRo5GQjiRBOqzTJIGiKq3rCaKcNOMpR5YEJlILzv5w
       Content-Type: application/json; charset=UTF-8
       Host: localhost:4000
       Content-Length: 22
      @@ -2783,7 +2875,7 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 201 Created
      @@ -2791,7 +2883,7 @@ 

      Response

      Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Location: /studylogs/1 -Date: Sun, 10 Jul 2022 14:06:50 GMT +Date: Sun, 21 Aug 2022 11:36:13 GMT Keep-Alive: timeout=60 Connection: keep-alive
      @@ -2801,24 +2893,24 @@

      Response

      사용자 스크랩 삭제

      -

      Request

      +

      Request

      DELETE /members/scrap?studylog=1 HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDExLCJleHAiOjE2NTc0NjU2MTEsInJvbGUiOiJDUkVXIn0.52VaLgNnZBOclcYJIZWwKJ88_pmkryaNjkPtUKos0vI
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzczLCJleHAiOjE2NjEwODUzNzMsInJvbGUiOiJDUkVXIn0.jJRo5GQjiRBOqzTJIGiKq3rCaKcNOMpR5YEJlILzv5w
       Host: localhost:4000
      -

      Response

      +

      Response

      HTTP/1.1 204 No Content
       Vary: Origin
       Vary: Access-Control-Request-Method
       Vary: Access-Control-Request-Headers
      -Date: Sun, 10 Jul 2022 14:06:50 GMT
      +Date: Sun, 21 Aug 2022 11:36:13 GMT
       Keep-Alive: timeout=60
       Connection: keep-alive
      @@ -2828,11 +2920,11 @@

      Response

      사용자 스크랩 조회

      -

      Request

      +

      Request

      GET /members/scrap HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDEwLCJleHAiOjE2NTc0NjU2MTAsInJvbGUiOiJDUkVXIn0.Yre5XZ1efV_m0X9NNeahhRKRdsl2wwpegJO2Ffqooe8
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzczLCJleHAiOjE2NjEwODUzNzMsInJvbGUiOiJDUkVXIn0.jJRo5GQjiRBOqzTJIGiKq3rCaKcNOMpR5YEJlILzv5w
       Content-Type: application/json; charset=UTF-8
       Host: localhost:4000
       Content-Length: 22
      @@ -2844,7 +2936,7 @@ 

      Request

      -

      Response

      +

      Response

      HTTP/1.1 200 OK
      @@ -2853,10 +2945,10 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:50 GMT +Date: Sun, 21 Aug 2022 11:36:12 GMT Keep-Alive: timeout=60 Connection: keep-alive -Content-Length: 3349 +Content-Length: 3441 { "data" : [ { @@ -2868,8 +2960,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:51.000547", - "updatedAt" : "2022-07-10T23:06:51.000547", + "createdAt" : "2022-08-21T20:36:13.240641", + "updatedAt" : "2022-08-21T20:36:13.240641", "session" : null, "mission" : { "id" : 1, @@ -2885,6 +2977,7 @@

      Response

      "id" : 1, "name" : "0번 태그" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -2899,8 +2992,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:51.010388", - "updatedAt" : "2022-07-10T23:06:51.010388", + "createdAt" : "2022-08-21T20:36:13.252062", + "updatedAt" : "2022-08-21T20:36:13.252062", "session" : null, "mission" : { "id" : 2, @@ -2916,6 +3009,7 @@

      Response

      "id" : 2, "name" : "1번 태그" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -2930,8 +3024,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:51.016538", - "updatedAt" : "2022-07-10T23:06:51.016538", + "createdAt" : "2022-08-21T20:36:13.263809", + "updatedAt" : "2022-08-21T20:36:13.263809", "session" : null, "mission" : { "id" : 3, @@ -2947,6 +3041,7 @@

      Response

      "id" : 3, "name" : "2번 태그" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -2961,8 +3056,8 @@

      Response

      "role" : "CREW", "imageUrl" : "https://avatars.githubusercontent.com/u/52682603?v=4" }, - "createdAt" : "2022-07-10T23:06:51.022768", - "updatedAt" : "2022-07-10T23:06:51.022768", + "createdAt" : "2022-08-21T20:36:13.277085", + "updatedAt" : "2022-08-21T20:36:13.277085", "session" : null, "mission" : { "id" : 4, @@ -2978,6 +3073,7 @@

      Response

      "id" : 4, "name" : "3번 태그" } ], + "abilities" : [ ], "scrap" : false, "read" : false, "viewCount" : 0, @@ -2995,18 +3091,18 @@

      Response

      사용자 스터디로그 좋아요

      -

      Request

      +

      Request

      POST /studylogs/1/likes HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA2LCJleHAiOjE2NTc0NjU2MDYsInJvbGUiOiJDUkVXIn0.Fd4QAud6Oj6RvzrUtOOgmOrZMFbKmiPlJjoKtj7DrMc
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzU3LCJleHAiOjE2NjEwODUzNTcsInJvbGUiOiJDUkVXIn0.SqpFnRMnaiNJbLyTkWXqhZABAPoVz2DeOAqfT2iTxmM
       Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
       Host: localhost:4000
      -

      Response

      +

      Response

      HTTP/1.1 200 OK
      @@ -3015,7 +3111,7 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:45 GMT +Date: Sun, 21 Aug 2022 11:35:57 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 40 @@ -3031,17 +3127,17 @@

      Response

      사용자 스터디로그 좋아요 취소

      -

      Request

      +

      Request

      DELETE /studylogs/1/likes HTTP/1.1
      -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjU3NDYyMDA2LCJleHAiOjE2NTc0NjU2MDYsInJvbGUiOiJDUkVXIn0.Fd4QAud6Oj6RvzrUtOOgmOrZMFbKmiPlJjoKtj7DrMc
      +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjYxMDgxNzU4LCJleHAiOjE2NjEwODUzNTgsInJvbGUiOiJDUkVXIn0.U3_iv1k7ehMOROob1rchV7zkYIhGBuiQIw6-wOd-AO0
       Host: localhost:4000
      -

      Response

      +

      Response

      HTTP/1.1 200 OK
      @@ -3050,7 +3146,7 @@ 

      Response

      Vary: Access-Control-Request-Headers Content-Type: application/json Transfer-Encoding: chunked -Date: Sun, 10 Jul 2022 14:06:46 GMT +Date: Sun, 21 Aug 2022 11:35:58 GMT Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 41 @@ -3069,7 +3165,7 @@

      Response

      diff --git a/backend/src/test/java/wooteco/prolog/studylog/application/BadgeServiceTest.java b/backend/src/test/java/wooteco/prolog/badge/application/BadgeServiceTest.java similarity index 67% rename from backend/src/test/java/wooteco/prolog/studylog/application/BadgeServiceTest.java rename to backend/src/test/java/wooteco/prolog/badge/application/BadgeServiceTest.java index 3013cb8c4..d8e41a81f 100644 --- a/backend/src/test/java/wooteco/prolog/studylog/application/BadgeServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/badge/application/BadgeServiceTest.java @@ -1,8 +1,7 @@ -package wooteco.prolog.studylog.application; +package wooteco.prolog.badge.application; import static org.assertj.core.api.Assertions.assertThat; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -10,6 +9,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import wooteco.prolog.badge.domain.BadgeType; import wooteco.prolog.member.domain.Member; import wooteco.prolog.member.domain.Role; import wooteco.prolog.member.domain.repository.MemberRepository; @@ -17,7 +17,8 @@ import wooteco.prolog.session.domain.Session; import wooteco.prolog.session.domain.repository.MissionRepository; import wooteco.prolog.session.domain.repository.SessionRepository; -import wooteco.prolog.studylog.domain.BadgeType; +import wooteco.prolog.studylog.application.StudylogLikeService; +import wooteco.prolog.studylog.application.StudylogService; import wooteco.prolog.studylog.domain.Studylog; import wooteco.support.utils.IntegrationTest; @@ -47,8 +48,12 @@ public class BadgeServiceTest { @BeforeEach void setUp() { - session1 = sessionRepository.save(new Session("세션1")); - session2 = sessionRepository.save(new Session("세션2")); + for (int i = 0; i < 9; i++) { + sessionRepository.save(new Session("세션" + i)); + } + + session1 = sessionRepository.save(new Session(10L, "세션10")); + session2 = sessionRepository.save(new Session(11L, "세션11")); Mission 체스미션 = missionRepository.save(new Mission("체스미션", session1)); Mission 지하철미션 = missionRepository.save(new Mission("지하철미션", session2)); @@ -60,24 +65,24 @@ void setUp() { for (int i = 0; i < 3; i++) { Studylog studylog = studylogService.save( - new Studylog(브라운, "체스 title" + i, "체스 content" + i, session1, 체스미션, - Collections.emptyList())); + new Studylog(브라운, "체스 title" + i, "체스 content" + i, session1, 체스미션, + Collections.emptyList())); likeService.likeStudylog(수달.getId(), studylog.getId(), true); likeService.likeStudylog(베루스.getId(), studylog.getId(), true); } for (int i = 0; i < 4; i++) { Studylog studylog = studylogService.save( - new Studylog(브라운, "지하철 title" + i, "지하철 content" + i, session2, 지하철미션, - Collections.emptyList())); + new Studylog(브라운, "지하철 title" + i, "지하철 content" + i, session2, 지하철미션, + Collections.emptyList())); likeService.likeStudylog(수달.getId(), studylog.getId(), true); likeService.likeStudylog(베루스.getId(), studylog.getId(), true); } for (int i = 0; i < 8; i++) { Studylog studylog = studylogService.save( - new Studylog(베루스, "장바구니 title" + i, "장바구니 content" + i, session2, 지하철미션, - Collections.emptyList())); + new Studylog(베루스, "장바구니 title" + i, "장바구니 content" + i, session2, 지하철미션, + Collections.emptyList())); likeService.likeStudylog(수달.getId(), studylog.getId(), true); likeService.likeStudylog(베루스.getId(), studylog.getId(), true); } @@ -86,39 +91,37 @@ void setUp() { @DisplayName("발급 받은 배지가 없는 사용자의 배지를 조회한다.") @Test void findEmptyBadge() { - List badges = badgeService.findBadges(토미.getUsername(), - Arrays.asList(session1.getId(), session2.getId())); + List badges = badgeService.getBadges(토미.getUsername() + ); assertThat(badges).isEmpty(); } @DisplayName("열정왕 배지를 발급 받은 사용자의 배지를 조회한다.") @Test void findPassionKingBadge() { - List badges = badgeService.findBadges(브라운.getUsername(), - Arrays.asList(session1.getId(), session2.getId())); - assertThat(badges).hasSize(1); - assertThat(badges.get(0).toString()).isEqualTo(BadgeType.PASSION_KING.name()); + List badges = badgeService.getBadges(브라운.getUsername() + ); + assertThat(badges).containsExactly(BadgeType.PASSION_KING); } @DisplayName("칭찬왕 배지를 받급받은 사용자의 배지를 조회한다.") @Test void findComplimentKingBadge() { - List badges = badgeService.findBadges(수달.getUsername(), - Arrays.asList(session1.getId(), session2.getId())); - assertThat(badges).hasSize(1); - assertThat(badges.get(0).toString()).isEqualTo(BadgeType.COMPLIMENT_KING.name()); + List badges = badgeService.getBadges(수달.getUsername() + ); + assertThat(badges).containsExactly(BadgeType.COMPLIMENT_KING); } @DisplayName("칭찬왕과 열정왕 배지를 발급받은 사용자의 배지를 조회한다.") @Test void findAllBadges() { - List badges = badgeService.findBadges(베루스.getUsername(), - Arrays.asList(session1.getId(), session2.getId())); + List badges = badgeService.getBadges(베루스.getUsername() + ); assertThat(badges).hasSize(2); List badgeNames = badges.stream() - .map(BadgeType::toString) - .collect(Collectors.toList()); + .map(BadgeType::toString) + .collect(Collectors.toList()); assertThat(badgeNames).containsExactlyInAnyOrder(BadgeType.PASSION_KING.name(), - BadgeType.COMPLIMENT_KING.name()); + BadgeType.COMPLIMENT_KING.name()); } } diff --git a/backend/src/test/java/wooteco/prolog/levellogs/application/LevelLogServiceTest.java b/backend/src/test/java/wooteco/prolog/levellogs/application/LevelLogServiceTest.java new file mode 100644 index 000000000..e32c7adb8 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/levellogs/application/LevelLogServiceTest.java @@ -0,0 +1,95 @@ +package wooteco.prolog.levellogs.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Arrays; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import wooteco.prolog.levellogs.application.dto.LevelLogRequest; +import wooteco.prolog.levellogs.application.dto.LevelLogResponse; +import wooteco.prolog.levellogs.application.dto.SelfDiscussionRequest; +import wooteco.prolog.levellogs.domain.LevelLog; +import wooteco.prolog.levellogs.domain.SelfDiscussion; +import wooteco.prolog.levellogs.domain.repository.LevelLogRepository; +import wooteco.prolog.levellogs.domain.repository.SelfDiscussionRepository; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; +import wooteco.prolog.member.domain.repository.MemberRepository; +import wooteco.support.utils.IntegrationTest; + +@IntegrationTest +class LevelLogServiceTest { + + @Autowired + private LevelLogService levelLogService; + + @Autowired + private LevelLogRepository levelLogRepository; + + @Autowired + private SelfDiscussionRepository selfDiscussionRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("레벨 로그를 작성한다.") + void createLevelLog() { + // arrange + Member author = memberRepository.save(new Member("sudal", "sudal", Role.CREW, 1L, "image")); + + final SelfDiscussionRequest selfDiscussionRequest1 = new SelfDiscussionRequest("질문2", + "응답2"); + final SelfDiscussionRequest selfDiscussionRequest2 = new SelfDiscussionRequest("질문1", + "응답1"); + final SelfDiscussionRequest selfDiscussionRequest3 = new SelfDiscussionRequest("질문3", + "응답3"); + + final LevelLogRequest levelLogRequest = new LevelLogRequest("제목", "내용", + Arrays.asList(selfDiscussionRequest1, selfDiscussionRequest2, selfDiscussionRequest3)); + + final LevelLogResponse response = levelLogService.insertLevellogs(author.getId(), + levelLogRequest); + + // act & assert +// Assertions.assertDoesNotThrow( +// () -> ; + } + + @Test + @DisplayName("레벨 로그를 수정한다.") + void updateLevelLog() { + // arrange + Member author = memberRepository.save(new Member("sudal", "sudal", Role.CREW, 1L, "image")); + final LevelLog levelLog = levelLogRepository.save(new LevelLog("title", "content", author)); + + selfDiscussionRepository.save(new SelfDiscussion(levelLog, "질문1", + "응답1")); + selfDiscussionRepository.save(new SelfDiscussion(levelLog, "질문2", + "응답2")); + selfDiscussionRepository.save(new SelfDiscussion(levelLog, "질문3", + "응답3")); + + final SelfDiscussionRequest selfDiscussionRequest1 = new SelfDiscussionRequest("수정질문1", + "수정응답1"); + final SelfDiscussionRequest selfDiscussionRequest2 = new SelfDiscussionRequest("수정질문2", + "수정응답2"); + final SelfDiscussionRequest selfDiscussionRequest3 = new SelfDiscussionRequest("수정질문3", + "수정응답3"); + final LevelLogRequest updateRequest = new LevelLogRequest("제목수정1", "내용수정1", + Arrays.asList(selfDiscussionRequest1, selfDiscussionRequest2, selfDiscussionRequest3)); + + // act + levelLogService.updateLevelLog(author.getId(), levelLog.getId(), updateRequest); + + //assert + final LevelLog foundLevelLog = levelLogService.findById(levelLog.getId()); + + assertAll( + () -> assertThat(foundLevelLog.getContent()).isEqualTo(updateRequest.getContent()), + () -> assertThat(foundLevelLog.getTitle()).isEqualTo(updateRequest.getTitle()) + ); + } +} diff --git a/backend/src/test/java/wooteco/prolog/levellogs/domain/LevelLogTest.java b/backend/src/test/java/wooteco/prolog/levellogs/domain/LevelLogTest.java new file mode 100644 index 000000000..1e145824a --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/levellogs/domain/LevelLogTest.java @@ -0,0 +1,22 @@ +package wooteco.prolog.levellogs.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; + +public class LevelLogTest { + + @DisplayName("작성자인지 확인한다.") + @Test + void isAuthor() { + Member another = new Member(2L, "another", "another", Role.CREW, 2L, "iamgeUrl"); + Member author = new Member(1L, "username", "nickname", Role.CREW, 1L, "iamgeUrl"); + LevelLog levelLog = new LevelLog(1L, "제목", "내용", author); + + assertThat(levelLog.isAuthor(author)).isTrue(); + assertThat(levelLog.isAuthor(another)).isFalse(); + } +} diff --git a/backend/src/test/java/wooteco/prolog/levellogs/ui/LevelLogsControllerTest.java b/backend/src/test/java/wooteco/prolog/levellogs/ui/LevelLogsControllerTest.java new file mode 100644 index 000000000..8252b7b3a --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/levellogs/ui/LevelLogsControllerTest.java @@ -0,0 +1,270 @@ +package wooteco.prolog.levellogs.ui; + +import static java.util.Comparator.comparing; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.http.ResponseEntity; +import wooteco.prolog.levellogs.application.LevelLogService; +import wooteco.prolog.levellogs.application.dto.LevelLogRequest; +import wooteco.prolog.levellogs.application.dto.LevelLogResponse; +import wooteco.prolog.levellogs.application.dto.LevelLogSummariesResponse; +import wooteco.prolog.levellogs.application.dto.LevelLogSummaryResponse; +import wooteco.prolog.levellogs.application.dto.SelfDiscussionRequest; +import wooteco.prolog.levellogs.domain.LevelLog; +import wooteco.prolog.levellogs.domain.SelfDiscussion; +import wooteco.prolog.levellogs.domain.repository.LevelLogRepository; +import wooteco.prolog.levellogs.domain.repository.SelfDiscussionRepository; +import wooteco.prolog.levellogs.exception.InvalidLevelLogAuthorException; +import wooteco.prolog.levellogs.exception.LevelLogNotFoundException; +import wooteco.prolog.login.ui.LoginMember; +import wooteco.prolog.login.ui.LoginMember.Authority; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; +import wooteco.prolog.member.domain.repository.MemberRepository; +import wooteco.prolog.member.exception.MemberNotFoundException; +import wooteco.support.utils.RepositoryTest; + +@RepositoryTest +public class LevelLogsControllerTest { + + @Autowired + private LevelLogRepository levelLogRepository; + + @Autowired + private SelfDiscussionRepository selfDiscussionRepository; + + @Autowired + private MemberRepository memberRepository; + + private LevelLogsController sut; + + @BeforeEach + void setUp() { + sut = new LevelLogsController( + new LevelLogService(memberRepository, levelLogRepository, selfDiscussionRepository)); + } + + @Test + @DisplayName("레벨 로그를 작성한다.") + void createLevelLog() { + // arrange + Member author = memberRepository.save(new Member("sudal", "sudal", Role.CREW, 1L, "image")); + + final SelfDiscussionRequest selfDiscussionRequest1 = new SelfDiscussionRequest("질문2", + "응답2"); + final SelfDiscussionRequest selfDiscussionRequest2 = new SelfDiscussionRequest("질문1", + "응답1"); + final SelfDiscussionRequest selfDiscussionRequest3 = new SelfDiscussionRequest("질문3", + "응답3"); + + final LevelLogRequest levelLogRequest = new LevelLogRequest("제목", "내용", + Arrays.asList(selfDiscussionRequest1, selfDiscussionRequest2, selfDiscussionRequest3)); + + // act + final ResponseEntity response = sut.create( + new LoginMember(author.getId(), Authority.MEMBER), + levelLogRequest); + + // assert + assertThat(response.getStatusCode()).isEqualTo(CREATED); + } + + @Test + @DisplayName("레벨 로그를 수정한다.") + void updateLevelLog() { + // arrange + Member author = memberRepository.save(new Member("verus", "verus", Role.CREW, 1L, "image")); + LevelLog levelLog = levelLogRepository.save(new LevelLog("제목", "내용", author)); + selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문2", "응답2")); + selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문1", "응답1")); + selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문3", "응답3")); + + final SelfDiscussionRequest selfDiscussionRequest1 = new SelfDiscussionRequest("수정 질문2", + "수정 응답2"); + final SelfDiscussionRequest selfDiscussionRequest2 = new SelfDiscussionRequest("수정 질문1", + "수정 응답1"); + final SelfDiscussionRequest selfDiscussionRequest3 = new SelfDiscussionRequest("수정 질문3", + "수정 응답3"); + + final LevelLogRequest levelLogRequest = new LevelLogRequest("수정 제목", "수정 내용", + Arrays.asList(selfDiscussionRequest1, selfDiscussionRequest2, selfDiscussionRequest3)); + + // act + final ResponseEntity response = sut.updateLovellog( + new LoginMember(author.getId(), Authority.MEMBER), levelLog.getId(), levelLogRequest); + + final LevelLogResponse levelLogResponse = sut.findById(levelLog.getId()).getBody(); + + // assert + + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(NO_CONTENT), + () -> assertThat(levelLogResponse.getContent()).isEqualTo(levelLogRequest.getContent()), + () -> assertThat(levelLogResponse.getTitle()).isEqualTo(levelLogRequest.getTitle()) + ); + } + + @Test + @DisplayName("레벨 로그 ID로 상세 정보를 조회한다.") + void findByLevelLogId() { + // arrange + Member author = memberRepository.save(new Member("verus", "verus", Role.CREW, 1L, "image")); + LevelLog levelLog = levelLogRepository.save(new LevelLog("제목", "내용", author)); + SelfDiscussion discussion2 = selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문2", "응답2")); + SelfDiscussion discussion1 = selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문1", "응답1")); + SelfDiscussion discussion3 = selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문3", "응답3")); + + // act + ResponseEntity response = sut.findById(levelLog.getId()); + + // assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(OK) + // () -> assertThat(response.getBody()).isEqualTo( + // new LevelLogResponse(levelLog, Arrays.asList(discussion2, discussion1, discussion3)) + // ) + ); + } + + @Test + @DisplayName("존재하지 않는 레벨 로그 ID로 상세 정보 조회 시 예외를 반환한다.") + void findByNotFoundLevelLogId() { + assertThatThrownBy(() -> sut.findById(1L)) + .isInstanceOf(LevelLogNotFoundException.class); + } + + @DisplayName("레벨 로그 목록을 조회한다.") + @Test + void findLevelLogs() { + // arrange + Member verus = memberRepository.save( + new Member("verus", "verus", Role.CREW, 1L, "verus-image")); + Member sudal = memberRepository.save( + new Member("sudal", "sudal", Role.CREW, 2L, "sudal-image")); + + List verusLevelLogs = saveLevelLog(verus, 3); + List sudalLevelLogs = saveLevelLog(sudal, 2); + + // act + ResponseEntity response = sut + .findAll(PageRequest.of(0, 3, Sort.by(Order.desc("createdAt")))); + + // assert + List totalLevelLogs = new ArrayList<>(verusLevelLogs); + totalLevelLogs.addAll(sudalLevelLogs); + + List expectedResponse = totalLevelLogs.stream() + .sorted(comparing(LevelLogSummaryResponse::getCreatedAt).reversed()) + .collect(Collectors.toList()); + + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(OK) + // () -> assertThat(response.getBody()).( + // new LevelLogSummariesResponse(expectedResponse.subList(0, 3), 5L, 2, 1) + // ) + ); + } + + private List saveLevelLog(Member member, int count) { + List responses = new ArrayList<>(); + for (int i = 0; i < count; i++) { + LevelLog levelLog = levelLogRepository.save(new LevelLog("제목 " + i, "내용 " + i, member)); + responses.add(new LevelLogSummaryResponse(levelLog)); + } + return responses; + } + + @DisplayName("본인이 작성한 레벨 로그를 삭제한다.") + @Test + void deleteLevelLog() { + // arrange + Member author = memberRepository.save( + new Member("verus", "verus", Role.CREW, 1L, "verus-image")); + LevelLog levelLog = levelLogRepository.save(new LevelLog("제목", "내용", author)); + SelfDiscussion discussion = selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문1", "응답1")); + + // act + ResponseEntity response = sut + .deleteById(new LoginMember(author.getId(), Authority.MEMBER), levelLog.getId()); + + // assert + assertThat(response.getStatusCode()).isEqualTo(NO_CONTENT); + assertThat(levelLogRepository.existsById(levelLog.getId())).isFalse(); + assertThat(selfDiscussionRepository.existsById(discussion.getId())).isFalse(); + } + + @DisplayName("존재하지 않는 레벨 로그 삭제 시 예외가 발생한다.") + @Test + void deleteNotFoundLevelLog() { + // arrange + Member author = memberRepository.save( + new Member("verus", "verus", Role.CREW, 1L, "verus-image")); + + // act & assert + assertThatThrownBy( + () -> sut.deleteById(new LoginMember(author.getId(), Authority.MEMBER), 1L)) + .isInstanceOf(LevelLogNotFoundException.class); + } + + @DisplayName("작성자 외의 사용자가 레벨 로그 삭제 시 예외가 발생한다.") + @Test + void deleteByAnotherMember() { + // arrange + Member another = memberRepository.save( + new Member("sudal", "sudal", Role.CREW, 2L, "sudal-image")); + + Member author = memberRepository.save( + new Member("verus", "verus", Role.CREW, 1L, "verus-image")); + LevelLog levelLog = levelLogRepository.save(new LevelLog("제목", "내용", author)); + SelfDiscussion discussion = selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문1", "응답1")); + + // act & assert + assertThatThrownBy(() -> sut.deleteById(new LoginMember(another.getId(), Authority.MEMBER), + levelLog.getId())) + .isInstanceOf(InvalidLevelLogAuthorException.class); + assertThat(levelLogRepository.existsById(levelLog.getId())).isTrue(); + assertThat(selfDiscussionRepository.existsById(discussion.getId())).isTrue(); + } + + @DisplayName("존재하지 않는 사용자가 레벨 로그 삭제 시 예외가 발생한다.") + @Test + void deleteByNotFoundMember() { + // arrange + Member author = memberRepository.save( + new Member("verus", "verus", Role.CREW, 1L, "verus-image")); + LevelLog levelLog = levelLogRepository.save(new LevelLog("제목", "내용", author)); + SelfDiscussion discussion = selfDiscussionRepository.save( + new SelfDiscussion(levelLog, "질문1", "응답1")); + + // act & assert + assertThatThrownBy( + () -> sut.deleteById(new LoginMember(2L, Authority.MEMBER), levelLog.getId())) + .isInstanceOf(MemberNotFoundException.class); + assertThat(levelLogRepository.existsById(levelLog.getId())).isTrue(); + assertThat(selfDiscussionRepository.existsById(discussion.getId())).isTrue(); + } +} diff --git a/backend/src/test/java/wooteco/prolog/member/domain/repository/GroupMemberRepositoryTest.java b/backend/src/test/java/wooteco/prolog/member/domain/repository/GroupMemberRepositoryTest.java new file mode 100644 index 000000000..75672f4d3 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/member/domain/repository/GroupMemberRepositoryTest.java @@ -0,0 +1,44 @@ +package wooteco.prolog.member.domain.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import wooteco.prolog.member.domain.GroupMember; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.MemberGroup; +import wooteco.prolog.member.domain.Role; + +@DataJpaTest +class GroupMemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + @Autowired + private GroupMemberRepository groupMemberRepository; + @Autowired + private MemberGroupRepository memberGroupRepository; + + @Test + @DisplayName("작성된 studylog의 Member가 GroupMember의 MemberGroup에 포함되는 경우 true를 반환한다.") + void existsGroupMemberByMemberAndMemberGroup() { + // given + Member saveMember = memberRepository.save( + new Member("username", "nickname", Role.CREW, 1L, "imageUrl")); + MemberGroup saveMemberGroup = memberGroupRepository.save( + new MemberGroup(null, "프론트엔드", "프론트엔드 설명") + ); + groupMemberRepository.save( + new GroupMember(null, saveMember, saveMemberGroup) + ); + + // when + boolean extract = groupMemberRepository.existsGroupMemberByMemberAndGroup( + saveMember, saveMemberGroup); + + // then + assertThat(extract).isTrue(); + } +} diff --git a/backend/src/test/java/wooteco/prolog/report/domain/ablity/AbilityTest.java b/backend/src/test/java/wooteco/prolog/report/domain/ablity/AbilityTest.java index db0b4ad76..2ca5e72d1 100644 --- a/backend/src/test/java/wooteco/prolog/report/domain/ablity/AbilityTest.java +++ b/backend/src/test/java/wooteco/prolog/report/domain/ablity/AbilityTest.java @@ -102,4 +102,15 @@ void colorDifferentBetweenParentAndChildException() { assertThatThrownBy(() -> childAbility.validateColorWithParent(Collections.singletonList(anotherParentAbility), parentAbility)) .isExactlyInstanceOf(AbilityParentChildColorDifferentException.class); } + + @DisplayName("역량이 멤버에게 속하는지 확인한다.") + @Test + void isBelongsToMember() { + Long abilityId = 1L; + Long memberId = 100_000L; + Member member = new Member(100_000L, null, null, null, null, null); + + Ability ability = Ability.parent(abilityId, "역량", "역량 매핑", "파란색", member); + assertThat(ability.isBelongsTo(memberId)).isTrue(); + } } diff --git a/backend/src/test/java/wooteco/prolog/session/domain/repository/SessionMemberRepositoryTest.java b/backend/src/test/java/wooteco/prolog/session/domain/repository/SessionMemberRepositoryTest.java new file mode 100644 index 000000000..828d07fa9 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/session/domain/repository/SessionMemberRepositoryTest.java @@ -0,0 +1,66 @@ +package wooteco.prolog.session.domain.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; +import wooteco.prolog.member.domain.repository.MemberRepository; +import wooteco.prolog.session.domain.Session; +import wooteco.prolog.session.domain.SessionMember; +import wooteco.prolog.session.exception.SessionNotFoundException; +import wooteco.support.utils.IntegrationTest; + +@IntegrationTest +class SessionMemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private SessionRepository sessionRepository; + @Autowired + private SessionMemberRepository sessionMemberRepository; + + @DisplayName("sessionId 와 member 에 일치하는 SessionMember 를 조회할 수 있다.") + @Test + void findSessionMemberBySessionIdAndMemberId() { + // given + Member 현구막 = 크루_생성("최현구", "현구막"); + Session 백엔드_레벨1 = 강의_생성("백엔드_레벨1"); + SessionMember 현구막_백엔드_레벨1 = 수강중인_강의_등록(현구막, 백엔드_레벨1); + + // when + Optional result = sessionMemberRepository.findBySessionIdAndMember( + 백엔드_레벨1.getId(), + 현구막 + ); + + // then + assertThat(result.isPresent()).isTrue(); + assertThat(result.orElseThrow(SessionNotFoundException::new)).isEqualTo(현구막_백엔드_레벨1); + } + + private Session 강의_생성(String name) { + return sessionRepository.save(new Session(name)); + } + + private Member 크루_생성(String username, String nickname) { + return memberRepository.save( + new Member( + username, + nickname, + Role.CREW, + 9L, + "기가막힌 URL" + ) + ); + } + + private SessionMember 수강중인_강의_등록(Member member, Session session) { + return sessionMemberRepository.save(new SessionMember(session.getId(), member)); + } +} diff --git a/backend/src/test/java/wooteco/prolog/studylog/application/CommentServiceTest.java b/backend/src/test/java/wooteco/prolog/studylog/application/CommentServiceTest.java new file mode 100644 index 000000000..0334a3a8a --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/studylog/application/CommentServiceTest.java @@ -0,0 +1,164 @@ +package wooteco.prolog.studylog.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.Collections; +import org.elasticsearch.common.collect.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; +import wooteco.prolog.member.domain.repository.MemberRepository; +import wooteco.prolog.session.domain.Mission; +import wooteco.prolog.session.domain.Session; +import wooteco.prolog.session.domain.repository.MissionRepository; +import wooteco.prolog.session.domain.repository.SessionRepository; +import wooteco.prolog.studylog.application.dto.CommentMemberResponse; +import wooteco.prolog.studylog.application.dto.CommentResponse; +import wooteco.prolog.studylog.application.dto.CommentSaveRequest; +import wooteco.prolog.studylog.application.dto.CommentUpdateRequest; +import wooteco.prolog.studylog.application.dto.CommentsResponse; +import wooteco.prolog.studylog.domain.Studylog; +import wooteco.prolog.studylog.exception.CommentNotFoundException; +import wooteco.support.utils.IntegrationTest; + +@IntegrationTest +class CommentServiceTest { + + @Autowired + private CommentService commentService; + @Autowired + private StudylogService studylogService; + @Autowired + private SessionRepository sessionRepository; + @Autowired + private MissionRepository missionRepository; + @Autowired + private MemberRepository memberRepository; + + private Member 브라운; + private Member 루키; + private Member 잉; + + private Session session_백엔드_레벨1; + private Studylog 체스_스터디로그; + private Studylog null_스터디로그; + + @BeforeEach + void setUp() { + 브라운 = memberRepository.save(new Member("brown", "브라운", Role.CREW, 1L, "imageUrl")); + 루키 = memberRepository.save(new Member("rookie", "루키", Role.CREW, 2L, "imageUrl")); + 잉 = memberRepository.save(new Member(" ", "잉", Role.CREW, 3L, "imageUrl")); + + session_백엔드_레벨1 = sessionRepository.save(new Session("백엔드Java 레벨1")); + Mission mission_체스미션 = missionRepository.save(new Mission("체스미션", session_백엔드_레벨1)); + + 체스_스터디로그 = studylogService.save( + new Studylog( + 브라운, + "체스 title", + "체스 content", + session_백엔드_레벨1, + mission_체스미션, + Collections.emptyList())); + + null_스터디로그 = studylogService.save( + new Studylog( + 루키, + "null title", + "null content", + null, + null, + Collections.emptyList())); + } + + @Test + @DisplayName("스터디로그 ID와 Member ID를 통해서 댓글을 등록한다.") + void create() { + // given + Long commentId = 루키_댓글_등록함(); + + // then + assertThat(commentId).isNotNull(); + } + + @Test + @DisplayName("댓글 등록 시 댓글 내용이 빈값일 경우 예외가 발생한다.") + void create_nullContentException() { + // given + CommentSaveRequest request = new CommentSaveRequest(루키.getId(), 체스_스터디로그.getId(), null); + + // when & then + assertThatThrownBy(() -> commentService.insertComment(request)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("스터디로그 ID를 통해서 등록된 댓글 목록을 조회한다.") + void findComments() { + // given + CommentSaveRequest 루키_요청 = new CommentSaveRequest(루키.getId(), 체스_스터디로그.getId(), "댓글의 내용"); + commentService.insertComment(루키_요청); + CommentSaveRequest 잉_요청 = new CommentSaveRequest(잉.getId(), 체스_스터디로그.getId(), "댓글의 내용"); + commentService.insertComment(잉_요청); + + // when + CommentsResponse commentsResponse = commentService.findComments(체스_스터디로그.getId()); + + // then + assertThat(commentsResponse.getData()).usingRecursiveComparison() + .ignoringFields("id", "createAt") + .isEqualTo(List.of( + new CommentResponse(null, + new CommentMemberResponse(루키.getId(), 루키.getUsername(), 루키.getNickname(), + "imageUrl", 루키.getRole().name()), "댓글의 내용", null), + new CommentResponse(null, + new CommentMemberResponse(잉.getId(), 잉.getUsername(), 잉.getNickname(), + "imageUrl", 잉.getRole().name()), "댓글의 내용", null) + )); + } + + @Test + @DisplayName("스터디로그 Id, Member Id, Comment Id로 댓글을 수정한다.") + void updateComment() { + Long 루키_댓글_아이디 = 루키_댓글_등록함(); + + CommentUpdateRequest 루키_수정_요청 = new CommentUpdateRequest( + 루키.getId(), 체스_스터디로그.getId(), 루키_댓글_아이디, "댓글 수정 내용"); + commentService.updateComment(루키_수정_요청); + + CommentsResponse commentsResponse = commentService.findComments(체스_스터디로그.getId()); + + assertThat(commentsResponse.getData().get(0).getContent()).isEqualTo("댓글 수정 내용"); + } + + @Test + @DisplayName("스터디로그 Id, Member Id, Comment Id로 댓글을 수정할 때, CommentId가 잘못됐으면 에러를 발생한다.") + void updateComment_exception() { + Long 루키_댓글_아이디 = 루키_댓글_등록함(); + + CommentUpdateRequest 루키_수정_요청 = new CommentUpdateRequest( + 루키.getId(), 체스_스터디로그.getId(), 루키_댓글_아이디 + 100L, "댓글 수정 내용"); + + assertThatThrownBy(() -> commentService.updateComment(루키_수정_요청)) + .isInstanceOf(CommentNotFoundException.class); + } + + @Test + @DisplayName("스터디로그 Id, Member Id, Comment Id로 댓글을 삭제한다.") + void deleteComment() { + Long 루키_댓글_아이디 = 루키_댓글_등록함(); + + assertDoesNotThrow( + () -> commentService.deleteComment(루키.getId(), 체스_스터디로그.getId(), 루키_댓글_아이디)); + } + + private Long 루키_댓글_등록함() { + CommentSaveRequest 루키_요청 = new CommentSaveRequest(루키.getId(), 체스_스터디로그.getId(), "댓글의 내용"); + return commentService.insertComment(루키_요청); + } +} diff --git a/backend/src/test/java/wooteco/prolog/studylog/domain/CommentTest.java b/backend/src/test/java/wooteco/prolog/studylog/domain/CommentTest.java new file mode 100644 index 000000000..c507541b2 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/studylog/domain/CommentTest.java @@ -0,0 +1,33 @@ +package wooteco.prolog.studylog.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; +import wooteco.prolog.session.domain.Mission; +import wooteco.prolog.session.domain.Session; +import wooteco.prolog.studylog.exception.CommentDeleteException; + +public class CommentTest { + + @Test + @DisplayName("isdelete가 true일 때 예외를 발생한다.") + void delete() { + //given + Member member = new Member("최현구", "현구막", Role.CREW, 1L, "image"); + Session session = new Session("세션 1"); + Mission mission = new Mission("자동차 미션", session); + Tag tag1 = new Tag("Java"); + Tag tag2 = new Tag("Spring"); + Studylog studylog = new Studylog(member, "제목", "내용", mission, Arrays.asList(tag1, tag2)); + Comment comment = new Comment(1L, member, studylog, "댓글이다."); + comment.delete(); + + assertThatThrownBy(() -> comment.delete()) + .isInstanceOf(CommentDeleteException.class); + } +} diff --git a/backend/src/test/java/wooteco/prolog/studylog/domain/repository/CommentRepositoryTest.java b/backend/src/test/java/wooteco/prolog/studylog/domain/repository/CommentRepositoryTest.java new file mode 100644 index 000000000..a34c28492 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/studylog/domain/repository/CommentRepositoryTest.java @@ -0,0 +1,89 @@ +package wooteco.prolog.studylog.domain.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; +import wooteco.prolog.member.domain.repository.MemberRepository; +import wooteco.prolog.session.domain.Mission; +import wooteco.prolog.session.domain.Session; +import wooteco.prolog.session.domain.repository.MissionRepository; +import wooteco.prolog.session.domain.repository.SessionRepository; +import wooteco.prolog.studylog.domain.Comment; +import wooteco.prolog.studylog.domain.Studylog; +import wooteco.support.utils.RepositoryTest; + +@RepositoryTest +class CommentRepositoryTest { + + @PersistenceContext + private EntityManager entityManager; + + @Autowired + private CommentRepository commentRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private StudylogRepository studylogRepository; + @Autowired + private SessionRepository sessionRepository; + @Autowired + private MissionRepository missionRepository; + + private Member 루키; + private Member 잉; + private Session session_백엔드_레벨1; + private Mission mission_백엔드_체스; + private Studylog 루키_스터디로그; + + @BeforeEach + void setUp() { + 루키 = memberRepository.save(new Member("wishoon", "루키", Role.CREW, 1L, "https://image.url")); + 잉 = memberRepository.save(new Member("iilo", "잉", Role.CREW, 2L, "https://image.url")); + session_백엔드_레벨1 = sessionRepository.save(new Session("백엔드Java 레벨1")); + mission_백엔드_체스 = missionRepository.save(new Mission("체스미션", session_백엔드_레벨1)); + 루키_스터디로그 = studylogRepository.save( + new Studylog(루키, "제목", "내용", session_백엔드_레벨1, mission_백엔드_체스, Lists.emptyList())); + } + + @Test + @DisplayName("스터디로그에 등록된 댓글 리스트를 조회할 수 있다.") + void findCommentByStudylog() { + // given + commentRepository.save(new Comment(null, 루키, 루키_스터디로그, "루키 스터디로그의 내용")); + commentRepository.save(new Comment(null, 잉, 루키_스터디로그, "루키 스터디로그의 내용")); + + // when + List findComments = commentRepository.findCommentByStudylog(루키_스터디로그); + + // then + assertThat(findComments).hasSize(2); + } + + @Test + @DisplayName("스터디로그에 등록된 댓글 리스트 중 isDelete가 false인 댓글만 조회할 수 있다.") + void findCommentOfIsDeleteFalseByStudylog() { + // given + Comment comment = commentRepository.save( + new Comment(null, 루키, 루키_스터디로그, "루키 스터디로그의 내용")); + commentRepository.save(new Comment(null, 잉, 루키_스터디로그, "루키 스터디로그의 내용")); + + // when + comment.delete(); + entityManager.flush(); + List findComments = commentRepository.findCommentByStudylog(루키_스터디로그); + + // then + assertThat(findComments).hasSize(1); + } +} diff --git a/backend/src/test/java/wooteco/prolog/studylog/studylog/application/PopularStudylogServiceTest.java b/backend/src/test/java/wooteco/prolog/studylog/studylog/application/PopularStudylogServiceTest.java index 6014fd9eb..26128f488 100644 --- a/backend/src/test/java/wooteco/prolog/studylog/studylog/application/PopularStudylogServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/studylog/studylog/application/PopularStudylogServiceTest.java @@ -4,26 +4,33 @@ import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import wooteco.prolog.login.application.dto.GithubProfileResponse; import wooteco.prolog.login.ui.LoginMember; import wooteco.prolog.login.ui.LoginMember.Authority; import wooteco.prolog.member.application.MemberService; +import wooteco.prolog.member.domain.GroupMember; import wooteco.prolog.member.domain.Member; -import wooteco.prolog.session.application.SessionService; +import wooteco.prolog.member.domain.MemberGroup; +import wooteco.prolog.member.domain.repository.GroupMemberRepository; +import wooteco.prolog.member.domain.repository.MemberGroupRepository; import wooteco.prolog.session.application.MissionService; -import wooteco.prolog.session.application.dto.SessionRequest; -import wooteco.prolog.session.application.dto.SessionResponse; +import wooteco.prolog.session.application.SessionService; import wooteco.prolog.session.application.dto.MissionRequest; import wooteco.prolog.session.application.dto.MissionResponse; -import wooteco.prolog.session.domain.Session; +import wooteco.prolog.session.application.dto.SessionRequest; +import wooteco.prolog.session.application.dto.SessionResponse; import wooteco.prolog.session.domain.Mission; +import wooteco.prolog.session.domain.Session; import wooteco.prolog.studylog.application.PopularStudylogService; import wooteco.prolog.studylog.application.StudylogLikeService; import wooteco.prolog.studylog.application.StudylogScrapService; @@ -71,10 +78,20 @@ class PopularStudylogServiceTest { private MemberService memberService; @Autowired private StudylogLikeService studylogLikeService; + @Autowired + private MemberGroupRepository memberGroupRepository; + @Autowired + private GroupMemberRepository groupMemberRepository; private Member member1; private Member member2; + private MemberGroup frontendMemberGroup; + private MemberGroup backendMemberGroup; + + private GroupMember frontendGroupMember; + private GroupMember backendGroupMember; + private LoginMember loginMember1; private LoginMember loginMember2; private LoginMember loginMember3; @@ -113,6 +130,18 @@ void setUp() { this.member2 = memberService.findOrCreateMember( new GithubProfileResponse("이름2", "별명2", "2", "image")); + this.frontendMemberGroup = memberGroupRepository.save( + new MemberGroup(null, "프론트엔드", "프론트엔드 설명") + ); + this.backendMemberGroup = memberGroupRepository.save( + new MemberGroup(null, "백엔드", "백엔드 설명") + ); + this.frontendGroupMember = groupMemberRepository.save( + new GroupMember(null, member1, frontendMemberGroup) + ); + this.backendGroupMember = groupMemberRepository.save( + new GroupMember(null, member2, backendMemberGroup) + ); this.loginMember1 = new LoginMember(member1.getId(), Authority.MEMBER); this.loginMember2 = new LoginMember(member2.getId(), Authority.MEMBER); this.loginMember3 = new LoginMember(null, Authority.ANONYMOUS); @@ -132,10 +161,12 @@ void setUp() { } @DisplayName("로그인하지 않은 상태에서 제시된 개수만큼 인기있는 스터디로그를 조회한다.") - @Test - void findPopularStudylogsWithoutLogin() { + @ParameterizedTest + @CsvSource(value = {"0, 3, 3, 3", "0, 2, 2, 2"}) + void findPopularStudylogsWithoutLogin(int page, int size, int totalSize, int dataSize) { // given - insertStudylogs(member1, studylog1, studylog2, studylog3); + insertStudylogs(member1, studylog1, studylog2); + insertStudylogs(member2, studylog3); studylogService.retrieveStudylogById(loginMember3, 2L, false); studylogScrapService.registerScrap(member1.getId(), 2L); studylogService.retrieveStudylogById(loginMember3, 3L, false); @@ -143,7 +174,7 @@ void findPopularStudylogsWithoutLogin() { studylogLikeService.likeStudylog(member1.getId(), 3L, true); // when - PageRequest pageRequest = PageRequest.of(0, 2); + PageRequest pageRequest = PageRequest.of(page, size); popularStudylogService.updatePopularStudylogs(pageRequest); PopularStudylogsResponse studylogs = popularStudylogService.findPopularStudylogs( pageRequest, @@ -152,53 +183,58 @@ void findPopularStudylogsWithoutLogin() { ); // then - assertThat(studylogs.getAllResponse().getStudylogResponses()).hasSize(3); + assertAll( + () -> assertThat(studylogs.getAllResponse().getTotalSize()).isEqualTo(totalSize), + () -> assertThat(studylogs.getAllResponse().getData()).hasSize(dataSize) + ); + for (StudylogResponse response : studylogs.getAllResponse().getData()) { + assertAll( + () -> assertThat(response.isScrap()).isFalse(), + () -> assertThat(response.isRead()).isFalse() + ); + } } @DisplayName("로그인한 상태에서 제시된 개수만큼 인기있는 스터디로그를 조회한다.") - @Test - void findPopularStudylogsWithLogin() { + @ParameterizedTest + @CsvSource(value = {"0, 3, 3, 3", "0, 2, 2, 2"}) + void findPopularStudylogsWithLogin(int page, int size, int totalSize, int dataSize) { // given - List insertResponses = insertStudylogs( - member1, - studylog1, - studylog2, - studylog3, - studylog4 - ); - - StudylogResponse studylogResponse1 = insertResponses.get(0); - StudylogResponse studylogResponse2 = insertResponses.get(1); - StudylogResponse studylogResponse3 = insertResponses.get(2); - StudylogResponse studylogResponse4 = insertResponses.get(3); + insertStudylogs(member1, studylog1, studylog2, studylog3); // 2번째 멤버가 1번째 멤버의 게시글 2번, 3번을 조회 - studylogService.retrieveStudylogById(loginMember2, studylogResponse1.getId(), false); - studylogService.retrieveStudylogById(loginMember2, studylogResponse2.getId(), false); - studylogService.retrieveStudylogById(loginMember2, studylogResponse3.getId(), false); - studylogService.retrieveStudylogById(loginMember2, studylogResponse4.getId(), false); + studylogService.retrieveStudylogById(loginMember2, 1L, false); + studylogService.retrieveStudylogById(loginMember2, 2L, false); + studylogService.retrieveStudylogById(loginMember2, 3L, false); // 2번, 3번 글 스크랩 - studylogScrapService.registerScrap(member2.getId(), studylogResponse1.getId()); - studylogScrapService.registerScrap(member2.getId(), studylogResponse2.getId()); - studylogScrapService.registerScrap(member2.getId(), studylogResponse3.getId()); - studylogScrapService.registerScrap(member2.getId(), studylogResponse4.getId()); + studylogScrapService.registerScrap(member2.getId(), 1L); + studylogScrapService.registerScrap(member2.getId(), 2L); + studylogScrapService.registerScrap(member2.getId(), 3L); // 3번 글 좋아요 - studylogLikeService.likeStudylog(member2.getId(), studylogResponse3.getId(), true); + studylogLikeService.likeStudylog(member2.getId(), 3L, true); // when - PageRequest pageRequest = PageRequest.of(0, 2); + PageRequest pageRequest = PageRequest.of(page, size); popularStudylogService.updatePopularStudylogs(pageRequest); - - PopularStudylogsResponse popularStudylogs = popularStudylogService.findPopularStudylogs( + PopularStudylogsResponse studylogs = popularStudylogService.findPopularStudylogs( pageRequest, member2.getId(), member2.isAnonymous() ); // then - assertThat(popularStudylogs.getAllResponse().getStudylogResponses()).hasSize(4); + assertAll( + () -> assertThat(studylogs.getAllResponse().getTotalSize()).isEqualTo(totalSize), + () -> assertThat(studylogs.getAllResponse().getData()).hasSize(dataSize) + ); + for (StudylogResponse response : studylogs.getAllResponse().getData()) { + assertAll( + () -> assertThat(response.isScrap()).isTrue(), + () -> assertThat(response.isRead()).isTrue() + ); + } } public List insertStudylogs(Member member, Studylog... studylogs) { @@ -213,7 +249,8 @@ private List insertStudylogs(Member member, List stu studylog.getContent(), studylog.getSession().getId(), studylog.getMission().getId(), - toTagRequests(studylog) + toTagRequests(studylog), + Collections.emptyList() ) ) .collect(toList()); diff --git a/backend/src/test/java/wooteco/prolog/studylog/studylog/application/StudylogServiceTest.java b/backend/src/test/java/wooteco/prolog/studylog/studylog/application/StudylogServiceTest.java index 121031077..5f8f35834 100644 --- a/backend/src/test/java/wooteco/prolog/studylog/studylog/application/StudylogServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/studylog/studylog/application/StudylogServiceTest.java @@ -11,6 +11,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -522,7 +523,8 @@ private List insertStudylogs(Member member, List stu studylog.getContent(), studylog.getSession().getId(), studylog.getMission().getId(), - toTagRequests(studylog) + toTagRequests(studylog), + Collections.emptyList() ) ) .collect(toList()); diff --git a/frontend/src/App.js b/frontend/src/App.js index 9621faa6a..53beafd8e 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,13 +1,8 @@ -import { QueryClient, QueryClientProvider } from 'react-query'; -import { ReactQueryDevtools } from 'react-query/devtools'; import TagManager from 'react-gtm-module'; import useSnackBar from './hooks/useSnackBar'; -import GlobalStyles from './GlobalStyles'; import PageRouter from './PageRouter'; -const queryClient = new QueryClient(); - const tagManagerArgs = { gtmId: process.env.REACT_APP_GTM_ID, }; @@ -19,11 +14,7 @@ const App = () => { return ( <> - - - - - + {isSnackBarOpen && } ); diff --git a/frontend/src/apis/ability.ts b/frontend/src/apis/ability.ts index 1111a25ad..4757d5e35 100644 --- a/frontend/src/apis/ability.ts +++ b/frontend/src/apis/ability.ts @@ -2,7 +2,6 @@ import axios, { AxiosError, Method } from 'axios'; import { BASE_URL } from '../configs/environment'; import LOCAL_STORAGE_KEY from '../constants/localStorage'; -import { getLocalStorageItem } from '../utils/localStorage'; export interface ErrorData { code: number; @@ -58,7 +57,7 @@ export class API { } const AbilityAPI = new API({ - accessToken: getLocalStorageItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN), + accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN), baseUrl: BASE_URL, }); diff --git a/frontend/src/apis/comment.ts b/frontend/src/apis/comment.ts new file mode 100644 index 000000000..670148de6 --- /dev/null +++ b/frontend/src/apis/comment.ts @@ -0,0 +1,34 @@ +import { client } from '.'; +import { CommentListResponse, CommentRequest } from '../models/Comment'; + +export const getComments = async (studylogId: number): Promise => { + const response = await client.get(`/studylogs/${studylogId}/comments`); + + return response.data; +}; + +export const createCommentRequest = ({ + studylogId, + body, +}: { + studylogId: number; + body: CommentRequest; +}) => client.post(`/studylogs/${studylogId}/comments`, body); + +export const editComment = ({ + studylogId, + commentId, + body, +}: { + studylogId: number; + commentId: number; + body: CommentRequest; +}) => client.put(`/studylogs/${studylogId}/comments/${commentId}`, body); + +export const deleteComment = ({ + studylogId, + commentId, +}: { + studylogId: number; + commentId: number; +}) => client.delete(`/studylogs/${studylogId}/comments/${commentId}`); diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/index.ts new file mode 100644 index 000000000..b02cb209f --- /dev/null +++ b/frontend/src/apis/index.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; +import { BASE_URL } from '../configs/environment'; +import LOCAL_STORAGE_KEY from '../constants/localStorage'; + +export const client = axios.create({ + baseURL: BASE_URL, + headers: { + Authorization: `Bearer ${localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN)}`, + }, +}); diff --git a/frontend/src/apis/levellogs.ts b/frontend/src/apis/levellogs.ts new file mode 100644 index 000000000..8ab913a52 --- /dev/null +++ b/frontend/src/apis/levellogs.ts @@ -0,0 +1,25 @@ +import { client } from '.'; +import { LevellogRequest } from '../models/Levellogs'; + +export const requestGetLevellogs = async (currPage: number) => { + const params = currPage !== 1 ? `?page=${currPage}` : ''; + + const { data } = await client.get(`/levellogs${params}`); + + return data; +}; + +export const createNewLevellogRequest = (body: LevellogRequest) => client.post(`/levellogs`, body); + +export const requestGetLevellog = async (id) => { + const { data } = await client.get(`/levellogs/${id}`); + + return data; +}; + +export const requestDeleteLevellog = async (id) => { + client.delete(`/levellogs/${id}`); +}; + +export const requestEditLevellog = (id, body: LevellogRequest) => + client.put(`/levellogs/${id}`, body); diff --git a/frontend/src/apis/studylogs.ts b/frontend/src/apis/studylogs.ts index fb78931b0..4441e0622 100644 --- a/frontend/src/apis/studylogs.ts +++ b/frontend/src/apis/studylogs.ts @@ -87,7 +87,8 @@ export const requestGetMissions = ({ accessToken, }: { accessToken: string; -}): Promise> => httpRequester.get('/missions/mine', getAuthConfig(accessToken)); +}): Promise> => + httpRequester.get('/missions/mine', getAuthConfig(accessToken)); export const requestGetSessions = ({ accessToken, }: { diff --git a/frontend/src/assets/images/minus_icon.svg b/frontend/src/assets/images/minus_icon.svg new file mode 100644 index 000000000..e2958818a --- /dev/null +++ b/frontend/src/assets/images/minus_icon.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/images/plus_icon.svg b/frontend/src/assets/images/plus_icon.svg new file mode 100644 index 000000000..b500dd8fd --- /dev/null +++ b/frontend/src/assets/images/plus_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/@shared/FlexBox/FlexBox.js b/frontend/src/components/@shared/FlexBox/FlexBox.ts similarity index 54% rename from frontend/src/components/@shared/FlexBox/FlexBox.js rename to frontend/src/components/@shared/FlexBox/FlexBox.ts index ed2e5c682..690c478db 100644 --- a/frontend/src/components/@shared/FlexBox/FlexBox.js +++ b/frontend/src/components/@shared/FlexBox/FlexBox.ts @@ -1,7 +1,12 @@ -import { css } from '@emotion/react'; +import { css, SerializedStyles } from '@emotion/react'; import styled from '@emotion/styled'; +import { CSSProperties } from 'react'; -const FlexBox = styled.div` +const FlexBox = styled.div< + Pick & { + css?: SerializedStyles; + } +>` display: flex; ${({ flexDirection, justifyContent, alignItems }) => css` flex-direction: ${flexDirection}; diff --git a/frontend/src/components/Chip/Chip.styles.ts b/frontend/src/components/Chip/Chip.styles.ts index 9a9d3f5c8..c95ff7991 100644 --- a/frontend/src/components/Chip/Chip.styles.ts +++ b/frontend/src/components/Chip/Chip.styles.ts @@ -27,6 +27,10 @@ export type ContainerProps = { * @default none */ lineHeight?: string; + /** + * @default 1.4rem + */ + marginRight?: string; /** * @default none */ @@ -36,7 +40,7 @@ export type ContainerProps = { const Container = styled.div` width: ${({ width }) => (width ? getSize(width) : 'fit-content')}; ${({ maxWidth }) => maxWidth && `max-width: ${getSize(maxWidth)}`}; - margin-right: 1.4rem; + margin-right: ${({ marginRight }) => marginRight ?? '1.4rem'}; padding: 0.2rem 0.8rem; display: flex; diff --git a/frontend/src/components/Comment/Comment.style.tsx b/frontend/src/components/Comment/Comment.style.tsx new file mode 100644 index 000000000..8c4d35a5c --- /dev/null +++ b/frontend/src/components/Comment/Comment.style.tsx @@ -0,0 +1,51 @@ +import styled from '@emotion/styled'; +import { COLOR } from '../../enumerations/color'; + +export const Root = styled.div` + display: flex; + flex-direction: column; + + padding-bottom: 28px; +`; + +export const Top = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const Left = styled.div` + display: flex; + align-items: center; + gap: 18px; + + & > a { + display: flex; + align-items: center; + gap: 10px; + } +`; + +export const Logo = styled.img` + width: 36px; + height: 36px; + border-radius: 12px; +`; + +export const CreatedDate = styled.span` + font-size: 12px; + color: ${COLOR.LIGHT_GRAY_900}; +`; + +export const Right = styled.div` + font-size: 14px; + color: ${COLOR.LIGHT_GRAY_900}; + + display: flex; + gap: 10px; +`; + +export const ButtonContainer = styled.div` + display: flex; + gap: 20px; +`; diff --git a/frontend/src/components/Comment/Comment.tsx b/frontend/src/components/Comment/Comment.tsx new file mode 100644 index 000000000..22c6a8dac --- /dev/null +++ b/frontend/src/components/Comment/Comment.tsx @@ -0,0 +1,144 @@ +/** @jsxImportSource @emotion/react */ + +import * as Styled from './Comment.style'; +import { Link } from 'react-router-dom'; + +// 마크다운 +import { Editor as ToastEditor, Viewer } from '@toast-ui/react-editor'; +import '@toast-ui/editor/dist/toastui-editor.css'; +import 'prismjs/themes/prism.css'; +import Prism from 'prismjs'; +import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js'; +import { CommentRequest, CommentType } from '../../models/Comment'; +import { EditorForm, SubmitButton, ViewerWrapper } from '../../pages/StudylogPage/styles'; +import { css } from '@emotion/react'; +import Editor from '../Editor/Editor'; +import { useContext, useRef, useState } from 'react'; +import { COLOR } from '../../enumerations/color'; +import { UserContext } from '../../contexts/UserProvider'; + +export interface CommentProps extends CommentType { + editComment: (commentId: number, body: CommentRequest) => void; + deleteComment: (commentId: number) => void; +} + +const Comment = ({ id, author, content, createAt, editComment, deleteComment }: CommentProps) => { + const { user } = useContext(UserContext); + const { username, nickname, imageUrl } = author; + + const [isEditMode, setIsEditMode] = useState(false); + const editorContentRef = useRef(null); + + const onSubmitEditedComment = () => { + const contentOnEdit = editorContentRef.current?.getInstance().getMarkdown() || ''; + + if (content === contentOnEdit) { + setIsEditMode(false); + + return; + } + + if (window.confirm('댓글을 수정하시겠아요?')) { + const content = editorContentRef.current?.getInstance().getMarkdown() || ''; + + editComment(id, { content }); + setIsEditMode(false); + } + }; + + const onClickEditButton = () => { + setIsEditMode(true); + }; + + const onClickCancelButton = () => { + const contentOnEdit = editorContentRef.current?.getInstance().getMarkdown() || ''; + + if (content === contentOnEdit) { + setIsEditMode(false); + + return; + } + + if (window.confirm('수정 중인 댓글이 사라집니다. 댓글 수정을 취소하시겠어요?')) { + setIsEditMode(false); + } + }; + + const onClickDeleteButton = () => { + if (window.confirm('댓글을 삭제하시겠아요?')) { + deleteComment(id); + } + }; + + return ( + <> + + + + + + {nickname} + + {new Date(createAt).toLocaleString('ko-KR')} + + {user.userId === author.id && ( + + + + + )} + + + + + + {isEditMode && ( + + + + + 취소 + + + 수정 + + + + )} + + ); +}; + +export default Comment; diff --git a/frontend/src/components/Comment/CommentList.style.tsx b/frontend/src/components/Comment/CommentList.style.tsx new file mode 100644 index 000000000..94f43efe3 --- /dev/null +++ b/frontend/src/components/Comment/CommentList.style.tsx @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; +import { COLOR } from '../../enumerations/color'; + +export const CommentsContainer = styled.div` + padding: 28px 12px 0; + + & > div + div { + padding-top: 18px; + border-top: 1px solid ${COLOR.LIGHT_GRAY_200}; + } +`; diff --git a/frontend/src/components/Comment/CommentList.tsx b/frontend/src/components/Comment/CommentList.tsx new file mode 100644 index 000000000..980dd9629 --- /dev/null +++ b/frontend/src/components/Comment/CommentList.tsx @@ -0,0 +1,36 @@ +import { CommentRequest, CommentType } from '../../models/Comment'; +import Comment from './Comment'; +import * as Styled from './CommentList.style'; +import { FormEventHandler, MutableRefObject } from 'react'; +import { Editor as ToastEditor } from '@toast-ui/react-editor'; + +interface CommentListProps { + comments: CommentType[]; + editComment: (commentId: number, body: CommentRequest) => void; + deleteComment: (commentId: number) => void; + onSubmit?: FormEventHandler; + editorContentRef?: MutableRefObject; +} + +const CommentList = ({ + comments, + editComment, + deleteComment, + onSubmit, + editorContentRef, +}: CommentListProps) => { + return ( + + {comments?.map((comment) => ( + + ))} + + ); +}; + +export default CommentList; diff --git a/frontend/src/components/CreatableSelectBox/CreatableSelectBox.js b/frontend/src/components/CreatableSelectBox/CreatableSelectBox.js index 3abcf9bde..da7ffc0d6 100644 --- a/frontend/src/components/CreatableSelectBox/CreatableSelectBox.js +++ b/frontend/src/components/CreatableSelectBox/CreatableSelectBox.js @@ -13,7 +13,6 @@ const selectStyles = { outline: 'none', border: '0', minHeight: '2.4rem', - height: '2.4rem', cursor: 'pointer', }), indicatorsContainer: (styles) => ({ ...styles, display: 'none' }), diff --git a/frontend/src/components/Editor/SideBar/StudyLogSelectAbilityBox.js b/frontend/src/components/Editor/SideBar/StudyLogSelectAbilityBox.js new file mode 100644 index 000000000..1ea52dfe6 --- /dev/null +++ b/frontend/src/components/Editor/SideBar/StudyLogSelectAbilityBox.js @@ -0,0 +1,110 @@ +import { useState, useContext } from 'react'; +import { Link } from 'react-router-dom'; +import { UserContext } from '../../../contexts/UserProvider'; +import * as Styled from './StudyLogSelectAbilityBox.styles'; + +/** + * 역량을 선택할 수 있다. + * 역량은 자식역량만 선택할 수 있다. + */ +const StudyLogSelectAbilityBox = ({ + setIsSelectAbilityBoxOpen, + selectedAbilities, + wholeAbility, + onSelectAbilities, +}) => { + const { + user: { username }, + } = useContext(UserContext); + + const [updatedAbilities, setUpdatedAbilities] = useState(selectedAbilities); + + const [searchTerm, setSearchTerm] = useState(''); + + const onChangeSearchInput = (e) => { + setSearchTerm(e.target.value); + }; + + const filteredAbilities = wholeAbility.filter((abilityObj) => { + const abilityName = abilityObj.name.trim().toLowerCase(); + const refinedSearchTerm = searchTerm.trim().toLowerCase(); + return abilityName.includes(refinedSearchTerm); + }); + + const onClickAbility = (event) => { + const targetAbilityId = Number(event.target.id); + const currAbilities = new Set(updatedAbilities); + + if (currAbilities.has(targetAbilityId)) { + currAbilities.delete(targetAbilityId); + } else { + currAbilities.add(targetAbilityId); + } + + setUpdatedAbilities([...currAbilities]); + }; + + const isChecked = (targetAbilityId) => { + return updatedAbilities.includes(targetAbilityId); + }; + + const onClickSelectButton = () => { + onSelectAbilities(updatedAbilities); + setIsSelectAbilityBoxOpen(false); + }; + + const onClickCloseButton = () => { + setIsSelectAbilityBoxOpen(false); + }; + + return ( + + +

      학습로그에 매핑될 역량을 선택해주세요.

      + 📢 역량은 하위역량만 선택가능합니다. + + + 역량 관리 페이지 이동 + + + + +
      + + + {wholeAbility.length === 0 ? ( + 등록된 역량이 없습니다. + ) : ( + filteredAbilities?.map((ability) => ( + + + + )) + )} + + + + + +
      + ); +}; + +export default StudyLogSelectAbilityBox; diff --git a/frontend/src/components/Editor/SideBar/StudyLogSelectAbilityBox.styles.js b/frontend/src/components/Editor/SideBar/StudyLogSelectAbilityBox.styles.js new file mode 100644 index 000000000..a8ee7a8b2 --- /dev/null +++ b/frontend/src/components/Editor/SideBar/StudyLogSelectAbilityBox.styles.js @@ -0,0 +1,177 @@ +import styled from '@emotion/styled'; +import { COLOR } from '../../../constants'; + +export const Wrapper = styled.div` + width: 35rem; + min-height: 20rem; + max-height: 45rem; + position: absolute; + right: 65%; + top: 0; + + border: 2px solid ${COLOR.LIGHT_GRAY_200}; + background-color: ${COLOR.WHITE}; + border-radius: 1rem; + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.25); + + z-index: 2; +`; + +export const Header = styled.div` + width: 100%; + min-height: 7rem; + max-height: 12rem; + padding: 1.2rem; + + text-align: center; + + border-bottom: 2px solid ${COLOR.LIGHT_GRAY_900}; + background-color: ${COLOR.WHITE}; + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; + + > #selectBox-title { + margin: 0; + display: block; + + font-size: 1.4rem; + font-weight: 500; + } + + > .ability-title { + display: block; + font-size: 1.2rem; + overflow: auto; + } + + > .ability-link { + display: block; + font-size: 1.2rem; + color: ${COLOR.LIGHT_BLUE_500}; + &:hover { + color: ${COLOR.LIGHT_BLUE_900}; + } + } +`; + +export const AbilityList = styled.ul` + width: 100%; + min-height: 12rem; + max-height: 32rem; + padding: 1rem; + + display: flex; + flex-direction: column; + overflow-y: auto; + + background-color: ${COLOR.WHITE}; + border-bottom-left-radius: 1rem; + border-bottom-right-radius: 1rem; + + > li:last-of-type { + border: none; + } +`; + +export const Ability = styled.li` + width: 100%; + height: fit-content; + + border-bottom: 1px solid ${COLOR.LIGHT_GRAY_200}; + + label { + width: 100%; + min-height: 4rem; + + display: flex; + align-items: center; + gap: 0.7rem; + } + + :last-of-type { + margin-bottom: 3rem; + } +`; + +export const ColorCircle = styled.div` + display: inline-block; + width: 1.4rem; + height: 1.4rem; + margin-top: 2px; + + background-color: ${({ backgroundColor }) => backgroundColor ?? 'transparent'}; + border: 1px solid ${COLOR.BLACK_OPACITY_100}; + border-radius: 0.7rem; +`; + +export const AbilityName = styled.span` + width: 25rem; + padding: 1rem 0; + font-size: 1.2rem; + cursor: pointer; +`; + +export const EmptyAbilityGuide = styled.p` + width: 100%; + height: 5rem; + display: flex; + justify-content: center; + align-items: center; +`; + +export const Footer = styled.div` + width: 100%; + height: 3.5rem; + position: absolute; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + + background-color: ${COLOR.WHITE}; + border-bottom-left-radius: 1rem; + border-bottom-right-radius: 1rem; + + > button { + width: 98%; + padding: 0.5rem; + font-size: 1.4rem; + background-color: ${COLOR.LIGHT_BLUE_600}; + border-radius: 1rem; + + :hover { + color: ${COLOR.WHITE}; + background-color: ${COLOR.LIGHT_BLUE_900}; + } + } +`; + +export const SearchInput = styled.input` + border: 1px solid ${COLOR.LIGHT_GRAY_200}; + border-radius: 1rem; + font-size: 1.2rem; + margin-bottom: 1rem; + padding: 0.6em 1.2em; + width: 100%; + :focus { + outline: none; + } +`; + +export const CloseButton = styled.button` + background-color: transparent; + border-radius: 50%; + color: ${COLOR.LIGHT_GRAY_200}; + border: none; + padding: 5px; + position: absolute; + top: 0; + right: 5px; + &::before { + content: 'x'; + } + &:hover { + color: ${COLOR.LIGHT_GRAY_700}; + } +`; diff --git a/frontend/src/components/Editor/Sidebar.tsx b/frontend/src/components/Editor/Sidebar.tsx index 39bf1b275..3e75c09cd 100644 --- a/frontend/src/components/Editor/Sidebar.tsx +++ b/frontend/src/components/Editor/Sidebar.tsx @@ -3,20 +3,39 @@ import CreatableSelectBox from '../CreatableSelectBox/CreatableSelectBox'; import { COLOR } from '../../enumerations/color'; import SelectBox from '../Controls/SelectBox'; -import { PLACEHOLDER } from '../../constants'; +import { ERROR_MESSAGE, PLACEHOLDER } from '../../constants'; import { Mission, Session, Tag } from '../../models/Studylogs'; import styled from '@emotion/styled'; import { useMissions, useSessions, useTags } from '../../hooks/queries/filters'; import { getRowGapStyle } from '../../styles/layout.styles'; +import StudyLogSelectAbilityBox from './SideBar/StudyLogSelectAbilityBox'; +import { useQuery } from 'react-query'; +import AbilityRequest, { ErrorData } from '../../apis/ability'; +import { useContext, useState } from 'react'; +import { UserContext } from '../../contexts/UserProvider'; +import Chip from '../Chip/Chip'; +import { Button } from '../../components'; +import FlexBox from '../@shared/FlexBox/FlexBox'; +import { css } from '@emotion/react'; interface SidebarProps { selectedSessionId: Session['id'] | null; selectedMissionId: Mission['id'] | null; selectedTagList: Tag[]; + selectedAbilities: number[]; onSelectSession: (session: { value: string; label: string }) => void; onSelectMission: (mission: { value: string; label: string }) => void; onSelectTag: (tags: Tag[], actionMeta: { option: { label: string } }) => void; + onSelectAbilities: (abilities: number[]) => void; } +const AbilitySelectList = styled.li` + position: relative; +`; +const AbilityList = styled.ul` + display: flex; + flex-wrap: wrap; + gap: 4px 3px; +`; const SidebarWrapper = styled.aside` width: 24rem; @@ -38,28 +57,102 @@ const FilterTitle = styled.h3` line-height: 1.5; `; +const FlexGap = css` + gap: 0.5rem; +`; + +const PlusButton = css` + background-color: ${COLOR.LIGHT_GRAY_100}; + font-weight: bold; + color: ${COLOR.LIGHT_GRAY_600}; + width: 22px; + height: 22px; +`; + const Sidebar = ({ selectedSessionId, selectedMissionId, selectedTagList, + selectedAbilities, onSelectMission, onSelectSession, onSelectTag, + onSelectAbilities, }: SidebarProps) => { + const [isSelectAbilityBoxOpen, setIsSelectAbilityBoxOpen] = useState(false); const { data: missions = [] } = useMissions(); const { data: tags = [] } = useTags(); const { data: sessions = [] } = useSessions(); - + const { + user: { username }, + } = useContext(UserContext); const tagOptions = tags.map(({ name }) => ({ value: name, label: `#${name}` })); - const missionOptions = missions.map(({ id, name, session }) => ({ value: `${id}`, label: `[${session?.name}] ${name}` })); + const missionOptions = missions.map(({ id, name, session }) => ({ + value: `${id}`, + label: `[${session?.name}] ${name}`, + })); const sessionOptions = sessions.map(({ id, name }) => ({ value: `${id}`, label: `${name}` })); const selectedSession = sessions.find(({ id }) => id === selectedSessionId); const selectedMission = missions.find(({ id }) => id === selectedMissionId); + /** 전체 역량 조회 */ + const { data: abilities = [] } = useQuery( + [`${username}-abilities`], + () => AbilityRequest.getAbilityList({ url: `/members/${username}/abilities` }), + { + onError: (errorData: ErrorData) => { + const errorCode = errorData?.code; + + alert(ERROR_MESSAGE[errorCode] ?? '역량을 가져오는데 실패하였습니다. 다시 시도해주세요.'); + }, + } + ); + + const wholeAbility = abilities?.map((parentAbility) => [...parentAbility.children]).flat(); + + /** 선택된 역량을 보여준다.*/ + const SelectedAbilityChips = ({ selectedAbilityIds }) => { + const selectedAbilities = wholeAbility.filter(({ id }) => selectedAbilityIds.includes(id)); + + return ( + + {selectedAbilities?.map((ability) => { + return ( +
    • + { + { + onSelectAbilities(selectedAbilityIds.filter((id) => id !== ability.id)); + }} + > + {ability.name} + + } +
    • + ); + })} +
      + ); + }; + return ( -
        +
        • session
          @@ -93,13 +186,42 @@ const Sidebar = ({
        • tags - ({ value: name, label: `#${name}` }))} - /> +
          + ({ value: name, label: `#${name}` }))} + /> +
        • + + + + + abilities + + + + + + {isSelectAbilityBoxOpen && ( + + )} + +
        ); diff --git a/frontend/src/components/Editor/StudylogEditor.tsx b/frontend/src/components/Editor/StudylogEditor.tsx index f33645b54..cfee75cad 100644 --- a/frontend/src/components/Editor/StudylogEditor.tsx +++ b/frontend/src/components/Editor/StudylogEditor.tsx @@ -26,6 +26,7 @@ interface StudylogEditorProps { contentRef: MutableRefObject; selectedMissionId?: number | null; selectedSessionId?: number | null; + selectedAbilities?: number[]; selectedTags?: Tag[]; content?: string | null; @@ -33,6 +34,7 @@ interface StudylogEditorProps { onSelectMission: (mission: SelectOption) => void; onSelectSession: (session: SelectOption) => void; onSelectTag: (tags: Tag[], actionMeta: { option: { label: string } }) => void; + onSelectAbilities: (abilities: number[]) => void; onSubmit?: FormEventHandler; } @@ -41,12 +43,14 @@ const StudylogEditor = ({ selectedMissionId = null, selectedSessionId = null, selectedTags = [], + selectedAbilities = [], contentRef, content, onChangeTitle, onSelectMission, onSelectSession, onSelectTag, + onSelectAbilities, onSubmit, }: StudylogEditorProps): JSX.Element => { return ( @@ -75,9 +79,11 @@ const StudylogEditor = ({ selectedMissionId={selectedMissionId} selectedSessionId={selectedSessionId} selectedTagList={selectedTags} + selectedAbilities={selectedAbilities} onSelectTag={onSelectTag} onSelectMission={onSelectMission} onSelectSession={onSelectSession} + onSelectAbilities={onSelectAbilities} />
      diff --git a/frontend/src/components/Introduction/Introduction.js b/frontend/src/components/Introduction/Introduction.js index c65009c60..f3c489699 100644 --- a/frontend/src/components/Introduction/Introduction.js +++ b/frontend/src/components/Introduction/Introduction.js @@ -1,15 +1,9 @@ /** @jsxImportSource @emotion/react */ -import { useContext, useState } from 'react'; +import { useContext, useState, useRef } from 'react'; import { css } from '@emotion/react'; import { useParams } from 'react-router-dom'; -import useMutation from '../../hooks/useMutation'; -import useRequest from '../../hooks/useRequest'; -import { - requestEditProfileIntroduction, - requestGetProfileIntroduction, -} from '../../service/requests'; import EditIntroduction from './EditIntroduction'; import { Button } from '..'; import { ERROR_MESSAGE } from '../../constants'; @@ -29,31 +23,29 @@ import { import { markdownStyle } from '../../styles/markdown.styles'; import { EditButtonStyle, WrapperStyle } from './Introduction.styles'; import { UserContext } from '../../contexts/UserProvider'; +import { + useGetProfileIntroduction, + usePutProfileIntroductionMutation, +} from '../../hooks/queries/profile'; const Introduction = () => { const { username } = useParams(); const [isEditing, setIsEditing] = useState(false); - const [editorContentRef, setEditorContentRef] = useState(''); + const editorContentRef = useRef(''); const { user } = useContext(UserContext); const { accessToken, username: loginName } = user; - const { response, fetchData } = useRequest({ text: '' }, () => - requestGetProfileIntroduction(username) - ); + const { data: response, refetch: fetchData } = useGetProfileIntroduction({ username }); + const data = response?.text ?? ''; const isOwner = username === loginName; const hasIntro = !!data.length; - const { mutate: editProfileIntro } = useMutation( - () => - requestEditProfileIntroduction( - username, - { text: editorContentRef.getInstance().getMarkdown() }, - accessToken - ), + const { mutate: editProfileIntro } = usePutProfileIntroductionMutation( + { username, editorContentRef, accessToken }, { onSuccess: async () => { await fetchData(); @@ -141,7 +133,7 @@ const Introduction = () => { setIsEditing(false)} /> )} diff --git a/frontend/src/components/Items/LevellogItem.styles.ts b/frontend/src/components/Items/LevellogItem.styles.ts new file mode 100644 index 000000000..15c1d946d --- /dev/null +++ b/frontend/src/components/Items/LevellogItem.styles.ts @@ -0,0 +1,62 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import { Link } from 'react-router-dom'; +import { COLOR } from '../../enumerations/color'; + +export const CardStyle = css` + transition: transform 0.2s ease; + cursor: pointer; + padding: 3rem; + height: 20rem; + + :hover { + transform: scale(1.005); + } +`; + +export const ContentStyle = css` + display: flex; + justify-content: space-between; + height: 100%; +`; + +export const DescriptionStyle = css` + width: calc(100% - 12rem); + display: flex; + flex-direction: column; + height: inherit; + + h3 { + font-size: 2.8rem; + color: ${COLOR.DARK_GRAY_900}; + font-weight: bold; + + height: 100%; + + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + + @media screen and (max-width: 420px) { + font-size: 2.5rem; + height: calc(2.5rem * 4.5); + } + } +`; + +export const ProfileChipLocationStyle = css` + flex-shrink: 0; + margin-left: 1rem; + + &:hover { + background-color: ${COLOR.LIGHT_BLUE_100}; + } +`; + +export const NoDefaultHoverLink = styled(Link)` + :hover { + font-weight: unset; + } +`; diff --git a/frontend/src/components/Items/LevellogItem.tsx b/frontend/src/components/Items/LevellogItem.tsx new file mode 100644 index 000000000..87f60d83a --- /dev/null +++ b/frontend/src/components/Items/LevellogItem.tsx @@ -0,0 +1,35 @@ +/** @jsxImportSource @emotion/react */ + +import { + CardStyle, + ContentStyle, + DescriptionStyle, + ProfileChipLocationStyle, +} from './StudylogItem.styles'; +import { AlignItemsEndStyle, FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; +import Card from '../Card/Card'; +import ProfileChip from '../ProfileChip/ProfileChip'; +import { NoDefaultHoverLink } from './LevellogItem.styles'; + +const LevellogItem = ({ levellog }) => { + const { author, title } = levellog; + + return ( + +
      +
      +

      {title}

      +
      +
      + + + {author.nickname} + + +
      +
      +
      + ); +}; + +export default LevellogItem; diff --git a/frontend/src/components/Items/NewLevellogQnAInputItem.styles.ts b/frontend/src/components/Items/NewLevellogQnAInputItem.styles.ts new file mode 100644 index 000000000..9b8495584 --- /dev/null +++ b/frontend/src/components/Items/NewLevellogQnAInputItem.styles.ts @@ -0,0 +1,64 @@ +import styled from '@emotion/styled'; +import TextareaAutosize from 'react-textarea-autosize'; + +import { COLOR } from '../../constants'; +import { + AlignItemsCenterStyle, + FlexColumnStyle, + FlexStyle, + JustifyContentCenterStyle, +} from '../../styles/flex.styles'; +import { ReactComponent as MinusIcon } from '../../assets/images/minus_icon.svg'; + +const QnAInputBasicStyle = { + padding: '1rem', + border: 'none', + + ':focus': { + border: 'none', + 'outline-color': `${COLOR.LIGHT_BLUE_200}`, + }, +}; + +export const Container = styled.div` + position: relative; +`; +export const QnAForm = styled.form` + ${FlexStyle} + ${FlexColumnStyle} + background-color: ${COLOR.LIGHT_BLUE_200}; + border-radius: 2rem; +`; +export const QnAQuestionWrapper = styled.div` + ${FlexStyle} + ${AlignItemsCenterStyle} + + gap:1rem; + padding: 1rem; +`; +export const QnAQuestionIndex = styled.span` + ${FlexStyle} + ${JustifyContentCenterStyle} + + width: 50px; + font-weight: bold; +`; +export const QnAQuestionInput = styled.input` + ${QnAInputBasicStyle} + width: 100%; + + border-radius: 1rem; +`; +export const QnAAnswerTextarea = styled(TextareaAutosize)` + ${QnAInputBasicStyle} +`; +export const DeleteQnAButton = styled(MinusIcon)` + position: absolute; + top: -10px; + right: -10px; + + width: 24px; + height: 24px; + + cursor: pointer; +`; diff --git a/frontend/src/components/Items/NewLevellogQnAInputItem.tsx b/frontend/src/components/Items/NewLevellogQnAInputItem.tsx new file mode 100644 index 000000000..ef4f7e8be --- /dev/null +++ b/frontend/src/components/Items/NewLevellogQnAInputItem.tsx @@ -0,0 +1,35 @@ +import * as S from './NewLevellogQnAInputItem.styles'; + +const NewLevellogQnAInputItem = ({ + question, + onChangeQuestion, + answer, + onChangeAnswer, + idx, + onDeleteQnA, +}) => { + return ( + + - + + + Q{idx} + + + + + + ); +}; + +export default NewLevellogQnAInputItem; diff --git a/frontend/src/components/Items/PopularStudylogItem.styles.ts b/frontend/src/components/Items/PopularStudylogItem.styles.ts index d355d95ca..553033efa 100644 --- a/frontend/src/components/Items/PopularStudylogItem.styles.ts +++ b/frontend/src/components/Items/PopularStudylogItem.styles.ts @@ -123,21 +123,21 @@ export const UserReactionIconStyle = css` `; export const ProfileAreaStyle = css` - div { - span { - margin-left: 1rem; + width: fit-content; - font-size: 1.4rem; - } + span { + margin-left: 1rem; + + font-size: 1.4rem; + } - img { - width: 2.7rem; - height: 2.7rem; + img { + width: 2.7rem; + height: 2.7rem; - border-radius: 3rem; + border-radius: 3rem; - z-index: 10; - } + z-index: 10; } `; diff --git a/frontend/src/components/Items/PopularStudylogItem.tsx b/frontend/src/components/Items/PopularStudylogItem.tsx index 6e3ffa7ab..a6e772f68 100644 --- a/frontend/src/components/Items/PopularStudylogItem.tsx +++ b/frontend/src/components/Items/PopularStudylogItem.tsx @@ -1,7 +1,6 @@ /** @jsxImportSource @emotion/react */ import { Link } from 'react-router-dom'; -import { css } from '@emotion/react'; import { Chip } from '..'; import { PATH } from '../../enumerations/path'; @@ -32,30 +31,16 @@ import { ReactComponent as UnScrapIcon } from '../../assets/images/scrap.svg'; import type { Studylog } from '../../models/Studylogs'; const PopularStudylogItem = ({ item }: { item: Studylog }) => { - const { - title, - content, - id, - author, - tags, - createdAt, - viewCount, - liked, - likesCount, - scrap, - scrapedCount, - } = item; + const { title, content, id, author, tags, createdAt, viewCount, liked, likesCount, scrap } = item; return (
      {/* 상단 영역 */}
      {/* 프로필 영역 */} - -
      - - {author.nickname} -
      + + + {author.nickname} {/* 제목 영역 */} @@ -111,7 +96,6 @@ const PopularStudylogItem = ({ item }: { item: Studylog }) => { ) : ( )} - {scrapedCount}
      {new Date(createdAt).toLocaleDateString('ko-KR')} diff --git a/frontend/src/components/Items/StudylogItem.tsx b/frontend/src/components/Items/StudylogItem.tsx index 41b77eea9..a62c7ab95 100644 --- a/frontend/src/components/Items/StudylogItem.tsx +++ b/frontend/src/components/Items/StudylogItem.tsx @@ -16,15 +16,13 @@ import { } from './StudylogItem.styles'; import { AlignItemsEndStyle, FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; -const StudylogItem = ({ - studylog, - onClick, - onProfileClick, -}: { +interface Props { studylog: Studylog; onClick: () => void; onProfileClick: (event?: MouseEvent) => void; -}) => { +} + +const StudylogItem = ({ studylog, onClick, onProfileClick }: Props) => { const { author, title, tags, read: isRead, viewCount, session } = studylog; return ( diff --git a/frontend/src/components/Lists/LevellogList.styles.ts b/frontend/src/components/Lists/LevellogList.styles.ts new file mode 100644 index 000000000..6d81666fe --- /dev/null +++ b/frontend/src/components/Lists/LevellogList.styles.ts @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; +import { Link } from 'react-router-dom'; + +export const NoDefaultHoverLink = styled(Link)` + :hover { + font-weight: unset; + } +`; diff --git a/frontend/src/components/Lists/LevellogList.tsx b/frontend/src/components/Lists/LevellogList.tsx new file mode 100644 index 000000000..11deb805c --- /dev/null +++ b/frontend/src/components/Lists/LevellogList.tsx @@ -0,0 +1,28 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import { PATH } from '../../constants'; + +import LevellogItem from '../Items/LevellogItem'; +import { NoDefaultHoverLink } from './LevellogList.styles'; + +const LevellogList = ({ levellogs }) => { + return ( +
        li:not(:last-child) { + margin-bottom: 1.6rem; + } + `} + > + {levellogs.map((levellog) => ( +
      • + + + +
      • + ))} +
      + ); +}; + +export default LevellogList; diff --git a/frontend/src/components/Lists/NewLevellogQnAInputList.styles.ts b/frontend/src/components/Lists/NewLevellogQnAInputList.styles.ts new file mode 100644 index 000000000..a9cbfd09b --- /dev/null +++ b/frontend/src/components/Lists/NewLevellogQnAInputList.styles.ts @@ -0,0 +1,33 @@ +import styled from '@emotion/styled'; + +import { FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; +import { ReactComponent as PlusIcon } from '../../assets/images/plus_icon.svg'; +import { COLOR } from '../../constants'; + +export const Container = styled.div` + ${FlexStyle} + ${FlexColumnStyle} + gap:15px; +`; +export const QnAItemsWrapper = styled.div` + ${FlexStyle} + ${FlexColumnStyle} + gap:15px; +`; +export const Label = styled.label` + font-size: larger; +`; +export const AddQnAButton = styled(PlusIcon)` + width: 40px; + height: 40px; + fill: ${COLOR.LIGHT_BLUE_500}; + + align-self: flex-end; + + cursor: pointer; + transition: all ease-in-out 0.1s; + :hover { + transform: scale(1.1); + opacity: 0.6; + } +`; diff --git a/frontend/src/components/Lists/NewLevellogQnAInputList.tsx b/frontend/src/components/Lists/NewLevellogQnAInputList.tsx new file mode 100644 index 000000000..a416e0371 --- /dev/null +++ b/frontend/src/components/Lists/NewLevellogQnAInputList.tsx @@ -0,0 +1,49 @@ +import { ChangeEvent } from 'react'; +import { QnAType } from '../../models/Levellogs'; +import NewLevellogQnAInputItem from '../Items/NewLevellogQnAInputItem'; + +import * as S from './NewLevellogQnAInputList.styles'; + +export interface NewLevellogQnAListProps { + QnAList: QnAType[]; + onAddQnA?: () => void; + onDeleteQnA: (index: number) => void; + onChangeQuestion: (value: string, index: number) => void; + onChangeAnswer: (value: string, index: number) => void; +} + +const NewLevellogQnAList = ({ + QnAList, + onAddQnA, + onDeleteQnA, + onChangeQuestion, + onChangeAnswer, +}: NewLevellogQnAListProps) => { + return ( + + Questions + + {QnAList.map(({ question, answer }, questionNumber) => ( + ) => { + onChangeQuestion(e.target.value, questionNumber); + }} + answer={answer} + onChangeAnswer={(e: ChangeEvent) => { + onChangeAnswer(e.target.value, questionNumber); + }} + onDeleteQnA={() => { + onDeleteQnA(questionNumber); + }} + /> + ))} + + {onAddQnA && } + + ); +}; + +export default NewLevellogQnAList; diff --git a/frontend/src/components/NavBar/NavBar.js b/frontend/src/components/NavBar/NavBar.js index 3c361b792..21c049beb 100644 --- a/frontend/src/components/NavBar/NavBar.js +++ b/frontend/src/components/NavBar/NavBar.js @@ -21,10 +21,11 @@ import { profileButtonStyle, Navigation, loginButtonStyle, + WritingDropdownStyle, } from './NavBar.styles'; import { ERROR_MESSAGE } from '../../constants/message'; import { UserContext } from '../../contexts/UserProvider'; -import { APP_MODE, isProd } from '../../configs/environment'; +import { APP_MODE, BASE_URL, isProd } from '../../configs/environment'; const navigationConfig = [ { @@ -35,6 +36,10 @@ const navigationConfig = [ path: PATH.STUDYLOG, title: '학습로그', }, + // { + // path: PATH.LEVELLOG, + // title: '레벨로그', + // }, ]; const NavBar = () => { @@ -46,25 +51,21 @@ const NavBar = () => { const { username, imageUrl: userImage = NoProfileImage, accessToken, isLoggedIn } = user; const [isDropdownToggled, setDropdownToggled] = useState(false); + const [isWritingDropdownToggled, setWritingDropdownToggled] = useState(false); const goMain = () => { history.push(PATH.ROOT); }; - const goNewStudylog = async () => { - if (!accessToken) { - alert(ERROR_MESSAGE.LOGIN_DEFAULT); - - return; - } - - history.push(PATH.NEW_STUDYLOG); - }; - const showDropdownMenu = () => { setDropdownToggled(true); }; + const hideWritingDropdownMenu = (event) => { + if (event.currentTarget === event.target) { + setWritingDropdownToggled(false); + } + }; const hideDropdownMenu = (event) => { if (event.currentTarget === event.target) { setDropdownToggled(false); @@ -74,11 +75,18 @@ const NavBar = () => { const onSelectMenu = (event) => { if (event.target.tagName === 'A') { setDropdownToggled(false); + setWritingDropdownToggled(false); } }; return ( - + { + hideDropdownMenu(e); + hideWritingDropdownMenu(e); + }} + > @@ -102,13 +110,38 @@ const NavBar = () => { {isLoggedIn ? ( <> - + /> ); }; diff --git a/frontend/src/pages/ProfilePageReports/ReportStudyLogs.styles.ts b/frontend/src/components/ReportStudyLogs/ReportStudyLogs.styles.ts similarity index 98% rename from frontend/src/pages/ProfilePageReports/ReportStudyLogs.styles.ts rename to frontend/src/components/ReportStudyLogs/ReportStudyLogs.styles.ts index 33882f7d5..58d6e7050 100644 --- a/frontend/src/pages/ProfilePageReports/ReportStudyLogs.styles.ts +++ b/frontend/src/components/ReportStudyLogs/ReportStudyLogs.styles.ts @@ -72,6 +72,7 @@ export const StudyLogTitle = styled.td` font-size: 1.6rem; a { + padding: 0.84rem 2rem; :hover { text-decoration: underline; } diff --git a/frontend/src/pages/ProfilePageReports/ReportStudyLogs.tsx b/frontend/src/components/ReportStudyLogs/ReportStudyLogs.tsx similarity index 97% rename from frontend/src/pages/ProfilePageReports/ReportStudyLogs.tsx rename to frontend/src/components/ReportStudyLogs/ReportStudyLogs.tsx index cdab24218..d687df2b0 100644 --- a/frontend/src/pages/ProfilePageReports/ReportStudyLogs.tsx +++ b/frontend/src/components/ReportStudyLogs/ReportStudyLogs.tsx @@ -21,8 +21,6 @@ const ReportStudyLogs = ({ studylogs }) => { }); }; - if (studylogs?.length === 0) return <>; - return ( <> 📝 학습로그 diff --git a/frontend/src/constants/errorCode.js b/frontend/src/constants/errorCode.js index f80682a41..b8d8ced40 100644 --- a/frontend/src/constants/errorCode.js +++ b/frontend/src/constants/errorCode.js @@ -4,6 +4,8 @@ const ERROR_CODE = { NO_CONTENT: 2001, NO_TITLE: 2002, + NOT_EXIST_LEVELLOG: 7002, + SERVER_ERROR: -9999, }; diff --git a/frontend/src/constants/message.js b/frontend/src/constants/message.js index 831d3de48..ec5630085 100644 --- a/frontend/src/constants/message.js +++ b/frontend/src/constants/message.js @@ -12,12 +12,16 @@ const ALERT_MESSAGE = { ACCESS_DENIED: '잘못된 접근입니다.', FAIL_TO_DELETE_STUDYLOG: '글을 삭제할 수 없습니다.', FAIL_TO_UPLOAD_IMAGE: '이미지 업로드를 할 수 없습니다.', + FAIL_TO_POST_LEVELLOG: '레벨로그 작성에 실패했습니다.', NEED_TO_LOGIN: '로그인 후 이용 가능합니다', OVER_PROFILE_NICKNAME_MAX_LENGTH: '닉네임은 4글자 이하로 입력해주세요.', + NO_EXIST_POST: '존재하지 않는 글입니다.', CANNOT_EDIT_OTHERS: '본인이 작성하지 않은 글은 수정할 수 없습니다.', NO_CONTENT: '내용을 입력하세요', NO_TITLE: '제목을 입력하세요', + NO_QUESTION_AND_ANSWER: '질답을 완성해주세요', + NO_QNA: '적어도 하나의 질답을 작성해주세요.', }; const ERROR_MESSAGE = { diff --git a/frontend/src/constants/path.js b/frontend/src/constants/path.js index 3aa6a1cdc..b232c7ab5 100644 --- a/frontend/src/constants/path.js +++ b/frontend/src/constants/path.js @@ -8,9 +8,11 @@ const PATH = { PROFILE_REPORT: '/:username/reports/:reportId', PROFILE_NEW_REPORT: '/:username/reports/write', LOGIN_CALLBACK: '/login/callback', + STUDYLOG: '/studylogs', NEW_STUDYLOG: '/studylog/write', + LEVELLOG: '/levellogs', + NEW_LEVELLOG: '/levellog/write', ABILITY: '/:username/ability', - STUDYLOG: '/studylogs', }; export default PATH; diff --git a/frontend/src/contexts/UserProvider.js b/frontend/src/contexts/UserProvider.js index cfd3b294e..edf55604c 100644 --- a/frontend/src/contexts/UserProvider.js +++ b/frontend/src/contexts/UserProvider.js @@ -1,10 +1,10 @@ import { createContext, useState, useEffect } from 'react'; +import { client } from '../apis'; import LOCAL_STORAGE_KEY from '../constants/localStorage'; -import useMutation from '../hooks/useMutation'; import useRequest from '../hooks/useRequest'; -import { getUserProfileRequest, loginRequest } from '../service/requests'; -import { getLocalStorageItem } from '../utils/localStorage'; +import { getUserProfileRequest } from '../service/requests'; +import { useLogin } from '../hooks/queries/auth'; const DEFAULT_USER = { userId: null, @@ -21,7 +21,7 @@ export const UserContext = createContext(DEFAULT_USER); const UserProvider = ({ children }) => { const [state, setState] = useState({ ...DEFAULT_USER, - accessToken: getLocalStorageItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN), + accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN), }); useEffect(() => { @@ -51,18 +51,17 @@ const UserProvider = ({ children }) => { onLogout ); - const { mutate: onLogin } = useMutation(loginRequest, { - onSuccess: ({ accessToken }) => { - localStorage.setItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN, JSON.stringify(accessToken)); + const { mutate: onLogin } = useLogin({ + onSuccess: (accessToken) => { + localStorage.setItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN, accessToken); + client.defaults.headers['Authorization'] = `Bearer ${accessToken}`; setState((prev) => ({ ...prev, accessToken })); }, - onError: (error) => { - alert(error.message); - }, }); function onLogout() { localStorage.removeItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN); + client.defaults.headers['Authorization'] = ''; setState(DEFAULT_USER); } diff --git a/frontend/src/hooks/Ability/useAbility.ts b/frontend/src/hooks/Ability/useAbility.ts index 17c3a3421..414d35ea9 100644 --- a/frontend/src/hooks/Ability/useAbility.ts +++ b/frontend/src/hooks/Ability/useAbility.ts @@ -1,3 +1,4 @@ +import { Dispatch, SetStateAction } from 'react'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import AbilityRequest, { ErrorData } from '../../apis/ability'; import { ERROR_MESSAGE } from '../../constants'; @@ -8,12 +9,12 @@ interface AbilityForm { name: string; description: string; color: string; - isParent: boolean | null; + isParent: boolean; } interface Props { username: string; - setAddFormStatus: (data: AbilityForm) => void; + setAddFormStatus: Dispatch>; addFormClose: () => void; } diff --git a/frontend/src/hooks/Ability/useParentAbilityForm.ts b/frontend/src/hooks/Ability/useParentAbilityForm.ts index 2eb183248..63e73eab9 100644 --- a/frontend/src/hooks/Ability/useParentAbilityForm.ts +++ b/frontend/src/hooks/Ability/useParentAbilityForm.ts @@ -5,7 +5,7 @@ export const DEFAULT_ABILITY_FORM = { name: '', description: '', color: '#000000', - isParent: null, + isParent: false, }; const useParentAbilityForm = () => { diff --git a/frontend/src/hooks/Comment/useStudylogComment.ts b/frontend/src/hooks/Comment/useStudylogComment.ts new file mode 100644 index 000000000..e9c8039b9 --- /dev/null +++ b/frontend/src/hooks/Comment/useStudylogComment.ts @@ -0,0 +1,37 @@ +import { CommentRequest } from '../../models/Comment'; +import { + useCreateComment, + useDeleteCommentMutation, + useEditCommentMutation, + useFetchComments, +} from '../queries/comment'; + +const useStudylogComment = (studylogId: number) => { + const { data } = useFetchComments(studylogId); + const comments = data?.data; + + const createCommentMutation = useCreateComment(studylogId); + const editCommentMutation = useEditCommentMutation(studylogId); + const deleteCommentMutation = useDeleteCommentMutation(studylogId); + + const createComment = (body: CommentRequest) => { + createCommentMutation.mutate(body); + }; + + const editComment = (commentId: number, body: CommentRequest) => { + editCommentMutation.mutate({ commentId, body }); + }; + + const deleteComment = (commentId: number) => { + deleteCommentMutation.mutate(commentId); + }; + + return { + comments, + createComment, + editComment, + deleteComment, + }; +}; + +export default useStudylogComment; diff --git a/frontend/src/hooks/Levellog/useEditLevellog.ts b/frontend/src/hooks/Levellog/useEditLevellog.ts new file mode 100644 index 000000000..546025005 --- /dev/null +++ b/frontend/src/hooks/Levellog/useEditLevellog.ts @@ -0,0 +1,115 @@ +import { MouseEvent, useContext, useRef } from 'react'; +import { ChangeEvent, useState } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { PATH } from '../../constants'; +import { SUCCESS_MESSAGE } from '../../constants/message'; +import { useEditLevellogMutation, useGetLevellog } from '../queries/levellog'; +import { ALERT_MESSAGE } from '../../constants'; +import useSnackBar from '../useSnackBar'; +import useQnAInputList from './useQnAInputList'; +import useBeforeunload from '../useBeforeunload'; +import { UserContext } from '../../contexts/UserProvider'; + +const useEditLevellog = () => { + const { id } = useParams<{ id: string }>(); + const { + user: { userId }, + } = useContext(UserContext); + + const history = useHistory(); + const { openSnackBar } = useSnackBar(); + const editorContentRef = useRef(null); + + useBeforeunload(editorContentRef); + + const { + QnAList, + setQnAList, + onChangeAnswer, + onChangeQuestion, + onDeleteQnA, + onAddQnA, + } = useQnAInputList(); + const EditLevellogQnAListProps = { + QnAList, + onChangeAnswer, + onChangeQuestion, + onDeleteQnA, + onAddQnA, + }; + + const [title, setTitle] = useState(''); + const onChangeTitle = (e: ChangeEvent) => { + setTitle(e.target.value); + }; + + const { data: levellog, isLoading } = useGetLevellog( + { id }, + { + onSuccess: (levellog) => { + if (levellog.author.id !== userId) { + openSnackBar(ALERT_MESSAGE.CANNOT_EDIT_OTHERS); + history.push(`${PATH.LEVELLOG}/${id}`); + } + + setTitle(levellog.title); + setQnAList( + levellog.levelLogs.map((log) => ({ question: log.question, answer: log.answer })) + ); + }, + } + ); + const { mutate: editLevellogRequest } = useEditLevellogMutation( + { id }, + { + onSuccess: () => { + openSnackBar(SUCCESS_MESSAGE.EDIT_POST); + history.push(`${PATH.LEVELLOG}/${id}`); + }, + } + ); + + const editLevellog = (e: MouseEvent) => { + e.preventDefault(); + + const content = editorContentRef.current?.getInstance().getMarkdown() || ''; + + if (title.length === 0) { + alert(ALERT_MESSAGE.NO_TITLE); + return; + } + + if (content.length === 0) { + alert(ALERT_MESSAGE.NO_CONTENT); + return; + } + + if (QnAList.length < 1) { + alert(ALERT_MESSAGE.NO_QNA); + return; + } + + if (QnAList.some((QnA) => QnA.answer.length < 1 || QnA.question.length < 1)) { + alert(ALERT_MESSAGE.NO_QUESTION_AND_ANSWER); + return; + } + + editLevellogRequest({ + title, + content: editorContentRef.current.getInstance().getMarkdown(), + levelLogs: QnAList, + }); + }; + + return { + editorContentRef, + title, + onChangeTitle, + content: levellog?.content, + EditLevellogQnAListProps, + isLoading, + editLevellog, + }; +}; + +export default useEditLevellog; diff --git a/frontend/src/hooks/Levellog/useLevellog.ts b/frontend/src/hooks/Levellog/useLevellog.ts new file mode 100644 index 000000000..856863509 --- /dev/null +++ b/frontend/src/hooks/Levellog/useLevellog.ts @@ -0,0 +1,63 @@ +import { useHistory, useParams } from 'react-router-dom'; +import TagManager from 'react-gtm-module'; + +import { useDeleteLevellogMutation, useGetLevellog } from '../queries/levellog'; +import { useContext } from 'react'; +import { UserContext } from '../../contexts/UserProvider'; +import { PATH, ERROR_MESSAGE, ALERT_MESSAGE } from '../../constants'; +import { SUCCESS_MESSAGE } from '../../constants/message'; +import useSnackBar from '../useSnackBar'; + +const useLevellog = () => { + const { id } = useParams<{ id: string }>(); + const history = useHistory(); + const { user } = useContext(UserContext); + const { openSnackBar } = useSnackBar(); + const { username, userId } = user; + + const { data: levellog, isLoading, refetch: getLevellog } = useGetLevellog( + { id }, + { + onSuccess: (levellog) => { + TagManager.dataLayer({ + dataLayer: { + event: 'page_view_levellog', + mine: username === levellog.author?.username, + user_id: userId, + username, + target: levellog.id, + }, + }); + }, + } + ); + const isCurrentUserAuthor = levellog?.author.username === username; + + const { mutate: deleteLevellog } = useDeleteLevellogMutation( + { id }, + { + onSuccess: () => { + openSnackBar(SUCCESS_MESSAGE.DELETE_STUDYLOG); + history.push(PATH.LEVELLOG); + }, + onError: (error: { code: number }) => { + alert(ERROR_MESSAGE[error.code] ?? ALERT_MESSAGE.FAIL_TO_POST_LEVELLOG); + }, + } + ); + + const goEditTargetPost = () => { + history.push(`${PATH.LEVELLOG}/${id}/edit`); + }; + + return { + levellog, + getLevellog, + deleteLevellog, + goEditTargetPost, + isCurrentUserAuthor, + isLoading, + }; +}; + +export default useLevellog; diff --git a/frontend/src/hooks/Levellog/useLevellogList.ts b/frontend/src/hooks/Levellog/useLevellogList.ts new file mode 100644 index 000000000..a1f0db909 --- /dev/null +++ b/frontend/src/hooks/Levellog/useLevellogList.ts @@ -0,0 +1,24 @@ +import { useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { UserContext } from '../../contexts/UserProvider'; +import { useGetLevellogs } from '../queries/levellog'; + +export const useLevellogList = () => { + const history = useHistory(); + const { user } = useContext(UserContext); + + const currPage = Number(history.location.search.replace('?page=', '')) || 1; + const { isLoggedIn } = user; + + const { data: levellogs, refetch: getAllLevellogs, isLoading } = useGetLevellogs(currPage); + + const onChangeCurrentPage = (page) => { + const params = page !== 1 ? `?page=${page}` : ''; + const url = `/levellogs${params}`; + + history.push(url); + window.scrollTo({ left: 0, top: 0 }); + }; + + return { levellogs, getAllLevellogs, isLoading, currPage, onChangeCurrentPage, isLoggedIn }; +}; diff --git a/frontend/src/hooks/Levellog/useNewLevellog.ts b/frontend/src/hooks/Levellog/useNewLevellog.ts new file mode 100644 index 000000000..43f250290 --- /dev/null +++ b/frontend/src/hooks/Levellog/useNewLevellog.ts @@ -0,0 +1,83 @@ +import { useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ALERT_MESSAGE, PATH, ERROR_MESSAGE } from '../../constants'; +import { QnAType } from '../../models/Levellogs'; +import { useCreateNewLevellogMutation } from '../queries/levellog'; +import useBeforeunload from '../useBeforeunload'; +import { SUCCESS_MESSAGE } from '../../constants/message'; +import useSnackBar from '../useSnackBar'; +import useQnAInputList from './useQnAInputList'; + +const useNewLevellog = () => { + const history = useHistory(); + const editorContentRef = useRef(null); + const { openSnackBar } = useSnackBar(); + + const [title, setTitle] = useState(''); + const onChangeTitle = (e) => { + setTitle(e.target.value); + }; + + useBeforeunload(editorContentRef); + + const { mutate: createNewLevellogRequest } = useCreateNewLevellogMutation({ + onSuccess: () => { + history.push(PATH.LEVELLOG); + alert(SUCCESS_MESSAGE.CREATE_POST); + }, + onError: (error: { code: number }) => { + openSnackBar(ERROR_MESSAGE[error.code] ?? ALERT_MESSAGE.FAIL_TO_POST_LEVELLOG); + }, + }); + + const { QnAList, onAddQnA, onChangeAnswer, onChangeQuestion, onDeleteQnA } = useQnAInputList(); + const NewLevellogQnAListProps = { + QnAList, + onAddQnA, + onChangeQuestion, + onChangeAnswer, + onDeleteQnA, + }; + + const createNewLevellog = (e) => { + e.preventDefault(); + + const content = editorContentRef.current?.getInstance().getMarkdown() || ''; + + if (title.length === 0) { + alert(ALERT_MESSAGE.NO_TITLE); + return; + } + + if (content.length === 0) { + alert(ALERT_MESSAGE.NO_CONTENT); + return; + } + + if (QnAList.length < 1) { + alert(ALERT_MESSAGE.NO_QNA); + return; + } + + if (QnAList.some((QnA) => QnA.answer.length < 1 || QnA.question.length < 1)) { + alert(ALERT_MESSAGE.NO_QUESTION_AND_ANSWER); + return; + } + + createNewLevellogRequest({ + title, + content, + levelLogs: [...QnAList], + }); + }; + + return { + createNewLevellog, + editorContentRef, + onChangeTitle, + title, + NewLevellogQnAListProps, + }; +}; + +export default useNewLevellog; diff --git a/frontend/src/hooks/Levellog/useQnAInputList.ts b/frontend/src/hooks/Levellog/useQnAInputList.ts new file mode 100644 index 000000000..72e849561 --- /dev/null +++ b/frontend/src/hooks/Levellog/useQnAInputList.ts @@ -0,0 +1,34 @@ +import { useState } from 'react'; +import { QnAType } from '../../models/Levellogs'; + +const useQnAInputList = () => { + const [QnAList, setQnAList] = useState([{ question: '', answer: '' }]); + + const onAddQnA: () => void = () => { + setQnAList((prev) => [...prev, { question: '', answer: '' }]); + + setTimeout(() => { + window.scrollTo({ left: 0, top: document.body.scrollHeight, behavior: 'smooth' }); + }, 100); + }; + + const onDeleteQnA: (index: number) => void = (index) => { + setQnAList((prev) => prev.filter((_, idx) => idx !== index)); + }; + + const onChangeQuestion: (value: string, index: number) => void = (value, index) => { + const changedQnAList = [...QnAList]; + changedQnAList[index].question = value; + setQnAList(changedQnAList); + }; + + const onChangeAnswer: (value: string, index: number) => void = (value, index) => { + const changedQnAList = [...QnAList]; + changedQnAList[index].answer = value; + setQnAList(changedQnAList); + }; + + return { QnAList, onAddQnA, onDeleteQnA, onChangeAnswer, onChangeQuestion, setQnAList }; +}; + +export default useQnAInputList; diff --git a/frontend/src/hooks/queries/auth.ts b/frontend/src/hooks/queries/auth.ts new file mode 100644 index 000000000..035ab835a --- /dev/null +++ b/frontend/src/hooks/queries/auth.ts @@ -0,0 +1,21 @@ +import { useMutation } from 'react-query'; +import { loginRequest } from '../../service/requests'; + +export const useLogin = ({ onSuccess }) => + useMutation( + async ({ code }) => { + const res = await loginRequest({ code }); + + return await res.json(); + }, + { + onSuccess: ({ accessToken }) => { + onSuccess(accessToken); + }, + onError: (error) => { + if (error instanceof Error) { + alert(error.message); + } + }, + } + ); diff --git a/frontend/src/hooks/queries/comment.ts b/frontend/src/hooks/queries/comment.ts new file mode 100644 index 000000000..c8807f31c --- /dev/null +++ b/frontend/src/hooks/queries/comment.ts @@ -0,0 +1,44 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { createCommentRequest, deleteComment, editComment, getComments } from '../../apis/comment'; +import { CommentRequest } from '../../models/Comment'; + +const QUERY_KEY = { + comments: 'comments', +}; + +export const useFetchComments = (studylogId: number) => + useQuery([QUERY_KEY.comments, studylogId], () => getComments(studylogId)); + +export const useCreateComment = (studylogId: number) => { + const queryClient = useQueryClient(); + + return useMutation((body: CommentRequest) => createCommentRequest({ studylogId, body }), { + onSuccess() { + queryClient.invalidateQueries([QUERY_KEY.comments, studylogId]); + }, + }); +}; + +export const useEditCommentMutation = (studylogId: number) => { + const queryClient = useQueryClient(); + + return useMutation( + ({ commentId, body }: { commentId: number; body: CommentRequest }) => + editComment({ studylogId, commentId, body }), + { + onSuccess() { + queryClient.invalidateQueries([QUERY_KEY.comments, studylogId]); + }, + } + ); +}; + +export const useDeleteCommentMutation = (studylogId: number) => { + const queryClient = useQueryClient(); + + return useMutation((commentId: number) => deleteComment({ studylogId, commentId }), { + onSuccess() { + queryClient.invalidateQueries([QUERY_KEY.comments, studylogId]); + }, + }); +}; diff --git a/frontend/src/hooks/queries/levellog.ts b/frontend/src/hooks/queries/levellog.ts new file mode 100644 index 000000000..1af4a8245 --- /dev/null +++ b/frontend/src/hooks/queries/levellog.ts @@ -0,0 +1,88 @@ +import { AxiosError } from 'axios'; +import { useEffect } from 'react'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { useHistory } from 'react-router-dom'; +import { + createNewLevellogRequest, + requestDeleteLevellog, + requestEditLevellog, + requestGetLevellog, + requestGetLevellogs, +} from '../../apis/levellogs'; +import { ALERT_MESSAGE, PATH } from '../../constants'; +import ERROR_CODE from '../../constants/errorCode'; +import { LevellogRequest, LevellogResponse } from '../../models/Levellogs'; +import useSnackBar from '../useSnackBar'; + +const QUERY_KEY = { + levellogs: 'levellogs', + levellog: 'levellog', +}; + +export const useGetLevellogs = (currPage: number) => { + const queryClient = useQueryClient(); + + useEffect(() => { + queryClient.prefetchQuery([QUERY_KEY.levellogs, currPage + 1], () => + requestGetLevellogs(currPage + 1) + ); + }, [currPage]); + + return useQuery([QUERY_KEY.levellogs, currPage], () => requestGetLevellogs(currPage)); +}; + +export const useCreateNewLevellogMutation = ({ + onSuccess = () => {}, + onError = (error: { code: number }) => {}, +} = {}) => + useMutation((body: LevellogRequest) => createNewLevellogRequest(body), { + onSuccess: () => { + onSuccess?.(); + }, + onError: (error: { code: number }) => { + onError?.(error); + }, + }); + +export const useGetLevellog = ( + { id }, + { onSuccess = (levellog: LevellogResponse) => {}, onError = () => {} } = {} +) => { + const history = useHistory(); + const { openSnackBar } = useSnackBar(); + return useQuery([QUERY_KEY.levellog, id], () => requestGetLevellog(id), { + onSuccess: (levellog: LevellogResponse) => { + onSuccess?.(levellog); + }, + onError: (error) => { + const { response } = (error as unknown) as AxiosError; + + if (response?.data.code === ERROR_CODE.NOT_EXIST_LEVELLOG) { + openSnackBar(ALERT_MESSAGE.NO_EXIST_POST); + history.push(PATH.LEVELLOG); + } + }, + refetchOnWindowFocus: false, + retry: false, + }); +}; + +export const useDeleteLevellogMutation = ( + { id }, + { onSuccess = () => {}, onError = (error: { code: number }) => {} } = {} +) => + useMutation(() => requestDeleteLevellog(id), { + onSuccess: () => { + onSuccess?.(); + }, + onError: (error: { code: number }) => { + onError?.(error); + }, + }); + +export const useEditLevellogMutation = ({ id }, { onSuccess = () => {} } = {}) => + useMutation((body: LevellogRequest) => requestEditLevellog(id, body), { + onSuccess: () => { + onSuccess?.(); + }, + }); diff --git a/frontend/src/hooks/queries/profile.ts b/frontend/src/hooks/queries/profile.ts new file mode 100644 index 000000000..ae15690c7 --- /dev/null +++ b/frontend/src/hooks/queries/profile.ts @@ -0,0 +1,83 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { + requestDeleteScrap, + requestEditProfile, + requestGetProfile, + requestEditProfileIntroduction, + requestGetMyScrap, + requestGetProfileIntroduction, +} from '../../service/requests'; + +const QUERY_KEY = { + scrap: 'scrap', + profile: 'profile', + introduction: 'introduction', +}; + +export const useGetMyScrapQuery = ({ username, accessToken, postQueryParams }) => { + return useQuery([QUERY_KEY.scrap, postQueryParams.page], () => + requestGetMyScrap({ + username, + accessToken, + postQueryParams, + }).then((res) => res.json()) + ); +}; + +export const useDeleteScrapMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(requestDeleteScrap, { + onSuccess() { + queryClient.invalidateQueries([QUERY_KEY.scrap]); + }, + }); +}; + +export const useGetProfileQuery = ({ username }, { onSuccess }) => { + return useQuery(QUERY_KEY.profile, () => requestGetProfile(username).then((res) => res.json()), { + onSuccess: (data) => { + onSuccess(data); + }, + }); +}; + +export const usePutProfileMutation = ({ user, nickname, accessToken }, { onSuccess }) => { + const queryClient = useQueryClient(); + + return useMutation( + () => + requestEditProfile( + { username: user.username, nickname: nickname, imageUrl: user.imageUrl }, + accessToken + ), + { + onSuccess: () => { + queryClient.invalidateQueries([QUERY_KEY.profile]); + onSuccess(); + }, + onError: () => { + alert('회원 정보 수정에 실패했습니다.'); + }, + } + ); +}; +export const usePutProfileIntroductionMutation = ( + { username, editorContentRef, accessToken }, + options +) => { + return useMutation( + () => + requestEditProfileIntroduction( + username, + editorContentRef.current.getInstance().getMarkdown(), + accessToken + ), + options + ); +}; + +export const useGetProfileIntroduction = ({ username }) => + useQuery(QUERY_KEY.introduction, () => + requestGetProfileIntroduction(username).then((res) => res.json()) + ); diff --git a/frontend/src/hooks/queries/report.ts b/frontend/src/hooks/queries/report.ts new file mode 100644 index 000000000..f6544a4e1 --- /dev/null +++ b/frontend/src/hooks/queries/report.ts @@ -0,0 +1,14 @@ +import { useQuery } from 'react-query'; +import { requestGetMatchedStudylogs } from '../../service/requests'; + +const QUERY_KEY = { + matchedStudylogs: 'matchedStudylogs', +}; + +export const useGetMatchedStudylogs = ({ accessToken, startDate, endDate }) => { + return useQuery( + [QUERY_KEY.matchedStudylogs], + () => requestGetMatchedStudylogs({ accessToken, startDate, endDate }).then((res) => res.json()), + { enabled: false } + ); +}; diff --git a/frontend/src/hooks/queries/studylog.ts b/frontend/src/hooks/queries/studylog.ts new file mode 100644 index 000000000..8f9dba14b --- /dev/null +++ b/frontend/src/hooks/queries/studylog.ts @@ -0,0 +1,124 @@ +import axios from 'axios'; +import { useContext } from 'react'; +import { useMutation, useQuery } from 'react-query'; +import { BASE_URL } from '../../configs/environment'; +import { UserContext } from '../../contexts/UserProvider'; +import { Studylog, StudyLogResponse } from '../../models/Studylogs'; +import { requestGetStudylogs } from '../../service/requests'; + +import { + requestPostScrap, + requestDeleteScrap, + requestDeleteLike, + requestDeleteStudylog, + requestPostLike, +} from '../../service/requests'; +import useSnackBar from '../useSnackBar'; +import { + ALERT_MESSAGE, + ERROR_MESSAGE, + SNACKBAR_MESSAGE, + SUCCESS_MESSAGE, +} from '../../constants/message'; +import { useHistory } from 'react-router-dom'; +import { PATH } from '../../constants'; + +const QUERY_KEY = { + recentStudylogs: 'recentStudylogs', + popularStudylogs: 'popularStudylogs', +}; + +export const useGetRecentStudylogsQuery = () => { + const { user } = useContext(UserContext); + const { accessToken } = user; + + return useQuery([QUERY_KEY.recentStudylogs], async () => { + const response = await requestGetStudylogs({ + query: { type: 'searchParams', data: 'size=3' }, + accessToken, + }); + const { data } = await response.json(); + + return data; + }); +}; + +export const useGetPopularStudylogsQuery = () => { + const { user } = useContext(UserContext); + const { accessToken } = user; + + return useQuery('popularStudyLogs', async () => { + const { data } = await axios({ + method: 'get', + url: `${BASE_URL}/studylogs/popular`, + headers: accessToken && { Authorization: 'Bearer ' + accessToken }, + }); + + return data; + }); +}; + +export const useDeleteStudylogMutation = () => { + const history = useHistory(); + const { openSnackBar } = useSnackBar(); + + return useMutation(requestDeleteStudylog, { + onSuccess() { + openSnackBar(SUCCESS_MESSAGE.DELETE_STUDYLOG); + history.push(PATH.STUDYLOG); + }, + onError: (error: { code: number }) => { + alert(ERROR_MESSAGE[error.code] ?? ALERT_MESSAGE.FAIL_TO_DELETE_STUDYLOG); + }, + }); +}; + +// 추가 리팩토링 필요. 아래의 4개 훅에 있는 +// getStudylog는 useStudylog에서 파생된 코드로서, useStudylog 커스텀훅의 의존성을 제거한 후 +// getStudylog 함수를 invalidateQueries로 변경하는 것이 바람직함 + +export const usePostScrapMutation = ({ getStudylog }) => { + const { openSnackBar } = useSnackBar(); + + return useMutation(requestPostScrap, { + onSuccess: async () => { + await getStudylog(); + openSnackBar(SNACKBAR_MESSAGE.SUCCESS_TO_SCRAP); + }, + }); +}; + +export const useDeleteScrapMutation = ({ getStudylog }) => { + const { openSnackBar } = useSnackBar(); + + return useMutation(requestDeleteScrap, { + onSuccess: async () => { + await getStudylog(); + openSnackBar(SNACKBAR_MESSAGE.DELETE_SCRAP); + }, + }); +}; + +export const usePostLikeMutation = ({ getStudylog }) => { + const { openSnackBar } = useSnackBar(); + + return useMutation(requestPostLike, { + onSuccess: async () => { + await getStudylog(); + openSnackBar(SNACKBAR_MESSAGE.SET_LIKE); + }, + onError: () => openSnackBar(SNACKBAR_MESSAGE.ERROR_SET_LIKE), + }); +}; + +export const useDeleteLikeMutation = ({ getStudylog }) => { + const { openSnackBar } = useSnackBar(); + + return useMutation(requestDeleteLike, { + onSuccess: async () => { + await getStudylog(); + openSnackBar(SNACKBAR_MESSAGE.UNSET_LIKE); + }, + onError: () => openSnackBar(SNACKBAR_MESSAGE.ERROR_UNSET_LIKE), + }); +}; diff --git a/frontend/src/hooks/useBeforeunload.ts b/frontend/src/hooks/useBeforeunload.ts new file mode 100644 index 000000000..238ef8f41 --- /dev/null +++ b/frontend/src/hooks/useBeforeunload.ts @@ -0,0 +1,17 @@ +import { Editor as ToastEditor } from '@toast-ui/react-editor'; +import { useEffect, MutableRefObject } from 'react'; + +const useBeforeunload = (editorContentRef: MutableRefObject) => { + useEffect(() => { + window.addEventListener('beforeunload', (event) => { + const content = editorContentRef.current?.getInstance().getMarkdown() || ''; + + if (content) { + event.preventDefault(); + event.returnValue = ''; + } + }); + }, []); +}; + +export default useBeforeunload; diff --git a/frontend/src/hooks/useFetchData.ts b/frontend/src/hooks/useFetchData.ts deleted file mode 100644 index f53f81471..000000000 --- a/frontend/src/hooks/useFetchData.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useState, useEffect } from 'react'; -import { getResponseData } from '../utils/response'; - -type ErrorType = { message: string; code: number }; - -const isError = (arg: any): arg is ErrorType => - arg && arg.message === 'string' && arg.code === 'number'; - -interface FetchOptions { - /** - * @description 요청 성공 시 실행할 함수 - */ - onSuccess?: (data: T) => void; - /** - * @description 요청 실패 시 실행할 함수 - */ - onError?: (data: unknown) => void; - /** - * @description 요청 완료 시 실행할 함수 - */ - onFinish?: () => void; - /** - * @description 첫 fetch 여부 - * @default true; - */ - initialFetch?: boolean; -} - -/** - * @description 데이터를 fetch 하는 커스텀 훅, 응답값과, 에러, 로딩 상태 및 refetch 수단을 반환합니다. - * @param defaultValue - * @param callback 데이터 fetch 함수 (ex, fetch, axios) - * @param options {onSuccess: 성공 시 실행할 콜백, onError: 에러 시 실행할 콜백, onFinish: 요청 종료시 실행할 콜백} - * @returns { response, error, fetchData } - */ - -const useFetchData = ( - defaultValue: T, - callback: () => Promise, - options?: FetchOptions -) => { - const { onSuccess, onError, onFinish, initialFetch = true } = options ?? {}; - - const [response, setResponse] = useState(defaultValue); - const [error, setError] = useState(''); - - const [isLoading, setIsLoading] = useState(initialFetch); - - const fetchData = async () => { - try { - const response = await callback(); - - if (!response) { - return; - } - - if (!response.ok) { - throw new Error(await response.text()); - } - - const responseData = await getResponseData(response); - - setResponse(responseData); - onSuccess?.(responseData); - } catch (error) { - if (isError(error)) { - const errorResponse = JSON.parse(error.message); - - console.error(errorResponse); - setError(errorResponse.code); - onError?.({ code: errorResponse.code, message: errorResponse.message }); - } - } finally { - onFinish?.(); - setIsLoading(false); - } - }; - - useEffect(() => { - if (isLoading) { - fetchData(); - } - }, [isLoading]); - - return { response, error, refetch: () => setIsLoading(true), isLoading }; -}; - -export default useFetchData; diff --git a/frontend/src/index.js b/frontend/src/index.js index 4c612d445..42d10d130 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -5,9 +5,14 @@ import 'antd/dist/antd.css'; import App from './App'; import store from './redux/store'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { ReactQueryDevtools } from 'react-query/devtools'; import UserProvider from './contexts/UserProvider'; +import GlobalStyles from './GlobalStyles'; -if (process.env.NODE_ENV === 'local') { +const queryClient = new QueryClient(); + +if (process.env.NODE_ENV === 'development') { const { worker } = require('./mocks/browser'); worker.start(); @@ -15,11 +20,15 @@ if (process.env.NODE_ENV === 'local') { ReactDOM.render( - + - + + + + - + + , document.getElementById('root') ); diff --git a/frontend/src/mocks/browser.js b/frontend/src/mocks/browser.js deleted file mode 100644 index 750e031c2..000000000 --- a/frontend/src/mocks/browser.js +++ /dev/null @@ -1,4 +0,0 @@ -import { setupWorker } from 'msw'; -import { handlers } from './handlers'; - -export const worker = setupWorker(...handlers); diff --git a/frontend/src/mocks/db/comments.json b/frontend/src/mocks/db/comments.json new file mode 100644 index 000000000..7be7763f2 --- /dev/null +++ b/frontend/src/mocks/db/comments.json @@ -0,0 +1,28 @@ +{ + "data": [ + { + "id": 1, + "author": { + "id": 1, + "username": "euijinkk", + "nickname": "잉", + "imageUrl": "https://avatars.githubusercontent.com/u/24906022?v=4", + "role": "crew" + }, + "content": "내용1", + "createAt": "2022-07-24" + }, + { + "id": 2, + "author": { + "id": 2, + "username": "euijinkk", + "nickname": "루키", + "imageUrl": "https://avatars.githubusercontent.com/u/24906022?v=4", + "role": "crew" + }, + "content": "내용2", + "createAt": "2022-07-24" + } + ] +} diff --git a/frontend/src/mocks/db/levellogs.js b/frontend/src/mocks/db/levellogs.js new file mode 100644 index 000000000..7af3e6e21 --- /dev/null +++ b/frontend/src/mocks/db/levellogs.js @@ -0,0 +1,45 @@ +const LEVELLOG_LIST_LENGTH = 100; +export const LEVELLOG_ITEM_PER_PAGE = 10; + +const levellogs = Array.from({ length: LEVELLOG_LIST_LENGTH }, (_, idx) => idx + 1).map((idx) => ({ + id: idx, + title: `자바를 하면서 궁금했던 것들 ${idx}번째 ㅎㅎ`, + content: '자바는 왜 이름이 자바일까?', + author: { + id: 1, + username: 'kwannee', + nickname: '후이', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/41886825?&v=4', + }, + levelLogs: [ + { + id: 1, + question: '레벨 인터뷰 예상 질문1', + answer: '질문에 대한 답변1', + }, + { + id: 2, + question: '레벨 인터뷰 예상 질문2', + answer: '질문에 대한 답변2', + }, + { + id: 3, + question: '레벨 인터뷰 예상 질문3', + answer: '질문에 대한 답변3', + }, + ], + createdAt: '2022-07-10T23:06:45.395224', + updatedAt: '2022-07-10T23:06:45.395224', +})); + +levellogs.sort((log1, log2) => log2.id - log1.id); + +const dummyLevellogsData = { + data: levellogs, + totalSize: LEVELLOG_LIST_LENGTH, + totalPage: LEVELLOG_LIST_LENGTH / LEVELLOG_ITEM_PER_PAGE, + currPage: 1, +}; + +export default dummyLevellogsData; diff --git a/frontend/src/mocks/db/popularStudyLog.json b/frontend/src/mocks/db/popularStudyLog.json index dc885b028..316556c29 100644 --- a/frontend/src/mocks/db/popularStudyLog.json +++ b/frontend/src/mocks/db/popularStudyLog.json @@ -1,136 +1,36 @@ { "allResponse": { "data": [ - { - "studylogResponse": { - "id": 2, - "author": { - "id": 1, - "username": "soulG", - "nickname": "소롱", - "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt": "2022-06-25T15:24:26.386593", - "updatedAt": "2022-06-25T15:24:26.386593", - "session": { - "id": 2, - "name": "백엔드Java 레벨1 - 2021" - }, - "mission": { - "id": 2, - "name": "세션3 - 프로젝트", - "session": { - "id": 2, - "name": "백엔드Java 레벨1 - 2021" - } - }, - "title": "JAVA", - "content": "Spring Data JPA를 학습함.", - "tags": [ - { - "id": 3, - "name": "java" - }, - { - "id": 4, - "name": "jpa" - } - ], - "scrap": false, - "read": true, - "viewCount": 0, - "liked": false, - "likesCount": 1 - }, - "scrapedCount": 0 - }, - { - "studylogResponse": { - "id": 1, - "author": { - "id": 1, - "username": "soulG", - "nickname": "소롱", - "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt": "2022-06-25T15:24:26.335733", - "updatedAt": "2022-06-25T15:24:26.335733", - "session": { - "id": 1, - "name": "프론트엔드JS 레벨1 - 2021" - }, - "mission": { - "id": 1, - "name": "세션1 - 지하철 노선도 미션", - "session": { - "id": 1, - "name": "프론트엔드JS 레벨1 - 2021" - } - }, - "title": "나는야 Joanne", - "content": "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", - "tags": [ - { - "id": 1, - "name": "spa" - }, - { - "id": 2, - "name": "router" - } - ], - "scrap": false, - "read": true, - "viewCount": 0, - "liked": false, - "likesCount": 0 - }, - "scrapedCount": 0 - } - ], - "totalSize": 0, - "totalPage": 0, - "currPage": 0, - "studylogResponses": [ { "id": 2, "author": { "id": 1, - "username": "soulG", - "nickname": "소롱", + "username": "gracefulBrown", + "nickname": "브라운", "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" }, - "createdAt": "2022-06-25T15:24:26.386593", - "updatedAt": "2022-06-25T15:24:26.386593", + "createdAt": "2022-06-04T20:14:47.177878", + "updatedAt": "2022-06-04T20:14:47.177878", "session": { - "id": 2, + "id": 4, "name": "백엔드Java 레벨1 - 2021" }, - "mission": { - "id": 2, - "name": "세션3 - 프로젝트", - "session": { - "id": 2, - "name": "백엔드Java 레벨1 - 2021" - } - }, - "title": "JAVA", - "content": "Spring Data JPA를 학습함.", + "mission": null, + "title": "[자바] JAVA 주세요", + "content": "Java를 잡아라", "tags": [ { - "id": 3, - "name": "java" + "id": 1, + "name": "자료구조" }, { - "id": 4, - "name": "jpa" + "id": 2, + "name": "알고리즘" } ], "scrap": false, - "read": true, + "read": false, "viewCount": 0, "liked": false, "likesCount": 1 @@ -139,231 +39,257 @@ "id": 1, "author": { "id": 1, - "username": "soulG", - "nickname": "소롱", + "username": "gracefulBrown", + "nickname": "브라운", "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" }, - "createdAt": "2022-06-25T15:24:26.335733", - "updatedAt": "2022-06-25T15:24:26.335733", + "createdAt": "2022-06-04T20:14:46.979205", + "updatedAt": "2022-06-04T20:14:46.979205", "session": { - "id": 1, + "id": 3, "name": "프론트엔드JS 레벨1 - 2021" }, - "mission": { - "id": 1, - "name": "세션1 - 지하철 노선도 미션", - "session": { + "mission": null, + "title": "[자바스크립트] JS JS JS 신나는 노래", + "content": "덤덤 노래 아시는 분", + "tags": [ + { "id": 1, - "name": "프론트엔드JS 레벨1 - 2021" + "name": "자료구조" + }, + { + "id": 2, + "name": "알고리즘" } + ], + "scrap": false, + "read": false, + "viewCount": 0, + "liked": false, + "likesCount": 0 + }, + { + "id": 3, + "author": { + "id": 1, + "username": "gracefulBrown", + "nickname": "브라운", + "role": "CREW", + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" + }, + "createdAt": "2022-06-04T20:14:47.27626", + "updatedAt": "2022-06-04T20:14:47.27626", + "session": { + "id": 3, + "name": "프론트엔드JS 레벨1 - 2021" }, - "title": "나는야 Joanne", - "content": "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", + "mission": null, + "title": "[자바스크립트] JS JS JS 신나는 노래", + "content": "덤덤 노래 아시는 분", "tags": [ { "id": 1, - "name": "spa" + "name": "자료구조" }, { "id": 2, - "name": "router" + "name": "알고리즘" } ], "scrap": false, - "read": true, + "read": false, "viewCount": 0, "liked": false, "likesCount": 0 - } - ] - }, - "frontResponse": { - "data": [ + }, { - "studylogResponse": { + "id": 4, + "author": { "id": 1, - "author": { - "id": 1, - "username": "soulG", - "nickname": "소롱", - "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt": "2022-06-25T15:24:26.335733", - "updatedAt": "2022-06-25T15:24:26.335733", - "session": { - "id": 1, - "name": "프론트엔드JS 레벨1 - 2021" - }, - "mission": { + "username": "gracefulBrown", + "nickname": "브라운", + "role": "CREW", + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" + }, + "createdAt": "2022-06-04T20:14:47.360507", + "updatedAt": "2022-06-04T20:14:47.360507", + "session": { + "id": 4, + "name": "백엔드Java 레벨1 - 2021" + }, + "mission": null, + "title": "[자바] JAVA 주세요", + "content": "Java를 잡아라", + "tags": [ + { "id": 1, - "name": "세션1 - 지하철 노선도 미션", - "session": { - "id": 1, - "name": "프론트엔드JS 레벨1 - 2021" - } + "name": "자료구조" }, - "title": "나는야 Joanne", - "content": "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", - "tags": [ - { - "id": 1, - "name": "spa" - }, - { - "id": 2, - "name": "router" - } - ], - "scrap": false, - "read": true, - "viewCount": 0, - "liked": false, - "likesCount": 0 - }, - "scrapedCount": 0 + { + "id": 2, + "name": "알고리즘" + } + ], + "scrap": false, + "read": false, + "viewCount": 0, + "liked": false, + "likesCount": 0 } ], - "totalSize": 0, - "totalPage": 0, - "currPage": 0, - "studylogResponses": [ + "totalSize": 4, + "totalPage": 1, + "currPage": 1 + }, + "frontResponse": { + "data": [ { "id": 1, "author": { "id": 1, - "username": "soulG", - "nickname": "소롱", + "username": "gracefulBrown", + "nickname": "브라운", "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" }, - "createdAt": "2022-06-25T15:24:26.335733", - "updatedAt": "2022-06-25T15:24:26.335733", + "createdAt": "2022-06-04T20:14:46.979205", + "updatedAt": "2022-06-04T20:14:46.979205", "session": { - "id": 1, + "id": 3, "name": "프론트엔드JS 레벨1 - 2021" }, - "mission": { - "id": 1, - "name": "세션1 - 지하철 노선도 미션", - "session": { + "mission": null, + "title": "[자바스크립트] JS JS JS 신나는 노래", + "content": "덤덤 노래 아시는 분", + "tags": [ + { "id": 1, - "name": "프론트엔드JS 레벨1 - 2021" + "name": "자료구조" + }, + { + "id": 2, + "name": "알고리즘" } + ], + "scrap": false, + "read": false, + "viewCount": 0, + "liked": false, + "likesCount": 0 + }, + { + "id": 3, + "author": { + "id": 1, + "username": "gracefulBrown", + "nickname": "브라운", + "role": "CREW", + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" + }, + "createdAt": "2022-06-04T20:14:47.27626", + "updatedAt": "2022-06-04T20:14:47.27626", + "session": { + "id": 3, + "name": "프론트엔드JS 레벨1 - 2021" }, - "title": "나는야 Joanne", - "content": "SPA 방식으로 앱을 구현하였음.\nrouter 를 구현 하여 이용함.\n", + "mission": null, + "title": "[자바스크립트] JS JS JS 신나는 노래", + "content": "덤덤 노래 아시는 분", "tags": [ { "id": 1, - "name": "spa" + "name": "자료구조" }, { "id": 2, - "name": "router" + "name": "알고리즘" } ], "scrap": false, - "read": true, + "read": false, "viewCount": 0, "liked": false, "likesCount": 0 } - ] + ], + "totalSize": 2, + "totalPage": 1, + "currPage": 1 }, "backResponse": { "data": [ - { - "studylogResponse": { - "id": 2, - "author": { - "id": 1, - "username": "soulG", - "nickname": "소롱", - "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" - }, - "createdAt": "2022-06-25T15:24:26.386593", - "updatedAt": "2022-06-25T15:24:26.386593", - "session": { - "id": 2, - "name": "백엔드Java 레벨1 - 2021" - }, - "mission": { - "id": 2, - "name": "세션3 - 프로젝트", - "session": { - "id": 2, - "name": "백엔드Java 레벨1 - 2021" - } - }, - "title": "JAVA", - "content": "Spring Data JPA를 학습함.", - "tags": [ - { - "id": 3, - "name": "java" - }, - { - "id": 4, - "name": "jpa" - } - ], - "scrap": false, - "read": true, - "viewCount": 0, - "liked": false, - "likesCount": 1 - }, - "scrapedCount": 0 - } - ], - "totalSize": 0, - "totalPage": 0, - "currPage": 0, - "studylogResponses": [ { "id": 2, "author": { "id": 1, - "username": "soulG", - "nickname": "소롱", + "username": "gracefulBrown", + "nickname": "브라운", "role": "CREW", - "imageUrl": "https://avatars.githubusercontent.com/u/52682603?v=4" + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" }, - "createdAt": "2022-06-25T15:24:26.386593", - "updatedAt": "2022-06-25T15:24:26.386593", + "createdAt": "2022-06-04T20:14:47.177878", + "updatedAt": "2022-06-04T20:14:47.177878", "session": { - "id": 2, + "id": 4, "name": "백엔드Java 레벨1 - 2021" }, - "mission": { - "id": 2, - "name": "세션3 - 프로젝트", - "session": { + "mission": null, + "title": "[자바] JAVA 주세요", + "content": "Java를 잡아라", + "tags": [ + { + "id": 1, + "name": "자료구조" + }, + { "id": 2, - "name": "백엔드Java 레벨1 - 2021" + "name": "알고리즘" } + ], + "scrap": false, + "read": false, + "viewCount": 0, + "liked": false, + "likesCount": 1 + }, + { + "id": 4, + "author": { + "id": 1, + "username": "gracefulBrown", + "nickname": "브라운", + "role": "CREW", + "imageUrl": "https://avatars.githubusercontent.com/u/46308949?v=4" }, - "title": "JAVA", - "content": "Spring Data JPA를 학습함.", + "createdAt": "2022-06-04T20:14:47.360507", + "updatedAt": "2022-06-04T20:14:47.360507", + "session": { + "id": 4, + "name": "백엔드Java 레벨1 - 2021" + }, + "mission": null, + "title": "[자바] JAVA 주세요", + "content": "Java를 잡아라", "tags": [ { - "id": 3, - "name": "java" + "id": 1, + "name": "자료구조" }, { - "id": 4, - "name": "jpa" + "id": 2, + "name": "알고리즘" } ], "scrap": false, - "read": true, + "read": false, "viewCount": 0, "liked": false, - "likesCount": 1 + "likesCount": 0 } - ] + ], + "totalSize": 2, + "totalPage": 1, + "currPage": 1 } } diff --git a/frontend/src/mocks/fixture.js b/frontend/src/mocks/fixture.js deleted file mode 100644 index fec8d1c1f..000000000 --- a/frontend/src/mocks/fixture.js +++ /dev/null @@ -1 +0,0 @@ -export const dummyBadgeList = ['PASSION_KING', 'COMPLIMENT_KING']; diff --git a/frontend/src/mocks/handlers.js b/frontend/src/mocks/handlers.js deleted file mode 100644 index 77089950c..000000000 --- a/frontend/src/mocks/handlers.js +++ /dev/null @@ -1,9 +0,0 @@ -import { rest } from 'msw'; -import { dummyBadgeList } from './fixture'; -import { BASE_URL } from '../configs/environment'; - -export const handlers = [ - rest.get(`${BASE_URL}/members/:username/badges`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(dummyBadgeList)); - }), -]; diff --git a/frontend/src/mocks/handlers/comment.ts b/frontend/src/mocks/handlers/comment.ts new file mode 100644 index 000000000..ce5b04f47 --- /dev/null +++ b/frontend/src/mocks/handlers/comment.ts @@ -0,0 +1,67 @@ +import { rest } from 'msw'; +import { BASE_URL } from '../../configs/environment'; +import { CommentRequest } from '../../models/Comment'; +import comments from '../db/comments.json'; + +export const commentsHandler = [ + rest.get(`${BASE_URL}/studylogs/:studylogId/comments`, (req, res, ctx) => { + return res(ctx.status(200), ctx.json(comments)); + }), + rest.post(`${BASE_URL}/studylogs/:studylogId/comments`, (req, res, ctx) => { + const { body } = req; + + comments.data = [ + ...comments.data, + { + id: comments.data.length + 1, + author: { + id: 1, + username: 'euijinkk', + nickname: '시지프', + imageUrl: 'https://avatars.githubusercontent.com/u/24906022?v=4', + role: 'crew', + }, + content: body.content, + createAt: '2022-07-24', + }, + ]; + + return res(ctx.status(201)); + }), + rest.put( + `${BASE_URL}/studylogs/:studylogId/comments/:commentId`, + (req, res, ctx) => { + const { + body: { content }, + params: { commentId }, + } = req; + + if (typeof Number(commentId) !== 'number' || typeof commentId !== 'string') { + return res(ctx.status(400)); + } + + const editedComments = comments.data.map((comment) => + comment.id === Number(commentId) ? { ...comment, content } : comment + ); + + comments.data = editedComments; + + return res(ctx.status(204)); + } + ), + rest.delete(`${BASE_URL}/studylogs/:studylogId/comments/:commentId`, (req, res, ctx) => { + const { + params: { commentId }, + } = req; + + if (typeof Number(commentId) !== 'number' || typeof commentId !== 'string') { + return res(ctx.status(400)); + } + + const omittedComments = comments.data.filter((comment) => comment.id !== Number(commentId)); + + comments.data = omittedComments; + + return res(ctx.status(204)); + }), +]; diff --git a/frontend/src/mocks/handlers/index.ts b/frontend/src/mocks/handlers/index.ts index 07c044d21..3775f44c3 100644 --- a/frontend/src/mocks/handlers/index.ts +++ b/frontend/src/mocks/handlers/index.ts @@ -1,3 +1,5 @@ import { popularStudyLogHandler } from './popularStudyLog'; +import { commentsHandler } from './comment'; +import { levellogHandler } from './levellog'; -export const handlers = [...popularStudyLogHandler]; +export const handlers = [...popularStudyLogHandler, ...commentsHandler, ...levellogHandler]; diff --git a/frontend/src/mocks/handlers/levellog.ts b/frontend/src/mocks/handlers/levellog.ts new file mode 100644 index 000000000..3e69f5b54 --- /dev/null +++ b/frontend/src/mocks/handlers/levellog.ts @@ -0,0 +1,92 @@ +import { rest } from 'msw'; +import { BASE_URL } from '../../configs/environment'; +import { LevellogRequest } from '../../models/Levellogs'; +import levellogs, { LEVELLOG_ITEM_PER_PAGE } from '../db/levellogs'; + +export const levellogHandler = [ + rest.get(`${BASE_URL}/levellogs`, (req, res, ctx) => { + const page = Number(req.url.searchParams.get('page')) || 1; + + const rangedLevellogs = levellogs.data.slice( + (page - 1) * LEVELLOG_ITEM_PER_PAGE, + page * LEVELLOG_ITEM_PER_PAGE + ); + + return res(ctx.status(200), ctx.json({ ...levellogs, data: rangedLevellogs, currPage: page })); + }), + + rest.post(`${BASE_URL}/levellogs`, (req, res, ctx) => { + const { body } = req; + + levellogs.data = [ + ...levellogs.data, + { + id: levellogs.data.length + 1, + title: body.title, + author: { + id: 1, + username: 'kwannee', + nickname: '후이', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/41886825?v=4', + }, + content: body.content, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + levelLogs: body.levelLogs.map((levelLog, idx) => ({ + id: idx, + question: levelLog.question, + answer: levelLog.answer, + })), + }, + ]; + + levellogs.data.sort( + (log1, log2) => Number(new Date(log2.createdAt)) - Number(new Date(log1.createdAt)) + ); + + return res(ctx.status(200), ctx.json(levellogs)); + }), + + rest.get(`${BASE_URL}/levellogs/:id`, (req, res, ctx) => { + const { + params: { id }, + } = req; + + const numberId = Number(id); + + return res( + ctx.status(200), + ctx.json(levellogs.data.find((levellog) => levellog.id === numberId)) + ); + }), + + rest.delete(`${BASE_URL}/levellogs/:id`, (req, res, ctx) => { + const { + params: { id }, + } = req; + + const numberId = Number(id); + + levellogs.data = levellogs.data.filter((levellog) => levellog.id !== numberId); + + return res(ctx.status(200)); + }), + + rest.put(`${BASE_URL}/levellogs/:id`, (req, res, ctx) => { + const { + params: { id }, + } = req; + + const editedLevellog = req.body as any; + + levellogs.data[Number(id) - 1] = { + ...levellogs.data[Number(id) - 1], + title: editedLevellog.title, + content: editedLevellog.content, + levelLogs: editedLevellog.levelLogs, + }; + + return res(ctx.status(200), ctx.json(levellogs.data)); + }), +]; diff --git a/frontend/src/models/Comment.ts b/frontend/src/models/Comment.ts new file mode 100644 index 000000000..375b138c4 --- /dev/null +++ b/frontend/src/models/Comment.ts @@ -0,0 +1,16 @@ +import { Author } from './Studylogs'; + +export interface CommentType { + id: number; + author: Author; + content: string; + createAt: string; +} + +export interface CommentListResponse { + data: CommentType[]; +} + +export interface CommentRequest { + content: string; +} diff --git a/frontend/src/models/Levellogs.ts b/frontend/src/models/Levellogs.ts new file mode 100644 index 000000000..dd613c337 --- /dev/null +++ b/frontend/src/models/Levellogs.ts @@ -0,0 +1,23 @@ +import { Author } from './Studylogs'; + +export interface LevellogRequest { + title: string; + content: string; + levelLogs: QnAType[]; +} + +export interface LevellogResponse { + id: number; + title: string; + content: string; + levelLogs: QnAType[]; + author: Author; + createdAt: string; + updatedAt: string; +} + +export interface QnAType { + id?: number; + question: string; + answer: string; +} diff --git a/frontend/src/models/Studylogs.ts b/frontend/src/models/Studylogs.ts index a46e27c7f..2a54bee5d 100644 --- a/frontend/src/models/Studylogs.ts +++ b/frontend/src/models/Studylogs.ts @@ -40,10 +40,11 @@ export interface Studylog { likesCount: number; scrap: boolean; scrapedCount: number; + abilities: number[]; } export interface StudyLogList { - data: { studylogResponse: Studylog; scrapedCount: number }[]; + data: Studylog[]; totalSize: number; totalPage: number; currPage: number; diff --git a/frontend/src/pages/AbilityPage/StudyLogs/SelectAbilityBox.js b/frontend/src/pages/AbilityPage/StudyLogs/SelectAbilityBox.js index 287673f1d..8d31193e5 100644 --- a/frontend/src/pages/AbilityPage/StudyLogs/SelectAbilityBox.js +++ b/frontend/src/pages/AbilityPage/StudyLogs/SelectAbilityBox.js @@ -15,11 +15,12 @@ const SelectAbilityBox = ({ refetch, }) => { const abilityIds = abilities.map((ability) => ability.id); - const [updatedAbilites, setUpdatedAbilities] = useState(abilityIds); + + const [updatedAbilities, setUpdatedAbilities] = useState(abilityIds); const onSelectAbility = (event) => { const targetAbilityId = Number(event.target.id); - const currAbilities = new Set(updatedAbilites); + const currAbilities = new Set(updatedAbilities); if (currAbilities.has(targetAbilityId)) { currAbilities.delete(targetAbilityId); @@ -31,7 +32,7 @@ const SelectAbilityBox = ({ }; const onMappingAbility = () => { - mappingAbility.mutate({ studylogId, abilities: updatedAbilites }); + mappingAbility.mutate({ studylogId, abilities: updatedAbilities }); setSelectAbilityBox({ id: studylogId, isOpen: false }); }; diff --git a/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.js b/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.js index 0b982b2c7..883069e28 100644 --- a/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.js +++ b/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.js @@ -135,7 +135,10 @@ const StudyLogTable = ({ id="add-ability-button" size="XX_SMALL" type="button" - css={{ backgroundColor: `${COLOR.LIGHT_BLUE_300}` }} + css={{ + backgroundColor: `${COLOR.LIGHT_BLUE_300}`, + color: `${COLOR.DARK_GRAY_900}`, + }} onClick={(event) => onOpenAbilityBox(event, studylog.id)} > + diff --git a/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.styles.js b/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.styles.js index 11aceed52..20f829c24 100644 --- a/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.styles.js +++ b/frontend/src/pages/AbilityPage/StudyLogs/StudyLogTable.styles.js @@ -105,13 +105,6 @@ export const MappedAbility = styled.td` margin: 0.2rem 0; } } - - button { - width: 10%; - margin-top: 0.1rem; - color: ${COLOR.DARK_GRAY_900}; - font-size: 2rem; - } `; export const EmptyTableGuide = styled.span` diff --git a/frontend/src/pages/EditLevellogPage/index.tsx b/frontend/src/pages/EditLevellogPage/index.tsx new file mode 100644 index 000000000..bf656427b --- /dev/null +++ b/frontend/src/pages/EditLevellogPage/index.tsx @@ -0,0 +1,49 @@ +/** @jsxImportSource @emotion/react */ + +import styled from '@emotion/styled'; +import Editor from '../../components/Editor/Editor'; +import EditLevellogQnAList from '../../components/Lists/NewLevellogQnAInputList'; +import { COLOR } from '../../constants'; +import useEditLevellog from '../../hooks/Levellog/useEditLevellog'; +import { MainContentStyle } from '../../PageRouter'; + +const EditLevellogPage = () => { + const { + title, + onChangeTitle, + content, + editorContentRef, + isLoading, + editLevellog, + EditLevellogQnAListProps, + } = useEditLevellog(); + + return ( +
      + {!isLoading && ( + <> + + + + 수정하기 + + + )} +
      + ); +}; + +export default EditLevellogPage; + +const SubmitButton = styled.button` + background-color: ${COLOR.LIGHT_BLUE_400}; + width: 100%; + border-radius: 4px; + padding: 1rem; +`; diff --git a/frontend/src/pages/EditStudylogPage/index.tsx b/frontend/src/pages/EditStudylogPage/index.tsx index cddcb00e1..c49456c2c 100644 --- a/frontend/src/pages/EditStudylogPage/index.tsx +++ b/frontend/src/pages/EditStudylogPage/index.tsx @@ -21,9 +21,20 @@ import { useMutation, useQuery, UseQueryResult } from 'react-query'; import REACT_QUERY_KEY from '../../constants/reactQueryKey'; import { requestEditStudylog, requestGetStudylog } from '../../apis/studylogs'; import { AxiosError, AxiosResponse } from 'axios'; -import { getLocalStorageItem } from '../../utils/localStorage'; import LOCAL_STORAGE_KEY from '../../constants/localStorage'; import { SUCCESS_MESSAGE } from '../../constants/message'; +import { ResponseError } from '../../apis/studylogs'; +import { ParentAbility } from '../../models/Ability'; + +type SelectOption = { value: string; label: string }; + +interface NewStudylogForm extends StudylogForm { + abilities: number[]; +} + +interface EditStudylog extends Omit { + abilities: ParentAbility[]; +} // 나중에 학습로그 작성 페이지와 같아질 수 있음(임시저장) const EditStudylogPage = () => { @@ -31,12 +42,13 @@ const EditStudylogPage = () => { const editorContentRef = useRef(null); - const [studylogContent, setStudylogContent] = useState({ + const [studylogContent, setStudylogContent] = useState({ title: '', content: null, missionId: null, sessionId: null, tags: [], + abilities: [], }); const { user } = useContext(UserContext); @@ -44,7 +56,7 @@ const EditStudylogPage = () => { const { id } = useParams<{ id: string }>(); - const fetchStudylogRequest: UseQueryResult, AxiosError> = useQuery( + const fetchStudylogRequest: UseQueryResult, AxiosError> = useQuery( [REACT_QUERY_KEY.STUDYLOG, id], () => requestGetStudylog({ id, accessToken }), { @@ -55,11 +67,16 @@ const EditStudylogPage = () => { missionId: data.mission?.id || null, sessionId: data.session?.id || null, tags: data.tags, + abilities: data.abilities.map(({ id }) => id), }); }, } ); + const onSelectAbilities = (abilities: number[]) => { + setStudylogContent({ ...studylogContent, abilities }); + }; + const onChangeTitle: ChangeEventHandler = (event) => { setStudylogContent({ ...studylogContent, title: event.target.value }); }; @@ -75,7 +92,7 @@ const EditStudylogPage = () => { }); }; - const onSelectMission = (mission: { value: string; label: string } | null) => { + const onSelectMission = (mission: SelectOption | null) => { if (!mission) { setStudylogContent({ ...studylogContent, missionId: null }); return; @@ -84,7 +101,7 @@ const EditStudylogPage = () => { setStudylogContent({ ...studylogContent, missionId: Number(mission.value) }); }; - const onSelectSession = (session: { value: string; label: string } | null) => { + const onSelectSession = (session: SelectOption | null) => { if (!session) { setStudylogContent({ ...studylogContent, sessionId: null }); return; @@ -118,7 +135,7 @@ const EditStudylogPage = () => { (data: StudylogForm) => requestEditStudylog({ id, - accessToken: getLocalStorageItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN), + accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) as string, data, }), { @@ -126,8 +143,8 @@ const EditStudylogPage = () => { alert(SUCCESS_MESSAGE.EDIT_POST); history.push(`${PATH.STUDYLOG}/${id}`); }, - onError: (error: { code: number; message: string }) => { - console.log(error); + + onError: (error: ResponseError) => { alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.FAIL_TO_EDIT_STUDYLOG); }, } @@ -155,10 +172,12 @@ const EditStudylogPage = () => { selectedMissionId={studylogContent.missionId} selectedSessionId={studylogContent.sessionId} selectedTags={studylogContent.tags} + selectedAbilities={studylogContent.abilities} onChangeTitle={onChangeTitle} onSelectMission={onSelectMission} onSelectSession={onSelectSession} onSelectTag={onSelectTag} + onSelectAbilities={onSelectAbilities} onSubmit={onEditStudylog} />
      diff --git a/frontend/src/pages/LevellogListPage/index.tsx b/frontend/src/pages/LevellogListPage/index.tsx new file mode 100644 index 000000000..54f38ce78 --- /dev/null +++ b/frontend/src/pages/LevellogListPage/index.tsx @@ -0,0 +1,92 @@ +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; + +import { Button, Pagination } from '../../components'; +import PencilIcon from '../../assets/images/pencil_icon.svg'; + +import { MainContentStyle } from '../../PageRouter'; +import { + AlignItemsCenterStyle, + FlexStyle, + JustifyContentEndStyle, + JustifyContentSpaceBtwStyle, +} from '../../styles/flex.styles'; +import { HeaderContainer, PostListContainer } from './styles'; +import LevellogList from '../../components/Lists/LevellogList'; +import { Link } from 'react-router-dom'; +import { PATH } from '../../constants'; +import { useLevellogList } from '../../hooks/Levellog/useLevellogList'; + +const LevellogListPage = () => { + const { levellogs, isLoading, isLoggedIn, onChangeCurrentPage } = useLevellogList(); + + return ( +
      + +
      +

      + 💬 레벨로그 +

      +
      +
      button { + display: none; + } + } + `, + ]} + > + {isLoggedIn && ( + + + + )} +
      +
      + {!isLoading && ( + <> + + {levellogs.data.length === 0 && '작성된 글이 없습니다.'} + {levellogs.data && } + + + + )} +
      + ); +}; + +export default LevellogListPage; diff --git a/frontend/src/pages/LevellogListPage/styles.ts b/frontend/src/pages/LevellogListPage/styles.ts new file mode 100644 index 000000000..30cb908c2 --- /dev/null +++ b/frontend/src/pages/LevellogListPage/styles.ts @@ -0,0 +1,19 @@ +import styled from '@emotion/styled'; + +const HeaderContainer = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + + @media screen and (max-width: 420px) { + margin-bottom: 0.8rem; + } +`; + +const PostListContainer = styled.div` + display: grid; + grid-row-gap: 2rem; + word-break: break-all; +`; + +export { HeaderContainer, PostListContainer }; diff --git a/frontend/src/pages/LevellogPage/Content.js b/frontend/src/pages/LevellogPage/Content.js new file mode 100644 index 000000000..249d99aea --- /dev/null +++ b/frontend/src/pages/LevellogPage/Content.js @@ -0,0 +1,67 @@ +/** @jsxImportSource @emotion/react */ + +import { Card, ProfileChip } from '../../components'; +import { FlexStyle, JustifyContentSpaceBtwStyle } from '../../styles/flex.styles'; +import { + CardInner, + IssuedDate, + ProfileChipStyle, + SubHeader, + SubHeaderRightContent, + Title, + ViewerWrapper, +} from './styles'; + +import defaultProfileImage from '../../assets/images/no-profile-image.png'; + +// 마크다운 +import { Viewer } from '@toast-ui/react-editor'; +import '@toast-ui/editor/dist/toastui-editor.css'; +import 'prismjs/themes/prism.css'; +import Prism from 'prismjs'; +import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js'; +import { Link } from 'react-router-dom'; + +const Content = ({ levellog }) => { + const { author = null, title = '', content = '', createdAt = null } = levellog; + + return ( + + +
      + + + {new Date(createdAt).toLocaleString('ko-KR')} + + + +
      + {title} +
      + + + + {author?.nickname} + + +
      + + + {content && ( + + )} + +
      +
      + ); +}; + +export default Content; diff --git a/frontend/src/pages/LevellogPage/QnAList.styles.tsx b/frontend/src/pages/LevellogPage/QnAList.styles.tsx new file mode 100644 index 000000000..69a0e83ae --- /dev/null +++ b/frontend/src/pages/LevellogPage/QnAList.styles.tsx @@ -0,0 +1,37 @@ +import styled from '@emotion/styled'; +import { COLOR } from '../../constants'; +import { FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; + +export const S = { + Container: styled.div` + ${FlexStyle} + ${FlexColumnStyle} + margin: 3rem 0; + `, + SubTitle: styled.span` + font-size: large; + font-weight: bold; + `, + QnAsWrapper: styled.div` + ${FlexStyle} + ${FlexColumnStyle} + gap: 3rem; + margin: 1rem 0; + `, + QnA: styled.div` + ${FlexStyle} + ${FlexColumnStyle} + border: 1px solid ${COLOR.LIGHT_GRAY_200}; + border-radius: 7px; + `, + Question: styled.div` + padding: 1rem; + background-color: ${COLOR.LIGHT_BLUE_300}; + font-weight: bold; + border-radius: 4px 4px 0 0; + `, + Answer: styled.div` + padding: 1rem; + border-radius: 0 0 4px 4px; + `, +}; diff --git a/frontend/src/pages/LevellogPage/QnAList.tsx b/frontend/src/pages/LevellogPage/QnAList.tsx new file mode 100644 index 000000000..52a9f3813 --- /dev/null +++ b/frontend/src/pages/LevellogPage/QnAList.tsx @@ -0,0 +1,19 @@ +import { S } from './QnAList.styles'; + +const QnAList = ({ QnAList }) => { + return ( + + Q & A + + {QnAList.map((QnA) => ( + + {QnA.question} + {QnA.answer} + + ))} + + + ); +}; + +export default QnAList; diff --git a/frontend/src/pages/LevellogPage/index.tsx b/frontend/src/pages/LevellogPage/index.tsx new file mode 100644 index 000000000..56b5ac1f3 --- /dev/null +++ b/frontend/src/pages/LevellogPage/index.tsx @@ -0,0 +1,61 @@ +/** @jsxImportSource @emotion/react */ + +import Content from './Content'; +import { Button, BUTTON_SIZE } from '../../components'; +import { ButtonList, EditButtonStyle, DeleteButtonStyle } from './styles'; + +import { MainContentStyle } from '../../PageRouter'; + +import useLevellog from '../../hooks/Levellog/useLevellog'; +import QnAList from './QnAList'; +import { CONFIRM_MESSAGE } from '../../constants'; + +const LevellogPage = () => { + const { + levellog, + deleteLevellog, + isCurrentUserAuthor, + isLoading, + goEditTargetPost, + } = useLevellog(); + + return ( +
      + {isCurrentUserAuthor && ( + + {[ + { title: '수정', cssProps: EditButtonStyle, onClick: goEditTargetPost }, + { + title: '삭제', + cssProps: DeleteButtonStyle, + onClick: () => { + if (!window.confirm(CONFIRM_MESSAGE.DELETE_STUDYLOG)) return; + deleteLevellog(); + }, + }, + ].map(({ title, cssProps, onClick }) => ( + + ))} + + )} + {isLoading ? ( +
      로딩중
      + ) : ( + <> + + + + )} +
      + ); +}; + +export default LevellogPage; diff --git a/frontend/src/pages/LevellogPage/styles.tsx b/frontend/src/pages/LevellogPage/styles.tsx new file mode 100644 index 000000000..884c824ac --- /dev/null +++ b/frontend/src/pages/LevellogPage/styles.tsx @@ -0,0 +1,163 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import COLOR from '../../constants/color'; +import { FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; +import { markdownStyle } from '../../styles/markdown.styles'; + +const ButtonList = styled.div` + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; +`; + +const EditButtonStyle = css` + border: 1px solid ${COLOR.LIGHT_GRAY_200}; + + background-color: ${COLOR.WHITE}; + color: ${COLOR.BLACK_800}; + + margin-right: 1rem; + + &:hover { + background-color: ${COLOR.LIGHT_GRAY_300}; + } +`; + +const DeleteButtonStyle = css` + border: 1px solid ${COLOR.LIGHT_GRAY_200}; + background-color: ${COLOR.RED_300}; + color: ${COLOR.BLACK_800}; + + &:hover { + background-color: ${COLOR.RED_400}; + } +`; + +const CardInner = styled.div` + display: flex; + flex-direction: column; + min-height: 38rem; + height: fit-content; + + & > *:not(:last-child) { + margin-bottom: 2rem; + } + + .toastui-editor-contents { + font-size: 1.6rem; + } +`; + +const SubHeader = styled.div` + display: flex; + justify-content: space-between; +`; + +const SubHeaderRightContent = styled.div` + display: flex; + align-items: center; + + & > button { + margin-left: 0.7rem; + font-size: 1.5rem; + color: ${COLOR.BLACK_OPACITY_600}; + } +`; + +const Title = styled.div` + font-size: 3.6rem; + color: ${COLOR.DARK_GRAY_900}; + font-weight: bold; + margin-bottom: 2rem; +`; + +const IssuedDate = styled.div` + color: ${COLOR.DARK_GRAY_800}; + font-size: 1.4rem; +`; + +const ProfileChipStyle = css` + border: none; + padding: 0.8rem; + cursor: pointer; + + &:hover { + background-color: ${COLOR.LIGHT_BLUE_100}; + } +`; + +const ViewerWrapper = styled.div` + word-break: break-all; + + .toastui-editor-contents h1, + .toastui-editor-contents h2, + .toastui-editor-contents h3, + .toastui-editor-contents h4, + .toastui-editor-contents h5, + .toastui-editor-contents h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + } + + .toastui-editor-contents h1 { + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid hsl(210deg 18% 87%); + } + + .toastui-editor-contents h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid hsl(210deg 18% 87%); + } + + .toastui-editor-contents h3 { + font-size: 1.25em; + } + + .toastui-editor-contents ul > li::before { + background-color: #222; + } + + .toastui-editor-contents ol > li::before { + color: #222; + } + + ${markdownStyle}; +`; + +const EditorForm = styled.form` + & .toastui-editor-toolbar { + border-radius: 10px 10px 0 0; + } +`; + +const SubmitButton = styled.button` + width: 100%; + padding: 1rem 0; + border-radius: 1.6rem; + + margin-top: 12px; + + background-color: ${COLOR.LIGHT_BLUE_300}; + :hover { + background-color: ${COLOR.LIGHT_BLUE_500}; + } +`; + +export { + ButtonList, + EditButtonStyle, + DeleteButtonStyle, + CardInner, + SubHeader, + SubHeaderRightContent, + Title, + IssuedDate, + ProfileChipStyle, + ViewerWrapper, + EditorForm, + SubmitButton, +}; diff --git a/frontend/src/pages/MainPage/PopularStudyLogList.tsx b/frontend/src/pages/MainPage/PopularStudyLogList.tsx index c5a77d1b4..da314b0bf 100644 --- a/frontend/src/pages/MainPage/PopularStudyLogList.tsx +++ b/frontend/src/pages/MainPage/PopularStudyLogList.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/react'; -import { Studylog, studyLogCategory, StudyLogResponse } from '../../models/Studylogs'; +import { studyLogCategory, StudyLogResponse } from '../../models/Studylogs'; import { PopularStudylogListStyle, SectionHeaderGapStyle, StyledChip } from './styles'; import PopularStudylogItem from '../../components/Items/PopularStudylogItem'; import { useState } from 'react'; @@ -40,9 +40,9 @@ const PopularStudyLogList = ({ studylogs }: { studylogs: StudyLogResponse }): JS
        {studylogs[ getKeyByValue(studyLogCategory, selectedCategory) as keyof typeof studylogs - ].data.map(({ studylogResponse, scrapedCount }) => ( -
      • - + ].data.map((studylog) => ( +
      • +
      • ))}
      diff --git a/frontend/src/pages/MainPage/index.tsx b/frontend/src/pages/MainPage/index.tsx index 7da63163c..762c9093a 100644 --- a/frontend/src/pages/MainPage/index.tsx +++ b/frontend/src/pages/MainPage/index.tsx @@ -3,51 +3,32 @@ import { useContext, useEffect } from 'react'; import { UserContext } from '../../contexts/UserProvider'; -import useFetchData from '../../hooks/useFetchData'; import bannerList from '../../configs/bannerList'; import BannerList from '../../components/Banner/BannerList'; import RecentStudylogList from './RecentStudylogList'; import PopularStudyLogList from './PopularStudyLogList'; -import { requestGetStudylogs } from '../../service/requests'; import { MainContentStyle } from '../../PageRouter'; import { getRowGapStyle } from '../../styles/layout.styles'; -import type { Studylog, StudyLogResponse } from '../../models/Studylogs'; -import { useQuery } from 'react-query'; -import axios from 'axios'; -import { BASE_URL } from '../../configs/environment'; +import { + useGetPopularStudylogsQuery, + useGetRecentStudylogsQuery, +} from '../../hooks/queries/studylog'; const MainPage = () => { const { user } = useContext(UserContext); const { accessToken } = user; - const fetchRecentStudylogsRequest = useFetchData<{ - data: Studylog[]; - }>( - { data: [] }, - () => requestGetStudylogs({ query: { type: 'searchParams', data: 'size=3' }, accessToken }), - { initialFetch: false } - ); + const { data: recentStudylogs, refetch: refetchRecentStudylogs } = useGetRecentStudylogsQuery(); - // @TODO: 로딩 및 에러 처리 const { - isLoading, - error, data: popularStudyLogs, refetch: refetchPopularStudyLogs, - } = useQuery('popularStudyLogs', async () => { - const { data } = await axios({ - method: 'get', - url: `${BASE_URL}/studylogs/popular`, - headers: accessToken && { Authorization: 'Bearer ' + accessToken }, - }); - - return data; - }); + } = useGetPopularStudylogsQuery(); useEffect(() => { - fetchRecentStudylogsRequest.refetch(); + refetchRecentStudylogs(); refetchPopularStudyLogs(); }, [accessToken]); @@ -55,11 +36,8 @@ const MainPage = () => { <>
      - {/* TODO: 로딩상태 관리 */} {popularStudyLogs && } - {fetchRecentStudylogsRequest.response.data.length !== 0 && ( - - )} + {recentStudylogs && }
      ); diff --git a/frontend/src/pages/MainPage/styles.ts b/frontend/src/pages/MainPage/styles.ts index 4c301839e..3a6aed596 100644 --- a/frontend/src/pages/MainPage/styles.ts +++ b/frontend/src/pages/MainPage/styles.ts @@ -20,7 +20,7 @@ export const SectionHeaderGapStyle = css` // 인기있는 학습로그 export const PopularStudylogListStyle = css` width: 100%; - height: 36rem; + height: 32rem; display: flex; justify-content: content; diff --git a/frontend/src/pages/NewLevellogPage/index.tsx b/frontend/src/pages/NewLevellogPage/index.tsx new file mode 100644 index 000000000..5a5c2546a --- /dev/null +++ b/frontend/src/pages/NewLevellogPage/index.tsx @@ -0,0 +1,55 @@ +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; + +import { MainContentStyle } from '../../PageRouter'; + +import Editor from '../../components/Editor/Editor'; +import useNewLevellog from '../../hooks/Levellog/useNewLevellog'; +import { FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; +import styled from '@emotion/styled'; +import { COLOR } from '../../constants'; +import NewLevellogQnAList from '../../components/Lists/NewLevellogQnAInputList'; + +const NewLevellogPage = () => { + const { + createNewLevellog, + editorContentRef, + onChangeTitle, + title, + NewLevellogQnAListProps, + } = useNewLevellog(); + + return ( +
      + + + + 제출하기 + +
      + ); +}; + +export default NewLevellogPage; + +const SubmitButton = styled.button` + background-color: ${COLOR.LIGHT_BLUE_400}; + width: 100%; + border-radius: 4px; + padding: 1rem; +`; diff --git a/frontend/src/pages/NewStudylogPage/index.tsx b/frontend/src/pages/NewStudylogPage/index.tsx index 60da71a36..6e4d2ade1 100644 --- a/frontend/src/pages/NewStudylogPage/index.tsx +++ b/frontend/src/pages/NewStudylogPage/index.tsx @@ -9,26 +9,40 @@ import { ERROR_MESSAGE, ALERT_MESSAGE, PATH } from '../../constants'; import { StudylogForm } from '../../models/Studylogs'; import { useMutation } from 'react-query'; -import { getLocalStorageItem } from '../../utils/localStorage'; import LOCAL_STORAGE_KEY from '../../constants/localStorage'; import { SUCCESS_MESSAGE } from '../../constants/message'; import { useHistory } from 'react-router-dom'; import { requestPostStudylog } from '../../apis/studylogs'; import StudylogEditor from '../../components/Editor/StudylogEditor'; +import useBeforeunload from '../../hooks/useBeforeunload'; +import { ResponseError } from '../../apis/studylogs'; + +interface NewStudylogForm extends StudylogForm { + abilities: number[]; +} + +type SelectOption = { value: string; label: string }; const NewStudylogPage = () => { const history = useHistory(); const editorContentRef = useRef(null); - const [studylogContent, setStudylogContent] = useState({ + useBeforeunload(editorContentRef); + + const [studylogContent, setStudylogContent] = useState({ title: '', content: '', missionId: null, sessionId: null, tags: [], + abilities: [], }); + const onSelectAbilities = (abilities: number[]) => { + setStudylogContent({ ...studylogContent, abilities }); + }; + const onChangeTitle: ChangeEventHandler = (event) => { setStudylogContent({ ...studylogContent, title: event.target.value }); }; @@ -44,10 +58,10 @@ const NewStudylogPage = () => { }); }; - const onSelectMission = (mission: { value: string; label: string }) => + const onSelectMission = (mission: SelectOption) => setStudylogContent({ ...studylogContent, missionId: Number(mission.value) }); - const onSelectSession = (session: { value: string; label: string }) => { + const onSelectSession = (session: SelectOption) => { setStudylogContent({ ...studylogContent, sessionId: Number(session.value) }); }; @@ -76,16 +90,15 @@ const NewStudylogPage = () => { const { mutate: createStudylogRequest } = useMutation( (data: StudylogForm) => requestPostStudylog({ - accessToken: getLocalStorageItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN), + accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) as string, data, }), { - onSuccess: async (data) => { + onSuccess: async () => { alert(SUCCESS_MESSAGE.CREATE_POST); history.push(PATH.STUDYLOG); }, - onError: (error: { code: number; message: string }) => { - console.log(error); + onError: (error: ResponseError) => { alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.DEFAULT); }, } @@ -107,10 +120,12 @@ const NewStudylogPage = () => { selectedMissionId={studylogContent.missionId} selectedSessionId={studylogContent.sessionId} selectedTags={studylogContent.tags} + selectedAbilities={studylogContent.abilities} onChangeTitle={onChangeTitle} onSelectMission={onSelectMission} onSelectSession={onSelectSession} onSelectTag={onSelectTag} + onSelectAbilities={onSelectAbilities} onSubmit={onCreateStudylog} />
      diff --git a/frontend/src/pages/ProfilePageEditReport/index.js b/frontend/src/pages/ProfilePageEditReport/index.tsx similarity index 79% rename from frontend/src/pages/ProfilePageEditReport/index.js rename to frontend/src/pages/ProfilePageEditReport/index.tsx index 33a5061c9..ce9c20a05 100644 --- a/frontend/src/pages/ProfilePageEditReport/index.js +++ b/frontend/src/pages/ProfilePageEditReport/index.tsx @@ -8,14 +8,24 @@ import { COLOR, ERROR_MESSAGE } from '../../constants'; import { Form, FormButtonWrapper } from '../ProfilePageNewReport/style'; import AbilityGraph from '../ProfilePageNewReport/AbilityGraph'; import { useMutation, useQuery, useQueryClient } from 'react-query'; -import axios from 'axios'; +import axios, { AxiosError, AxiosResponse } from 'axios'; import { BASE_URL } from '../../configs/environment'; +import { Editor } from '@toast-ui/react-editor'; +import { ErrorData } from '../../apis/ability'; + +type reportDataType = { + title: string; + description: string; + startDate: string; + endDate: string; + reportAbility: { abilityId: number; weight: number }[]; +}; const ProfilePageNewReport = () => { const history = useHistory(); const queryClient = useQueryClient(); - const { id, username } = useParams(); + const { id, username } = useParams<{ id: string; username: string }>(); const { user } = useContext(UserContext); const { isLoggedIn, accessToken } = user; const nickname = user.nickname ?? user.username; @@ -31,8 +41,8 @@ const ProfilePageNewReport = () => { }); /** 리포트 수정 */ - const onEditReport = useMutation( - async (reportData) => { + const onEditReport = useMutation, AxiosError, reportDataType>( + async (reportData: reportDataType) => { const { data } = await axios({ method: 'put', url: `${BASE_URL}/reports/${id}`, @@ -62,7 +72,7 @@ const ProfilePageNewReport = () => { ); const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); + const [description, setDescription] = useState(); const [abilities, setAbilities] = useState([]); useEffect(() => { @@ -89,15 +99,16 @@ const ProfilePageNewReport = () => { const onSubmitReport = async (event) => { event.preventDefault(); - const data = { - title, - description: description.getInstance().getMarkdown(), - startDate: reportData.startDate, - endDate: reportData.endDate, - reportAbility: abilities.map(({ id, weight }) => ({ abilityId: id, weight })), - }; - - onEditReport.mutate(data); + if (description instanceof Editor) { + const data = { + title, + description: description.getInstance().getMarkdown(), + startDate: reportData.startDate, + endDate: reportData.endDate, + reportAbility: abilities.map(({ id, weight }) => ({ abilityId: id, weight })), + }; + onEditReport.mutate(data); + } }; const onCancelWriteReport = () => { diff --git a/frontend/src/pages/ProfilePageNewReport/AbilityGraph.styles.js b/frontend/src/pages/ProfilePageNewReport/AbilityGraph.styles.ts similarity index 90% rename from frontend/src/pages/ProfilePageNewReport/AbilityGraph.styles.js rename to frontend/src/pages/ProfilePageNewReport/AbilityGraph.styles.ts index 1149a9970..b8e025b3e 100644 --- a/frontend/src/pages/ProfilePageNewReport/AbilityGraph.styles.js +++ b/frontend/src/pages/ProfilePageNewReport/AbilityGraph.styles.ts @@ -81,7 +81,13 @@ export const Tbody = styled.tbody` } `; -export const AbilityName = styled.td` +type RGB = `rgb(${number}, ${number}, ${number})`; +type RGBA = `rgba(${number}, ${number}, ${number}, ${number})`; +type HEX = `#${string}`; + +type Color = RGB | RGBA | HEX; + +export const AbilityName = styled.td<{ abilityColor: Color }>` height: 100%; width: 80%; padding: 0 1rem; diff --git a/frontend/src/pages/ProfilePageNewReport/AbilityGraph.jsx b/frontend/src/pages/ProfilePageNewReport/AbilityGraph.tsx similarity index 87% rename from frontend/src/pages/ProfilePageNewReport/AbilityGraph.jsx rename to frontend/src/pages/ProfilePageNewReport/AbilityGraph.tsx index 6a9374c0c..b5c1ce154 100644 --- a/frontend/src/pages/ProfilePageNewReport/AbilityGraph.jsx +++ b/frontend/src/pages/ProfilePageNewReport/AbilityGraph.tsx @@ -2,9 +2,34 @@ import Chart from 'react-apexcharts'; import * as Styled from './AbilityGraph.styles'; import { COLOR } from '../../constants'; +import { ApexOptions } from 'apexcharts'; + +interface ChartOption { + type?: + | 'line' + | 'area' + | 'bar' + | 'histogram' + | 'pie' + | 'donut' + | 'radialBar' + | 'scatter' + | 'bubble' + | 'heatmap' + | 'treemap' + | 'boxPlot' + | 'candlestick' + | 'radar' + | 'polarArea' + | 'rangeBar'; + series?: ApexOptions['series']; + width?: string | number; + options?: ApexOptions; + [key: string]: any; +} const AbilityGraph = ({ abilities, setAbilities, edit }) => { - const options = { + const options: ChartOption = { series: [ { name: '역량 가중치', diff --git a/frontend/src/pages/ProfilePageNewReport/ReportDescEditor.js b/frontend/src/pages/ProfilePageNewReport/ReportDescEditor.tsx similarity index 100% rename from frontend/src/pages/ProfilePageNewReport/ReportDescEditor.js rename to frontend/src/pages/ProfilePageNewReport/ReportDescEditor.tsx diff --git a/frontend/src/pages/ProfilePageNewReport/ReportInfo.styles.js b/frontend/src/pages/ProfilePageNewReport/ReportInfo.styles.ts similarity index 100% rename from frontend/src/pages/ProfilePageNewReport/ReportInfo.styles.js rename to frontend/src/pages/ProfilePageNewReport/ReportInfo.styles.ts diff --git a/frontend/src/pages/ProfilePageNewReport/ReportInfo.js b/frontend/src/pages/ProfilePageNewReport/ReportInfo.tsx similarity index 78% rename from frontend/src/pages/ProfilePageNewReport/ReportInfo.js rename to frontend/src/pages/ProfilePageNewReport/ReportInfo.tsx index 26ad39025..29396d9f3 100644 --- a/frontend/src/pages/ProfilePageNewReport/ReportInfo.js +++ b/frontend/src/pages/ProfilePageNewReport/ReportInfo.tsx @@ -3,6 +3,21 @@ import { DatePicker } from 'antd'; import * as Styled from './ReportInfo.styles'; import ReportDescEditor from './ReportDescEditor'; +import { Editor } from '@toast-ui/react-editor'; +import { Dispatch, SetStateAction } from 'react'; + +interface ReportInfoPros { + nickname: string; + title: string; + desc: Editor | string; + editorRef?: Dispatch>; + startDate?: string; + setStartDate?: Dispatch>; + setTitle: Dispatch>; + endDate?: string; + setEndDate?: Dispatch>; + edit?: boolean; +} const ReportInfo = ({ nickname, // @@ -15,12 +30,12 @@ const ReportInfo = ({ endDate, setEndDate, edit, -}) => { +}: ReportInfoPros) => { const onWriteTitle = ({ target: { value } }) => setTitle(value); const onSelectDate = (_, dateStrings) => { const [startDate, endDate] = dateStrings; - setStartDate(startDate); - setEndDate(endDate); + if (setStartDate) setStartDate(startDate); + if (setEndDate) setEndDate(endDate); }; const dateFormat = 'YYYY-MM-DD'; @@ -72,7 +87,6 @@ const ReportInfo = ({ ✏️ 리포트 설명 - {desc.length}/300 diff --git a/frontend/src/pages/ProfilePageNewReport/index.js b/frontend/src/pages/ProfilePageNewReport/index.tsx similarity index 68% rename from frontend/src/pages/ProfilePageNewReport/index.js rename to frontend/src/pages/ProfilePageNewReport/index.tsx index f07ebfa4b..529a908eb 100644 --- a/frontend/src/pages/ProfilePageNewReport/index.js +++ b/frontend/src/pages/ProfilePageNewReport/index.tsx @@ -1,7 +1,7 @@ import { useContext, useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { useMutation } from 'react-query'; -import axios from 'axios'; +import axios, { AxiosError, AxiosResponse } from 'axios'; import { UserContext } from '../../contexts/UserProvider'; import { Button } from '../../components'; @@ -11,17 +11,32 @@ import { Form, FormButtonWrapper } from './style'; import { BASE_URL } from '../../configs/environment'; import useAbility from '../../hooks/Ability/useAbility'; import AbilityGraph from './AbilityGraph'; +import ReportStudyLogs from '../../components/ReportStudyLogs/ReportStudyLogs'; +import { useGetMatchedStudylogs } from '../../hooks/queries/report'; +import useParentAbilityForm from '../../hooks/Ability/useParentAbilityForm'; +import { Editor } from '@toast-ui/react-editor'; + +type reportDataType = { + title: string; + description: string; + startDate: string; + endDate: string; + reportAbility: { abilityId: number; weight: number }[]; +}; const ProfilePageNewReport = () => { const history = useHistory(); - const { username } = useParams(); + const { username } = useParams<{ username: string }>(); const { user } = useContext(UserContext); const { isLoggedIn, accessToken } = user; const nickname = user.nickname ?? user.username; + const { setAddFormStatus, addFormClose } = useParentAbilityForm(); const { abilities: abilityList, isLoading } = useAbility({ username, + setAddFormStatus, + addFormClose, }); useEffect(() => { @@ -38,12 +53,24 @@ const ProfilePageNewReport = () => { } }, [isLoggedIn, username, user.data, history]); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); + const [description, setDescription] = useState(); const [abilities, setAbilities] = useState([]); + const { data: studylogsData, refetch: getMatchedStudylogs } = useGetMatchedStudylogs({ + accessToken, + startDate, + endDate, + }); + + useEffect(() => { + if (startDate && endDate) { + getMatchedStudylogs(); + } + }, [startDate, endDate]); + useEffect(() => { setAbilities( abilityList?.map(({ id, name, color }) => ({ @@ -56,7 +83,7 @@ const ProfilePageNewReport = () => { }, [isLoading]); /** 리포트 등록 */ - const onAddReport = useMutation( + const onAddReport = useMutation, AxiosError, reportDataType>( async (reportData) => { const { data } = await axios({ method: 'post', @@ -103,14 +130,16 @@ const ProfilePageNewReport = () => { return; } - /** 리포트 등록 */ - onAddReport.mutate({ - title, - description: description.getInstance().getMarkdown(), - startDate, - endDate, - reportAbility: abilities.map(({ id, weight }) => ({ abilityId: id, weight })), - }); + if (description instanceof Editor) { + /** 리포트 등록 */ + onAddReport.mutate({ + title, + description: description.getInstance().getMarkdown(), + startDate, + endDate, + reportAbility: abilities.map(({ id, weight }) => ({ abilityId: id, weight })), + }); + } }; const onCancelWriteReport = () => { @@ -119,6 +148,10 @@ const ProfilePageNewReport = () => { } }; + const studylogsMappingData = studylogsData?.map(({ studylog, abilities }) => { + return { studylog, studylogAbilities: abilities }; + }); + return ( <>
      @@ -128,13 +161,13 @@ const ProfilePageNewReport = () => { nickname={nickname} title={title} setTitle={setTitle} - desc={description} + desc={description!} editorRef={setDescription} setStartDate={setStartDate} setEndDate={setEndDate} /> - - + +
      diff --git a/frontend/src/pages/StudylogPage/index.js b/frontend/src/pages/StudylogPage/index.js deleted file mode 100644 index ad979af12..000000000 --- a/frontend/src/pages/StudylogPage/index.js +++ /dev/null @@ -1,215 +0,0 @@ -/** @jsxImportSource @emotion/react */ - -import { useContext, useEffect } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; -import TagManager from 'react-gtm-module'; - -import Content from './Content'; -import { Button, BUTTON_SIZE } from '../../components'; -import { ButtonList, EditButtonStyle, DeleteButtonStyle } from './styles'; - -import { MainContentStyle } from '../../PageRouter'; -import { UserContext } from '../../contexts/UserProvider'; -import useSnackBar from '../../hooks/useSnackBar'; -import useStudylog from '../../hooks/useStudylog'; -import useMutation from '../../hooks/useMutation'; -import debounce from '../../utils/debounce'; - -import { - requestPostScrap, - requestDeleteScrap, - requestDeleteLike, - requestDeleteStudylog, - requestPostLike, -} from '../../service/requests'; - -import { - ALERT_MESSAGE, - CONFIRM_MESSAGE, - ERROR_MESSAGE, - PATH, - SNACKBAR_MESSAGE, -} from '../../constants'; -import { SUCCESS_MESSAGE } from '../../constants/message'; - -const StudylogPage = () => { - const { id } = useParams(); - const history = useHistory(); - - const { user } = useContext(UserContext); - const { accessToken, isLoggedIn, username, userId } = user; - - const { openSnackBar } = useSnackBar(); - - const onSuccessWriteStudylog = (studylog) => { - TagManager.dataLayer({ - dataLayer: { - event: 'page_view_studylog', - mine: username === studylog.author?.username, - user_id: userId, - username, - target: studylog.id, - }, - }); - }; - - const { response: studylog, getData } = useStudylog({}, onSuccessWriteStudylog); - - const getStudylog = () => getData({ id, accessToken }); - - const { mutate: deleteStudylog } = useMutation( - () => { - if (!window.confirm(CONFIRM_MESSAGE.DELETE_STUDYLOG)) return; - - return requestDeleteStudylog({ id, accessToken }); - }, - { - onSuccess: () => { - openSnackBar(SUCCESS_MESSAGE.DELETE_STUDYLOG); - history.push(PATH.STUDYLOG); - }, - onError: (error) => { - alert(ERROR_MESSAGE[error.code] ?? ALERT_MESSAGE.FAIL_TO_DELETE_STUDYLOG); - }, - } - ); - - const { author = null, liked = false, scrap = false } = studylog; - - const goAuthorProfilePage = (event) => { - event.stopPropagation(); - - if (!author) { - return; - } - - history.push(`/${author?.username}`); - }; - - const goEditTargetPost = () => { - history.push(`${PATH.STUDYLOG}/${id}/edit`); - }; - - const { mutate: postScrap } = useMutation( - () => { - if (!isLoggedIn) { - alert(ALERT_MESSAGE.NEED_TO_LOGIN); - return; - } - - return requestPostScrap({ username, accessToken, id }); - }, - { - onSuccess: async () => { - await getStudylog(); - openSnackBar(SNACKBAR_MESSAGE.SUCCESS_TO_SCRAP); - }, - } - ); - - const { mutate: deleteScrap } = useMutation( - () => { - if (!window.confirm(CONFIRM_MESSAGE.DELETE_SCRAP)) return; - - return requestDeleteScrap({ username, accessToken, id }); - }, - { - onSuccess: async () => { - await getStudylog(); - openSnackBar(SNACKBAR_MESSAGE.DELETE_SCRAP); - }, - } - ); - - const { mutate: postLike } = useMutation( - () => { - if (!isLoggedIn) { - alert(ALERT_MESSAGE.NEED_TO_LOGIN); - return; - } - - return requestPostLike({ accessToken, id }); - }, - { - onSuccess: async () => { - await getStudylog(); - openSnackBar(SNACKBAR_MESSAGE.SET_LIKE); - }, - onError: () => openSnackBar(SNACKBAR_MESSAGE.ERROR_SET_LIKE), - } - ); - - const { mutate: deleteLike } = useMutation( - () => { - if (!window.confirm(CONFIRM_MESSAGE.DELETE_LIKE)) return; - - return requestDeleteLike({ accessToken, id }); - }, - { - onSuccess: async () => { - await getStudylog(); - openSnackBar(SNACKBAR_MESSAGE.UNSET_LIKE); - }, - onError: () => openSnackBar(SNACKBAR_MESSAGE.ERROR_UNSET_LIKE), - } - ); - - const toggleLike = () => { - liked - ? debounce(() => { - deleteLike(); - }, 300) - : debounce(() => { - postLike(); - }, 300); - }; - - const toggleScrap = () => { - if (scrap) { - deleteScrap(); - return; - } - - postScrap(); - }; - - useEffect(() => { - // accessToken 이 있을 시에 studylogs -> me -> studylogs 순서 제어를 위한 임시 코드 - const timeout = setTimeout(() => getStudylog(), 0); - - return () => { - clearTimeout(timeout); - }; - }, [accessToken, id]); - - return ( -
      - {username === author?.username && ( - - {[ - { title: '수정', cssProps: EditButtonStyle, onClick: goEditTargetPost }, - { title: '삭제', cssProps: DeleteButtonStyle, onClick: deleteStudylog }, - ].map(({ title, cssProps, onClick }) => ( - - ))} - - )} - -
      - ); -}; - -export default StudylogPage; diff --git a/frontend/src/pages/StudylogPage/index.tsx b/frontend/src/pages/StudylogPage/index.tsx new file mode 100644 index 000000000..ef72e7615 --- /dev/null +++ b/frontend/src/pages/StudylogPage/index.tsx @@ -0,0 +1,173 @@ +/** @jsxImportSource @emotion/react */ + +import { MouseEvent, useContext, useEffect, useRef } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import TagManager from 'react-gtm-module'; + +import Content from './Content'; +import { Button, BUTTON_SIZE } from '../../components'; +import { ButtonList, EditButtonStyle, DeleteButtonStyle, EditorForm, SubmitButton } from './styles'; + +import { MainContentStyle } from '../../PageRouter'; +import { UserContext } from '../../contexts/UserProvider'; +import useStudylog from '../../hooks/useStudylog'; +import debounce from '../../utils/debounce'; + +import { ALERT_MESSAGE, CONFIRM_MESSAGE, PATH } from '../../constants'; + +import CommentList from '../../components/Comment/CommentList'; +import useStudylogComment from '../../hooks/Comment/useStudylogComment'; +import useBeforeunload from '../../hooks/useBeforeunload'; +import Editor from '../../components/Editor/Editor'; +import { + useDeleteLikeMutation, + useDeleteScrapMutation, + useDeleteStudylogMutation, + usePostLikeMutation, + usePostScrapMutation, +} from '../../hooks/queries/studylog'; + +const StudylogPage = () => { + const { id } = useParams<{ id: string }>(); + const history = useHistory(); + + const { user } = useContext(UserContext); + const { accessToken, isLoggedIn, username, userId } = user; + + const onSuccessWriteStudylog = (studylog) => { + TagManager.dataLayer({ + dataLayer: { + event: 'page_view_studylog', + mine: username === studylog.author?.username, + user_id: userId, + username, + target: studylog.id, + }, + }); + }; + + const getStudylog = () => getData({ id, accessToken }); + + const { response: studylog, getData } = useStudylog({}, onSuccessWriteStudylog); + const { mutate: deleteStudylog } = useDeleteStudylogMutation(); + const { mutate: postScrap } = usePostScrapMutation({ getStudylog }); + const { mutate: deleteScrap } = useDeleteScrapMutation({ getStudylog }); + const { mutate: postLike } = usePostLikeMutation({ getStudylog }); + const { mutate: deleteLike } = useDeleteLikeMutation({ getStudylog }); + const { author = null, liked = false, scrap = false } = studylog; + + const goAuthorProfilePage = (event: MouseEvent) => { + event.stopPropagation(); + + if (!author) { + return; + } + + history.push(`/${author?.username}`); + }; + + const goEditTargetPost = () => { + history.push(`${PATH.STUDYLOG}/${id}/edit`); + }; + + const toggleLike = () => { + liked + ? debounce(() => { + if (!window.confirm(CONFIRM_MESSAGE.DELETE_LIKE)) return; + deleteLike({ accessToken, id }); + }, 300) + : debounce(() => { + if (!isLoggedIn) { + alert(ALERT_MESSAGE.NEED_TO_LOGIN); + return; + } + postLike({ accessToken, id }); + }, 300); + }; + + const toggleScrap = () => { + if (scrap) { + deleteScrap({ username, accessToken, id }); + return; + } + + postScrap({ username, accessToken, id }); + }; + + useEffect(() => { + // accessToken 이 있을 시에 studylogs -> me -> studylogs 순서 제어를 위한 임시 코드 + const timeout = setTimeout(() => getStudylog(), 0); + + return () => { + clearTimeout(timeout); + }; + }, [accessToken, id]); + + /* 댓글 로직 */ + const { comments, createComment, editComment, deleteComment } = useStudylogComment(Number(id)); + + const editorContentRef = useRef(null); + + const onSubmitComment = (event) => { + event.preventDefault(); + + const content = editorContentRef.current?.getInstance().getMarkdown() || ''; + + if (content.length === 0) { + alert(ALERT_MESSAGE.NO_CONTENT); + return; + } + + createComment({ content }); + editorContentRef.current?.getInstance().setMarkdown(''); + }; + + useBeforeunload(editorContentRef); + + return ( +
      + {username === author?.username && ( + + {[ + { title: '수정', cssProps: EditButtonStyle, onClick: goEditTargetPost }, + { + title: '삭제', + cssProps: DeleteButtonStyle, + onClick: () => { + if (!window.confirm(CONFIRM_MESSAGE.DELETE_STUDYLOG)) return; + deleteStudylog({ id, accessToken }); + }, + }, + ].map(({ title, cssProps, onClick }) => ( + + ))} + + )} + + {comments && ( + + )} + {isLoggedIn && ( + + + 작성 완료 + + )} +
      + ); +}; + +export default StudylogPage; diff --git a/frontend/src/pages/StudylogPage/styles.js b/frontend/src/pages/StudylogPage/styles.ts similarity index 89% rename from frontend/src/pages/StudylogPage/styles.js rename to frontend/src/pages/StudylogPage/styles.ts index b9fed32c8..43b62f3a6 100644 --- a/frontend/src/pages/StudylogPage/styles.js +++ b/frontend/src/pages/StudylogPage/styles.ts @@ -146,6 +146,25 @@ const BottomContainer = styled.div` margin-top: auto; `; +const EditorForm = styled.form` + & .toastui-editor-toolbar { + border-radius: 10px 10px 0 0; + } +`; + +const SubmitButton = styled.button` + width: 100%; + padding: 1rem 0; + border-radius: 1.6rem; + + margin-top: 12px; + + background-color: ${COLOR.LIGHT_BLUE_300}; + :hover { + background-color: ${COLOR.LIGHT_BLUE_500}; + } +`; + export { ButtonList, EditButtonStyle, @@ -160,4 +179,6 @@ export { ProfileChipStyle, BottomContainer, ViewerWrapper, + EditorForm, + SubmitButton, }; diff --git a/frontend/src/routes.js b/frontend/src/routes.js index f06964c91..944c1203e 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -14,6 +14,10 @@ import { StudylogListPage, } from './pages'; import AbilityPage from './pages/AbilityPage'; +import EditLevellogPage from './pages/EditLevellogPage'; +import LevellogListPage from './pages/LevellogListPage'; +import LevellogPage from './pages/LevellogPage'; +import NewLevellogPage from './pages/NewLevellogPage'; import ProfilePageReportsList from './pages/ProfilePageReportsList'; const pageRoutes = [ @@ -36,6 +40,22 @@ const pageRoutes = [ path: `${PATH.STUDYLOG}/:id/edit`, render: () => , }, + // { + // path: [PATH.LEVELLOG], + // render: () => , + // }, + // { + // path: [PATH.NEW_LEVELLOG], + // render: () => , + // }, + // { + // path: [`${PATH.LEVELLOG}/:id`], + // render: () => , + // }, + // { + // path: [`${PATH.LEVELLOG}/:id/edit`], + // render: () => , + // }, { path: [PATH.PROFILE], render: () => , diff --git a/frontend/src/service/requests.js b/frontend/src/service/requests.js index 993d5b64c..18c9f2dee 100644 --- a/frontend/src/service/requests.js +++ b/frontend/src/service/requests.js @@ -283,3 +283,11 @@ export const requestDeleteLike = ({ accessToken, id }) => Authorization: `Bearer ${accessToken}`, }, }); + +export const requestGetMatchedStudylogs = ({ accessToken, startDate, endDate }) => + fetch(`${BASE_URL}/studylogs/me?startDate=${startDate}&endDate=${endDate}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); diff --git a/frontend/src/utils/localStorage.js b/frontend/src/utils/localStorage.js deleted file mode 100644 index 31679dbce..000000000 --- a/frontend/src/utils/localStorage.js +++ /dev/null @@ -1,16 +0,0 @@ -// json 형식이 아닌것은 모두 에러로 봄 -export const getLocalStorageItem = (key) => { - const item = localStorage.getItem(key); - - if (!item) { - return null; - } - - try { - const json = JSON.parse(item); - - return json; - } catch (error) { - return null; - } -};