fix: 게시판 부분 maxLength 추가

This commit is contained in:
정기영 2024-08-09 15:45:32 +09:00
parent 591cfa1e30
commit 1a0b020ec5
23 changed files with 217 additions and 65 deletions

View File

@ -2,23 +2,42 @@ import styles from './ArticleDetailAnswerInput.module.css';
import { useAnswerWrite } from '../../../../hooks/api/useAnswerWrite'; import { useAnswerWrite } from '../../../../hooks/api/useAnswerWrite';
import { useAnswerEdit } from '../../../../hooks/api/useAnswerEdit'; import { useAnswerEdit } from '../../../../hooks/api/useAnswerEdit';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useState } from 'react'; import { useRef, useEffect, useState } from 'react';
import SendIcon from '/src/assets/icons/send.svg?react'; import SendIcon from '/src/assets/icons/send.svg?react';
export default function ArticleDetailAnswerInput({ onSubmit, initialAnswer, isEditing = false }) { export default function ArticleDetailAnswerInput({ onSubmit, initialAnswer, isEditing = false }) {
// TODO: Textarea . Input
const { answerWrite } = useAnswerWrite(); const { answerWrite } = useAnswerWrite();
const { answerEdit } = useAnswerEdit(); const { answerEdit } = useAnswerEdit();
const { questionId } = useParams(); const { questionId } = useParams();
const [newAnswer, setNewAnswer] = useState(initialAnswer); const [answer, setAnswer] = useState(initialAnswer);
const textareaRef = useRef(null);
const adjustTextareaHeight = () => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
};
useEffect(() => {
adjustTextareaHeight();
}, [answer]);
const handleInput = (e) => {
const { value } = e.target;
setAnswer(value);
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
console.log(isEditing); console.log(isEditing);
if (isEditing) { if (isEditing) {
await answerEdit(questionId, newAnswer); await answerEdit(questionId, answer);
} else { } else {
await answerWrite(questionId, newAnswer); await answerWrite(questionId, answer);
} }
onSubmit(newAnswer); onSubmit(answer);
}; };
return ( return (
@ -26,14 +45,16 @@ export default function ArticleDetailAnswerInput({ onSubmit, initialAnswer, isEd
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={styles.answer} className={styles.answer}
> >
<input <textarea
type="text" maxLength={1000}
maxLength={255} value={answer}
value={newAnswer} onChange={handleInput}
onChange={(e) => setNewAnswer(e.target.value)} ref={textareaRef}
placeholder="답변 작성하기" placeholder="답변 작성하기"
className={styles.input} className={styles.input}
/> />
{answer.length > 950 && <div className={styles.textLength}>{answer.length} / 1000</div>}
<button <button
type="submit" type="submit"
className={styles.button} className={styles.button}

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styles from './CreateArticle.module.css'; import styles from './CreateArticle.module.css';
import EditIcon from '/src/assets/icons/edit.svg?react'; import EditIcon from '/src/assets/icons/edit.svg?react';
@ -7,12 +7,22 @@ import BackIcon from '/src/assets/icons/back.svg?react';
export default function CreateArticle({ topic, title, onSubmit }) { export default function CreateArticle({ topic, title, onSubmit }) {
const [articleTitle, setArticleTitle] = useState(''); const [articleTitle, setArticleTitle] = useState('');
const [articleContent, setArticleContent] = useState(''); const [articleContent, setArticleContent] = useState('');
const [textAreaHeight, setTextAreaHeight] = useState('auto'); const textareaRef = useRef(null);
const adjustTextareaHeight = () => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
};
useEffect(() => {
adjustTextareaHeight();
}, [articleContent]);
const handleInput = (e) => { const handleInput = (e) => {
const { value, scrollHeight } = e.target; const { value } = e.target;
setArticleContent(value); setArticleContent(value);
setTextAreaHeight(scrollHeight + 'px');
}; };
return ( return (
@ -35,22 +45,26 @@ export default function CreateArticle({ topic, title, onSubmit }) {
<label className={styles.label}>제목</label> <label className={styles.label}>제목</label>
<input <input
type="text" type="text"
maxLength={255} maxLength={200}
className={styles.titleInput} className={styles.titleInput}
placeholder="제목을 입력하세요" placeholder="제목을 입력하세요"
value={articleTitle} value={articleTitle}
onChange={(e) => setArticleTitle(e.target.value)} onChange={(e) => setArticleTitle(e.target.value)}
/> />
{articleTitle.length > 190 && <div className={styles.textLength}>{articleTitle.length} / 200</div>}
</div> </div>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label className={styles.label}>내용</label> <label className={styles.label}>내용</label>
<textarea <textarea
ref={textareaRef}
className={styles.contentInput} className={styles.contentInput}
placeholder="내용을 입력하세요" placeholder="내용을 입력하세요"
value={articleContent} value={articleContent}
maxLength={1000}
onChange={handleInput} onChange={handleInput}
style={{ height: textAreaHeight, overflow: 'hidden' }} style={{ overflow: 'hidden' }}
></textarea> ></textarea>
{articleContent.length > 950 && <div className={styles.textLength}>{articleContent.length} / 1000</div>}
</div> </div>
<button <button
type="button" type="button"

View File

@ -108,3 +108,9 @@
cursor: not-allowed; cursor: not-allowed;
stroke: var(--text-color-tertiary); stroke: var(--text-color-tertiary);
} }
.textLength {
margin-top: 4px;
align-self: end;
color: var(--error-color);
}

View File

@ -9,10 +9,6 @@ export default function EditArticle({ topic, title, prevTitle, prevContent, onSu
const [articleContent, setArticleContent] = useState(prevContent); const [articleContent, setArticleContent] = useState(prevContent);
const textAreaRef = useRef(null); const textAreaRef = useRef(null);
// useEffect(() => {
// adjustTextAreaHeight();
// }, []);
useEffect(() => { useEffect(() => {
adjustTextAreaHeight(); adjustTextAreaHeight();
}, [articleContent]); }, [articleContent]);
@ -48,22 +44,25 @@ export default function EditArticle({ topic, title, prevTitle, prevContent, onSu
<label className={styles.label}>제목</label> <label className={styles.label}>제목</label>
<input <input
type="text" type="text"
maxLength={255} maxLength={200}
className={styles.titleInput} className={styles.titleInput}
placeholder="제목을 입력하세요" placeholder="제목을 입력하세요"
value={articleTitle} value={articleTitle}
onChange={(e) => setArticleTitle(e.target.value)} onChange={(e) => setArticleTitle(e.target.value)}
/> />
{articleTitle.length > 190 && <div className={styles.textLength}>{articleTitle.length} / 200</div>}
</div> </div>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label className={styles.label}>내용</label> <label className={styles.label}>내용</label>
<textarea <textarea
ref={textAreaRef} ref={textAreaRef}
className={styles.contentInput} className={styles.contentInput}
maxLength={1000}
placeholder="내용을 입력하세요" placeholder="내용을 입력하세요"
value={articleContent} value={articleContent}
onChange={handleInput} onChange={handleInput}
></textarea> ></textarea>
{articleContent.length > 950 && <div className={styles.textLength}>{articleContent.length} / 1000</div>}
</div> </div>
<button <button
type="button" type="button"

View File

@ -108,3 +108,9 @@
cursor: not-allowed; cursor: not-allowed;
stroke: var(--text-color-tertiary); stroke: var(--text-color-tertiary);
} }
.textLength {
margin-top: 4px;
align-self: end;
color: var(--error-color);
}

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useRef, useState, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styles from './EditFreeboard.module.css'; import styles from './EditFreeboard.module.css';
import EditIcon from '/src/assets/icons/edit.svg?react'; import EditIcon from '/src/assets/icons/edit.svg?react';
@ -7,11 +7,21 @@ import BackIcon from '/src/assets/icons/back.svg?react';
export default function EditFreeboard({ topic, title, prevContent, prevTitle, onSubmit }) { export default function EditFreeboard({ topic, title, prevContent, prevTitle, onSubmit }) {
const [articleTitle, setArticleTitle] = useState(prevTitle); const [articleTitle, setArticleTitle] = useState(prevTitle);
const [articleContent, setArticleContent] = useState(prevContent); const [articleContent, setArticleContent] = useState(prevContent);
const textAreaRef = useRef(null);
useEffect(() => {
adjustTextAreaHeight();
}, [articleContent]);
const adjustTextAreaHeight = () => {
if (textAreaRef.current) {
textAreaRef.current.style.height = 'auto';
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
}
};
const handleInput = (e) => { const handleInput = (e) => {
setArticleContent(e.target.value); setArticleContent(e.target.value);
e.target.style.height = 'auto';
e.target.style.height = e.target.scrollHeight + 'px';
}; };
return ( return (
@ -34,21 +44,25 @@ export default function EditFreeboard({ topic, title, prevContent, prevTitle, on
<label className={styles.label}>제목</label> <label className={styles.label}>제목</label>
<input <input
type="text" type="text"
maxLength={255} maxLength={200}
className={styles.titleInput} className={styles.titleInput}
placeholder={'제목을 입력하세요'} placeholder={'제목을 입력하세요'}
value={articleTitle} value={articleTitle}
onChange={(e) => setArticleTitle(e.target.value)} onChange={(e) => setArticleTitle(e.target.value)}
/> />
{articleTitle.length > 190 && <div className={styles.textLength}>{articleTitle.length} / 200</div>}
</div> </div>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label className={styles.label}>내용</label> <label className={styles.label}>내용</label>
<textarea <textarea
className={styles.contentInput} className={styles.contentInput}
placeholder="내용을 입력하세요" placeholder="내용을 입력하세요"
ref={textAreaRef}
maxLength={1000}
value={articleContent} value={articleContent}
onChange={handleInput} onChange={handleInput}
></textarea> ></textarea>
{articleContent.length > 950 && <div className={styles.textLength}>{articleContent.length} / 1000</div>}
</div> </div>
<button <button
type="button" type="button"

View File

@ -108,3 +108,9 @@
cursor: not-allowed; cursor: not-allowed;
stroke: var(--text-color-tertiary); stroke: var(--text-color-tertiary);
} }
.textLength {
margin-top: 4px;
align-self: end;
color: var(--error-color);
}

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useRef, useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styles from './EditQna.module.css'; import styles from './EditQna.module.css';
import EditIcon from '/src/assets/icons/edit.svg?react'; import EditIcon from '/src/assets/icons/edit.svg?react';
@ -7,11 +7,21 @@ import BackIcon from '/src/assets/icons/back.svg?react';
export default function EditQna({ topic, title, prevContent, prevTitle, onSubmit }) { export default function EditQna({ topic, title, prevContent, prevTitle, onSubmit }) {
const [articleTitle, setArticleTitle] = useState(prevTitle); const [articleTitle, setArticleTitle] = useState(prevTitle);
const [articleContent, setArticleContent] = useState(prevContent); const [articleContent, setArticleContent] = useState(prevContent);
const textAreaRef = useRef(null);
useEffect(() => {
adjustTextAreaHeight();
}, [articleContent]);
const adjustTextAreaHeight = () => {
if (textAreaRef.current) {
textAreaRef.current.style.height = 'auto';
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
}
};
const handleInput = (e) => { const handleInput = (e) => {
setArticleContent(e.target.value); setArticleContent(e.target.value);
e.target.style.height = 'auto';
e.target.style.height = e.target.scrollHeight + 'px';
}; };
return ( return (
@ -34,21 +44,25 @@ export default function EditQna({ topic, title, prevContent, prevTitle, onSubmit
<label className={styles.label}>제목</label> <label className={styles.label}>제목</label>
<input <input
type="text" type="text"
maxLength={255} maxLength={200}
className={styles.titleInput} className={styles.titleInput}
placeholder={'제목을 입력하세요'} placeholder={'제목을 입력하세요'}
value={articleTitle} value={articleTitle}
onChange={(e) => setArticleTitle(e.target.value)} onChange={(e) => setArticleTitle(e.target.value)}
/> />
{articleTitle.length > 190 && <div className={styles.textLength}>{articleTitle.length} / 200</div>}
</div> </div>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label className={styles.label}>내용</label> <label className={styles.label}>내용</label>
<textarea <textarea
ref={textAreaRef}
maxLength={1000}
className={styles.contentInput} className={styles.contentInput}
placeholder="내용을 입력하세요" placeholder="내용을 입력하세요"
value={articleContent} value={articleContent}
onChange={handleInput} onChange={handleInput}
></textarea> ></textarea>
{articleContent.length > 950 && <div className={styles.textLength}>{articleContent.length} / 1000</div>}
</div> </div>
<button <button
type="button" type="button"

View File

@ -108,3 +108,9 @@
cursor: not-allowed; cursor: not-allowed;
stroke: var(--text-color-tertiary); stroke: var(--text-color-tertiary);
} }
.textLength {
margin-top: 4px;
align-self: end;
color: var(--error-color);
}

View File

@ -41,11 +41,13 @@ export default function FreeboardComment({ content, author, onDeleteSubmit, onEd
<input <input
type="text" type="text"
value={newComment} value={newComment}
maxLength={200}
onChange={(e) => setNewComment(e.target.value)} onChange={(e) => setNewComment(e.target.value)}
placeholder="댓글 수정하기" placeholder="댓글 수정하기"
className={styles.input} className={styles.input}
required required
/> />
{newComment.length > 190 && <div className={styles.textLength}>{newComment.length} / 200</div>}
<button <button
type="submit" type="submit"
className={styles.button} className={styles.button}

View File

@ -91,3 +91,9 @@
.delete { .delete {
color: var(--error-color); color: var(--error-color);
} }
.textLength {
margin-top: 4px;
align-self: end;
color: var(--error-color);
}

View File

@ -19,11 +19,13 @@ export default function FreeboardCommentInput({ onCommentSubmit }) {
<input <input
type="text" type="text"
value={newComment} value={newComment}
maxLength={200}
onChange={(e) => setNewComment(e.target.value)} onChange={(e) => setNewComment(e.target.value)}
placeholder="댓글 작성하기" placeholder="댓글 작성하기"
className={styles.input} className={styles.input}
required required
/> />
{newComment.length > 190 && <div className={styles.textLength}>{newComment.length} / 200</div>}
<button <button
type="submit" type="submit"
className={styles.button} className={styles.button}

View File

@ -26,3 +26,9 @@
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
} }
.textLength {
margin-top: 4px;
align-self: end;
color: var(--error-color);
}

View File

@ -1,7 +1,7 @@
import styles from './InfoEditForm.module.css'; import styles from './InfoEditForm.module.css';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
export default function InfoEditForm({ name, email, onSubmit }) { export default function InfoEditForm({ name, email, onSubmit, usingEmail }) {
const [username, setUsername] = useState(name); const [username, setUsername] = useState(name);
const [useremail, setUseremail] = useState(email); const [useremail, setUseremail] = useState(email);
@ -39,17 +39,18 @@ export default function InfoEditForm({ name, email, onSubmit }) {
htmlFor="useremail" htmlFor="useremail"
className={styles.textBody} className={styles.textBody}
> >
이메일을 입력하세요. 이메일을 입력하세요
</label> </label>
<input <input
placeholder="이메일" placeholder="이메일"
type="text" type="text"
id="useremail" id="useremail"
className={`${styles.input} ${styles.textBody}`} className={`${styles.input} ${styles.textBody} ${usingEmail && styles.errorBox}`}
value={useremail} value={useremail}
onChange={(e) => setUseremail(e.target.value)} onChange={(e) => setUseremail(e.target.value)}
required required
/> />
{usingEmail && <div className={styles.errorText}>이미 사용중인 이메일입니다</div>}
</div> </div>
<button <button
disabled={(!username && !useremail) || (username == name && useremail == email)} disabled={(!username && !useremail) || (username == name && useremail == email)}

View File

@ -64,3 +64,12 @@
cursor: not-allowed; cursor: not-allowed;
stroke: var(--text-color-tertiary); stroke: var(--text-color-tertiary);
} }
.errorBox {
outline: none;
border: 1px solid var(--error-border);
}
.errorText {
color: var(--error-color);
}

View File

@ -1,11 +1,10 @@
import styles from './QuizCard.module.css'; import styles from './QuizCard.module.css';
import { STATIC_URL } from '../../constants'; import { STATIC_URL } from '../../constants';
export default function QuizCard({ index, question, answer, image, choices, userAnswer, correct = true }) { export default function QuizDetailCard({ index, question, answer, image, choices, userAnswer = null, correct = true }) {
// TODO: / console.log(correct);
console.log(choices);
return ( return (
<div className={styles.card}> <div className={`${styles.card} ${!correct && styles.incorrect}`}>
<div className={styles.header}> <div className={styles.header}>
<span className={styles.heading}>{index} 퀴즈</span> <span className={styles.heading}>{index} 퀴즈</span>
</div> </div>

View File

@ -6,6 +6,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
transition: background-color 0.3s ease;
}
.incorrect {
background-color: #db3232;
border: 1px solid var(--red300);
} }
.header { .header {

View File

@ -21,19 +21,16 @@ export default function QuizsetForm({ headerTitle, topic, to, onSubmit, initialV
}, [initialValue]); }, [initialValue]);
const handleAddQuiz = () => { const handleAddQuiz = () => {
console.log(quizzes);
setQuizzes([...quizzes, { id: quizId, question: '', answer: '', choices: [], image: null }]); setQuizzes([...quizzes, { id: quizId, question: '', answer: '', choices: [], image: null }]);
setQuizId(quizId + 1); setQuizId(quizId + 1);
}; };
const updateQuiz = (id, updatedQuiz) => { const updateQuiz = (id, updatedQuiz) => {
console.log(quizzes);
const updatedQuizzes = quizzes.map((quiz) => (quiz.id === id ? updatedQuiz : quiz)); const updatedQuizzes = quizzes.map((quiz) => (quiz.id === id ? updatedQuiz : quiz));
setQuizzes(updatedQuizzes); setQuizzes(updatedQuizzes);
}; };
const deleteQuiz = (id) => { const deleteQuiz = (id) => {
console.log(quizzes);
setQuizzes(quizzes.filter((quiz) => quiz.id !== id)); setQuizzes(quizzes.filter((quiz) => quiz.id !== id));
}; };

View File

@ -0,0 +1,10 @@
import instance from '../../utils/axios/instance';
import { API_URL } from '../../constants';
export function useReportSetDelete() {
const reportsetDelete = (reportsetId) => {
return instance.delete(`${API_URL}/report/teacher/report/${reportsetId}`);
};
return { reportsetDelete };
}

View File

@ -2,18 +2,26 @@ import { InfoEditForm } from '../../components/InfoEditForm';
import { useAuth } from '../../hooks/api/useAuth'; import { useAuth } from '../../hooks/api/useAuth';
import { useUserInfo } from '../../hooks/api/useUserInfo'; import { useUserInfo } from '../../hooks/api/useUserInfo';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
export default function MyInfoChangePage() { export default function MyInfoChangePage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data } = useUserInfo(); const { data } = useUserInfo();
const myInfo = data.data?.userInfo; const myInfo = data.data?.userInfo;
const { updateInfo } = useAuth(); const { updateInfo } = useAuth();
const [usingEmail, setUsingEmail] = useState(false);
const handleSubmit = async (e, username, useremail) => { const handleSubmit = async (e, username, useremail) => {
e.preventDefault(); e.preventDefault();
await updateInfo(username, useremail) await updateInfo(username, useremail)
.then(() => navigate('/')) .then(() => navigate('/'))
.catch((err) => console.log(err)); .catch((err) => {
console.log(err);
console.log(err.response.data);
if (err.response.data === '이미 사용 중인 이메일입니다.') {
setUsingEmail(true);
}
});
}; };
return ( return (
@ -21,6 +29,7 @@ export default function MyInfoChangePage() {
name={myInfo.name} name={myInfo.name}
email={myInfo.email} email={myInfo.email}
onSubmit={handleSubmit} onSubmit={handleSubmit}
usingEmail={usingEmail}
/> />
); );
} }

View File

@ -34,6 +34,7 @@ export default function StudentReportDetailPage() {
answer={quiz.answer} answer={quiz.answer}
choices={quiz.choices} choices={quiz.choices}
userAnswer={quiz.userAnswer} userAnswer={quiz.userAnswer}
correct={quiz.correct}
/> />
))} ))}
</div> </div>

View File

@ -9,8 +9,9 @@ export default function StudentReportPage() {
const { lectureId } = useParams(); const { lectureId } = useParams();
const { data } = useStudentReports(lectureId); const { data } = useStudentReports(lectureId);
const reports = data?.data; const reports = data?.data;
console.log(reports);
const totalCounts = reports.reduce( const totalCounts = reports.reduce?.(
(acc, report) => { (acc, report) => {
if (acc.allCount > 0) { if (acc.allCount > 0) {
acc.correctCount += report.correctCount; acc.correctCount += report.correctCount;

View File

@ -1,10 +1,13 @@
import { ArticleLink } from '../../components/ArticleLink'; import { ArticleLink } from '../../components/ArticleLink';
import ArticleBoard from '../../components/ArticleBoard/ArticleBoard'; import ArticleBoard from '../../components/ArticleBoard/ArticleBoard';
import { useReportSetDetail } from '../../hooks/api/useReportSetDetail'; import { useReportSetDetail } from '../../hooks/api/useReportSetDetail';
import { useParams } from 'react-router-dom'; import { useReportSetDelete } from '../../hooks/api/useReportSetDelete';
import { useParams, useNavigate } from 'react-router-dom';
export default function TeacherReportsetDetailPage() { export default function TeacherReportsetDetailPage() {
const { reportsetId } = useParams(); const { reportsetId } = useParams();
const navigate = useNavigate();
const { reportsetDelete } = useReportSetDelete();
const { data } = useReportSetDetail(reportsetId); const { data } = useReportSetDetail(reportsetId);
const reports = data?.data; const reports = data?.data;
@ -16,7 +19,14 @@ export default function TeacherReportsetDetailPage() {
}); });
}; };
const handleDelete = async (e) => {
e.preventDefault();
await reportsetDelete(reportsetId);
navigate('..');
};
return ( return (
<div>
<ArticleBoard <ArticleBoard
title="퀴즈 조회" title="퀴즈 조회"
canCreate={false} canCreate={false}
@ -38,5 +48,12 @@ export default function TeacherReportsetDetailPage() {
); );
})} })}
</ArticleBoard> </ArticleBoard>
<button
type="button"
onClick={handleDelete}
>
리포트 삭제
</button>
</div>
); );
} }