Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…coko-Front into feature/#79/Section_Part
  • Loading branch information
bluetree7878 committed Dec 24, 2024
2 parents 65cd43e + fb4f54a commit eba62f7
Show file tree
Hide file tree
Showing 30 changed files with 459 additions and 113 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/common/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,8 +50,7 @@ export default function Header() {
</>
)}
<S.ProfileWrapper ref={profileRef} onClick={handleProfileClick}>
<S.ProfileIcon src={getImageUrl('테두리.svg')} alt="프로필 테두리" />
<S.HeaderIcon src={getImageUrl('코코-프로필.svg')} alt="코코 프로필" />
<ProfileImage isIcon={true} />
{isLoggedIn() && isOpen && (
<S.ProfilePopover ref={popoverRef} onClick={e => e.stopPropagation()}>
<S.UserNameText>유저이름</S.UserNameText>
Expand Down
1 change: 1 addition & 0 deletions src/common/layout/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
27 changes: 27 additions & 0 deletions src/features/quiz/service/addLineNumbersToCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 코드 문자열의 각 줄에 줄 번호를 추가합니다.
*
* 주어진 코드 문자열을 줄 단위로 나눈 뒤,
* 각 줄의 앞에 줄 번호를 `<span>줄번호 |</span>` 형식으로 추가하고,
* 다시 하나의 문자열로 결합하여 반환합니다.
*
* @param {string} code - 줄 번호를 추가할 코드 문자열.
* @returns {string} 각 줄에 줄 번호가 추가된 문자열.
*
* @example
* const code = "function hello() {\n console.log('Hello, World!');\n}";
* const result = addLineNumbersToCode(code);
* console.log(result);
* // 출력:
* // "<span>1 |</span> function hello() {
* // <span>2 |</span> console.log('Hello, World!');
* // <span>3 |</span> }"
*/
const addLineNumbersToCode = (code: string) => {
const splittedCode = code.split('\n');
const lineAttachedCode = splittedCode
.map((code, i) => `<span>${i + 1} |</span> ${code}`)
.join('\n');
return lineAttachedCode;
};
export default addLineNumbersToCode;
11 changes: 0 additions & 11 deletions src/features/quiz/service/emptyChangeToDiv.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/features/quiz/service/lineChanger.ts

This file was deleted.

28 changes: 28 additions & 0 deletions src/features/quiz/service/replaceEmptyWithHTMLElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 특정 문자열(#empty#)을 HTML 요소로 대체합니다.
*
* 주어진 문자열에서 `#empty#`를 찾아
* `<span>` 요소로 대체하며, 각 요소에 고유 ID를 부여합니다.
* 대체되는 요소의 ID는 `emptyBlock0`, `emptyBlock1`과 같이 순차적으로 증가합니다.
*
* @param {string} text - HTML 요소로 대체할 입력 문자열.
* @returns {string} 대체된 HTML 요소를 포함한 문자열.
*
* @example
* const text = "이곳은 #empty#입니다. 여기도 #empty#입니다.";
* const result = replaceEmptyWithHTMLElement(text);
* console.log(result);
* // 출력:
* // "이곳은 <span id='emptyBlock0' class='empty'></span>입니다. 여기도 <span id='emptyBlock1' class='empty'></span>입니다."
*/
const replaceEmptyWithHTMLElement = (text: string) => {
let index = 0;
const newText = text.replace(/(#empty#)/g, () => {
const replacement = `<span id="emptyBlock${index}" class="empty"></span>`;
index++;
return replacement;
});

return newText;
};
export default replaceEmptyWithHTMLElement;
55 changes: 55 additions & 0 deletions src/features/quiz/service/useCodeHighlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import hljs from 'highlight.js';
import { DependencyList, useState, useLayoutEffect } from 'react';

/**
* 주어진 코드 문자열을 하이라이트 처리된 HTML로 변환하는 React 커스텀 훅입니다.
*
* `highlight.js` 라이브러리를 사용하여 특정 언어의 코드 구문을 강조하고,
* 이를 하이라이트 처리된 HTML 문자열로 반환합니다.
* 의존성 배열(`deps`)을 통해 재렌더링 조건을 제어할 수 있습니다.
* 코드 "문자열" 을 반환하기 때문에 html-react-parser과 xss에 취약한 점을 보완하기 위해 dompurify를 같이 사용하는것을 권장드립니다.
*
* @param {string} code - 하이라이트 처리할 코드 문자열.
* @param {DependencyList} [deps] - 훅 실행을 제어할 의존성 배열. 기본값은 `undefined`이며, 이 경우 `code`와 `language`를 기본 의존성으로 사용합니다.
* @param {string} [language='javascript'] - 코드 하이라이트에 사용할 언어. 기본값은 'javascript'입니다.
* @returns {string} 하이라이트 처리된 HTML 문자열.
*
* @example
* const code = `
* const greet = (name) => {
* console.log(\`Hello, \${name}!\`);
* };
* greet('World');
* `;
*
* const highlightedCode = useCodeHighlight(code, [code], 'javascript');
*
* return (
* <pre>
* <code>
* {parse(dompurify.sanitize(addLineNumberCode), options)}
* </code>
* </pre>
* );
*/
const useCodeHighlight = (
code: string,
deps?: DependencyList,
language: string = 'javascript'
) => {
const [highlightCode, setHighlightCode] = useState<string>('');

useLayoutEffect(() => {
try {
hljs.configure({ ignoreUnescapedHTML: true });
const highlightedCode = hljs.highlight(code, { language }).value;
setHighlightCode(highlightedCode);
} catch (error) {
setHighlightCode(code);
}
}, deps ?? [code, language]);

return highlightCode;
};

export default useCodeHighlight;
60 changes: 48 additions & 12 deletions src/features/quiz/ui/Combination.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,71 @@
import Quiz from '../../../types/Quiz';
import { CombinationSection, TextBlockButton } from '../styles';
import { useClientQuizStore } from '../../../store/useClientQuizStore';
import compact from '../../../utils/compact';
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 { 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, 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<HTMLButtonElement>,
value: string,
index: number
) => {
e.currentTarget.classList.add('drag-start');
setDragStartItem({ value, index });
};
const handleDragEnd = (e: React.DragEvent<HTMLButtonElement>) => {
e.preventDefault();
drop((dragStartItem, dragOverItem) => {
setUserResponseAtIndex(dragStartItem.value, dragOverItem.index);
});
e.currentTarget.classList.remove('drag-start');
};
return (
<>
<CombinationSection>
{answerChoice.map(value => {
{answerChoice.map((value, index) => {
const isSelect = userResponseAnswer.includes(value);
return (
return isSelect ? (
<TextBlockButton
key={value}
onClick={() => {
//답 수랑 내가 선택한 답 (공백빼고) 갯수 비교 정답보다 선택한게 많으면 안되니
answer.length > compact(userResponseAnswer).length &&
pushUserResponseAnswer(value);
}}
$selected={isSelect}
disabled={isSelect}
>
{value}
</TextBlockButton>
) : (
<TextBlockButton
key={value}
onClick={() => handleOnClick(value)}
draggable
onDragStart={e => handleDragStart(e, value, index)}
onDragEnd={handleDragEnd}
>
{value}
</TextBlockButton>
);
})}
</CombinationSection>
Expand Down
2 changes: 1 addition & 1 deletion src/features/quiz/ui/MultipleChoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/features/quiz/ui/OXSelector.tsx
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/features/quiz/ui/PartClear.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/features/quiz/ui/PartNavContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ButtonGrid,
KeyboardButton,
UpperBackgroundImg,
} from '../styles';
} from './styles.ts';
import { getImageUrl } from '@utils/getImageUrl';
import { useNavigate } from 'react-router-dom';
import getPartGridPosition from '@features/learn/service/getPartGridPosition';
Expand Down
Loading

0 comments on commit eba62f7

Please sign in to comment.