@j-solution/components 1.9.8 → 2.0.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"JFormField.vue2.cjs","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'vertical',\n labelAlign: 'left',\n labelWidth: undefined,\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' && labelWidth ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — CSS 변수는 scoped에서도 자식 요소에 상속됨 */\n.form-density-sm { --ctl-h: 2rem; /* 32px */ }\n.form-density-md { --ctl-h: 2.25rem; /* 36px (shadcn 기본에 근접) */ }\n.form-density-lg { --ctl-h: 2.5rem; /* 40px */ }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"mappings":"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"}
1
+ {"version":3,"file":"JFormField.vue2.cjs","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'horizontal',\n labelAlign: 'left',\n labelWidth: '80px',\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' && labelWidth ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — :root의 --j-ctl-h-* 를 참조, 앱에서 override 가능 */\n.form-density-sm { --ctl-h: var(--j-ctl-h-sm, 1.75rem); }\n.form-density-md { --ctl-h: var(--j-ctl-h-md, 2.25rem); }\n.form-density-lg { --ctl-h: var(--j-ctl-h-lg, 2.5rem); }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { defineComponent as q, ref as N, computed as s, createElementBlock as D, openBlock as a, normalizeClass as p, unref as o, createVNode as d, withCtx as i, normalizeStyle as R, createTextVNode as h, createCommentVNode as u, toDisplayString as y, createBlock as n, resolveDynamicComponent as w, mergeProps as B } from "vue";
1
+ import { defineComponent as q, ref as N, computed as s, createElementBlock as D, openBlock as a, normalizeClass as p, unref as o, createVNode as d, withCtx as i, normalizeStyle as R, createTextVNode as h, createCommentVNode as u, toDisplayString as y, createBlock as n, resolveDynamicComponent as w, mergeProps as z } from "vue";
2
2
  import "../shadcn/index.js";
3
3
  import "lucide-vue-next";
4
4
  /* empty css */
@@ -46,9 +46,9 @@ const ee = {
46
46
  errorMsg: {},
47
47
  type: { default: "input" },
48
48
  inlineLabel: {},
49
- orientation: { default: "vertical" },
49
+ orientation: { default: "horizontal" },
50
50
  labelAlign: { default: "left" },
51
- labelWidth: { default: void 0 },
51
+ labelWidth: { default: "80px" },
52
52
  id: {},
53
53
  modelValue: {},
54
54
  placeholder: {},
@@ -129,7 +129,7 @@ const ee = {
129
129
  default:
130
130
  return "form-density-sm";
131
131
  }
132
- }), z = s(() => {
132
+ }), B = s(() => {
133
133
  const t = "h-[var(--ctl-h)] leading-[var(--ctl-h)]";
134
134
  return e.type === "datepicker" ? `${t} max-w-xs` : e.type === "radio" && e.radioDirection === "vertical" ? "" : t;
135
135
  }), W = {
@@ -189,7 +189,7 @@ const ee = {
189
189
  class: "flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center"
190
190
  }, {
191
191
  default: i(() => [
192
- (a(), n(w(_.value), B(b.value, {
192
+ (a(), n(w(_.value), z(b.value, {
193
193
  "onUpdate:modelValue": k,
194
194
  onChange: $,
195
195
  onFocus: C,
@@ -212,23 +212,23 @@ const ee = {
212
212
  _: 1
213
213
  })) : l.type === "radio" ? (a(), n(o(v), { key: 1 }, {
214
214
  default: i(() => [
215
- (a(), n(w(_.value), B(b.value, {
215
+ (a(), n(w(_.value), z(b.value, {
216
216
  "onUpdate:modelValue": k,
217
217
  onChange: $,
218
218
  onFocus: C,
219
219
  onBlur: V,
220
- class: z.value
220
+ class: B.value
221
221
  }), null, 16, ["class"]))
222
222
  ]),
223
223
  _: 1
224
224
  })) : (a(), n(o(v), { key: 2 }, {
225
225
  default: i(() => [
226
- (a(), n(w(_.value), B(b.value, {
226
+ (a(), n(w(_.value), z(b.value, {
227
227
  "onUpdate:modelValue": k,
228
228
  onChange: $,
229
229
  onFocus: C,
230
230
  onBlur: V,
231
- class: z.value
231
+ class: B.value
232
232
  }), null, 16, ["class"]))
233
233
  ]),
234
234
  _: 1
@@ -1 +1 @@
1
- {"version":3,"file":"JFormField.vue2.js","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'vertical',\n labelAlign: 'left',\n labelWidth: undefined,\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' && labelWidth ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — CSS 변수는 scoped에서도 자식 요소에 상속됨 */\n.form-density-sm { --ctl-h: 2rem; /* 32px */ }\n.form-density-md { --ctl-h: 2.25rem; /* 36px (shadcn 기본에 근접) */ }\n.form-density-lg { --ctl-h: 2.5rem; /* 40px */ }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"mappings":"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"}
1
+ {"version":3,"file":"JFormField.vue2.js","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'horizontal',\n labelAlign: 'left',\n labelWidth: '80px',\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' && labelWidth ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — :root의 --j-ctl-h-* 를 참조, 앱에서 override 가능 */\n.form-density-sm { --ctl-h: var(--j-ctl-h-sm, 1.75rem); }\n.form-density-md { --ctl-h: var(--j-ctl-h-md, 2.25rem); }\n.form-density-lg { --ctl-h: var(--j-ctl-h-lg, 2.5rem); }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"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"),p=require("../../../types/sidebar.types.cjs"),a=require("../../atoms/JIcon.vue.cjs"),f=require("../../atoms/JTooltip.vue.cjs"),l=require("../../../lib/utils.cjs"),v={key:0,class:"absolute left-0 top-1 bottom-1 w-[3px] rounded-r bg-primary"},y={class:"flex-1 truncate text-xs"},g=e.defineComponent({__name:"JSidebarItem",props:{item:{}},emits:["menu-click"],setup(t,{emit:u}){const o=t,m=u,i=e.inject(p.SIDEBAR_INJECTION_KEY),r=e.computed(()=>!o.item.path||!i.activePath?!1:i.activePath===o.item.path),c=e.computed(()=>i.favorites.has(o.item.id)),s=n=>{o.item.disabled||m("menu-click",o.item,n)},d=n=>{n.stopPropagation(),i.toggleFavorite(o.item.id)};return(n,k)=>e.unref(i).collapsed?(e.openBlock(),e.createBlock(f.default,{key:0,content:t.item.label,side:"right",size:"sm"},{trigger:e.withCtx(()=>[e.createElementVNode("div",{class:e.normalizeClass(e.unref(l.cn)("flex items-center justify-center py-1.5 mx-1 rounded-md cursor-pointer transition-colors",r.value?"bg-primary/10 text-primary":"text-foreground hover:bg-accent/50",t.item.disabled&&"opacity-40 cursor-not-allowed")),onClick:s},[e.createVNode(a.default,{name:t.item.icon||"file",size:"sm"},null,8,["name"])],2)]),_:1},8,["content"])):(e.openBlock(),e.createElementBlock("div",{key:1,class:e.normalizeClass(e.unref(l.cn)("group flex items-center gap-1.5 py-0.5 px-2 rounded-md cursor-pointer transition-colors relative",r.value?"bg-primary/10 text-primary font-medium":"text-foreground hover:bg-accent/50",t.item.disabled&&"opacity-40 cursor-not-allowed")),onClick:s},[r.value?(e.openBlock(),e.createElementBlock("div",v)):e.createCommentVNode("",!0),t.item.icon?(e.openBlock(),e.createBlock(a.default,{key:1,name:t.item.icon,size:"sm",class:"flex-shrink-0"},null,8,["name"])):e.createCommentVNode("",!0),e.createElementVNode("span",y,e.toDisplayString(t.item.label),1),e.createElementVNode("button",{class:e.normalizeClass(e.unref(l.cn)("flex-shrink-0 p-0.5 rounded transition-opacity",c.value?"opacity-100 text-yellow-500":"opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-yellow-500")),onClick:d},[e.createVNode(a.default,{name:"star",size:"sm",class:e.normalizeClass(c.value?"fill-yellow-500":void 0)},null,8,["class"])],2)],2))}});exports.default=g;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),p=require("../../../types/sidebar.types.cjs"),a=require("../../atoms/JIcon.vue.cjs"),f=require("../../atoms/JTooltip.vue.cjs"),l=require("../../../lib/utils.cjs"),v={key:0,class:"absolute left-0 top-1 bottom-1 w-[3px] rounded-r bg-primary"},y={class:"flex-1 truncate text-[11px]"},g=e.defineComponent({__name:"JSidebarItem",props:{item:{}},emits:["menu-click"],setup(t,{emit:u}){const o=t,m=u,i=e.inject(p.SIDEBAR_INJECTION_KEY),r=e.computed(()=>!o.item.path||!i.activePath?!1:i.activePath===o.item.path),c=e.computed(()=>i.favorites.has(o.item.id)),s=n=>{o.item.disabled||m("menu-click",o.item,n)},d=n=>{n.stopPropagation(),i.toggleFavorite(o.item.id)};return(n,k)=>e.unref(i).collapsed?(e.openBlock(),e.createBlock(f.default,{key:0,content:t.item.label,side:"right",size:"sm"},{trigger:e.withCtx(()=>[e.createElementVNode("div",{class:e.normalizeClass(e.unref(l.cn)("flex items-center justify-center py-1.5 mx-1 rounded-md cursor-pointer transition-colors",r.value?"bg-primary/10 text-primary":"text-foreground hover:bg-accent/50",t.item.disabled&&"opacity-40 cursor-not-allowed")),onClick:s},[e.createVNode(a.default,{name:t.item.icon||"file",size:"sm"},null,8,["name"])],2)]),_:1},8,["content"])):(e.openBlock(),e.createElementBlock("div",{key:1,class:e.normalizeClass(e.unref(l.cn)("group flex items-center gap-1.5 py-0.5 px-2 rounded-md cursor-pointer transition-colors relative",r.value?"bg-primary/10 text-primary font-medium":"text-foreground hover:bg-accent/50",t.item.disabled&&"opacity-40 cursor-not-allowed")),onClick:s},[r.value?(e.openBlock(),e.createElementBlock("div",v)):e.createCommentVNode("",!0),t.item.icon?(e.openBlock(),e.createBlock(a.default,{key:1,name:t.item.icon,size:"sm",class:"flex-shrink-0"},null,8,["name"])):e.createCommentVNode("",!0),e.createElementVNode("span",y,e.toDisplayString(t.item.label),1),e.createElementVNode("button",{class:e.normalizeClass(e.unref(l.cn)("flex-shrink-0 p-0.5 rounded transition-opacity",c.value?"opacity-100 text-yellow-500":"opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-yellow-500")),onClick:d},[e.createVNode(a.default,{name:"star",size:"sm",class:e.normalizeClass(c.value?"fill-yellow-500":void 0)},null,8,["class"])],2)],2))}});exports.default=g;
2
2
  //# sourceMappingURL=JSidebarItem.vue.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"JSidebarItem.vue.cjs","sources":["../../../../../src/components/organisms/JSidebar/JSidebarItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, inject } from 'vue'\r\nimport type { SidebarMenuItem } from '@/types/sidebar.types'\r\nimport { SIDEBAR_INJECTION_KEY } from '@/types/sidebar.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport JTooltip from '@/components/atoms/JTooltip.vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\n/**\r\n * JSidebarItem - 메뉴 아이템 (menuType='L' 전용)\r\n * 링크 타입 메뉴만 렌더링. 폴더(F)는 JSidebarGroup에서 처리.\r\n */\r\n\r\nconst props = defineProps<{\r\n item: SidebarMenuItem\r\n}>()\r\n\r\nconst emit = defineEmits<{\r\n 'menu-click': [item: SidebarMenuItem, event: MouseEvent]\r\n}>()\r\n\r\nconst state = inject(SIDEBAR_INJECTION_KEY)!\r\n\r\nconst isActive = computed(() => {\r\n if (!props.item.path || !state.activePath) return false\r\n return state.activePath === props.item.path\r\n})\r\n\r\nconst isFav = computed(() => state.favorites.has(props.item.id))\r\n\r\nconst handleClick = (event: MouseEvent) => {\r\n if (props.item.disabled) return\r\n emit('menu-click', props.item, event)\r\n}\r\n\r\nconst handleFavoriteClick = (event: MouseEvent) => {\r\n event.stopPropagation()\r\n state.toggleFavorite(props.item.id)\r\n}\r\n</script>\r\n\r\n<template>\r\n <!-- Collapsed: 아이콘 + Tooltip -->\r\n <JTooltip\r\n v-if=\"state.collapsed\"\r\n :content=\"item.label\"\r\n side=\"right\"\r\n size=\"sm\"\r\n >\r\n <template #trigger>\r\n <div\r\n :class=\"cn(\r\n 'flex items-center justify-center py-1.5 mx-1 rounded-md cursor-pointer transition-colors',\r\n isActive\r\n ? 'bg-primary/10 text-primary'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <JIcon :name=\"item.icon || 'file'\" size=\"sm\" />\r\n </div>\r\n </template>\r\n </JTooltip>\r\n\r\n <!-- Expanded: 아이콘 + 라벨 + 즐겨찾기 -->\r\n <div\r\n v-else\r\n :class=\"cn(\r\n 'group flex items-center gap-1.5 py-0.5 px-2 rounded-md cursor-pointer transition-colors relative',\r\n isActive\r\n ? 'bg-primary/10 text-primary font-medium'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <!-- 활성 좌측 바 -->\r\n <div\r\n v-if=\"isActive\"\r\n class=\"absolute left-0 top-1 bottom-1 w-[3px] rounded-r bg-primary\"\r\n />\r\n\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span class=\"flex-1 truncate text-xs\">{{ item.label }}</span>\r\n\r\n <!-- 즐겨찾기 별 -->\r\n <button\r\n :class=\"cn(\r\n 'flex-shrink-0 p-0.5 rounded transition-opacity',\r\n isFav\r\n ? 'opacity-100 text-yellow-500'\r\n : 'opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-yellow-500'\r\n )\"\r\n @click=\"handleFavoriteClick\"\r\n >\r\n <JIcon\r\n name=\"star\"\r\n size=\"sm\"\r\n :class=\"isFav ? 'fill-yellow-500' : undefined\"\r\n />\r\n </button>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","state","inject","SIDEBAR_INJECTION_KEY","isActive","computed","isFav","handleClick","event","handleFavoriteClick","_unref","_createBlock","JTooltip","_createElementVNode","cn","_createVNode","JIcon","_createElementBlock","_openBlock","_hoisted_1","_hoisted_2","_toDisplayString","_normalizeClass"],"mappings":"0fAaA,MAAMA,EAAQC,EAIRC,EAAOC,EAIPC,EAAQC,EAAAA,OAAOC,uBAAqB,EAEpCC,EAAWC,EAAAA,SAAS,IACpB,CAACR,EAAM,KAAK,MAAQ,CAACI,EAAM,WAAmB,GAC3CA,EAAM,aAAeJ,EAAM,KAAK,IACxC,EAEKS,EAAQD,WAAS,IAAMJ,EAAM,UAAU,IAAIJ,EAAM,KAAK,EAAE,CAAC,EAEzDU,EAAeC,GAAsB,CACrCX,EAAM,KAAK,UACfE,EAAK,aAAcF,EAAM,KAAMW,CAAK,CACtC,EAEMC,EAAuBD,GAAsB,CACjDA,EAAM,gBAAA,EACNP,EAAM,eAAeJ,EAAM,KAAK,EAAE,CACpC,eAMUa,EAAAA,MAAAT,CAAA,EAAM,yBADdU,EAAAA,YAoBWC,UAAA,OAlBR,QAASd,EAAA,KAAK,MACf,KAAK,QACL,KAAK,IAAA,GAEM,kBACT,IAWM,CAXNe,EAAAA,mBAWM,MAAA,CAVH,uBAAOH,EAAAA,MAAAI,IAAA,6FAAsHV,EAAA,wEAAqHN,EAAA,KAAK,UAAQ,+BAAA,GAO/P,QAAOS,CAAA,GAERQ,EAAAA,YAA+CC,EAAAA,QAAA,CAAvC,KAAMlB,EAAA,KAAK,MAAI,OAAY,KAAK,IAAA,6DAM9CmB,EAAAA,mBAyCM,MAAA,OAvCH,uBAAOP,EAAAA,MAAAI,IAAA,qGAAsHV,EAAA,oFAAqHN,EAAA,KAAK,UAAQ,+BAAA,GAO/P,QAAOS,CAAA,GAIAH,EAAA,OADRc,EAAAA,UAAA,EAAAD,EAAAA,mBAGE,MAHFE,CAGE,+BAGMrB,EAAA,KAAK,oBADba,EAAAA,YAKEK,EAAAA,QAAA,OAHC,KAAMlB,EAAA,KAAK,KACZ,KAAK,KACL,MAAM,eAAA,gDAERe,qBAA6D,OAA7DO,EAA6DC,EAAAA,gBAApBvB,EAAA,KAAK,KAAK,EAAA,CAAA,EAGnDe,EAAAA,mBAcS,SAAA,CAbN,uBAAOH,EAAAA,MAAAI,IAAA,mDAAwER,EAAA,sHAM/E,QAAOG,CAAA,GAERM,EAAAA,YAIEC,EAAAA,QAAA,CAHA,KAAK,OACL,KAAK,KACJ,MAAKM,EAAAA,eAAEhB,EAAA,MAAK,kBAAuB,MAAS,CAAA"}
1
+ {"version":3,"file":"JSidebarItem.vue.cjs","sources":["../../../../../src/components/organisms/JSidebar/JSidebarItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, inject } from 'vue'\r\nimport type { SidebarMenuItem } from '@/types/sidebar.types'\r\nimport { SIDEBAR_INJECTION_KEY } from '@/types/sidebar.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport JTooltip from '@/components/atoms/JTooltip.vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\n/**\r\n * JSidebarItem - 메뉴 아이템 (menuType='L' 전용)\r\n * 링크 타입 메뉴만 렌더링. 폴더(F)는 JSidebarGroup에서 처리.\r\n */\r\n\r\nconst props = defineProps<{\r\n item: SidebarMenuItem\r\n}>()\r\n\r\nconst emit = defineEmits<{\r\n 'menu-click': [item: SidebarMenuItem, event: MouseEvent]\r\n}>()\r\n\r\nconst state = inject(SIDEBAR_INJECTION_KEY)!\r\n\r\nconst isActive = computed(() => {\r\n if (!props.item.path || !state.activePath) return false\r\n return state.activePath === props.item.path\r\n})\r\n\r\nconst isFav = computed(() => state.favorites.has(props.item.id))\r\n\r\nconst handleClick = (event: MouseEvent) => {\r\n if (props.item.disabled) return\r\n emit('menu-click', props.item, event)\r\n}\r\n\r\nconst handleFavoriteClick = (event: MouseEvent) => {\r\n event.stopPropagation()\r\n state.toggleFavorite(props.item.id)\r\n}\r\n</script>\r\n\r\n<template>\r\n <!-- Collapsed: 아이콘 + Tooltip -->\r\n <JTooltip\r\n v-if=\"state.collapsed\"\r\n :content=\"item.label\"\r\n side=\"right\"\r\n size=\"sm\"\r\n >\r\n <template #trigger>\r\n <div\r\n :class=\"cn(\r\n 'flex items-center justify-center py-1.5 mx-1 rounded-md cursor-pointer transition-colors',\r\n isActive\r\n ? 'bg-primary/10 text-primary'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <JIcon :name=\"item.icon || 'file'\" size=\"sm\" />\r\n </div>\r\n </template>\r\n </JTooltip>\r\n\r\n <!-- Expanded: 아이콘 + 라벨 + 즐겨찾기 -->\r\n <div\r\n v-else\r\n :class=\"cn(\r\n 'group flex items-center gap-1.5 py-0.5 px-2 rounded-md cursor-pointer transition-colors relative',\r\n isActive\r\n ? 'bg-primary/10 text-primary font-medium'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <!-- 활성 좌측 바 -->\r\n <div\r\n v-if=\"isActive\"\r\n class=\"absolute left-0 top-1 bottom-1 w-[3px] rounded-r bg-primary\"\r\n />\r\n\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span class=\"flex-1 truncate text-[11px]\">{{ item.label }}</span>\r\n\r\n <!-- 즐겨찾기 별 -->\r\n <button\r\n :class=\"cn(\r\n 'flex-shrink-0 p-0.5 rounded transition-opacity',\r\n isFav\r\n ? 'opacity-100 text-yellow-500'\r\n : 'opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-yellow-500'\r\n )\"\r\n @click=\"handleFavoriteClick\"\r\n >\r\n <JIcon\r\n name=\"star\"\r\n size=\"sm\"\r\n :class=\"isFav ? 'fill-yellow-500' : undefined\"\r\n />\r\n </button>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","state","inject","SIDEBAR_INJECTION_KEY","isActive","computed","isFav","handleClick","event","handleFavoriteClick","_unref","_createBlock","JTooltip","_createElementVNode","cn","_createVNode","JIcon","_createElementBlock","_openBlock","_hoisted_1","_hoisted_2","_toDisplayString","_normalizeClass"],"mappings":"8fAaA,MAAMA,EAAQC,EAIRC,EAAOC,EAIPC,EAAQC,EAAAA,OAAOC,uBAAqB,EAEpCC,EAAWC,EAAAA,SAAS,IACpB,CAACR,EAAM,KAAK,MAAQ,CAACI,EAAM,WAAmB,GAC3CA,EAAM,aAAeJ,EAAM,KAAK,IACxC,EAEKS,EAAQD,WAAS,IAAMJ,EAAM,UAAU,IAAIJ,EAAM,KAAK,EAAE,CAAC,EAEzDU,EAAeC,GAAsB,CACrCX,EAAM,KAAK,UACfE,EAAK,aAAcF,EAAM,KAAMW,CAAK,CACtC,EAEMC,EAAuBD,GAAsB,CACjDA,EAAM,gBAAA,EACNP,EAAM,eAAeJ,EAAM,KAAK,EAAE,CACpC,eAMUa,EAAAA,MAAAT,CAAA,EAAM,yBADdU,EAAAA,YAoBWC,UAAA,OAlBR,QAASd,EAAA,KAAK,MACf,KAAK,QACL,KAAK,IAAA,GAEM,kBACT,IAWM,CAXNe,EAAAA,mBAWM,MAAA,CAVH,uBAAOH,EAAAA,MAAAI,IAAA,6FAAsHV,EAAA,wEAAqHN,EAAA,KAAK,UAAQ,+BAAA,GAO/P,QAAOS,CAAA,GAERQ,EAAAA,YAA+CC,EAAAA,QAAA,CAAvC,KAAMlB,EAAA,KAAK,MAAI,OAAY,KAAK,IAAA,6DAM9CmB,EAAAA,mBAyCM,MAAA,OAvCH,uBAAOP,EAAAA,MAAAI,IAAA,qGAAsHV,EAAA,oFAAqHN,EAAA,KAAK,UAAQ,+BAAA,GAO/P,QAAOS,CAAA,GAIAH,EAAA,OADRc,EAAAA,UAAA,EAAAD,EAAAA,mBAGE,MAHFE,CAGE,+BAGMrB,EAAA,KAAK,oBADba,EAAAA,YAKEK,EAAAA,QAAA,OAHC,KAAMlB,EAAA,KAAK,KACZ,KAAK,KACL,MAAM,eAAA,gDAERe,qBAAiE,OAAjEO,EAAiEC,EAAAA,gBAApBvB,EAAA,KAAK,KAAK,EAAA,CAAA,EAGvDe,EAAAA,mBAcS,SAAA,CAbN,uBAAOH,EAAAA,MAAAI,IAAA,mDAAwER,EAAA,sHAM/E,QAAOG,CAAA,GAERM,EAAAA,YAIEC,EAAAA,QAAA,CAHA,KAAK,OACL,KAAK,KACJ,MAAKM,EAAAA,eAAEhB,EAAA,MAAK,kBAAuB,MAAS,CAAA"}
@@ -6,7 +6,7 @@ import { cn as m } from "../../../lib/utils.js";
6
6
  const B = {
7
7
  key: 0,
8
8
  class: "absolute left-0 top-1 bottom-1 w-[3px] rounded-r bg-primary"
9
- }, I = { class: "flex-1 truncate text-xs" }, A = /* @__PURE__ */ b({
9
+ }, I = { class: "flex-1 truncate text-[11px]" }, A = /* @__PURE__ */ b({
10
10
  __name: "JSidebarItem",
11
11
  props: {
12
12
  item: {}
@@ -1 +1 @@
1
- {"version":3,"file":"JSidebarItem.vue.js","sources":["../../../../../src/components/organisms/JSidebar/JSidebarItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, inject } from 'vue'\r\nimport type { SidebarMenuItem } from '@/types/sidebar.types'\r\nimport { SIDEBAR_INJECTION_KEY } from '@/types/sidebar.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport JTooltip from '@/components/atoms/JTooltip.vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\n/**\r\n * JSidebarItem - 메뉴 아이템 (menuType='L' 전용)\r\n * 링크 타입 메뉴만 렌더링. 폴더(F)는 JSidebarGroup에서 처리.\r\n */\r\n\r\nconst props = defineProps<{\r\n item: SidebarMenuItem\r\n}>()\r\n\r\nconst emit = defineEmits<{\r\n 'menu-click': [item: SidebarMenuItem, event: MouseEvent]\r\n}>()\r\n\r\nconst state = inject(SIDEBAR_INJECTION_KEY)!\r\n\r\nconst isActive = computed(() => {\r\n if (!props.item.path || !state.activePath) return false\r\n return state.activePath === props.item.path\r\n})\r\n\r\nconst isFav = computed(() => state.favorites.has(props.item.id))\r\n\r\nconst handleClick = (event: MouseEvent) => {\r\n if (props.item.disabled) return\r\n emit('menu-click', props.item, event)\r\n}\r\n\r\nconst handleFavoriteClick = (event: MouseEvent) => {\r\n event.stopPropagation()\r\n state.toggleFavorite(props.item.id)\r\n}\r\n</script>\r\n\r\n<template>\r\n <!-- Collapsed: 아이콘 + Tooltip -->\r\n <JTooltip\r\n v-if=\"state.collapsed\"\r\n :content=\"item.label\"\r\n side=\"right\"\r\n size=\"sm\"\r\n >\r\n <template #trigger>\r\n <div\r\n :class=\"cn(\r\n 'flex items-center justify-center py-1.5 mx-1 rounded-md cursor-pointer transition-colors',\r\n isActive\r\n ? 'bg-primary/10 text-primary'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <JIcon :name=\"item.icon || 'file'\" size=\"sm\" />\r\n </div>\r\n </template>\r\n </JTooltip>\r\n\r\n <!-- Expanded: 아이콘 + 라벨 + 즐겨찾기 -->\r\n <div\r\n v-else\r\n :class=\"cn(\r\n 'group flex items-center gap-1.5 py-0.5 px-2 rounded-md cursor-pointer transition-colors relative',\r\n isActive\r\n ? 'bg-primary/10 text-primary font-medium'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <!-- 활성 좌측 바 -->\r\n <div\r\n v-if=\"isActive\"\r\n class=\"absolute left-0 top-1 bottom-1 w-[3px] rounded-r bg-primary\"\r\n />\r\n\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span class=\"flex-1 truncate text-xs\">{{ item.label }}</span>\r\n\r\n <!-- 즐겨찾기 별 -->\r\n <button\r\n :class=\"cn(\r\n 'flex-shrink-0 p-0.5 rounded transition-opacity',\r\n isFav\r\n ? 'opacity-100 text-yellow-500'\r\n : 'opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-yellow-500'\r\n )\"\r\n @click=\"handleFavoriteClick\"\r\n >\r\n <JIcon\r\n name=\"star\"\r\n size=\"sm\"\r\n :class=\"isFav ? 'fill-yellow-500' : undefined\"\r\n />\r\n </button>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","state","inject","SIDEBAR_INJECTION_KEY","isActive","computed","isFav","handleClick","event","handleFavoriteClick","_unref","_createBlock","JTooltip","_createElementVNode","cn","_createVNode","JIcon","_createElementBlock","_openBlock","_hoisted_1","_hoisted_2","_toDisplayString","_normalizeClass"],"mappings":";;;;;;;;;;;;;;;AAaA,UAAMA,IAAQC,GAIRC,IAAOC,GAIPC,IAAQC,EAAOC,CAAqB,GAEpCC,IAAWC,EAAS,MACpB,CAACR,EAAM,KAAK,QAAQ,CAACI,EAAM,aAAmB,KAC3CA,EAAM,eAAeJ,EAAM,KAAK,IACxC,GAEKS,IAAQD,EAAS,MAAMJ,EAAM,UAAU,IAAIJ,EAAM,KAAK,EAAE,CAAC,GAEzDU,IAAc,CAACC,MAAsB;AACzC,MAAIX,EAAM,KAAK,YACfE,EAAK,cAAcF,EAAM,MAAMW,CAAK;AAAA,IACtC,GAEMC,IAAsB,CAACD,MAAsB;AACjD,MAAAA,EAAM,gBAAA,GACNP,EAAM,eAAeJ,EAAM,KAAK,EAAE;AAAA,IACpC;qBAMUa,EAAAT,CAAA,EAAM,kBADdU,EAoBWC,GAAA;AAAA;MAlBR,SAASd,EAAA,KAAK;AAAA,MACf,MAAK;AAAA,MACL,MAAK;AAAA,IAAA;MAEM,WACT,MAWM;AAAA,QAXNe,EAWM,OAAA;AAAA,UAVH,SAAOH,EAAAI,CAAA;AAAA;YAAsHV,EAAA;YAAqHN,EAAA,KAAK,YAAQ;AAAA,UAAA;UAO/P,SAAOS;AAAA,QAAA;UAERQ,EAA+CC,GAAA;AAAA,YAAvC,MAAMlB,EAAA,KAAK,QAAI;AAAA,YAAY,MAAK;AAAA,UAAA;;;;gCAM9CmB,EAyCM,OAAA;AAAA;MAvCH,SAAOP,EAAAI,CAAA;AAAA;QAAsHV,EAAA;QAAqHN,EAAA,KAAK,YAAQ;AAAA,MAAA;MAO/P,SAAOS;AAAA,IAAA;MAIAH,EAAA,SADRc,EAAA,GAAAD,EAGE,OAHFE,CAGE;MAGMrB,EAAA,KAAK,aADba,EAKEK,GAAA;AAAA;QAHC,MAAMlB,EAAA,KAAK;AAAA,QACZ,MAAK;AAAA,QACL,OAAM;AAAA,MAAA;MAERe,EAA6D,QAA7DO,GAA6DC,EAApBvB,EAAA,KAAK,KAAK,GAAA,CAAA;AAAA,MAGnDe,EAcS,UAAA;AAAA,QAbN,SAAOH,EAAAI,CAAA;AAAA;UAAwER,EAAA;;QAM/E,SAAOG;AAAA,MAAA;QAERM,EAIEC,GAAA;AAAA,UAHA,MAAK;AAAA,UACL,MAAK;AAAA,UACJ,OAAKM,EAAEhB,EAAA,QAAK,oBAAuB,MAAS;AAAA,QAAA;;;;;"}
1
+ {"version":3,"file":"JSidebarItem.vue.js","sources":["../../../../../src/components/organisms/JSidebar/JSidebarItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, inject } from 'vue'\r\nimport type { SidebarMenuItem } from '@/types/sidebar.types'\r\nimport { SIDEBAR_INJECTION_KEY } from '@/types/sidebar.types'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\nimport JTooltip from '@/components/atoms/JTooltip.vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\n/**\r\n * JSidebarItem - 메뉴 아이템 (menuType='L' 전용)\r\n * 링크 타입 메뉴만 렌더링. 폴더(F)는 JSidebarGroup에서 처리.\r\n */\r\n\r\nconst props = defineProps<{\r\n item: SidebarMenuItem\r\n}>()\r\n\r\nconst emit = defineEmits<{\r\n 'menu-click': [item: SidebarMenuItem, event: MouseEvent]\r\n}>()\r\n\r\nconst state = inject(SIDEBAR_INJECTION_KEY)!\r\n\r\nconst isActive = computed(() => {\r\n if (!props.item.path || !state.activePath) return false\r\n return state.activePath === props.item.path\r\n})\r\n\r\nconst isFav = computed(() => state.favorites.has(props.item.id))\r\n\r\nconst handleClick = (event: MouseEvent) => {\r\n if (props.item.disabled) return\r\n emit('menu-click', props.item, event)\r\n}\r\n\r\nconst handleFavoriteClick = (event: MouseEvent) => {\r\n event.stopPropagation()\r\n state.toggleFavorite(props.item.id)\r\n}\r\n</script>\r\n\r\n<template>\r\n <!-- Collapsed: 아이콘 + Tooltip -->\r\n <JTooltip\r\n v-if=\"state.collapsed\"\r\n :content=\"item.label\"\r\n side=\"right\"\r\n size=\"sm\"\r\n >\r\n <template #trigger>\r\n <div\r\n :class=\"cn(\r\n 'flex items-center justify-center py-1.5 mx-1 rounded-md cursor-pointer transition-colors',\r\n isActive\r\n ? 'bg-primary/10 text-primary'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <JIcon :name=\"item.icon || 'file'\" size=\"sm\" />\r\n </div>\r\n </template>\r\n </JTooltip>\r\n\r\n <!-- Expanded: 아이콘 + 라벨 + 즐겨찾기 -->\r\n <div\r\n v-else\r\n :class=\"cn(\r\n 'group flex items-center gap-1.5 py-0.5 px-2 rounded-md cursor-pointer transition-colors relative',\r\n isActive\r\n ? 'bg-primary/10 text-primary font-medium'\r\n : 'text-foreground hover:bg-accent/50',\r\n item.disabled && 'opacity-40 cursor-not-allowed'\r\n )\"\r\n @click=\"handleClick\"\r\n >\r\n <!-- 활성 좌측 바 -->\r\n <div\r\n v-if=\"isActive\"\r\n class=\"absolute left-0 top-1 bottom-1 w-[3px] rounded-r bg-primary\"\r\n />\r\n\r\n <JIcon\r\n v-if=\"item.icon\"\r\n :name=\"item.icon\"\r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n <span class=\"flex-1 truncate text-[11px]\">{{ item.label }}</span>\r\n\r\n <!-- 즐겨찾기 별 -->\r\n <button\r\n :class=\"cn(\r\n 'flex-shrink-0 p-0.5 rounded transition-opacity',\r\n isFav\r\n ? 'opacity-100 text-yellow-500'\r\n : 'opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-yellow-500'\r\n )\"\r\n @click=\"handleFavoriteClick\"\r\n >\r\n <JIcon\r\n name=\"star\"\r\n size=\"sm\"\r\n :class=\"isFav ? 'fill-yellow-500' : undefined\"\r\n />\r\n </button>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","state","inject","SIDEBAR_INJECTION_KEY","isActive","computed","isFav","handleClick","event","handleFavoriteClick","_unref","_createBlock","JTooltip","_createElementVNode","cn","_createVNode","JIcon","_createElementBlock","_openBlock","_hoisted_1","_hoisted_2","_toDisplayString","_normalizeClass"],"mappings":";;;;;;;;;;;;;;;AAaA,UAAMA,IAAQC,GAIRC,IAAOC,GAIPC,IAAQC,EAAOC,CAAqB,GAEpCC,IAAWC,EAAS,MACpB,CAACR,EAAM,KAAK,QAAQ,CAACI,EAAM,aAAmB,KAC3CA,EAAM,eAAeJ,EAAM,KAAK,IACxC,GAEKS,IAAQD,EAAS,MAAMJ,EAAM,UAAU,IAAIJ,EAAM,KAAK,EAAE,CAAC,GAEzDU,IAAc,CAACC,MAAsB;AACzC,MAAIX,EAAM,KAAK,YACfE,EAAK,cAAcF,EAAM,MAAMW,CAAK;AAAA,IACtC,GAEMC,IAAsB,CAACD,MAAsB;AACjD,MAAAA,EAAM,gBAAA,GACNP,EAAM,eAAeJ,EAAM,KAAK,EAAE;AAAA,IACpC;qBAMUa,EAAAT,CAAA,EAAM,kBADdU,EAoBWC,GAAA;AAAA;MAlBR,SAASd,EAAA,KAAK;AAAA,MACf,MAAK;AAAA,MACL,MAAK;AAAA,IAAA;MAEM,WACT,MAWM;AAAA,QAXNe,EAWM,OAAA;AAAA,UAVH,SAAOH,EAAAI,CAAA;AAAA;YAAsHV,EAAA;YAAqHN,EAAA,KAAK,YAAQ;AAAA,UAAA;UAO/P,SAAOS;AAAA,QAAA;UAERQ,EAA+CC,GAAA;AAAA,YAAvC,MAAMlB,EAAA,KAAK,QAAI;AAAA,YAAY,MAAK;AAAA,UAAA;;;;gCAM9CmB,EAyCM,OAAA;AAAA;MAvCH,SAAOP,EAAAI,CAAA;AAAA;QAAsHV,EAAA;QAAqHN,EAAA,KAAK,YAAQ;AAAA,MAAA;MAO/P,SAAOS;AAAA,IAAA;MAIAH,EAAA,SADRc,EAAA,GAAAD,EAGE,OAHFE,CAGE;MAGMrB,EAAA,KAAK,aADba,EAKEK,GAAA;AAAA;QAHC,MAAMlB,EAAA,KAAK;AAAA,QACZ,MAAK;AAAA,QACL,OAAM;AAAA,MAAA;MAERe,EAAiE,QAAjEO,GAAiEC,EAApBvB,EAAA,KAAK,KAAK,GAAA,CAAA;AAAA,MAGvDe,EAcS,UAAA;AAAA,QAbN,SAAOH,EAAAI,CAAA;AAAA;UAAwER,EAAA;;QAM/E,SAAOG;AAAA,MAAA;QAERM,EAIEC,GAAA;AAAA,UAHA,MAAK;AAAA,UACL,MAAK;AAAA,UACJ,OAAKM,EAAEhB,EAAA,QAAK,oBAAuB,MAAS;AAAA,QAAA;;;;;"}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),F=require("vue-router"),f=require("../../atoms/JIcon.vue.cjs"),c=require("../../../lib/utils.cjs"),S={key:1,class:"w-4 flex-shrink-0"},E={key:0,class:"border-l border-border/60 ml-[14px] pl-[6px]"},N=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:g}){const n=t,s=g,x=F.useRouter(),p=e.computed(()=>c.hasMenuPermission(n.item.menuKey,n.permissions)),m=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),l=e.computed(()=>n.item.menuType==="F"||Array.isArray(n.item.children)&&n.item.children.length>0),d=e.computed(()=>{if(!l.value)return!1;const a=n.item.menuKey||n.item.label;return n.expandedKeys?.has(a)??!1}),v=e.computed(()=>n.item.disabled||!p.value),C=e.computed(()=>({paddingLeft:"8px",paddingRight:"8px"})),b=a=>{if(v.value){a.preventDefault();return}if(l.value){const i=n.item.menuKey||n.item.label,r=!d.value;s("expandChange",i,r),s("menuClick",{menuItem:n.item,path:[n.item],event:a})}else!n.disableNavigation&&n.item.path&&x.push(n.item.path),s("menuClick",{menuItem:n.item,path:[n.item],event:a})},h={default:{itemClass:"flex items-center gap-1.5 py-0.5 rounded-md cursor-pointer transition-colors group",labelClass:"flex-1 truncate text-xs",iconSize:"sm"},minimal:{itemClass:"flex items-center gap-1 py-0.5 rounded-md cursor-pointer transition-colors group",labelClass:"flex-1 truncate text-xs",iconSize:"sm"}},u=e.computed(()=>h[n.styletype]??h.default),B=e.computed(()=>d.value?"chevronDown":"chevronRight"),k=e.computed(()=>{if(!l.value||!Array.isArray(n.item.children))return 0;const a=i=>{let r=0;for(const o of i)r++,Array.isArray(o.children)&&o.children.length>0&&(r+=a(o.children));return r};return a(n.item.children)});return(a,i)=>{const r=e.resolveComponent("JDynamicMenuItem",!0);return e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(e.unref(c.cn)("w-full",t.className))},[e.createElementVNode("div",{class:e.normalizeClass(e.unref(c.cn)(u.value.itemClass,{"bg-primary/5 text-primary border-l-2 border-primary shadow-sm":m.value,"hover:bg-accent/50":!v.value&&!m.value,"opacity-50 cursor-not-allowed":v.value,"font-medium":m.value,"font-semibold":l.value})),style:e.normalizeStyle(C.value),onClick:b},[l.value?(e.openBlock(),e.createBlock(f.default,{key:0,name:B.value,size:u.value.iconSize,class:"flex-shrink-0 opacity-60",style:{"stroke-width":"1.5"}},null,8,["name","size"])):(e.openBlock(),e.createElementBlock("span",S)),t.item.icon&&!l.value?(e.openBlock(),e.createBlock(f.default,{key:2,name:t.item.icon,size:u.value.iconSize,class:"flex-shrink-0"},null,8,["name","size"])):e.createCommentVNode("",!0),e.createElementVNode("span",{class:e.normalizeClass(u.value.labelClass)},e.toDisplayString(t.item.label),3),l.value&&k.value>0?(e.openBlock(),e.createElementBlock("span",{key:3,class:e.normalizeClass(e.unref(c.cn)("text-muted-foreground ml-1 flex-shrink-0",n.styletype==="minimal"?"text-[10px]":"text-xs"))}," ("+e.toDisplayString(k.value)+") ",3)):e.createCommentVNode("",!0),t.item.menuKey&&t.item.menuType==="L"&&t.onFavoriteToggle?(e.openBlock(),e.createElementBlock("button",{key:4,class:e.normalizeClass(e.unref(c.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:i[0]||(i[0]=e.withModifiers(o=>t.onFavoriteToggle(t.item.menuKey),["stop"]))},[e.createVNode(f.default,{name:(t.isFavorite&&t.isFavorite(t.item.menuKey),"star"),size:u.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),l.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",E,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.item.children,(o,z)=>(e.openBlock(),e.createBlock(r,{key:o.menuKey||o.label||z,item:o,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:i[1]||(i[1]=y=>s("menuClick",y)),onExpandChange:i[2]||(i[2]=(y,K)=>s("expandChange",y,K))},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=N;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),F=require("vue-router"),f=require("../../atoms/JIcon.vue.cjs"),c=require("../../../lib/utils.cjs"),S={key:1,class:"w-4 flex-shrink-0"},E={key:0,class:"border-l border-border/60 ml-[14px] pl-[6px]"},N=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:g}){const n=t,s=g,x=F.useRouter(),p=e.computed(()=>c.hasMenuPermission(n.item.menuKey,n.permissions)),m=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),a=e.computed(()=>n.item.menuType==="F"||Array.isArray(n.item.children)&&n.item.children.length>0),d=e.computed(()=>{if(!a.value)return!1;const l=n.item.menuKey||n.item.label;return n.expandedKeys?.has(l)??!1}),v=e.computed(()=>n.item.disabled||!p.value),C=e.computed(()=>({paddingLeft:"8px",paddingRight:"8px"})),b=l=>{if(v.value){l.preventDefault();return}if(a.value){const i=n.item.menuKey||n.item.label,r=!d.value;s("expandChange",i,r),s("menuClick",{menuItem:n.item,path:[n.item],event:l})}else!n.disableNavigation&&n.item.path&&x.push(n.item.path),s("menuClick",{menuItem:n.item,path:[n.item],event:l})},h={default:{itemClass:"flex items-center gap-1.5 py-0.5 rounded-md cursor-pointer transition-colors group",labelClass:"flex-1 truncate",iconSize:"sm"},minimal:{itemClass:"flex items-center gap-1 py-0.5 rounded-md cursor-pointer transition-colors group",labelClass:"flex-1 truncate",iconSize:"sm"}},u=e.computed(()=>h[n.styletype]??h.default),B=e.computed(()=>d.value?"chevronDown":"chevronRight"),k=e.computed(()=>{if(!a.value||!Array.isArray(n.item.children))return 0;const l=i=>{let r=0;for(const o of i)r++,Array.isArray(o.children)&&o.children.length>0&&(r+=l(o.children));return r};return l(n.item.children)});return(l,i)=>{const r=e.resolveComponent("JDynamicMenuItem",!0);return e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(e.unref(c.cn)("w-full",t.className))},[e.createElementVNode("div",{class:e.normalizeClass(e.unref(c.cn)(u.value.itemClass,{"bg-primary/5 text-primary border-l-2 border-primary shadow-sm":m.value,"hover:bg-accent/50":!v.value&&!m.value,"opacity-50 cursor-not-allowed":v.value,"font-medium":m.value,"font-semibold":a.value})),style:e.normalizeStyle(C.value),onClick:b},[a.value?(e.openBlock(),e.createBlock(f.default,{key:0,name:B.value,size:u.value.iconSize,class:"flex-shrink-0 opacity-60",style:{"stroke-width":"1.5"}},null,8,["name","size"])):(e.openBlock(),e.createElementBlock("span",S)),t.item.icon&&!a.value?(e.openBlock(),e.createBlock(f.default,{key:2,name:t.item.icon,size:u.value.iconSize,class:"flex-shrink-0"},null,8,["name","size"])):e.createCommentVNode("",!0),e.createElementVNode("span",{class:e.normalizeClass([u.value.labelClass,a.value?"text-xs":"text-[11px]"])},e.toDisplayString(t.item.label),3),a.value&&k.value>0?(e.openBlock(),e.createElementBlock("span",{key:3,class:e.normalizeClass(e.unref(c.cn)("text-muted-foreground ml-1 flex-shrink-0",n.styletype==="minimal"?"text-[10px]":"text-xs"))}," ("+e.toDisplayString(k.value)+") ",3)):e.createCommentVNode("",!0),t.item.menuKey&&t.item.menuType==="L"&&t.onFavoriteToggle?(e.openBlock(),e.createElementBlock("button",{key:4,class:e.normalizeClass(e.unref(c.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:i[0]||(i[0]=e.withModifiers(o=>t.onFavoriteToggle(t.item.menuKey),["stop"]))},[e.createVNode(f.default,{name:(t.isFavorite&&t.isFavorite(t.item.menuKey),"star"),size:u.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),a.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",E,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.item.children,(o,z)=>(e.openBlock(),e.createBlock(r,{key:o.menuKey||o.label||z,item:o,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:i[1]||(i[1]=y=>s("menuClick",y)),onExpandChange:i[2]||(i[2]=(y,K)=>s("expandChange",y,K))},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=N;
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\">\nimport { computed } from 'vue'\nimport { useRouter } from 'vue-router'\nimport type { SidebarMenuItem, MenuPermission, MenuClickEvent } from '@/types/sidebar-menu.types'\nimport JIcon from '@/components/atoms/JIcon.vue'\nimport { cn, hasMenuPermission } from '@/lib/utils'\n\n/**\n * JDynamicMenuItem - 재귀적 메뉴 아이템 컴포넌트\n * Recursive Menu Item Component\n * \n * @description\n * 다단계 메뉴 구조를 재귀적으로 렌더링하는 컴포넌트입니다.\n * 폴더 타입 메뉴는 확장/축소가 가능하고, 링크 타입 메뉴는 클릭 시 라우팅합니다.\n */\n\ntype StyleType = 'default' | 'minimal'\n\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\nconst emit = defineEmits<{\n /** 메뉴 클릭 이벤트 */\n menuClick: [event: MenuClickEvent]\n /** 확장 상태 변경 이벤트 */\n expandChange: [menuKey: number | string | undefined, expanded: boolean]\n}>()\n\nconst router = useRouter()\n\n/**\n * 권한 체크 함수\n * Permission check function\n * hasMenuPermission 유틸리티 함수를 사용하여 일관성 유지\n */\nconst checkPermission = computed(() => {\n return hasMenuPermission(props.item.menuKey, props.permissions)\n})\n\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\n/**\n * 메뉴 타입이 폴더인지 여부\n * 순환 참조 방지: children이 유효한 배열인지 확인\n */\nconst isFolder = computed(() => {\n return props.item.menuType === 'F' || (Array.isArray(props.item.children) && props.item.children.length > 0)\n})\n\n/**\n * 메뉴가 확장되어 있는지 여부\n */\nconst isExpanded = computed(() => {\n if (!isFolder.value) return false\n const key = props.item.menuKey || props.item.label\n return props.expandedKeys?.has(key) ?? false\n})\n\n/**\n * 메뉴가 비활성화되어 있는지 여부\n */\nconst isDisabled = computed(() => {\n return props.item.disabled || !checkPermission.value\n})\n\n/**\n * 레벨별 들여쓰기 스타일\n * 모든 레벨에서 동일한 padding 사용 (들여쓰기는 컨테이너의 ml로 조절)\n */\nconst indentStyle = computed(() => {\n return { paddingLeft: '8px', paddingRight: '8px' }\n})\n\n\n\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 // 폴더도 메뉴 클릭 이벤트 발생\n emit('menuClick', {\n menuItem: props.item,\n path: [props.item],\n event,\n })\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\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n itemClass: string\n labelClass: string\n iconSize: 'sm' | 'md'\n}> = {\n default: {\n itemClass: 'flex items-center gap-1.5 py-0.5 rounded-md cursor-pointer transition-colors group',\n labelClass: 'flex-1 truncate text-xs',\n iconSize: 'sm',\n },\n minimal: {\n itemClass: 'flex items-center gap-1 py-0.5 rounded-md cursor-pointer transition-colors group',\n labelClass: 'flex-1 truncate text-xs',\n iconSize: 'sm', // JIcon은 'xs'를 지원하지 않으므로 'sm' 사용\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * Chevron 아이콘 컴포넌트\n */\nconst ChevronIcon = computed(() => {\n return isExpanded.value ? 'chevronDown' : 'chevronRight'\n})\n\n/**\n * 하위 메뉴 갯수 계산 (재귀적으로 모든 하위 메뉴 포함)\n * 폴더 타입인 경우에만 표시\n */\nconst childrenCount = computed(() => {\n if (!isFolder.value || !Array.isArray(props.item.children)) {\n return 0\n }\n \n const countChildren = (items: SidebarMenuItem[]): number => {\n let count = 0\n for (const item of items) {\n count++ // 현재 아이템 카운트\n if (Array.isArray(item.children) && item.children.length > 0) {\n count += countChildren(item.children) // 재귀적으로 하위 메뉴 카운트\n }\n }\n return count\n }\n \n return countChildren(props.item.children)\n})\n</script>\n\n<template>\n <div :class=\"cn('w-full', className)\">\n <!-- 메뉴 아이템 -->\n <div\n :class=\"cn(\n preset.itemClass,\n {\n 'bg-primary/5 text-primary border-l-2 border-primary shadow-sm': isActive,\n 'hover:bg-accent/50': !isDisabled && !isActive,\n 'opacity-50 cursor-not-allowed': isDisabled,\n 'font-medium': isActive,\n 'font-semibold': isFolder, // 폴더인 경우 볼드체\n }\n )\"\n :style=\"indentStyle\"\n @click=\"handleMenuClick\"\n >\n <!-- Chevron 아이콘 (폴더 타입만, 덜 굵게) -->\n <JIcon\n v-if=\"isFolder\"\n :name=\"ChevronIcon\"\n :size=\"preset.iconSize\"\n class=\"flex-shrink-0 opacity-60\"\n style=\"stroke-width: 1.5;\"\n />\n <span v-else class=\"w-4 flex-shrink-0\" /> <!-- 폴더가 아닐 때 공간 확보 -->\n\n <!-- 메뉴 아이콘 (폴더가 아닌 경우만 표시) -->\n <JIcon\n v-if=\"item.icon && !isFolder\"\n :name=\"item.icon\"\n :size=\"preset.iconSize\"\n class=\"flex-shrink-0\"\n />\n\n <!-- 메뉴 라벨 -->\n <span :class=\"preset.labelClass\">{{ item.label }}</span>\n \n <!-- 하위 메뉴 갯수 (폴더인 경우만) -->\n <span\n v-if=\"isFolder && childrenCount > 0\"\n :class=\"cn(\n 'text-muted-foreground ml-1 flex-shrink-0',\n props.styletype === 'minimal' ? 'text-[10px]' : 'text-xs'\n )\"\n >\n ({{ childrenCount }})\n </span>\n \n <!-- 즐겨찾기 버튼 (menuType이 L인 경우만) -->\n <button\n v-if=\"item.menuKey && item.menuType === 'L' && onFavoriteToggle\"\n :class=\"cn(\n 'opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0',\n props.styletype === 'minimal' ? 'p-0.5' : 'p-1',\n isFavorite && isFavorite(item.menuKey) && 'opacity-100'\n )\"\n @click.stop=\"onFavoriteToggle(item.menuKey)\"\n >\n <JIcon\n :name=\"isFavorite && isFavorite(item.menuKey) ? 'star' : 'star'\"\n :size=\"preset.iconSize\"\n :class=\"isFavorite && isFavorite(item.menuKey) ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground'\"\n />\n </button>\n </div>\n\n <!-- 하위 메뉴 (폴더 타입이고 확장된 경우) -->\n <!-- 깊이 제한 체크: maxDepth를 초과하지 않는 경우에만 렌더링 -->\n <div\n v-if=\"isFolder && isExpanded && item.children && Array.isArray(item.children) && item.children.length > 0 && (level + 1) < maxDepth\"\n class=\"border-l border-border/60 ml-[14px] pl-[6px]\"\n >\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>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","router","useRouter","checkPermission","computed","hasMenuPermission","isActive","isFolder","isExpanded","key","isDisabled","indentStyle","handleMenuClick","event","newExpanded","STYLE_PRESETS","preset","ChevronIcon","childrenCount","countChildren","items","count","item","_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\">\nimport { computed } from 'vue'\nimport { useRouter } from 'vue-router'\nimport type { SidebarMenuItem, MenuPermission, MenuClickEvent } from '@/types/sidebar-menu.types'\nimport JIcon from '@/components/atoms/JIcon.vue'\nimport { cn, hasMenuPermission } from '@/lib/utils'\n\n/**\n * JDynamicMenuItem - 재귀적 메뉴 아이템 컴포넌트\n * Recursive Menu Item Component\n * \n * @description\n * 다단계 메뉴 구조를 재귀적으로 렌더링하는 컴포넌트입니다.\n * 폴더 타입 메뉴는 확장/축소가 가능하고, 링크 타입 메뉴는 클릭 시 라우팅합니다.\n */\n\ntype StyleType = 'default' | 'minimal'\n\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\nconst emit = defineEmits<{\n /** 메뉴 클릭 이벤트 */\n menuClick: [event: MenuClickEvent]\n /** 확장 상태 변경 이벤트 */\n expandChange: [menuKey: number | string | undefined, expanded: boolean]\n}>()\n\nconst router = useRouter()\n\n/**\n * 권한 체크 함수\n * Permission check function\n * hasMenuPermission 유틸리티 함수를 사용하여 일관성 유지\n */\nconst checkPermission = computed(() => {\n return hasMenuPermission(props.item.menuKey, props.permissions)\n})\n\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\n/**\n * 메뉴 타입이 폴더인지 여부\n * 순환 참조 방지: children이 유효한 배열인지 확인\n */\nconst isFolder = computed(() => {\n return props.item.menuType === 'F' || (Array.isArray(props.item.children) && props.item.children.length > 0)\n})\n\n/**\n * 메뉴가 확장되어 있는지 여부\n */\nconst isExpanded = computed(() => {\n if (!isFolder.value) return false\n const key = props.item.menuKey || props.item.label\n return props.expandedKeys?.has(key) ?? false\n})\n\n/**\n * 메뉴가 비활성화되어 있는지 여부\n */\nconst isDisabled = computed(() => {\n return props.item.disabled || !checkPermission.value\n})\n\n/**\n * 레벨별 들여쓰기 스타일\n * 모든 레벨에서 동일한 padding 사용 (들여쓰기는 컨테이너의 ml로 조절)\n */\nconst indentStyle = computed(() => {\n return { paddingLeft: '8px', paddingRight: '8px' }\n})\n\n\n\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 // 폴더도 메뉴 클릭 이벤트 발생\n emit('menuClick', {\n menuItem: props.item,\n path: [props.item],\n event,\n })\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\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n itemClass: string\n labelClass: string\n iconSize: 'sm' | 'md'\n}> = {\n default: {\n itemClass: 'flex items-center gap-1.5 py-0.5 rounded-md cursor-pointer transition-colors group',\n labelClass: 'flex-1 truncate',\n iconSize: 'sm',\n },\n minimal: {\n itemClass: 'flex items-center gap-1 py-0.5 rounded-md cursor-pointer transition-colors group',\n labelClass: 'flex-1 truncate',\n iconSize: 'sm', // JIcon은 'xs'를 지원하지 않으므로 'sm' 사용\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * Chevron 아이콘 컴포넌트\n */\nconst ChevronIcon = computed(() => {\n return isExpanded.value ? 'chevronDown' : 'chevronRight'\n})\n\n/**\n * 하위 메뉴 갯수 계산 (재귀적으로 모든 하위 메뉴 포함)\n * 폴더 타입인 경우에만 표시\n */\nconst childrenCount = computed(() => {\n if (!isFolder.value || !Array.isArray(props.item.children)) {\n return 0\n }\n \n const countChildren = (items: SidebarMenuItem[]): number => {\n let count = 0\n for (const item of items) {\n count++ // 현재 아이템 카운트\n if (Array.isArray(item.children) && item.children.length > 0) {\n count += countChildren(item.children) // 재귀적으로 하위 메뉴 카운트\n }\n }\n return count\n }\n \n return countChildren(props.item.children)\n})\n</script>\n\n<template>\n <div :class=\"cn('w-full', className)\">\n <!-- 메뉴 아이템 -->\n <div\n :class=\"cn(\n preset.itemClass,\n {\n 'bg-primary/5 text-primary border-l-2 border-primary shadow-sm': isActive,\n 'hover:bg-accent/50': !isDisabled && !isActive,\n 'opacity-50 cursor-not-allowed': isDisabled,\n 'font-medium': isActive,\n 'font-semibold': isFolder, // 폴더인 경우 볼드체\n }\n )\"\n :style=\"indentStyle\"\n @click=\"handleMenuClick\"\n >\n <!-- Chevron 아이콘 (폴더 타입만, 덜 굵게) -->\n <JIcon\n v-if=\"isFolder\"\n :name=\"ChevronIcon\"\n :size=\"preset.iconSize\"\n class=\"flex-shrink-0 opacity-60\"\n style=\"stroke-width: 1.5;\"\n />\n <span v-else class=\"w-4 flex-shrink-0\" /> <!-- 폴더가 아닐 때 공간 확보 -->\n\n <!-- 메뉴 아이콘 (폴더가 아닌 경우만 표시) -->\n <JIcon\n v-if=\"item.icon && !isFolder\"\n :name=\"item.icon\"\n :size=\"preset.iconSize\"\n class=\"flex-shrink-0\"\n />\n\n <!-- 메뉴 라벨 (폴더: text-xs, 링크: text-[11px]) -->\n <span :class=\"[preset.labelClass, isFolder ? 'text-xs' : 'text-[11px]']\">{{ item.label }}</span>\n \n <!-- 하위 메뉴 갯수 (폴더인 경우만) -->\n <span\n v-if=\"isFolder && childrenCount > 0\"\n :class=\"cn(\n 'text-muted-foreground ml-1 flex-shrink-0',\n props.styletype === 'minimal' ? 'text-[10px]' : 'text-xs'\n )\"\n >\n ({{ childrenCount }})\n </span>\n \n <!-- 즐겨찾기 버튼 (menuType이 L인 경우만) -->\n <button\n v-if=\"item.menuKey && item.menuType === 'L' && onFavoriteToggle\"\n :class=\"cn(\n 'opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent rounded flex-shrink-0',\n props.styletype === 'minimal' ? 'p-0.5' : 'p-1',\n isFavorite && isFavorite(item.menuKey) && 'opacity-100'\n )\"\n @click.stop=\"onFavoriteToggle(item.menuKey)\"\n >\n <JIcon\n :name=\"isFavorite && isFavorite(item.menuKey) ? 'star' : 'star'\"\n :size=\"preset.iconSize\"\n :class=\"isFavorite && isFavorite(item.menuKey) ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground'\"\n />\n </button>\n </div>\n\n <!-- 하위 메뉴 (폴더 타입이고 확장된 경우) -->\n <!-- 깊이 제한 체크: maxDepth를 초과하지 않는 경우에만 렌더링 -->\n <div\n v-if=\"isFolder && isExpanded && item.children && Array.isArray(item.children) && item.children.length > 0 && (level + 1) < maxDepth\"\n class=\"border-l border-border/60 ml-[14px] pl-[6px]\"\n >\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>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","router","useRouter","checkPermission","computed","hasMenuPermission","isActive","isFolder","isExpanded","key","isDisabled","indentStyle","handleMenuClick","event","newExpanded","STYLE_PRESETS","preset","ChevronIcon","childrenCount","countChildren","items","count","item","_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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"}