@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.
- package/build/components/examples/opsnow-common-forms-examples-data.js +51 -0
- package/build/components/examples/opsnow-common-layout-examples-data.js +8 -7
- package/build/components/examples/opsnow-common-select-examples-data.js +10 -10
- package/build/components/opsnow-common-calendar.js +8 -1
- package/build/components/opsnow-common-examples.js +2 -1
- package/build/components/opsnow-common-forms.js +93 -0
- package/build/components/opsnow-common-layout.js +12 -4
- package/build/components/opsnow-common-select.js +16 -12
- package/build/index.js +30 -13
- package/package.json +1 -1
|
@@ -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"와
|
|
5
|
+
description: 'Wrap="nowrap"와 간격 토큰(pagePadding·sectionGap·cardPadding·cardGap)을 적용한 4열 레이아웃 예제입니다. 색상 토큰처럼 theme.commonSpacing 을 참조하며 px 하드코딩하지 않습니다.',
|
|
6
6
|
code: `
|
|
7
|
-
<Grid container wrap="nowrap" spacing={0} sx={{ padding:
|
|
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:
|
|
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:
|
|
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:
|
|
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: '필터 여러 개 배치 (간격 규칙: 필터 행
|
|
65
|
-
code: `<Stack sx={{ flexDirection: 'row', gap:
|
|
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:
|
|
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:
|
|
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: '한 화면에 필터를 여러 개 배치할 때의 간격 규칙 예제입니다. 필터 행
|
|
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 - 필터 여러 개 배치 (간격 규칙: 필터 행
|
|
169
|
-
code: `<Stack sx={{ flexDirection: 'row', gap:
|
|
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:
|
|
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:
|
|
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: '한 화면에 필터를 여러 개 배치할 때의 간격 규칙 예제입니다. 필터 행
|
|
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:
|
|
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:
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- 간격은
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- 간격은
|
|
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:
|
|
87
|
+
**CRITICAL: Spacing Tokens (theme.commonSpacing) — DO NOT hardcode px**
|
|
88
88
|
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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:
|
|
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">
|
|
102
|
-
|
|
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
|
|
107
|
-
- Label and its filter: 8px
|
|
108
|
-
- A labeled group counts as ONE filter — still
|
|
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
|
});
|