Skip to content
New issue

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

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

Already on GitHub? # to your account

feat: 구인글 상세페이지 댓글, 대댓글 기능 구현 #61

Merged
merged 28 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1b6bdb7
feat: #57 컴포넌트화
prgmr99 Feb 20, 2024
205417f
feat: #57 컴포넌트화
prgmr99 Feb 20, 2024
264ec25
refactor: #57 Lifting- state up 적용
prgmr99 Feb 20, 2024
680ec23
feat: #57 댓글 달기
prgmr99 Feb 21, 2024
15f8439
feat: 답글 생성 댓글마다 분리
prgmr99 Feb 21, 2024
d9d57a9
feat: #57 답글 생성
prgmr99 Feb 21, 2024
6c113bb
feat: #57 답글 depth 1 고정
prgmr99 Feb 22, 2024
a72d753
feat: #57 케밥 추가
prgmr99 Feb 22, 2024
8d8c47f
refactor: #57 모든 댓글, 대댓글 구조 재설계
prgmr99 Feb 23, 2024
fa48c67
feat: #57 댓글 생성 리팩토링
prgmr99 Feb 23, 2024
5096c39
feat: #57 대댓글 삭제 구현
prgmr99 Feb 24, 2024
31a1d89
refactor: #57 대댓글 삭제 코드 축약
prgmr99 Feb 24, 2024
e65918c
feat: #57 댓글 삭제 구현
prgmr99 Feb 24, 2024
dd21643
feat: #57 댓글 및 대댓글 수정 구현
prgmr99 Feb 24, 2024
f6878e2
feat: #57 댓글 및 대댓글 수정 구현
prgmr99 Feb 24, 2024
7656a1f
chore: #57 사용하지 않는 코드 수정
prgmr99 Feb 24, 2024
43b6a2f
Merge branch 'release-1.0' into feat/#57_reply
prgmr99 Feb 24, 2024
71cff83
chore: #57 사용하지 않는 코드 수정
prgmr99 Feb 24, 2024
1641459
fix: PR 피드백 모두 반영
prgmr99 Feb 25, 2024
8873ee7
chore: div -> article로 수정
prgmr99 Feb 25, 2024
914a19e
chore: form 태그 추가
prgmr99 Feb 25, 2024
c93e8f8
chore: 함수 네이밍 수정(addComment -> addReply)
prgmr99 Feb 25, 2024
f623bf2
chore: 라우팅 경로 수정
prgmr99 Feb 25, 2024
bfb4d15
chore: 필요없는 파일 삭제
prgmr99 Feb 25, 2024
d3c99a7
chore: 필요없는 파일 삭제
prgmr99 Feb 25, 2024
477f797
fix: 답글 타입 제거
prgmr99 Feb 27, 2024
afc175d
fix: 답글 타입 제거
prgmr99 Feb 27, 2024
29fac90
fix: #57 type 에러 수정
prgmr99 Feb 27, 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
73 changes: 73 additions & 0 deletions src/components/comment/comment/Comment.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import styled from 'styled-components';

const Comment = styled.li`
.wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}

.container {
display: flex;
gap: 2rem;
align-items: center;
width: 100%;

.input-edit {
width: 86%;
border: none;
border-bottom: 1px solid #373f41;
background-color: transparent;
outline: none;
padding-bottom: 0.5rem;
font-size: 1.5rem;
}
}

.comment-icon {
display: flex;
}

.comment-info {
display: flex;
flex-direction: column;
gap: 0.3rem;

span:nth-child(1) {
color: #434343;
font-size: 1.2rem;
font-style: normal;
font-weight: 400;
line-height: 1.35rem;
letter-spacing: 0.015rem;
}
span:nth-child(2) {
padding: 0.4rem 1.5rem;
background-color: #fff;
color: #373f41;
font-size: 1.5rem;
font-style: normal;
font-weight: 400;
line-height: 150%;
letter-spacing: 0.015rem;
border-radius: 0.75rem;
}
}

.reply-btn {
background-color: transparent;
margin-left: -1.5rem;
}

.container-reply__lists {
display: flex;
flex-direction: column;
margin-top: 2rem;
margin-left: 5rem;
gap: 2rem;
}
`;

const S = { Comment };

export default S;
154 changes: 154 additions & 0 deletions src/components/comment/comment/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import { Icon, KebabMenu, ReplyComment, ReplyInput } from '../..';
import S from './Comment.styled';
import { CommentForm, ReplyForm } from '../../../types';
import { useNavigate } from 'react-router-dom';

const Comment = ({ id, username, content, replies, deleteComment }: CommentForm) => {
const navigate = useNavigate();
const isLogin = true; // 임시 코드
const [replyClicked, setReplyClicked] = useState<boolean>(false);
const [value, setValue] = useState<string>(content);
const [contents, setContents] = useState<string>('');
const [showKebab, setShowKebab] = useState<boolean>(true);
const isValid = isLogin && username === 'yeom';
const [repliesList, setRepliesList] = useState<ReplyForm[]>(replies);
const [isEdit, setIsEdit] = useState<boolean>(false);
const optionLists = [
{
title: '수정',
optionClickHandler: () => {
setIsEdit(true);
setShowKebab(false);
},
},
{
title: '삭제',
optionClickHandler: () => {
setShowKebab(false);
if (deleteComment) {
deleteComment(id);
}
},
},
];
const deleteReply = (id: string) => {
setRepliesList(prevReplies => prevReplies.filter(v => v.id !== id));
};

const handleReplyClick = () => {
setReplyClicked(true);
};

const addReply = () => {
if (contents !== '' && contents.trim() !== '') {
const newComment = {
id: id + '-' + repliesList.length.toString(),
username: 'yeom',
content: contents,
};
setRepliesList([...repliesList, newComment]);
setContents('');
setReplyClicked(false);
}
};
const editComment = () => {
setIsEdit(false);
setShowKebab(true);
};

const onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
const target = event.currentTarget;
if (target.value.length !== 0 && event.key === 'Enter') {
event.preventDefault();
addReply();
}
};

const onKeyPressEdit = (event: React.KeyboardEvent<HTMLInputElement>) => {
const target = event.currentTarget;
if (target.value.length !== 0 && event.key === 'Enter') {
event.preventDefault();
editComment();
}
};

const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
setContents(event.target.value);
};

const onClickInput = () => {
if (!isLogin) {
// 로그인 페이지로 이동
navigate('/signin');
}
};

const onChangeEdit = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};

return (
<S.Comment>
<section className='wrapper'>
<section className='container'>
<div className='comment-icon'>
<Icon />
</div>
{!isEdit ? (
<div className='comment-info'>
<span>{username}</span>
<span>{value}</span>
</div>
) : (
<input
type='text'
className='input-edit'
placeholder='댓글 입력'
value={value}
onChange={onChangeEdit}
onKeyPress={onKeyPressEdit}
/>
)}
{isValid && (
<button
type='button'
onClick={!isEdit ? handleReplyClick : editComment}
className='reply-btn'
>
{isEdit ? '수정' : '답글'}
</button>
)}
</section>
{isValid && showKebab && <KebabMenu options={optionLists} />}
</section>
<section>
<ul className='container-reply__lists'>
{repliesList?.map(reply => {
return (
<ReplyComment
key={reply.id}
id={reply.id}
username={reply.username}
content={reply.content}
deleteReply={() => deleteReply(reply.id)}
/>
);
})}
</ul>
{replyClicked && (
<ReplyInput
key={id}
contents={contents}
addComment={addReply}
onKeyPress={onKeyPress}
onChangeHandler={onChangeHandler}
onClickInput={onClickInput}
/>
)}
</section>
</S.Comment>
);
};

export default Comment;
20 changes: 20 additions & 0 deletions src/components/comment/comment/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { Comment } from '../../index';

const CommentList = (comments: Comment[]) => {
return (
<div>
{comments.map(comment => (
<Comment
key={comment.id}
id={comment.id}
username={comment.username}
content={comment.content}
replies={comment.replies}
/>
))}
</div>
);
};

export default CommentList;
11 changes: 11 additions & 0 deletions src/components/comment/comment/ReplyComment.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from 'styled-components';

const ReplyComment = styled.div`
.comment-icon {
width: 2.7rem;
}
`;

const S = { ReplyComment };

export default S;
81 changes: 81 additions & 0 deletions src/components/comment/comment/ReplyComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import S from './ReplyComment.styled';
import { Icon, KebabMenu } from '../..';
import { ReplyForm } from '../../../types';

const ReplyComment = ({ id, username, content, deleteReply }: ReplyForm) => {
const isLogin = true; // 임시 코드
const [value, setValue] = useState<string>(content);
const [showKebab, setShowKebab] = useState<boolean>(true);
const [isEdit, setIsEdit] = useState<boolean>(false);
const isValid = isLogin && username === 'yeom' && showKebab;
const optionLists = [
{
title: '수정',
optionClickHandler: () => {
setIsEdit(true);
setShowKebab(false);
},
},
{
title: '삭제',
optionClickHandler: () => {
setShowKebab(false);
if (deleteReply) {
deleteReply(id);
}
},
},
];

const editComment = () => {
setIsEdit(false);
setShowKebab(true);
};

const onChangeEdit = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};

const onKeyPressEdit = (event: React.KeyboardEvent<HTMLInputElement>) => {
const target = event.currentTarget;
if (target.value.length !== 0 && event.key === 'Enter') {
event.preventDefault();
editComment();
}
};
return (
<S.ReplyComment>
<section className='wrapper'>
<section className='container'>
<div className='comment-icon'>
<Icon />
</div>
{!isEdit ? (
<div className='comment-info'>
<span>{username}</span>
<span>{value}</span>
</div>
) : (
<>
<input
type='text'
className='input-edit'
placeholder='댓글 입력'
value={value}
onChange={onChangeEdit}
onKeyPress={onKeyPressEdit}
/>
<button type='button' onClick={editComment} className='reply-btn'>
수정
</button>
</>
)}
</section>
{isValid && <KebabMenu options={optionLists} />}
</section>
</S.ReplyComment>
);
};

export default ReplyComment;
52 changes: 52 additions & 0 deletions src/components/comment/commentInput/CommentInput.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import styled from 'styled-components';

const CommentInput = styled.div`
.user-input {
display: flex;
gap: 2rem;

.user-input__icon {
width: 3.15rem;
height: 3.15rem;
flex-shrink: 0;
}

.user-input__container {
display: flex;
gap: 1rem;
width: 100%;

input {
width: 89%;
height: 3.75rem;
flex-shrink: 0;
border-radius: 0.75rem;
border: 0.75px solid #bebebe;
background: #fff;
outline: none;
padding-left: 1.3rem;
}

button {
display: flex;
width: 6.7rem;
height: 3.75rem;
padding: 0.75rem;
justify-content: center;
align-items: center;
gap: 0.75rem;
flex-shrink: 0;
border-radius: 0.6rem;
background: var(--main-color, #5877fc);
border: none;
outline: none;
color: #fff;
cursor: pointer;
}
}
}
`;

const S = { CommentInput };

export default S;
Loading