Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { Metadata } from 'next';
import Footer from '@/app/(home)/components/Footer/Footer';
import HomeCarousel from '@/app/(home)/components/HomeCarousel/HomeCarousel';
import LatestLessons from '@/app/(home)/components/LatestLessons/LatestLessons';
import PopularGenre from '@/app/(home)/components/PopularGenre/PopularGenre';
import UpcomingLessons from '@/app/(home)/components/UpcomingLessons/UpcomingLessons';

export const metadata: Metadata = {
alternates: { canonical: '/' },
};

export default function Page() {
return (
<main>
Expand Down
32 changes: 30 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
import type { Metadata } from 'next';
import Script from 'next/script';
import Providers from '@/app/Providers';
import Header from '@/common/components/Header/Header';
import '@/shared/styles/index.css';

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://www.da-sh.kr';

export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: {
default: 'DASH - 댄스 클래스 예약 플랫폼',
template: '%s | DASH',
},
description:
'원하는 댄스 클래스를 찾고, 예약하고, 춤추세요. 힙합, 팝핑, 왁킹, K-POP 등 다양한 장르의 댄스 클래스를 한눈에.',
keywords: ['댄스 클래스', '댄스 수업', '댄스 예약', '힙합', '팝핑', '왁킹', 'K-POP', '브레이킹', 'DASH'],
icons: { icon: '/favicon.png' },
openGraph: {
type: 'website',
locale: 'ko_KR',
siteName: 'DASH',
title: 'DASH - 댄스 클래스 예약 플랫폼',
description: '원하는 댄스 클래스를 찾고, 예약하고, 춤추세요.',
images: [{ url: '/dash-Thumbnail.png', width: 1200, height: 630, alt: 'DASH 댄스 클래스 예약 플랫폼' }],
},
twitter: {
card: 'summary_large_image',
title: 'DASH - 댄스 클래스 예약 플랫폼',
description: '원하는 댄스 클래스를 찾고, 예약하고, 춤추세요.',
images: ['/dash-Thumbnail.png'],
},
robots: { index: true, follow: true },
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<head>
<meta charSet="UTF-8" />
<link rel="icon" href="/favicon.png" type="image/png" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"
Expand Down
16 changes: 16 additions & 0 deletions src/app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { MetadataRoute } from 'next';

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://www.da-sh.kr';

export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/my/', '/login', '/auth', '/onboarding', '/api/'],
},
],
sitemap: `${SITE_URL}/sitemap.xml`,
};
}
85 changes: 85 additions & 0 deletions src/app/search/SearchPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use client';

import { useSearchParams } from 'next/navigation';
import { Suspense, useState } from 'react';
import { useGetClassList, useGetDancerList } from '@/app/search/apis/queries';
import SearchBar from '@/app/search/components/SearchBar/SearchBar';
import SearchHeader from '@/app/search/components/SearchHeader/SearchHeader';
import TabContainer from '@/app/search/components/TabContainer/TabContainer';
import type { TAB_TYPES } from '@/app/search/constants/index';
import { DEFAULT_SORT_TAGS, SORT_LABELS, TAB } from '@/app/search/constants/index';
import { searchPageWrapperStyle } from '@/app/search/index.css';
import { formatDateEndTime, formatDateStartTime } from '@/app/search/utils/formatDate';
import { handleSearchChange } from '@/app/search/utils/searchHandlers';
import useDebounce from '@/common/hooks/useDebounce';
import { genreEngMapping, labelToSortOptionMap, levelEngMapping } from '@/shared/constants';
import { useTabNavigation } from '@/shared/hooks/useTabNavigation';

const Search = () => {
const searchParams = useSearchParams();
const { selectedTab, setSelectedTab } = useTabNavigation<TAB_TYPES>(TAB.CLASS);

const [genre, setGenre] = useState<string | null>(searchParams?.get('genre') ?? null);

const [searchValue, setSearchValue] = useState('');

const [level, setLevel] = useState<string | null>(null);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [selectedLabel, setSelectedLabel] = useState<keyof typeof labelToSortOptionMap>(SORT_LABELS.LATEST);

const debouncedSearchValue = useDebounce({ value: searchValue, delay: 300 });

const sortOption = labelToSortOptionMap[selectedLabel];

const { data: dancerList, error } = useGetDancerList({
keyword: debouncedSearchValue,
selectedTab: selectedTab as TAB_TYPES,
});

const { data: classList } = useGetClassList({
keyword: debouncedSearchValue,
genre: genre ? genreEngMapping[genre] : undefined,
level: level ? levelEngMapping[level] : undefined,
startDate: formatDateStartTime(startDate),
endDate: formatDateEndTime(endDate),
sortOption,
selectedTab: selectedTab as TAB_TYPES,
});

return (
<div className={searchPageWrapperStyle}>
<SearchHeader.Root>
<SearchHeader.BackIcon />
<SearchBar searchValue={searchValue} handleSearchChange={handleSearchChange(setSearchValue)} />
</SearchHeader.Root>

<TabContainer
defaultSortTags={DEFAULT_SORT_TAGS}
genre={genre}
level={level}
startDate={startDate}
endDate={endDate}
setGenre={setGenre}
setLevel={setLevel}
setStartDate={setStartDate}
setEndDate={setEndDate}
dancerList={dancerList}
classList={classList}
error={error}
selectedLabel={selectedLabel}
setSelectedLabel={setSelectedLabel}
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
/>
</div>
);
};

export default function SearchPage() {
return (
<Suspense fallback={null}>
<Search />
</Suspense>
);
}
87 changes: 7 additions & 80 deletions src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,12 @@
'use client';
import type { Metadata } from 'next';
import SearchPage from '@/app/search/SearchPage';

import { useSearchParams } from 'next/navigation';
import { Suspense, useState } from 'react';
import { useGetClassList, useGetDancerList } from '@/app/search/apis/queries';
import SearchBar from '@/app/search/components/SearchBar/SearchBar';
import SearchHeader from '@/app/search/components/SearchHeader/SearchHeader';
import TabContainer from '@/app/search/components/TabContainer/TabContainer';
import type { TAB_TYPES } from '@/app/search/constants/index';
import { DEFAULT_SORT_TAGS, SORT_LABELS, TAB } from '@/app/search/constants/index';
import { searchPageWrapperStyle } from '@/app/search/index.css';
import { formatDateEndTime, formatDateStartTime } from '@/app/search/utils/formatDate';
import { handleSearchChange } from '@/app/search/utils/searchHandlers';
import useDebounce from '@/common/hooks/useDebounce';
import { genreEngMapping, labelToSortOptionMap, levelEngMapping } from '@/shared/constants';
import { useTabNavigation } from '@/shared/hooks/useTabNavigation';

const Search = () => {
const searchParams = useSearchParams();
const { selectedTab, setSelectedTab } = useTabNavigation<TAB_TYPES>(TAB.CLASS);

const [genre, setGenre] = useState<string | null>(searchParams?.get('genre') ?? null);

const [searchValue, setSearchValue] = useState('');

const [level, setLevel] = useState<string | null>(null);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [selectedLabel, setSelectedLabel] = useState<keyof typeof labelToSortOptionMap>(SORT_LABELS.LATEST);

const debouncedSearchValue = useDebounce({ value: searchValue, delay: 300 });

const sortOption = labelToSortOptionMap[selectedLabel];

const { data: dancerList, error } = useGetDancerList({
keyword: debouncedSearchValue,
selectedTab: selectedTab as TAB_TYPES,
});

const { data: classList } = useGetClassList({
keyword: debouncedSearchValue,
genre: genre ? genreEngMapping[genre] : undefined,
level: level ? levelEngMapping[level] : undefined,
startDate: formatDateStartTime(startDate),
endDate: formatDateEndTime(endDate),
sortOption,
selectedTab: selectedTab as TAB_TYPES,
});

return (
<div className={searchPageWrapperStyle}>
<SearchHeader.Root>
<SearchHeader.BackIcon />
<SearchBar searchValue={searchValue} handleSearchChange={handleSearchChange(setSearchValue)} />
</SearchHeader.Root>

<TabContainer
defaultSortTags={DEFAULT_SORT_TAGS}
genre={genre}
level={level}
startDate={startDate}
endDate={endDate}
setGenre={setGenre}
setLevel={setLevel}
setStartDate={setStartDate}
setEndDate={setEndDate}
dancerList={dancerList}
classList={classList}
error={error}
selectedLabel={selectedLabel}
setSelectedLabel={setSelectedLabel}
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
/>
</div>
);
export const metadata: Metadata = {
title: '클래스 검색',
description: '장르, 난이도, 일정으로 나에게 맞는 댄스 클래스를 찾아보세요.',
alternates: { canonical: '/search' },
};

export default function Page() {
return (
<Suspense fallback={null}>
<Search />
</Suspense>
);
return <SearchPage />;
}
20 changes: 20 additions & 0 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { MetadataRoute } from 'next';

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://www.da-sh.kr';

export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: SITE_URL,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1.0,
},
{
url: `${SITE_URL}/search`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.8,
},
];
}
Loading