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

## 🎯 핵심 개념

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

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

---

## ✅ 완료된 작업

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

#### 📊 테이블 수정: `table_column_category_values`

**추가된 컬럼**:
```sql
menu_id INTEGER NOT NULL  -- 메뉴 스코프
```

**외래키 추가**:
```sql
CONSTRAINT fk_category_value_menu FOREIGN KEY (menu_id) 
  REFERENCES menu_info(menu_id)
```

**UNIQUE 제약조건 변경**:
```sql
-- 변경 전
UNIQUE (table_name, column_name, value_code, company_code)

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

**인덱스 추가**:
```sql
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`**:
```typescript
export interface TableCategoryValue {
  // ... 기존 필드
  menuId: number;  // ← 추가
  // ...
}
```

#### 🎛️ 서비스 로직 추가

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

1. **형제 메뉴 조회 함수**:
```typescript
async getSiblingMenuIds(menuId: number): Promise<number[]> {
  // 1. 현재 메뉴의 부모 ID 조회
  // 2. 같은 부모를 가진 형제 메뉴들 조회
  // 3. 형제 메뉴 ID 배열 반환
}
```

2. **카테고리 값 조회 수정**:
```typescript
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) 조건으로 필터링
}
```

3. **카테고리 값 추가 수정**:
```typescript
async addCategoryValue(value: TableCategoryValue, ...): Promise<TableCategoryValue> {
  // INSERT 시 menu_id 포함
  // VALUES (..., $13, ...)  // value.menuId
}
```

#### 🎮 컨트롤러 수정

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

```typescript
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`**:

```typescript
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`**:

```typescript
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`**:

```typescript
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`**:

```typescript
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 제거**:
```typescript
// ❌ 제거됨
import { CategoryValueManagerDialog } from "@/components/table-category/CategoryValueManagerDialog";
```

2. **상태 제거**:
```typescript
// ❌ 제거됨
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [categoryDialogData, setCategoryDialogData] = useState<...>(null);
```

3. **버튼 제거**:
```typescript
// ❌ 제거됨: "카테고리 값 관리" 버튼
{column.inputType === "category" && (
  <Button onClick={() => setCategoryDialogOpen(true)}>
    카테고리 값 관리
  </Button>
)}
```

4. **Dialog 렌더링 제거**:
```typescript
// ❌ 제거됨
{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` 컬럼이 있어도
- 구매관리의 카테고리는 표시되지 않음 ❌
- **이유**: 다른 부모 메뉴이므로 스코프가 다름

---

## 🔍 메뉴 스코프 로직 상세

### 백엔드 로직

```typescript
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]);
}
```

### 프론트엔드 호출

```typescript
// 발주 관리 화면 (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에 추가**:
```typescript
// frontend/types/screen.ts
export type ComponentType =
  | "text-input"
  | "code-select"
  | "entity-join"
  | "category-manager"  // ← 추가 필요
  | ...
```

2. **위젯 팔레트에 추가**:
```typescript
// frontend/components/screen/WidgetPalette.tsx
{
  type: "category-manager",
  label: "카테고리 관리",
  icon: FolderTree,
}
```

3. **RealtimePreview 렌더링**:
```typescript
// frontend/components/screen/RealtimePreview.tsx
case "category-manager":
  return (
    <CategoryWidget
      widgetId={widget.id}
      menuId={currentScreen.menuId}
      tableName={currentScreen.tableName}
    />
  );
```

4. **Config Panel 생성**:
- `CategoryManagerConfigPanel.tsx` 생성
- 위젯 설정 옵션 정의

---

## 📋 완료 체크리스트

### Phase 1: DB 및 백엔드 ✅
- [x] DB 마이그레이션: `menu_id` 컬럼 추가
- [x] 외래키 `menu_info(menu_id)` 추가
- [x] UNIQUE 제약조건에 `menu_id` 추가
- [x] 인덱스 추가
- [x] 타입에 `menuId` 추가
- [x] `getSiblingMenuIds()` 함수 구현
- [x] 모든 쿼리에 `menu_id` 필터링 추가
- [x] API 파라미터에 `menuId` 추가

### Phase 2: 프론트엔드 ✅
- [x] CategoryWidget 생성
- [x] CategoryColumnList 생성
- [x] CategoryValueManager에 `menuId` props 추가
- [x] API 클라이언트 수정
- [x] 타입에 `menuId` 추가

### Phase 3: 화면관리 시스템 통합 ⏳
- [ ] ComponentType 추가
- [ ] 위젯 팔레트 추가
- [ ] RealtimePreview 렌더링
- [ ] Config Panel 생성

### Phase 4: 정리 ✅
- [x] 테이블 타입 관리 Dialog 제거
- [x] 불필요한 파일 삭제
- [x] 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` 컬럼)
- ✅ **형제 메뉴 간 공유** (같은 부모 메뉴만)
- ✅ **화면관리 위젯**으로 통합

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

---

## 🔜 현재 상태

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

---

완료 일시: 2025-11-05

