Skip to content

Commit

Permalink
Merge pull request #21 from dnd-side-project/OZ-71-F-detail-page
Browse files Browse the repository at this point in the history
Feature : 포즈 상세 페이지 구현
  • Loading branch information
guesung committed Aug 23, 2023
2 parents cca0dde + 25e603c commit c060c09
Show file tree
Hide file tree
Showing 23 changed files with 363 additions and 80 deletions.
7 changes: 7 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare global {
interface Window {
Kakao;
}
}

export {};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@types/react": "18.2.17",
"@types/react-dom": "18.2.7",
"axios": "^1.4.0",
"clsx": "^2.0.0",
"eslint": "^8.46.0",
"eslint-config-next": "^13.4.12",
"framer-motion": "^10.15.0",
Expand All @@ -24,6 +25,7 @@
"react-dom": "18.2.0",
"react-lottie-player": "^1.5.4",
"react-tooltip": "^5.20.0",
"tailwind-merge": "^1.14.0",
"typescript": "5.1.6"
},
"devDependencies": {
Expand Down
10 changes: 4 additions & 6 deletions public/icons/bookmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 3 additions & 4 deletions src/apis/apis.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';

import { PosePickResponse, PoseTalkResponse } from '.';
import { PoseDetailResponse, PosePickResponse, PoseTalkResponse } from '.';
import publicApi from './config/publicApi';

export const getPosePick = (peopleCount: number) =>
publicApi.get<PosePickResponse>(`/pose/pick/${peopleCount}`);

export const getPoseDetail = (poseId: number) => publicApi.get(`/pose/${poseId}`);
export const getPoseDetail = (poseId: number) =>
publicApi.get<PoseDetailResponse>(`/pose/${poseId}`);

export const getPoseTalk = () => publicApi.get<PoseTalkResponse>('/pose/talk');
14 changes: 14 additions & 0 deletions src/apis/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ export interface PosePickResponse {
};
}

export interface PoseDetailResponse {
poseInfo: {
createdAt: string;
frameCount: number;
imageKey: string;
peopleCount: number;
poseId: number;
source: string;
sourceUrl: string;
tagAttributes: string;
updatedAt: string;
};
}

export interface PoseTalkResponse {
poseWord: {
content: string;
Expand Down
2 changes: 1 addition & 1 deletion src/app/(Main)/components/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function Tab() {
const path = usePathname();

return (
<nav className="inset-x-0 mx-auto flex h-48 items-center gap-16 border-b-2 border-b-divider">
<nav className="inset-x-0 mx-auto flex h-48 items-center gap-16 border-b-2 border-b-divider px-20">
{tabData.map((item) => (
<div key={item.path}>
{item.path === path ? (
Expand Down
22 changes: 22 additions & 0 deletions src/app/(Sub)/detail/[id]/components/DetailHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Image from 'next/image';

import IconButton from '@/components/Button/IconButton';
import { Header } from '@/components/Header';

export default function DetailHeader() {
return (
<Header
leftNode={
<IconButton size="large">
<Image src="/icons/close.svg" width={24} height={24} alt="back" />
</IconButton>
}
rightNode={
<IconButton size="large">
<Image src="/icons/bookmark.svg" width={24} height={24} alt="bookmark" />
</IconButton>
}
className="px-4"
/>
);
}
79 changes: 79 additions & 0 deletions src/app/(Sub)/detail/[id]/components/DetailSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client';

import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

import LinkShareModal from './LinkShareModal';
import { usePoseDetailQuery } from '@/apis';
import BottomFixedDiv from '@/components/BottomFixedDiv';
import { Button } from '@/components/Button';
import { useOverlay } from '@/components/Overlay/useOverlay';
import { BASE_SITE_URL } from '@/constants';
import useKakaoShare from '@/hooks/useKakaoShare';
import { copy } from '@/utils/copy';

interface DetailSectionProps {
poseId: number;
}

export default function DetailSection({ poseId }: DetailSectionProps) {
const { data } = usePoseDetailQuery(poseId);
const { shareKakao } = useKakaoShare();
const { open } = useOverlay();
const pathname = usePathname();

if (!data) return null;
const { imageKey, tagAttributes, sourceUrl } = data.poseInfo;

const handleShareLink = async () => {
await copy(BASE_SITE_URL + pathname);

open(({ exit }) => <LinkShareModal onClose={exit} />);
};

return (
<div>
{sourceUrl && (
<Link
href={'https://' + sourceUrl}
className="text-subtitle-2 flex h-26 justify-center text-tertiary"
>
↗ 이미지 출처
</Link>
)}
<div className="relative h-520">
<Image src={imageKey} fill alt="detailImage" />
</div>
<div className="flex gap-10 px-20 py-12">
{tagAttributes?.split(',').map((tag, index) => <Tag key={index} name={tag} />)}
</div>

<BottomFixedDiv className="flex gap-8">
<Button className="w-160 bg-sub-white" type="button" onClick={handleShareLink}>
링크 공유
</Button>
<Button
className="grow bg-main-violet text-white"
onClick={() => shareKakao(BASE_SITE_URL + pathname)}
>
카카오 공유
</Button>
</BottomFixedDiv>
</div>
);
}
interface TagProps {
name: string;
}

function Tag({ name }: TagProps) {
return (
<button
type="button"
className="text-subtitle-2 rounded-30 bg-sub-white px-12 py-5 text-secondary"
>
{name}
</button>
);
}
16 changes: 16 additions & 0 deletions src/app/(Sub)/detail/[id]/components/LinkShareModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Button } from '@/components/Button';
import { Popup } from '@/components/Modal';

interface LinkShareModalProps {
onClose: () => void;
}
export default function LinkShareModal({ onClose }: LinkShareModalProps) {
return (
<Popup>
<p className="py-32">링크가 복사되었습니다.</p>
<Button className="bg-main-violet text-white" onClick={onClose}>
확인
</Button>
</Popup>
);
}
17 changes: 17 additions & 0 deletions src/app/(Sub)/detail/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import DetailHeader from './components/DetailHeader';
import DetailSection from './components/DetailSection';
import { getPoseDetail } from '@/apis';
import { HydrationProvider } from '@/components/Provider/HydrationProvider';

export default function DetailPage({ params }: { params: { id: number } }) {
const { id } = params;

return (
<div>
<DetailHeader />
<HydrationProvider queryKey={['poseId', id]} queryFn={() => getPoseDetail(id)}>
<DetailSection poseId={id} />
</HydrationProvider>
</div>
);
}
2 changes: 1 addition & 1 deletion src/app/(Sub)/menu/components/MenuListSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Spacing } from '@/components/Spacing';

export default function MenuHeader() {
const LeftNode = (
<div className="flex">
<div className="flex px-20">
<Image width={24} height={24} src="/icons/close.svg" alt="back" />
<Spacing size={12} direction="horizontal" />
<h4>메뉴</h4>
Expand Down
1 change: 0 additions & 1 deletion src/app/(Sub)/menu/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Spacing } from '@/components/Spacing';
import LoginSection from './components/LoginSection';
import MakerSection from './components/MakerSection';
import MenuHeader from './components/MenuListSection';
Expand Down
65 changes: 65 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,68 @@ html {
font-size: 16px;
}
}

h1 {
font-weight: 700;
font-size: 3rem;
line-height: 3.75rem;
letter-spacing: -0.4px;
}
h2 {
font-weight: 700;
font-size: 2rem;
line-height: 3rem;
letter-spacing: -0.4px;
}
h3 {
font-weight: 700;
font-size: 1.5rem;
line-height: 2.25rem;
letter-spacing: -0.3px;
}
h4 {
font-weight: 700;
font-size: 1.25rem;
line-height: 1.875rem;
letter-spacing: -0.2px;
}

#subtitle-1 {
font-weight: 500;
font-size: 1rem;
line-height: 1.5rem;
letter-spacing: -0.2px;
}
#subtitle-2 {
font-weight: 500;
font-size: 0.875rem;
line-height: 1.375rem;
letter-spacing: -0.1px;
}
#subtitle-3 {
font-weight: 500;
font-size: 0.75rem;
line-height: 1.125rem;
letter-spacing: -0px;
}

h6 {
font-weight: 500;
font-size: 0.875rem;
line-height: 1.125rem;
letter-spacing: 0;
}

p {
font-weight: 400;
font-size: 1rem;
line-height: 1.5rem;
letter-spacing: -0.1px;
}

caption {
font-weight: 500;
font-size: 0.75rem;
line-height: 1.125rem;
letter-spacing: 0.4px;
}
4 changes: 2 additions & 2 deletions src/components/BottomFixedDiv/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export default function BottomFixedDiv({
className,
}: StrictPropsWithChildren<BottomFixedDivProps>) {
return (
<div className={`fixed inset-x-0 bottom-0 mx-auto max-w-440 ${className}`}>
<div className="px-20 pb-20">{children}</div>
<div className={`fixed inset-x-0 bottom-0 mx-auto max-w-440 px-20 pb-20 ${className}`}>
{children}
</div>
);
}
32 changes: 32 additions & 0 deletions src/components/Button/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import clsx from 'clsx';

import type { StrictPropsWithChildren } from '@/types';

interface IconButtonProps {
/**
* 버튼의 크기를 설정합니다. small: 24px, medium: 40px, large: 48px (default: small)
*/
size?: 'small' | 'medium' | 'large';
/**
* 버튼의 클릭 이벤트를 설정합니다.
*/
onClick?: () => void;
}
export default function IconButton({
onClick,
children,
size = 'small',
}: StrictPropsWithChildren<IconButtonProps>) {
return (
<button
className={clsx('flex items-center justify-center', {
'h-24 w-24': size === 'small',
'h-40 w-40': size === 'medium',
'h-48 w-48': size === 'large',
})}
onClick={onClick}
>
{children}
</button>
);
}
File renamed without changes.
2 changes: 1 addition & 1 deletion src/components/Modal/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default as Popup } from './Popup';
export { default as Popup } from './Modal';
29 changes: 29 additions & 0 deletions src/components/Provider/HydrationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Hydrate, QueryClient, dehydrate } from '@tanstack/react-query';
import { cache } from 'react';

import type { StrictPropsWithChildren } from '@/types';
import type { QueryFunction, QueryKey } from '@tanstack/react-query';

type HydrationProviderProps = {
queryKey: QueryKey;
queryFn: QueryFunction;
isInfiniteQuery?: boolean;
};

export const HydrationProvider = async ({
children,
queryKey,
queryFn,
isInfiniteQuery = false,
}: StrictPropsWithChildren<HydrationProviderProps>) => {
const getQueryClient = cache(() => new QueryClient());

const queryClient = getQueryClient();

if (isInfiniteQuery) await queryClient.prefetchInfiniteQuery(queryKey, queryFn);
else await queryClient.prefetchQuery(queryKey, queryFn);

const dehydratedState = dehydrate(queryClient);

return <Hydrate state={dehydratedState}>{children}</Hydrate>;
};
2 changes: 2 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const BASE_API_URL = process.env.NEXT_PUBLIC_API_URL;
export const BASE_SITE_URL = process.env.NEXT_PUBLIC_SITE_URL;
export const KAKAO_KEY = process.env.NEXT_PUBLIC_KAKAO_KEY;
Loading

0 comments on commit c060c09

Please sign in to comment.