-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 주제 조회 UI 및 기능 개발 #61
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
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6b2249e
feat: 주제 목록 UI 및 기능 구현 (#46)
haruyam15 53ff6aa
Merge branch 'develop' into feat/topics-list-46
haruyam15 d97355b
style: 이중언더스코어 제거(#46)
haruyam15 3681abc
design: EmptyTopickList 디자인수정 (#46)
haruyam15 d5b1f52
style: 린트, 프리티어 적용 (#46)
haruyam15 fabe1f6
refactor: 주제 목록 가상화 제거 및 낙관적 업데이트 개선 (#46)
haruyam15 5af2a44
chore: 카카오지도 환경변수 추가(#46)
haruyam15 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { useInfiniteScroll } from '@/shared/hooks/useInfiniteScroll' | ||
|
|
||
| import type { ConfirmedTopicItem } from '../topics.types' | ||
| import EmptyTopicList from './EmptyTopicList' | ||
| import TopicCard from './TopicCard' | ||
| import TopicListSkeleton from './TopicListSkeleton' | ||
|
|
||
| type ConfirmedTopicListProps = { | ||
| topics: ConfirmedTopicItem[] | ||
| hasNextPage: boolean | ||
| isFetchingNextPage: boolean | ||
| onLoadMore: () => void | ||
| pageSize?: number | ||
| } | ||
|
|
||
| export default function ConfirmedTopicList({ | ||
| topics, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| onLoadMore, | ||
| pageSize = 5, | ||
| }: ConfirmedTopicListProps) { | ||
| // 무한 스크롤: IntersectionObserver로 다음 페이지 로드 | ||
| const observerRef = useInfiniteScroll(onLoadMore, { | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| }) | ||
|
|
||
| if (topics.length === 0) { | ||
| return <EmptyTopicList /> | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-small"> | ||
| <ul className="flex flex-col gap-small"> | ||
| {topics.map((topic) => ( | ||
| <li key={topic.topicId}> | ||
| <TopicCard | ||
| title={topic.title} | ||
| topicTypeLabel={topic.topicTypeLabel} | ||
| description={topic.description} | ||
| createdByNickname={topic.createdByInfo.nickname} | ||
| likeCount={topic.likeCount} | ||
| isLikeDisabled | ||
| /> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| {/* 무한 스크롤 로딩 상태 */} | ||
| {isFetchingNextPage && <TopicListSkeleton count={pageSize} />} | ||
|
|
||
| {/* 무한 스크롤 트리거 */} | ||
| {hasNextPage && !isFetchingNextPage && <div ref={observerRef} className="h-4" />} | ||
| </div> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Badge, Card } from '@/shared/ui' | ||
|
|
||
| export default function DefaultTopicCard() { | ||
| return ( | ||
| <Card className="p-medium flex flex-col gap-small"> | ||
| <div className="flex gap-xsmall items-center"> | ||
| <p className="text-black typo-subtitle3">기본주제</p> | ||
| <Badge size="small" color="grey"> | ||
| 자유형 | ||
| </Badge> | ||
| </div> | ||
| <p className="typo-body4 text-grey-700">자유롭게 이야기해봅시다.</p> | ||
| <div className="flex justify-between items-end"> | ||
| <p className="typo-body6 text-grey-600">제안 : 도크도크</p> | ||
| </div> | ||
| </Card> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { Card } from '@/shared/ui' | ||
|
|
||
| export default function EmptyTopicList() { | ||
| return ( | ||
| <Card className="p-base w-full flex items-center justify-center h-[160px]"> | ||
| <p className="text-grey-600 typo-subtitle3 text-center">아직 확정된 주제가 없어요</p> | ||
| </Card> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { useInfiniteScroll } from '@/shared/hooks/useInfiniteScroll' | ||
|
|
||
| import type { ProposedTopicItem } from '../topics.types' | ||
| import DefaultTopicCard from './DefaultTopicCard' | ||
| import TopicCard from './TopicCard' | ||
| import TopicListSkeleton from './TopicListSkeleton' | ||
|
|
||
| type ProposedTopicListProps = { | ||
| topics: ProposedTopicItem[] | ||
| hasNextPage: boolean | ||
| isFetchingNextPage: boolean | ||
| onLoadMore: () => void | ||
| pageSize?: number | ||
| gatheringId: number | ||
| meetingId: number | ||
| } | ||
|
|
||
| export default function ProposedTopicList({ | ||
| topics, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| onLoadMore, | ||
| pageSize = 5, | ||
| gatheringId, | ||
| meetingId, | ||
| }: ProposedTopicListProps) { | ||
| // 무한 스크롤: IntersectionObserver로 다음 페이지 로드 | ||
| const observerRef = useInfiniteScroll(onLoadMore, { | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| }) | ||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-small"> | ||
| {/* 기본 주제는 항상 표시 */} | ||
| <DefaultTopicCard /> | ||
|
|
||
| {/* 제안된 주제 목록 */} | ||
| {topics.length > 0 && ( | ||
| <ul className="flex flex-col gap-small"> | ||
| {topics.map((topic) => ( | ||
| <li key={topic.topicId}> | ||
| <TopicCard | ||
| title={topic.title} | ||
| topicTypeLabel={topic.topicTypeLabel} | ||
| description={topic.description} | ||
| createdByNickname={topic.createdByInfo.nickname} | ||
| likeCount={topic.likeCount} | ||
| isLiked={topic.isLiked} | ||
| canDelete={topic.canDelete} | ||
| gatheringId={gatheringId} | ||
| meetingId={meetingId} | ||
| topicId={topic.topicId} | ||
| /> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| )} | ||
|
|
||
| {/* 무한 스크롤 로딩 상태 */} | ||
| {isFetchingNextPage && <TopicListSkeleton count={pageSize} />} | ||
|
|
||
| {/* 무한 스크롤 트리거 */} | ||
| {hasNextPage && !isFetchingNextPage && <div ref={observerRef} className="h-4" />} | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { Badge, Card, LikeButton, TextButton } from '@/shared/ui' | ||
| import { useGlobalModalStore } from '@/store' | ||
|
|
||
| import { useDeleteTopic, useLikeTopic } from '../hooks' | ||
|
|
||
| type TopicCardProps = { | ||
| title: string | ||
| topicTypeLabel: string | ||
| description: string | ||
| createdByNickname: string | ||
| likeCount: number | ||
| isLiked?: boolean | ||
| isLikeDisabled?: boolean | ||
| canDelete?: boolean | ||
| gatheringId?: number | ||
| meetingId?: number | ||
| topicId?: number | ||
| } | ||
|
|
||
| export default function TopicCard({ | ||
| title, | ||
| topicTypeLabel, | ||
| description, | ||
| createdByNickname, | ||
| likeCount, | ||
| isLiked = false, | ||
| isLikeDisabled = false, | ||
| canDelete = false, | ||
| gatheringId, | ||
| meetingId, | ||
| topicId, | ||
| }: TopicCardProps) { | ||
| const { openConfirm, openAlert, openError } = useGlobalModalStore() | ||
| const deleteMutation = useDeleteTopic() | ||
| const likeMutation = useLikeTopic() | ||
|
|
||
| const handleDelete = async () => { | ||
| if (!gatheringId || !meetingId || !topicId) { | ||
| openError('삭제 실패', '주제 삭제에 필요한 정보가 없습니다.') | ||
| return | ||
| } | ||
|
|
||
| const confirmed = await openConfirm('주제 삭제', '정말 이 주제를 삭제하시겠습니까?', { | ||
| confirmText: '삭제', | ||
| variant: 'danger', | ||
| }) | ||
|
|
||
| if (!confirmed) { | ||
| return | ||
| } | ||
|
|
||
| deleteMutation.mutate( | ||
| { gatheringId, meetingId, topicId }, | ||
| // TODO: 토스트 컴포넌트로 교체 예정 | ||
| { | ||
| onSuccess: () => { | ||
| openAlert('삭제 완료', '주제가 삭제되었습니다.') | ||
| }, | ||
| onError: (error) => { | ||
| openError('삭제 실패', error.userMessage) | ||
| }, | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| const handleLike = () => { | ||
| if (!gatheringId || !meetingId || !topicId) { | ||
| return | ||
| } | ||
|
|
||
| if (likeMutation.isPending) return | ||
|
|
||
| likeMutation.mutate( | ||
| { gatheringId, meetingId, topicId }, | ||
| { | ||
| onError: (error) => { | ||
| // TODO: 토스트 컴포넌트로 교체 예정 | ||
| alert(`좋아요 처리 중 오류가 발생했습니다: ${error.userMessage}`) | ||
| }, | ||
| } | ||
| ) | ||
| } | ||
| return ( | ||
| <Card className="p-medium flex flex-col gap-small"> | ||
| <div className="flex justify-between items-start"> | ||
| <div className="flex gap-xsmall items-center"> | ||
| <p className="text-black typo-subtitle3">{title}</p> | ||
| <Badge size="small" color="grey"> | ||
| {topicTypeLabel} | ||
| </Badge> | ||
| </div> | ||
| {canDelete && ( | ||
| <TextButton onClick={handleDelete} disabled={deleteMutation.isPending}> | ||
| 삭제하기 | ||
| </TextButton> | ||
| )} | ||
| </div> | ||
| <p className="typo-body4 text-grey-700">{description}</p> | ||
| <div className="flex justify-between items-end"> | ||
| <p className="typo-body6 text-grey-600">제안 : {createdByNickname}</p> | ||
| <LikeButton | ||
| count={likeCount} | ||
| isLiked={isLiked} | ||
| disabled={isLikeDisabled} | ||
| isPending={likeMutation.isPending} | ||
| onClick={handleLike} | ||
| /> | ||
| </div> | ||
| </Card> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import { format } from 'date-fns' | ||
| import { Check } from 'lucide-react' | ||
|
|
||
| import { Button } from '@/shared/ui' | ||
|
|
||
| type ProposedHeaderProps = { | ||
| activeTab: 'PROPOSED' | ||
| actions: { canConfirm: boolean; canSuggest: boolean } | ||
| confirmedTopic: boolean | ||
| confirmedTopicDate: string | null | ||
| } | ||
|
|
||
| type ConfirmedHeaderProps = { | ||
| activeTab: 'CONFIRMED' | ||
| actions: { canViewPreOpinions: boolean; canWritePreOpinions: boolean } | ||
| confirmedTopic: boolean | ||
| confirmedTopicDate: string | null | ||
| } | ||
|
|
||
| type TopicHeaderProps = ProposedHeaderProps | ConfirmedHeaderProps | ||
|
|
||
| export default function TopicHeader(props: TopicHeaderProps) { | ||
| return ( | ||
| <> | ||
| {/* 제안탭 */} | ||
| {props.activeTab === 'PROPOSED' && ( | ||
| <div className="flex justify-between"> | ||
| <div className="flex flex-col gap-tiny"> | ||
| {props.confirmedTopic && props.confirmedTopicDate ? ( | ||
| // 주제 확정됨 | ||
| <> | ||
| <p className="text-black typo-subtitle3"> | ||
| 주제 제안이 마감되었어요. 확정된 주제를 확인해보세요! | ||
| </p> | ||
| <p className="typo-body4 text-grey-600"> | ||
| {format(props.confirmedTopicDate, 'yyyy.MM.dd HH:mm')} 마감 | ||
| </p> | ||
| </> | ||
| ) : ( | ||
| // 주제 제안 중 | ||
| <> | ||
| <p className="text-black typo-subtitle3"> | ||
| 약속에서 나누고 싶은 주제를 제안해보세요 | ||
| </p> | ||
| <p className="typo-body4 text-grey-600"> | ||
| 주제를 미리 정하면 우리 모임이 훨씬 풍성하고 즐거워질 거예요 | ||
| </p> | ||
| </> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="flex gap-xsmall"> | ||
| {props.actions.canConfirm && ( | ||
| <Button variant="secondary" outline> | ||
| 주제 확정하기 | ||
| </Button> | ||
| )} | ||
|
|
||
| {props.actions.canSuggest && <Button>제안하기</Button>} | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* 제안탭 */} | ||
|
|
||
| {/* 확정탭 */} | ||
| {props.activeTab === 'CONFIRMED' && ( | ||
| <div className="flex justify-between"> | ||
| <div className="flex flex-col gap-tiny"> | ||
| {props.confirmedTopic ? ( | ||
| // 주제 확정됨 | ||
| <> | ||
| <p className="flex items-center text-black typo-subtitle3 gap-tiny"> | ||
| <Check size="20" /> 주제가 확정되었어요! | ||
| </p> | ||
| <p className="typo-body4 text-grey-600"> | ||
| 나의 생각을 미리 정리해서 공유하면 다른 멤버들의 의견도 바로 확인할 수 있어요 | ||
| </p> | ||
| </> | ||
| ) : ( | ||
| // 주제 제안 중 | ||
| <> | ||
| <p className="text-black typo-subtitle3">약속장이 주제를 선정하고 있어요</p> | ||
| <p className="typo-body4 text-grey-600"> | ||
| 주제가 확정되면 사전 의견을 남길 수 있는 창이 열려요 | ||
| </p> | ||
| </> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="flex gap-xsmall"> | ||
| <Button variant="secondary" outline disabled={!props.actions.canViewPreOpinions}> | ||
| 사전 의견 확인하기 | ||
| </Button> | ||
|
|
||
| <Button disabled={!props.actions.canWritePreOpinions}>사전 의견 작성하기</Button> | ||
| </div> | ||
| </div> | ||
| )} | ||
| {/* 확정탭 */} | ||
| </> | ||
| ) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.