feat: 카테고리 컬럼 메뉴별 매핑 기능 구현
- category_column_mapping 테이블 생성 (마이그레이션 054) - 테이블 타입 관리에서 2레벨 메뉴 선택 기능 추가 - 카테고리 컬럼 조회 시 현재 메뉴 및 상위 메뉴 매핑 자동 조회 - 캐시 무효화 로직 개선 - 메뉴별 카테고리 설정 저장 및 불러오기 기능 구현
This commit is contained in:
@@ -21,15 +21,22 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Plus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { createColumnMapping } from "@/lib/api/tableCategoryValue";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
interface SecondLevelMenu {
|
||||
menuObjid: number;
|
||||
menuName: string;
|
||||
parentMenuName: string;
|
||||
screenCode?: string;
|
||||
}
|
||||
|
||||
interface AddCategoryColumnDialogProps {
|
||||
tableName: string;
|
||||
menuObjid: number;
|
||||
menuName: string;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
@@ -37,27 +44,31 @@ interface AddCategoryColumnDialogProps {
|
||||
* 카테고리 컬럼 추가 다이얼로그
|
||||
*
|
||||
* 논리적 컬럼명과 물리적 컬럼명을 매핑하여 메뉴별로 독립적인 카테고리 관리 가능
|
||||
*
|
||||
* 2레벨 메뉴를 선택하면 해당 메뉴의 모든 하위 메뉴에서 사용 가능
|
||||
*/
|
||||
export function AddCategoryColumnDialog({
|
||||
tableName,
|
||||
menuObjid,
|
||||
menuName,
|
||||
onSuccess,
|
||||
}: AddCategoryColumnDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [physicalColumns, setPhysicalColumns] = useState<string[]>([]);
|
||||
const [secondLevelMenus, setSecondLevelMenus] = useState<SecondLevelMenu[]>([]);
|
||||
const [selectedMenus, setSelectedMenus] = useState<number[]>([]);
|
||||
const [logicalColumnName, setLogicalColumnName] = useState("");
|
||||
const [physicalColumnName, setPhysicalColumnName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
// 테이블의 실제 컬럼 목록 조회
|
||||
// 다이얼로그 열릴 때 데이터 로드
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadPhysicalColumns();
|
||||
loadSecondLevelMenus();
|
||||
}
|
||||
}, [open, tableName]);
|
||||
|
||||
// 테이블의 실제 컬럼 목록 조회
|
||||
const loadPhysicalColumns = async () => {
|
||||
try {
|
||||
const response = await tableManagementApi.getTableColumns(tableName);
|
||||
@@ -70,6 +81,32 @@ export function AddCategoryColumnDialog({
|
||||
}
|
||||
};
|
||||
|
||||
// 2레벨 메뉴 목록 조회
|
||||
const loadSecondLevelMenus = async () => {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: SecondLevelMenu[];
|
||||
}>("table-categories/second-level-menus");
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
setSecondLevelMenus(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("2레벨 메뉴 목록 조회 실패:", error);
|
||||
toast.error("메뉴 목록을 불러올 수 없습니다");
|
||||
}
|
||||
};
|
||||
|
||||
// 메뉴 선택/해제
|
||||
const toggleMenu = (menuObjid: number) => {
|
||||
setSelectedMenus((prev) =>
|
||||
prev.includes(menuObjid)
|
||||
? prev.filter((id) => id !== menuObjid)
|
||||
: [...prev, menuObjid]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
// 입력 검증
|
||||
if (!logicalColumnName.trim()) {
|
||||
@@ -82,24 +119,42 @@ export function AddCategoryColumnDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMenus.length === 0) {
|
||||
toast.error("최소 하나 이상의 메뉴를 선택해주세요");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await createColumnMapping({
|
||||
tableName,
|
||||
logicalColumnName: logicalColumnName.trim(),
|
||||
physicalColumnName,
|
||||
menuObjid,
|
||||
description: description.trim() || undefined,
|
||||
});
|
||||
// 선택된 각 메뉴에 대해 매핑 생성
|
||||
const promises = selectedMenus.map((menuObjid) =>
|
||||
createColumnMapping({
|
||||
tableName,
|
||||
logicalColumnName: logicalColumnName.trim(),
|
||||
physicalColumnName,
|
||||
menuObjid,
|
||||
description: description.trim() || undefined,
|
||||
})
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
toast.success("논리적 컬럼이 추가되었습니다");
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// 모든 요청이 성공했는지 확인
|
||||
const failedCount = results.filter((r) => !r.success).length;
|
||||
|
||||
if (failedCount === 0) {
|
||||
toast.success(`논리적 컬럼이 ${selectedMenus.length}개 메뉴에 추가되었습니다`);
|
||||
setOpen(false);
|
||||
resetForm();
|
||||
onSuccess();
|
||||
} else if (failedCount < results.length) {
|
||||
toast.warning(
|
||||
`${results.length - failedCount}개 메뉴에 추가 성공, ${failedCount}개 실패`
|
||||
);
|
||||
onSuccess();
|
||||
} else {
|
||||
toast.error(response.error || "컬럼 매핑 생성에 실패했습니다");
|
||||
toast.error("모든 메뉴에 대한 매핑 생성에 실패했습니다");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("컬럼 매핑 생성 실패:", error);
|
||||
@@ -113,6 +168,7 @@ export function AddCategoryColumnDialog({
|
||||
setLogicalColumnName("");
|
||||
setPhysicalColumnName("");
|
||||
setDescription("");
|
||||
setSelectedMenus([]);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -130,21 +186,11 @@ export function AddCategoryColumnDialog({
|
||||
카테고리 컬럼 추가
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
같은 물리적 컬럼을 여러 메뉴에서 다른 카테고리로 사용할 수 있습니다
|
||||
2레벨 메뉴를 선택하면 해당 메뉴의 모든 하위 메뉴에서 사용할 수 있습니다
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{/* 적용 메뉴 (읽기 전용) */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">적용 메뉴</Label>
|
||||
<Input
|
||||
value={menuName}
|
||||
disabled
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 실제 컬럼 선택 */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
@@ -179,10 +225,47 @@ export function AddCategoryColumnDialog({
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
이 메뉴에서만 사용할 고유한 이름을 입력하세요
|
||||
선택한 메뉴들에서 사용할 고유한 이름을 입력하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 적용할 2레벨 메뉴 선택 (체크박스) */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
적용할 메뉴 선택 (2레벨) *
|
||||
</Label>
|
||||
<div className="border rounded-lg p-3 sm:p-4 space-y-2 max-h-48 overflow-y-auto mt-2">
|
||||
{secondLevelMenus.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">로딩 중...</p>
|
||||
) : (
|
||||
secondLevelMenus.map((menu) => (
|
||||
<div key={menu.menuObjid} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`menu-${menu.menuObjid}`}
|
||||
checked={selectedMenus.includes(menu.menuObjid)}
|
||||
onCheckedChange={() => toggleMenu(menu.menuObjid)}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`menu-${menu.menuObjid}`}
|
||||
className="text-xs sm:text-sm cursor-pointer flex-1"
|
||||
>
|
||||
{menu.parentMenuName} → {menu.menuName}
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
선택한 메뉴의 모든 하위 메뉴에서 이 카테고리를 사용할 수 있습니다
|
||||
</p>
|
||||
{selectedMenus.length > 0 && (
|
||||
<p className="text-primary mt-1 text-[10px] sm:text-xs">
|
||||
{selectedMenus.length}개 메뉴 선택됨
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설명 (선택사항) */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">설명</Label>
|
||||
@@ -207,7 +290,7 @@ export function AddCategoryColumnDialog({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!logicalColumnName || !physicalColumnName || loading}
|
||||
disabled={!logicalColumnName || !physicalColumnName || selectedMenus.length === 0 || loading}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
{loading ? "추가 중..." : "추가"}
|
||||
|
||||
Reference in New Issue
Block a user