feat: 채번규칙 메뉴 스코프 전환 완료
✅ 주요 변경사항: - 백엔드: menuService.ts 추가 (형제 메뉴 조회 유틸리티) - 백엔드: numberingRuleService.getAvailableRulesForMenu() 메뉴 스코프 적용 - 백엔드: tableCategoryValueService 메뉴 스코프 준비 (menuObjid 파라미터 추가) - 프론트엔드: TextInputConfigPanel에 부모 메뉴 선택 UI 추가 - 프론트엔드: 메뉴별 채번규칙 필터링 (형제 메뉴 공유) 🔧 기술 세부사항: - getSiblingMenuObjids(): 같은 부모를 가진 형제 메뉴 OBJID 조회 - 채번규칙 우선순위: menu (형제) > table > global - 사용자 메뉴(menu_type='1') 레벨 2만 부모 메뉴로 선택 가능 📝 다음 단계: - 카테고리 컴포넌트도 메뉴 스코프로 전환 예정
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
@@ -21,8 +21,12 @@ import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감
|
||||
|
||||
export default function ScreenViewPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const screenId = parseInt(params.screenId as string);
|
||||
|
||||
// URL 쿼리에서 menuObjid 가져오기 (메뉴 스코프)
|
||||
const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined;
|
||||
|
||||
// 🆕 현재 로그인한 사용자 정보
|
||||
const { user, userName, companyCode } = useAuth();
|
||||
@@ -404,6 +408,7 @@ export default function ScreenViewPage() {
|
||||
userId={user?.userId}
|
||||
userName={userName}
|
||||
companyCode={companyCode}
|
||||
menuObjid={menuObjid}
|
||||
selectedRowsData={selectedRowsData}
|
||||
sortBy={tableSortBy}
|
||||
sortOrder={tableSortOrder}
|
||||
|
||||
@@ -26,6 +26,7 @@ interface NumberingRuleDesignerProps {
|
||||
isPreview?: boolean;
|
||||
className?: string;
|
||||
currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용)
|
||||
menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프)
|
||||
}
|
||||
|
||||
export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
@@ -36,6 +37,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
isPreview = false,
|
||||
className = "",
|
||||
currentTableName,
|
||||
menuObjid,
|
||||
}) => {
|
||||
const [savedRules, setSavedRules] = useState<NumberingRuleConfig[]>([]);
|
||||
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
|
||||
@@ -53,7 +55,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
const loadRules = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getNumberingRules();
|
||||
const response = await getNumberingRules(menuObjid);
|
||||
if (response.success && response.data) {
|
||||
setSavedRules(response.data);
|
||||
} else {
|
||||
@@ -64,7 +66,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [menuObjid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRule) {
|
||||
@@ -145,7 +147,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
"currentRule.tableName": currentRule.tableName,
|
||||
"ruleToSave.tableName": ruleToSave.tableName,
|
||||
"ruleToSave.scopeType": ruleToSave.scopeType,
|
||||
ruleToSave
|
||||
ruleToSave,
|
||||
});
|
||||
|
||||
let response;
|
||||
@@ -214,7 +216,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
|
||||
const handleNewRule = useCallback(() => {
|
||||
console.log("📋 새 규칙 생성 - currentTableName:", currentTableName);
|
||||
|
||||
|
||||
const newRule: NumberingRuleConfig = {
|
||||
ruleId: `rule-${Date.now()}`,
|
||||
ruleName: "새 채번 규칙",
|
||||
@@ -227,7 +229,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
};
|
||||
|
||||
console.log("📋 생성된 규칙 정보:", newRule);
|
||||
|
||||
|
||||
setSelectedRuleId(newRule.ruleId);
|
||||
setCurrentRule(newRule);
|
||||
|
||||
@@ -273,7 +275,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
savedRules.map((rule) => (
|
||||
<Card
|
||||
key={rule.ruleId}
|
||||
className={`py-2 border-border hover:bg-accent cursor-pointer transition-colors ${
|
||||
className={`border-border hover:bg-accent cursor-pointer py-2 transition-colors ${
|
||||
selectedRuleId === rule.ruleId ? "border-primary bg-primary/5" : "bg-card"
|
||||
}`}
|
||||
onClick={() => handleSelectRule(rule)}
|
||||
@@ -356,7 +358,7 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
||||
{currentTableName && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">적용 테이블</Label>
|
||||
<div className="flex h-9 items-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground">
|
||||
<div className="border-input bg-muted text-muted-foreground flex h-9 items-center rounded-md border px-3 text-sm">
|
||||
{currentTableName}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
|
||||
@@ -41,6 +41,7 @@ interface RealtimePreviewProps {
|
||||
userId?: string; // 🆕 현재 사용자 ID
|
||||
userName?: string; // 🆕 현재 사용자 이름
|
||||
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
||||
menuObjid?: number; // 🆕 현재 메뉴 OBJID (메뉴 스코프)
|
||||
selectedRowsData?: any[];
|
||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||
flowSelectedData?: any[];
|
||||
@@ -107,6 +108,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
userId, // 🆕 사용자 ID
|
||||
userName, // 🆕 사용자 이름
|
||||
companyCode, // 🆕 회사 코드
|
||||
menuObjid, // 🆕 메뉴 OBJID
|
||||
selectedRowsData,
|
||||
onSelectedRowsChange,
|
||||
flowSelectedData,
|
||||
@@ -344,6 +346,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={companyCode}
|
||||
menuObjid={menuObjid}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
flowSelectedData={flowSelectedData}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface TextTypeConfigPanelProps {
|
||||
config: TextTypeConfig;
|
||||
onConfigChange: (config: TextTypeConfig) => void;
|
||||
tableName?: string; // 화면의 테이블명 (선택)
|
||||
menuObjid?: number; // 메뉴 objid (선택)
|
||||
menuObjid?: number; // 메뉴 objid (선택) - 사용자가 선택한 부모 메뉴
|
||||
}
|
||||
|
||||
export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||
@@ -44,6 +44,10 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||
// 채번 규칙 목록 상태
|
||||
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||
const [loadingRules, setLoadingRules] = useState(false);
|
||||
|
||||
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
|
||||
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
||||
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(menuObjid);
|
||||
|
||||
// 로컬 상태로 실시간 입력 관리
|
||||
const [localValues, setLocalValues] = useState({
|
||||
@@ -60,31 +64,61 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||
numberingRuleId: safeConfig.numberingRuleId,
|
||||
});
|
||||
|
||||
// 채번 규칙 목록 로드
|
||||
// 부모 메뉴 목록 로드 (최상위 메뉴 또는 레벨 2 메뉴)
|
||||
useEffect(() => {
|
||||
const loadParentMenus = async () => {
|
||||
try {
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
|
||||
// 관리자 메뉴와 사용자 메뉴 모두 가져오기
|
||||
const [adminResponse, userResponse] = await Promise.all([
|
||||
apiClient.get("/admin/menus", { params: { menuType: "0" } }),
|
||||
apiClient.get("/admin/menus", { params: { menuType: "1" } })
|
||||
]);
|
||||
|
||||
const allMenus = [
|
||||
...(adminResponse.data?.data || []),
|
||||
...(userResponse.data?.data || [])
|
||||
];
|
||||
|
||||
// 레벨 2 이하 메뉴만 선택 가능 (부모가 있는 메뉴)
|
||||
const parentMenuList = allMenus.filter((menu: any) => {
|
||||
const level = menu.lev || menu.LEV || 0;
|
||||
return level >= 2; // 레벨 2 이상만 표시 (형제 메뉴가 있을 가능성)
|
||||
});
|
||||
|
||||
setParentMenus(parentMenuList);
|
||||
console.log("✅ 부모 메뉴 목록 로드:", parentMenuList.length);
|
||||
} catch (error) {
|
||||
console.error("❌ 부모 메뉴 목록 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadParentMenus();
|
||||
}, []);
|
||||
|
||||
// 채번 규칙 목록 로드 (선택된 메뉴 기준)
|
||||
useEffect(() => {
|
||||
const loadRules = async () => {
|
||||
console.log("🔄 채번 규칙 로드 시작:", {
|
||||
autoValueType: localValues.autoValueType,
|
||||
selectedMenuObjid,
|
||||
tableName,
|
||||
hasTableName: !!tableName,
|
||||
});
|
||||
|
||||
// 메뉴를 선택하지 않으면 로드하지 않음
|
||||
if (!selectedMenuObjid) {
|
||||
console.warn("⚠️ 메뉴를 선택해야 채번 규칙을 조회할 수 있습니다");
|
||||
setNumberingRules([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingRules(true);
|
||||
try {
|
||||
let response;
|
||||
|
||||
// 테이블명이 있으면 테이블 기반 필터링 사용
|
||||
if (tableName) {
|
||||
console.log("📋 테이블 기반 채번 규칙 조회 API 호출:", { tableName });
|
||||
response = await getAvailableNumberingRulesForScreen(tableName);
|
||||
console.log("📋 API 응답:", response);
|
||||
} else {
|
||||
// 테이블명이 없으면 빈 배열 (테이블 필수)
|
||||
console.warn("⚠️ 테이블명이 없어 채번 규칙을 조회할 수 없습니다");
|
||||
setNumberingRules([]);
|
||||
setLoadingRules(false);
|
||||
return;
|
||||
}
|
||||
// 선택된 메뉴의 채번 규칙 조회 (메뉴 스코프)
|
||||
console.log("📋 메뉴 기반 채번 규칙 조회 API 호출:", { menuObjid: selectedMenuObjid });
|
||||
const response = await getAvailableNumberingRules(selectedMenuObjid);
|
||||
console.log("📋 API 응답:", response);
|
||||
|
||||
if (response.success && response.data) {
|
||||
setNumberingRules(response.data);
|
||||
@@ -93,7 +127,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||
rules: response.data.map((r: any) => ({
|
||||
ruleId: r.ruleId,
|
||||
ruleName: r.ruleName,
|
||||
tableName: r.tableName,
|
||||
menuObjid: selectedMenuObjid,
|
||||
})),
|
||||
});
|
||||
} else {
|
||||
@@ -115,7 +149,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||
} else {
|
||||
console.log("⏭️ autoValueType !== 'numbering_rule', 규칙 로드 스킵:", localValues.autoValueType);
|
||||
}
|
||||
}, [localValues.autoValueType, tableName]);
|
||||
}, [localValues.autoValueType, selectedMenuObjid]);
|
||||
|
||||
// config가 변경될 때 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
@@ -314,37 +348,95 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
console.log("🔍 메뉴 선택 UI 렌더링 체크:", {
|
||||
autoValueType: localValues.autoValueType,
|
||||
isNumberingRule: localValues.autoValueType === "numbering_rule",
|
||||
parentMenusCount: parentMenus.length,
|
||||
selectedMenuObjid,
|
||||
});
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{localValues.autoValueType === "numbering_rule" && (
|
||||
<div>
|
||||
<Label htmlFor="numberingRuleId" className="text-sm font-medium">
|
||||
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={localValues.numberingRuleId}
|
||||
onValueChange={(value) => updateConfig("numberingRuleId", value)}
|
||||
disabled={loadingRules}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
|
||||
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{numberingRules.length === 0 ? (
|
||||
<SelectItem value="no-rules" disabled>
|
||||
사용 가능한 규칙이 없습니다
|
||||
</SelectItem>
|
||||
) : (
|
||||
numberingRules.map((rule) => (
|
||||
<SelectItem key={rule.ruleId} value={rule.ruleId}>
|
||||
{rule.ruleName} ({rule.ruleId})
|
||||
<>
|
||||
{/* 부모 메뉴 선택 */}
|
||||
<div>
|
||||
<Label htmlFor="parentMenu" className="text-sm font-medium">
|
||||
대상 메뉴 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedMenuObjid?.toString() || ""}
|
||||
onValueChange={(value) => setSelectedMenuObjid(parseInt(value))}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 w-full px-2 py-0 text-xs">
|
||||
<SelectValue placeholder="채번 규칙을 사용할 메뉴 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{parentMenus.length === 0 ? (
|
||||
<SelectItem value="no-menus" disabled>
|
||||
사용 가능한 메뉴가 없습니다
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||
현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
parentMenus.map((menu) => {
|
||||
const objid = menu.objid || menu.OBJID;
|
||||
const menuName = menu.menu_name_kor || menu.MENU_NAME_KOR;
|
||||
return (
|
||||
<SelectItem key={objid} value={objid.toString()}>
|
||||
{menuName}
|
||||
</SelectItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||
이 필드가 어느 메뉴에서 사용될 것인지 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 채번 규칙 선택 */}
|
||||
<div>
|
||||
<Label htmlFor="numberingRuleId" className="text-sm font-medium">
|
||||
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={localValues.numberingRuleId}
|
||||
onValueChange={(value) => updateConfig("numberingRuleId", value)}
|
||||
disabled={loadingRules || !selectedMenuObjid}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 w-full px-2 py-0 text-xs">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!selectedMenuObjid
|
||||
? "먼저 메뉴를 선택하세요"
|
||||
: loadingRules
|
||||
? "규칙 로딩 중..."
|
||||
: "채번 규칙 선택"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{numberingRules.length === 0 ? (
|
||||
<SelectItem value="no-rules" disabled>
|
||||
{!selectedMenuObjid
|
||||
? "메뉴를 먼저 선택하세요"
|
||||
: "사용 가능한 규칙이 없습니다"}
|
||||
</SelectItem>
|
||||
) : (
|
||||
numberingRules.map((rule) => (
|
||||
<SelectItem key={rule.ruleId} value={rule.ruleId}>
|
||||
{rule.ruleName} ({rule.ruleId})
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground mt-1 text-[10px]">
|
||||
선택한 메뉴와 형제 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{localValues.autoValueType === "custom" && (
|
||||
|
||||
@@ -8,14 +8,15 @@ import { GripVertical } from "lucide-react";
|
||||
interface CategoryWidgetProps {
|
||||
widgetId: string;
|
||||
tableName: string; // 현재 화면의 테이블
|
||||
menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프)
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 관리 위젯 (좌우 분할)
|
||||
* - 좌측: 현재 테이블의 카테고리 타입 컬럼 목록
|
||||
* - 우측: 선택된 컬럼의 카테고리 값 관리 (테이블 스코프)
|
||||
* - 우측: 선택된 컬럼의 카테고리 값 관리 (메뉴 스코프)
|
||||
*/
|
||||
export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||
export function CategoryWidget({ widgetId, tableName, menuObjid }: CategoryWidgetProps) {
|
||||
const [selectedColumn, setSelectedColumn] = useState<{
|
||||
columnName: string;
|
||||
columnLabel: string;
|
||||
@@ -69,6 +70,7 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||
onColumnSelect={(columnName, columnLabel) =>
|
||||
setSelectedColumn({ columnName, columnLabel })
|
||||
}
|
||||
menuObjid={menuObjid}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -87,6 +89,7 @@ export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||
tableName={tableName}
|
||||
columnName={selectedColumn.columnName}
|
||||
columnLabel={selectedColumn.columnLabel}
|
||||
menuObjid={menuObjid}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
|
||||
|
||||
@@ -16,13 +16,14 @@ interface CategoryColumnListProps {
|
||||
tableName: string;
|
||||
selectedColumn: string | null;
|
||||
onColumnSelect: (columnName: string, columnLabel: string) => void;
|
||||
menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프)
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 컬럼 목록 (좌측 패널)
|
||||
* - 현재 테이블에서 input_type='category'인 컬럼들을 표시 (테이블 스코프)
|
||||
* - 현재 테이블에서 input_type='category'인 컬럼들을 표시 (메뉴 스코프)
|
||||
*/
|
||||
export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }: CategoryColumnListProps) {
|
||||
export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) {
|
||||
const [columns, setColumns] = useState<CategoryColumn[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -89,7 +90,7 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect }
|
||||
// 각 컬럼의 값 개수 가져오기
|
||||
let valueCount = 0;
|
||||
try {
|
||||
const valuesResult = await getCategoryValues(tableName, colName, false);
|
||||
const valuesResult = await getCategoryValues(tableName, colName, false, menuObjid);
|
||||
if (valuesResult.success && valuesResult.data) {
|
||||
valueCount = valuesResult.data.length;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ interface CategoryValueManagerProps {
|
||||
columnName: string;
|
||||
columnLabel: string;
|
||||
onValueCountChange?: (count: number) => void;
|
||||
menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프)
|
||||
}
|
||||
|
||||
export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
@@ -36,6 +37,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
columnName,
|
||||
columnLabel,
|
||||
onValueCountChange,
|
||||
menuObjid,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [values, setValues] = useState<TableCategoryValue[]>([]);
|
||||
@@ -81,7 +83,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// includeInactive: true로 비활성 값도 포함
|
||||
const response = await getCategoryValues(tableName, columnName, true);
|
||||
const response = await getCategoryValues(tableName, columnName, true, menuObjid);
|
||||
if (response.success && response.data) {
|
||||
setValues(response.data);
|
||||
setFilteredValues(response.data);
|
||||
@@ -101,11 +103,23 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
|
||||
const handleAddValue = async (newValue: TableCategoryValue) => {
|
||||
try {
|
||||
const response = await addCategoryValue({
|
||||
...newValue,
|
||||
tableName,
|
||||
columnName,
|
||||
});
|
||||
if (!menuObjid) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: "메뉴 정보가 없습니다. 카테고리 값을 추가할 수 없습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await addCategoryValue(
|
||||
{
|
||||
...newValue,
|
||||
tableName,
|
||||
columnName,
|
||||
},
|
||||
menuObjid
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
await loadCategoryValues();
|
||||
@@ -128,7 +142,7 @@ export const CategoryValueManager: React.FC<CategoryValueManagerProps> = ({
|
||||
title: "오류",
|
||||
description: error.message || "카테고리 값 추가에 실패했습니다",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,19 +21,30 @@ export async function getCategoryColumns(tableName: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 목록 조회 (테이블 스코프)
|
||||
* 카테고리 값 목록 조회 (메뉴 스코프)
|
||||
*
|
||||
* @param tableName 테이블명
|
||||
* @param columnName 컬럼명
|
||||
* @param includeInactive 비활성 값 포함 여부
|
||||
* @param menuObjid 메뉴 OBJID (선택사항, 제공 시 형제 메뉴의 카테고리 값 포함)
|
||||
*/
|
||||
export async function getCategoryValues(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
includeInactive: boolean = false
|
||||
includeInactive: boolean = false,
|
||||
menuObjid?: number
|
||||
) {
|
||||
try {
|
||||
const params: any = { includeInactive };
|
||||
if (menuObjid) {
|
||||
params.menuObjid = menuObjid;
|
||||
}
|
||||
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: TableCategoryValue[];
|
||||
}>(`/table-categories/${tableName}/${columnName}/values`, {
|
||||
params: { includeInactive },
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
@@ -43,14 +54,23 @@ export async function getCategoryValues(
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 값 추가
|
||||
* 카테고리 값 추가 (메뉴 스코프)
|
||||
*
|
||||
* @param value 카테고리 값 정보
|
||||
* @param menuObjid 메뉴 OBJID (필수)
|
||||
*/
|
||||
export async function addCategoryValue(value: TableCategoryValue) {
|
||||
export async function addCategoryValue(
|
||||
value: TableCategoryValue,
|
||||
menuObjid: number
|
||||
) {
|
||||
try {
|
||||
const response = await apiClient.post<{
|
||||
success: boolean;
|
||||
data: TableCategoryValue;
|
||||
}>("/table-categories/values", value);
|
||||
}>("/table-categories/values", {
|
||||
...value,
|
||||
menuObjid, // ← menuObjid 포함
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error("카테고리 값 추가 실패:", error);
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface DynamicComponentRendererProps {
|
||||
screenId?: number;
|
||||
tableName?: string;
|
||||
menuId?: number; // 🆕 메뉴 ID (카테고리 관리 등에 필요)
|
||||
menuObjid?: number; // 🆕 메뉴 OBJID (메뉴 스코프 - 카테고리/채번)
|
||||
selectedScreen?: any; // 🆕 화면 정보 전체 (menuId 등 추출용)
|
||||
userId?: string; // 🆕 현재 사용자 ID
|
||||
userName?: string; // 🆕 현재 사용자 이름
|
||||
@@ -224,6 +225,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
onFormDataChange,
|
||||
tableName,
|
||||
menuId, // 🆕 메뉴 ID
|
||||
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
|
||||
selectedScreen, // 🆕 화면 정보
|
||||
onRefresh,
|
||||
onClose,
|
||||
@@ -319,6 +321,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
onChange: handleChange, // 개선된 onChange 핸들러 전달
|
||||
tableName,
|
||||
menuId, // 🆕 메뉴 ID
|
||||
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
|
||||
selectedScreen, // 🆕 화면 정보
|
||||
onRefresh,
|
||||
onClose,
|
||||
|
||||
@@ -15,32 +15,65 @@ export interface TextInputConfigPanelProps {
|
||||
config: TextInputConfig;
|
||||
onChange: (config: Partial<TextInputConfig>) => void;
|
||||
screenTableName?: string; // 🆕 현재 화면의 테이블명
|
||||
menuObjid?: number; // 🆕 메뉴 OBJID (사용자 선택)
|
||||
}
|
||||
|
||||
/**
|
||||
* TextInput 설정 패널
|
||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||
*/
|
||||
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange, screenTableName }) => {
|
||||
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange, screenTableName, menuObjid }) => {
|
||||
// 채번 규칙 목록 상태
|
||||
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
||||
const [loadingRules, setLoadingRules] = useState(false);
|
||||
|
||||
// 부모 메뉴 목록 상태 (채번규칙 사용을 위한 선택)
|
||||
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
||||
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(menuObjid);
|
||||
const [loadingMenus, setLoadingMenus] = useState(false);
|
||||
|
||||
// 채번 규칙 목록 로드
|
||||
// 부모 메뉴 목록 로드 (사용자 메뉴의 레벨 2만)
|
||||
useEffect(() => {
|
||||
const loadMenus = async () => {
|
||||
setLoadingMenus(true);
|
||||
try {
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const response = await apiClient.get("/admin/menus");
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const allMenus = response.data.data;
|
||||
|
||||
// 사용자 메뉴(menu_type='1')의 레벨 2만 필터링
|
||||
const level2UserMenus = allMenus.filter((menu: any) =>
|
||||
menu.menu_type === '1' && menu.lev === 2
|
||||
);
|
||||
|
||||
setParentMenus(level2UserMenus);
|
||||
console.log("✅ 부모 메뉴 로드 완료:", level2UserMenus.length, "개", level2UserMenus);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("부모 메뉴 로드 실패:", error);
|
||||
} finally {
|
||||
setLoadingMenus(false);
|
||||
}
|
||||
};
|
||||
loadMenus();
|
||||
}, []);
|
||||
|
||||
// 채번 규칙 목록 로드 (선택된 메뉴 기준)
|
||||
useEffect(() => {
|
||||
const loadRules = async () => {
|
||||
// 메뉴가 선택되지 않았으면 로드하지 않음
|
||||
if (!selectedMenuObjid) {
|
||||
console.log("⚠️ 메뉴가 선택되지 않아 채번 규칙을 로드하지 않습니다");
|
||||
setNumberingRules([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingRules(true);
|
||||
try {
|
||||
let response;
|
||||
|
||||
// 🆕 테이블명이 있으면 테이블 기반 필터링, 없으면 전체 조회
|
||||
if (screenTableName) {
|
||||
console.log("🔍 TextInputConfigPanel: 테이블 기반 채번 규칙 로드", { screenTableName });
|
||||
response = await getAvailableNumberingRulesForScreen(screenTableName);
|
||||
} else {
|
||||
console.log("🔍 TextInputConfigPanel: 전체 채번 규칙 로드 (테이블명 없음)");
|
||||
response = await getAvailableNumberingRules();
|
||||
}
|
||||
console.log("🔍 선택된 메뉴 기반 채번 규칙 로드", { selectedMenuObjid });
|
||||
const response = await getAvailableNumberingRules(selectedMenuObjid);
|
||||
|
||||
if (response.success && response.data) {
|
||||
setNumberingRules(response.data);
|
||||
@@ -48,6 +81,7 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("채번 규칙 목록 로드 실패:", error);
|
||||
setNumberingRules([]);
|
||||
} finally {
|
||||
setLoadingRules(false);
|
||||
}
|
||||
@@ -57,7 +91,7 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||
if (config.autoGeneration?.type === "numbering_rule") {
|
||||
loadRules();
|
||||
}
|
||||
}, [config.autoGeneration?.type, screenTableName]);
|
||||
}, [config.autoGeneration?.type, selectedMenuObjid]);
|
||||
|
||||
const handleChange = (key: keyof TextInputConfig, value: any) => {
|
||||
onChange({ [key]: value });
|
||||
@@ -157,50 +191,100 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||
|
||||
{/* 채번 규칙 선택 */}
|
||||
{config.autoGeneration?.type === "numbering_rule" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="numberingRuleId">
|
||||
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={config.autoGeneration?.options?.numberingRuleId || ""}
|
||||
onValueChange={(value) => {
|
||||
const currentConfig = config.autoGeneration!;
|
||||
handleChange("autoGeneration", {
|
||||
...currentConfig,
|
||||
options: {
|
||||
...currentConfig.options,
|
||||
numberingRuleId: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={loadingRules}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{numberingRules.length === 0 ? (
|
||||
<SelectItem value="no-rules" disabled>
|
||||
사용 가능한 규칙이 없습니다
|
||||
</SelectItem>
|
||||
) : (
|
||||
numberingRules.map((rule) => (
|
||||
<SelectItem key={rule.ruleId} value={rule.ruleId}>
|
||||
{rule.ruleName}
|
||||
{rule.description && (
|
||||
<span className="text-muted-foreground ml-2 text-xs">
|
||||
- {rule.description}
|
||||
</span>
|
||||
)}
|
||||
<>
|
||||
{/* 부모 메뉴 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="targetMenu">
|
||||
대상 메뉴 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedMenuObjid?.toString() || ""}
|
||||
onValueChange={(value) => {
|
||||
const menuObjid = parseInt(value);
|
||||
setSelectedMenuObjid(menuObjid);
|
||||
console.log("✅ 메뉴 선택됨:", menuObjid);
|
||||
}}
|
||||
disabled={loadingMenus}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingMenus ? "메뉴 로딩 중..." : "채번규칙을 사용할 메뉴 선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{parentMenus.length === 0 ? (
|
||||
<SelectItem value="no-menus" disabled>
|
||||
사용 가능한 메뉴가 없습니다
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
현재 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
parentMenus.map((menu) => (
|
||||
<SelectItem key={menu.objid} value={menu.objid.toString()}>
|
||||
{menu.menu_name_kor}
|
||||
{menu.menu_name_eng && (
|
||||
<span className="text-muted-foreground ml-2 text-xs">
|
||||
({menu.menu_name_eng})
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
이 입력 필드가 어느 메뉴에 속할지 선택하세요 (해당 메뉴의 채번규칙이 적용됩니다)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 채번 규칙 선택 (메뉴 선택 후) */}
|
||||
{selectedMenuObjid ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="numberingRuleId">
|
||||
채번 규칙 선택 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={config.autoGeneration?.options?.numberingRuleId || ""}
|
||||
onValueChange={(value) => {
|
||||
const currentConfig = config.autoGeneration!;
|
||||
handleChange("autoGeneration", {
|
||||
...currentConfig,
|
||||
options: {
|
||||
...currentConfig.options,
|
||||
numberingRuleId: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={loadingRules}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingRules ? "규칙 로딩 중..." : "채번 규칙 선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{numberingRules.length === 0 ? (
|
||||
<SelectItem value="no-rules" disabled>
|
||||
선택된 메뉴에 사용 가능한 규칙이 없습니다
|
||||
</SelectItem>
|
||||
) : (
|
||||
numberingRules.map((rule) => (
|
||||
<SelectItem key={rule.ruleId} value={rule.ruleId}>
|
||||
{rule.ruleName}
|
||||
{rule.description && (
|
||||
<span className="text-muted-foreground ml-2 text-xs">
|
||||
- {rule.description}
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
선택된 메뉴 및 형제 메뉴에서 사용 가능한 채번 규칙만 표시됩니다
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
|
||||
먼저 대상 메뉴를 선택하세요
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user