Skip to content

Commit 2afeb7a

Browse files
authored
Merge pull request #55 from prgrms-web-devcourse-final-project/feat/cocktailPage#17
Feat/cocktail page#17
2 parents c1fe944 + 95961ba commit 2afeb7a

File tree

17 files changed

+231
-62
lines changed

17 files changed

+231
-62
lines changed

next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { NextConfig } from 'next';
22

33
const nextConfig: NextConfig = {
4+
45
// TurboPack 설정
56
experimental: {
67
turbo: {

src/api/test.tsx

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import SelectBox from '@/shared/components/InputBox/SelectBox';
4+
5+
const selectOption = [
6+
{
7+
id: 'abv',
8+
option: ['', '약한 도수', '가벼운 도수', '중간 도수', '센 도수', '매우 센 도수'],
9+
title: '도수',
10+
},
11+
{
12+
id: 'base',
13+
option: ['', '위스키', '진', '럼', '보드카', '데킬라', '리큐르'],
14+
title: '베이스',
15+
},
16+
{
17+
id: 'glass',
18+
option: ['', '클래식', '롱', '슈터', '숏'],
19+
title: '글라스',
20+
},
21+
];
22+
23+
function Accordion() {
24+
return (
25+
<ul className="flex w-full gap-3">
26+
{selectOption.map(({ id, option, title }) => {
27+
return (
28+
<li key={id}>
29+
<SelectBox option={option} title={title} id={id} groupKey="filter" />
30+
</li>
31+
);
32+
})}
33+
</ul>
34+
);
35+
}
36+
export default Accordion;

src/app/recipe/page.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import PageHeader from '@/shared/components/pageHeader/PageHeader';
22
import { Metadata } from 'next';
3-
import Glass from '@/shared/assets/images/recipe_page_header.png';
3+
import Glass from '@/shared/assets/images/recipe_page_header.webp';
4+
import SelectBox from '@/shared/components/InputBox/SelectBox';
5+
import Input from '@/shared/components/InputBox/Input';
6+
import CocktailList from '@/shared/components/recipePage/cocktailList/CocktailList';
7+
import Accordion from './components/Accordion';
48

59
export const metadata: Metadata = {
610
title: 'SSOUL | 칵테일레시피',
@@ -10,12 +14,33 @@ export const metadata: Metadata = {
1014
function Page() {
1115
return (
1216
<div className="w-full">
13-
<PageHeader
14-
src={Glass}
15-
title="Cocktail Recipes"
16-
description="다양하고 재밌는 칵테일 레시피"
17-
/>
18-
<div className="page-layout max-w-1224"></div>
17+
<section>
18+
<PageHeader
19+
src={Glass}
20+
title="Cocktail Recipes"
21+
description="다양하고 재밌는 칵테일 레시피"
22+
/>
23+
</section>
24+
<div className="page-layout max-w-1224 mt-6">
25+
<section className="flex flex-col-reverse items-start gap-6 md:flex-row md:justify-between md:items-center ">
26+
<Accordion />
27+
<Input
28+
placeholder="내용을 입력해 주세요."
29+
id="search"
30+
variant="search"
31+
className="w-full md:max-w-80"
32+
/>
33+
</section>
34+
<section>
35+
<div className="h-10 flex justify-between items-center mt-3 border-b-1 border-gray-light">
36+
<p>n개</p>
37+
<SelectBox option={['', '댓글순', '인기순']} title="최신순" />
38+
</div>
39+
<section className="mt-5">
40+
<CocktailList />
41+
</section>
42+
</section>
43+
</div>
1944
</div>
2045
);
2146
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// zustand
2+
import { create } from 'zustand';
3+
4+
// select박스 아코디언 메뉴
5+
export type ID = string | number;
6+
7+
type AccordionState = {
8+
openByGroup: Record<string, ID | null>;
9+
};
10+
11+
type AccordionAction = {
12+
setOpen: (group: string, id: ID | null) => void;
13+
toggle: (group: string, id: ID) => void;
14+
closeGroup: (group: string) => void;
15+
};
16+
17+
type Accordion = AccordionState & AccordionAction;
18+
19+
export const useAccordionStore = create<Accordion>((set) => ({
20+
openByGroup: {},
21+
setOpen: (group, id) => set((s) => ({ openByGroup: { ...s.openByGroup, [group]: id } })),
22+
23+
// 같은 id가 이미 열려있으면 닫고 id 교체
24+
toggle: (group, id) =>
25+
set((s) => {
26+
const cur = s.openByGroup[group] ?? null;
27+
return { openByGroup: { ...s.openByGroup, [group]: cur === id ? null : id } };
28+
}),
29+
30+
// 선택 후 닫기
31+
closeGroup: (group) => set((s) => ({ openByGroup: { ...s.openByGroup, [group]: null } })),
32+
}));

src/shared/@store/store.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.
81.6 KB
Loading
-224 KB
Binary file not shown.
178 KB
Loading

src/shared/components/InputBox/SelectBox.tsx

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,82 @@
1-
import { Ref, useState } from 'react';
1+
'use client';
2+
import { Ref, useMemo, useState } from 'react';
23
import Down from '@/shared/assets/icons/selectDown_24.svg';
4+
import { ID, useAccordionStore } from '@/shared/@store/accordionStore';
5+
import { useShallow } from 'zustand/shallow';
36

47
interface Props {
8+
id?: ID;
9+
groupKey?: string;
510
ref?: Ref<HTMLButtonElement | null>;
611
option: string[];
712
title: string;
13+
onChange?: (value: string) => void;
814
}
915

10-
function SelectBox({ ref, option, title }: Props) {
16+
// groupKey를 Props로 내릴경우 == 아코디언 없는 경우 == select박스
17+
function SelectBox({ id, groupKey, ref, option, title, onChange }: Props) {
1118
const [isOpen, setIsOpen] = useState(false);
1219
const [select, setSelect] = useState('');
1320

21+
const ingroup = !!groupKey;
22+
// groupkey일 경우 전달받은 ID로 식별 아닐경우 title로 식별
23+
const keyId = useMemo<ID>(() => id ?? title, [id, title]);
24+
25+
const { openId, toggleGroup, closeGroup } = useAccordionStore(
26+
useShallow((s) => ({
27+
openId: ingroup ? (s.openByGroup[groupKey] ?? null) : null,
28+
toggleGroup: s.toggle,
29+
closeGroup: s.closeGroup,
30+
}))
31+
);
32+
33+
//groupkey가 있을 떄와 없을때로 구분해서 state혹은 store로 관리
34+
const localOpen = ingroup ? openId === keyId : isOpen;
35+
36+
const toggle = () => {
37+
if (ingroup) toggleGroup(groupKey, keyId);
38+
else setIsOpen((v) => !v);
39+
};
40+
41+
const close = () => {
42+
if (ingroup) closeGroup(groupKey);
43+
else setIsOpen(false);
44+
};
45+
1446
const handleChoose = (v: string) => {
15-
setIsOpen(!isOpen);
16-
if (!v) {
17-
setSelect(title);
18-
} else {
19-
setSelect(v);
20-
}
47+
const value = v || title;
48+
setSelect(value);
49+
onChange?.(value);
50+
close();
2151
};
2252

2353
return (
2454
<div className="flex flex-col gap-2 relative h-6">
2555
<button
2656
ref={ref}
2757
className="flex gap-2 cursor-pointer text-base"
28-
onClick={() => setIsOpen(!isOpen)}
58+
onClick={toggle}
2959
aria-expanded={isOpen}
3060
>
3161
{select ? select : title}
32-
{isOpen ? (
62+
{localOpen ? (
3363
<Down className="rotate-180 duration-300" />
3464
) : (
3565
<Down className="rotate-0 duration-300" />
3666
)}
3767
</button>
3868

3969
<ul
40-
className={`w-30 bg-white text-gray-dark p-2 rounded-xl duration-200 absolute transition-all
41-
${isOpen ? 'opacity-100 top-8 right-0' : 'opacity-0 pointer-events-none top-4 right-0'}`}
70+
className={`w-30 bg-white text-gray-dark p-2 rounded-xl z-99 duration-200 absolute transition-all
71+
${
72+
groupKey
73+
? localOpen
74+
? 'opacity-100 top-8 left-0'
75+
: 'opacity-0 pointer-events-none top-4 left-0'
76+
: localOpen
77+
? 'opacity-100 top-8 right-0'
78+
: 'opacity-0 pointer-events-none top-4 right-0'
79+
}`}
4280
role="listbox"
4381
>
4482
{option.map((v, i) => (

0 commit comments

Comments
 (0)