Files
vexplor/카테고리_시스템_재구현_완료_보고서.md
2025-11-05 15:23:57 +09:00

18 KiB

카테고리 시스템 재구현 완료 보고서

🎯 핵심 개념

메뉴 계층 기반 카테고리 스코프

  • 카테고리는 생성된 메뉴의 형제 메뉴들 간에만 공유됩니다
  • 다른 부모를 가진 메뉴에서는 사용할 수 없습니다
  • 화면관리 시스템의 위젯으로 통합되어 관리됩니다

완료된 작업

1. 데이터베이스 (Phase 1)

📊 테이블 수정: table_column_category_values

추가된 컬럼:

menu_id INTEGER NOT NULL  -- 메뉴 스코프

외래키 추가:

CONSTRAINT fk_category_value_menu FOREIGN KEY (menu_id) 
  REFERENCES menu_info(menu_id)

UNIQUE 제약조건 변경:

-- 변경 전
UNIQUE (table_name, column_name, value_code, company_code)

-- 변경 후
UNIQUE (table_name, column_name, value_code, menu_id, company_code)

인덱스 추가:

CREATE INDEX idx_category_values_menu ON table_column_category_values(menu_id);

📁 파일

  • db/migrations/036_create_table_column_category_values.sql

2. 백엔드 (Phase 1)

🔧 타입 수정

backend-node/src/types/tableCategoryValue.ts:

export interface TableCategoryValue {
  // ... 기존 필드
  menuId: number;  // ← 추가
  // ...
}

🎛️ 서비스 로직 추가

backend-node/src/services/tableCategoryValueService.ts:

  1. 형제 메뉴 조회 함수:
async getSiblingMenuIds(menuId: number): Promise<number[]> {
  // 1. 현재 메뉴의 부모 ID 조회
  // 2. 같은 부모를 가진 형제 메뉴들 조회
  // 3. 형제 메뉴 ID 배열 반환
}
  1. 카테고리 값 조회 수정:
async getCategoryValues(
  tableName: string,
  columnName: string,
  menuId: number,  // ← menuId 파라미터 추가
  companyCode: string,
  includeInactive: boolean = false
): Promise<TableCategoryValue[]> {
  // 형제 메뉴들의 카테고리도 포함
  const siblingMenuIds = await this.getSiblingMenuIds(menuId);
  
  // WHERE menu_id = ANY($3) 조건으로 필터링
}
  1. 카테고리 값 추가 수정:
async addCategoryValue(value: TableCategoryValue, ...): Promise<TableCategoryValue> {
  // INSERT 시 menu_id 포함
  // VALUES (..., $13, ...)  // value.menuId
}

🎮 컨트롤러 수정

backend-node/src/controllers/tableCategoryValueController.ts:

export const getCategoryValues = async (req: Request, res: Response) => {
  const menuId = parseInt(req.query.menuId as string, 10);
  
  if (!menuId || isNaN(menuId)) {
    return res.status(400).json({
      success: false,
      message: "menuId 파라미터가 필요합니다",
    });
  }
  
  const values = await tableCategoryValueService.getCategoryValues(
    tableName,
    columnName,
    menuId,  // ← menuId 전달
    companyCode,
    includeInactive
  );
  // ...
}

📁 수정된 파일

  • backend-node/src/types/tableCategoryValue.ts
  • backend-node/src/services/tableCategoryValueService.ts
  • backend-node/src/controllers/tableCategoryValueController.ts

3. 프론트엔드 (Phase 2)

📦 컴포넌트 생성

1) CategoryWidget (메인 좌우 분할 위젯)

frontend/components/screen/widgets/CategoryWidget.tsx:

interface CategoryWidgetProps {
  widgetId: string;
  menuId: number;      // ← 현재 화면의 menuId
  tableName: string;   // ← 현재 화면의 테이블
}

export function CategoryWidget({ widgetId, menuId, tableName }: CategoryWidgetProps) {
  const [selectedColumn, setSelectedColumn] = useState<{
    columnName: string;
    columnLabel: string;
  } | null>(null);

  return (
    <div className="flex h-full min-h-[600px] gap-6">
      {/* 좌측: 카테고리 컬럼 리스트 (30%) */}
      <div className="w-[30%] border-r pr-6">
        <CategoryColumnList
          tableName={tableName}
          menuId={menuId}
          selectedColumn={selectedColumn?.columnName || null}
          onColumnSelect={(columnName, columnLabel) =>
            setSelectedColumn({ columnName, columnLabel })
          }
        />
      </div>

      {/* 우측: 카테고리 값 관리 (70%) */}
      <div className="w-[70%]">
        {selectedColumn ? (
          <CategoryValueManager
            tableName={tableName}
            columnName={selectedColumn.columnName}
            columnLabel={selectedColumn.columnLabel}
            menuId={menuId}
          />
        ) : (
          <EmptyState />
        )}
      </div>
    </div>
  );
}
2) CategoryColumnList (좌측 패널)

frontend/components/table-category/CategoryColumnList.tsx:

  • 현재 테이블에서 input_type='category'인 컬럼 조회
  • 컬럼 목록을 카드 형태로 표시
  • 선택된 컬럼 하이라이트
3) CategoryValueManager 수정 (우측 패널)

frontend/components/table-category/CategoryValueManager.tsx:

interface CategoryValueManagerProps {
  tableName: string;
  columnName: string;
  columnLabel: string;
  menuId: number;  // ← 추가
}

// API 호출 시 menuId 전달
const response = await getCategoryValues(tableName, columnName, menuId);

const handleAddValue = async (newValue: TableCategoryValue) => {
  await addCategoryValue({
    ...newValue,
    tableName,
    columnName,
    menuId,  // ← 포함
  });
};

🔌 API 클라이언트 수정

frontend/lib/api/tableCategoryValue.ts:

export async function getCategoryValues(
  tableName: string,
  columnName: string,
  menuId: number,  // ← 추가
  includeInactive: boolean = false
) {
  const response = await apiClient.get(
    `/table-categories/${tableName}/${columnName}/values`,
    {
      params: { menuId, includeInactive },  // ← menuId 쿼리 파라미터
    }
  );
  return response.data;
}

🔤 타입 수정

frontend/types/tableCategoryValue.ts:

export interface TableCategoryValue {
  // ... 기존 필드
  menuId: number;  // ← 추가
  // ...
}

📁 생성/수정된 파일

  • frontend/components/screen/widgets/CategoryWidget.tsx (신규)
  • frontend/components/table-category/CategoryColumnList.tsx (복원)
  • frontend/components/table-category/CategoryValueManager.tsx (수정)
  • frontend/lib/api/tableCategoryValue.ts (수정)
  • frontend/types/tableCategoryValue.ts (수정)

4. 정리 작업 (Phase 4)

🗑️ 삭제된 파일

  • frontend/components/table-category/CategoryValueManagerDialog.tsx (Dialog 래퍼)

🔧 테이블 타입 관리 페이지 수정

frontend/app/(main)/admin/tableMng/page.tsx:

  1. Import 제거:
// ❌ 제거됨
import { CategoryValueManagerDialog } from "@/components/table-category/CategoryValueManagerDialog";
  1. 상태 제거:
// ❌ 제거됨
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [categoryDialogData, setCategoryDialogData] = useState<...>(null);
  1. 버튼 제거:
// ❌ 제거됨: "카테고리 값 관리" 버튼
{column.inputType === "category" && (
  <Button onClick={() => setCategoryDialogOpen(true)}>
    카테고리  관리
  </Button>
)}
  1. Dialog 렌더링 제거:
// ❌ 제거됨
{categoryDialogData && (
  <CategoryValueManagerDialog ... />
)}

📖 사용 시나리오

시나리오: 구매관리 시스템에서 카테고리 관리

1단계: 메뉴 구조

구매관리 (parent_id: 0, menu_id: 100)
├── 발주 관리 (parent_id: 100, menu_id: 101)
├── 입고 관리 (parent_id: 100, menu_id: 102)
├── 카테고리 관리 (parent_id: 100, menu_id: 103) ← 여기서 카테고리 생성
└── 거래처 관리 (parent_id: 100, menu_id: 104)

2단계: 카테고리 관리 화면 생성

  1. 메뉴 등록: 구매관리 > 카테고리 관리 (menu_id: 103)
  2. 화면 생성: 화면관리 시스템에서 화면 생성
  3. 테이블 연결: purchase_orders 테이블 선택
  4. 위젯 배치: CategoryWidget 드래그앤드롭

3단계: 카테고리 값 등록

  1. 좌측 패널: purchase_orders 테이블의 카테고리 컬럼 표시

    • order_type (발주 유형)
    • order_status (발주 상태)
    • priority (우선순위)
  2. 컬럼 선택: order_type 클릭

  3. 우측 패널: 카테고리 값 관리

    • 추가 버튼 클릭
    • 코드: MATERIAL, 라벨: 자재 발주
    • 저장 시 menu_id = 103으로 자동 저장됨

4단계: 다른 화면에서 사용

형제 메뉴에서 사용 가능

발주 관리 화면 (menu_id: 101, 형제 메뉴):

  • order_type 컬럼을 Code Select 위젯으로 배치
  • 드롭다운에 자재 발주, 외주 발주 등 표시됨
  • 이유: 101과 103은 같은 부모(100)를 가진 형제 메뉴

입고 관리 화면 (menu_id: 102, 형제 메뉴):

  • 동일하게 구매관리의 카테고리 사용 가능
다른 부모 메뉴에서 사용 불가

영업관리 > 주문 관리 (parent_id: 200):

  • 같은 order_type 컬럼이 있어도
  • 구매관리의 카테고리는 표시되지 않음
  • 이유: 다른 부모 메뉴이므로 스코프가 다름

🔍 메뉴 스코프 로직 상세

백엔드 로직

async getSiblingMenuIds(menuId: number): Promise<number[]> {
  // 예: menuId = 103 (카테고리 관리)
  
  // 1. 부모 ID 조회
  const parentResult = await pool.query(
    "SELECT parent_id FROM menu_info WHERE menu_id = $1",
    [103]
  );
  const parentId = parentResult.rows[0].parent_id;  // 100 (구매관리)
  
  // 2. 형제 메뉴들 조회
  const siblingsResult = await pool.query(
    "SELECT menu_id FROM menu_info WHERE parent_id = $1",
    [100]
  );
  
  // 3. 형제 메뉴 ID 배열 반환
  return [101, 102, 103, 104];  // 발주, 입고, 카테고리, 거래처
}

async getCategoryValues(..., menuId: number, ...): Promise<TableCategoryValue[]> {
  // 형제 메뉴들의 카테고리도 포함
  const siblingMenuIds = await this.getSiblingMenuIds(103);  // [101, 102, 103, 104]
  
  // WHERE menu_id = ANY([101, 102, 103, 104])
  const query = `
    SELECT * FROM table_column_category_values
    WHERE table_name = $1
      AND column_name = $2
      AND menu_id = ANY($3)  -- 형제 메뉴들의 카테고리 포함
      AND (company_code = $4 OR company_code = '*')
  `;
  
  return await pool.query(query, [tableName, columnName, siblingMenuIds, companyCode]);
}

프론트엔드 호출

// 발주 관리 화면 (menu_id: 101)
const values = await getCategoryValues(
  "purchase_orders",
  "order_type",
  101  // ← 현재 화면의 menuId
);

// 백엔드에서:
// 1. getSiblingMenuIds(101) → [101, 102, 103, 104]
// 2. WHERE menu_id = ANY([101, 102, 103, 104])
// 3. 카테고리 관리(103)에서 생성한 카테고리도 조회됨 ✅

🎨 UI 구조

CategoryWidget (좌우 분할)

┌─────────────────────────────────────────────────────────┐
│  카테고리 관리                                            │
├──────────────┬──────────────────────────────────────────┤
│ 카테고리 컬럼  │  카테고리 값 관리                          │
│ (30%)       │  (70%)                                   │
├──────────────┤                                          │
│ ┌──────────┐│  ┌────────────────────────────────────┐  │
│ │발주 유형  ││  │ 🔍 검색                            │  │
│ │order_type││  │ ┌─────────────┐ ┌─────────┐       │  │
│ └──────────┘│  │ │ 검색...      │ │ ✚ 추가  │       │  │
│             │  │ └─────────────┘ └─────────┘       │  │
│ ┌──────────┐│  │                                    │  │
│ │발주 상태  ││  │ ┌────────────────────────────┐    │  │
│ │status    ││  │ │ ☑ 자재 발주  [편집] [삭제]  │    │  │
│ └──────────┘│  │ │ Code: MATERIAL              │    │  │
│             │  │ │ 🎨 #3b82f6                 │    │  │
│ ┌──────────┐│  │ └────────────────────────────┘    │  │
│ │우선순위   ││  │                                    │  │
│ │priority  ││  │ ┌────────────────────────────┐    │  │
│ └──────────┘│  │ │ ☑ 외주 발주  [편집] [삭제]  │    │  │
│             │  │ │ Code: OUTSOURCE             │    │  │
│             │  │ │ 🎨 #10b981                 │    │  │
│             │  │ └────────────────────────────┘    │  │
│             │  └────────────────────────────────────┘  │
└──────────────┴──────────────────────────────────────────┘

📊 데이터 흐름

카테고리 값 생성 시

사용자: 카테고리 관리 화면 (menu_id: 103)
  ↓
프론트엔드: addCategoryValue({ ..., menuId: 103 })
  ↓
백엔드: INSERT INTO table_column_category_values (..., menu_id)
        VALUES (..., 103)
  ↓
DB: 
  table_name: purchase_orders
  column_name: order_type
  value_code: MATERIAL
  value_label: 자재 발주
  menu_id: 103  ← 카테고리 관리 화면의 menu_id

카테고리 값 조회 시

사용자: 발주 관리 화면 (menu_id: 101)
  ↓
프론트엔드: getCategoryValues(..., menuId: 101)
  ↓
백엔드: 
  1. getSiblingMenuIds(101)
     → [101, 102, 103, 104]
  2. WHERE menu_id = ANY([101, 102, 103, 104])
  ↓
DB: menu_id가 101, 102, 103, 104인 모든 카테고리 반환
  ↓
결과: 카테고리 관리(103)에서 만든 카테고리도 포함됨 ✅

🚀 다음 단계 (필요 시)

화면관리 시스템 통합 (미완성)

현재 CategoryWidget은 독립 컴포넌트로 생성되었지만, 화면관리 시스템에는 아직 통합되지 않았습니다.

통합을 위해 필요한 작업:

  1. ComponentType에 추가:
// frontend/types/screen.ts
export type ComponentType =
  | "text-input"
  | "code-select"
  | "entity-join"
  | "category-manager"  // ← 추가 필요
  | ...
  1. 위젯 팔레트에 추가:
// frontend/components/screen/WidgetPalette.tsx
{
  type: "category-manager",
  label: "카테고리 관리",
  icon: FolderTree,
}
  1. RealtimePreview 렌더링:
// frontend/components/screen/RealtimePreview.tsx
case "category-manager":
  return (
    <CategoryWidget
      widgetId={widget.id}
      menuId={currentScreen.menuId}
      tableName={currentScreen.tableName}
    />
  );
  1. Config Panel 생성:
  • CategoryManagerConfigPanel.tsx 생성
  • 위젯 설정 옵션 정의

📋 완료 체크리스트

Phase 1: DB 및 백엔드

  • DB 마이그레이션: menu_id 컬럼 추가
  • 외래키 menu_info(menu_id) 추가
  • UNIQUE 제약조건에 menu_id 추가
  • 인덱스 추가
  • 타입에 menuId 추가
  • getSiblingMenuIds() 함수 구현
  • 모든 쿼리에 menu_id 필터링 추가
  • API 파라미터에 menuId 추가

Phase 2: 프론트엔드

  • CategoryWidget 생성
  • CategoryColumnList 생성
  • CategoryValueManager에 menuId props 추가
  • API 클라이언트 수정
  • 타입에 menuId 추가

Phase 3: 화면관리 시스템 통합

  • ComponentType 추가
  • 위젯 팔레트 추가
  • RealtimePreview 렌더링
  • Config Panel 생성

Phase 4: 정리

  • 테이블 타입 관리 Dialog 제거
  • 불필요한 파일 삭제
  • Import 및 상태 제거

📁 파일 목록

생성된 파일

frontend/components/screen/widgets/CategoryWidget.tsx (신규)
frontend/components/table-category/CategoryColumnList.tsx (복원)

수정된 파일

db/migrations/036_create_table_column_category_values.sql
backend-node/src/types/tableCategoryValue.ts
backend-node/src/services/tableCategoryValueService.ts
backend-node/src/controllers/tableCategoryValueController.ts
frontend/components/table-category/CategoryValueManager.tsx
frontend/lib/api/tableCategoryValue.ts
frontend/types/tableCategoryValue.ts
frontend/app/(main)/admin/tableMng/page.tsx

삭제된 파일

frontend/components/table-category/CategoryValueManagerDialog.tsx

🎯 핵심 요약

기존 문제점

  • 카테고리가 전역으로 관리됨
  • 메뉴별 격리가 안됨
  • 테이블 타입 관리에서 직접 관리

해결 방법

  • 메뉴 스코프 도입 (menu_id 컬럼)
  • 형제 메뉴 간 공유 (같은 부모 메뉴만)
  • 화면관리 위젯으로 통합

핵심 로직

// 메뉴 103(카테고리 관리)에서 생성된 카테고리는
// 메뉴 101, 102, 104(형제 메뉴들)에서만 사용 가능
// 다른 부모를 가진 메뉴에서는 사용 불가

🔜 현재 상태

  • DB 및 백엔드: 완전히 구현 완료
  • 프론트엔드 컴포넌트: 완전히 구현 완료
  • 화면관리 시스템 통합: 컴포넌트는 준비되었으나 시스템 통합은 미완성
  • 정리: 불필요한 코드 제거 완료

완료 일시: 2025-11-05