# 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 ์ œ๋ชฉ ๋‚ด์šฉ // โœ… ์˜๋ฏธ์ ์œผ๋กœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ์กฐํ•ฉ // Dialog๋Š” ๋ณ„๋„ UI ๋ ˆ์ด์–ด ... // โœ… ๊ทธ๋ฆฌ๋“œ/๋ฆฌ์ŠคํŠธ ๋‚ด๋ถ€์˜ 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๊ฐœ ์ดํ•˜ | `` | ๋น ๋ฅธ ์„ ํƒ | **์›์น™:** - ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์š”์ฒญํ•˜์ง€ ์•Š์œผ๋ฉด ์ผ๋ฐ˜ Select ์‚ฌ์šฉ - ๋งŽ์€ ํ•ญ๋ชฉ(10๊ฐœ ์ด์ƒ)์„ ๋‹ค๋ฃฐ ๋•Œ๋Š” Combobox ๊ถŒ์žฅ - ์ผ๊ด€๋œ ๋ฐ˜์‘ํ˜• ํฌ๊ธฐ ์œ ์ง€ - ๊ฒ€์ƒ‰ ํ”Œ๋ ˆ์ด์Šคํ™€๋”๋Š” ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑ ### 19. Form Validation (ํผ ๊ฒ€์ฆ) **์ž…๋ ฅ ํ•„๋“œ ์ƒํƒœ๋ณ„ ์Šคํƒ€์ผ:** ```tsx // Default (๊ธฐ๋ณธ) className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" // Error (์—๋Ÿฌ) className="flex h-10 w-full rounded-md border border-destructive bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive" // Success (์„ฑ๊ณต) className="flex h-10 w-full rounded-md border border-success bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-success" // Disabled (๋น„ํ™œ์„ฑ) className="flex h-10 w-full rounded-md border border-input bg-muted px-3 py-2 text-sm opacity-50 cursor-not-allowed" ``` **Helper Text (๋„์›€๋ง ํ…์ŠคํŠธ):** ```tsx // ๊ธฐ๋ณธ Helper Text

8์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”

// Error Message

์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค

// Success Message

์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค

``` **Form Label (ํผ ๋ผ๋ฒจ):** ```tsx // ๊ธฐ๋ณธ ๋ผ๋ฒจ // ํ•„์ˆ˜ ํ•ญ๋ชฉ ํ‘œ์‹œ ``` **์ „์ฒด ํผ ํ•„๋“œ ๊ตฌ์กฐ:** ```tsx
{error && (

{errorMessage}

)} {!error && helperText && (

{helperText}

)}
``` **์‹ค์‹œ๊ฐ„ ๊ฒ€์ฆ ํ”ผ๋“œ๋ฐฑ:** ```tsx // ๋กœ๋”ฉ ์ค‘ (๊ฒ€์ฆ ์ง„ํ–‰)
// ์„ฑ๊ณต
// ์‹คํŒจ
``` ### 20. Loading States (๋กœ๋”ฉ ์ƒํƒœ) **Spinner (์Šคํ”ผ๋„ˆ) ํฌ๊ธฐ๋ณ„:** ```tsx // Small // Default // Large ``` **Spinner ์ƒ‰์ƒ๋ณ„:** ```tsx // Primary // Muted // White (๋‹คํฌ ๋ฐฐ๊ฒฝ์šฉ) ``` **Button Loading:** ```tsx ``` **Skeleton UI:** ```tsx // ํ…์ŠคํŠธ ์Šค์ผˆ๋ ˆํ†ค
// ์นด๋“œ ์Šค์ผˆ๋ ˆํ†ค
``` **Progress Bar (์ง„ํ–‰๋ฅ ):** ```tsx // ๊ธฐ๋ณธ Progress Bar
// ๋ผ๋ฒจ ํฌํ•จ
์—…๋กœ๋“œ ์ค‘... {progress}%
``` **Full Page Loading:** ```tsx

๋กœ๋”ฉ ์ค‘...

``` ### 21. Empty States (๋นˆ ์ƒํƒœ) **๊ธฐ๋ณธ Empty State:** ```tsx

๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

์•„์ง ์ƒ์„ฑ๋œ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”.

``` **๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ:** ```tsx

๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

"{searchQuery}"์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ฒ€์ƒ‰์–ด๋กœ ์‹œ๋„ํ•ด๋ณด์„ธ์š”.

``` **์—๋Ÿฌ ์ƒํƒœ:** ```tsx

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค

์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.

``` **์•„์ด์ฝ˜ ๊ฐ€์ด๋“œ:** - ๋ฐ์ดํ„ฐ ์—†์Œ: Inbox, Package, FileText - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ: Search, SearchX - ํ•„ํ„ฐ ๊ฒฐ๊ณผ ์—†์Œ: Filter, FilterX - ์—๋Ÿฌ: AlertCircle, XCircle - ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜: WifiOff, CloudOff - ๊ถŒํ•œ ์—†์Œ: Lock, ShieldOff --- ## ์ถ”๊ฐ€ ํ”„๋กœ์ ํŠธ ๊ทœ์น™ - ๋ฐฑ์—”๋“œ ์žฌ์‹คํ–‰ ๊ธˆ์ง€ - ํ•ญ์ƒ ํ•œ๊ธ€๋กœ ๋‹ต๋ณ€ - ์ด๋ชจ์ง€ ์‚ฌ์šฉ ๊ธˆ์ง€ (๋ช…์‹œ์  ์š”์ฒญ ์—†์ด) - ์‹ฌํ”Œํ•˜๊ณ  ๊น”๋”ํ•œ ๋””์ž์ธ ์œ ์ง€ --- ## ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ํ•„์ˆ˜ ๊ทœ์น™ ### ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(SUPER_ADMIN) ๊ฐ€์‹œ์„ฑ ์ œํ•œ **ํ•ต์‹ฌ ์›์น™**: ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž(COMPANY_ADMIN)์™€ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž(USER)๋Š” **์ ˆ๋Œ€๋กœ** ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(company_code = "*")๋ฅผ ๋ณผ ์ˆ˜ ์—†์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. #### ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ ํ•„์ˆ˜์‚ฌํ•ญ ๋ชจ๋“  ์‚ฌ์šฉ์ž ๊ด€๋ จ API์—์„œ ๋‹ค์Œ ํ•„ํ„ฐ๋ง ๋กœ์ง์„ **๋ฐ˜๋“œ์‹œ** ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: ```typescript // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ํ•„ํ„ฐ๋ง (ํ•„์ˆ˜) if (req.user && req.user.companyCode !== "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ, company_code๊ฐ€ "*"์ธ ์‚ฌ์šฉ์ž๋Š” ์ œ์™ธ whereConditions.push(`company_code != '*'`); logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ํ•„ํ„ฐ๋ง ์ ์šฉ", { userCompanyCode: req.user.companyCode }); } ``` **SQL ์ฟผ๋ฆฌ ์˜ˆ์‹œ:** ```sql SELECT * FROM user_info WHERE 1=1 AND company_code != '*' -- ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ œ์™ธ AND company_code = $1 -- ํšŒ์‚ฌ๋ณ„ ํ•„ํ„ฐ๋ง ``` #### ์ ์šฉ ๋Œ€์ƒ API (ํ•„์ˆ˜) ๋‹ค์Œ ์‚ฌ์šฉ์ž ๊ด€๋ จ API์— ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ํ•„ํ„ฐ๋ง์„ **๋ฐ˜๋“œ์‹œ** ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: 1. **์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ** (`GET /api/admin/users`) - ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ํŽ˜์ด์ง€ - ๊ถŒํ•œ ๊ทธ๋ฃน ๋ฉค๋ฒ„ ์„ ํƒ (Dual List Box) - ๊ฒ€์ƒ‰/ํ•„ํ„ฐ ๊ฒฐ๊ณผ 2. **์‚ฌ์šฉ์ž ๊ฒ€์ƒ‰** (`GET /api/admin/users/search`) - ์ž๋™์™„์„ฑ/ํƒ€์ž…์–ดํ—ค๋“œ - ๋“œ๋กญ๋‹ค์šด ์„ ํƒ 3. **๋ถ€์„œ๋ณ„ ์‚ฌ์šฉ์ž ์กฐํšŒ** (`GET /api/admin/users/by-department`) - ๋ถ€์„œ ํ•„ํ„ฐ๋ง ์‹œ 4. **์‚ฌ์šฉ์ž ์ƒ์„ธ ์กฐํšŒ** (`GET /api/admin/users/:userId`) - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž์˜ ์ƒ์„ธ ์ •๋ณด๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ๋ณผ ์ˆ˜ ์žˆ์Œ #### ํ”„๋ก ํŠธ์—”๋“œ ์ถ”๊ฐ€ ๋ณดํ˜ธ (๊ถŒ์žฅ) ๋ฐฑ์—”๋“œ์—์„œ ์ด๋ฏธ ํ•„ํ„ฐ๋ง๋˜์ง€๋งŒ, ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋„ ์ถ”๊ฐ€ ์ฒดํฌ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค: ```typescript // ์ปดํฌ๋„ŒํŠธ์—์„œ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ œ์™ธ const visibleUsers = users.filter(user => { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Œ if (user.companyCode === "*" && !isSuperAdmin) { return false; } return true; }); ``` #### ์˜ˆ์™ธ ์‚ฌํ•ญ - **์ตœ๊ณ  ๊ด€๋ฆฌ์ž(company_code = "*")** ๋Š” ๋ชจ๋“  ์‚ฌ์šฉ์ž(๋‹ค๋ฅธ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ํฌํ•จ)๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋Š” ๋‹ค๋ฅธ ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ๋„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. #### ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž ๊ด€๋ จ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์‹œ ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”: - [ ] `req.user.companyCode !== "*"` ์ฒดํฌ ์ถ”๊ฐ€ - [ ] `company_code != '*'` WHERE ์กฐ๊ฑด ์ถ”๊ฐ€ - [ ] ๋กœ๊น…์œผ๋กœ ํ•„ํ„ฐ๋ง ์ ์šฉ ์—ฌ๋ถ€ ํ™•์ธ - [ ] ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ์ •์ƒ ์ž‘๋™ ํ™•์ธ - [ ] ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ์•ˆ ๋ณด์ด๋Š”์ง€ ํ™•์ธ #### ๊ด€๋ จ ํŒŒ์ผ - `backend-node/src/controllers/adminController.ts` - `getUserList()` ํ•จ์ˆ˜ ์ฐธ๊ณ  - `backend-node/src/middleware/authMiddleware.ts` - ๊ถŒํ•œ ์ฒดํฌ - `frontend/components/admin/UserManagement.tsx` - ์‚ฌ์šฉ์ž ๋ชฉ๋ก UI - `frontend/components/admin/RoleDetailManagement.tsx` - ๋ฉค๋ฒ„ ์„ ํƒ UI #### ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ - ํด๋ผ์ด์–ธํŠธ ์ธก ํ•„ํ„ฐ๋ง๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค (์šฐํšŒ ๊ฐ€๋Šฅ). - ๋ฐ˜๋“œ์‹œ ๋ฐฑ์—”๋“œ SQL ์ฟผ๋ฆฌ์—์„œ ํ•„ํ„ฐ๋งํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - API ์‘๋‹ต์— ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ •๋ณด๊ฐ€ ์ ˆ๋Œ€ ํฌํ•จ๋˜์–ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. - ๋กœ๊ทธ์— ํ•„ํ„ฐ๋ง ์—ฌ๋ถ€๋ฅผ ๊ธฐ๋กํ•˜์—ฌ ๊ฐ์‚ฌ ์ถ”์ ์„ ๋‚จ๊ธฐ์„ธ์š”. --- ## ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ(Multi-Tenancy) ํ•„์ˆ˜ ๊ทœ์น™ ### ํ•ต์‹ฌ ์›์น™ **๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ/์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ ๋กœ์ง์€ ๋ฐ˜๋“œ์‹œ ํšŒ์‚ฌ๋ณ„(company_code)๋กœ ๊ฒฉ๋ฆฌ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.** ์ด ์‹œ์Šคํ…œ์€ ๋ฉ€ํ‹ฐํ…Œ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, ๊ฐ ํšŒ์‚ฌ(tenant)๋Š” ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ### 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์š”๊ตฌ์‚ฌํ•ญ #### company_code ์ปฌ๋Ÿผ ํ•„์ˆ˜ ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ํ…Œ์ด๋ธ”์€ `company_code` ์ปฌ๋Ÿผ์„ **๋ฐ˜๋“œ์‹œ** ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: ```sql CREATE TABLE example_table ( id SERIAL PRIMARY KEY, company_code VARCHAR(20) NOT NULL, -- ํ•„์ˆ˜! name VARCHAR(100), created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT fk_company FOREIGN KEY (company_code) REFERENCES company_info(company_code) ); -- ์„ฑ๋Šฅ์„ ์œ„ํ•œ ์ธ๋ฑ์Šค (ํ•„์ˆ˜) CREATE INDEX idx_example_company_code ON example_table(company_code); ``` #### ์˜ˆ์™ธ ํ…Œ์ด๋ธ” ๋‹ค์Œ ํ…Œ์ด๋ธ”๋“ค๋งŒ `company_code` ์—†์ด ์ „์—ญ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: - `company_info` (ํšŒ์‚ฌ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ) - `user_info` (์‚ฌ์šฉ์ž๋Š” company_code ๋ณด์œ ) - ์‹œ์Šคํ…œ ์„ค์ • ํ…Œ์ด๋ธ” (`system_config` ๋“ฑ) - ๊ฐ์‚ฌ ๋กœ๊ทธ ํ…Œ์ด๋ธ” (`audit_log` ๋“ฑ) ### 2. ๋ฐฑ์—”๋“œ API ๊ตฌํ˜„ ํ•„์ˆ˜ ์‚ฌํ•ญ #### ์กฐํšŒ(SELECT) ์ฟผ๋ฆฌ **๋ชจ๋“  SELECT ์ฟผ๋ฆฌ๋Š” company_code ํ•„ํ„ฐ๋ง์„ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:** ```typescript // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• async function getDataList(req: Request, res: Response) { const companyCode = req.user!.companyCode; // ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ํšŒ์‚ฌ ์ฝ”๋“œ const query = ` SELECT * FROM example_table WHERE company_code = $1 ORDER BY created_at DESC `; const result = await pool.query(query, [companyCode]); logger.info("๋ฐ์ดํ„ฐ ์กฐํšŒ", { companyCode, rowCount: result.rowCount }); return res.json({ success: true, data: result.rows }); } // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• - company_code ํ•„ํ„ฐ๋ง ์—†์Œ async function getDataList(req: Request, res: Response) { const query = `SELECT * FROM example_table`; // ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ๋…ธ์ถœ! const result = await pool.query(query); return res.json({ success: true, data: result.rows }); } ``` #### ์ƒ์„ฑ(INSERT) ์ฟผ๋ฆฌ **๋ชจ๋“  INSERT ์ฟผ๋ฆฌ๋Š” company_code๋ฅผ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:** ```typescript // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• async function createData(req: Request, res: Response) { const companyCode = req.user!.companyCode; const { name, description } = req.body; const query = ` INSERT INTO example_table (company_code, name, description) VALUES ($1, $2, $3) RETURNING * `; const result = await pool.query(query, [companyCode, name, description]); logger.info("๋ฐ์ดํ„ฐ ์ƒ์„ฑ", { companyCode, id: result.rows[0].id }); return res.json({ success: true, data: result.rows[0] }); } // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• - company_code ๋ˆ„๋ฝ async function createData(req: Request, res: Response) { const { name, description } = req.body; const query = ` INSERT INTO example_table (name, description) VALUES ($1, $2) `; // company_code ๋ˆ„๋ฝ! ๋‹ค๋ฅธ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ์™€ ์„ž์ž„ const result = await pool.query(query, [name, description]); return res.json({ success: true, data: result.rows[0] }); } ``` #### ์ˆ˜์ •(UPDATE) ์ฟผ๋ฆฌ **WHERE ์ ˆ์— company_code๋ฅผ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:** ```typescript // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• async function updateData(req: Request, res: Response) { const companyCode = req.user!.companyCode; const { id } = req.params; const { name, description } = req.body; const query = ` UPDATE example_table SET name = $1, description = $2, updated_at = NOW() WHERE id = $3 AND company_code = $4 RETURNING * `; const result = await pool.query(query, [name, description, id, companyCode]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค" }); } logger.info("๋ฐ์ดํ„ฐ ์ˆ˜์ •", { companyCode, id }); return res.json({ success: true, data: result.rows[0] }); } // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• - ๋‹ค๋ฅธ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ๋„ ์ˆ˜์ • ๊ฐ€๋Šฅ async function updateData(req: Request, res: Response) { const { id } = req.params; const { name, description } = req.body; const query = ` UPDATE example_table SET name = $1, description = $2 WHERE id = $3 `; // ๋‹ค๋ฅธ ํšŒ์‚ฌ์˜ ๊ฐ™์€ ID ๋ฐ์ดํ„ฐ๋„ ์ˆ˜์ •๋จ! const result = await pool.query(query, [name, description, id]); return res.json({ success: true, data: result.rows[0] }); } ``` #### ์‚ญ์ œ(DELETE) ์ฟผ๋ฆฌ **WHERE ์ ˆ์— company_code๋ฅผ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:** ```typescript // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• async function deleteData(req: Request, res: Response) { const companyCode = req.user!.companyCode; const { id } = req.params; const query = ` DELETE FROM example_table WHERE id = $1 AND company_code = $2 RETURNING id `; const result = await pool.query(query, [id, companyCode]); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: "๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค" }); } logger.info("๋ฐ์ดํ„ฐ ์‚ญ์ œ", { companyCode, id }); return res.json({ success: true }); } // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• - ๋‹ค๋ฅธ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ๋„ ์‚ญ์ œ ๊ฐ€๋Šฅ async function deleteData(req: Request, res: Response) { const { id } = req.params; const query = `DELETE FROM example_table WHERE id = $1`; const result = await pool.query(query, [id]); return res.json({ success: true }); } ``` ### 3. company_code = "*" ์˜๋ฏธ **์ค‘์š”**: `company_code = "*"`๋Š” **์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๋ฐ์ดํ„ฐ**๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. - โŒ ์ž˜๋ชป๋œ ์ดํ•ด: `company_code = "*"` = ๋ชจ๋“  ํšŒ์‚ฌ๊ฐ€ ๊ณต์œ ํ•˜๋Š” ๊ณตํ†ต ๋ฐ์ดํ„ฐ - โœ… ์˜ฌ๋ฐ”๋ฅธ ์ดํ•ด: `company_code = "*"` = ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ๊ด€๋ฆฌํ•˜๋Š” ์ „์šฉ ๋ฐ์ดํ„ฐ **ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ์›์น™**: - ํšŒ์‚ฌ A (`company_code = "COMPANY_A"`): ํšŒ์‚ฌ A ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ/์ˆ˜์ •/์‚ญ์ œ ๊ฐ€๋Šฅ - ํšŒ์‚ฌ B (`company_code = "COMPANY_B"`): ํšŒ์‚ฌ B ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ/์ˆ˜์ •/์‚ญ์ œ ๊ฐ€๋Šฅ - ์ตœ๊ณ  ๊ด€๋ฆฌ์ž (`company_code = "*"`): ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ + ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ ### 4. ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(SUPER_ADMIN) ์˜ˆ์™ธ ์ฒ˜๋ฆฌ **์ตœ๊ณ  ๊ด€๋ฆฌ์ž(company_code = "*")๋Š” ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:** ```typescript async function getDataList(req: Request, res: Response) { const companyCode = req.user!.companyCode; let query: string; let params: any[]; if (companyCode === "*") { // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ๋ชจ๋“  ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ query = ` SELECT * FROM example_table ORDER BY company_code, created_at DESC `; params = []; logger.info("์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์ฒด ๋ฐ์ดํ„ฐ ์กฐํšŒ"); } else { // ์ผ๋ฐ˜ ํšŒ์‚ฌ: ์ž์‹ ์˜ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ (company_code = "*" ๋ฐ์ดํ„ฐ๋Š” ์ œ์™ธ) query = ` SELECT * FROM example_table WHERE company_code = $1 ORDER BY created_at DESC `; params = [companyCode]; logger.info("ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ", { companyCode }); } const result = await pool.query(query, params); return res.json({ success: true, data: result.rows }); } ``` **ํ•ต์‹ฌ**: ์ผ๋ฐ˜ ํšŒ์‚ฌ ์‚ฌ์šฉ์ž๋Š” `company_code = "*"` ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค! ### 5. JOIN ์ฟผ๋ฆฌ์—์„œ์˜ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ **๋ชจ๋“  JOIN๋œ ํ…Œ์ด๋ธ”์—๋„ company_code ํ•„ํ„ฐ๋ง์„ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:** ```typescript // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• const query = ` SELECT a.*, b.name as category_name, c.name as user_name FROM example_table a LEFT JOIN category_table b ON a.category_id = b.id AND a.company_code = b.company_code -- JOIN ์กฐ๊ฑด์—๋„ company_code ํ•„์ˆ˜ LEFT JOIN user_info c ON a.user_id = c.user_id AND a.company_code = c.company_code WHERE a.company_code = $1 `; // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• - JOIN์—์„œ ๋‹ค๋ฅธ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ์™€ ์„ž์ž„ const query = ` SELECT a.*, b.name as category_name FROM example_table a LEFT JOIN category_table b ON a.category_id = b.id -- company_code ์—†์Œ! WHERE a.company_code = $1 `; ``` ### 6. ์„œ๋น„์Šค ๊ณ„์ธต ํŒจํ„ด **์„œ๋น„์Šค ํ•จ์ˆ˜๋Š” ํ•ญ์ƒ companyCode๋ฅผ ์ฒซ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค:** ```typescript // โœ… ์˜ฌ๋ฐ”๋ฅธ ์„œ๋น„์Šค ํŒจํ„ด class ExampleService { async findAll(companyCode: string, filters?: any) { const query = ` SELECT * FROM example_table WHERE company_code = $1 `; return await pool.query(query, [companyCode]); } async findById(companyCode: string, id: number) { const query = ` SELECT * FROM example_table WHERE id = $1 AND company_code = $2 `; const result = await pool.query(query, [id, companyCode]); return result.rows[0]; } async create(companyCode: string, data: any) { const query = ` INSERT INTO example_table (company_code, name, description) VALUES ($1, $2, $3) RETURNING * `; const result = await pool.query(query, [companyCode, data.name, data.description]); return result.rows[0]; } } // ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์‚ฌ์šฉ const exampleService = new ExampleService(); async function getDataList(req: Request, res: Response) { const companyCode = req.user!.companyCode; const data = await exampleService.findAll(companyCode, req.query); return res.json({ success: true, data }); } ``` ### 7. ํ”„๋ก ํŠธ์—”๋“œ ๊ณ ๋ ค์‚ฌํ•ญ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ์ง์ ‘ company_code๋ฅผ ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐฑ์—”๋“œ API๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ```typescript // โœ… ํ”„๋ก ํŠธ์—”๋“œ - company_code ๋ถˆํ•„์š” async function fetchData() { const response = await apiClient.get("/api/example/list"); // ๋ฐฑ์—”๋“œ์—์„œ ์ž๋™์œผ๋กœ ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ company_code๋กœ ํ•„ํ„ฐ๋ง๋จ return response.data; } // โŒ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ company_code๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ „๋‹ฌํ•˜์ง€ ์•Š์Œ async function fetchData(companyCode: string) { const response = await apiClient.get(`/api/example/list?companyCode=${companyCode}`); return response.data; } ``` ### 8. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ƒˆ๋กœ์šด ํ…Œ์ด๋ธ”์ด๋‚˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•˜์„ธ์š”: #### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค - [ ] ํ…Œ์ด๋ธ”์— `company_code VARCHAR(20) NOT NULL` ์ปฌ๋Ÿผ ์ถ”๊ฐ€ - [ ] `company_info` ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€ - [ ] `company_code`์— ์ธ๋ฑ์Šค ์ƒ์„ฑ - [ ] ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ์— ์˜ฌ๋ฐ”๋ฅธ `company_code` ๊ฐ’ ํฌํ•จ #### ๋ฐฑ์—”๋“œ API - [ ] SELECT ์ฟผ๋ฆฌ์— `WHERE company_code = $1` ์ถ”๊ฐ€ - [ ] INSERT ์ฟผ๋ฆฌ์— `company_code` ์ปฌ๋Ÿผ ํฌํ•จ - [ ] UPDATE/DELETE ์ฟผ๋ฆฌ์˜ WHERE ์ ˆ์— `company_code` ์กฐ๊ฑด ์ถ”๊ฐ€ - [ ] JOIN ์ฟผ๋ฆฌ์˜ ON ์ ˆ์— `company_code` ๋งค์นญ ์กฐ๊ฑด ์ถ”๊ฐ€ - [ ] ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(`company_code = "*"`) ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - [ ] ๋กœ๊ทธ์— `companyCode` ์ •๋ณด ํฌํ•จ #### ํ…Œ์ŠคํŠธ - [ ] ํšŒ์‚ฌ A๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ํšŒ์‚ฌ A ๋ฐ์ดํ„ฐ๋งŒ ๋ณด์ด๋Š”์ง€ ํ™•์ธ - [ ] ํšŒ์‚ฌ B๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ํšŒ์‚ฌ B ๋ฐ์ดํ„ฐ๋งŒ ๋ณด์ด๋Š”์ง€ ํ™•์ธ - [ ] ํšŒ์‚ฌ A๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ํšŒ์‚ฌ B ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ - [ ] ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณด์ด๋Š”์ง€ ํ™•์ธ - [ ] ์ง์ ‘ SQL ์ธ์ ์…˜ ์‹œ๋„ํ•˜์—ฌ ๋‹ค๋ฅธ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅ ํ™•์ธ ### 9. ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ #### ํด๋ผ์ด์–ธํŠธ ์ž…๋ ฅ ๊ฒ€์ฆ ```typescript // โŒ ์œ„ํ—˜ - ํด๋ผ์ด์–ธํŠธ๊ฐ€ company_code๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Œ async function createData(req: Request, res: Response) { const { companyCode, name } = req.body; // ์‚ฌ์šฉ์ž๊ฐ€ ์ž„์˜์˜ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ๊ฐ€๋Šฅ! const query = `INSERT INTO example_table (company_code, name) VALUES ($1, $2)`; await pool.query(query, [companyCode, name]); } // โœ… ์•ˆ์ „ - ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ company_code๋งŒ ์‚ฌ์šฉ async function createData(req: Request, res: Response) { const companyCode = req.user!.companyCode; // ์„œ๋ฒ„์—์„œ ํ™•์ • const { name } = req.body; const query = `INSERT INTO example_table (company_code, name) VALUES ($1, $2)`; await pool.query(query, [companyCode, name]); } ``` #### ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ชจ๋“  ์ค‘์š”ํ•œ ์ž‘์—…์— ํšŒ์‚ฌ ์ •๋ณด๋ฅผ ๋กœ๊น…ํ•˜์„ธ์š”: ```typescript logger.info("๋ฐ์ดํ„ฐ ์ƒ์„ฑ", { companyCode: req.user!.companyCode, userId: req.user!.userId, tableName: "example_table", action: "INSERT", recordId: result.rows[0].id, }); logger.warn("๊ถŒํ•œ ์—†๋Š” ์ ‘๊ทผ ์‹œ๋„", { companyCode: req.user!.companyCode, userId: req.user!.userId, attemptedRecordId: req.params.id, message: "๋‹ค๋ฅธ ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ์‹œ๋„", }); ``` ### 10. ์ผ๋ฐ˜์ ์ธ ์‹ค์ˆ˜์™€ ํ•ด๊ฒฐ๋ฐฉ๋ฒ• #### ์‹ค์ˆ˜ 1: ์„œ๋ธŒ์ฟผ๋ฆฌ์—์„œ company_code ๋ˆ„๋ฝ ```typescript // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• const query = ` SELECT * FROM example_table WHERE category_id IN ( SELECT id FROM category_table WHERE active = true ) AND company_code = $1 `; // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• const query = ` SELECT * FROM example_table WHERE category_id IN ( SELECT id FROM category_table WHERE active = true AND company_code = $1 ) AND company_code = $1 `; ``` #### ์‹ค์ˆ˜ 2: COUNT/SUM ์ง‘๊ณ„ ํ•จ์ˆ˜ ```typescript // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• - ๋ชจ๋“  ํšŒ์‚ฌ์˜ ์ดํ•ฉ const query = `SELECT COUNT(*) as total FROM example_table`; // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• const query = ` SELECT COUNT(*) as total FROM example_table WHERE company_code = $1 `; ``` #### ์‹ค์ˆ˜ 3: EXISTS ์„œ๋ธŒ์ฟผ๋ฆฌ ```typescript // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• const query = ` SELECT * FROM example_table a WHERE EXISTS ( SELECT 1 FROM related_table b WHERE b.example_id = a.id ) AND a.company_code = $1 `; // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• const query = ` SELECT * FROM example_table a WHERE EXISTS ( SELECT 1 FROM related_table b WHERE b.example_id = a.id AND b.company_code = a.company_code ) AND a.company_code = $1 `; ``` ### 11. ์ฐธ๊ณ  ์ž๋ฃŒ - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ: `db/migrations/033_add_company_code_to_code_tables.sql` - ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๋ถ„์„ ๋ฌธ์„œ: `docs/๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ_๊ตฌํ˜„_ํ˜„ํ™ฉ_๋ถ„์„_๋ณด๊ณ ์„œ.md` - ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ์ปจํŠธ๋กค๋Ÿฌ: `backend-node/src/controllers/adminController.ts` - ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด: `backend-node/src/middleware/authMiddleware.ts` ### 12. ์š”์•ฝ **๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ํšŒ์‚ฌ๋ณ„ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค:** 1. ๋ชจ๋“  ํ…Œ์ด๋ธ”์— `company_code` ์ปฌ๋Ÿผ ์ถ”๊ฐ€ 2. ๋ชจ๋“  ์ฟผ๋ฆฌ์— `company_code` ํ•„ํ„ฐ๋ง ์ ์šฉ 3. ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ `req.user.companyCode` ์‚ฌ์šฉ 4. ํด๋ผ์ด์–ธํŠธ ์ž…๋ ฅ์œผ๋กœ `company_code`๋ฅผ ๋ฐ›์ง€ ์•Š์Œ 5. ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(`company_code = "*"`)๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฐ€๋Šฅ 6. **์ผ๋ฐ˜ ํšŒ์‚ฌ๋Š” `company_code = "*"` ๋ฐ์ดํ„ฐ๋ฅผ ๋ณผ ์ˆ˜ ์—†์Œ** (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ) 7. JOIN, ์„œ๋ธŒ์ฟผ๋ฆฌ, ์ง‘๊ณ„ ํ•จ์ˆ˜์—๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ 8. ๋ชจ๋“  ์ž‘์—…์„ ๋กœ๊น…ํ•˜์—ฌ ๊ฐ์‚ฌ ์ถ”์  ๊ฐ€๋Šฅ **์ ˆ๋Œ€ ์žŠ์ง€ ๋งˆ์„ธ์š”: ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ๋Š” ๋ณด์•ˆ์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค!** **company_code = "*"๋Š” ๊ณตํ†ต ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹Œ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค!**