@opsnow-mcp/opsnow-mcp-common-ui-server 1.0.26 → 1.0.28

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.
@@ -0,0 +1,91 @@
1
+ // CoachMark 컴포넌트 예제 데이터
2
+ export const CoachMarkExamples = [
3
+ {
4
+ title: "storageKey-title-content-placement",
5
+ code: `<OpsnowCommonCoachMark
6
+ storageKey="dashboard-filter-guide"
7
+ title="필터 사용법"
8
+ content="여기서 기간과 계정을 선택해 비용을 필터링할 수 있어요."
9
+ placement="bottom"
10
+ >
11
+ <Button>필터</Button>
12
+ </OpsnowCommonCoachMark>`,
13
+ description: "기본 가이드 말풍선 예제입니다. storageKey는 가이드마다 고유하게 지정하며(localStorage 키로 사용), X로 닫으면 localStorage에 저장되어 새로고침해도 다시 뜨지 않습니다.",
14
+ },
15
+ {
16
+ title: "content-only",
17
+ code: `<OpsnowCommonCoachMark
18
+ storageKey="content-only-guide"
19
+ content="제목 없이 본문만 표시할 수도 있어요."
20
+ placement="right"
21
+ >
22
+ <Button>본문만</Button>
23
+ </OpsnowCommonCoachMark>`,
24
+ description: "title 없이 content만 넘기면 본문만 표시되는 예제입니다.",
25
+ },
26
+ {
27
+ title: "placement-top",
28
+ code: `<OpsnowCommonCoachMark
29
+ storageKey="top-guide"
30
+ title="위쪽 말풍선"
31
+ content="대상 요소 위에 표시됩니다."
32
+ placement="top"
33
+ >
34
+ <Button>placement top</Button>
35
+ </OpsnowCommonCoachMark>`,
36
+ description: "placement로 말풍선 위치를 지정하는 예제입니다 (top/bottom/left/right).",
37
+ },
38
+ {
39
+ title: "placement-left",
40
+ code: `<OpsnowCommonCoachMark
41
+ storageKey="left-guide"
42
+ title="왼쪽 말풍선"
43
+ content="대상 요소 왼쪽에 표시됩니다."
44
+ placement="left"
45
+ >
46
+ <Button>placement left</Button>
47
+ </OpsnowCommonCoachMark>`,
48
+ description: "대상 요소 왼쪽에 표시되는 예제입니다. 공간이 부족하면 MUI가 자동으로 반대쪽으로 flip 됩니다.",
49
+ },
50
+ {
51
+ title: "jsx-content-onClose",
52
+ code: `<OpsnowCommonCoachMark
53
+ storageKey="jsx-guide"
54
+ title="새 기능 안내"
55
+ content={
56
+ <>
57
+ <div>여러 줄로 구성된 안내 문구입니다.</div>
58
+ <div><b>굵은 텍스트</b>나 링크도 넣을 수 있어요.</div>
59
+ </>
60
+ }
61
+ placement="right"
62
+ onClose={() => console.log('closed')}
63
+ >
64
+ <Button>JSX content</Button>
65
+ </OpsnowCommonCoachMark>`,
66
+ description: "content에 JSX를 넘겨 여러 줄/굵은 글씨/링크 등을 표현하고, onClose로 닫힐 때 콜백을 실행하는 예제입니다.",
67
+ },
68
+ {
69
+ title: "actions-button",
70
+ code: `<OpsnowCommonCoachMark
71
+ storageKey="actions-guide"
72
+ title="새 기능 안내"
73
+ content="아래 확인 버튼을 누르면 안내가 닫힙니다."
74
+ placement="bottom"
75
+ actions={(close) => (
76
+ <Button size="small" onClick={close}>확인</Button>
77
+ )}
78
+ >
79
+ <Button>actions 버튼</Button>
80
+ </OpsnowCommonCoachMark>`,
81
+ description: "actions에 함수를 주면 close 인자로 '닫기+영구저장' 핸들러를 받아 하단 우측에 액션 버튼을 표시하는 예제입니다.",
82
+ },
83
+ {
84
+ title: "reset-dismissed-guides",
85
+ code: `Object.keys(window.localStorage)
86
+ .filter((k) => k.startsWith('opsnow-common-coach-mark:'))
87
+ .forEach((k) => window.localStorage.removeItem(k));
88
+ window.location.reload();`,
89
+ description: "닫힘 기록을 초기화하는 예제입니다. localStorage에서 'opsnow-common-coach-mark:' prefix 키를 제거하면 닫았던 가이드가 다시 표시됩니다.",
90
+ },
91
+ ];
@@ -2325,6 +2325,96 @@ export const DataGridExamples = [
2325
2325
  langCd={i18n.getLocale()}
2326
2326
  />`
2327
2327
  },
2328
+ {
2329
+ title: 'Auto Row Height (행 수 분기 + 스크롤)',
2330
+ description: 'autoHeight는 모든 행을 펼쳐 그리드가 무한정 길어집니다. 행 수가 가변적이거나 많아질 수 있는 경우(목록/검색 결과 등) 행 수 기준으로 분기해야 합니다: 10행 이하면 autoHeight로 콘텐츠에 맞게 확장하고, 10행 초과면 고정 높이 + 내부 스크롤로 전환합니다. autoHeight 셀은 행마다 높이가 달라 정확한 "10행 높이"를 픽셀로 맞출 수 없으므로 고정 픽셀값(예: 600)을 사용합니다.',
2331
+ code_props_usage: `
2332
+ import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader'
2333
+ import i18n from '@opsnow-common/opsnow-finops-common-i18n'
2334
+
2335
+ const ROW_LIMIT = 10 // 이 행 수를 넘으면 고정 높이 + 스크롤
2336
+ const SCROLL_HEIGHT = 600 // 초과 시 고정 높이(px)
2337
+
2338
+ const columnDefs = [
2339
+ { headerName: 'ID', field: 'id', flex: 1, minWidth: 80 },
2340
+ {
2341
+ headerName: 'Description',
2342
+ field: 'description',
2343
+ flex: 3,
2344
+ minWidth: 300,
2345
+ wrapText: true, // 텍스트 줄바꿈 - 필수
2346
+ autoHeight: true, // 행 높이 자동 조절 - 필수
2347
+ suppressSizeToFit: true,
2348
+ cellStyle: { alignItems: 'center' } // autoHeight 작동을 위해 필수
2349
+ },
2350
+ { headerName: 'Status', field: 'status', flex: 1, minWidth: 120 },
2351
+ ]
2352
+
2353
+ const defaultColDef = {
2354
+ wrapText: true, // 텍스트 줄바꿈 활성화
2355
+ autoHeight: true, // 셀 높이 자동 조절
2356
+ }
2357
+
2358
+ const { OpsnowCommonDataGrid } = useCommonComponents()
2359
+ const { CommonConst } = useGlobalContext()
2360
+
2361
+ // 4행 데이터 (10행 이하 → autoHeight, 스크롤 없음)
2362
+ const fewRowData = [
2363
+ { id: 1, description: 'This is a very long description text that will wrap to multiple lines when the column width is not sufficient to display all content in a single line.', status: 'Active' },
2364
+ { id: 2, description: 'Short text', status: 'Inactive' },
2365
+ { id: 3, description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', status: 'Pending' },
2366
+ { id: 4, description: 'Another example of long text content that needs multiple lines', status: 'Active' },
2367
+ ]
2368
+
2369
+ // 12행 데이터 (10행 초과 → 고정 높이 600px + 내부 스크롤)
2370
+ const manyRowData = Array.from({ length: 12 }, (_, i) => ({
2371
+ id: i + 1,
2372
+ description: 'Row ' + (i + 1) + ': long description text that wraps to multiple lines when the column is narrow, demonstrating autoHeight with scroll.',
2373
+ status: i % 2 === 0 ? 'Active' : 'Inactive',
2374
+ }))
2375
+
2376
+ const [rowData, setRowData] = useState([])
2377
+ const [status, setStatus] = useState(CommonConst.GRID_STATUS.LOADING)
2378
+
2379
+ useEffect(() => {
2380
+ const fetchData = async () => {
2381
+ try {
2382
+ await new Promise(resolve => setTimeout(resolve, 500))
2383
+ setRowData(fewRowData) // 실제로는 API 응답 데이터
2384
+ setStatus(CommonConst.GRID_STATUS.DATA_EXISTS)
2385
+ } catch (error) {
2386
+ setStatus(CommonConst.GRID_STATUS.ERROR)
2387
+ }
2388
+ }
2389
+ fetchData()
2390
+ }, [])
2391
+
2392
+ // 행 수 기준 분기 - 핵심
2393
+ const isScroll = rowData.length > ROW_LIMIT
2394
+ const gridOptions = {
2395
+ animateRows: true,
2396
+ rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
2397
+ tooltipShowDelay: 100,
2398
+ domLayout: isScroll ? 'normal' : 'autoHeight', // 10행 이하: 콘텐츠 맞춤 / 초과: 고정+스크롤
2399
+ suppressCellFocus: true,
2400
+ }
2401
+ `,
2402
+ code: `<>
2403
+ <div style={{ marginBottom: '8px', display: 'flex', gap: '8px' }}>
2404
+ <OpsnowCommonButton label="4행 (스크롤 X)" size="large" variant="outlined" color="secondary" onClick={() => setRowData(fewRowData)} />
2405
+ <OpsnowCommonButton label="12행 (스크롤 O)" size="large" variant="outlined" color="secondary" onClick={() => setRowData(manyRowData)} />
2406
+ </div>
2407
+ <OpsnowCommonDataGrid
2408
+ columnDefs={columnDefs}
2409
+ defaultColDef={defaultColDef}
2410
+ rowData={rowData}
2411
+ status={status}
2412
+ gridOptions={gridOptions}
2413
+ gridHeight={isScroll ? SCROLL_HEIGHT : undefined}
2414
+ langCd={i18n.getLocale()}
2415
+ />
2416
+ </>`
2417
+ },
2328
2418
  {
2329
2419
  title: 'API 데이터 로딩 (getAxios 사용)',
2330
2420
  description: 'API에서 데이터를 가져와 그리드에 표시하는 예제입니다. axios 직접 사용 대신 반드시 getAxios를 사용해야 합니다.',
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ // CoachMark 컴포넌트 관련 스키마 정의
3
+ export const CoachMarkSchema = z.object({
4
+ storageKey: z.string().describe("가이드마다 고유하게 지정하는 localStorage 키 (내부적으로 'opsnow-common-coach-mark:' prefix가 붙음) - X로 닫으면 localStorage에 저장되어 새로고침해도 다시 표시되지 않음"),
5
+ title: z.string().optional().describe("가이드 말풍선 제목 - 생략 시 본문만 표시"),
6
+ content: z.string().describe("가이드 말풍선 본문 - 문자열 또는 JSX 사용 가능 (여러 줄/굵은 글씨/링크 등 표현 시 JSX)"),
7
+ isJsxContent: z.boolean().optional().describe("content를 JSX로 렌더링할지 여부 - true면 content를 JSX 표현식으로 출력"),
8
+ placement: z.enum(["top", "bottom", "left", "right"]).optional().describe("말풍선 표시 위치 (top/bottom/left/right) - 공간이 부족하면 MUI가 자동으로 반대쪽으로 flip"),
9
+ onClose: z.string().optional().describe("말풍선이 닫힐 때 실행할 콜백 함수명"),
10
+ actionLabel: z.string().optional().describe("하단 우측 액션 버튼 라벨 - 지정 시 actions={(close) => <Button onClick={close}>라벨</Button>} 형태로 렌더링 (close는 '닫기+영구저장' 핸들러)"),
11
+ });
12
+ // CoachMark 컴포넌트 함수 - 배열 반환
13
+ export function createCoachMarkComponent() {
14
+ return [
15
+ {
16
+ name: "createCoachMark",
17
+ description: `코치마크(가이드 말풍선) 컴포넌트 - 신규 기능/사용법 안내용 말풍선을 대상 요소에 표시
18
+ X로 닫으면 localStorage('opsnow-common-coach-mark:' + storageKey)에 저장되어 새로고침해도 다시 표시되지 않음
19
+ **import 예시:**
20
+ \`\`\`javascript
21
+ import { useCommonComponents } from '@opsnow-common/opsnow-finops-common-ui-loader';
22
+ const { OpsnowCommonCoachMark } = useCommonComponents();
23
+ \`\`\`
24
+ `,
25
+ parameters: CoachMarkSchema,
26
+ handler: async (args) => {
27
+ const props = [];
28
+ props.push(`storageKey="${args.storageKey}"`);
29
+ if (args.title)
30
+ props.push(`title="${args.title}"`);
31
+ if (args.isJsxContent) {
32
+ props.push(`content={${args.content}}`);
33
+ }
34
+ else {
35
+ props.push(`content="${args.content}"`);
36
+ }
37
+ if (args.placement)
38
+ props.push(`placement="${args.placement}"`);
39
+ if (args.onClose)
40
+ props.push(`onClose={${args.onClose}}`);
41
+ if (args.actionLabel) {
42
+ props.push(`actions={(close) => (<Button size="small" variant="contained" onClick={close}>${args.actionLabel}</Button>)}`);
43
+ }
44
+ const code = `
45
+ <OpsnowCommonCoachMark
46
+ ${props.join("\n ")}
47
+ >
48
+ // 가이드 대상 요소 필요(Mui Button or OpsnowCommonButton 등)
49
+ </OpsnowCommonCoachMark>
50
+ `;
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: `\`\`\`jsx\n${code}\n\`\`\``,
56
+ },
57
+ ],
58
+ };
59
+ },
60
+ },
61
+ ];
62
+ }
@@ -3,6 +3,7 @@ import { IconExamples, MuiIconExamples } from "./examples/opsnow-common-icons-ex
3
3
  import { CalendarExamples } from "./examples/opsnow-common-calendar-examples-data.js";
4
4
  import { GaugeChartExamples, PieChartExamples, BarChartExamples, LineChartExamples, StackChartExamples, XyMultiChartExamples, HeatmapChartExamples, BubbleChartExamples } from "./examples/opsnow-common-chart-examples-data.js";
5
5
  import { TooltipExamples } from "./examples/opsnow-common-tooltip-examples-data.js";
6
+ import { CoachMarkExamples } from "./examples/opsnow-common-coach-mark-examples-data.js";
6
7
  import { DataGridExamples } from "./examples/opsnow-common-grid-examples-data.js";
7
8
  import { PopupExamples } from "./examples/opsnow-common-popup-examples-data.js";
8
9
  import { StepperExamples } from "./examples/opsnow-common-stepper-examples-data.js";
@@ -47,6 +48,7 @@ const EXAMPLES_MAP = {
47
48
  HeatmapChart: HeatmapChartExamples,
48
49
  BubbleChart: BubbleChartExamples,
49
50
  Tooltip: TooltipExamples,
51
+ CoachMark: CoachMarkExamples,
50
52
  DataGrid: DataGridExamples,
51
53
  Popup: PopupExamples,
52
54
  Stepper: StepperExamples,
@@ -239,6 +239,19 @@ export function createDataGridComponent() {
239
239
 
240
240
  ⚠️ defaultColDef에 wrapText, autoHeight가 없으면 텍스트가 넘칩니다!
241
241
 
242
+ **CRITICAL: 행 수가 많을 수 있으면 - 높이 분기 필수**
243
+ autoHeight(domLayout:'autoHeight')는 모든 행을 펼쳐서 그리드가 무한정 길어짐.
244
+ 행 수가 가변적이거나 많아질 수 있으면 행 수 기준으로 분기:
245
+
246
+ const ROW_LIMIT = 10;
247
+ const isScroll = rowData.length > ROW_LIMIT;
248
+ const gridOptions = {
249
+ // ... 다른 설정들
250
+ domLayout: isScroll ? 'normal' : 'autoHeight', // 10행 이하: 콘텐츠 맞춤 / 초과: 고정+스크롤
251
+ };
252
+ // 10행 초과 시에만 고정 높이 부여 (autoHeight 셀은 가변이라 픽셀 고정값 사용)
253
+ <OpsnowCommonDataGrid gridOptions={gridOptions} gridHeight={isScroll ? 600 : undefined} />
254
+
242
255
  **import:**
243
256
  \`\`\`javascript
244
257
  import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader';
package/build/index.js CHANGED
@@ -15,6 +15,7 @@ import { createTabComponent } from "./components/opsnow-common-tab.js";
15
15
  import { createGridLayoutComponent } from "./components/opsnow-common-layout.js";
16
16
  import { createTypographyComponent } from "./components/opsnow-common-typography.js";
17
17
  import { createTooltipComponent } from "./components/opsnow-common-tooltip.js";
18
+ import { createCoachMarkComponent } from "./components/opsnow-common-coach-mark.js";
18
19
  import { createSelectComponent } from "./components/opsnow-common-select.js";
19
20
  import { createPopupComponent } from "./components/opsnow-common-popup.js";
20
21
  import { createStepperComponent } from "./components/opsnow-common-stepper.js";
@@ -103,6 +104,7 @@ const componentFactories = [
103
104
  createGridLayoutComponent,
104
105
  createTypographyComponent,
105
106
  createTooltipComponent,
107
+ createCoachMarkComponent,
106
108
  createSelectComponent,
107
109
  createPopupComponent,
108
110
  createStepperComponent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsnow-mcp/opsnow-mcp-common-ui-server",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "bin": {