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

631 lines
25 KiB
Markdown
Raw Normal View History

# 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. 테이블 헤더 스타일을 페이지마다 다르게 적용 (★ 통일 규격 준수)