diff --git a/frontend/src/Router.jsx b/frontend/src/Router.jsx index 8868ec1..66b19f3 100644 --- a/frontend/src/Router.jsx +++ b/frontend/src/Router.jsx @@ -25,6 +25,7 @@ const MyInfoChangePage = lazy(async () => await import('./pages/MyInfoChangePage const PasswordChangePage = lazy(async () => await import('./pages/PasswordChangePage')); const LearningLecturesPage = lazy(async () => await import('./pages/LearningLecturesPage')); const LectureCreatePage = lazy(async () => await import('./pages/LectureCreatePage')); +const EditQuestionPage = lazy(async () => await import('./pages/EditQuestionPage')); const router = createBrowserRouter([ { @@ -97,7 +98,20 @@ const router = createBrowserRouter([ }, { path: ':questionId', - element: , + children: [ + { + index: true, + element: , + }, + { + path: 'edit', + element: , + }, + ], + }, + { + path: ':questionId/edit', + element: , }, { path: 'write', diff --git a/frontend/src/components/Article/ArticleDetail/ArticleDetail.jsx b/frontend/src/components/Article/ArticleDetail/ArticleDetail.jsx index 9315604..3c6c692 100644 --- a/frontend/src/components/Article/ArticleDetail/ArticleDetail.jsx +++ b/frontend/src/components/Article/ArticleDetail/ArticleDetail.jsx @@ -2,29 +2,81 @@ import BackIcon from '/src/assets/icons/back.svg?react'; import { Link } from 'react-router-dom'; import styles from './ArticleDetail.module.css'; 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 }) { - // TODO: 답변 작성 기능 추가 +export default function ArticleDetail({ topic, title, author = null, content, answer = null, onDelete }) { + 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 (
- - - {topic} - -
-

{title}

- {author && {author}} +
+ + + {topic} + +
+

{title}

+ {author && {author}} +
+ + + 수정하기 + +

{content}

- {answer && } + {/* TODO: 이 부분에서 answer 만든다음 뒤로가기로 나갔다가 돌아오면 0.1초 정도 input 칸이 보였다가 answer 로 바뀜. 수정필요 */} + {submittedAnswer && !isEditing ? ( + + ) : ( + + )}
); } diff --git a/frontend/src/components/Article/ArticleDetail/ArticleDetail.module.css b/frontend/src/components/Article/ArticleDetail/ArticleDetail.module.css index b3d813a..11ca0f6 100644 --- a/frontend/src/components/Article/ArticleDetail/ArticleDetail.module.css +++ b/frontend/src/components/Article/ArticleDetail/ArticleDetail.module.css @@ -11,6 +11,12 @@ } .header { + display: flex; + justify-content: space-between; + align-items: start; +} + +.headerInside { display: flex; flex-direction: column; align-items: start; @@ -49,3 +55,39 @@ margin: 0; 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; +} diff --git a/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.jsx b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.jsx index ab8adc8..5385bea 100644 --- a/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.jsx +++ b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.jsx @@ -1,14 +1,42 @@ import styles from './ArticleDetailAnswer.module.css'; 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 ( -
-
- -
선생님의 답변
-
-

{answer}

-
+ <> +
+
+ +
선생님의 답변
+
+

{answer}

+ + +
+ ); } diff --git a/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.module.css b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.module.css index e60ad67..13afc32 100644 --- a/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.module.css +++ b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswer.module.css @@ -29,3 +29,37 @@ margin: 0; 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; +} diff --git a/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswerInput.jsx b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswerInput.jsx new file mode 100644 index 0000000..284401a --- /dev/null +++ b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswerInput.jsx @@ -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 ( +
+ {/* TODO: 여기 css 부분은 내가 임의로 넣었음 */} + setNewAnswer(e.target.value)} + placeholder="답변 작성" + className={styles.input} + /> + +
+ ); +} diff --git a/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswerInput.module.css b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswerInput.module.css new file mode 100644 index 0000000..6e2cbb5 --- /dev/null +++ b/frontend/src/components/Article/ArticleDetail/ArticleDetailAnswer/ArticleDetailAnswerInput.module.css @@ -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; +} diff --git a/frontend/src/components/Article/EditQna/EditQna.jsx b/frontend/src/components/Article/EditQna/EditQna.jsx new file mode 100644 index 0000000..fa8125d --- /dev/null +++ b/frontend/src/components/Article/EditQna/EditQna.jsx @@ -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 ( +
+
+ + + {title} + +
{topic}
+
+
onSubmit(e, articleTitle, articleContent)} + > +
+ + setArticleTitle(e.target.value)} + /> +
+
+ + +
+ +
+
+ ); +} diff --git a/frontend/src/components/Article/EditQna/EditQna.module.css b/frontend/src/components/Article/EditQna/EditQna.module.css new file mode 100644 index 0000000..54109d5 --- /dev/null +++ b/frontend/src/components/Article/EditQna/EditQna.module.css @@ -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); +} diff --git a/frontend/src/components/Article/index.js b/frontend/src/components/Article/index.js index 8a32a75..ef20771 100644 --- a/frontend/src/components/Article/index.js +++ b/frontend/src/components/Article/index.js @@ -3,3 +3,5 @@ export { default as ArticleDetailAnswer } from './ArticleDetail/ArticleDetailAns export { default as CreateArticle } from './CreateArticle/CreateArticle.jsx'; export { default as ArticlePreview } from './ArticlePreview/ArticlePreview.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'; diff --git a/frontend/src/hooks/api/useAnswerDelete.js b/frontend/src/hooks/api/useAnswerDelete.js new file mode 100644 index 0000000..39c303e --- /dev/null +++ b/frontend/src/hooks/api/useAnswerDelete.js @@ -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 }; +} diff --git a/frontend/src/hooks/api/useAnswerEdit.js b/frontend/src/hooks/api/useAnswerEdit.js new file mode 100644 index 0000000..446b9c1 --- /dev/null +++ b/frontend/src/hooks/api/useAnswerEdit.js @@ -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 }; +} diff --git a/frontend/src/hooks/api/useAnswerWrite.js b/frontend/src/hooks/api/useAnswerWrite.js new file mode 100644 index 0000000..03a3427 --- /dev/null +++ b/frontend/src/hooks/api/useAnswerWrite.js @@ -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 }; +} diff --git a/frontend/src/hooks/api/useNoticeDelete.js b/frontend/src/hooks/api/useNoticeDelete.js new file mode 100644 index 0000000..428a12b --- /dev/null +++ b/frontend/src/hooks/api/useNoticeDelete.js @@ -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 }; +} diff --git a/frontend/src/hooks/api/useQnaDelete.js b/frontend/src/hooks/api/useQnaDelete.js new file mode 100644 index 0000000..a56d1db --- /dev/null +++ b/frontend/src/hooks/api/useQnaDelete.js @@ -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 }; +} diff --git a/frontend/src/hooks/api/useQnaEdit.js b/frontend/src/hooks/api/useQnaEdit.js new file mode 100644 index 0000000..bebfe67 --- /dev/null +++ b/frontend/src/hooks/api/useQnaEdit.js @@ -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 }; +} diff --git a/frontend/src/pages/EditQuestionPage/EditQuestionPage.jsx b/frontend/src/pages/EditQuestionPage/EditQuestionPage.jsx new file mode 100644 index 0000000..3b123c7 --- /dev/null +++ b/frontend/src/pages/EditQuestionPage/EditQuestionPage.jsx @@ -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 ( + + ); +} diff --git a/frontend/src/pages/EditQuestionPage/index.js b/frontend/src/pages/EditQuestionPage/index.js new file mode 100644 index 0000000..4c65c54 --- /dev/null +++ b/frontend/src/pages/EditQuestionPage/index.js @@ -0,0 +1 @@ +export { default } from './EditQuestionPage'; diff --git a/frontend/src/pages/NoticeDetailPage/NoticeDetailPage.jsx b/frontend/src/pages/NoticeDetailPage/NoticeDetailPage.jsx index 5955fe4..4902956 100644 --- a/frontend/src/pages/NoticeDetailPage/NoticeDetailPage.jsx +++ b/frontend/src/pages/NoticeDetailPage/NoticeDetailPage.jsx @@ -1,19 +1,28 @@ 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 { useNoticeDelete } from '../../hooks/api/useNoticeDelete'; export default function NoticeDetailPage() { const params = useParams(); const noticeId = params.noticeId; const { data } = useNoticeDetail(noticeId); const notice = data?.data; + const navigate = useNavigate(); + const { noticeDelete } = useNoticeDelete(); // TODO: 수정 버튼 추가(여기에 또는 ArticleDetail에) + const handleDelete = async () => { + await noticeDelete(noticeId); + navigate('..'); + }; + return ( ); } diff --git a/frontend/src/pages/QuestionDetailPage/QuestionDetailPage.jsx b/frontend/src/pages/QuestionDetailPage/QuestionDetailPage.jsx index f36a8c1..14b4328 100644 --- a/frontend/src/pages/QuestionDetailPage/QuestionDetailPage.jsx +++ b/frontend/src/pages/QuestionDetailPage/QuestionDetailPage.jsx @@ -1,12 +1,20 @@ 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 { useQnaDelete } from '../../hooks/api/useQnaDelete'; export default function QuestionDetailPage() { const params = useParams(); const qnaId = params.questionId; const { data } = useQnaDetail(qnaId); const qna = data?.data; + const { qnaDelete } = useQnaDelete(); + const navigate = useNavigate(); + + const handleDelete = async () => { + await qnaDelete(qnaId); + navigate('..'); + }; return ( ); }