# 카테고리 시스템 재구현 완료 보고서 ## 🎯 핵심 개념 **메뉴 계층 기반 카테고리 스코프** - 카테고리는 **생성된 메뉴의 형제 메뉴들 간에만** 공유됩니다 - 다른 부모를 가진 메뉴에서는 사용할 수 없습니다 - 화면관리 시스템의 위젯으로 통합되어 관리됩니다 --- ## ✅ 완료된 작업 ### 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 { // 1. 현재 메뉴의 부모 ID 조회 // 2. 같은 부모를 가진 형제 메뉴들 조회 // 3. 형제 메뉴 ID 배열 반환 } ``` 2. **카테고리 값 조회 수정**: ```typescript async getCategoryValues( tableName: string, columnName: string, menuId: number, // ← menuId 파라미터 추가 companyCode: string, includeInactive: boolean = false ): Promise { // 형제 메뉴들의 카테고리도 포함 const siblingMenuIds = await this.getSiblingMenuIds(menuId); // WHERE menu_id = ANY($3) 조건으로 필터링 } ``` 3. **카테고리 값 추가 수정**: ```typescript async addCategoryValue(value: TableCategoryValue, ...): Promise { // 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 (
{/* 좌측: 카테고리 컬럼 리스트 (30%) */}
setSelectedColumn({ columnName, columnLabel }) } />
{/* 우측: 카테고리 값 관리 (70%) */}
{selectedColumn ? ( ) : ( )}
); } ``` ##### 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" && ( )} ``` 4. **Dialog 렌더링 제거**: ```typescript // ❌ 제거됨 {categoryDialogData && ( )} ``` --- ## 📖 사용 시나리오 ### 시나리오: 구매관리 시스템에서 카테고리 관리 #### 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 { // 예: 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 { // 형제 메뉴들의 카테고리도 포함 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 ( ); ``` 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