# 카테고리 컴포넌트 메뉴 기반 전환 계획서

## 📋 현재 문제점

### 테이블 기반 스코프의 한계

**현재 상황**:

- 카테고리와 채번 컴포넌트가 **테이블 기준**으로 데이터를 불러옴
- `table_column_category_values` 테이블에서 `table_name + column_name`으로 카테고리 조회

**문제 발생**:

```
영업관리 (menu_id: 200)
├── 고객관리 (menu_id: 201) - 테이블: customer_info
├── 계약관리 (menu_id: 202) - 테이블: contract_info
├── 주문관리 (menu_id: 203) - 테이블: order_info
└── 영업관리 공통코드 (menu_id: 204) - 어떤 테이블 선택?
```

**문제**:

- 영업관리 전체에서 사용할 공통 코드/카테고리를 관리하고 싶은데
- 각 하위 메뉴가 서로 다른 테이블을 사용하므로
- 특정 테이블 하나를 선택하면 다른 메뉴에서 사용할 수 없음

### 예시: 영업관리 공통 코드 관리 불가

**원하는 동작**:

- "영업관리 > 공통코드 관리" 메뉴에서 카테고리 생성
- 이 카테고리는 영업관리의 **모든 하위 메뉴**에서 사용 가능
- 고객관리, 계약관리, 주문관리 화면 모두에서 같은 카테고리 공유

**현재 동작**:

- 테이블별로 카테고리가 격리됨
- `customer_info` 테이블의 카테고리는 `contract_info`에서 사용 불가
- 각 테이블마다 동일한 카테고리를 중복 생성해야 함 (비효율)

---

## ✅ 해결 방안: 메뉴 기반 스코프

### 핵심 개념

**메뉴 계층 구조를 카테고리 스코프로 사용**:

- 카테고리를 생성할 때 `menu_id`를 기록
- 같은 부모 메뉴를 가진 **형제 메뉴들**이 카테고리를 공유
- 테이블과 무관하게 메뉴 구조에 따라 스코프 결정

### 메뉴 스코프 규칙

```
영업관리 (parent_id: 0, menu_id: 200)
├── 고객관리 (parent_id: 200, menu_id: 201)
├── 계약관리 (parent_id: 200, menu_id: 202)
├── 주문관리 (parent_id: 200, menu_id: 203)
└── 공통코드 관리 (parent_id: 200, menu_id: 204) ← 여기서 카테고리 생성
```

**스코프 규칙**:

- 204번 메뉴에서 카테고리 생성 → `menu_id = 204`로 저장
- 형제 메뉴 (201, 202, 203, 204)에서 **모두 사용 가능**
- 다른 부모의 메뉴 (예: 구매관리)에서는 사용 불가

---

## 📐 데이터베이스 설계

### 기존 테이블 수정

```sql
-- table_column_category_values 테이블에 menu_id 추가
ALTER TABLE table_column_category_values
ADD COLUMN menu_id INTEGER;

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

-- UNIQUE 제약조건 수정 (menu_id 추가)
ALTER TABLE table_column_category_values
DROP CONSTRAINT IF EXISTS unique_category_value;

ALTER TABLE table_column_category_values
ADD CONSTRAINT unique_category_value
UNIQUE (table_name, column_name, value_code, menu_id, company_code);

-- 인덱스 추가
CREATE INDEX idx_category_value_menu
ON table_column_category_values(menu_id, table_name, column_name, company_code);
```

### 필드 설명

| 필드           | 설명                     | 예시                  |
| -------------- | ------------------------ | --------------------- |
| `table_name`   | 어떤 테이블의 컬럼인지   | `customer_info`       |
| `column_name`  | 어떤 컬럼의 값인지       | `customer_type`       |
| `menu_id`      | 어느 메뉴에서 생성했는지 | `204` (공통코드 관리) |
| `company_code` | 멀티테넌시               | `COMPANY_A`           |

---

## 🔧 백엔드 구현

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

#### 형제 메뉴 조회 함수

```typescript
// backend-node/src/services/menuService.ts

/**
 * 메뉴의 형제 메뉴 ID 목록 조회
 * (같은 부모를 가진 메뉴들)
 */
export async function getSiblingMenuIds(menuId: number): Promise<number[]> {
  const pool = getPool();

  // 1. 현재 메뉴의 부모 찾기
  const parentQuery = `
    SELECT parent_id FROM menu_info WHERE menu_id = $1
  `;
  const parentResult = await pool.query(parentQuery, [menuId]);

  if (parentResult.rows.length === 0) {
    return [menuId]; // 메뉴가 없으면 자기 자신만
  }

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

  if (!parentId || parentId === 0) {
    // 최상위 메뉴인 경우 자기 자신만
    return [menuId];
  }

  // 2. 같은 부모를 가진 형제 메뉴들 조회
  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 수정

#### 서비스 로직 수정

```typescript
// backend-node/src/services/tableCategoryValueService.ts

/**
 * 카테고리 값 목록 조회 (메뉴 스코프 적용)
 */
async getCategoryValues(
  tableName: string,
  columnName: string,
  menuId: number, // ← 추가
  companyCode: string,
  includeInactive: boolean = false
): Promise<TableCategoryValue[]> {
  logger.info("카테고리 값 조회 (메뉴 스코프)", {
    tableName,
    columnName,
    menuId,
    companyCode,
  });

  const pool = getPool();

  // 1. 형제 메뉴 ID 조회
  const siblingMenuIds = await getSiblingMenuIds(menuId);

  logger.info("형제 메뉴 ID 목록", { menuId, siblingMenuIds });

  // 2. 카테고리 값 조회
  let query: string;
  let params: any[];

  if (companyCode === "*") {
    // 최고 관리자: 모든 회사 데이터 조회
    query = `
      SELECT
        value_id AS "valueId",
        table_name AS "tableName",
        column_name AS "columnName",
        value_code AS "valueCode",
        value_label AS "valueLabel",
        value_order AS "valueOrder",
        parent_value_id AS "parentValueId",
        depth,
        description,
        color,
        icon,
        is_active AS "isActive",
        is_default AS "isDefault",
        company_code AS "companyCode",
        menu_id AS "menuId",
        created_at AS "createdAt",
        created_by AS "createdBy"
      FROM table_column_category_values
      WHERE table_name = $1
        AND column_name = $2
        AND menu_id = ANY($3)  -- ← 형제 메뉴 포함
        ${!includeInactive ? 'AND is_active = true' : ''}
      ORDER BY value_order, value_label
    `;
    params = [tableName, columnName, siblingMenuIds];
  } else {
    // 일반 회사: 자신의 데이터만 조회
    query = `
      SELECT
        value_id AS "valueId",
        table_name AS "tableName",
        column_name AS "columnName",
        value_code AS "valueCode",
        value_label AS "valueLabel",
        value_order AS "valueOrder",
        parent_value_id AS "parentValueId",
        depth,
        description,
        color,
        icon,
        is_active AS "isActive",
        is_default AS "isDefault",
        company_code AS "companyCode",
        menu_id AS "menuId",
        created_at AS "createdAt",
        created_by AS "createdBy"
      FROM table_column_category_values
      WHERE table_name = $1
        AND column_name = $2
        AND menu_id = ANY($3)  -- ← 형제 메뉴 포함
        AND company_code = $4  -- ← 회사별 필터링
        ${!includeInactive ? 'AND is_active = true' : ''}
      ORDER BY value_order, value_label
    `;
    params = [tableName, columnName, siblingMenuIds, companyCode];
  }

  const result = await pool.query(query, params);

  logger.info(`카테고리 값 ${result.rows.length}개 조회 완료`);

  return result.rows;
}
```

### 3. 카테고리 값 추가 API 수정

```typescript
/**
 * 카테고리 값 추가 (menu_id 저장)
 */
async addCategoryValue(
  value: TableCategoryValue,
  menuId: number, // ← 추가
  companyCode: string,
  userId: string
): Promise<TableCategoryValue> {
  logger.info("카테고리 값 추가 (메뉴 스코프)", {
    tableName: value.tableName,
    columnName: value.columnName,
    valueCode: value.valueCode,
    menuId,
    companyCode,
  });

  const pool = getPool();

  const query = `
    INSERT INTO table_column_category_values (
      table_name, column_name,
      value_code, value_label, value_order,
      parent_value_id, depth,
      description, color, icon,
      is_active, is_default,
      company_code, menu_id,  -- ← menu_id 추가
      created_by
    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
    RETURNING
      value_id AS "valueId",
      table_name AS "tableName",
      column_name AS "columnName",
      value_code AS "valueCode",
      value_label AS "valueLabel",
      value_order AS "valueOrder",
      parent_value_id AS "parentValueId",
      depth,
      description,
      color,
      icon,
      is_active AS "isActive",
      is_default AS "isDefault",
      company_code AS "companyCode",
      menu_id AS "menuId",
      created_at AS "createdAt",
      created_by AS "createdBy"
  `;

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

  logger.info("카테고리 값 추가 성공", {
    valueId: result.rows[0].valueId,
    menuId,
  });

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

### 4. 컨트롤러 수정

```typescript
// backend-node/src/controllers/tableCategoryValueController.ts

/**
 * 카테고리 값 목록 조회
 */
export async function getCategoryValues(
  req: AuthenticatedRequest,
  res: Response
): Promise<void> {
  try {
    const { tableName, columnName } = req.params;
    const { menuId, includeInactive } = req.query; // ← menuId 추가
    const companyCode = req.user!.companyCode;

    if (!menuId) {
      res.status(400).json({
        success: false,
        message: "menuId는 필수입니다",
      });
      return;
    }

    const service = new TableCategoryValueService();
    const values = await service.getCategoryValues(
      tableName,
      columnName,
      Number(menuId), // ← menuId 전달
      companyCode,
      includeInactive === "true"
    );

    res.json({
      success: true,
      data: values,
    });
  } catch (error: any) {
    logger.error("카테고리 값 조회 실패:", error);
    res.status(500).json({
      success: false,
      message: "카테고리 값 조회 중 오류 발생",
      error: error.message,
    });
  }
}

/**
 * 카테고리 값 추가
 */
export async function addCategoryValue(
  req: AuthenticatedRequest,
  res: Response
): Promise<void> {
  try {
    const { menuId, ...value } = req.body; // ← menuId 추가
    const companyCode = req.user!.companyCode;
    const userId = req.user!.userId;

    if (!menuId) {
      res.status(400).json({
        success: false,
        message: "menuId는 필수입니다",
      });
      return;
    }

    const service = new TableCategoryValueService();
    const newValue = await service.addCategoryValue(
      value,
      menuId, // ← menuId 전달
      companyCode,
      userId
    );

    res.json({
      success: true,
      data: newValue,
    });
  } catch (error: any) {
    logger.error("카테고리 값 추가 실패:", error);
    res.status(500).json({
      success: false,
      message: "카테고리 값 추가 중 오류 발생",
      error: error.message,
    });
  }
}
```

---

## 🎨 프론트엔드 구현

### 1. 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, // ← menuId 쿼리 파라미터 추가
        includeInactive,
      },
    });
    return response.data;
  } catch (error: any) {
    console.error("카테고리 값 조회 실패:", error);
    return { success: false, error: error.message };
  }
}

/**
 * 카테고리 값 추가
 */
export async function addCategoryValue(
  value: TableCategoryValue,
  menuId: number // ← 추가
) {
  try {
    const response = await apiClient.post<{
      success: boolean;
      data: TableCategoryValue;
    }>("/table-categories/values", {
      ...value,
      menuId, // ← menuId 포함
    });
    return response.data;
  } catch (error: any) {
    console.error("카테고리 값 추가 실패:", error);
    return { success: false, error: error.message };
  }
}
```

### 2. CategoryColumnList 컴포넌트 수정

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

interface CategoryColumnListProps {
  tableName: string;
  menuId: number; // ← 추가
  selectedColumn: string | null;
  onColumnSelect: (columnName: string, columnLabel: string) => void;
}

export function CategoryColumnList({
  tableName,
  menuId, // ← 추가
  selectedColumn,
  onColumnSelect,
}: CategoryColumnListProps) {
  const [columns, setColumns] = useState<CategoryColumn[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    loadCategoryColumns();
  }, [tableName, menuId]); // ← menuId 의존성 추가

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

      const allColumns = Array.isArray(response.data)
        ? response.data
        : response.data.data?.columns || [];

      // category 타입만 필터링
      const categoryColumns = allColumns.filter(
        (col: any) =>
          col.inputType === "category" || col.input_type === "category"
      );

      const columnsWithCount = await Promise.all(
        categoryColumns.map(async (col: any) => {
          const colName = col.columnName || col.column_name;
          const colLabel = col.columnLabel || col.column_label || colName;

          // 각 컬럼의 값 개수 가져오기 (menuId 전달)
          let valueCount = 0;
          try {
            const valuesResult = await getCategoryValues(
              tableName,
              colName,
              menuId, // ← menuId 전달
              false
            );
            if (valuesResult.success && valuesResult.data) {
              valueCount = valuesResult.data.length;
            }
          } catch (error) {
            console.error(`항목 개수 조회 실패 (${colName}):`, error);
          }

          return {
            columnName: colName,
            columnLabel: colLabel,
            inputType: col.inputType || col.input_type,
            valueCount,
          };
        })
      );

      setColumns(columnsWithCount);

      // 첫 번째 컬럼 자동 선택
      if (columnsWithCount.length > 0 && !selectedColumn) {
        const firstCol = columnsWithCount[0];
        onColumnSelect(firstCol.columnName, firstCol.columnLabel);
      }
    } catch (error) {
      console.error("❌ 카테고리 컬럼 조회 실패:", error);
      setColumns([]);
    } finally {
      setIsLoading(false);
    }
  };

  // ... 나머지 렌더링 로직
}
```

### 3. CategoryValueManager 컴포넌트 수정

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

interface CategoryValueManagerProps {
  tableName: string;
  columnName: string;
  menuId: number; // ← 추가
  columnLabel?: string;
  onValueCountChange?: (count: number) => void;
}

export function CategoryValueManager({
  tableName,
  columnName,
  menuId, // ← 추가
  columnLabel,
  onValueCountChange,
}: CategoryValueManagerProps) {
  const [values, setValues] = useState<TableCategoryValue[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    loadCategoryValues();
  }, [tableName, columnName, menuId]); // ← menuId 의존성 추가

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

      if (response.success && response.data) {
        setValues(response.data);
        onValueCountChange?.(response.data.length);
      }
    } catch (error) {
      console.error("카테고리 값 조회 실패:", error);
    } finally {
      setIsLoading(false);
    }
  };

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

      if (response.success) {
        loadCategoryValues();
        toast.success("카테고리 값이 추가되었습니다");
      }
    } catch (error) {
      console.error("카테고리 값 추가 실패:", error);
      toast.error("카테고리 값 추가 중 오류가 발생했습니다");
    }
  };

  // ... 나머지 CRUD 로직 (menuId를 항상 포함)
}
```

### 4. 화면관리 시스템에서 menuId 전달

#### 화면 디자이너에서 menuId 추출

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

export function ScreenDesigner() {
  const [selectedScreen, setSelectedScreen] = useState<Screen | null>(null);

  // 선택된 화면의 menuId 추출
  const currentMenuId = selectedScreen?.menuId;

  // CategoryWidget 렌더링 시 menuId 전달
  return (
    <div>
      {/* ... */}
      <CategoryWidget
        tableName={selectedScreen?.tableName}
        menuId={currentMenuId} // ← menuId 전달
      />
    </div>
  );
}
```

#### CategoryWidget 컴포넌트 (신규 또는 수정)

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

interface CategoryWidgetProps {
  tableName: string;
  menuId: number; // ← 추가
}

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

  const handleColumnSelect = (columnName: string, columnLabel: string) => {
    setSelectedColumn(columnName);
    setSelectedColumnLabel(columnLabel);
  };

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

      {/* 우측: 카테고리 값 관리 */}
      <div className="w-[70%]">
        {selectedColumn ? (
          <CategoryValueManager
            tableName={tableName}
            columnName={selectedColumn}
            menuId={menuId} // ← menuId 전달
            columnLabel={selectedColumnLabel}
          />
        ) : (
          <div className="flex items-center justify-center py-12 text-center">
            <p className="text-sm text-muted-foreground">
              좌측에서 카테고리 컬럼을 선택하세요
            </p>
          </div>
        )}
      </div>
    </div>
  );
}
```

---

## 🔄 기존 데이터 마이그레이션

### 마이그레이션 스크립트

```sql
-- db/migrations/047_add_menu_id_to_category_values.sql

-- 1. menu_id 컬럼 추가 (NULL 허용)
ALTER TABLE table_column_category_values
ADD COLUMN IF NOT EXISTS menu_id INTEGER;

-- 2. 기존 데이터에 임시 menu_id 설정
-- (관리자가 수동으로 올바른 menu_id로 변경해야 함)
UPDATE table_column_category_values
SET menu_id = 1
WHERE menu_id IS NULL;

-- 3. menu_id를 NOT NULL로 변경
ALTER TABLE table_column_category_values
ALTER COLUMN menu_id SET NOT NULL;

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

-- 5. UNIQUE 제약조건 재생성
ALTER TABLE table_column_category_values
DROP CONSTRAINT IF EXISTS unique_category_value;

ALTER TABLE table_column_category_values
ADD CONSTRAINT unique_category_value
UNIQUE (table_name, column_name, value_code, menu_id, company_code);

-- 6. 인덱스 추가
CREATE INDEX idx_category_value_menu
ON table_column_category_values(menu_id, table_name, column_name, company_code);

COMMENT ON COLUMN table_column_category_values.menu_id IS '카테고리를 생성한 메뉴 ID (형제 메뉴에서 공유)';
```

---

## 📊 사용 시나리오

### 시나리오: 영업관리 공통코드 관리

#### 1단계: 메뉴 구조

```
영업관리 (parent_id: 0, menu_id: 200)
├── 고객관리 (parent_id: 200, menu_id: 201) - customer_info 테이블
├── 계약관리 (parent_id: 200, menu_id: 202) - contract_info 테이블
├── 주문관리 (parent_id: 200, menu_id: 203) - order_info 테이블
└── 공통코드 관리 (parent_id: 200, menu_id: 204) - 카테고리 관리 전용
```

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

1. **메뉴 등록**: 영업관리 > 공통코드 관리 (menu_id: 204)
2. **화면 생성**: 화면관리 시스템에서 화면 생성
3. **테이블 선택**: 영업관리에서 사용할 **아무 테이블** (예: `customer_info`)
   - 테이블 선택은 컬럼 목록을 가져오기 위한 것일 뿐
   - 실제 스코프는 `menu_id`로 결정됨
4. **위젯 배치**: 카테고리 관리 위젯 드래그앤드롭

#### 3단계: 카테고리 값 등록

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

   - `customer_type` (고객 유형)
   - `customer_grade` (고객 등급)

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

3. **우측 패널**: 카테고리 값 관리
   - 추가 버튼 클릭
   - 코드: `REGULAR`, 라벨: `일반 고객`
   - 색상: `#3b82f6`
   - **저장 시 `menu_id = 204`로 자동 저장됨**

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

##### ✅ 형제 메뉴에서 사용 가능

**고객관리 화면** (menu_id: 201):

- `customer_type` 컬럼을 category-select 위젯으로 배치
- 드롭다운에 `일반 고객`, `VIP 고객` 등 표시됨 ✅
- **이유**: 201과 204는 같은 부모(200)를 가진 형제 메뉴

**계약관리 화면** (menu_id: 202):

- `contract_info` 테이블에 `customer_type` 컬럼이 있다면
- 동일한 카테고리 값 사용 가능 ✅
- **이유**: 202와 204도 형제 메뉴

**주문관리 화면** (menu_id: 203):

- `order_info` 테이블에 `customer_type` 컬럼이 있다면
- 동일한 카테고리 값 사용 가능 ✅
- **이유**: 203과 204도 형제 메뉴

##### ❌ 다른 부모 메뉴에서 사용 불가

**구매관리 > 발주관리** (parent_id: 300):

- `purchase_orders` 테이블에 `customer_type` 컬럼이 있어도
- 영업관리의 카테고리는 표시되지 않음 ❌
- **이유**: 다른 부모 메뉴이므로 스코프가 다름
- 구매관리는 자체 카테고리를 별도로 생성해야 함

---

## 📝 구현 순서

### Phase 1: 데이터베이스 마이그레이션 (30분)

1. ✅ 마이그레이션 파일 작성 (`047_add_menu_id_to_category_values.sql`)
2. ⏳ DB 마이그레이션 실행
3. ⏳ 기존 데이터 임시 menu_id 설정 (관리자 수동 정리 필요)

### Phase 2: 백엔드 구현 (2-3시간)

4. ⏳ `menuService.ts`에 `getSiblingMenuIds()` 함수 추가
5. ⏳ `tableCategoryValueService.ts`에 menu_id 로직 추가
   - `getCategoryValues()` 메서드에 menuId 파라미터 추가
   - `addCategoryValue()` 메서드에 menuId 파라미터 추가
6. ⏳ `tableCategoryValueController.ts` 수정
   - 쿼리 파라미터에서 menuId 추출
   - 서비스 호출 시 menuId 전달
7. ⏳ 백엔드 테스트

### Phase 3: 프론트엔드 API 클라이언트 (30분)

8. ⏳ `frontend/lib/api/tableCategoryValue.ts` 수정
   - `getCategoryValues()` 함수에 menuId 파라미터 추가
   - `addCategoryValue()` 함수에 menuId 파라미터 추가

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

9. ⏳ `CategoryColumnList.tsx` 수정
   - props에 `menuId` 추가
   - `getCategoryValues()` 호출 시 menuId 전달
10. ⏳ `CategoryValueManager.tsx` 수정
    - props에 `menuId` 추가
    - 모든 API 호출 시 menuId 전달
11. ⏳ `CategoryWidget.tsx` 수정 또는 신규 생성
    - `menuId` prop 추가
    - 하위 컴포넌트에 menuId 전달

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

12. ⏳ 화면 정보에서 menuId 추출 로직 추가
13. ⏳ CategoryWidget에 menuId 전달
14. ⏳ 카테고리 관리 화면 테스트

### Phase 6: 테스트 및 문서화 (1시간)

15. ⏳ 전체 플로우 테스트
16. ⏳ 메뉴 스코프 동작 검증
17. ⏳ 사용 가이드 작성

---

## 🧪 테스트 체크리스트

### 백엔드 테스트

- [ ] `getSiblingMenuIds()` 함수가 올바른 형제 메뉴 반환
- [ ] 최상위 메뉴의 경우 자기 자신만 반환
- [ ] 카테고리 값 조회 시 형제 메뉴의 값도 포함
- [ ] 다른 부모 메뉴의 카테고리는 조회되지 않음
- [ ] 멀티테넌시 필터링 정상 작동

### 프론트엔드 테스트

- [ ] 카테고리 컬럼 목록 정상 표시
- [ ] 카테고리 값 목록 정상 표시 (형제 메뉴 포함)
- [ ] 카테고리 값 추가 시 menuId 포함
- [ ] 카테고리 값 수정/삭제 정상 작동

### 통합 테스트

- [ ] 영업관리 > 공통코드 관리에서 카테고리 생성
- [ ] 영업관리 > 고객관리에서 카테고리 사용 가능
- [ ] 영업관리 > 계약관리에서 카테고리 사용 가능
- [ ] 구매관리에서는 영업관리 카테고리 사용 불가

---

## 📦 예상 소요 시간

| Phase            | 작업 내용           | 예상 시간    |
| ---------------- | ------------------- | ------------ |
| Phase 1          | DB 마이그레이션     | 30분         |
| Phase 2          | 백엔드 구현         | 2-3시간      |
| Phase 3          | API 클라이언트      | 30분         |
| Phase 4          | 프론트엔드 컴포넌트 | 2-3시간      |
| Phase 5          | 화면관리 통합       | 1-2시간      |
| Phase 6          | 테스트 및 문서      | 1시간        |
| **총 예상 시간** |                     | **7-11시간** |

---

## 💡 이점

### 1. 메뉴별 독립 관리

- 영업관리, 구매관리, 생산관리 등 각 부서별 카테고리 독립 관리
- 부서 간 카테고리 충돌 방지

### 2. 형제 메뉴 간 공유

- 같은 부서의 화면들이 카테고리 공유
- 중복 생성 불필요

### 3. 테이블 독립성

- 테이블이 달라도 같은 카테고리 사용 가능
- 테이블 구조 변경에 영향 없음

### 4. 직관적인 관리

- 메뉴 구조가 곧 카테고리 스코프
- 이해하기 쉬운 권한 체계

---

## 🚀 다음 단계

### 1. 계획 승인 후 즉시 구현 시작

이 계획서를 검토하고 승인받으면 바로 구현을 시작합니다.

### 2. 채번규칙 시스템도 동일하게 전환

카테고리 시스템 전환이 완료되면, 채번규칙 시스템도 동일한 메뉴 기반 스코프로 전환합니다.

### 3. 공통 유틸리티 함수 재사용

`getSiblingMenuIds()` 함수는 카테고리와 채번규칙 모두에서 재사용 가능합니다.

---

이 계획서대로 구현하면 영업관리 전체의 공통코드를 효과적으로 관리할 수 있습니다.
바로 구현을 시작할까요?
