Skip to content

Epic/answer 화면 구현 -> merge 브랜치 병합 #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/assets/images/icons/ic_Close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/assets/images/icons/ic_Edit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/assets/images/icons/ic_Rejection.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 60 additions & 20 deletions src/components/AnswerContent/index.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import PropTypes from 'prop-types';
import { useLocation } from 'react-router-dom';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import formatCreatedAt from 'utils/dateUtils';
import { postAnswer } from 'api/answers';

const AnswerContent = ({ answer, name, imageSource }) => {
const AnswerContent = ({ answer, name, imageSource, id, onAnswerSubmit }) => {
AnswerContent.propTypes = {
answer: PropTypes.shape({
id: PropTypes.number.isRequired,
content: PropTypes.string.isRequired,
isRejected: PropTypes.bool,
createdAt: PropTypes.string.isRequired,
}),
name: PropTypes.string.isRequired,
imageSource: PropTypes.string,
id: PropTypes.number.isRequired,
onAnswerSubmit: PropTypes.func.isRequired,
};

AnswerContent.defaultProps = {
Expand All @@ -20,6 +24,8 @@ const AnswerContent = ({ answer, name, imageSource }) => {

const location = useLocation();
const [textareaValue, setTextareaValue] = useState('');
const [updatedAnswer, setUpdatedAnswer] = useState(answer);
const [isLoading, setIsLoading] = useState(false);

const isFeedPage = location.pathname.startsWith('/post/') && !location.pathname.includes('/answer');
const isAnswerPage = location.pathname.startsWith('/post/') && location.pathname.includes('/answer');
Expand All @@ -28,32 +34,63 @@ const AnswerContent = ({ answer, name, imageSource }) => {
setTextareaValue(event.target.value);
};

const handleAnswerPost = async (e) => {
e.preventDefault();
const postBody = {
questionId: id,
content: textareaValue,
isRejected: false,
team: '12-6',
};

let response;
try {
setIsLoading(true);
// setError(null);
response = await postAnswer(id, postBody);
setUpdatedAnswer(response);
onAnswerSubmit(id, response);
} catch (err) {
// setError(`${response} : 답변을 등록하던 중 오류가 발생했습니다. 페이지를 새로고침합니다.`);
// setTimeout(() => {
// window.location.reload();
// }, 2000);
} finally {
setIsLoading(false);
}
};

const renderProfileImg = () => <img src={imageSource} alt={`${name}의 프로필`} className='w-[32px] h-[32px] md:w-[48px] md:h-[48px] rounded-full object-cover' />;

const renderAnswerHeader = () => (
<div className='flex items-center mb-[4px]'>
<p className='mr-[8px] inline-block text-sm leading-[18px] md:text-lg md:leading-[24px] font-actor'>{name}</p>
<p className='text-sm font-medium leading-[18px] text-gray-40'>{formatCreatedAt(answer.createdAt)}</p>
<p className='mr-[8px] inline-block text-sm leading-[18px] md:text-lg md:leading-[24px]'>{name}</p>
<p className='text-sm font-medium leading-[18px] text-gray-40'>{formatCreatedAt(updatedAnswer.createdAt)}</p>
</div>
);

const renderAnswerContent = () => <p className='text-base leading-[22px]'>{answer.content}</p>;
const renderAnswerContent = () => <p className='text-base leading-[22px]'>{updatedAnswer.content}</p>;

const renderAnswerForm = () => (
<form className='flex w-full flex-col gap-[8px]'>
<form onSubmit={handleAnswerPost} className='flex w-full flex-col gap-[8px]'>
<textarea
className='w-full h-[186px] resize-none rounded-lg border-none p-[16px] bg-gray-20 text-base leading-[22px] text-secondary-900 placeholder:text-base placeholder:leading-[22px] placeholder:text-gray-40 focus:outline-brown-40'
placeholder='답변을 입력해주세요'
value={textareaValue}
onChange={handleTextareaChange}
/>
<button type='submit' className='py-[12px] rounded-lg bg-brown-40 text-base leading-[22px] text-gray-10 disabled:bg-brown-30' disabled={textareaValue.trim() === ''}>
<button type='submit' className='py-[12px] rounded-lg bg-brown-40 text-base leading-[22px] text-gray-10 disabled:bg-brown-30' disabled={textareaValue.trim() === '' || isLoading}>
답변 완료
</button>
</form>
);

if (answer && answer.isRejected) {
useEffect(() => {
setUpdatedAnswer(answer);
setTextareaValue('');
}, [answer]);

if (updatedAnswer && updatedAnswer.isRejected) {
return (
<div className='flex gap-[12px]'>
{renderProfileImg()}
Expand All @@ -66,7 +103,7 @@ const AnswerContent = ({ answer, name, imageSource }) => {
}

if (isFeedPage) {
if (answer) {
if (updatedAnswer) {
return (
<div className='flex gap-[12px]'>
{renderProfileImg()}
Expand All @@ -81,26 +118,29 @@ const AnswerContent = ({ answer, name, imageSource }) => {
}

if (isAnswerPage) {
if (answer) {
if (updatedAnswer === null || !updatedAnswer) {
return (
<div className='flex gap-[12px]'>
{renderProfileImg()}
<div className='flex-1'>
<p className='mb-[4px] mr-[8px] inline-block text-sm leading-[18px] md:text-lg md:leading-[24px]'>{name}</p>
{renderAnswerForm()}
</div>
</div>
);
}

if (updatedAnswer) {
return (
<div className='flex gap-[12px]'>
{renderProfileImg()}
<div>
{renderAnswerHeader()}
<p className='text-base leading-[22px]'>{answer.content}</p>
<p className='text-base leading-[22px]'>{updatedAnswer.content}</p>
</div>
</div>
);
}
return (
<div className='flex gap-[12px]'>
{renderProfileImg()}
<div className='flex-1'>
<p className='mb-[4px] mr-[8px] inline-block text-sm leading-[18px] md:text-lg md:leading-[24px] font-actor'>{name}</p>
{renderAnswerForm()}
</div>
</div>
);
}

return null;
Expand Down
46 changes: 46 additions & 0 deletions src/components/AnswerDelete/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { deleteAnswer } from 'api/answers';
import { ReactComponent as Close } from 'assets/images/icons/ic_Close.svg';
import PropTypes from 'prop-types';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const AnswerDelete = ({ answerId, onAnswerDeleted }) => {
AnswerDelete.propTypes = {
answerId: PropTypes.number.isRequired,
onAnswerDeleted: PropTypes.func.isRequired,
};

const [isDeleting, setIsDeleting] = useState(false);
// const [error, setError] = useState(null);
const navigate = useNavigate();

const handleAnswerDelete = async () => {
setIsDeleting(true);

try {
const response = await deleteAnswer(answerId);
if (!response.ok) {
throw new Error('답변 삭제 중 오류가 발생했습니다.');
}
onAnswerDeleted(answerId);
} catch (err) {
navigate('/');
} finally {
setIsDeleting(false);
}
};

return (
<button
type='button'
className='flex justify-center items-center gap-2 rounded-lg w-[103px] h-[30px] text-gray-50 hover:text-gray-60 hover:bg-gray-20'
disabled={isDeleting}
onClick={handleAnswerDelete}
>
<Close className='w-3.5 h-3.5 fill-current' />
<p>답변삭제</p>
</button>
);
};

export default AnswerDelete;
23 changes: 23 additions & 0 deletions src/components/AnswerEdit/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import PropTypes from 'prop-types';
import { ReactComponent as Edit } from 'assets/images/icons/ic_Edit.svg';

// eslint-disable-next-line
const AnswerEdit = ({ id, setEditId, answerId }) => {
AnswerEdit.propTypes = {
id: PropTypes.number.isRequired,
setEditId: PropTypes.func.isRequired,
};

const handleEdit = () => {
setEditId(answerId);
};

return (
<button type='button' className='flex justify-center items-center gap-2 rounded-lg w-[103px] h-[30px] text-gray-50 hover:text-gray-60 hover:bg-gray-20' onClick={handleEdit}>
<Edit className='w-3.5 h-3.5 fill-current' />
답변수정
</button>
);
};

export default AnswerEdit;
86 changes: 86 additions & 0 deletions src/components/AnswerEditForm/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
import { putAnswer } from 'api/answers';

// eslint-disable-next-line
const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionList }) => {
AnswerEditForm.propTypes = {
answer: PropTypes.shape({
id: PropTypes.number.isRequired,
content: PropTypes.string.isRequired,
isRejected: PropTypes.bool,
createdAt: PropTypes.string.isRequired,
}),
name: PropTypes.string.isRequired,
imageSource: PropTypes.string,
id: PropTypes.number.isRequired,
};

AnswerEditForm.defaultProps = {
imageSource: 'https://fastly.picsum.photos/id/772/200/200.jpg?hmac=9euSj4JHTPr7uT5QWVmeNJ8JaqAXY8XmJnYfr_DfBJc',
};

// 초기값 설정 시 answer.content가 null일 경우 빈 문자열로 처리
const [textareaValue, setTextareaValue] = useState(answer.content === null || answer.content === 'reject' ? '' : answer.content);
const [isLoading, setIsLoading] = useState(false);
const [isValid, setIsValid] = useState(false);

const handleTextareaChange = (event) => {
const text = event.target.value;
setTextareaValue(text);
const isFormValid = text.trim() !== answer.content.trim() && text !== '' && text !== 'reject';
setIsValid(isFormValid);
};

const handleAnswerPatch = async (e) => {
e.preventDefault();
try {
setIsLoading(true);
const result = await putAnswer(answer.id, {
content: textareaValue, // textareaValue에서 내용을 가져옵니다.
isRejected: false, // 필요하다면 다른 데이터도 추가 가능합니다.
});
setQuestionList((prevQuestions) =>
prevQuestions.map((question) => {
if (question.id === id) {
return { ...question, answer: result };
}
return question;
}),
);
} catch (err) {
// handle error here (e.g., show error message)
} finally {
setIsLoading(false);
setEditId(null);
}
};

const renderProfileImg = () => <img src={imageSource} alt={`${name}의 프로필`} className='w-[32px] h-[32px] md:w-[48px] md:h-[48px] rounded-full object-cover' />;

const renderAnswerForm = () => (
<form onSubmit={handleAnswerPatch} className='flex w-full flex-col gap-[8px]'>
<textarea
className='w-full h-[186px] resize-none rounded-lg border-none p-[16px] bg-gray-20 text-base leading-[22px] text-secondary-900 placeholder:text-base placeholder:leading-[22px] placeholder:text-gray-40 focus:outline-brown-40'
placeholder='답변을 입력해주세요'
value={textareaValue}
onChange={handleTextareaChange}
/>
<button type='submit' className='py-[12px] rounded-lg bg-brown-40 text-base leading-[22px] text-gray-10 disabled:bg-brown-30' disabled={!isValid || isLoading}>
수정 완료
</button>
</form>
);

return (
<div className='flex gap-[12px]'>
{renderProfileImg()}
<div className='flex-1'>
<p className='mb-[4px] mr-[8px] inline-block text-sm leading-[18px] md:text-lg md:leading-[24px]'>{name}</p>
{renderAnswerForm()}
</div>
</div>
);
};

export default AnswerEditForm;
58 changes: 58 additions & 0 deletions src/components/AnswerRejection/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
import { postAnswer } from 'api/answers';
import { ReactComponent as Rejection } from 'assets/images/icons/ic_Rejection.svg';

const AnswerRejection = ({ id, setQuestionList }) => {
AnswerRejection.propTypes = {
id: PropTypes.number.isRequired,
setQuestionList: PropTypes.func.isRequired,
};

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

const handleRejection = async () => {
const defaultContent = 'reject';
try {
setIsLoading(true);
setError(null);

const result = await postAnswer(id, {
content: defaultContent,
isRejected: true,
});

setQuestionList((prevQuestions) =>
prevQuestions.map((question) => {
if (question.id === id) {
return { ...question, answer: result };
}
return question;
}),
);
} catch (err) {
setError('답변 거절 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};

if (error) {
return <div>에러: {error}</div>;
}

return (
<button
type='button'
className='flex justify-center items-center gap-2 rounded-lg w-[103px] h-[30px] text-gray-50 hover:text-gray-60 hover:bg-gray-20'
onClick={handleRejection}
disabled={isLoading}
>
<Rejection className='w-3.5 h-3.5 fill-current' />
답변거절
</button>
);
};

export default AnswerRejection;
Loading
Loading