Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
53 changes: 53 additions & 0 deletions src/components/gauge/block-gauge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useState } from "react";
import BlockGauge 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,
},
maxBlocks: {
control: { type: "number", min: 1, max: 10 },
description: "전체 블록 수",
defaultValue: 6,
},
},
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(3);

return (
<div>
<BlockGauge level={level} onChange={setLevel} />
</div>
);
},
};
57 changes: 57 additions & 0 deletions src/components/gauge/block-gauge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { cn } from "@/lib/utils";

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

const BlockGauge = ({
level,
maxBlocks = 6,
color = "bg-black",
onChange,
}: BlockGaugeProps) => {
// 레벨이 범위를 벗어나지 않도록 조정
const safeLevel = Math.max(0, Math.min(level, maxBlocks));

// 블록 클릭 핸들러
const handleClick = (clickedIndex: number) => {
if (!onChange) return;

const newLevel = clickedIndex + 1;
// 같은 레벨을 클릭하면 0으로, 다른 레벨을 클릭하면 해당 레벨로
onChange(newLevel === safeLevel ? 0 : newLevel);
};

return (
<div className="flex w-full gap-0.5 tablet:gap-1 pc:gap-1">
{Array.from({ length: maxBlocks }).map((_, index) => (
<button
key={index}
type="button"
style={{ flex: "1 0 0" }} // flex: 1 0 0 스타일 적용
className={cn(
// 높이 조정
"h-2.5 tablet:h-2.5 pc:h-2.5",
// 모서리 둥글기
index === 0 ? "rounded-l-md" : "",
index === maxBlocks - 1 ? "rounded-r-md" : "",
// 활성화 여부에 따른 색상
index < safeLevel ? color : "bg-neutral200",
// 클릭 가능한 경우에만 호버 효과와 커서 추가
onChange && [
"cursor-pointer hover:opacity-80",
"focus:outline-none focus:ring-2 focus:ring-gray600",
]
)}
onClick={() => handleClick(index)}
disabled={!onChange} // onChange가 없으면 비활성화
/>
))}
</div>
);
};

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

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(4);
const [tanninLevel, setTanninLevel] = useState(2);
const [sweetnessLevel, setSweetnessLevel] = useState(1);
const [acidityLevel, setAcidityLevel] = useState(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으로 됩니다.",
},
},
},
};
53 changes: 53 additions & 0 deletions src/components/taste/Taste.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { cn } from "@/lib/utils";
import BlockGauge from "../gauge/block-gauge";

interface TasteProps {
type: string;
data: number; // 0-6 사이의 값
taste: string;
onChange?: (newLevel: number) => 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