diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java index 78fce5b9f..eb2b1cf7b 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/StudylogAcceptanceFixture.java @@ -117,7 +117,7 @@ public enum StudylogAcceptanceFixture { .map(TagAcceptanceFixture::getTagRequest) .collect(toList()); this.studylogRequest = new StudylogRequest(title, content, sessionId, missionId, - tagRequests); + tagRequests, null); } public static List findByMissionNumber(Long missionId) { diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ProfileStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ProfileStepDefinitions.java index 557ab26da..3c4c1c49f 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ProfileStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ProfileStepDefinitions.java @@ -6,9 +6,9 @@ import io.cucumber.java.en.When; import wooteco.prolog.AcceptanceSteps; import wooteco.prolog.fixtures.GithubResponses; -import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.member.application.dto.ProfileIntroRequest; import wooteco.prolog.member.application.dto.ProfileIntroResponse; +import wooteco.prolog.member.application.dto.ProfileResponse; import wooteco.prolog.studylog.application.dto.StudylogsResponse; public class ProfileStepDefinitions extends AcceptanceSteps { @@ -34,7 +34,7 @@ public class ProfileStepDefinitions extends AcceptanceSteps { @Then("{string}의 멤버 프로필이 조회된다") public void 멤버프로필이조회된다(String member) { - String memberName = context.response.as(MemberResponse.class).getNickname(); + String memberName = context.response.as(ProfileResponse.class).getNickname(); assertThat(memberName).isEqualTo(member); } diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java index ffaf15cdf..c89870ae3 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/StudylogStepDefinitions.java @@ -58,7 +58,7 @@ public class StudylogStepDefinitions extends AcceptanceSteps { Lists.newArrayList( new TagRequest(TAG1.getTagName()), new TagRequest(TAG2.getTagName()) - ) + ), null ); context.invokeHttpPostWithToken("/studylogs", studylogRequest); @@ -78,7 +78,7 @@ 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()) { @@ -454,7 +454,7 @@ public class StudylogStepDefinitions extends AcceptanceSteps { @Given("{long}, {long} 역량을 맵핑한 {string} 스터디로그를 작성하고") public void 역량을맵핑한스터디로그를작성하고(long abilityId1, long abilityId2, String studylogName) { StudylogRequest studylogRequest = new StudylogRequest(studylogName, "content", null, 1L, - Collections.emptyList()); + Collections.emptyList(), Collections.emptyList()); context.invokeHttpPostWithToken("/studylogs", studylogRequest); context.storage.put(studylogName, context.response.as(StudylogResponse.class)); } @@ -462,7 +462,7 @@ public class StudylogStepDefinitions extends AcceptanceSteps { @Given("{long} 역량 한개를 맵핑한 {string} 스터디로그를 작성하고") public void 역량한개를맵핑한스터디로그를작성하고(long abilityId1, String studylogName) { StudylogRequest studylogRequest = new StudylogRequest(studylogName, "content", null, 1L, - Collections.emptyList()); + Collections.emptyList(), Collections.emptyList()); context.invokeHttpPostWithToken("/studylogs", studylogRequest); context.storage.put(studylogName, context.response.as(StudylogResponse.class)); } diff --git a/backend/src/main/java/wooteco/prolog/session/application/AnswerService.java b/backend/src/main/java/wooteco/prolog/session/application/AnswerService.java new file mode 100644 index 000000000..529be09b6 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/application/AnswerService.java @@ -0,0 +1,94 @@ +package wooteco.prolog.session.application; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import wooteco.prolog.session.domain.Answer; +import wooteco.prolog.session.domain.AnswerTemp; +import wooteco.prolog.session.domain.Question; +import wooteco.prolog.session.domain.repository.AnswerRepository; +import wooteco.prolog.session.domain.repository.AnswerTempRepository; +import wooteco.prolog.studylog.application.dto.AnswerRequest; +import wooteco.prolog.studylog.domain.Studylog; +import wooteco.prolog.studylog.domain.StudylogTemp; + +@AllArgsConstructor +@Service +public class AnswerService { + + private QuestionService questionService; + private AnswerRepository answerRepository; + private AnswerTempRepository answerTempRepository; + + public List saveAnswers(Long memberId, List answerRequests, Studylog studylog) { + List questions = questionService.findByIds(answerRequests.stream() + .map(AnswerRequest::getQuestionId) + .collect(Collectors.toList())); + + List answers = answerRequests.stream() + .map(answerRequest -> new Answer(studylog, findQuestionById(questions, answerRequest.getQuestionId()), + memberId, answerRequest.getAnswerContent())) + .collect(Collectors.toList()); + + deleteAnswerTemp(memberId); + return answerRepository.saveAll(answers); + } + + public List saveAnswerTemp(Long memberId, List answerRequests, + StudylogTemp studylogTemp) { + List questions = questionService.findByIds(answerRequests.stream() + .map(AnswerRequest::getQuestionId) + .collect(Collectors.toList())); + + List answers = answerRequests.stream() + .map(answerRequest -> new AnswerTemp(studylogTemp, + findQuestionById(questions, answerRequest.getQuestionId()), + memberId, answerRequest.getAnswerContent())) + .collect(Collectors.toList()); + + deleteAnswerTemp(memberId); + return answerTempRepository.saveAll(answers); + } + + private void deleteAnswerTemp(Long memberId) { + if (answerTempRepository.existsByMemberId(memberId)) { + answerTempRepository.deleteByMemberId(memberId); + } + } + + private Question findQuestionById(List questions, Long questionId) { + return questions.stream() + .filter(it -> Objects.equals(it.getId(), questionId)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당하는 질문이 없습니다.")); + } + + public List findAnswersTempByMemberId(Long memberId) { + return answerTempRepository.findByMemberId(memberId); + } + + public List findAnswersByStudylogId(Long studylogId) { + return answerRepository.findByStudylogId(studylogId); + } + + public void updateAnswers(List answerRequests, Studylog studylog) { + List answers = answerRepository.findByStudylogId(studylog.getId()); + + answers.forEach(answer -> answerRequests.stream() + .filter(it -> Objects.equals(it.getQuestionId(), answer.getQuestion().getId())) + .findFirst() + .ifPresent(it -> answer.updateContent(it.getAnswerContent()))); + } + + public Map> findAnswersByStudylogs(List studylogs) { + List studylogIds = studylogs.stream() + .map(Studylog::getId) + .collect(Collectors.toList()); + + return answerRepository.findByStudylogIdIn(studylogIds).stream() + .collect(Collectors.groupingBy(answer -> answer.getStudylog().getId(), Collectors.toList())); + } +} diff --git a/backend/src/main/java/wooteco/prolog/session/application/QuestionService.java b/backend/src/main/java/wooteco/prolog/session/application/QuestionService.java new file mode 100644 index 000000000..85bca5577 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/application/QuestionService.java @@ -0,0 +1,22 @@ +package wooteco.prolog.session.application; + +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import wooteco.prolog.session.domain.Question; +import wooteco.prolog.session.domain.repository.QuestionRepository; + +@AllArgsConstructor +@Service +public class QuestionService { + + private QuestionRepository questionRepository; + + public List findByIds(List questionIds) { + return questionRepository.findAllById(questionIds); + } + + public List findQuestionsByMissionId(Long missionId) { + return questionRepository.findByMissionId(missionId); + } +} diff --git a/backend/src/main/java/wooteco/prolog/session/application/SessionService.java b/backend/src/main/java/wooteco/prolog/session/application/SessionService.java index 9f82f7509..3c999b759 100644 --- a/backend/src/main/java/wooteco/prolog/session/application/SessionService.java +++ b/backend/src/main/java/wooteco/prolog/session/application/SessionService.java @@ -4,6 +4,7 @@ import static wooteco.prolog.common.exception.BadRequestCode.DUPLICATE_SESSION_EXCEPTION; import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -59,6 +60,13 @@ public List findAll() { .collect(toList()); } + public List findAllOrderByDesc() { + return sessionRepository.findAll().stream() + .map(SessionResponse::of) + .sorted((s1, s2) -> Long.compare(s2.getId(), s1.getId())) + .collect(toList()); + } + public List findAllByOrderByIdDesc() { return sessionRepository.findAllByOrderByIdDesc().stream() .map(SessionResponse::of) @@ -93,4 +101,12 @@ public List findAllWithMySessionFirst(LoginMember loginMember) .flatMap(Collection::stream) .collect(toList()); } + + public List findMySessionResponses(LoginMember loginMember) { + if (loginMember.isAnonymous()) { + return new ArrayList<>(); + } + + return findMySessions(loginMember); + } } diff --git a/backend/src/main/java/wooteco/prolog/session/application/dto/MissionQuestionResponse.java b/backend/src/main/java/wooteco/prolog/session/application/dto/MissionQuestionResponse.java new file mode 100644 index 000000000..b214f9ada --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/application/dto/MissionQuestionResponse.java @@ -0,0 +1,13 @@ +package wooteco.prolog.session.application.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class MissionQuestionResponse { + + private Long missionId; + private List questions; +} diff --git a/backend/src/main/java/wooteco/prolog/session/application/dto/QuestionResponse.java b/backend/src/main/java/wooteco/prolog/session/application/dto/QuestionResponse.java new file mode 100644 index 000000000..b545b61c6 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/application/dto/QuestionResponse.java @@ -0,0 +1,13 @@ +package wooteco.prolog.session.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class QuestionResponse { + + private Long id; + private String content; + +} diff --git a/backend/src/main/java/wooteco/prolog/session/application/dto/SessionResponse.java b/backend/src/main/java/wooteco/prolog/session/application/dto/SessionResponse.java index 12925ef08..0eb662bf5 100644 --- a/backend/src/main/java/wooteco/prolog/session/application/dto/SessionResponse.java +++ b/backend/src/main/java/wooteco/prolog/session/application/dto/SessionResponse.java @@ -1,6 +1,7 @@ package wooteco.prolog.session.application.dto; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import wooteco.prolog.session.domain.Session; @@ -8,6 +9,7 @@ @AllArgsConstructor @NoArgsConstructor @Getter +@EqualsAndHashCode public class SessionResponse { private Long id; diff --git a/backend/src/main/java/wooteco/prolog/session/domain/Answer.java b/backend/src/main/java/wooteco/prolog/session/domain/Answer.java new file mode 100644 index 000000000..f1eff31a2 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/domain/Answer.java @@ -0,0 +1,38 @@ +package wooteco.prolog.session.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.studylog.domain.Studylog; + +@NoArgsConstructor +@Entity +@Getter +public class Answer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private Studylog studylog; + @ManyToOne + private Question question; + private Long memberId; + private String content; + + public Answer(Studylog studylog, Question question, Long memberId, String content) { + this.studylog = studylog; + this.question = question; + this.memberId = memberId; + this.content = content; + } + + public void updateContent(String content) { + this.content = content; + } +} diff --git a/backend/src/main/java/wooteco/prolog/session/domain/AnswerTemp.java b/backend/src/main/java/wooteco/prolog/session/domain/AnswerTemp.java new file mode 100644 index 000000000..b82b7cc66 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/domain/AnswerTemp.java @@ -0,0 +1,34 @@ +package wooteco.prolog.session.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.studylog.domain.StudylogTemp; + +@NoArgsConstructor +@Entity +@Getter +public class AnswerTemp { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private StudylogTemp studylogTemp; + @ManyToOne + private Question question; + private Long memberId; + private String content; + + public AnswerTemp(StudylogTemp studylogTemp, Question question, Long memberId, String content) { + this.studylogTemp = studylogTemp; + this.question = question; + this.memberId = memberId; + this.content = content; + } +} diff --git a/backend/src/main/java/wooteco/prolog/session/domain/Question.java b/backend/src/main/java/wooteco/prolog/session/domain/Question.java new file mode 100644 index 000000000..472abee74 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/domain/Question.java @@ -0,0 +1,24 @@ +package wooteco.prolog.session.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Entity +@Getter +public class Question { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String content; + + @ManyToOne + private Mission mission; + +} diff --git a/backend/src/main/java/wooteco/prolog/session/domain/repository/AnswerRepository.java b/backend/src/main/java/wooteco/prolog/session/domain/repository/AnswerRepository.java new file mode 100644 index 000000000..776828312 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/domain/repository/AnswerRepository.java @@ -0,0 +1,13 @@ +package wooteco.prolog.session.domain.repository; + +import java.util.Arrays; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.session.domain.Answer; + +public interface AnswerRepository extends JpaRepository { + + List findByStudylogId(Long studylogId); + + List findByStudylogIdIn(List studylogIds); +} diff --git a/backend/src/main/java/wooteco/prolog/session/domain/repository/AnswerTempRepository.java b/backend/src/main/java/wooteco/prolog/session/domain/repository/AnswerTempRepository.java new file mode 100644 index 000000000..3cfeed134 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/domain/repository/AnswerTempRepository.java @@ -0,0 +1,14 @@ +package wooteco.prolog.session.domain.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.session.domain.AnswerTemp; + +public interface AnswerTempRepository extends JpaRepository { + + boolean existsByMemberId(Long memberId); + + void deleteByMemberId(Long memberId); + + List findByMemberId(Long memberId); +} diff --git a/backend/src/main/java/wooteco/prolog/session/domain/repository/QuestionRepository.java b/backend/src/main/java/wooteco/prolog/session/domain/repository/QuestionRepository.java new file mode 100644 index 000000000..31e0b0e8c --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/domain/repository/QuestionRepository.java @@ -0,0 +1,10 @@ +package wooteco.prolog.session.domain.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import wooteco.prolog.session.domain.Question; + +public interface QuestionRepository extends JpaRepository { + + List findByMissionId(Long missionId); +} diff --git a/backend/src/main/java/wooteco/prolog/session/ui/QuestionController.java b/backend/src/main/java/wooteco/prolog/session/ui/QuestionController.java new file mode 100644 index 000000000..7d23b001d --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/session/ui/QuestionController.java @@ -0,0 +1,32 @@ +package wooteco.prolog.session.ui; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import wooteco.prolog.session.application.QuestionService; +import wooteco.prolog.session.application.dto.MissionQuestionResponse; +import wooteco.prolog.session.application.dto.QuestionResponse; +import wooteco.prolog.session.domain.Question; + +@RestController +@AllArgsConstructor +public class QuestionController { + + private QuestionService questionService; + + @GetMapping("/questions") + public ResponseEntity questions(@RequestParam Long missionId) { + + List questions = questionService.findQuestionsByMissionId(missionId); + List questionResponses = questions.stream() + .map(it -> new QuestionResponse(it.getId(), it.getContent())) + .collect(Collectors.toList()); + + MissionQuestionResponse missionQuestionResponse = new MissionQuestionResponse(missionId, questionResponses); + return ResponseEntity.ok(missionQuestionResponse); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/FilterService.java b/backend/src/main/java/wooteco/prolog/studylog/application/FilterService.java index 199f76358..c6c673d05 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/FilterService.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/FilterService.java @@ -1,9 +1,16 @@ package wooteco.prolog.studylog.application; +import static java.util.stream.Collectors.toList; + +import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import wooteco.prolog.login.ui.LoginMember; +import wooteco.prolog.organization.application.OrganizationService; +import wooteco.prolog.organization.domain.OrganizationGroupSession; import wooteco.prolog.session.application.MissionService; import wooteco.prolog.session.application.SessionService; import wooteco.prolog.session.application.dto.MissionResponse; @@ -16,10 +23,25 @@ public class FilterService { private final SessionService sessionService; private final MissionService missionService; + private final OrganizationService organizationService; public FilterResponse showAll(LoginMember loginMember) { - List sessionResponses = sessionService.findAllWithMySessionFirst(loginMember); + List sessionResponses = sessionService.findAllOrderByDesc(); List missionResponses = missionService.findAllWithMyMissionFirst(loginMember); - return new FilterResponse(sessionResponses, missionResponses); + + List mySessionResponses = sessionService.findMySessionResponses(loginMember); + List organizationGroupSessions = organizationService.findOrganizationGroupSessionsByMemberId( + loginMember.getId()); + List organizationSessions = organizationGroupSessions.stream() + .map(it -> SessionResponse.of(it.getSession())) + .collect(Collectors.toList()); + mySessionResponses.removeAll(organizationSessions); + + List mySessions = Stream.of(mySessionResponses, organizationSessions) + .flatMap(Collection::stream) + .sorted((s1, s2) -> Long.compare(s2.getId(), s1.getId())) + .collect(toList()); + + return new FilterResponse(sessionResponses, mySessions, missionResponses); } } 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 9689b1356..f8a68ef1f 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/StudylogService.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/StudylogService.java @@ -13,6 +13,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -31,10 +32,14 @@ import wooteco.prolog.member.application.MemberTagService; import wooteco.prolog.member.domain.Member; import wooteco.prolog.member.domain.Role; +import wooteco.prolog.session.application.AnswerService; import wooteco.prolog.session.application.MissionService; import wooteco.prolog.session.application.SessionService; +import wooteco.prolog.session.domain.Answer; +import wooteco.prolog.session.domain.AnswerTemp; import wooteco.prolog.session.domain.Mission; import wooteco.prolog.session.domain.Session; +import wooteco.prolog.studylog.application.dto.AnswerRequest; import wooteco.prolog.studylog.application.dto.CalendarStudylogResponse; import wooteco.prolog.studylog.application.dto.StudylogDocumentResponse; import wooteco.prolog.studylog.application.dto.StudylogMissionRequest; @@ -71,6 +76,7 @@ public class StudylogService { private final DocumentService studylogDocumentService; private final MemberService memberService; private final TagService tagService; + private final AnswerService answerService; private final SessionService sessionService; private final MissionService missionService; private final StudylogRepository studylogRepository; @@ -110,10 +116,20 @@ public StudylogResponse insertStudylog(Long memberId, StudylogRequest studylogRe tags.getList()) ); + List answers = saveAnswers(member.getId(), studylogRequest.getAnswers(), persistStudylog); + onStudylogCreatedEvent(member, tags, persistStudylog); deleteStudylogTemp(memberId); - return StudylogResponse.of(persistStudylog); + return StudylogResponse.of(persistStudylog, answers); + } + + private List saveAnswers(Long memberId, List answers, Studylog persistStudylog) { + if (answers == null || answers.isEmpty()) { + return new ArrayList<>(); + } + + return answerService.saveAnswers(memberId, answers, persistStudylog); } private void validateMemberIsCrew(final Member member) { @@ -126,7 +142,10 @@ private void validateMemberIsCrew(final Member member) { public StudylogTempResponse insertStudylogTemp(Long memberId, StudylogRequest studylogRequest) { StudylogTemp createdStudylogTemp = creteStudylogTemp(memberId, studylogRequest); - return StudylogTempResponse.from(createdStudylogTemp); + List answerTemps + = answerService.saveAnswerTemp(memberId, studylogRequest.getAnswers(), createdStudylogTemp); + + return StudylogTempResponse.from(createdStudylogTemp, answerTemps); } private StudylogTemp creteStudylogTemp(Long memberId, StudylogRequest studylogRequest) { @@ -145,8 +164,7 @@ private StudylogTemp creteStudylogTemp(Long memberId, StudylogRequest studylogRe tags.getList()); deleteStudylogTemp(memberId); - StudylogTemp createdStudylogTemp = studylogTempRepository.save(requestedStudylogTemp); - return createdStudylogTemp; + return studylogTempRepository.save(requestedStudylogTemp); } private void onStudylogCreatedEvent(Member foundMember, Tags tags, Studylog createdStudylog) { @@ -170,7 +188,8 @@ public StudylogsResponse findStudylogs(StudylogsSearchRequest request, Long memb public StudylogTempResponse findStudylogTemp(Long memberId) { if (studylogTempRepository.existsByMemberId(memberId)) { StudylogTemp studylogTemp = studylogTempRepository.findByMemberId(memberId); - return StudylogTempResponse.from(studylogTemp); + List answerTemps = answerService.findAnswersTempByMemberId(memberId); + return StudylogTempResponse.from(studylogTemp, answerTemps); } return StudylogTempResponse.toNull(); } @@ -256,8 +275,9 @@ public StudylogsResponse findStudylogsWithoutKeyword( .and(StudylogSpecification.orderByIdDesc()); Page studylogs = studylogRepository.findAll(specs, pageable); + Map> answers = answerService.findAnswersByStudylogs(studylogs.getContent()); Map commentCounts = commentCounts(studylogs.getContent()); - return StudylogsResponse.of(studylogs, memberId, commentCounts); + return StudylogsResponse.of(studylogs, answers, memberId, commentCounts); } public StudylogsResponse findStudylogsOf(String username, Pageable pageable) { @@ -285,9 +305,11 @@ public StudylogResponse retrieveStudylogById(LoginMember loginMember, Long study Studylog studylog = findStudylogById(studylogId); + List answers = answerService.findAnswersByStudylogId(studylog.getId()); + onStudylogRetrieveEvent(loginMember, studylog, isViewed); - return toStudylogResponse(loginMember, studylog); + return toStudylogResponse(loginMember, studylog, answers); } @Transactional @@ -335,16 +357,14 @@ private void onStudylogRetrieveEvent(LoginMember loginMember, Studylog studylog, } } - private StudylogResponse toStudylogResponse(LoginMember loginMember, Studylog studylog) { + private StudylogResponse toStudylogResponse(LoginMember loginMember, Studylog studylog, List answers) { boolean liked = studylog.likedByMember(loginMember.getId()); - boolean read = studylogReadRepository.findByMemberIdAndStudylogId(loginMember.getId(), - studylog.getId()) + boolean read = studylogReadRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId()) .isPresent(); - boolean scraped = studylogScrapRepository.findByMemberIdAndStudylogId(loginMember.getId(), - studylog.getId()) + boolean scraped = studylogScrapRepository.findByMemberIdAndStudylogId(loginMember.getId(), studylog.getId()) .isPresent(); - return StudylogResponse.of(studylog, scraped, read, liked); + return StudylogResponse.of(studylog, answers, scraped, read, liked); } public StudylogResponse findByIdAndReturnStudylogResponse(Long id) { @@ -391,6 +411,8 @@ public void updateStudylog(Long memberId, Long studylogId, StudylogRequest study Tags newTags = tagService.findOrCreate(studylogRequest.getTags()); studylog.update(studylogRequest.getTitle(), studylogRequest.getContent(), session, mission, newTags); + + answerService.updateAnswers(studylogRequest.getAnswers(), studylog); memberTagService.updateMemberTag(originalTags, newTags, foundMember); studylogDocumentService.update(studylog.toStudylogDocument()); diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/AnswerRequest.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/AnswerRequest.java new file mode 100644 index 000000000..4e73bbebc --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/AnswerRequest.java @@ -0,0 +1,27 @@ +package wooteco.prolog.studylog.application.dto; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.session.domain.Answer; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class AnswerRequest { + + private Long questionId; + private String answerContent; + + public static List listOf(List answers) { + return answers.stream() + .map(answer -> new AnswerRequest(answer.getQuestion().getId(), answer.getContent())) + .collect(Collectors.toList()); + } + public static List emptyListOf() { + return new ArrayList<>(); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/AnswerResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/AnswerResponse.java new file mode 100644 index 000000000..1efcc8e59 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/AnswerResponse.java @@ -0,0 +1,41 @@ +package wooteco.prolog.studylog.application.dto; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import wooteco.prolog.session.domain.Answer; +import wooteco.prolog.session.domain.AnswerTemp; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class AnswerResponse { + + private Long id; + private String answerContent; + private Long questionId; + private String questionContent; + + public static List emptyListOf() { + return new ArrayList<>(); + } + + public static List listOf(List answers) { + if (answers == null || answers.isEmpty()) { + return emptyListOf(); + } + + return answers.stream() + .map(answer -> new AnswerResponse(answer.getId(), answer.getContent(), answer.getQuestion().getId(), + answer.getQuestion().getContent())) + .collect(Collectors.toList()); + } + + public static AnswerResponse of(AnswerTemp answerTemp) { + return new AnswerResponse(answerTemp.getId(), answerTemp.getContent(), answerTemp.getQuestion().getId(), + answerTemp.getQuestion().getContent()); + } +} diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/FilterResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/FilterResponse.java index f30528276..a07898edb 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/FilterResponse.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/FilterResponse.java @@ -13,5 +13,6 @@ public class FilterResponse { private List sessions; + private List mySessions; private List missions; } 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..ed2788b75 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 @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import org.apache.commons.compress.utils.Lists; @NoArgsConstructor @AllArgsConstructor @@ -14,7 +15,8 @@ public class StudylogRequest { private String content; private Long sessionId; private Long missionId; - private List tags; + private List tags = Lists.newArrayList(); + private List answers = Lists.newArrayList(); public StudylogRequest(String title, String content, Long missionId, List tags) { this.title = title; 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 bbc3349ff..a29e7b1bf 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 @@ -10,6 +10,7 @@ import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.session.application.dto.MissionResponse; import wooteco.prolog.session.application.dto.SessionResponse; +import wooteco.prolog.session.domain.Answer; import wooteco.prolog.session.domain.Mission; import wooteco.prolog.session.domain.Session; import wooteco.prolog.studylog.domain.Studylog; @@ -26,6 +27,7 @@ public class StudylogResponse { private LocalDateTime updatedAt; private SessionResponse session; private MissionResponse mission; + private List answers; private String title; private String content; private List tags; @@ -49,6 +51,34 @@ public StudylogResponse( studylog.getUpdatedAt(), sessionResponse, missionResponse, + AnswerResponse.emptyListOf(), + studylog.getTitle(), + studylog.getContent(), + tagResponses, + false, + false, + studylog.getViewCount(), + liked, + studylog.getLikeCount(), + commentCount + ); + } + + public StudylogResponse( + Studylog studylog, + SessionResponse sessionResponse, + MissionResponse missionResponse, + List answerResponses, + List tagResponses, + boolean liked, long commentCount) { + this( + studylog.getId(), + MemberResponse.of(studylog.getMember()), + studylog.getCreatedAt(), + studylog.getUpdatedAt(), + sessionResponse, + missionResponse, + answerResponses, studylog.getTitle(), studylog.getContent(), tagResponses, @@ -99,6 +129,7 @@ public static StudylogResponse of(Studylog studylog, boolean scrap, studylog.getUpdatedAt(), SessionResponse.of(studylog.getSession()), MissionResponse.of(studylog.getMission()), + AnswerResponse.emptyListOf(), studylog.getTitle(), studylog.getContent(), tagResponses, @@ -111,8 +142,7 @@ public static StudylogResponse of(Studylog studylog, boolean scrap, ); } - public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read, - boolean liked) { + public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read, boolean liked) { List studylogTags = studylog.getStudylogTags(); List tagResponses = toTagResponses(studylogTags); @@ -123,6 +153,7 @@ public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read studylog.getUpdatedAt(), SessionResponse.of(studylog.getSession()), MissionResponse.of(studylog.getMission()), + AnswerResponse.emptyListOf(), studylog.getTitle(), studylog.getContent(), tagResponses, @@ -140,8 +171,11 @@ public static StudylogResponse of(Studylog studylog) { return of(studylog, false, false, null); } - public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read, - Long memberId) { + public static StudylogResponse of(Studylog studylog, List answers) { + return of(studylog, answers, false, false, false); + } + + public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read, Long memberId) { return StudylogResponse.of(studylog, scrap, read, studylog.likedByMember(memberId)); } @@ -175,6 +209,7 @@ public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read studylog.getUpdatedAt(), SessionResponse.of(session), MissionResponse.of(mission), + AnswerResponse.emptyListOf(), studylog.getTitle(), studylog.getContent(), tagResponses, @@ -187,6 +222,31 @@ public static StudylogResponse of(Studylog studylog, boolean scrap, boolean read ); } + public static StudylogResponse of(Studylog studylog, List answers, + boolean scraped, 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()), + AnswerResponse.listOf(answers), + studylog.getTitle(), + studylog.getContent(), + tagResponses, + scraped, + read, + studylog.getViewCount(), + liked, + studylog.getLikeCount(), + 0 + ); + } + public void setScrap(boolean isScrap) { this.scrap = isScrap; } diff --git a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogTempResponse.java b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogTempResponse.java index ae0ba39ec..92bd7b377 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogTempResponse.java +++ b/backend/src/main/java/wooteco/prolog/studylog/application/dto/StudylogTempResponse.java @@ -8,6 +8,7 @@ import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.session.application.dto.MissionResponse; import wooteco.prolog.session.application.dto.SessionResponse; +import wooteco.prolog.session.domain.AnswerTemp; import wooteco.prolog.studylog.domain.StudylogTemp; import wooteco.prolog.studylog.domain.StudylogTempTags; @@ -22,26 +23,37 @@ public class StudylogTempResponse { private SessionResponse session; private MissionResponse mission; private List tags; + private List answers; private StudylogTempResponse(MemberResponse author, String title, String content, SessionResponse session, - MissionResponse mission, List tags) { + MissionResponse mission, List tags, + List answers) { this.author = author; this.title = title; this.content = content; this.session = session; this.mission = mission; this.tags = tags; + this.answers = answers; } - public static StudylogTempResponse from(StudylogTemp studylogTemp) { + public static StudylogTempResponse from(StudylogTemp studylogTemp, List answerTemps) { return new StudylogTempResponse( MemberResponse.of(studylogTemp.getMember()), studylogTemp.getTitle(), studylogTemp.getContent(), SessionResponse.of(studylogTemp.getSession()), MissionResponse.of(studylogTemp.getMission()), - toTagResponses(studylogTemp.getStudylogTempTags())); + toTagResponses(studylogTemp.getStudylogTempTags()), + toAnswerRequest(answerTemps) + ); + } + + private static List toAnswerRequest(List answerTemps) { + return answerTemps.stream() + .map(answerTemp -> new AnswerRequest(answerTemp.getQuestion().getId(), answerTemp.getContent())) + .collect(toList()); } //todo TagResponse의 정적팩토리메서드로 리팩터링 @@ -52,6 +64,6 @@ private static List toTagResponses(StudylogTempTags tags) { } public static StudylogTempResponse toNull() { - return new StudylogTempResponse(null, null, null, null, null, null); + return new StudylogTempResponse(null, null, null, null, null, null, null); } } 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 8d4d4b5cb..26006495d 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 @@ -12,6 +12,7 @@ import org.springframework.data.domain.PageImpl; import wooteco.prolog.session.application.dto.MissionResponse; import wooteco.prolog.session.application.dto.SessionResponse; +import wooteco.prolog.session.domain.Answer; import wooteco.prolog.studylog.domain.Studylog; import wooteco.prolog.studylog.domain.StudylogTag; import wooteco.prolog.studylog.domain.Tag; @@ -32,8 +33,7 @@ public static StudylogsResponse of(Page page) { return of(page, null); } - public static StudylogsResponse of(Page page, Long memberId, - Map commentCounts) { + public static StudylogsResponse of(Page page, Long memberId, Map commentCounts) { Page responsePage = new PageImpl<>( toResponses(page.getContent(), memberId, commentCounts), page.getPageable(), @@ -46,6 +46,20 @@ public static StudylogsResponse of(Page page, Long memberId, responsePage.getNumber() + ONE_INDEXED_PARAMETER); } + public static StudylogsResponse of(Page page, Map> answers, Long memberId, + Map commentCounts) { + Page responsePage = new PageImpl<>( + toResponses(page.getContent(), answers, memberId, commentCounts), + page.getPageable(), + page.getTotalElements() + ); + + return new StudylogsResponse(responsePage.getContent(), + responsePage.getTotalElements(), + responsePage.getTotalPages(), + responsePage.getNumber() + ONE_INDEXED_PARAMETER); + } + public static StudylogsResponse of(Page page, Long memberId) { Page responsePage = new PageImpl<>( toResponses(page.getContent(), memberId), @@ -65,10 +79,9 @@ public static StudylogsResponse of( int totalPage, int currPage, Long memberId, - Map commentCounts - ) { - final List studylogResponses = convertToStudylogResponse(studylogs, - memberId, commentCounts); + Map commentCounts) { + + List studylogResponses = convertToStudylogResponse(studylogs, memberId, commentCounts); return new StudylogsResponse(studylogResponses, totalSize, totalPage, @@ -91,11 +104,35 @@ private static List toResponses(List studylogs, Long .collect(toList()); } + private static List toResponses(List studylogs, Map> answers, + Long memberId, + Map commentCounts) { + return studylogs.stream() + .map(studylog -> toResponse(studylog, answers.get(studylog.getId()), memberId, + commentCounts.get(studylog.getId()))) + .collect(toList()); + } + private static List toResponses(List studylogs, Long memberId) { return studylogs.stream().map(studylog -> toResponse(studylog, memberId)).collect(toList()); } - private static StudylogResponse toResponse(Studylog studylog, Long memberId, + private static StudylogResponse toResponse(Studylog studylog, Long memberId, long commentCount) { + List studylogTags = studylog.getStudylogTags(); + final List tags = studylogTags.stream() + .map(StudylogTag::getTag) + .collect(toList()); + + return new StudylogResponse( + studylog, + SessionResponse.of(studylog.getSession()), + MissionResponse.of(studylog.getMission()), + toResponse(tags), + studylog.likedByMember(memberId), commentCount + ); + } + + private static StudylogResponse toResponse(Studylog studylog, List answers, Long memberId, long commentCount) { List studylogTags = studylog.getStudylogTags(); final List tags = studylogTags.stream() @@ -106,6 +143,7 @@ private static StudylogResponse toResponse(Studylog studylog, Long memberId, studylog, SessionResponse.of(studylog.getSession()), MissionResponse.of(studylog.getMission()), + AnswerResponse.listOf(answers), toResponse(tags), studylog.likedByMember(memberId), commentCount ); 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 d0e4d3e9b..7fa1856cf 100644 --- a/backend/src/main/java/wooteco/prolog/studylog/ui/StudylogController.java +++ b/backend/src/main/java/wooteco/prolog/studylog/ui/StudylogController.java @@ -55,8 +55,7 @@ public ResponseEntity createStudylog(@AuthMemberPrincipal Logi @MemberOnly public ResponseEntity createStudylogTemp( @AuthMemberPrincipal LoginMember member, @RequestBody StudylogRequest studylogRequest) { - StudylogTempResponse studylogTempResponse = studylogService.insertStudylogTemp( - member.getId(), studylogRequest); + StudylogTempResponse studylogTempResponse = studylogService.insertStudylogTemp(member.getId(), studylogRequest); return ResponseEntity.created(URI.create("/studylogs/temp/" + studylogTempResponse.getId())) .body(studylogTempResponse); } @@ -65,8 +64,7 @@ public ResponseEntity createStudylogTemp( @MemberOnly public ResponseEntity showStudylogTemp( @AuthMemberPrincipal LoginMember member) { - StudylogTempResponse studylogTempResponse = studylogService.findStudylogTemp( - member.getId()); + StudylogTempResponse studylogTempResponse = studylogService.findStudylogTemp(member.getId()); return ResponseEntity.ok(studylogTempResponse); } @@ -88,10 +86,10 @@ public ResponseEntity showStudylog(@PathVariable String id, throw new BadRequestException(STUDYLOG_NOT_FOUND); } - viewedStudyLogCookieGenerator.setViewedStudyLogCookie(viewedStudyLogs, id, - httpServletResponse); - return ResponseEntity.ok(studylogService.retrieveStudylogById(member, Long.parseLong(id), - viewedStudyLogCookieGenerator.isViewed(viewedStudyLogs, id))); + viewedStudyLogCookieGenerator.setViewedStudyLogCookie(viewedStudyLogs, id, httpServletResponse); + boolean viewed = viewedStudyLogCookieGenerator.isViewed(viewedStudyLogs, id); + StudylogResponse body = studylogService.retrieveStudylogById(member, Long.parseLong(id), viewed); + return ResponseEntity.ok(body); } @PutMapping("/{id}") diff --git a/backend/src/main/resources/db/migration/prod/V12__create_table_question.sql b/backend/src/main/resources/db/migration/prod/V12__create_table_question.sql new file mode 100644 index 000000000..031228286 --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V12__create_table_question.sql @@ -0,0 +1,34 @@ +create table if not exists prolog.question ( + id bigint auto_increment primary key, + content varchar(255), + mission_id bigint not null, + foreign key (mission_id) references prolog.mission (id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; + +create table if not exists prolog.answer ( + id bigint auto_increment primary key, + content varchar(255), + studylog_id bigint not null, + question_id bigint not null, + member_id bigint not null, + foreign key (studylog_id) references prolog.studylog (id), + foreign key (question_id) references prolog.question (id), + foreign key (member_id) references prolog.member (id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; + +create table if not exists prolog.answer_temp ( + id bigint auto_increment primary key, + content varchar(255), + studylog_temp_id bigint not null, + question_id bigint not null, + member_id bigint not null, + foreign key (studylog_temp_id) references prolog.studylog_temp (id), + foreign key (question_id) references prolog.question (id), + foreign key (member_id) references prolog.member (id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; diff --git a/backend/src/main/resources/db/migration/prod/V9__alter_table_group_and_member.sql b/backend/src/main/resources/db/migration/prod/V9__alter_table_group_and_member.sql index 6766b3a8e..a19a32a12 100644 --- a/backend/src/main/resources/db/migration/prod/V9__alter_table_group_and_member.sql +++ b/backend/src/main/resources/db/migration/prod/V9__alter_table_group_and_member.sql @@ -11,7 +11,7 @@ create table if not exists department_member id bigint auto_increment primary key, member_id bigint not null, department_id bigint not null, - constraint FK_DEPARTMENT_MEMBER_ON_MEMBERㅇ + constraint FK_DEPARTMENT_MEMBER_ON_MEMBER foreign key (member_id) references prolog.member (id), constraint FK_DEPARTMENT_MEMBER_ON_DEPARTMENT foreign key (department_id) references prolog.department (id) diff --git a/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java b/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java index dd71e8d90..1fdcd383f 100644 --- a/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/session/application/SessionServiceTest.java @@ -6,12 +6,10 @@ import static org.mockito.Mockito.doReturn; import static wooteco.prolog.common.exception.BadRequestCode.DUPLICATE_SESSION_EXCEPTION; import static wooteco.prolog.common.exception.BadRequestCode.ROADMAP_SESSION_NOT_FOUND_EXCEPTION; -import static wooteco.prolog.login.ui.LoginMember.Authority.ANONYMOUS; import static wooteco.prolog.login.ui.LoginMember.Authority.MEMBER; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -142,7 +140,7 @@ void findMySessions() { sessionMembers.add(new SessionMember(1L, new Member("member1", "베베", Role.CREW, Long.MIN_VALUE, "img"))); doReturn(sessionMembers).when(sessionMemberService).findByMemberId(member.getId()); - doReturn(sessions).when(sessionRepository).findAllById(Arrays.asList(1L)); + doReturn(sessions).when(sessionRepository).findAllByIdInOrderByIdDesc(Arrays.asList(1L)); // when List responses = sessionService.findMySessions(member); @@ -171,61 +169,24 @@ void findMySessionIds() { ); } - @DisplayName("LoginMember와 관련된 Session을 목록 상단에 보여주도록 정렬한다.(LoginMember의 Session이 최상단에 위치)") + @DisplayName("현재 로그인한 Member의 Session을 조회한다.") @Test - void findALlWithMySessionFirst() { + void findMySessionResponses() { // given - final LoginMember loginMember = new LoginMember(MEMBER); - final Session session1 = new Session("session1"); - final Session session2 = new Session("session2"); - final Session session3 = new Session("session3"); - - final List mySessions = new ArrayList<>(); - mySessions.add(session2); - doReturn(mySessions).when(sessionRepository).findAllById(Collections.emptyList()); - - final List allSessions = new ArrayList<>(); - allSessions.add(session1); - allSessions.add(session2); - allSessions.add(session3); - doReturn(allSessions).when(sessionRepository).findAll(); - - // when - List responses = sessionService.findAllWithMySessionFirst(loginMember); - - // then - assertAll( - () -> assertThat(responses.get(0).getName()).isEqualTo("session2"), - () -> assertThat(responses).extracting(SessionResponse::getName).contains("session1", "session2", "session3") - ); - } + final LoginMember member = new LoginMember(1L, MEMBER); + final List sessions = new ArrayList<>(); + sessions.add(new Session("session1")); - @DisplayName("LoginMember의 Authority가 Anonymous일 때 모든 Session을 반환한다.") - @Test - void findALlWithMySessionFirstReturnFindAll() { - // given - final LoginMember loginMember = new LoginMember(ANONYMOUS); - final Session session1 = new Session("session1"); - final Session session2 = new Session("session2"); - final Session session3 = new Session("session3"); + final List sessionMembers = new ArrayList<>(); + sessionMembers.add(new SessionMember(1L, new Member("member1", "베베", Role.CREW, Long.MIN_VALUE, "img"))); - final List allSessions = new ArrayList<>(); - allSessions.add(session1); - allSessions.add(session2); - allSessions.add(session3); - doReturn(allSessions).when(sessionRepository).findAll(); + doReturn(sessionMembers).when(sessionMemberService).findByMemberId(member.getId()); + doReturn(sessions).when(sessionRepository).findAllByIdInOrderByIdDesc(Arrays.asList(1L)); // when - final List responses = sessionService.findAllWithMySessionFirst(loginMember); - final SessionResponse firstResponse = responses.get(0); + List responses = sessionService.findMySessionResponses(member); // then - assertAll( - () -> assertThat(firstResponse.getName()).isEqualTo("session1"), - () -> assertThat(responses.get(0).getName()).isEqualTo("session1"), - () -> assertThat(responses).extracting(SessionResponse::getName).contains("session1", "session2", "session3"), - () -> assertThat(responses).hasSize(3) - ); + assertThat(responses.get(0).getName()).isEqualTo("session1"); } - } diff --git a/backend/src/test/java/wooteco/prolog/studylog/application/FilterServiceTest.java b/backend/src/test/java/wooteco/prolog/studylog/application/FilterServiceTest.java index 7df5fbf52..e3a9951e0 100644 --- a/backend/src/test/java/wooteco/prolog/studylog/application/FilterServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/studylog/application/FilterServiceTest.java @@ -4,7 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.Mockito.doReturn; -import java.util.ArrayList; +import de.flapdoodle.embed.process.collections.Collections; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,11 +13,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import wooteco.prolog.login.ui.LoginMember; -import wooteco.prolog.member.application.MemberService; -import wooteco.prolog.roadmap.application.dto.SessionResponse; +import wooteco.prolog.login.ui.LoginMember.Authority; +import wooteco.prolog.organization.application.OrganizationService; +import wooteco.prolog.organization.domain.OrganizationGroupSession; import wooteco.prolog.session.application.MissionService; import wooteco.prolog.session.application.SessionService; import wooteco.prolog.session.application.dto.MissionResponse; +import wooteco.prolog.session.application.dto.SessionResponse; +import wooteco.prolog.session.domain.Session; import wooteco.prolog.studylog.application.dto.FilterResponse; @ExtendWith(MockitoExtension.class) @@ -33,33 +36,41 @@ class FilterServiceTest { MissionService missionService; @Mock - TagService tagService; + OrganizationService organizationService; - @Mock - MemberService memberService; - - @DisplayName("로그인 멤버와 관련된 세션, 미션, 태그, 멤버 정보를 가져와서 FilterResponse를 반환한다.") + @DisplayName("로그인 멤버와 관련된 FilterResponse를 반환한다.") @Test void showAll() { // given - LoginMember loginMember = new LoginMember(); + LoginMember loginMember = new LoginMember(1L, Authority.MEMBER); - List sessionResponses = new ArrayList<>(); - sessionResponses.add(new SessionResponse(1L, "session1")); - doReturn(sessionResponses).when(sessionService).findAllWithMySessionFirst(loginMember); + SessionResponse session1 = new SessionResponse(1L, "session1"); + List sessionResponses = Collections.newArrayList(session1); + doReturn(sessionResponses).when(sessionService).findAllOrderByDesc(); - List missionResponses = new ArrayList<>(); - missionResponses.add(new MissionResponse(1L, "mission1", - new wooteco.prolog.session.application.dto.SessionResponse(1L, "session1"))); + List missionResponses = Collections.newArrayList( + new MissionResponse(1L, "mission1", session1)); doReturn(missionResponses).when(missionService).findAllWithMyMissionFirst(loginMember); + SessionResponse session2 = new SessionResponse(2L, "session2"); + List mySessionResponses = Collections.newArrayList(session2); + doReturn(mySessionResponses).when(sessionService).findMySessionResponses(loginMember); + + Session session3 = new Session(3L, null, "session3"); + List organizationGroupSessions = Collections.newArrayList( + new OrganizationGroupSession(1L, session3)); + doReturn(organizationGroupSessions).when(organizationService) + .findOrganizationGroupSessionsByMemberId(loginMember.getId()); + // when - FilterResponse response = filterService.showAll(loginMember); + FilterResponse filterResponse = filterService.showAll(loginMember); + mySessionResponses.add(SessionResponse.of(session3)); // then assertAll( - () -> assertThat(response.getSessions()).isEqualTo(sessionResponses), - () -> assertThat(response.getMissions()).isEqualTo(missionResponses) + () -> assertThat(filterResponse.getSessions()).isEqualTo(sessionResponses), + () -> assertThat(filterResponse.getMySessions()).containsExactlyInAnyOrderElementsOf(mySessionResponses), + () -> assertThat(filterResponse.getMissions()).isEqualTo(missionResponses) ); } } diff --git a/backend/src/test/java/wooteco/prolog/studylog/application/StudylogServiceTest.java b/backend/src/test/java/wooteco/prolog/studylog/application/StudylogServiceTest.java index 7663ba901..c75e92800 100644 --- a/backend/src/test/java/wooteco/prolog/studylog/application/StudylogServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/studylog/application/StudylogServiceTest.java @@ -10,6 +10,7 @@ import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -49,6 +50,7 @@ import wooteco.prolog.member.application.dto.MemberResponse; import wooteco.prolog.member.domain.Member; import wooteco.prolog.member.domain.Role; +import wooteco.prolog.session.application.AnswerService; import wooteco.prolog.session.application.MissionService; import wooteco.prolog.session.application.SessionService; import wooteco.prolog.session.application.dto.MissionResponse; @@ -98,6 +100,8 @@ class StudylogServiceTest { @Mock private MissionService missionService; @Mock + private AnswerService answerService; + @Mock private StudylogRepository studylogRepository; @Mock private StudylogScrapRepository studylogScrapRepository; @@ -119,8 +123,7 @@ class insertStudylog { Tags tags = Tags.of(singletonList("스터디로그")); Member member = new Member(1L, "김동해", "오션", Role.CREW, 1L, "image"); List tagRequests = singletonList(new TagRequest("스터디로그")); - StudylogRequest studylogRequest = new StudylogRequest(title, content, null, null, - tagRequests); + StudylogRequest studylogRequest = new StudylogRequest(title, content, null, null, tagRequests, null); Studylog studylog = new Studylog(member, studylogRequest.getTitle(), studylogRequest.getContent(), null, null, tags.getList()); List expectedTagResponses = singletonList(new TagResponse(null, "스터디로그")); @@ -180,8 +183,7 @@ class insertStudylogTemp { Member member = new Member(1L, "문채원", "라온", Role.CREW, 1L, "image"); List tagRequests = singletonList(new TagRequest("스터디로그")); List tagResponses = singletonList(new TagResponse(null, "스터디로그")); - StudylogRequest studylogRequest = new StudylogRequest(title, content, null, null, - tagRequests); + StudylogRequest studylogRequest = new StudylogRequest(title, content, null, null, tagRequests, null); StudylogTemp studylogTemp = new StudylogTemp(member, studylogRequest.getTitle(), studylogRequest.getContent(), null, null, tags.getList()); @@ -193,6 +195,7 @@ void insertStudylogTemp_existStudylogTemp() { when(tagService.findOrCreate(anyList())).thenReturn(tags); when(studylogTempRepository.save(any())).thenReturn(studylogTemp); when(studylogTempRepository.existsByMemberId(1L)).thenReturn(true); + when(answerService.saveAnswerTemp(any(), any(), any())).thenReturn(emptyList()); // when StudylogTempResponse studylogTempResponse = studylogService.insertStudylogTemp(1L, @@ -262,6 +265,7 @@ void findStudylogsWithoutKeyword() { //given when(studylogRepository.findAll((Specification) any(), (Pageable) any())) .thenReturn(Page.empty()); + when(answerService.findAnswersByStudylogs(any())).thenReturn(Collections.emptyMap()); //when studylogService.findStudylogsWithoutKeyword(emptyList(), @@ -321,6 +325,7 @@ void insertStudyLogs() { new Studylog(member, "제목", "내용", mission, emptyList())); when(memberService.findById(any())).thenReturn(member); when(tagService.findOrCreate(any())).thenReturn(new Tags(emptyList())); + when(answerService.saveAnswers(anyLong(), any(), any())).thenReturn(emptyList()); //when studylogService.insertStudylogs(1L, studylogRequests); @@ -372,6 +377,9 @@ void retrieveStudylogById_anonymous() { final LoginMember loginMember = new LoginMember(LoginMember.Authority.ANONYMOUS); given(studylogRepository.findById(anyLong())) .willReturn(Optional.of(studylog)); + given(answerService.findAnswersByStudylogId(anyLong())) + .willReturn(emptyList()); + final int previousViewCount = studylog.getViewCount(); //when @@ -402,6 +410,7 @@ void retrieveStudylogById_readOtherUserStudylog() { .willReturn(Optional.of(new StudylogScrap(null, null))); given(memberService.findById(anyLong())) .willReturn(new Member(otherUserId, null, null, null, null, null)); + given(answerService.findAnswersByStudylogId(anyLong())).willReturn(emptyList()); final int previousViewCount = studylog.getViewCount(); @@ -435,6 +444,7 @@ void retrieveStudylogById_readMineStudyLog() { .willReturn(Optional.of(new StudylogScrap(null, null))); given(memberService.findById(anyLong())) .willReturn(new Member(1L, null, null, null, null, null)); + given(answerService.findAnswersByStudylogId(anyLong())).willReturn(emptyList()); final int previousViewCount = studylog.getViewCount(); @@ -706,6 +716,9 @@ void findStudylogs_findBySpecs() { ) ); + given(answerService.findAnswersByStudylogs(any())) + .willReturn(Collections.emptyMap()); + //when final StudylogsResponse studylogsResponse = studylogService.findStudylogs( studylogsSearchRequest, 1L); @@ -834,7 +847,7 @@ void updateStudylog() { "변경된 내용", 3L, 5L, - tagRequests + tagRequests, null ); final Studylog studylog = new Studylog( @@ -864,6 +877,8 @@ void updateStudylog() { when(tagService.findOrCreate(any())) .thenReturn(newTags); + doNothing().when(answerService).updateAnswers(any(), any()); + //when studylogService.updateStudylog( memberId, @@ -914,6 +929,9 @@ void findStudylogTemp() { when(studylogTempRepository.findByMemberId(anyLong())) .thenReturn(studylogTemp); + when(answerService.findAnswersTempByMemberId(any())) + .thenReturn(Collections.emptyList()); + //when final StudylogTempResponse studylogTempResponse = studylogService.findStudylogTemp(1L); @@ -995,7 +1013,7 @@ void updateRead() { private List makeStudylogResponseFor(final long id, final int times) { return Stream.generate(() -> new StudylogResponse(id, - null, null, null, null, null, null, null, null, false, false, 0, false, 0, 0)) + null, null, null, null, null, null, null, null, null, false, false, 0, false, 0, 0)) .limit(times) .collect(Collectors.toList()); } diff --git a/frontend/package.json b/frontend/package.json index c7ad4b177..ea921499b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@toast-ui/editor-plugin-color-syntax": "3.0.1", "@toast-ui/react-editor": "3.0.3", "axios": "^0.26.1", + "bootstrap": "^5.3.3", "cross-env": "7.0.3", "d3": "^7.8.5", "moment": "^2.29.1", diff --git a/frontend/src/apis/questions.ts b/frontend/src/apis/questions.ts new file mode 100644 index 000000000..3d44618de --- /dev/null +++ b/frontend/src/apis/questions.ts @@ -0,0 +1,8 @@ +import { AxiosResponse } from 'axios'; +import { createAxiosInstance } from '../utils/axiosInstance'; + +const instanceWithoutToken = createAxiosInstance(); + +/** 질문 조회 **/ +export const fetchQuestionsByMissionId = (missionId: number): Promise> => + instanceWithoutToken.get(`/questions?missionId=${missionId}`); diff --git a/frontend/src/components/Card/Card.styles.ts b/frontend/src/components/Card/Card.styles.ts index 488013f63..284c4888e 100644 --- a/frontend/src/components/Card/Card.styles.ts +++ b/frontend/src/components/Card/Card.styles.ts @@ -29,9 +29,11 @@ const sizeStyle = { }; const Container = styled.section` + padding: 2.5rem; background-color: ${COLOR.WHITE}; - border: 1px solid ${COLOR.LIGHT_GRAY_400}; - border-radius: 2rem; + border-radius: 8px; + border: 1px solid ${COLOR.LIGHT_GRAY_50}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); ${({ size }) => sizeStyle[size] || sizeStyle.SMALL} ${({ css }) => css} diff --git a/frontend/src/components/Comment/Comment.style.tsx b/frontend/src/components/Comment/Comment.style.tsx index 8c4d35a5c..3c5ee82a4 100644 --- a/frontend/src/components/Comment/Comment.style.tsx +++ b/frontend/src/components/Comment/Comment.style.tsx @@ -1,11 +1,13 @@ import styled from '@emotion/styled'; import { COLOR } from '../../enumerations/color'; +import { css } from '@emotion/react'; export const Root = styled.div` display: flex; flex-direction: column; - - padding-bottom: 28px; + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid ${COLOR.LIGHT_GRAY_200}; `; export const Top = styled.div` @@ -17,7 +19,7 @@ export const Top = styled.div` export const Left = styled.div` display: flex; align-items: center; - gap: 18px; + gap: 10px; & > a { display: flex; @@ -27,9 +29,16 @@ export const Left = styled.div` `; export const Logo = styled.img` - width: 36px; - height: 36px; + width: 50px; + height: 50px; border-radius: 12px; + padding: 0.5rem; + border: 1px solid ${COLOR.LIGHT_GRAY_100}; +`; + +export const MemberName = styled.span` + color: ${COLOR.DARK_GRAY_900}; + width: fit-content; `; export const CreatedDate = styled.span` @@ -49,3 +58,22 @@ export const ButtonContainer = styled.div` display: flex; gap: 20px; `; + +export const ActionButton = styled.button` + padding: 0.5rem 1rem; + border-radius: 10px; + border: 1px solid ${COLOR.LIGHT_GRAY_50}; + &:hover { + border: 1px solid ${COLOR.LIGHT_GRAY_100}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } +`; + +export const DeleteButton = styled.button` + padding: 0.5rem 1rem; + border-radius: 10px; + &:hover { + background-color: ${COLOR.RED_50}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } +`; diff --git a/frontend/src/components/Comment/Comment.tsx b/frontend/src/components/Comment/Comment.tsx index 22c6a8dac..f1b763440 100644 --- a/frontend/src/components/Comment/Comment.tsx +++ b/frontend/src/components/Comment/Comment.tsx @@ -16,6 +16,7 @@ import Editor from '../Editor/Editor'; import { useContext, useRef, useState } from 'react'; import { COLOR } from '../../enumerations/color'; import { UserContext } from '../../contexts/UserProvider'; +import {ActionButton, DeleteButton} from './Comment.style'; export interface CommentProps extends CommentType { editComment: (commentId: number, body: CommentRequest) => void; @@ -77,24 +78,33 @@ const Comment = ({ id, author, content, createAt, editComment, deleteComment }: - {nickname} - {new Date(createAt).toLocaleString('ko-KR')} +
+ + {nickname} + + {new Date(createAt).toLocaleString('ko-KR')} +
{user.userId === author.id && ( - - + )} void; deleteComment: (commentId: number) => void; - onSubmit?: FormEventHandler; - editorContentRef?: MutableRefObject; } const CommentList = ({ comments, editComment, deleteComment, - onSubmit, - editorContentRef, }: CommentListProps) => { return ( diff --git a/frontend/src/components/CommentCount/CommentCount.styles.ts b/frontend/src/components/CommentCount/CommentCount.styles.ts deleted file mode 100644 index 88ae78e33..000000000 --- a/frontend/src/components/CommentCount/CommentCount.styles.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { css } from '@emotion/react'; -import { COLOR } from '../../constants'; - -export const CommentContainerStyle = css` - display: flex; - align-items: center; - color: ${COLOR.LIGHT_GRAY_900}; - font-size: 1.4rem; - gap: 0.2em; - margin-top: 0.5em; -`; diff --git a/frontend/src/components/CommentCount/CommentCount.tsx b/frontend/src/components/CommentCount/CommentCount.tsx deleted file mode 100644 index 939cd4130..000000000 --- a/frontend/src/components/CommentCount/CommentCount.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** @jsxImportSource @emotion/react */ - -import { ReactComponent as CommentIcon } from '../../assets/images/comment.svg'; -import { CommentContainerStyle } from './CommentCount.styles'; - -interface CommentCountProps { - count: number; -} - -const CommentCount = ({ count }: CommentCountProps) => { - return ( -
- - {count} -
- ); -}; - -export default CommentCount; diff --git a/frontend/src/components/Controls/SelectBox.tsx b/frontend/src/components/Controls/SelectBox.tsx index 168875962..7e073bc8f 100644 --- a/frontend/src/components/Controls/SelectBox.tsx +++ b/frontend/src/components/Controls/SelectBox.tsx @@ -10,23 +10,28 @@ const selectStyles = { ...styles, fontSize: '1.4rem', }), - control: (styles) => ({ + control: (styles, { isDisabled }) => ({ ...styles, outline: 'none', - border: 'none', + border: '1px solid ' + COLOR.LIGHT_GRAY_100, boxShadow: 'none', - color: COLOR.DARK_GRAY_900, + color: isDisabled ? COLOR.LIGHT_GRAY_500 : COLOR.DARK_GRAY_900, paddingLeft: '1rem', - cursor: 'pointer', - backgroundColor: COLOR.LIGHT_GRAY_100, + cursor: isDisabled ? 'not-allowed' : 'pointer', + backgroundColor: isDisabled ? COLOR.LIGHT_GRAY_200 : COLOR.WHITE, + fontSize: '1.2rem', }), indicatorsContainer: (styles) => ({ ...styles }), valueContainer: (styles) => ({ ...styles, padding: '0' }), menu: (styles) => ({ ...styles, - fontSize: '1.4rem', + fontSize: '1.2rem', fontColor: COLOR.DARK_GRAY_900, }), + placeholder: (styles) => ({ + ...styles, + fontSize: '1.2rem', + }), }; interface SelectOption { @@ -35,23 +40,23 @@ interface SelectOption { } /** - FIXME: value props type SelectOption['value'] 로 변경되어야 함. - 아래 예시처럼 type을 좁힐 수 없는 문제가 있음. + FIXME: value props type SelectOption['value'] 로 변경되어야 함. + 아래 예시처럼 type을 좁힐 수 없는 문제가 있음. - const CATEGORY_OPTIONS = [ - { value: '', label: '전체보기' }, - { value: 'frontend', label: '프론트엔드' }, - { value: 'backend', label: '백엔드' }, - { value: 'android', label: '안드로이드' }, - ]; + const CATEGORY_OPTIONS = [ + { value: '', label: '전체보기' }, + { value: 'frontend', label: '프론트엔드' }, + { value: 'backend', label: '백엔드' }, + { value: 'android', label: '안드로이드' }, + ]; - type CategoryOptions = typeof CATEGORY_OPTIONS[number]; + type CategoryOptions = typeof CATEGORY_OPTIONS[number]; - ->type CategoryOptions = { - value: string; - label: string; - } - 위 처럼 value type을 좁힐 수 없음. + ->type CategoryOptions = { + value: string; + label: string; + } + 위 처럼 value type을 좁힐 수 없음. */ interface SelectBoxProps { isMulti?: boolean; @@ -62,6 +67,7 @@ interface SelectBoxProps { value?: SelectOption; selectedSessionId?: string; isClearable?: boolean; + editable: boolean; } const SelectBox: React.VFC = ({ @@ -72,6 +78,7 @@ const SelectBox: React.VFC = ({ onChange, value, defaultOption, + editable = true, }: SelectBoxProps) => (
= ({ } > div:hover { - border: 2px solid ${COLOR.LIGHT_BLUE_500}; + border: 2px solid ${COLOR.LIGHT_GRAY_300}; } .css-clear-indicator { @@ -103,6 +110,7 @@ const SelectBox: React.VFC = ({ styles={selectStyles} defaultValue={defaultOption} value={value} + isDisabled={!editable} />
); diff --git a/frontend/src/components/Count/CommentCount.tsx b/frontend/src/components/Count/CommentCount.tsx new file mode 100644 index 000000000..5ccd965fa --- /dev/null +++ b/frontend/src/components/Count/CommentCount.tsx @@ -0,0 +1,32 @@ +/** @jsxImportSource @emotion/react */ + +import { SerializedStyles } from '@emotion/cache/node_modules/@emotion/utils'; +import { ReactComponent as CommentIcon } from '../../assets/images/comment.svg'; +import styled from "@emotion/styled"; +import {COLOR} from "../../constants"; + +const Container = styled.div<{ css?: SerializedStyles }>` + color: ${COLOR.LIGHT_GRAY_900}; + font-size: 1.4rem; + margin-left: 1rem; + + & > svg { + margin-right: 0.25rem; + } + + ${({ css }) => css}; +`; + +const Count = styled.span` + vertical-align: bottom; +`; + + +const CommentCount = ({ css, count }: { css?: SerializedStyles; count: number }) => ( + + + {count} + +); + +export default CommentCount; diff --git a/frontend/src/components/Count/LikeCount.tsx b/frontend/src/components/Count/LikeCount.tsx new file mode 100644 index 000000000..da1f97162 --- /dev/null +++ b/frontend/src/components/Count/LikeCount.tsx @@ -0,0 +1,31 @@ +/** @jsxImportSource @emotion/react */ + +import { SerializedStyles } from '@emotion/cache/node_modules/@emotion/utils'; +import { ReactComponent as LikeIcon } from '../../assets/images/heart.svg'; +import styled from "@emotion/styled"; +import {COLOR} from "../../constants"; + +const Container = styled.div<{ css?: SerializedStyles }>` + color: ${COLOR.LIGHT_GRAY_900}; + font-size: 1.4rem; + + & > svg { + margin-right: 0.25rem; + } + + ${({ css }) => css}; +`; + +const Count = styled.span` + vertical-align: bottom; +`; + + +const ViewCount = ({ css, count }: { css?: SerializedStyles; count: number }) => ( + + + {count} + +); + +export default ViewCount; diff --git a/frontend/src/components/Count/ViewCount.tsx b/frontend/src/components/Count/ViewCount.tsx new file mode 100644 index 000000000..d0507c2f5 --- /dev/null +++ b/frontend/src/components/Count/ViewCount.tsx @@ -0,0 +1,32 @@ +/** @jsxImportSource @emotion/react */ + +import { SerializedStyles } from '@emotion/cache/node_modules/@emotion/utils'; +import { ReactComponent as ViewIcon } from '../../assets/images/view.svg'; +import styled from "@emotion/styled"; +import {COLOR} from "../../constants"; + +const Container = styled.div<{ css?: SerializedStyles }>` + color: ${COLOR.LIGHT_GRAY_900}; + font-size: 1.4rem; + margin-left: 1rem; + + & > svg { + margin-right: 0.25rem; + } + + ${({ css }) => css}; +`; + +const Count = styled.span` + vertical-align: bottom; +`; + + +const ViewCount = ({ css, count }: { css?: SerializedStyles; count: number }) => ( + + + {count} + +); + +export default ViewCount; diff --git a/frontend/src/components/Editor/Editor.styles.ts b/frontend/src/components/Editor/Editor.styles.ts index c8a1255d1..89d43d002 100644 --- a/frontend/src/components/Editor/Editor.styles.ts +++ b/frontend/src/components/Editor/Editor.styles.ts @@ -2,9 +2,6 @@ import { css } from '@emotion/react'; import { COLOR } from '../../constants'; export const EditorWrapperStyle = css` - border-radius: 2rem; - border: 1px solid ${COLOR.LIGHT_GRAY_100}; - .toastui-editor-mode-switch { height: 4.8rem; border-bottom-right-radius: 2rem; @@ -13,16 +10,15 @@ export const EditorWrapperStyle = css` `; export const EditorTitleStyle = css` - padding: 2rem 1.6rem 1.6rem; - background-color: ${COLOR.LIGHT_BLUE_200}; - border-top-right-radius: 2rem; - border-top-left-radius: 2rem; + padding-bottom: 2rem; + margin-bottom: 2rem; + border-bottom: 1px solid ${COLOR.LIGHT_GRAY_100}; > input { width: 100%; font-size: 2.4rem; - border-radius: 1rem; border: none; - padding: 0.4rem 1rem; + //padding: 0.4rem 1rem; + outline: none; } `; diff --git a/frontend/src/components/Editor/Editor.tsx b/frontend/src/components/Editor/Editor.tsx index a54bcafea..b3ab6634d 100644 --- a/frontend/src/components/Editor/Editor.tsx +++ b/frontend/src/components/Editor/Editor.tsx @@ -9,7 +9,7 @@ import Prism from 'prismjs'; import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js'; import { markdownStyle } from '../../styles/markdown.styles'; import { EditorStyle } from '../Introduction/Introduction.styles'; -import { EditorTitleStyle, EditorWrapperStyle } from './Editor.styles'; +import { EditorWrapperStyle } from './Editor.styles'; import { ChangeEventHandler, MutableRefObject } from 'react'; import { getSize } from '../../utils/styles'; import useImage from '../../hooks/useImage'; @@ -21,7 +21,7 @@ interface EditorProps { titlePlaceholder?: string; titleReadOnly?: boolean; editorContentRef: MutableRefObject; - content?: string | null; + content?: string; onChangeTitle?: ChangeEventHandler; onChangeContent?: () => void; toolbarItems?: string[][]; @@ -35,44 +35,28 @@ const DEFAULT_TOOLBAR_ITEMS = [ ]; const Editor = (props: EditorProps): JSX.Element => { - const { - height, - hasTitle = true, - title = '', - titlePlaceholder = '제목을 입력하세요', - titleReadOnly = false, - content, - onChangeTitle, - editorContentRef, - toolbarItems = DEFAULT_TOOLBAR_ITEMS, - } = props; + const { height, content, editorContentRef, toolbarItems = DEFAULT_TOOLBAR_ITEMS } = props; const { uploadImage } = useImage(); return (
- {hasTitle && ( -
- -
- )} - {/* FIXME: 임시방편 editor에 상태 값을 초기값으로 넣는 법 찾기 */} - {content !== null && ( - { - editorContentRef.current = element; - }} - initialValue={content} - height={getSize(height)} - initialEditType="markdown" - toolbarItems={toolbarItems} - extendedAutolinks={true} - plugins={[[codeSyntaxHighlight, { highlighter: Prism }]]} - hooks={{ - addImageBlobHook: uploadImage, - }} - /> - )} + { + editorContentRef.current = element; + }} + initialValue={content} + height={getSize(height)} + initialEditType="markdown" + hideModeSwitch={true} + toolbarItems={toolbarItems} + extendedAutolinks={true} + previewStyle={'tab'} + plugins={[[codeSyntaxHighlight, { highlighter: Prism }]]} + hooks={{ + addImageBlobHook: uploadImage, + }} + />
); }; diff --git a/frontend/src/components/Editor/Sidebar.tsx b/frontend/src/components/Editor/Sidebar.tsx deleted file mode 100644 index a10f98dc6..000000000 --- a/frontend/src/components/Editor/Sidebar.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/** @jsxImportSource @emotion/react */ - -import CreatableSelectBox from '../CreatableSelectBox/CreatableSelectBox'; -import { COLOR } from '../../enumerations/color'; -import SelectBox from '../Controls/SelectBox'; -import { PLACEHOLDER } from '../../constants'; -import { Mission, Session, Tag } from '../../models/Studylogs'; -import styled from '@emotion/styled'; -import { useGetMySessions, useMissions, useTags } from '../../hooks/queries/filters'; -import { getRowGapStyle } from '../../styles/layout.styles'; -import { useContext } from 'react'; -import { UserContext } from '../../contexts/UserProvider'; -import { css } from '@emotion/react'; - -interface SidebarProps { - selectedSessionId: Session['sessionId'] | null; - selectedMissionId: Mission['id'] | null; - selectedTagList: Tag[]; - onSelectSession: (session: { value: string; label: string }) => void; - onSelectMission: (mission: { value: string; label: string }) => void; - onSelectTag: (tags: Tag[], actionMeta: { option: { label: string } }) => void; -} - -const SidebarWrapper = styled.aside` - width: 24rem; - padding: 1rem; - background-color: white; - border: 1px solid ${COLOR.LIGHT_GRAY_100}; - border-radius: 2rem; -`; - -const FilterTitle = styled.h3` - margin-bottom: 10px; - padding-bottom: 2px; - border-bottom: 1px solid ${COLOR.DARK_GRAY_500}; - font-size: 1.8rem; - font-weight: bold; - line-height: 1.5; -`; - -const Sidebar = ({ - selectedSessionId, - selectedMissionId, - selectedTagList, - onSelectMission, - onSelectSession, - onSelectTag, - }: SidebarProps) => { - const { data: missions = [] } = useMissions(); - const { data: tags = [] } = useTags(); - const { data: sessions = [] } = useGetMySessions(); - 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 sessionOptions = sessions.map(({ sessionId, name }) => ({ - value: `${sessionId}`, - label: `${name}`, - })); - - const selectedSession = sessions.find(({ sessionId }) => sessionId === selectedSessionId); - const selectedMission = missions.find(({ id }) => id === selectedMissionId); - - return ( - -
    -
  • - session -
    - -
    -
  • -
  • - mission -
    - -
    -
  • -
  • - tags -
    - ({ value: name, label: `#${name}` }))} - /> -
    -
  • -
-
- ); -}; - -export default Sidebar; diff --git a/frontend/src/components/Editor/StudylogEditor.tsx b/frontend/src/components/Editor/StudylogEditor.tsx deleted file mode 100644 index d01595d2e..000000000 --- a/frontend/src/components/Editor/StudylogEditor.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/** @jsxImportSource @emotion/react */ - -import { css } from '@emotion/react'; -import { ChangeEventHandler, FormEventHandler, MouseEventHandler, MutableRefObject } from 'react'; -import { COLOR } from '../../constants'; -import { Tag } from '../../models/Studylogs'; -import { getFlexStyle } from '../../styles/flex.styles'; -import Editor from './Editor'; -import Sidebar from './Sidebar'; - -const SubmitButtonStyle = css` - width: 100%; - background-color: ${COLOR.LIGHT_BLUE_300}; - padding: 1rem 0; - border-radius: 1.6rem; - - :hover { - background-color: ${COLOR.LIGHT_BLUE_500}; - } -`; - -const TempSaveButtonStyle = css` - width: 100%; - background-color: ${COLOR.LIGHT_GRAY_400}; - padding: 1rem 0; - border-radius: 1.6rem; - - :hover { - background-color: ${COLOR.LIGHT_GRAY_600}; - } -`; - -type SelectOption = { value: string; label: string }; - -interface StudylogEditorProps { - title: string; - contentRef: MutableRefObject; - selectedMissionId?: number | null; - selectedSessionId?: number | null; - selectedTags?: Tag[]; - content?: string | null; - - onChangeTitle: ChangeEventHandler; - onSelectMission: (mission: SelectOption) => void; - onSelectSession: (session: SelectOption) => void; - onSelectTag: (tags: Tag[], actionMeta: { option: { label: string } }) => void; - onSubmit?: FormEventHandler; - onTempSave?: MouseEventHandler; -} - -const StudylogEditor = ({ - title, - selectedMissionId = null, - selectedSessionId = null, - selectedTags = [], - contentRef, - content, - onChangeTitle, - onSelectMission, - onSelectSession, - onSelectTag, - onSubmit, - onTempSave, -}: StudylogEditorProps): JSX.Element => { - return ( -
-
-
- -
- {onTempSave && ( - - )} - -
-
- -
-
- ); -}; - -export default StudylogEditor; diff --git a/frontend/src/components/Introduction/EditIntroduction.tsx b/frontend/src/components/Introduction/EditIntroduction.tsx index 3be739be9..a20632ab6 100644 --- a/frontend/src/components/Introduction/EditIntroduction.tsx +++ b/frontend/src/components/Introduction/EditIntroduction.tsx @@ -37,11 +37,11 @@ const EditIntroduction = ({ }: EditIntroductionProps) => { return (
-

자기소개 수정

diff --git a/frontend/src/components/Introduction/Introduction.styles.ts b/frontend/src/components/Introduction/Introduction.styles.ts index 0fd0f4158..81834bf9f 100644 --- a/frontend/src/components/Introduction/Introduction.styles.ts +++ b/frontend/src/components/Introduction/Introduction.styles.ts @@ -11,7 +11,7 @@ export const WrapperStyle = css` background-color: ${COLOR.WHITE}; border: 1px solid ${COLOR.LIGHT_GRAY_200}; - border-radius: 2rem; + border-radius: 8px; `; export const EditButtonStyle = css` @@ -26,7 +26,7 @@ export const EditButtonStyle = css` background-color: ${COLOR.LIGHT_GRAY_100}; border: 1px solid ${COLOR.LIGHT_GRAY_200}; - border-radius: 2rem; + border-radius: 2rem;border-radius: 2rem; font-size: 1.4rem; color: ${COLOR.BLACK_800}; @@ -47,7 +47,7 @@ export const EditorWrapperStyle = css` font-size: 2rem; background-color: ${COLOR.LIGHT_BLUE_200}; - border-radius: inherit; + border-radius: 8px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } @@ -55,11 +55,14 @@ export const EditorWrapperStyle = css` export const EditorStyle = css` .toastui-editor-defaultUI { - border: 0; + //border: 1px solid ${COLOR.LIGHT_GRAY_200}; + border-radius: 8px; + overflow: hidden; + margin: 2rem; } .toastui-editor-toolbar { - background-color: ${COLOR.LIGHT_BLUE_200}; + background-color: ${COLOR.LIGHT_GRAY_50}; } .toastui-editor-md-tab-container, @@ -77,6 +80,15 @@ export const EditorStyle = css` .toastui-editor-main-container { background-color: ${COLOR.WHITE}; + border-width: 0; + } + + .toastui-editor-main { + } + + .toastui-editor-defaultUI-toolbar button { + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); + background-color: transparent; } `; diff --git a/frontend/src/components/Items/StudylogItem.styles.ts b/frontend/src/components/Items/StudylogItem.styles.ts index 00eae82f3..ea6de06ef 100644 --- a/frontend/src/components/Items/StudylogItem.styles.ts +++ b/frontend/src/components/Items/StudylogItem.styles.ts @@ -29,9 +29,6 @@ export const DescriptionStyle = css` 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; @@ -53,16 +50,19 @@ export const MissionStyle = css` `; export const TagListStyle = css` - font-size: 1.2rem; + font-size: 1.4rem; color: ${COLOR.LIGHT_GRAY_900}; margin-top: auto; + margin-bottom: 0.5rem; `; export const ProfileChipLocationStyle = css` flex-shrink: 0; - margin-left: 1rem; + border: 1px solid ${COLOR.WHITE}; + flex-direction: column; + padding: 0; &:hover { - background-color: ${COLOR.LIGHT_BLUE_100}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } `; diff --git a/frontend/src/components/Items/StudylogItem.tsx b/frontend/src/components/Items/StudylogItem.tsx index b2bdceeeb..a9c6d273d 100644 --- a/frontend/src/components/Items/StudylogItem.tsx +++ b/frontend/src/components/Items/StudylogItem.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/react'; import { Card, ProfileChip } from '..'; -import ViewCount from '../ViewCount/ViewCount'; +import ViewCount from '../Count/ViewCount'; import { Studylog } from '../../models/Studylogs'; import { COLOR } from '../../enumerations/color'; @@ -14,8 +14,19 @@ import { TagListStyle, ProfileChipLocationStyle, } from './StudylogItem.styles'; -import { AlignItemsEndStyle, FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; -import CommentCount from '../CommentCount/CommentCount'; +import { + AlignItemsCenterStyle, + AlignItemsEndStyle, + FlexColumnStyle, FlexRowStyle, + FlexStyle, + getFlexStyle, + JustifyContentSpaceBtwStyle, +} from '../../styles/flex.styles'; +import CommentCount from '../Count/CommentCount'; +import Like from '../Reaction/Like'; +import LikeCount from '../Count/LikeCount'; +import * as Styled from '../Comment/Comment.style'; +import ProfileChipMax from "../ProfileChipMax/ProfileChipMax"; interface Props { studylog: Studylog; @@ -24,41 +35,61 @@ interface Props { } const StudylogItem = ({ studylog, onClick, onProfileClick }: Props) => { - const { author, title, tags, read: isRead, viewCount, session, commentCount } = studylog; + const { + author, + title, + tags, + read: isRead, + viewCount, + mission, + createdAt, + commentCount, + likesCount, + } = studylog; return ( - +
-
-

{session?.name}

-

{title}

+
+
+
+

{mission?.name}

+
+ {new Date(createdAt).toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} +
+
+ +

{title}

+
    {tags?.map(({ id, name }) => ( {`#${name} `} ))}
+
+ + + +
-
- + {author.nickname} - - - +
diff --git a/frontend/src/components/ProfileChip/ProfilChip.styles.ts b/frontend/src/components/ProfileChip/ProfilChip.styles.ts index eb5f7ab3d..86c3e1346 100644 --- a/frontend/src/components/ProfileChip/ProfilChip.styles.ts +++ b/frontend/src/components/ProfileChip/ProfilChip.styles.ts @@ -16,22 +16,23 @@ const Container = styled.div<{ css?: SerializedStyles }>` `; const Image = styled.img` - width: 3.8rem; - height: 3.8rem; - border-radius: 1.3rem; + width: 50px; + height: 50px; + border-radius: 12px; + padding: 0.5rem; + //border: 1px solid ${COLOR.LIGHT_GRAY_100}; `; const Nickname = styled.span` flex-shrink: 0; - + padding: 0 5px; max-width: 100px; - + white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - margin-left: 0.8rem; font-size: 1.6rem; line-height: 1.5; color: ${COLOR.DARK_GRAY_900}; diff --git a/frontend/src/components/ProfileChipMax/ProfilChipMax.styles.ts b/frontend/src/components/ProfileChipMax/ProfilChipMax.styles.ts new file mode 100644 index 000000000..6d12647a5 --- /dev/null +++ b/frontend/src/components/ProfileChipMax/ProfilChipMax.styles.ts @@ -0,0 +1,40 @@ +import { SerializedStyles } from '@emotion/serialize'; +import styled from '@emotion/styled'; +import COLOR from '../../constants/color'; + +const Container = styled.div<{ css?: SerializedStyles }>` + height: 4.8rem; + border: 1px solid ${COLOR.LIGHT_GRAY_400}; + border-radius: 1.6rem; + background-color: ${COLOR.WHITE}; + padding: 0.5rem; + box-sizing: border-box; + display: inline-flex; + align-items: center; + + ${({ css }) => css} +`; + +const Image = styled.img` + width: 100px; + height: 100px; + border-radius: 12px; + //border: 1px solid ${COLOR.LIGHT_GRAY_100}; +`; + +const Nickname = styled.span` + flex-shrink: 0; + + padding: 0 5px; + max-width: 100px; + + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + font-size: 1.6rem; + line-height: 1.5; + color: ${COLOR.DARK_GRAY_900}; +`; + +export { Container, Image, Nickname }; diff --git a/frontend/src/components/ProfileChipMax/ProfileChipMax.tsx b/frontend/src/components/ProfileChipMax/ProfileChipMax.tsx new file mode 100644 index 000000000..5d2b4b101 --- /dev/null +++ b/frontend/src/components/ProfileChipMax/ProfileChipMax.tsx @@ -0,0 +1,38 @@ +import { SerializedStyles } from '@emotion/react'; +import PropTypes from 'prop-types'; +import { PropsWithChildren } from 'react'; + +import NoProfileImage from '../../assets/images/no-profile-image.png'; +import { Container, Image, Nickname } from './ProfilChipMax.styles'; + +interface Props { + imageSrc: string; + css?: SerializedStyles; + // (event?: MouseEvent) => void와 MouseEventHandler가 동시에 들어올 수 있는 구조라 방법을 찾지 못해 any로 하였습니다. + onClick: any; + cssProps: SerializedStyles; +} + +const ProfileChipMax = ({ imageSrc, children, css, cssProps, onClick }: PropsWithChildren) => { + return ( + + {`${children} + {children} + + ); +}; + +ProfileChipMax.propTypes = { + imageSrc: PropTypes.string, + children: PropTypes.string, + css: PropTypes.object, + cssProps: PropTypes.object, + onClick: PropTypes.func, +}; + +ProfileChipMax.defaultProps = { + children: 'nickname', + imageSrc: NoProfileImage, +}; + +export default ProfileChipMax; diff --git a/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.styles.ts b/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.styles.ts index 7343ff970..eb3c3e1a0 100644 --- a/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.styles.ts +++ b/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.styles.ts @@ -46,47 +46,48 @@ const Image = styled.img` height: 20rem; border-top-left-radius: 1.6rem; border-top-right-radius: 1.6rem; + padding: 2rem; `; const Nickname = styled.div` display: flex; align-items: center; - font-size: 1.8rem; + font-size: 1.5rem; border-top: none; - padding-left: 1.2rem; + color: ${COLOR.LIGHT_GRAY_900}; +`; + +const UpdateButton = styled.button` + border-radius: 8px; `; const RssFeedUrl = styled.div` display: flex; align-items: center; - font-size: 1rem; + font-size: 1.5rem; border-top: none; - padding-left: 1.2rem; `; const Role = styled.div` margin-bottom: 1rem; - padding-left: 1.2rem; font-size: 1.2rem; color: ${COLOR.LIGHT_GRAY_900}; `; const RoleButton = styled.button` - margin-top: .3rem; + margin-top: 0.3rem; font-size: 1.2rem; color: ${COLOR.DARK_GRAY_700}; `; const RoleLabel = styled.div` - margin-top: 1rem; - padding-left: 1.2rem; + margin-top: 1.5rem; font-size: 1.5rem; color: ${COLOR.DARK_GRAY_700}; `; const RssLinkLabel = styled.div` margin-top: 1rem; - padding-left: 1.2rem; font-size: 1.2rem; color: ${COLOR.LIGHT_GRAY_900}; `; @@ -154,10 +155,8 @@ const MenuButton = styled.button` `; const NicknameInput = styled.input` - margin: 0.5rem 1.2rem; - margin-right: 0; padding: 0.2rem 0.5rem; - font-size: 1.6rem; + font-size: 1.5rem; outline: none; border-radius: 0.5rem; border: 1px solid ${COLOR.LIGHT_GRAY_900}; @@ -172,8 +171,6 @@ const NicknameWrapper = styled.div` `; const RssFeedInput = styled.input` - margin: 0.5rem 1.2rem; - margin-right: 0; padding: 0.2rem 0.5rem; font-size: 1.6rem; outline: none; @@ -209,6 +206,7 @@ export { Nickname, RssFeedUrl, Role, + UpdateButton, RoleButton, RoleLabel, RssLinkLabel, diff --git a/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.tsx b/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.tsx index 1a4edd543..9ff85efa6 100644 --- a/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.tsx +++ b/frontend/src/components/ProfilePageSideBar/ProfilePageSideBar.tsx @@ -1,8 +1,9 @@ +/** @jsxImportSource @emotion/react */ + import { useState, useContext } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { useQuery } from 'react-query'; import { UserContext } from '../../contexts/UserProvider'; -import BadgeList from '../Badge/BadgeList'; import getMenuList from './getMenuList'; import { Button, BUTTON_SIZE } from '..'; import { BASE_URL } from '../../configs/environment'; @@ -26,7 +27,10 @@ import { NicknameInput, RssFeedWrapper, RssFeedInput, + UpdateButton, } from './ProfilePageSideBar.styles'; +import { css } from '@emotion/react'; +import { FlexStyle, JustifyContentEndStyle } from '../../styles/flex.styles'; interface ProfilePageSideBarProps { menu: string; @@ -68,7 +72,7 @@ const ProfilePageSideBar = ({ menu }: ProfilePageSideBarProps) => { return badges; }); - const { mutate: editProfile } = usePutProfileMutation( + const { mutate: editNickname } = usePutProfileMutation( { user, nickname, @@ -78,6 +82,19 @@ const ProfilePageSideBar = ({ menu }: ProfilePageSideBarProps) => { { onSuccess: () => { setIsProfileEditing(false); + }, + } + ); + + const { mutate: editRssUrl } = usePutProfileMutation( + { + user, + nickname, + rssFeedUrl, + accessToken, + }, + { + onSuccess: () => { setIsRssFeedEditing(false); }, } @@ -88,11 +105,11 @@ const ProfilePageSideBar = ({ menu }: ProfilePageSideBarProps) => { history.push(path); }; -const [showAll, setShowAll] = useState(false); // 상태 관리: 전체 표시 여부 + const [showAll, setShowAll] = useState(false); // 상태 관리: 전체 표시 여부 -const toggleShowAll = () => setShowAll((prev) => !prev); // 상태 토글 함수 + const toggleShowAll = () => setShowAll((prev) => !prev); // 상태 토글 함수 -const displayedGroups = showAll + const displayedGroups = showAll ? user?.organizationGroups // 전체 항목 표시 : user?.organizationGroups?.slice(0, 2); // 첫 2개만 표시 @@ -100,67 +117,70 @@ const displayedGroups = showAll 프로필 이미지 +
소속 - {displayedGroups?.map((group, index) => ( -
{group}
- ))} - - {user?.organizationGroups?.length > 2 && ( - - {showAll ? "가리기" : "더보기"} - - )} + {displayedGroups && displayedGroups.length === 0 &&
소속된 그룹이 없습니다.
} + {displayedGroups?.map((group, index) => ( +
{group}
+ ))} + + {user?.organizationGroups?.length > 2 && ( + {showAll ? '가리기' : '더보기'} + )}
- - {isProfileEditing ? ( - setNickname(target.value)} - /> - ) : ( - {nickname} - )} - {isOwner && ( - - )} - - RSS Link - - {isRssFeedEditing ? ( - setRssFeedUrl(target.value)} - /> - ) : ( - {rssFeedUrl} - )} - {isOwner && ( - - )} - + 닉네임 + + {isProfileEditing ? ( + setNickname(target.value)} + /> + ) : ( + {nickname} + )} + {isOwner && ( + { + isProfileEditing ? editNickname() : setIsProfileEditing(true); + }} + > + {isProfileEditing ? '완료' : '수정'} + + )} + + RSS Link + + {isRssFeedEditing ? ( + setRssFeedUrl(target.value)} + /> + ) : ( + {rssFeedUrl} + )} + {isOwner && ( + { + isRssFeedEditing ? editRssUrl() : setIsRssFeedEditing(true); + }} + > + {isRssFeedEditing ? '완료' : '수정'} + + )} + +
{/*{isLoading ? <> : }*/} diff --git a/frontend/src/components/Reaction/Like.styles.ts b/frontend/src/components/Reaction/Like.styles.ts index cdc00c731..3be06a4ae 100644 --- a/frontend/src/components/Reaction/Like.styles.ts +++ b/frontend/src/components/Reaction/Like.styles.ts @@ -2,17 +2,18 @@ import { css } from '@emotion/react'; import { COLOR } from '../../constants'; export const LikeIconStyle = css` - flex-direction: column; padding: 0; width: fit-content; font-size: 1.4rem; + margin-left: 1rem; + margin-right: 1rem; + height: inherit; background-color: transparent; - color: ${COLOR.BLACK_800}; + color: ${COLOR.DARK_GRAY_400}; & > img { margin-right: 0; width: 2.4rem; - height: 2.4rem; } `; diff --git a/frontend/src/components/Reaction/Like.tsx b/frontend/src/components/Reaction/Like.tsx index 8674733eb..0104bf066 100644 --- a/frontend/src/components/Reaction/Like.tsx +++ b/frontend/src/components/Reaction/Like.tsx @@ -1,8 +1,8 @@ import { MouseEventHandler } from 'react'; -import { Button, BUTTON_SIZE } from '..'; +import {Button, BUTTON_SIZE} from '..'; -import likedIcon from '../../assets/images/heart-filled.svg'; -import unLikeIcon from '../../assets/images/heart.svg'; +import { ReactComponent as LikeIcon } from '../../assets/images/heart-filled.svg'; +import { ReactComponent as UnLikeIcon } from '../../assets/images/heart.svg'; import { LikeIconStyle } from './Like.styles'; interface Props { @@ -12,18 +12,11 @@ interface Props { } const Like = ({ liked, likesCount, onClick }: Props) => { - const likeIcon = liked ? likedIcon : unLikeIcon; const likeIconAlt = liked ? '좋아요' : '좋아요 취소'; return ( - ); diff --git a/frontend/src/components/Reaction/Scrap.styles.ts b/frontend/src/components/Reaction/Scrap.styles.ts index 976533905..64ab6fc2d 100644 --- a/frontend/src/components/Reaction/Scrap.styles.ts +++ b/frontend/src/components/Reaction/Scrap.styles.ts @@ -6,13 +6,13 @@ export const DefaultScrapButtonStyle = css` padding: 0; width: fit-content; font-size: 1.4rem; + height: inherit; background-color: transparent; - color: ${COLOR.BLACK_800}; + color: ${COLOR.DARK_GRAY_400}; & > img { margin-right: 0; width: 2.4rem; - height: 2.4rem; } `; diff --git a/frontend/src/components/Reaction/Scrap.tsx b/frontend/src/components/Reaction/Scrap.tsx index 9a30aa9ec..aef4b4b42 100644 --- a/frontend/src/components/Reaction/Scrap.tsx +++ b/frontend/src/components/Reaction/Scrap.tsx @@ -1,8 +1,8 @@ import { MouseEventHandler } from 'react'; import { Button, BUTTON_SIZE } from '..'; -import scrappedIcon from '../../assets/images/scrap_filled.svg'; -import unScrapIcon from '../../assets/images/scrap.svg'; +import { ReactComponent as ScrapIcon } from '../../assets/images/scrap_filled.svg'; +import { ReactComponent as UnScrapIcon } from '../../assets/images/scrap.svg'; import { DefaultScrapButtonStyle } from './Scrap.styles'; import { SerializedStyles } from '@emotion/react'; @@ -13,18 +13,21 @@ interface Props { } const Scrap = ({ scrap, onClick, cssProps }: Props) => { - const scrapIcon = scrap ? scrappedIcon : unScrapIcon; const scrapIconAlt = scrap ? '스크랩 취소' : '스크랩'; return ( ); }; diff --git a/frontend/src/components/StudylogEditor/QuestionAnswerStyles.ts b/frontend/src/components/StudylogEditor/QuestionAnswerStyles.ts new file mode 100644 index 000000000..1e341f6dd --- /dev/null +++ b/frontend/src/components/StudylogEditor/QuestionAnswerStyles.ts @@ -0,0 +1,48 @@ +import styled from '@emotion/styled'; +import { COLOR } from '../../constants'; +import { css } from '@emotion/react'; + +export const MainContainer = styled.div` + gap: 1.5rem; +`; + +export const AnswerTextArea = styled.textarea` + width: 100%; + min-height: 80px; + padding: 0.5rem; + border-radius: 4px; + border: 1px solid #ccc; + font-size: 1.2rem; +`; + +export const NoQuestionMessage = styled.div` + min-height: 20rem; + align-content: center; + text-align: center; + font-size: 1.2rem; + color: gray; +`; + +export const AccordionHeader = styled.h2` + padding: 1rem; +`; + +export const AccordionButton = styled.button` + color: ${COLOR.BLACK_700}; + font-size: 1.2rem; + outline: none; + + :not(.collapsed) { + background-color: ${COLOR.WHITE}; + color: ${COLOR.BLACK_700}; + } + :focus { + box-shadow: none; + } +`; + +export const AnswerBody = styled.div` + padding: 1rem 2rem 2rem 2rem; + font-size: 1.2rem; + color: ${COLOR.BLACK_700}; +`; diff --git a/frontend/src/components/StudylogEditor/QuestionAnswers.tsx b/frontend/src/components/StudylogEditor/QuestionAnswers.tsx new file mode 100644 index 000000000..98ca47718 --- /dev/null +++ b/frontend/src/components/StudylogEditor/QuestionAnswers.tsx @@ -0,0 +1,139 @@ +import React, { MutableRefObject, useEffect, useState } from 'react'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.bundle.min.js'; +import { + AccordionHeader, + AccordionButton, + AnswerTextArea, + MainContainer, + NoQuestionMessage, + AnswerBody, +} from './QuestionAnswerStyles'; +import { Answer, Question, QuestionAnswer } from '../../models/Studylogs'; + +interface EditableProps { + editable: true; + questions: Question[]; + editorAnswerRef: MutableRefObject; +} + +interface NonEditableProps { + editable: false; + questionAnswers: QuestionAnswer[]; +} + +type QuestionAnswerProps = EditableProps | NonEditableProps; + +const QuestionAnswers: React.FC = (props) => { + const [answers, setAnswers] = useState([]); + + useEffect(() => { + if (props.editable) { + const initialAnswers = props.questions.map((question) => { + return ( + props.editorAnswerRef.current.find((answer) => answer.questionId === question.id) || { + questionId: question.id, + answerContent: '', + } + ); + }); + setAnswers(initialAnswers); + } + }, [props]); + + const handleAnswerChange = (questionId: number, answerContent: string) => { + if (!props.editable) return; // editable이 false면 변경 불가 + + const updatedAnswers = answers.map((answer) => + answer.questionId === questionId ? { ...answer, answerContent } : answer + ); + setAnswers(updatedAnswers); + + if (props.editable) { + const existingAnswerIndex = props.editorAnswerRef.current.findIndex( + (answer) => answer.questionId === questionId + ); + + if (existingAnswerIndex !== -1) { + props.editorAnswerRef.current[existingAnswerIndex].answerContent = answerContent; + } else { + props.editorAnswerRef.current.push({ questionId, answerContent }); + } + } + }; + + return ( + +
+ {props.editable ? ( + props.questions.length === 0 ? ( + '질문이 없습니다. 질문을 추가해주세요.' + ) : ( + props.questions.map((question) => { + const answer = answers.find((a) => a.questionId === question.id); + return ( +
+ + + +
+ + handleAnswerChange(question.id, e.target.value)} + placeholder="답변을 입력하세요..." + /> + +
+
+ ); + }) + ) + ) : props.questionAnswers.length === 0 ? ( + '질문이 없습니다. 질문을 추가해주세요.' + ) : ( + props.questionAnswers.map((qa) => ( +
+ + + +
+ +
{qa.answerContent || '답변이 없습니다.'}
+
+
+
+ )) + )} +
+
+ ); +}; + +export default QuestionAnswers; diff --git a/frontend/src/components/StudylogEditor/Sidebar.tsx b/frontend/src/components/StudylogEditor/Sidebar.tsx new file mode 100644 index 000000000..307c03727 --- /dev/null +++ b/frontend/src/components/StudylogEditor/Sidebar.tsx @@ -0,0 +1,200 @@ +/** @jsxImportSource @emotion/react */ + +import CreatableSelectBox from '../CreatableSelectBox/CreatableSelectBox'; +import { COLOR } from '../../enumerations/color'; +import SelectBox from '../Controls/SelectBox'; +import { ERROR_MESSAGE, PLACEHOLDER } from '../../constants'; +import { Answer, Mission, Question, Session, StudylogForm, Tag } from '../../models/Studylogs'; +import styled from '@emotion/styled'; +import { useGetMySessions, useMissions, useTags } from '../../hooks/queries/filters'; +import { getRowGapStyle } from '../../styles/layout.styles'; +import { MutableRefObject, useContext } from 'react'; +import { UserContext } from '../../contexts/UserProvider'; +import { css } from '@emotion/react'; +import { fetchQuestionsByMissionId } from '../../apis/questions'; + +interface SidebarProps { + mode: 'create' | 'edit'; + questions: Question[]; + setQuestions: React.Dispatch>; + editorAnswersRef: MutableRefObject; + studylogContent: StudylogForm; + setStudylogContent: React.Dispatch>; +} + +const SidebarWrapper = styled.aside` + width: 24rem; + height: 100%; + padding: 2rem; +`; + +const FilterTitle = styled.h3` + margin-bottom: 10px; + padding-bottom: 2px; + line-height: 1.5; + font-weight: 600; + font-size: 1.5rem; + color: ${COLOR.DARK_GRAY_600}; +`; + +type SelectOption = { value: string; label: string }; + +const Sidebar = ({ + mode, + questions, + setQuestions, + editorAnswersRef, + studylogContent, + setStudylogContent, +}: SidebarProps) => { + const { data: missions = [] } = useMissions(); + const { data: tags = [] } = useTags(); + const { data: sessions = [] } = useGetMySessions(); + const { + user: { username }, + } = useContext(UserContext); + const tagOptions = tags.map(({ name }) => ({ value: name, label: `#${name}` })); + const missionOptions = missions.map(({ id, name, session }) => ({ + value: `${id}`, + label: `${name}`, + })); + const sessionOptions = sessions.map(({ sessionId, name }) => ({ + value: `${sessionId}`, + label: `${name}`, + })); + + const selectedSession = sessions.find(({ sessionId }) => sessionId === studylogContent.sessionId); + const selectedMission = missions.find(({ id }) => id === studylogContent.missionId); + + const onSelectMission = async (mission: SelectOption | null) => { + if (mode === 'edit') { + alert('수정화면에서는 미션을 수정할 수 없습니다.'); + return; + } + + if (questions.length > 0) { + const confirmed = window.confirm('입력한 답변이 초기화 됩니다. 계속 진행하시겠습니까?'); + if (!confirmed) return; + } + + if (!mission) { + editorAnswersRef.current = []; // 기존 답변 초기화 + setStudylogContent((prev) => ({ ...prev, missionId: null, answers: [] })); + setQuestions([]); + return; + } + + setStudylogContent((prev) => ({ ...prev, missionId: Number(mission.value) })); + + try { + const response = await fetchQuestionsByMissionId(Number(mission.value)); + const { questions: fetchedQuestions } = response.data; + + setQuestions(fetchedQuestions); + + editorAnswersRef.current = fetchedQuestions.map((question) => ({ + questionId: question.id, + answerContent: '', + })); + } catch (error) { + console.error('Failed to fetch questions:', error); + alert(ERROR_MESSAGE.DEFAULT); + } + }; + + const onSelectSession = (session: SelectOption) => { + if (mode === 'edit') { + alert('수정화면에서는 세션을 수정할 수 없습니다.'); + return; + } + + setStudylogContent({ ...studylogContent, sessionId: Number(session.value) }); + }; + + const onSelectTag = (tags, actionMeta) => { + if (actionMeta.action === 'create-option') { + actionMeta.option.label = '#' + actionMeta.option.label; + } + + setStudylogContent({ + ...studylogContent, + tags: tags.map(({ value }) => ({ name: value.replace(/#/, '') })), + }); + }; + + return ( + +
    +
  • + Session +
    + +
    +
  • +
  • + Mission +
    + +
    +
  • +
  • + Tag +
    + ({ value: name, label: `#${name}` }))} + /> +
    +
  • +
+
+ ); +}; + +export default Sidebar; diff --git a/frontend/src/components/StudylogEditor/StudylogEditor.tsx b/frontend/src/components/StudylogEditor/StudylogEditor.tsx new file mode 100644 index 000000000..de427619d --- /dev/null +++ b/frontend/src/components/StudylogEditor/StudylogEditor.tsx @@ -0,0 +1,371 @@ +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; +import { + ChangeEventHandler, + FormEventHandler, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { MainContentStyle } from '../../PageRouter'; + +import { ALERT_MESSAGE, COLOR, ERROR_MESSAGE, PATH } from '../../constants'; + +import { Answer, Question, Studylog, StudylogForm } from '../../models/Studylogs'; +import { useMutation, useQuery, UseQueryResult } from 'react-query'; +import LOCAL_STORAGE_KEY from '../../constants/localStorage'; +import { CONFIRM_MESSAGE, SUCCESS_MESSAGE } from '../../constants/message'; +import { useHistory, useParams } from 'react-router-dom'; +import { + requestEditStudylog, + requestGetStudylog, + requestPostStudylog, + ResponseError, +} from '../../apis/studylogs'; +import useBeforeunload from '../../hooks/useBeforeunload'; +import useTempSavedStudylog from '../../hooks/Studylog/useTempSavedStudylog'; +import { fetchQuestionsByMissionId } from '../../apis/questions'; +import { getFlexStyle } from '../../styles/flex.styles'; +import Editor from '../../components/Editor/Editor'; +import Sidebar from './Sidebar'; +import styled from '@emotion/styled'; +import QuestionAnswers from './QuestionAnswers'; +import { EditorStyle } from '../../components/Introduction/Introduction.styles'; +import { markdownStyle } from '../../styles/markdown.styles'; +import { EditorTitleStyle, EditorWrapperStyle } from '../../components/Editor/Editor.styles'; +import { Card, SectionName } from './styles'; +import { UserContext } from '../../contexts/UserProvider'; +import { AxiosError, AxiosResponse } from 'axios'; +import REACT_QUERY_KEY from '../../constants/reactQueryKey'; + +const SubmitButtonStyle = css` + width: 100%; + background-color: ${COLOR.LIGHT_BLUE_300}; + padding: 1rem 0; + border-radius: 1.6rem; + + :hover { + background-color: ${COLOR.LIGHT_BLUE_500}; + } +`; + +const TempSaveButtonStyle = css` + width: 100%; + background-color: ${COLOR.LIGHT_GRAY_400}; + padding: 1rem 0; + border-radius: 1.6rem; + + :hover { + background-color: ${COLOR.LIGHT_GRAY_600}; + } +`; + +const sidebarStyle = css` + display: flex; + flex-direction: column; + justify-content: space-between; + height: auto; + gap: 1.5rem; + margin-left: 1.5rem; + border-radius: 8px; + border: 1px solid ${COLOR.LIGHT_GRAY_50}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background-color: ${COLOR.WHITE}; +`; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 1.5rem; +`; + +interface StudylogEditorProps { + mode: 'create' | 'edit'; +} + +const StudylogEditor = ({ mode }: StudylogEditorProps): JSX.Element => { + const history = useHistory(); + const editorContentRef = useRef(null); + const editorAnswersRef = useRef([]); + const [questions, setQuestions] = useState([]); + + const { + tempSavedStudylog, + createTempSavedStudylog, + removeCachedTempSavedStudylog, + } = useTempSavedStudylog(); + + useBeforeunload(editorContentRef); + + const [studylogContent, setStudylogContent] = useState({ + title: '', + content: null, + missionId: null, + sessionId: null, + tags: [], + answers: [], + }); + + const { id } = useParams<{ id: string }>(); + + const onChangeTitle: ChangeEventHandler = (event) => { + setStudylogContent({ ...studylogContent, title: event.target.value }); + }; + + const onCreateStudylog: FormEventHandler = (event) => { + event.preventDefault(); + + const content = editorContentRef.current?.getInstance().getMarkdown() || ''; + const answers: Answer[] = Object.values(editorAnswersRef.current); + + if (studylogContent.title.length === 0) { + alert(ALERT_MESSAGE.NO_TITLE); + return; + } + + if (content.length === 0) { + alert(ALERT_MESSAGE.NO_CONTENT); + return; + } + + if (mode === 'edit') { + editStudylogRequest({ + ...studylogContent, + content, + answers, + }); + } else if (mode === 'create') { + createStudylogRequest({ + ...studylogContent, + content, + answers, + }); + } + }; + + const fetchQuestions = async (missionId: number, answers: Answer[] | null) => { + try { + const response = await fetchQuestionsByMissionId(missionId); + const { questions: fetchedQuestions } = response.data; + + if (!answers || answers.length === 0) { + return; + } + + setQuestions(fetchedQuestions); + + editorAnswersRef.current = answers.map((answer) => ({ + questionId: answer.questionId, + answerContent: answer.answerContent, + })); + } catch (error) { + console.error('Failed to fetch questions:', error); + alert(ERROR_MESSAGE.DEFAULT); + } + }; + + const { mutate: createStudylogRequest } = useMutation( + (data: StudylogForm) => + requestPostStudylog({ + accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) as string, + data, + }), + { + onSuccess: async () => { + removeCachedTempSavedStudylog(); + alert(SUCCESS_MESSAGE.CREATE_POST); + history.push(PATH.STUDYLOG); + }, + onError: (error: ResponseError) => { + alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.DEFAULT); + }, + } + ); + + const onTempSaveStudylog = () => { + const content = editorContentRef.current?.getInstance().getMarkdown() || ''; + const answers: Answer[] = editorAnswersRef.current; + + if (studylogContent.title.length === 0 && content.length === 0) { + alert(ALERT_MESSAGE.NO_TITLE_OR_CONTENT); + return; + } + + if (window.confirm(CONFIRM_MESSAGE.TEMP_SAVE_STUDYLOG)) { + createTempSavedStudylog({ + ...studylogContent, + content, + answers, + }); + } + }; + + const fetchStudylogRequest: UseQueryResult, AxiosError> = useQuery( + [REACT_QUERY_KEY.STUDYLOG, id], + () => requestGetStudylog({ id, accessToken }), + { + onSuccess: ({ data }) => { + setStudylogContent({ + title: data.title, + content: data.content, + missionId: data.mission?.id || null, + sessionId: data.session?.sessionId || null, + tags: data.tags, + answers: data.answers, + }); + + fetchQuestions(data.mission!!.id, data.answers); + }, + } + ); + + const { mutate: editStudylogRequest } = useMutation( + (data: StudylogForm) => + requestEditStudylog({ + id, + accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) as string, + data, + }), + { + onSuccess: async () => { + alert(SUCCESS_MESSAGE.EDIT_POST); + history.push(`${PATH.STUDYLOG}/${id}`); + }, + + onError: (error: ResponseError) => { + alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.FAIL_TO_EDIT_STUDYLOG); + }, + } + ); + + const { user } = useContext(UserContext); + const { accessToken, username } = user; + + useEffect(() => { + if (mode === 'edit') { + const author = fetchStudylogRequest.data?.data.author; + + if (author && username !== author.username) { + alert(ALERT_MESSAGE.CANNOT_EDIT_OTHERS); + history.push(`${PATH.STUDYLOG}/${id}`); + return; + } + + fetchStudylogRequest.refetch(); + } + }, [mode, username, id]); + + useEffect(() => { + if (mode !== 'create' || !tempSavedStudylog) { + return; + } + + const isTempSavedStudylogExist = Object.entries(tempSavedStudylog).some( + ([_, value]) => value !== null + ); + + if (isTempSavedStudylogExist) { + setStudylogContent({ + title: tempSavedStudylog.title ?? '', + content: tempSavedStudylog.content, + missionId: tempSavedStudylog.mission?.id ?? null, + sessionId: tempSavedStudylog.session?.sessionId ?? null, + tags: tempSavedStudylog.tags ?? [], + answers: tempSavedStudylog.answers ?? [], + }); + + if (tempSavedStudylog.mission) { + fetchQuestions(tempSavedStudylog.mission.id, tempSavedStudylog.answers); + } + } + }, [mode, tempSavedStudylog]); + + const onSubmit = mode === 'edit' ? onCreateStudylog : onCreateStudylog; + + const content = studylogContent.content ? studylogContent.content : ''; + + useEffect(() => { + if (editorContentRef.current && content) { + (editorContentRef.current as any).getInstance().setMarkdown(content); + } + }, [content]); + + return ( +
+
+
+ +
+ +
+ +
+ Content + {/* FIXME: 임시방편 editor에 상태 값을 초기값으로 넣는 법 찾기 */} + +
+ {questions && editorAnswersRef && ( + + Question + + + )} +
+
+ +
+ +
+
+
+ {mode === 'create' && ( + + )} + +
+
+
+ ); +}; + +export default StudylogEditor; diff --git a/frontend/src/components/StudylogEditor/styles.ts b/frontend/src/components/StudylogEditor/styles.ts new file mode 100644 index 000000000..268e911fe --- /dev/null +++ b/frontend/src/components/StudylogEditor/styles.ts @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; +import COLOR from '../../constants/color'; + +const Card = styled.div` + padding: 2.5rem; + background-color: ${COLOR.WHITE}; + border-radius: 8px; + border: 1px solid ${COLOR.LIGHT_GRAY_50}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const SectionName = styled.h3` + margin-top: 1rem; + margin-bottom: 2rem; + font-weight: 600; + font-size: 1.5rem; + color: ${COLOR.DARK_GRAY_600}; +`; + +export { Card, SectionName }; diff --git a/frontend/src/components/ViewCount/ViewCount.styles.ts b/frontend/src/components/ViewCount/ViewCount.styles.ts deleted file mode 100644 index 2a66862be..000000000 --- a/frontend/src/components/ViewCount/ViewCount.styles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SerializedStyles } from '@emotion/cache/node_modules/@emotion/utils'; -import styled from '@emotion/styled'; -import { COLOR } from '../../constants'; - -const Container = styled.div<{ css?: SerializedStyles }>` - flex-shrink: 0; - color: ${COLOR.LIGHT_GRAY_900}; - font-size: 1.4rem; - margin-top: 0.5rem; - - & > svg { - margin-right: 0.25rem; - } - - ${({ css }) => css}; -`; - -const Count = styled.span` - vertical-align: top; -`; - -export { Container, Count }; diff --git a/frontend/src/components/ViewCount/ViewCount.tsx b/frontend/src/components/ViewCount/ViewCount.tsx deleted file mode 100644 index acf23ba09..000000000 --- a/frontend/src/components/ViewCount/ViewCount.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SerializedStyles } from '@emotion/cache/node_modules/@emotion/utils'; -import { ReactComponent as ViewIcon } from '../../assets/images/view.svg'; -import { Container, Count } from './ViewCount.styles'; - -const ViewCount = ({ css, count }: { css?: SerializedStyles; count: number }) => ( - - - {count} - -); - -export default ViewCount; diff --git a/frontend/src/models/Studylogs.ts b/frontend/src/models/Studylogs.ts index 18fb9c1ca..f60e341d0 100644 --- a/frontend/src/models/Studylogs.ts +++ b/frontend/src/models/Studylogs.ts @@ -1,6 +1,5 @@ -import { Ability } from '../models/Ability'; - type Role = 'UNVALIDATED' | 'CREW' | 'COACH' | 'ADMIN'; + export interface Author { id: number; username: string; @@ -25,6 +24,23 @@ export interface Tag { name: string; } +export interface Question { + id: number; + content: string; +} + +export interface Answer { + questionId: number; + answerContent: string; +} + +export interface QuestionAnswer { + id: number; + answerContent: string; + questionId: number; + questionContent: string; +} + // TODO: read, scrap => isRead, isScrapped로 변경 export interface Studylog { id: number; @@ -34,6 +50,7 @@ export interface Studylog { session?: Session; title: string; tags: Tag[]; + answers: Answer[]; createdAt: Date; updatedAt?: Date; read: boolean; @@ -47,12 +64,13 @@ export interface Studylog { export type TempSavedStudyLog = Pick< Studylog, - 'id' | 'author' | 'title' | 'content' | 'session' | 'mission' | 'tags' + 'id' | 'author' | 'title' | 'content' | 'session' | 'mission' | 'tags' | 'answers' >; export type TempSavedStudyLogForm = Pick & { missionId: number | null; sessionId: number | null; + answers: Answer[]; }; export interface StudyLogList { @@ -66,6 +84,7 @@ export type StudylogForm = Pick & { content: string | null; missionId: number | null; sessionId: number | null; + answers: Answer[]; }; export const studyLogCategory = { diff --git a/frontend/src/pages/EditEssayAnswerPage/index.tsx b/frontend/src/pages/EditEssayAnswerPage/index.tsx index b3b7b18b7..e6697e141 100644 --- a/frontend/src/pages/EditEssayAnswerPage/index.tsx +++ b/frontend/src/pages/EditEssayAnswerPage/index.tsx @@ -21,7 +21,7 @@ const EditEssayAnswerPage = () => { const { user: { username } } = useContext(UserContext); const [quizTitle, setQuizTitle] = useState(''); - const [answer, setAnswer] = useState(null); + const [answer, setAnswer] = useState(''); const editorContentRef = useRef(null); diff --git a/frontend/src/pages/EditStudylogPage/index.tsx b/frontend/src/pages/EditStudylogPage/index.tsx index 9ce48001c..ec549587b 100644 --- a/frontend/src/pages/EditStudylogPage/index.tsx +++ b/frontend/src/pages/EditStudylogPage/index.tsx @@ -1,170 +1,7 @@ -/** @jsxImportSource @emotion/react */ +import StudylogEditor from '../../components/StudylogEditor/StudylogEditor'; -import { - ChangeEventHandler, - FormEventHandler, - useContext, - useEffect, - useRef, - useState, -} from 'react'; -import { useHistory, useParams } from 'react-router-dom'; - -import { ALERT_MESSAGE, ERROR_MESSAGE, PATH } from '../../constants'; - -import { MainContentStyle } from '../../PageRouter'; -import { UserContext } from '../../contexts/UserProvider'; - -import StudylogEditor from '../../components/Editor/StudylogEditor'; -import { Studylog, StudylogForm } from '../../models/Studylogs'; -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 LOCAL_STORAGE_KEY from '../../constants/localStorage'; -import { SUCCESS_MESSAGE } from '../../constants/message'; -import { ResponseError } from '../../apis/studylogs'; - -type SelectOption = { value: string; label: string }; - -// 나중에 학습로그 작성 페이지와 같아질 수 있음(임시저장) const EditStudylogPage = () => { - const history = useHistory(); - - const editorContentRef = useRef(null); - - const [studylogContent, setStudylogContent] = useState({ - title: '', - content: null, - missionId: null, - sessionId: null, - tags: [], - }); - - const { user } = useContext(UserContext); - const { accessToken, username } = user; - - const { id } = useParams<{ id: string }>(); - - const fetchStudylogRequest: UseQueryResult, AxiosError> = useQuery( - [REACT_QUERY_KEY.STUDYLOG, id], - () => requestGetStudylog({ id, accessToken }), - { - onSuccess: ({ data }) => { - setStudylogContent({ - title: data.title, - content: data.content, - missionId: data.mission?.id || null, - sessionId: data.session?.sessionId || null, - tags: data.tags, - }); - }, - } - ); - - const onChangeTitle: ChangeEventHandler = (event) => { - setStudylogContent({ ...studylogContent, title: event.target.value }); - }; - - const onSelectTag = (tags, actionMeta) => { - if (actionMeta.action === 'create-option') { - actionMeta.option.label = '#' + actionMeta.option.label; - } - - setStudylogContent({ - ...studylogContent, - tags: tags.map(({ value }) => ({ name: value.replace(/#/, '') })), - }); - }; - - const onSelectMission = (mission: SelectOption | null) => { - if (!mission) { - setStudylogContent({ ...studylogContent, missionId: null }); - return; - } - - setStudylogContent({ ...studylogContent, missionId: Number(mission.value) }); - }; - - const onSelectSession = (session: SelectOption | null) => { - if (!session) { - setStudylogContent({ ...studylogContent, sessionId: null }); - return; - } - - setStudylogContent({ ...studylogContent, sessionId: Number(session.value) }); - }; - - const onEditStudylog: FormEventHandler = (event) => { - event.preventDefault(); - - const content = editorContentRef.current?.getInstance().getMarkdown() || ''; - - if (studylogContent.title.length === 0) { - alert(ALERT_MESSAGE.NO_TITLE); - return; - } - - if (content.length === 0) { - alert(ALERT_MESSAGE.NO_CONTENT); - return; - } - - editStudylogRequest({ - ...studylogContent, - content, - }); - }; - - const { mutate: editStudylogRequest } = useMutation( - (data: StudylogForm) => - requestEditStudylog({ - id, - accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) as string, - data, - }), - { - onSuccess: async () => { - alert(SUCCESS_MESSAGE.EDIT_POST); - history.push(`${PATH.STUDYLOG}/${id}`); - }, - - onError: (error: ResponseError) => { - alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.FAIL_TO_EDIT_STUDYLOG); - }, - } - ); - - const author = fetchStudylogRequest.data?.data.author; - - useEffect(() => { - if (author && username !== author.username) { - alert(ALERT_MESSAGE.CANNOT_EDIT_OTHERS); - history.push(`${PATH.STUDYLOG}/${id}`); - } - }, [username, author]); - - useEffect(() => { - fetchStudylogRequest.refetch(); - }, [id]); - - return ( -
- -
- ); + return ; }; export default EditStudylogPage; diff --git a/frontend/src/pages/NewStudylogPage/index.tsx b/frontend/src/pages/NewStudylogPage/index.tsx index 865a678ce..b62e29632 100644 --- a/frontend/src/pages/NewStudylogPage/index.tsx +++ b/frontend/src/pages/NewStudylogPage/index.tsx @@ -1,172 +1,7 @@ -/** @jsxImportSource @emotion/react */ - -import { css } from '@emotion/react'; - -import { useState, ChangeEventHandler, FormEventHandler, useRef, useEffect } from 'react'; -import { MainContentStyle } from '../../PageRouter'; - -import { ERROR_MESSAGE, ALERT_MESSAGE, PATH } from '../../constants'; - -import { StudylogForm } from '../../models/Studylogs'; -import { useMutation } from 'react-query'; -import LOCAL_STORAGE_KEY from '../../constants/localStorage'; -import { CONFIRM_MESSAGE, 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'; -import useTempSavedStudylog from '../../hooks/Studylog/useTempSavedStudylog'; - -type SelectOption = { value: string; label: string }; +import StudylogEditor from '../../components/StudylogEditor/StudylogEditor'; const NewStudylogPage = () => { - const history = useHistory(); - const editorContentRef = useRef(null); - const { - tempSavedStudylog, - createTempSavedStudylog, - removeCachedTempSavedStudylog, - } = useTempSavedStudylog(); - - useBeforeunload(editorContentRef); - - const [studylogContent, setStudylogContent] = useState({ - title: '', - content: null, - missionId: null, - sessionId: null, - tags: [], - }); - - const onChangeTitle: ChangeEventHandler = (event) => { - setStudylogContent({ ...studylogContent, title: event.target.value }); - }; - - const onSelectTag = (tags, actionMeta) => { - if (actionMeta.action === 'create-option') { - actionMeta.option.label = '#' + actionMeta.option.label; - } - - setStudylogContent({ - ...studylogContent, - tags: tags.map(({ value }) => ({ name: value.replace(/#/, '') })), - }); - }; - - const onSelectMission = (mission: SelectOption) => - setStudylogContent({ ...studylogContent, missionId: Number(mission.value) }); - - const onSelectSession = (session: SelectOption) => { - setStudylogContent({ ...studylogContent, sessionId: Number(session.value) }); - }; - - const onCreateStudylog: FormEventHandler = (event) => { - event.preventDefault(); - - const content = editorContentRef.current?.getInstance().getMarkdown() || ''; - - if (studylogContent.title.length === 0) { - alert(ALERT_MESSAGE.NO_TITLE); - - return; - } - - if (content.length === 0) { - alert(ALERT_MESSAGE.NO_CONTENT); - - return; - } - - // FIXME: 한 타임 밀림 - createStudylogRequest({ - ...studylogContent, - content, - }); - }; - - const onTempSaveStudylog = () => { - const content = editorContentRef.current?.getInstance().getMarkdown() || ''; - - if (studylogContent.title.length === 0 && content.length === 0) { - alert(ALERT_MESSAGE.NO_TITLE_OR_CONTENT); - - return; - } - - if (window.confirm(CONFIRM_MESSAGE.TEMP_SAVE_STUDYLOG)) { - createTempSavedStudylog({ - ...studylogContent, - content, - }); - } - }; - - const { mutate: createStudylogRequest } = useMutation( - (data: StudylogForm) => - requestPostStudylog({ - accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) as string, - data, - }), - { - onSuccess: async () => { - removeCachedTempSavedStudylog(); - alert(SUCCESS_MESSAGE.CREATE_POST); - history.push(PATH.STUDYLOG); - }, - onError: (error: ResponseError) => { - alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.DEFAULT); - }, - } - ); - - useEffect(() => { - if (tempSavedStudylog) { - const isTempSavedStudylogExist = Object.entries(tempSavedStudylog).some( - ([_, value]) => value !== null - ); - - if (isTempSavedStudylogExist) { - setStudylogContent({ - title: tempSavedStudylog.title ?? '', - content: tempSavedStudylog.content, - missionId: tempSavedStudylog.mission?.id ?? null, - sessionId: tempSavedStudylog.session?.sessionId ?? null, - tags: tempSavedStudylog.tags ?? [], - }); - - return; - } - - setStudylogContent({ ...studylogContent, content: '' }); - } - }, [tempSavedStudylog]); - - return ( -
- -
- ); + return ; }; export default NewStudylogPage; diff --git a/frontend/src/pages/StudylogListPage/index.tsx b/frontend/src/pages/StudylogListPage/index.tsx index f7d0ea234..7a8624ec3 100644 --- a/frontend/src/pages/StudylogListPage/index.tsx +++ b/frontend/src/pages/StudylogListPage/index.tsx @@ -3,31 +3,19 @@ import { css } from '@emotion/react'; import { useContext, useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import PencilIcon from '../../assets/images/pencil_icon.svg'; -import { Button, FilterList, Pagination } from '../../components'; -import Chip from '../../components/Chip/Chip'; +import { Pagination } from '../../components'; import StudylogList from '../../components/Lists/StudylogList'; -import SearchBar from '../../components/SearchBar/SearchBar'; -import { PATH } from '../../constants'; -import MEDIA_QUERY from '../../constants/mediaQuery'; +import { COLOR, PATH } from '../../constants'; import { ERROR_MESSAGE } from '../../constants/message'; import { UserContext } from '../../contexts/UserProvider'; import useFetch from '../../hooks/useFetch'; import useFilterWithParams from '../../hooks/useFilterWithParams'; import useStudylog from '../../hooks/useStudylog'; import { MainContentStyle } from '../../PageRouter'; -import { requestGetFilters } from '../../service/requests'; -import { - AlignItemsCenterStyle, - FlexStyle, - JustifyContentSpaceBtwStyle, -} from '../../styles/flex.styles'; -import { - FilterListWrapper, - HeaderContainer, - PostListContainer, - SelectedFilterList, -} from './styles'; +import { requestGetFilters, requestGetFiltersWithAccessToken } from '../../service/requests'; +import { FlexStyle, getFlexStyle } from '../../styles/flex.styles'; +import { HeaderContainer, StudylogListContainer } from './styles'; +import { Card, SectionName } from '../../components/StudylogEditor/styles'; const StudylogListPage = (): JSX.Element => { const { @@ -52,9 +40,13 @@ const StudylogListPage = (): JSX.Element => { const authorized = isLoggedIn && role !== 'GUEST'; const { response: studylogs, getAllData: getStudylogs } = useStudylog([]); - const [filters] = useFetch([], requestGetFilters); + const [filters] = useFetch( + [], + accessToken ? () => requestGetFiltersWithAccessToken(accessToken) : requestGetFilters + ); - const [searchKeywords, setSearchKeywords] = useState(''); + const [showAllMySessions, setShowAllMySessions] = useState(false); + const [showAllAllSessions, setShowAllAllSessions] = useState(false); const goNewStudylog = () => { if (!accessToken) { @@ -66,30 +58,27 @@ const StudylogListPage = (): JSX.Element => { history.push(PATH.NEW_STUDYLOG); }; - const onDeleteSearchKeyword = () => { - const params = new URLSearchParams(history.location.search); - params.delete('keyword'); - - history.push(`${PATH.STUDYLOG}?${params.toString()}`); - }; - - const onSearchKeywordsChange = (value: string) => { - setSearchKeywords(value); - }; - - const onSearch = async (event) => { - event.preventDefault(); + const findFilterItem = (key, id) => + selectedFilterDetails.find( + (filterItem) => filterItem.filterType === key && filterItem.filterDetailId === id + ); - const query = new URLSearchParams(history.location.search); - query.set('page', '1'); + const toggleFilterDetails = (filterType, filterDetailId, name) => { + const targetFilterItem = { filterType, filterDetailId, name }; + const isExistFilterItem = findFilterItem(filterType, filterDetailId); - if (searchKeywords) { - query.set('keyword', searchKeywords); + if (isExistFilterItem) { + setSelectedFilterDetails(selectedFilterDetails.filter((item) => item !== isExistFilterItem)); } else { - query.delete('keyword'); + setSelectedFilterDetails([...selectedFilterDetails, targetFilterItem]); } + }; - history.push(`${PATH.STUDYLOG}?${query.toString()}`); + const checkSelectedSession = (sessionId) => { + return selectedFilterDetails.some( + (filterItem) => + filterItem.filterType === 'sessions' && filterItem.filterDetailId === sessionId + ); }; useEffect(() => { @@ -125,81 +114,143 @@ const StudylogListPage = (): JSX.Element => { setSelectedFilterDetails(selectedFilterDetailsWithName); }, [filters]); - useEffect(() => { - const query = new URLSearchParams(history.location.search); + const renderSessionList = (sessions, showAll, toggleShowAll) => { + const sessionsToShow = showAll ? sessions : sessions.slice(0, 5); - setSearchKeywords(query.get('keyword') ?? ''); - }, [history.location.search]); + return ( +
+ {sessionsToShow.map((session) => ( +
- -
button { - display: none; - } + &:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } - `, - ]} - > - - - - {authorized && ( -
+ ))} + {sessions.length > 5 && ( + + )} +
+ ); + }; + + return ( +
+ + 내 강의 목록 + +
+ {filters && filters.mySessions && filters.mySessions.length === 0 && ( +
+ 수강중인 강의가 없습니다. +
+ )} + {filters && + filters.mySessions && + renderSessionList(filters.mySessions, showAllMySessions, () => + setShowAllMySessions(!showAllMySessions) + )} +
+
+ 전체 강의 목록 + +
+ {filters && + filters.sessions && + renderSessionList(filters.sessions, showAllAllSessions, () => + setShowAllAllSessions(!showAllAllSessions) + )} +
+
+
+
+ + {studylogs?.data?.length === 0 && ( + - 글쓰기 - + 작성된 글이 없습니다. + )} -
- - -
    - {!!search && ( -
  • - {`검색어 : ${search}`} -
  • - )} - {selectedFilterDetails.map(({ filterType, filterDetailId, name }) => ( -
  • - onUnsetFilter({ filterType, filterDetailId })} - >{`${filterType}: ${name}`} -
  • - ))} -
-
- - - - {studylogs?.data?.length === 0 && '작성된 글이 없습니다.'} - {studylogs && studylogs.data && } - - + {studylogs && studylogs.data && } + + +
); }; diff --git a/frontend/src/pages/StudylogListPage/styles.ts b/frontend/src/pages/StudylogListPage/styles.ts index 860a025e1..0cb162475 100644 --- a/frontend/src/pages/StudylogListPage/styles.ts +++ b/frontend/src/pages/StudylogListPage/styles.ts @@ -30,10 +30,10 @@ const SelectedFilterList = styled.div` } `; -const PostListContainer = styled.div` +const StudylogListContainer = styled.div` display: grid; grid-row-gap: 2rem; word-break: break-all; `; -export { HeaderContainer, FilterListWrapper, SelectedFilterList, PostListContainer }; +export { HeaderContainer, FilterListWrapper, SelectedFilterList, StudylogListContainer }; diff --git a/frontend/src/pages/StudylogPage/Content.tsx b/frontend/src/pages/StudylogPage/Content.tsx index afbf3887a..dfcb672a6 100644 --- a/frontend/src/pages/StudylogPage/Content.tsx +++ b/frontend/src/pages/StudylogPage/Content.tsx @@ -1,8 +1,10 @@ /** @jsxImportSource @emotion/react */ -import { MouseEventHandler } from 'react'; -import { Card, ProfileChip } from '../../components'; -import ViewCount from '../../components/ViewCount/ViewCount'; +import React, { MouseEventHandler, useContext } from 'react'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.bundle.min.js'; +import { Button, BUTTON_SIZE, Card, ProfileChip } from '../../components'; +import ViewCount from '../../components/Count/ViewCount'; import { AlignItemsBaseLineStyle, FlexStyle, @@ -10,7 +12,10 @@ import { } from '../../styles/flex.styles'; import { BottomContainer, + ButtonList, CardInner, + DeleteButtonStyle, + EditButtonStyle, IssuedDate, Mission, ProfileChipStyle, @@ -20,7 +25,7 @@ import { Title, ViewerWrapper, } from './styles'; -import { Studylog } from '../../models/Studylogs'; +import { QuestionAnswer, Studylog } from '../../models/Studylogs'; import defaultProfileImage from '../../assets/images/no-profile-image.png'; import { css } from '@emotion/react'; import Like from '../../components/Reaction/Like'; @@ -32,6 +37,14 @@ 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 { ALERT_MESSAGE, CONFIRM_MESSAGE, ERROR_MESSAGE, PATH } from '../../constants'; +import { useHistory, useParams } from 'react-router-dom'; +import useSnackBar from '../../hooks/useSnackBar'; +import { useMutation } from 'react-query'; +import { requestDeleteStudylog } from '../../service/requests'; +import { SUCCESS_MESSAGE } from '../../constants/message'; +import { useDeleteStudylogMutation } from '../../hooks/queries/studylog'; +import { UserContext } from '../../contexts/UserProvider'; interface Props { studylog: Studylog; @@ -39,9 +52,16 @@ interface Props { toggleScrap: MouseEventHandler; // ProfileChip 내부 타이핑 불가로인하여 any로 단언 goAuthorProfilePage: any; + answers: QuestionAnswer[]; } -const Content = ({ studylog, toggleLike, toggleScrap, goAuthorProfilePage }: Props) => { +const Content: React.FC = ({ + studylog, + toggleLike, + toggleScrap, + goAuthorProfilePage, + answers, +}) => { const { author, mission, @@ -55,31 +75,96 @@ const Content = ({ studylog, toggleLike, toggleScrap, goAuthorProfilePage }: Pro scrap, } = studylog; + const history = useHistory(); + const { user } = useContext(UserContext); + const { accessToken, username } = user; + const { id } = useParams<{ id: string }>(); + + const { mutate: deleteStudylog } = useDeleteStudylogMutation(); + + const goEditTargetPost = () => { + history.push(`${PATH.STUDYLOG}/${id}/edit`); + }; return (
{mission?.name} - - {new Date(createdAt).toLocaleString('ko-KR')} - +
{title} -
+
+
+
+ + {author?.nickname} + + {new Date(createdAt).toLocaleString('ko-KR')} +
+
- + {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 }) => ( + + ))} + + )} +
+
+ +
- {author?.nickname} - + + + +
- {content && ( {`#${name} `} ))} -
*:not(:last-child) { - margin-right: 1rem; - } - `, - ]} - > - - -
+ {/**/} + {/* */} + {/* */} + {/* */} + {/**/}
diff --git a/frontend/src/pages/StudylogPage/index.tsx b/frontend/src/pages/StudylogPage/index.tsx index ef72e7615..51b9e82a3 100644 --- a/frontend/src/pages/StudylogPage/index.tsx +++ b/frontend/src/pages/StudylogPage/index.tsx @@ -5,8 +5,7 @@ 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 { EditorForm, SubmitButton } from './styles'; import { MainContentStyle } from '../../PageRouter'; import { UserContext } from '../../contexts/UserProvider'; @@ -26,6 +25,9 @@ import { usePostLikeMutation, usePostScrapMutation, } from '../../hooks/queries/studylog'; +import { Card, SectionName } from '../../components/StudylogEditor/styles'; +import { css } from '@emotion/react'; +import QuestionAnswers from '../../components/StudylogEditor/QuestionAnswers'; const StudylogPage = () => { const { id } = useParams<{ id: string }>(); @@ -66,10 +68,6 @@ const StudylogPage = () => { history.push(`/${author?.username}`); }; - const goEditTargetPost = () => { - history.push(`${PATH.STUDYLOG}/${id}/edit`); - }; - const toggleLike = () => { liked ? debounce(() => { @@ -126,46 +124,45 @@ const StudylogPage = () => { 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 && ( - - - 작성 완료 - + {studylog.answers && studylog.answers.length > 0 && ( + + Question + + )} + + Comment + {isLoggedIn && ( + + + 작성 완료 + + )} + {comments && ( + + )} +
); }; diff --git a/frontend/src/pages/StudylogPage/styles.ts b/frontend/src/pages/StudylogPage/styles.ts index 43b62f3a6..c659d3815 100644 --- a/frontend/src/pages/StudylogPage/styles.ts +++ b/frontend/src/pages/StudylogPage/styles.ts @@ -10,25 +10,27 @@ const ButtonList = styled.div` `; const EditButtonStyle = css` - border: 1px solid ${COLOR.LIGHT_GRAY_200}; - + padding: 0.5rem 1rem; + border-radius: 10px; + border: 1px solid ${COLOR.LIGHT_GRAY_50}; background-color: ${COLOR.WHITE}; - color: ${COLOR.BLACK_800}; - - margin-right: 1rem; - + color: ${COLOR.DARK_GRAY_600}; &:hover { - background-color: ${COLOR.LIGHT_GRAY_300}; + border: 1px solid ${COLOR.LIGHT_GRAY_100}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } `; const DeleteButtonStyle = css` - border: 1px solid ${COLOR.LIGHT_GRAY_200}; - background-color: ${COLOR.RED_300}; - color: ${COLOR.BLACK_800}; - + margin-left: 1rem; + padding: 0.5rem 1rem; + border-radius: 10px; + border: 1px solid ${COLOR.RED_50}; + background-color: ${COLOR.RED_50}; + color: ${COLOR.DARK_GRAY_600}; &:hover { - background-color: ${COLOR.RED_400}; + background-color: ${COLOR.RED_50}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } `; @@ -64,8 +66,8 @@ const SubHeaderRightContent = styled.div` `; const Mission = styled.div` - font-size: 2rem; - color: ${COLOR.DARK_GRAY_900}; + font-size: 1.5rem; + color: ${COLOR.DARK_GRAY_600}; font-weight: lighter; `; @@ -73,7 +75,7 @@ const Title = styled.div` font-size: 3.6rem; color: ${COLOR.DARK_GRAY_900}; font-weight: bold; - margin-bottom: 2rem; + margin-bottom: 1rem; `; const Tags = styled.div` @@ -84,13 +86,12 @@ const Tags = styled.div` `; const IssuedDate = styled.div` - color: ${COLOR.DARK_GRAY_800}; + color: ${COLOR.LIGHT_GRAY_900}; font-size: 1.4rem; `; const ProfileChipStyle = css` border: none; - padding: 0.8rem; cursor: pointer; &:hover { @@ -137,6 +138,10 @@ const ViewerWrapper = styled.div` color: #222; } + .toastui-editor-contents p { + color: ${COLOR.DARK_GRAY_900}; + } + ${markdownStyle}; `; @@ -144,6 +149,8 @@ const BottomContainer = styled.div` display: flex; justify-content: space-between; margin-top: auto; + //border-top: 1px solid #e6e6e6; + //border-bottom: 1px solid #e6e6e6; `; const EditorForm = styled.form` diff --git a/frontend/src/routes.js b/frontend/src/routes.js index b8b0c7bfb..0bbfc1403 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -30,7 +30,8 @@ const pageRoutes = [ }, { path: [PATH.ROOT], - render: () => , + // render: () => , + render: () => , }, { path: [PATH.ARTICLE], diff --git a/frontend/src/service/requests.ts b/frontend/src/service/requests.ts index 26754b69a..67dbb588a 100644 --- a/frontend/src/service/requests.ts +++ b/frontend/src/service/requests.ts @@ -66,6 +66,9 @@ export const requestGetMyScrap = ({ username, accessToken, postQueryParams }) => /* @deprecated 의존성 완전 삭제 이후 코드 삭제*/ export const requestGetFilters = () => customAxios().get('/filters'); +export const requestGetFiltersWithAccessToken = (accessToken) => + customAxios(accessToken).get('/filters'); + /* @deprecated 의존성 완전 삭제 이후 코드 삭제*/ export const requestGetMissions = () => customAxios().get('/missions'); diff --git a/frontend/src/styles/flex.styles.ts b/frontend/src/styles/flex.styles.ts index 1e3718cde..7097d1921 100644 --- a/frontend/src/styles/flex.styles.ts +++ b/frontend/src/styles/flex.styles.ts @@ -39,6 +39,11 @@ export const FlexStyle = css` display: flex; `; +export const FlexRowStyle = css` + flex-direction: row; +`; + + export const FlexColumnStyle = css` flex-direction: column; `; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 03144253d..6d9a3c9fb 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5035,6 +5035,11 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +bootstrap@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.3.tgz#de35e1a765c897ac940021900fcbb831602bac38" + integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg== + boxen@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz"