# 카테고리 시스템 재구현 계획서

## 기존 구조의 문제점

### ❌ 잘못 이해한 부분

1. **테이블 타입 관리에서 직접 카테고리 값 관리**

   - 카테고리가 전역으로 관리됨
   - 메뉴별 스코프가 없음

2. **모든 메뉴에서 사용 가능한 전역 카테고리**
   - 구매관리에서 만든 카테고리를 영업관리에서도 사용 가능
   - 메뉴 간 격리가 안됨

## 올바른 구조

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

```
구매관리 (2레벨 메뉴, menu_id: 100)
├── 발주 관리 (menu_id: 101)
├── 입고 관리 (menu_id: 102)
├── 카테고리 관리 (menu_id: 103) ← 여기서 카테고리 생성 (menuId = 103)
└── 거래처 관리 (menu_id: 104)
```

**카테고리 스코프 규칙**:

- 카테고리 관리 화면의 `menu_id = 103`으로 카테고리 생성
- 이 카테고리는 **같은 부모를 가진 형제 메뉴** (101, 102, 103, 104)에서만 사용 가능
- 다른 2레벨 메뉴 (예: 영업관리)의 하위에서는 사용 불가

### ✅ 화면관리 시스템 통합

```
화면 편집기
├── 위젯 팔레트
│   ├── 텍스트 입력
│   ├── 코드 선택
│   ├── 엔티티 조인
│   └── 카테고리 관리 ← 신규 위젯
└── 캔버스
    └── 카테고리 관리 위젯 드래그앤드롭
        ├── 좌측: 현재 화면 테이블의 카테고리 컬럼 목록
        └── 우측: 선택된 컬럼의 카테고리 값 관리
```

---

## 데이터베이스 구조

### table_column_category_values 테이블

```sql
CREATE TABLE table_column_category_values (
  value_id SERIAL PRIMARY KEY,
  table_name VARCHAR(100) NOT NULL,
  column_name VARCHAR(100) NOT NULL,

  -- 값 정보
  value_code VARCHAR(50) NOT NULL,
  value_label VARCHAR(100) NOT NULL,
  value_order INTEGER DEFAULT 0,

  -- 계층 구조
  parent_value_id INTEGER,
  depth INTEGER DEFAULT 1,

  -- 추가 정보
  description TEXT,
  color VARCHAR(20),
  icon VARCHAR(50),
  is_active BOOLEAN DEFAULT true,
  is_default BOOLEAN DEFAULT false,

  -- 멀티테넌시
  company_code VARCHAR(20) NOT NULL,

  -- 메뉴 스코프 (핵심!)
  menu_id INTEGER NOT NULL,

  -- 메타 정보
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  created_by VARCHAR(50),
  updated_by VARCHAR(50),

  FOREIGN KEY (company_code) REFERENCES company_mng(company_code),
  FOREIGN KEY (menu_id) REFERENCES menu_info(menu_id),
  FOREIGN KEY (parent_value_id) REFERENCES table_column_category_values(value_id),
  UNIQUE (table_name, column_name, value_code, menu_id, company_code)
);
```

**변경사항**:

- ✅ `menu_id` 컬럼 추가 (필수)
- ✅ 외래키: `menu_info(menu_id)`
- ✅ UNIQUE 제약조건에 `menu_id` 추가

---

## 백엔드 구현

### 1. 메뉴 스코프 로직

#### 메뉴 계층 구조 조회

```typescript
/**
 * 메뉴의 형제 메뉴 ID 목록 조회
 * (같은 부모를 가진 메뉴들)
 */
async function getSiblingMenuIds(menuId: number): Promise<number[]> {
  const query = `
    WITH RECURSIVE menu_tree AS (
      -- 현재 메뉴
      SELECT menu_id, parent_id, 0 AS level
      FROM menu_info
      WHERE menu_id = $1
      
      UNION ALL
      
      -- 부모로 올라가기
      SELECT m.menu_id, m.parent_id, mt.level + 1
      FROM menu_info m
      INNER JOIN menu_tree mt ON m.menu_id = mt.parent_id
    )
    -- 현재 메뉴의 직접 부모 찾기
    SELECT parent_id FROM menu_tree WHERE level = 1
  `;

  const parentResult = await pool.query(query, [menuId]);

  if (parentResult.rows.length === 0) {
    // 최상위 메뉴인 경우 자기 자신만 반환
    return [menuId];
  }

  const parentId = parentResult.rows[0].parent_id;

  // 같은 부모를 가진 형제 메뉴들 조회
  const siblingsQuery = `
    SELECT menu_id FROM menu_info WHERE parent_id = $1
  `;
  const siblingsResult = await pool.query(siblingsQuery, [parentId]);

  return siblingsResult.rows.map((row) => row.menu_id);
}
```

### 2. API 엔드포인트 수정

#### 기존 API 문제점

```typescript
// ❌ 잘못된 방식: menu_id 없이 조회
GET /api/table-categories/:tableName/:columnName/values
```

#### 올바른 API

```typescript
// ✅ 올바른 방식: menu_id로 필터링
GET /api/table-categories/:tableName/:columnName/values?menuId=103
```

**쿼리 로직**:

```typescript
async getCategoryValues(
  tableName: string,
  columnName: string,
  menuId: number,
  companyCode: string,
  includeInactive: boolean = false
): Promise<TableCategoryValue[]> {
  // 1. 메뉴 스코프 확인: 형제 메뉴들의 카테고리도 포함
  const siblingMenuIds = await this.getSiblingMenuIds(menuId);

  // 2. 카테고리 값 조회
  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 = '*')
      ${!includeInactive ? 'AND is_active = true' : ''}
    ORDER BY value_order, value_label
  `;

  const result = await pool.query(query, [
    tableName,
    columnName,
    siblingMenuIds,
    companyCode,
  ]);

  return result.rows;
}
```

### 3. 카테고리 추가 시 menu_id 저장

```typescript
async addCategoryValue(
  value: TableCategoryValue,
  menuId: number,
  companyCode: string,
  userId: string
): Promise<TableCategoryValue> {
  const query = `
    INSERT INTO table_column_category_values (
      table_name, column_name,
      value_code, value_label, value_order,
      description, color, icon,
      is_active, is_default,
      menu_id, company_code,
      created_by
    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
    RETURNING *
  `;

  const result = await pool.query(query, [
    value.tableName,
    value.columnName,
    value.valueCode,
    value.valueLabel,
    value.valueOrder || 0,
    value.description,
    value.color,
    value.icon,
    value.isActive !== false,
    value.isDefault || false,
    menuId,  // ← 카테고리 관리 화면의 menu_id
    companyCode,
    userId,
  ]);

  return result.rows[0];
}
```

---

## 프론트엔드 구현

### 1. 화면관리 위젯: CategoryWidget

```typescript
// frontend/components/screen/widgets/CategoryWidget.tsx

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

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

  return (
    <div className="flex h-full gap-6">
      {/* 좌측: 카테고리 컬럼 리스트 */}
      <div className="w-[30%] border-r pr-6">
        <CategoryColumnList
          tableName={tableName}
          menuId={menuId}
          selectedColumn={selectedColumn}
          onColumnSelect={setSelectedColumn}
        />
      </div>

      {/* 우측: 카테고리 값 관리 */}
      <div className="w-[70%]">
        {selectedColumn ? (
          <CategoryValueManager
            tableName={tableName}
            columnName={selectedColumn}
            menuId={menuId}
          />
        ) : (
          <EmptyState message="좌측에서 카테고리 컬럼을 선택하세요" />
        )}
      </div>
    </div>
  );
}
```

### 2. 좌측 패널: CategoryColumnList

```typescript
// frontend/components/table-category/CategoryColumnList.tsx

interface CategoryColumnListProps {
  tableName: string;
  menuId: number;
  selectedColumn: string | null;
  onColumnSelect: (columnName: string) => void;
}

export function CategoryColumnList({
  tableName,
  menuId,
  selectedColumn,
  onColumnSelect,
}: CategoryColumnListProps) {
  const [columns, setColumns] = useState<CategoryColumn[]>([]);

  useEffect(() => {
    loadCategoryColumns();
  }, [tableName, menuId]);

  const loadCategoryColumns = async () => {
    // table_type_columns에서 input_type = 'category'인 컬럼 조회
    const response = await apiClient.get(
      `/table-management/tables/${tableName}/columns`
    );

    const categoryColumns = response.data.columns.filter(
      (col: any) => col.inputType === "category"
    );

    setColumns(categoryColumns);
  };

  return (
    <div className="space-y-3">
      <h3 className="text-lg font-semibold">카테고리 컬럼</h3>
      <div className="space-y-2">
        {columns.map((column) => (
          <div
            key={column.columnName}
            onClick={() => onColumnSelect(column.columnName)}
            className={`cursor-pointer rounded-lg border p-4 transition-all ${
              selectedColumn === column.columnName
                ? "border-primary bg-primary/10"
                : "hover:bg-muted/50"
            }`}
          >
            <h4 className="text-sm font-semibold">{column.columnLabel}</h4>
            <p className="text-xs text-muted-foreground mt-1">
              {column.columnName}
            </p>
          </div>
        ))}
      </div>
    </div>
  );
}
```

### 3. 우측 패널: CategoryValueManager (수정)

```typescript
// frontend/components/table-category/CategoryValueManager.tsx

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

export function CategoryValueManager({
  tableName,
  columnName,
  menuId,
  columnLabel,
}: CategoryValueManagerProps) {
  const [values, setValues] = useState<TableCategoryValue[]>([]);

  useEffect(() => {
    loadCategoryValues();
  }, [tableName, columnName, menuId]);

  const loadCategoryValues = async () => {
    const response = await getCategoryValues(
      tableName,
      columnName,
      menuId // ← menuId 전달
    );

    if (response.success && response.data) {
      setValues(response.data);
    }
  };

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

    if (response.success) {
      loadCategoryValues();
      toast.success("카테고리 값이 추가되었습니다");
    }
  };

  // ... 나머지 CRUD 로직
}
```

### 4. API 클라이언트 수정

```typescript
// frontend/lib/api/tableCategoryValue.ts

export async function getCategoryValues(
  tableName: string,
  columnName: string,
  menuId: number, // ← 추가
  includeInactive: boolean = false
) {
  try {
    const response = await apiClient.get<{
      success: boolean;
      data: TableCategoryValue[];
    }>(`/table-categories/${tableName}/${columnName}/values`, {
      params: { menuId, includeInactive }, // ← menuId 쿼리 파라미터
    });
    return response.data;
  } catch (error: any) {
    console.error("카테고리 값 조회 실패:", error);
    return { success: false, error: error.message };
  }
}
```

---

## 화면관리 시스템 통합

### 1. ComponentType에 추가

```typescript
// frontend/types/screen.ts

export type ComponentType =
  | "text-input"
  | "code-select"
  | "entity-join"
  | "category-manager"  // ← 신규
  | "number-input"
  | ...
```

### 2. 위젯 팔레트에 추가

```typescript
// frontend/components/screen/WidgetPalette.tsx

const WIDGET_CATEGORIES = {
  input: [
    { type: "text-input", label: "텍스트 입력", icon: Type },
    { type: "number-input", label: "숫자 입력", icon: Hash },
    // ...
  ],
  reference: [
    { type: "code-select", label: "코드 선택", icon: Code },
    { type: "entity-join", label: "엔티티 조인", icon: Database },
    { type: "category-manager", label: "카테고리 관리", icon: FolderTree }, // ← 신규
  ],
  // ...
};
```

### 3. RealtimePreview에 렌더링 추가

```typescript
// frontend/components/screen/RealtimePreview.tsx

function renderWidget(widget: ScreenWidget) {
  switch (widget.type) {
    case "text-input":
      return <TextInputWidget {...widget} />;
    case "code-select":
      return <CodeSelectWidget {...widget} />;
    case "category-manager": // ← 신규
      return (
        <CategoryWidget
          widgetId={widget.id}
          config={widget.config}
          menuId={currentScreen.menuId}
          tableName={currentScreen.tableName}
        />
      );
    // ...
  }
}
```

---

## 테이블 타입 관리 통합 제거

### 기존 코드 제거

1. **`app/(main)/admin/tableMng/page.tsx`에서 제거**:

   - "카테고리 값 관리" 버튼 제거
   - CategoryValueManagerDialog import 제거
   - 관련 상태 및 핸들러 제거

2. **`CategoryValueManagerDialog.tsx` 삭제**:
   - Dialog 래퍼 컴포넌트 삭제

**이유**: 카테고리는 화면관리 시스템에서만 관리해야 함

---

## 사용 시나리오

### 1. 카테고리 관리 화면 생성

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

### 2. 카테고리 값 등록

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

   - `order_type` (발주 유형)
   - `order_status` (발주 상태)
   - `priority` (우선순위)

2. **컬럼 선택**: `order_type` 클릭

3. **우측 패널**: 카테고리 값 관리
   - "추가" 버튼 클릭
   - 코드: `MATERIAL`, 라벨: `자재 발주`
   - 색상: `#3b82f6`, 설명: `생산 자재 발주`
   - **저장 시 `menu_id = 103`으로 자동 저장됨**

### 3. 다른 화면에서 카테고리 사용

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

   - `order_type` 컬럼을 Code Select 위젯으로 배치
   - 드롭다운에 `자재 발주`, `외주 발주` 등 표시됨 ✅

2. **영업관리 > 주문 관리** (다른 2레벨 메뉴)
   - 같은 `order_type` 컬럼이 있어도
   - 구매관리의 카테고리는 표시되지 않음 ❌
   - 영업관리 자체 카테고리만 사용 가능

---

## 마이그레이션 작업

### 1. DB 마이그레이션 실행

```bash
psql -U postgres -d plm < db/migrations/036_create_table_column_category_values.sql
```

### 2. 기존 카테고리 데이터 마이그레이션

```sql
-- 기존 데이터에 menu_id 추가 (임시로 1번 메뉴로 설정)
ALTER TABLE table_column_category_values
ADD COLUMN IF NOT EXISTS menu_id INTEGER DEFAULT 1;

-- 외래키 추가
ALTER TABLE table_column_category_values
ADD CONSTRAINT fk_category_value_menu
FOREIGN KEY (menu_id) REFERENCES menu_info(menu_id);
```

---

## 구현 순서

### Phase 1: DB 및 백엔드 (1-2시간)

1. ✅ DB 마이그레이션: `menu_id` 컬럼 추가
2. ⏳ 백엔드 타입 수정: `menuId` 필드 추가
3. ⏳ 백엔드 서비스: 메뉴 스코프 로직 구현
4. ⏳ API 컨트롤러: `menuId` 파라미터 추가

### Phase 2: 프론트엔드 컴포넌트 (2-3시간)

5. ⏳ CategoryWidget 생성 (좌우 분할)
6. ⏳ CategoryColumnList 복원 및 수정
7. ⏳ CategoryValueManager에 `menuId` 추가
8. ⏳ API 클라이언트 수정

### Phase 3: 화면관리 시스템 통합 (1-2시간)

9. ⏳ ComponentType에 `category-manager` 추가
10. ⏳ 위젯 팔레트에 추가
11. ⏳ RealtimePreview 렌더링 추가
12. ⏳ Config Panel 생성

### Phase 4: 정리 (30분)

13. ⏳ 테이블 타입 관리에서 카테고리 Dialog 제거
14. ⏳ 불필요한 파일 제거
15. ⏳ 테스트 및 문서화

---

## 예상 소요 시간

- **Phase 1**: 1-2시간
- **Phase 2**: 2-3시간
- **Phase 3**: 1-2시간
- **Phase 4**: 30분
- **총 예상 시간**: 5-8시간

---

## 완료 체크리스트

### DB

- [ ] `menu_id` 컬럼 추가
- [ ] 외래키 `menu_info(menu_id)` 추가
- [ ] UNIQUE 제약조건에 `menu_id` 추가
- [ ] 인덱스 추가

### 백엔드

- [ ] 타입에 `menuId` 추가
- [ ] `getSiblingMenuIds()` 함수 구현
- [ ] 모든 쿼리에 `menu_id` 필터링 추가
- [ ] API 파라미터에 `menuId` 추가

### 프론트엔드

- [ ] CategoryWidget 생성
- [ ] CategoryColumnList 수정
- [ ] CategoryValueManager에 `menuId` props 추가
- [ ] API 클라이언트 수정

### 화면관리 시스템

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

### 정리

- [ ] 테이블 타입 관리 Dialog 제거
- [ ] 불필요한 파일 삭제
- [ ] 테스트
- [ ] 문서 작성

---

지금 바로 구현을 시작할까요?
