# J-Component 사용 가이드

> Claude Code를 이용한 프론트엔드 개발 시 참조하는 Props 기반 컴포넌트 사용 가이드

## 설치 및 Import

```ts
// NPM 패키지 (권장) - CSS 자동 포함
import { JButton, JInput, JGrid } from '@j-solution/components'

// Standard 방식 (파일 복사)
import { JButton, JInput, JGrid } from '@/components'
```

---

## 컴포넌트 목록

### Atoms (26개)

| 컴포넌트 | 설명 |
|---------|------|
| JAvatar | 사용자 아바타 |
| JBadge | 상태 표시 배지 |
| JButton | 버튼 |
| JCheckbox | 체크박스 (Y/N 문자열) |
| JCombo | 드롭다운 선택 |
| JDatepicker | 날짜 선택기 |
| JDivider | 구분선 |
| JEditor | 마크다운 에디터 |
| JGrid | 데이터 그리드 (AG Grid) |
| JIcon | 아이콘 (Lucide) |
| JImage | 이미지 |
| JInput | 텍스트 입력 |
| JKbd | 키보드 단축키 표시 |
| JLabel | 라벨 |
| JLink | 링크 |
| JPopover | 팝오버 |
| JPreview | 마크다운/HTML 뷰어 |
| JProgress | 진행률 표시 |
| JRadio | 라디오 버튼 (options 배열) |
| JSearchCombo | 검색 가능한 드롭다운 |
| JSpinner | 로딩 스피너 |
| JSwitch | 토글 스위치 (Y/N 문자열) |
| JTextarea | 멀티라인 텍스트 입력 |
| JToaster | Toast 알림 컨테이너 |
| JTooltip | 툴팁 |
| JTree | 트리 뷰 |

### Molecules (11개)

| 컴포넌트 | 설명 |
|---------|------|
| JAccordion | 접기/펼치기 섹션 |
| JAlert | 알림 메시지 |
| JBreadcrumb | 경로 탐색 |
| JButtonGroup | 버튼 그룹 |
| JCard | 카드 |
| JContextMenu | 우클릭 메뉴 |
| JFormField | 폼 필드 래퍼 (label + error + 입력) |
| JGroupCombo | 그룹화된 드롭다운 |
| JSearchAddr | 주소 검색 (Daum API) |
| JTabs | 탭 UI |
| JTitlebar | 타이틀바 |

### Organisms (9개)

| 컴포넌트 | 설명 |
|---------|------|
| JDynamicForm | 스키마 기반 동적 폼 |
| JDynamicTabs | 동적 탭 관리 |
| JFormModal | 폼 모달 (JDynamicForm 기반) |
| JHeader | 애플리케이션 헤더 |
| JModal | 모달 다이얼로그 |
| JPageContainer | 페이지 컨테이너 |
| JSearchPanel | 검색 조건 패널 |
| JSidebarAdvanced | 고급 사이드바 |
| JSidebarSimple | 간단한 사이드바 |

### Templates (3개)

| 컴포넌트 | 설명 |
|---------|------|
| JLayout | 기본 레이아웃 (커스텀) |
| JLayoutAdvanced | 고급 레이아웃 (탭 기반) |
| JLayoutSimple | 간단한 레이아웃 (단일 페이지) |

---

## 공통 Props

대부분의 입력 컴포넌트가 지원하는 공통 props:

| Prop | 타입 | 설명 | 기본값 |
|------|------|------|--------|
| `modelValue` | 컴포넌트별 상이 | v-model 바인딩 | - |
| `placeholder` | `string` | 안내문 | `''` |
| `disabled` | `boolean` | 비활성화 | `false` |
| `readonly` | `boolean` | 읽기 전용 (Input/Textarea) | `false` |
| `required` | `boolean` | 필수 여부 | `false` |
| `id` | `string` | HTML id | - |
| `name` | `string` | form 필드명 | - |
| `class` | `string` | 추가 CSS 클래스 | - |

### 공통 이벤트

| 이벤트 | 설명 |
|--------|------|
| `update:modelValue` | v-model 업데이트 |
| `change` | 값 변경 |
| `focus` | 포커스 |
| `blur` | 포커스 해제 |

---

## Atoms Props 레퍼런스

### JButton

```vue
<JButton
  type="button"         <!-- 'button'|'submit'|'reset' -->
  :disabled="false"
  :loading="false"      <!-- true이면 스피너 표시 -->
  @click="handler"
>
  저장
</JButton>
```

---

### JInput

```vue
<JInput
  v-model="value"
  type="text"           <!-- 'text'|'email'|'password'|'number'|'tel'|'url' -->
  placeholder="입력하세요"
  :disabled="false"
  :readonly="false"
  :required="false"
  @change="handler"
/>
```

---

### JTextarea

```vue
<JTextarea
  v-model="value"
  placeholder="입력하세요"
  :rows="3"
  :disabled="false"
  :readonly="false"
/>
```

---

### JCombo

```vue
<JCombo
  v-model="selected"
  :options="[
    { value: 'opt1', label: '옵션 1' },
    { value: 'opt2', label: '옵션 2' },
  ]"
  placeholder="선택하세요"
  :multiple="false"
  :disabled="false"
/>
```

---

### JSearchCombo

> **주의**: `modelValue`는 `{ value, label }` 객체 타입

```vue
<JSearchCombo
  v-model="selectedOption"
  :options="[
    { value: '1', label: '항목 1' },
    { value: '2', label: '항목 2' },
  ]"
  placeholder="선택해주세요."
  searchPlaceholder="검색어 입력"
  emptyText="검색 결과가 없습니다."
  :multiple="false"
  :disabled="false"
/>
```

```ts
const selectedOption = ref<{ value: string | number; label: string } | undefined>()
```

---

### JCheckbox

> **주의**: `modelValue`는 `boolean`이 아닌 `'Y'` / `'N'` 문자열

```vue
<JCheckbox
  v-model="checked"
  label="동의합니다"
  :disabled="false"
  :required="false"
/>
```

```ts
const checked = ref('N')  // 'Y' 또는 'N'
```

---

### JSwitch

> **주의**: `modelValue`는 `boolean`이 아닌 `'Y'` / `'N'` 문자열

```vue
<JSwitch
  v-model="enabled"
  label="알림 받기"
  :disabled="false"
/>
```

```ts
const enabled = ref('N')  // 'Y' 또는 'N'
```

---

### JRadio

> **주의**: `options` 배열로 라디오 항목 정의 (개별 나열 아님)

```vue
<JRadio
  v-model="selected"
  :options="[
    { value: 'opt1', label: '옵션 1' },
    { value: 'opt2', label: '옵션 2' },
    { value: 'opt3', label: '옵션 3', disabled: true },
  ]"
/>
```

---

### JDatepicker

```vue
<JDatepicker
  v-model="date"
  placeholder="날짜를 선택하세요"
  :disabled="false"
/>
```

```ts
const date = ref<string | null>(null)  // ISO 8601: 'YYYY-MM-DD'
```

---

### JGrid (AG Grid)

```vue
<JGrid
  :column-defs="columnDefs"
  :row-data="rowData"
  theme="ag-theme-balham"
  :pagination="false"
  :checkbox="false"
  :summary-column="false"
  :hidden-column="false"
  :enable-grouping="false"     <!-- Enterprise -->
  :enable-pivot="false"        <!-- Enterprise -->
  :enable-excel-export="false" <!-- Enterprise -->
/>
```

```ts
const columnDefs = [
  { field: 'name', headerName: '이름' },
  { field: 'age', headerName: '나이' },
  { field: 'email', headerName: '이메일' },
]

const rowData = [
  { name: '홍길동', age: 30, email: 'hong@example.com' },
]

// ref로 접근하여 Excel 내보내기
gridRef.value?.exportToExcel()
```

---

### JIcon

```vue
<JIcon
  name="user"          <!-- Lucide 아이콘 이름 (필수) -->
  size="md"            <!-- 'sm'|'md'|'lg'|'xl' -->
  color="#333"          <!-- CSS 색상값 (선택) -->
/>
```

---

### JAvatar

```vue
<JAvatar
  src="https://example.com/avatar.jpg"
  alt="사용자 이름"
  fallback="JD"
  size="lg"
  shape="circle"
  status="online"
/>
```

---

### JBadge

```vue
<JBadge size="md">활성</JBadge>
```

---

### JProgress

```vue
<JProgress :value="75" />
```

---

### JEditor

```vue
<JEditor
  v-model="markdown"
  placeholder="마크다운을 입력하세요..."
  :height="500"
  theme="light"        <!-- 'light'|'dark' -->
  :disabled="false"
  :readonly="false"
  @save="handleSave"   <!-- Ctrl+S -->
/>
```

---

### JPreview

```vue
<JPreview
  :model-value="markdownOrHtml"
  theme="light"        <!-- 'light'|'dark' -->
/>
```

> HTML은 `<!DOCTYPE html>` 또는 `<html>` 태그로 시작하면 자동 감지, 그 외는 마크다운 처리

---

### JTooltip

```vue
<JTooltip
  content="설명 텍스트"
  side="top"           <!-- 'top'|'right'|'bottom'|'left' -->
  align="center"       <!-- 'start'|'center'|'end' -->
  :delay="200"
  maxWidth="200px"
  trigger="hover"      <!-- 'hover'|'focus'|'click'|'manual' -->
  :disabled="false"
>
  <JButton>호버하세요</JButton>
</JTooltip>
```

---

### JDivider

```vue
<!-- 기본 구분선 -->
<JDivider />

<!-- 텍스트 포함 구분선 -->
<JDivider>또는</JDivider>
```

---

### JKbd

```vue
<div>
  저장: <JKbd>Ctrl</JKbd> + <JKbd>S</JKbd>
</div>
```

---

### JLabel

```vue
<JLabel for="input-id">이름</JLabel>
<JInput id="input-id" v-model="name" />
```

---

### JLink

```vue
<JLink href="/about">소개</JLink>
```

---

### JImage

```vue
<JImage
  src="https://example.com/image.jpg"
  alt="이미지 설명"
  width="300"
  height="200"
/>
```

---

### JPopover

```vue
<JPopover>
  <template #trigger>
    <JButton>팝오버 열기</JButton>
  </template>
  <template #content>
    <div class="p-4">
      <p>팝오버 내용입니다.</p>
    </div>
  </template>
</JPopover>
```

---

### JSpinner

```vue
<JSpinner v-if="loading" />
<div v-else>콘텐츠</div>
```

---

### JTree

```vue
<JTree
  :items="treeItems"
  v-model:expanded-keys="expandedKeys"
  v-model:active-key="activeKey"
  :permissions="permissions"
  :max-depth="10"
  searchQuery=""
  @node-click="handleNodeClick"
  @expand-change="handleExpandChange"
/>
```

---

## Molecules Props 레퍼런스

### JFormField

> **권장**: 폼 필드 구성 시 JFormField 사용. `type` prop으로 입력 컴포넌트 선택.

```vue
<JFormField
  type="input"           <!-- 'input'|'textarea'|'checkbox'|'switch'|'combo'|'radio'|'searchCombo'|'datepicker' -->
  label="이름"
  description="설명 텍스트"
  error-msg="에러 메시지"
  v-model="value"
  placeholder="입력하세요"
  :required="true"
  :disabled="false"
  :readonly="false"
  orientation="vertical"  <!-- 'vertical'|'horizontal'|'responsive' -->
  labelAlign="left"       <!-- 'left'|'middle'|'right' -->
  labelWidth="8rem"
  inputType="text"        <!-- type="input"일 때: 'text'|'email'|'password'|'number' -->
  :options="[]"           <!-- type이 combo/radio/searchCombo일 때 -->
  :multiple="false"       <!-- type이 combo/searchCombo일 때 -->
  radioDirection="horizontal"  <!-- type="radio"일 때: 'horizontal'|'vertical' -->
/>
```

**장점:**
- `label` prop → 라벨 자동 배치 + 접근성 연결
- `error-msg` prop → 에러 메시지 자동 표시
- `orientation` → 레이아웃 일관성

---

### JCard

```vue
<JCard title="카드 제목" description="설명" footer="푸터">
  <p>카드 내용</p>
</JCard>
```

---

### JAlert

```vue
<JAlert
  variant="default"     <!-- 'default'|'destructive' -->
  title="알림"
  description="메시지 내용"
  buttonText="확인"
  :showFooter="true"
  @confirm="handler"
/>
```

**Slots**: `header`, default, `footer`

---

### JTabs

```vue
<JTabs
  :tabs="[
    { id: 'tab1', label: '탭 1', closable: false },
    { id: 'tab2', label: '탭 2', closable: true },
  ]"
  v-model:active-tab-id="activeTab"
  @tab-change="handler"
  @tab-close="handler"
>
  <template #content-tab1>탭 1 내용</template>
  <template #content-tab2>탭 2 내용</template>
</JTabs>
```

---

### JAccordion

```vue
<JAccordion
  :items="[
    { value: 'item-1', title: '제목 1', content: '내용 1' },
    { value: 'item-2', title: '제목 2', content: '내용 2' },
  ]"
  type="single"
  collapsible
/>
```

---

### JBreadcrumb

```vue
<JBreadcrumb :items="[
  { label: '홈', href: '/' },
  { label: '카테고리', href: '/category' },
  { label: '현재 페이지' },
]" />
```

---

### JButtonGroup

```vue
<JButtonGroup>
  <JButton>저장</JButton>
  <JButton>취소</JButton>
  <JButton>삭제</JButton>
</JButtonGroup>
```

---

### JTitlebar

```vue
<JTitlebar title="페이지 제목" description="설명">
  <template #actions>
    <JButton>액션</JButton>
  </template>
</JTitlebar>
```

---

### JContextMenu

```vue
<JContextMenu :items="menuGroups" :disabled="false" @select="handler">
  <div>우클릭 영역</div>
</JContextMenu>
```

---

### JGroupCombo

```vue
<JGroupCombo
  v-model="selected"
  :groups="[
    {
      label: '그룹 1',
      options: [
        { value: '1-1', label: '옵션 1-1' },
        { value: '1-2', label: '옵션 1-2' },
      ],
    },
    {
      label: '그룹 2',
      options: [
        { value: '2-1', label: '옵션 2-1' },
      ],
    },
  ]"
  placeholder="그룹에서 선택하세요"
/>
```

---

### JSearchAddr

```vue
<JSearchAddr
  v-model="address"
  @select="handleAddressSelect"
/>
```

---

## Organisms Props 레퍼런스

### JModal

```vue
<JModal
  :open="isOpen"
  title="모달 제목"
  description="설명"
  size="md"              <!-- 'sm'|'md'|'lg'|'xl'|'2xl'|'full' -->
  buttonType="OkCancel"  <!-- 'Ok'|'OkCancel' -->
  confirmText="확인"
  cancelText="취소"
  confirmVariant="default"  <!-- 'default'|'destructive'|'outline'|'secondary'|'ghost'|'link' -->
  :confirmDisabled="false"
  :showFormField="false"
  formFieldType="input"     <!-- 'input'|'textarea'|'checkbox'|'switch'|'combo'|'radio'|'searchCombo'|'datepicker' -->
  formFieldLabel="필드 라벨"
  formFieldInputType="text"
  formFieldInputPlaceholder="입력"
  :formFieldRequired="false"
  :formFieldValue="''"
  formFieldError=""
  @confirm="handler"     <!-- showFormField일 때 입력값 전달 -->
  @cancel="handler"
  @update:open="handler"
>
  <template #body>
    커스텀 내용
  </template>
</JModal>
```

---

### JDynamicForm

```vue
<JDynamicForm
  :schema="formSchema"
  v-model="formData"
  @submit="handler"
  @change="handler"      <!-- { field, value } -->
  @error="handler"
/>
```

```ts
const formSchema = {
  type: 'simple',  // 'simple' | 'sectioned' | 'wizard'
  fields: [
    {
      controlName: 'name',       // 필드 키
      label: '이름',
      type: 'input',             // 'input'|'textarea'|'checkbox'|'switch'|'combo'|'radio'|'searchCombo'|'datepicker'
      isRequired: true,
      inputType: 'text',         // type='input'일 때
      placeholder: '이름 입력',
      options: [],               // combo/radio/searchCombo일 때
      disabled: false,
    },
  ],
  // sectioned 타입일 때
  sections: [
    {
      title: '섹션 제목',
      fields: [/* fields 배열 */],
    },
  ],
}
```

---

### JFormModal

```vue
<JFormModal
  :open="isOpen"
  :schema="formSchema"
  v-model="formData"
  title="폼 모달"
  size="md"
  @confirm="handler"
  @cancel="handler"
/>
```

---

### JSearchPanel

```vue
<JSearchPanel
  title="조회조건"
  :schema="searchSchema"
  v-model="searchData"
  :defaultCollapsed="false"
  :collapsible="true"
  @submit="handleSearch"
  @reset="handleReset"
/>
```

> 비어있지 않은 필드는 조건 뱃지로 표시, 개별 필드 초기화 지원

---

### JDynamicTabs

```vue
<JDynamicTabs
  ref="tabsRef"
  :initial-tabs="initialTabs"
  defaultActiveId="tab1"
  :maxTabs="0"           <!-- 0 = 무제한 -->
  emptyMessage="탭을 추가해주세요."
  @tab-add="handler"
  @tab-change="handler"
  @tab-close="handler"
>
  <template #content-tab1>내용</template>
</JDynamicTabs>
```

**Exposed 메서드:**
- `tabsRef.value?.addTab({ id, label, closable, meta })`
- `tabsRef.value?.closeTab(id)`
- `tabsRef.value?.activateTab(id)`
- `tabsRef.value?.closeAllTabs()`

---

### JHeader

```vue
<JHeader
  :menu-items="menuItems"
  :user="{ name: '홍길동', avatar: 'https://example.com/avatar.jpg' }"
  @menu-click="handler"
/>
```

---

### JPageContainer

```vue
<JPageContainer>
  <template #header>
    <h1>페이지 제목</h1>
  </template>
  <template #content>
    <p>페이지 내용</p>
  </template>
</JPageContainer>
```

---

### JSidebarSimple

```vue
<JSidebarSimple
  :menu-items="[
    { id: '1', label: '홈', path: '/' },
    { id: '2', label: '설정', path: '/settings' },
  ]"
  @menu-click="handler"
/>
```

---

### JSidebarAdvanced

```vue
<JSidebarAdvanced
  :menu-items="[
    { id: '1', label: '홈', path: '/', icon: 'house' },
    { id: '2', label: '설정', path: '/settings', icon: 'settings' },
  ]"
  :favorites="['1']"
  @menu-click="handler"
/>
```

---

## Templates Props 레퍼런스

### JLayout

```vue
<JLayout>
  <template #header>
    <JHeader :menu-items="menuItems" />
  </template>
  <template #sidebar>
    <JSidebarSimple :menu-items="menuItems" />
  </template>
  <template #content>
    <RouterView />
  </template>
</JLayout>
```

---

### JLayoutSimple

```vue
<JLayoutSimple
  :menu-items="menuItems"
  :permissions="permissions"
  :contentScroll="true"
>
  <template #content>
    <YourPageContent />
  </template>
</JLayoutSimple>
```

**Slots**: `header`, `sidebar`, `content`

---

### JLayoutAdvanced

```vue
<JLayoutAdvanced
  :menu-items="menuItems"
  :favorites="favorites"
  :permissions="permissions"
  :contentScroll="true"
  @menu-click="handler"
  @tab-add="handler"
  @tab-change="handler"
  @tab-close="handler"
  @favorite-change="handler"
>
  <template #content-tab-{id}>
    <RouterView />
  </template>
</JLayoutAdvanced>
```

**Slots**: `header`, `sidebar`, `content`, `content-tab-{id}`

---

## Toast 사용법

```vue
<!-- 루트에 JToaster 추가 -->
<template>
  <RouterView />
  <JToaster />
</template>
```

```ts
import { JToast } from '@/components/atoms'

JToast('기본 메시지')
JToast.success('성공')
JToast.error('오류')
JToast.info('정보')
JToast.warning('경고')

// 옵션
JToast('제목', { description: '설명', action: { label: '취소', onClick: () => {} } })

// Promise
JToast.promise(fetchData(), {
  loading: '로딩 중...',
  success: (data) => `완료: ${data.name}`,
  error: '오류 발생',
})
```

---

## 상황별 추천 컴포넌트

| 상황 | 컴포넌트 |
|------|---------|
| 단일 텍스트 입력 | `JInput` |
| 여러 줄 텍스트 | `JTextarea` |
| 날짜 선택 | `JDatepicker` |
| 옵션 적은 단일 선택 | `JCombo` |
| 옵션 많은 단일 선택 | `JSearchCombo` |
| 다중 선택 드롭다운 | `JCombo` `:multiple="true"` |
| ON/OFF 토글 | `JSwitch` |
| 체크박스 | `JCheckbox` |
| 라디오 선택 | `JRadio` |
| 라벨+에러 포함 폼 필드 | `JFormField` |
| 스키마 기반 폼 생성 | `JDynamicForm` |
| 데이터 테이블 | `JGrid` |
| 모달 다이얼로그 | `JModal` |
| 폼 모달 | `JFormModal` |
| 검색 조건 패널 | `JSearchPanel` |
| 전역 알림 | `JToast` + `JToaster` |
| 탭 전환 (정적) | `JTabs` |
| 탭 전환 (동적) | `JDynamicTabs` |
| 트리 뷰 | `JTree` |
| 전체 레이아웃 (탭 기반) | `JLayoutAdvanced` |
| 전체 레이아웃 (단일 페이지) | `JLayoutSimple` |

---

## 주의사항

1. **JCheckbox / JSwitch의 modelValue는 `'Y'` / `'N'` 문자열** (boolean 아님)
2. **JSearchCombo의 modelValue는 `{ value, label }` 객체** (string 아님)
3. **JRadio는 `options` 배열**로 항목 정의 (개별 나열 아님)
4. **JDatepicker의 modelValue는 `string | null`** (ISO 8601: `'YYYY-MM-DD'`)
5. **JGrid의 AG Grid Enterprise 기능** (`enableGrouping`, `enablePivot`, `enableExcelExport`)은 라이선스 필요
6. **JDynamicForm 스키마의 필드 `type`과 JFormField의 `type`은 동일한 값 사용**

---

## JDynamicTabs 경로 기반 컴포넌트 로딩

### 방법 1: RouterView (Router 있는 경우) — 권장

```vue
<template>
  <JDynamicTabs ref="tabsRef" :initial-tabs="initialTabs">
    <template v-for="tab in allTabs" :key="tab.id" #[`content-${tab.id}`]>
      <RouterView v-if="tab.meta?.path" :key="tab.id" />
    </template>
  </JDynamicTabs>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const tabsRef = ref()
const allTabs = ref([])

const handleMenuClick = (item) => {
  const tabId = item.id || `tab-${item.path.replace(/[^a-zA-Z0-9]/g, '-')}`
  tabsRef.value?.addTab({ id: tabId, label: item.label, closable: true, meta: { path: item.path } })
  allTabs.value.push({ id: tabId, label: item.label, meta: { path: item.path } })
  router.push({ path: item.path })
}
</script>
```

### 방법 2: 동적 import (Router 없는 경우)

```vue
<script setup>
import { defineAsyncComponent } from 'vue'

const componentMap = {
  '/user/info': () => import('@/pages/UserInfo.vue'),
  '/user/list': () => import('@/pages/UserList.vue'),
}

const loadComponentByPath = (path) => {
  if (componentMap[path]) return defineAsyncComponent({ loader: componentMap[path] })
  return { setup: () => () => h('div', `경로 미등록: ${path}`) }
}
</script>
```

**선택 기준**: Router 있으면 방법 1, 없으면 방법 2

---

**문서 버전**: v1.3.0
**최종 업데이트**: 2025년 12월
