From 178e3ae935bc7be862ab2de291bd16af3ac06e90 Mon Sep 17 00:00:00 2001 From: solo5star Date: Wed, 19 Jul 2023 08:51:10 +0000 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=ED=95=84=ED=84=B0=20msw=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: WaiNaat --- .../src/components/Lists/QuizAnswerList.tsx | 39 ++ frontend/src/mocks/fixtures/essayAnswers.ts | 87 ++++ frontend/src/mocks/fixtures/keywords.ts | 393 +++++++++--------- frontend/src/mocks/fixtures/quizs.ts | 19 +- frontend/src/mocks/handlers/essayAnswers.ts | 41 ++ frontend/src/mocks/handlers/index.ts | 10 +- frontend/src/mocks/handlers/keywords.ts | 9 +- 7 files changed, 400 insertions(+), 198 deletions(-) create mode 100644 frontend/src/components/Lists/QuizAnswerList.tsx create mode 100644 frontend/src/mocks/fixtures/essayAnswers.ts create mode 100644 frontend/src/mocks/handlers/essayAnswers.ts diff --git a/frontend/src/components/Lists/QuizAnswerList.tsx b/frontend/src/components/Lists/QuizAnswerList.tsx new file mode 100644 index 000000000..3656ae034 --- /dev/null +++ b/frontend/src/components/Lists/QuizAnswerList.tsx @@ -0,0 +1,39 @@ +/** @jsxImportSource @emotion/react */ +import {css} from '@emotion/react'; +import EssayAnswerItem from "../Items/EssayAnswerItem"; +import { NoDefaultHoverLink } from '../Items/EssayAnswerItem.styles'; +import { EssayAnswerResponse } from '../../models/EssayAnswers'; + +interface QuizAnswerListProps { + essayAnswers: EssayAnswerResponse[]; + showQuizTitle?: boolean; +} + +const QuizAnswerList = (props: QuizAnswerListProps) => { + const { essayAnswers, showQuizTitle = false } = props; + + return ( + + ); +}; + +export default QuizAnswerList; diff --git a/frontend/src/mocks/fixtures/essayAnswers.ts b/frontend/src/mocks/fixtures/essayAnswers.ts new file mode 100644 index 000000000..debf18cab --- /dev/null +++ b/frontend/src/mocks/fixtures/essayAnswers.ts @@ -0,0 +1,87 @@ +import { EssayAnswerResponse } from '../../models/EssayAnswers'; + +type EssayAnswersFilterParams = { + curriculumId: number; + keywordId?: number; + quizIds?: number[]; + memberIds?: number[]; + page?: number; + size?: number; +}; + +const data: Array<{ + curriculumId: number; + essayAnswers: (EssayAnswerResponse & { + keywordId: number; + })[]; +}> = [ + { + curriculumId: 1, + essayAnswers: [ + { + id: 71, + keywordId: 1, + quiz: { + quizId: 29, + question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '# Varargs\n***\nVarargs(가변인자)는 JDK 5에서 새로 도입된 기능이다.\nparameter의 수를 가변적으로 조절할 수 있게 해주는 기능이다.\n
\n\n## 사용법\n***\nparameter를 여러개 정의하고 싶은 method를 선언할 때, parameter 부분에 ...을 붙이면 된다.\n\n```java\npublic int calculateSum(int... numbers) {\n return Arrays.stream(numbers).sum();\n}\n```\n\n위 코드에서 유추할 수 있듯이, varargs는 내부적으로 array를 반환한다.\ncompile 시에 Object array가 만들어지고, varargs에 할당된 인자들이 array의 원소로 들어간다.\n
\n\n## 장점\n***\n만약 위의 method에서, varargs를 사용하지 않는다고 가정해보자.\n그렇다면, parameter가 2개, 3개, 4개, ..., 인 calculateSum() method를 전부 overloading해야 할 것이다.\n\n```java\npublic int getSum(int number1, int number2) {\n\treturn number1 + number2;\n}\npublic int getSum(int number1, int number2, int number3) {\n\treturn number1 + number2 + number3;\n}\n\n...\n\n\n```\n\nvarargs를 사용하면 이러한 overloading을 하지 않도록 할 수 있다.', + author: { + id: 304, + username: 'Jaeyoung22', + nickname: 'ReO', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/89302528?v=4', + }, + createdAt: '2023-04-09T17:56:25.642891', + updatedAt: '2023-04-09T17:56:25.642891', + }, + { + id: 69, + keywordId: 1, + quiz: { + quizId: 29, + question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '# Varargs 탄생 배경\n\nJDK 1.4 전까지는 다양한 수의 인자를 가진 메서드를 선언할 수 없었습니다.\n이를 해결하기 위해선 두 가지 방식을 사용했습니다.\n1. 메서드 오버로딩\n이 방법은 코드의 길이를 무한대로 증가시킵니다.\n2. 전달할 값들을 배열로 받기\n\n결국, Varargs는 **다양한 범위의 인자를 받기 위한 해결책**으로 탄생하게 되었습니다.\n\n# Varargs 사용법 및 특징\n\n```java\npublic class VarargsPractice {\n public static void main(String[] args) {\n printValues(); // 특징 1. 0개 이상의 인자를 전달할 수 있습니다.\n printValues("1");\n printValues("1", "2");\n printValues("1", "2", "3");\n printValues(new String[]{"1", "2", "3"}); // 특징 2. 실제로는 배열로 동작합니다.\n }\n\n // 사용법: 파라미터에 타입과 ... 을 함께 작성합니다. (String...)\n public static void printValues(final String... values) {\n for (int i = 0; i < values.length; i++) {\n System.out.println(values[i]); // 배열로 동작하기에 인덱스로 접근 가능합니다.\n }\n }\n}\n```\n\n# 잘못된 Varargs 사용법\n\n1. 한 메서드에 2가지 타입의 가변인자(varargs)는 사용하지 못합니다.\n```java\nvoid method(String... inputs, int... numbers);\n```\n\n2. 가변인자를 첫 번째 파라미터로 선언하지 못합니다.\n```java\nvoid method(int... numbers, String input);\n```\n\n# Varargs의 장점\n\n1. 메서드 인자의 수를 동적으로 결정 및 처리할 수 있습니다.\n2. 코드를 간결하게 유지할 수 있습니다.\n\n# Varargs의 단점\n\n1. 공간 복잡도가 증가합니다.\n가변 인자는 곧, 배열입니다.\n가변 인자를 전달받은 메서드는 매번 새로운 배열을 생성해야 합니다.\n\n# Varargs를 사용하는 기준\n(개인적인 기준입니다.)\n\n1. 인자의 개수를 다양하게 받아야 하는 상황이다.\n2. 성능상 그다지 중요하지 않은 상황이다.', + author: { + id: 287, + username: 'kdkdhoho', + nickname: '도기\uD83D\uDC36', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/66300965?v=4', + }, + createdAt: '2023-04-08T23:54:48.667266', + updatedAt: '2023-04-08T23:54:48.667266', + }, + ], + }, +]; + +const essayAnswersMock = { + data, + filter(params: EssayAnswersFilterParams) { + const { curriculumId, keywordId, memberIds, page = 1, quizIds, size = 10 } = params; + + const essayAnswers = + this.data.find( + (essayAnswersByCurriculum) => essayAnswersByCurriculum.curriculumId === curriculumId + )?.essayAnswers ?? []; + + return essayAnswers + .filter((essayAnswer) => { + if (essayAnswer.keywordId !== keywordId) return false; + + if (memberIds && !memberIds.includes(essayAnswer.author.id)) return false; + + if (quizIds && quizIds.includes(essayAnswer.quiz.quizId)) return false; + + return true; + }) + .slice(0, size); + }, +}; + +export default essayAnswersMock; diff --git a/frontend/src/mocks/fixtures/keywords.ts b/frontend/src/mocks/fixtures/keywords.ts index 0a0a6b805..67c7e33f3 100644 --- a/frontend/src/mocks/fixtures/keywords.ts +++ b/frontend/src/mocks/fixtures/keywords.ts @@ -1,187 +1,199 @@ -import { quizMock } from './quizs'; +import { KeywordResponse, Quiz } from '../../models/Keywords'; +import quizMock from './quizs'; -export default { - data: [ - // 세션 1 - { - keywordId: 1, - sessionId: 1, // for mock - name: 'JavaScript', - // order: 1, - importance: 5, - parentKeywordId: null, - description: '동적 타이핑, 스크립트 언어입니다.', - quizs: { - data: quizMock[1].data, - }, - childrenKeywords: [ - { - keywordId: 2, - sessionId: 1, // for mock - name: 'let, const, var', - // order: 1, - importance: 5, - parentKeywordId: 1, - description: 'let, const, var', - quizs: { - data: quizMock[2].data, - }, - childrenKeywords: [ - { - keywordId: 3, - sessionId: 1, // for mock - name: 'let', - // order: 1, - importance: 5, - parentKeywordId: 2, - description: 'let', - quizs: { - data: quizMock[3].data, - }, - childrenKeywords: {}, - }, - { - keywordId: 4, - sessionId: 1, // for mock - name: 'const', - // order: 1, - importance: 5, - parentKeywordId: 2, - description: 'const', - quizs: { - data: quizMock[4].data, - }, - childrenKeywords: {}, +type WithSession = T & { + sessionId: number; + quizs: { + data: Quiz[]; + }; + childrenKeywords: + | (T extends { childrenKeywords: Array | null } ? WithSession[] : null) + | null; +}; + +const data: Array> = [ + // 세션 1 + { + keywordId: 1, + sessionId: 1, // for mock + name: 'JavaScript', + order: 1, + importance: 5, + parentKeywordId: null, + description: '동적 타이핑, 스크립트 언어입니다.', + quizs: { + data: quizMock.filterByKeyword(1), + }, + childrenKeywords: [ + { + keywordId: 2, + sessionId: 1, // for mock + name: 'let, const, var', + order: 1, + importance: 5, + parentKeywordId: 1, + description: 'let, const, var', + quizs: { + data: quizMock.filterByKeyword(2), + }, + childrenKeywords: [ + { + keywordId: 3, + sessionId: 1, // for mock + name: 'let', + order: 1, + importance: 5, + parentKeywordId: 2, + description: 'let', + quizs: { + data: quizMock.filterByKeyword(3), }, - { - keywordId: 5, - sessionId: 1, // for mock - name: 'var', - // order: 1, - importance: 5, - parentKeywordId: 2, - description: 'var', - quizs: { - data: quizMock[5].data, - }, - childrenKeywords: {}, + childrenKeywords: null, + }, + { + keywordId: 4, + sessionId: 1, // for mock + name: 'const', + order: 1, + importance: 5, + parentKeywordId: 2, + description: 'const', + quizs: { + data: quizMock.filterByKeyword(4), }, - ], - }, - ], - }, - // 세션 1 - React 키워드 - { - keywordId: 6, - sessionId: 1, // for mock - name: 'React', - // order: 1, - importance: 5, - parentKeywordId: null, - description: 'React입니다.', - quizs: { - data: quizMock[6].data, - }, - childrenKeywords: [ - { - keywordId: 7, - sessionId: 1, // for mock - name: 'lifecycle', - // order: 1, - importance: 5, - parentKeywordId: 6, - description: 'lifecycle 설명', - quizs: { - data: quizMock[7].data, + childrenKeywords: null, }, - childrenKeywords: [ - { - keywordId: 8, - sessionId: 1, // for mock - name: 'mount', - // order: 1, - importance: 5, - parentKeywordId: 7, - description: 'mount 설명', - quizs: { - data: quizMock[8].data, - }, - childrenKeywords: {}, + { + keywordId: 5, + sessionId: 1, // for mock + name: 'var', + order: 1, + importance: 5, + parentKeywordId: 2, + description: 'var', + quizs: { + data: quizMock.filterByKeyword(5), }, - { - keywordId: 9, - sessionId: 1, // for mock - name: 'unmount', - // order: 1, - importance: 5, - parentKeywordId: 7, - description: 'unmount 설명', - quizs: { - data: quizMock[9].data, - }, - childrenKeywords: {}, + childrenKeywords: null, + }, + ], + }, + ], + }, + // 세션 1 - React 키워드 + { + keywordId: 6, + sessionId: 1, // for mock + name: 'React', + order: 1, + importance: 5, + parentKeywordId: null, + description: 'React입니다.', + quizs: { + data: quizMock.filterByKeyword(6), + }, + childrenKeywords: [ + { + keywordId: 7, + sessionId: 1, // for mock + name: 'lifecycle', + order: 1, + importance: 5, + parentKeywordId: 6, + description: 'lifecycle 설명', + quizs: { + data: quizMock.filterByKeyword(7), + }, + childrenKeywords: [ + { + keywordId: 8, + sessionId: 1, // for mock + name: 'mount', + order: 1, + importance: 5, + parentKeywordId: 7, + description: 'mount 설명', + quizs: { + data: quizMock.filterByKeyword(8), }, - { - keywordId: 10, - sessionId: 1, // for mock - name: 'update', - // order: 1, - importance: 5, - parentKeywordId: 7, - description: 'update 설명', - quizs: { - data: quizMock[10].data, - }, - childrenKeywords: {}, + childrenKeywords: null, + }, + { + keywordId: 9, + sessionId: 1, // for mock + name: 'unmount', + order: 1, + importance: 5, + parentKeywordId: 7, + description: 'unmount 설명', + quizs: { + data: quizMock.filterByKeyword(9), }, - ], - }, - ], - }, - // 세션 2 - Test - { - keywordId: 11, - sessionId: 2, // for mock - name: 'Test', - // order: 1, - importance: 5, - parentKeywordId: null, - description: 'Test입니다.', - quizs: { - data: quizMock[11].data, - }, - childrenKeywords: [ - { - keywordId: 12, - sessionId: 2, // for mock - name: 'Jest', - // order: 1, - importance: 5, - parentKeywordId: 11, - description: 'Jest 설명', - quizs: { - data: quizMock[12].data, + childrenKeywords: null, }, - childrenKeywords: [ - { - keywordId: 13, - sessionId: 2, // for mock - name: 'ReactTestingLibrary', - // order: 1, - importance: 5, - parentKeywordId: 12, - description: 'ReactTestingLibrary 설명', - quizs: { - data: quizMock[13].data, - }, - childrenKeywords: {}, + { + keywordId: 10, + sessionId: 1, // for mock + name: 'update', + order: 1, + importance: 5, + parentKeywordId: 7, + description: 'update 설명', + quizs: { + data: quizMock.filterByKeyword(10), }, - ], - }, - ], + childrenKeywords: null, + }, + ], + }, + ], + }, + // 세션 2 - Test + { + keywordId: 11, + sessionId: 2, // for mock + name: 'Test', + order: 1, + importance: 5, + parentKeywordId: null, + description: 'Test입니다.', + quizs: { + data: quizMock.filterByKeyword(11), }, - ], - // 5 + childrenKeywords: [ + { + keywordId: 12, + sessionId: 2, // for mock + name: 'Jest', + order: 1, + importance: 5, + parentKeywordId: 11, + description: 'Jest 설명', + quizs: { + data: quizMock.filterByKeyword(12), + }, + childrenKeywords: [ + { + keywordId: 13, + sessionId: 2, // for mock + name: 'ReactTestingLibrary', + order: 1, + importance: 5, + parentKeywordId: 12, + description: 'ReactTestingLibrary 설명', + quizs: { + data: quizMock.filterByKeyword(13), + }, + childrenKeywords: null, + }, + ], + }, + ], + }, +]; + +const keywordMock = { + data, filterKeywordsBySession(sessionId: string | readonly string[]) { const filteredData = this.data.filter((item) => item.sessionId === Number(sessionId)); @@ -189,7 +201,6 @@ export default { data: filteredData, }; }, - // 4 findKeyword(keywordId: string | readonly string[]) { // data를 순회하면서, childrenKeywords를 순회하면서 해당 keyword가 있는지 확인한다. const data = this.data @@ -199,21 +210,25 @@ export default { return depth1Item; } - return depth1Item.childrenKeywords.map((depth2Item) => { - // 2뎁스 순회 - if (depth2Item.keywordId === Number(keywordId)) { - return depth2Item; - } - - return depth2Item.childrenKeywords.map((depth3Item) => { - // 3뎁스 순회 - if (depth3Item.keywordId === Number(keywordId)) { - return depth3Item; + return ( + depth1Item.childrenKeywords?.map((depth2Item) => { + // 2뎁스 순회 + if (depth2Item.keywordId === Number(keywordId)) { + return depth2Item; } - return undefined; - }); - }); + return ( + depth2Item.childrenKeywords?.map((depth3Item) => { + // 3뎁스 순회 + if (depth3Item.keywordId === Number(keywordId)) { + return depth3Item; + } + + return undefined; + }) ?? [] + ); + }) ?? [] + ); }) .find((item) => item !== undefined); @@ -230,3 +245,5 @@ export default { }; }, }; + +export default keywordMock; diff --git a/frontend/src/mocks/fixtures/quizs.ts b/frontend/src/mocks/fixtures/quizs.ts index 95ca4b016..ea18ec268 100644 --- a/frontend/src/mocks/fixtures/quizs.ts +++ b/frontend/src/mocks/fixtures/quizs.ts @@ -1,4 +1,9 @@ -export const quizMock = [ +import { Quiz } from '../../models/Keywords'; + +const data: Array<{ + keywordId: number; + data: Quiz[]; +}> = [ { keywordId: 0, data: [], @@ -225,3 +230,15 @@ export const quizMock = [ ], }, ]; + +const quizMock = { + data, + findQuiz(quizId) { + return this.data.flatMap(({ data }) => data).find((quiz) => quiz.quizId === quizId) ?? null; + }, + filterByKeyword(keywordId) { + return this.data.find((quizs) => quizs.keywordId === keywordId)?.data ?? []; + }, +}; + +export default quizMock; diff --git a/frontend/src/mocks/handlers/essayAnswers.ts b/frontend/src/mocks/handlers/essayAnswers.ts new file mode 100644 index 000000000..1a4f376cd --- /dev/null +++ b/frontend/src/mocks/handlers/essayAnswers.ts @@ -0,0 +1,41 @@ +import { rest } from 'msw'; +import { BASE_URL } from '../../configs/environment'; +import essayAnswersMock from '../fixtures/essayAnswers'; + +export const essayAnswerHandler = [ + rest.get(`${BASE_URL}/essay-answers`, (req, res, ctx) => { + const { searchParams } = req.url; + + const params = { + curriculumId: searchParams.get('curriculumId'), + keywordId: searchParams.get('keywordId'), + quizIds: searchParams.get('quizIds'), + memberIds: searchParams.get('memberIds'), + page: searchParams.get('page'), + size: searchParams.get('size'), + }; + + const curriculumId = Number(params.curriculumId); + if (Number.isNaN(curriculumId)) { + return res(ctx.status(400)); + } + + return res( + ctx.status(200), + ctx.json( + essayAnswersMock.filter({ + curriculumId: curriculumId, + keywordId: params.curriculumId ? Number(params.curriculumId) : undefined, + quizIds: params.curriculumId + ? params.curriculumId.slice(1, -1).split(',').map(Number) ?? [] + : undefined, + memberIds: params.memberIds + ? params.memberIds.slice(1, -1).split(',').map(Number) ?? [] + : undefined, + page: params.page ? Number(params.page) : undefined, + size: params.size ? Number(params.size) : undefined, + }) + ) + ); + }), +]; diff --git a/frontend/src/mocks/handlers/index.ts b/frontend/src/mocks/handlers/index.ts index fe98ad59c..65d0d384a 100644 --- a/frontend/src/mocks/handlers/index.ts +++ b/frontend/src/mocks/handlers/index.ts @@ -1,11 +1,13 @@ -import { popularStudyLogHandler } from './popularStudyLog'; import { commentsHandler } from './comment'; -import { levellogHandler } from './levellog'; +import { essayAnswerHandler } from './essayAnswers'; import { roadmapHandler } from './keywords'; +import { levellogHandler } from './levellog'; +import { popularStudyLogHandler } from './popularStudyLog'; export const handlers = [ - ...popularStudyLogHandler, ...commentsHandler, - ...levellogHandler, + ...essayAnswerHandler, ...roadmapHandler, + ...levellogHandler, + ...popularStudyLogHandler, ]; diff --git a/frontend/src/mocks/handlers/keywords.ts b/frontend/src/mocks/handlers/keywords.ts index 620fb4030..396f5bef8 100644 --- a/frontend/src/mocks/handlers/keywords.ts +++ b/frontend/src/mocks/handlers/keywords.ts @@ -1,9 +1,8 @@ -import { BASE_URL } from '../../configs/environment'; import { rest } from 'msw'; -import keywordsMock from '../fixtures/keywords'; +import { BASE_URL } from '../../configs/environment'; import curriculums from '../fixtures/curriculums'; - -import { quizMock } from '../fixtures/quizs'; +import keywordsMock from '../fixtures/keywords'; +import quizMock from '../fixtures/quizs'; import { sessionsMock } from '../fixtures/sessions'; export const roadmapHandler = [ @@ -62,6 +61,6 @@ export const roadmapHandler = [ params: { sessionId, keywordId }, } = req; - return res(ctx.status(200), ctx.json(quizMock[Number(keywordId)])); + return res(ctx.status(200), ctx.json(quizMock.findQuiz(Number(keywordId)))); }), ]; From 630013261b1d13c7497f42c826047358e78f8dcd Mon Sep 17 00:00:00 2001 From: solo5star Date: Wed, 19 Jul 2023 08:52:31 +0000 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20=EB=A1=9C=EB=93=9C=EB=A7=B5?= =?UTF-8?q?=20=ED=80=B4=EC=A6=88=20=EB=8B=B5=EB=B3=80=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: WaiNaat --- .../src/components/Items/EssayAnswerItem.tsx | 11 +++- .../src/components/Lists/EssayAnswerList.tsx | 26 ---------- .../src/pages/QuizAnswerListPage/index.tsx | 52 +++++++++++++++++++ .../src/pages/QuizAnswerListPage/styles.ts | 20 +++++++ 4 files changed, 81 insertions(+), 28 deletions(-) delete mode 100644 frontend/src/components/Lists/EssayAnswerList.tsx create mode 100644 frontend/src/pages/QuizAnswerListPage/index.tsx create mode 100644 frontend/src/pages/QuizAnswerListPage/styles.ts diff --git a/frontend/src/components/Items/EssayAnswerItem.tsx b/frontend/src/components/Items/EssayAnswerItem.tsx index f823c5f8a..cafefab4b 100644 --- a/frontend/src/components/Items/EssayAnswerItem.tsx +++ b/frontend/src/components/Items/EssayAnswerItem.tsx @@ -10,13 +10,20 @@ import { import { AlignItemsEndStyle, FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; import Card from '../Card/Card'; import ProfileChip from '../ProfileChip/ProfileChip'; +import { EssayAnswerResponse } from '../../models/EssayAnswers'; -const EssayAnswerItem = ({ essayAnswer }) => { - const { author, answer } = essayAnswer; +type EssayAnswerItemProps = ( + Pick + & { title?: string; showTitle?: boolean } +); + +const EssayAnswerItem = (props: EssayAnswerItemProps) => { + const { author, answer, title, showTitle } = props; return (
+ { showTitle &&

{title}

}
{answer}
diff --git a/frontend/src/components/Lists/EssayAnswerList.tsx b/frontend/src/components/Lists/EssayAnswerList.tsx deleted file mode 100644 index 5838d5a80..000000000 --- a/frontend/src/components/Lists/EssayAnswerList.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import {css} from '@emotion/react'; -import EssayAnswerItem from "../Items/EssayAnswerItem"; -import { NoDefaultHoverLink } from '../Items/EssayAnswerItem.styles'; - -const EssayAnswerList = ({ essayAnswers }) => { - return ( -
    li:not(:last-child) { - margin-bottom: 1.6rem; - } - `} - > - {essayAnswers.map((essayAnswer) => ( -
  • - - - -
  • - ))} -
- ); -}; - -export default EssayAnswerList; diff --git a/frontend/src/pages/QuizAnswerListPage/index.tsx b/frontend/src/pages/QuizAnswerListPage/index.tsx new file mode 100644 index 000000000..f86215b51 --- /dev/null +++ b/frontend/src/pages/QuizAnswerListPage/index.tsx @@ -0,0 +1,52 @@ +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; +import QuizAnswerList from '../../components/Lists/QuizAnswerList'; +import MEDIA_QUERY from '../../constants/mediaQuery'; +import { useEssayAnswerList } from '../../hooks/EssayAnswer/useEssayAnswerList'; +import { MainContentStyle } from '../../PageRouter'; +import { + AlignItemsCenterStyle, + FlexStyle, + JustifyContentSpaceBtwStyle +} from '../../styles/flex.styles'; +import { HeaderContainer, PostListContainer } from './styles'; + +const QuizAnswerListPage = () => { + const { quiz, essayAnswers } = useEssayAnswerList(); + + return ( +
+ +
+

+ {!!quiz && quiz.question} 🤔 +

+
+
+ + {(!essayAnswers || essayAnswers.length === 0) && '작성된 글이 없습니다.'} + {!!essayAnswers && } + +
+ ); +}; + +export default QuizAnswerListPage; diff --git a/frontend/src/pages/QuizAnswerListPage/styles.ts b/frontend/src/pages/QuizAnswerListPage/styles.ts new file mode 100644 index 000000000..542b66cfa --- /dev/null +++ b/frontend/src/pages/QuizAnswerListPage/styles.ts @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; +import MEDIA_QUERY from '../../constants/mediaQuery'; + +const HeaderContainer = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 1.5rem; + + ${MEDIA_QUERY.xs} { + margin-bottom: 0.8rem; + } +`; + +const PostListContainer = styled.div` + display: grid; + grid-row-gap: 2rem; + word-break: break-all; +`; + +export { HeaderContainer, PostListContainer }; From 02a659b236b49f3bf74c9b8578957f992ddfebbc Mon Sep 17 00:00:00 2001 From: solo5star Date: Wed, 19 Jul 2023 08:53:05 +0000 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EC=A0=84=EC=B2=B4=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: WaiNaat --- frontend/src/models/EssayAnswers.ts | 27 ++++++++++++++++--- frontend/src/models/Keywords.ts | 2 +- .../src/pages/EssayAnswerListPage/index.tsx | 20 +++++++++----- frontend/src/pages/index.js | 21 ++++++++------- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/frontend/src/models/EssayAnswers.ts b/frontend/src/models/EssayAnswers.ts index 5323d79f6..d9adecb87 100644 --- a/frontend/src/models/EssayAnswers.ts +++ b/frontend/src/models/EssayAnswers.ts @@ -1,14 +1,12 @@ +import { Quiz } from './Keywords'; import { Author } from './Studylogs'; -import {Quiz} from "./Keywords"; export interface EssayAnswerRequest { quizId: number; answer: string; } -export interface EssayEditRequest { - answer: string; -} +export type EssayEditRequest = Pick; export interface EssayAnswerResponse { id: number; @@ -18,3 +16,24 @@ export interface EssayAnswerResponse { createdAt: string; updatedAt: string; } + +export type EssayAnswerFilter = { + curriculumId: number; + keywordId?: number; + quizIds?: number[]; + memberIds?: number[]; +}; + +export type EssayAnswerListRequest = EssayAnswerFilter & { + page?: number; + size?: number; +}; + +export type EssayAnswerFilterRequest = { + curriculumId: number; + keywordId?: number; + quizIds?: number[]; + memberIds?: number[]; + page?: number; + size?: number; +}; diff --git a/frontend/src/models/Keywords.ts b/frontend/src/models/Keywords.ts index 0c0c5276f..13f3681f2 100644 --- a/frontend/src/models/Keywords.ts +++ b/frontend/src/models/Keywords.ts @@ -23,7 +23,7 @@ export interface KeywordResponse { keywordId: number; order: number; importance: number; - parentKeywordId: number; + parentKeywordId: number | null; description: string; childrenKeywords: KeywordResponse[] | null; } diff --git a/frontend/src/pages/EssayAnswerListPage/index.tsx b/frontend/src/pages/EssayAnswerListPage/index.tsx index 88b4e1ea7..e348044cc 100644 --- a/frontend/src/pages/EssayAnswerListPage/index.tsx +++ b/frontend/src/pages/EssayAnswerListPage/index.tsx @@ -1,19 +1,27 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import EssayAnswerList from '../../components/Lists/EssayAnswerList'; -import MEDIA_QUERY from '../../constants/mediaQuery'; -import { useEssayAnswerList } from '../../hooks/EssayAnswer/useEssayAnswerList'; import { MainContentStyle } from '../../PageRouter'; +import EssayAnswerList from '../../components/Lists/QuizAnswerList'; +import MEDIA_QUERY from '../../constants/mediaQuery'; +import { useGetCurriculums } from '../../hooks/queries/curriculum'; +import { useGetEssayAnswers } from '../../hooks/queries/essayanswer'; import { AlignItemsCenterStyle, FlexStyle, JustifyContentSpaceBtwStyle } from '../../styles/flex.styles'; import { HeaderContainer, PostListContainer } from './styles'; +import { useLocation } from 'react-router'; const EssayAnswerListPage = () => { - const { quiz, essayAnswers } = useEssayAnswerList(); + const { curriculums } = useGetCurriculums(); + const { search } = useLocation(); + const searchParams = new URLSearchParams(search); + const curriculumId = Number(searchParams.get('curriculumId') ?? '1'); + const selectedCurriculum = (curriculums ?? []).find(curriculum => curriculum.id === curriculumId)?.name ?? '😎'; + + const { data: essayAnswers } = useGetEssayAnswers({ curriculumId }); return (
@@ -37,13 +45,13 @@ const EssayAnswerListPage = () => { font-size: 3.4rem; `} > - {!!quiz && quiz.question} 🤔 + {selectedCurriculum} 분야의 모든 로드맵 답변
{(!essayAnswers || essayAnswers.length === 0) && '작성된 글이 없습니다.'} - {!!essayAnswers && } + {!!essayAnswers && }
); diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.js index 9ec9ad6e6..3002a138b 100644 --- a/frontend/src/pages/index.js +++ b/frontend/src/pages/index.js @@ -1,15 +1,16 @@ +export { default as EditEssayAnswerPage } from './EditEssayAnswerPage'; +export { default as EditStudylogPage } from './EditStudylogPage'; +export { default as EssayAnswerListPage } from './EssayAnswerListPage'; +export { default as EssayAnswerPage } from './EssayAnswerPage'; +export { default as LoginCallbackPage } from './LoginCallbackPage'; export { default as MainPage } from './MainPage'; -export { default as StudylogPage } from './StudylogPage'; +export { default as NewEssayAnswerPage } from './NewEssayAnswerPage'; export { default as NewStudylogPage } from './NewStudylogPage'; export { default as ProfilePage } from './ProfilePage'; -export { default as ProfilePageStudylogs } from './ProfilePageStudylogs'; -export { default as ProfilePageScraps } from './ProfilePageScraps'; export { default as ProfilePageAccount } from './ProfilePageAccount'; -export { default as LoginCallbackPage } from './LoginCallbackPage'; -export { default as EditStudylogPage } from './EditStudylogPage'; -export { default as StudylogListPage } from './StudylogListPage'; +export { default as ProfilePageScraps } from './ProfilePageScraps'; +export { default as ProfilePageStudylogs } from './ProfilePageStudylogs'; +export { default as QuizAnswerListPage } from './QuizAnswerListPage'; export { default as RoadmapPage } from './RoadmapPage'; -export { default as NewEssayAnswerPage } from './NewEssayAnswerPage'; -export { default as EssayAnswerPage } from './EssayAnswerPage'; -export { default as EssayAnswerListPage } from './EssayAnswerListPage'; -export { default as EditEssayAnswerPage } from './EditEssayAnswerPage'; +export { default as StudylogListPage } from './StudylogListPage'; +export { default as StudylogPage } from './StudylogPage'; From 1d45708dcc529a8b66bdbf62115359dba6cfad14 Mon Sep 17 00:00:00 2001 From: solo5star Date: Wed, 19 Jul 2023 08:53:50 +0000 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EB=AA=A9=EB=A1=9D=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?api=20=ED=86=B5=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: WaiNaat --- frontend/src/apis/essayanswers.ts | 65 +++++---- frontend/src/constants/reactQueryKey.js | 4 +- .../hooks/EssayAnswer/useEssayAnswerList.ts | 4 +- frontend/src/hooks/queries/essayanswer.ts | 126 +++++++++--------- frontend/src/routes.js | 34 +++-- 5 files changed, 129 insertions(+), 104 deletions(-) diff --git a/frontend/src/apis/essayanswers.ts b/frontend/src/apis/essayanswers.ts index 09a3180dc..f28444664 100644 --- a/frontend/src/apis/essayanswers.ts +++ b/frontend/src/apis/essayanswers.ts @@ -1,44 +1,57 @@ -import { EssayAnswerRequest, EssayEditRequest } from "../models/EssayAnswers"; -import { AxiosPromise, AxiosResponse } from "axios"; -import { Quiz } from "../models/Keywords"; -import { createAxiosInstance } from "../utils/axiosInstance"; +import { AxiosPromise, AxiosResponse } from 'axios'; +import { client } from '.'; +import { EssayAnswerListRequest, EssayAnswerRequest, EssayEditRequest } from '../models/EssayAnswers'; +import { Quiz } from '../models/Keywords'; + +export const requestGetEssayAnswers = async (params: EssayAnswerListRequest) => { + const { + quizIds, + memberIds, + ...otherParams + } = params; + + const axiosParams: Omit & { + quizIds?: string; + memberIds?: string; + } = { ...otherParams }; + + if (quizIds) axiosParams.quizIds = '[' + quizIds.join(',') + ']'; + if (memberIds) axiosParams.memberIds = '[' + memberIds.join(',') + ']'; + + const { data } = await client.get('/essay-answers', { + params: axiosParams, + }); -import LOCAL_STORAGE_KEY from "../constants/localStorage"; - -const anyoneInstance = createAxiosInstance(); -const loggedInUserInstance = createAxiosInstance({ - accessToken: localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN) ?? '', -}); + return data; +}; -export const createNewEssayAnswerRequest = (body: EssayAnswerRequest) => ( - loggedInUserInstance.post(`/essay-answers`, body) -); +export const createNewEssayAnswerRequest = (body: EssayAnswerRequest) => + client.post(`/essay-answers`, body); -export const requestGetEssayAnswer = async (essayAnswerId) => { - const { data } = await anyoneInstance.get(`/essay-answers/${essayAnswerId}`); - return data +export const requestGetEssayAnswer = async (essayAnswerId: number) => { + const { data } = await client.get(`/essay-answers/${essayAnswerId}`); + return data; }; export const requestEditEssayAnswer = async (essayAnswerId: number, body: EssayEditRequest) => { - await loggedInUserInstance.patch(`/essay-answers/${essayAnswerId}`, body); + await client.patch(`/essay-answers/${essayAnswerId}`, body); }; -export const requestDeleteEssayAnswer = async (essayAnswerId) => { - await loggedInUserInstance.delete(`/essay-answers/${essayAnswerId}`); +export const requestDeleteEssayAnswer = async (essayAnswerId: number) => { + await client.delete(`/essay-answers/${essayAnswerId}`); }; -export const requestGetEssayAnswerList = async (quizId) => { - const { data } = await anyoneInstance.get(`/quizzes/${quizId}/essay-answers`); +export const requestGetQuizAnswers = async (quizId: number) => { + const { data } = await client.get(`/quizzes/${quizId}/essay-answers`); return data; }; -export const requestGetQuizAsync = async (quizId) => { - const { data } = await anyoneInstance.get(`/quizzes/${quizId}`); +export const requestGetQuizAsync = async (quizId: number) => { + const { data } = await client.get(`/quizzes/${quizId}`); return data; }; -export const requestGetQuiz = (quizId: Number): AxiosPromise> => ( - anyoneInstance.get>(`/quizzes/${quizId}`) -); +export const requestGetQuiz = (quizId: Number): AxiosPromise> => + client.get>(`/quizzes/${quizId}`); diff --git a/frontend/src/constants/reactQueryKey.js b/frontend/src/constants/reactQueryKey.js index d68ae254d..4f0d9cc86 100644 --- a/frontend/src/constants/reactQueryKey.js +++ b/frontend/src/constants/reactQueryKey.js @@ -1,8 +1,8 @@ const REACT_QUERY_KEY = { STUDYLOG: 'STUDYLOG', - QUIZ:'QUIZ', + QUIZ: 'QUIZ', ESSAY_ANSWER: 'ESSAY_ANSWER', - ESSAY_ANSWER_LIST: 'ESSAY_ANSWER_LIST', + QUIZ_ANSWERS: 'QUIZ_ANSWERS', }; export default REACT_QUERY_KEY; diff --git a/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts b/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts index d94a5285c..b2c702f64 100644 --- a/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts +++ b/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts @@ -1,10 +1,10 @@ -import { useGetEssayAnswerList, useGetQuiz } from '../queries/essayanswer'; +import { useGetQuizAnswerList, useGetQuiz } from '../queries/essayanswer'; import { useParams } from 'react-router-dom'; export const useEssayAnswerList = () => { const { quizId } = useParams<{ quizId: string }>(); - const { data: essayAnswers } = useGetEssayAnswerList({ quizId }); + const { data: essayAnswers } = useGetQuizAnswerList({ quizId }); const { data: quiz } = useGetQuiz({ quizId }) return { quiz, essayAnswers }; diff --git a/frontend/src/hooks/queries/essayanswer.ts b/frontend/src/hooks/queries/essayanswer.ts index 37e929aa7..f21b4b4f2 100644 --- a/frontend/src/hooks/queries/essayanswer.ts +++ b/frontend/src/hooks/queries/essayanswer.ts @@ -1,23 +1,27 @@ +import { AxiosError } from 'axios'; import { useMutation, useQuery } from 'react-query'; +import { useHistory } from 'react-router-dom'; import { createNewEssayAnswerRequest, requestDeleteEssayAnswer, requestEditEssayAnswer, requestGetEssayAnswer, - requestGetEssayAnswerList, - requestGetQuizAsync + requestGetEssayAnswers, + requestGetQuizAnswers, + requestGetQuizAsync, } from '../../apis/essayanswers'; -import { EssayAnswerRequest, EssayAnswerResponse } from '../../models/EssayAnswers'; - -import { AxiosError } from 'axios'; -import { useHistory } from 'react-router-dom'; +import { ResponseError } from '../../apis/studylogs'; import { ALERT_MESSAGE, PATH } from '../../constants'; import ERROR_CODE from '../../constants/errorCode'; -import useSnackBar from '../useSnackBar'; +import { ERROR_MESSAGE, SUCCESS_MESSAGE } from '../../constants/message'; import REACT_QUERY_KEY from '../../constants/reactQueryKey'; +import { + EssayAnswerFilterRequest, + EssayAnswerRequest, + EssayAnswerResponse, +} from '../../models/EssayAnswers'; import { Quiz } from '../../models/Keywords'; -import { ERROR_MESSAGE, SUCCESS_MESSAGE } from '../../constants/message'; -import { ResponseError } from '../../apis/studylogs'; +import useSnackBar from '../useSnackBar'; export const useCreateNewEssayAnswerMutation = ({ onSuccess = () => {}, @@ -32,51 +36,46 @@ export const useCreateNewEssayAnswerMutation = ({ }, }); -export const useEditEssayAnswer = ( - { essayAnswerId }: { essayAnswerId: number }, -) => { +export const useEditEssayAnswer = ({ essayAnswerId }: { essayAnswerId: number }) => { const history = useHistory(); - return useMutation( - (data: { answer: string }) => - requestEditEssayAnswer(essayAnswerId, data), - { - onSuccess: () => { - alert(SUCCESS_MESSAGE.EDIT_POST); - history.push(`/essay-answers/${essayAnswerId}`); - }, + return useMutation((data: { answer: string }) => requestEditEssayAnswer(essayAnswerId, data), { + onSuccess: () => { + alert(SUCCESS_MESSAGE.EDIT_POST); + history.push(`/essay-answers/${essayAnswerId}`); + }, - onError: (error: ResponseError) => { - alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.FAIL_TO_EDIT_STUDYLOG); - }, - } - ); -} + onError: (error: ResponseError) => { + alert(ERROR_MESSAGE[error.code] ?? ERROR_MESSAGE.FAIL_TO_EDIT_STUDYLOG); + }, + }); +}; export const useGetEssayAnswer = ( { essayAnswerId }, - { - onSuccess = (essayAnswer: EssayAnswerResponse) => {}, - onError = () => {} - } = {} + { onSuccess = (essayAnswer: EssayAnswerResponse) => {}, onError = () => {} } = {} ) => { const history = useHistory(); const { openSnackBar } = useSnackBar(); - return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER, essayAnswerId], () => requestGetEssayAnswer(essayAnswerId), { - onSuccess: (essayAnswer: EssayAnswerResponse) => { - onSuccess?.(essayAnswer); - }, - onError: (error) => { - const { response } = (error as unknown) as AxiosError; + return useQuery( + [REACT_QUERY_KEY.ESSAY_ANSWER, essayAnswerId], + () => requestGetEssayAnswer(essayAnswerId), + { + onSuccess: (essayAnswer: EssayAnswerResponse) => { + onSuccess?.(essayAnswer); + }, + onError: (error) => { + const { response } = (error as unknown) as AxiosError; - if (response?.data.code === ERROR_CODE.NO_CONTENT) { - openSnackBar(ALERT_MESSAGE.NO_EXIST_POST); - history.push(PATH.ROADMAP); - } - }, - refetchOnWindowFocus: false, - retry: false, - }); + if (response?.data.code === ERROR_CODE.NO_CONTENT) { + openSnackBar(ALERT_MESSAGE.NO_EXIST_POST); + history.push(PATH.ROADMAP); + } + }, + refetchOnWindowFocus: false, + retry: false, + } + ); }; export const useDeleteEssayAnswerMutation = ({ @@ -92,35 +91,38 @@ export const useDeleteEssayAnswerMutation = ({ }, }); -export const useGetEssayAnswerList = ( +export const useGetQuizAnswerList = ( { quizId }, - { - onSuccess = (essayAnswer: EssayAnswerResponse[]) => {}, - onError = () => {} - } = {} + { onSuccess = (essayAnswer: EssayAnswerResponse[]) => {}, onError = () => {} } = {} ) => { - return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER_LIST, quizId], () => requestGetEssayAnswerList(quizId), { - onSuccess: (essayAnswer: EssayAnswerResponse[]) => { - onSuccess?.(essayAnswer); - }, - onError: (error) => {}, - refetchOnWindowFocus: false, - retry: false, - }); + return useQuery( + [REACT_QUERY_KEY.QUIZ_ANSWERS, quizId], + () => requestGetQuizAnswers(quizId), + { + onSuccess: (essayAnswer: EssayAnswerResponse[]) => { + onSuccess?.(essayAnswer); + }, + onError: (error) => {}, + refetchOnWindowFocus: false, + retry: false, + } + ); +}; + +export const useGetEssayAnswers = (filter: EssayAnswerFilterRequest) => { + return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER_FILTER_LIST, filter], () => + requestGetEssayAnswers(filter) + ); }; export const useGetQuiz = ( { quizId }, - { - onSuccess = (quiz: Quiz) => {}, - onError = () => {} - } = {} + { onSuccess = (quiz: Quiz) => {}, onError = () => {} } = {} ) => { return useQuery([REACT_QUERY_KEY.QUIZ, quizId], () => requestGetQuizAsync(quizId), { onSuccess: (quiz: Quiz) => { onSuccess?.(quiz); }, - onError: (error) => {}, refetchOnWindowFocus: false, retry: false, }); diff --git a/frontend/src/routes.js b/frontend/src/routes.js index cab11fb6d..dc4951216 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -1,22 +1,27 @@ import { PATH, PROFILE_PAGE_MENU } from './constants'; import { + EditEssayAnswerPage, EditStudylogPage, + EssayAnswerListPage, + EssayAnswerPage, LoginCallbackPage, MainPage, + NewEssayAnswerPage, NewStudylogPage, - StudylogPage, ProfilePage, - ProfilePageStudylogs, ProfilePageScraps, - StudylogListPage, + ProfilePageStudylogs, + QuizAnswerListPage, RoadmapPage, - NewEssayAnswerPage, - EssayAnswerPage, - EssayAnswerListPage, - EditEssayAnswerPage + StudylogListPage, + StudylogPage, } from './pages'; const pageRoutes = [ + { + path: '/essay-answers', + render: () => , + }, { path: [PATH.ROADMAP], render: () => , @@ -25,13 +30,18 @@ const pageRoutes = [ path: [PATH.ROOT], render: () => , }, - { path: [PATH.LOGIN_CALLBACK], render: () => , }, - { path: [PATH.STUDYLOG], render: () => }, - { path: [PATH.NEW_STUDYLOG], render: () => }, + { + path: [PATH.STUDYLOG], + render: () => , + }, + { + path: [PATH.NEW_STUDYLOG], + render: () => , + }, { path: [`${PATH.STUDYLOG}/:id`], render: () => , @@ -70,12 +80,12 @@ const pageRoutes = [ }, { path: [PATH.ESSAY_ANSWER_LIST], - render: () => , + render: () => , }, { path: '/essay-answers/:id/edit', render: () => , - } + }, ]; export default pageRoutes; From 62b06a617411a2746fd10d2a1af6ecd0299bfdf0 Mon Sep 17 00:00:00 2001 From: solo5star Date: Thu, 20 Jul 2023 08:17:03 +0000 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EB=AA=87=EB=AA=87=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=ED=83=80=EC=9E=85=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A1=9C=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DropdownMenu/DropdownMenu.js | 12 ------- ...nu.stories.js => DropdownMenu.stories.tsx} | 5 +-- ...nMenu.styles.js => DropdownMenu.styles.ts} | 6 ++-- .../components/DropdownMenu/DropdownMenu.tsx | 14 ++++++++ ...erList.styles.js => FilterList.styles.tsx} | 6 ++-- .../{FilterList.js => FilterList.tsx} | 4 ++- ...edFilterList.js => SelectedFilterList.tsx} | 0 .../src/components/SearchBar/SearchBar.js | 16 --------- ...earchBar.styles.js => SearchBar.styles.ts} | 4 ++- .../src/components/SearchBar/SearchBar.tsx | 34 +++++++++++++++++++ frontend/src/pages/StudylogListPage/index.tsx | 5 ++- 11 files changed, 66 insertions(+), 40 deletions(-) delete mode 100644 frontend/src/components/DropdownMenu/DropdownMenu.js rename frontend/src/components/DropdownMenu/{DropdownMenu.stories.js => DropdownMenu.stories.tsx} (78%) rename frontend/src/components/DropdownMenu/{DropdownMenu.styles.js => DropdownMenu.styles.ts} (89%) create mode 100644 frontend/src/components/DropdownMenu/DropdownMenu.tsx rename frontend/src/components/FilterList/{FilterList.styles.js => FilterList.styles.tsx} (96%) rename frontend/src/components/FilterList/{FilterList.js => FilterList.tsx} (96%) rename frontend/src/components/FilterList/{SelectedFilterList.js => SelectedFilterList.tsx} (100%) delete mode 100644 frontend/src/components/SearchBar/SearchBar.js rename frontend/src/components/SearchBar/{SearchBar.styles.js => SearchBar.styles.ts} (78%) create mode 100644 frontend/src/components/SearchBar/SearchBar.tsx diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.js b/frontend/src/components/DropdownMenu/DropdownMenu.js deleted file mode 100644 index 0065f8a93..000000000 --- a/frontend/src/components/DropdownMenu/DropdownMenu.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; -import { Container } from './DropdownMenu.styles'; - -const DropdownMenu = ({ children, cssProps, css }) => { - return {children}; -}; - -DropdownMenu.propTypes = { - children: PropTypes.node, -}; - -export default DropdownMenu; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.stories.js b/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx similarity index 78% rename from frontend/src/components/DropdownMenu/DropdownMenu.stories.js rename to frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx index 784c59559..0fddab5a8 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.stories.js +++ b/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx @@ -1,12 +1,13 @@ import DropdownMenu from './DropdownMenu'; +import { Story, Meta } from '@storybook/react'; export default { title: 'Component/DropdownMenu', component: DropdownMenu, argTypes: { children: { control: 'text' } }, -}; +} as Meta; -const Template = (args) => ; +const Template: Story = (args) => ; export const Basic = Template.bind({}); diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.styles.js b/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts similarity index 89% rename from frontend/src/components/DropdownMenu/DropdownMenu.styles.js rename to frontend/src/components/DropdownMenu/DropdownMenu.styles.ts index 34d7bae60..9538e5d0e 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.styles.js +++ b/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts @@ -1,7 +1,9 @@ +import { InterpolationWithTheme } from '@emotion/core'; +import { Theme } from '@emotion/react'; import styled from '@emotion/styled'; import COLOR from '../../constants/color'; -const Container = styled.div` +const Container = styled.div<{ css?: InterpolationWithTheme }>` height: fit-content; max-height: 32rem; white-space: nowrap; @@ -16,7 +18,7 @@ const Container = styled.div` /* transform: translateY(30%); */ && { - ${(props) => props.css} + ${({ css }) => css} } /* &:before { diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.tsx b/frontend/src/components/DropdownMenu/DropdownMenu.tsx new file mode 100644 index 000000000..ffd785d40 --- /dev/null +++ b/frontend/src/components/DropdownMenu/DropdownMenu.tsx @@ -0,0 +1,14 @@ +import { Theme } from '@emotion/react'; +import { InterpolationWithTheme } from '@emotion/core'; +import { PropsWithChildren } from 'react'; +import { Container } from './DropdownMenu.styles'; + +type DropdownMenuProps = PropsWithChildren<{ + css?: InterpolationWithTheme; +}> + +const DropdownMenu = ({ children, css }: DropdownMenuProps) => { + return {children}; +}; + +export default DropdownMenu; diff --git a/frontend/src/components/FilterList/FilterList.styles.js b/frontend/src/components/FilterList/FilterList.styles.tsx similarity index 96% rename from frontend/src/components/FilterList/FilterList.styles.js rename to frontend/src/components/FilterList/FilterList.styles.tsx index 1dead8d96..36721f98f 100644 --- a/frontend/src/components/FilterList/FilterList.styles.js +++ b/frontend/src/components/FilterList/FilterList.styles.tsx @@ -18,7 +18,7 @@ const DropdownToggledStyle = css` } `; -const Container = styled.div` +const Container = styled.div<{ isDropdownToggled: boolean }>` background-color: ${COLOR.LIGHT_GRAY_50}; border: 1px solid ${COLOR.DARK_GRAY_400}; @@ -30,7 +30,7 @@ const Container = styled.div` align-items: center; ${({ isDropdownToggled }) => isDropdownToggled && DropdownToggledStyle} - ${({ css }) => css && css} + ${({ css }) => css} & > div:not(:last-child) { margin-right: 3.2rem; @@ -145,7 +145,7 @@ const ResetFilter = styled.div` flex-shrink: 0; `; -const CheckIcon = styled.img` +const CheckIcon = styled.img<{ checked: boolean }>` ${({ checked }) => !checked && 'visibility: hidden;'} `; diff --git a/frontend/src/components/FilterList/FilterList.js b/frontend/src/components/FilterList/FilterList.tsx similarity index 96% rename from frontend/src/components/FilterList/FilterList.js rename to frontend/src/components/FilterList/FilterList.tsx index 0c2269c84..925d12b6d 100644 --- a/frontend/src/components/FilterList/FilterList.js +++ b/frontend/src/components/FilterList/FilterList.tsx @@ -1,3 +1,5 @@ +/** @jsxImportSource @emotion/react */ + import { useState } from 'react'; import { DropdownMenu } from '..'; import SearchBar from '../SearchBar/SearchBar'; @@ -81,7 +83,7 @@ const FilterList = ({ setSearchKeyword(target.value)} + onChange={(value) => setSearchKeyword(value)} value={searchKeyword} /> diff --git a/frontend/src/components/FilterList/SelectedFilterList.js b/frontend/src/components/FilterList/SelectedFilterList.tsx similarity index 100% rename from frontend/src/components/FilterList/SelectedFilterList.js rename to frontend/src/components/FilterList/SelectedFilterList.tsx diff --git a/frontend/src/components/SearchBar/SearchBar.js b/frontend/src/components/SearchBar/SearchBar.js deleted file mode 100644 index e7f2c2cb3..000000000 --- a/frontend/src/components/SearchBar/SearchBar.js +++ /dev/null @@ -1,16 +0,0 @@ -import Button from '../Button/Button'; -import SearchIcon from '../../assets/images/search_icon.svg'; -import { Container } from './SearchBar.styles'; - -const SearchBar = ({ css, onSubmit, onChange, value }) => { - return ( -
- - - {onSubmit && + {searchKeyword === item && ( + + + + )} + + ); + })} + + {Object.keys(params).length !== 0 && } +
+ ); +}; + +export default RoadmapFilter; From 824ac6a6c8b5990f56dc548e54422f2b70d46353 Mon Sep 17 00:00:00 2001 From: solo5star Date: Fri, 6 Oct 2023 08:26:17 +0000 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=8B=A4=EC=9A=B4=20sheet=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoadmapFilter/RoadmapSelectedFilter.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx diff --git a/frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx b/frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx new file mode 100644 index 000000000..11839af55 --- /dev/null +++ b/frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx @@ -0,0 +1,18 @@ +interface RoadmapSelectedFilterProps { + itemName: string; + data: string[]; + handleFilter: (filterName, filterItem) => void; +} + +const RoadmapSelectedFilter = ({ itemName, data, handleFilter }: RoadmapSelectedFilterProps) => { + return ( +
    + {data && + data.map((item) => { + return
  • handleFilter(itemName, item)}>{item}
  • ; + })} +
+ ); +}; + +export default RoadmapSelectedFilter; From 4b9eeb7a192fb81244efb1271a2e4d4effbbe1ff Mon Sep 17 00:00:00 2001 From: solo5star Date: Fri, 6 Oct 2023 08:27:45 +0000 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20essayAnswerHandler=EC=97=90?= =?UTF-8?q?=20=EC=9E=98=EB=AA=BB=20=EC=A0=81=ED=9E=8C=20params=20=EA=B0=92?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/handlers/essayAnswers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/mocks/handlers/essayAnswers.ts b/frontend/src/mocks/handlers/essayAnswers.ts index 1a4f376cd..01fbb2bb2 100644 --- a/frontend/src/mocks/handlers/essayAnswers.ts +++ b/frontend/src/mocks/handlers/essayAnswers.ts @@ -25,9 +25,9 @@ export const essayAnswerHandler = [ ctx.json( essayAnswersMock.filter({ curriculumId: curriculumId, - keywordId: params.curriculumId ? Number(params.curriculumId) : undefined, - quizIds: params.curriculumId - ? params.curriculumId.slice(1, -1).split(',').map(Number) ?? [] + keywordId: params.keywordId ? Number(params.keywordId) : undefined, + quizIds: params.quizIds + ? params.quizIds.slice(1, -1).split(',').map(Number) ?? [] : undefined, memberIds: params.memberIds ? params.memberIds.slice(1, -1).split(',').map(Number) ?? [] From 6a3fa9dc006cc505c0665365fcebb490bca23dca Mon Sep 17 00:00:00 2001 From: solo5star Date: Fri, 6 Oct 2023 08:28:45 +0000 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20mock=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20keywordId=20=ED=83=80=EC=9E=85=20=EA=B0=80=EB=93=9C?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/fixtures/essayAnswers.ts | 47 ++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/frontend/src/mocks/fixtures/essayAnswers.ts b/frontend/src/mocks/fixtures/essayAnswers.ts index debf18cab..cffc63b6e 100644 --- a/frontend/src/mocks/fixtures/essayAnswers.ts +++ b/frontend/src/mocks/fixtures/essayAnswers.ts @@ -39,7 +39,7 @@ const data: Array<{ }, { id: 69, - keywordId: 1, + keywordId: 2, quiz: { quizId: 29, question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', @@ -58,6 +58,49 @@ const data: Array<{ }, ], }, + { + curriculumId: 2, + essayAnswers: [ + { + id: 71, + keywordId: 1, + quiz: { + quizId: 29, + question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '수를 가변적으로 조절할 수 있게 해주는 기능이다.\n
\n\n## 사용법\n***\nparameter를 여러개 정의하고 싶은 method를 선언할 때, parameter 부분에 ...을 붙이면 된다.\n\n```java\npublic int calculateSum(int... numbers) {\n return Arrays.stream(numbers).sum();\n}\n```\n\n위 코드에서 유추할 수 있듯이, varargs는 내부적으로 array를 반환한다.\ncompile 시에 Object array가 만들어지고, varargs에 할당된 인자들이 array의 원소로 들어간다.\n
\n\n## 장점\n***\n만약 위의 method에서, varargs를 사용하지 않는다고 가정해보자.\n그렇다면, parameter가 2개, 3개, 4개, ..., 인 calculateSum() method를 전부 overloading해야 할 것이다.\n\n```java\npublic int getSum(int number1, int number2) {\n\treturn number1 + number2;\n}\npublic int getSum(int number1, int number2, int number3) {\n\treturn number1 + number2 + number3;\n}\n\n...\n\n\n```\n\nvarargs를 사용하면 이러한 overloading을 하지 않도록 할 수 있다.', + author: { + id: 304, + username: 'Jaeyoung22', + nickname: 'ReO', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/89302528?v=4', + }, + createdAt: '2023-04-09T17:56:25.642891', + updatedAt: '2023-04-09T17:56:25.642891', + }, + { + id: 69, + keywordId: 1, + quiz: { + quizId: 29, + question: 'Vara하고, 사용했을 때 장점은 무엇인가요?', + }, + answer: + '# Va습니다.\n이를 해결하기 위해선 두 가지 방식을 사용했습니다.\n1. 메서드 오버로딩\n이 방법은 코드의 길이를 무한대로 증가시킵니다.\n2. 전달할 값들을 배열로 받기\n\n결국, Varargs는 **다양한 범위의 인자를 받기 위한 해결책**으로 탄생하게 되었습니다.\n\n# Varargs 사용법 및 특징\n\n```java\npublic class VarargsPractice {\n public static void main(String[] args) {\n printValues(); // 특징 1. 0개 이상의 인자를 전달할 수 있습니다.\n printValues("1");\n printValues("1", "2");\n printValues("1", "2", "3");\n printValues(new String[]{"1", "2", "3"}); // 특징 2. 실제로는 배열로 동작합니다.\n }\n\n // 사용법: 파라미터에 타입과 ... 을 함께 작성합니다. (String...)\n public static void printValues(final String... values) {\n for (int i = 0; i < values.length; i++) {\n System.out.println(values[i]); // 배열로 동작하기에 인덱스로 접근 가능합니다.\n }\n }\n}\n```\n\n# 잘못된 Varargs 사용법\n\n1. 한 메서드에 2가지 타입의 가변인자(varargs)는 사용하지 못합니다.\n```java\nvoid method(String... inputs, int... numbers);\n```\n\n2. 가변인자를 첫 번째 파라미터로 선언하지 못합니다.\n```java\nvoid method(int... numbers, String input);\n```\n\n# Varargs의 장점\n\n1. 메서드 인자의 수를 동적으로 결정 및 처리할 수 있습니다.\n2. 코드를 간결하게 유지할 수 있습니다.\n\n# Varargs의 단점\n\n1. 공간 복잡도가 증가합니다.\n가변 인자는 곧, 배열입니다.\n가변 인자를 전달받은 메서드는 매번 새로운 배열을 생성해야 합니다.\n\n# Varargs를 사용하는 기준\n(개인적인 기준입니다.)\n\n1. 인자의 개수를 다양하게 받아야 하는 상황이다.\n2. 성능상 그다지 중요하지 않은 상황이다.', + author: { + id: 287, + username: 'kdkdhoho', + nickname: '도기\uD83D\uDC36', + role: 'CREW', + imageUrl: 'https://avatars.githubusercontent.com/u/66300965?v=4', + }, + createdAt: '2023-04-08T23:54:48.667266', + updatedAt: '2023-04-08T23:54:48.667266', + }, + ], + }, ]; const essayAnswersMock = { @@ -72,7 +115,7 @@ const essayAnswersMock = { return essayAnswers .filter((essayAnswer) => { - if (essayAnswer.keywordId !== keywordId) return false; + if (keywordId && essayAnswer.keywordId !== keywordId) return false; if (memberIds && !memberIds.includes(essayAnswer.author.id)) return false; From e6877ca1d1a46c8e8149ab4393a99a4365a7fc77 Mon Sep 17 00:00:00 2001 From: solo5star Date: Fri, 6 Oct 2023 08:29:13 +0000 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20quizIds,=20memberIds=20join?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/essayanswers.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/apis/essayanswers.ts b/frontend/src/apis/essayanswers.ts index f28444664..b2720fd61 100644 --- a/frontend/src/apis/essayanswers.ts +++ b/frontend/src/apis/essayanswers.ts @@ -1,22 +1,22 @@ import { AxiosPromise, AxiosResponse } from 'axios'; import { client } from '.'; -import { EssayAnswerListRequest, EssayAnswerRequest, EssayEditRequest } from '../models/EssayAnswers'; +import { + EssayAnswerListRequest, + EssayAnswerRequest, + EssayEditRequest, +} from '../models/EssayAnswers'; import { Quiz } from '../models/Keywords'; export const requestGetEssayAnswers = async (params: EssayAnswerListRequest) => { - const { - quizIds, - memberIds, - ...otherParams - } = params; + const { quizIds, memberIds, ...otherParams } = params; const axiosParams: Omit & { quizIds?: string; memberIds?: string; } = { ...otherParams }; - if (quizIds) axiosParams.quizIds = '[' + quizIds.join(',') + ']'; - if (memberIds) axiosParams.memberIds = '[' + memberIds.join(',') + ']'; + if (quizIds) axiosParams.quizIds = quizIds.join(','); + if (memberIds) axiosParams.memberIds = memberIds.join(','); const { data } = await client.get('/essay-answers', { params: axiosParams, From 6563330f4481b51c46d6025c7f94fffb9dfc102d Mon Sep 17 00:00:00 2001 From: solo5star Date: Fri, 6 Oct 2023 08:29:46 +0000 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A4=80=EC=9D=B4=20?= =?UTF-8?q?=EB=90=98=EB=8A=94=20filter=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/EssayAnswerListPage/index.tsx | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/EssayAnswerListPage/index.tsx b/frontend/src/pages/EssayAnswerListPage/index.tsx index e348044cc..aa4bc28c8 100644 --- a/frontend/src/pages/EssayAnswerListPage/index.tsx +++ b/frontend/src/pages/EssayAnswerListPage/index.tsx @@ -1,27 +1,46 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; +import { useState } from 'react'; +import { useLocation } from 'react-router'; import { MainContentStyle } from '../../PageRouter'; import EssayAnswerList from '../../components/Lists/QuizAnswerList'; +import RoadmapFilter from '../../components/RoadmapFilter/RoadmapFilter'; import MEDIA_QUERY from '../../constants/mediaQuery'; import { useGetCurriculums } from '../../hooks/queries/curriculum'; import { useGetEssayAnswers } from '../../hooks/queries/essayanswer'; import { AlignItemsCenterStyle, FlexStyle, - JustifyContentSpaceBtwStyle + JustifyContentSpaceBtwStyle, } from '../../styles/flex.styles'; import { HeaderContainer, PostListContainer } from './styles'; -import { useLocation } from 'react-router'; + +export interface FilterlingType { + curriculumId: number; + keywordId: number; + quizIds: number[]; + memberIds: number[]; +} const EssayAnswerListPage = () => { const { curriculums } = useGetCurriculums(); const { search } = useLocation(); - const searchParams = new URLSearchParams(search); + const [searchKeyword, setSearchKeyword] = useState(''); + const searchParams = new URLSearchParams(search); // keywordId quizIds memberIds const curriculumId = Number(searchParams.get('curriculumId') ?? '1'); - const selectedCurriculum = (curriculums ?? []).find(curriculum => curriculum.id === curriculumId)?.name ?? '😎'; + const keywordId = Number(searchParams.get('keywordId') ?? undefined); + const quizIds = searchParams.get('quizIds')?.split(',').map(Number) ?? undefined; + const memberIds = searchParams.get('memberIds')?.split(',').map(Number) ?? undefined; + const selectedCurriculum = + (curriculums ?? []).find((curriculum) => curriculum.id === curriculumId)?.name ?? '😎'; - const { data: essayAnswers } = useGetEssayAnswers({ curriculumId }); + const { data: essayAnswers } = useGetEssayAnswers({ + curriculumId, + keywordId, + quizIds, + memberIds, + }); return (
@@ -49,6 +68,8 @@ const EssayAnswerListPage = () => {
+ + {(!essayAnswers || essayAnswers.length === 0) && '작성된 글이 없습니다.'} {!!essayAnswers && } From 338306bd8b3b3cc89327f336fcaee215d36868ec Mon Sep 17 00:00:00 2001 From: solo5star Date: Wed, 11 Oct 2023 08:07:24 +0000 Subject: [PATCH 12/13] refactor: Rename files --- frontend/src/mocks/fixtures/{quizs.ts => quizzes.ts} | 0 frontend/src/mocks/fixtures/{keywords.ts => roadmap.ts} | 2 +- frontend/src/mocks/handlers/index.ts | 2 +- frontend/src/mocks/handlers/{keywords.ts => roadmap.ts} | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/mocks/fixtures/{quizs.ts => quizzes.ts} (100%) rename frontend/src/mocks/fixtures/{keywords.ts => roadmap.ts} (99%) rename frontend/src/mocks/handlers/{keywords.ts => roadmap.ts} (95%) diff --git a/frontend/src/mocks/fixtures/quizs.ts b/frontend/src/mocks/fixtures/quizzes.ts similarity index 100% rename from frontend/src/mocks/fixtures/quizs.ts rename to frontend/src/mocks/fixtures/quizzes.ts diff --git a/frontend/src/mocks/fixtures/keywords.ts b/frontend/src/mocks/fixtures/roadmap.ts similarity index 99% rename from frontend/src/mocks/fixtures/keywords.ts rename to frontend/src/mocks/fixtures/roadmap.ts index 67c7e33f3..0817adeb4 100644 --- a/frontend/src/mocks/fixtures/keywords.ts +++ b/frontend/src/mocks/fixtures/roadmap.ts @@ -1,5 +1,5 @@ import { KeywordResponse, Quiz } from '../../models/Keywords'; -import quizMock from './quizs'; +import quizMock from './quizzes'; type WithSession = T & { sessionId: number; diff --git a/frontend/src/mocks/handlers/index.ts b/frontend/src/mocks/handlers/index.ts index 65d0d384a..c9183fd4b 100644 --- a/frontend/src/mocks/handlers/index.ts +++ b/frontend/src/mocks/handlers/index.ts @@ -1,6 +1,6 @@ import { commentsHandler } from './comment'; import { essayAnswerHandler } from './essayAnswers'; -import { roadmapHandler } from './keywords'; +import { roadmapHandler } from './roadmap'; import { levellogHandler } from './levellog'; import { popularStudyLogHandler } from './popularStudyLog'; diff --git a/frontend/src/mocks/handlers/keywords.ts b/frontend/src/mocks/handlers/roadmap.ts similarity index 95% rename from frontend/src/mocks/handlers/keywords.ts rename to frontend/src/mocks/handlers/roadmap.ts index 396f5bef8..8630772a6 100644 --- a/frontend/src/mocks/handlers/keywords.ts +++ b/frontend/src/mocks/handlers/roadmap.ts @@ -1,8 +1,8 @@ import { rest } from 'msw'; import { BASE_URL } from '../../configs/environment'; import curriculums from '../fixtures/curriculums'; -import keywordsMock from '../fixtures/keywords'; -import quizMock from '../fixtures/quizs'; +import keywordsMock from '../fixtures/roadmap'; +import quizMock from '../fixtures/quizzes'; import { sessionsMock } from '../fixtures/sessions'; export const roadmapHandler = [ From ca72aff129cbffaa02925930b2b4a2a8dba306a1 Mon Sep 17 00:00:00 2001 From: solo5star Date: Fri, 17 Nov 2023 08:50:36 +0000 Subject: [PATCH 13/13] =?UTF-8?q?feat:=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80=20=EC=A0=84=EC=B2=B4=20=EA=B8=80=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=95=84=ED=84=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: WaiNaat Co-authored-by: gc-park --- frontend/src/apis/essayanswers.ts | 7 + frontend/src/apis/filter.ts | 11 + .../DropdownMenu/DropdownMenu.stories.tsx | 11 +- .../DropdownMenu/DropdownMenu.styles.ts | 1 - .../DropdownMenu/DropdownMenu.styles.tsx | 42 --- .../components/DropdownMenu/DropdownMenu.tsx | 15 +- .../{FilterList.tsx => FilterList.jsx} | 0 .../src/components/Items/EssayAnswerItem.tsx | 4 +- .../src/components/Lists/QuizAnswerList.tsx | 4 +- .../RoadmapFilter/RoadmapFilter.tsx | 121 ------- .../RoadmapFilter/RoadmapSelectedFilter.tsx | 18 - .../src/components/SearchBar/SearchBar.tsx | 2 +- frontend/src/constants/reactQueryKey.ts | 2 + .../hooks/EssayAnswer/useEssayAnswerList.ts | 11 - frontend/src/hooks/queries/essayanswer.ts | 22 +- frontend/src/hooks/queries/filters.ts | 17 +- frontend/src/hooks/queries/keywords.ts | 10 +- frontend/src/mocks/fixtures/essayAnswers.ts | 8 +- frontend/src/mocks/fixtures/quizzes.ts | 19 +- frontend/src/mocks/fixtures/roadmap.ts | 308 +++++++++++------- frontend/src/mocks/handlers/roadmap.ts | 57 +--- frontend/src/models/EssayAnswers.ts | 9 +- frontend/src/models/filter.ts | 13 + .../src/pages/EditEssayAnswerPage/index.tsx | 4 +- .../RoadmapFilter/RoadmapFilter.styles.ts | 4 +- .../RoadmapFilter/RoadmapFilter.tsx | 132 ++++++++ .../src/pages/EssayAnswerListPage/index.tsx | 51 +-- .../src/pages/QuizAnswerListPage/index.tsx | 8 +- frontend/src/pages/RoadmapPage/index.tsx | 36 +- frontend/src/pages/RoadmapPage/styles.tsx | 5 + frontend/src/pages/StudylogListPage/index.tsx | 2 +- frontend/src/routes.js | 4 - 32 files changed, 500 insertions(+), 458 deletions(-) create mode 100644 frontend/src/apis/filter.ts delete mode 100644 frontend/src/components/DropdownMenu/DropdownMenu.styles.tsx rename frontend/src/components/FilterList/{FilterList.tsx => FilterList.jsx} (100%) delete mode 100644 frontend/src/components/RoadmapFilter/RoadmapFilter.tsx delete mode 100644 frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx delete mode 100644 frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts create mode 100644 frontend/src/models/filter.ts rename frontend/src/{ => pages/EssayAnswerListPage}/components/RoadmapFilter/RoadmapFilter.styles.ts (91%) create mode 100644 frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.tsx diff --git a/frontend/src/apis/essayanswers.ts b/frontend/src/apis/essayanswers.ts index 7c39f3ab4..6ba6b8cde 100644 --- a/frontend/src/apis/essayanswers.ts +++ b/frontend/src/apis/essayanswers.ts @@ -55,3 +55,10 @@ export const requestGetQuizAsync = async (quizId: number) => { export const requestGetQuiz = (quizId: Number): AxiosPromise> => client.get>(`/quizzes/${quizId}`); + +export const requestGetQuizzes = async (curriculumId: number) => { + const { data } = await client.get<{ id: number; question: string }[]>( + `/curriculums/${curriculumId}/quizzes` + ); + return data; +}; diff --git a/frontend/src/apis/filter.ts b/frontend/src/apis/filter.ts new file mode 100644 index 000000000..330950aba --- /dev/null +++ b/frontend/src/apis/filter.ts @@ -0,0 +1,11 @@ +import { client } from '.'; +import { FilterResponse } from '../models/filter'; +import { Author } from '../models/Studylogs'; + +export const getMembersForFilter = async (): Promise => { + const { + data: { members }, + } = await client.get(`/filters`); + + return members; +}; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx b/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx index 0fddab5a8..fcc5e656a 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx +++ b/frontend/src/components/DropdownMenu/DropdownMenu.stories.tsx @@ -1,13 +1,16 @@ -import DropdownMenu from './DropdownMenu'; +/** @jsxImportSource @emotion/react */ + +import DropdownMenu, { DropdownMenuProps } from './DropdownMenu'; import { Story, Meta } from '@storybook/react'; +import { css } from '@emotion/react'; export default { title: 'Component/DropdownMenu', component: DropdownMenu, argTypes: { children: { control: 'text' } }, -} as Meta; +} as Meta; -const Template: Story = (args) => ; +const Template: Story> = (args) => ; export const Basic = Template.bind({}); @@ -25,5 +28,5 @@ Basic.args = { ), - onLogoClick: () => {}, + css: css``, }; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts b/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts index a9226f09d..9538e5d0e 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts +++ b/frontend/src/components/DropdownMenu/DropdownMenu.styles.ts @@ -2,7 +2,6 @@ import { InterpolationWithTheme } from '@emotion/core'; import { Theme } from '@emotion/react'; import styled from '@emotion/styled'; import COLOR from '../../constants/color'; -import { css } from '@emotion/react'; const Container = styled.div<{ css?: InterpolationWithTheme }>` height: fit-content; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.styles.tsx b/frontend/src/components/DropdownMenu/DropdownMenu.styles.tsx deleted file mode 100644 index 499ae1e5a..000000000 --- a/frontend/src/components/DropdownMenu/DropdownMenu.styles.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import styled from '@emotion/styled'; -import COLOR from '../../constants/color'; - -const Container = styled.div<{ css: {} }>` - height: fit-content; - max-height: 32rem; - white-space: nowrap; - overflow-y: auto; - background-color: ${COLOR.WHITE}; - border-radius: 1.2rem; - box-shadow: 0px 0px 6px ${COLOR.BLACK_OPACITY_300}; - padding: 1rem 1.2rem; - position: absolute; - z-index: 100; - - && { - ${(props) => props.css} - } - - & li { - height: 4rem; - display: flex; - align-items: center; - padding: 0 0.8rem; - width: 100%; - - & > * { - width: 100%; - font-size: 1.4rem; - font-weight: 500; - color: ${COLOR.DARK_GRAY_900}; - transition: font-size 0.1s ease; - text-align: left; - } - } - - & li:not(:last-child) { - border-bottom: 0.7px solid ${COLOR.LIGHT_GRAY_700}; - } -`; - -export { Container }; diff --git a/frontend/src/components/DropdownMenu/DropdownMenu.tsx b/frontend/src/components/DropdownMenu/DropdownMenu.tsx index dd834dfb7..117152974 100644 --- a/frontend/src/components/DropdownMenu/DropdownMenu.tsx +++ b/frontend/src/components/DropdownMenu/DropdownMenu.tsx @@ -1,12 +1,15 @@ -import PropTypes from 'prop-types'; +/** @jsxImportSource @emotion/react */ + +// import PropTypes from 'prop-types'; import { Container } from './DropdownMenu.styles'; +import { css } from '@emotion/react'; -const DropdownMenu = ({ children, css }) => { - return {children}; -}; +export interface DropdownMenuProps { + css: ReturnType; +} -DropdownMenu.propTypes = { - children: PropTypes.node, +const DropdownMenu = ({ children, css }: React.PropsWithChildren) => { + return {children}; }; export default DropdownMenu; diff --git a/frontend/src/components/FilterList/FilterList.tsx b/frontend/src/components/FilterList/FilterList.jsx similarity index 100% rename from frontend/src/components/FilterList/FilterList.tsx rename to frontend/src/components/FilterList/FilterList.jsx diff --git a/frontend/src/components/Items/EssayAnswerItem.tsx b/frontend/src/components/Items/EssayAnswerItem.tsx index cafefab4b..0b2d055b0 100644 --- a/frontend/src/components/Items/EssayAnswerItem.tsx +++ b/frontend/src/components/Items/EssayAnswerItem.tsx @@ -10,10 +10,10 @@ import { import { AlignItemsEndStyle, FlexColumnStyle, FlexStyle } from '../../styles/flex.styles'; import Card from '../Card/Card'; import ProfileChip from '../ProfileChip/ProfileChip'; -import { EssayAnswerResponse } from '../../models/EssayAnswers'; +import { EssayAnswer } from '../../models/EssayAnswers'; type EssayAnswerItemProps = ( - Pick + Pick & { title?: string; showTitle?: boolean } ); diff --git a/frontend/src/components/Lists/QuizAnswerList.tsx b/frontend/src/components/Lists/QuizAnswerList.tsx index 3656ae034..e3f8e6002 100644 --- a/frontend/src/components/Lists/QuizAnswerList.tsx +++ b/frontend/src/components/Lists/QuizAnswerList.tsx @@ -2,10 +2,10 @@ import {css} from '@emotion/react'; import EssayAnswerItem from "../Items/EssayAnswerItem"; import { NoDefaultHoverLink } from '../Items/EssayAnswerItem.styles'; -import { EssayAnswerResponse } from '../../models/EssayAnswers'; +import { EssayAnswer } from '../../models/EssayAnswers'; interface QuizAnswerListProps { - essayAnswers: EssayAnswerResponse[]; + essayAnswers: EssayAnswer[]; showQuizTitle?: boolean; } diff --git a/frontend/src/components/RoadmapFilter/RoadmapFilter.tsx b/frontend/src/components/RoadmapFilter/RoadmapFilter.tsx deleted file mode 100644 index 55a2d12ad..000000000 --- a/frontend/src/components/RoadmapFilter/RoadmapFilter.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { getCurriculums } from '../../apis/curriculum'; -import DropdownMenu from '../DropdownMenu/DropdownMenu'; -import { Container, DropdownStyle, FilterContainer } from './RoadmapFilter.styles'; -import RoadmapSelectedFilter from './RoadmapSelectedFilter'; - -const filterKeyword = ['curriculumId', 'keywordId', 'quizIds', 'memberIds']; - -const filterData = { - curriculumId: [1, 2], - keywordId: [1, 2, 3, 4, 5], - quizIds: [29, 2, 3, 4], - memberIds: [304, 287, 3], -}; - -interface RoadmapFilterProps { - searchKeyword: string; - setSearchKeyword: Dispatch>; -} - -const RoadmapFilter = ({ searchKeyword, setSearchKeyword }: RoadmapFilterProps) => { - const history = useHistory(); - const [params, setParams] = useState>({}); - - const handleFilter = (filterName, filterItem) => { - if (filterName === 'curriculumId' || filterName === 'keywordId') { - if (!params[filterName] || params[filterName] !== filterItem) { - setParams({ ...params, [filterName]: filterItem }); - return; - } - - if (params[filterName] === filterItem) { - setParams((prevParams) => { - const updatedParams = { ...prevParams }; - delete updatedParams[filterName]; - - return updatedParams; - }); - return; - } - } - - if (filterName === 'quizIds' || filterName === 'memberIds') { - if (!params[filterName]) { - setParams({ ...params, [filterName]: String(filterItem) }); - return; - } - const idsList = params[filterName].split(','); - if (!idsList.includes(String(filterItem))) { - const ids = params[filterName] + ',' + String(filterItem); - setParams({ ...params, [filterName]: ids }); - return; - } - const idsFilteredList = idsList.filter((id) => Number(id) !== filterItem); - if (idsFilteredList.length === 0) { - setParams((prevParams) => { - const updatedParams = { ...prevParams }; - delete updatedParams[filterName]; - - return updatedParams; - }); - return; - } - - setParams({ ...params, [filterName]: idsFilteredList.join(',') }); - return; - } - }; - - const resetFilter = () => { - setParams({}); - }; - - const closeDropdown = () => { - setSearchKeyword(''); - }; - - useEffect(() => { - history.push(`/essay-answers?${new URLSearchParams(params).toString()}`); - }, [params]); - - useEffect(() => { - window.addEventListener('click', closeDropdown); - - return () => window.removeEventListener('click', closeDropdown); - }, []); - - return ( - - - {filterKeyword && - filterKeyword.map((item, index) => { - return ( -
event.stopPropagation()}> - - {searchKeyword === item && ( - - - - )} -
- ); - })} -
- {Object.keys(params).length !== 0 && } -
- ); -}; - -export default RoadmapFilter; diff --git a/frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx b/frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx deleted file mode 100644 index 11839af55..000000000 --- a/frontend/src/components/RoadmapFilter/RoadmapSelectedFilter.tsx +++ /dev/null @@ -1,18 +0,0 @@ -interface RoadmapSelectedFilterProps { - itemName: string; - data: string[]; - handleFilter: (filterName, filterItem) => void; -} - -const RoadmapSelectedFilter = ({ itemName, data, handleFilter }: RoadmapSelectedFilterProps) => { - return ( -
    - {data && - data.map((item) => { - return
  • handleFilter(itemName, item)}>{item}
  • ; - })} -
- ); -}; - -export default RoadmapSelectedFilter; diff --git a/frontend/src/components/SearchBar/SearchBar.tsx b/frontend/src/components/SearchBar/SearchBar.tsx index 9a241ee79..43af72e7a 100644 --- a/frontend/src/components/SearchBar/SearchBar.tsx +++ b/frontend/src/components/SearchBar/SearchBar.tsx @@ -7,7 +7,7 @@ import { ChangeEventHandler, FormEventHandler } from 'react'; interface SearchBarProps { onSubmit?: FormEventHandler; onChange: ChangeEventHandler; - css: ReturnType; + css?: ReturnType; value: string; } diff --git a/frontend/src/constants/reactQueryKey.ts b/frontend/src/constants/reactQueryKey.ts index 4f0d9cc86..5d32e9f12 100644 --- a/frontend/src/constants/reactQueryKey.ts +++ b/frontend/src/constants/reactQueryKey.ts @@ -1,8 +1,10 @@ const REACT_QUERY_KEY = { STUDYLOG: 'STUDYLOG', QUIZ: 'QUIZ', + QUIZZES: 'QUIZZES', ESSAY_ANSWER: 'ESSAY_ANSWER', QUIZ_ANSWERS: 'QUIZ_ANSWERS', + ESSAY_ANSWER_FILTER_LIST: 'ESSAY_ANSWER_FILTER_LIST', }; export default REACT_QUERY_KEY; diff --git a/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts b/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts deleted file mode 100644 index b2c702f64..000000000 --- a/frontend/src/hooks/EssayAnswer/useEssayAnswerList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useGetQuizAnswerList, useGetQuiz } from '../queries/essayanswer'; -import { useParams } from 'react-router-dom'; - -export const useEssayAnswerList = () => { - const { quizId } = useParams<{ quizId: string }>(); - - const { data: essayAnswers } = useGetQuizAnswerList({ quizId }); - const { data: quiz } = useGetQuiz({ quizId }) - - return { quiz, essayAnswers }; -}; diff --git a/frontend/src/hooks/queries/essayanswer.ts b/frontend/src/hooks/queries/essayanswer.ts index f21b4b4f2..c9988ca74 100644 --- a/frontend/src/hooks/queries/essayanswer.ts +++ b/frontend/src/hooks/queries/essayanswer.ts @@ -9,6 +9,7 @@ import { requestGetEssayAnswers, requestGetQuizAnswers, requestGetQuizAsync, + requestGetQuizzes, } from '../../apis/essayanswers'; import { ResponseError } from '../../apis/studylogs'; import { ALERT_MESSAGE, PATH } from '../../constants'; @@ -16,6 +17,7 @@ import ERROR_CODE from '../../constants/errorCode'; import { ERROR_MESSAGE, SUCCESS_MESSAGE } from '../../constants/message'; import REACT_QUERY_KEY from '../../constants/reactQueryKey'; import { + EssayAnswer, EssayAnswerFilterRequest, EssayAnswerRequest, EssayAnswerResponse, @@ -53,15 +55,15 @@ export const useEditEssayAnswer = ({ essayAnswerId }: { essayAnswerId: number }) export const useGetEssayAnswer = ( { essayAnswerId }, - { onSuccess = (essayAnswer: EssayAnswerResponse) => {}, onError = () => {} } = {} + { onSuccess = (essayAnswer: EssayAnswer) => {}, onError = () => {} } = {} ) => { const history = useHistory(); const { openSnackBar } = useSnackBar(); - return useQuery( + return useQuery( [REACT_QUERY_KEY.ESSAY_ANSWER, essayAnswerId], () => requestGetEssayAnswer(essayAnswerId), { - onSuccess: (essayAnswer: EssayAnswerResponse) => { + onSuccess: (essayAnswer: EssayAnswer) => { onSuccess?.(essayAnswer); }, onError: (error) => { @@ -93,13 +95,13 @@ export const useDeleteEssayAnswerMutation = ({ export const useGetQuizAnswerList = ( { quizId }, - { onSuccess = (essayAnswer: EssayAnswerResponse[]) => {}, onError = () => {} } = {} + { onSuccess = (essayAnswer: EssayAnswer[]) => {}, onError = () => {} } = {} ) => { - return useQuery( + return useQuery( [REACT_QUERY_KEY.QUIZ_ANSWERS, quizId], () => requestGetQuizAnswers(quizId), { - onSuccess: (essayAnswer: EssayAnswerResponse[]) => { + onSuccess: (essayAnswer: EssayAnswer[]) => { onSuccess?.(essayAnswer); }, onError: (error) => {}, @@ -110,7 +112,7 @@ export const useGetQuizAnswerList = ( }; export const useGetEssayAnswers = (filter: EssayAnswerFilterRequest) => { - return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER_FILTER_LIST, filter], () => + return useQuery([REACT_QUERY_KEY.ESSAY_ANSWER_FILTER_LIST, filter], () => requestGetEssayAnswers(filter) ); }; @@ -127,3 +129,9 @@ export const useGetQuiz = ( retry: false, }); }; + +export const useGetQuizzes = ({ curriculumId }: { curriculumId: number }) => { + return useQuery>([REACT_QUERY_KEY.QUIZZES], () => + requestGetQuizzes(curriculumId) + ); +}; diff --git a/frontend/src/hooks/queries/filters.ts b/frontend/src/hooks/queries/filters.ts index 86c6d9e38..312a2cf95 100644 --- a/frontend/src/hooks/queries/filters.ts +++ b/frontend/src/hooks/queries/filters.ts @@ -1,14 +1,14 @@ +import { useContext } from 'react'; import { useQuery } from 'react-query'; - +import { getMembersForFilter } from '../../apis/filter'; import { requestGetMissions, requestGetSessions, requestGetTags, ResponseError, } from '../../apis/studylogs'; -import { Mission, Session, Tag } from '../../models/Studylogs'; -import { useContext } from 'react'; import { UserContext } from '../../contexts/UserProvider'; +import { Author, Mission, Session, Tag } from '../../models/Studylogs'; export const useTags = () => useQuery( @@ -47,3 +47,14 @@ export const useGetMySessions = () => { { initialData: [] } ); }; + +export const useGetMembers = () => { + return useQuery( + ['memberList'], + async () => { + const members = await getMembersForFilter(); + return members; + }, + { initialData: [] } + ); +}; diff --git a/frontend/src/hooks/queries/keywords.ts b/frontend/src/hooks/queries/keywords.ts index 0f66e98a7..855db360f 100644 --- a/frontend/src/hooks/queries/keywords.ts +++ b/frontend/src/hooks/queries/keywords.ts @@ -1,6 +1,6 @@ import { useQuery } from 'react-query'; import { getKeyword, getQuizListByKeyword, getRoadmap } from '../../apis/keywords'; -import type { RoadmapRequest } from '../../models/Keywords'; +import type { KeywordResponse, RoadmapRequest } from '../../models/Keywords'; const QUERY_KEY = { keyword: 'keyword', @@ -16,12 +16,8 @@ export const useRoadmap = ({ curriculumId }: RoadmapRequest) => { }); }; -export const useGetKeyword = ({ keywordId }: { keywordId: number }) => { - const { data } = useQuery([QUERY_KEY.keyword, keywordId], () => getKeyword({ keywordId })); - - return { - keyword: data, - }; +export const useGetKeywords = ({ keywordId }: { keywordId: number }) => { + return useQuery([QUERY_KEY.keyword, keywordId], () => getKeyword({ keywordId })); }; export const useGetQuizListByKeyword = ({ keywordId }: { keywordId: number }) => { diff --git a/frontend/src/mocks/fixtures/essayAnswers.ts b/frontend/src/mocks/fixtures/essayAnswers.ts index cffc63b6e..8b1735259 100644 --- a/frontend/src/mocks/fixtures/essayAnswers.ts +++ b/frontend/src/mocks/fixtures/essayAnswers.ts @@ -1,4 +1,4 @@ -import { EssayAnswerResponse } from '../../models/EssayAnswers'; +import { EssayAnswer } from '../../models/EssayAnswers'; type EssayAnswersFilterParams = { curriculumId: number; @@ -11,7 +11,7 @@ type EssayAnswersFilterParams = { const data: Array<{ curriculumId: number; - essayAnswers: (EssayAnswerResponse & { + essayAnswers: (EssayAnswer & { keywordId: number; })[]; }> = [ @@ -23,6 +23,7 @@ const data: Array<{ keywordId: 1, quiz: { quizId: 29, + isLearning: true, question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', }, answer: @@ -42,6 +43,7 @@ const data: Array<{ keywordId: 2, quiz: { quizId: 29, + isLearning: false, question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', }, answer: @@ -66,6 +68,7 @@ const data: Array<{ keywordId: 1, quiz: { quizId: 29, + isLearning: true, question: 'Varargs는 어떻게 사용하고, 사용했을 때 장점은 무엇인가요?', }, answer: @@ -85,6 +88,7 @@ const data: Array<{ keywordId: 1, quiz: { quizId: 29, + isLearning: false, question: 'Vara하고, 사용했을 때 장점은 무엇인가요?', }, answer: diff --git a/frontend/src/mocks/fixtures/quizzes.ts b/frontend/src/mocks/fixtures/quizzes.ts index ea18ec268..95ca4b016 100644 --- a/frontend/src/mocks/fixtures/quizzes.ts +++ b/frontend/src/mocks/fixtures/quizzes.ts @@ -1,9 +1,4 @@ -import { Quiz } from '../../models/Keywords'; - -const data: Array<{ - keywordId: number; - data: Quiz[]; -}> = [ +export const quizMock = [ { keywordId: 0, data: [], @@ -230,15 +225,3 @@ const data: Array<{ ], }, ]; - -const quizMock = { - data, - findQuiz(quizId) { - return this.data.flatMap(({ data }) => data).find((quiz) => quiz.quizId === quizId) ?? null; - }, - filterByKeyword(keywordId) { - return this.data.find((quizs) => quizs.keywordId === keywordId)?.data ?? []; - }, -}; - -export default quizMock; diff --git a/frontend/src/mocks/fixtures/roadmap.ts b/frontend/src/mocks/fixtures/roadmap.ts index 0817adeb4..d8071fc19 100644 --- a/frontend/src/mocks/fixtures/roadmap.ts +++ b/frontend/src/mocks/fixtures/roadmap.ts @@ -1,80 +1,111 @@ -import { KeywordResponse, Quiz } from '../../models/Keywords'; -import quizMock from './quizzes'; +import { RoadmapResponse } from '../../models/Keywords'; -type WithSession = T & { - sessionId: number; - quizs: { - data: Quiz[]; - }; - childrenKeywords: - | (T extends { childrenKeywords: Array | null } ? WithSession[] : null) - | null; -}; - -const data: Array> = [ +const roadmapData: RoadmapResponse['data'] = [ // 세션 1 { keywordId: 1, - sessionId: 1, // for mock name: 'JavaScript', order: 1, - importance: 5, + importance: 4, parentKeywordId: null, description: '동적 타이핑, 스크립트 언어입니다.', - quizs: { - data: quizMock.filterByKeyword(1), - }, + doneQuizCount: 4, + totalQuizCount: 5, + recommendedPosts: [ + { + id: 1, + url: 'https://solo5star.tistory.com', + }, + { + id: 2, + url: 'https://solo5star.tistory.com', + }, + { + id: 3, + url: 'https://solo5star.tistory.com', + }, + { + id: 4, + url: 'https://solo5star.tistory.com', + }, + { + id: 5, + url: 'https://solo5star.tistory.com', + }, + ], childrenKeywords: [ { keywordId: 2, - sessionId: 1, // for mock name: 'let, const, var', order: 1, - importance: 5, + importance: 4, parentKeywordId: 1, description: 'let, const, var', - quizs: { - data: quizMock.filterByKeyword(2), - }, + doneQuizCount: 0, + totalQuizCount: 0, + recommendedPosts: [], childrenKeywords: [ { keywordId: 3, - sessionId: 1, // for mock name: 'let', order: 1, - importance: 5, + importance: 4, parentKeywordId: 2, description: 'let', - quizs: { - data: quizMock.filterByKeyword(3), - }, - childrenKeywords: null, + doneQuizCount: 1, + totalQuizCount: 4, + recommendedPosts: [], + childrenKeywords: [], }, { keywordId: 4, - sessionId: 1, // for mock name: 'const', order: 1, - importance: 5, + importance: 4, parentKeywordId: 2, description: 'const', - quizs: { - data: quizMock.filterByKeyword(4), - }, - childrenKeywords: null, + doneQuizCount: 3, + totalQuizCount: 3, + recommendedPosts: [ + { + id: 1, + url: 'https://solo5star.tistory.com', + }, + { + id: 2, + url: 'https://solo5star.tistory.com', + }, + ], + childrenKeywords: [], }, { keywordId: 5, - sessionId: 1, // for mock name: 'var', order: 1, - importance: 5, + importance: 2, parentKeywordId: 2, description: 'var', - quizs: { - data: quizMock.filterByKeyword(5), - }, - childrenKeywords: null, + doneQuizCount: 0, + totalQuizCount: 5, + recommendedPosts: [ + { + id: 1, + url: 'https://solo5star.tistory.com', + }, + { + id: 12, + url: 'https://solo5star.tistory.com', + }, + { + id: 13, + url: 'https://solo5star.tistory.com', + }, + { + id: 14, + url: 'https://solo5star.tistory.com', + }, + ], + childrenKeywords: [], }, ], }, @@ -83,66 +114,110 @@ const data: Array> = [ // 세션 1 - React 키워드 { keywordId: 6, - sessionId: 1, // for mock name: 'React', order: 1, - importance: 5, + importance: 4, parentKeywordId: null, description: 'React입니다.', - quizs: { - data: quizMock.filterByKeyword(6), - }, + doneQuizCount: 3, + totalQuizCount: 3, + recommendedPosts: [], childrenKeywords: [ { keywordId: 7, - sessionId: 1, // for mock name: 'lifecycle', order: 1, - importance: 5, + importance: 3, parentKeywordId: 6, description: 'lifecycle 설명', - quizs: { - data: quizMock.filterByKeyword(7), - }, + doneQuizCount: 2, + totalQuizCount: 4, + recommendedPosts: [], childrenKeywords: [ { keywordId: 8, - sessionId: 1, // for mock name: 'mount', order: 1, - importance: 5, + importance: 1, parentKeywordId: 7, description: 'mount 설명', - quizs: { - data: quizMock.filterByKeyword(8), - }, - childrenKeywords: null, + doneQuizCount: 1, + totalQuizCount: 0, + recommendedPosts: [], + childrenKeywords: [], }, { keywordId: 9, - sessionId: 1, // for mock name: 'unmount', order: 1, importance: 5, parentKeywordId: 7, description: 'unmount 설명', - quizs: { - data: quizMock.filterByKeyword(9), - }, - childrenKeywords: null, + doneQuizCount: 0, + totalQuizCount: 0, + recommendedPosts: [], + childrenKeywords: [], }, { keywordId: 10, - sessionId: 1, // for mock name: 'update', order: 1, - importance: 5, + importance: 4, parentKeywordId: 7, description: 'update 설명', - quizs: { - data: quizMock.filterByKeyword(10), - }, - childrenKeywords: null, + doneQuizCount: 0, + totalQuizCount: 2, + recommendedPosts: [], + childrenKeywords: [], + }, + ], + }, + { + keywordId: 14, + name: 'hooks', + order: 1, + importance: 4, + parentKeywordId: 6, + description: 'hooks 설명', + doneQuizCount: 0, + totalQuizCount: 1, + recommendedPosts: [], + childrenKeywords: [ + { + keywordId: 15, + name: 'useState', + order: 1, + importance: 4, + parentKeywordId: 14, + description: 'useState 설명', + doneQuizCount: 5, + totalQuizCount: 5, + recommendedPosts: [], + childrenKeywords: [], + }, + { + keywordId: 16, + name: 'useEffect', + order: 1, + importance: 4, + parentKeywordId: 14, + description: 'useEffect 설명', + doneQuizCount: 4, + totalQuizCount: 5, + recommendedPosts: [], + childrenKeywords: [], + }, + { + keywordId: 17, + name: 'useMemo', + order: 1, + importance: 4, + parentKeywordId: 14, + description: 'useMemo 설명', + doneQuizCount: 0, + totalQuizCount: 2, + recommendedPosts: [], + childrenKeywords: [], }, ], }, @@ -151,40 +226,49 @@ const data: Array> = [ // 세션 2 - Test { keywordId: 11, - sessionId: 2, // for mock name: 'Test', order: 1, - importance: 5, + importance: 3, parentKeywordId: null, description: 'Test입니다.', - quizs: { - data: quizMock.filterByKeyword(11), - }, + doneQuizCount: 0, + totalQuizCount: 4, + recommendedPosts: [], childrenKeywords: [ { keywordId: 12, - sessionId: 2, // for mock name: 'Jest', order: 1, - importance: 5, + importance: 1, parentKeywordId: 11, description: 'Jest 설명', - quizs: { - data: quizMock.filterByKeyword(12), - }, + doneQuizCount: 0, + totalQuizCount: 2, + recommendedPosts: [], childrenKeywords: [ { keywordId: 13, - sessionId: 2, // for mock - name: 'ReactTestingLibrary', + name: 'React Testing Library', order: 1, - importance: 5, + importance: 1, parentKeywordId: 12, description: 'ReactTestingLibrary 설명', - quizs: { - data: quizMock.filterByKeyword(13), - }, - childrenKeywords: null, + doneQuizCount: 0, + totalQuizCount: 3, + recommendedPosts: [], + childrenKeywords: [], + }, + { + keywordId: 18, + name: 'jest', + order: 1, + importance: 2, + parentKeywordId: 12, + description: 'jest 설명', + doneQuizCount: 0, + totalQuizCount: 6, + recommendedPosts: [], + childrenKeywords: [], }, ], }, @@ -192,14 +276,10 @@ const data: Array> = [ }, ]; -const keywordMock = { - data, - filterKeywordsBySession(sessionId: string | readonly string[]) { - const filteredData = this.data.filter((item) => item.sessionId === Number(sessionId)); - - return { - data: filteredData, - }; +export default { + data: roadmapData, + getKeywords() { + return this.data; }, findKeyword(keywordId: string | readonly string[]) { // data를 순회하면서, childrenKeywords를 순회하면서 해당 keyword가 있는지 확인한다. @@ -210,40 +290,24 @@ const keywordMock = { return depth1Item; } - return ( - depth1Item.childrenKeywords?.map((depth2Item) => { - // 2뎁스 순회 - if (depth2Item.keywordId === Number(keywordId)) { - return depth2Item; - } + return depth1Item.childrenKeywords.map((depth2Item) => { + // 2뎁스 순회 + if (depth2Item.keywordId === Number(keywordId)) { + return depth2Item; + } - return ( - depth2Item.childrenKeywords?.map((depth3Item) => { - // 3뎁스 순회 - if (depth3Item.keywordId === Number(keywordId)) { - return depth3Item; - } + return depth2Item.childrenKeywords.map((depth3Item) => { + // 3뎁스 순회 + if (depth3Item.keywordId === Number(keywordId)) { + return depth3Item; + } - return undefined; - }) ?? [] - ); - }) ?? [] - ); + return undefined; + }); + }); }) .find((item) => item !== undefined); return data; }, - // 6-1 - filterChildrenKeywords(keywordId: string | readonly string[]) { - const childrenKeywords = this.data.find((depth1Item) => { - return depth1Item.keywordId === Number(keywordId); - })?.childrenKeywords; - - return { - childrenKeywords, - }; - }, }; - -export default keywordMock; diff --git a/frontend/src/mocks/handlers/roadmap.ts b/frontend/src/mocks/handlers/roadmap.ts index 8630772a6..c15c3d1f1 100644 --- a/frontend/src/mocks/handlers/roadmap.ts +++ b/frontend/src/mocks/handlers/roadmap.ts @@ -1,66 +1,37 @@ import { rest } from 'msw'; import { BASE_URL } from '../../configs/environment'; import curriculums from '../fixtures/curriculums'; -import keywordsMock from '../fixtures/roadmap'; -import quizMock from '../fixtures/quizzes'; -import { sessionsMock } from '../fixtures/sessions'; +import { quizMock } from '../fixtures/quizzes'; +import roadmapMock from '../fixtures/roadmap'; export const roadmapHandler = [ - // 커리큘럼 목록 조회 rest.get(`${BASE_URL}/curriculums`, (req, res, ctx) => { return res(ctx.status(200), ctx.json(curriculums)); }), - // 커리큘럼별 Session 목록 조회 - rest.get(`${BASE_URL}/curriculums/:curriculumId/sessions`, (req, res, ctx) => { - const { - params: { curriculumId }, - } = req; - - const sessions = sessionsMock[Number(curriculumId)]; - - return res(ctx.status(200), ctx.json(sessions)); - }), - - /** 5. 세션별 키워드 목록 조회. 1 depth */ - rest.get(`${BASE_URL}/sessions/:sessionId/keywords`, (req, res, ctx) => { - const { - params: { sessionId }, - } = req; + rest.get(`${BASE_URL}/roadmaps`, (req, res, ctx) => { + const { url } = req; + const curriculumId = url.searchParams.get('curriculumId'); - const keywordsList = keywordsMock.filterKeywordsBySession(sessionId); + const keywordsList = roadmapMock.getKeywords(); - return res(ctx.status(200), ctx.json({ ...keywordsList })); + return res(ctx.status(200), ctx.json({ data: keywordsList })); }), - /** 4. 키워드 단건 조회. 1, 2, 3 depth */ - rest.get(`${BASE_URL}/sessions/:sessionId/keywords/:keywordId`, (req, res, ctx) => { + rest.get(`${BASE_URL}/keywords/:keywordId`, (req, res, ctx) => { const { - params: { sessionId, keywordId }, + params: { keywordId }, } = req; - const keywordData = keywordsMock.findKeyword(keywordId); + const keywordData = roadmapMock.findKeyword(keywordId); return res(ctx.status(200), ctx.json({ ...keywordData })); }), - /** 6-1. 1 -> 2,3 키워드 조회 */ - rest.get(`${BASE_URL}/sessions/:sessionId/keywords/:keywordId/children`, (req, res, ctx) => { - const { - params: { sessionId, keywordId }, - } = req; - - const childrenKeywords = keywordsMock.filterChildrenKeywords(keywordId); - - return res(ctx.status(200), ctx.json({ ...childrenKeywords })); - }), - - /** 10. 키워드별 Quiz 조회 */ - rest.get(`${BASE_URL}/sessions/:sessionId/keywords/:keywordId/quizs`, (req, res, ctx) => { - const { - params: { sessionId, keywordId }, - } = req; + rest.get(`${BASE_URL}/quizzes`, (req, res, ctx) => { + const { url } = req; + const keywordId = url.searchParams.get('keywordId'); - return res(ctx.status(200), ctx.json(quizMock.findQuiz(Number(keywordId)))); + return res(ctx.status(200), ctx.json(quizMock[Number(keywordId)])); }), ]; diff --git a/frontend/src/models/EssayAnswers.ts b/frontend/src/models/EssayAnswers.ts index d9adecb87..3b893bfef 100644 --- a/frontend/src/models/EssayAnswers.ts +++ b/frontend/src/models/EssayAnswers.ts @@ -8,7 +8,7 @@ export interface EssayAnswerRequest { export type EssayEditRequest = Pick; -export interface EssayAnswerResponse { +export interface EssayAnswer { id: number; quiz: Quiz; answer: string; @@ -17,6 +17,13 @@ export interface EssayAnswerResponse { updatedAt: string; } +export interface EssayAnswerResponse { + data: EssayAnswer[]; + totalSize: number; + totalPage: number; + currPage: number; +} + export type EssayAnswerFilter = { curriculumId: number; keywordId?: number; diff --git a/frontend/src/models/filter.ts b/frontend/src/models/filter.ts new file mode 100644 index 000000000..e7c66cfbb --- /dev/null +++ b/frontend/src/models/filter.ts @@ -0,0 +1,13 @@ +import type { Author } from './Studylogs'; + +interface FilterData { + id: number; + name: string; +} + +export interface FilterResponse { + sessions: FilterData[]; + missions: unknown[]; + tags: FilterData[]; + members: Author[]; +} diff --git a/frontend/src/pages/EditEssayAnswerPage/index.tsx b/frontend/src/pages/EditEssayAnswerPage/index.tsx index cb142934c..b3b7b18b7 100644 --- a/frontend/src/pages/EditEssayAnswerPage/index.tsx +++ b/frontend/src/pages/EditEssayAnswerPage/index.tsx @@ -12,7 +12,7 @@ import {useContext, useEffect, useRef, useState} from "react"; import { UserContext } from '../../contexts/UserProvider'; import { useHistory, useParams } from 'react-router'; import { useEditEssayAnswer, useGetEssayAnswer } from '../../hooks/queries/essayanswer'; -import { EssayAnswerResponse } from '../../models/EssayAnswers'; +import { EssayAnswer } from '../../models/EssayAnswers'; const EditEssayAnswerPage = () => { const history = useHistory(); @@ -28,7 +28,7 @@ const EditEssayAnswerPage = () => { const previousEssayAnswer = useGetEssayAnswer( { essayAnswerId: id }, { - onSuccess: ({ quiz: { question }, answer }: EssayAnswerResponse) => { + onSuccess: ({ quiz: { question }, answer }: EssayAnswer) => { setAnswer(answer); setQuizTitle(question); }, diff --git a/frontend/src/components/RoadmapFilter/RoadmapFilter.styles.ts b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.styles.ts similarity index 91% rename from frontend/src/components/RoadmapFilter/RoadmapFilter.styles.ts rename to frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.styles.ts index 38e7e990c..8fa79dd9a 100644 --- a/frontend/src/components/RoadmapFilter/RoadmapFilter.styles.ts +++ b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.styles.ts @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import COLOR from '../../constants/color'; -import MEDIA_QUERY from '../../constants/mediaQuery'; +import COLOR from '../../../../constants/color'; +import MEDIA_QUERY from '../../../../constants/mediaQuery'; export const Container = styled.div` background-color: ${COLOR.LIGHT_GRAY_50}; diff --git a/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.tsx b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.tsx new file mode 100644 index 000000000..18c560177 --- /dev/null +++ b/frontend/src/pages/EssayAnswerListPage/components/RoadmapFilter/RoadmapFilter.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from 'react'; +import DropdownMenu from '../../../../components/DropdownMenu/DropdownMenu'; +import { useGetQuizzes } from '../../../../hooks/queries/essayanswer'; +import { useGetMembers } from '../../../../hooks/queries/filters'; +import { useRoadmap } from '../../../../hooks/queries/keywords'; +import { KeywordResponse } from '../../../../models/Keywords'; +import { Container, DropdownStyle, FilterContainer } from './RoadmapFilter.styles'; + +const filterKoreanNames: Record = { + keywordId: '주제', + memberIds: '회원', + quizIds: '질문', +} + +const useGetKeywords = ({ curriculumId }: { curriculumId: number }) => { + const { data: roadmap } = useRoadmap({ curriculumId }); + + const extractKeywords = ( + keywordResponse: KeywordResponse + ): Pick[] => { + return [ + { keywordId: keywordResponse.keywordId, name: keywordResponse.name }, + ...keywordResponse.childrenKeywords.map(extractKeywords).flat(), + ]; + }; + + return roadmap?.data.map(extractKeywords).flat() ?? []; +}; + +interface RoadmapFilterProps { + curriculumId: number; + filter: Record; + onFilterChange: (filter: Record) => void; +} + +const RoadmapFilter = ({ curriculumId, filter, onFilterChange }: RoadmapFilterProps) => { + const [activeFilterKeyword, setActiveFilterKeyword] = useState(null); + + const keywords = useGetKeywords({ curriculumId }); + const { data: quizzes } = useGetQuizzes({ curriculumId }); + const { data: members } = useGetMembers(); + + const filterData: Record> = { + keywordId: keywords.map((keyword) => ({ key: String(keyword.keywordId), label: keyword.name })), + memberIds: members?.map((member) => ({ key: String(member.id), label: member.nickname })) ?? [], + quizIds: quizzes?.map((quiz) => ({ key: String(quiz.id), label: quiz.question })) ?? [], + }; + + const handleFilter = (filterName: string, filterItemKey: string) => { + if (filterName === 'curriculumId' || filterName === 'keywordId') { + if (!filter[filterName] || filter[filterName] !== filterItemKey) { + onFilterChange({ ...filter, [filterName]: filterItemKey }); + return; + } + + if (filter[filterName] === filterItemKey) { + const updatedFilter = { ...filter }; + delete updatedFilter[filterName]; + + onFilterChange(updatedFilter); + return; + } + } + + if (filterName === 'quizIds' || filterName === 'memberIds') { + if (!filter[filterName]) { + onFilterChange({ ...filter, [filterName]: String(filterItemKey) }); + return; + } + const idsList = filter[filterName].split(','); + if (!idsList.includes(String(filterItemKey))) { + const ids = filter[filterName] + ',' + String(filterItemKey); + onFilterChange({ ...filter, [filterName]: ids }); + return; + } + const idsFilteredList = idsList.filter((id) => id !== filterItemKey); + if (idsFilteredList.length === 0) { + const updatedFilter = { ...filter }; + delete updatedFilter[filterName]; + + onFilterChange(updatedFilter); + return; + } + + onFilterChange({ ...filter, [filterName]: idsFilteredList.join(',') }); + return; + } + }; + + const resetFilter = () => { + onFilterChange({}); + }; + + const closeDropdown = () => { + setActiveFilterKeyword(null); + }; + + useEffect(() => { + window.addEventListener('click', closeDropdown); + + return () => window.removeEventListener('click', closeDropdown); + }, []); + + return ( + + + { + Object.keys(filterData).map((filterKeyword, index) => ( +
event.stopPropagation()}> + + {activeFilterKeyword === filterKeyword && ( + +
    + {filterData[filterKeyword].map((item) => ( +
  • handleFilter(filterKeyword, item.key)}>{item.label}
  • + ))} +
+
+ )} +
+ ))} +
+ {Object.keys(filter).length !== 0 && } +
+ ); +}; + +export default RoadmapFilter; diff --git a/frontend/src/pages/EssayAnswerListPage/index.tsx b/frontend/src/pages/EssayAnswerListPage/index.tsx index aa4bc28c8..18f6a0c4b 100644 --- a/frontend/src/pages/EssayAnswerListPage/index.tsx +++ b/frontend/src/pages/EssayAnswerListPage/index.tsx @@ -1,11 +1,11 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import { useState } from 'react'; -import { useLocation } from 'react-router'; +import { useEffect, useMemo, useState } from 'react'; +import { useHistory, useLocation } from 'react-router'; import { MainContentStyle } from '../../PageRouter'; import EssayAnswerList from '../../components/Lists/QuizAnswerList'; -import RoadmapFilter from '../../components/RoadmapFilter/RoadmapFilter'; +import RoadmapFilter from './components/RoadmapFilter/RoadmapFilter'; import MEDIA_QUERY from '../../constants/mediaQuery'; import { useGetCurriculums } from '../../hooks/queries/curriculum'; import { useGetEssayAnswers } from '../../hooks/queries/essayanswer'; @@ -17,31 +17,42 @@ import { import { HeaderContainer, PostListContainer } from './styles'; export interface FilterlingType { - curriculumId: number; - keywordId: number; - quizIds: number[]; - memberIds: number[]; + curriculumId: string; + keywordId: string; + quizIds: string; + memberIds: string; } const EssayAnswerListPage = () => { + const history = useHistory(); const { curriculums } = useGetCurriculums(); const { search } = useLocation(); - const [searchKeyword, setSearchKeyword] = useState(''); - const searchParams = new URLSearchParams(search); // keywordId quizIds memberIds - const curriculumId = Number(searchParams.get('curriculumId') ?? '1'); - const keywordId = Number(searchParams.get('keywordId') ?? undefined); - const quizIds = searchParams.get('quizIds')?.split(',').map(Number) ?? undefined; - const memberIds = searchParams.get('memberIds')?.split(',').map(Number) ?? undefined; + + const [filter, setFilter] = useState>(() => + Object.fromEntries(new URLSearchParams(search).entries()) + ); + const selectedCurriculum = - (curriculums ?? []).find((curriculum) => curriculum.id === curriculumId)?.name ?? '😎'; + (curriculums ?? []).find((curriculum) => curriculum.id === Number(filter.curriculumId))?.name ?? + '😎'; - const { data: essayAnswers } = useGetEssayAnswers({ - curriculumId, - keywordId, - quizIds, - memberIds, + const { curriculumId, keywordId, quizIds, memberIds } = filter; + const { data: { data: essayAnswers } = { data: [] } } = useGetEssayAnswers({ + curriculumId: Number(curriculumId), + keywordId: keywordId ? Number(keywordId) : undefined, + quizIds: quizIds ? quizIds.split(',').map(Number) : undefined, + memberIds: memberIds ? memberIds.split(',').map(Number) : undefined, }); + const handleFilterChange = (filter: Record) => { + filter['curriculumId'] = curriculumId; + setFilter(filter); + } + + useEffect(() => { + history.replace(`/essay-answers?${new URLSearchParams(filter).toString()}`); + }, [filter]); + return (
@@ -68,7 +79,7 @@ const EssayAnswerListPage = () => {
- + {(!essayAnswers || essayAnswers.length === 0) && '작성된 글이 없습니다.'} diff --git a/frontend/src/pages/QuizAnswerListPage/index.tsx b/frontend/src/pages/QuizAnswerListPage/index.tsx index f86215b51..232f8e8ae 100644 --- a/frontend/src/pages/QuizAnswerListPage/index.tsx +++ b/frontend/src/pages/QuizAnswerListPage/index.tsx @@ -1,9 +1,10 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; +import { useParams } from 'react-router'; import QuizAnswerList from '../../components/Lists/QuizAnswerList'; import MEDIA_QUERY from '../../constants/mediaQuery'; -import { useEssayAnswerList } from '../../hooks/EssayAnswer/useEssayAnswerList'; +import { useGetQuiz, useGetQuizAnswerList } from '../../hooks/queries/essayanswer'; import { MainContentStyle } from '../../PageRouter'; import { AlignItemsCenterStyle, @@ -13,7 +14,10 @@ import { import { HeaderContainer, PostListContainer } from './styles'; const QuizAnswerListPage = () => { - const { quiz, essayAnswers } = useEssayAnswerList(); + const { quizId } = useParams<{ quizId: string }>(); + + const { data: essayAnswers } = useGetQuizAnswerList({ quizId }); + const { data: quiz } = useGetQuiz({ quizId }); return (
diff --git a/frontend/src/pages/RoadmapPage/index.tsx b/frontend/src/pages/RoadmapPage/index.tsx index b2ee342a7..ad6806524 100644 --- a/frontend/src/pages/RoadmapPage/index.tsx +++ b/frontend/src/pages/RoadmapPage/index.tsx @@ -9,6 +9,7 @@ import { useGetCurriculums } from '../../hooks/queries/curriculum'; import ImportanceLegend from './components/ImportanceLegend/ImportanceLegend'; import ResponsiveButton from '../../components/Button/ResponsiveButton'; import { COLOR } from '../../constants'; +import { Link } from 'react-router-dom'; const lastSeenCurriculumId = Number(localStorage.getItem('curriculumId') ?? 1); @@ -52,22 +53,25 @@ const RoadmapPage = () => {
커리큘럼 - - {curriculums?.map((curriculum) => ( - handleClickCurriculum(curriculum.id)} - text={curriculum.name} - color={selectedCurriculumId === curriculum.id ? COLOR.WHITE : COLOR.BLACK_600} - backgroundColor={ - selectedCurriculumId === curriculum.id - ? `hsl(${getHueHeuristically(curriculum.name)}, 50%, 40%)` - : COLOR.LIGHT_GRAY_400 - } - height="32px" - /> - ))} - + + + {curriculums?.map((curriculum) => ( + handleClickCurriculum(curriculum.id)} + text={curriculum.name} + color={selectedCurriculumId === curriculum.id ? COLOR.WHITE : COLOR.BLACK_600} + backgroundColor={ + selectedCurriculumId === curriculum.id + ? `hsl(${getHueHeuristically(curriculum.name)}, 50%, 40%)` + : COLOR.LIGHT_GRAY_400 + } + height="32px" + /> + ))} + + {selectedCurriculum && 전체 답변 보러가기} +
diff --git a/frontend/src/pages/RoadmapPage/styles.tsx b/frontend/src/pages/RoadmapPage/styles.tsx index 1f8960834..575de0a4b 100644 --- a/frontend/src/pages/RoadmapPage/styles.tsx +++ b/frontend/src/pages/RoadmapPage/styles.tsx @@ -24,6 +24,11 @@ export const RoadmapContainer = styled.div` margin: 0 -4rem; `; +export const RoadmapHeader = styled.div` + display: flex; + justify-content: space-between; +` + export const CurriculumButtonList = styled.div` display: flex; flex-wrap: wrap; diff --git a/frontend/src/pages/StudylogListPage/index.tsx b/frontend/src/pages/StudylogListPage/index.tsx index f3f43d8a5..6acdf9370 100644 --- a/frontend/src/pages/StudylogListPage/index.tsx +++ b/frontend/src/pages/StudylogListPage/index.tsx @@ -157,7 +157,7 @@ const StudylogListPage = (): JSX.Element => { {/* 타입스크립트 일부 적용 이슈로 인한 css 빈 string 전달 */} onSearchKeywordsChange(value)} value={searchKeywords} />
diff --git a/frontend/src/routes.js b/frontend/src/routes.js index 9893416db..b8b0c7bfb 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -13,10 +13,6 @@ import { ProfilePageStudylogs, QuizAnswerListPage, RoadmapPage, - NewEssayAnswerPage, - EssayAnswerPage, - EssayAnswerListPage, - EditEssayAnswerPage, NewArticlePage, ArticleListPage, StudylogListPage,