@j-solution/components 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/assets/jwms-portal-frontend-ZDGjD3Lz.css +1 -0
- package/assets/styles/j-components.css +1 -1
- package/components/atoms/JCombo.vue.cjs +1 -1
- package/components/atoms/JCombo.vue.cjs.map +1 -1
- package/components/atoms/JCombo.vue.js +40 -36
- package/components/atoms/JCombo.vue.js.map +1 -1
- package/components/atoms/JGrid.vue.cjs +1 -1
- package/components/atoms/JGrid.vue.js +2 -2
- package/components/atoms/JGrid.vue2.cjs +1 -1
- package/components/atoms/JGrid.vue2.cjs.map +1 -1
- package/components/atoms/JGrid.vue2.js +59 -43
- package/components/atoms/JGrid.vue2.js.map +1 -1
- package/components/organisms/JFilterBar.vue.cjs +1 -1
- package/components/organisms/JFilterBar.vue.cjs.map +1 -1
- package/components/organisms/JFilterBar.vue.js +112 -30
- package/components/organisms/JFilterBar.vue.js.map +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs.map +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js +32 -28
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js.map +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +51 -4
- package/assets/jwms-portal-frontend-Cs1trVbC.css +0 -1
|
@@ -1,51 +1,133 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
import { ChevronDown as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { defineComponent as T, computed as g, createElementBlock as c, openBlock as o, createElementVNode as r, withDirectives as $, createCommentVNode as u, createVNode as v, unref as x, normalizeClass as j, Fragment as z, renderList as E, createBlock as d, withCtx as p, toDisplayString as f, withModifiers as F, renderSlot as V, createTextVNode as b, vShow as N } from "vue";
|
|
2
|
+
import { ChevronDown as D, X as R } from "lucide-vue-next";
|
|
3
|
+
import O from "../atoms/JBadge.vue.js";
|
|
4
|
+
import _ from "../atoms/JButton.vue.js";
|
|
5
|
+
const J = { class: "w-full rounded-lg border bg-card text-card-foreground" }, L = { class: "flex items-center justify-between px-4 py-2" }, M = { class: "flex items-center gap-2" }, X = {
|
|
6
|
+
key: 1,
|
|
7
|
+
class: "flex items-center gap-1 flex-wrap"
|
|
8
|
+
}, q = { class: "text-muted-foreground" }, G = ["onClick"], H = { class: "flex items-center gap-2" }, I = { class: "px-4 pb-4" }, W = /* @__PURE__ */ T({
|
|
7
9
|
__name: "JFilterBar",
|
|
8
10
|
props: {
|
|
9
|
-
mode: { default: "full" },
|
|
10
11
|
collapsed: { type: Boolean, default: !1 },
|
|
11
|
-
collapsible: { type: Boolean, default: !0 }
|
|
12
|
+
collapsible: { type: Boolean, default: !0 },
|
|
13
|
+
filterValues: { default: () => ({}) },
|
|
14
|
+
filterConfig: { default: () => ({}) },
|
|
15
|
+
showResetButton: { type: Boolean, default: !1 },
|
|
16
|
+
showSearchButton: { type: Boolean, default: !1 },
|
|
17
|
+
resetButtonText: { default: "초기화" },
|
|
18
|
+
searchButtonText: { default: "조회" }
|
|
12
19
|
},
|
|
13
|
-
emits: ["update:collapsed"],
|
|
14
|
-
setup(
|
|
15
|
-
const
|
|
16
|
-
function
|
|
17
|
-
|
|
20
|
+
emits: ["update:collapsed", "update:filterValues", "search"],
|
|
21
|
+
setup(n, { emit: w }) {
|
|
22
|
+
const l = n, a = w, m = g(() => l.collapsible ? !l.collapsed : !0);
|
|
23
|
+
function B(e) {
|
|
24
|
+
return !!(e == null || typeof e == "string" && e.trim() === "" || Array.isArray(e) && e.length === 0);
|
|
18
25
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
const h = g(() => {
|
|
27
|
+
const e = [];
|
|
28
|
+
for (const [t, s] of Object.entries(l.filterConfig)) {
|
|
29
|
+
const i = l.filterValues[t];
|
|
30
|
+
if (B(i)) continue;
|
|
31
|
+
const y = s.displayValue ? s.displayValue(i) : String(i);
|
|
32
|
+
y.trim() !== "" && e.push({
|
|
33
|
+
key: t,
|
|
34
|
+
label: s.label,
|
|
35
|
+
value: y
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return e;
|
|
39
|
+
});
|
|
40
|
+
function k() {
|
|
41
|
+
a("update:collapsed", !l.collapsed);
|
|
42
|
+
}
|
|
43
|
+
function C() {
|
|
44
|
+
const e = {};
|
|
45
|
+
for (const t of Object.keys(l.filterValues)) {
|
|
46
|
+
const s = l.filterValues[t];
|
|
47
|
+
typeof s == "string" ? e[t] = "" : Array.isArray(s) ? e[t] = [] : e[t] = null;
|
|
48
|
+
}
|
|
49
|
+
a("update:filterValues", e);
|
|
50
|
+
}
|
|
51
|
+
function S() {
|
|
52
|
+
a("search");
|
|
53
|
+
}
|
|
54
|
+
function A(e) {
|
|
55
|
+
const t = { ...l.filterValues }, s = t[e];
|
|
56
|
+
typeof s == "string" ? t[e] = "" : Array.isArray(s) ? t[e] = [] : t[e] = null, a("update:filterValues", t);
|
|
57
|
+
}
|
|
58
|
+
return (e, t) => (o(), c("div", J, [
|
|
59
|
+
r("div", L, [
|
|
60
|
+
r("div", M, [
|
|
61
|
+
n.collapsible ? (o(), c("button", {
|
|
23
62
|
key: 0,
|
|
24
63
|
type: "button",
|
|
25
64
|
class: "flex items-center justify-center h-7 w-7 rounded hover:bg-accent hover:text-accent-foreground transition-colors",
|
|
26
|
-
onClick:
|
|
65
|
+
onClick: k
|
|
27
66
|
}, [
|
|
28
|
-
|
|
29
|
-
class:
|
|
67
|
+
v(x(D), {
|
|
68
|
+
class: j([
|
|
30
69
|
"h-4 w-4 transition-transform",
|
|
31
|
-
|
|
70
|
+
m.value ? "rotate-0" : "-rotate-90"
|
|
32
71
|
])
|
|
33
72
|
}, null, 8, ["class"])
|
|
34
|
-
])) :
|
|
73
|
+
])) : u("", !0),
|
|
74
|
+
h.value.length > 0 ? (o(), c("div", X, [
|
|
75
|
+
(o(!0), c(z, null, E(h.value, (s) => (o(), d(O, {
|
|
76
|
+
key: s.key,
|
|
77
|
+
variant: "secondary",
|
|
78
|
+
size: "sm",
|
|
79
|
+
class: "flex items-center gap-1 cursor-default"
|
|
80
|
+
}, {
|
|
81
|
+
default: p(() => [
|
|
82
|
+
r("span", q, f(s.label) + ":", 1),
|
|
83
|
+
r("span", null, f(s.value), 1),
|
|
84
|
+
r("button", {
|
|
85
|
+
type: "button",
|
|
86
|
+
class: "ml-0.5 rounded-full hover:bg-gray-300 p-0.5 transition-colors",
|
|
87
|
+
onClick: F((i) => A(s.key), ["stop"])
|
|
88
|
+
}, [
|
|
89
|
+
v(x(R), { class: "h-3 w-3" })
|
|
90
|
+
], 8, G)
|
|
91
|
+
]),
|
|
92
|
+
_: 2
|
|
93
|
+
}, 1024))), 128))
|
|
94
|
+
])) : u("", !0)
|
|
35
95
|
]),
|
|
36
|
-
|
|
37
|
-
|
|
96
|
+
r("div", H, [
|
|
97
|
+
V(e.$slots, "actions"),
|
|
98
|
+
n.showResetButton ? (o(), d(_, {
|
|
99
|
+
key: 0,
|
|
100
|
+
variant: "outline",
|
|
101
|
+
size: "sm",
|
|
102
|
+
onClick: C
|
|
103
|
+
}, {
|
|
104
|
+
default: p(() => [
|
|
105
|
+
b(f(n.resetButtonText), 1)
|
|
106
|
+
]),
|
|
107
|
+
_: 1
|
|
108
|
+
})) : u("", !0),
|
|
109
|
+
n.showSearchButton ? (o(), d(_, {
|
|
110
|
+
key: 1,
|
|
111
|
+
styletype: "primary",
|
|
112
|
+
size: "sm",
|
|
113
|
+
onClick: S
|
|
114
|
+
}, {
|
|
115
|
+
default: p(() => [
|
|
116
|
+
b(f(n.searchButtonText), 1)
|
|
117
|
+
]),
|
|
118
|
+
_: 1
|
|
119
|
+
})) : u("", !0)
|
|
38
120
|
])
|
|
39
121
|
]),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
], 512)
|
|
43
|
-
[
|
|
44
|
-
])
|
|
122
|
+
$(r("div", I, [
|
|
123
|
+
V(e.$slots, "filters")
|
|
124
|
+
], 512), [
|
|
125
|
+
[N, m.value]
|
|
126
|
+
])
|
|
45
127
|
]));
|
|
46
128
|
}
|
|
47
129
|
});
|
|
48
130
|
export {
|
|
49
|
-
|
|
131
|
+
W as default
|
|
50
132
|
};
|
|
51
133
|
//# sourceMappingURL=JFilterBar.vue.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JFilterBar.vue.js","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\r\n <div class=\"w-full rounded-lg border bg-card text-card-foreground\">\r\n <!-- Row 1: toolbar -->\r\n <div class=\"flex items-center justify-between px-4 py-2\">\r\n <div class=\"flex items-center\">\r\n <button\r\n v-if=\"
|
|
1
|
+
{"version":3,"file":"JFilterBar.vue.js","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\r\n <div class=\"w-full rounded-lg border bg-card text-card-foreground\">\r\n <!-- Row 1: toolbar -->\r\n <div class=\"flex items-center justify-between px-4 py-2\">\r\n <div class=\"flex items-center gap-2\">\r\n <button\r\n v-if=\"collapsible\"\r\n type=\"button\"\r\n class=\"flex items-center justify-center h-7 w-7 rounded hover:bg-accent hover:text-accent-foreground transition-colors\"\r\n @click=\"toggleCollapsed\"\r\n >\r\n <ChevronDown\r\n :class=\"[\r\n 'h-4 w-4 transition-transform',\r\n isExpanded ? 'rotate-0' : '-rotate-90',\r\n ]\"\r\n />\r\n </button>\r\n <!-- 선택된 필터 뱃지 표시 -->\r\n <div v-if=\"activeFilters.length > 0\" class=\"flex items-center gap-1 flex-wrap\">\r\n <JBadge\r\n v-for=\"filter in activeFilters\"\r\n :key=\"filter.key\"\r\n variant=\"secondary\"\r\n size=\"sm\"\r\n class=\"flex items-center gap-1 cursor-default\"\r\n >\r\n <span class=\"text-muted-foreground\">{{ filter.label }}:</span>\r\n <span>{{ filter.value }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"ml-0.5 rounded-full hover:bg-gray-300 p-0.5 transition-colors\"\r\n @click.stop=\"removeFilter(filter.key)\"\r\n >\r\n <X class=\"h-3 w-3\" />\r\n </button>\r\n </JBadge>\r\n </div>\r\n </div>\r\n <div class=\"flex items-center gap-2\">\r\n <slot name=\"actions\" />\r\n <JButton\r\n v-if=\"showResetButton\"\r\n variant=\"outline\"\r\n size=\"sm\"\r\n @click=\"handleReset\"\r\n >\r\n {{ resetButtonText }}\r\n </JButton>\r\n <JButton\r\n v-if=\"showSearchButton\"\r\n styletype=\"primary\"\r\n size=\"sm\"\r\n @click=\"handleSearch\"\r\n >\r\n {{ searchButtonText }}\r\n </JButton>\r\n </div>\r\n </div>\r\n\r\n <!-- Row 2: filters -->\r\n <div v-show=\"isExpanded\" class=\"px-4 pb-4\">\r\n <slot name=\"filters\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport { ChevronDown, X } from 'lucide-vue-next'\r\nimport JBadge from '@/components/atoms/JBadge.vue'\r\nimport JButton from '@/components/atoms/JButton.vue'\r\n\r\n/** 활성 필터 아이템 타입 */\r\nexport interface ActiveFilterItem {\r\n /** 필터 식별 키 */\r\n key: string\r\n /** 표시할 라벨 (필터명) */\r\n label: string\r\n /** 표시할 값 */\r\n value: string\r\n}\r\n\r\n/** 필터 설정 타입 */\r\nexport interface FilterConfigItem {\r\n /** 표시할 라벨 */\r\n label: string\r\n /** 값을 표시용 문자열로 변환 (예: combo value -> label) */\r\n displayValue?: (value: unknown) => string\r\n}\r\n\r\nexport interface JFilterBarProps {\r\n /** 필터 접힘 상태 (v-model 지원) */\r\n collapsed?: boolean\r\n /** 접기/펼치기 가능 여부. false면 토글 버튼 숨김 & 필터 항상 표시 */\r\n collapsible?: boolean\r\n /** 필터 값 객체 (v-model:filterValues 지원) */\r\n filterValues?: Record<string, unknown>\r\n /** 필터 설정 (label, displayValue 등) */\r\n filterConfig?: Record<string, FilterConfigItem>\r\n /** 초기화 버튼 표시 여부 */\r\n showResetButton?: boolean\r\n /** 조회 버튼 표시 여부 */\r\n showSearchButton?: boolean\r\n /** 초기화 버튼 텍스트 */\r\n resetButtonText?: string\r\n /** 조회 버튼 텍스트 */\r\n searchButtonText?: string\r\n}\r\n\r\nconst props = withDefaults(defineProps<JFilterBarProps>(), {\r\n collapsed: false,\r\n collapsible: true,\r\n filterValues: () => ({}),\r\n filterConfig: () => ({}),\r\n showResetButton: false,\r\n showSearchButton: false,\r\n resetButtonText: '초기화',\r\n searchButtonText: '조회',\r\n})\r\n\r\nconst emit = defineEmits<{\r\n 'update:collapsed': [value: boolean]\r\n 'update:filterValues': [value: Record<string, unknown>]\r\n /** 조회 버튼 클릭 */\r\n search: []\r\n}>()\r\n\r\nconst isExpanded = computed(() => {\r\n if (!props.collapsible) return true\r\n return !props.collapsed\r\n})\r\n\r\n/** 값이 비어있는지 확인 */\r\nfunction isEmpty(value: unknown): boolean {\r\n if (value === null || value === undefined) return true\r\n if (typeof value === 'string' && value.trim() === '') return true\r\n if (Array.isArray(value) && value.length === 0) return true\r\n return false\r\n}\r\n\r\n/** filterValues + filterConfig 기반으로 활성 필터 목록 자동 생성 */\r\nconst activeFilters = computed<ActiveFilterItem[]>(() => {\r\n const filters: ActiveFilterItem[] = []\r\n\r\n for (const [key, config] of Object.entries(props.filterConfig)) {\r\n const value = props.filterValues[key]\r\n if (isEmpty(value)) continue\r\n\r\n const displayValue = config.displayValue ? config.displayValue(value) : String(value)\r\n if (displayValue.trim() === '') continue\r\n\r\n filters.push({\r\n key,\r\n label: config.label,\r\n value: displayValue,\r\n })\r\n }\r\n\r\n return filters\r\n})\r\n\r\nfunction toggleCollapsed() {\r\n emit('update:collapsed', !props.collapsed)\r\n}\r\n\r\nfunction handleReset() {\r\n // filterValues의 모든 값을 초기화\r\n const resetValues: Record<string, unknown> = {}\r\n for (const key of Object.keys(props.filterValues)) {\r\n const currentValue = props.filterValues[key]\r\n if (typeof currentValue === 'string') {\r\n resetValues[key] = ''\r\n } else if (Array.isArray(currentValue)) {\r\n resetValues[key] = []\r\n } else {\r\n resetValues[key] = null\r\n }\r\n }\r\n emit('update:filterValues', resetValues)\r\n}\r\n\r\nfunction handleSearch() {\r\n emit('search')\r\n}\r\n\r\nfunction removeFilter(key: string) {\r\n // filterValues 업데이트 (해당 키 값을 초기화)\r\n const newValues = { ...props.filterValues }\r\n const currentValue = newValues[key]\r\n\r\n // 타입에 따라 적절한 초기값으로 설정\r\n if (typeof currentValue === 'string') {\r\n newValues[key] = ''\r\n } else if (Array.isArray(currentValue)) {\r\n newValues[key] = []\r\n } else {\r\n newValues[key] = null\r\n }\r\n\r\n emit('update:filterValues', newValues)\r\n}\r\n</script>\r\n"],"names":["props","__props","emit","__emit","isExpanded","computed","isEmpty","value","activeFilters","filters","key","config","displayValue","toggleCollapsed","handleReset","resetValues","currentValue","handleSearch","removeFilter","newValues","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_unref","ChevronDown","_normalizeClass","_hoisted_4","_Fragment","_renderList","filter","_createBlock","JBadge","_hoisted_5","_toDisplayString","_withModifiers","$event","X","_hoisted_7","_renderSlot","_ctx","JButton","_withDirectives","_hoisted_8"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),B=require("vue-router"),f=require("../../atoms/JIcon.vue.cjs"),
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),B=require("vue-router"),f=require("../../atoms/JIcon.vue.cjs"),m=require("../../../lib/utils.cjs"),F={key:1,class:"w-4 flex-shrink-0"},P={key:0,class:"w-full"},S=e.defineComponent({__name:"JDynamicMenuItem",props:{item:{},level:{default:0},permissions:{default:()=>[]},activePath:{},expandedKeys:{default:()=>new Set},favorites:{default:()=>[]},onFavoriteToggle:{},isFavorite:{},styletype:{default:"default"},className:{},maxDepth:{default:10},disableNavigation:{type:Boolean,default:!1},activeKey:{default:null}},emits:["menuClick","expandChange"],setup(t,{emit:h}){const n=t,l=h,k=B.useRouter(),x=e.computed(()=>m.hasMenuPermission(n.item.menuKey,n.permissions)),r=e.computed(()=>n.activeKey!==void 0&&n.activeKey!==null?n.item.menuKey===n.activeKey:!n.item.path||!n.activePath?!1:n.activePath===n.item.path),c=e.computed(()=>n.item.menuType==="F"||Array.isArray(n.item.children)&&n.item.children.length>0),d=e.computed(()=>{if(!c.value)return!1;const i=n.item.menuKey||n.item.label;return n.expandedKeys?.has(i)??!1}),v=e.computed(()=>n.item.disabled||!x.value),C=e.computed(()=>({paddingLeft:`${12+(n.level||0)*16}px`})),b=i=>{if(v.value){i.preventDefault();return}if(c.value){const a=n.item.menuKey||n.item.label,u=!d.value;l("expandChange",a,u),l("menuClick",{menuItem:n.item,path:[n.item],event:i})}else!n.disableNavigation&&n.item.path&&k.push(n.item.path),l("menuClick",{menuItem:n.item,path:[n.item],event:i})},g={default:{itemClass:"flex items-center gap-2 py-2 rounded-md cursor-pointer transition-colors group",labelClass:"flex-1 truncate",iconSize:"sm"},minimal:{itemClass:"flex items-center gap-1.5 py-1.5 rounded-md cursor-pointer transition-colors group",labelClass:"flex-1 truncate text-xs",iconSize:"sm"}},o=e.computed(()=>g[n.styletype]??g.default),p=e.computed(()=>d.value?"chevronDown":"chevronRight");return(i,a)=>{const u=e.resolveComponent("JDynamicMenuItem",!0);return e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(e.unref(m.cn)("w-full",t.className))},[e.createElementVNode("div",{class:e.normalizeClass(e.unref(m.cn)(o.value.itemClass,{"bg-accent text-accent-foreground":r.value,"hover:bg-accent/50":!v.value&&!r.value,"opacity-50 cursor-not-allowed":v.value,"font-medium":r.value})),style:e.normalizeStyle(C.value),onClick:b},[c.value?(e.openBlock(),e.createBlock(f.default,{key:0,name:p.value,size:o.value.iconSize,class:"flex-shrink-0"},null,8,["name","size"])):(e.openBlock(),e.createElementBlock("span",F)),t.item.icon?(e.openBlock(),e.createBlock(f.default,{key:2,name:t.item.icon,size:o.value.iconSize,class:"flex-shrink-0"},null,8,["name","size"])):e.createCommentVNode("",!0),e.createElementVNode("span",{class:e.normalizeClass(o.value.labelClass)},e.toDisplayString(t.item.label),3),t.item.menuKey&&t.item.menuType==="L"&&t.onFavoriteToggle?(e.openBlock(),e.createElementBlock("button",{key:3,class:e.normalizeClass(e.unref(m.cn)("opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0",n.styletype==="minimal"?"p-0.5":"p-1",t.isFavorite&&t.isFavorite(t.item.menuKey)&&"opacity-100")),onClick:a[0]||(a[0]=e.withModifiers(s=>t.onFavoriteToggle(t.item.menuKey),["stop"]))},[e.createVNode(f.default,{name:(t.isFavorite&&t.isFavorite(t.item.menuKey),"star"),size:o.value.iconSize,class:e.normalizeClass(t.isFavorite&&t.isFavorite(t.item.menuKey)?"text-yellow-500 fill-yellow-500":"text-muted-foreground")},null,8,["name","size","class"])],2)):e.createCommentVNode("",!0)],6),c.value&&d.value&&t.item.children&&Array.isArray(t.item.children)&&t.item.children.length>0&&t.level+1<t.maxDepth?(e.openBlock(),e.createElementBlock("div",P,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.item.children,(s,K)=>(e.openBlock(),e.createBlock(u,{key:s.menuKey||s.label||K,item:s,level:t.level+1,"max-depth":t.maxDepth,permissions:t.permissions,"active-path":t.activePath,"expanded-keys":t.expandedKeys,favorites:t.favorites,"on-favorite-toggle":t.onFavoriteToggle,"is-favorite":t.isFavorite,styletype:t.styletype,"disable-navigation":t.disableNavigation,"active-key":t.activeKey,onMenuClick:a[1]||(a[1]=y=>l("menuClick",y)),onExpandChange:a[2]||(a[2]=(y,z)=>l("expandChange",y,z))},null,8,["item","level","max-depth","permissions","active-path","expanded-keys","favorites","on-favorite-toggle","is-favorite","styletype","disable-navigation","active-key"]))),128))])):e.createCommentVNode("",!0)],2)}}});exports.default=S;
|
|
2
2
|
//# sourceMappingURL=JDynamicMenuItem.vue.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JDynamicMenuItem.vue.cjs","sources":["../../../../../src/components/organisms/JSidebarSimple/JDynamicMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport { useRouter } from 'vue-router'\r\nimport type { SidebarMenuItem, MenuPermission, MenuClickEvent } from '@/types/sidebar-menu.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport { cn, hasMenuPermission } from '@/lib/utils'\r\n\r\n/**\r\n * JDynamicMenuItem - 재귀적 메뉴 아이템 컴포넌트\r\n * Recursive Menu Item Component\r\n * \r\n * @description\r\n * 다단계 메뉴 구조를 재귀적으로 렌더링하는 컴포넌트입니다.\r\n * 폴더 타입 메뉴는 확장/축소가 가능하고, 링크 타입 메뉴는 클릭 시 라우팅합니다.\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(\n defineProps<{\n /** 메뉴 아이템 */\n item: SidebarMenuItem\n /** 메뉴 레벨 (들여쓰기용, 0부터 시작) */\n level?: number\n /** 권한 목록 */\n permissions?: MenuPermission[]\n /** 활성화된 메뉴 경로 */\n activePath?: string\n /** 확장된 메뉴 키 목록 */\n expandedKeys?: Set<number | string>\n /** 즐겨찾기 메뉴 키 목록 */\n favorites?: (number | string)[]\n /** 즐겨찾기 변경 핸들러 */\n onFavoriteToggle?: (menuKey: number | string | undefined) => void\n /** 즐겨찾기 확인 함수 */\n isFavorite?: (menuKey: number | string | undefined) => boolean\n /** 스타일 타입 */\n styletype?: StyleType\n /** 추가 CSS 클래스 */\n className?: string\n /** 최대 깊이 제한 (무한 루프 방지, 기본값: 10) */\n maxDepth?: number\n /** 네비게이션 비활성화 (true일 때 router.push 건너뛰고 emit만 수행) */\n disableNavigation?: boolean\n /** 활성화된 메뉴 키 (menuKey 기반 활성화, activePath보다 우선) */\n activeKey?: number | string | null\n }>(),\n {\n level: 0,\n permissions: () => [],\n expandedKeys: () => new Set(),\n favorites: () => [],\n styletype: 'default',\n maxDepth: 10,\n disableNavigation: false,\n activeKey: null,\n },\n)\n\r\nconst emit = defineEmits<{\r\n /** 메뉴 클릭 이벤트 */\r\n menuClick: [event: MenuClickEvent]\r\n /** 확장 상태 변경 이벤트 */\r\n expandChange: [menuKey: number | string | undefined, expanded: boolean]\r\n}>()\r\n\r\nconst router = useRouter()\r\n\r\n/**\r\n * 권한 체크 함수\r\n * Permission check function\r\n * hasMenuPermission 유틸리티 함수를 사용하여 일관성 유지\r\n */\r\nconst checkPermission = computed(() => {\r\n return hasMenuPermission(props.item.menuKey, props.permissions)\r\n})\r\n\r\n/**\n * 메뉴가 활성화되어 있는지 여부\n * activeKey가 제공되면 menuKey 매칭, 아니면 경로 매칭\n */\nconst isActive = computed(() => {\n // activeKey가 제공되면 menuKey 기반 매칭 (우선순위 높음)\n if (props.activeKey !== undefined && props.activeKey !== null) {\n return props.item.menuKey === props.activeKey\n }\n // 경로 기반 매칭 (기본 동작)\n if (!props.item.path || !props.activePath) return false\n return props.activePath === props.item.path\n})\n\r\n/**\r\n * 메뉴 타입이 폴더인지 여부\r\n * 순환 참조 방지: children이 유효한 배열인지 확인\r\n */\r\nconst isFolder = computed(() => {\r\n return props.item.menuType === 'F' || (Array.isArray(props.item.children) && props.item.children.length > 0)\r\n})\r\n\r\n/**\r\n * 메뉴가 확장되어 있는지 여부\r\n */\r\nconst isExpanded = computed(() => {\r\n if (!isFolder.value) return false\r\n const key = props.item.menuKey || props.item.label\r\n return props.expandedKeys?.has(key) ?? false\r\n})\r\n\r\n/**\r\n * 메뉴가 비활성화되어 있는지 여부\r\n */\r\nconst isDisabled = computed(() => {\r\n return props.item.disabled || !checkPermission.value\r\n})\r\n\r\n/**\r\n * 레벨별 들여쓰기 스타일\r\n * Tailwind의 표준 클래스는 제한적이므로 인라인 스타일 사용\r\n */\r\nconst indentStyle = computed(() => {\r\n const basePadding = 12 // 기본 패딩 (px)\r\n const level = props.level || 0\r\n const levelPadding = level * 16 // 레벨당 16px\r\n const totalPadding = basePadding + levelPadding\r\n return { paddingLeft: `${totalPadding}px` }\r\n})\r\n\r\n/**\n * 메뉴 클릭 핸들러\n */\nconst handleMenuClick = (event: MouseEvent) => {\n if (isDisabled.value) {\n event.preventDefault()\n return\n }\n\n if (isFolder.value) {\n // 폴더 타입: 확장/축소 토글\n const key = props.item.menuKey || props.item.label\n const newExpanded = !isExpanded.value\n emit('expandChange', key, newExpanded)\n } else {\n // 링크 타입: 라우팅 (disableNavigation이 false일 때만)\n if (!props.disableNavigation && props.item.path) {\n router.push(props.item.path)\n }\n \n // 메뉴 클릭 이벤트 발생\n emit('menuClick', {\n menuItem: props.item,\n path: [props.item], // 단순화된 경로 (필요시 부모 경로 포함하도록 확장 가능)\n event,\n })\n }\n}\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n itemClass: string\r\n labelClass: string\r\n iconSize: 'sm' | 'md'\r\n}> = {\r\n default: {\r\n itemClass: 'flex items-center gap-2 py-2 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate',\r\n iconSize: 'sm',\r\n },\r\n minimal: {\r\n itemClass: 'flex items-center gap-1.5 py-1.5 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate text-xs',\r\n iconSize: 'sm', // JIcon은 'xs'를 지원하지 않으므로 'sm' 사용\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * Chevron 아이콘 컴포넌트\r\n */\r\nconst ChevronIcon = computed(() => {\r\n return isExpanded.value ? 'chevronDown' : 'chevronRight'\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"cn('w-full', className)\">\r\n <!-- 메뉴 아이템 -->\r\n <div\r\n :class=\"cn(\r\n preset.itemClass,\r\n {\r\n 'bg-accent text-accent-foreground': isActive,\r\n 'hover:bg-accent/50': !isDisabled && !isActive,\r\n 'opacity-50 cursor-not-allowed': isDisabled,\r\n 'font-medium': isActive,\r\n }\r\n )\"\r\n :style=\"indentStyle\"\r\n @click=\"handleMenuClick\"\r\n >\r\n <!-- Chevron 아이콘 (폴더 타입만) -->\r\n <JIcon\r\n v-if=\"isFolder\"\r\n :name=\"ChevronIcon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span v-else class=\"w-4 flex-shrink-0\" /> <!-- 폴더가 아닐 때 공간 확보 -->\r\n\r\n <!-- 메뉴 아이콘 -->\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n\r\n <!-- 메뉴 라벨 -->\r\n <span :class=\"preset.labelClass\">{{ item.label }}</span>\r\n \r\n <!-- 즐겨찾기 버튼 (menuType이 L인 경우만) -->\r\n <button\r\n v-if=\"item.menuKey && item.menuType === 'L' && onFavoriteToggle\"\r\n :class=\"cn(\r\n 'opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0',\r\n props.styletype === 'minimal' ? 'p-0.5' : 'p-1',\r\n isFavorite && isFavorite(item.menuKey) && 'opacity-100'\r\n )\"\r\n @click.stop=\"onFavoriteToggle(item.menuKey)\"\r\n >\r\n <JIcon\r\n :name=\"isFavorite && isFavorite(item.menuKey) ? 'star' : 'star'\"\r\n :size=\"preset.iconSize\"\r\n :class=\"isFavorite && isFavorite(item.menuKey) ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground'\"\r\n />\r\n </button>\r\n </div>\r\n\r\n <!-- 하위 메뉴 (폴더 타입이고 확장된 경우) -->\r\n <!-- 깊이 제한 체크: maxDepth를 초과하지 않는 경우에만 렌더링 -->\r\n <div\r\n v-if=\"isFolder && isExpanded && item.children && Array.isArray(item.children) && item.children.length > 0 && (level + 1) < maxDepth\"\r\n class=\"w-full\"\r\n >\r\n <JDynamicMenuItem\n v-for=\"(child, index) in item.children\"\n :key=\"child.menuKey || child.label || index\"\n :item=\"child\"\n :level=\"level + 1\"\n :max-depth=\"maxDepth\"\n :permissions=\"permissions\"\n :active-path=\"activePath\"\n :expanded-keys=\"expandedKeys\"\n :favorites=\"favorites\"\n :on-favorite-toggle=\"onFavoriteToggle\"\n :is-favorite=\"isFavorite\"\n :styletype=\"styletype\"\n :disable-navigation=\"disableNavigation\"\n :active-key=\"activeKey\"\n @menu-click=\"emit('menuClick', $event)\"\n @expand-change=\"(menuKey, expanded) => emit('expandChange', menuKey, expanded)\"\n />\n </div>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","router","useRouter","checkPermission","computed","hasMenuPermission","isActive","isFolder","isExpanded","key","isDisabled","indentStyle","handleMenuClick","event","newExpanded","STYLE_PRESETS","preset","ChevronIcon","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_createBlock","JIcon","_openBlock","_hoisted_1","_toDisplayString","_cache","_withModifiers","$event","_createVNode","_hoisted_2","_Fragment","child","index","_component_JDynamicMenuItem","menuKey","expanded"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JDynamicMenuItem.vue.cjs","sources":["../../../../../src/components/organisms/JSidebarSimple/JDynamicMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport { useRouter } from 'vue-router'\r\nimport type { SidebarMenuItem, MenuPermission, MenuClickEvent } from '@/types/sidebar-menu.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport { cn, hasMenuPermission } from '@/lib/utils'\r\n\r\n/**\r\n * JDynamicMenuItem - 재귀적 메뉴 아이템 컴포넌트\r\n * Recursive Menu Item Component\r\n * \r\n * @description\r\n * 다단계 메뉴 구조를 재귀적으로 렌더링하는 컴포넌트입니다.\r\n * 폴더 타입 메뉴는 확장/축소가 가능하고, 링크 타입 메뉴는 클릭 시 라우팅합니다.\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n /** 메뉴 아이템 */\r\n item: SidebarMenuItem\r\n /** 메뉴 레벨 (들여쓰기용, 0부터 시작) */\r\n level?: number\r\n /** 권한 목록 */\r\n permissions?: MenuPermission[]\r\n /** 활성화된 메뉴 경로 */\r\n activePath?: string\r\n /** 확장된 메뉴 키 목록 */\r\n expandedKeys?: Set<number | string>\r\n /** 즐겨찾기 메뉴 키 목록 */\r\n favorites?: (number | string)[]\r\n /** 즐겨찾기 변경 핸들러 */\r\n onFavoriteToggle?: (menuKey: number | string | undefined) => void\r\n /** 즐겨찾기 확인 함수 */\r\n isFavorite?: (menuKey: number | string | undefined) => boolean\r\n /** 스타일 타입 */\r\n styletype?: StyleType\r\n /** 추가 CSS 클래스 */\r\n className?: string\r\n /** 최대 깊이 제한 (무한 루프 방지, 기본값: 10) */\r\n maxDepth?: number\r\n /** 네비게이션 비활성화 (true일 때 router.push 건너뛰고 emit만 수행) */\r\n disableNavigation?: boolean\r\n /** 활성화된 메뉴 키 (menuKey 기반 활성화, activePath보다 우선) */\r\n activeKey?: number | string | null\r\n }>(),\r\n {\r\n level: 0,\r\n permissions: () => [],\r\n expandedKeys: () => new Set(),\r\n favorites: () => [],\r\n styletype: 'default',\r\n maxDepth: 10,\r\n disableNavigation: false,\r\n activeKey: null,\r\n },\r\n)\r\n\r\nconst emit = defineEmits<{\r\n /** 메뉴 클릭 이벤트 */\r\n menuClick: [event: MenuClickEvent]\r\n /** 확장 상태 변경 이벤트 */\r\n expandChange: [menuKey: number | string | undefined, expanded: boolean]\r\n}>()\r\n\r\nconst router = useRouter()\r\n\r\n/**\r\n * 권한 체크 함수\r\n * Permission check function\r\n * hasMenuPermission 유틸리티 함수를 사용하여 일관성 유지\r\n */\r\nconst checkPermission = computed(() => {\r\n return hasMenuPermission(props.item.menuKey, props.permissions)\r\n})\r\n\r\n/**\r\n * 메뉴가 활성화되어 있는지 여부\r\n * activeKey가 제공되면 menuKey 매칭, 아니면 경로 매칭\r\n */\r\nconst isActive = computed(() => {\r\n // activeKey가 제공되면 menuKey 기반 매칭 (우선순위 높음)\r\n if (props.activeKey !== undefined && props.activeKey !== null) {\r\n return props.item.menuKey === props.activeKey\r\n }\r\n // 경로 기반 매칭 (기본 동작)\r\n if (!props.item.path || !props.activePath) return false\r\n return props.activePath === props.item.path\r\n})\r\n\r\n/**\r\n * 메뉴 타입이 폴더인지 여부\r\n * 순환 참조 방지: children이 유효한 배열인지 확인\r\n */\r\nconst isFolder = computed(() => {\r\n return props.item.menuType === 'F' || (Array.isArray(props.item.children) && props.item.children.length > 0)\r\n})\r\n\r\n/**\r\n * 메뉴가 확장되어 있는지 여부\r\n */\r\nconst isExpanded = computed(() => {\r\n if (!isFolder.value) return false\r\n const key = props.item.menuKey || props.item.label\r\n return props.expandedKeys?.has(key) ?? false\r\n})\r\n\r\n/**\r\n * 메뉴가 비활성화되어 있는지 여부\r\n */\r\nconst isDisabled = computed(() => {\r\n return props.item.disabled || !checkPermission.value\r\n})\r\n\r\n/**\r\n * 레벨별 들여쓰기 스타일\r\n * Tailwind의 표준 클래스는 제한적이므로 인라인 스타일 사용\r\n */\r\nconst indentStyle = computed(() => {\r\n const basePadding = 12 // 기본 패딩 (px)\r\n const level = props.level || 0\r\n const levelPadding = level * 16 // 레벨당 16px\r\n const totalPadding = basePadding + levelPadding\r\n return { paddingLeft: `${totalPadding}px` }\r\n})\r\n\r\n/**\r\n * 메뉴 클릭 핸들러\r\n */\r\nconst handleMenuClick = (event: MouseEvent) => {\r\n if (isDisabled.value) {\r\n event.preventDefault()\r\n return\r\n }\r\n\r\n if (isFolder.value) {\r\n // 폴더 타입: 확장/축소 토글\r\n const key = props.item.menuKey || props.item.label\r\n const newExpanded = !isExpanded.value\r\n emit('expandChange', key, newExpanded)\r\n // 폴더도 메뉴 클릭 이벤트 발생\r\n emit('menuClick', {\r\n menuItem: props.item,\r\n path: [props.item],\r\n event,\r\n })\r\n } else {\r\n // 링크 타입: 라우팅 (disableNavigation이 false일 때만)\r\n if (!props.disableNavigation && props.item.path) {\r\n router.push(props.item.path)\r\n }\r\n \r\n // 메뉴 클릭 이벤트 발생\r\n emit('menuClick', {\r\n menuItem: props.item,\r\n path: [props.item], // 단순화된 경로 (필요시 부모 경로 포함하도록 확장 가능)\r\n event,\r\n })\r\n }\r\n}\r\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n itemClass: string\r\n labelClass: string\r\n iconSize: 'sm' | 'md'\r\n}> = {\r\n default: {\r\n itemClass: 'flex items-center gap-2 py-2 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate',\r\n iconSize: 'sm',\r\n },\r\n minimal: {\r\n itemClass: 'flex items-center gap-1.5 py-1.5 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate text-xs',\r\n iconSize: 'sm', // JIcon은 'xs'를 지원하지 않으므로 'sm' 사용\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * Chevron 아이콘 컴포넌트\r\n */\r\nconst ChevronIcon = computed(() => {\r\n return isExpanded.value ? 'chevronDown' : 'chevronRight'\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"cn('w-full', className)\">\r\n <!-- 메뉴 아이템 -->\r\n <div\r\n :class=\"cn(\r\n preset.itemClass,\r\n {\r\n 'bg-accent text-accent-foreground': isActive,\r\n 'hover:bg-accent/50': !isDisabled && !isActive,\r\n 'opacity-50 cursor-not-allowed': isDisabled,\r\n 'font-medium': isActive,\r\n }\r\n )\"\r\n :style=\"indentStyle\"\r\n @click=\"handleMenuClick\"\r\n >\r\n <!-- Chevron 아이콘 (폴더 타입만) -->\r\n <JIcon\r\n v-if=\"isFolder\"\r\n :name=\"ChevronIcon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span v-else class=\"w-4 flex-shrink-0\" /> <!-- 폴더가 아닐 때 공간 확보 -->\r\n\r\n <!-- 메뉴 아이콘 -->\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n\r\n <!-- 메뉴 라벨 -->\r\n <span :class=\"preset.labelClass\">{{ item.label }}</span>\r\n \r\n <!-- 즐겨찾기 버튼 (menuType이 L인 경우만) -->\r\n <button\r\n v-if=\"item.menuKey && item.menuType === 'L' && onFavoriteToggle\"\r\n :class=\"cn(\r\n 'opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0',\r\n props.styletype === 'minimal' ? 'p-0.5' : 'p-1',\r\n isFavorite && isFavorite(item.menuKey) && 'opacity-100'\r\n )\"\r\n @click.stop=\"onFavoriteToggle(item.menuKey)\"\r\n >\r\n <JIcon\r\n :name=\"isFavorite && isFavorite(item.menuKey) ? 'star' : 'star'\"\r\n :size=\"preset.iconSize\"\r\n :class=\"isFavorite && isFavorite(item.menuKey) ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground'\"\r\n />\r\n </button>\r\n </div>\r\n\r\n <!-- 하위 메뉴 (폴더 타입이고 확장된 경우) -->\r\n <!-- 깊이 제한 체크: maxDepth를 초과하지 않는 경우에만 렌더링 -->\r\n <div\r\n v-if=\"isFolder && isExpanded && item.children && Array.isArray(item.children) && item.children.length > 0 && (level + 1) < maxDepth\"\r\n class=\"w-full\"\r\n >\r\n <JDynamicMenuItem\r\n v-for=\"(child, index) in item.children\"\r\n :key=\"child.menuKey || child.label || index\"\r\n :item=\"child\"\r\n :level=\"level + 1\"\r\n :max-depth=\"maxDepth\"\r\n :permissions=\"permissions\"\r\n :active-path=\"activePath\"\r\n :expanded-keys=\"expandedKeys\"\r\n :favorites=\"favorites\"\r\n :on-favorite-toggle=\"onFavoriteToggle\"\r\n :is-favorite=\"isFavorite\"\r\n :styletype=\"styletype\"\r\n :disable-navigation=\"disableNavigation\"\r\n :active-key=\"activeKey\"\r\n @menu-click=\"emit('menuClick', $event)\"\r\n @expand-change=\"(menuKey, expanded) => emit('expandChange', menuKey, expanded)\"\r\n />\r\n </div>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","router","useRouter","checkPermission","computed","hasMenuPermission","isActive","isFolder","isExpanded","key","isDisabled","indentStyle","handleMenuClick","event","newExpanded","STYLE_PRESETS","preset","ChevronIcon","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_createBlock","JIcon","_openBlock","_hoisted_1","_toDisplayString","_cache","_withModifiers","$event","_createVNode","_hoisted_2","_Fragment","child","index","_component_JDynamicMenuItem","menuKey","expanded"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent as
|
|
1
|
+
import { defineComponent as T, computed as a, resolveComponent as M, createElementBlock as r, openBlock as n, normalizeClass as u, unref as h, createElementVNode as F, createCommentVNode as x, normalizeStyle as I, createBlock as k, toDisplayString as A, withModifiers as B, createVNode as L, Fragment as J, renderList as R } from "vue";
|
|
2
2
|
import { useRouter as V } from "vue-router";
|
|
3
3
|
import C from "../../atoms/JIcon.vue.js";
|
|
4
4
|
import { hasMenuPermission as $, cn as b } from "../../../lib/utils.js";
|
|
@@ -8,7 +8,7 @@ const Y = {
|
|
|
8
8
|
}, j = {
|
|
9
9
|
key: 0,
|
|
10
10
|
class: "w-full"
|
|
11
|
-
}, Q = /* @__PURE__ */
|
|
11
|
+
}, Q = /* @__PURE__ */ T({
|
|
12
12
|
__name: "JDynamicMenuItem",
|
|
13
13
|
props: {
|
|
14
14
|
item: {},
|
|
@@ -27,7 +27,7 @@ const Y = {
|
|
|
27
27
|
},
|
|
28
28
|
emits: ["menuClick", "expandChange"],
|
|
29
29
|
setup(e, { emit: z }) {
|
|
30
|
-
const t = e,
|
|
30
|
+
const t = e, s = z, P = V(), S = a(() => $(t.item.menuKey, t.permissions)), v = a(() => t.activeKey !== void 0 && t.activeKey !== null ? t.item.menuKey === t.activeKey : !t.item.path || !t.activePath ? !1 : t.activePath === t.item.path), c = a(() => t.item.menuType === "F" || Array.isArray(t.item.children) && t.item.children.length > 0), y = a(() => {
|
|
31
31
|
if (!c.value) return !1;
|
|
32
32
|
const l = t.item.menuKey || t.item.label;
|
|
33
33
|
return t.expandedKeys?.has(l) ?? !1;
|
|
@@ -38,9 +38,13 @@ const Y = {
|
|
|
38
38
|
}
|
|
39
39
|
if (c.value) {
|
|
40
40
|
const i = t.item.menuKey || t.item.label, d = !y.value;
|
|
41
|
-
|
|
41
|
+
s("expandChange", i, d), s("menuClick", {
|
|
42
|
+
menuItem: t.item,
|
|
43
|
+
path: [t.item],
|
|
44
|
+
event: l
|
|
45
|
+
});
|
|
42
46
|
} else
|
|
43
|
-
!t.disableNavigation && t.item.path && P.push(t.item.path),
|
|
47
|
+
!t.disableNavigation && t.item.path && P.push(t.item.path), s("menuClick", {
|
|
44
48
|
menuItem: t.item,
|
|
45
49
|
path: [t.item],
|
|
46
50
|
// 단순화된 경로 (필요시 부모 경로 포함하도록 확장 가능)
|
|
@@ -58,15 +62,15 @@ const Y = {
|
|
|
58
62
|
iconSize: "sm"
|
|
59
63
|
// JIcon은 'xs'를 지원하지 않으므로 'sm' 사용
|
|
60
64
|
}
|
|
61
|
-
},
|
|
65
|
+
}, o = a(() => K[t.styletype] ?? K.default), p = a(() => y.value ? "chevronDown" : "chevronRight");
|
|
62
66
|
return (l, i) => {
|
|
63
67
|
const d = M("JDynamicMenuItem", !0);
|
|
64
|
-
return n(),
|
|
65
|
-
class:
|
|
68
|
+
return n(), r("div", {
|
|
69
|
+
class: u(h(b)("w-full", e.className))
|
|
66
70
|
}, [
|
|
67
71
|
F("div", {
|
|
68
|
-
class:
|
|
69
|
-
|
|
72
|
+
class: u(h(b)(
|
|
73
|
+
o.value.itemClass,
|
|
70
74
|
{
|
|
71
75
|
"bg-accent text-accent-foreground": v.value,
|
|
72
76
|
"hover:bg-accent/50": !f.value && !v.value,
|
|
@@ -74,44 +78,44 @@ const Y = {
|
|
|
74
78
|
"font-medium": v.value
|
|
75
79
|
}
|
|
76
80
|
)),
|
|
77
|
-
style:
|
|
81
|
+
style: I(w.value),
|
|
78
82
|
onClick: D
|
|
79
83
|
}, [
|
|
80
84
|
c.value ? (n(), k(C, {
|
|
81
85
|
key: 0,
|
|
82
|
-
name:
|
|
83
|
-
size:
|
|
86
|
+
name: p.value,
|
|
87
|
+
size: o.value.iconSize,
|
|
84
88
|
class: "flex-shrink-0"
|
|
85
|
-
}, null, 8, ["name", "size"])) : (n(),
|
|
89
|
+
}, null, 8, ["name", "size"])) : (n(), r("span", Y)),
|
|
86
90
|
e.item.icon ? (n(), k(C, {
|
|
87
91
|
key: 2,
|
|
88
92
|
name: e.item.icon,
|
|
89
|
-
size:
|
|
93
|
+
size: o.value.iconSize,
|
|
90
94
|
class: "flex-shrink-0"
|
|
91
95
|
}, null, 8, ["name", "size"])) : x("", !0),
|
|
92
96
|
F("span", {
|
|
93
|
-
class:
|
|
94
|
-
},
|
|
95
|
-
e.item.menuKey && e.item.menuType === "L" && e.onFavoriteToggle ? (n(),
|
|
97
|
+
class: u(o.value.labelClass)
|
|
98
|
+
}, A(e.item.label), 3),
|
|
99
|
+
e.item.menuKey && e.item.menuType === "L" && e.onFavoriteToggle ? (n(), r("button", {
|
|
96
100
|
key: 3,
|
|
97
|
-
class:
|
|
101
|
+
class: u(h(b)(
|
|
98
102
|
"opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0",
|
|
99
103
|
t.styletype === "minimal" ? "p-0.5" : "p-1",
|
|
100
104
|
e.isFavorite && e.isFavorite(e.item.menuKey) && "opacity-100"
|
|
101
105
|
)),
|
|
102
|
-
onClick: i[0] || (i[0] = B((
|
|
106
|
+
onClick: i[0] || (i[0] = B((m) => e.onFavoriteToggle(e.item.menuKey), ["stop"]))
|
|
103
107
|
}, [
|
|
104
108
|
L(C, {
|
|
105
109
|
name: (e.isFavorite && e.isFavorite(e.item.menuKey), "star"),
|
|
106
|
-
size:
|
|
107
|
-
class:
|
|
110
|
+
size: o.value.iconSize,
|
|
111
|
+
class: u(e.isFavorite && e.isFavorite(e.item.menuKey) ? "text-yellow-500 fill-yellow-500" : "text-muted-foreground")
|
|
108
112
|
}, null, 8, ["name", "size", "class"])
|
|
109
113
|
], 2)) : x("", !0)
|
|
110
114
|
], 6),
|
|
111
|
-
c.value && y.value && e.item.children && Array.isArray(e.item.children) && e.item.children.length > 0 && e.level + 1 < e.maxDepth ? (n(),
|
|
112
|
-
(n(!0),
|
|
113
|
-
key:
|
|
114
|
-
item:
|
|
115
|
+
c.value && y.value && e.item.children && Array.isArray(e.item.children) && e.item.children.length > 0 && e.level + 1 < e.maxDepth ? (n(), r("div", j, [
|
|
116
|
+
(n(!0), r(J, null, R(e.item.children, (m, E) => (n(), k(d, {
|
|
117
|
+
key: m.menuKey || m.label || E,
|
|
118
|
+
item: m,
|
|
115
119
|
level: e.level + 1,
|
|
116
120
|
"max-depth": e.maxDepth,
|
|
117
121
|
permissions: e.permissions,
|
|
@@ -123,8 +127,8 @@ const Y = {
|
|
|
123
127
|
styletype: e.styletype,
|
|
124
128
|
"disable-navigation": e.disableNavigation,
|
|
125
129
|
"active-key": e.activeKey,
|
|
126
|
-
onMenuClick: i[1] || (i[1] = (g) =>
|
|
127
|
-
onExpandChange: i[2] || (i[2] = (g,
|
|
130
|
+
onMenuClick: i[1] || (i[1] = (g) => s("menuClick", g)),
|
|
131
|
+
onExpandChange: i[2] || (i[2] = (g, N) => s("expandChange", g, N))
|
|
128
132
|
}, null, 8, ["item", "level", "max-depth", "permissions", "active-path", "expanded-keys", "favorites", "on-favorite-toggle", "is-favorite", "styletype", "disable-navigation", "active-key"]))), 128))
|
|
129
133
|
])) : x("", !0)
|
|
130
134
|
], 2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JDynamicMenuItem.vue.js","sources":["../../../../../src/components/organisms/JSidebarSimple/JDynamicMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport { useRouter } from 'vue-router'\r\nimport type { SidebarMenuItem, MenuPermission, MenuClickEvent } from '@/types/sidebar-menu.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport { cn, hasMenuPermission } from '@/lib/utils'\r\n\r\n/**\r\n * JDynamicMenuItem - 재귀적 메뉴 아이템 컴포넌트\r\n * Recursive Menu Item Component\r\n * \r\n * @description\r\n * 다단계 메뉴 구조를 재귀적으로 렌더링하는 컴포넌트입니다.\r\n * 폴더 타입 메뉴는 확장/축소가 가능하고, 링크 타입 메뉴는 클릭 시 라우팅합니다.\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(\n defineProps<{\n /** 메뉴 아이템 */\n item: SidebarMenuItem\n /** 메뉴 레벨 (들여쓰기용, 0부터 시작) */\n level?: number\n /** 권한 목록 */\n permissions?: MenuPermission[]\n /** 활성화된 메뉴 경로 */\n activePath?: string\n /** 확장된 메뉴 키 목록 */\n expandedKeys?: Set<number | string>\n /** 즐겨찾기 메뉴 키 목록 */\n favorites?: (number | string)[]\n /** 즐겨찾기 변경 핸들러 */\n onFavoriteToggle?: (menuKey: number | string | undefined) => void\n /** 즐겨찾기 확인 함수 */\n isFavorite?: (menuKey: number | string | undefined) => boolean\n /** 스타일 타입 */\n styletype?: StyleType\n /** 추가 CSS 클래스 */\n className?: string\n /** 최대 깊이 제한 (무한 루프 방지, 기본값: 10) */\n maxDepth?: number\n /** 네비게이션 비활성화 (true일 때 router.push 건너뛰고 emit만 수행) */\n disableNavigation?: boolean\n /** 활성화된 메뉴 키 (menuKey 기반 활성화, activePath보다 우선) */\n activeKey?: number | string | null\n }>(),\n {\n level: 0,\n permissions: () => [],\n expandedKeys: () => new Set(),\n favorites: () => [],\n styletype: 'default',\n maxDepth: 10,\n disableNavigation: false,\n activeKey: null,\n },\n)\n\r\nconst emit = defineEmits<{\r\n /** 메뉴 클릭 이벤트 */\r\n menuClick: [event: MenuClickEvent]\r\n /** 확장 상태 변경 이벤트 */\r\n expandChange: [menuKey: number | string | undefined, expanded: boolean]\r\n}>()\r\n\r\nconst router = useRouter()\r\n\r\n/**\r\n * 권한 체크 함수\r\n * Permission check function\r\n * hasMenuPermission 유틸리티 함수를 사용하여 일관성 유지\r\n */\r\nconst checkPermission = computed(() => {\r\n return hasMenuPermission(props.item.menuKey, props.permissions)\r\n})\r\n\r\n/**\n * 메뉴가 활성화되어 있는지 여부\n * activeKey가 제공되면 menuKey 매칭, 아니면 경로 매칭\n */\nconst isActive = computed(() => {\n // activeKey가 제공되면 menuKey 기반 매칭 (우선순위 높음)\n if (props.activeKey !== undefined && props.activeKey !== null) {\n return props.item.menuKey === props.activeKey\n }\n // 경로 기반 매칭 (기본 동작)\n if (!props.item.path || !props.activePath) return false\n return props.activePath === props.item.path\n})\n\r\n/**\r\n * 메뉴 타입이 폴더인지 여부\r\n * 순환 참조 방지: children이 유효한 배열인지 확인\r\n */\r\nconst isFolder = computed(() => {\r\n return props.item.menuType === 'F' || (Array.isArray(props.item.children) && props.item.children.length > 0)\r\n})\r\n\r\n/**\r\n * 메뉴가 확장되어 있는지 여부\r\n */\r\nconst isExpanded = computed(() => {\r\n if (!isFolder.value) return false\r\n const key = props.item.menuKey || props.item.label\r\n return props.expandedKeys?.has(key) ?? false\r\n})\r\n\r\n/**\r\n * 메뉴가 비활성화되어 있는지 여부\r\n */\r\nconst isDisabled = computed(() => {\r\n return props.item.disabled || !checkPermission.value\r\n})\r\n\r\n/**\r\n * 레벨별 들여쓰기 스타일\r\n * Tailwind의 표준 클래스는 제한적이므로 인라인 스타일 사용\r\n */\r\nconst indentStyle = computed(() => {\r\n const basePadding = 12 // 기본 패딩 (px)\r\n const level = props.level || 0\r\n const levelPadding = level * 16 // 레벨당 16px\r\n const totalPadding = basePadding + levelPadding\r\n return { paddingLeft: `${totalPadding}px` }\r\n})\r\n\r\n/**\n * 메뉴 클릭 핸들러\n */\nconst handleMenuClick = (event: MouseEvent) => {\n if (isDisabled.value) {\n event.preventDefault()\n return\n }\n\n if (isFolder.value) {\n // 폴더 타입: 확장/축소 토글\n const key = props.item.menuKey || props.item.label\n const newExpanded = !isExpanded.value\n emit('expandChange', key, newExpanded)\n } else {\n // 링크 타입: 라우팅 (disableNavigation이 false일 때만)\n if (!props.disableNavigation && props.item.path) {\n router.push(props.item.path)\n }\n \n // 메뉴 클릭 이벤트 발생\n emit('menuClick', {\n menuItem: props.item,\n path: [props.item], // 단순화된 경로 (필요시 부모 경로 포함하도록 확장 가능)\n event,\n })\n }\n}\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n itemClass: string\r\n labelClass: string\r\n iconSize: 'sm' | 'md'\r\n}> = {\r\n default: {\r\n itemClass: 'flex items-center gap-2 py-2 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate',\r\n iconSize: 'sm',\r\n },\r\n minimal: {\r\n itemClass: 'flex items-center gap-1.5 py-1.5 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate text-xs',\r\n iconSize: 'sm', // JIcon은 'xs'를 지원하지 않으므로 'sm' 사용\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * Chevron 아이콘 컴포넌트\r\n */\r\nconst ChevronIcon = computed(() => {\r\n return isExpanded.value ? 'chevronDown' : 'chevronRight'\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"cn('w-full', className)\">\r\n <!-- 메뉴 아이템 -->\r\n <div\r\n :class=\"cn(\r\n preset.itemClass,\r\n {\r\n 'bg-accent text-accent-foreground': isActive,\r\n 'hover:bg-accent/50': !isDisabled && !isActive,\r\n 'opacity-50 cursor-not-allowed': isDisabled,\r\n 'font-medium': isActive,\r\n }\r\n )\"\r\n :style=\"indentStyle\"\r\n @click=\"handleMenuClick\"\r\n >\r\n <!-- Chevron 아이콘 (폴더 타입만) -->\r\n <JIcon\r\n v-if=\"isFolder\"\r\n :name=\"ChevronIcon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span v-else class=\"w-4 flex-shrink-0\" /> <!-- 폴더가 아닐 때 공간 확보 -->\r\n\r\n <!-- 메뉴 아이콘 -->\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n\r\n <!-- 메뉴 라벨 -->\r\n <span :class=\"preset.labelClass\">{{ item.label }}</span>\r\n \r\n <!-- 즐겨찾기 버튼 (menuType이 L인 경우만) -->\r\n <button\r\n v-if=\"item.menuKey && item.menuType === 'L' && onFavoriteToggle\"\r\n :class=\"cn(\r\n 'opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0',\r\n props.styletype === 'minimal' ? 'p-0.5' : 'p-1',\r\n isFavorite && isFavorite(item.menuKey) && 'opacity-100'\r\n )\"\r\n @click.stop=\"onFavoriteToggle(item.menuKey)\"\r\n >\r\n <JIcon\r\n :name=\"isFavorite && isFavorite(item.menuKey) ? 'star' : 'star'\"\r\n :size=\"preset.iconSize\"\r\n :class=\"isFavorite && isFavorite(item.menuKey) ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground'\"\r\n />\r\n </button>\r\n </div>\r\n\r\n <!-- 하위 메뉴 (폴더 타입이고 확장된 경우) -->\r\n <!-- 깊이 제한 체크: maxDepth를 초과하지 않는 경우에만 렌더링 -->\r\n <div\r\n v-if=\"isFolder && isExpanded && item.children && Array.isArray(item.children) && item.children.length > 0 && (level + 1) < maxDepth\"\r\n class=\"w-full\"\r\n >\r\n <JDynamicMenuItem\n v-for=\"(child, index) in item.children\"\n :key=\"child.menuKey || child.label || index\"\n :item=\"child\"\n :level=\"level + 1\"\n :max-depth=\"maxDepth\"\n :permissions=\"permissions\"\n :active-path=\"activePath\"\n :expanded-keys=\"expandedKeys\"\n :favorites=\"favorites\"\n :on-favorite-toggle=\"onFavoriteToggle\"\n :is-favorite=\"isFavorite\"\n :styletype=\"styletype\"\n :disable-navigation=\"disableNavigation\"\n :active-key=\"activeKey\"\n @menu-click=\"emit('menuClick', $event)\"\n @expand-change=\"(menuKey, expanded) => emit('expandChange', menuKey, expanded)\"\n />\n </div>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","router","useRouter","checkPermission","computed","hasMenuPermission","isActive","isFolder","isExpanded","key","isDisabled","indentStyle","handleMenuClick","event","newExpanded","STYLE_PRESETS","preset","ChevronIcon","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_createBlock","JIcon","_openBlock","_hoisted_1","_toDisplayString","_cache","_withModifiers","$event","_createVNode","_hoisted_2","_Fragment","child","index","_component_JDynamicMenuItem","menuKey","expanded"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JDynamicMenuItem.vue.js","sources":["../../../../../src/components/organisms/JSidebarSimple/JDynamicMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport { useRouter } from 'vue-router'\r\nimport type { SidebarMenuItem, MenuPermission, MenuClickEvent } from '@/types/sidebar-menu.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport { cn, hasMenuPermission } from '@/lib/utils'\r\n\r\n/**\r\n * JDynamicMenuItem - 재귀적 메뉴 아이템 컴포넌트\r\n * Recursive Menu Item Component\r\n * \r\n * @description\r\n * 다단계 메뉴 구조를 재귀적으로 렌더링하는 컴포넌트입니다.\r\n * 폴더 타입 메뉴는 확장/축소가 가능하고, 링크 타입 메뉴는 클릭 시 라우팅합니다.\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n /** 메뉴 아이템 */\r\n item: SidebarMenuItem\r\n /** 메뉴 레벨 (들여쓰기용, 0부터 시작) */\r\n level?: number\r\n /** 권한 목록 */\r\n permissions?: MenuPermission[]\r\n /** 활성화된 메뉴 경로 */\r\n activePath?: string\r\n /** 확장된 메뉴 키 목록 */\r\n expandedKeys?: Set<number | string>\r\n /** 즐겨찾기 메뉴 키 목록 */\r\n favorites?: (number | string)[]\r\n /** 즐겨찾기 변경 핸들러 */\r\n onFavoriteToggle?: (menuKey: number | string | undefined) => void\r\n /** 즐겨찾기 확인 함수 */\r\n isFavorite?: (menuKey: number | string | undefined) => boolean\r\n /** 스타일 타입 */\r\n styletype?: StyleType\r\n /** 추가 CSS 클래스 */\r\n className?: string\r\n /** 최대 깊이 제한 (무한 루프 방지, 기본값: 10) */\r\n maxDepth?: number\r\n /** 네비게이션 비활성화 (true일 때 router.push 건너뛰고 emit만 수행) */\r\n disableNavigation?: boolean\r\n /** 활성화된 메뉴 키 (menuKey 기반 활성화, activePath보다 우선) */\r\n activeKey?: number | string | null\r\n }>(),\r\n {\r\n level: 0,\r\n permissions: () => [],\r\n expandedKeys: () => new Set(),\r\n favorites: () => [],\r\n styletype: 'default',\r\n maxDepth: 10,\r\n disableNavigation: false,\r\n activeKey: null,\r\n },\r\n)\r\n\r\nconst emit = defineEmits<{\r\n /** 메뉴 클릭 이벤트 */\r\n menuClick: [event: MenuClickEvent]\r\n /** 확장 상태 변경 이벤트 */\r\n expandChange: [menuKey: number | string | undefined, expanded: boolean]\r\n}>()\r\n\r\nconst router = useRouter()\r\n\r\n/**\r\n * 권한 체크 함수\r\n * Permission check function\r\n * hasMenuPermission 유틸리티 함수를 사용하여 일관성 유지\r\n */\r\nconst checkPermission = computed(() => {\r\n return hasMenuPermission(props.item.menuKey, props.permissions)\r\n})\r\n\r\n/**\r\n * 메뉴가 활성화되어 있는지 여부\r\n * activeKey가 제공되면 menuKey 매칭, 아니면 경로 매칭\r\n */\r\nconst isActive = computed(() => {\r\n // activeKey가 제공되면 menuKey 기반 매칭 (우선순위 높음)\r\n if (props.activeKey !== undefined && props.activeKey !== null) {\r\n return props.item.menuKey === props.activeKey\r\n }\r\n // 경로 기반 매칭 (기본 동작)\r\n if (!props.item.path || !props.activePath) return false\r\n return props.activePath === props.item.path\r\n})\r\n\r\n/**\r\n * 메뉴 타입이 폴더인지 여부\r\n * 순환 참조 방지: children이 유효한 배열인지 확인\r\n */\r\nconst isFolder = computed(() => {\r\n return props.item.menuType === 'F' || (Array.isArray(props.item.children) && props.item.children.length > 0)\r\n})\r\n\r\n/**\r\n * 메뉴가 확장되어 있는지 여부\r\n */\r\nconst isExpanded = computed(() => {\r\n if (!isFolder.value) return false\r\n const key = props.item.menuKey || props.item.label\r\n return props.expandedKeys?.has(key) ?? false\r\n})\r\n\r\n/**\r\n * 메뉴가 비활성화되어 있는지 여부\r\n */\r\nconst isDisabled = computed(() => {\r\n return props.item.disabled || !checkPermission.value\r\n})\r\n\r\n/**\r\n * 레벨별 들여쓰기 스타일\r\n * Tailwind의 표준 클래스는 제한적이므로 인라인 스타일 사용\r\n */\r\nconst indentStyle = computed(() => {\r\n const basePadding = 12 // 기본 패딩 (px)\r\n const level = props.level || 0\r\n const levelPadding = level * 16 // 레벨당 16px\r\n const totalPadding = basePadding + levelPadding\r\n return { paddingLeft: `${totalPadding}px` }\r\n})\r\n\r\n/**\r\n * 메뉴 클릭 핸들러\r\n */\r\nconst handleMenuClick = (event: MouseEvent) => {\r\n if (isDisabled.value) {\r\n event.preventDefault()\r\n return\r\n }\r\n\r\n if (isFolder.value) {\r\n // 폴더 타입: 확장/축소 토글\r\n const key = props.item.menuKey || props.item.label\r\n const newExpanded = !isExpanded.value\r\n emit('expandChange', key, newExpanded)\r\n // 폴더도 메뉴 클릭 이벤트 발생\r\n emit('menuClick', {\r\n menuItem: props.item,\r\n path: [props.item],\r\n event,\r\n })\r\n } else {\r\n // 링크 타입: 라우팅 (disableNavigation이 false일 때만)\r\n if (!props.disableNavigation && props.item.path) {\r\n router.push(props.item.path)\r\n }\r\n \r\n // 메뉴 클릭 이벤트 발생\r\n emit('menuClick', {\r\n menuItem: props.item,\r\n path: [props.item], // 단순화된 경로 (필요시 부모 경로 포함하도록 확장 가능)\r\n event,\r\n })\r\n }\r\n}\r\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n itemClass: string\r\n labelClass: string\r\n iconSize: 'sm' | 'md'\r\n}> = {\r\n default: {\r\n itemClass: 'flex items-center gap-2 py-2 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate',\r\n iconSize: 'sm',\r\n },\r\n minimal: {\r\n itemClass: 'flex items-center gap-1.5 py-1.5 rounded-md cursor-pointer transition-colors group',\r\n labelClass: 'flex-1 truncate text-xs',\r\n iconSize: 'sm', // JIcon은 'xs'를 지원하지 않으므로 'sm' 사용\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * Chevron 아이콘 컴포넌트\r\n */\r\nconst ChevronIcon = computed(() => {\r\n return isExpanded.value ? 'chevronDown' : 'chevronRight'\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"cn('w-full', className)\">\r\n <!-- 메뉴 아이템 -->\r\n <div\r\n :class=\"cn(\r\n preset.itemClass,\r\n {\r\n 'bg-accent text-accent-foreground': isActive,\r\n 'hover:bg-accent/50': !isDisabled && !isActive,\r\n 'opacity-50 cursor-not-allowed': isDisabled,\r\n 'font-medium': isActive,\r\n }\r\n )\"\r\n :style=\"indentStyle\"\r\n @click=\"handleMenuClick\"\r\n >\r\n <!-- Chevron 아이콘 (폴더 타입만) -->\r\n <JIcon\r\n v-if=\"isFolder\"\r\n :name=\"ChevronIcon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span v-else class=\"w-4 flex-shrink-0\" /> <!-- 폴더가 아닐 때 공간 확보 -->\r\n\r\n <!-- 메뉴 아이콘 -->\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n :size=\"preset.iconSize\"\r\n class=\"flex-shrink-0\"\r\n />\r\n\r\n <!-- 메뉴 라벨 -->\r\n <span :class=\"preset.labelClass\">{{ item.label }}</span>\r\n \r\n <!-- 즐겨찾기 버튼 (menuType이 L인 경우만) -->\r\n <button\r\n v-if=\"item.menuKey && item.menuType === 'L' && onFavoriteToggle\"\r\n :class=\"cn(\r\n 'opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0',\r\n props.styletype === 'minimal' ? 'p-0.5' : 'p-1',\r\n isFavorite && isFavorite(item.menuKey) && 'opacity-100'\r\n )\"\r\n @click.stop=\"onFavoriteToggle(item.menuKey)\"\r\n >\r\n <JIcon\r\n :name=\"isFavorite && isFavorite(item.menuKey) ? 'star' : 'star'\"\r\n :size=\"preset.iconSize\"\r\n :class=\"isFavorite && isFavorite(item.menuKey) ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground'\"\r\n />\r\n </button>\r\n </div>\r\n\r\n <!-- 하위 메뉴 (폴더 타입이고 확장된 경우) -->\r\n <!-- 깊이 제한 체크: maxDepth를 초과하지 않는 경우에만 렌더링 -->\r\n <div\r\n v-if=\"isFolder && isExpanded && item.children && Array.isArray(item.children) && item.children.length > 0 && (level + 1) < maxDepth\"\r\n class=\"w-full\"\r\n >\r\n <JDynamicMenuItem\r\n v-for=\"(child, index) in item.children\"\r\n :key=\"child.menuKey || child.label || index\"\r\n :item=\"child\"\r\n :level=\"level + 1\"\r\n :max-depth=\"maxDepth\"\r\n :permissions=\"permissions\"\r\n :active-path=\"activePath\"\r\n :expanded-keys=\"expandedKeys\"\r\n :favorites=\"favorites\"\r\n :on-favorite-toggle=\"onFavoriteToggle\"\r\n :is-favorite=\"isFavorite\"\r\n :styletype=\"styletype\"\r\n :disable-navigation=\"disableNavigation\"\r\n :active-key=\"activeKey\"\r\n @menu-click=\"emit('menuClick', $event)\"\r\n @expand-change=\"(menuKey, expanded) => emit('expandChange', menuKey, expanded)\"\r\n />\r\n </div>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","router","useRouter","checkPermission","computed","hasMenuPermission","isActive","isFolder","isExpanded","key","isDisabled","indentStyle","handleMenuClick","event","newExpanded","STYLE_PRESETS","preset","ChevronIcon","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_createBlock","JIcon","_openBlock","_hoisted_1","_toDisplayString","_cache","_withModifiers","$event","_createVNode","_hoisted_2","_Fragment","child","index","_component_JDynamicMenuItem","menuKey","expanded"],"mappings":"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"}
|