Skip to content
Merged
10 changes: 9 additions & 1 deletion src/components/AnswerContent/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import formatCreatedAt from 'utils/dateUtils';
import { postAnswer } from 'api/answers';

const AnswerContent = ({ answer, name, imageSource, id, onAnswerSubmit }) => {
const AnswerContent = ({ answer, name, imageSource, id, onAnswerSubmit, setIsKebabLoading, setIsToast }) => {
AnswerContent.propTypes = {
answer: PropTypes.shape({
id: PropTypes.number.isRequired,
Expand All @@ -16,6 +16,8 @@ const AnswerContent = ({ answer, name, imageSource, id, onAnswerSubmit }) => {
imageSource: PropTypes.string,
id: PropTypes.number.isRequired,
onAnswerSubmit: PropTypes.func.isRequired,
setIsKebabLoading: PropTypes.func.isRequired,
setIsToast: PropTypes.func.isRequired,
};

AnswerContent.defaultProps = {
Expand Down Expand Up @@ -45,15 +47,21 @@ const AnswerContent = ({ answer, name, imageSource, id, onAnswerSubmit }) => {

let response;
try {
setIsKebabLoading(true);
setIsLoading(true);
response = await postAnswer(id, postBody);
setUpdatedAnswer(response);
onAnswerSubmit(id, response);
setIsToast('등록');
} catch (err) {
// eslint-disable-next-line
console.error(err);
} finally {
setIsKebabLoading(false);
setIsLoading(false);
setTimeout(() => {
setIsToast(null);
}, 3000);
}
};

Expand Down
55 changes: 43 additions & 12 deletions src/components/AnswerDelete/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,38 @@ 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';
import ConfirmModal from 'components/ConfirmModal'; // Import the modal component

const AnswerDelete = ({ answerId, onAnswerDeleted }) => {
const AnswerDelete = ({ id, answerId, onAnswerDeleted, onKebabClick, setIsKebabLoading, setIsToast, editId, setEditId }) => {
AnswerDelete.propTypes = {
id: PropTypes.number.isRequired,
answerId: PropTypes.number.isRequired,
onAnswerDeleted: PropTypes.func.isRequired,
onKebabClick: PropTypes.func.isRequired,
setIsKebabLoading: PropTypes.func.isRequired,
setIsToast: PropTypes.func.isRequired,
editId: PropTypes.number.isRequired,
setEditId: PropTypes.func.isRequired,
};

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

const handleAnswerDelete = async () => {
const [showModal, setShowModal] = useState(false);

const handleDelete = async () => {
setShowModal(true); // Show the modal when delete is clicked
};

const handleModalCancel = () => {
onKebabClick(id);
setShowModal(false); // Close the modal if canceled
};

const handleModalConfirm = async () => {
onKebabClick(id);
setIsKebabLoading(true);
setShowModal(false); // Close the modal
setIsDeleting(true);

try {
Expand All @@ -23,23 +43,34 @@ const AnswerDelete = ({ answerId, onAnswerDeleted }) => {
throw new Error('답변 삭제 중 오류가 발생했습니다.');
}
onAnswerDeleted(answerId);
setIsToast('답변');
} catch (err) {
navigate('/');
} finally {
setIsDeleting(false);
setIsKebabLoading(false);
if (editId !== null) {
setEditId(null);
}
setTimeout(() => {
setIsToast(null);
}, 3000);
}
};

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>
<>
<button
type='button'
className='flex justify-center items-center gap-2 rounded-lg w-[103px] h-[30px] text-gray-50 hover:text-blue-50 hover:bg-gray-20'
disabled={isDeleting}
onClick={handleDelete}
>
<Close className='w-3.5 h-3.5 fill-current' />
<p>답변삭제</p>
</button>
<ConfirmModal isOpen={showModal} onConfirm={handleModalConfirm} onCancel={handleModalCancel} message='답변을 삭제하시겠습니까?' />
</>
);
};

Expand Down
15 changes: 10 additions & 5 deletions src/components/AnswerEdit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ 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 }) => {
const AnswerEdit = ({ id, editId, setEditId, answerId, onKebabClick }) => {
AnswerEdit.propTypes = {
id: PropTypes.number.isRequired,
editId: PropTypes.number.isRequired,
setEditId: PropTypes.func.isRequired,
onKebabClick: PropTypes.func.isRequired,
};

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

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

Expand Down
48 changes: 40 additions & 8 deletions src/components/AnswerEditForm/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
import { putAnswer } from 'api/answers';
import { ReactComponent as Close } from 'assets/images/icons/ic_Close.svg';
import ConfirmModal from 'components/ConfirmModal';

// eslint-disable-next-line
const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionList }) => {
const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionList, setIsKebabLoading, setIsToast }) => {
AnswerEditForm.propTypes = {
answer: PropTypes.shape({
id: PropTypes.number.isRequired,
Expand All @@ -14,6 +16,8 @@ const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionL
name: PropTypes.string.isRequired,
imageSource: PropTypes.string,
id: PropTypes.number.isRequired,
setIsKebabLoading: PropTypes.func.isRequired,
setIsToast: PropTypes.func.isRequired,
};

AnswerEditForm.defaultProps = {
Expand All @@ -24,6 +28,7 @@ const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionL
const [textareaValue, setTextareaValue] = useState(answer.content === null || answer.content === 'reject' ? '' : answer.content);
const [isLoading, setIsLoading] = useState(false);
const [isValid, setIsValid] = useState(false);
const [showModal, setShowModal] = useState(false);

const handleTextareaChange = (event) => {
const text = event.target.value;
Expand All @@ -35,11 +40,13 @@ const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionL
const handleAnswerPatch = async (e) => {
e.preventDefault();
try {
setIsKebabLoading(true);
setIsLoading(true);
const result = await putAnswer(answer.id, {
content: textareaValue, // textareaValue에서 내용을 가져옵니다.
isRejected: false, // 필요하다면 다른 데이터도 추가 가능합니다.
});
setIsToast('수정');
setQuestionList((prevQuestions) =>
prevQuestions.map((question) => {
if (question.id === id) {
Expand All @@ -51,15 +58,32 @@ const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionL
} catch (err) {
// handle error here (e.g., show error message)
} finally {
setIsKebabLoading(false);
setIsLoading(false);
setEditId(null);
setTimeout(() => {
setIsToast(null);
}, 3000);
}
};

const onCancelClick = () => {
setShowModal(true);
};

const handleModalCancel = () => {
setShowModal(false);
};

const handleModalConfirm = () => {
setShowModal(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]'>
<form onSubmit={handleAnswerPatch} className='relative 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='답변을 입력해주세요'
Expand All @@ -73,13 +97,21 @@ const AnswerEditForm = ({ answer, name, imageSource, id, setEditId, setQuestionL
);

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 className='flex gap-[12px]'>
{renderProfileImg()}
<div className='flex-1'>
<div className='flex justify-between items-center mb-[4px]'>
<p className='mr-[8px] inline-block text-sm leading-[18px] md:text-lg md:leading-[24px]'>{name}</p>
<button type='button' className='mr-[4px] w-3 h-3 md:w-4 md:h-4 fill-current text-gray-50' onClick={onCancelClick}>
<Close className='w-3 h-3 md:w-4 md:h-4' />
</button>
</div>
{renderAnswerForm()}
</div>
</div>
</div>
<ConfirmModal isOpen={showModal} onConfirm={handleModalConfirm} onCancel={handleModalCancel} message='수정 중인 답변이 있습니다. 취소하시겠습니까?' />
</>
);
};

Expand Down
14 changes: 12 additions & 2 deletions src/components/AnswerRejection/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { useState } from 'react';
import { postAnswer } from 'api/answers';
import { ReactComponent as Rejection } from 'assets/images/icons/ic_Rejection.svg';

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

const [isLoading, setIsLoading] = useState(false);
Expand All @@ -15,6 +18,8 @@ const AnswerRejection = ({ id, setQuestionList }) => {
const handleRejection = async () => {
const defaultContent = 'reject';
try {
onKebabClick(id);
setIsKebabLoading(true);
setIsLoading(true);
setError(null);

Expand All @@ -31,10 +36,15 @@ const AnswerRejection = ({ id, setQuestionList }) => {
return question;
}),
);
setIsToast('거절');
} catch (err) {
setError('답변 거절 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
setIsKebabLoading(false);
setTimeout(() => {
setIsToast(null);
}, 3000);
}
};

Expand All @@ -45,7 +55,7 @@ const AnswerRejection = ({ id, setQuestionList }) => {
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'
className='flex justify-center items-center gap-2 rounded-lg w-[103px] h-[30px] text-gray-50 hover:text-blue-50 hover:bg-gray-20'
onClick={handleRejection}
disabled={isLoading}
>
Expand Down
57 changes: 57 additions & 0 deletions src/components/ConfirmModal/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';

const ConfirmModal = ({ isOpen, onConfirm, onCancel, message }) => {
ConfirmModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
message: PropTypes.string.isRequired,
};

const modalRef = useRef(null);

// Close the modal if the user clicks outside of it
useEffect(() => {
const handleClickOutside = (event) => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
onCancel(); // Close the modal if clicked outside
}
};

if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onCancel]);

if (!isOpen) return null;

return (
<div className='modal-overlay fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50'>
<div
ref={modalRef}
className='fixed left-1/2 -translate-x-1/2 modal-content p-6 rounded-2xl bg-gray-20 shadow-2pt animate-slide-up-fade-modal'
role='dialog'
aria-labelledby='confirm-modal-title'
>
<h2 id='confirm-modal-title' className='text-center text-lg font-medium mb-4'>
{message}
</h2>
<div className='flex justify-center gap-4'>
<button type='button' onClick={onConfirm} className='bg-brown-40 text-white px-5 py-2 rounded-xl transition ease-in-out hover:-translate-y-1 hover:scale-105'>
확인
</button>
<button type='button' onClick={onCancel} className='bg-gray-300 text-black px-5 py-2 rounded-xl transition ease-in-out hover:-translate-y-1 hover:scale-105'>
취소
</button>
</div>
</div>
</div>
);
};

export default ConfirmModal;
2 changes: 1 addition & 1 deletion src/components/DeleteIdBtn/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const DeleteIdBtn = ({ onClick, id }) => {
<div className='relative w-full max-w-[716px] min-w-[144px] h-[25px] md:h-[35px] mx-6 mb-1.5 md:mx-8 md:mb-[9px]'>
<button
type='button'
className='absolute right-0 w-[70px] min-w-[70px] h-[25px] md:w-[100px] md:h-[35px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] px-[17.5px] md:px-[24px] md:py-[5px] rounded-[200px] bg-brown-40 font-normal text-gray-10 text-[10px]/[25px] md:text-[15px]/[25px]'
className='absolute right-0 w-[70px] min-w-[70px] h-[25px] md:w-[100px] md:h-[35px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] px-[17.5px] md:px-[24px] md:py-[5px] rounded-[200px] bg-brown-40 font-normal text-gray-10 text-[10px]/[25px] md:text-[15px]/[25px] hover:scale-105'
onClick={handleDelete}
>
삭제하기
Expand Down
Loading
Loading