@opsnow-mcp/opsnow-mcp-common-ui-server 1.0.28 → 1.0.29
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/all-component-tools.js +45 -0
- package/build/components/examples/opsnow-common-select-examples-data.js +21 -0
- package/build/components/opsnow-common-calendar.js +11 -12
- package/build/components/opsnow-common-chart.js +433 -17
- package/build/components/opsnow-common-coach-mark.js +9 -2
- package/build/components/opsnow-common-data-status.js +0 -5
- package/build/components/opsnow-common-examples.js +113 -29
- package/build/components/opsnow-common-file-upload.js +1 -0
- package/build/components/opsnow-common-forms.js +82 -3
- package/build/components/opsnow-common-icons.js +2 -5
- package/build/components/opsnow-common-notification.js +6 -4
- package/build/components/opsnow-common-pagination.js +9 -6
- package/build/components/opsnow-common-popup.js +30 -9
- package/build/components/opsnow-common-select.js +23 -7
- package/build/components/opsnow-common-spec-tools.js +166 -0
- package/build/components/opsnow-common-stepper.js +5 -8
- package/build/components/opsnow-common-toast-popup.js +29 -17
- package/build/components/opsnow-common-tooltip.js +0 -6
- package/build/components/opsnow-common-typography.js +6 -0
- package/build/components/opsnow-common-validate-props.js +0 -0
- package/build/index.js +33 -51
- package/build/validate-props-cli.js +71 -0
- package/package.json +4 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// optional/default/describe 래퍼를 벗겨가며 타입·기본값·설명 수집
|
|
3
|
+
// 중첩 z.object는 'options.color' 형태의 dotted name으로 펼침
|
|
4
|
+
function inspectField(name, field, depth = 0) {
|
|
5
|
+
let required = true;
|
|
6
|
+
let defaultValue = "-";
|
|
7
|
+
let description = "";
|
|
8
|
+
let current = field;
|
|
9
|
+
while (true) {
|
|
10
|
+
if (!description && current.description)
|
|
11
|
+
description = current.description;
|
|
12
|
+
const def = current._def;
|
|
13
|
+
if (def.typeName === z.ZodFirstPartyTypeKind.ZodOptional) {
|
|
14
|
+
required = false;
|
|
15
|
+
current = def.innerType;
|
|
16
|
+
}
|
|
17
|
+
else if (def.typeName === z.ZodFirstPartyTypeKind.ZodDefault) {
|
|
18
|
+
required = false;
|
|
19
|
+
try {
|
|
20
|
+
const dv = def.defaultValue();
|
|
21
|
+
defaultValue = typeof dv === "string" ? `"${dv}"` : JSON.stringify(dv);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
defaultValue = "-";
|
|
25
|
+
}
|
|
26
|
+
current = def.innerType;
|
|
27
|
+
}
|
|
28
|
+
else if (def.typeName === z.ZodFirstPartyTypeKind.ZodNullable) {
|
|
29
|
+
current = def.innerType;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const row = {
|
|
36
|
+
name,
|
|
37
|
+
type: typeToString(current),
|
|
38
|
+
required,
|
|
39
|
+
defaultValue,
|
|
40
|
+
description: description.split("\n")[0].trim(),
|
|
41
|
+
};
|
|
42
|
+
// 중첩 객체는 2단계까지만 펼침 (DataGrid 같은 대형 스키마 폭주 방지)
|
|
43
|
+
if (current._def.typeName === z.ZodFirstPartyTypeKind.ZodObject && depth < 2) {
|
|
44
|
+
const children = Object.entries(current.shape).flatMap(([key, child]) => inspectField(`${name}.${key}`, child, depth + 1));
|
|
45
|
+
return [row, ...children];
|
|
46
|
+
}
|
|
47
|
+
return [row];
|
|
48
|
+
}
|
|
49
|
+
function typeToString(t) {
|
|
50
|
+
const def = t._def;
|
|
51
|
+
switch (def.typeName) {
|
|
52
|
+
case z.ZodFirstPartyTypeKind.ZodString: return "string";
|
|
53
|
+
case z.ZodFirstPartyTypeKind.ZodNumber: return "number";
|
|
54
|
+
case z.ZodFirstPartyTypeKind.ZodBoolean: return "boolean";
|
|
55
|
+
case z.ZodFirstPartyTypeKind.ZodEnum: {
|
|
56
|
+
const vals = def.values.map(v => `"${v}"`);
|
|
57
|
+
return vals.length > 6 ? vals.slice(0, 6).join(" \\| ") + " ..." : vals.join(" \\| ");
|
|
58
|
+
}
|
|
59
|
+
case z.ZodFirstPartyTypeKind.ZodArray:
|
|
60
|
+
return `${typeToString(def.type)}[]`;
|
|
61
|
+
case z.ZodFirstPartyTypeKind.ZodObject: return "object";
|
|
62
|
+
case z.ZodFirstPartyTypeKind.ZodUnion:
|
|
63
|
+
return def.options.map(typeToString).join(" \\| ");
|
|
64
|
+
case z.ZodFirstPartyTypeKind.ZodRecord: return "Record";
|
|
65
|
+
case z.ZodFirstPartyTypeKind.ZodAny: return "any";
|
|
66
|
+
default: return "unknown";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ============= 이름 매칭 =============
|
|
70
|
+
// 'datagrid', 'data-grid', 'DATAGRID' → 'DataGrid'
|
|
71
|
+
function normalize(s) {
|
|
72
|
+
return s.toLowerCase().replace(/[-_\s]/g, "");
|
|
73
|
+
}
|
|
74
|
+
// createButton → Button
|
|
75
|
+
function toolToComponentName(toolName) {
|
|
76
|
+
return toolName.replace(/^create/, "");
|
|
77
|
+
}
|
|
78
|
+
// ============= 툴 팩토리 =============
|
|
79
|
+
/**
|
|
80
|
+
* createXxx 툴 정의 목록에서 listComponents / getComponentSpec 툴 2개를 생성한다.
|
|
81
|
+
* @param componentTools createXxx 팩토리들이 반환한 툴 정의 (getUIExamples 제외)
|
|
82
|
+
* @param exampleKeys getUIExamples가 지원하는 uiType 키 목록 (예제 안내용)
|
|
83
|
+
*/
|
|
84
|
+
export function createSpecTools(componentTools, exampleKeys) {
|
|
85
|
+
// 컴포넌트명 → 툴 정의
|
|
86
|
+
const byName = new Map();
|
|
87
|
+
for (const tool of componentTools) {
|
|
88
|
+
byName.set(toolToComponentName(tool.name), tool);
|
|
89
|
+
}
|
|
90
|
+
const componentNames = [...byName.keys()];
|
|
91
|
+
// 컴포넌트명 → getUIExamples uiType 키
|
|
92
|
+
// 정확 일치 우선 (부분 일치를 먼저 하면 MuiIcon이 Icon에 잘못 매칭됨)
|
|
93
|
+
function exampleKeyFor(componentName) {
|
|
94
|
+
const target = normalize(componentName);
|
|
95
|
+
const exact = exampleKeys.find(k => normalize(k) === target);
|
|
96
|
+
if (exact)
|
|
97
|
+
return exact;
|
|
98
|
+
return exampleKeys.find(k => {
|
|
99
|
+
const nk = normalize(k);
|
|
100
|
+
return nk.includes(target) || target.includes(nk);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function summaryOf(tool) {
|
|
104
|
+
return tool.description.split("\n")[0].trim();
|
|
105
|
+
}
|
|
106
|
+
const listComponents = {
|
|
107
|
+
name: "listComponents",
|
|
108
|
+
description: "OpsNow 공통 UI 컴포넌트 카탈로그(이름 + 용도)를 반환합니다. 어떤 컴포넌트를 쓸지 정하지 못했거나 이름이 확실하지 않을 때 먼저 호출하세요.",
|
|
109
|
+
parameters: z.object({}),
|
|
110
|
+
handler: async () => {
|
|
111
|
+
let md = `# OpsNow 공통 UI 컴포넌트 카탈로그 (${componentNames.length}개)\n\n`;
|
|
112
|
+
md += `| 이름 | 용도 |\n|------|------|\n`;
|
|
113
|
+
for (const [name, tool] of byName) {
|
|
114
|
+
md += `| ${name} | ${summaryOf(tool)} |\n`;
|
|
115
|
+
}
|
|
116
|
+
md += `\n다음 단계:\n`;
|
|
117
|
+
md += `1. 컴포넌트를 골랐으면 props/import 확인: getComponentSpec({ component: "이름" })\n`;
|
|
118
|
+
md += `2. 실제 사용 예제 확인: getUIExamples({ uiType: "이름" })`;
|
|
119
|
+
return { content: [{ type: "text", text: md }] };
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const componentSpecParams = z.object({
|
|
123
|
+
component: z
|
|
124
|
+
.string()
|
|
125
|
+
.describe(`스펙을 조회할 컴포넌트 이름. 사용 가능: ${componentNames.join(", ")}`),
|
|
126
|
+
});
|
|
127
|
+
const getComponentSpec = {
|
|
128
|
+
name: "getComponentSpec",
|
|
129
|
+
description: `OpsNow 공통 UI 컴포넌트의 스펙(props 테이블, import 방법, 사용 규칙)을 반환합니다.
|
|
130
|
+
**컴포넌트 코드를 작성하기 전에 반드시 해당 컴포넌트로 호출하세요.**
|
|
131
|
+
사용 가능한 컴포넌트: ${componentNames.join(", ")}`,
|
|
132
|
+
parameters: componentSpecParams,
|
|
133
|
+
handler: async (args) => {
|
|
134
|
+
const input = args.component ?? "";
|
|
135
|
+
const target = normalize(input);
|
|
136
|
+
const matched = componentNames.find(n => normalize(n) === target);
|
|
137
|
+
// 매칭 실패 → 빈 응답 금지, 복붙 가능한 대안 호출문 반환
|
|
138
|
+
if (!matched) {
|
|
139
|
+
const candidates = componentNames.filter(n => normalize(n).includes(target) || target.includes(normalize(n)));
|
|
140
|
+
const listed = (candidates.length > 0 ? candidates : componentNames)
|
|
141
|
+
.map(n => `getComponentSpec({ component: "${n}" })`)
|
|
142
|
+
.join("\n");
|
|
143
|
+
const intro = candidates.length > 0
|
|
144
|
+
? `'${input}' 컴포넌트를 찾을 수 없습니다. 아래 중 하나로 다시 호출하세요:`
|
|
145
|
+
: `'${input}' 컴포넌트를 찾을 수 없습니다. 사용 가능한 전체 목록입니다:`;
|
|
146
|
+
return { content: [{ type: "text", text: `${intro}\n${listed}` }] };
|
|
147
|
+
}
|
|
148
|
+
const tool = byName.get(matched);
|
|
149
|
+
const rows = Object.entries(tool.parameters.shape).flatMap(([key, field]) => inspectField(key, field));
|
|
150
|
+
// description 원문(용도 + import 예시 + 규칙 포함)을 그대로 싣고 props 테이블 추가
|
|
151
|
+
let md = `# ${matched}\n\n${tool.description.trim()}\n\n`;
|
|
152
|
+
if (rows.length > 0) {
|
|
153
|
+
md += `## Props\n\n| Prop | 타입 | 필수 | 기본값 | 설명 |\n|------|------|:---:|--------|------|\n`;
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
md += `| \`${row.name}\` | ${row.type} | ${row.required ? "✅" : ""} | ${row.defaultValue} | ${row.description} |\n`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const exKey = exampleKeyFor(matched);
|
|
159
|
+
if (exKey) {
|
|
160
|
+
md += `\n상세 사용 예제: getUIExamples({ uiType: "${exKey}" }) 를 호출하세요.`;
|
|
161
|
+
}
|
|
162
|
+
return { content: [{ type: "text", text: md }] };
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
return [listComponents, getComponentSpec];
|
|
166
|
+
}
|
|
@@ -19,9 +19,7 @@ export const StepperSchema = z.object({
|
|
|
19
19
|
contentHeight: z.string().optional().describe("컨텐츠 높이 스타일"),
|
|
20
20
|
nonLinear: z.boolean().optional().describe("논리니어 여부"),
|
|
21
21
|
contentDependency: z.boolean().optional().describe("컨텐츠 의존성 여부"),
|
|
22
|
-
|
|
23
|
-
onChange: z.string().optional().describe("스텝 변경 이벤트 핸들러 함수명"),
|
|
24
|
-
clickable: z.boolean().optional().default(false).describe("스텝 클릭 가능 여부")
|
|
22
|
+
setActiveStep: z.string().optional().describe("현재 스텝(activeStep) 상태를 변경하는 setter 함수명 (문자열, 예: 'setActiveStep'). (index: number) => void 시그니처로, 보통 useState의 setter 함수명을 그대로 전달. nonLinear 모드에서 스텝 클릭 이동 시 컴포넌트가 호출함. 스텝 클릭 동작은 nonLinear + onStepClick/setActiveStep으로만 제어됨")
|
|
25
23
|
});
|
|
26
24
|
// Stepper 컴포넌트 함수 - 배열 반환
|
|
27
25
|
export function createStepperComponent() {
|
|
@@ -37,7 +35,7 @@ export function createStepperComponent() {
|
|
|
37
35
|
\`\`\``,
|
|
38
36
|
parameters: StepperSchema,
|
|
39
37
|
handler: async (args) => {
|
|
40
|
-
const { steps, activeStep = 0, orientation = "horizontal", align, isSimpleView, connector, onStepClick, contentPadding, contentHeight, nonLinear, contentDependency
|
|
38
|
+
const { steps, activeStep = 0, orientation = "horizontal", align, isSimpleView, connector, onStepClick, contentPadding, contentHeight, nonLinear, contentDependency } = args;
|
|
41
39
|
// 태그 props만 생성
|
|
42
40
|
const propList = [
|
|
43
41
|
`steps={steps}`,
|
|
@@ -50,11 +48,10 @@ export function createStepperComponent() {
|
|
|
50
48
|
contentPadding ? `contentPadding=\"${contentPadding}\"` : '',
|
|
51
49
|
contentHeight ? `contentHeight=\"${contentHeight}\"` : '',
|
|
52
50
|
nonLinear ? 'nonLinear' : '',
|
|
53
|
-
typeof contentDependency === 'boolean' ? `contentDependency={${contentDependency}}` : ''
|
|
54
|
-
`size=\"${size}\"`,
|
|
55
|
-
onChange ? `onChange={${onChange}}` : '',
|
|
56
|
-
clickable ? 'clickable' : ''
|
|
51
|
+
typeof contentDependency === 'boolean' ? `contentDependency={${contentDependency}}` : ''
|
|
57
52
|
].filter(Boolean);
|
|
53
|
+
if (args.setActiveStep)
|
|
54
|
+
propList.push(`setActiveStep={${args.setActiveStep}}`);
|
|
58
55
|
// StepContent 샘플
|
|
59
56
|
const stepContent = steps.map((_, idx) => ` <OpsnowCommonStepContent stepIndex={${idx}} />`).join('\n');
|
|
60
57
|
const code = `<OpsnowCommonStepper\n ${propList.join('\n ')}\n>\n${stepContent}\n</OpsnowCommonStepper>`;
|
|
@@ -12,16 +12,12 @@ export const ToastPopupSchema = z.object({
|
|
|
12
12
|
.number()
|
|
13
13
|
.optional()
|
|
14
14
|
.describe("토스트 메시지 노출 시간(ms)"),
|
|
15
|
-
useCancelButton: z
|
|
16
|
-
.boolean()
|
|
17
|
-
.optional()
|
|
18
|
-
.describe("취소 버튼 사용 여부"),
|
|
19
15
|
useTitle: z
|
|
20
16
|
.boolean()
|
|
21
17
|
.optional()
|
|
22
18
|
.describe("타이틀 사용 여부 - title 속성 사용 시 자동으로 true로 설정"),
|
|
23
19
|
title: z.string().optional().describe("토스트 팝업 타이틀"),
|
|
24
|
-
|
|
20
|
+
customActionBtn: z.string().optional().describe("토스트 팝업 내부 버튼 커스텀 코드(반드시 OpsnowCommonButton 컴포넌트 사용) - 예시: <OpsnowCommonButton onClick={handleCustomAction} /> "),
|
|
25
21
|
anchorOrigin: z
|
|
26
22
|
.object({
|
|
27
23
|
vertical: z
|
|
@@ -56,10 +52,16 @@ export const ToastPopupSchema = z.object({
|
|
|
56
52
|
.optional()
|
|
57
53
|
.describe("토스트 애니메이션 지속 시간(ms)"),
|
|
58
54
|
action: z.string().optional().describe("JSX 노드 또는 key를 받아 노드를 반환하는 함수도 지원"),
|
|
59
|
-
TransitionComponent: z.string().optional().describe("MUI 트랜지션 컴포넌트(Grow, Zoom, Fade, Slide 등)를 import해서 함수명 그대로 입력 (예: Grow, Zoom
|
|
55
|
+
TransitionComponent: z.string().optional().describe("MUI 트랜지션 컴포넌트(Grow, Zoom, Fade, Slide 등)를 import해서 함수명 그대로 입력 (예: Grow, Zoom)"),
|
|
60
56
|
style: z.record(z.string(), z.string()).optional().describe("토스트 팝업에 적용할 인라인 스타일 객체. 모든 CSS 속성(key)과 값(value)을 string으로 입력 (예: { 'boxShadow': '...', 'borderLeft': '...', 'backgroundColor': '...' })")
|
|
61
57
|
}),
|
|
62
|
-
|
|
58
|
+
open: z.boolean().optional().describe("토스트 표시 여부 (boolean) - <OpsnowCommonToastPopup> 컴포넌트를 직접 사용할 때의 flat prop"),
|
|
59
|
+
color: z.enum(["success", "error", "info", "warning", "default"]).optional().describe("토스트 타입/색상 (success | error | info | warning | default, 기본값 default) - 컴포넌트 직접 사용 시 flat prop"),
|
|
60
|
+
autoHideDuration: z.number().optional().describe("토스트 메시지 자동 숨김 시간(ms, number) - 컴포넌트 직접 사용 시 flat prop"),
|
|
61
|
+
useTitle: z.boolean().optional().describe("타이틀 사용 여부 (boolean) - title 속성 사용 시 true로 설정. 컴포넌트 직접 사용 시 flat prop"),
|
|
62
|
+
title: z.string().optional().describe("토스트 팝업 타이틀 (string) - 컴포넌트 직접 사용 시 flat prop"),
|
|
63
|
+
customActionBtn: z.string().optional().describe("토스트 내부 커스텀 액션 버튼 JSX 코드 문자열 (React.ReactNode) - 예시: <OpsnowCommonButton label='확인' onClick={handleAction} />. 컴포넌트 직접 사용 시 flat prop"),
|
|
64
|
+
}).describe("토스트 팝업 - showToast(message, options) 함수 API가 표준. flat prop 컴포넌트는 하위호환 레거시 래퍼. 버튼 컴포넌트 반드시 동시 호출 필요");
|
|
63
65
|
// Toast Popup 컴포넌트 함수 - 배열 반환
|
|
64
66
|
export function createToastPopupComponent() {
|
|
65
67
|
return [
|
|
@@ -85,23 +87,33 @@ export function createToastPopupComponent() {
|
|
|
85
87
|
optionProps.push(`color: '${args.options.color}'`);
|
|
86
88
|
if (args.options.autoHideDuration)
|
|
87
89
|
optionProps.push(`autoHideDuration: ${args.options.autoHideDuration}`);
|
|
88
|
-
if (args.options.useCancelButton)
|
|
89
|
-
optionProps.push(`useCancelButton: ${args.options.useCancelButton}`);
|
|
90
90
|
if (args.options.useTitle)
|
|
91
91
|
optionProps.push(`useTitle: ${args.options.useTitle}`);
|
|
92
92
|
if (args.options.title)
|
|
93
|
-
optionProps.push(`title: ${args.options.title}`);
|
|
94
|
-
if (args.options.
|
|
95
|
-
optionProps.push(`
|
|
93
|
+
optionProps.push(`title: '${args.options.title}'`);
|
|
94
|
+
if (args.options.customActionBtn) {
|
|
95
|
+
optionProps.push(`customActionBtn: (${args.options.customActionBtn})`);
|
|
96
|
+
}
|
|
97
|
+
if (args.options.anchorOrigin?.vertical || args.options.anchorOrigin?.horizontal) {
|
|
98
|
+
const anchorParts = [];
|
|
99
|
+
if (args.options.anchorOrigin?.vertical)
|
|
100
|
+
anchorParts.push(`vertical: '${args.options.anchorOrigin.vertical}'`);
|
|
101
|
+
if (args.options.anchorOrigin?.horizontal)
|
|
102
|
+
anchorParts.push(`horizontal: '${args.options.anchorOrigin.horizontal}'`);
|
|
103
|
+
optionProps.push(`anchorOrigin: { ${anchorParts.join(', ')} }`);
|
|
96
104
|
}
|
|
97
|
-
if (args.options.anchorOrigin?.vertical || args.options.anchorOrigin?.horizontal)
|
|
98
|
-
optionProps.push(`anchorOrigin: ${args.options.anchorOrigin?.vertical ? `vertical: ${args.options.anchorOrigin?.vertical}` : ""}, ${args.options.anchorOrigin?.horizontal ? `horizontal: ${args.options.anchorOrigin?.horizontal}` : ""}`);
|
|
99
105
|
if (args.options.preventDuplicate)
|
|
100
106
|
optionProps.push(`preventDuplicate: ${args.options.preventDuplicate}`);
|
|
101
107
|
if (args.options.persist)
|
|
102
108
|
optionProps.push(`persist: ${args.options.persist}`);
|
|
103
|
-
if (args.options.transitionDuration?.enter || args.options.transitionDuration?.exit)
|
|
104
|
-
|
|
109
|
+
if (args.options.transitionDuration?.enter || args.options.transitionDuration?.exit) {
|
|
110
|
+
const durationParts = [];
|
|
111
|
+
if (args.options.transitionDuration?.enter)
|
|
112
|
+
durationParts.push(`enter: ${args.options.transitionDuration.enter}`);
|
|
113
|
+
if (args.options.transitionDuration?.exit)
|
|
114
|
+
durationParts.push(`exit: ${args.options.transitionDuration.exit}`);
|
|
115
|
+
optionProps.push(`transitionDuration: { ${durationParts.join(', ')} }`);
|
|
116
|
+
}
|
|
105
117
|
if (args.options.action)
|
|
106
118
|
optionProps.push(`action: ${args.options.action}`);
|
|
107
119
|
if (args.options.TransitionComponent)
|
|
@@ -118,7 +130,7 @@ export function createToastPopupComponent() {
|
|
|
118
130
|
}
|
|
119
131
|
code += `
|
|
120
132
|
const (버튼 컴포넌트 onClick 함수명) = () => {
|
|
121
|
-
showToast(${args.message ? args.message
|
|
133
|
+
showToast(${args.message ? `'${args.message}', ` : ""}${toastCode});
|
|
122
134
|
}
|
|
123
135
|
`;
|
|
124
136
|
return {
|
|
@@ -38,8 +38,6 @@ export const TooltipSchema = z.object({
|
|
|
38
38
|
.describe("아이콘 이름 - iconProps.useIcon이 true일 경우 필수"),
|
|
39
39
|
useIcon: z.boolean().optional().describe("아이콘 사용 여부 - iconName이 있을 경우 iconProps.useIcon은 true여야 함, useMuiIcon 과 동시에 true안됨"),
|
|
40
40
|
useMuiIcon: z.boolean().optional().describe("MUI 아이콘 사용 여부 - useIcon 과 동시에 true 안됨"),
|
|
41
|
-
alignItems: z.string().optional().describe("아이콘 정렬 방식"),
|
|
42
|
-
justifyContent: z.string().optional().describe("아이콘 정렬 방식"),
|
|
43
41
|
label: z.string().describe("툴팁 라벨"),
|
|
44
42
|
colorProps: z
|
|
45
43
|
.object({
|
|
@@ -130,10 +128,6 @@ export function createTooltipComponent() {
|
|
|
130
128
|
props.push(`useIcon={${args.iconTooltipProps.useIcon}}`);
|
|
131
129
|
if (args.iconTooltipProps.useMuiIcon)
|
|
132
130
|
props.push(`useMuiIcon={${args.iconTooltipProps.useMuiIcon}}`);
|
|
133
|
-
if (args.iconTooltipProps.alignItems)
|
|
134
|
-
props.push(`alignItems="${args.iconTooltipProps.alignItems}"`);
|
|
135
|
-
if (args.iconTooltipProps.justifyContent)
|
|
136
|
-
props.push(`justifyContent="${args.iconTooltipProps.justifyContent}"`);
|
|
137
131
|
if (args.iconTooltipProps.colorProps) {
|
|
138
132
|
if (args.iconTooltipProps.colorProps.commonColor)
|
|
139
133
|
props.push(`commonColor={'${args.iconTooltipProps.colorProps.commonColor}'}`);
|
|
@@ -42,6 +42,8 @@ export const TypographySchema = z.object({
|
|
|
42
42
|
component: z.string().optional().describe("커스텀 태그(component) 지정"),
|
|
43
43
|
textDecoration: z.string().optional().describe("텍스트 데코레이션(underline 등)"),
|
|
44
44
|
textTransform: z.string().optional().describe("텍스트 변환(uppercase 등)"),
|
|
45
|
+
fontWeight: z.enum(["300", "400", "500", "600"]).optional().describe("폰트 굵기 (숫자값 300 | 400 | 500 | 600 중 하나로 지정, 예: 600)"),
|
|
46
|
+
fontSize: z.number().optional().describe("폰트 크기 (숫자, ⚠️ rem 단위로 해석됨 — 예: 1.4 → 1.4rem. px 아님! 14를 넣으면 14rem이 되어 매우 커짐). variant 기본 크기를 덮어씀"),
|
|
45
47
|
});
|
|
46
48
|
export function createTypographyComponent() {
|
|
47
49
|
return [
|
|
@@ -77,6 +79,10 @@ export function createTypographyComponent() {
|
|
|
77
79
|
props.push(`textDecoration="${args.textDecoration}"`);
|
|
78
80
|
if (args.textTransform)
|
|
79
81
|
props.push(`textTransform="${args.textTransform}"`);
|
|
82
|
+
if (args.fontWeight)
|
|
83
|
+
props.push(`fontWeight={${args.fontWeight}}`);
|
|
84
|
+
if (args.fontSize !== undefined)
|
|
85
|
+
props.push(`fontSize={${args.fontSize}}`);
|
|
80
86
|
const propsString = props.join(' ');
|
|
81
87
|
const code = `<OpsnowCommonTypography ${propsString}>${args.text}</OpsnowCommonTypography>`;
|
|
82
88
|
return {
|
|
Binary file
|
package/build/index.js
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
//
|
|
5
|
-
import {
|
|
6
|
-
import { createChartComponent } from "./components/opsnow-common-chart.js";
|
|
7
|
-
import { createDataGridComponent } from "./components/opsnow-common-grid.js";
|
|
8
|
-
import { createFormsComponent } from "./components/opsnow-common-forms.js";
|
|
9
|
-
import { createDataStatusComponent } from "./components/opsnow-common-data-status.js";
|
|
10
|
-
import { createProgressComponent } from "./components/opsnow-common-progress.js";
|
|
11
|
-
import { createNotificationComponent } from "./components/opsnow-common-notification.js";
|
|
12
|
-
import { createPaginationComponent } from "./components/opsnow-common-pagination.js";
|
|
13
|
-
import { createIconsComponent } from "./components/opsnow-common-icons.js";
|
|
14
|
-
import { createTabComponent } from "./components/opsnow-common-tab.js";
|
|
15
|
-
import { createGridLayoutComponent } from "./components/opsnow-common-layout.js";
|
|
16
|
-
import { createTypographyComponent } from "./components/opsnow-common-typography.js";
|
|
17
|
-
import { createTooltipComponent } from "./components/opsnow-common-tooltip.js";
|
|
18
|
-
import { createCoachMarkComponent } from "./components/opsnow-common-coach-mark.js";
|
|
19
|
-
import { createSelectComponent } from "./components/opsnow-common-select.js";
|
|
20
|
-
import { createPopupComponent } from "./components/opsnow-common-popup.js";
|
|
21
|
-
import { createStepperComponent } from "./components/opsnow-common-stepper.js";
|
|
22
|
-
import { createToastPopupComponent } from "./components/opsnow-common-toast-popup.js";
|
|
23
|
-
import { createFileUploaderComponent } from "./components/opsnow-common-file-upload.js";
|
|
4
|
+
// 컴포넌트 툴 수집 (팩토리 목록은 all-component-tools.ts에서 관리)
|
|
5
|
+
import { collectComponentTools } from "./components/all-component-tools.js";
|
|
24
6
|
// 예제 컴포넌트
|
|
25
|
-
import { createCommonExamplesComponent } from "./components/opsnow-common-examples.js";
|
|
7
|
+
import { createCommonExamplesComponent, EXAMPLES_MAP } from "./components/opsnow-common-examples.js";
|
|
8
|
+
// 툴 통합: createXxx 46개 → listComponents + getComponentSpec
|
|
9
|
+
import { createSpecTools } from "./components/opsnow-common-spec-tools.js";
|
|
10
|
+
// props 검증 툴 (스키마 대조 - createXxx 제거 후 props 지어내기 방지)
|
|
11
|
+
import { createValidatePropsTool } from "./components/opsnow-common-validate-props.js";
|
|
12
|
+
// OPSNOW_MCP_LEGACY_TOOLS=1 이면 기존처럼 createXxx 46개를 개별 등록 (A/B 테스트/롤백용)
|
|
13
|
+
const LEGACY_MODE = process.env.OPSNOW_MCP_LEGACY_TOOLS === "1";
|
|
14
|
+
const toolFlowInstructions = LEGACY_MODE ? "" : `
|
|
15
|
+
**컴포넌트 구현 흐름 (필수)**
|
|
16
|
+
|
|
17
|
+
1. 어떤 컴포넌트를 쓸지 불확실하면: listComponents 호출 (카탈로그)
|
|
18
|
+
2. 코드 작성 전: getComponentSpec({ component: "이름" }) 으로 props/import 확인
|
|
19
|
+
3. 실제 패턴 확인: getUIExamples({ uiType: "이름" })
|
|
20
|
+
4. 코드 작성/수정 후: validateProps({ code: "작성한 코드 전체" }) 로 props 검증
|
|
21
|
+
`;
|
|
26
22
|
// MCP 서버 생성
|
|
27
23
|
const server = new McpServer({
|
|
28
24
|
name: "opsnow-mcp-common-ui-server",
|
|
@@ -32,6 +28,7 @@ const server = new McpServer({
|
|
|
32
28
|
|
|
33
29
|
IMPORTANT: All components require React 18.x or higher environment.
|
|
34
30
|
Always include required import statements and i18n configuration when needed.
|
|
31
|
+
${toolFlowInstructions}
|
|
35
32
|
|
|
36
33
|
**CRITICAL: API 호출 규칙**
|
|
37
34
|
|
|
@@ -89,43 +86,28 @@ Before implementing OpsnowCommonDataGrid, you MUST use getUIExamples tool with '
|
|
|
89
86
|
|
|
90
87
|
WARNING: Violating these rules will cause the UI to malfunction.`
|
|
91
88
|
});
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
createTabComponent,
|
|
104
|
-
createGridLayoutComponent,
|
|
105
|
-
createTypographyComponent,
|
|
106
|
-
createTooltipComponent,
|
|
107
|
-
createCoachMarkComponent,
|
|
108
|
-
createSelectComponent,
|
|
109
|
-
createPopupComponent,
|
|
110
|
-
createStepperComponent,
|
|
111
|
-
createToastPopupComponent,
|
|
112
|
-
createFileUploaderComponent,
|
|
113
|
-
// 예제 컴포넌트
|
|
114
|
-
createCommonExamplesComponent,
|
|
115
|
-
];
|
|
89
|
+
// createXxx 툴 정의 수집 (등록 여부는 모드에 따라 결정)
|
|
90
|
+
const componentTools = collectComponentTools();
|
|
91
|
+
// getUIExamples는 양쪽 모드 공통 등록
|
|
92
|
+
const exampleTools = createCommonExamplesComponent();
|
|
93
|
+
// props 검증 툴 (양쪽 모드 공통 - 스키마는 createXxx 정의에서 재사용)
|
|
94
|
+
const validateTools = createValidatePropsTool(componentTools);
|
|
95
|
+
// 기본: 조회형 2개(listComponents, getComponentSpec) + getUIExamples + validateProps = 4개
|
|
96
|
+
// LEGACY: createXxx 45개 + getUIExamples + validateProps = 47개
|
|
97
|
+
const toolsToRegister = LEGACY_MODE
|
|
98
|
+
? [...componentTools, ...exampleTools, ...validateTools]
|
|
99
|
+
: [...createSpecTools(componentTools, Object.keys(EXAMPLES_MAP)), ...exampleTools, ...validateTools];
|
|
116
100
|
const allTools = [];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
allTools.push({ name: tool.name, description: tool.description });
|
|
121
|
-
server.tool(tool.name, tool.description, tool.parameters.shape, tool.handler);
|
|
122
|
-
});
|
|
101
|
+
toolsToRegister.forEach(tool => {
|
|
102
|
+
allTools.push({ name: tool.name, description: tool.description });
|
|
103
|
+
server.tool(tool.name, tool.description, tool.parameters.shape, tool.handler);
|
|
123
104
|
});
|
|
124
105
|
// 서버 시작
|
|
125
106
|
async function main() {
|
|
126
107
|
const transport = new StdioServerTransport();
|
|
127
108
|
await server.connect(transport);
|
|
128
109
|
console.error("OpsNow MCP Common UI Server running on stdio");
|
|
110
|
+
console.error(`Mode: ${LEGACY_MODE ? "legacy (createXxx 개별 등록)" : "consolidated (조회형 툴)"}`);
|
|
129
111
|
console.error(`Registered ${allTools.length} tools:`);
|
|
130
112
|
allTools.forEach(tool => {
|
|
131
113
|
console.error(` - ${tool.name}`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validateProps CLI - Claude Code hook / CI용 props 검증
|
|
4
|
+
*
|
|
5
|
+
* 사용법:
|
|
6
|
+
* 1) Claude Code PostToolUse 훅 (자동): stdin으로 훅 페이로드 JSON을 받음
|
|
7
|
+
* 소비 프로젝트 .claude/settings.json:
|
|
8
|
+
* {
|
|
9
|
+
* "hooks": {
|
|
10
|
+
* "PostToolUse": [
|
|
11
|
+
* { "matcher": "Edit|Write",
|
|
12
|
+
* "hooks": [{ "type": "command",
|
|
13
|
+
* "command": "node <이 저장소 경로>/build/validate-props-cli.js" }] }
|
|
14
|
+
* ]
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* → 모델이 .jsx/.tsx 파일을 수정할 때마다 자동 검증되고,
|
|
18
|
+
* 의심 건 발견 시 exit 2 + stderr 리포트가 모델에게 피드백된다.
|
|
19
|
+
*
|
|
20
|
+
* 2) 수동/CI: node build/validate-props-cli.js <파일...>
|
|
21
|
+
* 의심 건 있으면 exit 1
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from "fs";
|
|
24
|
+
import { collectComponentTools } from "./components/all-component-tools.js";
|
|
25
|
+
import { validatePropsInCode } from "./components/opsnow-common-validate-props.js";
|
|
26
|
+
const VALID_EXT = /\.(jsx|tsx|js|ts)$/;
|
|
27
|
+
function validateFile(filePath) {
|
|
28
|
+
if (!VALID_EXT.test(filePath) || !fs.existsSync(filePath))
|
|
29
|
+
return null;
|
|
30
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
31
|
+
if (!code.includes("OpsnowCommon"))
|
|
32
|
+
return null;
|
|
33
|
+
const result = validatePropsInCode(code, collectComponentTools());
|
|
34
|
+
if (result.checkedComponents === 0)
|
|
35
|
+
return null;
|
|
36
|
+
return { suspects: result.suspectCount, text: result.text };
|
|
37
|
+
}
|
|
38
|
+
function main() {
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
// 수동/CI 모드: 파일 경로 인자
|
|
41
|
+
if (args.length > 0) {
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const file of args) {
|
|
44
|
+
const r = validateFile(file);
|
|
45
|
+
if (!r)
|
|
46
|
+
continue;
|
|
47
|
+
console.log(`== ${file}\n${r.text}\n`);
|
|
48
|
+
total += r.suspects;
|
|
49
|
+
}
|
|
50
|
+
process.exit(total > 0 ? 1 : 0);
|
|
51
|
+
}
|
|
52
|
+
// 훅 모드: stdin으로 Claude Code PostToolUse 페이로드
|
|
53
|
+
let payload;
|
|
54
|
+
try {
|
|
55
|
+
payload = JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
process.exit(0); // 페이로드 없음/파싱 실패 → 조용히 통과
|
|
59
|
+
}
|
|
60
|
+
const filePath = payload?.tool_input?.file_path;
|
|
61
|
+
if (!filePath)
|
|
62
|
+
process.exit(0);
|
|
63
|
+
const r = validateFile(filePath);
|
|
64
|
+
if (!r || r.suspects === 0)
|
|
65
|
+
process.exit(0);
|
|
66
|
+
// exit 2 + stderr → Claude Code가 모델에게 자동 피드백
|
|
67
|
+
console.error(`[OpsNow props 검증] ${filePath}\n${r.text}\n` +
|
|
68
|
+
`위 의심 prop들을 확인하고 수정하세요. (MCP getComponentSpec 툴로 스키마 확인 가능)`);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsnow-mcp/opsnow-mcp-common-ui-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"opsnow-mcp-common-ui": "./build/index.js"
|
|
7
|
+
"opsnow-mcp-common-ui": "./build/index.js",
|
|
8
|
+
"opsnow-ui-validate": "./build/validate-props-cli.js"
|
|
8
9
|
},
|
|
9
10
|
"module": "src/index.ts",
|
|
10
11
|
"scripts": {
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
43
|
+
"@opsnow-common/claude-skill-atlassian-skills": "^1.0.65",
|
|
42
44
|
"zod": "^3.24.3"
|
|
43
45
|
}
|
|
44
46
|
}
|