25 KiB
25 KiB
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 컴포넌트 직접 구현 |
사용 가능한 공통 컴포넌트 (예외)
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 기본 (필수)
"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만)
import {
Plus, Trash2, Save, Loader2, Search, X, Pencil,
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight,
RotateCcw, Download, Upload, FileSpreadsheet, Settings2,
ChevronDown, ChevronUp, Package, Inbox,
} from "lucide-react";
유틸
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을 적용:
<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>
★ 선택 행 스타일 통일
<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:
<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)
// ★ 목록 조회 (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. 카테고리 옵션 로드
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. 코드→라벨 변환
const resolveLabel = (code: string, optionKey: string): string => {
const opts = categoryOptions[optionKey] || [];
return opts.find(o => o.code === code)?.label || code || "-";
};
6-4. 마스터-디테일 저장 패턴
// 신규: 마스터 add → 디테일 각 행 add
// 수정: 마스터 edit → 기존 디테일 delete → 디테일 재 add
// 시스템 필드 제외: id, created_date, updated_date, writer, company_code, created_by, updated_by
7. 테이블 구현 상세
7-1. 기본 테이블 구조 (★ 모든 테이블에 이 패턴 적용)
<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 패턴
// 코드/번호 → 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. 검색 필터 구현
<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) 구현
<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. 빈 상태 구현
{/* 테이블 빈 상태 */}
<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)
<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. 유틸 함수
// 천단위 포맷
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 레지스트리 등록
"/{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'으로 변경.
메뉴 표시 우선순위:
screen_menu_assignments(is_active='Y') → 로우코드 화면menu_info.screen_group_id→ 로우코드 화면menu_info.menu_url→ 직접 구현 화면 (AdminPageRenderer) → 1, 2를 비활성화해야 3번이 동작함
14. UI 리디자인 규칙 (기존 화면 수정 시)
- 기존 return문(JSX)을 전부 삭제하고 프리셋 HTML 기준으로 새로 작성
useState,useEffect,useCallback, API 호출 함수는 그대로 유지- DataGrid 등 공통 컴포넌트를 쓰고 있으면 제거하고 shadcn/ui 기본으로 교체
- "현재 코드가 이미 충족한다"고 판단하지 말 것. 무조건 프리셋 기준으로 새로 작성
- git diff 기준 최소 100줄 이상 변경 필수
15. 절대 금지 사항
fetch()직접 사용 (반드시apiClient사용)- 하드코딩 색상 (
bg-gray-*,bg-white,text-black,#hex) - 새 라이브러리 설치
console.log남기기any타입 남발 (최소한의 타입 정의)- 비즈니스 로직 변경 (UI 리디자인 태스크일 때)
- 다른 화면 UI를 그대로 복사 (패턴 참고만, 컬럼/필드는 독립 설계)
- DataGrid, DynamicSearchFilter, SmartSelect, FullscreenDialog, FormDatePicker import
- 테이블 헤더 스타일을 페이지마다 다르게 적용 (★ 통일 규격 준수)