@opsnow-mcp/opsnow-mcp-common-ui-server 1.0.39 → 1.0.41

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.
Files changed (37) hide show
  1. package/build/components/all-component-tools.js +2 -0
  2. package/build/components/data/icon-names.generated.js +341 -0
  3. package/build/components/data/icon-names.js +32 -16
  4. package/build/components/examples/opsnow-common-chart-examples-data.js +73 -78
  5. package/build/components/examples/opsnow-common-forms-examples-data.js +1 -11
  6. package/build/components/examples/opsnow-common-grid-examples-data.js +271 -20
  7. package/build/components/examples/opsnow-common-layout-examples-data.js +12 -13
  8. package/build/components/examples/opsnow-common-popup-examples-data.js +14 -28
  9. package/build/components/examples/opsnow-common-progress-examples-data.js +0 -5
  10. package/build/components/examples/opsnow-common-stepper-examples-data.js +2 -3
  11. package/build/components/examples/opsnow-common-storage-examples-data.js +23 -4
  12. package/build/components/examples/opsnow-common-tooltip-examples-data.js +1 -21
  13. package/build/components/opsnow-common-calendar.js +36 -15
  14. package/build/components/opsnow-common-chart.js +213 -132
  15. package/build/components/opsnow-common-coach-mark.js +3 -2
  16. package/build/components/opsnow-common-data-status.js +17 -11
  17. package/build/components/opsnow-common-dropdown.js +80 -0
  18. package/build/components/opsnow-common-examples.js +4 -2
  19. package/build/components/opsnow-common-file-upload.js +4 -1
  20. package/build/components/opsnow-common-forms.js +73 -36
  21. package/build/components/opsnow-common-grid.js +72 -29
  22. package/build/components/opsnow-common-icons.js +23 -39
  23. package/build/components/opsnow-common-jsx-helper.js +36 -0
  24. package/build/components/opsnow-common-layout.js +66 -25
  25. package/build/components/opsnow-common-notification.js +3 -2
  26. package/build/components/opsnow-common-pagination.js +4 -4
  27. package/build/components/opsnow-common-popup.js +12 -11
  28. package/build/components/opsnow-common-progress.js +10 -16
  29. package/build/components/opsnow-common-select.js +26 -13
  30. package/build/components/opsnow-common-stepper.js +30 -7
  31. package/build/components/opsnow-common-tab.js +5 -3
  32. package/build/components/opsnow-common-toast-popup.js +1 -0
  33. package/build/components/opsnow-common-tooltip.js +44 -46
  34. package/build/components/opsnow-common-typography.js +11 -5
  35. package/build/components/opsnow-common-validate-props.js +0 -0
  36. package/build/index.js +2 -0
  37. package/package.json +2 -1
@@ -492,9 +492,8 @@ export const DataGridExamples = [
492
492
  defaultColDef={defaultColDef}
493
493
  rowData={rowData}
494
494
  status={status} // 상태를 전달
495
- gridOptions={gridOptions}
495
+ gridOptions={{ ...gridOptions, pinnedTopRowData }} // pinned row는 gridOptions로 전달
496
496
  pagination={true}
497
- pinnedTopRowData={pinnedTopRowData} // pinned row 전달
498
497
  langCd={i18n.getLocale()}
499
498
  />`
500
499
  },
@@ -1271,8 +1270,6 @@ export const DataGridExamples = [
1271
1270
  // 달력 컴포넌트를 searchAddon에 전달
1272
1271
  searchAddon={
1273
1272
  <OpsnowCommonDatePicker
1274
- clearable={true}
1275
- placeholderText="날짜를 선택해주세요"
1276
1273
  type="date"
1277
1274
  value={time}
1278
1275
  onChange={onChangeMonth}
@@ -2309,7 +2306,7 @@ export const DataGridExamples = [
2309
2306
  animateRows: true,
2310
2307
  rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
2311
2308
  tooltipShowDelay: 100,
2312
- domLayout: 'autoHeight', // 그리드 전체 레이아웃 자동 높이
2309
+ domLayout: 'autoHeight', // 콘텐츠 높이에 맞춤, 최대 maxGridHeight(기본 600px)까지, 넘치면 내부 스크롤
2313
2310
  suppressCellFocus: true,
2314
2311
  }
2315
2312
 
@@ -2326,15 +2323,12 @@ export const DataGridExamples = [
2326
2323
  />`
2327
2324
  },
2328
2325
  {
2329
- title: 'Auto Row Height (행 수 분기 + 스크롤)',
2330
- description: 'autoHeight는 모든 행을 펼쳐 그리드가 무한정 길어집니다. 행 수가 가변적이거나 많아질 수 있는 경우(목록/검색 결과 등) 행 수 기준으로 분기해야 합니다: 10행 이하면 autoHeight로 콘텐츠에 맞게 확장하고, 10행 초과면 고정 높이 + 내부 스크롤로 전환합니다. autoHeight 셀은 행마다 높이가 달라 정확한 "10행 높이"를 픽셀로 맞출 수 없으므로 고정 픽셀값(예: 600)을 사용합니다.',
2326
+ title: 'Auto Row Height (최대 높이 + 스크롤)',
2327
+ description: 'domLayout: \'autoHeight\'는 행이 적으면 콘텐츠 높이에 맞추고, maxGridHeight(기본 600px, 헤더 42px 포함)를 넘으면 본문에 세로 스크롤이 자동으로 생깁니다(grid ≥2.0.6). 행 수로 domLayout을 분기하지 마세요 — domLayout: \'normal\'을 주면 그리드 높이가 0이 되어 화면에서 사라집니다. 상한은 maxGridHeight로 바꾸고, 모든 행을 펼치려면 unlimitedHeight를 씁니다. 고정 높이가 필요하면 domLayout 키를 빼고 gridHeight를 지정합니다.',
2331
2328
  code_props_usage: `
2332
2329
  import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader'
2333
2330
  import i18n from '@opsnow-common/opsnow-finops-common-i18n'
2334
2331
 
2335
- const ROW_LIMIT = 10 // 이 행 수를 넘으면 고정 높이 + 스크롤
2336
- const SCROLL_HEIGHT = 600 // 초과 시 고정 높이(px)
2337
-
2338
2332
  const columnDefs = [
2339
2333
  { headerName: 'ID', field: 'id', flex: 1, minWidth: 80 },
2340
2334
  {
@@ -2355,10 +2349,10 @@ export const DataGridExamples = [
2355
2349
  autoHeight: true, // 셀 높이 자동 조절
2356
2350
  }
2357
2351
 
2358
- const { OpsnowCommonDataGrid } = useCommonComponents()
2352
+ const { OpsnowCommonDataGrid, OpsnowCommonButton } = useCommonComponents()
2359
2353
  const { CommonConst } = useGlobalContext()
2360
2354
 
2361
- // 4행 데이터 (10행 이하 → autoHeight, 스크롤 없음)
2355
+ // 4행 데이터 (최대 높이 미만 → 콘텐츠 높이, 스크롤 없음)
2362
2356
  const fewRowData = [
2363
2357
  { 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
2358
  { id: 2, description: 'Short text', status: 'Inactive' },
@@ -2366,8 +2360,8 @@ export const DataGridExamples = [
2366
2360
  { id: 4, description: 'Another example of long text content that needs multiple lines', status: 'Active' },
2367
2361
  ]
2368
2362
 
2369
- // 12행 데이터 (10행 초과 → 고정 높이 600px + 내부 스크롤)
2370
- const manyRowData = Array.from({ length: 12 }, (_, i) => ({
2363
+ // 30행 데이터 (최대 높이 초과 → 600px에서 잘리고 내부 스크롤)
2364
+ const manyRowData = Array.from({ length: 30 }, (_, i) => ({
2371
2365
  id: i + 1,
2372
2366
  description: 'Row ' + (i + 1) + ': long description text that wraps to multiple lines when the column is narrow, demonstrating autoHeight with scroll.',
2373
2367
  status: i % 2 === 0 ? 'Active' : 'Inactive',
@@ -2389,20 +2383,19 @@ export const DataGridExamples = [
2389
2383
  fetchData()
2390
2384
  }, [])
2391
2385
 
2392
- // 행 수 기준 분기 - 핵심
2393
- const isScroll = rowData.length > ROW_LIMIT
2386
+ // 행 수와 무관하게 autoHeight 하나만 — 높이 상한은 그리드가 처리
2394
2387
  const gridOptions = {
2395
2388
  animateRows: true,
2396
2389
  rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
2397
2390
  tooltipShowDelay: 100,
2398
- domLayout: isScroll ? 'normal' : 'autoHeight', // 10행 이하: 콘텐츠 맞춤 / 초과: 고정+스크롤
2391
+ domLayout: 'autoHeight',
2399
2392
  suppressCellFocus: true,
2400
2393
  }
2401
2394
  `,
2402
2395
  code: `<>
2403
- <div style={{ marginBottom: '8px', display: 'flex', gap: '8px' }}>
2396
+ <div style={{ marginBottom: 'var(--opsnow-spacing-filter-label-gap)', display: 'flex', gap: 'var(--opsnow-spacing-filter-label-gap)' }}>
2404
2397
  <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)} />
2398
+ <OpsnowCommonButton label="30행 (스크롤 O)" size="large" variant="outlined" color="secondary" onClick={() => setRowData(manyRowData)} />
2406
2399
  </div>
2407
2400
  <OpsnowCommonDataGrid
2408
2401
  columnDefs={columnDefs}
@@ -2410,7 +2403,7 @@ export const DataGridExamples = [
2410
2403
  rowData={rowData}
2411
2404
  status={status}
2412
2405
  gridOptions={gridOptions}
2413
- gridHeight={isScroll ? SCROLL_HEIGHT : undefined}
2406
+ maxGridHeight={600} // 기본값과 같음. 상한을 바꾸려면 이 값만 조정 (모두 펼치려면 unlimitedHeight)
2414
2407
  langCd={i18n.getLocale()}
2415
2408
  />
2416
2409
  </>`
@@ -2476,4 +2469,262 @@ export const DataGridExamples = [
2476
2469
  langCd={i18n.getLocale()}
2477
2470
  />`
2478
2471
  },
2472
+ {
2473
+ title: '셀 단위 로딩 (Partial Loading — refreshCells) + 컬럼 전체 / 실패·재시도',
2474
+ description: '"이 셀만 갱신 중" 같은 부분 로딩 예제입니다. status={GRID_STATUS.LOADING}은 그리드 전체를 가리므로 부분 로딩에 쓸 수 없습니다 — status는 DATA_EXISTS로 두고, 로딩 중인 셀 키를 useRef(Set)에 담은 뒤 ref API refreshCells({ rowNodes, columns, force: true })로 해당 셀만 다시 그립니다. 값이 그대로라 force: true가 필수입니다. rowNodes를 생략하면 그 컬럼의 모든 셀이 대상(컬럼 단위)이며, 실패도 같은 패턴(별도 Set)으로 처리해 인라인 "재시도" 링크를 띄웁니다. 인디케이터로 셀 콘텐츠를 치환하지 말고 visibility: hidden으로 남기고 겹쳐야(CellProgress) autoHeight 행 높이가 유지됩니다.',
2475
+ code_props_usage: `
2476
+ import { useCallback, useMemo, useRef, useState } from 'react'
2477
+ import Box from '@mui/material/Box'
2478
+ import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader'
2479
+ import i18n from '@opsnow-common/opsnow-finops-common-i18n'
2480
+
2481
+ // 원래 콘텐츠를 hidden 으로 남겨 행 높이를 유지하고 인디케이터를 겹친다 (모듈 최상위에 둘 것)
2482
+ const CellProgress = ({ children }) => {
2483
+ const { OpsnowCommonLoading } = useCommonComponents()
2484
+ return (
2485
+ <Box sx={{ position: 'relative', display: 'inline-flex', alignItems: 'center', minWidth: '18px', minHeight: '20px' }}>
2486
+ <Box aria-hidden sx={{ visibility: 'hidden' }}>{children}</Box>
2487
+ <Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center' }}>
2488
+ <OpsnowCommonLoading size={18} />
2489
+ </Box>
2490
+ </Box>
2491
+ )
2492
+ }
2493
+
2494
+ const formatCost = (value) => '$' + Number(value).toLocaleString()
2495
+ const fakeApi = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
2496
+
2497
+ const { OpsnowCommonDataGrid, OpsnowCommonButton, OpsnowCommonLink } = useCommonComponents()
2498
+ const { CommonConst } = useGlobalContext()
2499
+ const gridRef = useRef(null)
2500
+
2501
+ const [rowData] = useState([
2502
+ { id: 'r1', account: 'opsnow-prod', service: 'Amazon EC2', cost: 12480 },
2503
+ { id: 'r2', account: 'opsnow-dev', service: 'Amazon S3', cost: 3120 },
2504
+ { id: 'r3', account: 'opsnow-stg', service: 'Amazon RDS', cost: 8640 },
2505
+ ])
2506
+ const status = CommonConst.GRID_STATUS.DATA_EXISTS
2507
+
2508
+ // state 가 아니라 ref — state 면 그리드 전체 리렌더로 스크롤이 튄다
2509
+ const loadingCellsRef = useRef(new Set())
2510
+ const errorCellsRef = useRef(new Set())
2511
+
2512
+ const refreshCostCells = useCallback((nodes) => {
2513
+ gridRef.current?.refreshCells({
2514
+ rowNodes: nodes,
2515
+ columns: ['cost'],
2516
+ force: true, // 값이 그대로라 필수
2517
+ })
2518
+ }, [])
2519
+
2520
+ const fetchCost = useCallback(async (node) => {
2521
+ const key = node.data.id + ':cost'
2522
+ loadingCellsRef.current.add(key)
2523
+ errorCellsRef.current.delete(key)
2524
+ refreshCostCells([node])
2525
+ try {
2526
+ await fakeApi(1200)
2527
+ if (node.data.id === 'r3' && !node.data.retried) { // 데모: 첫 시도만 실패
2528
+ node.data.retried = true
2529
+ throw new Error('cost fetch failed')
2530
+ }
2531
+ node.setDataValue('cost', Math.round(node.data.cost * (0.9 + Math.random() * 0.3)))
2532
+ } catch {
2533
+ errorCellsRef.current.add(key)
2534
+ } finally {
2535
+ loadingCellsRef.current.delete(key)
2536
+ refreshCostCells([node])
2537
+ }
2538
+ }, [refreshCostCells])
2539
+
2540
+ const refreshCostColumn = useCallback(async () => {
2541
+ const nodes = []
2542
+ gridRef.current?.forEachNode((node) => nodes.push(node))
2543
+ nodes.forEach((node) => loadingCellsRef.current.add(node.data.id + ':cost'))
2544
+ gridRef.current?.refreshCells({ columns: ['cost'], force: true }) // rowNodes 생략 = 컬럼 단위
2545
+ await Promise.all(nodes.map((node) => fetchCost(node)))
2546
+ }, [fetchCost])
2547
+
2548
+ const CostCell = useCallback((params) => {
2549
+ const value = <span>{formatCost(params.value)}</span>
2550
+ const key = params.data.id + ':' + params.colDef.field
2551
+ if (loadingCellsRef.current.has(key)) return <CellProgress>{value}</CellProgress>
2552
+ if (errorCellsRef.current.has(key)) {
2553
+ // 버튼을 넣으면 행 높이가 바뀜 — 인라인 링크로
2554
+ return (
2555
+ <Box component="span" sx={(theme) => ({ display: 'inline-flex', alignItems: 'center', gap: '6px', color: theme.palette.error.main })}>
2556
+ 불러오기 실패
2557
+ <OpsnowCommonLink color="primary" sx={{ cursor: 'pointer' }} onClick={() => fetchCost(params.node)}>재시도</OpsnowCommonLink>
2558
+ </Box>
2559
+ )
2560
+ }
2561
+ return value
2562
+ }, [fetchCost])
2563
+
2564
+ const RecalcButtonCell = useCallback((params) => (
2565
+ <OpsnowCommonButton label="재계산" size="small" variant="outlined" color="secondary" onClick={() => fetchCost(params.node)} />
2566
+ ), [fetchCost])
2567
+
2568
+ // useMemo 로 고정 — 매 렌더마다 새로 만들면 그리드가 통째로 다시 그려진다
2569
+ const columnDefs = useMemo(() => [
2570
+ { headerName: '계정', field: 'account', flex: 1, minWidth: 140 },
2571
+ { headerName: '서비스', field: 'service', flex: 1, minWidth: 160 },
2572
+ { headerName: '비용', field: 'cost', flex: 1, minWidth: 200, cellRenderer: CostCell },
2573
+ { headerName: '', colId: 'action', width: 110, sortable: false, cellRenderer: RecalcButtonCell },
2574
+ ], [CostCell, RecalcButtonCell])
2575
+
2576
+ const gridOptions = useMemo(() => ({
2577
+ domLayout: 'autoHeight',
2578
+ suppressCellFocus: true,
2579
+ animateRows: true,
2580
+ }), [])
2581
+ `,
2582
+ code: `<>
2583
+ <div style={{ marginBottom: '16px' }}>
2584
+ <OpsnowCommonButton label="비용 컬럼 새로고침 (컬럼 전체)" size="large" variant="outlined" color="secondary" onClick={refreshCostColumn} />
2585
+ </div>
2586
+ <OpsnowCommonDataGrid
2587
+ ref={gridRef}
2588
+ langCd={i18n.getLocale()}
2589
+ columnDefs={columnDefs}
2590
+ rowData={rowData}
2591
+ status={status}
2592
+ gridOptions={gridOptions}
2593
+ />
2594
+ </>`
2595
+ },
2596
+ {
2597
+ title: '행 단위 로딩 (Partial Loading — rowClassRules + redrawRows) + 선택 행 일괄 로딩',
2598
+ description: '"이 행만 동기화 중" 같은 행 단위 부분 로딩 예제입니다. 로딩 중인 행 id를 useRef(Set)에 담고, gridOptions.rowClassRules로 그 행에 클래스를 붙여 dim + 클릭 차단하며, ref API redrawRows({ rowNodes })로 해당 행만 다시 그리면 rowClassRules가 재평가됩니다. 인디케이터는 컬럼 렌더러를 감싸 행 전체 컬럼(버튼 컬럼 포함)에 적용해야 로딩 중 행 높이가 유지됩니다. 여러 행(체크박스 선택)을 켤 때는 rowNodes 배열로 묶어 redrawRows를 한 번만 호출하고, 끌 때는 응답이 온 행부터 개별 해제합니다. 선택 노드는 forEachNode + node.isSelected()로 수집하고(getSelectedNodes는 ref API에 없음), onSelectionChanged는 gridOptions가 아니라 top-level prop으로 넘겨야 호출됩니다(인자 = 선택된 노드 배열).',
2599
+ code_props_usage: `
2600
+ import { useCallback, useMemo, useRef, useState } from 'react'
2601
+ import Box from '@mui/material/Box'
2602
+ import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader'
2603
+ import i18n from '@opsnow-common/opsnow-finops-common-i18n'
2604
+
2605
+ // '셀 단위 로딩' 예제와 동일
2606
+ const CellProgress = ({ children }) => {
2607
+ const { OpsnowCommonLoading } = useCommonComponents()
2608
+ return (
2609
+ <Box sx={{ position: 'relative', display: 'inline-flex', alignItems: 'center', minWidth: '18px', minHeight: '20px' }}>
2610
+ <Box aria-hidden sx={{ visibility: 'hidden' }}>{children}</Box>
2611
+ <Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center' }}>
2612
+ <OpsnowCommonLoading size={18} />
2613
+ </Box>
2614
+ </Box>
2615
+ )
2616
+ }
2617
+
2618
+ const fakeApi = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
2619
+ const nowLabel = () => new Date().toLocaleTimeString()
2620
+
2621
+ const { OpsnowCommonDataGrid, OpsnowCommonButton } = useCommonComponents()
2622
+ const { CommonConst } = useGlobalContext()
2623
+ const gridRef = useRef(null)
2624
+
2625
+ const [rowData] = useState([
2626
+ { id: 'r1', account: 'opsnow-prod', service: 'Amazon EC2', status: '동기화 완료' },
2627
+ { id: 'r2', account: 'opsnow-dev', service: 'Amazon S3', status: '동기화 완료' },
2628
+ { id: 'r3', account: 'opsnow-stg', service: 'Amazon RDS', status: '동기화 완료' },
2629
+ ])
2630
+ const status = CommonConst.GRID_STATUS.DATA_EXISTS
2631
+
2632
+ const loadingRowsRef = useRef(new Set())
2633
+ const [selectedCount, setSelectedCount] = useState(0)
2634
+ const [running, setRunning] = useState(false)
2635
+
2636
+ const setRowLoading = useCallback((node, loading) => {
2637
+ if (loading) loadingRowsRef.current.add(node.data.id)
2638
+ else loadingRowsRef.current.delete(node.data.id)
2639
+ gridRef.current?.redrawRows({ rowNodes: [node] }) // rowClassRules 재평가
2640
+ }, [])
2641
+
2642
+ const refreshRow = useCallback(async (node) => {
2643
+ setRowLoading(node, true)
2644
+ try {
2645
+ await fakeApi(1800)
2646
+ node.setDataValue('status', '동기화 ' + nowLabel())
2647
+ } finally {
2648
+ setRowLoading(node, false)
2649
+ }
2650
+ }, [setRowLoading])
2651
+
2652
+ const syncSelectedRows = useCallback(async () => {
2653
+ const nodes = []
2654
+ gridRef.current?.forEachNode((node) => { if (node.isSelected()) nodes.push(node) }) // getSelectedNodes 없음
2655
+ if (nodes.length === 0) return
2656
+ setRunning(true)
2657
+ nodes.forEach((node) => loadingRowsRef.current.add(node.data.id))
2658
+ gridRef.current?.redrawRows({ rowNodes: nodes }) // 묶어서 한 번만
2659
+ await Promise.all(nodes.map(async (node, index) => {
2660
+ await fakeApi(1000 + index * 600)
2661
+ node.setDataValue('status', '동기화 ' + nowLabel())
2662
+ loadingRowsRef.current.delete(node.data.id)
2663
+ gridRef.current?.redrawRows({ rowNodes: [node] })
2664
+ }))
2665
+ setRunning(false)
2666
+ }, [])
2667
+
2668
+ // defaultColDef 는 마운트 시점에 굳으므로 컬럼 렌더러를 감싼다
2669
+ const withRowLoading = useCallback((Renderer) => {
2670
+ const Wrapped = (params) => {
2671
+ const content = Renderer ? <Renderer {...params} /> : <span>{params.value}</span>
2672
+ return loadingRowsRef.current.has(params.data?.id) ? <CellProgress>{content}</CellProgress> : content
2673
+ }
2674
+ return Wrapped
2675
+ }, [])
2676
+
2677
+ const RefreshButtonCell = useCallback((params) => (
2678
+ <OpsnowCommonButton label="행 새로고침" size="small" variant="outlined" color="secondary" onClick={() => refreshRow(params.node)} />
2679
+ ), [refreshRow])
2680
+
2681
+ // 감싼 렌더러는 useMemo 안에서 한 번만 만든다
2682
+ const columnDefs = useMemo(() => {
2683
+ const valueCell = withRowLoading()
2684
+ return [
2685
+ { headerName: '계정', field: 'account', flex: 1, minWidth: 140, cellRenderer: valueCell },
2686
+ { headerName: '서비스', field: 'service', flex: 1, minWidth: 160, cellRenderer: valueCell },
2687
+ { headerName: '상태', field: 'status', flex: 1, minWidth: 180, cellRenderer: valueCell },
2688
+ // 버튼 컬럼도 감싸야 행 높이가 유지된다
2689
+ { headerName: '', colId: 'action', width: 140, sortable: false, cellRenderer: withRowLoading(RefreshButtonCell) },
2690
+ ]
2691
+ }, [withRowLoading, RefreshButtonCell])
2692
+
2693
+ const gridOptions = useMemo(() => ({
2694
+ domLayout: 'autoHeight',
2695
+ suppressCellFocus: true,
2696
+ animateRows: true,
2697
+ rowSelection: { mode: 'multiRow', checkboxes: true, headerCheckbox: true, enableClickSelection: false },
2698
+ rowClassRules: {
2699
+ 'row-loading': (params) => loadingRowsRef.current.has(params.data?.id),
2700
+ },
2701
+ }), [])
2702
+
2703
+ // gridOptions 가 아니라 top-level prop — 인자는 선택된 노드 배열
2704
+ const handleSelectionChanged = useCallback((selectedNodes) => {
2705
+ setSelectedCount(selectedNodes.length)
2706
+ }, [])
2707
+ `,
2708
+ code: `<Box sx={{ '& .ag-row.row-loading': { opacity: 0.45, pointerEvents: 'none' } }}>
2709
+ <div style={{ marginBottom: '16px' }}>
2710
+ <OpsnowCommonButton
2711
+ label={'선택 행 동기화 (' + selectedCount + ')'}
2712
+ size="large"
2713
+ variant="outlined"
2714
+ color="secondary"
2715
+ disabled={selectedCount === 0 || running}
2716
+ onClick={syncSelectedRows}
2717
+ />
2718
+ </div>
2719
+ <OpsnowCommonDataGrid
2720
+ ref={gridRef}
2721
+ langCd={i18n.getLocale()}
2722
+ columnDefs={columnDefs}
2723
+ rowData={rowData}
2724
+ status={status}
2725
+ gridOptions={gridOptions}
2726
+ onSelectionChanged={handleSelectionChanged}
2727
+ />
2728
+ </Box>`
2729
+ },
2479
2730
  ];
@@ -2,10 +2,10 @@
2
2
  export const GridLayoutExamples = [
3
3
  {
4
4
  title: '4열 그리드 레이아웃',
5
- description: 'Wrap="nowrap"와 간격 토큰(pagePadding·sectionGap·cardPadding·cardGap)을 적용한 4열 레이아웃 예제입니다. 색상 토큰처럼 theme.commonSpacing 을 참조하며 px 하드코딩하지 않습니다.',
5
+ description: 'MUI 9 Grid size 문법과 간격 토큰(pagePadding·sectionGap·cardPadding·cardGap)을 적용한 4열 레이아웃 예제입니다. 섹션 간격은 Grid spacing에 토큰 CSS 변수로 지정합니다(sx gap은 size 폭 계산에 들어가지 않아 줄바꿈이 깨짐). 색상 토큰처럼 theme.commonSpacing 을 참조하며 px 하드코딩하지 않습니다.',
6
6
  code: `
7
- <Grid container wrap="nowrap" spacing={0} sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}>
8
- <Grid item xs={12} sm={6} md={3} key={0}>
7
+ <Grid container spacing="var(--opsnow-spacing-section-gap)" sx={theme => ({ padding: theme.commonSpacing.pagePadding })}>
8
+ <Grid size={{ xs: 12, sm: 6, md: 3 }} key={0}>
9
9
  <Box sx={theme => ({
10
10
  border: '1px solid #ddd',
11
11
  borderRadius: '12px',
@@ -17,8 +17,7 @@ export const GridLayoutExamples = [
17
17
  })}>
18
18
  </Box>
19
19
  </Grid>
20
- <!-- 총 4개 아이템 반복 -->
21
- <!-- ... -->
20
+ {/* 총 4개 아이템 반복 (sm: 2열, md: 4열) */}
22
21
  </Grid>
23
22
  `,
24
23
  },
@@ -26,12 +25,12 @@ export const GridLayoutExamples = [
26
25
  title: '2열 그리드 레이아웃',
27
26
  description: '2개의 박스를 한 줄에 배치하는 예제입니다.',
28
27
  code: `
29
- <Grid container wrap="nowrap" spacing={0} sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}>
30
- <Grid item xs={12} md={6} key={0}>
28
+ <Grid container spacing="var(--opsnow-spacing-section-gap)" sx={theme => ({ padding: theme.commonSpacing.pagePadding })}>
29
+ <Grid size={{ xs: 12, md: 6 }} key={0}>
31
30
  <Box sx={{ /* 동일한 스타일 */ }}>
32
31
  </Box>
33
32
  </Grid>
34
- <Grid item xs={12} md={6} key={1}>
33
+ <Grid size={{ xs: 12, md: 6 }} key={1}>
35
34
  <Box sx={{ /* 동일한 스타일 */ }}>
36
35
  </Box>
37
36
  </Grid>
@@ -42,16 +41,16 @@ export const GridLayoutExamples = [
42
41
  title: '3열 그리드 레이아웃',
43
42
  description: '3개의 박스를 한 줄에 배치하는 예제입니다.',
44
43
  code: `
45
- <Grid container wrap="nowrap" spacing={0} sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}>
46
- <Grid item xs={12} md={4} key={0}>
44
+ <Grid container spacing="var(--opsnow-spacing-section-gap)" sx={theme => ({ padding: theme.commonSpacing.pagePadding })}>
45
+ <Grid size={{ xs: 12, md: 4 }} key={0}>
47
46
  <Box sx={{ /* 동일한 스타일 */ }}>
48
47
  </Box>
49
48
  </Grid>
50
- <Grid item xs={12} md={4} key={1}>
49
+ <Grid size={{ xs: 12, md: 4 }} key={1}>
51
50
  <Box sx={{ /* 동일한 스타일 */ }}>
52
51
  </Box>
53
52
  </Grid>
54
- <Grid item xs={12} md={4} key={2}>
53
+ <Grid size={{ xs: 12, md: 4 }} key={2}>
55
54
  <Box sx={{ /* 동일한 스타일 */ }}>
56
55
  </Box>
57
56
  </Grid>
@@ -61,7 +60,7 @@ export const GridLayoutExamples = [
61
60
  {
62
61
  title: 'Tile with Checkbox UI',
63
62
  description: 'Tile 내부에 체크박스와 텍스트가 포함된 2열 레이아웃 예제입니다.',
64
- code: `<Grid container spacing={3} sx={{ marginTop: '20px' }}><Grid item xs={12} md={6} sx={{ display: 'flex' }}><OpsnowCommonTile sx={{ width: '100%' }}><OpsnowCommonTile.Content><Stack direction="row" alignItems="flex-start" spacing={2}><OpsnowCommonCheckbox name="organizationScope" checked={checkboxStates.organizationScope} onChange={handleCheckboxChange} color="primary" /><Stack spacing={1}><OpsnowCommonTypography variant="e-b10">Use Organization Scope(Cost Allocation)</OpsnowCommonTypography><OpsnowCommonTypography variant="e-r11" color="commonPalette.gray.600">The same scope defined in Organization Settings is used as the baseline for Budget and KPI tracking.</OpsnowCommonTypography></Stack></Stack></OpsnowCommonTile.Content></OpsnowCommonTile></Grid><Grid item xs={12} md={6} sx={{ display: 'flex' }}><OpsnowCommonTile sx={{ width: '100%' }}><OpsnowCommonTile.Content><Stack direction="row" alignItems="flex-start" spacing={2}><OpsnowCommonCheckbox name="customizeScope" checked={checkboxStates.customizeScope} onChange={handleCheckboxChange} color="primary" /><Stack spacing={1}><OpsnowCommonTypography variant="e-b10">Customize Scope</OpsnowCommonTypography><OpsnowCommonTypography variant="e-r11" color="commonPalette.gray.600">Apply only to selected dimensions, such as specific services (e.g., EC2)</OpsnowCommonTypography></Stack></Stack></OpsnowCommonTile.Content></OpsnowCommonTile></Grid></Grid>`,
63
+ code: `<Grid container spacing={3} sx={{ marginTop: '20px' }}><Grid size={{ xs: 12, md: 6 }} sx={{ display: 'flex' }}><OpsnowCommonTile><OpsnowCommonTile.Content><Stack direction="row" alignItems="flex-start" spacing={2}><OpsnowCommonCheckbox name="organizationScope" checked={checkboxStates.organizationScope} onChange={handleCheckboxChange} color="primary" /><Stack spacing={1}><OpsnowCommonTypography variant="e-b10">Use Organization Scope(Cost Allocation)</OpsnowCommonTypography><OpsnowCommonTypography variant="e-r11" color="commonPalette.gray.600">The same scope defined in Organization Settings is used as the baseline for Budget and KPI tracking.</OpsnowCommonTypography></Stack></Stack></OpsnowCommonTile.Content></OpsnowCommonTile></Grid><Grid size={{ xs: 12, md: 6 }} sx={{ display: 'flex' }}><OpsnowCommonTile><OpsnowCommonTile.Content><Stack direction="row" alignItems="flex-start" spacing={2}><OpsnowCommonCheckbox name="customizeScope" checked={checkboxStates.customizeScope} onChange={handleCheckboxChange} color="primary" /><Stack spacing={1}><OpsnowCommonTypography variant="e-b10">Customize Scope</OpsnowCommonTypography><OpsnowCommonTypography variant="e-r11" color="commonPalette.gray.600">Apply only to selected dimensions, such as specific services (e.g., EC2)</OpsnowCommonTypography></Stack></Stack></OpsnowCommonTile.Content></OpsnowCommonTile></Grid></Grid>`,
65
64
  },
66
65
  ];
67
66
  export const MegaFilterExamples = [
@@ -23,27 +23,27 @@ export const PopupExamples = [
23
23
  },
24
24
  {
25
25
  title: 'Footer 버튼 커스텀',
26
- code: `<OpsnowCommonPopup title="Footer Custom" open={true} onClose={handleClose} footer={[{ text: '확인', type: 'primary', action: 'onConfirm' }, { text: '취소', type: 'default', action: 'onCancel' }]}>
27
- <p>Popup content here</p>
28
- </OpsnowCommonPopup>`,
29
- description: 'footer prop을 통해 하단 버튼을 커스텀하는 예제입니다.'
30
- },
31
- {
32
- title: 'Closable/MaskClosable 옵션',
33
- code: `<OpsnowCommonPopup title="Closable Popup" open={true} onClose={handleClose} closable={false} maskClosable={false}>
26
+ code: `<OpsnowCommonPopup
27
+ title="Footer Custom"
28
+ open={true}
29
+ onClose={handleClose}
30
+ confirmButton={{ text: '확인', variant: 'contained' }}
31
+ cancelButton={{ text: '취소', variant: 'outlined' }}
32
+ onConfirm={handleConfirm}
33
+ >
34
34
  <p>Popup content here</p>
35
35
  </OpsnowCommonPopup>`,
36
- description: '닫기 버튼과 마스크 클릭으로 닫기 비활성화 예제입니다.'
36
+ description: '하단 버튼은 confirmButton/cancelButton 객체({ text, variant, disabled, color })로 커스텀합니다. 취소 버튼은 기본 표시(useFooterCancelBtn 기본 true)이며 onClose가 호출됩니다. footer prop은 없습니다.'
37
37
  },
38
38
  {
39
- title: 'Position(위치) 지정',
40
- code: `<OpsnowCommonPopup title="Bottom Popup" open={true} onClose={handleClose} position="bottom">
39
+ title: '헤더 닫기(X) 버튼 숨김',
40
+ code: `<OpsnowCommonPopup title="No Close Button" open={true} onClose={handleClose} hideHeaderClose>
41
41
  <p>Popup content here</p>
42
42
  </OpsnowCommonPopup>`,
43
- description: '팝업 위치를 bottom으로 지정한 예제입니다.'
43
+ description: 'hideHeaderClose로 헤더의 닫기(X) 버튼을 숨깁니다. closable/maskClosable prop은 없습니다.'
44
44
  },
45
45
  {
46
- title: 'Wing 타입, 작은 사이즈 + cancel/confirm + disableEscapeKeyDown',
46
+ title: 'Wing 타입, 작은 사이즈 + cancel/confirm',
47
47
  code: `<OpsnowCommonPopup
48
48
  title="Example Popup"
49
49
  open={true}
@@ -52,11 +52,10 @@ export const PopupExamples = [
52
52
  cancelButton={{ variant: 'outlined' }}
53
53
  confirmButton={{ color: 'primary', text: 'Save' }}
54
54
  type="wing"
55
- disableEscapeKeyDown
56
55
  >
57
56
  <p>Popup content here</p>
58
57
  </OpsnowCommonPopup>`,
59
- description: 'wing 타입, 작은 사이즈(sm)에 cancel/confirm 버튼과 disableEscapeKeyDown 옵션을 적용한 예제입니다.'
58
+ description: 'wing 타입, 작은 사이즈(sm)에 cancel/confirm 버튼을 적용한 예제입니다. (ESC·배경 클릭으로 닫히지 않게 하는 옵션은 없음 — onClose가 reason을 받지 않음)'
60
59
  },
61
60
  {
62
61
  title: 'Wing 타입, md, Special Title & Footer Action Button',
@@ -188,18 +187,5 @@ export const PopupExamples = [
188
187
  <p>Popup content here</p>
189
188
  </OpsnowCommonPopup>`,
190
189
  description: 'bottom 타입에서 cancelButton과 confirmButton을 사용하는 예제입니다.'
191
- },
192
- {
193
- title: '고정 크기(width/height)',
194
- code: `<OpsnowCommonPopup
195
- title="Size Popup"
196
- open={true}
197
- onClose={handleClose}
198
- width={400}
199
- height={300}
200
- >
201
- <p>Popup content here</p>
202
- </OpsnowCommonPopup>`,
203
- description: '팝업의 너비와 높이를 고정(px)으로 지정하는 예제입니다.'
204
190
  }
205
191
  ];
@@ -5,11 +5,6 @@ export const ProgressExamples = [
5
5
  code: `<OpsnowCommonProgress value={50} />`,
6
6
  description: "50% 진행률을 보여주는 기본 Progress 예제입니다.",
7
7
  },
8
- {
9
- title: "value-size",
10
- code: `<OpsnowCommonProgress value={50} size={32}/>`,
11
- description: "사이즈가 32이고 value가 50인 기본 Progress 예제입니다.",
12
- },
13
8
  {
14
9
  title: "value-inherit",
15
10
  code: `<OpsnowCommonProgress value={75} color="inherit" />`,
@@ -62,12 +62,11 @@ export const StepperExamples = [
62
62
  description: 'Simplified stepper view without connectors.'
63
63
  },
64
64
  {
65
- title: 'Right-aligned Stepper with Custom Connector',
65
+ title: 'Right-aligned Stepper',
66
66
  code: `<OpsnowCommonStepper
67
67
  steps={steps}
68
68
  activeStep={activeStepRight}
69
69
  align="right"
70
- connector={<CustomConnector />}
71
70
  >
72
71
  <OpsnowCommonStepContent stepIndex={0}>
73
72
  <TextField label="Input 1" variant="outlined" fullWidth value={input1} onChange={(e) => setInput1(e.target.value)} />
@@ -79,7 +78,7 @@ export const StepperExamples = [
79
78
  <Typography>This is the content for Step 3</Typography>
80
79
  </OpsnowCommonStepContent>
81
80
  </OpsnowCommonStepper>`,
82
- description: 'Stepper aligned to the right with a custom connector style.'
81
+ description: 'Stepper aligned to the right. (커넥터는 내부에서 고정되어 connector prop으로 바꿀 수 없음)'
83
82
  },
84
83
  {
85
84
  title: 'Vertical Non-linear Stepper',
@@ -1,8 +1,27 @@
1
- // Storage 컴포넌트 예제 데이터
1
+ // Storage 예제 데이터 (컴포넌트가 아니라 useGlobalContext()의 스토리지 API)
2
2
  export const StorageExamples = [
3
3
  {
4
- title: '기본 스토리지',
5
- code: `<OpsnowCommonStorage />`,
6
- description: '기본 스토리지 예제입니다.'
4
+ title: '로컬 스토리지 읽기/쓰기',
5
+ code: `const { commonUILocalStorage } = useGlobalContext()
6
+
7
+ // 저장: 문자열·숫자·객체 모두 가능 (객체는 내부에서 직렬화)
8
+ commonUILocalStorage.setItem('cost-filter', { vendor: 'AWS', period: '3M' })
9
+
10
+ // 읽기: 파싱된 값 반환, 없으면 null
11
+ const saved = commonUILocalStorage.getItem('cost-filter')
12
+
13
+ // 삭제 / 접두사로 일괄 삭제
14
+ commonUILocalStorage.removeItem('cost-filter')
15
+ commonUILocalStorage.clear('cost-')`,
16
+ description: 'localStorage 래퍼입니다. import { useGlobalContext } from \'@opsnow-common/opsnow-finops-common-ui-loader\' 후 함수 컴포넌트 안에서 꺼내 씁니다. 그 밖에 getItemsByPrefix(prefix), getKeysByPrefix(prefix), setItemByApi(...)(API 응답 캐시), 암호화 저장(setItem의 secretKey 인자)을 지원합니다.'
17
+ },
18
+ {
19
+ title: '세션 스토리지 읽기/쓰기',
20
+ code: `const { commonUISessionStorage } = useGlobalContext()
21
+
22
+ commonUISessionStorage.setItem('wizard-step', 2)
23
+ const step = commonUISessionStorage.getItem('wizard-step') ?? 0
24
+ commonUISessionStorage.removeItem('wizard-step')`,
25
+ description: 'sessionStorage 래퍼입니다. getItem, setItem, removeItem, clear 4개만 제공합니다 (clear는 접두사 인자 없음).'
7
26
  }
8
27
  ];
@@ -52,29 +52,9 @@ export const TooltipExamples = [
52
52
  code: `<OpsnowCommonTooltip title="툴팁 내용" placement="bottom" enterDelay={1000}><Button>Hover me</Button></OpsnowCommonTooltip>`,
53
53
  description: "툴팁 열림 지연 시간 예제입니다.",
54
54
  },
55
- {
56
- title: "titleProps.title-bottom-enterTouchDelay",
57
- code: `<OpsnowCommonTooltip title="툴팁 내용" placement="bottom" enterTouchDelay={1000}><Button>Hover me</Button></OpsnowCommonTooltip>`,
58
- description: "툴팁 열림 터치 지연 시간 예제입니다.",
59
- },
60
- {
61
- title: "titleProps.title-bottom-enterNextDelay",
62
- code: `<OpsnowCommonTooltip title="툴팁 내용" placement="bottom" enterNextDelay={1000}><Button>Hover me</Button></OpsnowCommonTooltip>`,
63
- description: "툴팁 열림 다음 지연 시간 예제입니다.",
64
- },
65
55
  {
66
56
  title: "titleProps.title-bottom-leaveDelay",
67
57
  code: `<OpsnowCommonTooltip title="툴팁 내용" placement="bottom" leaveDelay={1000}><Button>Hover me</Button></OpsnowCommonTooltip>`,
68
58
  description: "툴팁 닫힘 지연 시간 예제입니다.",
69
- },
70
- {
71
- title: "titleProps.title-bottom-leaveNextDelay",
72
- code: `<OpsnowCommonTooltip title="툴팁 내용" placement="bottom" leaveNextDelay={1000}><Button>Hover me</Button></OpsnowCommonTooltip>`,
73
- description: "툴팁 닫힘 다음 지연 시간 예제입니다.",
74
- },
75
- {
76
- title: "titleProps.title-bottom-green",
77
- code: `<OpsnowCommonTooltip title="툴팁 내용" placement="bottom" color={'green'}><Button>Hover me</Button></OpsnowCommonTooltip>`,
78
- description: "툴팁 색상 예제입니다.",
79
- },
59
+ }
80
60
  ];