@j-solution/components 1.2.1 → 1.4.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.
- package/README.md +6 -14
- package/USAGE_GUIDE.md +772 -1480
- package/assets/jwms-portal-frontend-Cs1trVbC.css +1 -0
- package/assets/styles/j-components.css +1 -1
- package/components/atoms/JEditor.vue.cjs +1 -1
- package/components/atoms/JEditor.vue.js +2 -2
- package/components/atoms/JEditor.vue2.cjs +1 -1
- package/components/atoms/JEditor.vue2.cjs.map +1 -1
- package/components/atoms/JEditor.vue2.js +1 -0
- package/components/atoms/JEditor.vue2.js.map +1 -1
- package/components/atoms/JPreview.vue.cjs +1 -1
- package/components/atoms/JPreview.vue.js +1 -1
- package/components/atoms/JPreview.vue2.cjs +1 -1
- package/components/atoms/JPreview.vue2.cjs.map +1 -1
- package/components/atoms/JPreview.vue2.js +2 -1
- package/components/atoms/JPreview.vue2.js.map +1 -1
- package/components/molecules/JFormField.vue.cjs +1 -1
- package/components/molecules/JFormField.vue.cjs.map +1 -1
- package/components/molecules/JFormField.vue.js +1 -1
- package/components/molecules/JFormField.vue.js.map +1 -1
- package/components/organisms/JFilterBar.vue.cjs +2 -0
- package/components/organisms/JFilterBar.vue.cjs.map +1 -0
- package/components/organisms/JFilterBar.vue.js +51 -0
- package/components/organisms/JFilterBar.vue.js.map +1 -0
- package/components/organisms/JFilterBar.vue2.cjs +2 -0
- package/components/organisms/JFilterBar.vue2.cjs.map +1 -0
- package/components/organisms/JFilterBar.vue2.js +5 -0
- package/components/organisms/JFilterBar.vue2.js.map +1 -0
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs.map +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js +45 -40
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js.map +1 -1
- package/components/organisms/JSidebarSimple.vue.cjs +1 -1
- package/components/organisms/JSidebarSimple.vue.js +2 -2
- package/components/organisms/JSidebarSimple.vue2.cjs +1 -1
- package/components/organisms/JSidebarSimple.vue2.cjs.map +1 -1
- package/components/organisms/JSidebarSimple.vue2.js +40 -70
- package/components/organisms/JSidebarSimple.vue2.js.map +1 -1
- package/components/organisms/JTree.vue.cjs +2 -0
- package/components/organisms/JTree.vue.cjs.map +1 -0
- package/components/organisms/JTree.vue.js +83 -0
- package/components/organisms/JTree.vue.js.map +1 -0
- package/components/organisms/JTree.vue2.cjs +2 -0
- package/components/organisms/JTree.vue2.cjs.map +1 -0
- package/components/organisms/JTree.vue2.js +5 -0
- package/components/organisms/JTree.vue2.js.map +1 -0
- package/index.cjs +1 -1
- package/index.js +37 -33
- package/lib/menu-utils.cjs +2 -0
- package/lib/menu-utils.cjs.map +1 -0
- package/lib/menu-utils.js +33 -0
- package/lib/menu-utils.js.map +1 -0
- package/package.json +1 -1
- package/types/index.d.ts +156 -34
- package/assets/jwms-portal-frontend-D8DdrheA.css +0 -1
package/USAGE_GUIDE.md
CHANGED
|
@@ -1,1755 +1,1047 @@
|
|
|
1
|
-
# J-Component
|
|
1
|
+
# J-Component 사용 가이드
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Claude Code를 이용한 프론트엔드 개발 시 참조하는 Props 기반 컴포넌트 사용 가이드
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
1. [프로젝트 개요](#프로젝트-개요)
|
|
8
|
-
2. [컴포넌트 목록](#컴포넌트-목록)
|
|
9
|
-
3. [컴포넌트 배치 가이드](#컴포넌트-배치-가이드)
|
|
10
|
-
4. [컴포넌트 활용 가이드](#컴포넌트-활용-가이드)
|
|
11
|
-
5. [기본 사용 규칙](#기본-사용-규칙)
|
|
12
|
-
6. [고급 사용법](#고급-사용법)
|
|
13
|
-
- [JDynamicTabs - 경로 기반 컴포넌트 로딩](#jdynamictabs---경로-기반-컴포넌트-로딩)
|
|
14
|
-
7. [레이아웃 템플릿 가이드](#레이아웃-템플릿-가이드)
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## 프로젝트 개요
|
|
19
|
-
|
|
20
|
-
J-Component는 Vue.js 3와 TypeScript를 기반으로 한 재사용 가능한 UI 컴포넌트 라이브러리입니다.
|
|
21
|
-
|
|
22
|
-
### 핵심 특징
|
|
23
|
-
|
|
24
|
-
- **Atomic Design 패턴**: Atoms → Molecules → Organisms → Templates 계층 구조
|
|
25
|
-
- **TypeScript 완전 지원**: 타입 안정성 보장
|
|
26
|
-
- **shadcn/ui 기반**: 일관된 디자인 시스템
|
|
27
|
-
- **TailwindCSS**: 유틸리티 기반 스타일링
|
|
28
|
-
- **다크모드 지원**: 라이트/다크 모드 자동 전환
|
|
29
|
-
- **CSS 자동 포함** (NPM 패키지): 컴포넌트만 import하면 스타일이 자동으로 적용됩니다
|
|
30
|
-
|
|
31
|
-
### 기술 스택
|
|
32
|
-
|
|
33
|
-
- Vue.js 3.5
|
|
34
|
-
- TypeScript 5.9
|
|
35
|
-
- TailwindCSS 3.4
|
|
36
|
-
- shadcn/ui (radix-vue 기반)
|
|
37
|
-
|
|
38
|
-
### 설치 및 사용
|
|
39
|
-
|
|
40
|
-
#### NPM 패키지 사용 (권장)
|
|
5
|
+
## 설치 및 Import
|
|
41
6
|
|
|
42
7
|
```ts
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```
|
|
8
|
+
// NPM 패키지 (권장) - CSS 자동 포함
|
|
9
|
+
import { JButton, JInput, JGrid } from '@j-solution/components'
|
|
46
10
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#### Standard 방식 사용 (파일 복사)
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
import { JButton } from '@/components'
|
|
53
|
-
// themes.css를 별도로 import해야 함 (INSTALLATION_GUIDE.md 참고)
|
|
11
|
+
// Standard 방식 (파일 복사)
|
|
12
|
+
import { JButton, JInput, JGrid } from '@/components'
|
|
54
13
|
```
|
|
55
14
|
|
|
56
|
-
> 📖 **설치 가이드**: 자세한 설치 방법은 [INSTALLATION_GUIDE.md](./INSTALLATION_GUIDE.md)를 참고하세요.
|
|
57
|
-
|
|
58
15
|
---
|
|
59
16
|
|
|
60
17
|
## 컴포넌트 목록
|
|
61
18
|
|
|
62
|
-
### Atoms (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
|
117
|
-
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Organisms를 조합하여 만든 페이지 레이아웃 구조입니다.
|
|
131
|
-
|
|
132
|
-
| 컴포넌트 | 설명 | 주요 용도 |
|
|
133
|
-
|---------|------|----------|
|
|
134
|
-
| **JLayout** | 기본 레이아웃 | 커스텀 레이아웃 구성 |
|
|
135
|
-
| **JLayoutAdvanced** | 고급 레이아웃 | JHeader + JSidebarAdvanced + JDynamicTabs |
|
|
136
|
-
| **JLayoutSimple** | 간단한 레이아웃 | JHeader + JSidebarSimple + JPageContainer |
|
|
19
|
+
### Atoms (26개)
|
|
20
|
+
|
|
21
|
+
| 컴포넌트 | 설명 |
|
|
22
|
+
|---------|------|
|
|
23
|
+
| JAvatar | 사용자 아바타 |
|
|
24
|
+
| JBadge | 상태 표시 배지 |
|
|
25
|
+
| JButton | 버튼 |
|
|
26
|
+
| JCheckbox | 체크박스 (Y/N 문자열) |
|
|
27
|
+
| JCombo | 드롭다운 선택 |
|
|
28
|
+
| JDatepicker | 날짜 선택기 |
|
|
29
|
+
| JDivider | 구분선 |
|
|
30
|
+
| JEditor | 마크다운 에디터 |
|
|
31
|
+
| JGrid | 데이터 그리드 (AG Grid) |
|
|
32
|
+
| JIcon | 아이콘 (Lucide) |
|
|
33
|
+
| JImage | 이미지 |
|
|
34
|
+
| JInput | 텍스트 입력 |
|
|
35
|
+
| JKbd | 키보드 단축키 표시 |
|
|
36
|
+
| JLabel | 라벨 |
|
|
37
|
+
| JLink | 링크 |
|
|
38
|
+
| JPopover | 팝오버 |
|
|
39
|
+
| JPreview | 마크다운/HTML 뷰어 |
|
|
40
|
+
| JProgress | 진행률 표시 |
|
|
41
|
+
| JRadio | 라디오 버튼 (options 배열) |
|
|
42
|
+
| JSearchCombo | 검색 가능한 드롭다운 |
|
|
43
|
+
| JSpinner | 로딩 스피너 |
|
|
44
|
+
| JSwitch | 토글 스위치 (Y/N 문자열) |
|
|
45
|
+
| JTextarea | 멀티라인 텍스트 입력 |
|
|
46
|
+
| JToaster | Toast 알림 컨테이너 |
|
|
47
|
+
| JTooltip | 툴팁 |
|
|
48
|
+
| JTree | 트리 뷰 |
|
|
49
|
+
|
|
50
|
+
### Molecules (11개)
|
|
51
|
+
|
|
52
|
+
| 컴포넌트 | 설명 |
|
|
53
|
+
|---------|------|
|
|
54
|
+
| JAccordion | 접기/펼치기 섹션 |
|
|
55
|
+
| JAlert | 알림 메시지 |
|
|
56
|
+
| JBreadcrumb | 경로 탐색 |
|
|
57
|
+
| JButtonGroup | 버튼 그룹 |
|
|
58
|
+
| JCard | 카드 |
|
|
59
|
+
| JContextMenu | 우클릭 메뉴 |
|
|
60
|
+
| JFormField | 폼 필드 래퍼 (label + error + 입력) |
|
|
61
|
+
| JGroupCombo | 그룹화된 드롭다운 |
|
|
62
|
+
| JSearchAddr | 주소 검색 (Daum API) |
|
|
63
|
+
| JTabs | 탭 UI |
|
|
64
|
+
| JTitlebar | 타이틀바 |
|
|
65
|
+
|
|
66
|
+
### Organisms (9개)
|
|
67
|
+
|
|
68
|
+
| 컴포넌트 | 설명 |
|
|
69
|
+
|---------|------|
|
|
70
|
+
| JDynamicForm | 스키마 기반 동적 폼 |
|
|
71
|
+
| JDynamicTabs | 동적 탭 관리 |
|
|
72
|
+
| JFormModal | 폼 모달 (JDynamicForm 기반) |
|
|
73
|
+
| JHeader | 애플리케이션 헤더 |
|
|
74
|
+
| JModal | 모달 다이얼로그 |
|
|
75
|
+
| JPageContainer | 페이지 컨테이너 |
|
|
76
|
+
| JSearchPanel | 검색 조건 패널 |
|
|
77
|
+
| JSidebarAdvanced | 고급 사이드바 |
|
|
78
|
+
| JSidebarSimple | 간단한 사이드바 |
|
|
79
|
+
|
|
80
|
+
### Templates (3개)
|
|
81
|
+
|
|
82
|
+
| 컴포넌트 | 설명 |
|
|
83
|
+
|---------|------|
|
|
84
|
+
| JLayout | 기본 레이아웃 (커스텀) |
|
|
85
|
+
| JLayoutAdvanced | 고급 레이아웃 (탭 기반) |
|
|
86
|
+
| JLayoutSimple | 간단한 레이아웃 (단일 페이지) |
|
|
137
87
|
|
|
138
88
|
---
|
|
139
89
|
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
### Atomic Design 계층 구조
|
|
143
|
-
|
|
144
|
-
컴포넌트는 다음 계층 구조를 따릅니다:
|
|
145
|
-
|
|
146
|
-
```
|
|
147
|
-
Templates (템플릿)
|
|
148
|
-
└── Organisms (유기체)
|
|
149
|
-
└── Molecules (분자)
|
|
150
|
-
└── Atoms (원자)
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### 배치 원칙
|
|
154
|
-
|
|
155
|
-
1. **하위 계층 우선 사용**
|
|
156
|
-
- 상위 계층에 해당 컴포넌트가 없을 때에 한해서, 가능한 한 가장 낮은 계층의 컴포넌트를 사용하세요
|
|
157
|
-
- 예: 단순 버튼은 `JButton` (Atoms) 사용, `JButtonGroup` (Molecules)은 여러 버튼을 묶을 때만 사용
|
|
158
|
-
|
|
159
|
-
2. **조합 우선**
|
|
160
|
-
- 기존 컴포넌트로 원하는 기능을 구현할 수 있는 경우, 새로운 컴포넌트를 만들기보다 기존 컴포넌트를 조합하여 사용하세요
|
|
161
|
-
- 예: `JCard` + `JButton` + `JInput` 조합으로 카드 형태의 폼 구성
|
|
90
|
+
## 공통 Props
|
|
162
91
|
|
|
163
|
-
|
|
164
|
-
- 전체 페이지 레이아웃은 Templates 사용
|
|
165
|
-
- 예: `JLayoutAdvanced` 또는 `JLayoutSimple` 사용
|
|
166
|
-
|
|
167
|
-
### 계층별 사용 가이드
|
|
168
|
-
|
|
169
|
-
#### Atoms 사용 시
|
|
170
|
-
|
|
171
|
-
```vue
|
|
172
|
-
<template>
|
|
173
|
-
<!-- 단일 기능만 필요한 경우 -->
|
|
174
|
-
<JButton @click="handleClick">저장</JButton>
|
|
175
|
-
<JInput v-model="name" placeholder="이름 입력" />
|
|
176
|
-
</template>
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
#### Molecules 사용 시
|
|
180
|
-
|
|
181
|
-
```vue
|
|
182
|
-
<template>
|
|
183
|
-
<!-- 여러 Atoms 조합이 필요한 경우 -->
|
|
184
|
-
<JFormField
|
|
185
|
-
type="input"
|
|
186
|
-
label="이름"
|
|
187
|
-
required
|
|
188
|
-
v-model="name"
|
|
189
|
-
placeholder="이름을 입력하세요"
|
|
190
|
-
/>
|
|
191
|
-
|
|
192
|
-
<JCard title="제목">
|
|
193
|
-
<p>콘텐츠</p>
|
|
194
|
-
</JCard>
|
|
195
|
-
</template>
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
#### Organisms 사용 시
|
|
199
|
-
|
|
200
|
-
```vue
|
|
201
|
-
<template>
|
|
202
|
-
<!-- 완전한 기능 블록이 필요한 경우 -->
|
|
203
|
-
<JModal :open="isOpen" title="확인" @confirm="handleConfirm" />
|
|
204
|
-
|
|
205
|
-
<JDynamicForm :schema="formSchema" v-model="formData" />
|
|
206
|
-
</template>
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
#### Templates 사용 시
|
|
210
|
-
|
|
211
|
-
```vue
|
|
212
|
-
<template>
|
|
213
|
-
<!-- 전체 페이지 레이아웃 -->
|
|
214
|
-
<JLayoutAdvanced
|
|
215
|
-
:menu-items="menuItems"
|
|
216
|
-
:favorites="favorites"
|
|
217
|
-
>
|
|
218
|
-
<template #content-tab-{id}>
|
|
219
|
-
<RouterView />
|
|
220
|
-
</template>
|
|
221
|
-
</JLayoutAdvanced>
|
|
222
|
-
</template>
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## 컴포넌트 활용 가이드
|
|
228
|
-
|
|
229
|
-
### 상황별 추천 컴포넌트
|
|
230
|
-
|
|
231
|
-
#### 폼 입력
|
|
232
|
-
|
|
233
|
-
| 상황 | 추천 컴포넌트 | 이유 |
|
|
234
|
-
|------|--------------|------|
|
|
235
|
-
| 단일 라인 텍스트 입력 | `JInput` | 가장 기본적인 텍스트 입력 |
|
|
236
|
-
| 여러 라인 텍스트 입력 | `JTextarea` | 긴 텍스트 입력 |
|
|
237
|
-
| 날짜 선택 | `JDatepicker` | 날짜 선택 UI 제공 |
|
|
238
|
-
| 단일 선택 (옵션 적음) | `JCombo` | 드롭다운 선택 |
|
|
239
|
-
| 단일 선택 (옵션 많음) | `JSearchCombo` | 검색 기능으로 옵션 탐색 용이 |
|
|
240
|
-
| 다중 선택 | `JCombo` (multiple) | 다중 선택 지원 |
|
|
241
|
-
| ON/OFF 설정 | `JSwitch` | 토글 스위치 UI |
|
|
242
|
-
| 체크박스 선택 | `JCheckbox` | 다중 선택 |
|
|
243
|
-
| 라디오 선택 | `JRadio` | 단일 선택 (옵션 표시) |
|
|
244
|
-
| 폼 필드 (라벨 + 에러 포함) | `JFormField` | 라벨, 에러 메시지 자동 처리 |
|
|
245
|
-
| 동적 폼 생성 | `JDynamicForm` | JSON 스키마로 폼 자동 생성 |
|
|
246
|
-
|
|
247
|
-
#### 데이터 표시
|
|
248
|
-
|
|
249
|
-
| 상황 | 추천 컴포넌트 | 이유 |
|
|
250
|
-
|------|--------------|------|
|
|
251
|
-
| 테이블 데이터 | `JGrid` | 정렬, 필터링, 페이지네이션 지원 |
|
|
252
|
-
| 상태 표시 | `JBadge` | 색상으로 상태 구분 |
|
|
253
|
-
| 진행률 표시 | `JProgress` | 작업 진행 상태 시각화 |
|
|
254
|
-
| 로딩 표시 | `JSpinner` | 비동기 작업 로딩 표시 |
|
|
255
|
-
|
|
256
|
-
#### 사용자 인터랙션
|
|
257
|
-
|
|
258
|
-
| 상황 | 추천 컴포넌트 | 이유 |
|
|
259
|
-
|------|--------------|------|
|
|
260
|
-
| 액션 버튼 | `JButton` | 다양한 스타일 지원 |
|
|
261
|
-
| 버튼 그룹 | `JButtonGroup` | 관련 버튼 묶기 |
|
|
262
|
-
| 모달 다이얼로그 | `JModal` | 다양한 크기 지원 (sm, md, lg, xl, 2xl, full) |
|
|
263
|
-
| 폼 모달 | `JFormModal` | JDynamicForm 기반 모달 |
|
|
264
|
-
| 탭 전환 | `JTabs` | 정적 탭 |
|
|
265
|
-
| 동적 탭 | `JDynamicTabs` | 런타임에 탭 추가/제거 |
|
|
266
|
-
|
|
267
|
-
#### 레이아웃
|
|
268
|
-
|
|
269
|
-
| 상황 | 추천 컴포넌트 | 이유 |
|
|
270
|
-
|------|--------------|------|
|
|
271
|
-
| 콘텐츠 그룹화 | `JCard` | 카드 형태로 콘텐츠 구분 |
|
|
272
|
-
| 접기/펼치기 | `JAccordion` | 섹션 접기/펼치기 |
|
|
273
|
-
| 페이지 헤더 | `JHeader` | 애플리케이션 상단 헤더 |
|
|
274
|
-
| 사이드바 (고급) | `JSidebarAdvanced` | 검색, 즐겨찾기 지원 |
|
|
275
|
-
| 사이드바 (간단) | `JSidebarSimple` | 기본 메뉴만 필요할 때 |
|
|
276
|
-
| 전체 레이아웃 (고급) | `JLayoutAdvanced` | 탭 기반 멀티 페이지 |
|
|
277
|
-
| 전체 레이아웃 (간단) | `JLayoutSimple` | 단일 페이지 레이아웃 |
|
|
278
|
-
|
|
279
|
-
#### 피드백
|
|
280
|
-
|
|
281
|
-
| 상황 | 추천 컴포넌트 | 이유 |
|
|
282
|
-
|------|--------------|------|
|
|
283
|
-
| 성공/경고/에러 메시지 | `JAlert` | 타입별 스타일 자동 적용 |
|
|
284
|
-
| 전역 알림 메시지 (Toast) | `JToast` (함수) + `JToaster` (컴포넌트) | 화면 상단/하단에 일시적으로 표시되는 알림 |
|
|
285
|
-
| 추가 설명 | `JTooltip` | 호버 시 설명 표시 |
|
|
286
|
-
| 추가 정보 | `JPopover` | 클릭 시 정보 표시 |
|
|
287
|
-
|
|
288
|
-
#### 검색
|
|
289
|
-
|
|
290
|
-
| 상황 | 추천 컴포넌트 | 이유 |
|
|
291
|
-
|------|--------------|------|
|
|
292
|
-
| 검색 조건 패널 | `JSearchPanel` | JDynamicForm 기반, 접기/펼치기 지원 |
|
|
293
|
-
| 주소 검색 | `JSearchAddr` | Daum Postcode API 통합 |
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## 기본 사용 규칙
|
|
298
|
-
|
|
299
|
-
### Props 공통 규칙
|
|
300
|
-
|
|
301
|
-
대부분의 입력 컴포넌트는 다음 공통 props를 지원합니다:
|
|
92
|
+
대부분의 입력 컴포넌트가 지원하는 공통 props:
|
|
302
93
|
|
|
303
94
|
| Prop | 타입 | 설명 | 기본값 |
|
|
304
95
|
|------|------|------|--------|
|
|
305
|
-
| `modelValue` |
|
|
306
|
-
| `placeholder` | `string` |
|
|
307
|
-
| `disabled` | `boolean` | 비활성화
|
|
308
|
-
| `readonly` | `boolean` | 읽기 전용
|
|
309
|
-
| `required` | `boolean` | 필수
|
|
310
|
-
| `id` | `string` | HTML id
|
|
311
|
-
| `name` | `string` | form
|
|
312
|
-
| `styletype` | `string` | 스타일 테마 프리셋 | `'default'` |
|
|
96
|
+
| `modelValue` | 컴포넌트별 상이 | v-model 바인딩 | - |
|
|
97
|
+
| `placeholder` | `string` | 안내문 | `''` |
|
|
98
|
+
| `disabled` | `boolean` | 비활성화 | `false` |
|
|
99
|
+
| `readonly` | `boolean` | 읽기 전용 (Input/Textarea) | `false` |
|
|
100
|
+
| `required` | `boolean` | 필수 여부 | `false` |
|
|
101
|
+
| `id` | `string` | HTML id | - |
|
|
102
|
+
| `name` | `string` | form 필드명 | - |
|
|
313
103
|
| `class` | `string` | 추가 CSS 클래스 | - |
|
|
314
104
|
|
|
315
|
-
###
|
|
316
|
-
|
|
317
|
-
`styletype` prop은 컴포넌트의 스타일을 빠르게 변경할 수 있는 프리셋입니다.
|
|
318
|
-
|
|
319
|
-
#### JButton styletype
|
|
105
|
+
### 공통 이벤트
|
|
320
106
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
<JButton styletype="link">Link</JButton>
|
|
328
|
-
<JButton styletype="sm">Small</JButton>
|
|
329
|
-
<JButton styletype="lg">Large</JButton>
|
|
330
|
-
```
|
|
107
|
+
| 이벤트 | 설명 |
|
|
108
|
+
|--------|------|
|
|
109
|
+
| `update:modelValue` | v-model 업데이트 |
|
|
110
|
+
| `change` | 값 변경 |
|
|
111
|
+
| `focus` | 포커스 |
|
|
112
|
+
| `blur` | 포커스 해제 |
|
|
331
113
|
|
|
332
|
-
|
|
333
|
-
- `primary`: 주요 액션 (저장, 확인 등)
|
|
334
|
-
- `secondary`: 보조 액션
|
|
335
|
-
- `danger`: 위험한 액션 (삭제 등)
|
|
336
|
-
- `outline`: 덜 강조된 액션
|
|
337
|
-
- `ghost`: 최소한의 시각적 강조
|
|
114
|
+
---
|
|
338
115
|
|
|
339
|
-
|
|
116
|
+
## Atoms Props 레퍼런스
|
|
340
117
|
|
|
341
|
-
|
|
118
|
+
### JButton
|
|
342
119
|
|
|
343
120
|
```vue
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const name = ref('')
|
|
354
|
-
const selected = ref(null)
|
|
355
|
-
const enabled = ref(false)
|
|
356
|
-
</script>
|
|
121
|
+
<JButton
|
|
122
|
+
type="button" <!-- 'button'|'submit'|'reset' -->
|
|
123
|
+
:disabled="false"
|
|
124
|
+
:loading="false" <!-- true이면 스피너 표시 -->
|
|
125
|
+
@click="handler"
|
|
126
|
+
>
|
|
127
|
+
저장
|
|
128
|
+
</JButton>
|
|
357
129
|
```
|
|
358
130
|
|
|
359
|
-
|
|
131
|
+
---
|
|
360
132
|
|
|
361
|
-
|
|
133
|
+
### JInput
|
|
362
134
|
|
|
363
135
|
```vue
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
<script setup>
|
|
375
|
-
import { ref } from 'vue'
|
|
376
|
-
|
|
377
|
-
const name = ref('')
|
|
378
|
-
const isOpen = ref(false)
|
|
379
|
-
|
|
380
|
-
const handleClick = () => {
|
|
381
|
-
console.log('버튼 클릭')
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const handleChange = (value) => {
|
|
385
|
-
console.log('값 변경:', value)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const handleConfirm = () => {
|
|
389
|
-
console.log('확인')
|
|
390
|
-
isOpen.value = false
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const handleCancel = () => {
|
|
394
|
-
console.log('취소')
|
|
395
|
-
isOpen.value = false
|
|
396
|
-
}
|
|
397
|
-
</script>
|
|
136
|
+
<JInput
|
|
137
|
+
v-model="value"
|
|
138
|
+
type="text" <!-- 'text'|'email'|'password'|'number'|'tel'|'url' -->
|
|
139
|
+
placeholder="입력하세요"
|
|
140
|
+
:disabled="false"
|
|
141
|
+
:readonly="false"
|
|
142
|
+
:required="false"
|
|
143
|
+
@change="handler"
|
|
144
|
+
/>
|
|
398
145
|
```
|
|
399
146
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
#### Atoms (원자 컴포넌트)
|
|
147
|
+
---
|
|
403
148
|
|
|
404
|
-
|
|
149
|
+
### JTextarea
|
|
405
150
|
|
|
406
151
|
```vue
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
alt="사용자 이름"
|
|
415
|
-
fallback="JD"
|
|
416
|
-
size="lg"
|
|
417
|
-
shape="circle"
|
|
418
|
-
status="online"
|
|
419
|
-
/>
|
|
420
|
-
</template>
|
|
421
|
-
|
|
422
|
-
<script setup>
|
|
423
|
-
import { JAvatar } from '@/components/atoms'
|
|
424
|
-
</script>
|
|
152
|
+
<JTextarea
|
|
153
|
+
v-model="value"
|
|
154
|
+
placeholder="입력하세요"
|
|
155
|
+
:rows="3"
|
|
156
|
+
:disabled="false"
|
|
157
|
+
:readonly="false"
|
|
158
|
+
/>
|
|
425
159
|
```
|
|
426
160
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
```vue
|
|
430
|
-
<template>
|
|
431
|
-
<JBadge styletype="success">활성</JBadge>
|
|
432
|
-
<JBadge styletype="warning">대기</JBadge>
|
|
433
|
-
<JBadge styletype="danger">비활성</JBadge>
|
|
434
|
-
<JBadge styletype="primary" size="lg">프리미엄</JBadge>
|
|
435
|
-
</template>
|
|
436
|
-
|
|
437
|
-
<script setup>
|
|
438
|
-
import { JBadge } from '@/components/atoms'
|
|
439
|
-
</script>
|
|
440
|
-
```
|
|
161
|
+
---
|
|
441
162
|
|
|
442
|
-
|
|
163
|
+
### JCombo
|
|
443
164
|
|
|
444
165
|
```vue
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const terms = ref(false)
|
|
456
|
-
</script>
|
|
166
|
+
<JCombo
|
|
167
|
+
v-model="selected"
|
|
168
|
+
:options="[
|
|
169
|
+
{ value: 'opt1', label: '옵션 1' },
|
|
170
|
+
{ value: 'opt2', label: '옵션 2' },
|
|
171
|
+
]"
|
|
172
|
+
placeholder="선택하세요"
|
|
173
|
+
:multiple="false"
|
|
174
|
+
:disabled="false"
|
|
175
|
+
/>
|
|
457
176
|
```
|
|
458
177
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
```vue
|
|
462
|
-
<template>
|
|
463
|
-
<JCombo
|
|
464
|
-
v-model="selected"
|
|
465
|
-
:options="options"
|
|
466
|
-
placeholder="선택하세요"
|
|
467
|
-
/>
|
|
468
|
-
</template>
|
|
178
|
+
---
|
|
469
179
|
|
|
470
|
-
|
|
471
|
-
import { ref } from 'vue'
|
|
472
|
-
import { JCombo } from '@/components/atoms'
|
|
180
|
+
### JSearchCombo
|
|
473
181
|
|
|
474
|
-
|
|
475
|
-
const options = [
|
|
476
|
-
{ value: 'option1', label: '옵션 1' },
|
|
477
|
-
{ value: 'option2', label: '옵션 2' },
|
|
478
|
-
{ value: 'option3', label: '옵션 3' },
|
|
479
|
-
]
|
|
480
|
-
</script>
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
##### JDatepicker - 날짜 선택기
|
|
182
|
+
> **주의**: `modelValue`는 `{ value, label }` 객체 타입
|
|
484
183
|
|
|
485
184
|
```vue
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
</script>
|
|
185
|
+
<JSearchCombo
|
|
186
|
+
v-model="selectedOption"
|
|
187
|
+
:options="[
|
|
188
|
+
{ value: '1', label: '항목 1' },
|
|
189
|
+
{ value: '2', label: '항목 2' },
|
|
190
|
+
]"
|
|
191
|
+
placeholder="선택해주세요."
|
|
192
|
+
searchPlaceholder="검색어 입력"
|
|
193
|
+
emptyText="검색 결과가 없습니다."
|
|
194
|
+
:multiple="false"
|
|
195
|
+
:disabled="false"
|
|
196
|
+
/>
|
|
499
197
|
```
|
|
500
198
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
```vue
|
|
504
|
-
<template>
|
|
505
|
-
<div>위쪽 콘텐츠</div>
|
|
506
|
-
<JDivider />
|
|
507
|
-
<div>아래쪽 콘텐츠</div>
|
|
508
|
-
|
|
509
|
-
<!-- 텍스트와 함께 사용 -->
|
|
510
|
-
<JDivider>또는</JDivider>
|
|
511
|
-
</template>
|
|
512
|
-
|
|
513
|
-
<script setup>
|
|
514
|
-
import { JDivider } from '@/components/atoms'
|
|
515
|
-
</script>
|
|
199
|
+
```ts
|
|
200
|
+
const selectedOption = ref<{ value: string | number; label: string } | undefined>()
|
|
516
201
|
```
|
|
517
202
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
```vue
|
|
521
|
-
<template>
|
|
522
|
-
<JGrid
|
|
523
|
-
:column-defs="columnDefs"
|
|
524
|
-
:row-data="rowData"
|
|
525
|
-
/>
|
|
526
|
-
</template>
|
|
527
|
-
|
|
528
|
-
<script setup>
|
|
529
|
-
import { JGrid } from '@/components/atoms'
|
|
530
|
-
|
|
531
|
-
const columnDefs = [
|
|
532
|
-
{ field: 'name', headerName: '이름' },
|
|
533
|
-
{ field: 'age', headerName: '나이' },
|
|
534
|
-
{ field: 'email', headerName: '이메일' },
|
|
535
|
-
]
|
|
203
|
+
---
|
|
536
204
|
|
|
537
|
-
|
|
538
|
-
{ name: '홍길동', age: 30, email: 'hong@example.com' },
|
|
539
|
-
{ name: '김철수', age: 25, email: 'kim@example.com' },
|
|
540
|
-
]
|
|
541
|
-
</script>
|
|
542
|
-
```
|
|
205
|
+
### JCheckbox
|
|
543
206
|
|
|
544
|
-
|
|
207
|
+
> **주의**: `modelValue`는 `boolean`이 아닌 `'Y'` / `'N'` 문자열
|
|
545
208
|
|
|
546
209
|
```vue
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
<script setup>
|
|
554
|
-
import { JIcon } from '@/components/atoms'
|
|
555
|
-
</script>
|
|
210
|
+
<JCheckbox
|
|
211
|
+
v-model="checked"
|
|
212
|
+
label="동의합니다"
|
|
213
|
+
:disabled="false"
|
|
214
|
+
:required="false"
|
|
215
|
+
/>
|
|
556
216
|
```
|
|
557
217
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
```vue
|
|
561
|
-
<template>
|
|
562
|
-
<JImage
|
|
563
|
-
src="https://example.com/image.jpg"
|
|
564
|
-
alt="이미지 설명"
|
|
565
|
-
width="300"
|
|
566
|
-
height="200"
|
|
567
|
-
/>
|
|
568
|
-
</template>
|
|
569
|
-
|
|
570
|
-
<script setup>
|
|
571
|
-
import { JImage } from '@/components/atoms'
|
|
572
|
-
</script>
|
|
218
|
+
```ts
|
|
219
|
+
const checked = ref('N') // 'Y' 또는 'N'
|
|
573
220
|
```
|
|
574
221
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
```vue
|
|
578
|
-
<template>
|
|
579
|
-
<div>
|
|
580
|
-
저장: <JKbd>Ctrl</JKbd> + <JKbd>S</JKbd>
|
|
581
|
-
복사: <JKbd>Ctrl</JKbd> + <JKbd>C</JKbd>
|
|
582
|
-
</div>
|
|
583
|
-
</template>
|
|
222
|
+
---
|
|
584
223
|
|
|
585
|
-
|
|
586
|
-
import { JKbd } from '@/components/atoms'
|
|
587
|
-
</script>
|
|
588
|
-
```
|
|
224
|
+
### JSwitch
|
|
589
225
|
|
|
590
|
-
|
|
226
|
+
> **주의**: `modelValue`는 `boolean`이 아닌 `'Y'` / `'N'` 문자열
|
|
591
227
|
|
|
592
228
|
```vue
|
|
593
|
-
<
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
<script setup>
|
|
599
|
-
import { ref } from 'vue'
|
|
600
|
-
import { JLabel, JInput } from '@/components/atoms'
|
|
601
|
-
|
|
602
|
-
const name = ref('')
|
|
603
|
-
</script>
|
|
229
|
+
<JSwitch
|
|
230
|
+
v-model="enabled"
|
|
231
|
+
label="알림 받기"
|
|
232
|
+
:disabled="false"
|
|
233
|
+
/>
|
|
604
234
|
```
|
|
605
235
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
```vue
|
|
609
|
-
<template>
|
|
610
|
-
<JLink href="/about">소개</JLink>
|
|
611
|
-
<JLink href="/contact" styletype="primary">연락처</JLink>
|
|
612
|
-
</template>
|
|
613
|
-
|
|
614
|
-
<script setup>
|
|
615
|
-
import { JLink } from '@/components/atoms'
|
|
616
|
-
</script>
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
##### JPopover - 팝오버
|
|
620
|
-
|
|
621
|
-
```vue
|
|
622
|
-
<template>
|
|
623
|
-
<JPopover>
|
|
624
|
-
<template #trigger>
|
|
625
|
-
<JButton>팝오버 열기</JButton>
|
|
626
|
-
</template>
|
|
627
|
-
<template #content>
|
|
628
|
-
<div class="p-4">
|
|
629
|
-
<p>팝오버 내용입니다.</p>
|
|
630
|
-
</div>
|
|
631
|
-
</template>
|
|
632
|
-
</JPopover>
|
|
633
|
-
</template>
|
|
634
|
-
|
|
635
|
-
<script setup>
|
|
636
|
-
import { JPopover, JButton } from '@/components/atoms'
|
|
637
|
-
</script>
|
|
236
|
+
```ts
|
|
237
|
+
const enabled = ref('N') // 'Y' 또는 'N'
|
|
638
238
|
```
|
|
639
239
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
```vue
|
|
643
|
-
<template>
|
|
644
|
-
<JProgress :value="progress" />
|
|
645
|
-
<JProgress :value="75" styletype="success" />
|
|
646
|
-
</template>
|
|
647
|
-
|
|
648
|
-
<script setup>
|
|
649
|
-
import { ref } from 'vue'
|
|
650
|
-
import { JProgress } from '@/components/atoms'
|
|
240
|
+
---
|
|
651
241
|
|
|
652
|
-
|
|
653
|
-
</script>
|
|
654
|
-
```
|
|
242
|
+
### JRadio
|
|
655
243
|
|
|
656
|
-
|
|
244
|
+
> **주의**: `options` 배열로 라디오 항목 정의 (개별 나열 아님)
|
|
657
245
|
|
|
658
246
|
```vue
|
|
659
|
-
<
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
import { JRadio } from '@/components/atoms'
|
|
668
|
-
|
|
669
|
-
const selected = ref('option1')
|
|
670
|
-
</script>
|
|
247
|
+
<JRadio
|
|
248
|
+
v-model="selected"
|
|
249
|
+
:options="[
|
|
250
|
+
{ value: 'opt1', label: '옵션 1' },
|
|
251
|
+
{ value: 'opt2', label: '옵션 2' },
|
|
252
|
+
{ value: 'opt3', label: '옵션 3', disabled: true },
|
|
253
|
+
]"
|
|
254
|
+
/>
|
|
671
255
|
```
|
|
672
256
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
```vue
|
|
676
|
-
<template>
|
|
677
|
-
<JSearchCombo
|
|
678
|
-
v-model="selected"
|
|
679
|
-
:options="options"
|
|
680
|
-
placeholder="검색하여 선택하세요"
|
|
681
|
-
/>
|
|
682
|
-
</template>
|
|
683
|
-
|
|
684
|
-
<script setup>
|
|
685
|
-
import { ref } from 'vue'
|
|
686
|
-
import { JSearchCombo } from '@/components/atoms'
|
|
687
|
-
|
|
688
|
-
const selected = ref('')
|
|
689
|
-
const options = [
|
|
690
|
-
{ value: '1', label: '항목 1' },
|
|
691
|
-
{ value: '2', label: '항목 2' },
|
|
692
|
-
// ... 많은 옵션들
|
|
693
|
-
]
|
|
694
|
-
</script>
|
|
695
|
-
```
|
|
257
|
+
---
|
|
696
258
|
|
|
697
|
-
|
|
259
|
+
### JDatepicker
|
|
698
260
|
|
|
699
261
|
```vue
|
|
700
|
-
<
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
<script setup>
|
|
706
|
-
import { ref } from 'vue'
|
|
707
|
-
import { JSpinner } from '@/components/atoms'
|
|
708
|
-
|
|
709
|
-
const loading = ref(true)
|
|
710
|
-
</script>
|
|
262
|
+
<JDatepicker
|
|
263
|
+
v-model="date"
|
|
264
|
+
placeholder="날짜를 선택하세요"
|
|
265
|
+
:disabled="false"
|
|
266
|
+
/>
|
|
711
267
|
```
|
|
712
268
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
```vue
|
|
716
|
-
<template>
|
|
717
|
-
<JSwitch v-model="enabled" label="알림 받기" />
|
|
718
|
-
</template>
|
|
719
|
-
|
|
720
|
-
<script setup>
|
|
721
|
-
import { ref } from 'vue'
|
|
722
|
-
import { JSwitch } from '@/components/atoms'
|
|
723
|
-
|
|
724
|
-
const enabled = ref(false)
|
|
725
|
-
</script>
|
|
269
|
+
```ts
|
|
270
|
+
const date = ref<string | null>(null) // ISO 8601: 'YYYY-MM-DD'
|
|
726
271
|
```
|
|
727
272
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
```vue
|
|
731
|
-
<template>
|
|
732
|
-
<JTextarea
|
|
733
|
-
v-model="description"
|
|
734
|
-
placeholder="설명을 입력하세요"
|
|
735
|
-
rows="5"
|
|
736
|
-
/>
|
|
737
|
-
</template>
|
|
738
|
-
|
|
739
|
-
<script setup>
|
|
740
|
-
import { ref } from 'vue'
|
|
741
|
-
import { JTextarea } from '@/components/atoms'
|
|
742
|
-
|
|
743
|
-
const description = ref('')
|
|
744
|
-
</script>
|
|
745
|
-
```
|
|
273
|
+
---
|
|
746
274
|
|
|
747
|
-
|
|
275
|
+
### JGrid (AG Grid)
|
|
748
276
|
|
|
749
277
|
```vue
|
|
750
|
-
<
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
278
|
+
<JGrid
|
|
279
|
+
:column-defs="columnDefs"
|
|
280
|
+
:row-data="rowData"
|
|
281
|
+
theme="ag-theme-balham"
|
|
282
|
+
:pagination="false"
|
|
283
|
+
:checkbox="false"
|
|
284
|
+
:summary-column="false"
|
|
285
|
+
:hidden-column="false"
|
|
286
|
+
:enable-grouping="false" <!-- Enterprise -->
|
|
287
|
+
:enable-pivot="false" <!-- Enterprise -->
|
|
288
|
+
:enable-excel-export="false" <!-- Enterprise -->
|
|
289
|
+
/>
|
|
759
290
|
```
|
|
760
291
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
- `styletype="primary"`: 주요 액션 (저장, 확인 등)
|
|
767
|
-
- `styletype="secondary"`: 보조 액션
|
|
768
|
-
- `styletype="danger"`: 위험한 액션 (삭제 등)
|
|
769
|
-
- `styletype="outline"`: 덜 강조된 액션
|
|
770
|
-
- `styletype="ghost"`: 최소한의 시각적 강조
|
|
771
|
-
- `disabled`: 비활성화 상태
|
|
772
|
-
- `loading`: 비동기 작업 진행 중 표시
|
|
773
|
-
|
|
774
|
-
##### JInput - 텍스트 입력 필드
|
|
775
|
-
|
|
776
|
-
**주요 props**: `modelValue`, `type` (text, email, password, number 등), `placeholder`, `disabled`, `readonly`, `required`, `styletype` (default, error, success, warning, sm, lg)
|
|
777
|
-
|
|
778
|
-
**권장 사용 시나리오**:
|
|
779
|
-
- `type="text"`: 일반 텍스트 입력
|
|
780
|
-
- `type="email"`: 이메일 주소 입력
|
|
781
|
-
- `type="password"`: 비밀번호 입력
|
|
782
|
-
- `type="number"`: 숫자 입력
|
|
783
|
-
- `styletype="error"`: 유효성 검사 실패 시
|
|
784
|
-
- `styletype="success"`: 유효성 검사 통과 시
|
|
785
|
-
- `readonly`: 읽기 전용 표시
|
|
786
|
-
|
|
787
|
-
##### JEditor - 마크다운 에디터
|
|
788
|
-
|
|
789
|
-
**주요 props**: `modelValue` (마크다운 문자열), `placeholder`, `disabled`, `readonly`, `height`, `theme` (light, dark)
|
|
790
|
-
|
|
791
|
-
**권장 사용 시나리오**:
|
|
792
|
-
- `modelValue`: v-model로 마크다운 내용 바인딩
|
|
793
|
-
- `height`: 에디터 높이 설정 (기본값: 500)
|
|
794
|
-
- `theme`: 다크모드 지원
|
|
795
|
-
- `readonly`: 읽기 전용 모드 (프리뷰 전용)
|
|
796
|
-
|
|
797
|
-
##### JPreview - 마크다운/HTML 뷰어
|
|
798
|
-
|
|
799
|
-
**주요 props**: `modelValue` (마크다운 또는 HTML 문자열), `theme` (light, dark)
|
|
800
|
-
|
|
801
|
-
**권장 사용 시나리오**:
|
|
802
|
-
- `modelValue`: 마크다운 또는 HTML 문자열 자동 감지 및 렌더링
|
|
803
|
-
- HTML 문서는 `<!DOCTYPE html>` 또는 `<html>` 태그로 시작하면 자동 감지
|
|
804
|
-
- 마크다운은 그 외 모든 경우에 자동 처리
|
|
805
|
-
- `theme`: 다크모드 지원
|
|
806
|
-
- JEditor로 작성한 마크다운을 자동으로 변환하여 표시
|
|
807
|
-
|
|
808
|
-
##### JToaster - Toast 알림 컨테이너
|
|
809
|
-
|
|
810
|
-
**권장 사용 시나리오**:
|
|
811
|
-
- 앱의 루트 레벨에 `<JToaster />` 컴포넌트 추가
|
|
812
|
-
- `JToast` 함수와 함께 사용: `JToast.success()`, `JToast.error()`, `JToast.info()`, `JToast.warning()`
|
|
813
|
-
- 자동 사라짐 기능 내장
|
|
814
|
-
- 여러 Toast 동시 표시 가능
|
|
815
|
-
|
|
816
|
-
#### Molecules (분자 컴포넌트)
|
|
817
|
-
|
|
818
|
-
##### JAccordion - 접을 수 있는 콘텐츠 섹션
|
|
819
|
-
|
|
820
|
-
```vue
|
|
821
|
-
<template>
|
|
822
|
-
<JAccordion
|
|
823
|
-
:items="items"
|
|
824
|
-
type="single"
|
|
825
|
-
collapsible
|
|
826
|
-
/>
|
|
827
|
-
</template>
|
|
828
|
-
|
|
829
|
-
<script setup>
|
|
830
|
-
import { JAccordion } from '@/components/molecules'
|
|
831
|
-
|
|
832
|
-
const items = [
|
|
833
|
-
{
|
|
834
|
-
value: 'item-1',
|
|
835
|
-
title: '첫 번째 항목',
|
|
836
|
-
content: '첫 번째 항목의 내용입니다.',
|
|
837
|
-
},
|
|
838
|
-
{
|
|
839
|
-
value: 'item-2',
|
|
840
|
-
title: '두 번째 항목',
|
|
841
|
-
content: '두 번째 항목의 내용입니다.',
|
|
842
|
-
},
|
|
292
|
+
```ts
|
|
293
|
+
const columnDefs = [
|
|
294
|
+
{ field: 'name', headerName: '이름' },
|
|
295
|
+
{ field: 'age', headerName: '나이' },
|
|
296
|
+
{ field: 'email', headerName: '이메일' },
|
|
843
297
|
]
|
|
844
|
-
</script>
|
|
845
|
-
```
|
|
846
|
-
|
|
847
|
-
##### JAlert - 알림 메시지 표시
|
|
848
|
-
|
|
849
|
-
```vue
|
|
850
|
-
<template>
|
|
851
|
-
<JAlert
|
|
852
|
-
variant="default"
|
|
853
|
-
title="알림"
|
|
854
|
-
description="이것은 알림 메시지입니다."
|
|
855
|
-
button-text="확인"
|
|
856
|
-
:show-footer="true"
|
|
857
|
-
@confirm="handleConfirm"
|
|
858
|
-
/>
|
|
859
|
-
</template>
|
|
860
298
|
|
|
861
|
-
|
|
862
|
-
|
|
299
|
+
const rowData = [
|
|
300
|
+
{ name: '홍길동', age: 30, email: 'hong@example.com' },
|
|
301
|
+
]
|
|
863
302
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
</script>
|
|
303
|
+
// ref로 접근하여 Excel 내보내기
|
|
304
|
+
gridRef.value?.exportToExcel()
|
|
868
305
|
```
|
|
869
306
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
```vue
|
|
873
|
-
<template>
|
|
874
|
-
<JBreadcrumb :items="breadcrumbItems" />
|
|
875
|
-
</template>
|
|
876
|
-
|
|
877
|
-
<script setup>
|
|
878
|
-
import { JBreadcrumb } from '@/components/molecules'
|
|
879
|
-
|
|
880
|
-
const breadcrumbItems = [
|
|
881
|
-
{ label: '홈', href: '/' },
|
|
882
|
-
{ label: '카테고리', href: '/category' },
|
|
883
|
-
{ label: '현재 페이지' },
|
|
884
|
-
]
|
|
885
|
-
</script>
|
|
886
|
-
```
|
|
307
|
+
---
|
|
887
308
|
|
|
888
|
-
|
|
309
|
+
### JIcon
|
|
889
310
|
|
|
890
311
|
```vue
|
|
891
|
-
<
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
</JButtonGroup>
|
|
897
|
-
</template>
|
|
898
|
-
|
|
899
|
-
<script setup>
|
|
900
|
-
import { JButtonGroup, JButton } from '@/components/molecules'
|
|
901
|
-
</script>
|
|
312
|
+
<JIcon
|
|
313
|
+
name="user" <!-- Lucide 아이콘 이름 (필수) -->
|
|
314
|
+
size="md" <!-- 'sm'|'md'|'lg'|'xl' -->
|
|
315
|
+
color="#333" <!-- CSS 색상값 (선택) -->
|
|
316
|
+
/>
|
|
902
317
|
```
|
|
903
318
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
```vue
|
|
907
|
-
<template>
|
|
908
|
-
<JCard
|
|
909
|
-
title="카드 제목"
|
|
910
|
-
description="카드 설명"
|
|
911
|
-
footer="푸터 텍스트"
|
|
912
|
-
>
|
|
913
|
-
<p>카드 내용입니다.</p>
|
|
914
|
-
</JCard>
|
|
915
|
-
</template>
|
|
916
|
-
|
|
917
|
-
<script setup>
|
|
918
|
-
import { JCard } from '@/components/molecules'
|
|
919
|
-
</script>
|
|
920
|
-
```
|
|
319
|
+
---
|
|
921
320
|
|
|
922
|
-
|
|
321
|
+
### JAvatar
|
|
923
322
|
|
|
924
323
|
```vue
|
|
925
|
-
<
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
<div class="p-2 hover:bg-gray-100 cursor-pointer">붙여넣기</div>
|
|
934
|
-
<div class="p-2 hover:bg-gray-100 cursor-pointer">삭제</div>
|
|
935
|
-
</div>
|
|
936
|
-
</template>
|
|
937
|
-
</JContextMenu>
|
|
938
|
-
</template>
|
|
939
|
-
|
|
940
|
-
<script setup>
|
|
941
|
-
import { JContextMenu } from '@/components/molecules'
|
|
942
|
-
</script>
|
|
324
|
+
<JAvatar
|
|
325
|
+
src="https://example.com/avatar.jpg"
|
|
326
|
+
alt="사용자 이름"
|
|
327
|
+
fallback="JD"
|
|
328
|
+
size="lg"
|
|
329
|
+
shape="circle"
|
|
330
|
+
status="online"
|
|
331
|
+
/>
|
|
943
332
|
```
|
|
944
333
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
```vue
|
|
948
|
-
<template>
|
|
949
|
-
<JGroupCombo
|
|
950
|
-
v-model="selected"
|
|
951
|
-
:groups="groupedOptions"
|
|
952
|
-
placeholder="그룹에서 선택하세요"
|
|
953
|
-
/>
|
|
954
|
-
</template>
|
|
955
|
-
|
|
956
|
-
<script setup>
|
|
957
|
-
import { ref } from 'vue'
|
|
958
|
-
import { JGroupCombo } from '@/components/molecules'
|
|
959
|
-
|
|
960
|
-
const selected = ref('')
|
|
961
|
-
const groupedOptions = [
|
|
962
|
-
{
|
|
963
|
-
label: '그룹 1',
|
|
964
|
-
options: [
|
|
965
|
-
{ value: '1-1', label: '옵션 1-1' },
|
|
966
|
-
{ value: '1-2', label: '옵션 1-2' },
|
|
967
|
-
],
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
label: '그룹 2',
|
|
971
|
-
options: [
|
|
972
|
-
{ value: '2-1', label: '옵션 2-1' },
|
|
973
|
-
{ value: '2-2', label: '옵션 2-2' },
|
|
974
|
-
],
|
|
975
|
-
},
|
|
976
|
-
]
|
|
977
|
-
</script>
|
|
978
|
-
```
|
|
334
|
+
---
|
|
979
335
|
|
|
980
|
-
|
|
336
|
+
### JBadge
|
|
981
337
|
|
|
982
338
|
```vue
|
|
983
|
-
<
|
|
984
|
-
<JSearchAddr
|
|
985
|
-
v-model="address"
|
|
986
|
-
@select="handleAddressSelect"
|
|
987
|
-
/>
|
|
988
|
-
</template>
|
|
989
|
-
|
|
990
|
-
<script setup>
|
|
991
|
-
import { ref } from 'vue'
|
|
992
|
-
import { JSearchAddr } from '@/components/molecules'
|
|
993
|
-
|
|
994
|
-
const address = ref('')
|
|
995
|
-
|
|
996
|
-
const handleAddressSelect = (data) => {
|
|
997
|
-
console.log('선택된 주소:', data)
|
|
998
|
-
}
|
|
999
|
-
</script>
|
|
339
|
+
<JBadge size="md">활성</JBadge>
|
|
1000
340
|
```
|
|
1001
341
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
```vue
|
|
1005
|
-
<template>
|
|
1006
|
-
<JTabs
|
|
1007
|
-
:tabs="tabs"
|
|
1008
|
-
v-model:active-tab-id="activeTab"
|
|
1009
|
-
@tab-change="handleTabChange"
|
|
1010
|
-
/>
|
|
1011
|
-
</template>
|
|
1012
|
-
|
|
1013
|
-
<script setup>
|
|
1014
|
-
import { ref } from 'vue'
|
|
1015
|
-
import { JTabs } from '@/components/molecules'
|
|
1016
|
-
|
|
1017
|
-
const activeTab = ref('tab1')
|
|
1018
|
-
const tabs = [
|
|
1019
|
-
{ id: 'tab1', label: '탭 1', closable: false },
|
|
1020
|
-
{ id: 'tab2', label: '탭 2', closable: true },
|
|
1021
|
-
{ id: 'tab3', label: '탭 3', closable: true },
|
|
1022
|
-
]
|
|
1023
|
-
|
|
1024
|
-
const handleTabChange = (id) => {
|
|
1025
|
-
console.log('탭 변경:', id)
|
|
1026
|
-
}
|
|
1027
|
-
</script>
|
|
1028
|
-
```
|
|
342
|
+
---
|
|
1029
343
|
|
|
1030
|
-
|
|
344
|
+
### JProgress
|
|
1031
345
|
|
|
1032
346
|
```vue
|
|
1033
|
-
<
|
|
1034
|
-
<JTitlebar
|
|
1035
|
-
title="페이지 제목"
|
|
1036
|
-
description="페이지 설명"
|
|
1037
|
-
>
|
|
1038
|
-
<template #actions>
|
|
1039
|
-
<JButton>액션 1</JButton>
|
|
1040
|
-
<JButton>액션 2</JButton>
|
|
1041
|
-
</template>
|
|
1042
|
-
</JTitlebar>
|
|
1043
|
-
</template>
|
|
1044
|
-
|
|
1045
|
-
<script setup>
|
|
1046
|
-
import { JTitlebar, JButton } from '@/components/molecules'
|
|
1047
|
-
</script>
|
|
347
|
+
<JProgress :value="75" />
|
|
1048
348
|
```
|
|
1049
349
|
|
|
1050
|
-
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### JEditor
|
|
1051
353
|
|
|
1052
|
-
|
|
354
|
+
```vue
|
|
355
|
+
<JEditor
|
|
356
|
+
v-model="markdown"
|
|
357
|
+
placeholder="마크다운을 입력하세요..."
|
|
358
|
+
:height="500"
|
|
359
|
+
theme="light" <!-- 'light'|'dark' -->
|
|
360
|
+
:disabled="false"
|
|
361
|
+
:readonly="false"
|
|
362
|
+
@save="handleSave" <!-- Ctrl+S -->
|
|
363
|
+
/>
|
|
364
|
+
```
|
|
1053
365
|
|
|
1054
|
-
|
|
1055
|
-
- `type` prop으로 원하는 입력 컴포넌트 선택
|
|
1056
|
-
- `label` prop으로 라벨 값을 입력하면 UI 배치시 라벨이 포함되며, 입력 필드와 자동 연결 (접근성)
|
|
1057
|
-
- `errorMsg` prop으로 에러 메시지 값을 입력하면 UI 배치시 에러 메시지가 포함됨
|
|
1058
|
-
- `orientation="horizontal"`: 레이블과 입력 필드를 가로로 배치
|
|
1059
|
-
- `orientation="vertical"`: 레이블과 입력 필드를 세로로 배치 (기본값)
|
|
1060
|
-
- `orientation="responsive"`: 화면 크기에 따라 자동 조정
|
|
1061
|
-
- 레이아웃 일관성 유지
|
|
366
|
+
---
|
|
1062
367
|
|
|
1063
|
-
|
|
368
|
+
### JPreview
|
|
1064
369
|
|
|
1065
|
-
|
|
370
|
+
```vue
|
|
371
|
+
<JPreview
|
|
372
|
+
:model-value="markdownOrHtml"
|
|
373
|
+
theme="light" <!-- 'light'|'dark' -->
|
|
374
|
+
/>
|
|
375
|
+
```
|
|
1066
376
|
|
|
1067
|
-
|
|
377
|
+
> HTML은 `<!DOCTYPE html>` 또는 `<html>` 태그로 시작하면 자동 감지, 그 외는 마크다운 처리
|
|
1068
378
|
|
|
1069
|
-
|
|
1070
|
-
- `type="simple"`: 단순한 폼
|
|
1071
|
-
- `type="sectioned"`: 섹션별로 구분된 폼
|
|
1072
|
-
- `type="wizard"`: 단계별 입력 폼
|
|
1073
|
-
- JSON 스키마로 폼 자동 생성
|
|
1074
|
-
- `schema.fields` 배열에 필드 정의: `controlName`, `label`, `type`, `isRequired` 등
|
|
1075
|
-
- Props 기반의 사용을 권장합니다
|
|
379
|
+
---
|
|
1076
380
|
|
|
1077
|
-
|
|
381
|
+
### JTooltip
|
|
1078
382
|
|
|
1079
|
-
|
|
383
|
+
```vue
|
|
384
|
+
<JTooltip
|
|
385
|
+
content="설명 텍스트"
|
|
386
|
+
side="top" <!-- 'top'|'right'|'bottom'|'left' -->
|
|
387
|
+
align="center" <!-- 'start'|'center'|'end' -->
|
|
388
|
+
:delay="200"
|
|
389
|
+
maxWidth="200px"
|
|
390
|
+
trigger="hover" <!-- 'hover'|'focus'|'click'|'manual' -->
|
|
391
|
+
:disabled="false"
|
|
392
|
+
>
|
|
393
|
+
<JButton>호버하세요</JButton>
|
|
394
|
+
</JTooltip>
|
|
395
|
+
```
|
|
1080
396
|
|
|
1081
|
-
|
|
1082
|
-
- `open`: 모달 열림/닫힘 상태 제어
|
|
1083
|
-
- `size="sm"`: 작은 모달
|
|
1084
|
-
- `size="md"`: 중간 모달 (기본값)
|
|
1085
|
-
- `size="lg"`: 큰 모달
|
|
1086
|
-
- `size="xl"`: 매우 큰 모달
|
|
1087
|
-
- `size="2xl"`: 초대형 모달
|
|
1088
|
-
- `size="full"`: 전체 화면 모달
|
|
1089
|
-
- `title`: 모달 제목
|
|
1090
|
-
- `show-footer`: 푸터 표시 여부
|
|
1091
|
-
- `@confirm`: 확인 버튼 클릭 이벤트
|
|
1092
|
-
- `@cancel`: 취소 버튼 클릭 이벤트
|
|
1093
|
-
- Props 기반의 사용을 권장합니다
|
|
397
|
+
---
|
|
1094
398
|
|
|
1095
|
-
|
|
399
|
+
### JDivider
|
|
1096
400
|
|
|
1097
401
|
```vue
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
:open="isOpen"
|
|
1101
|
-
:schema="formSchema"
|
|
1102
|
-
v-model="formData"
|
|
1103
|
-
title="폼 모달"
|
|
1104
|
-
@confirm="handleConfirm"
|
|
1105
|
-
@cancel="handleCancel"
|
|
1106
|
-
/>
|
|
1107
|
-
</template>
|
|
402
|
+
<!-- 기본 구분선 -->
|
|
403
|
+
<JDivider />
|
|
1108
404
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
405
|
+
<!-- 텍스트 포함 구분선 -->
|
|
406
|
+
<JDivider>또는</JDivider>
|
|
407
|
+
```
|
|
1112
408
|
|
|
1113
|
-
|
|
1114
|
-
const formData = ref({})
|
|
1115
|
-
const formSchema = {
|
|
1116
|
-
type: 'simple',
|
|
1117
|
-
fields: [
|
|
1118
|
-
{ controlName: 'name', label: '이름', type: 'input', isRequired: true },
|
|
1119
|
-
{ controlName: 'email', label: '이메일', type: 'input', inputType: 'email' },
|
|
1120
|
-
],
|
|
1121
|
-
}
|
|
409
|
+
---
|
|
1122
410
|
|
|
1123
|
-
|
|
1124
|
-
console.log('확인:', formData.value)
|
|
1125
|
-
isOpen.value = false
|
|
1126
|
-
}
|
|
411
|
+
### JKbd
|
|
1127
412
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
</
|
|
413
|
+
```vue
|
|
414
|
+
<div>
|
|
415
|
+
저장: <JKbd>Ctrl</JKbd> + <JKbd>S</JKbd>
|
|
416
|
+
</div>
|
|
1132
417
|
```
|
|
1133
418
|
|
|
1134
|
-
|
|
419
|
+
---
|
|
1135
420
|
|
|
1136
|
-
|
|
1137
|
-
<template>
|
|
1138
|
-
<JHeader
|
|
1139
|
-
:menu-items="menuItems"
|
|
1140
|
-
:user="user"
|
|
1141
|
-
@menu-click="handleMenuClick"
|
|
1142
|
-
/>
|
|
1143
|
-
</template>
|
|
421
|
+
### JLabel
|
|
1144
422
|
|
|
1145
|
-
|
|
1146
|
-
|
|
423
|
+
```vue
|
|
424
|
+
<JLabel for="input-id">이름</JLabel>
|
|
425
|
+
<JInput id="input-id" v-model="name" />
|
|
426
|
+
```
|
|
1147
427
|
|
|
1148
|
-
|
|
1149
|
-
{ id: '1', label: '홈', path: '/' },
|
|
1150
|
-
{ id: '2', label: '설정', path: '/settings' },
|
|
1151
|
-
]
|
|
428
|
+
---
|
|
1152
429
|
|
|
1153
|
-
|
|
1154
|
-
name: '홍길동',
|
|
1155
|
-
avatar: 'https://example.com/avatar.jpg',
|
|
1156
|
-
}
|
|
430
|
+
### JLink
|
|
1157
431
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
</script>
|
|
432
|
+
```vue
|
|
433
|
+
<JLink href="/about">소개</JLink>
|
|
1162
434
|
```
|
|
1163
435
|
|
|
1164
|
-
|
|
436
|
+
---
|
|
1165
437
|
|
|
1166
|
-
|
|
1167
|
-
<template>
|
|
1168
|
-
<JPageContainer>
|
|
1169
|
-
<template #header>
|
|
1170
|
-
<h1>페이지 제목</h1>
|
|
1171
|
-
</template>
|
|
1172
|
-
<template #content>
|
|
1173
|
-
<p>페이지 내용입니다.</p>
|
|
1174
|
-
</template>
|
|
1175
|
-
</JPageContainer>
|
|
1176
|
-
</template>
|
|
438
|
+
### JImage
|
|
1177
439
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
440
|
+
```vue
|
|
441
|
+
<JImage
|
|
442
|
+
src="https://example.com/image.jpg"
|
|
443
|
+
alt="이미지 설명"
|
|
444
|
+
width="300"
|
|
445
|
+
height="200"
|
|
446
|
+
/>
|
|
1181
447
|
```
|
|
1182
448
|
|
|
1183
|
-
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
### JPopover
|
|
1184
452
|
|
|
1185
453
|
```vue
|
|
1186
|
-
<
|
|
1187
|
-
<
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
454
|
+
<JPopover>
|
|
455
|
+
<template #trigger>
|
|
456
|
+
<JButton>팝오버 열기</JButton>
|
|
457
|
+
</template>
|
|
458
|
+
<template #content>
|
|
459
|
+
<div class="p-4">
|
|
460
|
+
<p>팝오버 내용입니다.</p>
|
|
461
|
+
</div>
|
|
462
|
+
</template>
|
|
463
|
+
</JPopover>
|
|
464
|
+
```
|
|
1193
465
|
|
|
1194
|
-
|
|
1195
|
-
import { ref } from 'vue'
|
|
1196
|
-
import { JSearchPanel } from '@/components/organisms'
|
|
466
|
+
---
|
|
1197
467
|
|
|
1198
|
-
|
|
1199
|
-
const searchSchema = {
|
|
1200
|
-
type: 'simple',
|
|
1201
|
-
fields: [
|
|
1202
|
-
{ controlName: 'keyword', label: '검색어', type: 'input' },
|
|
1203
|
-
{ controlName: 'category', label: '카테고리', type: 'combo', options: [] },
|
|
1204
|
-
],
|
|
1205
|
-
}
|
|
468
|
+
### JSpinner
|
|
1206
469
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
</script>
|
|
470
|
+
```vue
|
|
471
|
+
<JSpinner v-if="loading" />
|
|
472
|
+
<div v-else>콘텐츠</div>
|
|
1211
473
|
```
|
|
1212
474
|
|
|
1213
|
-
|
|
475
|
+
---
|
|
1214
476
|
|
|
1215
|
-
|
|
1216
|
-
<template>
|
|
1217
|
-
<JSidebarAdvanced
|
|
1218
|
-
:menu-items="menuItems"
|
|
1219
|
-
:favorites="favorites"
|
|
1220
|
-
@menu-click="handleMenuClick"
|
|
1221
|
-
/>
|
|
1222
|
-
</template>
|
|
477
|
+
### JTree
|
|
1223
478
|
|
|
1224
|
-
|
|
1225
|
-
|
|
479
|
+
```vue
|
|
480
|
+
<JTree
|
|
481
|
+
:items="treeItems"
|
|
482
|
+
v-model:expanded-keys="expandedKeys"
|
|
483
|
+
v-model:active-key="activeKey"
|
|
484
|
+
:permissions="permissions"
|
|
485
|
+
:max-depth="10"
|
|
486
|
+
searchQuery=""
|
|
487
|
+
@node-click="handleNodeClick"
|
|
488
|
+
@expand-change="handleExpandChange"
|
|
489
|
+
/>
|
|
490
|
+
```
|
|
1226
491
|
|
|
1227
|
-
|
|
1228
|
-
{ id: '1', label: '홈', path: '/', icon: 'house' },
|
|
1229
|
-
{ id: '2', label: '설정', path: '/settings', icon: 'settings' },
|
|
1230
|
-
]
|
|
492
|
+
---
|
|
1231
493
|
|
|
1232
|
-
|
|
494
|
+
## Molecules Props 레퍼런스
|
|
1233
495
|
|
|
1234
|
-
|
|
1235
|
-
console.log('메뉴 클릭:', item)
|
|
1236
|
-
}
|
|
1237
|
-
</script>
|
|
1238
|
-
```
|
|
496
|
+
### JFormField
|
|
1239
497
|
|
|
1240
|
-
|
|
498
|
+
> **권장**: 폼 필드 구성 시 JFormField 사용. `type` prop으로 입력 컴포넌트 선택.
|
|
1241
499
|
|
|
1242
500
|
```vue
|
|
1243
|
-
<
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
501
|
+
<JFormField
|
|
502
|
+
type="input" <!-- 'input'|'textarea'|'checkbox'|'switch'|'combo'|'radio'|'searchCombo'|'datepicker' -->
|
|
503
|
+
label="이름"
|
|
504
|
+
description="설명 텍스트"
|
|
505
|
+
error-msg="에러 메시지"
|
|
506
|
+
v-model="value"
|
|
507
|
+
placeholder="입력하세요"
|
|
508
|
+
:required="true"
|
|
509
|
+
:disabled="false"
|
|
510
|
+
:readonly="false"
|
|
511
|
+
orientation="vertical" <!-- 'vertical'|'horizontal'|'responsive' -->
|
|
512
|
+
labelAlign="left" <!-- 'left'|'middle'|'right' -->
|
|
513
|
+
labelWidth="8rem"
|
|
514
|
+
inputType="text" <!-- type="input"일 때: 'text'|'email'|'password'|'number' -->
|
|
515
|
+
:options="[]" <!-- type이 combo/radio/searchCombo일 때 -->
|
|
516
|
+
:multiple="false" <!-- type이 combo/searchCombo일 때 -->
|
|
517
|
+
radioDirection="horizontal" <!-- type="radio"일 때: 'horizontal'|'vertical' -->
|
|
518
|
+
/>
|
|
519
|
+
```
|
|
1249
520
|
|
|
1250
|
-
|
|
1251
|
-
|
|
521
|
+
**장점:**
|
|
522
|
+
- `label` prop → 라벨 자동 배치 + 접근성 연결
|
|
523
|
+
- `error-msg` prop → 에러 메시지 자동 표시
|
|
524
|
+
- `orientation` → 레이아웃 일관성
|
|
1252
525
|
|
|
1253
|
-
|
|
1254
|
-
{ id: '1', label: '홈', path: '/' },
|
|
1255
|
-
{ id: '2', label: '설정', path: '/settings' },
|
|
1256
|
-
]
|
|
526
|
+
---
|
|
1257
527
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
528
|
+
### JCard
|
|
529
|
+
|
|
530
|
+
```vue
|
|
531
|
+
<JCard title="카드 제목" description="설명" footer="푸터">
|
|
532
|
+
<p>카드 내용</p>
|
|
533
|
+
</JCard>
|
|
1262
534
|
```
|
|
1263
535
|
|
|
1264
536
|
---
|
|
1265
537
|
|
|
1266
|
-
|
|
538
|
+
### JAlert
|
|
1267
539
|
|
|
1268
|
-
|
|
540
|
+
```vue
|
|
541
|
+
<JAlert
|
|
542
|
+
variant="default" <!-- 'default'|'destructive' -->
|
|
543
|
+
title="알림"
|
|
544
|
+
description="메시지 내용"
|
|
545
|
+
buttonText="확인"
|
|
546
|
+
:showFooter="true"
|
|
547
|
+
@confirm="handler"
|
|
548
|
+
/>
|
|
549
|
+
```
|
|
1269
550
|
|
|
1270
|
-
|
|
551
|
+
**Slots**: `header`, default, `footer`
|
|
1271
552
|
|
|
1272
|
-
|
|
553
|
+
---
|
|
1273
554
|
|
|
1274
|
-
|
|
555
|
+
### JTabs
|
|
1275
556
|
|
|
1276
557
|
```vue
|
|
1277
|
-
<
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
558
|
+
<JTabs
|
|
559
|
+
:tabs="[
|
|
560
|
+
{ id: 'tab1', label: '탭 1', closable: false },
|
|
561
|
+
{ id: 'tab2', label: '탭 2', closable: true },
|
|
562
|
+
]"
|
|
563
|
+
v-model:active-tab-id="activeTab"
|
|
564
|
+
@tab-change="handler"
|
|
565
|
+
@tab-close="handler"
|
|
566
|
+
>
|
|
567
|
+
<template #content-tab1>탭 1 내용</template>
|
|
568
|
+
<template #content-tab2>탭 2 내용</template>
|
|
569
|
+
</JTabs>
|
|
570
|
+
```
|
|
1281
571
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### JAccordion
|
|
575
|
+
|
|
576
|
+
```vue
|
|
577
|
+
<JAccordion
|
|
578
|
+
:items="[
|
|
579
|
+
{ value: 'item-1', title: '제목 1', content: '내용 1' },
|
|
580
|
+
{ value: 'item-2', title: '제목 2', content: '내용 2' },
|
|
581
|
+
]"
|
|
582
|
+
type="single"
|
|
583
|
+
collapsible
|
|
584
|
+
/>
|
|
1287
585
|
```
|
|
1288
586
|
|
|
1289
|
-
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
### JBreadcrumb
|
|
1290
590
|
|
|
1291
591
|
```vue
|
|
1292
|
-
<
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
592
|
+
<JBreadcrumb :items="[
|
|
593
|
+
{ label: '홈', href: '/' },
|
|
594
|
+
{ label: '카테고리', href: '/category' },
|
|
595
|
+
{ label: '현재 페이지' },
|
|
596
|
+
]" />
|
|
597
|
+
```
|
|
1296
598
|
|
|
1297
|
-
|
|
1298
|
-
const showToast = () => {
|
|
1299
|
-
JToast('메시지가 표시됩니다')
|
|
1300
|
-
}
|
|
599
|
+
---
|
|
1301
600
|
|
|
1302
|
-
|
|
1303
|
-
const showSuccess = () => {
|
|
1304
|
-
JToast.success('성공적으로 저장되었습니다')
|
|
1305
|
-
}
|
|
601
|
+
### JButtonGroup
|
|
1306
602
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
603
|
+
```vue
|
|
604
|
+
<JButtonGroup>
|
|
605
|
+
<JButton>저장</JButton>
|
|
606
|
+
<JButton>취소</JButton>
|
|
607
|
+
<JButton>삭제</JButton>
|
|
608
|
+
</JButtonGroup>
|
|
609
|
+
```
|
|
1311
610
|
|
|
1312
|
-
|
|
1313
|
-
const showInfo = () => {
|
|
1314
|
-
JToast.info('정보 메시지입니다')
|
|
1315
|
-
}
|
|
611
|
+
---
|
|
1316
612
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
613
|
+
### JTitlebar
|
|
614
|
+
|
|
615
|
+
```vue
|
|
616
|
+
<JTitlebar title="페이지 제목" description="설명">
|
|
617
|
+
<template #actions>
|
|
618
|
+
<JButton>액션</JButton>
|
|
619
|
+
</template>
|
|
620
|
+
</JTitlebar>
|
|
1322
621
|
```
|
|
1323
622
|
|
|
1324
|
-
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
### JContextMenu
|
|
1325
626
|
|
|
1326
627
|
```vue
|
|
1327
|
-
<
|
|
1328
|
-
|
|
628
|
+
<JContextMenu :items="menuGroups" :disabled="false" @select="handler">
|
|
629
|
+
<div>우클릭 영역</div>
|
|
630
|
+
</JContextMenu>
|
|
631
|
+
```
|
|
1329
632
|
|
|
1330
|
-
|
|
1331
|
-
const showWithDescription = () => {
|
|
1332
|
-
JToast('제목', {
|
|
1333
|
-
description: '추가 설명 텍스트',
|
|
1334
|
-
})
|
|
1335
|
-
}
|
|
633
|
+
---
|
|
1336
634
|
|
|
1337
|
-
|
|
1338
|
-
const showWithAction = () => {
|
|
1339
|
-
JToast('제목', {
|
|
1340
|
-
description: '설명 텍스트',
|
|
1341
|
-
action: {
|
|
1342
|
-
label: '실행 취소',
|
|
1343
|
-
onClick: () => {
|
|
1344
|
-
console.log('실행 취소')
|
|
1345
|
-
},
|
|
1346
|
-
},
|
|
1347
|
-
})
|
|
1348
|
-
}
|
|
635
|
+
### JGroupCombo
|
|
1349
636
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
637
|
+
```vue
|
|
638
|
+
<JGroupCombo
|
|
639
|
+
v-model="selected"
|
|
640
|
+
:groups="[
|
|
1354
641
|
{
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
}
|
|
1361
|
-
|
|
642
|
+
label: '그룹 1',
|
|
643
|
+
options: [
|
|
644
|
+
{ value: '1-1', label: '옵션 1-1' },
|
|
645
|
+
{ value: '1-2', label: '옵션 1-2' },
|
|
646
|
+
],
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
label: '그룹 2',
|
|
650
|
+
options: [
|
|
651
|
+
{ value: '2-1', label: '옵션 2-1' },
|
|
652
|
+
],
|
|
653
|
+
},
|
|
654
|
+
]"
|
|
655
|
+
placeholder="그룹에서 선택하세요"
|
|
656
|
+
/>
|
|
1362
657
|
```
|
|
1363
658
|
|
|
1364
|
-
|
|
1365
|
-
- `JToast`: Toast 메시지를 표시하는 함수 (`JToast.success()`, `JToast.error()` 등)
|
|
1366
|
-
- `JToaster`: Toast를 렌더링하는 컨테이너 컴포넌트 (`<JToaster />`)
|
|
1367
|
-
- 자동 사라짐: 기본적으로 몇 초 후 자동으로 사라집니다
|
|
1368
|
-
- 여러 Toast 동시 표시 가능
|
|
1369
|
-
- 위치 설정 가능: `top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center`, `bottom-right`
|
|
1370
|
-
|
|
1371
|
-
### JFormField 사용 권장사항
|
|
659
|
+
---
|
|
1372
660
|
|
|
1373
|
-
|
|
661
|
+
### JSearchAddr
|
|
1374
662
|
|
|
1375
663
|
```vue
|
|
1376
|
-
<
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
label="이름"
|
|
1381
|
-
required
|
|
1382
|
-
error-msg="이름을 입력해주세요"
|
|
1383
|
-
v-model="name"
|
|
1384
|
-
placeholder="이름을 입력하세요"
|
|
1385
|
-
/>
|
|
1386
|
-
|
|
1387
|
-
<JFormField
|
|
1388
|
-
type="combo"
|
|
1389
|
-
label="카테고리"
|
|
1390
|
-
v-model="category"
|
|
1391
|
-
:options="categoryOptions"
|
|
1392
|
-
/>
|
|
1393
|
-
|
|
1394
|
-
<JFormField
|
|
1395
|
-
type="datepicker"
|
|
1396
|
-
label="생년월일"
|
|
1397
|
-
v-model="birthDate"
|
|
1398
|
-
/>
|
|
1399
|
-
</template>
|
|
664
|
+
<JSearchAddr
|
|
665
|
+
v-model="address"
|
|
666
|
+
@select="handleAddressSelect"
|
|
667
|
+
/>
|
|
1400
668
|
```
|
|
1401
669
|
|
|
1402
|
-
|
|
1403
|
-
- `type` prop으로 다양한 입력 컴포넌트 선택 가능 (input, textarea, combo, datepicker 등)
|
|
1404
|
-
- `label` prop으로 라벨 값을 입력하면 UI 배치시 라벨이 포함되며, 입력 필드와 자동 연결 (접근성)
|
|
1405
|
-
- `error-msg` prop으로 에러 메시지 값을 입력하면 UI 배치시 에러 메시지가 포함됨
|
|
1406
|
-
- 레이아웃 일관성 유지
|
|
670
|
+
---
|
|
1407
671
|
|
|
1408
|
-
|
|
672
|
+
## Organisms Props 레퍼런스
|
|
1409
673
|
|
|
1410
|
-
|
|
674
|
+
### JModal
|
|
1411
675
|
|
|
1412
676
|
```vue
|
|
1413
|
-
<
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
677
|
+
<JModal
|
|
678
|
+
:open="isOpen"
|
|
679
|
+
title="모달 제목"
|
|
680
|
+
description="설명"
|
|
681
|
+
size="md" <!-- 'sm'|'md'|'lg'|'xl'|'2xl'|'full' -->
|
|
682
|
+
buttonType="OkCancel" <!-- 'Ok'|'OkCancel' -->
|
|
683
|
+
confirmText="확인"
|
|
684
|
+
cancelText="취소"
|
|
685
|
+
confirmVariant="default" <!-- 'default'|'destructive'|'outline'|'secondary'|'ghost'|'link' -->
|
|
686
|
+
:confirmDisabled="false"
|
|
687
|
+
:showFormField="false"
|
|
688
|
+
formFieldType="input" <!-- 'input'|'textarea'|'checkbox'|'switch'|'combo'|'radio'|'searchCombo'|'datepicker' -->
|
|
689
|
+
formFieldLabel="필드 라벨"
|
|
690
|
+
formFieldInputType="text"
|
|
691
|
+
formFieldInputPlaceholder="입력"
|
|
692
|
+
:formFieldRequired="false"
|
|
693
|
+
:formFieldValue="''"
|
|
694
|
+
formFieldError=""
|
|
695
|
+
@confirm="handler" <!-- showFormField일 때 입력값 전달 -->
|
|
696
|
+
@cancel="handler"
|
|
697
|
+
@update:open="handler"
|
|
698
|
+
>
|
|
699
|
+
<template #body>
|
|
700
|
+
커스텀 내용
|
|
701
|
+
</template>
|
|
702
|
+
</JModal>
|
|
703
|
+
```
|
|
1420
704
|
|
|
1421
|
-
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
### JDynamicForm
|
|
708
|
+
|
|
709
|
+
```vue
|
|
710
|
+
<JDynamicForm
|
|
711
|
+
:schema="formSchema"
|
|
712
|
+
v-model="formData"
|
|
713
|
+
@submit="handler"
|
|
714
|
+
@change="handler" <!-- { field, value } -->
|
|
715
|
+
@error="handler"
|
|
716
|
+
/>
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
```ts
|
|
1422
720
|
const formSchema = {
|
|
1423
721
|
type: 'simple', // 'simple' | 'sectioned' | 'wizard'
|
|
1424
722
|
fields: [
|
|
1425
723
|
{
|
|
1426
|
-
controlName: 'name',
|
|
724
|
+
controlName: 'name', // 필드 키
|
|
1427
725
|
label: '이름',
|
|
1428
|
-
type: 'input',
|
|
1429
|
-
isRequired: true
|
|
726
|
+
type: 'input', // 'input'|'textarea'|'checkbox'|'switch'|'combo'|'radio'|'searchCombo'|'datepicker'
|
|
727
|
+
isRequired: true,
|
|
728
|
+
inputType: 'text', // type='input'일 때
|
|
729
|
+
placeholder: '이름 입력',
|
|
730
|
+
options: [], // combo/radio/searchCombo일 때
|
|
731
|
+
disabled: false,
|
|
1430
732
|
},
|
|
733
|
+
],
|
|
734
|
+
// sectioned 타입일 때
|
|
735
|
+
sections: [
|
|
1431
736
|
{
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1437
|
-
]
|
|
737
|
+
title: '섹션 제목',
|
|
738
|
+
fields: [/* fields 배열 */],
|
|
739
|
+
},
|
|
740
|
+
],
|
|
1438
741
|
}
|
|
1439
|
-
</script>
|
|
1440
742
|
```
|
|
1441
743
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
### JFormModal
|
|
747
|
+
|
|
748
|
+
```vue
|
|
749
|
+
<JFormModal
|
|
750
|
+
:open="isOpen"
|
|
751
|
+
:schema="formSchema"
|
|
752
|
+
v-model="formData"
|
|
753
|
+
title="폼 모달"
|
|
754
|
+
size="md"
|
|
755
|
+
@confirm="handler"
|
|
756
|
+
@cancel="handler"
|
|
757
|
+
/>
|
|
758
|
+
```
|
|
1446
759
|
|
|
1447
|
-
|
|
760
|
+
---
|
|
1448
761
|
|
|
1449
|
-
|
|
762
|
+
### JSearchPanel
|
|
1450
763
|
|
|
1451
|
-
|
|
764
|
+
```vue
|
|
765
|
+
<JSearchPanel
|
|
766
|
+
title="조회조건"
|
|
767
|
+
:schema="searchSchema"
|
|
768
|
+
v-model="searchData"
|
|
769
|
+
:defaultCollapsed="false"
|
|
770
|
+
:collapsible="true"
|
|
771
|
+
@submit="handleSearch"
|
|
772
|
+
@reset="handleReset"
|
|
773
|
+
/>
|
|
774
|
+
```
|
|
1452
775
|
|
|
1453
|
-
|
|
776
|
+
> 비어있지 않은 필드는 조건 뱃지로 표시, 개별 필드 초기화 지원
|
|
1454
777
|
|
|
1455
|
-
|
|
1456
|
-
- ✅ Router가 없는 프로젝트: 독립적인 컴포넌트 로딩 방식 사용
|
|
778
|
+
---
|
|
1457
779
|
|
|
1458
|
-
|
|
780
|
+
### JDynamicTabs
|
|
1459
781
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
-
|
|
782
|
+
```vue
|
|
783
|
+
<JDynamicTabs
|
|
784
|
+
ref="tabsRef"
|
|
785
|
+
:initial-tabs="initialTabs"
|
|
786
|
+
defaultActiveId="tab1"
|
|
787
|
+
:maxTabs="0" <!-- 0 = 무제한 -->
|
|
788
|
+
emptyMessage="탭을 추가해주세요."
|
|
789
|
+
@tab-add="handler"
|
|
790
|
+
@tab-change="handler"
|
|
791
|
+
@tab-close="handler"
|
|
792
|
+
>
|
|
793
|
+
<template #content-tab1>내용</template>
|
|
794
|
+
</JDynamicTabs>
|
|
795
|
+
```
|
|
1464
796
|
|
|
1465
|
-
|
|
1466
|
-
-
|
|
1467
|
-
-
|
|
1468
|
-
-
|
|
1469
|
-
-
|
|
1470
|
-
- ✅ 유지보수 용이: Router 설정만 수정하면 됨
|
|
797
|
+
**Exposed 메서드:**
|
|
798
|
+
- `tabsRef.value?.addTab({ id, label, closable, meta })`
|
|
799
|
+
- `tabsRef.value?.closeTab(id)`
|
|
800
|
+
- `tabsRef.value?.activateTab(id)`
|
|
801
|
+
- `tabsRef.value?.closeAllTabs()`
|
|
1471
802
|
|
|
1472
|
-
|
|
1473
|
-
- Router 의존적: Vue Router 필수
|
|
1474
|
-
- RouterView는 Router의 현재 경로를 사용하므로, 각 탭에 독립적인 경로를 보장하려면 Router 관리 필요
|
|
803
|
+
---
|
|
1475
804
|
|
|
1476
|
-
|
|
805
|
+
### JHeader
|
|
1477
806
|
|
|
1478
807
|
```vue
|
|
1479
|
-
<
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
#[`content-${tab.id}`]
|
|
1486
|
-
>
|
|
1487
|
-
<!-- RouterView: Router의 routes를 재사용하여 통일성 보장 -->
|
|
1488
|
-
<RouterView
|
|
1489
|
-
v-if="tab.meta?.path"
|
|
1490
|
-
:key="tab.id"
|
|
1491
|
-
/>
|
|
1492
|
-
</template>
|
|
1493
|
-
</JDynamicTabs>
|
|
1494
|
-
</template>
|
|
808
|
+
<JHeader
|
|
809
|
+
:menu-items="menuItems"
|
|
810
|
+
:user="{ name: '홍길동', avatar: 'https://example.com/avatar.jpg' }"
|
|
811
|
+
@menu-click="handler"
|
|
812
|
+
/>
|
|
813
|
+
```
|
|
1495
814
|
|
|
1496
|
-
|
|
1497
|
-
import { ref } from 'vue'
|
|
1498
|
-
import { RouterView, useRouter } from 'vue-router'
|
|
1499
|
-
import JDynamicTabs from '@/components/organisms/JDynamicTabs.vue'
|
|
1500
|
-
import type { DynamicTab } from '@/types/dynamic-tabs.types'
|
|
815
|
+
---
|
|
1501
816
|
|
|
1502
|
-
|
|
1503
|
-
const tabsRef = ref()
|
|
1504
|
-
const allTabs = ref<DynamicTab[]>([])
|
|
817
|
+
### JPageContainer
|
|
1505
818
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
meta: { path: item.path }, // 경로를 meta에 저장
|
|
1516
|
-
})
|
|
1517
|
-
|
|
1518
|
-
// allTabs에도 추가 (슬롯 이름 생성용)
|
|
1519
|
-
allTabs.value.push({
|
|
1520
|
-
id: tabId,
|
|
1521
|
-
label: item.label,
|
|
1522
|
-
meta: { path: item.path },
|
|
1523
|
-
})
|
|
1524
|
-
|
|
1525
|
-
// ⭐ Router의 경로로 이동 (Router에 이미 정의된 경로 사용)
|
|
1526
|
-
router.push({ path: item.path })
|
|
1527
|
-
}
|
|
1528
|
-
</script>
|
|
819
|
+
```vue
|
|
820
|
+
<JPageContainer>
|
|
821
|
+
<template #header>
|
|
822
|
+
<h1>페이지 제목</h1>
|
|
823
|
+
</template>
|
|
824
|
+
<template #content>
|
|
825
|
+
<p>페이지 내용</p>
|
|
826
|
+
</template>
|
|
827
|
+
</JPageContainer>
|
|
1529
828
|
```
|
|
1530
829
|
|
|
1531
|
-
|
|
830
|
+
---
|
|
1532
831
|
|
|
1533
|
-
|
|
1534
|
-
- 프로젝트에 Vue Router가 설치되어 있지 않은 경우
|
|
1535
|
-
- Router의 routes와 완전히 독립적인 경로가 필요한 경우
|
|
1536
|
-
- Router 없이도 동작해야 하는 경우
|
|
832
|
+
### JSidebarSimple
|
|
1537
833
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
-
|
|
1541
|
-
|
|
1542
|
-
|
|
834
|
+
```vue
|
|
835
|
+
<JSidebarSimple
|
|
836
|
+
:menu-items="[
|
|
837
|
+
{ id: '1', label: '홈', path: '/' },
|
|
838
|
+
{ id: '2', label: '설정', path: '/settings' },
|
|
839
|
+
]"
|
|
840
|
+
@menu-click="handler"
|
|
841
|
+
/>
|
|
842
|
+
```
|
|
1543
843
|
|
|
1544
|
-
|
|
1545
|
-
- ❌ Router의 routes와 중복 정의 필요
|
|
1546
|
-
- ❌ Route 통일성 보장 어려움
|
|
1547
|
-
- ❌ 경로 변경 시 두 곳 모두 수정 필요 (Router가 나중에 추가되는 경우)
|
|
844
|
+
---
|
|
1548
845
|
|
|
1549
|
-
|
|
846
|
+
### JSidebarAdvanced
|
|
1550
847
|
|
|
1551
848
|
```vue
|
|
1552
|
-
<
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
:is="loadComponentByPath(tab.meta.path)"
|
|
1562
|
-
:key="tab.id"
|
|
1563
|
-
/>
|
|
1564
|
-
</template>
|
|
1565
|
-
</JDynamicTabs>
|
|
1566
|
-
</template>
|
|
849
|
+
<JSidebarAdvanced
|
|
850
|
+
:menu-items="[
|
|
851
|
+
{ id: '1', label: '홈', path: '/', icon: 'house' },
|
|
852
|
+
{ id: '2', label: '설정', path: '/settings', icon: 'settings' },
|
|
853
|
+
]"
|
|
854
|
+
:favorites="['1']"
|
|
855
|
+
@menu-click="handler"
|
|
856
|
+
/>
|
|
857
|
+
```
|
|
1567
858
|
|
|
1568
|
-
|
|
1569
|
-
import { ref, defineAsyncComponent, h } from 'vue'
|
|
1570
|
-
import JDynamicTabs from '@/components/organisms/JDynamicTabs.vue'
|
|
1571
|
-
import type { DynamicTab } from '@/types/dynamic-tabs.types'
|
|
859
|
+
---
|
|
1572
860
|
|
|
1573
|
-
|
|
1574
|
-
const allTabs = ref<DynamicTab[]>([])
|
|
1575
|
-
|
|
1576
|
-
// 경로 기반 컴포넌트 로드
|
|
1577
|
-
const loadComponentByPath = (path: string) => {
|
|
1578
|
-
// 경로를 컴포넌트 경로로 변환하는 매핑
|
|
1579
|
-
// ⚠️ 주의: Router가 있다면 이 매핑은 Router의 routes와 중복될 수 있음
|
|
1580
|
-
const componentMap: Record<string, () => Promise<any>> = {
|
|
1581
|
-
'/user/info': () => import('@/pages/UserInfo.vue'),
|
|
1582
|
-
'/user/list': () => import('@/pages/UserList.vue'),
|
|
1583
|
-
'/product/detail': () => import('@/pages/ProductDetail.vue'),
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
if (componentMap[path]) {
|
|
1587
|
-
return defineAsyncComponent({
|
|
1588
|
-
loader: componentMap[path],
|
|
1589
|
-
loadingComponent: {
|
|
1590
|
-
setup: () => () => h('div', { class: 'p-6' }, '로딩 중...'),
|
|
1591
|
-
},
|
|
1592
|
-
errorComponent: {
|
|
1593
|
-
setup: () => () => h('div', { class: 'p-6 text-red-600' }, '컴포넌트 로드 실패'),
|
|
1594
|
-
},
|
|
1595
|
-
delay: 200,
|
|
1596
|
-
timeout: 3000,
|
|
1597
|
-
})
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
// 매핑되지 않은 경로는 기본 컴포넌트 표시
|
|
1601
|
-
return {
|
|
1602
|
-
setup: () => () => h('div', { class: 'p-6' }, [
|
|
1603
|
-
h('h2', { class: 'text-xl font-bold mb-4' }, '경로: ' + path),
|
|
1604
|
-
h('p', { class: 'text-muted-foreground' }, '해당 경로의 컴포넌트를 매핑에 추가하세요.'),
|
|
1605
|
-
])
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
861
|
+
## Templates Props 레퍼런스
|
|
1608
862
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
meta: { path: item.path },
|
|
1624
|
-
})
|
|
1625
|
-
}
|
|
1626
|
-
</script>
|
|
863
|
+
### JLayout
|
|
864
|
+
|
|
865
|
+
```vue
|
|
866
|
+
<JLayout>
|
|
867
|
+
<template #header>
|
|
868
|
+
<JHeader :menu-items="menuItems" />
|
|
869
|
+
</template>
|
|
870
|
+
<template #sidebar>
|
|
871
|
+
<JSidebarSimple :menu-items="menuItems" />
|
|
872
|
+
</template>
|
|
873
|
+
<template #content>
|
|
874
|
+
<RouterView />
|
|
875
|
+
</template>
|
|
876
|
+
</JLayout>
|
|
1627
877
|
```
|
|
1628
878
|
|
|
1629
|
-
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
### JLayoutSimple
|
|
1630
882
|
|
|
883
|
+
```vue
|
|
884
|
+
<JLayoutSimple
|
|
885
|
+
:menu-items="menuItems"
|
|
886
|
+
:permissions="permissions"
|
|
887
|
+
:contentScroll="true"
|
|
888
|
+
>
|
|
889
|
+
<template #content>
|
|
890
|
+
<YourPageContent />
|
|
891
|
+
</template>
|
|
892
|
+
</JLayoutSimple>
|
|
1631
893
|
```
|
|
1632
|
-
프로젝트에 Vue Router가 있나요?
|
|
1633
|
-
│
|
|
1634
|
-
├─ 예 → 방법 1 (RouterView 또는 Router의 routes 재사용)
|
|
1635
|
-
│ ✅ Route 통일성 보장
|
|
1636
|
-
│ ✅ 중복 정의 없음
|
|
1637
|
-
│ ✅ 유지보수 용이
|
|
1638
|
-
│
|
|
1639
|
-
└─ 아니오 → 방법 2 (동적 import)
|
|
1640
|
-
✅ Router 독립적
|
|
1641
|
-
✅ 가벼움
|
|
1642
|
-
⚠️ 나중에 Router 추가 시 중복 주의
|
|
1643
|
-
```
|
|
1644
894
|
|
|
1645
|
-
|
|
895
|
+
**Slots**: `header`, `sidebar`, `content`
|
|
896
|
+
|
|
897
|
+
---
|
|
1646
898
|
|
|
1647
|
-
|
|
899
|
+
### JLayoutAdvanced
|
|
1648
900
|
|
|
1649
|
-
|
|
901
|
+
```vue
|
|
902
|
+
<JLayoutAdvanced
|
|
903
|
+
:menu-items="menuItems"
|
|
904
|
+
:favorites="favorites"
|
|
905
|
+
:permissions="permissions"
|
|
906
|
+
:contentScroll="true"
|
|
907
|
+
@menu-click="handler"
|
|
908
|
+
@tab-add="handler"
|
|
909
|
+
@tab-change="handler"
|
|
910
|
+
@tab-close="handler"
|
|
911
|
+
@favorite-change="handler"
|
|
912
|
+
>
|
|
913
|
+
<template #content-tab-{id}>
|
|
914
|
+
<RouterView />
|
|
915
|
+
</template>
|
|
916
|
+
</JLayoutAdvanced>
|
|
917
|
+
```
|
|
1650
918
|
|
|
1651
|
-
|
|
1652
|
-
- Router의 routes를 재사용하여 통일성 보장
|
|
1653
|
-
- 중복 정의 제거
|
|
919
|
+
**Slots**: `header`, `sidebar`, `content`, `content-tab-{id}`
|
|
1654
920
|
|
|
1655
|
-
|
|
1656
|
-
- Router의 routes와 `componentMap`을 동기화 유지
|
|
1657
|
-
- 경로 변경 시 두 곳 모두 수정
|
|
921
|
+
---
|
|
1658
922
|
|
|
1659
|
-
|
|
923
|
+
## Toast 사용법
|
|
1660
924
|
|
|
1661
|
-
|
|
1662
|
-
|
|
925
|
+
```vue
|
|
926
|
+
<!-- 루트에 JToaster 추가 -->
|
|
927
|
+
<template>
|
|
928
|
+
<RouterView />
|
|
929
|
+
<JToaster />
|
|
930
|
+
</template>
|
|
931
|
+
```
|
|
1663
932
|
|
|
1664
|
-
|
|
933
|
+
```ts
|
|
934
|
+
import { JToast } from '@/components/atoms'
|
|
1665
935
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
936
|
+
JToast('기본 메시지')
|
|
937
|
+
JToast.success('성공')
|
|
938
|
+
JToast.error('오류')
|
|
939
|
+
JToast.info('정보')
|
|
940
|
+
JToast.warning('경고')
|
|
941
|
+
|
|
942
|
+
// 옵션
|
|
943
|
+
JToast('제목', { description: '설명', action: { label: '취소', onClick: () => {} } })
|
|
944
|
+
|
|
945
|
+
// Promise
|
|
946
|
+
JToast.promise(fetchData(), {
|
|
947
|
+
loading: '로딩 중...',
|
|
948
|
+
success: (data) => `완료: ${data.name}`,
|
|
949
|
+
error: '오류 발생',
|
|
950
|
+
})
|
|
951
|
+
```
|
|
1673
952
|
|
|
1674
953
|
---
|
|
1675
954
|
|
|
1676
|
-
##
|
|
1677
|
-
|
|
1678
|
-
|
|
955
|
+
## 상황별 추천 컴포넌트
|
|
956
|
+
|
|
957
|
+
| 상황 | 컴포넌트 |
|
|
958
|
+
|------|---------|
|
|
959
|
+
| 단일 텍스트 입력 | `JInput` |
|
|
960
|
+
| 여러 줄 텍스트 | `JTextarea` |
|
|
961
|
+
| 날짜 선택 | `JDatepicker` |
|
|
962
|
+
| 옵션 적은 단일 선택 | `JCombo` |
|
|
963
|
+
| 옵션 많은 단일 선택 | `JSearchCombo` |
|
|
964
|
+
| 다중 선택 드롭다운 | `JCombo` `:multiple="true"` |
|
|
965
|
+
| ON/OFF 토글 | `JSwitch` |
|
|
966
|
+
| 체크박스 | `JCheckbox` |
|
|
967
|
+
| 라디오 선택 | `JRadio` |
|
|
968
|
+
| 라벨+에러 포함 폼 필드 | `JFormField` |
|
|
969
|
+
| 스키마 기반 폼 생성 | `JDynamicForm` |
|
|
970
|
+
| 데이터 테이블 | `JGrid` |
|
|
971
|
+
| 모달 다이얼로그 | `JModal` |
|
|
972
|
+
| 폼 모달 | `JFormModal` |
|
|
973
|
+
| 검색 조건 패널 | `JSearchPanel` |
|
|
974
|
+
| 전역 알림 | `JToast` + `JToaster` |
|
|
975
|
+
| 탭 전환 (정적) | `JTabs` |
|
|
976
|
+
| 탭 전환 (동적) | `JDynamicTabs` |
|
|
977
|
+
| 트리 뷰 | `JTree` |
|
|
978
|
+
| 전체 레이아웃 (탭 기반) | `JLayoutAdvanced` |
|
|
979
|
+
| 전체 레이아웃 (단일 페이지) | `JLayoutSimple` |
|
|
1679
980
|
|
|
1680
|
-
|
|
981
|
+
---
|
|
1681
982
|
|
|
1682
|
-
|
|
983
|
+
## 주의사항
|
|
1683
984
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
<template #sidebar>
|
|
1691
|
-
<JSidebarSimple :menu-items="menuItems" />
|
|
1692
|
-
</template>
|
|
1693
|
-
<template #content>
|
|
1694
|
-
<RouterView />
|
|
1695
|
-
</template>
|
|
1696
|
-
</JLayout>
|
|
1697
|
-
</template>
|
|
985
|
+
1. **JCheckbox / JSwitch의 modelValue는 `'Y'` / `'N'` 문자열** (boolean 아님)
|
|
986
|
+
2. **JSearchCombo의 modelValue는 `{ value, label }` 객체** (string 아님)
|
|
987
|
+
3. **JRadio는 `options` 배열**로 항목 정의 (개별 나열 아님)
|
|
988
|
+
4. **JDatepicker의 modelValue는 `string | null`** (ISO 8601: `'YYYY-MM-DD'`)
|
|
989
|
+
5. **JGrid의 AG Grid Enterprise 기능** (`enableGrouping`, `enablePivot`, `enableExcelExport`)은 라이선스 필요
|
|
990
|
+
6. **JDynamicForm 스키마의 필드 `type`과 JFormField의 `type`은 동일한 값 사용**
|
|
1698
991
|
|
|
1699
|
-
|
|
1700
|
-
import { JLayout, JHeader, JSidebarSimple } from '@/components'
|
|
1701
|
-
</script>
|
|
1702
|
-
```
|
|
992
|
+
---
|
|
1703
993
|
|
|
1704
|
-
|
|
994
|
+
## JDynamicTabs 경로 기반 컴포넌트 로딩
|
|
1705
995
|
|
|
1706
|
-
|
|
1707
|
-
- 메뉴 클릭 시 자동 탭 생성
|
|
1708
|
-
- 검색, 즐겨찾기 기능 필요
|
|
1709
|
-
- 권한 관리 필요
|
|
996
|
+
### 방법 1: RouterView (Router 있는 경우) — 권장
|
|
1710
997
|
|
|
1711
998
|
```vue
|
|
1712
999
|
<template>
|
|
1713
|
-
<
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
:permissions="permissions"
|
|
1717
|
-
>
|
|
1718
|
-
<template #content-tab-{id}>
|
|
1719
|
-
<RouterView />
|
|
1000
|
+
<JDynamicTabs ref="tabsRef" :initial-tabs="initialTabs">
|
|
1001
|
+
<template v-for="tab in allTabs" :key="tab.id" #[`content-${tab.id}`]>
|
|
1002
|
+
<RouterView v-if="tab.meta?.path" :key="tab.id" />
|
|
1720
1003
|
</template>
|
|
1721
|
-
</
|
|
1004
|
+
</JDynamicTabs>
|
|
1722
1005
|
</template>
|
|
1723
1006
|
|
|
1724
1007
|
<script setup>
|
|
1725
|
-
import {
|
|
1008
|
+
import { ref } from 'vue'
|
|
1009
|
+
import { useRouter } from 'vue-router'
|
|
1010
|
+
|
|
1011
|
+
const router = useRouter()
|
|
1012
|
+
const tabsRef = ref()
|
|
1013
|
+
const allTabs = ref([])
|
|
1014
|
+
|
|
1015
|
+
const handleMenuClick = (item) => {
|
|
1016
|
+
const tabId = item.id || `tab-${item.path.replace(/[^a-zA-Z0-9]/g, '-')}`
|
|
1017
|
+
tabsRef.value?.addTab({ id: tabId, label: item.label, closable: true, meta: { path: item.path } })
|
|
1018
|
+
allTabs.value.push({ id: tabId, label: item.label, meta: { path: item.path } })
|
|
1019
|
+
router.push({ path: item.path })
|
|
1020
|
+
}
|
|
1726
1021
|
</script>
|
|
1727
1022
|
```
|
|
1728
1023
|
|
|
1729
|
-
###
|
|
1730
|
-
|
|
1731
|
-
- 단일 페이지 애플리케이션
|
|
1732
|
-
- 간단한 메뉴 구조
|
|
1733
|
-
- 검색 기능만 필요
|
|
1024
|
+
### 방법 2: 동적 import (Router 없는 경우)
|
|
1734
1025
|
|
|
1735
1026
|
```vue
|
|
1736
|
-
<template>
|
|
1737
|
-
<JLayoutSimple :menu-items="menuItems">
|
|
1738
|
-
<template #content>
|
|
1739
|
-
<YourPageContent />
|
|
1740
|
-
</template>
|
|
1741
|
-
</JLayoutSimple>
|
|
1742
|
-
</template>
|
|
1743
|
-
|
|
1744
1027
|
<script setup>
|
|
1745
|
-
import {
|
|
1028
|
+
import { defineAsyncComponent } from 'vue'
|
|
1029
|
+
|
|
1030
|
+
const componentMap = {
|
|
1031
|
+
'/user/info': () => import('@/pages/UserInfo.vue'),
|
|
1032
|
+
'/user/list': () => import('@/pages/UserList.vue'),
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const loadComponentByPath = (path) => {
|
|
1036
|
+
if (componentMap[path]) return defineAsyncComponent({ loader: componentMap[path] })
|
|
1037
|
+
return { setup: () => () => h('div', `경로 미등록: ${path}`) }
|
|
1038
|
+
}
|
|
1746
1039
|
</script>
|
|
1747
1040
|
```
|
|
1748
1041
|
|
|
1749
|
-
|
|
1042
|
+
**선택 기준**: Router 있으면 방법 1, 없으면 방법 2
|
|
1750
1043
|
|
|
1751
1044
|
---
|
|
1752
1045
|
|
|
1753
|
-
**문서 버전**: v1.
|
|
1046
|
+
**문서 버전**: v1.3.0
|
|
1754
1047
|
**최종 업데이트**: 2025년 12월
|
|
1755
|
-
|