feat: 채번규칙 메뉴 스코프 전환 완료

 주요 변경사항:
- 백엔드: menuService.ts 추가 (형제 메뉴 조회 유틸리티)
- 백엔드: numberingRuleService.getAvailableRulesForMenu() 메뉴 스코프 적용
- 백엔드: tableCategoryValueService 메뉴 스코프 준비 (menuObjid 파라미터 추가)
- 프론트엔드: TextInputConfigPanel에 부모 메뉴 선택 UI 추가
- 프론트엔드: 메뉴별 채번규칙 필터링 (형제 메뉴 공유)

🔧 기술 세부사항:
- getSiblingMenuObjids(): 같은 부모를 가진 형제 메뉴 OBJID 조회
- 채번규칙 우선순위: menu (형제) > table > global
- 사용자 메뉴(menu_type='1') 레벨 2만 부모 메뉴로 선택 가능

📝 다음 단계:
- 카테고리 컴포넌트도 메뉴 스코프로 전환 예정
This commit is contained in:
kjs
2025-11-11 14:32:00 +09:00
parent 532c80a86b
commit 668b45d4ea
16 changed files with 1838 additions and 268 deletions

View File

@@ -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>
)}