카테고리
This commit is contained in:
parent
573a300a4a
commit
fe1c99c727
|
|
@ -148,7 +148,12 @@ class TableCategoryValueService {
|
||||||
AND (company_code = $4 OR company_code = '*')
|
AND (company_code = $4 OR company_code = '*')
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params: any[] = [tableName, columnName, siblingMenuIds, companyCode];
|
const params: any[] = [
|
||||||
|
tableName,
|
||||||
|
columnName,
|
||||||
|
siblingMenuIds,
|
||||||
|
companyCode,
|
||||||
|
];
|
||||||
|
|
||||||
if (!includeInactive) {
|
if (!includeInactive) {
|
||||||
query += ` AND is_active = true`;
|
query += ` AND is_active = true`;
|
||||||
|
|
@ -494,4 +499,3 @@ class TableCategoryValueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TableCategoryValueService();
|
export default new TableCategoryValueService();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
**공통코드**와 유사하지만 **메뉴별로 독립적으로 관리**되는 코드 시스템입니다.
|
**공통코드**와 유사하지만 **메뉴별로 독립적으로 관리**되는 코드 시스템입니다.
|
||||||
|
|
||||||
**주요 특징**:
|
**주요 특징**:
|
||||||
|
|
||||||
- 메뉴별 독립 관리: 각 화면(메뉴)마다 별도의 카테고리 목록 관리
|
- 메뉴별 독립 관리: 각 화면(메뉴)마다 별도의 카테고리 목록 관리
|
||||||
- 계층 구조 지원: 상위 카테고리 → 하위 카테고리 (최대 3단계)
|
- 계층 구조 지원: 상위 카테고리 → 하위 카테고리 (최대 3단계)
|
||||||
- 멀티테넌시: 회사별로 독립적으로 관리
|
- 멀티테넌시: 회사별로 독립적으로 관리
|
||||||
|
|
@ -20,16 +21,19 @@
|
||||||
### 1.2 사용 예시
|
### 1.2 사용 예시
|
||||||
|
|
||||||
#### 프로젝트 관리 화면
|
#### 프로젝트 관리 화면
|
||||||
|
|
||||||
- 프로젝트 유형: 개발, 유지보수, 컨설팅
|
- 프로젝트 유형: 개발, 유지보수, 컨설팅
|
||||||
- 프로젝트 상태: 계획, 진행중, 완료, 보류
|
- 프로젝트 상태: 계획, 진행중, 완료, 보류
|
||||||
- 우선순위: 긴급, 높음, 보통, 낮음
|
- 우선순위: 긴급, 높음, 보통, 낮음
|
||||||
|
|
||||||
#### 계약관리 화면
|
#### 계약관리 화면
|
||||||
|
|
||||||
- 계약 유형: 판매, 구매, 임대, 용역
|
- 계약 유형: 판매, 구매, 임대, 용역
|
||||||
- 계약 상태: 작성중, 검토중, 체결, 종료
|
- 계약 상태: 작성중, 검토중, 체결, 종료
|
||||||
- 결제 방식: 현금, 카드, 계좌이체, 어음
|
- 결제 방식: 현금, 카드, 계좌이체, 어음
|
||||||
|
|
||||||
#### 자산관리 화면
|
#### 자산관리 화면
|
||||||
|
|
||||||
- 자산 분류: IT장비, 비품, 차량, 부동산
|
- 자산 분류: IT장비, 비품, 차량, 부동산
|
||||||
- 자산 상태: 정상, 수리중, 폐기, 분실
|
- 자산 상태: 정상, 수리중, 폐기, 분실
|
||||||
- 위치: 본사, 지점A, 지점B, 창고
|
- 위치: 본사, 지점A, 지점B, 창고
|
||||||
|
|
@ -546,9 +550,7 @@ class CategoryService {
|
||||||
const checkResult = await pool.query(checkQuery, [valueId, companyCode]);
|
const checkResult = await pool.query(checkQuery, [valueId, companyCode]);
|
||||||
|
|
||||||
if (parseInt(checkResult.rows[0].count) > 0) {
|
if (parseInt(checkResult.rows[0].count) > 0) {
|
||||||
throw new Error(
|
throw new Error("하위 카테고리 값이 있어 삭제할 수 없습니다");
|
||||||
"하위 카테고리 값이 있어 삭제할 수 없습니다"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비활성화
|
// 비활성화
|
||||||
|
|
@ -899,9 +901,10 @@ import { CategoryConfig, CategoryValue } from "@/types/category";
|
||||||
*/
|
*/
|
||||||
export async function getCategoriesByMenu(menuObjid: number) {
|
export async function getCategoriesByMenu(menuObjid: number) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get<{ success: boolean; data: CategoryConfig[] }>(
|
const response = await apiClient.get<{
|
||||||
`/api/categories/menu/${menuObjid}`
|
success: boolean;
|
||||||
);
|
data: CategoryConfig[];
|
||||||
|
}>(`/api/categories/menu/${menuObjid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 목록 조회 실패:", error);
|
console.error("카테고리 목록 조회 실패:", error);
|
||||||
|
|
@ -914,9 +917,10 @@ export async function getCategoriesByMenu(menuObjid: number) {
|
||||||
*/
|
*/
|
||||||
export async function getCategoryValues(categoryId: string) {
|
export async function getCategoryValues(categoryId: string) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get<{ success: boolean; data: CategoryValue[] }>(
|
const response = await apiClient.get<{
|
||||||
`/api/categories/${categoryId}/values`
|
success: boolean;
|
||||||
);
|
data: CategoryValue[];
|
||||||
|
}>(`/api/categories/${categoryId}/values`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 값 조회 실패:", error);
|
console.error("카테고리 값 조회 실패:", error);
|
||||||
|
|
@ -929,10 +933,10 @@ export async function getCategoryValues(categoryId: string) {
|
||||||
*/
|
*/
|
||||||
export async function createCategory(config: CategoryConfig) {
|
export async function createCategory(config: CategoryConfig) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<{ success: boolean; data: CategoryConfig }>(
|
const response = await apiClient.post<{
|
||||||
"/api/categories",
|
success: boolean;
|
||||||
config
|
data: CategoryConfig;
|
||||||
);
|
}>("/api/categories", config);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 생성 실패:", error);
|
console.error("카테고리 생성 실패:", error);
|
||||||
|
|
@ -945,10 +949,10 @@ export async function createCategory(config: CategoryConfig) {
|
||||||
*/
|
*/
|
||||||
export async function addCategoryValue(value: CategoryValue) {
|
export async function addCategoryValue(value: CategoryValue) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<{ success: boolean; data: CategoryValue }>(
|
const response = await apiClient.post<{
|
||||||
"/api/categories/values",
|
success: boolean;
|
||||||
value
|
data: CategoryValue;
|
||||||
);
|
}>("/api/categories/values", value);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 값 추가 실패:", error);
|
console.error("카테고리 값 추가 실패:", error);
|
||||||
|
|
@ -959,12 +963,15 @@ export async function addCategoryValue(value: CategoryValue) {
|
||||||
/**
|
/**
|
||||||
* 카테고리 값 수정
|
* 카테고리 값 수정
|
||||||
*/
|
*/
|
||||||
export async function updateCategoryValue(valueId: number, updates: Partial<CategoryValue>) {
|
export async function updateCategoryValue(
|
||||||
|
valueId: number,
|
||||||
|
updates: Partial<CategoryValue>
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.put<{ success: boolean; data: CategoryValue }>(
|
const response = await apiClient.put<{
|
||||||
`/api/categories/values/${valueId}`,
|
success: boolean;
|
||||||
updates
|
data: CategoryValue;
|
||||||
);
|
}>(`/api/categories/values/${valueId}`, updates);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 값 수정 실패:", error);
|
console.error("카테고리 값 수정 실패:", error);
|
||||||
|
|
@ -977,9 +984,10 @@ export async function updateCategoryValue(valueId: number, updates: Partial<Cate
|
||||||
*/
|
*/
|
||||||
export async function deleteCategoryValue(valueId: number) {
|
export async function deleteCategoryValue(valueId: number) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.delete<{ success: boolean; message: string }>(
|
const response = await apiClient.delete<{
|
||||||
`/api/categories/values/${valueId}`
|
success: boolean;
|
||||||
);
|
message: string;
|
||||||
|
}>(`/api/categories/values/${valueId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("카테고리 값 삭제 실패:", error);
|
console.error("카테고리 값 삭제 실패:", error);
|
||||||
|
|
@ -1022,13 +1030,23 @@ export type WebType =
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Plus, X, ChevronRight } from "lucide-react";
|
import { Plus, X, ChevronRight } from "lucide-react";
|
||||||
import { getCategoriesByMenu, getCategoryValues, addCategoryValue } from "@/lib/api/category";
|
import {
|
||||||
|
getCategoriesByMenu,
|
||||||
|
getCategoryValues,
|
||||||
|
addCategoryValue,
|
||||||
|
} from "@/lib/api/category";
|
||||||
import { CategoryConfig, CategoryValue } from "@/types/category";
|
import { CategoryConfig, CategoryValue } from "@/types/category";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
|
@ -1038,14 +1056,14 @@ interface CategoryTypeConfigPanelProps {
|
||||||
menuObjid?: number;
|
menuObjid?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryTypeConfigPanel: React.FC<CategoryTypeConfigPanelProps> = ({
|
export const CategoryTypeConfigPanel: React.FC<
|
||||||
config,
|
CategoryTypeConfigPanelProps
|
||||||
onUpdate,
|
> = ({ config, onUpdate, menuObjid }) => {
|
||||||
menuObjid,
|
|
||||||
}) => {
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [categories, setCategories] = useState<CategoryConfig[]>([]);
|
const [categories, setCategories] = useState<CategoryConfig[]>([]);
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>(config.categoryId || "");
|
const [selectedCategory, setSelectedCategory] = useState<string>(
|
||||||
|
config.categoryId || ""
|
||||||
|
);
|
||||||
const [categoryValues, setCategoryValues] = useState<CategoryValue[]>([]);
|
const [categoryValues, setCategoryValues] = useState<CategoryValue[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -1161,12 +1179,16 @@ export const CategoryTypeConfigPanel: React.FC<CategoryTypeConfigPanelProps> = (
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-sm">{value.valueLabel}</span>
|
<span className="text-sm">{value.valueLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
{value.children && value.children.length > 0 && renderCategoryValues(value.children, depth + 1)}
|
{value.children &&
|
||||||
|
value.children.length > 0 &&
|
||||||
|
renderCategoryValues(value.children, depth + 1)}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedCategoryConfig = categories.find((c) => c.categoryId === selectedCategory);
|
const selectedCategoryConfig = categories.find(
|
||||||
|
(c) => c.categoryId === selectedCategory
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -1179,7 +1201,11 @@ export const CategoryTypeConfigPanel: React.FC<CategoryTypeConfigPanelProps> = (
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<SelectItem key={cat.categoryId} value={cat.categoryId} className="text-xs sm:text-sm">
|
<SelectItem
|
||||||
|
key={cat.categoryId}
|
||||||
|
value={cat.categoryId}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
{cat.categoryName}
|
{cat.categoryName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1198,7 +1224,9 @@ export const CategoryTypeConfigPanel: React.FC<CategoryTypeConfigPanelProps> = (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="multiSelect"
|
id="multiSelect"
|
||||||
checked={config.isMultiSelect || false}
|
checked={config.isMultiSelect || false}
|
||||||
onCheckedChange={(checked) => onUpdate({ isMultiSelect: checked as boolean })}
|
onCheckedChange={(checked) =>
|
||||||
|
onUpdate({ isMultiSelect: checked as boolean })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="multiSelect" className="text-xs sm:text-sm">
|
<Label htmlFor="multiSelect" className="text-xs sm:text-sm">
|
||||||
다중 선택 허용
|
다중 선택 허용
|
||||||
|
|
@ -1209,7 +1237,9 @@ export const CategoryTypeConfigPanel: React.FC<CategoryTypeConfigPanelProps> = (
|
||||||
{/* 카테고리 값 목록 */}
|
{/* 카테고리 값 목록 */}
|
||||||
{selectedCategory && categoryValues.length > 0 && (
|
{selectedCategory && categoryValues.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-medium sm:text-sm">카테고리 값 목록</Label>
|
<Label className="text-xs font-medium sm:text-sm">
|
||||||
|
카테고리 값 목록
|
||||||
|
</Label>
|
||||||
<div className="mt-2 max-h-64 overflow-y-auto rounded-md border p-2">
|
<div className="mt-2 max-h-64 overflow-y-auto rounded-md border p-2">
|
||||||
{renderCategoryValues(categoryValues)}
|
{renderCategoryValues(categoryValues)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1220,15 +1250,26 @@ export const CategoryTypeConfigPanel: React.FC<CategoryTypeConfigPanelProps> = (
|
||||||
{selectedCategory && selectedCategoryConfig?.allowCustomValues && (
|
{selectedCategory && selectedCategoryConfig?.allowCustomValues && (
|
||||||
<div>
|
<div>
|
||||||
{!isAddingValue ? (
|
{!isAddingValue ? (
|
||||||
<Button variant="outline" size="sm" onClick={() => setIsAddingValue(true)} className="w-full">
|
<Button
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
variant="outline"
|
||||||
새 값 추가
|
size="sm"
|
||||||
|
onClick={() => setIsAddingValue(true)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />새 값 추가
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2 rounded-md border p-3">
|
<div className="space-y-2 rounded-md border p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs font-medium sm:text-sm">새 카테고리 값</Label>
|
<Label className="text-xs font-medium sm:text-sm">
|
||||||
<Button variant="ghost" size="icon" onClick={() => setIsAddingValue(false)} className="h-6 w-6">
|
새 카테고리 값
|
||||||
|
</Label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setIsAddingValue(false)}
|
||||||
|
className="h-6 w-6"
|
||||||
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1256,8 +1297,17 @@ export const CategoryTypeConfigPanel: React.FC<CategoryTypeConfigPanelProps> = (
|
||||||
{selectedCategoryConfig && (
|
{selectedCategoryConfig && (
|
||||||
<div className="rounded-md bg-muted p-3 text-xs">
|
<div className="rounded-md bg-muted p-3 text-xs">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Badge variant={selectedCategoryConfig.allowCustomValues ? "default" : "secondary"} className="text-[10px]">
|
<Badge
|
||||||
{selectedCategoryConfig.allowCustomValues ? "사용자 정의 허용" : "고정 값"}
|
variant={
|
||||||
|
selectedCategoryConfig.allowCustomValues
|
||||||
|
? "default"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
className="text-[10px]"
|
||||||
|
>
|
||||||
|
{selectedCategoryConfig.allowCustomValues
|
||||||
|
? "사용자 정의 허용"
|
||||||
|
: "고정 값"}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="text-[10px]">
|
<Badge variant="outline" className="text-[10px]">
|
||||||
최대 깊이: {selectedCategoryConfig.maxDepth}단계
|
최대 깊이: {selectedCategoryConfig.maxDepth}단계
|
||||||
|
|
@ -1348,12 +1398,14 @@ const renderCategoryOptions = (values: CategoryValue[], depth: number = 0) => {
|
||||||
### 시나리오 1: 프로젝트 관리 화면에서 카테고리 사용
|
### 시나리오 1: 프로젝트 관리 화면에서 카테고리 사용
|
||||||
|
|
||||||
1. **관리자가 카테고리 생성**
|
1. **관리자가 카테고리 생성**
|
||||||
|
|
||||||
- 화면관리에서 "프로젝트 관리" 화면 선택
|
- 화면관리에서 "프로젝트 관리" 화면 선택
|
||||||
- 카테고리 관리 메뉴로 이동
|
- 카테고리 관리 메뉴로 이동
|
||||||
- "프로젝트 유형" 카테고리 생성
|
- "프로젝트 유형" 카테고리 생성
|
||||||
- 값 추가: 개발, 유지보수, 컨설팅
|
- 값 추가: 개발, 유지보수, 컨설팅
|
||||||
|
|
||||||
2. **화면에 카테고리 컴포넌트 배치**
|
2. **화면에 카테고리 컴포넌트 배치**
|
||||||
|
|
||||||
- 위젯 추가 → 웹타입: "category" 선택
|
- 위젯 추가 → 웹타입: "category" 선택
|
||||||
- 카테고리 선택: "프로젝트 유형"
|
- 카테고리 선택: "프로젝트 유형"
|
||||||
- 라벨 설정: "프로젝트 유형"
|
- 라벨 설정: "프로젝트 유형"
|
||||||
|
|
@ -1366,6 +1418,7 @@ const renderCategoryOptions = (values: CategoryValue[], depth: number = 0) => {
|
||||||
### 시나리오 2: 계층 구조 카테고리
|
### 시나리오 2: 계층 구조 카테고리
|
||||||
|
|
||||||
1. **관리자가 3단계 카테고리 생성**
|
1. **관리자가 3단계 카테고리 생성**
|
||||||
|
|
||||||
- "지역" 카테고리 생성 (maxDepth: 3)
|
- "지역" 카테고리 생성 (maxDepth: 3)
|
||||||
- 1단계: 서울특별시, 부산광역시
|
- 1단계: 서울특별시, 부산광역시
|
||||||
- 2단계: 강남구, 송파구 (서울 하위)
|
- 2단계: 강남구, 송파구 (서울 하위)
|
||||||
|
|
@ -1457,12 +1510,14 @@ SELECT 'Migration 036: Menu Categories created successfully!' AS status;
|
||||||
## 8. 구현 체크리스트
|
## 8. 구현 체크리스트
|
||||||
|
|
||||||
### 데이터베이스 ✅
|
### 데이터베이스 ✅
|
||||||
|
|
||||||
- [ ] 마이그레이션 파일 작성 (036_create_menu_categories.sql)
|
- [ ] 마이그레이션 파일 작성 (036_create_menu_categories.sql)
|
||||||
- [ ] 테이블 생성 및 인덱스
|
- [ ] 테이블 생성 및 인덱스
|
||||||
- [ ] 샘플 데이터 삽입
|
- [ ] 샘플 데이터 삽입
|
||||||
- [ ] 외래키 제약조건 설정
|
- [ ] 외래키 제약조건 설정
|
||||||
|
|
||||||
### 백엔드 ✅
|
### 백엔드 ✅
|
||||||
|
|
||||||
- [ ] 타입 정의 (backend-node/src/types/category.ts)
|
- [ ] 타입 정의 (backend-node/src/types/category.ts)
|
||||||
- [ ] 서비스 레이어 (categoryService.ts)
|
- [ ] 서비스 레이어 (categoryService.ts)
|
||||||
- [ ] 컨트롤러 레이어 (categoryController.ts)
|
- [ ] 컨트롤러 레이어 (categoryController.ts)
|
||||||
|
|
@ -1470,6 +1525,7 @@ SELECT 'Migration 036: Menu Categories created successfully!' AS status;
|
||||||
- [ ] app.ts에 라우트 등록
|
- [ ] app.ts에 라우트 등록
|
||||||
|
|
||||||
### 프론트엔드 ✅
|
### 프론트엔드 ✅
|
||||||
|
|
||||||
- [ ] 타입 정의 (frontend/types/category.ts)
|
- [ ] 타입 정의 (frontend/types/category.ts)
|
||||||
- [ ] API 클라이언트 (frontend/lib/api/category.ts)
|
- [ ] API 클라이언트 (frontend/lib/api/category.ts)
|
||||||
- [ ] WebType에 "category" 추가
|
- [ ] WebType에 "category" 추가
|
||||||
|
|
@ -1478,6 +1534,7 @@ SELECT 'Migration 036: Menu Categories created successfully!' AS status;
|
||||||
- [ ] RealtimePreview에 렌더링 로직 추가
|
- [ ] RealtimePreview에 렌더링 로직 추가
|
||||||
|
|
||||||
### 테스트 ✅
|
### 테스트 ✅
|
||||||
|
|
||||||
- [ ] 카테고리 생성/조회/수정/삭제 API 테스트
|
- [ ] 카테고리 생성/조회/수정/삭제 API 테스트
|
||||||
- [ ] 메뉴별 카테고리 필터링 테스트
|
- [ ] 메뉴별 카테고리 필터링 테스트
|
||||||
- [ ] 계층 구조 렌더링 테스트
|
- [ ] 계층 구조 렌더링 테스트
|
||||||
|
|
@ -1488,18 +1545,22 @@ SELECT 'Migration 036: Menu Categories created successfully!' AS status;
|
||||||
## 9. 향후 확장 가능성
|
## 9. 향후 확장 가능성
|
||||||
|
|
||||||
### 9.1 동적 카테고리 검색
|
### 9.1 동적 카테고리 검색
|
||||||
|
|
||||||
- 카테고리 값이 많을 때 Combobox로 변경
|
- 카테고리 값이 많을 때 Combobox로 변경
|
||||||
- 자동완성 검색 기능
|
- 자동완성 검색 기능
|
||||||
|
|
||||||
### 9.2 카테고리 템플릿
|
### 9.2 카테고리 템플릿
|
||||||
|
|
||||||
- 자주 사용하는 카테고리를 템플릿으로 저장
|
- 자주 사용하는 카테고리를 템플릿으로 저장
|
||||||
- 새 메뉴 생성 시 템플릿 적용
|
- 새 메뉴 생성 시 템플릿 적용
|
||||||
|
|
||||||
### 9.3 카테고리 분석
|
### 9.3 카테고리 분석
|
||||||
|
|
||||||
- 가장 많이 사용되는 카테고리 값 통계
|
- 가장 많이 사용되는 카테고리 값 통계
|
||||||
- 사용되지 않는 카테고리 값 정리 제안
|
- 사용되지 않는 카테고리 값 정리 제안
|
||||||
|
|
||||||
### 9.4 카테고리 권한 관리
|
### 9.4 카테고리 권한 관리
|
||||||
|
|
||||||
- 특정 사용자만 특정 카테고리 값 선택 가능
|
- 특정 사용자만 특정 카테고리 값 선택 가능
|
||||||
- 카테고리 값별 접근 권한 설정
|
- 카테고리 값별 접근 권한 설정
|
||||||
|
|
||||||
|
|
@ -1510,6 +1571,7 @@ SELECT 'Migration 036: Menu Categories created successfully!' AS status;
|
||||||
**카테고리 시스템**은 채번 규칙과 유사한 구조로 메뉴별로 독립적으로 관리되는 코드 시스템입니다.
|
**카테고리 시스템**은 채번 규칙과 유사한 구조로 메뉴별로 독립적으로 관리되는 코드 시스템입니다.
|
||||||
|
|
||||||
**핵심 특징**:
|
**핵심 특징**:
|
||||||
|
|
||||||
- ✅ 메뉴별 독립 관리
|
- ✅ 메뉴별 독립 관리
|
||||||
- ✅ 계층 구조 지원 (최대 3단계)
|
- ✅ 계층 구조 지원 (최대 3단계)
|
||||||
- ✅ 멀티테넌시 격리
|
- ✅ 멀티테넌시 격리
|
||||||
|
|
@ -1517,8 +1579,8 @@ SELECT 'Migration 036: Menu Categories created successfully!' AS status;
|
||||||
- ✅ 다중 선택 옵션
|
- ✅ 다중 선택 옵션
|
||||||
|
|
||||||
**공통코드와의 차이점**:
|
**공통코드와의 차이점**:
|
||||||
|
|
||||||
- 공통코드: 전사 공통 (모든 메뉴에서 동일)
|
- 공통코드: 전사 공통 (모든 메뉴에서 동일)
|
||||||
- 카테고리: 메뉴별 독립 (각 화면마다 다른 값)
|
- 카테고리: 메뉴별 독립 (각 화면마다 다른 값)
|
||||||
|
|
||||||
이제 이 계획서를 기반으로 구현을 시작하시겠습니까?
|
이제 이 계획서를 기반으로 구현을 시작하시겠습니까?
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue