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
49 changes: 49 additions & 0 deletions src/components/gauge/block-gauge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useState } from "react";
import BlockGauge, { type GaugeLevel } from "./block-gauge";

const meta: Meta = {
title: "Components/BlockGauge",
component: BlockGauge,
parameters: {
layout: "centered",
docs: {
description: {
component:
"와인의 맛 강도를 시각적으로 표현하는 블록 게이지 컴포넌트입니다.",
},
},
},
tags: ["autodocs"],
argTypes: {
level: {
control: { type: "number", min: 0, max: 6 },
description: "게이지 레벨 (0-6)",
defaultValue: 3,
},
},
decorators: [
(Story) => (
<div className="p-4" style={{ width: "343px" }}>
<Story />
</div>
),
],
};

export default meta;

type Story = StoryObj<typeof meta>;

export const ClickTest: Story = {
render: () => {
const [level, setLevel] = useState<GaugeLevel>(3);

return (
<div>
<p className="mb-2 text-sm">레벨: {level}</p>
<BlockGauge level={level} onChange={setLevel} />
</div>
);
},
};
52 changes: 52 additions & 0 deletions src/components/gauge/block-gauge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import { cn } from "@/lib/utils";

type GaugeLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6;

interface BlockGaugeProps {
level: GaugeLevel; // 0-6 사이의 값 (0: 비어있음, 6: 가득 참)
color?: string; // 활성화된 블록 색상 (기본: bg-black)
onChange?: (newLevel: GaugeLevel) => void; // 클릭 시 호출될 함수
}

const BlockGauge = ({
level,
color = "bg-black",
onChange,
}: BlockGaugeProps) => {
const handleClick = (clickedIndex: number) => {
if (!onChange) return;

const newLevel = (clickedIndex + 1) as GaugeLevel;
onChange(newLevel === level ? 0 : newLevel);
};

return (
<div className="flex w-full gap-0.5 tablet:gap-1 pc:gap-1">
{Array.from({ length: 6 }).map((_, index) => (
<button
key={index}
type="button"
className={cn(
"h-2.5 tablet:h-2.5 pc:h-2.5",
"flex-1 shrink-0 basis-0",
index === 0 && "rounded-l-md",
index === 5 && "rounded-r-md",
index < level ? color : "bg-neutral200",
onChange && [
"cursor-pointer hover:opacity-80",
"focus:outline-none focus:ring-2 focus:ring-gray600",
]
)}
onClick={() => handleClick(index)}
disabled={!onChange}
/>
))}
</div>
);
};

export default BlockGauge;

export type { GaugeLevel };
157 changes: 157 additions & 0 deletions src/components/taste/Taste.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useState } from "react";
import Taste from "./Taste";
import { cn } from "@/lib/utils";
import { GaugeLevel } from "../gauge/block-gauge";

const meta: Meta = {
title: "Components/Taste",
component: Taste,
parameters: {
layout: "centered",
docs: {
description: {
component:
"와인의 맛 특성과 강도를 표시하는 컴포넌트입니다. 모바일 : 343px, 태블릿과 PC : 480px",
},
},
},
tags: ["autodocs"],
argTypes: {
type: {
control: "text",
description: "와인 맛의 종류",
defaultValue: "바디감",
},
data: {
control: { type: "number", min: 0, max: 6 },
description: "맛 강도 데이터 (0-6)",
defaultValue: 3,
},
taste: {
control: "text",
description: "맛의 특징",
defaultValue: "진해요",
},
},
};

export default meta;

type Story = StoryObj<typeof meta>;

const ResponsiveTasteWrapper = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<div
className={cn(
"flex flex-col items-start",
"w-[343px] md:w-[480px] lg:w-[480px]",
"gap-3 md:gap-4 lg:gap-4"
)}
>
{children}
</div>
);
};

export const InteractiveWineProfile: Story = {
render: () => {
const [bodyLevel, setBodyLevel] = useState<GaugeLevel>(4);
const [tanninLevel, setTanninLevel] = useState<GaugeLevel>(2);
const [sweetnessLevel, setSweetnessLevel] = useState<GaugeLevel>(1);
const [acidityLevel, setAcidityLevel] = useState<GaugeLevel>(3);

return (
<div>
<div className="space-y-4">
<ResponsiveTasteWrapper>
<Taste
type="바디감"
data={bodyLevel}
taste={
bodyLevel === 0
? "없음"
: bodyLevel <= 2
? "가벼움"
: bodyLevel <= 4
? "중간"
: "진해요"
}
onChange={setBodyLevel}
/>
</ResponsiveTasteWrapper>

<ResponsiveTasteWrapper>
<Taste
type="탄닌"
data={tanninLevel}
taste={
tanninLevel === 0
? "없음"
: tanninLevel <= 2
? "부드러움"
: tanninLevel <= 4
? "적당함"
: "떫어요"
}
onChange={setTanninLevel}
/>
</ResponsiveTasteWrapper>

<ResponsiveTasteWrapper>
<Taste
type="당도"
data={sweetnessLevel}
taste={
sweetnessLevel === 0
? "없음"
: sweetnessLevel <= 2
? "약간 단맛"
: sweetnessLevel <= 4
? "중간 단맛"
: "달아요"
}
onChange={setSweetnessLevel}
/>
</ResponsiveTasteWrapper>

<ResponsiveTasteWrapper>
<Taste
type="산미"
data={acidityLevel}
taste={
acidityLevel === 0
? "없음"
: acidityLevel <= 2
? "부족함"
: acidityLevel <= 4
? "적당함"
: "많이셔요"
}
onChange={setAcidityLevel}
/>
</ResponsiveTasteWrapper>
</div>

{/* 현재 설정값 표시 */}
<div className="mt-6 rounded-lg bg-gray-50 p-3 text-xs">
<strong>현재 와인 프로필:</strong> 바디감 {bodyLevel}, 탄닌{" "}
{tanninLevel}, 당도 {sweetnessLevel}, 산미 {acidityLevel}
</div>
</div>
);
},
parameters: {
layout: "padded",
docs: {
description: {
story:
"블럭을 마우스로 클릭하면 게이지가 차오릅니다. 또한 어느 블럭이든 두 번 클릭하면 게이지가 0으로 됩니다.",
},
},
},
};
55 changes: 55 additions & 0 deletions src/components/taste/Taste.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { cn } from "@/lib/utils";
import BlockGauge, { type GaugeLevel } from "../gauge/block-gauge";

interface TasteProps {
type: string;
data: GaugeLevel; // 0-6 사이의 값
taste: string;
onChange?: (newLevel: GaugeLevel) => void;
}

const Taste = ({ type, data, taste, onChange }: TasteProps) => {
return (
<div
className={cn(
"flex flex-col items-start",
"w-[343px] tablet:w-[480px] pc:w-[480px]",
"gap-3 tablet:gap-4 pc:gap-4"
)}
>
<div className="flex w-full items-center gap-3">
{/* 왼쪽: type */}
<div
className={cn(
"truncate rounded-md bg-neutral200 px-1 py-1 text-center text-body-sm text-gray600",
"w-[53px] tablet:w-[70px] pc:w-[70px]"
)}
title={type}
>
{type}
</div>

{/* 중간: 게이지 */}
<div className="flex flex-1 justify-center">
<BlockGauge level={data} onChange={onChange} />
</div>

{/* 오른쪽: taste - data가 0일 때 색상 변경 */}
<div
className={cn(
"truncate px-1 py-1 text-center text-body-sm",
"w-[68px] tablet:w-[80px] pc:w-[80px]",
data === 0 ? "text-neutral400" : "text-black"
)}
title={taste}
>
{taste}
</div>
</div>
</div>
);
};

export default Taste;
2 changes: 2 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default {
gray600: "#8C8C8B",
gray800: "#484746",
primary: "#1A1918",
neutral200: "#F2F2F2",
neutral400: "#BABABA",
},
screens: {
mobile: { max: "743px" },
Expand Down