# 카테고리 메뉴 스코프 개선 계획서

## 1. 문제 정의

### 현재 문제점
**카테고리 컴포넌트가 형제 메뉴 기반으로 카테고리를 조회하여 메뉴별 카테고리 구분 불가**

#### 구체적 상황
- `기준정보 > 품목정보` (item_info 테이블 사용)
- `영업관리 > 판매품목정보` (item_info 테이블 사용)
- 두 메뉴가 같은 테이블(`item_info`)을 사용하지만, 표시되어야 할 카테고리는 달라야 함

#### 현재 로직의 문제
```typescript
// 형제 메뉴들이 사용하는 모든 테이블의 카테고리 컬럼 조회
const siblings = await getSiblingMenus(currentMenuId);
const categories = await getCategoriesFromTables(siblings.tables);
```

**결과**: 
- `기준정보 > 품목정보`에서 보이지 않아야 할 카테고리가 표시됨
- `영업관리 > 판매품목정보`에서만 보여야 할 카테고리가 `기준정보`에도 표시됨

---

## 2. 해결 방안

### 핵심 아이디어
**카테고리 값이 어떤 2레벨 메뉴에 속하는지 명시적으로 저장하고, 조회 시 2레벨 메뉴 기준으로 필터링**

### 2.1 데이터베이스 구조 (이미 준비됨)

```sql
-- table_column_category_values 테이블에 이미 menu_objid 컬럼 존재
SELECT * FROM table_column_category_values;

-- 컬럼 구조
-- value_id: PK
-- table_name: 테이블명
-- column_name: 컬럼명
-- value_code: 카테고리 코드
-- value_label: 카테고리 라벨
-- menu_objid: 2레벨 메뉴 OBJID (핵심!)
-- company_code: 회사 코드
-- ...
```

### 2.2 메뉴 계층 구조

```
1레벨 메뉴 (기준정보, objid=1)
├── 2레벨 메뉴 (회사정보, objid=101)
├── 2레벨 메뉴 (부서관리, objid=102)
└── 2레벨 메뉴 (품목정보, objid=103) ← 여기에 item_info 테이블 사용

1레벨 메뉴 (영업관리, objid=2)
├── 2레벨 메뉴 (견적관리, objid=201)
├── 2레벨 메뉴 (수주관리, objid=202)
└── 2레벨 메뉴 (판매품목정보, objid=203) ← 여기도 item_info 테이블 사용
```

### 2.3 카테고리 값 저장 방식

```sql
-- 기준정보 > 품목정보에서 사용할 카테고리
INSERT INTO table_column_category_values 
(table_name, column_name, value_code, value_label, menu_objid, company_code)
VALUES 
('item_info', 'category_type', 'STOCK_ITEM', '재고품목', 103, 'COMPANY_A'),
('item_info', 'category_type', 'ASSET', '자산', 103, 'COMPANY_A');

-- 영업관리 > 판매품목정보에서 사용할 카테고리
INSERT INTO table_column_category_values 
(table_name, column_name, value_code, value_label, menu_objid, company_code)
VALUES 
('item_info', 'category_type', 'SALES_ITEM', '판매품목', 203, 'COMPANY_A'),
('item_info', 'category_type', 'SERVICE', '서비스', 203, 'COMPANY_A');
```

---

## 3. 구현 단계

### Phase 1: 백엔드 API 수정

#### 3.1 카테고리 조회 API 개선

**파일**: `backend-node/src/controllers/categoryController.ts`

**현재 로직**:
```typescript
export async function getCategoryColumns(req: Request, res: Response) {
  const { tableName } = req.params;
  const companyCode = req.user!.companyCode;
  
  // 형제 메뉴들의 테이블에서 카테고리 컬럼 조회
  const siblings = await getSiblingMenuTables(menuObjid);
  
  const query = `
    SELECT DISTINCT 
      table_name, 
      column_name,
      value_code,
      value_label
    FROM table_column_category_values
    WHERE table_name IN (${siblings.join(',')})
      AND company_code = $1
  `;
}
```

**개선된 로직**:
```typescript
export async function getCategoryColumns(req: Request, res: Response) {
  const { tableName, menuObjid } = req.params; // menuObjid 추가
  const companyCode = req.user!.companyCode;
  
  // 2레벨 메뉴 OBJID 찾기
  const topLevelMenuId = await getTopLevelMenuId(menuObjid);
  
  const query = `
    SELECT 
      table_name, 
      column_name,
      value_code,
      value_label,
      value_order,
      parent_value_id,
      depth,
      description,
      color,
      icon
    FROM table_column_category_values
    WHERE table_name = $1
      AND menu_objid = $2
      AND company_code = $3
      AND is_active = true
    ORDER BY value_order, value_label
  `;
  
  const result = await pool.query(query, [tableName, topLevelMenuId, companyCode]);
  
  logger.info("카테고리 컬럼 조회 (메뉴 스코프)", {
    tableName,
    menuObjid: topLevelMenuId,
    companyCode,
    categoryCount: result.rowCount,
  });
  
  return res.json({
    success: true,
    data: result.rows,
  });
}
```

#### 3.2 2레벨 메뉴 OBJID 찾기 함수

```typescript
/**
 * 현재 메뉴의 최상위(2레벨) 메뉴 OBJID 찾기
 * 
 * @param menuObjid - 현재 메뉴 OBJID
 * @returns 2레벨 메뉴 OBJID
 */
async function getTopLevelMenuId(menuObjid: number): Promise<number> {
  const query = `
    WITH RECURSIVE menu_hierarchy AS (
      -- 현재 메뉴
      SELECT 
        objid,
        parent_obj_id,
        menu_type,
        1 as level
      FROM menu_info
      WHERE objid = $1
      
      UNION ALL
      
      -- 부모 메뉴들 재귀 조회
      SELECT 
        m.objid,
        m.parent_obj_id,
        m.menu_type,
        mh.level + 1
      FROM menu_info m
      JOIN menu_hierarchy mh ON m.objid = mh.parent_obj_id
    )
    SELECT objid
    FROM menu_hierarchy
    WHERE parent_obj_id IS NULL OR menu_type = 1 -- 1레벨 메뉴의 자식 = 2레벨
    ORDER BY level DESC
    LIMIT 1;
  `;
  
  const result = await pool.query(query, [menuObjid]);
  
  if (result.rowCount === 0) {
    throw new Error(`메뉴를 찾을 수 없습니다: ${menuObjid}`);
  }
  
  return result.rows[0].objid;
}
```

#### 3.3 카테고리 값 생성 API 개선

**파일**: `backend-node/src/controllers/categoryController.ts`

```typescript
export async function createCategoryValue(req: Request, res: Response) {
  const companyCode = req.user!.companyCode;
  const { 
    tableName, 
    columnName, 
    valueCode, 
    valueLabel,
    menuObjid, // 필수로 추가
    parentValueId,
    depth,
    description,
    color,
    icon,
  } = req.body;
  
  // 입력 검증
  if (!tableName || !columnName || !valueCode || !valueLabel || !menuObjid) {
    return res.status(400).json({
      success: false,
      message: "필수 필드가 누락되었습니다",
    });
  }
  
  const query = `
    INSERT INTO table_column_category_values (
      table_name,
      column_name,
      value_code,
      value_label,
      menu_objid,
      parent_value_id,
      depth,
      description,
      color,
      icon,
      company_code,
      created_by,
      updated_by
    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
    RETURNING *
  `;
  
  const result = await pool.query(query, [
    tableName,
    columnName,
    valueCode,
    valueLabel,
    menuObjid,
    parentValueId || null,
    depth || 1,
    description || null,
    color || null,
    icon || null,
    companyCode,
    req.user!.userId,
    req.user!.userId,
  ]);
  
  logger.info("카테고리 값 생성", {
    tableName,
    columnName,
    valueCode,
    menuObjid,
    companyCode,
  });
  
  return res.json({
    success: true,
    data: result.rows[0],
  });
}
```

### Phase 2: 프론트엔드 - 테이블 타입 관리 UI 개선

#### 2.1 테이블 타입 관리 컴포넌트 수정

**파일**: `frontend/components/admin/table-type-management/TableTypeManagement.tsx`

**추가할 기능**:
1. 카테고리 컬럼 설정 시 "적용할 메뉴 선택" 기능
2. 2레벨 메뉴 목록 조회 및 다중 선택 UI
3. 선택된 메뉴별로 카테고리 값 생성

```typescript
interface CategoryMenuScope {
  menuObjid: number;
  menuName: string;
  parentMenuName: string;
  isSelected: boolean;
}

function CategoryColumnConfig({ tableName, columnName }: Props) {
  const [menuScopes, setMenuScopes] = useState<CategoryMenuScope[]>([]);
  const [selectedMenus, setSelectedMenus] = useState<number[]>([]);
  
  // 2레벨 메뉴 목록 조회
  useEffect(() => {
    async function loadMenuScopes() {
      const response = await apiClient.get("/api/menus/second-level");
      if (response.data.success) {
        setMenuScopes(response.data.data);
      }
    }
    loadMenuScopes();
  }, []);
  
  // 카테고리 값 저장 시 선택된 메뉴들에 대해 각각 저장
  const handleSaveCategoryValue = async (categoryData: any) => {
    for (const menuObjid of selectedMenus) {
      await apiClient.post("/api/categories/values", {
        ...categoryData,
        tableName,
        columnName,
        menuObjid, // 메뉴별로 저장
      });
    }
    
    toast.success("카테고리가 선택된 메뉴에 저장되었습니다");
  };
  
  return (
    <div className="space-y-4">
      <Label>적용할 메뉴 선택</Label>
      <div className="border rounded-lg p-4 space-y-2 max-h-60 overflow-y-auto">
        {menuScopes.map((scope) => (
          <div key={scope.menuObjid} className="flex items-center gap-2">
            <Checkbox
              checked={selectedMenus.includes(scope.menuObjid)}
              onCheckedChange={(checked) => {
                if (checked) {
                  setSelectedMenus([...selectedMenus, scope.menuObjid]);
                } else {
                  setSelectedMenus(selectedMenus.filter(id => id !== scope.menuObjid));
                }
              }}
            />
            <span className="text-sm">
              {scope.parentMenuName} → {scope.menuName}
            </span>
          </div>
        ))}
      </div>
      
      {selectedMenus.length === 0 && (
        <p className="text-xs text-destructive">
          최소 하나 이상의 메뉴를 선택해주세요
        </p>
      )}
      
      {/* 카테고리 값 추가 UI */}
      <CategoryValueEditor
        onSave={handleSaveCategoryValue}
        disabled={selectedMenus.length === 0}
      />
    </div>
  );
}
```

#### 2.2 2레벨 메뉴 조회 API 추가

**파일**: `backend-node/src/controllers/menuController.ts`

```typescript
/**
 * 2레벨 메뉴 목록 조회
 * (카테고리 스코프 선택용)
 */
export async function getSecondLevelMenus(req: Request, res: Response) {
  const companyCode = req.user!.companyCode;
  
  const query = `
    SELECT 
      m2.objid as menu_objid,
      m2.menu_name_kor as menu_name,
      m1.menu_name_kor as parent_menu_name,
      m2.screen_code
    FROM menu_info m2
    JOIN menu_info m1 ON m2.parent_obj_id = m1.objid
    WHERE m2.menu_type = 2 -- 2레벨 메뉴
      AND (m2.company_code = $1 OR m2.company_code = '*')
      AND m2.status = 'Y'
    ORDER BY m1.seq, m2.seq
  `;
  
  const result = await pool.query(query, [companyCode]);
  
  return res.json({
    success: true,
    data: result.rows,
  });
}
```

**라우트 등록**:
```typescript
router.get("/api/menus/second-level", authenticate, getSecondLevelMenus);
```

### Phase 3: 프론트엔드 - 카테고리 컴포넌트 개선

#### 3.1 카테고리 컴포넌트 조회 로직 변경

**파일**: `frontend/lib/registry/components/category/CategoryComponent.tsx`

**현재 로직** (형제 메뉴 기반):
```typescript
useEffect(() => {
  async function loadCategories() {
    // 형제 메뉴들의 테이블에서 카테고리 조회
    const response = await apiClient.get(`/api/categories/${tableName}`);
  }
  loadCategories();
}, [tableName]);
```

**개선된 로직** (2레벨 메뉴 기반):
```typescript
useEffect(() => {
  async function loadCategories() {
    // 현재 메뉴 OBJID 가져오기
    const menuObjid = screenConfig.menuObjid; // 또는 URL에서 파싱
    
    if (!menuObjid) {
      console.warn("메뉴 OBJID를 찾을 수 없습니다");
      return;
    }
    
    // 2레벨 메뉴 기준으로 카테고리 조회
    const response = await apiClient.get(
      `/api/categories/${tableName}/menu/${menuObjid}`
    );
    
    if (response.data.success) {
      setCategories(response.data.data);
    }
  }
  loadCategories();
}, [tableName, screenConfig.menuObjid]);
```

#### 3.2 API 클라이언트 함수 추가

**파일**: `frontend/lib/api/category.ts`

```typescript
/**
 * 특정 메뉴 스코프의 카테고리 컬럼 조회
 * 
 * @param tableName - 테이블명
 * @param menuObjid - 메뉴 OBJID
 */
export async function getCategoriesByMenuScope(
  tableName: string,
  menuObjid: number
): Promise<ApiResponse<CategoryColumn[]>> {
  try {
    const response = await apiClient.get(
      `/api/categories/${tableName}/menu/${menuObjid}`
    );
    return response.data;
  } catch (error: any) {
    return {
      success: false,
      error: error.message,
    };
  }
}
```

---

## 4. 데이터 마이그레이션

### 4.1 기존 카테고리 값에 menu_objid 설정

**문제**: 기존 카테고리 값들은 `menu_objid`가 설정되지 않음

**해결**: 관리자가 테이블 타입 관리에서 기존 카테고리 값들의 메뉴 스코프를 선택하도록 유도

#### 마이그레이션 스크립트 (선택사항)

```sql
-- db/migrations/053_backfill_category_menu_objid.sql

-- Step 1: 기존 카테고리 값 확인
SELECT 
  value_id,
  table_name,
  column_name,
  value_label,
  menu_objid,
  company_code
FROM table_column_category_values
WHERE menu_objid IS NULL OR menu_objid = 0;

-- Step 2: 기본값 설정 (예시)
-- 관리자가 수동으로 올바른 menu_objid를 설정해야 함
UPDATE table_column_category_values
SET menu_objid = 103 -- 예: 기준정보 > 품목정보
WHERE table_name = 'item_info'
  AND column_name = 'category_type'
  AND value_code IN ('STOCK_ITEM', 'ASSET')
  AND (menu_objid IS NULL OR menu_objid = 0);

UPDATE table_column_category_values
SET menu_objid = 203 -- 예: 영업관리 > 판매품목정보
WHERE table_name = 'item_info'
  AND column_name = 'category_type'
  AND value_code IN ('SALES_ITEM', 'SERVICE')
  AND (menu_objid IS NULL OR menu_objid = 0);
```

### 4.2 UI에서 menu_objid 미설정 경고

```typescript
// TableTypeManagement.tsx
function CategoryColumnList({ categoryColumns }: Props) {
  const unassignedCategories = categoryColumns.filter(
    (col) => !col.menuObjid || col.menuObjid === 0
  );
  
  if (unassignedCategories.length > 0) {
    return (
      <Alert variant="destructive">
        <AlertCircle className="h-4 w-4" />
        <AlertTitle>메뉴 스코프 미설정</AlertTitle>
        <AlertDescription>
          {unassignedCategories.length}개의 카테고리 값에 메뉴가 설정되지 않았습니다.
          <br />
          각 카테고리가 어떤 메뉴에서 사용될지 설정해주세요.
        </AlertDescription>
      </Alert>
    );
  }
  
  return <div>...</div>;
}
```

---

## 5. 테스트 시나리오

### 5.1 기본 기능 테스트

1. **2레벨 메뉴 조회**
   - `GET /api/menus/second-level`
   - 기준정보, 영업관리 등 2레벨 메뉴들이 조회되는지 확인

2. **카테고리 값 생성 (메뉴 스코프 포함)**
   - 테이블 타입 관리에서 카테고리 컬럼 선택
   - 적용할 메뉴 선택 (예: 영업관리 > 판매품목정보)
   - 카테고리 값 추가
   - DB에 올바른 `menu_objid`로 저장되는지 확인

3. **카테고리 컴포넌트 조회 (메뉴별 필터링)**
   - `기준정보 > 품목정보` 화면 접속
   - 해당 메뉴의 카테고리만 표시되는지 확인
   - `영업관리 > 판매품목정보` 화면 접속
   - 다른 카테고리가 표시되는지 확인

### 5.2 엣지 케이스 테스트

1. **menu_objid 미설정 카테고리**
   - 기존 카테고리 값 (menu_objid = NULL)
   - 경고 메시지 표시
   - 조회 시 제외되는지 확인

2. **여러 메뉴에 동일 카테고리**
   - 하나의 카테고리를 여러 메뉴에 적용
   - 각 메뉴에서 독립적으로 관리되는지 확인

3. **최고 관리자 권한**
   - 최고 관리자는 모든 메뉴의 카테고리를 볼 수 있는지 확인

---

## 6. 롤백 계획

만약 문제가 발생하면 다음 단계로 롤백:

### 6.1 백엔드 API 롤백
```bash
git revert <commit-hash>
```

### 6.2 데이터베이스 롤백
```sql
-- menu_objid 조건 제거 (임시)
-- 기존 로직으로 돌아감
```

### 6.3 프론트엔드 롤백
- 카테고리 조회 시 `menuObjid` 파라미터 제거
- 형제 메뉴 기반 로직으로 복원

---

## 7. 구현 우선순위

### Phase 1 (필수)
1. ✅ 백엔드: `getTopLevelMenuId()` 함수 구현
2. ✅ 백엔드: 카테고리 조회 API에 `menu_objid` 필터링 추가
3. ✅ 백엔드: 카테고리 값 생성 시 `menu_objid` 필수 처리
4. ✅ 백엔드: 2레벨 메뉴 목록 조회 API 추가

### Phase 2 (필수)
5. ✅ 프론트엔드: 테이블 타입 관리에서 메뉴 선택 UI 추가
6. ✅ 프론트엔드: 카테고리 컴포넌트 조회 로직 변경

### Phase 3 (권장)
7. ⏳ 데이터 마이그레이션: 기존 카테고리 값에 `menu_objid` 설정
8. ⏳ UI: `menu_objid` 미설정 경고 표시
9. ⏳ 테스트: 시나리오별 검증

### Phase 4 (선택)
10. ⏳ 관리자 대시보드: 카테고리 사용 현황 통계
11. ⏳ 벌크 업데이트: 여러 카테고리의 메뉴 스코프 일괄 변경

---

## 8. 기대 효과

### 8.1 문제 해결
- ✅ 같은 테이블을 사용하는 메뉴들도 서로 다른 카테고리 사용 가능
- ✅ 메뉴별 카테고리 독립성 보장
- ✅ 관리자가 메뉴별로 카테고리를 명확히 제어

### 8.2 사용자 경험 개선
- ✅ 카테고리가 메뉴 문맥에 맞게 표시됨
- ✅ 불필요한 카테고리가 표시되지 않음
- ✅ 직관적인 카테고리 관리 UI

### 8.3 확장성
- ✅ 향후 메뉴별 세밀한 권한 제어 가능
- ✅ 메뉴별 카테고리 통계 및 분석 가능
- ✅ 다른 컴포넌트에도 유사한 메뉴 스코프 패턴 적용 가능

---

## 9. 참고 자료

- 데이터베이스 스키마: `table_column_category_values` 테이블
- 메뉴 구조: `menu_info` 테이블
- 카테고리 컴포넌트: `frontend/lib/registry/components/category/CategoryComponent.tsx`
- 테이블 타입 관리: `frontend/components/admin/table-type-management/TableTypeManagement.tsx`
- API 클라이언트: `frontend/lib/api/category.ts`

---

## 10. 결론

사용자가 제안한 **"테이블 타입 관리에서 카테고리 컬럼 설정 시 2레벨 메뉴 선택"** 방식이 가장 현실적이고 효과적인 해결책입니다.

**핵심 변경사항**:
1. 카테고리 값 저장 시 `menu_objid` 필수 입력
2. 카테고리 조회 시 2레벨 메뉴 기준 필터링 (형제 메뉴 기준 제거)
3. 테이블 타입 관리 UI에서 메뉴 선택 기능 추가

이 방식으로 같은 테이블을 사용하는 서로 다른 메뉴들이 각자의 카테고리를 독립적으로 관리할 수 있습니다.

