-
Notifications
You must be signed in to change notification settings - Fork 39
[김수민] Sprint7 #193
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
The head ref may contain hidden characters: "React-\uAE40\uC218\uBBFC-sprint7"
[김수민] Sprint7 #193
Changes from all commits
dcca953
d0a2f04
3572fbc
13e30e0
f611aad
42a2331
b55eeaf
9e3bce7
b7ebfcc
decf210
2a5f239
e400e98
ea0dc96
fd9cc77
8ff8160
0d57de3
9bac9bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { baseAPI } from './axios'; | ||
|
|
||
| export const productCommentAPI = { | ||
| // 상품 댓글 조회 | ||
| getProductComments: async (productId, limit, cursor) => { | ||
|
Comment on lines
+3
to
+5
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안 https://developer.mozilla.org/ko/docs/Web/API/URLSearchParams
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안 |
||
| try { | ||
| // cursor가 null이면 limit만 전달 | ||
| const queryParams = cursor | ||
| ? `limit=${limit}&cursor=${cursor}` | ||
| : `limit=${limit}`; | ||
|
|
||
| const response = await baseAPI.get( | ||
| `/products/${productId}/comments?${queryParams}` | ||
| ); | ||
| return response.data; | ||
| } catch (error) { | ||
| throw new Error('댓글을 불러오는데 실패했습니다.'); | ||
| } | ||
| }, | ||
|
|
||
| }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 여담
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import styles from './styles/CommentItemMenu.module.css'; | ||
|
|
||
| export default function CommentItemMenu({ isOpen, onClick }) { | ||
| return ( | ||
| <div className={styles.commentItemMenu}> | ||
| <button type='button' onClick={onClick}> | ||
| <img src='/images/common/ic_kebab.svg' alt='메뉴 열기' /> | ||
| </button> | ||
| {isOpen && ( | ||
| <ul> | ||
| <li> | ||
| <button type='button'>수정하기</button> | ||
| </li> | ||
| <li> | ||
| <button type='button'>삭제하기</button> | ||
| </li> | ||
| </ul> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||||||||
| import { useLocation } from 'react-router-dom'; | ||||||||||||
| import styles from './styles/DetailBackToListButton.module.css'; | ||||||||||||
| import { useNavigate } from 'react-router-dom'; | ||||||||||||
|
Comment on lines
+1
to
+3
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안
Suggested change
|
||||||||||||
|
|
||||||||||||
| export default function DetailBackToListButton() { | ||||||||||||
| const navigate = useNavigate(); | ||||||||||||
| const location = useLocation(); | ||||||||||||
| const searchParams = new URLSearchParams(location.search); | ||||||||||||
|
Comment on lines
+6
to
+8
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💊 제안
Suggested change
|
||||||||||||
|
|
||||||||||||
| const handleBackToList = () => { | ||||||||||||
| const prevQuery = location.state?.prevQuery || ''; | ||||||||||||
| navigate(`/items${prevQuery}`); | ||||||||||||
| }; | ||||||||||||
| return ( | ||||||||||||
| <button className={styles.backToList} onClick={handleBackToList}> | ||||||||||||
| 목록으로 돌아가기 <img src='/images/common/ic_back.svg' alt='목록으로 돌아가기' /> | ||||||||||||
| </button> | ||||||||||||
| ); | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import styles from './styles/DetailComment.module.css'; | ||
| import { useProductComment } from '@/hooks/useProductComment'; | ||
| import { relativeTime } from '@/utils/relativeTimeUtils'; | ||
| import DetailProfile from '@/components/Detail/DetailProfile'; | ||
| import CommentItemMenu from '@/components/Detail/CommentItemMenu'; | ||
| import { useState } from 'react'; | ||
|
|
||
| export default function DetailComment({ productId }) { | ||
| const { productComments, loading, error } = useProductComment(productId, 3, null); | ||
| const [openMenuId, setOpenMenuId] = useState(null); | ||
|
|
||
| if (loading) return <div>로딩 중...</div>; | ||
| if (error) return <div className={styles.error}>{error}</div>; | ||
| if (!productComments?.list?.length) return ( | ||
| <div className={styles.noComments}> | ||
| <img src='/images/common/img_inquiry_empty.png' alt='댓글이 없습니다.' /> | ||
| <p>아직 문의가 없어요</p> | ||
| </div> | ||
| ); | ||
|
|
||
| const onClickMenu = (commentId) => { | ||
| setOpenMenuId(openMenuId === commentId ? null : commentId); | ||
| }; | ||
|
|
||
| return ( | ||
| <ul className={styles.detailComment}> | ||
| {productComments.list.map((comment) => ( | ||
| <li key={comment.id} className={styles.commentItem}> | ||
| <div className={styles.commentItemDesc}> | ||
| <p className={styles.commentContent}>{comment.content}</p> | ||
| <CommentItemMenu isOpen={openMenuId === comment.id} onClick={() => onClickMenu(comment.id)} /> | ||
| </div> | ||
| <DetailProfile | ||
| profileImage={comment.writer.image} | ||
| profileNickname={comment.writer.nickname} | ||
| profileUpdate={relativeTime(comment.updatedAt)} | ||
| isComment | ||
| /> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import styles from './styles/DetailInquiry.module.css'; | ||
| import CommonButton from '@/components/Common/CommonButton'; | ||
| import { useState } from 'react'; | ||
|
|
||
| export default function DetailInquiry({ label }) { | ||
| const [inquiry, setInquiry] = useState(''); | ||
|
|
||
| return ( | ||
| <form className={styles.detailInquiry}> | ||
| <label htmlFor='inquiry'>{label}</label> | ||
| <textarea | ||
| id='inquiry' | ||
| placeholder='개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다.' | ||
| value={inquiry} | ||
| onChange={(e) => setInquiry(e.target.value)} | ||
| /> | ||
| <InquiryButton disabled={inquiry.length === 0} /> | ||
| </form> | ||
| ); | ||
| } | ||
|
|
||
| const InquiryButton = ({ disabled }) => { | ||
| return <CommonButton buttonType={{ buttonType: 'submit', buttonStyle: 'primary', buttonText: '등록' }} disabled={disabled} />; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import styles from './styles/DetailProfile.module.css'; | ||
|
|
||
| export default function DetailProfile({ profileImage, profileNickname, profileUpdate, isComment=false}) { | ||
| return ( | ||
| <div className={`${styles.profile} ${isComment ? styles.isComment : ''}`}> | ||
| <div className={styles.profileImage}> | ||
| <img | ||
| src={profileImage || '/images/common/ic_log.svg'} | ||
| alt='프로필 이미지' | ||
| onError={(e) => { | ||
| e.target.onerror = null; | ||
| e.target.src = '/images/common/ic_log.svg'; | ||
| }} | ||
| /> | ||
| </div> | ||
| <div> | ||
| <p className={styles.profileNickName}>{profileNickname}</p> | ||
| <p className={styles.profileUpdate}>{profileUpdate}</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| .commentItemMenu { | ||
| position: relative; | ||
| > button { | ||
| border-radius: 50%; | ||
| transition: background-color 0.2s ease; | ||
| outline: none; | ||
| &:hover { | ||
| background-color: var(--gray100); | ||
| } | ||
| } | ||
| ul { | ||
| position: absolute; | ||
| right: 0; | ||
| bottom: -10px; | ||
| transform: translateY(100%); | ||
| width: 140px; | ||
| border: 1px solid #d1d5db; | ||
| border-radius: 8px; | ||
| padding-block: 4px; | ||
| background-color: #fff; | ||
| z-index: 2; | ||
| li { | ||
| button { | ||
| width: 100%; | ||
| height: 100%; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding-block: 8px; | ||
| color: var(--gray500); | ||
| font-size: 16px; | ||
| font-weight: 400; | ||
| line-height: 1.62; | ||
| transition: color 0.2s ease; | ||
| &:hover { | ||
| color: var(--gray800); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @media (max-width: 768px) { | ||
| .commentItemMenu ul { | ||
| bottom: -1.3vw; | ||
| width: 13.28vw; | ||
| border-radius: 2.08vw; | ||
| padding-block: 0.52vw; | ||
| li button { | ||
| padding-block: 1.56vw; | ||
| font-size: 1.82vw; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @media (max-width: 425px) { | ||
| .commentItemMenu ul { | ||
| bottom: -2.35vw; | ||
| width: 24vw; | ||
| border-radius: 3.76vw; | ||
| padding-block: 0.94vw; | ||
| li button { | ||
| padding-block: 2.82vw; | ||
| font-size: 3.29vw; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| .backToList { | ||
| margin: 64px auto 0; | ||
| background-color: var(--blue); | ||
| color: var(--gray100); | ||
| font-size: 18px; | ||
| font-weight: 600; | ||
| height: 48px; | ||
| border-radius: 2em; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| gap: 8px; | ||
| padding-inline: 40px; | ||
| border: 1px solid transparent; | ||
| transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; | ||
| img { | ||
| width: 24px; | ||
| height: 24px; | ||
| transition: filter 0.2s ease; | ||
| } | ||
| &:hover { | ||
| background-color: #fff; | ||
| color: var(--blue); | ||
| border-color: var(--blue); | ||
| img { | ||
| filter: invert(40%) sepia(80%) saturate(2619%) hue-rotate(203deg) brightness(101%) contrast(101%); | ||
| } | ||
| } | ||
| } | ||
| @media (max-width: 768px) { | ||
| .backToList { | ||
| margin: 7.29vw auto 0; | ||
| font-size: 2.34vw; | ||
| height: 6.25vw; | ||
| gap: 1.04vw; | ||
| padding-inline: 5.21vw; | ||
| img { | ||
| width: 3.13vw; | ||
| height: 3.13vw; | ||
| } | ||
| } | ||
| } | ||
| @media (max-width: 425px) { | ||
| .backToList { | ||
| margin: 9.41vw auto 0; | ||
| font-size: 4.24vw; | ||
| height: 11.29vw; | ||
| gap: 1.88vw; | ||
| padding-inline: 9.41vw; | ||
| img { | ||
| width: 5.65vw; | ||
| height: 5.65vw; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💊 제안
이렇게 컴포넌트로 분리하신 이유를 모르겠어요.
기존의 코드들을 하나의 컴포넌트로 단순히 분리만 한거라 구조를 파악하고 싶을때 하나의 파일을 더 봐야해서 가독성에도 좋지 않은 것 같아요.
만약 route 파일을 따로 빼고 싶으시다면 지금은 react-router-dom 라이브러리의 declative 모드로 작업하고 계시는데 Data 모드나 Framework 모드를 사용하시는 것이 더 적절할 것 같아요~