Skip to content

Commit

Permalink
Merge pull request #24 from dnd-side-project/feature/OZ-38
Browse files Browse the repository at this point in the history
feat : 포즈피드 및 필터 기능 완성
  • Loading branch information
seondal authored Aug 24, 2023
2 parents 110e9d7 + 0805c9b commit d50d86e
Show file tree
Hide file tree
Showing 43 changed files with 838 additions and 132 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"react-dom": "18.2.0",
"react-lottie-player": "^1.5.4",
"react-tooltip": "^5.20.0",
"recoil": "^0.7.7",
"tailwind-merge": "^1.14.0",
"typescript": "5.1.6"
},
Expand Down
10 changes: 4 additions & 6 deletions public/icons/restart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion src/apis/apis.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PoseDetailResponse, PosePickResponse, PoseTalkResponse } from '.';
import { FilterTagsResponse, PoseFeedResponse, PosePickResponse, PoseTalkResponse } from '.';
import publicApi from './config/publicApi';

export const getPosePick = (peopleCount: number) =>
Expand All @@ -8,3 +8,15 @@ export const getPoseDetail = (poseId: number) =>
publicApi.get<PoseDetailResponse>(`/pose/${poseId}`);

export const getPoseTalk = () => publicApi.get<PoseTalkResponse>('/pose/talk');

export const getPoseFeed = (peopleCount: number, frameCount: number, tags: string) =>
publicApi.get<PoseFeedResponse>(`/pose`, {
params: {
frameCount,
pageNumber: 0,
peopleCount,
tags,
},
});

export const getFilterTag = () => publicApi.get<FilterTagsResponse>('/pose/tags');
24 changes: 22 additions & 2 deletions src/apis/queries.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';

import { FilterState } from '@/hooks/useFilterState';
import {
type PosePickResponse,
type PoseTalkResponse,
FilterTagsResponse,
PoseFeedResponse,
PosePickResponse,
PoseTalkResponse,
getFilterTag,
getPoseDetail,
getPoseFeed,
getPosePick,
getPoseTalk,
} from '.';
Expand All @@ -22,3 +27,18 @@ export const usePosePickQuery = (

export const usePoseTalkQuery = (options?: UseQueryOptions<PoseTalkResponse>) =>
useQuery<PoseTalkResponse>(['poseTalk'], getPoseTalk, { enabled: false, ...options });

export const usePoseFeedQuery = (
{ peopleCount, frameCount, tags }: FilterState,
options?: UseQueryOptions<PoseFeedResponse>
) =>
useQuery<PoseFeedResponse>(
['poseFeed', peopleCount, frameCount, tags],
() => getPoseFeed(peopleCount, frameCount, tags.join(',')),
{
...options,
}
);

export const useFilterTagQuery = (options?: UseQueryOptions<FilterTagsResponse>) =>
useQuery<FilterTagsResponse>(['filterTag'], getFilterTag, { ...options });
66 changes: 55 additions & 11 deletions src/apis/type.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,59 @@
export interface PosePickResponse {
poseInfo: {
createdAt: string;
frameCount: number;
imageKey: string;
peopleCount: number;
poseId: number;
source: string;
sourceUrl: string;
tagAttributes: string;
updatedAt: string;
export interface PoseInfo {
createdAt: string;
frameCount: number;
imageKey: string;
peopleCount: number;
poseId: number;
source: string;
sourceUrl: string;
tagAttributes: string;
updatedAt: string;
}

// 포즈피드
interface PoseFeedContentsSort {
empty: boolean;
sorted: boolean;
unsorted: boolean;
}
interface PoseFeedContents {
content: Array<{ poseInfo: PoseInfo }>;
pageable: {
sort: PoseFeedContentsSort;
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
};
number: number;
sort: PoseFeedContentsSort;
size: number;
numberOfElements: number;
first: boolean;
last: boolean;
empty: boolean;
}
export interface PoseFeedResponse {
recommendation: boolean;
filteredContents: PoseFeedContents;
recommendedContents: PoseFeedContents;
}

// 필터 태그
interface FilterTag {
createdAt: string;
updatedAt: string;
attributeId: number;
attribute: string;
}
export interface FilterTagsResponse {
poseTagAttributes: FilterTag[];
}

// 포즈픽
export interface PosePickResponse {
poseInfo: PoseInfo;
}

export interface PoseDetailResponse {
Expand Down
16 changes: 16 additions & 0 deletions src/app/(Main)/bookmark/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import EmptyCase from '../feed/components/EmptyCase';
import PhotoList from '../feed/components/PhotoList';

export default function BookmarkPage() {
return (
<>
<EmptyCase
title={'포즈를 보관해 보세요!'}
text={`북마크 버튼으로 포즈를 보관할 수 있어요.\n포즈피드에서 멋진 포즈를 찾아 보관해 보세요.`}
button={'포즈피드 바로가기'}
path={'/feed'}
/>
<PhotoList />
</>
);
}
1 change: 1 addition & 0 deletions src/app/(Main)/components/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const tabData = [
{ path: '/pick', title: '포즈픽' },
{ path: '/talk', title: '포즈톡' },
{ path: '/feed', title: '포즈피드' },
{ path: '/bookmark', title: '북마크' },
];

export default function Tab() {
Expand Down
29 changes: 29 additions & 0 deletions src/app/(Main)/feed/components/EmptyCase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Link from 'next/link';

import { PrimaryButton } from '@/components/Button';
import { Spacing } from '@/components/Spacing';

interface EmptyCase {
title: string;
text: string;
button: string;
path: string;
}

export default function EmptyCase(props: EmptyCase) {
const { title, text, button, path } = props;

return (
<div className="py-80 text-center">
<h4 className="text-secondary">{title}</h4>
<Spacing size={8} />
<p className="text-tertiary">{text}</p>
<Spacing size={32} />
<div className="flex justify-center">
<Link href={path}>
<PrimaryButton text={button} type="secondary" />
</Link>
</div>
</div>
);
}
13 changes: 0 additions & 13 deletions src/app/(Main)/feed/components/Filter.tsx

This file was deleted.

84 changes: 84 additions & 0 deletions src/app/(Main)/feed/components/FilterSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react';

import { FilterTagsResponse, useFilterTagQuery } from '@/apis';
import { PrimaryButton } from '@/components/Button';
import BottomSheet from '@/components/Modal/BottomSheet';
import { SelectionBasic, SelectionTagList } from '@/components/Selection';
import { frameCountList, peopleCountList } from '@/constants/filterList';
import { ICON } from '@/constants/icon';
import useBottomSheet from '@/hooks/useBottomSheet';
import useFilterState from '@/hooks/useFilterState';

export default function FilterSheet() {
const { data: tagListData } = useFilterTagQuery();

const { filterState, updateFilterState } = useFilterState();
const { isBottomSheetOpen, closeBottomSheet } = useBottomSheet();

const [countState, setCountState] = useState<number>(0);
const [frameState, setFrameState] = useState<number>(0);
const [tagState, setTagState] = useState<string[]>([]);

function resetFilter() {
setCountState(0);
setFrameState(0);
setTagState([]);
}

function decideFilter() {
updateFilterState({ peopleCount: countState, frameCount: frameState, tags: tagState });
closeBottomSheet();
}

useEffect(() => {
setCountState(filterState.peopleCount);
setFrameState(filterState.frameCount);
setTagState(filterState.tags);
}, [isBottomSheetOpen, filterState]);

function refineTagListData(tagListData: FilterTagsResponse) {
const tagList: string[] = [];
for (const tag of tagListData.poseTagAttributes) {
tagList.push(tag.attribute);
}
return tagList;
}

return (
<BottomSheet>
<section>
<div id="subtitle-2" className="mb-8 text-secondary">
인원 수
</div>
<SelectionBasic data={peopleCountList} state={countState} setState={setCountState} />
</section>
<section>
<div id="subtitle-2" className="mb-8 text-secondary">
프레임 수
</div>
<SelectionBasic data={frameCountList} state={frameState} setState={setFrameState} />
</section>
<section>
<div id="subtitle-2" className="mb-8 text-secondary">
태그
</div>
{tagListData && (
<SelectionTagList
data={refineTagListData(tagListData)}
state={tagState}
setState={setTagState}
/>
)}
</section>
<div className="flex gap-8 py-20 [&>*]:flex-1">
<PrimaryButton
type="outline"
icon={ICON.restart}
text="필터 초기화"
onClick={resetFilter}
/>
<PrimaryButton type="fill" text="포즈보기" onClick={decideFilter} />
</div>
</BottomSheet>
);
}
39 changes: 39 additions & 0 deletions src/app/(Main)/feed/components/FilterTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Image from 'next/image';

import Tag from '@/components/Selection/Tag';
import { ICON } from '@/constants/icon';
import useBottomSheet from '@/hooks/useBottomSheet';
import useFilterState from '@/hooks/useFilterState';

export default function FilterTab() {
const { openBottomSheet } = useBottomSheet();
const { selectedFilterItems, deleteSelectedFilterItem } = useFilterState();
const tags = selectedFilterItems();
const isFiltered = tags.length !== 0;

return (
<div className="fixed inset-x-0 top-104 z-10 flex h-56 items-center gap-8 bg-white px-20">
<button
className={`flex min-w-fit items-center gap-8 rounded-8 ${
isFiltered
? 'border-1 border-main-violet bg-main-violet-base text-main-violet'
: 'bg-sub-white'
} px-16 py-9`}
onClick={openBottomSheet}
>
<h5 id="subtitle-2">필터</h5>
<Image src={ICON.carat.down} alt="▾" width={16} height={16} priority />
</button>
{isFiltered && (
<>
<div className="text-divider">|</div>
<div className="flex gap-8 overflow-x-scroll">
{tags.map((tag) => (
<Tag key={tag.value} text={tag.value} onClick={() => deleteSelectedFilterItem(tag)} />
))}
</div>
</>
)}
</div>
);
}
23 changes: 23 additions & 0 deletions src/app/(Main)/feed/components/Photo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Image from 'next/image';

import { ICON } from '@/constants/icon';

interface Photo {
imageKey?: string;
source?: string;
}

export default function Photo({ imageKey, source }: Photo) {
return (
<div className={`relative z-0 mb-16 inline-block h-fit w-full rounded-8`}>
{imageKey && (
<>
<img src={imageKey} alt={source} className="rounded-8" />
<div className="absolute bottom-6 right-6 h-36 w-36 rounded-24 bg-white bg-opacity-30 p-6">
<Image src={ICON.bookmark.empty} width={24} height={24} alt="🔖" />
</div>
</>
)}
</div>
);
}
24 changes: 24 additions & 0 deletions src/app/(Main)/feed/components/PhotoList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Photo from './Photo';
import { PoseInfo } from '@/apis';

interface PhotoList {
data?: Array<{ poseInfo: PoseInfo }>;
}

export default function PhotoList({ data }: PhotoList) {
return (
<div className="columns-2 overflow-y-scroll">
{data ? (
data.map((item) => (
<Photo
key={item.poseInfo.poseId}
imageKey={item.poseInfo.imageKey}
source={item.poseInfo.source}
/>
))
) : (
<Photo />
)}
</div>
);
}
3 changes: 0 additions & 3 deletions src/app/(Main)/feed/components/Thumbnails.tsx

This file was deleted.

Loading

0 comments on commit d50d86e

Please sign in to comment.