# ERP 화면 로우코드 규칙 (Low-Code Mode) 이 문서는 **로우코드** 방식으로 화면을 개발/리디자인할 때 참조하는 규칙입니다. DataGrid, DynamicSearchFilter 등 공통 컴포넌트를 활용하여 설정(config) 기반으로 빠르게 화면을 구성합니다. > **적용 대상:** COMPANY_7(탑씰) 기존 화면, COMPANY_29, COMPANY_9 등 공통 컴포넌트 기반 화면 > **주의:** 이 모드는 기존 로우코드 화면 유지보수용. 신규 개발은 날코딩 규칙(`erp-coding-rules-rawcode.md`) 권장. --- ## 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 - **공통 컴포넌트:** frontend/components/common/ --- ## 2. 사용 가능한 공통 컴포넌트 ### 2-1. DataGrid (테이블) - 파일: `@/components/common/DataGrid` - 용도: 편집 가능한 데이터 테이블 (컬럼 정의만으로 테이블 렌더링) - 기능: 정렬, 행 선택, 체크박스, 인라인 편집, 컬럼 리사이즈 ```tsx import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; const GRID_COLUMNS: DataGridColumn[] = [ { key: "order_no", label: "수주번호", width: "w-[120px]", type: "text" }, { key: "part_code", label: "품번", width: "w-[120px]", editable: true }, { key: "qty", label: "수량", width: "w-[80px]", type: "number", align: "right" }, { key: "status", label: "상태", width: "w-[100px]", type: "badge", badgeMap: { active: { label: "확정", variant: "default" }, draft: { label: "작성중", variant: "secondary" } } }, ]; ``` ### 2-2. DynamicSearchFilter (검색 필터) - 파일: `@/components/common/DynamicSearchFilter` - 용도: 설정 기반 동적 검색 필터 UI 자동 생성 - 기능: 텍스트, 셀렉트, 날짜 범위, 콤보박스 필터 자동 렌더링 ```tsx import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; const FILTER_CONFIG = [ { key: "order_no", label: "수주번호", type: "text" }, { key: "status", label: "상태", type: "select", options: statusOptions }, { key: "order_date", label: "수주일", type: "dateRange" }, { key: "customer", label: "거래처", type: "combo", options: customerOptions }, ]; ``` ### 2-3. SmartSelect (검색 가능 셀렉트) - 파일: `@/components/common/SmartSelect` - 용도: 검색 + 선택 통합 드롭다운 (거래처, 품목 선택 등) ```tsx import { SmartSelect } from "@/components/common/SmartSelect"; ``` ### 2-4. FullscreenDialog (전체화면 다이얼로그) - 파일: `@/components/common/FullscreenDialog` - 용도: 등록/수정 시 전체화면 모달 ```tsx import { FullscreenDialog } from "@/components/common/FullscreenDialog"; {/* 폼 내용 */} ``` ### 2-5. TableSettingsModal (테이블 설정) - 파일: `@/components/common/TableSettingsModal` - 용도: 컬럼 표시/숨김, 순서 변경, 너비 조정 ```tsx import { TableSettingsModal, loadTableSettings, saveTableSettings } from "@/components/common/TableSettingsModal"; { setGridColumns(newColumns); saveTableSettings(SETTINGS_KEY, newColumns); }} settingsKey={SETTINGS_KEY} /> ``` ### 2-6. FormDatePicker (날짜 선택) - 파일: `@/components/common/FormDatePicker` - 용도: 캘린더 UI 기반 날짜 입력 ```tsx import { FormDatePicker } from "@/components/common/FormDatePicker"; setFormData(prev => ({ ...prev, order_date: date }))} placeholder="날짜를 선택해주세요" /> ``` ### 2-7. 기타 사용 가능 컴포넌트 ```tsx import { useConfirmDialog } from "@/components/common/ConfirmDialog"; // 확인 다이얼로그 훅 import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; // 엑셀 업로드 import { exportToExcel } from "@/lib/utils/excelExport"; // 엑셀 다운로드 유틸 import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchModal"; // 출하계획 배치 import { TimelineScheduler } from "@/components/common/TimelineScheduler"; // 타임라인 스케줄러 import { EditableSpreadsheet } from "@/components/common/EditableSpreadsheet"; // 스프레드시트형 편집 ``` --- ## 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; ``` ### 아이콘 (lucide-react만) ```tsx import { Plus, Trash2, Save, Loader2, Search, X, Pencil, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RotateCcw, Download, Upload, FileSpreadsheet, Settings2, } 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-muted-foreground, bg-muted, bg-destructive, border-border ❌ 금지: bg-gray-50, bg-white, text-black, text-gray-500, #3b82f6 ``` ### 4-2. 테마 - 라이트 모드 기본 + `.dark` 클래스로 다크 모드 자동 전환 - Primary: Vivid Blue (HSL 217 91% 60%) - Dark 배경: Deep Navy (HSL 222 47% 6%) ### 4-3. 컴포넌트 규칙 - shadcn/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. 화면 유형별 레이아웃 ### Type A: 단일 테이블형 ``` page-container(flex flex-col h-full gap-3 p-4) ├─ DynamicSearchFilter ├─ card(flex-1 flex flex-col border rounded-lg) │ ├─ panel-header (타이틀 + 건수 Badge + 액션 버튼) │ ├─ DataGrid (컬럼 정의로 자동 렌더링) │ └─ pagination └─ FullscreenDialog (등록/수정 모달) ``` ### Type B: 마스터-디테일형 (좌우 분할) ``` page-container ├─ DynamicSearchFilter └─ ResizablePanelGroup(horizontal) ├─ ResizablePanel (좌측: DataGrid) ├─ ResizableHandle └─ ResizablePanel (우측: 상세 Tabs) ├─ 미선택: empty-state └─ 선택: Tabs (탭별 DataGrid 또는 상세 폼) ``` ### Type C: 트리+디테일형 ``` Type B + 우측에 트리뷰 + 상세카드 ``` ### Type D: 탭 멀티뷰형 ``` page-container └─ card > Tabs └─ TabsContent × N (각각 DynamicSearchFilter + DataGrid) ``` ### Type E: 카드 리스트형 ``` Type B에서 좌측을 DataGrid 대신 카드 리스트로 구현 ``` ### Type F: 리포트형 ``` ReportEngine에 config 전달 (선언적) ``` --- ## 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 }); } 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, }); // ★ 수정 (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. 로우코드 페이지 기본 골격 ```tsx "use client"; import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { FullscreenDialog } from "@/components/common/FullscreenDialog"; import { TableSettingsModal, loadTableSettings } from "@/components/common/TableSettingsModal"; import { SmartSelect } from "@/components/common/SmartSelect"; import { FormDatePicker } from "@/components/common/FormDatePicker"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { exportToExcel } from "@/lib/utils/excelExport"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { Plus, Trash2, Download, Upload, Settings2, Loader2 } from "lucide-react"; const TABLE_NAME = "your_table"; const SETTINGS_KEY = "table_settings_your_page"; const GRID_COLUMNS: DataGridColumn[] = [ { key: "col1", label: "컬럼1", width: "w-[120px]" }, { key: "col2", label: "컬럼2", width: "w-[100px]", type: "number" }, { key: "status", label: "상태", width: "w-[100px]", type: "badge" }, ]; const FILTER_CONFIG = [ { key: "col1", label: "컬럼1", type: "text" }, { key: "status", label: "상태", type: "select", options: [] }, ]; export default function YourPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [totalCount, setTotalCount] = useState(0); const [filterValues, setFilterValues] = useState({}); const [gridColumns, setGridColumns] = useState(GRID_COLUMNS); const [isModalOpen, setIsModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [saving, setSaving] = useState(false); const [formData, setFormData] = useState>({}); const [selectedId, setSelectedId] = useState(null); const [checkedIds, setCheckedIds] = useState([]); const [settingsOpen, setSettingsOpen] = useState(false); const [categoryOptions, setCategoryOptions] = useState>({}); const fetchData = useCallback(async () => { /* table-management API */ }, [filterValues]); const loadCategories = useCallback(async () => { /* 카테고리 로드 */ }, []); useEffect(() => { loadCategories(); }, [loadCategories]); useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { const saved = loadTableSettings(SETTINGS_KEY); if (saved) setGridColumns(saved); }, []); return (
{/* 검색 필터 */} { setFilterValues({}); }} /> {/* 메인 카드 */}
{/* 패널 헤더 */}

목록

{totalCount}
{/* 데이터 그리드 */} setSelectedId(row.id)} onCheckChange={setCheckedIds} loading={loading} emptyMessage="데이터가 없어요" />
{/* 등록/수정 모달 */}
{/* SmartSelect, FormDatePicker 등 공통 컴포넌트 활용 */}
{/* 테이블 설정 모달 */} {ConfirmDialogComponent}
); } ``` --- ## 8. 메뉴 등록 규칙 (신규 화면 필수) ### 8-1. AdminPageRenderer 레지스트리 등록 ```tsx "/{COMPANY}/{category}/{screen}": dynamic( () => import("@/app/(main)/{COMPANY}/{category}/{screen}/page"), { ssr: false, loading: LoadingFallback } ), ``` ### 8-2. menu_info 테이블 업데이트 - `menu_url`: 새 경로 / `screen_code`: NULL / `screen_group_id`: NULL ### 8-3. screen_menu_assignments 비활성화 해당 메뉴의 모든 할당을 `is_active = 'N'`으로 변경. --- ## 9. 절대 금지 사항 1. `fetch()` 직접 사용 (반드시 `apiClient` 사용) 2. 하드코딩 색상 (`bg-gray-*`, `bg-white`, `text-black`, `#hex`) 3. 새 라이브러리 설치 4. `console.log` 남기기 5. `any` 타입 남발 (최소한의 타입 정의) 6. 비즈니스 로직 변경 (UI 리디자인 태스크일 때) 7. Card-in-Card 중첩 8. 인라인 콘텐츠에 `max-h` / `overflow-y-auto` --- ## 10. 화면별 고유 디자인 원칙 - 다른 화면을 "복사"하지 말 것. 각 화면의 컬럼/필드/모달은 독립 설계 - `ref_files`에 다른 화면 코드가 있어도 **패턴 참고**일 뿐 - 각 화면의 DB 테이블과 비즈니스 로직에 맞게 구성