feat: Qna와 Answer 수정 삭제 생성 기능 추가
This commit is contained in:
parent
ebf5b15daa
commit
bdcd01acb7
@ -25,6 +25,7 @@ const MyInfoChangePage = lazy(async () => await import('./pages/MyInfoChangePage
|
|||||||
const PasswordChangePage = lazy(async () => await import('./pages/PasswordChangePage'));
|
const PasswordChangePage = lazy(async () => await import('./pages/PasswordChangePage'));
|
||||||
const LearningLecturesPage = lazy(async () => await import('./pages/LearningLecturesPage'));
|
const LearningLecturesPage = lazy(async () => await import('./pages/LearningLecturesPage'));
|
||||||
const LectureCreatePage = lazy(async () => await import('./pages/LectureCreatePage'));
|
const LectureCreatePage = lazy(async () => await import('./pages/LectureCreatePage'));
|
||||||
|
const EditQuestionPage = lazy(async () => await import('./pages/EditQuestionPage'));
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -97,7 +98,20 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':questionId',
|
path: ':questionId',
|
||||||
element: <QuestionDetailPage />,
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <QuestionDetailPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
element: <EditQuestionPage />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':questionId/edit',
|
||||||
|
element: <EditQuestionPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'write',
|
path: 'write',
|
||||||
|
@ -2,29 +2,81 @@ import BackIcon from '/src/assets/icons/back.svg?react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styles from './ArticleDetail.module.css';
|
import styles from './ArticleDetail.module.css';
|
||||||
import ArticleDetailAnswer from './ArticleDetailAnswer/ArticleDetailAnswer';
|
import ArticleDetailAnswer from './ArticleDetailAnswer/ArticleDetailAnswer';
|
||||||
|
import ArticleDetailAnswerInput from './ArticleDetailAnswer/ArticleDetailAnswerInput';
|
||||||
|
import EditIcon from '/src/assets/icons/edit.svg?react';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export default function ArticleDetail({ topic, title, author = null, content, answer = null }) {
|
export default function ArticleDetail({ topic, title, author = null, content, answer = null, onDelete }) {
|
||||||
// TODO: 답변 작성 기능 추가
|
const [submittedAnswer, setSubmittedAnswer] = useState(answer);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSubmittedAnswer(answer);
|
||||||
|
}, [answer]);
|
||||||
|
|
||||||
|
const handleAnswerSubmit = (newAnswer) => {
|
||||||
|
setSubmittedAnswer(newAnswer);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = () => {
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSubmit = () => {
|
||||||
|
setSubmittedAnswer(null);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.articleDetail}>
|
<div className={styles.articleDetail}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<Link
|
<div className={styles.headerInside}>
|
||||||
to={'..'}
|
<Link
|
||||||
className={styles.goBack}
|
to={'..'}
|
||||||
>
|
className={styles.goBack}
|
||||||
<BackIcon />
|
>
|
||||||
<span>{topic}</span>
|
<BackIcon />
|
||||||
</Link>
|
<span>{topic}</span>
|
||||||
<div>
|
</Link>
|
||||||
<h1 className={styles.title}>{title}</h1>
|
<div>
|
||||||
{author && <span className={styles.author}>{author}</span>}
|
<h1 className={styles.title}>{title}</h1>
|
||||||
|
{author && <span className={styles.author}>{author}</span>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Link
|
||||||
|
type="button"
|
||||||
|
className={styles.editButton}
|
||||||
|
to={'edit'}
|
||||||
|
state={{ title: title, content: content, answer: answer }}
|
||||||
|
>
|
||||||
|
<EditIcon className={styles.icon} />
|
||||||
|
<span>수정하기</span>
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.deleteButton}
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
삭제하기
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
<p className={styles.content}>{content}</p>
|
<p className={styles.content}>{content}</p>
|
||||||
</div>
|
</div>
|
||||||
{answer && <ArticleDetailAnswer answer={answer} />}
|
{/* TODO: 이 부분에서 answer 만든다음 뒤로가기로 나갔다가 돌아오면 0.1초 정도 input 칸이 보였다가 answer 로 바뀜. 수정필요 */}
|
||||||
|
{submittedAnswer && !isEditing ? (
|
||||||
|
<ArticleDetailAnswer
|
||||||
|
answer={submittedAnswer}
|
||||||
|
onEditClick={handleEditClick}
|
||||||
|
onDeleteSubmit={handleDeleteSubmit}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ArticleDetailAnswerInput
|
||||||
|
onSubmit={handleAnswerSubmit}
|
||||||
|
initialAnswer={submittedAnswer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerInside {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
@ -49,3 +55,39 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
stroke: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--error-color);
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: var(--on-primary);
|
||||||
|
stroke: var(--error-color);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
align-self: end;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@ -1,14 +1,42 @@
|
|||||||
import styles from './ArticleDetailAnswer.module.css';
|
import styles from './ArticleDetailAnswer.module.css';
|
||||||
import ReplyIcon from '/src/assets/icons/reply.svg?react';
|
import ReplyIcon from '/src/assets/icons/reply.svg?react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useAnswerDelete } from '../../../../hooks/api/useAnswerDelete';
|
||||||
|
|
||||||
|
export default function ArticleDetailAnswer({ answer, onEditClick, onDeleteSubmit }) {
|
||||||
|
const { questionId } = useParams();
|
||||||
|
const { answerDelete } = useAnswerDelete();
|
||||||
|
|
||||||
|
const handleDeleteSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
await answerDelete(questionId);
|
||||||
|
onDeleteSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
export default function ArticleDetailAnswer({ answer }) {
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.answer}>
|
<>
|
||||||
<div className={styles.answerHeader}>
|
<section className={styles.answer}>
|
||||||
<ReplyIcon />
|
<div className={styles.answerHeader}>
|
||||||
<div className={styles.author}>선생님의 답변</div>
|
<ReplyIcon />
|
||||||
</div>
|
<div className={styles.author}>선생님의 답변</div>
|
||||||
<p className={styles.content}>{answer}</p>
|
</div>
|
||||||
</section>
|
<p className={styles.content}>{answer}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.deleteButton}
|
||||||
|
onClick={handleDeleteSubmit}
|
||||||
|
>
|
||||||
|
<div>삭제</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.editButton}
|
||||||
|
onClick={onEditClick}
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -29,3 +29,37 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--on-primary);
|
||||||
|
stroke: var(--on-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
align-self: end;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--error-color);
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: var(--on-primary);
|
||||||
|
stroke: var(--on-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
align-self: end;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import styles from './ArticleDetailAnswerInput.module.css';
|
||||||
|
import { useAnswerWrite } from '../../../../hooks/api/useAnswerWrite';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function ArticleDetailAnswerInput({ onSubmit, initialAnswer }) {
|
||||||
|
const { answerWrite } = useAnswerWrite();
|
||||||
|
const { questionId } = useParams();
|
||||||
|
const [newAnswer, setNewAnswer] = useState(initialAnswer);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await answerWrite(questionId, newAnswer);
|
||||||
|
onSubmit(newAnswer);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className={styles.answer}
|
||||||
|
>
|
||||||
|
{/* TODO: 여기 css 부분은 내가 임의로 넣었음 */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newAnswer}
|
||||||
|
onChange={(e) => setNewAnswer(e.target.value)}
|
||||||
|
placeholder="답변 작성"
|
||||||
|
className={styles.input}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
작성
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
.answer {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--on-primary);
|
||||||
|
stroke: var(--on-primary);
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
64
frontend/src/components/Article/EditQna/EditQna.jsx
Normal file
64
frontend/src/components/Article/EditQna/EditQna.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import styles from './EditQna.module.css';
|
||||||
|
import EditIcon from '/src/assets/icons/edit.svg?react';
|
||||||
|
import BackIcon from '/src/assets/icons/back.svg?react';
|
||||||
|
|
||||||
|
export default function EditQna({ topic, title, prevContent, prevTitle, onSubmit }) {
|
||||||
|
const [articleTitle, setArticleTitle] = useState(prevTitle);
|
||||||
|
const [articleContent, setArticleContent] = useState(prevContent);
|
||||||
|
|
||||||
|
const handleInput = (e) => {
|
||||||
|
setArticleContent(e.target.value);
|
||||||
|
e.target.style.height = 'auto';
|
||||||
|
e.target.style.height = e.target.scrollHeight + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.createArticle}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Link
|
||||||
|
to={'..'}
|
||||||
|
className={styles.goBack}
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
<span>{title}</span>
|
||||||
|
</Link>
|
||||||
|
<div className={styles.title}>{topic}</div>
|
||||||
|
</header>
|
||||||
|
<form
|
||||||
|
className={styles.formWrapper}
|
||||||
|
onSubmit={(e) => onSubmit(e, articleTitle, articleContent)}
|
||||||
|
>
|
||||||
|
<div className={styles.fieldWrapper}>
|
||||||
|
<label className={styles.label}>제목</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.titleInput}
|
||||||
|
placeholder={'제목을 입력하세요'}
|
||||||
|
value={articleTitle}
|
||||||
|
onChange={(e) => setArticleTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.fieldWrapper}>
|
||||||
|
<label className={styles.label}>내용</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.contentInput}
|
||||||
|
placeholder="내용을 입력하세요"
|
||||||
|
value={articleContent}
|
||||||
|
onChange={handleInput}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.button}
|
||||||
|
onClick={(e) => onSubmit(e, articleTitle, articleContent)}
|
||||||
|
disabled={!articleTitle || !articleContent}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
<div>글 수정하기</div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
110
frontend/src/components/Article/EditQna/EditQna.module.css
Normal file
110
frontend/src/components/Article/EditQna/EditQna.module.css
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
.createArticle {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goBack {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
stroke: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleInput {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleInput::placeholder {
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentInput {
|
||||||
|
padding: 20px;
|
||||||
|
height: 80px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--background-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentInput::placeholder {
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--on-primary);
|
||||||
|
stroke: var(--on-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 700;
|
||||||
|
align-self: end;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.25s,
|
||||||
|
border-color 0.25s,
|
||||||
|
stroke 0.25s,
|
||||||
|
color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled,
|
||||||
|
.button[disabled] {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
|
color: var(--text-color-tertiary);
|
||||||
|
cursor: not-allowed;
|
||||||
|
stroke: var(--text-color-tertiary);
|
||||||
|
}
|
@ -3,3 +3,5 @@ export { default as ArticleDetailAnswer } from './ArticleDetail/ArticleDetailAns
|
|||||||
export { default as CreateArticle } from './CreateArticle/CreateArticle.jsx';
|
export { default as CreateArticle } from './CreateArticle/CreateArticle.jsx';
|
||||||
export { default as ArticlePreview } from './ArticlePreview/ArticlePreview.jsx';
|
export { default as ArticlePreview } from './ArticlePreview/ArticlePreview.jsx';
|
||||||
export { default as EditArticle } from './EditArticle/EditArticle.jsx';
|
export { default as EditArticle } from './EditArticle/EditArticle.jsx';
|
||||||
|
export { default as EditQna } from './EditQna/EditQna.jsx';
|
||||||
|
export { default as ArticleDetailAnswerInput } from './ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswerInput.jsx';
|
||||||
|
13
frontend/src/hooks/api/useAnswerDelete.js
Normal file
13
frontend/src/hooks/api/useAnswerDelete.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import instance from '../../utils/axios/instance';
|
||||||
|
import { API_URL } from '../../constants';
|
||||||
|
|
||||||
|
export function useAnswerDelete() {
|
||||||
|
const answerDelete = (questionId) => {
|
||||||
|
const newAnswer = {
|
||||||
|
answer: null,
|
||||||
|
};
|
||||||
|
return instance.post(`${API_URL}/qna/answer/create/${questionId}`, newAnswer);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { answerDelete };
|
||||||
|
}
|
15
frontend/src/hooks/api/useAnswerEdit.js
Normal file
15
frontend/src/hooks/api/useAnswerEdit.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import instance from '../../utils/axios/instance';
|
||||||
|
import { API_URL } from '../../constants';
|
||||||
|
|
||||||
|
export function useAnswerEdit() {
|
||||||
|
const answerEdit = (questionId, title, content, answer) => {
|
||||||
|
const newAnswer = {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
answer,
|
||||||
|
};
|
||||||
|
return instance.put(`${API_URL}/qna/answer/update/${questionId}`, newAnswer);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { answerEdit };
|
||||||
|
}
|
13
frontend/src/hooks/api/useAnswerWrite.js
Normal file
13
frontend/src/hooks/api/useAnswerWrite.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import instance from '../../utils/axios/instance';
|
||||||
|
import { API_URL } from '../../constants';
|
||||||
|
|
||||||
|
export function useAnswerWrite() {
|
||||||
|
const answerWrite = (questionId, answer) => {
|
||||||
|
const newAnswer = {
|
||||||
|
answer: answer,
|
||||||
|
};
|
||||||
|
return instance.post(`${API_URL}/qna/answer/create/${questionId}`, newAnswer);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { answerWrite };
|
||||||
|
}
|
10
frontend/src/hooks/api/useNoticeDelete.js
Normal file
10
frontend/src/hooks/api/useNoticeDelete.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import instance from '../../utils/axios/instance';
|
||||||
|
import { API_URL } from '../../constants';
|
||||||
|
|
||||||
|
export function useNoticeDelete() {
|
||||||
|
const noticeDelete = (boardId) => {
|
||||||
|
return instance.delete(`${API_URL}/board/${boardId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { noticeDelete };
|
||||||
|
}
|
10
frontend/src/hooks/api/useQnaDelete.js
Normal file
10
frontend/src/hooks/api/useQnaDelete.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import instance from '../../utils/axios/instance';
|
||||||
|
import { API_URL } from '../../constants';
|
||||||
|
|
||||||
|
export function useQnaDelete() {
|
||||||
|
const qnaDelete = (questionId) => {
|
||||||
|
return instance.delete(`${API_URL}/qna/${questionId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { qnaDelete };
|
||||||
|
}
|
15
frontend/src/hooks/api/useQnaEdit.js
Normal file
15
frontend/src/hooks/api/useQnaEdit.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import instance from '../../utils/axios/instance';
|
||||||
|
import { API_URL } from '../../constants';
|
||||||
|
|
||||||
|
export function useQnaEdit() {
|
||||||
|
const qnaEdit = (questionId, title, content, answer) => {
|
||||||
|
const newQna = {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
answer,
|
||||||
|
};
|
||||||
|
return instance.put(`${API_URL}/qna/${questionId}`, newQna);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { qnaEdit };
|
||||||
|
}
|
27
frontend/src/pages/EditQuestionPage/EditQuestionPage.jsx
Normal file
27
frontend/src/pages/EditQuestionPage/EditQuestionPage.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useQnaEdit } from '../../hooks/api/useQnaEdit';
|
||||||
|
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { EditQna } from '../../components/Article';
|
||||||
|
|
||||||
|
export default function EditQuestionPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { questionId } = useParams();
|
||||||
|
const { qnaEdit } = useQnaEdit();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const handleSubmit = async (e, title, content, answer) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
await qnaEdit(questionId, title, content, answer);
|
||||||
|
navigate('..');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<EditQna
|
||||||
|
topic="질문하기"
|
||||||
|
title="Q&A"
|
||||||
|
prevTitle={location.state.title}
|
||||||
|
prevContent={location.state.content}
|
||||||
|
prevAnswer={location.state.answer}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
1
frontend/src/pages/EditQuestionPage/index.js
Normal file
1
frontend/src/pages/EditQuestionPage/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './EditQuestionPage';
|
@ -1,19 +1,28 @@
|
|||||||
import { ArticleDetail } from '../../components/Article';
|
import { ArticleDetail } from '../../components/Article';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useNoticeDetail } from '../../hooks/api/useNoticeDetail';
|
import { useNoticeDetail } from '../../hooks/api/useNoticeDetail';
|
||||||
|
import { useNoticeDelete } from '../../hooks/api/useNoticeDelete';
|
||||||
|
|
||||||
export default function NoticeDetailPage() {
|
export default function NoticeDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const noticeId = params.noticeId;
|
const noticeId = params.noticeId;
|
||||||
const { data } = useNoticeDetail(noticeId);
|
const { data } = useNoticeDetail(noticeId);
|
||||||
const notice = data?.data;
|
const notice = data?.data;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { noticeDelete } = useNoticeDelete();
|
||||||
// TODO: 수정 버튼 추가(여기에 또는 ArticleDetail에)
|
// TODO: 수정 버튼 추가(여기에 또는 ArticleDetail에)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await noticeDelete(noticeId);
|
||||||
|
navigate('..');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArticleDetail
|
<ArticleDetail
|
||||||
topic="공지사항"
|
topic="공지사항"
|
||||||
title={notice.title}
|
title={notice.title}
|
||||||
content={notice.content}
|
content={notice.content}
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import { ArticleDetail } from '../../components/Article';
|
import { ArticleDetail } from '../../components/Article';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useQnaDetail } from '../../hooks/api/useQnaDetail';
|
import { useQnaDetail } from '../../hooks/api/useQnaDetail';
|
||||||
|
import { useQnaDelete } from '../../hooks/api/useQnaDelete';
|
||||||
|
|
||||||
export default function QuestionDetailPage() {
|
export default function QuestionDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const qnaId = params.questionId;
|
const qnaId = params.questionId;
|
||||||
const { data } = useQnaDetail(qnaId);
|
const { data } = useQnaDetail(qnaId);
|
||||||
const qna = data?.data;
|
const qna = data?.data;
|
||||||
|
const { qnaDelete } = useQnaDelete();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await qnaDelete(qnaId);
|
||||||
|
navigate('..');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArticleDetail
|
<ArticleDetail
|
||||||
@ -15,6 +23,7 @@ export default function QuestionDetailPage() {
|
|||||||
author={qna.username}
|
author={qna.username}
|
||||||
content={qna.content}
|
content={qna.content}
|
||||||
answer={qna?.answer}
|
answer={qna?.answer}
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user