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

25 KiB
Raw Blame 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 컴포넌트 직접 구현

사용 가능한 공통 컴포넌트 (예외)

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'으로 변경.

메뉴 표시 우선순위:

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