Files
vexplor_dev/docs/coding-rules/erp-coding-rules-direct.md

631 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ERP 화면 직접 구현 규칙 (Direct Implementation Mode)
이 문서는 **직접 구현** 방식으로 화면을 개발/리디자인할 때 참조하는 규칙입니다.
프리셋 HTML(Type A~F) 디자인 규격에 따라 shadcn/ui 기본 컴포넌트로 직접 조립하여 구현합니다.
> **적용 대상:** COMPANY_16(하이큐마그) 전체, 향후 신규 화면 개발
> **로우코드와의 차이:** 로우코드는 DataGrid/DynamicSearchFilter 등 공통 컴포넌트에 설정만 넘기는 방식. 직접 구현은 프리셋 규격에 맞춰 shadcn/ui 기본으로 직접 조립.
---
## 1. 프로젝트 정보
- **프로젝트:** erp-node (Next.js App Router + Node.js/Express + PostgreSQL)
- **프론트엔드:** /Users/gbpark/erp-node/frontend/
- **백엔드:** /Users/gbpark/erp-node/backend-node/
- **화면 경로:** frontend/app/(main)/{COMPANY}/{category}/{screen}/page.tsx
- **프리셋 HTML:** _local/erp-preset-type-{a~f}-*.html
- **공통 CSS:** _local/erp-preset-common.css
- **글로벌 CSS:** frontend/app/globals.css (테이블 Enhancement 포함)
---
## 2. 절대 금지 컴포넌트 (★ 최우선 규칙)
아래 공통 컴포넌트는 **절대 import/사용 금지**. shadcn/ui 기본으로 직접 구현:
| 금지 | 대체 방법 |
|------|-----------|
| `DataGrid` | `Table, TableBody, TableCell, TableHead, TableHeader, TableRow` 직접 조합 |
| `DynamicSearchFilter` | `Input, Select, Button`으로 검색 영역 직접 구현 |
| `SmartSelect` | `Select` 또는 `Popover + Command` 조합으로 직접 구현 |
| `FullscreenDialog` | `Dialog` (shadcn/ui)로 직접 구현 |
| `TableSettingsModal` | 필요 시 인라인으로 직접 구현 |
| `FormDatePicker` | `Input type="date"` 또는 `Calendar` 컴포넌트 직접 구현 |
### 사용 가능한 공통 컴포넌트 (예외)
```tsx
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; // OK (로직 훅)
import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; // OK (복잡한 유틸)
import { exportToExcel } from "@/lib/utils/excelExport"; // OK (유틸 함수)
import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchModal"; // OK (도메인 모달)
import { TimelineScheduler } from "@/components/common/TimelineScheduler"; // OK (특수 컴포넌트)
```
---
## 3. 허용 import 패턴
### shadcn/ui 기본 (필수)
```tsx
"use client";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";
```
### 아이콘 (lucide-react만)
```tsx
import {
Plus, Trash2, Save, Loader2, Search, X, Pencil,
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
RotateCcw, Download, Upload, FileSpreadsheet, Settings2,
ChevronDown, ChevronUp, Package, Inbox,
} from "lucide-react";
```
### 유틸
```tsx
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
```
---
## 4. 디자인 규칙
### 4-1. 색상: CSS 변수만 사용 (하드코딩 절대 금지)
```
✅ 허용:
bg-primary, text-primary-foreground, bg-muted, text-muted-foreground
bg-destructive, text-destructive, bg-accent, text-accent-foreground
bg-card, text-card-foreground, bg-background, text-foreground
border-border, border-primary, border-destructive
hsl(var(--primary)), hsl(var(--border)), hsl(var(--muted))
❌ 금지 (하드코딩):
bg-gray-50, bg-gray-100, bg-white, bg-slate-*
text-black, text-gray-500, text-blue-500
border-gray-200, border-slate-*
#3b82f6, #ffffff, rgb(0,0,0)
```
### 4-2. 테마
- 라이트 모드 기본 + `.dark` 클래스로 다크 모드 자동 전환
- Primary: Vivid Blue (HSL 217 91% 60%)
- Dark 배경: Deep Navy (HSL 222 47% 6%)
### 4-3. 컴포넌트 규칙
- shadcn/ui 컴포넌트만 사용 (`@/components/ui/*`)
- 아이콘: lucide-react만 사용
- Card-in-Card 중첩 금지
- 인라인 콘텐츠에 `max-h` / `overflow-y-auto` 금지
- 새 UI 라이브러리 설치 금지
### 4-4. 텍스트 톤
- Toss 스타일 **~해요 체** 사용
- 빈 상태: "수주를 등록해주세요", "좌측에서 거래처를 선택해주세요"
- 확인 다이얼로그: "삭제하시겠어요?", "저장되었어요"
- 플레이스홀더: "품목명을 입력해주세요"
### 4-5. 사이즈 표준
- 입력필드 높이: `h-9` (36px)
- 버튼 기본: `h-9`, 소형: `h-8` (`size="sm"`)
- 폰트: `text-sm` (14px) 기본, `text-xs` (12px) 보조
---
## 5. 프리셋 6종 — 화면 유형별 레이아웃
### ★ 테이블 공통 스타일 (globals.css에서 전역 적용)
globals.css에 아래 스타일이 전역 적용되므로, 각 페이지에서는 구조만 맞추면 됨:
- **데이터 셀**: padding 12px 16px, font-size 14px, line-height 1.5
- **헤더 셀**: padding 12px 16px, font-size 14px, font-weight 700
- **짝수 행 stripe**: 미세 배경색 (라이트: muted/0.35, 다크: muted/0.18)
- **hover**: bg-accent
- **다크모드 체크박스**: 밝은 테두리 (흰색 계열)
### ★ 테이블 헤더 통일 규격 (★ 반드시 준수)
모든 테이블의 TableHead 데이터 컬럼에 아래 className을 적용:
```tsx
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
// 숫자 컬럼 (우측 정렬)
<TableHead className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
// 너비 지정 시 앞에 추가
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
// TableHeader 행
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
...
</TableRow>
</TableHeader>
```
### ★ 선택 행 스타일 통일
```tsx
<TableRow
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isSelected ? "border-l-primary bg-primary/5" : "hover:bg-accent"
)}
>
```
---
### Type A: 단일 테이블형
- 프리셋: `_local/erp-preset-type-a-single-table.html`
- 적용 페이지: sales/order, purchase/order, master-data/item-info, logistics/packaging, production/work-instruction, quality/item-inspection, design/change-management, design/design-request
- 구조:
```
page-container(flex flex-col h-full gap-3 p-4)
├─ 브레드크럼 (text-xs text-muted-foreground)
├─ 검색 필터 (card: rounded-lg border bg-card px-5 py-4)
│ ├─ grid(grid-cols-5 gap-3 items-end)
│ │ ├─ field-group(flex flex-col gap-1) × N
│ │ │ ├─ label(text-[11px] font-semibold uppercase tracking-wide text-muted-foreground)
│ │ │ └─ Input(h-9) / Select(h-9) / date range
│ │ └─ 조회/초기화 Button (h-9)
├─ 액션 바 (flex justify-between)
│ ├─ 타이틀(text-lg font-bold) + 건수(font-mono text-[11px] text-primary bg-primary/5 rounded-full)
│ └─ 버튼 그룹 (등록/수정/삭제 | 엑셀업로드/다운로드 | 설정)
├─ 테이블 카드 (flex-1 overflow-hidden rounded-lg border bg-card)
│ └─ Table (★ 테이블 공통 스타일 적용)
│ ├─ TableHeader(sticky top-0 z-10) → TableRow(bg-muted hover:bg-muted)
│ │ └─ TableHead(text-[11px] font-bold uppercase tracking-wide text-muted-foreground)
│ └─ TableBody → 데이터 행 (선택 행 스타일, hover)
└─ 등록/수정 모달 (Dialog: max-w-[95vw] max-h-[92vh])
├─ DialogHeader (타이틀 + 설명)
├─ 4열 그리드 폼 (grid grid-cols-4 gap-4)
│ └─ Label(text-[11px] font-semibold uppercase) + Input(h-9) / Select / date
├─ 디테일 리피터 (마스터-디테일인 경우)
└─ DialogFooter (취소 + 저장)
```
### Type B: 마스터-디테일형 (좌우 분할)
- 프리셋: `_local/erp-preset-type-b-master-detail.html`
- 적용 페이지: sales/customer, sales/sales-item, sales/claim, sales/shipping-*, purchase/purchase-item, purchase/supplier, logistics/inventory, logistics/warehouse, logistics/outbound, logistics/receiving, logistics/material-status, master-data/department, equipment/info, outsourcing/*, design/task-management, mold/info, production/plan-management
- 구조:
```
page-container(flex flex-col h-full gap-3 p-4)
├─ 검색 필터 (Type A와 동일 구조)
└─ ResizablePanelGroup(direction="horizontal" className="flex-1")
├─ ResizablePanel(defaultSize={55} minSize={30}) ← 좌측: 마스터
│ └─ card(flex flex-col h-full border rounded-lg bg-card)
│ ├─ 패널 헤더(flex justify-between p-3 border-b bg-muted)
│ │ ├─ 타이틀(text-[13px] font-bold) + 건수 Badge
│ │ └─ 버튼 (등록/수정/삭제/설정)
│ └─ Table (★ 테이블 공통 스타일 적용)
├─ ResizableHandle(withHandle)
└─ ResizablePanel(defaultSize={45} minSize={25}) ← 우측: 상세
├─ 미선택 시: empty-state
│ └─ border-2 border-dashed rounded-lg + 아이콘 + "좌측에서 ~를 선택해주세요"
└─ 선택 시: 상세 콘텐츠 또는 Tabs
└─ Tabs (탭별 상세 테이블/폼)
```
### Type C: 트리+디테일형
- 프리셋: `_local/erp-preset-type-c-tree-detail.html`
- 적용 페이지: production/bom, master-data/company, design/project
- Type B와 동일하되 우측에 **트리뷰 + 상세카드**
- 트리 노드: 재귀 컴포넌트로 직접 구현 (라이브러리 금지)
- 트리 indent: `pl-{level * 6}`
- expand/collapse: `expandedNodes: Set<string>` 상태 관리
- 트리 노드 UI:
```tsx
<div className={cn("flex items-center gap-1.5 px-3 py-1.5 cursor-pointer rounded hover:bg-muted/50",
selected && "bg-primary/5 border-l-[3px] border-l-primary"
)} style={{ paddingLeft: `${level * 24}px` }}>
<ChevronRight className={cn("h-4 w-4 transition-transform", expanded && "rotate-90")} />
<span className="text-sm">{node.name}</span>
</div>
```
### Type D: 탭 멀티뷰형
- 프리셋: `_local/erp-preset-type-d-tab-multiview.html`
- 적용 페이지: equipment/plc-settings, logistics/info, quality/inspection
- 구조:
```
page-container(flex flex-col h-full gap-3 p-4)
└─ card(flex-1 flex flex-col border rounded-lg)
├─ Tabs (shadcn)
│ ├─ TabsList
│ │ └─ TabsTrigger × N (탭 이름 + 건수 Badge)
│ └─ TabsContent × N
│ ├─ 패널 헤더 (타이틀 + 버튼)
│ └─ Table (★ 테이블 공통 스타일 적용)
└─ 등록/수정 모달
```
### Type E: 카드 리스트형
- 프리셋: `_local/erp-preset-type-e-card-list.html`
- 적용 페이지: design/my-work (칸반/리스트/타임시트 뷰)
- Type B에서 좌측이 Table 대신 카드 리스트
- 카드: `border rounded-lg p-4 cursor-pointer hover:border-primary`
### Type F: 리포트형
- 프리셋: `_local/erp-preset-type-f-report.html`
- 적용 페이지: 현재 없음 (향후 대시보드/리포트 화면용)
- 선언적 config 객체 + ReportEngine 사용
- 차트 + 요약 카드 + 상세 테이블 구조
---
## 6. 공통 API 패턴
### 6-1. 범용 CRUD (table-management)
```tsx
// ★ 목록 조회 (POST)
const fetchData = async () => {
setLoading(true);
try {
const filters: any[] = [];
if (searchValue) {
filters.push({ columnName: "column_name", operator: "contains", value: searchValue });
}
if (searchStatus !== "all") {
filters.push({ columnName: "status", operator: "equals", value: searchStatus });
}
const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, {
page: currentPage,
size: pageSize,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
sort: { columnName: "created_date", order: "desc" },
});
const rows = res.data?.data?.data || res.data?.data?.rows || [];
const total = res.data?.data?.totalCount || res.data?.data?.total || 0;
setData(rows);
setTotalCount(total);
} catch (err) {
toast.error("조회에 실패했어요");
} finally {
setLoading(false);
}
};
// ★ 등록 (POST /add)
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
field1: value1,
field2: value2,
// 시스템 필드 제외: id, created_date, updated_date, writer, company_code, created_by, updated_by
});
// ★ 수정 (PUT /edit)
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
originalData: { id: selectedId },
updatedData: { field1: newValue1, field2: newValue2 },
});
// ★ 삭제 (DELETE /delete)
await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, {
data: selectedIds.map(id => ({ id })),
});
```
### 6-2. 카테고리 옵션 로드
```tsx
const res = await apiClient.get(`/table-categories/${TABLE_NAME}/${COLUMN}/values`);
const flatten = (items: any[]): { code: string; label: string }[] => {
return items.reduce((acc, item) => {
acc.push({ code: item.code || item.value, label: item.label || item.name });
if (item.children?.length > 0) {
acc.push(...flatten(item.children));
}
return acc;
}, [] as any[]);
};
const options = res.data?.data?.length > 0 ? flatten(res.data.data) : [];
```
### 6-3. 코드→라벨 변환
```tsx
const resolveLabel = (code: string, optionKey: string): string => {
const opts = categoryOptions[optionKey] || [];
return opts.find(o => o.code === code)?.label || code || "-";
};
```
### 6-4. 마스터-디테일 저장 패턴
```tsx
// 신규: 마스터 add → 디테일 각 행 add
// 수정: 마스터 edit → 기존 디테일 delete → 디테일 재 add
// 시스템 필드 제외: id, created_date, updated_date, writer, company_code, created_by, updated_by
```
---
## 7. 테이블 구현 상세
### 7-1. 기본 테이블 구조 (★ 모든 테이블에 이 패턴 적용)
```tsx
<div className="flex-1 overflow-auto">
<Table>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="w-10 pl-4">
<Checkbox checked={allChecked} onCheckedChange={handleCheckAll} />
</TableHead>
{/* ★ 모든 데이터 컬럼에 동일한 스타일 적용 */}
<TableHead className="w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={colCount} className="py-16 text-center">
<Loader2 className="mx-auto h-6 w-6 animate-spin text-primary" />
</TableCell>
</TableRow>
) : data.length === 0 ? (
<TableRow>
<TableCell colSpan={colCount} className="py-16 text-center">
<div className="flex flex-col items-center gap-2 text-muted-foreground">
<Inbox className="h-8 w-8 opacity-30" />
<span className="text-sm"> </span>
</div>
</TableCell>
</TableRow>
) : (
data.map(row => (
<TableRow
key={row.id}
className={cn(
"cursor-pointer border-l-[3px] border-l-transparent transition-all",
isChecked ? "border-l-primary bg-primary/5" : "hover:bg-accent"
)}
>
<TableCell className="pl-4">
<Checkbox checked={isChecked} onCheckedChange={() => toggleCheck(row.id)} />
</TableCell>
<TableCell className="font-mono text-[13px]">{row.part_code}</TableCell>
<TableCell className="text-[13px]">{row.part_name}</TableCell>
<TableCell className="text-right font-mono text-[13px]">{Number(row.qty).toLocaleString()}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
```
### 7-2. 데이터 셀 className 패턴
```tsx
// 코드/번호 → font-mono
<TableCell className="font-mono text-[13px]">{row.order_no}</TableCell>
// 일반 텍스트
<TableCell className="text-[13px]">{row.part_name}</TableCell>
// 숫자 → text-right font-mono
<TableCell className="text-right font-mono text-[13px]">{Number(row.qty).toLocaleString()}</TableCell>
// 금액 → text-right font-mono font-semibold
<TableCell className="text-right font-mono text-[13px] font-semibold">{Number(row.amount).toLocaleString()}</TableCell>
// 보조 정보 (규격, 메모 등) → text-muted-foreground
<TableCell className="text-[13px] text-muted-foreground">{row.spec}</TableCell>
// 긴 텍스트 → truncate
<TableCell className="text-[13px] max-w-[150px]">
<span className="block truncate" title={row.item_name}>{row.item_name}</span>
</TableCell>
// 날짜
<TableCell className="text-[13px]">{row.due_date}</TableCell>
// 뱃지
<TableCell><Badge variant={getVariant(row.status)}>{resolveLabel(row.status, "status")}</Badge></TableCell>
```
---
## 8. 검색 필터 구현
```tsx
<div className="rounded-lg border bg-card px-5 py-4">
<div className="grid grid-cols-5 gap-3 items-end">
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></label>
<Input value={search} onChange={e => setSearch(e.target.value)}
onKeyDown={e => e.key === "Enter" && fetchData()} placeholder="SO-2026-0001" className="h-9" />
</div>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"></label>
<div className="flex items-center gap-1.5">
<Input type="date" className="h-9 flex-1" value={dateFrom} onChange={e => setDateFrom(e.target.value)} />
<span className="text-xs text-muted-foreground/50">~</span>
<Input type="date" className="h-9 flex-1" value={dateTo} onChange={e => setDateTo(e.target.value)} />
</div>
</div>
<div className="flex items-end gap-2">
<Button variant="ghost" size="sm" className="h-9" onClick={handleReset}>
<RotateCcw className="w-4 h-4" />
</Button>
<Button size="sm" className="h-9 flex-1" onClick={fetchData}>
<Search className="w-4 h-4" />
</Button>
</div>
</div>
</div>
```
---
## 9. 모달(Dialog) 구현
```tsx
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="flex flex-col gap-0 p-0 max-w-[95vw] w-full" style={{ maxHeight: "92vh" }}>
<DialogHeader className="shrink-0 border-b px-6 py-5">
<DialogTitle className="text-[17px] font-bold">{isEditMode ? "수정" : "등록"}</DialogTitle>
<DialogDescription> </DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-5 space-y-6">
{/* 섹션 구분 */}
<div className="space-y-3">
<div className="flex items-center gap-2 text-[13px] font-bold text-muted-foreground">
<div className="flex-1 h-px bg-border" />
</div>
<div className="grid grid-cols-4 gap-3.5">
<div className="space-y-1">
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
<span className="text-destructive">*</span>
</Label>
<Input className="h-9" value={form.field || ""} onChange={e => setForm(p => ({ ...p, field: e.target.value }))} />
</div>
</div>
</div>
</div>
<DialogFooter className="shrink-0 border-t px-6 py-4">
<Button variant="outline" onClick={() => setIsModalOpen(false)}></Button>
<Button onClick={handleSave} disabled={saving}>
{saving && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
```
---
## 10. 빈 상태 구현
```tsx
{/* 테이블 빈 상태 */}
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground py-16">
<Inbox className="h-8 w-8 opacity-30" />
<span className="text-sm"> </span>
</div>
{/* 마스터-디테일 우측 빈 상태 */}
<div className="flex flex-col items-center justify-center gap-3 h-full border-2 border-dashed rounded-lg">
<Package className="h-10 w-10 text-muted-foreground/40" />
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-xs text-muted-foreground/70"> </p>
</div>
```
---
## 11. 콤보박스 (Popover + Command)
```tsx
<Popover open={comboOpen} onOpenChange={setComboOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-9 w-full justify-between text-sm font-normal">
{selectedValue ? options.find(o => o.code === selectedValue)?.label : "선택해주세요"}
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="검색..." />
<CommandList>
<CommandEmpty> </CommandEmpty>
<CommandGroup>
{options.map(opt => (
<CommandItem key={opt.code} value={opt.label} onSelect={() => { setSelectedValue(opt.code); setComboOpen(false); }}>
{opt.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
```
---
## 12. 유틸 함수
```tsx
// 천단위 포맷
const formatNumber = (val: string | number) => {
const num = String(val).replace(/[^\d.-]/g, "");
if (!num) return "";
const parts = num.split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
};
const parseNumber = (val: string) => val.replace(/,/g, "");
```
---
## 13. 메뉴 등록 규칙 (신규 화면 필수)
### 13-1. AdminPageRenderer 레지스트리 등록
```tsx
"/{COMPANY}/{category}/{screen}": dynamic(
() => import("@/app/(main)/{COMPANY}/{category}/{screen}/page"),
{ ssr: false, loading: LoadingFallback }
),
```
### 13-2. menu_info 테이블 업데이트
- `menu_url`: 새 경로 / `screen_code`: NULL / `screen_group_id`: NULL
### 13-3. screen_menu_assignments 비활성화
해당 메뉴의 모든 할당을 `is_active = 'N'`으로 변경.
**메뉴 표시 우선순위:**
1. `screen_menu_assignments` (is_active='Y') → 로우코드 화면
2. `menu_info.screen_group_id` → 로우코드 화면
3. `menu_info.menu_url` → 직접 구현 화면 (AdminPageRenderer)
→ 1, 2를 비활성화해야 3번이 동작함
---
## 14. UI 리디자인 규칙 (기존 화면 수정 시)
1. 기존 return문(JSX)을 **전부 삭제**하고 프리셋 HTML 기준으로 **새로 작성**
2. `useState`, `useEffect`, `useCallback`, API 호출 함수는 **그대로 유지**
3. DataGrid 등 공통 컴포넌트를 쓰고 있으면 **제거하고 shadcn/ui 기본으로 교체**
4. "현재 코드가 이미 충족한다"고 판단하지 말 것. **무조건 프리셋 기준으로 새로 작성**
5. git diff 기준 **최소 100줄 이상 변경** 필수
---
## 15. 절대 금지 사항
1. `fetch()` 직접 사용 (반드시 `apiClient` 사용)
2. 하드코딩 색상 (`bg-gray-*`, `bg-white`, `text-black`, `#hex`)
3. 새 라이브러리 설치
4. `console.log` 남기기
5. `any` 타입 남발 (최소한의 타입 정의)
6. 비즈니스 로직 변경 (UI 리디자인 태스크일 때)
7. 다른 화면 UI를 그대로 복사 (패턴 참고만, 컬럼/필드는 독립 설계)
8. DataGrid, DynamicSearchFilter, SmartSelect, FullscreenDialog, FormDatePicker import
9. 테이블 헤더 스타일을 페이지마다 다르게 적용 (★ 통일 규격 준수)