Skip to content
Merged
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
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import { RouterProvider } from 'react-router-dom';
import { queryClient } from './libs/queryClient';
import router from '@/routes/route';
import ToastProvider from './components/common/Toast/ToastProvider';
import { FilterProvider } from '@/contexts/FilterContext';

function App() {
return (
<QueryClientProvider client={queryClient}>
<ToastProvider>
<RouterProvider router={router} />
<FilterProvider>
<RouterProvider router={router} />
</FilterProvider>
</ToastProvider>
</QueryClientProvider>
);
}

export default App;
export default App;
97 changes: 72 additions & 25 deletions src/pages/search/ReviewFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Fragment, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/utils/cn';
import { useFilter } from '@/contexts/FilterContext';
import { Header, AccordionSection, FilterCheckbox } from '@/components';
Expand All @@ -7,63 +8,101 @@ const sortOptions = ['가장 인기있는 순', '평점순', '최신순'];
const cinemaOptions = {
IMAX: ['용산아이파크몰', '왕심리', '천호'],
'Dolby Cinema': [
'코에스점',
'남양주현대아울렌스페이스원',
'하른스타필드점',
'안성스타필드점',
'수원AK플라자점',
'송도점',
'대전신세계 아트애드사인스점',
'대구신세계점',
'코엑스점', '남양주현대아울렌스페이스원', '하른스타필드점', '안성스타필드점',
'수원AK플라자점', '송도점', '대전신세계 아트애드사인스페이스점', '대구신세계점',
],
Comment on lines -10 to 13
Copy link
Member

Choose a reason for hiding this comment

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

영화관 목록은 api로 가져와야할 것 같아요..ㅠㅠ 피그마에는 일부만 나와있어서 ..

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵 최대한수정해보겠습니다!

};
const soundOptions = ['Dolby Atmos', 'DTS:X'];
const environmentOptions = ['리클라이너', '카포트'];

const auditoriumIdMap: Record<string, number> = {
'용산아이파크몰': 1,
'왕심리': 2,
'천호': 3,
'코엑스점': 4,
'남양주현대아울렌스페이스원': 5,
'하른스타필드점': 6,
'안성스타필드점': 7,
'수원AK플라자점': 8,
'송도점': 9,
'대전신세계 아트애드사인스페이스점': 10,
'대구신세계점': 11,
};

export default function ReviewFilter() {
const [activeSort, setActiveSort] = useState('인기순');
const navigate = useNavigate();
const [activeSort, setActiveSort] = useState('가장 인기있는 순');
const [openSections, setOpenSections] = useState<string[]>(['Dolby Cinema', '음향', '관람 환경']);
const [selectedCinemas, setSelectedCinemas] = useState<Record<string, boolean>>({});
const [selectedSounds, setSelectedSounds] = useState<Record<string, boolean>>({});
const [selectedEnvironments, setSelectedEnvironments] = useState<Record<string, boolean>>({});

const { setIsFiltered } = useFilter();

const handleBackClick = () => {
navigate(-1);
};

const toggleSection = (sectionName: string) => {
setOpenSections((prev) =>
setOpenSections(prev =>
prev.includes(sectionName)
? prev.filter((name) => name !== sectionName)
: [...prev, sectionName],
? prev.filter(name => name !== sectionName)
: [...prev, sectionName]
);
};

const handleCheckboxChange = (
setter: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
option: string,
option: string
) => {
setter((prev) => {
setter(prev => {
const updated = { ...prev, [option]: !prev[option] };
setIsFiltered(true);
return updated;
});
};

const handleApplyFilter = () => {
const selectedAuditoriumNames = Object.keys(selectedCinemas).filter(key => selectedCinemas[key]);
const selectedAuditoriumIds = selectedAuditoriumNames
.map(name => auditoriumIdMap[name])
.filter(Boolean); // ID가 없는 값은 제거

const sortParamMap: Record<string, string> = {
'가장 인기있는 순': 'POPULAR',
'평점순': 'RATING',
'최신순': 'LATEST',
};

const searchParams: Record<string, string | string[]> = {
sort: sortParamMap[activeSort],
};

if (selectedAuditoriumIds.length > 0) {
searchParams.auditoriumId = selectedAuditoriumIds[0].toString(); // 단일 선택 가정
}

navigate(`/search/result?${new URLSearchParams(searchParams as any).toString()}`);
};

return (
<div className="min-h-screen text-white">
<div className="mx-auto w-full max-w-[400px] px-4">
<Header leftSection="BACK">필터</Header>
<Header leftSection="BACK" onBackClick={handleBackClick}>
필터
</Header>

{/* 정렬 */}
<section className="py-4">
<h2 className="text-title-3 mb-3">정렬</h2>
<section className="py-4 mt-12">
<h2 className="mb-3 text-title-3">정렬</h2>
<div className="flex w-full items-center">
{sortOptions.map((option, index) => (
<Fragment key={option}>
<button
onClick={() => setActiveSort(option)}
className={cn(
'text-title-4 flex-1 text-center',
activeSort === option ? 'font-bold text-red-400' : 'text-gray-300',
'flex-1 text-center text-title-4',
activeSort === option ? 'font-bold text-red-400' : 'text-gray-300'
)}
>
{option}
Expand All @@ -76,7 +115,7 @@ export default function ReviewFilter() {

{/* 영화관 */}
<div className="py-4">
<h2 className="text-title-3 mb-2">영화관</h2>
<h2 className="mb-2 text-title-3">영화관</h2>
<div className="flex flex-col pl-4">
{Object.entries(cinemaOptions).map(([cinema, branches]) => (
<AccordionSection
Expand All @@ -86,7 +125,7 @@ export default function ReviewFilter() {
onToggle={() => toggleSection(cinema)}
>
<div className="flex flex-col gap-y-3 pl-2">
{branches.map((branch) => (
{branches.map(branch => (
<FilterCheckbox
key={branch}
option={branch}
Expand All @@ -107,8 +146,8 @@ export default function ReviewFilter() {
isOpen={openSections.includes('음향')}
onToggle={() => toggleSection('음향')}
>
<div className="flex flex-col gap-y-3 pt-4 pl-4">
{soundOptions.map((option) => (
<div className="flex flex-col gap-y-3 pl-4 pt-4">
{soundOptions.map(option => (
<FilterCheckbox
key={option}
option={option}
Expand All @@ -127,8 +166,8 @@ export default function ReviewFilter() {
isOpen={openSections.includes('관람 환경')}
onToggle={() => toggleSection('관람 환경')}
>
<div className="flex flex-col gap-y-3 pt-4 pl-4">
{environmentOptions.map((option) => (
<div className="flex flex-col gap-y-3 pl-4 pt-4">
{environmentOptions.map(option => (
<FilterCheckbox
key={option}
option={option}
Expand All @@ -139,6 +178,14 @@ export default function ReviewFilter() {
</div>
</AccordionSection>
</div>

{/* 적용 버튼 */}
<button
onClick={handleApplyFilter}
className="w-full bg-red-500 py-3 rounded text-white font-bold mt-6"
>
필터 적용하기
</button>
</div>
</div>
);
Expand Down
91 changes: 62 additions & 29 deletions src/pages/search/ReviewSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,92 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {SearchInput, Badge, BottomNavigation} from '@/components'
const initialKeywords = [
'뭔가검색했겠지...', '뭐가있지', '아무거나',
'두줄은', '채워야되니까', '일단써보기'
];
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { SearchInput, Badge, BottomNavigation } from '@/components';
import api from '@/api/api';

type Keyword = {
content: string;
};

export default function ReviewSearchPage() {
const [search, setSearch] = useState('');
const [recentKeywords, setRecentKeywords] = useState(initialKeywords);
const navigate = useNavigate();
const [recentKeywords, setRecentKeywords] = useState<Keyword[]>([]);
const navigate = useNavigate();

const handleRemove = (index: number) => {
setRecentKeywords(prev => prev.filter((_, i) => i !== index));
};
useEffect(() => {
const fetchRecentKeywords = async () => {
try {
const res = await api.get('/search');

if (res.data?.success && Array.isArray(res.data.data)) {
setRecentKeywords(res.data.data);
} else {
console.warn('유효하지 않은 검색어 응답:', res.data);
setRecentKeywords([]);
}
} catch (error) {
console.error('최근 검색어 조회 실패', error);
setRecentKeywords([]);
}
};

const handleSearch = () => {
fetchRecentKeywords();
}, []);

const handleSearch = () => {
if (!search.trim()) {
alert('검색어를 입력해주세요.');
return;
}
navigate(`/search/result?query=${search}`);

navigate(`/search/result?query=${encodeURIComponent(search)}`);
};

const handleRemove = async (index: number) => {
const keyword = recentKeywords[index].content;

try {
await api.delete('/search', {
params: { keyword },
});
setRecentKeywords((prev) => prev.filter((_, i) => i !== index));
} catch (error) {
console.error('검색어 삭제 실패', error);
}
};

return (
<div className="relative flex justify-center min-h-screen text-white ">
return (
<div className="relative flex justify-center min-h-screen text-white">
<div className="w-full max-w-[400px] px-4 pt-4 pb-20">
<SearchInput
value={search}
onChange={setSearch}
placeholder="검색어를 입력해주세요"
onSearch={handleSearch}
onSearch={handleSearch}
/>

<div className="mt-6">
<h2 className="mb-2 text-title-3">최근 검색어</h2>
<div className="flex flex-wrap gap-2">
{recentKeywords.map((word, index) => (
<Badge
key={index}
type="removable"
onRemove={() => handleRemove(index)}
>
{word}
</Badge>
))}
{recentKeywords.length > 0 ? (
recentKeywords.map((wordObj, index) => (
<Badge
key={index}
type="removable"
onRemove={() => handleRemove(index)}
>
{wordObj.content}
</Badge>
))
) : (
<p className="text-body-3 text-gray-400">최근 검색어가 없습니다.</p>
)}
</div>
</div>
</div>

<div className="fixed bottom-0 left-1/2 w-full max-w-[430px] -translate-x-1/2">
<BottomNavigation />
</div>
</div>
);
}


Loading