@opsnow-mcp/opsnow-mcp-common-ui-server 1.0.32 → 1.0.34

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.
@@ -1607,6 +1607,57 @@ const handleDownload = useCallback(() => {
1607
1607
  description: '팝업에서 텍스트 콘텐츠를 표시하고 RTF 파일로 다운로드하는 예제입니다. downloadInsightAsRtf 유틸리티를 사용하면 어떤 텍스트든 RTF 문서로 내보낼 수 있습니다. [검색 키워드: RTF 다운로드, 콘텐츠 내보내기, 문서 저장, 텍스트 내보내기, 리포트 다운로드]'
1608
1608
  }
1609
1609
  ];
1610
+ export const CurrencySwitcherExamples = [
1611
+ {
1612
+ title: 'basic-currency-switcher',
1613
+ code: `const { OpsnowCommonCurrencySwitcher } = useCommonComponents()
1614
+
1615
+ const [currency, setCurrency] = useState('USD')
1616
+ const [appliedRate, setAppliedRate] = useState(1)
1617
+
1618
+ // BE 환율 API 응답을 그대로 주입 (컴포넌트는 API를 호출하지 않음)
1619
+ const rateInfo = {
1620
+ baseDate: '2026-07-08', // 적용 환율 기준일
1621
+ source: '한국수출입은행', // 출처 (선택)
1622
+ rates: { USD: 1, KRW: 1450, JPY: 156.58, VND: 25508, AED: 3.67 }, // 기준 통화(USD) 1단위당 환율
1623
+ }
1624
+
1625
+ // 환율 상세 보기 차트용 월별(또는 일별) 추이 — 과거 → 현재 순, 마지막 값 = 현재 환율
1626
+ const history = [
1627
+ { label: '25.07', rate: 1495.3 },
1628
+ { label: '26.06', rate: 1462.1 },
1629
+ { label: '26.07', rate: 1450 },
1630
+ ]
1631
+
1632
+ <OpsnowCommonCurrencySwitcher
1633
+ value={currency}
1634
+ onChange={(code, rate) => {
1635
+ setCurrency(code)
1636
+ setAppliedRate(rate) // 금액 표시 변환은 소비 측에서 rate로 처리
1637
+ }}
1638
+ rateInfo={rateInfo}
1639
+ history={history}
1640
+ />`,
1641
+ description: '통화 전환 드롭다운 + 환율 팝오버 기본 예제입니다. 환율 데이터는 컴포넌트가 조회하지 않으므로 BE 환율 API 응답을 rateInfo/history props로 주입합니다. [검색 키워드: 통화 전환, 환율, 환율 필터, 통화 필터, 통화 변환, currency, exchange rate]'
1642
+ },
1643
+ {
1644
+ title: 'custom-options-no-detail',
1645
+ code: `<OpsnowCommonCurrencySwitcher
1646
+ value={currency}
1647
+ onChange={handleCurrencyChange}
1648
+ rateInfo={rateInfo}
1649
+ currencyOptions={[
1650
+ { code: 'USD', symbol: '$' },
1651
+ { code: 'KRW', symbol: '₩', digits: 0 },
1652
+ { code: 'JPY', symbol: '¥' },
1653
+ ]}
1654
+ baseCurrency="USD"
1655
+ size="small"
1656
+ showDetail={false}
1657
+ />`,
1658
+ description: '팝오버 통화 목록을 커스텀하고 환율 상세 보기 드릴다운 없이 small 크기 트리거로 사용하는 예제입니다.'
1659
+ }
1660
+ ];
1610
1661
  export const ToggleButtonExamples = [
1611
1662
  {
1612
1663
  title: 'large-toggle-group',
@@ -2,18 +2,19 @@
2
2
  export const GridLayoutExamples = [
3
3
  {
4
4
  title: '4열 그리드 레이아웃',
5
- description: 'Wrap="nowrap"와 고정 패딩·간격을 적용한 4열 레이아웃 예제입니다.',
5
+ description: 'Wrap="nowrap"와 간격 토큰(pagePadding·sectionGap·cardPadding·cardGap)을 적용한 4열 레이아웃 예제입니다. 색상 토큰처럼 theme.commonSpacing 을 참조하며 px 하드코딩하지 않습니다.',
6
6
  code: `
7
- <Grid container wrap="nowrap" spacing={0} sx={{ padding: '27px', gap: '27px' }}>
7
+ <Grid container wrap="nowrap" spacing={0} sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}>
8
8
  <Grid item xs={12} sm={6} md={3} key={0}>
9
- <Box sx={{
9
+ <Box sx={theme => ({
10
10
  border: '1px solid #ddd',
11
11
  borderRadius: '12px',
12
- padding: 2,
12
+ padding: theme.commonSpacing.cardPadding,
13
+ gap: theme.commonSpacing.cardGap,
13
14
  display: 'flex',
14
15
  flexDirection: 'column',
15
16
  boxShadow: '2px 2px 8px 0 #4254663D',
16
- }}>
17
+ })}>
17
18
  </Box>
18
19
  </Grid>
19
20
  <!-- 총 4개 아이템 반복 -->
@@ -25,7 +26,7 @@ export const GridLayoutExamples = [
25
26
  title: '2열 그리드 레이아웃',
26
27
  description: '2개의 박스를 한 줄에 배치하는 예제입니다.',
27
28
  code: `
28
- <Grid container wrap="nowrap" spacing={0} sx={{ padding: '27px', gap: '27px' }}>
29
+ <Grid container wrap="nowrap" spacing={0} sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}>
29
30
  <Grid item xs={12} md={6} key={0}>
30
31
  <Box sx={{ /* 동일한 스타일 */ }}>
31
32
  </Box>
@@ -41,7 +42,7 @@ export const GridLayoutExamples = [
41
42
  title: '3열 그리드 레이아웃',
42
43
  description: '3개의 박스를 한 줄에 배치하는 예제입니다.',
43
44
  code: `
44
- <Grid container wrap="nowrap" spacing={0} sx={{ padding: '27px', gap: '27px' }}>
45
+ <Grid container wrap="nowrap" spacing={0} sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}>
45
46
  <Grid item xs={12} md={4} key={0}>
46
47
  <Box sx={{ /* 동일한 스타일 */ }}>
47
48
  </Box>
@@ -61,19 +61,19 @@ export const SelectExamples = [
61
61
  description: 'outlined variant, small 사이즈, label과 placeholder를 함께 사용하는 다중 선택 셀렉트 예제입니다.'
62
62
  },
63
63
  {
64
- title: '필터 여러 개 배치 (간격 규칙: 필터 행 gap 24px, 라벨-필터 gap 8px)',
65
- code: `<Stack sx={{ flexDirection: 'row', gap: '24px' }}>
64
+ title: '필터 여러 개 배치 (간격 규칙: 필터 행 filterGap(24px), 라벨-필터 filterLabelGap(8px))',
65
+ code: `<Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterGap }}>
66
66
  <OpsnowCommonSelect items={vendorItems} size="small" variant="outlined" placeholder="벤더" value={vendor} onChange={e => setVendor(e.target.value)} />
67
- <Stack sx={{ flexDirection: 'row', gap: '8px' }}>
67
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
68
68
  <OpsnowCommonTypography variant="e-m8">계정 :</OpsnowCommonTypography>
69
69
  <OpsnowCommonSelect items={accountItems} size="small" variant="outlined" placeholder="계정 선택" value={account} onChange={e => setAccount(e.target.value)} />
70
70
  </Stack>
71
- <Stack sx={{ flexDirection: 'row', gap: '8px' }}>
71
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
72
72
  <OpsnowCommonTypography variant="e-m8">리전 :</OpsnowCommonTypography>
73
73
  <OpsnowCommonSelect items={regionItems} size="small" variant="outlined" placeholder="리전 선택" value={region} onChange={e => setRegion(e.target.value)} />
74
74
  </Stack>
75
75
  </Stack>`,
76
- description: '한 화면에 필터를 여러 개 배치할 때의 간격 규칙 예제입니다. 필터 행 컨테이너의 gap은 무조건 24px, 라벨이 있는 필터는 라벨+필터를 gap 8px Stack으로 감싸며, 그 묶음과 앞 필터 사이는 동일하게 24px입니다. 간격은 spacing prop 대신 gap으로 지정합니다.'
76
+ description: '한 화면에 필터를 여러 개 배치할 때의 간격 규칙 예제입니다. 색상 토큰(theme.palette.commonPalette)과 동일하게 간격도 theme.commonSpacing 토큰을 참조합니다. 필터 행 컨테이너 gap은 theme.commonSpacing.filterGap(24px), 라벨이 있는 필터는 라벨+필터를 theme.commonSpacing.filterLabelGap(8px) Stack으로 감싸며, 그 묶음과 앞 필터 사이는 동일하게 filterGap입니다. px 하드코딩 대신 토큰으로 지정합니다.'
77
77
  }
78
78
  ];
79
79
  // Autocomplete(오토컴플리트) 컴포넌트 예제 데이터
@@ -165,22 +165,22 @@ export const AutocompleteExamples = [
165
165
  description: 'dropdownMaxWidth prop으로 확장 최대 너비(px)를 지정하는 예제입니다. 드롭다운이 입력 필드보다 넓게 확장되되 지정한 너비(360px)로 제한되고, 그보다 긴 라벨은 줄바꿈됩니다.'
166
166
  },
167
167
  {
168
- title: 'Autocomplete - 필터 여러 개 배치 (간격 규칙: 필터 행 gap 24px, 라벨-필터 gap 8px)',
169
- code: `<Stack sx={{ flexDirection: 'row', gap: '24px' }}>
168
+ title: 'Autocomplete - 필터 여러 개 배치 (간격 규칙: 필터 행 filterGap(24px), 라벨-필터 filterLabelGap(8px))',
169
+ code: `<Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterGap }}>
170
170
  <OpsnowCommonAutocomplete items={vendorItems} multiple value={vendors} onChange={setVendors} useCheckboxOption useSelectAll label="벤더" />
171
- <Stack sx={{ flexDirection: 'row', gap: '8px' }}>
171
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
172
172
  <OpsnowCommonTypography variant="e-m8">계정 :</OpsnowCommonTypography>
173
173
  <Box sx={{ minWidth: 200 }}>
174
174
  <OpsnowCommonAutocomplete items={accountItems} multiple value={accounts} onChange={setAccounts} useCheckboxOption useSelectAll />
175
175
  </Box>
176
176
  </Stack>
177
- <Stack sx={{ flexDirection: 'row', gap: '8px' }}>
177
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
178
178
  <OpsnowCommonTypography variant="e-m8">서비스 :</OpsnowCommonTypography>
179
179
  <Box sx={{ minWidth: 200 }}>
180
180
  <OpsnowCommonAutocomplete items={serviceItems} value={service} onChange={setService} />
181
181
  </Box>
182
182
  </Stack>
183
183
  </Stack>`,
184
- description: '한 화면에 필터를 여러 개 배치할 때의 간격 규칙 예제입니다. 필터 행 컨테이너의 gap은 무조건 24px, 라벨이 있는 필터는 라벨+필터를 gap 8px Stack으로 감싸며, 그 묶음과 앞 필터 사이는 동일하게 24px입니다. 간격은 spacing prop 대신 gap으로 지정하고, Autocomplete는 Box(minWidth)로 최소 폭을 확보합니다. Select와 Autocomplete를 섞어 배치할 때도 같은 규칙이 적용됩니다.'
184
+ description: '한 화면에 필터를 여러 개 배치할 때의 간격 규칙 예제입니다. 색상 토큰(theme.palette.commonPalette)과 동일하게 간격도 theme.commonSpacing 토큰을 참조합니다. 필터 행 컨테이너 gap은 theme.commonSpacing.filterGap(24px), 라벨이 있는 필터는 라벨+필터를 theme.commonSpacing.filterLabelGap(8px) Stack으로 감싸며, 그 묶음과 앞 필터 사이는 동일하게 filterGap입니다. px 하드코딩 대신 토큰으로 지정하고, Autocomplete는 Box(minWidth)로 최소 폭을 확보합니다. Select와 Autocomplete를 섞어 배치할 때도 같은 규칙이 적용됩니다.'
185
185
  }
186
186
  ];
@@ -65,7 +65,14 @@ export function createCalendarComponent() {
65
65
  name: "createCalendar",
66
66
  description: `캘린더 컴포넌트 - 날짜 선택 및 이벤트 관리
67
67
  **주의사항:**
68
- - 선택된 날짜를 표시할 때 selectedDate가 null인 경우 'None'으로 표시됩니다.
68
+ - 선택된 날짜를 표시할 때 selectedDate가 null인 경우 'None'으로 표시됩니다.
69
+ **필터로 배치할 때 간격 규칙 (다른 필터(Select/Autocomplete)와 함께 한 행에 둘 때, Select와 동일, px 하드코딩 금지):**
70
+ - 필터 행 컨테이너 gap: theme.commonSpacing.filterGap (24px)
71
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterGap }}>
72
+ - "기간 : <DatePicker>"처럼 라벨이 붙으면 라벨+DatePicker를 theme.commonSpacing.filterLabelGap (8px) Stack으로 감쌈
73
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
74
+ - 색상 토큰(theme.palette.commonPalette)과 동일하게 theme.commonSpacing 참조. 순수 CSS는 var(--opsnow-spacing-filter-gap) / var(--opsnow-spacing-filter-label-gap)
75
+
69
76
  **사용 예시:**
70
77
  \`\`\`jsx
71
78
  <p>
@@ -1,4 +1,4 @@
1
- import { ButtonExamples, BadgeExamples, TextareaExamples, SwitchExamples, ChipExamples, TextFieldExamples, AvatarExamples, CheckboxExamples, RadioGroupExamples, CollapseExamples, SliderExamples, LinkExamples, TreeViewExamples, InsightCardExamples, InsightContentExamples, ToggleButtonExamples } from "./examples/opsnow-common-forms-examples-data.js";
1
+ import { ButtonExamples, BadgeExamples, TextareaExamples, SwitchExamples, ChipExamples, TextFieldExamples, AvatarExamples, CheckboxExamples, RadioGroupExamples, CollapseExamples, SliderExamples, LinkExamples, TreeViewExamples, InsightCardExamples, InsightContentExamples, CurrencySwitcherExamples, ToggleButtonExamples } from "./examples/opsnow-common-forms-examples-data.js";
2
2
  import { IconExamples, MuiIconExamples } from "./examples/opsnow-common-icons-examples-data.js";
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";
@@ -36,6 +36,7 @@ export const EXAMPLES_MAP = {
36
36
  TreeView: TreeViewExamples,
37
37
  InsightCard: InsightCardExamples,
38
38
  InsightContent: InsightContentExamples,
39
+ CurrencySwitcher: CurrencySwitcherExamples,
39
40
  ToggleButtonGroup: ToggleButtonExamples,
40
41
  Icon: IconExamples,
41
42
  MuiIcon: MuiIconExamples,
@@ -290,6 +290,23 @@ export const InsightContentSchema = z.object({
290
290
  emptyMessage: z.string().optional().describe("insightText가 없을 때 표시할 빈 메시지"),
291
291
  sx: z.string().optional().describe("MUI sx 스타일 객체(JSX/문자열)"),
292
292
  });
293
+ // CurrencySwitcher 컴포넌트 관련 스키마 정의 (통화 전환 드롭다운 + 환율 팝오버, opsnow-common-forms >= 1.0.19)
294
+ export const CurrencySwitcherSchema = z.object({
295
+ value: z.string().describe("현재 표시 통화 코드 상태 변수명 (controlled, 예: currency)"),
296
+ onChange: z.string().describe("통화 선택/직접 입력 시 호출되는 핸들러 함수명 — (currency, rate) => void 형태, rate는 기준 통화 1단위당 적용 환율 (예: handleCurrencyChange)"),
297
+ rateInfo: z.string().optional().describe("기준일 환율 정보 객체 변수명 — { baseDate, source?, rates } 형태로 BE 환율 API 응답을 주입 (없으면 목록에 환율이 '-'로 표시, 예: rateInfo)"),
298
+ history: z.string().optional().describe("환율 상세 보기 추이 차트 데이터 배열 변수명 — Array<{ label, rate }>, 과거 → 현재 순이며 마지막 값 = 현재 환율 (예: history)"),
299
+ currencyOptions: z.string().optional().describe("팝오버 통화 목록 배열 변수명 — Array<{ code, symbol, digits? }> (기본: USD/KRW/JPY/VND/AED)"),
300
+ baseCurrency: z.string().optional().describe("변환 기준 통화 코드 (기본값: 'USD')"),
301
+ defaultTargetCurrency: z.string().optional().describe("기준 통화 선택 중일 때 상세 보기가 다룰 초기 비교 통화 코드"),
302
+ customRate: z.string().optional().describe("직접 입력 시뮬레이션 환율 상태 변수명 (number | null, controlled 모드 — 미지정 시 내부 관리)"),
303
+ onCustomRateChange: z.string().optional().describe("직접 입력 환율 변경 핸들러 함수명 — (rate) => void 형태"),
304
+ showDetail: z.boolean().optional().describe("환율 상세 보기 드릴다운 노출 여부 (기본값: true)"),
305
+ renderChart: z.string().optional().describe("추이 차트 교체 슬롯 함수명 — (history, currency) => ReactNode 형태 (기본: 내장 SVG 스파크라인)"),
306
+ labels: z.string().optional().describe("문구 개별 커스텀 객체 변수명 — Partial<CurrencySwitcherLabels>, i18n(ko/en/ja) 번역 위에 항목별로 덮어씀 ({placeholder} 템플릿 치환 지원)"),
307
+ size: z.enum(["small", "medium"]).optional().describe("트리거 크기"),
308
+ disabled: z.boolean().optional().describe("비활성화 여부"),
309
+ });
293
310
  // Forms 컴포넌트 함수 - 배열 반환
294
311
  export function createFormsComponent() {
295
312
  return [
@@ -1004,6 +1021,11 @@ export function createFormsComponent() {
1004
1021
  name: "createInsightCard",
1005
1022
  description: `InsightCard 컴포넌트 - AI 인사이트, 로딩 상태 등 지원
1006
1023
 
1024
+ **간격 규칙 (여러 카드/섹션을 배치할 때, px 하드코딩 금지):**
1025
+ - 카드(섹션 그룹) 사이 간격: theme.commonSpacing.sectionGap (24px)
1026
+ - 카드 내부 요소 간 간격: theme.commonSpacing.cardGap (16px)
1027
+ - 색상 토큰(theme.palette.commonPalette)과 동일하게 theme.commonSpacing 참조. 순수 CSS는 var(--opsnow-spacing-section-gap) / var(--opsnow-spacing-card-gap)
1028
+
1007
1029
  **import:**
1008
1030
  \`\`\`javascript
1009
1031
  import { useCommonComponents } from '@opsnow-common/opsnow-finops-common-ui-loader';
@@ -1051,6 +1073,11 @@ export function createFormsComponent() {
1051
1073
  name: "createInsightContent",
1052
1074
  description: `InsightContent 컴포넌트 - 타일 없이 AI 인사이트 콘텐츠만 표시
1053
1075
 
1076
+ **간격 규칙 (콘텐츠를 카드/섹션 안에 조합할 때, px 하드코딩 금지):**
1077
+ - 카드 내부 요소 간 간격: theme.commonSpacing.cardGap (16px)
1078
+ - 섹션(카드 그룹) 사이 간격: theme.commonSpacing.sectionGap (24px)
1079
+ - 색상 토큰(theme.palette.commonPalette)과 동일하게 theme.commonSpacing 참조
1080
+
1054
1081
  **import:**
1055
1082
  \`\`\`javascript
1056
1083
  import { useCommonComponents } from '@opsnow-common/opsnow-finops-common-ui-loader';
@@ -1083,6 +1110,72 @@ export function createFormsComponent() {
1083
1110
  ]
1084
1111
  };
1085
1112
  }
1113
+ },
1114
+ {
1115
+ name: "createCurrencySwitcher",
1116
+ description: `CurrencySwitcher 컴포넌트 - 통화 전환 드롭다운 + 환율 팝오버, 환율/통화 필터로 사용 (opsnow-common-forms >= 1.0.19)
1117
+
1118
+ 트리거에 현재 선택된 통화가 표시되고, 클릭하면 팝오버에서 적용 환율 기준일 안내와
1119
+ 통화 목록(클릭 시 즉시 적용)을 제공합니다. "환율 상세 보기" 드릴다운에서 직접 입력
1120
+ 시뮬레이션, 현재/최고/최저/변동 스탯, 환율 추이 차트를 확인할 수 있습니다.
1121
+
1122
+ **데이터 주입 규칙 (중요):**
1123
+ - 환율 데이터는 컴포넌트가 조회하지 않습니다 — BE 환율 API 응답을 rateInfo / history props로 주입하세요.
1124
+ - rateInfo: { baseDate: 'YYYY-MM-DD', source?: '출처', rates: { USD: 1, KRW: 1450, ... } } — 기준 통화 1단위당 환율
1125
+ - history: [{ label: '25.07', rate: 1495.3 }, ...] — 과거 → 현재 순, 마지막 값 = 현재 환율
1126
+ - BE API 호출 시 axios 직접 import 금지 — getAxios() 공통 axios 사용
1127
+
1128
+ **동작 규칙:**
1129
+ - 선택/직접 입력 시 onChange(code, rate)가 호출됩니다 — 금액 표시 변환은 소비 프로젝트에서 rate로 처리
1130
+ - 직접 입력 환율은 시뮬레이션 용도이며 저장되지 않습니다
1131
+ - 문구는 i18n 현재 언어(ko/en/ja)를 자동으로 따르고, labels prop으로 항목별 커스텀 가능
1132
+
1133
+ **import:**
1134
+ \`\`\`javascript
1135
+ import { useCommonComponents } from '@opsnow-common/opsnow-finops-common-ui-loader';
1136
+ const { OpsnowCommonCurrencySwitcher } = useCommonComponents();
1137
+ \`\`\``,
1138
+ parameters: CurrencySwitcherSchema,
1139
+ handler: async (args) => {
1140
+ const props = [];
1141
+ if (args.value)
1142
+ props.push(`value={${args.value}}`);
1143
+ if (args.onChange)
1144
+ props.push(`onChange={${args.onChange}}`);
1145
+ if (args.rateInfo)
1146
+ props.push(`rateInfo={${args.rateInfo}}`);
1147
+ if (args.history)
1148
+ props.push(`history={${args.history}}`);
1149
+ if (args.currencyOptions)
1150
+ props.push(`currencyOptions={${args.currencyOptions}}`);
1151
+ if (args.baseCurrency)
1152
+ props.push(`baseCurrency="${args.baseCurrency}"`);
1153
+ if (args.defaultTargetCurrency)
1154
+ props.push(`defaultTargetCurrency="${args.defaultTargetCurrency}"`);
1155
+ if (args.customRate)
1156
+ props.push(`customRate={${args.customRate}}`);
1157
+ if (args.onCustomRateChange)
1158
+ props.push(`onCustomRateChange={${args.onCustomRateChange}}`);
1159
+ if (args.showDetail !== undefined)
1160
+ props.push(`showDetail={${args.showDetail}}`);
1161
+ if (args.renderChart)
1162
+ props.push(`renderChart={${args.renderChart}}`);
1163
+ if (args.labels)
1164
+ props.push(`labels={${args.labels}}`);
1165
+ if (args.size)
1166
+ props.push(`size="${args.size}"`);
1167
+ if (args.disabled)
1168
+ props.push(`disabled`);
1169
+ const code = `<OpsnowCommonCurrencySwitcher\n ${props.join('\n ')}\n/>`;
1170
+ return {
1171
+ content: [
1172
+ {
1173
+ type: "text",
1174
+ text: `\`\`\`jsx\n${code}\n\`\`\``
1175
+ }
1176
+ ]
1177
+ };
1178
+ }
1086
1179
  }
1087
1180
  ];
1088
1181
  }
@@ -42,6 +42,13 @@ export function createGridLayoutComponent() {
42
42
  name: "createGridLayout",
43
43
  description: `Grid 및 Box 기반 레이아웃 생성
44
44
 
45
+ **간격 규칙 (px 하드코딩 금지, 색상 토큰처럼 theme.commonSpacing 참조):**
46
+ - 페이지/그리드 컨테이너 패딩: theme.commonSpacing.pagePadding (24px)
47
+ - 섹션(카드 그룹) 사이 간격: theme.commonSpacing.sectionGap (24px) — Grid container의 gap
48
+ - 카드(Box) 내부 패딩: theme.commonSpacing.cardPadding ('16px 20px')
49
+ - 카드 내부 요소 간 간격: theme.commonSpacing.cardGap (16px)
50
+ - 값이 여러 개이므로 sx는 콜백 형태로: sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}
51
+
45
52
  **import:**
46
53
  \`\`\`jsx
47
54
  import { Box, Grid } from '@mui/material';
@@ -56,20 +63,21 @@ export function createGridLayoutComponent() {
56
63
  .map(([k, v]) => `${k}={${v}}`)
57
64
  .join(" ");
58
65
  return ` <Grid item ${bpProps} key={${i}}>` +
59
- `\n <Box sx={{` +
66
+ `\n <Box sx={theme => ({` +
60
67
  `\n border: '1px solid #ddd',` +
61
68
  `\n borderRadius: '12px',` +
62
- `\n padding: 2,` +
69
+ `\n padding: theme.commonSpacing.cardPadding,` +
70
+ `\n gap: theme.commonSpacing.cardGap,` +
63
71
  `\n display: 'flex',` +
64
72
  `\n flexDirection: 'column',` +
65
73
  `\n boxShadow: '2px 2px 8px 0 #4254663D',` +
66
- `\n }}>` +
74
+ `\n })}>` +
67
75
  `\n ${item}` +
68
76
  `\n </Box>` +
69
77
  `\n </Grid>`;
70
78
  }).join("\n");
71
79
  const code = [
72
- `<Grid container wrap="nowrap" spacing={0} sx={{ padding: '27px', gap: '27px' }}>`,
80
+ `<Grid container wrap="nowrap" spacing={0} sx={theme => ({ padding: theme.commonSpacing.pagePadding, gap: theme.commonSpacing.sectionGap })}>`,
73
81
  gridItems,
74
82
  `</Grid>`,
75
83
  ].join("\n");
@@ -56,12 +56,14 @@ export function createSelectComponent() {
56
56
  - value/onChange는 단일/다중 선택에 따라 배열 또는 단일 값
57
57
 
58
58
  **필터 배치 간격 규칙 (한 화면에 필터 여러 개 배치 시 무조건 적용):**
59
- - 필터를 감싸는 행(row) 컨테이너의 gap은 무조건 24px:
60
- <Stack sx={{ flexDirection: 'row', gap: '24px' }}>
61
- - "계정 : <필터>"처럼 라벨이 붙는 경우 라벨+필터를 gap 8px Stack으로 감쌈:
62
- <Stack sx={{ flexDirection: 'row', gap: '8px' }}>
63
- - 라벨이 있는 필터도 필터와의 간격은 동일하게 24px (라벨+필터 묶음 전체가 하나의 필터로 취급됨)
64
- - 간격은 spacing prop이 아니라 gap으로 지정
59
+ 색상 토큰(theme.palette.commonPalette) 동일하게, 간격도 시맨틱 토큰 theme.commonSpacing 을 참조합니다 (px 하드코딩 금지).
60
+ - 필터를 감싸는 행(row) 컨테이너 gap theme.commonSpacing.filterGap (= 24px):
61
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterGap }}>
62
+ - "계정 : <필터>"처럼 라벨이 붙는 경우 라벨+필터를 theme.commonSpacing.filterLabelGap (= 8px) Stack으로 감쌈:
63
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
64
+ - 라벨이 있는 필터도 앞 필터와의 간격은 동일하게 filterGap (라벨+필터 묶음 전체가 하나의 필터로 취급됨)
65
+ - 간격은 spacing prop이나 px 하드코딩이 아니라 theme.commonSpacing 토큰으로 지정 (theme.commonSpacing. 까지 치면 자동완성)
66
+ - theme 컨텍스트를 못 쓰는 순수 CSS는 var(--opsnow-spacing-filter-gap) / var(--opsnow-spacing-filter-label-gap) 사용
65
67
  - 필터가 많아 화면 폭을 넘칠 수 있으면 행 컨테이너에 flexWrap: 'wrap' 추가 (선택)
66
68
 
67
69
  **import:**
@@ -109,12 +111,14 @@ export function createSelectComponent() {
109
111
  - value/onChange는 단일/다중 선택에 따라 배열 또는 단일 값
110
112
 
111
113
  **필터 배치 간격 규칙 (한 화면에 필터 여러 개 배치 시 무조건 적용):**
112
- - 필터를 감싸는 행(row) 컨테이너의 gap은 무조건 24px:
113
- <Stack sx={{ flexDirection: 'row', gap: '24px' }}>
114
- - "계정 : <필터>"처럼 라벨이 붙는 경우 라벨+필터를 gap 8px Stack으로 감쌈:
115
- <Stack sx={{ flexDirection: 'row', gap: '8px' }}>
116
- - 라벨이 있는 필터도 필터와의 간격은 동일하게 24px (라벨+필터 묶음 전체가 하나의 필터로 취급됨)
117
- - 간격은 spacing prop이 아니라 gap으로 지정
114
+ 색상 토큰(theme.palette.commonPalette) 동일하게, 간격도 시맨틱 토큰 theme.commonSpacing 을 참조합니다 (px 하드코딩 금지).
115
+ - 필터를 감싸는 행(row) 컨테이너 gap theme.commonSpacing.filterGap (= 24px):
116
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterGap }}>
117
+ - "계정 : <필터>"처럼 라벨이 붙는 경우 라벨+필터를 theme.commonSpacing.filterLabelGap (= 8px) Stack으로 감쌈:
118
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
119
+ - 라벨이 있는 필터도 앞 필터와의 간격은 동일하게 filterGap (라벨+필터 묶음 전체가 하나의 필터로 취급됨)
120
+ - 간격은 spacing prop이나 px 하드코딩이 아니라 theme.commonSpacing 토큰으로 지정 (theme.commonSpacing. 까지 치면 자동완성)
121
+ - theme 컨텍스트를 못 쓰는 순수 CSS는 var(--opsnow-spacing-filter-gap) / var(--opsnow-spacing-filter-label-gap) 사용
118
122
  - 필터가 많아 화면 폭을 넘칠 수 있으면 행 컨테이너에 flexWrap: 'wrap' 추가 (선택)
119
123
 
120
124
  **import:**
package/build/index.js CHANGED
@@ -84,28 +84,45 @@ Before implementing OpsnowCommonDataGrid, you MUST use getUIExamples tool with '
84
84
 
85
85
  Reference example: "Search + Pagination"
86
86
 
87
- **CRITICAL: Filter Layout Spacing Rules (Select/Autocomplete)**
87
+ **CRITICAL: Spacing Tokens (theme.commonSpacing) — DO NOT hardcode px**
88
88
 
89
- When placing multiple filters (Select/Autocomplete) in one row, the gap values are MANDATORY:
89
+ Just like color tokens (theme.palette.commonPalette), all layout spacing uses the semantic token theme.commonSpacing
90
+ (requires @opsnow-common/opsnow-common-style 1.0.11+, type-augmented → autocompletes, light/dark 공통):
90
91
 
91
- [CORRECT] Row container MUST have gap 24px, label+filter group MUST have gap 8px:
92
- <Stack sx={{ flexDirection: 'row', gap: '24px' }}>
92
+ filterGap 24px - gap between filters (filter row gap)
93
+ filterLabelGap 8px - gap between label and its filter
94
+ filterRowGap 12px - marginBottom below a filter row
95
+ sectionGap 24px - gap between sections (card groups)
96
+ cardGap 16px - gap between elements inside a card
97
+ cardPadding '16px 20px' - padding of a filter bar / card container
98
+ pagePadding 24px - page content padding
99
+
100
+ Use theme in sx/styled: sx={theme => ({ gap: theme.commonSpacing.filterGap })} or sx={{ gap: theme => theme.commonSpacing.filterLabelGap }}.
101
+ Pure CSS (no theme context): var(--opsnow-spacing-filter-gap), --opsnow-spacing-filter-label-gap, --opsnow-spacing-section-gap, --opsnow-spacing-card-gap, --opsnow-spacing-card-padding, --opsnow-spacing-page-padding, --opsnow-spacing-filter-row-gap.
102
+ Outside theme context (constants/utils): import { commonSpacing } from '@opsnow-common/opsnow-common-style'; commonSpacing.filterGap // '24px'.
103
+ Found hardcoded px spacing? You MUST replace it: (1) use the token for that purpose → (2) if none fits, use theme.spacing(n) for generic multiples (base 4px → theme.spacing(2) = 8px) → (3) still nothing? DO NOT keep px — ask to add a new token in the style package (one row).
104
+
105
+ --- Filter layout (Select/Autocomplete/DatePicker/DataGrid search area) ---
106
+ When placing multiple filters in one row, the gap values are MANDATORY:
107
+
108
+ [CORRECT] Row container MUST use theme.commonSpacing.filterGap (24px), label+filter group MUST use theme.commonSpacing.filterLabelGap (8px):
109
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterGap }}>
93
110
  <OpsnowCommonSelect ... />
94
- <Stack sx={{ flexDirection: 'row', gap: '8px' }}>
111
+ <Stack sx={{ flexDirection: 'row', gap: theme => theme.commonSpacing.filterLabelGap }}>
95
112
  <OpsnowCommonTypography variant="e-m8">계정 :</OpsnowCommonTypography>
96
113
  <OpsnowCommonAutocomplete ... />
97
114
  </Stack>
98
115
  </Stack>
99
116
 
100
- [FORBIDDEN] DO NOT place filters without gap:
101
- <Stack direction="row"> <- gap 누락 금지!
102
- <OpsnowCommonSelect ... />
103
- <OpsnowCommonSelect ... />
104
- </Stack>
117
+ [FORBIDDEN] DO NOT hardcode px and DO NOT place filters without gap:
118
+ <Stack direction="row" sx={{ gap: '24px' }}> <- px 하드코딩 금지, 토큰 사용!
119
+ <Stack direction="row"> <- gap 누락 금지!
105
120
 
106
- - Adjacent filters: 24px gap (gap style, NOT spacing prop)
107
- - Label and its filter: 8px gap (wrap label+filter in one Stack)
108
- - A labeled group counts as ONE filter — still 24px from the previous filter
121
+ - Adjacent filters: theme.commonSpacing.filterGap (24px) token, NOT hardcoded px or spacing prop
122
+ - Label and its filter: theme.commonSpacing.filterLabelGap (8px) wrap label+filter in one Stack
123
+ - A labeled group counts as ONE filter — still filterGap from the previous filter
124
+ - Pure CSS (no theme context): use var(--opsnow-spacing-filter-gap) / var(--opsnow-spacing-filter-label-gap)
125
+ - Requires @opsnow-common/opsnow-common-style 1.0.11+ (theme.commonSpacing is type-augmented → autocompletes)
109
126
 
110
127
  WARNING: Violating these rules will cause the UI to malfunction.`
111
128
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsnow-mcp/opsnow-mcp-common-ui-server",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "bin": {