# Cursor Rules for ERP-node Project
## ๐จ ์ต์ฐ์ ๋ณด์ ๊ท์น: ๋ฉํฐํ
๋์
**๋ชจ๋ ์ฝ๋ ์์ฑ/์์ ์๋ฃ ํ ๋ฐ๋์ ๋ค์ ํ์ผ์ ํ์ธํ์ธ์:**
- [๋ฉํฐํ
๋์ ํ์ ๊ตฌํ ๊ฐ์ด๋](.cursor/rules/multi-tenancy-guide.mdc)
**AI ์์ด์ ํธ๋ ๋ค์ ์ํฉ์์ ๋ฐ๋์ ๋ฉํฐํ
๋์ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํ์ธํด์ผ ํฉ๋๋ค:**
1. ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
์์ฑ ์
2. ๋ฐฑ์๋ API (SELECT/INSERT/UPDATE/DELETE) ์์ฑ/์์ ์
3. ํ๋ก ํธ์๋ ๋ฐ์ดํฐ API ํธ์ถ ์์ฑ/์์ ์
4. ํ
์คํธ ์๋ฃ ์
**ํต์ฌ ์์น:**
- โ
๋ชจ๋ ํ
์ด๋ธ์ `company_code` ํ์ (company_mng ์ ์ธ)
- โ
๋ชจ๋ ์ฟผ๋ฆฌ์ `company_code` ํํฐ๋ง ํ์
- โ
ํ๋ก ํธ์๋ API ํธ์ถ ์ `autoFilter` ์ ๋ฌ ํ์
---
## shadcn/ui ์น ์คํ์ผ ๊ฐ์ด๋๋ผ์ธ
๋ชจ๋ ํ๋ก ํธ์๋ ๊ฐ๋ฐ ์ ๋ค์ shadcn/ui ๊ธฐ๋ฐ ์คํ์ผ ๊ฐ์ด๋๋ผ์ธ์ ์ค์ํด์ผ ํฉ๋๋ค.
### 1. Color System (์์ ์์คํ
)
#### CSS Variables ์ฌ์ฉ
shadcn์ CSS Variables๋ฅผ ์ฌ์ฉํ์ฌ ํ
๋ง๋ฅผ ๊ด๋ฆฌํ๋ฉฐ, ๋ชจ๋ ์์์ HSL ํ์์ผ๋ก ์ ์๋ฉ๋๋ค.
**๊ธฐ๋ณธ ์์ ํ ํฐ (ํญ์ ์ฌ์ฉ):**
- `--background`: ํ์ด์ง ๋ฐฐ๊ฒฝ
- `--foreground`: ๊ธฐ๋ณธ ํ
์คํธ
- `--primary`: ๋ฉ์ธ ์ก์
(Indigo ๊ณ์ด)
- `--primary-foreground`: Primary ์ ํ
์คํธ
- `--secondary`: ๋ณด์กฐ ์ก์
- `--muted`: ์ฝํ ๋ฐฐ๊ฒฝ
- `--muted-foreground`: ๋ณด์กฐ ํ
์คํธ
- `--destructive`: ์ญ์ /์๋ฌ (Rose ๊ณ์ด)
- `--border`: ํ
๋๋ฆฌ
- `--ring`: ํฌ์ปค์ค ๋ง
**Tailwind ํด๋์ค๋ก ์ฌ์ฉ:**
```tsx
bg-primary text-primary-foreground
bg-secondary text-secondary-foreground
bg-muted text-muted-foreground
bg-destructive text-destructive-foreground
border-border
```
**์ถ๊ฐ ์๋งจํฑ ์ปฌ๋ฌ:**
- Success: `--success` (Emerald-600 ๊ณ์ด)
- Warning: `--warning` (Amber-500 ๊ณ์ด)
- Info: `--info` (Cyan-500 ๊ณ์ด)
### 2. Spacing System (๊ฐ๊ฒฉ)
**Tailwind Scale (4px ๊ธฐ์ค):**
- 0.5 = 2px, 1 = 4px, 2 = 8px, 3 = 12px, 4 = 16px, 6 = 24px, 8 = 32px
**์ปดํฌ๋ํธ๋ณ ๊ถ์ฅ ๊ฐ๊ฒฉ:**
- ์นด๋ ํจ๋ฉ: `p-6` (24px)
- ์นด๋ ๊ฐ ๋ง์ง: `gap-6` (24px)
- ํผ ํ๋ ๊ฐ๊ฒฉ: `space-y-4` (16px)
- ๋ฒํผ ๋ด๋ถ ํจ๋ฉ: `px-4 py-2`
- ์น์
๊ฐ๊ฒฉ: `space-y-8` ๋๋ `space-y-12`
### 3. Typography (ํ์ดํฌ๊ทธ๋ํผ)
**์ฉ๋๋ณ ํ์ดํฌ๊ทธ๋ํผ (ํ์):**
- ํ์ด์ง ์ ๋ชฉ: `text-3xl font-bold`
- ์น์
์ ๋ชฉ: `text-2xl font-semibold`
- ์นด๋ ์ ๋ชฉ: `text-xl font-semibold`
- ์๋ธ ์ ๋ชฉ: `text-lg font-medium`
- ๋ณธ๋ฌธ ํ
์คํธ: `text-sm text-muted-foreground`
- ์์ ํ
์คํธ: `text-xs text-muted-foreground`
- ๋ฒํผ ํ
์คํธ: `text-sm font-medium`
- ํผ ๋ผ๋ฒจ: `text-sm font-medium`
### 4. Button Variants (๋ฒํผ ์คํ์ผ)
**ํ์ ์ฌ์ฉ ํจํด:**
```tsx
// Primary (๊ธฐ๋ณธ)
// Secondary
// Outline
// Ghost
// Destructive
```
**๋ฒํผ ํฌ๊ธฐ:**
- `size="sm"`: ์์ ๋ฒํผ (h-9 px-3)
- `size="default"`: ๊ธฐ๋ณธ ๋ฒํผ (h-10 px-4 py-2)
- `size="lg"`: ํฐ ๋ฒํผ (h-11 px-8)
- `size="icon"`: ์์ด์ฝ ์ ์ฉ (h-10 w-10)
### 5. Input States (์
๋ ฅ ํ๋ ์ํ)
**ํ์ ์ ์ฉ ์ํ:**
```tsx
// Default
className="border-input"
// Focus (๋ชจ๋ ์
๋ ฅ ํ๋ ํ์)
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
// Error
className="border-destructive focus-visible:ring-destructive"
// Disabled
className="disabled:opacity-50 disabled:cursor-not-allowed"
```
### 6. Card Structure (์นด๋ ๊ตฌ์กฐ)
**ํ์ค ์นด๋ ๊ตฌ์กฐ (ํ์):**
```tsx
์ ๋ชฉ
์ค๋ช
{/* ๋ด์ฉ */}
{/* ์ก์
๋ฒํผ๋ค */}
```
### 7. Border & Radius (ํ
๋๋ฆฌ ๋ฐ ๋ฅ๊ทผ ๋ชจ์๋ฆฌ)
**์ปดํฌ๋ํธ๋ณ ๊ถ์ฅ:**
- ๋ฒํผ: `rounded-md` (6px)
- ์
๋ ฅ ํ๋: `rounded-md` (6px)
- ์นด๋: `rounded-lg` (8px)
- ๋ฐฐ์ง: `rounded-full`
- ๋ชจ๋ฌ/๋ํ์์: `rounded-lg` (8px)
- ๋๋กญ๋ค์ด: `rounded-md` (6px)
### 8. Shadow (๊ทธ๋ฆผ์)
**์ฉ๋๋ณ ๊ถ์ฅ:**
- ์นด๋: `shadow-sm`
- ๋๋กญ๋ค์ด: `shadow-md`
- ๋ชจ๋ฌ: `shadow-lg`
- ํ์ค๋ฒ: `shadow-md`
- ๋ฒํผ ํธ๋ฒ: `shadow-sm`
### 9. Interactive States (์ํธ์์ฉ ์ํ)
**ํ์ ์ ์ฉ ํจํด:**
```tsx
// Hover
hover:bg-primary/90 // ๋ฒํผ
hover:bg-accent // Ghost ๋ฒํผ
hover:underline // ๋งํฌ
hover:shadow-md transition-shadow // ์นด๋
// Focus (๋ชจ๋ ์ธํฐ๋ํฐ๋ธ ์์ ํ์)
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
// Active
active:scale-95 transition-transform // ๋ฒํผ
// Disabled
disabled:opacity-50 disabled:cursor-not-allowed
```
### 10. Animation (์ ๋๋ฉ์ด์
)
**๊ถ์ฅ Duration:**
- ๋น ๋ฅธ ํผ๋๋ฐฑ: `duration-75`
- ๊ธฐ๋ณธ: `duration-150`
- ๋ถ๋๋ฌ์ด ์ ํ: `duration-300`
**๊ถ์ฅ ํจํด:**
- ๋ฒํผ ํด๋ฆญ: `transition-transform duration-150 active:scale-95`
- ์์ ์ ํ: `transition-colors duration-150`
- ๋๋กญ๋ค์ด ์ด๊ธฐ: `transition-all duration-200`
### 11. Responsive (๋ฐ์ํ)
**Breakpoints:**
- `sm`: 640px (๋ชจ๋ฐ์ผ ๊ฐ๋ก)
- `md`: 768px (ํ๋ธ๋ฆฟ)
- `lg`: 1024px (๋
ธํธ๋ถ)
- `xl`: 1280px (๋ฐ์คํฌํฑ)
**๋ฐ์ํ ํจํด:**
```tsx
// ๋ชจ๋ฐ์ผ ์ฐ์ ์ ๊ทผ
className="flex-col md:flex-row"
className="grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
className="text-2xl md:text-3xl lg:text-4xl"
className="p-4 md:p-6 lg:p-8"
```
### 12. Accessibility (์ ๊ทผ์ฑ)
**ํ์ ์ ์ฉ ์ฌํญ:**
1. ํฌ์ปค์ค ํ์: ๋ชจ๋ ์ธํฐ๋ํฐ๋ธ ์์์ `focus-visible:ring-2` ์ ์ฉ
2. ARIA ๋ ์ด๋ธ: ์ ์ ํ `aria-label`, `aria-describedby` ์ฌ์ฉ
3. ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
: Tab, Enter, Space, Esc ์ง์
4. ์์ ๋๋น: ์ต์ 4.5:1 (์ผ๋ฐ ํ
์คํธ), 3:1 (ํฐ ํ
์คํธ)
### 13. Class ์์ (์ผ๊ด์ฑ ์ ์ง)
**ํญ์ ์ด ์์๋ก ์์ฑ:**
1. Layout: `flex`, `grid`, `block`
2. Sizing: `w-full`, `h-10`
3. Spacing: `p-4`, `m-2`, `gap-4`
4. Typography: `text-sm`, `font-medium`
5. Colors: `bg-primary`, `text-white`
6. Border: `border`, `rounded-md`
7. Effects: `shadow-sm`, `opacity-50`
8. States: `hover:`, `focus:`, `disabled:`
9. Responsive: `md:`, `lg:`
### 14. ์ค๋ฌด ์ ์ฉ ๊ท์น
1. **shadcn ์ปดํฌ๋ํธ ์ฐ์ ์ฌ์ฉ**: ์ปค์คํ
์คํ์ผ๋ณด๋ค shadcn ๊ธฐ๋ณธ ์ปดํฌ๋ํธ ํ์ฉ
2. **cn ์ ํธ๋ฆฌํฐ ์ฌ์ฉ**: ์กฐ๊ฑด๋ถ ํด๋์ค๋ `cn()` ํจ์๋ก ๊ฒฐํฉ
3. **ํ
๋ง ๋ณ์ ์ฌ์ฉ**: ํ๋์ฝ๋ฉ๋ ์์ ๋์ CSS ๋ณ์ ์ฌ์ฉ
4. **๋คํฌ๋ชจ๋ ๊ณ ๋ ค**: ๋ชจ๋ ์ปดํฌ๋ํธ๋ ๋คํฌ๋ชจ๋ ํธํ ํ์
5. **์ผ๊ด์ฑ ์ ์ง**: ๊ฐ์ ์ฉ๋์ ์ปดํฌ๋ํธ๋ ๊ฐ์ ์คํ์ผ ์ฌ์ฉ
### 15. ๊ธ์ง ์ฌํญ
1. โ ํ๋์ฝ๋ฉ๋ ์์ ๊ฐ ์ฌ์ฉ (์: `bg-blue-500` ๋์ `bg-primary`)
2. โ ์ธ๋ผ์ธ ์คํ์ผ๋ก ์์ ์ง์ (์: `style={{ color: '#3b82f6' }}`)
3. โ ํฌ์ปค์ค ์คํ์ผ ์ ๊ฑฐ (`outline-none`๋ง ๋จ๋
์ฌ์ฉ)
4. โ ์ ๊ทผ์ฑ ๋ฌด์ (ARIA ๋ ์ด๋ธ ๋๋ฝ)
5. โ ๋ฐ์ํ ๋ฌด์ (๋ฐ์คํฌํฑ ์ ์ฉ ์คํ์ผ)
6. โ **์ค์ฒฉ ๋ฐ์ค ๊ธ์ง**: ์ฌ์ฉ์๊ฐ ๋ช
์์ ์ผ๋ก ์์ฒญํ์ง ์๋ ํ Card ์์ Card, Border ์์ Border ๊ฐ์ ์ค์ฒฉ๋ ์ปจํ
์ด๋ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ง ์์
### 16. ์ค์ฒฉ ๋ฐ์ค ๊ธ์ง ์์ธ ๊ท์น
**๊ธ์ง๋๋ ํจํด (์ฌ์ฉ์ ์์ฒญ ์์ด):**
```tsx
// โ Card ์์ Card
// ์ค์ฒฉ ๊ธ์ง!
๋ด์ฉ
// โ Border ์์ Border
// โ ๋ถํ์ํ ๋ํผ
```
**ํ์ฉ๋๋ ํจํด:**
```tsx
// โ
๋จ์ผ Card
์ ๋ชฉ
๋ด์ฉ
// โ
์๋ฏธ์ ์ผ๋ก ๋ค๋ฅธ ์ปดํฌ๋ํธ ์กฐํฉ
// โ
๊ทธ๋ฆฌ๋/๋ฆฌ์คํธ ๋ด๋ถ์ Card๋ค
ํญ๋ชฉ 1
ํญ๋ชฉ 2
ํญ๋ชฉ 3
```
**์์ธ ์ํฉ (์ฌ์ฉ์๊ฐ ๋ช
์์ ์ผ๋ก ์์ฒญํ ๊ฒฝ์ฐ๋ง):**
- ๋์๋ณด๋์์ ์น์
๋ณ ๊ทธ๋ฃนํ์ด ํ์ํ ๊ฒฝ์ฐ
- ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์๊ฐ์ ์ผ๋ก ๊ตฌ๋ถํด์ผ ํ๋ ๊ฒฝ์ฐ
- ๋๋๊ทธ์ค๋๋กญ ๋ฑ ํน์ ๊ธฐ๋ฅ์ ์ํ ๊ฒฝ์ฐ
**์์น:**
- ์ฌํํ๊ณ ๊น๋ํ ๋์์ธ ์ ์ง
- ๋ถํ์ํ ์๊ฐ์ ๋ ์ด์ด ์ ๊ฑฐ
- ์ฌ์ฉ์๊ฐ ๋ช
์์ ์ผ๋ก "๋ฐ์ค ์์ ๋ฐ์ค", "์ค์ฒฉ๋ ์นด๋" ๋ฑ์ ์์ฒญํ์ง ์์ผ๋ฉด ๋จ์ผ ๋ ๋ฒจ ์ ์ง
### 17. ํ์ค ๋ชจ๋ฌ(Dialog) ๋์์ธ ํจํด
**ํ๋ก์ ํธ ํ์ค ๋ชจ๋ฌ ๊ตฌ์กฐ (ํ๋ก์ฐ ๊ด๋ฆฌ ๊ธฐ์ค):**
```tsx
```
**ํ์ ์ ์ฉ ์ฌํญ:**
1. **๋ฐ์ํ ํฌ๊ธฐ**
- ๋ชจ๋ฐ์ผ: `max-w-[95vw]` (ํ๋ฉด ๋๋น์ 95%)
- ๋ฐ์คํฌํฑ: `sm:max-w-[500px]` (๊ณ ์ 500px)
2. **ํค๋ ๊ตฌ์กฐ**
- DialogTitle: `text-base sm:text-lg` (16px โ 18px)
- DialogDescription: `text-xs sm:text-sm` (12px โ 14px)
- ํญ์ ์ ๋ชฉ๊ณผ ์ค๋ช
๋ชจ๋ ํฌํจ
3. **์ปจํ
์ธ ๊ฐ๊ฒฉ**
- ํ๋ ๊ฐ ๊ฐ๊ฒฉ: `space-y-3 sm:space-y-4` (12px โ 16px)
- ๊ฐ ํ๋๋ `` ๋ก ๊ฐ์ธ๊ธฐ
4. **์
๋ ฅ ํ๋ ํจํด**
- Label: `text-xs sm:text-sm` + ํ์ ํ๋๋ `*` ํ์
- Input/Select: `h-8 text-xs sm:h-10 sm:text-sm` (32px โ 40px)
- ๋์๋ง: `text-muted-foreground mt-1 text-[10px] sm:text-xs`
5. **ํธํฐ ๋ฒํผ**
- ์ปจํ
์ด๋: `gap-2 sm:gap-0` (๋ชจ๋ฐ์ผ์์ ๊ฐ๊ฒฉ, ๋ฐ์คํฌํฑ์์ ์๋)
- ๋ฒํผ: `h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm`
- ๋ชจ๋ฐ์ผ: ๊ฐ์ ํฌ๊ธฐ (`flex-1`)
- ๋ฐ์คํฌํฑ: ์๋ ํฌ๊ธฐ (`flex-none`)
- ์์: ์ทจ์(outline) โ ํ์ธ(default)
6. **์ ๊ทผ์ฑ**
- Label์ `htmlFor`์ Input์ `id` ๋งค์นญ
- Button์ ์ ์ ํ `onClick` ํธ๋ค๋ฌ
- Dialog์ `open`๊ณผ `onOpenChange` ํ์
**ํ์ธ ๋ชจ๋ฌ (๊ฐ๋จํ ๊ฒฝ๊ณ /ํ์ธ):**
```tsx
```
**์์น:**
- ๋ชจ๋ ๋ชจ๋ฌ์ ๋ชจ๋ฐ์ผ ์ฐ์ ๋ฐ์ํ ๋์์ธ
- ์ผ๊ด๋ ํฌ๊ธฐ, ๊ฐ๊ฒฉ, ํฐํธ ํฌ๊ธฐ ์ฌ์ฉ
- ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ ํฌ๊ธฐ๋ฅผ ๋ช
์ํ์ง ์์ผ๋ฉด `sm:max-w-[500px]` ์ฌ์ฉ
- ์ญ์ /์ํํ ์์
์ `variant="destructive"` ์ฌ์ฉ
### 18. ๊ฒ์ ๊ฐ๋ฅํ Select ๋ฐ์ค (Combobox ํจํด)
**์ ์ฉ ์กฐ๊ฑด**: ์ฌ์ฉ์๊ฐ "๊ฒ์ ๊ธฐ๋ฅ์ด ์๋ Select ๋ฐ์ค" ๋๋ "Combobox"๋ฅผ ๋ช
์์ ์ผ๋ก ์์ฒญํ ๊ฒฝ์ฐ๋ง ์ฌ์ฉ
**ํ์ค Combobox ๊ตฌ์กฐ (ํ๋ก์ฐ ๊ด๋ฆฌ ๊ธฐ์ค):**
```tsx
import { Check, ChevronsUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
// ์ํ ๊ด๋ฆฌ
const [open, setOpen] = useState(false);
const [value, setValue] = useState("");
// ๋ ๋๋ง
ํญ๋ชฉ์ ์ฐพ์ ์ ์์ต๋๋ค.
{items.map((item) => (
{
setValue(currentValue === value ? "" : currentValue);
setOpen(false);
}}
className="text-xs sm:text-sm"
>
{item.label}
))}
```
**๋ณต์กํ ๋ฐ์ดํฐ ํ์ (๋ผ๋ฒจ + ์ค๋ช
):**
```tsx
{
setValue(currentValue);
setOpen(false);
}}
className="text-xs sm:text-sm"
>
{item.label}
{item.description && (
{item.description}
)}
```
**ํ์ ์ ์ฉ ์ฌํญ:**
1. **๋ฐ์ํ ํฌ๊ธฐ**
- ๋ฒํผ ๋์ด: `h-8 sm:h-10` (32px โ 40px)
- ํ
์คํธ ํฌ๊ธฐ: `text-xs sm:text-sm` (12px โ 14px)
- PopoverContent ๋๋น: `width: "var(--radix-popover-trigger-width)"` (ํธ๋ฆฌ๊ฑฐ์ ๋์ผ)
2. **ํ์ ์ปดํฌ๋ํธ**
- Popover: ๋๋กญ๋ค์ด ์ปจํ
์ด๋
- Command: ๊ฒ์ ๋ฐ ํํฐ๋ง ๊ธฐ๋ฅ
- CommandInput: ๊ฒ์ ์
๋ ฅ ํ๋
- CommandList: ํญ๋ชฉ ๋ชฉ๋ก ์ปจํ
์ด๋
- CommandEmpty: ๊ฒ์ ๊ฒฐ๊ณผ ์์ ๋ฉ์์ง
- CommandGroup: ํญ๋ชฉ ๊ทธ๋ฃน
- CommandItem: ๊ฐ๋ณ ํญ๋ชฉ
3. **์์ด์ฝ ์ฌ์ฉ**
- ChevronsUpDown: ๋๋กญ๋ค์ด ํ์ ์์ด์ฝ (์ค๋ฅธ์ชฝ)
- Check: ์ ํ๋ ํญ๋ชฉ ํ์ (์ผ์ชฝ)
4. **์ ๊ทผ์ฑ**
- `role="combobox"`: ARIA ์ญํ ๋ช
์
- `aria-expanded={open}`: ์ด๋ฆผ/๋ซํ ์ํ
- PopoverTrigger์ `asChild` ์ฌ์ฉ
5. **๋ก๋ฉ ์ํ**
```tsx
```
**์ผ๋ฐ Select vs Combobox ์ ํ ๊ธฐ์ค:**
| ์ํฉ | ์ปดํฌ๋ํธ | ์ด์ |
|------|----------|------|
| ํญ๋ชฉ 5๊ฐ ์ดํ | `