Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5024f5c
:sparkles: feat : 피드 헤더 레이아웃 구현
Dec 14, 2024
2372391
:lipstick: chore : 공유하기 버튼 svg 스타일링
Dec 14, 2024
3db96a1
:white_check_mark: chore : 프로필 사진 및 이름 불러오는 handleLoad 함수 구현(테스트용)
Dec 14, 2024
4585c65
:bento: chore : 헤더이미지파일 추가 및 코드 수정
Dec 15, 2024
39c91cf
:lipstick: chore : PR 리뷰 내용 반영하여 스타일 수정
Dec 15, 2024
dc77f77
:twisted_rightward_arrows: chore : feat/헤더_레이아웃을 epic/Answer_화면_구현으로 …
Dec 15, 2024
8ba225d
:sparkles: feat : URL 복사 알림용 토스트 구현 시작
Dec 15, 2024
d53c802
:lipstick: chore : 토스트 기본 레이아웃 구현
Dec 15, 2024
c03bb9f
:sparkles: chore : URL버튼 클릭시 클립보드에 현재 URL 복사하는 handleCopyUrl추가 및 onCl…
Dec 15, 2024
2bc9a0f
:sparkles: chore : npm prop types 다운로드, 링크 복사 버튼 클릭 시 토스트 메시지 보여주는 동작 구현
Dec 15, 2024
e0ca7f6
:lipstick: chore : 토스트 애니메이션 효과 구현, tailwind.config.js 수정
Dec 16, 2024
d6b016a
:lipstick: chore : 토스트 애니메이션 개선 및 tailwind.config.js 수정
Dec 17, 2024
a18ff84
:sparkles: feat : 삭제하기 버튼 기본 레이아웃 및 스타일 작업
Dec 18, 2024
b2efb23
Merge branch 'merge' into feat/삭제하기_버튼_구현
Dec 18, 2024
8e2c0c5
:sparkles: feat : 삭제하기 버튼 클릭 시 로컬id 삭제 및 루트로 이동 기능
Dec 18, 2024
e779909
:truck: chore : toast 컴포넌트 및 파일 이름을 toastUrlCopy로 변경
Dec 18, 2024
c2d9dc8
:sparkles: feat : 삭제하기 버튼 클릭 시 삭제 완료 안내 토스트 구현
Dec 18, 2024
91ae4f0
:art: chore : 토스트 애니메이션 동작 시간 수정 and :bug: fixed : 프로필 사진 렌더링 시 src 오…
Dec 18, 2024
cc07fd7
Merge branch 'epic/Answer_화면_구현' into feat/삭제하기_버튼_구현
junAlexx Dec 18, 2024
6408b4f
:bug: fixed: prettier 문제로 발생한 빌드 오류 수정에 관한 커밋
Dec 18, 2024
836e30d
:truck: chore : 첫글자 소문자로 적은 폴더 이름 대문자로 변경, 기존 toast 컴포넌트와 폴더 이름을 toa…
Dec 18, 2024
ddb9f35
:truck: chore : 로컬에서 폴더명을 수정했으나 원격에서 인식하지 못해 다시 원상복구 시키는 커밋입니다.
Dec 18, 2024
f50d4b8
Merge branch 'feat/삭제하기_버튼_구현' into epic/Answer_화면_구현
Dec 18, 2024
746131b
Merge branch 'epic/Answer_화면_구현' of https://github.com/codeit-part2-p…
Dec 19, 2024
8532805
Merge branch 'epic/Answer_화면_구현' of https://github.com/codeit-part2-p…
Dec 21, 2024
3bd91d5
:sparkles: feat : 답변 등록 구현
Dec 22, 2024
a3d7264
Merge branch 'epic/Answer_화면_구현' of https://github.com/codeit-part2-p…
Dec 22, 2024
6c12ba0
:sparkles: feat : 케밥 답변삭제 구현
Dec 22, 2024
5d66a5e
Merge branch 'epic/Answer_화면_구현' into feat/케밥_답변삭제_구현
Dec 22, 2024
14df884
:sparkles: feat : 답변 삭제 버튼 클릭 시 원래 상태로 렌더링 되는 기능 구현
Dec 22, 2024
c9f90ae
:recycle: chore : textarea 초기화
ToKyun02 Dec 22, 2024
3c086be
:art: chore : 필요없는 주석 삭제 및 코드 정리
Dec 22, 2024
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
79 changes: 60 additions & 19 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,9 @@ const AnswerContent = ({ answer, name, imageSource }) => {

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

const isFeedPage = location.pathname.startsWith('/post/') && !location.pathname.includes('/answer');
const isAnswerPage = location.pathname.startsWith('/post/') && location.pathname.includes('/answer');
Expand All @@ -28,32 +35,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]'>{name}</p>
<p className='text-sm font-medium leading-[18px] text-gray-40'>{formatCreatedAt(answer.createdAt)}</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 +104,7 @@ const AnswerContent = ({ answer, name, imageSource }) => {
}

if (isFeedPage) {
if (answer) {
if (updatedAnswer) {
return (
<div className='flex gap-[12px]'>
{renderProfileImg()}
Expand All @@ -81,26 +119,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]'>{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;
1 change: 0 additions & 1 deletion src/components/AnswerStatus/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import PropTypes from 'prop-types';
// import Kebab from 'components/Kebab';

const AnswerStatus = ({ answer }) => {
AnswerStatus.propTypes = {
Expand Down
26 changes: 12 additions & 14 deletions src/components/Kebab/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@ import QuestionDelete from 'components/QuestionDelete';
import kebab from 'assets/images/icons/ic_Kebab.svg';
import { ReactComponent as Rejection } from 'assets/images/icons/ic_Rejection.svg';
import { ReactComponent as Edit } from 'assets/images/icons/ic_Edit.svg';
import { ReactComponent as Close } from 'assets/images/icons/ic_Close.svg';
import AnswerDelete from 'components/AnswerDelete';

const Kebab = ({ id, isAnswer, isKebabOpen, onKebabClick, onDeleteQuestion }) => {
const Kebab = ({ id, isAnswer, isKebabOpen, onKebabClick, onDeleteQuestion, onAnswerDeleted }) => {
Kebab.propTypes = {
id: PropTypes.number.isRequired,
isAnswer: PropTypes.shape({
id: PropTypes.number.isRequired,
questionId: PropTypes.number.isRequired,
content: PropTypes.string.isRequired,
isRejected: PropTypes.bool.isRequired,
createdAt: PropTypes.string.isRequired,
}),
id: PropTypes.number.isRequired,
isKebabOpen: PropTypes.bool.isRequired,
onKebabClick: PropTypes.func.isRequired,
onDeleteQuestion: PropTypes.func.isRequired,
onAnswerDeleted: PropTypes.func.isRequired,
};

const menuRef = useRef(null);
Expand All @@ -28,8 +29,10 @@ const Kebab = ({ id, isAnswer, isKebabOpen, onKebabClick, onDeleteQuestion }) =>
onKebabClick(id);
};

const handleMenuItemClick = () => {
onKebabClick(id);
const onDeleteAnswer = () => {
if (isAnswer) {
onAnswerDeleted(isAnswer.id);
}
};

useEffect(() => {
Expand All @@ -46,19 +49,17 @@ const Kebab = ({ id, isAnswer, isKebabOpen, onKebabClick, onDeleteQuestion }) =>
};
}, [id, onKebabClick]);

// () => onKebabClick(id)

return (
<div ref={menuRef} className='relative flex items-center'>
<button type='button' onClick={handleMenuToggle}>
<img src={kebab} alt='kebab' className='w-[26px] h-[26px]' />
</button>
{isKebabOpen && (
<menu className='absolute top-[26px] end-0 w-[103px] py-1 bg-gray-10 text-sm/[18px] font-medium border border-gray-30 rounded-lg shadow-2pt'>
<menu type='button' className='absolute top-[26px] end-0 w-[103px] py-1 bg-gray-10 text-sm/[18px] font-medium border border-gray-30 rounded-lg shadow-2pt'>
{!isAnswer ? (
<>
<div className='flex justify-center items-center rounded-lg'>
<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={handleMenuItemClick}>
<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'>
<Rejection className='w-3.5 h-3.5 fill-current' />
<p>답변거절</p>
</button>
Expand All @@ -70,7 +71,7 @@ const Kebab = ({ id, isAnswer, isKebabOpen, onKebabClick, onDeleteQuestion }) =>
) : (
<>
<div className='flex justify-center items-center'>
<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={handleMenuItemClick}>
<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'>
<Edit className='w-3.5 h-3.5 fill-current' />
<p>수정하기</p>
</button>
Expand All @@ -79,10 +80,7 @@ const Kebab = ({ id, isAnswer, isKebabOpen, onKebabClick, onDeleteQuestion }) =>
<QuestionDelete id={id} onDeleteQuestion={onDeleteQuestion} />
</div>
<div className='flex justify-center items-center'>
<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={handleMenuItemClick}>
<Close className='w-3.5 h-3.5 fill-current' />
<p>답변삭제</p>
</button>
<AnswerDelete answerId={isAnswer.id} onAnswerDeleted={onDeleteAnswer} />
</div>
</>
)}
Expand Down
19 changes: 17 additions & 2 deletions src/components/QnAList/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,28 @@ const QnAList = ({ name, imageSource, questionList, setQuestionList, onDeleteQue

const handleKebabClick = (id) => {
setVisibleMenuId((prevVisibleMenuId) => (prevVisibleMenuId === id ? null : id));
// setVisibleMenuId(visibleMenuId === id ? null : id);
};

const handleDeleteQuestion = (questionId) => {
setQuestionList((prevQuestions) => prevQuestions.filter((question) => question.id !== questionId));
onDeleteQuestion(questionId);
};

const handleAnswerSubmit = (questionId, answer) => {
setQuestionList((prevQuestions) => prevQuestions.map((question) => (question.id === questionId ? { ...question, answer } : question)));
};

const handleAnswerDeleted = (answerId) => {
setQuestionList((prevQuestions) =>
prevQuestions.map((question) => {
if (question.answer && question.answer.id === answerId) {
return { ...question, answer: null };
}
return question;
}),
);
};

return (
<div>
{questionList.length > 0 ? (
Expand All @@ -63,11 +77,12 @@ const QnAList = ({ name, imageSource, questionList, setQuestionList, onDeleteQue
onKebabClick={handleKebabClick}
onClick={handleDeleteQuestion}
onDeleteQuestion={handleDeleteQuestion}
onAnswerDeleted={handleAnswerDeleted}
/>
)}
</div>
<QuestionContent createdAt={question.createdAt} content={question.content} />
<AnswerContent answer={question.answer} name={name} imageSource={imageSource} />
<AnswerContent answer={question.answer} name={name} imageSource={imageSource} id={question.id} onAnswerSubmit={handleAnswerSubmit} />
<CountFavorite like={question.like} dislike={question.dislike} />
</li>
))}
Expand Down
1 change: 1 addition & 0 deletions src/pages/Answer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const Answer = () => {

fetchQuestions();
}, [subjectId, offset]);

const loadMoreQuestions = useCallback(
(entries) => {
const [entry] = entries;
Expand Down
Loading