Files
vexplor/카테고리_시스템_재구현_완료_보고서.md

630 lines
18 KiB
Markdown
Raw Permalink Normal View History

2025-11-05 15:23:57 +09:00
# 카테고리 시스템 재구현 완료 보고서
## 🎯 핵심 개념
**메뉴 계층 기반 카테고리 스코프**
- 카테고리는 **생성된 메뉴의 형제 메뉴들 간에만** 공유됩니다
- 다른 부모를 가진 메뉴에서는 사용할 수 없습니다
- 화면관리 시스템의 위젯으로 통합되어 관리됩니다
---
## ✅ 완료된 작업
### 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