Skip to content

Commit

Permalink
[Feat] 게더링 디테일 페이지 구현 (#69)
Browse files Browse the repository at this point in the history
* feat: 게더링 디테일 페이지 설정

* feat: gathering detail page useParams 설정

* feat: 게더링 타입 설정 및 무한 스크롤 api

* feat: 게더링 연관된 페이지 props 전달

* feat: Breadcrumb 페이지 내 위치를 표시하는 탐색 경로 컴포넌트 구현

* fix: 버튼 경로 변경

* feat: 게더링 상세페이지 훅 및 타입 구현

* feat: 게더링 디테일 페이지 유저 정보 컴포넌트 구현

* feat: 게더링 디테일 페이지 상단 헤더 위젯 컴포넌트 구현

* feat: 게더링 디테일 옵션 컴포넌트 구현

* feat: 게더링 디테일 페이지 옵션 그리드 위젯 구현

* feat: 게더링 하단 버튼 위젯 구현

* feat: 게더링 디테일 페이지 구현

* feat: 게더링 디테일 관련 인덱스 정리

* fix: 게더링 그리드 배포 오류 수정

* fix: 쿼리키 수정
  • Loading branch information
joarthvr authored Dec 1, 2024
1 parent f2c5378 commit 30a3fec
Show file tree
Hide file tree
Showing 31 changed files with 616 additions and 183 deletions.
15 changes: 10 additions & 5 deletions src/app/appRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { createBrowserRouter } from 'react-router-dom';

import {
GatheringListPage,
DetailArchivePage,
ArchiveListPage,
WriteGatheringPage,
DetailArchivePage,
GatheringDetailPage,
GatheringListPage,
PortfolioListPage,
WriteArchivePage,
UserPage,
RegisterPage,
SearchPage,
UserPage,
WriteArchivePage,
WriteGatheringPage,
} from '@/pages';
import { Layout } from '@/widgets';

Expand Down Expand Up @@ -46,6 +47,10 @@ const AppRouter = () => {
path: '/gathering/write',
element: <WriteGatheringPage />,
},
{
path: '/gathering/:gatheringId',
element: <GatheringDetailPage />,
},
{
path: '/search',
element: <SearchPage />,
Expand Down
53 changes: 53 additions & 0 deletions src/features/gathering/api/gathering.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { GatheringDetailResponse } from '../model/gathering.dto';
import type {
GatheringItemDto,
GatheringSortType,
GatheringPeriod,
GatheringPosition,
} from '../model/gathering.dto';

import api from '@/shared/api/baseApi';

interface GetGatheringsParams {
sort?: GatheringSortType;
period?: GatheringPeriod;
position?: GatheringPosition;
status?: '모집중' | '모집완료';
size?: number;
gatheringId?: number;
}

interface GetGatheringsParams {
sort?: GatheringSortType;
period?: GatheringPeriod;
position?: GatheringPosition;
status?: '모집중' | '모집완료';
size?: number;
nextLikeId?: number;
}

interface GatheringListResponse {
data: {
content: GatheringItemDto[];
hasNext: boolean;
nextLikeId: number;
};
timeStamp: string;
}

export const getGatheringList = {
getGatherings: async (params: GetGatheringsParams): Promise<GatheringListResponse> => {
const { data } = await api.get<GatheringListResponse>('/gathering', { params });
return data;
},
};
interface GatheringDetailApi {
getGatheringById: (id: string) => Promise<GatheringDetailResponse>;
}

export const gatheringDetailApi: GatheringDetailApi = {
getGatheringById: async (id: string) => {
const { data } = await api.get<GatheringDetailResponse>(`/gathering/${id}`);
return data;
},
};
62 changes: 62 additions & 0 deletions src/features/gathering/api/gathering.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useQuery } from '@tanstack/react-query';

import { getGatheringList } from '../api/gathering.api';
import { gatheringDetailApi } from '../api/gathering.api';
import type { GatheringDetailResponse } from '../model/gathering.dto';
import type {
GatheringItemDto,
// GatheringResponseDto,
GatheringSortType,
GatheringPeriod,
GatheringPosition,
} from '../model/gathering.dto';

import { useCustomInfiniteQuery } from '@/shared/hook/useCustomInfiniteQuery';

interface TransformedGatheringResponse {
data: GatheringItemDto[];
}

export const useGatheringList = (
status: '모집중' | '모집완료',
sort?: GatheringSortType,
period?: GatheringPeriod,
position?: GatheringPosition,
) => {
return useCustomInfiniteQuery<TransformedGatheringResponse, GatheringItemDto, Error>(
['/gathering/list', sort ?? '', period ?? '', position ?? '', status],
async ({ pageParam }) => {
const response = await getGatheringList.getGatherings({
sort,
period,
position,
status,
size: 9,
nextLikeId: pageParam || undefined,
});

// API 응답을 useCustomInfiniteQuery가 기대하는 형태로 변환
return {
data: response.data.content,
};
},
9,
true,
);
};

export const useGatheringDetail = (gatheringId: string) => {
const { data, isLoading, isError, error } = useQuery<GatheringDetailResponse, Error>({
queryKey: ['/gathering', 'detail', gatheringId],
queryFn: () => gatheringDetailApi.getGatheringById(gatheringId),
enabled: !!gatheringId,
staleTime: 1000 * 60 * 5, // 5분 동안 캐시 유지
});

return {
gathering: data?.data,
isLoading,
isError,
error,
};
};
8 changes: 5 additions & 3 deletions src/features/gathering/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from './model/options';
export * from './model/types';
export { GatheringSelect } from './ui/GatheringSelect';
export { GatheringDatePicker } from './ui/GatheringDatePicker';
export { GatheringTagInput } from './ui/GatheringTagInput';
export * from './ui/GatheringDetail/index';
export { GatheringLinkInput } from './ui/GatheringLinkInput';
export { GatheringTitleInput } from './ui/GatheringTitIeInput';
export { GatheringMarkdownEditor } from './ui/GatheringMarkdownEditor';
export { GatheringSelect } from './ui/GatheringSelect';
export { GatheringTagInput } from './ui/GatheringTagInput';
export { GatheringTitleInput } from './ui/GatheringTitIeInput';

80 changes: 80 additions & 0 deletions src/features/gathering/model/gathering.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export type GatheringContactType = '온라인' | '오프라인' | '온라인&오프라인';
export type GatheringSortType = '스터디' | '프로젝트' | '동아리' | '기타';
export type GatheringPeriod = '1개월' | '3개월' | '6개월' | '6개월 이상';
export type GatheringPersonnel = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10명 이상';
export type GatheringPosition = '개발자' | '디자이너' | '기획자' | '마케터';

export type StudySubjectType = '개발' | '디자인' | '어학' | '기타';
export type ProjectSubjectType = '개발' | '디자인' | '기획' | '마케팅' | '기타';
export type ClubSubjectType = '취미' | '운동' | '음악' | '기타';
export type EtcSubjectType = '기타';

// sort에 따른 subject를 매핑하는 타입
export type GatheringSubjectMap = {
스터디: StudySubjectType;
프로젝트: ProjectSubjectType;
동아리: ClubSubjectType;
기타: EtcSubjectType;
};

export interface GatheringResponseDto {
data: {
content: GatheringItemDto[];
hasNext: boolean;
nextLikeId: number;
};
timeStamp: string;
}

// 폼데이터 이거로 바꿀 것입니다.
export interface GatheringItemDto<T extends GatheringSortType = GatheringSortType> {
gatheringId: string;
userId: string;
contactType: GatheringContactType;
sort: T;
subject: GatheringSubjectMap[T];
period: GatheringPeriod;
personnel: GatheringPersonnel;
position: GatheringPosition[];
title: string;
deadLine: string;
username: string;
tags: string[];
}

export interface GatheringDetailResponseDto<T extends GatheringSortType = GatheringSortType> {
data: {
sort: T;
username: string;
createTime: string;
subject: GatheringSubjectMap[T];
contact: GatheringContactType;
personnel: GatheringPersonnel;
period: GatheringPeriod;
deadLine: string;
position: GatheringPosition[];
gatheringTag: string[];
contactUrl: string;
title: string;
content: string;
};
timeStamp: string;
}
export interface GatheringDetailResponse {
data: {
sort: GatheringSortType;
username: string;
createTime: string;
subject: string;
contact: GatheringContactType;
personnel: number;
period: GatheringPeriod;
deadLine: string;
position: string;
gatheringTag: string[];
contactUrl: string;
title: string;
content: string;
};
timeStamp: string;
}
9 changes: 1 addition & 8 deletions src/features/gathering/model/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
export type GatheringContactType = '온라인' | '오프라인' | '온라인&오프라인';
export type GatheringSortType = '스터디' | '프로젝트' | '동아리' | '기타';
export type GatheringSubjectType = '개발' | '디자인' | '기획' | '마케팅';
export type GatheringPeriod = '1개월' | '3개월' | '6개월' | '6개월 이상';
export type GatheringPersonnel = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10명 이상';
export type GatheringPosition = '개발자' | '디자이너' | '기획자' | '마케터';

export type SelectOption = {
value: string;
label: string;
Expand All @@ -21,7 +14,7 @@ export interface GatheringFormData {
title: string;
url: string;
content: string;
deadLine: string ;
deadLine: string;
}

export interface GatheringFilterOptions {
Expand Down
4 changes: 3 additions & 1 deletion src/features/gathering/ui/GatheringCard/GatheringCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface GatheringCardProps {
introduction?: string;
tag?: string[];
deadline?: string;
gatheringId?: string;
}

export const GatheringCard = ({
Expand All @@ -22,6 +23,7 @@ export const GatheringCard = ({
introduction,
tag,
deadline,
gatheringId,
}: GatheringCardProps) => {
return (
<Link
Expand All @@ -32,7 +34,7 @@ export const GatheringCard = ({
},
className, // 외부 클래스 추가
)}
to='/gathering'
to={`/gathering/${gatheringId}`}
>
<li>
<h2 className={cn(styles.card__title)}>{title}</h2>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.author {
display: flex;
gap: 1rem;
align-items: center;

.profileImg {
width: 48px;
height: 48px;
border-radius: 50%;
}

.name {
font-weight: bold;
}

.position {
color: #666;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import styles from './GatheringDetailUserInfo.module.scss';

interface GatheringDetailUserInfoProps {
username: string;
position?: string;
profileImage?: string;
}

export const GatheringDetailUserInfo = ({
username,
position = 'Front Developer',
profileImage = '/default-profile.png',
}: GatheringDetailUserInfoProps) => {
return (
<div className={styles.author}>
<img alt={username} className={styles.profileImg} src={profileImage} />
<span className={styles.name}>{username}</span>
<span className={styles.position}>{position}</span>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.infoItem {
display: flex;
gap: 1.5625rem;
padding: 1rem 0;
border-bottom: 1px solid #eee;

.label {
font-weight: 600;
color: $third-color;
}

.value {
font-weight: 600;
color: $primary-color;
}
}
13 changes: 13 additions & 0 deletions src/features/gathering/ui/GatheringDetail/GatheringInfoItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface GatheringInfoItemProps {
label: string;
value: string | number | string[];
}
import styles from './GatheringInfoItem.module.scss';
export const GatheringInfoItem = ({ label, value }: GatheringInfoItemProps) => {
return (
<li className={styles.infoItem}>
<span className={styles.label}>{label}</span>
<span className={styles.value}>{value}</span>
</li>
);
};
3 changes: 3 additions & 0 deletions src/features/gathering/ui/GatheringDetail/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { GatheringDetailUserInfo } from './GatheringDetailUserInfo';
export { GatheringInfoItem } from './GatheringInfoItem';

Loading

1 comment on commit 30a3fec

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ Lighthouse report for http://localhost:3000/

Category Score
🔴 Performance 26
🟢 Accessibility 95
🟢 Best Practices 100
🟠 SEO 83

Detailed Metrics

Metric Value
🔴 First Contentful Paint 41.6 s
🔴 Largest Contentful Paint 70.4 s
🔴 Total Blocking Time 830 ms
🟢 Cumulative Layout Shift 0
🔴 Speed Index 54.7 s

Please sign in to comment.