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