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
64 changes: 1 addition & 63 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,63 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

echo "๐Ÿ”Ž pre-commit: lint-staged ์‹คํ–‰ ๋ฐ ์ตœ์ข… ์ ๊ฒ€ ์‹œ์ž‘"

# 0) ์ปค๋ฐ‹์— ํฌํ•จ๋œ ํŒŒ์ผ ๋ชฉ๋ก (์ถ”๊ฐ€/๋ณต์‚ฌ/์ˆ˜์ •)
STAGED_FILES="$(git diff --name-only --cached --diff-filter=ACMR)"
if [ -z "$STAGED_FILES" ]; then
echo "โ„น๏ธ ์Šคํ…Œ์ด์ง•๋œ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค."
exit 0
fi

# 1) ๋ณ€๊ฒฝ ํŒŒ์ผ๋งŒ ์ž๋™ ์ˆ˜์ • (eslint --fix, prettier --write)
if ! pnpm lint-staged; then
echo
echo "โŒ lint-staged ๋‹จ๊ณ„์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
echo " - ์ž๋™์ˆ˜์ • ๋ถˆ๊ฐ€ํ•œ ๋ฆฐํŠธ ์—๋Ÿฌ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์–ด์š”."
echo " - ๋กœ์ปฌ์—์„œ 'pnpm lint'๋กœ ์—๋Ÿฌ๋ฅผ ํ™•์ธํ•˜๊ณ  ์ˆ˜์ •ํ•œ ๋’ค ๋‹ค์‹œ ์ปค๋ฐ‹ํ•˜์„ธ์š”."
exit 1
fi

# 2) ์ตœ์ข… ์ ๊ฒ€: ์Šคํ…Œ์ด์ง•๋œ ํŒŒ์ผ๋งŒ ๋Œ€์ƒ์œผ๋กœ ๊ฒ€์‚ฌ(์ˆ˜์ • ์—†์ด ์‹คํŒจ๋งŒ ๊ฐ์ง€)
PRETTIER_FAIL=0
ESLINT_FAIL=0

# ๊ฐœํ–‰๋งŒ ๊ตฌ๋ถ„์ž๋กœ ์‚ฌ์šฉ (๊ณต๋ฐฑ ํฌํ•จ ํŒŒ์ผ๋ช… ์•ˆ์ „)
IFS="$(printf '\n')"
for f in $STAGED_FILES; do
case "$f" in
*.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
# Prettier ํฌ๋งท ์ค€์ˆ˜ ํ™•์ธ (์ˆ˜์ • ์—†์ด ๊ฒ€์‚ฌ)
pnpm -s prettier --check "$f" || PRETTIER_FAIL=1
;;
esac
done

for f in $STAGED_FILES; do
case "$f" in
*.js|*.jsx|*.ts|*.tsx)
# ESLint ์ตœ์ข… ํ™•์ธ (์ˆ˜์ • ์—†์ด ๊ฒ€์‚ฌ)
pnpm -s eslint --max-warnings=0 "$f" || ESLINT_FAIL=1
;;
esac
done
unset IFS

if [ "$PRETTIER_FAIL" -ne 0 ]; then
echo
echo "โŒ Prettier ํฌ๋งท ์œ„๋ฐ˜์ด ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค."
echo " - 'pnpm format' ํ›„ ๋‹ค์‹œ ์ปค๋ฐ‹ํ•˜์„ธ์š”."
exit 1
fi

if [ "$ESLINT_FAIL" -ne 0 ]; then
echo
echo "โŒ ESLint ์œ„๋ฐ˜์ด ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค."
echo " - 'pnpm lint:fix' ๋˜๋Š” ์ˆ˜๋™ ์ˆ˜์ • ํ›„ ๋‹ค์‹œ ์ปค๋ฐ‹ํ•˜์„ธ์š”."
exit 1
fi

COUNT="$(printf '%s\n' "$STAGED_FILES" | wc -l | tr -d ' ')"
echo "โœ… pre-commit ํ†ต๊ณผ: ${COUNT}๊ฐœ ํŒŒ์ผ ์ ๊ฒ€ ์™„๋ฃŒ"
exit 0
pnpm lint-staged
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"eslint --fix --max-warnings=0",
"prettier --write"
],
"*.{json,css,md}": [
Expand All @@ -24,6 +24,8 @@
},
"dependencies": {
"@fontsource/pretendard": "^5.2.5",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0"
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 26 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Icon } from "@/shared/ui/Icon";
import { ICON_KEYS } from "@/shared/ui/Icon.registry";
import clsx from "clsx";

export default function Home() {
// ๋ฐ๋ชจ์šฉ ํ”Œ๋ž˜๊ทธ (์ƒํ™ฉ์— ๋”ฐ๋ผ ํ† ๊ธ€)
const compact = false; // true๋ฉด ์—ฌ๋ฐฑ/๊ทธ๋ฆฌ๋“œ ์กฐ๊ธˆ ๋” ์ด˜์ด˜ํžˆ

return (
<main className="p-8 space-y-12">
<main className={clsx("p-8 space-y-12", compact && "p-6 space-y-8")}>
{/* === 0. ํŽ˜์ด์ง€ ์•ˆ๋‚ด === */}
<header className="space-y-2">
<h1 className="t-32-b">globals.css ์œ ํ‹ธ ์ข…ํ•ฉ ํ…Œ์ŠคํŠธ</h1>
Expand Down Expand Up @@ -270,6 +277,24 @@ export default function Home() {
<p className="t-14-m">ํฐ ์นด๋“œ ์œ„์˜ ๋Œ€๋น„ ์ฒดํฌ</p>
</div>
</section>

{/* === ์•„์ด์ฝ˜ ์‡ผ์ผ€์ด์Šค === */}
<h1 className="mb-6 text-2xl font-bold">๐Ÿ“ฆ Icon Showcase</h1>
<div
className={clsx(
"grid gap-6",
compact
? "grid-cols-3 sm:grid-cols-4 md:grid-cols-5"
: "grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8",
)}
>
{ICON_KEYS.map((name) => (
<div key={name} className="flex flex-col items-center gap-2">
<Icon name={name} size={28} />
<span className="text-xs text-gray-600">{name}</span>
</div>
))}
</div>
</main>
);
}
80 changes: 80 additions & 0 deletions src/shared/ui/Icon.registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
ArrowLeft,
ArrowRight,
Bell,
BellRing,
Calendar,
CalendarDays,
CalendarRange,
Check,
CheckSquare,
Clock,
Eye,
EyeOff,
Grid3X3,
HelpCircle,
Info,
Laptop,
Layout,
Mail,
Maximize,
Menu,
Minus,
Monitor,
Move,
Plus,
RotateCcw,
Search,
Settings,
Smartphone,
StickyNote,
UserPlus,
Users,
X,
} from "lucide-react";

/**
* ์•„์ด์ฝ˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ
* - ํ•„์š”ํ•œ Lucide ์•„์ด์ฝ˜๋งŒ import ํ›„ ๊ฐ์ฒด๋กœ ๋งคํ•‘
* - key๋Š” ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•  ์ด๋ฆ„ (camelCase)
*/
export const icons = {
eye: Eye,
eyeOff: EyeOff,
check: Check,
x: X,
mail: Mail,
arrowLeft: ArrowLeft,
arrowRight: ArrowRight,
search: Search,
userPlus: UserPlus,
monitor: Monitor,
laptop: Laptop,
smartphone: Smartphone,
calendar: Calendar,
calendarDays: CalendarDays,
calendarRange: CalendarRange,
checkSquare: CheckSquare,
stickyNote: StickyNote,
rotateCcw: RotateCcw,
bell: Bell,
clock: Clock,
users: Users,
bellRing: BellRing,
settings: Settings,
menu: Menu,
info: Info,
helpCircle: HelpCircle,
minus: Minus,
plus: Plus,
grid3X3: Grid3X3,
layout: Layout,
move: Move,
maximize: Maximize,
} as const;

/** ์•„์ด์ฝ˜ ์ด๋ฆ„ ํƒ€์ž… */
export type IconName = keyof typeof icons;

/** ์•„์ด์ฝ˜ key ๋ฆฌ์ŠคํŠธ (์˜ˆ: map ๋ Œ๋”๋ง์— ํ™œ์šฉ) */
export const ICON_KEYS = Object.keys(icons) as IconName[];
18 changes: 18 additions & 0 deletions src/shared/ui/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { IconProps } from "@/types/icon";
import clsx from "clsx";
import { icons } from "./Icon.registry";

export function Icon({ name, size = 24, strokeWidth = 2, label, className, ...rest }: IconProps) {
Comment on lines +1 to +5

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Mark Icon wrapper as a client component

The shared Icon component is meant to be a reusable wrapper around lucide icons, but the file does not start with 'use client'. Next.js therefore treats this module as a server component, and any interactive component that tries to import it will fail to compile with โ€œYouโ€™re importing a component that needs to be rendered on the serverโ€ฆโ€. Since icons are commonly needed inside client components (buttons, inputs, etc.), this component should be marked as a client component so it can be imported safely from both server and client modules.

Useful? React with ๐Ÿ‘ย / ๐Ÿ‘Ž.

const Cmp = icons[name];
return (
<Cmp
size={size}
strokeWidth={strokeWidth}
color="currentColor"
className={clsx("text-gray-900", className)}
aria-label={label}
aria-hidden={label ? undefined : true}
{...rest}
/>
);
}
23 changes: 23 additions & 0 deletions src/types/icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { IconName } from "@/shared/ui/Icon.registry";
import type { Calendar } from "lucide-react";
import type { ComponentProps } from "react";

/** Lucide ์•„์ด์ฝ˜ ๊ณตํ†ต props */
type LucideBaseProps = ComponentProps<typeof Calendar>;

/**
* ๊ณตํ†ต Icon ์ปดํฌ๋„ŒํŠธ Props
* - ์ƒ‰์ƒ์€ Tailwind text-* ํด๋ž˜์Šค๋กœ ์ œ์–ด
* - color prop์€ ์ œ์™ธํ•˜๊ณ  ํ•ญ์ƒ currentColor ์‚ฌ์šฉ
*/
export type IconProps = {
name: IconName;
/** ์Šคํฌ๋ฆฐ๋ฆฌ๋”์šฉ ๋ผ๋ฒจ (์‹œ๊ฐ์  title ์•„๋‹˜) */
label?: string;
/** ์•„์ด์ฝ˜ ํฌ๊ธฐ (๊ธฐ๋ณธ 24) */
size?: number;
/** ์„  ๊ตต๊ธฐ (๊ธฐ๋ณธ 2) */
strokeWidth?: number;
/** Tailwind ํด๋ž˜์Šค์™€ ๊ฒฐํ•ฉ ๊ฐ€๋Šฅ */
className?: string;
} & Omit<LucideBaseProps, "ref" | "color">;