@infomind-ux/infoux-mcp 0.1.0
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/README.md +81 -0
- package/bin/server.js +331 -0
- package/data/contract.md +215 -0
- package/data/manifest.json +286 -0
- package/data/references/accessibility.md +243 -0
- package/data/references/forbidden-patterns.md +462 -0
- package/data/references/guide-import.md +93 -0
- package/data/references/html-semantics.md +475 -0
- package/data/references/krds-components.md +1426 -0
- package/data/references/krds-source.md +1125 -0
- package/data/references/krds-tokens.md +201 -0
- package/data/references/project-profiles.md +99 -0
- package/data/references/snippet-template.md +185 -0
- package/data/references/tailwind-mapping.md +53 -0
- package/data/rules.json +780 -0
- package/data/snippets/accordion.md +49 -0
- package/data/snippets/alert.md +37 -0
- package/data/snippets/badge.md +29 -0
- package/data/snippets/boilerplate.md +125 -0
- package/data/snippets/breadcrumb.md +26 -0
- package/data/snippets/btn.md +63 -0
- package/data/snippets/calendar.md +64 -0
- package/data/snippets/card.md +50 -0
- package/data/snippets/carousel.md +43 -0
- package/data/snippets/check-radio.md +55 -0
- package/data/snippets/disclosure.md +44 -0
- package/data/snippets/file-upload.md +34 -0
- package/data/snippets/form.md +68 -0
- package/data/snippets/header.md +56 -0
- package/data/snippets/list.md +64 -0
- package/data/snippets/main-menu.md +49 -0
- package/data/snippets/modal.md +54 -0
- package/data/snippets/pagination.md +41 -0
- package/data/snippets/progress.md +30 -0
- package/data/snippets/select.md +34 -0
- package/data/snippets/side-panel.md +57 -0
- package/data/snippets/spinner.md +24 -0
- package/data/snippets/step-indicator.md +38 -0
- package/data/snippets/switch.md +32 -0
- package/data/snippets/tab.md +52 -0
- package/data/snippets/table.md +63 -0
- package/data/snippets/tag.md +42 -0
- package/data/snippets/toast.md +48 -0
- package/data/snippets/tooltip.md +38 -0
- package/data/tokens.css +390 -0
- package/data/workflows/change-token.md +20 -0
- package/data/workflows/create-component.md +71 -0
- package/data/workflows/design-form.md +20 -0
- package/data/workflows/design-page.md +22 -0
- package/data/workflows/design-widget.md +19 -0
- package/data/workflows/init-project.md +68 -0
- package/data/workflows/review-ui.md +14 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @infomind-ux/infoux-mcp
|
|
2
|
+
|
|
3
|
+
INFOMIND UX팀의 퍼블리싱 기준(infoUX)을 MCP로 제공한다. 팀원이 스킬 파일을 각자 복사하지 않아도, 쓰는 에이전트가 무엇이든 같은 기준을 보게 하는 배포 경로다.
|
|
4
|
+
|
|
5
|
+
2026-07-29 스킬 배포 방식을 폐기하고 MCP로 일원화했다. 스킬은 Claude Code 전용인 데다 각자 `~/.claude/skills/`로 복사해야 해서 도구가 다른 팀원은 받을 수 없었다. MCP는 등록 한 줄로 끝나고 Codex·Cursor에서도 똑같이 붙는다.
|
|
6
|
+
|
|
7
|
+
## 제공 도구
|
|
8
|
+
|
|
9
|
+
| 도구 | 용도 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `get_contract` | infoUX 작업 컨트랙트 전문. UI 작업 시작 전에 한 번 읽는다 |
|
|
12
|
+
| `list_components` | 컴포넌트 카탈로그 29종 |
|
|
13
|
+
| `get_component` | 컴포넌트 마크업 스니펫 + 접근성 요건 |
|
|
14
|
+
| `get_tokens` | 색상·폰트·브레이크포인트 토큰. `query`로 필터, `raw`로 tokens.css 원본 |
|
|
15
|
+
| `get_rules` | 코딩 규칙 R-01~R-22 (위반·준수 예시 포함) |
|
|
16
|
+
| `get_reference` | 접근성·금지패턴·Tailwind 매핑·HTML 시맨틱·사이트 유형 프로필 |
|
|
17
|
+
| `get_workflow` | 작업 절차 — 페이지·폼·위젯 설계, 컴포넌트 생성, 토큰 변경, UI 리뷰, 프로젝트 초기화 |
|
|
18
|
+
| `search_docs` | 어느 문서를 봐야 할지 모를 때 전체 검색 |
|
|
19
|
+
|
|
20
|
+
서버는 접속 시 **지시문**도 함께 넘긴다 — 사이트 유형 판정, 토큰 강제, 카탈로그 우선, 규칙 준수 순서가 에이전트에 자동으로 걸린다.
|
|
21
|
+
|
|
22
|
+
## 설치
|
|
23
|
+
|
|
24
|
+
### Claude Code
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
claude mcp add infoux -- npx -y @infomind-ux/infoux-mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
저장소를 clone해서 쓰는 경우(사내 배포 전):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
claude mcp add infoux -- node /절대경로/infoUX/mcp/bin/server.js
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Codex
|
|
37
|
+
|
|
38
|
+
`~/.codex/config.toml`
|
|
39
|
+
|
|
40
|
+
```toml
|
|
41
|
+
[mcp_servers.infoux]
|
|
42
|
+
command = "npx"
|
|
43
|
+
args = ["-y", "@infomind-ux/infoux-mcp"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Cursor / Claude Desktop
|
|
47
|
+
|
|
48
|
+
`mcpServers` 설정에 추가한다.
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"infoux": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["-y", "@infomind-ux/infoux-mcp"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 확인
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx -y @infomind-ux/infoux-mcp
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`infoUX MCP 준비됨 — 빌드 <sha>, 도구 8종`이 stderr로 나오면 정상이다. stdout은 프로토콜 채널이라 로그를 싣지 않는다.
|
|
68
|
+
|
|
69
|
+
## 데이터 갱신
|
|
70
|
+
|
|
71
|
+
`mcp/data/`는 가이드 원본에서 생성한 번들이다. **직접 수정하지 않는다.**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm run build:mcp
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
원본이 바뀌면(`rules.json`, `skill/`, `src/snippets/`, `tokens/`) 재생성 후 커밋한다. `npm run build`와 `npm test`에 이미 포함돼 있다.
|
|
78
|
+
|
|
79
|
+
## 범위
|
|
80
|
+
|
|
81
|
+
문서·토큰·규칙만 노출한다. 가이드 저장소 자체(스타터, CLI, 빌드 스크립트)는 포함하지 않는다. 납품 범위 결정과 충돌하지 않는다 — 발주처에 넘어가는 것은 생성된 사이트이지 이 서버가 아니다.
|
package/bin/server.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* infoUX MCP 서버
|
|
4
|
+
*
|
|
5
|
+
* 팀원이 스킬 파일을 각자 복사하지 않고도 infoUX 기준을 쓰게 하는 배포 경로다.
|
|
6
|
+
* Claude Code / Codex / Cursor 어디서든 `npx @infomind-ux/infoux-mcp` 한 줄로 붙는다.
|
|
7
|
+
*
|
|
8
|
+
* 데이터는 mcp/data/에 번들되어 있다(`npm run build:mcp`로 생성). 저장소를
|
|
9
|
+
* clone하지 않은 PC에서도 답할 수 있어야 하기 때문이다.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs')
|
|
13
|
+
const path = require('node:path')
|
|
14
|
+
|
|
15
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js')
|
|
16
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js')
|
|
17
|
+
const {
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
ListToolsRequestSchema
|
|
20
|
+
} = require('@modelcontextprotocol/sdk/types.js')
|
|
21
|
+
|
|
22
|
+
const DATA_DIR = path.resolve(__dirname, '..', 'data')
|
|
23
|
+
|
|
24
|
+
function readData(...segments) {
|
|
25
|
+
return fs.readFileSync(path.join(DATA_DIR, ...segments), 'utf8')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const manifest = JSON.parse(readData('manifest.json'))
|
|
29
|
+
const rules = JSON.parse(readData('rules.json'))
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────
|
|
32
|
+
// 서버 지시문 — 에이전트가 도구를 언제 써야 하는지 알려준다
|
|
33
|
+
// ─────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const INSTRUCTIONS = `INFOMIND UX팀의 HTML/CSS 퍼블리싱 기준(infoUX)을 제공한다.
|
|
36
|
+
|
|
37
|
+
이 서버가 붙어 있으면, HTML·CSS·UI 작업 전에 다음을 지킨다.
|
|
38
|
+
|
|
39
|
+
1. 사이트 유형을 판정한다 — 일반사이트 / 공공서비스 / 공공기관 / CMS·관리자 / 커머스·예약.
|
|
40
|
+
판단이 서지 않으면 get_reference("project-profiles")를 읽는다.
|
|
41
|
+
2. 색상은 반드시 토큰을 쓴다. hex/rgb/hsl 직접 작성 금지. get_tokens로 확인한다.
|
|
42
|
+
토큰명을 지어내지 않는다 — 목록에 없으면 사용자에게 확인한다.
|
|
43
|
+
3. 컴포넌트는 카탈로그를 먼저 본다. list_components → get_component 순으로 확인하고
|
|
44
|
+
기존 스니펫을 조합한다. 카탈로그 밖 컴포넌트는 임의 생성하지 않는다.
|
|
45
|
+
페이지·폼·위젯 설계나 컴포넌트 신규 생성처럼 절차가 정해진 작업은 get_workflow를 먼저 읽는다.
|
|
46
|
+
4. 규칙 R-01~R-22를 지킨다. get_rules로 확인한다. BEM, 접근성, 금지 패턴이 여기 있다.
|
|
47
|
+
5. 간격·크기·타이포 스케일·반경·모션은 토큰이 아니라 CSS/Tailwind 직접값으로 쓴다.
|
|
48
|
+
|
|
49
|
+
기술 스택은 Tailwind v4 + 표준 CSS nesting + BEM + ITCSS 5계층이다. SCSS는 쓰지 않는다.`
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────
|
|
52
|
+
// 도구
|
|
53
|
+
// ─────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const TOOLS = [
|
|
56
|
+
{
|
|
57
|
+
name: 'get_contract',
|
|
58
|
+
description:
|
|
59
|
+
'infoUX 작업 컨트랙트 전문을 반환한다. UI 작업을 시작하기 전에 한 번 읽는다. ' +
|
|
60
|
+
'작업 순서, 판단 기준, 금지사항이 들어 있다.',
|
|
61
|
+
inputSchema: { type: 'object', properties: {} }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'list_components',
|
|
65
|
+
description:
|
|
66
|
+
'사용 가능한 컴포넌트 카탈로그를 반환한다. 새 컴포넌트를 만들기 전에 반드시 확인한다 — ' +
|
|
67
|
+
'카탈로그에 있으면 그것을 쓴다.',
|
|
68
|
+
inputSchema: { type: 'object', properties: {} }
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'get_component',
|
|
72
|
+
description:
|
|
73
|
+
'컴포넌트 하나의 마크업 스니펫과 접근성 요건을 반환한다. BEM 블록명은 CSS 파일명과 같다.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
name: { type: 'string', description: '컴포넌트 이름 (예: btn, accordion, table)' }
|
|
78
|
+
},
|
|
79
|
+
required: ['name']
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'get_tokens',
|
|
84
|
+
description:
|
|
85
|
+
'색상·폰트·브레이크포인트 토큰 카탈로그를 반환한다. 색상 값이 필요할 때 반드시 여기서 확인한다. ' +
|
|
86
|
+
'query를 주면 해당 문자열이 든 줄만 걸러 낸다.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
query: { type: 'string', description: '필터 문자열 (예: primary, danger, border)' },
|
|
91
|
+
raw: { type: 'boolean', description: 'true면 생성된 tokens.css 원본을 반환한다' }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'get_rules',
|
|
97
|
+
description:
|
|
98
|
+
'infoUX 코딩 규칙 R-01~R-22를 반환한다. CSS·BEM·HTML·접근성 규칙과 위반 예시가 들어 있다.',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
id: { type: 'string', description: '규칙 ID 단건 조회 (예: R-12)' },
|
|
103
|
+
category: { type: 'string', description: '분류로 필터' },
|
|
104
|
+
severity: { type: 'string', description: 'error / warn / info' }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'get_reference',
|
|
110
|
+
description:
|
|
111
|
+
'레퍼런스 문서를 반환한다. name 없이 부르면 목록을 준다. ' +
|
|
112
|
+
'접근성 기준, 금지 패턴, Tailwind 매핑, HTML 시맨틱, 사이트 유형 프로필 등이 있다.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
name: { type: 'string', description: '문서 이름 (예: accessibility, forbidden-patterns)' }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'get_workflow',
|
|
122
|
+
description:
|
|
123
|
+
'작업 절차를 반환한다. name 없이 부르면 목록을 준다. 페이지·폼·위젯 설계, 컴포넌트 신규 생성, ' +
|
|
124
|
+
'토큰 변경, UI 리뷰, 프로젝트 초기화 절차가 있다. 해당 작업을 시작하기 전에 읽는다.',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
name: { type: 'string', description: '절차 이름 (예: design-page, change-token, create-component)' }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'search_docs',
|
|
134
|
+
description:
|
|
135
|
+
'infoUX 문서 전체(레퍼런스·스니펫·규칙)에서 문자열을 찾는다. 어느 문서를 봐야 할지 모를 때 먼저 쓴다.',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
query: { type: 'string', description: '찾을 문자열' }
|
|
140
|
+
},
|
|
141
|
+
required: ['query']
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
// ─────────────────────────────────────────────────────
|
|
147
|
+
// 도구 구현
|
|
148
|
+
// ─────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function text(body) {
|
|
151
|
+
return { content: [{ type: 'text', text: body }] }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function notFound(what, available) {
|
|
155
|
+
return text(`${what}을(를) 찾을 수 없다.\n\n사용 가능: ${available.join(', ')}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function listComponents() {
|
|
159
|
+
const lines = ['# infoUX 컴포넌트 카탈로그', '']
|
|
160
|
+
lines.push(`총 ${manifest.snippets.length}종. get_component(name)으로 마크업을 가져온다.`, '')
|
|
161
|
+
for (const item of manifest.snippets) {
|
|
162
|
+
lines.push(`- **${item.id}** — ${item.summary || '(설명 없음)'}`)
|
|
163
|
+
}
|
|
164
|
+
lines.push('', '카탈로그에 없는 컴포넌트는 임의로 만들지 않는다. UX팀 판단이 필요하다.')
|
|
165
|
+
return text(lines.join('\n'))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getComponent(name) {
|
|
169
|
+
const entry = manifest.snippets.find(item => item.id === name)
|
|
170
|
+
if (!entry) return notFound(`컴포넌트 "${name}"`, manifest.snippets.map(i => i.id))
|
|
171
|
+
return text(readData('snippets', entry.file))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getTokens({ query, raw } = {}) {
|
|
175
|
+
if (raw) return text(readData('tokens.css'))
|
|
176
|
+
|
|
177
|
+
const body = readData('references', 'krds-tokens.md')
|
|
178
|
+
if (!query) return text(body)
|
|
179
|
+
|
|
180
|
+
const needle = query.toLowerCase()
|
|
181
|
+
const matched = body.split('\n').filter(line => line.toLowerCase().includes(needle))
|
|
182
|
+
if (matched.length === 0) {
|
|
183
|
+
return text(`"${query}"에 해당하는 토큰이 없다. 토큰명을 지어내지 말고 get_tokens()로 전체 목록을 확인한다.`)
|
|
184
|
+
}
|
|
185
|
+
return text([`# 토큰 검색 — "${query}" (${matched.length}건)`, '', ...matched].join('\n'))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getRules({ id, category, severity } = {}) {
|
|
189
|
+
let found = rules.rules
|
|
190
|
+
|
|
191
|
+
if (id) {
|
|
192
|
+
const wanted = id.toUpperCase()
|
|
193
|
+
found = found.filter(rule => rule.id.toUpperCase() === wanted)
|
|
194
|
+
if (found.length === 0) return notFound(`규칙 "${id}"`, rules.rules.map(r => r.id))
|
|
195
|
+
}
|
|
196
|
+
if (category) found = found.filter(rule => rule.category === category)
|
|
197
|
+
if (severity) found = found.filter(rule => rule.severity === severity)
|
|
198
|
+
|
|
199
|
+
const lines = [`# infoUX 규칙 (${found.length}건)`, '']
|
|
200
|
+
for (const rule of found) {
|
|
201
|
+
lines.push(`## ${rule.id} — ${rule.summary}`)
|
|
202
|
+
lines.push(`- 심각도: ${rule.severity} / 분류: ${rule.category}`)
|
|
203
|
+
if (rule.enforcement?.length) lines.push(`- 검사: ${rule.enforcement.join(', ')}`)
|
|
204
|
+
if (rule.rationale) lines.push(`- 이유: ${rule.rationale}`)
|
|
205
|
+
if (rule.bad) lines.push('', '위반:', '```', String(rule.bad), '```')
|
|
206
|
+
if (rule.good) lines.push('', '준수:', '```', String(rule.good), '```')
|
|
207
|
+
lines.push('')
|
|
208
|
+
}
|
|
209
|
+
return text(lines.join('\n'))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getWorkflow(name) {
|
|
213
|
+
if (!name) {
|
|
214
|
+
const lines = ['# infoUX 작업 절차', '', '작업을 시작하기 전에 해당 절차를 읽는다.', '']
|
|
215
|
+
for (const item of manifest.workflows) {
|
|
216
|
+
lines.push(`- **${item.id}** — ${item.summary || '(설명 없음)'}`)
|
|
217
|
+
}
|
|
218
|
+
return text(lines.join('\n'))
|
|
219
|
+
}
|
|
220
|
+
const entry = manifest.workflows.find(item => item.id === name)
|
|
221
|
+
if (!entry) return notFound(`절차 "${name}"`, manifest.workflows.map(i => i.id))
|
|
222
|
+
return text(readData('workflows', entry.file))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getReference(name) {
|
|
226
|
+
if (!name) {
|
|
227
|
+
const lines = ['# infoUX 레퍼런스 문서', '']
|
|
228
|
+
for (const item of manifest.references) {
|
|
229
|
+
lines.push(`- **${item.id}** — ${item.summary || '(설명 없음)'}`)
|
|
230
|
+
}
|
|
231
|
+
return text(lines.join('\n'))
|
|
232
|
+
}
|
|
233
|
+
const entry = manifest.references.find(item => item.id === name)
|
|
234
|
+
if (!entry) return notFound(`문서 "${name}"`, manifest.references.map(i => i.id))
|
|
235
|
+
return text(readData('references', entry.file))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function searchDocs(query) {
|
|
239
|
+
const needle = query.toLowerCase()
|
|
240
|
+
const hits = []
|
|
241
|
+
|
|
242
|
+
const scan = (kind, dir, items) => {
|
|
243
|
+
for (const item of items) {
|
|
244
|
+
const body = readData(dir, item.file)
|
|
245
|
+
const lines = body.split('\n')
|
|
246
|
+
const matched = lines
|
|
247
|
+
.map((line, index) => ({ line: line.trim(), index }))
|
|
248
|
+
.filter(({ line }) => line.toLowerCase().includes(needle))
|
|
249
|
+
if (matched.length === 0) continue
|
|
250
|
+
hits.push({ kind, id: item.id, count: matched.length, samples: matched.slice(0, 3) })
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
scan('reference', 'references', manifest.references)
|
|
255
|
+
scan('workflow', 'workflows', manifest.workflows)
|
|
256
|
+
scan('component', 'snippets', manifest.snippets)
|
|
257
|
+
|
|
258
|
+
const ruleHits = rules.rules.filter(rule =>
|
|
259
|
+
JSON.stringify(rule).toLowerCase().includes(needle)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if (hits.length === 0 && ruleHits.length === 0) {
|
|
263
|
+
return text(`"${query}" 검색 결과 없음.`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const lines = [`# 검색 — "${query}"`, '']
|
|
267
|
+
if (ruleHits.length) {
|
|
268
|
+
lines.push(`## 규칙 ${ruleHits.length}건`, '')
|
|
269
|
+
for (const rule of ruleHits) lines.push(`- ${rule.id} — ${rule.summary} (get_rules로 상세 조회)`)
|
|
270
|
+
lines.push('')
|
|
271
|
+
}
|
|
272
|
+
for (const hit of hits) {
|
|
273
|
+
const getter =
|
|
274
|
+
hit.kind === 'reference' ? 'get_reference' : hit.kind === 'workflow' ? 'get_workflow' : 'get_component'
|
|
275
|
+
lines.push(`## ${hit.id} (${hit.kind}, ${hit.count}건) — ${getter}("${hit.id}")`)
|
|
276
|
+
for (const sample of hit.samples) lines.push(` ${sample.index + 1}: ${sample.line}`)
|
|
277
|
+
lines.push('')
|
|
278
|
+
}
|
|
279
|
+
return text(lines.join('\n'))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─────────────────────────────────────────────────────
|
|
283
|
+
// 서버 기동
|
|
284
|
+
// ─────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
const server = new Server(
|
|
287
|
+
{ name: 'infoux', version: require('../package.json').version },
|
|
288
|
+
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS }
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
292
|
+
|
|
293
|
+
server.setRequestHandler(CallToolRequestSchema, async request => {
|
|
294
|
+
const { name, arguments: args = {} } = request.params
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
switch (name) {
|
|
298
|
+
case 'get_contract':
|
|
299
|
+
return text(readData('contract.md'))
|
|
300
|
+
case 'list_components':
|
|
301
|
+
return listComponents()
|
|
302
|
+
case 'get_component':
|
|
303
|
+
return getComponent(args.name)
|
|
304
|
+
case 'get_tokens':
|
|
305
|
+
return getTokens(args)
|
|
306
|
+
case 'get_rules':
|
|
307
|
+
return getRules(args)
|
|
308
|
+
case 'get_reference':
|
|
309
|
+
return getReference(args.name)
|
|
310
|
+
case 'get_workflow':
|
|
311
|
+
return getWorkflow(args.name)
|
|
312
|
+
case 'search_docs':
|
|
313
|
+
return searchDocs(args.query)
|
|
314
|
+
default:
|
|
315
|
+
return { ...text(`알 수 없는 도구: ${name}`), isError: true }
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return { ...text(`도구 실행 실패 (${name}): ${error.message}`), isError: true }
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
async function main() {
|
|
323
|
+
await server.connect(new StdioServerTransport())
|
|
324
|
+
// stdout은 프로토콜 채널이다. 로그는 stderr로만 낸다.
|
|
325
|
+
console.error(`infoUX MCP 준비됨 — 빌드 ${manifest.version}, 도구 ${TOOLS.length}종`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
main().catch(error => {
|
|
329
|
+
console.error(`infoUX MCP 기동 실패: ${error.message}`)
|
|
330
|
+
process.exit(1)
|
|
331
|
+
})
|
package/data/contract.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# infoUX — INFOMIND UX 작업 컨트랙트
|
|
2
|
+
|
|
3
|
+
> **이것은 권장이 아니라 컨트랙트다.**
|
|
4
|
+
> infoUX MCP가 연결된 이상 아래 명시된 규정 외의 결정을 LLM이 임의로 할 수 없다.
|
|
5
|
+
> 위반 발견 시 **즉시 작업을 중단하고 사용자에게 보고**한다.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 컨트랙트 발효 조건
|
|
10
|
+
|
|
11
|
+
다음 트리거 문구가 사용자 메시지에 등장하면 이 컨트랙트가 발효된다:
|
|
12
|
+
|
|
13
|
+
- "info-design 스킬 기준으로 가자"
|
|
14
|
+
- "infomind 디자인 기준"
|
|
15
|
+
- "ux 가이드대로"
|
|
16
|
+
- 또는 사용자가 명시적으로 이 스킬을 호출
|
|
17
|
+
|
|
18
|
+
**발효 즉시 LLM은 아래 모든 규정을 무조건 따른다.** 발효된 후에는 LLM 자신의 판단으로 규정을 우회·예외 처리하지 않는다.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. 절대 금지 (MUST NOT)
|
|
23
|
+
|
|
24
|
+
다음을 **위반하면 코드를 작성하지 않고 작업을 중단**한다:
|
|
25
|
+
|
|
26
|
+
| 규칙 | 항목 | 금지 사유 |
|
|
27
|
+
|------|------|----------|
|
|
28
|
+
| **R-01** | 색상 raw hex/rgb/hsl 직접 작성 (`#fff`, `rgb(0,0,0)`) | KRDS 토큰만 허용 |
|
|
29
|
+
| **R-01** | Tailwind 기본 팔레트 raw 컬러 유틸리티 | 색상 토큰 또는 프로젝트 색상 유틸만 |
|
|
30
|
+
| **R-02** | `!important` (정당한 사유 주석 없을 시) | 토큰 시스템 우회 위험 |
|
|
31
|
+
| **R-03** | SCSS 사용 (`*.scss`, `@use`, `@forward`, `$var`) | Tailwind v4 + CSS Custom Properties만 허용 |
|
|
32
|
+
| **R-04** | BEM 미준수 클래스명 — `.block__element--modifier` 형식 외 | 일관성 |
|
|
33
|
+
| **R-05** | BEM element 2단계 중첩 (`.card__body__title`) | 평탄화 — `.card__title` 또는 `.card__body-title` |
|
|
34
|
+
| **R-06** | 옛 variant 이름 사용 (`btn--ghost`, `btn--outline`, `btn--link`) | KRDS 4 variant만: `--primary` / `--secondary` / `--tertiary` / `--text` |
|
|
35
|
+
| **R-07** | 인라인 `style="..."` (CSS 변수 주입 외) | 토큰 추적 불가 |
|
|
36
|
+
| **R-08** | HTML 클래스 BEM 2단계 element 중첩 | R-05 HTML 적용 |
|
|
37
|
+
| **R-09** | 이미지 `alt` 누락 | WCAG 1.1.1 |
|
|
38
|
+
| **R-10** | `div/span 클릭 핸들러 패턴` 처럼 div를 인터랙티브로 쓰는 패턴 | 시맨틱 HTML 사용 (button/a) |
|
|
39
|
+
| **R-11** | `:focus { outline: none }` | 접근성 위반. focus-visible은 reset.css 전역 처리 |
|
|
40
|
+
| **R-12** | 색상 대비 부족 (일반 4.5:1 / 큰 텍스트 3:1 미만) | WCAG 1.4.3 — pa11y-ci 자동 검증 |
|
|
41
|
+
| **R-13** | 터치 영역 44×44px 미만 인터랙티브 요소 (모바일 컨텍스트) | WCAG 2.5.5 · INFOMIND 정책 |
|
|
42
|
+
| **R-14** | 페이지에 `.skip-to-content` 건너뛰기 링크 누락 | WCAG 2.4.1 — `<body>` 진입 직후 필수 |
|
|
43
|
+
| **R-15** | 기존 인포마인드 HTML 기본 골격을 무시한 임의 구조 | 유지보수 패턴 단절 |
|
|
44
|
+
| **R-16** | 인터랙티브 컴포넌트(modal/tab/accordion/tooltip/disclosure/carousel/calendar) 필수 ARIA 누락 | 스크린리더 접근 불가 |
|
|
45
|
+
| **R-17** | 비-BEM 상태 클래스 (`.is-active`, `.has-error`, `.is-open` 등) | BEM modifier + ARIA로만 표현 |
|
|
46
|
+
| **R-18** | 시각적 단어 modifier (`--blue`, `--red`, `--big`, `--rounded`, `--shadow` 등) | 의미 기반 modifier만 |
|
|
47
|
+
| — | 폼 요소 `<label>` 누락 (placeholder만 사용) | WCAG 3.3.2 |
|
|
48
|
+
|
|
49
|
+
**모호한 케이스는 사용자에게 질문한다.** 자체 판단으로 우회하지 않는다.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. 절대 준수 (MUST)
|
|
54
|
+
|
|
55
|
+
| 항목 | 규정 |
|
|
56
|
+
|------|------|
|
|
57
|
+
| 사이트 유형 판정 | 코드 생성 전 `references/project-profiles.md` 기준으로 일반사이트/공공서비스/공공기관/CMS·관리자/커머스·예약 중 먼저 판정 |
|
|
58
|
+
| 색상 | `var(--color-*)` 시맨틱 토큰만 사용 |
|
|
59
|
+
| 간격/크기 | CSS/Tailwind 직접값. CMS·관리자 화면은 정보 밀도에 맞게 조정 |
|
|
60
|
+
| 폰트 | `var(--font-sans)`, `var(--font-mono)` — 직접 폰트 패밀리 작성 금지 |
|
|
61
|
+
| 폰트 사이즈 | 프로젝트 타입 계층 우선. 토큰화하지 않음 |
|
|
62
|
+
| CSS 작성 | 표준 CSS nesting 허용. Tailwind v4 `@apply`, `@theme`, `@utility` 사용 가능 |
|
|
63
|
+
| 반응형 작성 | 단순 속성 변경은 `@apply tablet:*` / `@apply pc:*` 우선. 복잡한 중첩 선택자만 관련 선택자 내부 `@media` 사용 |
|
|
64
|
+
| line-height | 한국어 가독성을 위해 1.4~1.6 권장. `line-height: 1` 금지 |
|
|
65
|
+
| 반경/그림자 | CSS/Tailwind 직접값 |
|
|
66
|
+
| z-index | CSS/Tailwind 직접값. 레이어 의미가 보이도록 주석 또는 클래스명으로 관리 |
|
|
67
|
+
| 모션 | CSS/Tailwind 직접값. 접근성 `prefers-reduced-motion` 고려 |
|
|
68
|
+
| 터치 영역 | 44px 이상 — 모바일 인터랙티브 요소 강제 |
|
|
69
|
+
| 브레이크포인트 | 360 / 768 / 1280 (3단계, INFOUX 표준) |
|
|
70
|
+
| 컨테이너 max-width | 1200px (KRDS contents-size) |
|
|
71
|
+
| BEM | `.block__element--modifier` (소문자, 하이픈) — 5-objects · 6-components 레이어에만 |
|
|
72
|
+
| ITCSS | 5-layer 순서: 3-generic → 4-elements → 5-objects → 6-components → 7-utilities (`tokens/`는 외부 소스) |
|
|
73
|
+
| @import (CSS) | Tailwind v4 `@import` 사용. SCSS `@use/@forward` 또는 `@import` 사용 금지 (SCSS 자체가 폐기됨) |
|
|
74
|
+
| 다크/고대비 | `[data-color-mode="high-contrast"]` 어트리뷰트로 자동 대응 — 컴포넌트 코드에 색 수동 분기 작성 금지 |
|
|
75
|
+
| HTML 기본 구조 | 큰 영역은 `header/main/footer`, `main` 안은 `section > .container` 구조 |
|
|
76
|
+
| HTML 컴포넌트화 | 페이지 전체가 아니라 `main` 내부의 section 단위로 분리 |
|
|
77
|
+
| 조건부 정부/공공 요소 | 공식 배너, 정부 상징, 운영기관 식별자, 공공 푸터 필수 링크는 적용 대상이 확인된 경우에만 생성. 민간·사내·일반 CMS에서는 N/A |
|
|
78
|
+
| Page shell | `<a href="#main" class="skip-to-content">` → `header#header` → `main#main` → `footer#footer` 순서. `main` 직계 자식은 section |
|
|
79
|
+
| Section 구조 | 각 `section`은 `.container`를 직접 포함하고 heading 또는 `aria-labelledby`/`aria-label`로 접근 가능한 이름 제공 |
|
|
80
|
+
| 컴포넌트 root 태그 | `references/html-semantics.md`는 참고 기준. 기존 패턴 위에 시맨틱/ARIA 보강 |
|
|
81
|
+
| 인터랙티브 위젯 ARIA | WAI-ARIA 1.2 APG 패턴 + KRDS 보강 — modal `aria-modal`+`aria-labelledby`, tab `role`+`aria-selected`+`aria-controls`, accordion `aria-expanded`+`aria-controls` 등 |
|
|
82
|
+
| 상태 표현 | BEM modifier (시각) + ARIA 속성 (의미) 동시 사용 — `.tab__item--selected` + `aria-selected="true"` |
|
|
83
|
+
| modifier 어휘 | KRDS 정의만: variant(`--primary/--secondary/--tertiary/--text`), 사이즈(`--xsmall..--xlarge`), 상태(`--selected/--disabled/--expanded/--loading/--error/--success/--current/--done/--todo`), 톤(`--info/--success/--warning/--danger/--inverse`), 레이아웃(`--horizontal/--vertical/--block/--inline`), section archetype(`--intro/--content/--list/--form/--data/--search/--process/--notice`) |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. references — 작업 맥락별 로드 순서
|
|
88
|
+
|
|
89
|
+
**필요한 파일만 로드**한다. 전체를 한꺼번에 읽지 않는다.
|
|
90
|
+
|
|
91
|
+
| 작업 맥락 | 로드 |
|
|
92
|
+
|-----------|------|
|
|
93
|
+
| 코드 생성 전 사이트 유형 판정 | `references/project-profiles.md` |
|
|
94
|
+
| 토큰 결정 (색상·기본 폰트·브레이크포인트) | `references/krds-tokens.md` |
|
|
95
|
+
| 컴포넌트 마크업 작성 — 시각/스타일 카탈로그 | `references/krds-components.md` |
|
|
96
|
+
| 컴포넌트 마크업 작성 — 인포마인드 기본 골격·ARIA·키보드 | 기존 인포마인드 구축 사이트 HTML + `references/html-semantics.md` |
|
|
97
|
+
| Tailwind 유틸리티 사용 | `references/tailwind-mapping.md` |
|
|
98
|
+
| 접근성 검증 | `references/accessibility.md` |
|
|
99
|
+
| 코드 검토 — 무엇을 막을지 | `references/forbidden-patterns.md` |
|
|
100
|
+
| 이 스킬을 개발팀 레포에 임포트 | `references/guide-import.md` |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 4. 작업 시작 전 자가 점검
|
|
105
|
+
|
|
106
|
+
코드를 한 줄이라도 작성하기 전에:
|
|
107
|
+
|
|
108
|
+
1. ✅ 사이트 유형을 일반사이트, 공공서비스, 공공기관, CMS·관리자, 커머스·예약 중 하나로 판정했는가?
|
|
109
|
+
2. ✅ 정부 상징/공식 배너/운영기관 식별자 적용 여부가 확인됐는가? 확인되지 않았으면 생성하지 않았는가?
|
|
110
|
+
3. ✅ 사용하려는 색상은 `--color-*` 시맨틱 토큰인가?
|
|
111
|
+
4. ✅ 폰트는 `--font-sans` 또는 `--font-mono` 기준인가?
|
|
112
|
+
5. ✅ CSS nesting과 `@apply`를 쓰더라도 결과 구조가 읽기 쉬운가?
|
|
113
|
+
6. ✅ 기존 컴포넌트 패턴으로 해결 가능한가?
|
|
114
|
+
7. ✅ `main` 직계는 section이고 각 section에 `.container`와 접근 가능한 이름이 있는가?
|
|
115
|
+
8. ✅ 인터랙티브 요소는 `<button>`/`<a>`/시맨틱 HTML인가?
|
|
116
|
+
9. ✅ 이미지 `alt`, 폼 `<label>`, focus 처리 모두 충족했는가?
|
|
117
|
+
10. ✅ 모바일 터치 영역 44px 이상인가?
|
|
118
|
+
|
|
119
|
+
위 10개 중 하나라도 No면 **작성 중단 → 사용자에게 어느 항목이 막히는지 보고** + 어떻게 해결할지 옵션 제시.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 5. 옛 시스템 흔적 발견 시
|
|
124
|
+
|
|
125
|
+
이전 infoUX (KRDS 미적용 시절) 흔적을 발견하면:
|
|
126
|
+
|
|
127
|
+
- 옛 토큰명 (`--color-text-secondary`, 옛 spacing/font-size/radius/transition/leading 토큰) → 현행 토큰 또는 프로젝트 규칙으로 교체 권장
|
|
128
|
+
- 옛 variant (`btn--ghost`, `btn--outline`, `btn--link`) → KRDS 4 variant로 매핑
|
|
129
|
+
- SCSS 파일 (`*.scss`, `@use`, `@forward`) → CSS로 마이그레이션 필요 (SCSS 폐기됨)
|
|
130
|
+
|
|
131
|
+
**자동 마이그레이션은 하지 말고, 사용자에게 어디를 바꿀지 보고**한 후 명시적 지시받고 진행한다.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 6. 작업 후 검증
|
|
136
|
+
|
|
137
|
+
infoUX 하네스가 설치된 프로젝트에서는 가능한 범위에서 다음을 실행한다:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm run check
|
|
141
|
+
npm run lint
|
|
142
|
+
npm run build
|
|
143
|
+
npm run test
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
외부 개발팀 프로젝트처럼 infoUX 하네스가 없는 경우에도 자체 검증을 생략하지 않는다.
|
|
147
|
+
|
|
148
|
+
1. 프로젝트의 `package.json`, CI 설정, README에서 기존 `check`/`lint`/`build`/`test`/`a11y` 명령을 먼저 찾아 실행한다.
|
|
149
|
+
2. `scripts/check-violations.js`, `scripts/check-html-structure.js`, `pa11y`/`axe` 설정이 없으면 변경 파일을 직접 점검한다.
|
|
150
|
+
3. 직접 점검 최소 항목:
|
|
151
|
+
- raw `hex`/`rgb`/`hsl`, Tailwind raw 컬러 유틸, SCSS 문법, `!important`
|
|
152
|
+
- 핵심 CSS의 `:has()` 의존, `:focus { outline: none }`
|
|
153
|
+
- BEM 2단계 element, 비-BEM 상태 클래스, 시각적 modifier
|
|
154
|
+
- `div/span` 클릭 핸들러, 이미지 `alt`, 폼 `label`, 필수 ARIA, skip link/page shell
|
|
155
|
+
- 모바일 360px 기준 터치 영역 44px 이상, 텍스트 겹침, 가로 overflow
|
|
156
|
+
4. 최종 보고에는 실행한 자동 검증, 대체한 수동 검증, 남은 수동 QA 항목을 구분해 적는다.
|
|
157
|
+
|
|
158
|
+
위반 발견 시 **즉시 작업을 중단하고 사용자에게 위반 항목·근거·수정 옵션을 보고**한다.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 7. 모호함 해소 — 사용자에게 질문할 케이스
|
|
163
|
+
|
|
164
|
+
다음은 자체 판단 금지. 사용자에게 묻는다:
|
|
165
|
+
|
|
166
|
+
- 새 컴포넌트를 공통화할지 일회성 프로젝트 패턴으로 둘지 모호
|
|
167
|
+
- 사이트가 일반사이트/공공서비스/공공기관/CMS·관리자/커머스·예약 중 무엇인지 모호
|
|
168
|
+
- 공공기관 프로젝트지만 공식 배너, 정부 상징, 운영기관 식별자 적용 여부가 확인되지 않음
|
|
169
|
+
- KRDS 수치 체계를 그대로 따를지 프로젝트 밀도에 맞게 조정할지 모호
|
|
170
|
+
- 두 토큰 중 어느 쪽이 적절한지 모호 (예: `--color-text` vs `--color-text-bolder`)
|
|
171
|
+
- 옛 시스템 흔적을 발견 — 어떻게 처리할지
|
|
172
|
+
- 모바일/PC에서 다른 동작이 필요한데 어느 브레이크포인트에서 분기할지 모호
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 8. 카탈로그에 없는 컴포넌트 요구 시
|
|
177
|
+
|
|
178
|
+
`references/krds-components.md`에 정의된 28개 컴포넌트 외에 새 컴포넌트가 필요해 보이면:
|
|
179
|
+
|
|
180
|
+
1. 기존 컴포넌트 조합으로 해결 가능한지 먼저 확인
|
|
181
|
+
2. 새 패턴이 더 적절하면 사용자에게 근거 보고
|
|
182
|
+
3. 옵션 제시:
|
|
183
|
+
- (a) 일회성 프로젝트 패턴으로 구현
|
|
184
|
+
- (b) 공통 컴포넌트 후보로 등록
|
|
185
|
+
4. 공통화 가치가 있으면 코드 주석이나 문서에 `/* TODO: UX팀 정식 컴포넌트화 검토 */` 명시
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 9. 출처 — 단일 소스
|
|
190
|
+
|
|
191
|
+
이 스킬의 콘텐츠는 자동 생성된다. 직접 수정 금지.
|
|
192
|
+
|
|
193
|
+
- 토큰 출처: `tokens/foundation.json`
|
|
194
|
+
- 컴포넌트 출처: `src/styles/6-components/*.css` + `src/snippets/*.md`
|
|
195
|
+
- 빌드: `node scripts/build-skill.js` → `skill/` 폴더 갱신
|
|
196
|
+
- 배포: `references/` → `npm run build:mcp` → infoUX MCP. 팀원은 서버 등록만 하면 최신 기준을 받는다
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 10. 분쟁 시 우선순위
|
|
201
|
+
|
|
202
|
+
여러 규정이 충돌하면 다음 순서로 우선:
|
|
203
|
+
|
|
204
|
+
1. **접근성 (WCAG/KWCAG AA)** — 양보 불가
|
|
205
|
+
2. **프로젝트 목적과 사용성** — CMS/관리자 화면은 업무 밀도와 반복 효율 우선
|
|
206
|
+
3. **사이트 유형 판정** — 정부 아이덴티티 요소는 적용 조건이 확인된 경우만
|
|
207
|
+
4. **INFOUX 파운데이션** — `tokens/foundation.json`의 색상·기본 폰트·브레이크포인트 기준
|
|
208
|
+
5. **KRDS 정본** — 접근성·구조·색상 참고 기준
|
|
209
|
+
6. **프로젝트 스타일** — 프로젝트별 CSS 확장
|
|
210
|
+
7. **사용자 명시 지시** — 단, 접근성을 깨면 사용자에게 확인
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
> 이 스킬이 활성화된 이상, 모든 LLM 출력은 위 규정의 적용 대상이다.
|
|
215
|
+
> 규정에 모호한 부분이 있으면 자체 판단하지 말고 사용자에게 묻는다.
|