Skip to content

Commit 2aa6999

Browse files
authored
Feat/recommend list#96 (#102)
* [feat] 추천칵테일기능 * [fix]머지전 수정 * [chore] stash머지 * [feat]칵테일 추천리스트 기능 * [fix]알코올도수 props변경 * [chore]포매팅
1 parent a75d485 commit 2aa6999

File tree

10 files changed

+212
-43
lines changed

10 files changed

+212
-43
lines changed

src/domains/recipe/components/details/BackBtn.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
'use client';
22
import Back from '@/shared/assets/icons/back_36.svg';
3-
import { useRouter } from 'next/navigation';
3+
import Link from 'next/link';
44

55
function BackBtn() {
6-
const router = useRouter();
7-
86
return (
9-
<button type="button" className="z-1" onClick={router.back} aria-label="뒤로가기">
10-
<Back />
7+
<button type="button" className="z-1" aria-label="뒤로가기">
8+
<Link href="/recipe">
9+
<Back />
10+
</Link>
1111
</button>
1212
);
1313
}

src/domains/recipe/details/DetailItem.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Image from 'next/image';
2-
import Short from '@/shared/assets/icons/short_36.svg';
32
import Label from '@/domains/shared/components/label/Label';
43
import AbvGraph from '@/domains/shared/components/abv-graph/AbvGraph';
54
import { labelTitle } from '../utills/labelTitle';
5+
import useGlass from './hook/useGlass';
66

77
interface Props {
88
name: string;
@@ -15,6 +15,19 @@ interface Props {
1515

1616
function DetailItem({ name, nameKo, story, src, abv, glassType }: Props) {
1717
const alcoholTitle = labelTitle(abv);
18+
const abvNum = abv
19+
.replace(/\(([^)]*)\)/g, '$1')
20+
.split(' ')
21+
.reverse()
22+
.slice(0, 1)
23+
.toString();
24+
const maxAbv = abvNum
25+
.replace(/[~%]/g, ' ')
26+
.split(' ')
27+
.filter((str) => str.trim() !== '')
28+
.map(Number);
29+
30+
const glassIcon = useGlass(glassType);
1831

1932
return (
2033
<div className="flex flex-col items-center">
@@ -37,8 +50,15 @@ function DetailItem({ name, nameKo, story, src, abv, glassType }: Props) {
3750
<span className="absolute w-3 h-3 rounded-full -bottom-38 z-2 left-1/2 -translate-x-1/2 bg-secondary md:bg-transparent"></span>
3851
</div>
3952

40-
<div className="rounded-2xl overflow-hidden w-75 h-93.75 mt-32 md:mt-4 lg:mt-7 [filter:drop-shadow(0_0_20px_rgba(255,255,255,0.3))]">
41-
<Image src={src} alt={`${nameKo}사진`} fill className="object-cover" />
53+
<div className="rounded-2xl overflow-hidden w-75 h-93.75 mt-32 md:mt-4 lg:mt-7 [filter:drop-shadow(0_0_20px_rgba(255,255,255,0.3))] relative">
54+
<Image
55+
src={src}
56+
alt={`${nameKo}사진`}
57+
fill
58+
className="object-cover"
59+
sizes="300px"
60+
priority
61+
/>
4262
</div>
4363

4464
<dl className="flex flex-col mt-5 gap-3 w-75">
@@ -48,18 +68,22 @@ function DetailItem({ name, nameKo, story, src, abv, glassType }: Props) {
4868
<span>|</span>
4969
</dt>
5070
<dd className="flex gap-3 items-center">
51-
<p className="text-xs">{abv}</p>
52-
<AbvGraph />
71+
<p className="text-xs">{abvNum}</p>
72+
<AbvGraph abv={Math.max(...maxAbv)} max={40} />
5373
</dd>
5474
</div>
5575
<div className="flex items-center gap-3">
5676
<dt className="flex gap-2 items-center">
5777
<p>글래스 타입</p>
5878
<span>|</span>
5979
</dt>
60-
<dd className="flex items-center gap-2">
61-
<Short />
62-
<p>{glassType} 드링크</p>
80+
<dd className="flex items-center ">
81+
{glassIcon}
82+
<p>
83+
{glassType == '숏' || glassType == '롱'
84+
? `${glassType} 드링크`
85+
: `${glassType} 칵테일`}
86+
</p>
6387
</dd>
6488
</div>
6589
</dl>

src/domains/recipe/details/DetailList.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,38 @@
1+
'use client';
2+
import { useEffect, useState } from 'react';
13
import DetailRecommendList from './DetailRecommendList';
4+
import { getApi } from '@/app/api/config/appConfig';
5+
import { useParams } from 'next/navigation';
6+
import { RecommendCocktail } from '../types/types';
7+
import Link from 'next/link';
28

39
function DetailList() {
10+
const { id } = useParams();
11+
const url = new URL(`${getApi}/cocktails/recommend/related`);
12+
url.searchParams.set('cocktailId', String(id));
13+
14+
const [recommendItem, setRecommendItem] = useState<RecommendCocktail[]>([]);
15+
16+
const recommentFetch = async () => {
17+
const res = await fetch(url.toString());
18+
const json = await res.json();
19+
if (!res.ok) throw new Error('데이터 요청 실패');
20+
21+
setRecommendItem(json.data);
22+
};
23+
useEffect(() => {
24+
recommentFetch();
25+
}, []);
26+
427
return (
5-
<ul className="flex justify-between gap-2">
6-
<li>
7-
<DetailRecommendList />
8-
</li>
9-
<li>
10-
<DetailRecommendList />
11-
</li>
12-
<li>
13-
<DetailRecommendList />
14-
</li>
28+
<ul className="grid place-content-between [grid-template-columns:repeat(3,minmax(0,250px))] gap-4 ">
29+
{recommendItem.map(({ cocktailImgUrl, cocktailName, cocktailNameKo, id }) => (
30+
<li key={id}>
31+
<Link href={`/recipe/${String(id)}`}>
32+
<DetailRecommendList src={cocktailImgUrl} name={cocktailName} nameKo={cocktailNameKo} />
33+
</Link>
34+
</li>
35+
))}
1536
</ul>
1637
);
1738
}

src/domains/recipe/details/DetailRecipe.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,45 @@
1+
import { ozToMl } from './hook/ozToMl';
2+
3+
type Recipe = {
4+
ingredientName: string;
5+
amount: string;
6+
unit: string;
7+
};
8+
19
interface Props {
2-
ingredient: string;
10+
ingredient: Recipe[];
311
recipe: string;
412
}
513

614
function DetailRecipe({ ingredient, recipe }: Props) {
7-
const ingredients = ingredient.trim().split(',').filter(Boolean);
815
const recipes = recipe.trim().split('.').filter(Boolean);
16+
const arr = ingredient.map((a) => ({
17+
...a,
18+
convert: ozToMl(a.amount),
19+
}));
920

1021
return (
1122
<div className="flex flex-col md:flex-row px-5 gap-5">
1223
<article className="flex flex-col gap-4 w-[50%]">
1324
<h4 className="text-2xl font-bold">재료</h4>
1425
<ul className="flex flex-col gap-2">
15-
{ingredients.map((v, i) => (
16-
<li key={i}>{v}</li>
17-
))}
26+
{arr.map((v, i) => {
27+
return (
28+
<li key={i} className="flex gap-3">
29+
<p>{v.ingredientName}</p>
30+
<span className="text-white/80 text-sm">
31+
{v.amount}
32+
{v.unit}
33+
34+
{v.unit == 'oz' && `${' '}|${' '} ${v.convert} ml`}
35+
</span>
36+
</li>
37+
);
38+
})}
1839
</ul>
1940
</article>
2041

21-
<span className="border-t-1 w-1/2 pt-5 md:border-l-1 md:border-t-0 md:px-10 border-white">
42+
<span className="border-t-1 w-full md:w-1/2 pt-5 md:border-l-1 md:border-t-0 md:px-10 border-white">
2243
<article className="flex flex-col gap-4 ">
2344
<h4 className="text-2xl font-bold">만드는 법</h4>
2445
<ol className="flex flex-col gap-2 pl-4 list-decimal">

src/domains/recipe/details/DetailRecommendList.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
// import Image from 'next/image';
2-
function DetailRecommendList() {
1+
import Image from 'next/image';
2+
3+
interface Props {
4+
src: string;
5+
name: string;
6+
nameKo: string;
7+
}
8+
9+
function DetailRecommendList({ src, name, nameKo }: Props) {
310
return (
411
<div className="flex flex-col gap-3">
5-
<div className="max-h-75">{/* <Image src="" alt="" /> */}</div>
12+
<div
13+
className="
14+
relative overflow-hidden rounded-2xl
15+
w-full max-w-62.5 aspect-[5/6]
16+
"
17+
>
18+
<Image src={src} alt={`${nameKo} 사진`} fill className="object-cover" sizes="250px" />
19+
</div>
620
<div className="flex flex-col gap-1">
7-
<h4 className="font-serif text-base lg:text-lg">Old Fashioned</h4>
8-
<p className="font-serif text-base">올드패션드</p>
21+
<h4 className="font-serif text-base truncate lg:text-lg">{name}</h4>
22+
<p className="font-serif text-sm sm:text-base">{nameKo}</p>
923
</div>
1024
</div>
1125
);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// 유니코드 분수 매핑 (소수점 대신 정수 ml로 변환)
2+
const FRAC_MAP: Record<string, number> = {
3+
'¼': 8, // 0.25 * 30
4+
'½': 15, // 0.5 * 30
5+
'¾': 23, // 0.75 * 30
6+
'⅓': 10, // 1/3 * 30 ≈ 10
7+
'⅔': 20, // 2/3 * 30 ≈ 20
8+
'⅕': 6, // 1/5 * 30
9+
'⅖': 12, // 2/5 * 30
10+
'⅗': 18, // 3/5 * 30
11+
'⅘': 24, // 4/5 * 30
12+
'⅙': 5, // 1/6 * 30
13+
'⅚': 25, // 5/6 * 30
14+
'⅛': 4, // 1/8 * 30
15+
'⅜': 11, // 3/8 * 30
16+
'⅝': 19, // 5/8 * 30
17+
'⅞': 26, // 7/8 * 30
18+
};
19+
20+
const FRAC_CLASS = Object.keys(FRAC_MAP).join('');
21+
22+
export function ozToMl(input: string): number | '' {
23+
if (!input) return '';
24+
25+
const trimmed = input.trim();
26+
27+
// 혼합 분수: "1 ⅔", "2 ½"
28+
const mixed = trimmed.match(new RegExp(`^(\\d+)\\s*([${FRAC_CLASS}])$`));
29+
if (mixed) {
30+
const whole = Number(mixed[1]);
31+
const frac = FRAC_MAP[mixed[2]] ?? 0;
32+
return whole * 30 + frac;
33+
}
34+
35+
// 분수 단독: "⅔", "½"
36+
if (FRAC_MAP[trimmed] != null) {
37+
return FRAC_MAP[trimmed];
38+
}
39+
40+
// 순수 숫자: "1", "2"
41+
if (!isNaN(Number(trimmed))) {
42+
return Number(trimmed) * 30;
43+
}
44+
45+
return '';
46+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Shooter from '@/shared/assets/icons/shooter_36.svg';
2+
import Short from '@/shared/assets/icons/short_36.svg';
3+
import Long from '@/shared/assets/icons/long_36.svg';
4+
import Classic from '@/shared/assets/icons/classic_36.svg';
5+
6+
const useGlass = (glass: string) => {
7+
switch (glass) {
8+
case '슈터':
9+
return <Shooter />;
10+
case '숏':
11+
return <Short />;
12+
case '롱':
13+
return <Long />;
14+
case '클래식':
15+
return <Classic />;
16+
}
17+
};
18+
export default useGlass;

src/domains/recipe/types/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,12 @@ export interface Cocktail {
55
cocktailImgUrl: string;
66
cocktailNameKo: string;
77
}
8+
9+
export interface RecommendCocktail {
10+
id: number;
11+
cocktailNameKo: string;
12+
cocktailName: string;
13+
cocktailImgUrl: string;
14+
alcoholStrength: string;
15+
alcoholBaseType: string;
16+
}

src/domains/shared/components/abv-graph/AbvGraph.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
1-
function AbvGraph() {
1+
import clsx from 'clsx';
2+
3+
interface Props {
4+
max?: number;
5+
abv?: number;
6+
}
7+
8+
function AbvGraph({ max, abv }: Props) {
9+
if (!abv) return;
10+
const safeMax = Math.max(0, max || 0.0001);
11+
const rawPct = (abv / safeMax) * 100;
12+
const pct = Math.min(100, Math.max(0, Number.isFinite(rawPct) ? rawPct : 0));
13+
14+
const bandClass = clsx(
15+
'h-full rounded-full transition-[width] duration-500',
16+
'bg-gradient-to-r from-amber-300 to-red-500', // 기본 그라데이션
17+
pct >= 80 && 'shadow-[0_0_12px_rgba(250,36,36,0.45)]'
18+
);
19+
220
return (
321
<div
4-
className="w-full md:w-49 h-3 rounded-full overflow-hidden border-[0.5px] border-gray relative"
22+
className="w-45 h-3 rounded-full overflow-hidden border-[0.5px] border-gray relative"
523
role="progressbar"
624
aria-label="나의 알코올 도수"
25+
aria-valuemin={0}
26+
aria-valuemax={max}
727
>
8-
<div
9-
className="absolute top-0 left-0 w-10 h-3
10-
bg-linear-to-r from-[#FFCA8D] to-[#FA2424]
11-
"
12-
></div>
28+
<div className={bandClass} style={{ width: `${pct}%` }}></div>
1329
</div>
1430
);
1531
}

src/domains/shared/components/cocktail-card/CocktailCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ function CocktailCard({
3434
<div className="flex flex-col gap-4">
3535
<div
3636
className={tw(
37-
`${!className && 'w-80 h-75 md:w-62.5 '} rounded-xl overflow-hidden relative`,
37+
`${!className && 'w-80 h-75 md:w-62.5 '} rounded-xl overflow-hidden relative`,
3838
className
3939
)}
4040
>
41-
<Image src={src} alt={name} fill className="object-cover" />
41+
<Image src={src} alt={name} fill className="object-cover" sizes="320px" priority />
4242
{keep && (
4343
<div className="flex w-full pl-4 px-3 py-2 items-center justify-between absolute left-0 top-0">
4444
<div>{alcoholTitle && <Label title={alcoholTitle} />}</div>

0 commit comments

Comments
 (0)