From 2d2a8246d895e9c608ae562ff3e35b99bf8c9293 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Mon, 16 Dec 2024 11:51:57 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=E2=9C=A8=20Feature(#87):=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=95=98=EC=9D=B4=EB=9D=BC=EC=9D=B4=ED=8C=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 10 +++ package.json | 1 + src/features/quiz/service/emptyChangeToDiv.ts | 2 +- src/features/quiz/service/useCodeHighlight.ts | 33 ++++++++ src/features/quiz/ui/Combination.tsx | 2 +- src/features/quiz/ui/MultipleChoice.tsx | 2 +- src/features/quiz/ui/OXSelector.tsx | 2 +- src/features/quiz/ui/PartClear.tsx | 2 +- src/features/quiz/ui/PartNavContainer.tsx | 2 +- src/features/quiz/ui/Question.tsx | 81 +++++-------------- src/features/quiz/ui/QuizSection.tsx | 2 +- src/features/quiz/ui/Result.tsx | 2 +- src/features/quiz/ui/ShortAnswer.tsx | 2 +- src/features/quiz/ui/TotalResults.tsx | 2 +- src/features/quiz/{ => ui}/styles.css | 5 +- src/features/quiz/{ => ui}/styles.ts | 11 ++- 16 files changed, 86 insertions(+), 75 deletions(-) create mode 100644 src/features/quiz/service/useCodeHighlight.ts rename src/features/quiz/{ => ui}/styles.css (80%) rename src/features/quiz/{ => ui}/styles.ts (97%) diff --git a/package-lock.json b/package-lock.json index 9f8d31d..978952b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@types/dompurify": "^3.0.5", "axios": "^1.7.7", "dompurify": "^3.1.7", + "highlight.js": "^11.10.0", "html-react-parser": "^5.1.18", "query-string": "^9.1.1", "react": "^18.3.1", @@ -2491,6 +2492,15 @@ "node": ">=8" } }, + "node_modules/highlight.js": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", diff --git a/package.json b/package.json index 8e9c3b1..cc91dba 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/dompurify": "^3.0.5", "axios": "^1.7.7", "dompurify": "^3.1.7", + "highlight.js": "^11.10.0", "html-react-parser": "^5.1.18", "query-string": "^9.1.1", "react": "^18.3.1", diff --git a/src/features/quiz/service/emptyChangeToDiv.ts b/src/features/quiz/service/emptyChangeToDiv.ts index cea67d1..171965d 100644 --- a/src/features/quiz/service/emptyChangeToDiv.ts +++ b/src/features/quiz/service/emptyChangeToDiv.ts @@ -1,7 +1,7 @@ const emptyChangeToDiv = (text: string) => { let index = 0; const newText = text.replace(/(#empty#)/g, () => { - const replacement = `
`; + const replacement = ``; index++; return replacement; }); diff --git a/src/features/quiz/service/useCodeHighlight.ts b/src/features/quiz/service/useCodeHighlight.ts new file mode 100644 index 0000000..5fe2873 --- /dev/null +++ b/src/features/quiz/service/useCodeHighlight.ts @@ -0,0 +1,33 @@ +import hljs from 'highlight.js'; +import { useLayoutEffect, DependencyList, useState } from 'react'; +import emptyChangeToDiv from '@/features/quiz/service/emptyChangeToDiv'; + +/** + * 코드 하이라이팅을 적용하는 커스텀텀훅 + * + * 이 훅은 `dependency` 값이 변경될 때마다 ref로 지정된 코드 블록()을 하이라이팅합니다. + * + * @param {DependencyList} deps - 하이라이팅을 다시 적용할 조건이 되는 의존성. 의존성이 변경될 때마다 하이라이팅이 갱신됩니다. + * @returns {React.RefObject} - 코드 블록을 참조할 수 있는 `ref` + * + * @example + * const codeRef = useCodeHighlight(code); + * return
{code}
; + */ +const useCodeHighlight = (code: string, deps?: DependencyList) => { + const [highlightCode, setHighlightCode] = useState(''); + useLayoutEffect(() => { + hljs.configure({ ignoreUnescapedHTML: true }); + const highlightedCode = hljs.highlight(code, { + language: 'javascript', + }).value; + const splittedCode = highlightedCode.split('\n'); + const lineAttachedCode = splittedCode + .map((code, i) => `
${i + 1} | ${code}`) + .join('\n'); + setHighlightCode(emptyChangeToDiv(lineAttachedCode)); + }, deps); + return highlightCode; +}; + +export default useCodeHighlight; diff --git a/src/features/quiz/ui/Combination.tsx b/src/features/quiz/ui/Combination.tsx index 6057daf..82dc1f9 100644 --- a/src/features/quiz/ui/Combination.tsx +++ b/src/features/quiz/ui/Combination.tsx @@ -1,5 +1,5 @@ import Quiz from '../../../types/Quiz'; -import { CombinationSection, TextBlockButton } from '../styles'; +import { CombinationSection, TextBlockButton } from './styles'; import { useClientQuizStore } from '../../../store/useClientQuizStore'; import compact from '../../../utils/compact'; interface CombinationProps { diff --git a/src/features/quiz/ui/MultipleChoice.tsx b/src/features/quiz/ui/MultipleChoice.tsx index 4d45bba..9abdfc8 100644 --- a/src/features/quiz/ui/MultipleChoice.tsx +++ b/src/features/quiz/ui/MultipleChoice.tsx @@ -4,7 +4,7 @@ import { MultipleChoiceSection, MultipleChoiceButtonDiv, Img, -} from '../styles'; +} from './styles'; import { useClientQuizStore } from '../../../store/useClientQuizStore'; const IMG_BASE_URL = import.meta.env.VITE_IMG_BASE_URL; interface MultipleChoiceProps { diff --git a/src/features/quiz/ui/OXSelector.tsx b/src/features/quiz/ui/OXSelector.tsx index 6008ee4..2f5464e 100644 --- a/src/features/quiz/ui/OXSelector.tsx +++ b/src/features/quiz/ui/OXSelector.tsx @@ -1,4 +1,4 @@ -import { Img, OXButtonSection } from '../styles'; +import { Img, OXButtonSection } from './styles'; import { useClientQuizStore } from '../../../store/useClientQuizStore'; const IMG_BASE_URL = import.meta.env.VITE_IMG_BASE_URL; diff --git a/src/features/quiz/ui/PartClear.tsx b/src/features/quiz/ui/PartClear.tsx index 86615e0..1097a21 100644 --- a/src/features/quiz/ui/PartClear.tsx +++ b/src/features/quiz/ui/PartClear.tsx @@ -1,5 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import { CompensationSection } from '../styles'; +import { CompensationSection } from './styles'; import { pointQuery } from '@queries/usersQuery'; import { useTimeout } from '@modern-kit/react'; import useUserStore from '@store/useUserStore'; diff --git a/src/features/quiz/ui/PartNavContainer.tsx b/src/features/quiz/ui/PartNavContainer.tsx index 8f62ec3..2e37d13 100644 --- a/src/features/quiz/ui/PartNavContainer.tsx +++ b/src/features/quiz/ui/PartNavContainer.tsx @@ -7,7 +7,7 @@ import { SectionTitle, ButtonGrid, KeyboardButton, -} from '../styles'; +} from './styles'; import getPartGridPosition from '../../learn/service/getPartGridPosition'; import { getImageUrl } from '@utils/getImageUrl'; diff --git a/src/features/quiz/ui/Question.tsx b/src/features/quiz/ui/Question.tsx index ae93fe6..94b9bd3 100644 --- a/src/features/quiz/ui/Question.tsx +++ b/src/features/quiz/ui/Question.tsx @@ -1,75 +1,32 @@ -import { QuestionSection, TextBlockButton, Titlediv } from './../styles'; +import * as S from './styles'; import { useClientQuizStore } from '../../../store/useClientQuizStore'; -import parse, { HTMLReactParserOptions, Element } from 'html-react-parser'; -import '../styles.css'; -import emptyChangeToDiv from '../service/emptyChangeToDiv'; -import lineChanger from '../service/lineChanger'; -import { useRef } from 'react'; -import Dompurify from 'dompurify'; +import './styles.css'; import Quiz from '../../../types/Quiz'; +import 'highlight.js/styles/base16/atelier-cave-light.css'; +import useCodeHighlight from '@/features/quiz/service/useCodeHighlight'; +import emptyChangeToDiv from '@/features/quiz/service/emptyChangeToDiv'; +import dompurify from 'dompurify'; +import parse from 'html-react-parser'; + interface QuestionProps { title: Quiz['title']; question: Quiz['question']; category: Quiz['category']; } export default function Question({ title, question, category }: QuestionProps) { - const { - currentPage, - userResponseAnswer, - swapUserResponseAnswer, - spliceUserResponseAnswer, - } = useClientQuizStore(); - - //\n을 줄바꿈 요소로 변경 - const lineChangeQuestion = lineChanger(question); - //#empty#을 html Element로 변경 - const nonEmptyQuestion = emptyChangeToDiv(lineChangeQuestion); - const dragItem = useRef(null); // 드래그할 아이템의 인덱스 - const dragOverItem = useRef(null); // 드랍할 위치의 아이템의 인덱스 - const options: HTMLReactParserOptions = { - replace: domNode => { - //class명이 empty인(빈칸) html Element를 찾음 - if (domNode instanceof Element && domNode.attribs.class === 'empty') { - //그 노드의 id를 가져옴 - const id = Number(domNode.attribs.id.match(/\d+$/)); - //전역상태(유저의 응답) 배열에서 해당 id의 값 가져와서 빈칸을 TextBlock 컴포넌트로 변경 해당 인덱스가 빈칸이면 그대로 domNod - return userResponseAnswer[id] ? ( - spliceUserResponseAnswer(id)} - draggable - onDragStart={() => { - dragOverItem.current = id; - }} - onDragEnter={() => { - dragOverItem.current = id; - }} - onDragEnd={() => { - if (dragItem.current !== null && dragOverItem.current != null) { - swapUserResponseAnswer(dragItem.current, dragOverItem.current); - } - }} - onDragOver={e => e.preventDefault()} - > - {userResponseAnswer[id]} - - ) : ( - domNode - ); - } - }, - }; - + const { currentPage } = useClientQuizStore(); + const code = useCodeHighlight(question, [question]); return ( - - + +

문제{currentPage + 1}.

{title}

-
-
- {/* Dompurify를 이용한 xss공격 방어 문자열 랜더링*/} - {/*
{parse(Dompurify.sanitize(nonEmptyQuestion), options)}
*/} - {parse(Dompurify.sanitize(nonEmptyQuestion), options)} -
-
+ + + + {parse(dompurify.sanitize(code))} + + + ); } diff --git a/src/features/quiz/ui/QuizSection.tsx b/src/features/quiz/ui/QuizSection.tsx index 856d371..64aa60f 100644 --- a/src/features/quiz/ui/QuizSection.tsx +++ b/src/features/quiz/ui/QuizSection.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from 'react'; -import { QuizBox } from '../styles'; +import { QuizBox } from './styles'; export default function QuizSection({ children }: PropsWithChildren) { return {children}; diff --git a/src/features/quiz/ui/Result.tsx b/src/features/quiz/ui/Result.tsx index 1838dfe..f4ee818 100644 --- a/src/features/quiz/ui/Result.tsx +++ b/src/features/quiz/ui/Result.tsx @@ -2,7 +2,7 @@ import { progressQuery } from '../../../queries/usersQuery'; import { useClientQuizStore } from '../../../store/useClientQuizStore'; import useUserStore from '../../../store/useUserStore'; import Quiz from '../../../types/Quiz'; -import { AnswerDiv, NextPageButton, ScoreSection } from '../styles'; +import { AnswerDiv, NextPageButton, ScoreSection } from './styles'; import { getImageUrl } from '@utils/getImageUrl'; interface ResultProps { diff --git a/src/features/quiz/ui/ShortAnswer.tsx b/src/features/quiz/ui/ShortAnswer.tsx index 76c4232..636a233 100644 --- a/src/features/quiz/ui/ShortAnswer.tsx +++ b/src/features/quiz/ui/ShortAnswer.tsx @@ -1,5 +1,5 @@ import { useClientQuizStore } from '../../../store/useClientQuizStore'; -import { ShortAnswerSection, ShortAnswerInput, Img } from '../styles'; +import { ShortAnswerSection, ShortAnswerInput, Img } from './styles'; const IMG_BASE_URL = import.meta.env.VITE_IMG_BASE_URL; export default function ShortAnswer() { diff --git a/src/features/quiz/ui/TotalResults.tsx b/src/features/quiz/ui/TotalResults.tsx index 0af3a36..c20df5e 100644 --- a/src/features/quiz/ui/TotalResults.tsx +++ b/src/features/quiz/ui/TotalResults.tsx @@ -9,7 +9,7 @@ import { CompensationSection, TotalResultsRewardDiv, TotalResultsTextDiv, -} from '../styles'; +} from './styles'; import { getImageUrl } from '@utils/getImageUrl'; import { useTimeout } from '@modern-kit/react'; import { useNavigate } from 'react-router-dom'; diff --git a/src/features/quiz/styles.css b/src/features/quiz/ui/styles.css similarity index 80% rename from src/features/quiz/styles.css rename to src/features/quiz/ui/styles.css index af07405..a81c187 100644 --- a/src/features/quiz/styles.css +++ b/src/features/quiz/ui/styles.css @@ -3,11 +3,14 @@ height: 10px; } .empty { - width: 150px; + position: relative; + display: inline-block; + width: 100px; height: 26px; border: 2px solid #00b6c0; border-radius: 8px; background: #e8e7e7; + top: 8px; } .text-block { cursor: pointer; diff --git a/src/features/quiz/styles.ts b/src/features/quiz/ui/styles.ts similarity index 97% rename from src/features/quiz/styles.ts rename to src/features/quiz/ui/styles.ts index 6769d82..46f378b 100644 --- a/src/features/quiz/styles.ts +++ b/src/features/quiz/ui/styles.ts @@ -1,5 +1,5 @@ import styled, { css, keyframes } from 'styled-components'; -import Quiz from '../../types/Quiz'; +import Quiz from '../../../types/Quiz'; const imgUrl = import.meta.env.VITE_IMG_BASE_URL; @@ -32,7 +32,7 @@ export const QuestionSection = styled.section<{ padding: 26px 0 0 80px; } `; -export const Titlediv = styled.div<{ +export const Title = styled.h3<{ $category: Quiz['category']; }>` display: flex; @@ -418,3 +418,10 @@ export const KeyboardButton = styled.button` display: block; } `; +export const Pre = styled.pre` + padding: 20px 0 0 20px; +`; +export const Code = styled.code` + background-color: transparent; + line-height: 1.5; +`; From eb4da06dbec4d92054158f6f28374d8156544e05 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Mon, 16 Dec 2024 12:08:18 +0900 Subject: [PATCH 02/13] =?UTF-8?q?=E2=9C=A8=20Feature(#87):=20=ED=9B=85,=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quiz/service/addLineNumbersToCode.ts | 8 ++++++++ ...ToDiv.ts => replaceEmptyWithHTMLElement.ts} | 4 ++-- src/features/quiz/service/useCodeHighlight.ts | 18 +++++++++--------- src/features/quiz/ui/Question.tsx | 10 +++++++--- 4 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 src/features/quiz/service/addLineNumbersToCode.ts rename src/features/quiz/service/{emptyChangeToDiv.ts => replaceEmptyWithHTMLElement.ts} (68%) diff --git a/src/features/quiz/service/addLineNumbersToCode.ts b/src/features/quiz/service/addLineNumbersToCode.ts new file mode 100644 index 0000000..9de9ee2 --- /dev/null +++ b/src/features/quiz/service/addLineNumbersToCode.ts @@ -0,0 +1,8 @@ +const addLineNumbersToCode = (code: string) => { + const splittedCode = code.split('\n'); + const lineAttachedCode = splittedCode + .map((code, i) => `
${i + 1} | ${code}`) + .join('\n'); + return lineAttachedCode; +}; +export default addLineNumbersToCode; diff --git a/src/features/quiz/service/emptyChangeToDiv.ts b/src/features/quiz/service/replaceEmptyWithHTMLElement.ts similarity index 68% rename from src/features/quiz/service/emptyChangeToDiv.ts rename to src/features/quiz/service/replaceEmptyWithHTMLElement.ts index 171965d..7c6448f 100644 --- a/src/features/quiz/service/emptyChangeToDiv.ts +++ b/src/features/quiz/service/replaceEmptyWithHTMLElement.ts @@ -1,4 +1,4 @@ -const emptyChangeToDiv = (text: string) => { +const replaceEmptyWithHTMLElement = (text: string) => { let index = 0; const newText = text.replace(/(#empty#)/g, () => { const replacement = ``; @@ -8,4 +8,4 @@ const emptyChangeToDiv = (text: string) => { return newText; }; -export default emptyChangeToDiv; +export default replaceEmptyWithHTMLElement; diff --git a/src/features/quiz/service/useCodeHighlight.ts b/src/features/quiz/service/useCodeHighlight.ts index 5fe2873..7086520 100644 --- a/src/features/quiz/service/useCodeHighlight.ts +++ b/src/features/quiz/service/useCodeHighlight.ts @@ -1,11 +1,13 @@ import hljs from 'highlight.js'; -import { useLayoutEffect, DependencyList, useState } from 'react'; -import emptyChangeToDiv from '@/features/quiz/service/emptyChangeToDiv'; +import { useLayoutEffect, DependencyList, useState, useEffect } from 'react'; +import replaceEmptyWithHTMLElement from './replaceEmptyWithHTMLElement'; /** * 코드 하이라이팅을 적용하는 커스텀텀훅 * - * 이 훅은 `dependency` 값이 변경될 때마다 ref로 지정된 코드 블록()을 하이라이팅합니다. + * 이 훅은 `dependency` 값이 변경될 때마다 코드를 하이라이팅 합니다. + * #empty#로 되어있는 부분을 빈 코드블럭으로 변경시켜줍니다. + * 문제 페이지에서 사용되는 훅입니다. * * @param {DependencyList} deps - 하이라이팅을 다시 적용할 조건이 되는 의존성. 의존성이 변경될 때마다 하이라이팅이 갱신됩니다. * @returns {React.RefObject} - 코드 블록을 참조할 수 있는 `ref` @@ -16,17 +18,15 @@ import emptyChangeToDiv from '@/features/quiz/service/emptyChangeToDiv'; */ const useCodeHighlight = (code: string, deps?: DependencyList) => { const [highlightCode, setHighlightCode] = useState(''); - useLayoutEffect(() => { + + useEffect(() => { hljs.configure({ ignoreUnescapedHTML: true }); const highlightedCode = hljs.highlight(code, { language: 'javascript', }).value; - const splittedCode = highlightedCode.split('\n'); - const lineAttachedCode = splittedCode - .map((code, i) => `
${i + 1} | ${code}`) - .join('\n'); - setHighlightCode(emptyChangeToDiv(lineAttachedCode)); + setHighlightCode(highlightedCode); }, deps); + return highlightCode; }; diff --git a/src/features/quiz/ui/Question.tsx b/src/features/quiz/ui/Question.tsx index 94b9bd3..9be9e0f 100644 --- a/src/features/quiz/ui/Question.tsx +++ b/src/features/quiz/ui/Question.tsx @@ -4,9 +4,10 @@ import './styles.css'; import Quiz from '../../../types/Quiz'; import 'highlight.js/styles/base16/atelier-cave-light.css'; import useCodeHighlight from '@/features/quiz/service/useCodeHighlight'; -import emptyChangeToDiv from '@/features/quiz/service/emptyChangeToDiv'; import dompurify from 'dompurify'; import parse from 'html-react-parser'; +import replaceEmptyWithHTMLElement from '@/features/quiz/service/replaceEmptyWithHTMLElement'; +import addLineNumbersToCode from '@/features/quiz/service/addLineNumbersToCode'; interface QuestionProps { title: Quiz['title']; @@ -15,7 +16,10 @@ interface QuestionProps { } export default function Question({ title, question, category }: QuestionProps) { const { currentPage } = useClientQuizStore(); - const code = useCodeHighlight(question, [question]); + const highlightCode = useCodeHighlight(question, [question, currentPage]); + const replaceEmptyCode = replaceEmptyWithHTMLElement(highlightCode); + const addLineNumberCode = addLineNumbersToCode(replaceEmptyCode); + return ( @@ -24,7 +28,7 @@ export default function Question({ title, question, category }: QuestionProps) { - {parse(dompurify.sanitize(code))} + {parse(dompurify.sanitize(addLineNumberCode))} From 13f89300709ed2afc59ab0947c474babaadbff66 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Mon, 16 Dec 2024 13:49:41 +0900 Subject: [PATCH 03/13] =?UTF-8?q?=E2=9C=A8=20Feature(#87):=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=EC=97=90=20=EB=A7=9E=EB=8A=94=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B8=94=EB=A1=9D=20=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/quiz/ui/Question.tsx | 17 ++++++++++++++--- src/features/quiz/ui/Result.tsx | 3 +-- src/features/quiz/ui/TextBlock.tsx | 11 +++++++++++ src/features/quiz/ui/styles.ts | 13 ++++++++++--- src/pages/quiz/Quiz.tsx | 3 +-- 5 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 src/features/quiz/ui/TextBlock.tsx diff --git a/src/features/quiz/ui/Question.tsx b/src/features/quiz/ui/Question.tsx index 9be9e0f..5651816 100644 --- a/src/features/quiz/ui/Question.tsx +++ b/src/features/quiz/ui/Question.tsx @@ -5,9 +5,10 @@ import Quiz from '../../../types/Quiz'; import 'highlight.js/styles/base16/atelier-cave-light.css'; import useCodeHighlight from '@/features/quiz/service/useCodeHighlight'; import dompurify from 'dompurify'; -import parse from 'html-react-parser'; +import parse, { HTMLReactParserOptions, Element } from 'html-react-parser'; import replaceEmptyWithHTMLElement from '@/features/quiz/service/replaceEmptyWithHTMLElement'; import addLineNumbersToCode from '@/features/quiz/service/addLineNumbersToCode'; +import TextBlock from '@/features/quiz/ui/TextBlock'; interface QuestionProps { title: Quiz['title']; @@ -15,11 +16,21 @@ interface QuestionProps { category: Quiz['category']; } export default function Question({ title, question, category }: QuestionProps) { - const { currentPage } = useClientQuizStore(); + const { currentPage, userResponseAnswer } = useClientQuizStore(); + const highlightCode = useCodeHighlight(question, [question, currentPage]); const replaceEmptyCode = replaceEmptyWithHTMLElement(highlightCode); const addLineNumberCode = addLineNumbersToCode(replaceEmptyCode); + const options: HTMLReactParserOptions = { + replace(domNode) { + if (domNode instanceof Element && domNode.attribs.class === 'empty') { + const id = Number(domNode.attribs.id.match(/\d+$/)); + + return ; + } + }, + }; return ( @@ -28,7 +39,7 @@ export default function Question({ title, question, category }: QuestionProps) { - {parse(dompurify.sanitize(addLineNumberCode))} + {parse(dompurify.sanitize(addLineNumberCode), options)} diff --git a/src/features/quiz/ui/Result.tsx b/src/features/quiz/ui/Result.tsx index f4ee818..fb1d786 100644 --- a/src/features/quiz/ui/Result.tsx +++ b/src/features/quiz/ui/Result.tsx @@ -17,8 +17,7 @@ export default function Result({ result, closeModal, }: ResultProps) { - const { nextPage, resetUserResponseAnswer, pushTotalResults, currentPage } = - useClientQuizStore(); + const { nextPage, resetUserResponseAnswer } = useClientQuizStore(); //임시 유저 가져오기 const { user } = useUserStore(); const userId = user?.id; diff --git a/src/features/quiz/ui/TextBlock.tsx b/src/features/quiz/ui/TextBlock.tsx new file mode 100644 index 0000000..a9e1461 --- /dev/null +++ b/src/features/quiz/ui/TextBlock.tsx @@ -0,0 +1,11 @@ +import * as S from './styles'; +interface TextBlockProps { + text: string; +} +export default function TextBlock({ text }: TextBlockProps) { + return ( + <> + {text ?? ''} + + ); +} diff --git a/src/features/quiz/ui/styles.ts b/src/features/quiz/ui/styles.ts index 46f378b..8db45c0 100644 --- a/src/features/quiz/ui/styles.ts +++ b/src/features/quiz/ui/styles.ts @@ -164,10 +164,15 @@ export const TextBlockButton = styled.button<{ $selected?: boolean }>` //화면 하단의 export const EmptyDiv = styled.div` + position: relative; + display: inline-block; width: 100px; - height: 20px; - background-color: gray; - border-radius: 15px; + height: 26px; + border: 2px solid #00b6c0; + border-radius: 8px; + background: #e8e7e7; + top: 8px; + line-height: 1.5; `; const fadeIn = keyframes` from { @@ -418,9 +423,11 @@ export const KeyboardButton = styled.button` display: block; } `; + export const Pre = styled.pre` padding: 20px 0 0 20px; `; + export const Code = styled.code` background-color: transparent; line-height: 1.5; diff --git a/src/pages/quiz/Quiz.tsx b/src/pages/quiz/Quiz.tsx index 52cfcc3..da47787 100644 --- a/src/pages/quiz/Quiz.tsx +++ b/src/pages/quiz/Quiz.tsx @@ -58,7 +58,6 @@ export default function Quiz() { pushTotalResults, } = useClientQuizStore(); const { user } = useUserStore(); - const [result, setResult] = useState(false); const { Modal, closeModal, openModal, isShow } = useModal(); const { partId, state } = useLocation().state as { partId: number; @@ -86,7 +85,7 @@ export default function Quiz() { if (totalResults.length !== 0) { openModal(); } - }, [totalResults, result]); + }, [totalResults]); useUnmount(() => reset()); useBeforeUnload({ enabled: !isQuizFinished, From 1f6011ab34ed35020048a22e174c0f80df6fb860 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Mon, 16 Dec 2024 14:05:46 +0900 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=90=9E=20BugFix(#87):=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=ED=95=B4=EC=9E=AC?= =?UTF-8?q?=EB=A1=9C=20=EC=95=88=EC=A0=95=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/Modal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/layout/Modal.tsx b/src/common/layout/Modal.tsx index ff4f3da..7e64fbe 100644 --- a/src/common/layout/Modal.tsx +++ b/src/common/layout/Modal.tsx @@ -13,6 +13,7 @@ export default function Modal({ useEffect(() => { if (isShow) { document.body.style.overflow = 'hidden'; + (document.activeElement as HTMLElement).blur(); } else { document.body.style.overflow = 'auto'; } From 92234ae43b70edf35a9b8a934f2cb06ebb01653b Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Wed, 18 Dec 2024 12:47:48 +0900 Subject: [PATCH 05/13] =?UTF-8?q?=E2=9C=A8=20Feature(#87):=20=EB=93=9C?= =?UTF-8?q?=EB=A0=88=EA=B7=B8=EC=97=94=EB=93=9C=EB=A1=AD=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 --- .../quiz/service/addLineNumbersToCode.ts | 2 +- src/features/quiz/ui/Combination.tsx | 42 +++++++++++++--- src/features/quiz/ui/Question.tsx | 5 +- src/features/quiz/ui/TextBlock.tsx | 50 ++++++++++++++++++- src/features/quiz/ui/styles.ts | 15 +++--- src/store/useClientQuizStore.ts | 7 +++ src/store/useDnDStore.ts | 28 +++++++++++ 7 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 src/store/useDnDStore.ts diff --git a/src/features/quiz/service/addLineNumbersToCode.ts b/src/features/quiz/service/addLineNumbersToCode.ts index 9de9ee2..518cea1 100644 --- a/src/features/quiz/service/addLineNumbersToCode.ts +++ b/src/features/quiz/service/addLineNumbersToCode.ts @@ -1,7 +1,7 @@ const addLineNumbersToCode = (code: string) => { const splittedCode = code.split('\n'); const lineAttachedCode = splittedCode - .map((code, i) => `
${i + 1} | ${code}`) + .map((code, i) => `${i + 1} | ${code}`) .join('\n'); return lineAttachedCode; }; diff --git a/src/features/quiz/ui/Combination.tsx b/src/features/quiz/ui/Combination.tsx index 82dc1f9..a8d7e8b 100644 --- a/src/features/quiz/ui/Combination.tsx +++ b/src/features/quiz/ui/Combination.tsx @@ -1,23 +1,41 @@ -import Quiz from '../../../types/Quiz'; +import Quiz from '@type/Quiz'; import { CombinationSection, TextBlockButton } from './styles'; -import { useClientQuizStore } from '../../../store/useClientQuizStore'; +import { useClientQuizStore } from '@/store/useClientQuizStore'; import compact from '../../../utils/compact'; +import { useDnDStore } from '@/store/useDnDStore'; +import { useEffect } from 'react'; + interface CombinationProps { answerChoice: Quiz['answerChoice']; answer: Quiz['answer']; } + export default function Combination({ answerChoice, answer, }: CombinationProps) { - const { userResponseAnswer, pushUserResponseAnswer } = useClientQuizStore(); - + const { userResponseAnswer, pushUserResponseAnswer, setUserResponseAtIndex } = + useClientQuizStore(); + const { setdragStartItem, dragOverItem, dragStartItem } = useDnDStore(); + useEffect(() => { + for (let i = 0; i < answer.length; i++) { + setUserResponseAtIndex('', i); + } + }, []); return ( <> - {answerChoice.map(value => { + {answerChoice.map((value, index) => { const isSelect = userResponseAnswer.includes(value); - return ( + return isSelect ? ( + + {value} + + ) : ( { @@ -25,8 +43,16 @@ export default function Combination({ answer.length > compact(userResponseAnswer).length && pushUserResponseAnswer(value); }} - $selected={isSelect} - disabled={isSelect} + draggable + onDragStart={() => setdragStartItem({ value, index })} + onDragEnd={() => { + if (dragOverItem && dragStartItem) { + setUserResponseAtIndex( + dragStartItem?.value, + dragOverItem?.index + ); + } + }} > {value} diff --git a/src/features/quiz/ui/Question.tsx b/src/features/quiz/ui/Question.tsx index 5651816..da6acfd 100644 --- a/src/features/quiz/ui/Question.tsx +++ b/src/features/quiz/ui/Question.tsx @@ -9,6 +9,7 @@ import parse, { HTMLReactParserOptions, Element } from 'html-react-parser'; import replaceEmptyWithHTMLElement from '@/features/quiz/service/replaceEmptyWithHTMLElement'; import addLineNumbersToCode from '@/features/quiz/service/addLineNumbersToCode'; import TextBlock from '@/features/quiz/ui/TextBlock'; +import { useState } from 'react'; interface QuestionProps { title: Quiz['title']; @@ -26,11 +27,11 @@ export default function Question({ title, question, category }: QuestionProps) { replace(domNode) { if (domNode instanceof Element && domNode.attribs.class === 'empty') { const id = Number(domNode.attribs.id.match(/\d+$/)); - - return ; + return ; } }, }; + return ( diff --git a/src/features/quiz/ui/TextBlock.tsx b/src/features/quiz/ui/TextBlock.tsx index a9e1461..4a6ac48 100644 --- a/src/features/quiz/ui/TextBlock.tsx +++ b/src/features/quiz/ui/TextBlock.tsx @@ -1,11 +1,57 @@ +import { useClientQuizStore } from '@/store/useClientQuizStore'; import * as S from './styles'; +import { useDnDStore } from '@/store/useDnDStore'; interface TextBlockProps { text: string; + index: number; } -export default function TextBlock({ text }: TextBlockProps) { +export default function TextBlock({ text, index }: TextBlockProps) { + const { + spliceUserResponseAnswer, + swapUserResponseAnswer, + setUserResponseAtIndex, + } = useClientQuizStore(); + const { setdragStartItem, setdragOverItem, drop } = useDnDStore(); return ( <> - {text ?? ''} + {!text ? ( + setdragOverItem({ value: '', index })} + /> + ) : ( + setdragStartItem({ value: text, index })} + onDragEnter={() => setdragOverItem({ value: text, index })} + onDragLeave={() => { + console.log('벗어남'); + }} + onDragEnd={() => + drop((dragStartItem, dragOverItem) => { + if (dragStartItem && dragOverItem) { + //빈칸에 드레그했을 때 + if (dragOverItem.value === '') { + setUserResponseAtIndex( + dragStartItem?.value, + dragOverItem?.index + ); + spliceUserResponseAnswer(dragStartItem?.index); + //텍스트가 있는 칸에 드레그했을 때때 + } else { + swapUserResponseAnswer( + dragStartItem.index, + dragOverItem.index + ); + } + } + }) + } + onClick={() => spliceUserResponseAnswer(index)} + > + {text} + + )} ); } diff --git a/src/features/quiz/ui/styles.ts b/src/features/quiz/ui/styles.ts index 8db45c0..5a66d11 100644 --- a/src/features/quiz/ui/styles.ts +++ b/src/features/quiz/ui/styles.ts @@ -143,8 +143,11 @@ export const CombinationSection = styled.section` //블럭유형에서 각 텍스트에 해당하는 리스트 스타일 export const TextBlockButton = styled.button<{ $selected?: boolean }>` + position: relative; + display: inline-block; padding: 0 20px; border-radius: 8px; + line-height: 1.5; height: 26px; border: 2px solid #a5ecf0; background-color: #f4f4f4; @@ -158,6 +161,7 @@ export const TextBlockButton = styled.button<{ $selected?: boolean }>` background-color: #00d9e9; border-color: #00b6c0; color: #00d9e9; + cursor: default; `}; `; @@ -165,14 +169,15 @@ export const TextBlockButton = styled.button<{ $selected?: boolean }>` export const EmptyDiv = styled.div` position: relative; - display: inline-block; + display: inline-flex; width: 100px; height: 26px; border: 2px solid #00b6c0; border-radius: 8px; background: #e8e7e7; - top: 8px; line-height: 1.5; + vertical-align: middle; + font-size: 18px; `; const fadeIn = keyframes` from { @@ -426,9 +431,7 @@ export const KeyboardButton = styled.button` export const Pre = styled.pre` padding: 20px 0 0 20px; -`; - -export const Code = styled.code` - background-color: transparent; line-height: 1.5; `; + +export const Code = styled.code``; diff --git a/src/store/useClientQuizStore.ts b/src/store/useClientQuizStore.ts index 8164fdd..b53eaf3 100644 --- a/src/store/useClientQuizStore.ts +++ b/src/store/useClientQuizStore.ts @@ -7,6 +7,7 @@ interface State { interface Actions { nextPage: () => void; setUserResponseAnswer: (userResposne: string) => void; + setUserResponseAtIndex: (userResponse: string, index: number) => void; pushUserResponseAnswer: (userResponse: string) => void; spliceUserResponseAnswer: (choiceIndex: number) => void; swapUserResponseAnswer: (index1: number, index2: number) => void; @@ -23,6 +24,12 @@ export const useClientQuizStore = create(set => ({ //유저의 응답을 추가(단답형, OX, 객관식 ) setUserResponseAnswer: userResposne => set(() => ({ userResponseAnswer: [userResposne] })), + //유저의 응답을 특정 인덱스에 업데이트 + setUserResponseAtIndex: (userResponse, index) => + set(state => { + state.userResponseAnswer[index] = userResponse; + return { userResponseAnswer: state.userResponseAnswer }; + }), //유저 응답 누적 (조합형, 답이 여러개인 것) pushUserResponseAnswer: userResponse => set(state => { diff --git a/src/store/useDnDStore.ts b/src/store/useDnDStore.ts new file mode 100644 index 0000000..7725809 --- /dev/null +++ b/src/store/useDnDStore.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand'; +type dragItemType = { value: string; index: number } | null; +interface State { + dragStartItem: dragItemType; + dragOverItem: dragItemType; +} +interface Action { + setdragStartItem: (item: dragItemType) => void; + setdragOverItem: (item: dragItemType) => void; + drop: ( + callback: (dragStartItem: dragItemType, dragOverItem: dragItemType) => void + ) => void; + reset: () => void; +} +export const useDnDStore = create((set, get) => ({ + dragStartItem: null, + dragOverItem: null, + setdragStartItem: index => set(() => ({ dragStartItem: index })), + + setdragOverItem: index => set(() => ({ dragOverItem: index })), + + reset: () => set(() => ({ dragOverItem: null, dragStartItem: null })), + drop: callback => { + const { dragStartItem, dragOverItem, reset } = get(); + callback(dragStartItem, dragOverItem); + reset(); + }, +})); From c85b9336a49bb088cef4231315e72681e20b8616 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Thu, 19 Dec 2024 17:11:52 +0900 Subject: [PATCH 06/13] =?UTF-8?q?=E2=9C=A8=20Feature(#87):=20=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/quiz/ui/Combination.tsx | 11 +++++--- src/features/quiz/ui/Question.tsx | 16 +++++++++--- src/features/quiz/ui/TextBlock.tsx | 39 ++++++++++++++++++---------- src/features/quiz/ui/styles.css | 3 +++ src/hooks/useDragAndDrop.ts | 20 ++++++++++++++ src/store/useDnDStore.ts | 26 ++++++++++++------- 6 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 src/hooks/useDragAndDrop.ts diff --git a/src/features/quiz/ui/Combination.tsx b/src/features/quiz/ui/Combination.tsx index a8d7e8b..3ed1bbf 100644 --- a/src/features/quiz/ui/Combination.tsx +++ b/src/features/quiz/ui/Combination.tsx @@ -16,7 +16,7 @@ export default function Combination({ }: CombinationProps) { const { userResponseAnswer, pushUserResponseAnswer, setUserResponseAtIndex } = useClientQuizStore(); - const { setdragStartItem, dragOverItem, dragStartItem } = useDnDStore(); + const { setDragStartItem, dragOverItem, dragStartItem } = useDnDStore(); useEffect(() => { for (let i = 0; i < answer.length; i++) { setUserResponseAtIndex('', i); @@ -44,14 +44,19 @@ export default function Combination({ pushUserResponseAnswer(value); }} draggable - onDragStart={() => setdragStartItem({ value, index })} - onDragEnd={() => { + onDragStart={e => { + e.currentTarget.classList.add('drag-start'); + setDragStartItem({ value, index }); + }} + onDragEnd={e => { + e.preventDefault(); if (dragOverItem && dragStartItem) { setUserResponseAtIndex( dragStartItem?.value, dragOverItem?.index ); } + e.currentTarget.classList.remove('drag-start'); }} > {value} diff --git a/src/features/quiz/ui/Question.tsx b/src/features/quiz/ui/Question.tsx index da6acfd..3f37cee 100644 --- a/src/features/quiz/ui/Question.tsx +++ b/src/features/quiz/ui/Question.tsx @@ -10,6 +10,7 @@ import replaceEmptyWithHTMLElement from '@/features/quiz/service/replaceEmptyWit import addLineNumbersToCode from '@/features/quiz/service/addLineNumbersToCode'; import TextBlock from '@/features/quiz/ui/TextBlock'; import { useState } from 'react'; +import { useDnDStore } from '@/store/useDnDStore'; interface QuestionProps { title: Quiz['title']; @@ -17,8 +18,9 @@ interface QuestionProps { category: Quiz['category']; } export default function Question({ title, question, category }: QuestionProps) { - const { currentPage, userResponseAnswer } = useClientQuizStore(); - + const { currentPage, userResponseAnswer, spliceUserResponseAnswer } = + useClientQuizStore(); + const { setOutsideDropZone } = useDnDStore(); const highlightCode = useCodeHighlight(question, [question, currentPage]); const replaceEmptyCode = replaceEmptyWithHTMLElement(highlightCode); const addLineNumberCode = addLineNumbersToCode(replaceEmptyCode); @@ -33,7 +35,15 @@ export default function Question({ title, question, category }: QuestionProps) { }; return ( - + { + if (e.currentTarget === e.target) { + setOutsideDropZone(true); + } + }} + >

문제{currentPage + 1}.

{title}

diff --git a/src/features/quiz/ui/TextBlock.tsx b/src/features/quiz/ui/TextBlock.tsx index 4a6ac48..cd21e4d 100644 --- a/src/features/quiz/ui/TextBlock.tsx +++ b/src/features/quiz/ui/TextBlock.tsx @@ -11,42 +11,53 @@ export default function TextBlock({ text, index }: TextBlockProps) { swapUserResponseAnswer, setUserResponseAtIndex, } = useClientQuizStore(); - const { setdragStartItem, setdragOverItem, drop } = useDnDStore(); + const { + setDragStartItem, + setDragOverItem, + drop, + isOutsideDropZone, + setOutsideDropZone, + } = useDnDStore(); return ( <> {!text ? ( setdragOverItem({ value: '', index })} + onDragEnter={() => { + setOutsideDropZone(false); + setDragOverItem({ value: '', index }); + }} /> ) : ( setdragStartItem({ value: text, index })} - onDragEnter={() => setdragOverItem({ value: text, index })} - onDragLeave={() => { - console.log('벗어남'); + onDragStart={e => { + e.currentTarget.classList.add('drag-start'); + setDragStartItem({ value: text, index }); }} - onDragEnd={() => - drop((dragStartItem, dragOverItem) => { - if (dragStartItem && dragOverItem) { - //빈칸에 드레그했을 때 + onDragEnter={() => setDragOverItem({ value: text, index })} + onDragEnd={e => { + e.preventDefault(); + if (isOutsideDropZone) { + spliceUserResponseAnswer(index); + } else { + drop((dragStartItem, dragOverItem) => { if (dragOverItem.value === '') { setUserResponseAtIndex( dragStartItem?.value, dragOverItem?.index ); spliceUserResponseAnswer(dragStartItem?.index); - //텍스트가 있는 칸에 드레그했을 때때 } else { swapUserResponseAnswer( dragStartItem.index, dragOverItem.index ); + e.currentTarget.classList.remove('drag-start'); } - } - }) - } + }); + } + }} onClick={() => spliceUserResponseAnswer(index)} > {text} diff --git a/src/features/quiz/ui/styles.css b/src/features/quiz/ui/styles.css index a81c187..74a1098 100644 --- a/src/features/quiz/ui/styles.css +++ b/src/features/quiz/ui/styles.css @@ -21,3 +21,6 @@ padding: 0 20px; height: 26px; } +.drag-start { + opacity: 50%; +} diff --git a/src/hooks/useDragAndDrop.ts b/src/hooks/useDragAndDrop.ts new file mode 100644 index 0000000..d70912d --- /dev/null +++ b/src/hooks/useDragAndDrop.ts @@ -0,0 +1,20 @@ +import { useRef } from 'react'; + +const useDragAndDrop = () => { + const dragItem = useRef(null); // Specify the type for dragItem + const dragOverItem = useRef(null); // Specify the type for dragOverItem + + const dragStart = (position: number) => { + dragItem.current = position; + }; + + const dragEnter = (position: number) => { + dragOverItem.current = position; + }; + + const drop = (callback: () => void) => { + callback(); + }; + return { dragStart, dragEnter, drop }; +}; +export default useDragAndDrop; diff --git a/src/store/useDnDStore.ts b/src/store/useDnDStore.ts index 7725809..a62fd5b 100644 --- a/src/store/useDnDStore.ts +++ b/src/store/useDnDStore.ts @@ -1,28 +1,34 @@ import { create } from 'zustand'; -type dragItemType = { value: string; index: number } | null; +type dragItemType = { value: string; index: number }; interface State { - dragStartItem: dragItemType; - dragOverItem: dragItemType; + dragStartItem: dragItemType | null; + dragOverItem: dragItemType | null; + isOutsideDropZone: boolean; } interface Action { - setdragStartItem: (item: dragItemType) => void; - setdragOverItem: (item: dragItemType) => void; + setDragStartItem: (item: dragItemType) => void; + setDragOverItem: (item: dragItemType) => void; drop: ( callback: (dragStartItem: dragItemType, dragOverItem: dragItemType) => void ) => void; + setOutsideDropZone: (isOutSide: boolean) => void; reset: () => void; } export const useDnDStore = create((set, get) => ({ dragStartItem: null, dragOverItem: null, - setdragStartItem: index => set(() => ({ dragStartItem: index })), + isOutsideDropZone: false, - setdragOverItem: index => set(() => ({ dragOverItem: index })), - - reset: () => set(() => ({ dragOverItem: null, dragStartItem: null })), + setDragStartItem: item => set(() => ({ dragStartItem: item })), + setDragOverItem: item => set(() => ({ dragOverItem: item })), + setOutsideDropZone: isOutSide => + set(() => ({ isOutsideDropZone: isOutSide })), drop: callback => { const { dragStartItem, dragOverItem, reset } = get(); - callback(dragStartItem, dragOverItem); + if (dragStartItem && dragOverItem) { + callback(dragStartItem, dragOverItem); + } reset(); }, + reset: () => set(() => ({ dragOverItem: null, dragStartItem: null })), })); From 5ed1d82c73870026db7d9de0300658db936119f5 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Thu, 19 Dec 2024 18:17:14 +0900 Subject: [PATCH 07/13] =?UTF-8?q?=E2=9C=A8=20Feature(#89):=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9E=84=EC=8B=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/Header.tsx | 4 +- src/features/user/ui/MyCharacter.tsx | 11 ++-- src/features/user/ui/ProfileImage.tsx | 12 +++-- src/features/user/ui/styles.ts | 78 ++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 10 deletions(-) diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index c830b6c..5c1fd2b 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -10,6 +10,7 @@ import isLoggedIn from '@utils/isLoggedIn'; import useModal from '@hooks/useModal'; import useUserStore from '@store/useUserStore'; import usePopover from '@hooks/usePopover'; +import ProfileImage from '@/features/user/ui/ProfileImage'; export default function Header() { const points: number = 2999999999; @@ -49,8 +50,7 @@ export default function Header() { )} - - + {isLoggedIn() && isOpen && ( e.stopPropagation()}> 유저이름 diff --git a/src/features/user/ui/MyCharacter.tsx b/src/features/user/ui/MyCharacter.tsx index 048326a..81c4a35 100644 --- a/src/features/user/ui/MyCharacter.tsx +++ b/src/features/user/ui/MyCharacter.tsx @@ -1,12 +1,15 @@ import { getImageUrl } from '@utils/getImageUrl'; import { MyCharacterImage } from './styles'; - +import * as S from './styles'; export default function MyCharacter() { return ( <> - + + + + + + ); } diff --git a/src/features/user/ui/ProfileImage.tsx b/src/features/user/ui/ProfileImage.tsx index 211f33d..65ddbde 100644 --- a/src/features/user/ui/ProfileImage.tsx +++ b/src/features/user/ui/ProfileImage.tsx @@ -1,10 +1,16 @@ import { getImageUrl } from '@/utils/getImageUrl'; - -export default function ProfileImage() { +import * as S from './styles'; +import MyCharacter from '@/features/user/ui/MyCharacter'; +export default function ProfileImage({ isIcon }: { isIcon: boolean }) { //기능 제작 시 자기 캐릭터에서 프로필이미지 추출하는 로직 추가 return ( <> - + + + + + + ); } diff --git a/src/features/user/ui/styles.ts b/src/features/user/ui/styles.ts index 2434665..93a5d8d 100644 --- a/src/features/user/ui/styles.ts +++ b/src/features/user/ui/styles.ts @@ -1,4 +1,5 @@ -import { styled } from 'styled-components'; +import { getImageUrl } from '@/utils/getImageUrl'; +import { css, styled } from 'styled-components'; export const BadgeWrapper = styled.div` display: flex; margin-top: 39px; @@ -53,4 +54,79 @@ export const BadgeListItem = styled.li` export const MyCharacterImage = styled.img` width: 171px; height: 138px; + position: absolute; + z-index: 1; +`; + +export const MyCharacterBox = styled.div` + position: relative; + width: 170px; + display: flex; + flex-direction: column; + align-items: center; +`; + +export const ItemImageHead = styled.img` + position: absolute; + z-index: 10; + top: -20px; +`; + +export const ItemImageBody = styled.img` + position: absolute; + z-index: 10; + top: 70px; + right: 19px; +`; + +export const ItemImageAccessori = styled.img` + position: absolute; + z-index: 20; + top: 18px; + right: 17px; +`; + +export const ProfileBox = styled.span<{ $isIcon: boolean }>` + position: absolute; + width: 150px; + height: 150px; + border-radius: 50%; /* 동그란 모양 만들기 */ + overflow: hidden; /* 동그라미 영역 밖은 잘라내기 */ + background-color: #f0f0f0; /* 배경색 설정 */ + > div { + transform: translateX(-15px) translateY(5px) scale(1.6); + } + ${({ $isIcon }) => + $isIcon && + css` + width: 30px; /* 프로필 이미지의 가로 크기 */ + height: 30px; /* 프로필 이미지의 세로 크기 */ + > div { + transform: translateX(-70px) scale(0.35); + } + `} +`; +export const ProfileBorderBox = styled.div<{ $isIcon: boolean }>` + width: 150px; + height: 150px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + > img:first-child { + position: absolute; + z-index: 20; + transform: scale(1.6); + } + ${({ $isIcon }) => + $isIcon && + css` + width: 30px; + height: 30px; + > img:first-child { + position: absolute; + z-index: 20; + transform: scale(0.35); + } + `} `; From c1d66277e5b02c393e9788f6ab5c05abfb0bbf70 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Fri, 20 Dec 2024 11:13:13 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#87):=20=EC=B5=9C?= =?UTF-8?q?=EC=A2=85=20=EA=B2=80=ED=86=A0,=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/Header.tsx | 2 +- .../quiz/service/addLineNumbersToCode.ts | 19 ++++++ src/features/quiz/service/lineChanger.ts | 5 -- .../service/replaceEmptyWithHTMLElement.ts | 17 ++++++ src/features/quiz/service/useCodeHighlight.ts | 58 +++++++++++++------ src/features/quiz/ui/Combination.tsx | 8 +-- src/features/quiz/ui/Question.tsx | 22 +++---- src/features/quiz/ui/TextBlock.tsx | 4 +- src/hooks/useDragAndDrop.ts | 20 ------- 9 files changed, 92 insertions(+), 63 deletions(-) delete mode 100644 src/features/quiz/service/lineChanger.ts delete mode 100644 src/hooks/useDragAndDrop.ts diff --git a/src/common/layout/Header.tsx b/src/common/layout/Header.tsx index 5c1fd2b..48d83d9 100644 --- a/src/common/layout/Header.tsx +++ b/src/common/layout/Header.tsx @@ -10,7 +10,7 @@ import isLoggedIn from '@utils/isLoggedIn'; import useModal from '@hooks/useModal'; import useUserStore from '@store/useUserStore'; import usePopover from '@hooks/usePopover'; -import ProfileImage from '@/features/user/ui/ProfileImage'; +import ProfileImage from '@features/user/ui/ProfileImage'; export default function Header() { const points: number = 2999999999; diff --git a/src/features/quiz/service/addLineNumbersToCode.ts b/src/features/quiz/service/addLineNumbersToCode.ts index 518cea1..88a6034 100644 --- a/src/features/quiz/service/addLineNumbersToCode.ts +++ b/src/features/quiz/service/addLineNumbersToCode.ts @@ -1,3 +1,22 @@ +/** + * 코드 문자열의 각 줄에 줄 번호를 추가합니다. + * + * 주어진 코드 문자열을 줄 단위로 나눈 뒤, + * 각 줄의 앞에 줄 번호를 `줄번호 |` 형식으로 추가하고, + * 다시 하나의 문자열로 결합하여 반환합니다. + * + * @param {string} code - 줄 번호를 추가할 코드 문자열. + * @returns {string} 각 줄에 줄 번호가 추가된 문자열. + * + * @example + * const code = "function hello() {\n console.log('Hello, World!');\n}"; + * const result = addLineNumbersToCode(code); + * console.log(result); + * // 출력: + * // "1 | function hello() { + * // 2 | console.log('Hello, World!'); + * // 3 | }" + */ const addLineNumbersToCode = (code: string) => { const splittedCode = code.split('\n'); const lineAttachedCode = splittedCode diff --git a/src/features/quiz/service/lineChanger.ts b/src/features/quiz/service/lineChanger.ts deleted file mode 100644 index a253453..0000000 --- a/src/features/quiz/service/lineChanger.ts +++ /dev/null @@ -1,5 +0,0 @@ -const lineChanger = (text: string = '') => { - return text.replace(/\n/g, "
"); -}; - -export default lineChanger; diff --git a/src/features/quiz/service/replaceEmptyWithHTMLElement.ts b/src/features/quiz/service/replaceEmptyWithHTMLElement.ts index 7c6448f..62e3812 100644 --- a/src/features/quiz/service/replaceEmptyWithHTMLElement.ts +++ b/src/features/quiz/service/replaceEmptyWithHTMLElement.ts @@ -1,3 +1,20 @@ +/** + * 특정 문자열(#empty#)을 HTML 요소로 대체합니다. + * + * 주어진 문자열에서 `#empty#`를 찾아 + * `` 요소로 대체하며, 각 요소에 고유 ID를 부여합니다. + * 대체되는 요소의 ID는 `emptyBlock0`, `emptyBlock1`과 같이 순차적으로 증가합니다. + * + * @param {string} text - HTML 요소로 대체할 입력 문자열. + * @returns {string} 대체된 HTML 요소를 포함한 문자열. + * + * @example + * const text = "이곳은 #empty#입니다. 여기도 #empty#입니다."; + * const result = replaceEmptyWithHTMLElement(text); + * console.log(result); + * // 출력: + * // "이곳은 입니다. 여기도 입니다." + */ const replaceEmptyWithHTMLElement = (text: string) => { let index = 0; const newText = text.replace(/(#empty#)/g, () => { diff --git a/src/features/quiz/service/useCodeHighlight.ts b/src/features/quiz/service/useCodeHighlight.ts index 7086520..aeb2045 100644 --- a/src/features/quiz/service/useCodeHighlight.ts +++ b/src/features/quiz/service/useCodeHighlight.ts @@ -1,31 +1,53 @@ import hljs from 'highlight.js'; -import { useLayoutEffect, DependencyList, useState, useEffect } from 'react'; -import replaceEmptyWithHTMLElement from './replaceEmptyWithHTMLElement'; +import { DependencyList, useState, useLayoutEffect } from 'react'; /** - * 코드 하이라이팅을 적용하는 커스텀텀훅 + * 주어진 코드 문자열을 하이라이트 처리된 HTML로 변환하는 React 커스텀 훅입니다. * - * 이 훅은 `dependency` 값이 변경될 때마다 코드를 하이라이팅 합니다. - * #empty#로 되어있는 부분을 빈 코드블럭으로 변경시켜줍니다. - * 문제 페이지에서 사용되는 훅입니다. + * `highlight.js` 라이브러리를 사용하여 특정 언어의 코드 구문을 강조하고, + * 이를 하이라이트 처리된 HTML 문자열로 반환합니다. + * 의존성 배열(`deps`)을 통해 재렌더링 조건을 제어할 수 있습니다. + * 코드 "문자열" 을 반환하기 때문에 html-react-parser과과xss에 취약한 점을 보완하기 위해 dompurify를 같이 사용하는것을 권장드립니다다. * - * @param {DependencyList} deps - 하이라이팅을 다시 적용할 조건이 되는 의존성. 의존성이 변경될 때마다 하이라이팅이 갱신됩니다. - * @returns {React.RefObject} - 코드 블록을 참조할 수 있는 `ref` + * @param {string} code - 하이라이트 처리할 코드 문자열. + * @param {DependencyList} [deps] - 훅 실행을 제어할 의존성 배열. 기본값은 `undefined`이며, 이 경우 `code`와 `language`를 기본 의존성으로 사용합니다. + * @param {string} [language='javascript'] - 코드 하이라이트에 사용할 언어. 기본값은 'javascript'입니다. + * @returns {string} 하이라이트 처리된 HTML 문자열. * * @example - * const codeRef = useCodeHighlight(code); - * return
{code}
; + * const code = ` + * const greet = (name) => { + * console.log(\`Hello, \${name}!\`); + * }; + * greet('World'); + * `; + * + * const highlightedCode = useCodeHighlight(code, [code], 'javascript'); + * + * return ( + *
+ *     
+ *      {parse(dompurify.sanitize(addLineNumberCode), options)}
+ *     
+ *   
+ * ); */ -const useCodeHighlight = (code: string, deps?: DependencyList) => { +const useCodeHighlight = ( + code: string, + deps?: DependencyList, + language: string = 'javascript' +) => { const [highlightCode, setHighlightCode] = useState(''); - useEffect(() => { - hljs.configure({ ignoreUnescapedHTML: true }); - const highlightedCode = hljs.highlight(code, { - language: 'javascript', - }).value; - setHighlightCode(highlightedCode); - }, deps); + useLayoutEffect(() => { + try { + hljs.configure({ ignoreUnescapedHTML: true }); + const highlightedCode = hljs.highlight(code, { language }).value; + setHighlightCode(highlightedCode); + } catch (error) { + setHighlightCode(code); + } + }, deps ?? [code, language]); return highlightCode; }; diff --git a/src/features/quiz/ui/Combination.tsx b/src/features/quiz/ui/Combination.tsx index 3ed1bbf..446608f 100644 --- a/src/features/quiz/ui/Combination.tsx +++ b/src/features/quiz/ui/Combination.tsx @@ -1,8 +1,8 @@ -import Quiz from '@type/Quiz'; +import type Quiz from '@type/Quiz'; import { CombinationSection, TextBlockButton } from './styles'; -import { useClientQuizStore } from '@/store/useClientQuizStore'; -import compact from '../../../utils/compact'; -import { useDnDStore } from '@/store/useDnDStore'; +import { useClientQuizStore } from '@store/useClientQuizStore'; +import compact from '@utils/compact'; +import { useDnDStore } from '@store/useDnDStore'; import { useEffect } from 'react'; interface CombinationProps { diff --git a/src/features/quiz/ui/Question.tsx b/src/features/quiz/ui/Question.tsx index 3f37cee..5d062b7 100644 --- a/src/features/quiz/ui/Question.tsx +++ b/src/features/quiz/ui/Question.tsx @@ -1,16 +1,15 @@ import * as S from './styles'; -import { useClientQuizStore } from '../../../store/useClientQuizStore'; +import { useClientQuizStore } from '@store/useClientQuizStore'; import './styles.css'; -import Quiz from '../../../types/Quiz'; +import type Quiz from '@type/Quiz'; import 'highlight.js/styles/base16/atelier-cave-light.css'; -import useCodeHighlight from '@/features/quiz/service/useCodeHighlight'; +import useCodeHighlight from '../service/useCodeHighlight'; import dompurify from 'dompurify'; import parse, { HTMLReactParserOptions, Element } from 'html-react-parser'; -import replaceEmptyWithHTMLElement from '@/features/quiz/service/replaceEmptyWithHTMLElement'; -import addLineNumbersToCode from '@/features/quiz/service/addLineNumbersToCode'; -import TextBlock from '@/features/quiz/ui/TextBlock'; -import { useState } from 'react'; -import { useDnDStore } from '@/store/useDnDStore'; +import replaceEmptyWithHTMLElement from '../service/replaceEmptyWithHTMLElement'; +import addLineNumbersToCode from '../service/addLineNumbersToCode'; +import TextBlock from './TextBlock'; +import { useDnDStore } from '@store/useDnDStore'; interface QuestionProps { title: Quiz['title']; @@ -18,8 +17,7 @@ interface QuestionProps { category: Quiz['category']; } export default function Question({ title, question, category }: QuestionProps) { - const { currentPage, userResponseAnswer, spliceUserResponseAnswer } = - useClientQuizStore(); + const { currentPage, userResponseAnswer } = useClientQuizStore(); const { setOutsideDropZone } = useDnDStore(); const highlightCode = useCodeHighlight(question, [question, currentPage]); const replaceEmptyCode = replaceEmptyWithHTMLElement(highlightCode); @@ -49,9 +47,7 @@ export default function Question({ title, question, category }: QuestionProps) {

{title}

- - {parse(dompurify.sanitize(addLineNumberCode), options)} - + {parse(dompurify.sanitize(addLineNumberCode), options)}
); diff --git a/src/features/quiz/ui/TextBlock.tsx b/src/features/quiz/ui/TextBlock.tsx index cd21e4d..9e2ce31 100644 --- a/src/features/quiz/ui/TextBlock.tsx +++ b/src/features/quiz/ui/TextBlock.tsx @@ -1,6 +1,6 @@ -import { useClientQuizStore } from '@/store/useClientQuizStore'; +import { useClientQuizStore } from '@store/useClientQuizStore'; import * as S from './styles'; -import { useDnDStore } from '@/store/useDnDStore'; +import { useDnDStore } from '@store/useDnDStore'; interface TextBlockProps { text: string; index: number; diff --git a/src/hooks/useDragAndDrop.ts b/src/hooks/useDragAndDrop.ts deleted file mode 100644 index d70912d..0000000 --- a/src/hooks/useDragAndDrop.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useRef } from 'react'; - -const useDragAndDrop = () => { - const dragItem = useRef(null); // Specify the type for dragItem - const dragOverItem = useRef(null); // Specify the type for dragOverItem - - const dragStart = (position: number) => { - dragItem.current = position; - }; - - const dragEnter = (position: number) => { - dragOverItem.current = position; - }; - - const drop = (callback: () => void) => { - callback(); - }; - return { dragStart, dragEnter, drop }; -}; -export default useDragAndDrop; From 83712c694f929531857546915d9ebcd19e40bfb1 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Fri, 20 Dec 2024 11:41:41 +0900 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=93=9D=20Modify(#87):=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/quiz/service/useCodeHighlight.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/quiz/service/useCodeHighlight.ts b/src/features/quiz/service/useCodeHighlight.ts index aeb2045..076a825 100644 --- a/src/features/quiz/service/useCodeHighlight.ts +++ b/src/features/quiz/service/useCodeHighlight.ts @@ -7,7 +7,7 @@ import { DependencyList, useState, useLayoutEffect } from 'react'; * `highlight.js` 라이브러리를 사용하여 특정 언어의 코드 구문을 강조하고, * 이를 하이라이트 처리된 HTML 문자열로 반환합니다. * 의존성 배열(`deps`)을 통해 재렌더링 조건을 제어할 수 있습니다. - * 코드 "문자열" 을 반환하기 때문에 html-react-parser과과xss에 취약한 점을 보완하기 위해 dompurify를 같이 사용하는것을 권장드립니다다. + * 코드 "문자열" 을 반환하기 때문에 html-react-parser과 xss에 취약한 점을 보완하기 위해 dompurify를 같이 사용하는것을 권장드립니다. * * @param {string} code - 하이라이트 처리할 코드 문자열. * @param {DependencyList} [deps] - 훅 실행을 제어할 의존성 배열. 기본값은 `undefined`이며, 이 경우 `code`와 `language`를 기본 의존성으로 사용합니다. From 1818c52cba03190e406634e653b456bc8caa5ea8 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Fri, 20 Dec 2024 11:51:05 +0900 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#87):=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/quiz/ui/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/quiz/ui/styles.ts b/src/features/quiz/ui/styles.ts index 5a66d11..368e665 100644 --- a/src/features/quiz/ui/styles.ts +++ b/src/features/quiz/ui/styles.ts @@ -1,5 +1,5 @@ import styled, { css, keyframes } from 'styled-components'; -import Quiz from '../../../types/Quiz'; +import type Quiz from '@type/Quiz'; const imgUrl = import.meta.env.VITE_IMG_BASE_URL; From d8a1f9d88fced2ca76b83d0d984eae3248d6bafa Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Fri, 20 Dec 2024 15:30:46 +0900 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#87):=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=ED=95=B8=EB=93=A4=EB=9F=AC=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EC=AA=BC=EA=B0=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/quiz/ui/Combination.tsx | 45 ++++++++-------- src/features/quiz/ui/TextBlock.tsx | 76 +++++++++++++++------------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/src/features/quiz/ui/Combination.tsx b/src/features/quiz/ui/Combination.tsx index 446608f..822d06e 100644 --- a/src/features/quiz/ui/Combination.tsx +++ b/src/features/quiz/ui/Combination.tsx @@ -16,12 +16,33 @@ export default function Combination({ }: CombinationProps) { const { userResponseAnswer, pushUserResponseAnswer, setUserResponseAtIndex } = useClientQuizStore(); - const { setDragStartItem, dragOverItem, dragStartItem } = useDnDStore(); + const { setDragStartItem, dragOverItem, dragStartItem, drop } = useDnDStore(); + useEffect(() => { for (let i = 0; i < answer.length; i++) { setUserResponseAtIndex('', i); } }, []); + + const handleOnClick = (value: string) => { + answer.length > compact(userResponseAnswer).length && + pushUserResponseAnswer(value); + }; + const handleDragStart = ( + e: React.DragEvent, + value: string, + index: number + ) => { + e.currentTarget.classList.add('drag-start'); + setDragStartItem({ value, index }); + }; + const handleDragEnd = (e: React.DragEvent) => { + e.preventDefault(); + drop((dragStartItem, dragOverItem) => { + setUserResponseAtIndex(dragStartItem.value, dragOverItem.index); + }); + e.currentTarget.classList.remove('drag-start'); + }; return ( <> @@ -38,26 +59,10 @@ export default function Combination({ ) : ( { - //답 수랑 내가 선택한 답 (공백빼고) 갯수 비교 정답보다 선택한게 많으면 안되니 - answer.length > compact(userResponseAnswer).length && - pushUserResponseAnswer(value); - }} + onClick={() => handleOnClick(value)} draggable - onDragStart={e => { - e.currentTarget.classList.add('drag-start'); - setDragStartItem({ value, index }); - }} - onDragEnd={e => { - e.preventDefault(); - if (dragOverItem && dragStartItem) { - setUserResponseAtIndex( - dragStartItem?.value, - dragOverItem?.index - ); - } - e.currentTarget.classList.remove('drag-start'); - }} + onDragStart={e => handleDragStart(e, value, index)} + onDragEnd={handleDragEnd} > {value} diff --git a/src/features/quiz/ui/TextBlock.tsx b/src/features/quiz/ui/TextBlock.tsx index 9e2ce31..7d366c3 100644 --- a/src/features/quiz/ui/TextBlock.tsx +++ b/src/features/quiz/ui/TextBlock.tsx @@ -18,47 +18,53 @@ export default function TextBlock({ text, index }: TextBlockProps) { isOutsideDropZone, setOutsideDropZone, } = useDnDStore(); + + const handleEmptyDivDragEnter = () => { + setOutsideDropZone(false); + setDragOverItem({ value: '', index }); + }; + + const handleDragStart = (e: React.DragEvent) => { + e.currentTarget.classList.add('drag-start'); + setDragStartItem({ value: text, index }); + }; + + const handleDragEnter = () => { + setDragOverItem({ value: text, index }); + }; + + const handleDragEnd = (e: React.DragEvent) => { + e.preventDefault(); + + if (isOutsideDropZone) { + spliceUserResponseAnswer(index); + } else { + drop((dragStartItem, dragOverItem) => { + if (dragOverItem.value === '') { + setUserResponseAtIndex(dragStartItem?.value, dragOverItem?.index); + spliceUserResponseAnswer(dragStartItem?.index); + } else { + swapUserResponseAnswer(dragStartItem.index, dragOverItem.index); + e.currentTarget.classList.remove('drag-start'); + } + }); + } + }; + const handleOnClick = () => { + spliceUserResponseAnswer(index); + }; + return ( <> {!text ? ( - { - setOutsideDropZone(false); - setDragOverItem({ value: '', index }); - }} - /> + ) : ( { - e.currentTarget.classList.add('drag-start'); - setDragStartItem({ value: text, index }); - }} - onDragEnter={() => setDragOverItem({ value: text, index })} - onDragEnd={e => { - e.preventDefault(); - if (isOutsideDropZone) { - spliceUserResponseAnswer(index); - } else { - drop((dragStartItem, dragOverItem) => { - if (dragOverItem.value === '') { - setUserResponseAtIndex( - dragStartItem?.value, - dragOverItem?.index - ); - spliceUserResponseAnswer(dragStartItem?.index); - } else { - swapUserResponseAnswer( - dragStartItem.index, - dragOverItem.index - ); - e.currentTarget.classList.remove('drag-start'); - } - }); - } - }} - onClick={() => spliceUserResponseAnswer(index)} + onDragStart={handleDragStart} + onDragEnter={handleDragEnter} + onDragEnd={handleDragEnd} + onClick={handleOnClick} > {text} From fd74e8a5993bdd9e8c3a682b4eb2f160d94312c8 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Fri, 20 Dec 2024 15:39:28 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=94=A8=20Refactor(#87):=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20state=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/quiz/ui/Combination.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/quiz/ui/Combination.tsx b/src/features/quiz/ui/Combination.tsx index 822d06e..c41e601 100644 --- a/src/features/quiz/ui/Combination.tsx +++ b/src/features/quiz/ui/Combination.tsx @@ -16,7 +16,7 @@ export default function Combination({ }: CombinationProps) { const { userResponseAnswer, pushUserResponseAnswer, setUserResponseAtIndex } = useClientQuizStore(); - const { setDragStartItem, dragOverItem, dragStartItem, drop } = useDnDStore(); + const { setDragStartItem, drop } = useDnDStore(); useEffect(() => { for (let i = 0; i < answer.length; i++) { From 798ac4d4e582606b06b1e1993fb951edacd66c74 Mon Sep 17 00:00:00 2001 From: GuDoYoon Date: Fri, 20 Dec 2024 17:43:45 +0900 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=90=9E=20BugFix(#92):=20=ED=94=84?= =?UTF-8?q?=EB=A1=AD=EC=8A=A4=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/profile/Profile.tsx | 2 +- src/pages/store/Store.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx index 2767c63..c74d7bd 100644 --- a/src/pages/profile/Profile.tsx +++ b/src/pages/profile/Profile.tsx @@ -55,7 +55,7 @@ export default function Profile() {
- + 유저 이름 2024.10.01
diff --git a/src/pages/store/Store.tsx b/src/pages/store/Store.tsx index 6fd5579..f2d8d2d 100644 --- a/src/pages/store/Store.tsx +++ b/src/pages/store/Store.tsx @@ -51,7 +51,11 @@ export default function Store() { 내가 구매한 아이템 초기화
- {itemQuery === 'profile' ? : } + {itemQuery === 'profile' ? ( + + ) : ( + + )}