diff --git a/.storybook/main.ts b/.storybook/main.ts index a70eab0e..aaa61f6f 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -20,5 +20,20 @@ const config: StorybookConfig = { prop.parent ? !/node_modules/.test(prop.parent.fileName) : true, }, }, + webpackFinal: async (config) => { + const imageRule = config.module?.rules?.find((rule) => { + const test = (rule as { test: RegExp }).test; + if (!test) return false; + return test.test(".svg"); + }) as { [key: string]: any }; + imageRule.exclude = /\.svg$/; + + config.module?.rules?.push({ + test: /\.svg$/, + use: ["@svgr/webpack"], + }); + + return config; + }, }; export default config; diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 00000000..2ff14783 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,4 @@ +declare module "*.svg" { + const content: React.FunctionComponent>; + export default content; +} diff --git a/next.config.ts b/next.config.ts index e9ffa308..61e331f5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,21 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + webpack(config) { + config.module.rules.push({ + test: /\.svg$/i, + use: [ + { + loader: "@svgr/webpack", + options: { + configFile: "./svgr.config.js", + }, + }, + ], + }); + + return config; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 039ddabb..859342a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", "axios": "^1.12.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "next": "^15.5.2", "react": "^19.1.0", @@ -6240,6 +6241,18 @@ "dev": true, "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", diff --git a/package.json b/package.json index 595cceef..84a2c8fe 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", "axios": "^1.12.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "next": "^15.5.2", "react": "^19.1.0", diff --git a/public/icons/flavor/ic-apple.svg b/public/icons/flavor/ic-apple.svg new file mode 100644 index 00000000..80ee1186 --- /dev/null +++ b/public/icons/flavor/ic-apple.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/flavor/ic-baking.svg b/public/icons/flavor/ic-baking.svg new file mode 100644 index 00000000..761a70a2 --- /dev/null +++ b/public/icons/flavor/ic-baking.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/flavor/ic-berry.svg b/public/icons/flavor/ic-berry.svg new file mode 100644 index 00000000..3b435d98 --- /dev/null +++ b/public/icons/flavor/ic-berry.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/flavor/ic-caramel.svg b/public/icons/flavor/ic-caramel.svg new file mode 100644 index 00000000..97ba7e34 --- /dev/null +++ b/public/icons/flavor/ic-caramel.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-cherry.svg b/public/icons/flavor/ic-cherry.svg new file mode 100644 index 00000000..44ed969a --- /dev/null +++ b/public/icons/flavor/ic-cherry.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/flavor/ic-chocolate.svg b/public/icons/flavor/ic-chocolate.svg new file mode 100644 index 00000000..214de5d0 --- /dev/null +++ b/public/icons/flavor/ic-chocolate.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-citrus.svg b/public/icons/flavor/ic-citrus.svg new file mode 100644 index 00000000..c8e77f09 --- /dev/null +++ b/public/icons/flavor/ic-citrus.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-earth.svg b/public/icons/flavor/ic-earth.svg new file mode 100644 index 00000000..c74ef425 --- /dev/null +++ b/public/icons/flavor/ic-earth.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-flower.svg b/public/icons/flavor/ic-flower.svg new file mode 100644 index 00000000..3e11700b --- /dev/null +++ b/public/icons/flavor/ic-flower.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-grass.svg b/public/icons/flavor/ic-grass.svg new file mode 100644 index 00000000..184baebd --- /dev/null +++ b/public/icons/flavor/ic-grass.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/flavor/ic-leather.svg b/public/icons/flavor/ic-leather.svg new file mode 100644 index 00000000..66d27365 --- /dev/null +++ b/public/icons/flavor/ic-leather.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/flavor/ic-mineral.svg b/public/icons/flavor/ic-mineral.svg new file mode 100644 index 00000000..405c7d76 --- /dev/null +++ b/public/icons/flavor/ic-mineral.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/flavor/ic-oak.svg b/public/icons/flavor/ic-oak.svg new file mode 100644 index 00000000..d6cad513 --- /dev/null +++ b/public/icons/flavor/ic-oak.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-peach.svg b/public/icons/flavor/ic-peach.svg new file mode 100644 index 00000000..f772eacc --- /dev/null +++ b/public/icons/flavor/ic-peach.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-pepper.svg b/public/icons/flavor/ic-pepper.svg new file mode 100644 index 00000000..52528366 --- /dev/null +++ b/public/icons/flavor/ic-pepper.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/icons/flavor/ic-spice.svg b/public/icons/flavor/ic-spice.svg new file mode 100644 index 00000000..294f90eb --- /dev/null +++ b/public/icons/flavor/ic-spice.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/icons/flavor/ic-tobacco.svg b/public/icons/flavor/ic-tobacco.svg new file mode 100644 index 00000000..b709263d --- /dev/null +++ b/public/icons/flavor/ic-tobacco.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/flavor/ic-tropical.svg b/public/icons/flavor/ic-tropical.svg new file mode 100644 index 00000000..deabe361 --- /dev/null +++ b/public/icons/flavor/ic-tropical.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/flavor/ic-vanilla.svg b/public/icons/flavor/ic-vanilla.svg new file mode 100644 index 00000000..c89eab79 --- /dev/null +++ b/public/icons/flavor/ic-vanilla.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/icons/ic-alert.svg b/public/icons/ic-alert.svg new file mode 100644 index 00000000..34c1baf3 --- /dev/null +++ b/public/icons/ic-alert.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/ic-arrow-down.svg b/public/icons/ic-arrow-down.svg new file mode 100644 index 00000000..b15c83fe --- /dev/null +++ b/public/icons/ic-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-arrow-left.svg b/public/icons/ic-arrow-left.svg new file mode 100644 index 00000000..76cea3b7 --- /dev/null +++ b/public/icons/ic-arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-arrow-right.svg b/public/icons/ic-arrow-right.svg new file mode 100644 index 00000000..27b8b452 --- /dev/null +++ b/public/icons/ic-arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-arrow-up.svg b/public/icons/ic-arrow-up.svg new file mode 100644 index 00000000..67145529 --- /dev/null +++ b/public/icons/ic-arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-camera.svg b/public/icons/ic-camera.svg new file mode 100644 index 00000000..41637cea --- /dev/null +++ b/public/icons/ic-camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-filter.svg b/public/icons/ic-filter.svg new file mode 100644 index 00000000..e5e74077 --- /dev/null +++ b/public/icons/ic-filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-hamburger.svg b/public/icons/ic-hamburger.svg new file mode 100644 index 00000000..305d7a3d --- /dev/null +++ b/public/icons/ic-hamburger.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/ic-like-off.svg b/public/icons/ic-like-off.svg new file mode 100644 index 00000000..fcf86e29 --- /dev/null +++ b/public/icons/ic-like-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-like-on.svg b/public/icons/ic-like-on.svg new file mode 100644 index 00000000..d04e86dc --- /dev/null +++ b/public/icons/ic-like-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-profile.svg b/public/icons/ic-profile.svg new file mode 100644 index 00000000..c928c29a --- /dev/null +++ b/public/icons/ic-profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/ic-search.svg b/public/icons/ic-search.svg new file mode 100644 index 00000000..013328af --- /dev/null +++ b/public/icons/ic-search.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-sns-google.svg b/public/icons/ic-sns-google.svg new file mode 100644 index 00000000..ce852ca8 --- /dev/null +++ b/public/icons/ic-sns-google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/icons/ic-sns-kakao.svg b/public/icons/ic-sns-kakao.svg new file mode 100644 index 00000000..5c28d52b --- /dev/null +++ b/public/icons/ic-sns-kakao.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-star.svg b/public/icons/ic-star.svg new file mode 100644 index 00000000..905e46cf --- /dev/null +++ b/public/icons/ic-star.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/ic-wine.svg b/public/icons/ic-wine.svg new file mode 100644 index 00000000..8dfd44e9 --- /dev/null +++ b/public/icons/ic-wine.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/icons/ic-x.svg b/public/icons/ic-x.svg new file mode 100644 index 00000000..d4d2c8cd --- /dev/null +++ b/public/icons/ic-x.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 00000000..90686cbb --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/globals.css b/src/app/globals.css index 41aeb618..b758fb7d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -42,4 +42,12 @@ body { .flex-col-center { @apply flex flex-col items-center justify-center; } + + [class*="text-"] svg path, + [class*="text-"] svg circle, + [class*="text-"] svg rect, + [class*="text-"] svg polygon { + fill: currentColor; + /* stroke: currentColor; */ + } } diff --git a/src/components/icon/Icon.stories.tsx b/src/components/icon/Icon.stories.tsx new file mode 100644 index 00000000..52806607 --- /dev/null +++ b/src/components/icon/Icon.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import Icon from "./Icon"; +import ICON_MAP from "./icon-map"; + +const meta = { + title: "Components/Icon", + component: Icon, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + icon: { + control: "select", + options: Object.keys(ICON_MAP), + description: "표시할 아이콘 이름", + }, + size: { + control: "select", + options: ["xs", "sm", "md", "lg", "xl", "2xl"], + description: "아이콘 크기", + }, + color: { + control: "select", + options: [ + "default", + "primary", + "gray800", + "gray600", + "gray300", + "gray100", + "danger100", + "danger200", + "danger300", + "danger400", + "white", + ], + description: "아이콘 색상", + }, + className: { + control: "text", + description: "추가 CSS 클래스", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + icon: "AppleIcon", + }, +}; + +export const IconChart: Story = { + args: { + icon: "AppleIcon", + }, + render: () => { + const iconNames = Object.keys(ICON_MAP); + + return ( +
+

아이콘 차트

+
+ {iconNames.map((iconName) => ( +
+ + + {iconName} + +
+ ))} +
+
+ ); + }, + parameters: { + layout: "fullscreen", + docs: { + description: { + story: "아이콘 차트", + }, + }, + }, +}; diff --git a/src/components/icon/Icon.tsx b/src/components/icon/Icon.tsx new file mode 100644 index 00000000..3b33a249 --- /dev/null +++ b/src/components/icon/Icon.tsx @@ -0,0 +1,54 @@ +import { Suspense, lazy, useMemo, ComponentProps } from "react"; +import { cn } from "@/lib/utils"; +import { VariantProps, cva } from "class-variance-authority"; +import ICON_MAP from "./icon-map"; + +const iconVariants = cva("inline-block", { + variants: { + color: { + default: "", + primary: "text-black", + gray800: "text-gray800", + gray600: "text-gray600", + gray300: "text-gray300", + gray100: "text-gray100", + danger100: "text-red100", + danger200: "text-red200", + danger300: "text-red300", + danger400: "text-red400", + white: "text-white", + }, + size: { + xs: "ic-xs", + sm: "ic-sm", + md: "ic-md", + lg: "ic-lg", + xl: "ic-xl", + "2xl": "ic-2xl", + }, + }, + defaultVariants: { + color: "default", + size: "md", + }, +}); + +type IconPropsType = { + icon: keyof typeof ICON_MAP; +} & VariantProps & + ComponentProps<"span">; + +const Icon = ({ icon, color, size, className, ...props }: IconPropsType) => { + const IconComponent = useMemo(() => lazy(ICON_MAP[icon]), [icon]); + const mergeClassName = cn(iconVariants({ color, size }), className); + + return ( + + + + + + ); +}; + +export default Icon; diff --git a/src/components/icon/icon-map.ts b/src/components/icon/icon-map.ts new file mode 100644 index 00000000..6c3b4584 --- /dev/null +++ b/src/components/icon/icon-map.ts @@ -0,0 +1,42 @@ +const ICON_MAP = { + AlertIcon: () => import("/public/icons/ic-alert.svg"), + ArrowDownIcon: () => import("/public/icons/ic-arrow-down.svg"), + ArrowLeftIcon: () => import("/public/icons/ic-arrow-left.svg"), + ArrowRightIcon: () => import("/public/icons/ic-arrow-right.svg"), + ArrowUpIcon: () => import("/public/icons/ic-arrow-up.svg"), + CameraIcon: () => import("/public/icons/ic-camera.svg"), + FilterIcon: () => import("/public/icons/ic-filter.svg"), + HamburgerIcon: () => import("/public/icons/ic-hamburger.svg"), + LikeOffIcon: () => import("/public/icons/ic-like-off.svg"), + LikeOnIcon: () => import("/public/icons/ic-like-on.svg"), + ProfileIcon: () => import("/public/icons/ic-profile.svg"), + SearchIcon: () => import("/public/icons/ic-search.svg"), + GoogleIcon: () => import("/public/icons/ic-sns-google.svg"), + KakaoIcon: () => import("/public/icons/ic-sns-kakao.svg"), + StarIcon: () => import("/public/icons/ic-star.svg"), + WineIcon: () => import("/public/icons/ic-wine.svg"), + XIcon: () => import("/public/icons/ic-x.svg"), + + // flavor + AppleIcon: () => import("/public/icons/flavor/ic-apple.svg"), + BakingIcon: () => import("/public/icons/flavor/ic-baking.svg"), + BerryIcon: () => import("/public/icons/flavor/ic-berry.svg"), + CaramelIcon: () => import("/public/icons/flavor/ic-caramel.svg"), + CherryIcon: () => import("/public/icons/flavor/ic-cherry.svg"), + ChocolateIcon: () => import("/public/icons/flavor/ic-chocolate.svg"), + CitrusIcon: () => import("/public/icons/flavor/ic-citrus.svg"), + EarthIcon: () => import("/public/icons/flavor/ic-earth.svg"), + FlowerIcon: () => import("/public/icons/flavor/ic-flower.svg"), + GrassIcon: () => import("/public/icons/flavor/ic-grass.svg"), + LeatherIcon: () => import("/public/icons/flavor/ic-leather.svg"), + MineralIcon: () => import("/public/icons/flavor/ic-mineral.svg"), + OakIcon: () => import("/public/icons/flavor/ic-oak.svg"), + PeachIcon: () => import("/public/icons/flavor/ic-peach.svg"), + PepperIcon: () => import("/public/icons/flavor/ic-pepper.svg"), + SpiceIcon: () => import("/public/icons/flavor/ic-spice.svg"), + TobaccoIcon: () => import("/public/icons/flavor/ic-tobacco.svg"), + TropicalIcon: () => import("/public/icons/flavor/ic-tropical.svg"), + VanillaIcon: () => import("/public/icons/flavor/ic-vanilla.svg"), +}; + +export default ICON_MAP; diff --git a/src/components/index.ts b/src/components/index.ts index 3cffee1d..fbe8e787 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,3 +2,4 @@ export { default as Gnb } from "./gnb/Gnb"; export { default as Header } from "./header/Header"; export { TextInput, ModalTextInput } from "./text-input/text-input"; export { default as SelectType } from "./select-type/select-type"; +export { default as Icon } from "./icon/Icon"; diff --git a/src/components/select-type/select-type.tsx b/src/components/select-type/select-type.tsx index 1be83f09..2dcbb910 100644 --- a/src/components/select-type/select-type.tsx +++ b/src/components/select-type/select-type.tsx @@ -68,7 +68,7 @@ const SelectType = ({ isError, ...props }: SelectTypeValue) => { return (
-

타입

+

타입

{isError && (

와인 타입은 필수 입력이에요 diff --git a/src/components/taste/Taste.tsx b/src/components/taste/Taste.tsx index 02a68982..b9642dac 100644 --- a/src/components/taste/Taste.tsx +++ b/src/components/taste/Taste.tsx @@ -23,7 +23,7 @@ const Taste = ({ type, data, taste, onChange }: TasteProps) => { {/* 왼쪽: type */}

{ "px-4 py-3", "rounded border border-gray-300", "text-[14px] leading-5 tracking-[0.02em] text-default", - "placeholder:text-body-sm placeholder:font-normal placeholder:text-tertiary", + "placeholder:text-tertiary placeholder:text-body-sm placeholder:font-normal", "focus:outline-none", "pc:w-[400px] pc:text-[16px] pc:leading-6 pc:placeholder:text-body-md pc:placeholder:font-normal", - errorMsg && "border-2 border-danger" + errorMsg && "border-danger border-2" )} type="text" placeholder={placeholder ? placeholder : "내용을 입력해주세요"} @@ -54,7 +54,7 @@ const TextInput = ({ title, placeholder, errorMsg }: TextInputValue) => {

- {errorMsg &&

{errorMsg}

} + {errorMsg &&

{errorMsg}

} ); }; @@ -73,7 +73,7 @@ const ModalTextInput = ({ title, placeholder, errorMsg }: TextInputValue) => {

{title ? title : "제목"}

- {errorMsg &&

{errorMsg}

} + {errorMsg &&

{errorMsg}

}
diff --git a/svgr.config.js b/svgr.config.js new file mode 100644 index 00000000..de253ab4 --- /dev/null +++ b/svgr.config.js @@ -0,0 +1,18 @@ +module.exports = { + icon: true, + svgoConfig: { + plugins: [ + { + name: "removeViewBox", + active: false, + }, + { + name: "removeDimensions", + active: true, + }, + ], + }, + svgProps: { + fill: "currentColor", + }, +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 2a170928..ace6cba2 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from "tailwindcss"; +import plugin from "tailwindcss/plugin"; export default { content: [ @@ -6,6 +7,7 @@ export default { "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], + safelist: ["ic-xs", "ic-sm", "ic-md", "ic-lg", "ic-xl", "ic-2xl"], theme: { extend: { colors: { @@ -65,7 +67,20 @@ export default { "button-lg": ["16px", { lineHeight: "20px", fontWeight: "700" }], "button-md": ["14px", { lineHeight: "18px", fontWeight: "700" }], }, + fill: ({ theme }) => theme("colors"), + stroke: ({ theme }) => theme("colors"), }, }, - plugins: [], + plugins: [ + plugin(function ({ addUtilities }) { + addUtilities({ + ".ic-xs": { width: "16px", height: "16px" }, + ".ic-sm": { width: "20px", height: "20px" }, + ".ic-md": { width: "24px", height: "24px" }, + ".ic-lg": { width: "32px", height: "32px" }, + ".ic-xl": { width: "40px", height: "40px" }, + ".ic-2xl": { width: "48px", height: "48px" }, + }); + }), + ], } satisfies Config; diff --git a/tsconfig.json b/tsconfig.json index c1334095..59a85bd3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,12 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "global.d.ts", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], "exclude": ["node_modules"] }