sellmate-design-system-react 9.0.0-beta.74 → 9.0.0-beta.76
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/AGENTS.md +37 -2
- package/README.md +5 -5
- package/bin/sellmate-ds.mjs +91 -59
- package/dist/components/SCircleProgress/README.md +6 -3
- package/dist/components/SCircleProgress/SCircleProgress.d.ts +11 -3
- package/dist/components/SLauncherListBox/README.md +1 -1
- package/dist/components/SLauncherListBox/launcherListBox.config.d.ts +2 -2
- package/dist/components/SLinearProgress/README.md +2 -2
- package/dist/components/SLinearProgress/SLinearProgress.d.ts +8 -2
- package/dist/components/SLogo/README.md +1 -0
- package/dist/components/SLogo/logo.config.d.ts +1 -1
- package/dist/components/SSelect/README.md +18 -2
- package/dist/components/SSelect/SSelect.d.ts +28 -5
- package/dist/components/SSelect/index.d.ts +1 -1
- package/dist/components/STable/README.md +29 -20
- package/dist/components/STable/STable.d.ts +47 -31
- package/dist/components/STable/index.d.ts +1 -1
- package/dist/index.cjs +167 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +167 -50
- package/dist/index.js.map +1 -1
- package/dist/llms-full.txt +94 -31
- package/dist/llms.txt +39 -3
- package/dist/styles.css +16 -0
- package/dist/theme.css +3 -0
- package/eslint/index.mjs +42 -42
- package/eslint/lib/class-names.mjs +23 -25
- package/eslint/lib/table-column.mjs +11 -11
- package/eslint/rules/component-group-gap.mjs +32 -30
- package/eslint/rules/divider-vertical-height.mjs +22 -22
- package/eslint/rules/field-width-grade.mjs +50 -50
- package/eslint/rules/no-arbitrary-class.mjs +63 -20
- package/eslint/rules/no-off-scale-spacing.mjs +61 -41
- package/eslint/rules/no-raw-html-control.mjs +38 -36
- package/eslint/rules/prefer-typo-preset.mjs +23 -17
- package/eslint/rules/require-locale-number.mjs +14 -19
- package/eslint/rules/table-column-width.mjs +21 -17
- package/eslint/rules/table-numeric-align.mjs +14 -14
- package/eslint/scale.gen.mjs +1 -1
- package/package.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -1053,6 +1053,25 @@ const columns: STableColumn[] = [
|
|
|
1053
1053
|
- **계산값은 픽셀 단위까지 맞추면 어긋난다** — 서브픽셀 반올림 때문이다. 여유 8px 을 얹고 8 단위로 올림한다.
|
|
1054
1054
|
- 좌우 패딩은 `STable` 이 토큰으로 넣으므로 직접 주지 않는다. 그만큼을 뺀 나머지가 요소 몫이라는 점만 계산에 넣는다.
|
|
1055
1055
|
|
|
1056
|
+
#### 행 타입을 알면 넘긴다
|
|
1057
|
+
|
|
1058
|
+
`STable`·`STableColumn` 은 행 타입을 받는다. 넘기면 `render`·`format`·`onRowClick`·`selected` 가 그 타입으로 좁혀져, 없는 키나 오타가 컴파일에서 잡힌다. 넘기지 않으면 행은 `SRow`(키만 아는 느슨한 레코드)다 — 기존 코드는 그대로 동작한다.
|
|
1059
|
+
|
|
1060
|
+
```tsx
|
|
1061
|
+
interface Order { id: number; orderNo: string; qty: number }
|
|
1062
|
+
|
|
1063
|
+
const columns: STableColumn<Order>[] = [
|
|
1064
|
+
{ name: 'orderNo', label: '주문번호', field: 'orderNo', width: 140, align: 'center' },
|
|
1065
|
+
{ name: 'qty', label: '수량', field: 'qty', width: 80, align: 'right',
|
|
1066
|
+
format: v => `${Number(v).toLocaleString()}개` },
|
|
1067
|
+
];
|
|
1068
|
+
|
|
1069
|
+
<STable<Order> columns={columns} rows={orders} onRowClick={order => open(order.id)} />
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
- 행 타입을 넘기면 `format`·`render` 의 **셀 값은 `unknown`** 이다. `field` 가 접근 함수일 수도 있어 행 타입만으로는 값의 타입이 정해지지 않으므로, `Number(v)`·`String(v)` 로 받는 쪽에서 확인한다. 행이 필요하면 두 번째 인자(`format`)·첫 번째 인자(`render`)의 행을 쓴다.
|
|
1073
|
+
- `SSelect` 도 같은 방식으로 `onValueChange` 의 값 타입을 받는다 — `<SSelect<string> valueAsPrimitive … />`. 실제 모양은 `type`·`valueAsPrimitive` 조합이 정하므로 그 조합에 맞는 타입을 넘긴다.
|
|
1074
|
+
|
|
1056
1075
|
#### 컨트롤이 들어가는 컬럼
|
|
1057
1076
|
|
|
1058
1077
|
`<td>` 는 폭을 넘는 내용을 잘라낸다(`overflow: hidden`). 텍스트라면 말줄임으로 끝나지만, 셀에 `STag` · `SButton` · `SGhostButton` · `SSelect` · `SInput` · `SNumberInput` 처럼 **자기 폭을 가진 요소**를 넣으면 요소 자체가 잘려 **누르거나 읽거나 입력할 수 없게 된다.**
|
|
@@ -1580,7 +1599,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1580
1599
|
```
|
|
1581
1600
|
|
|
1582
1601
|
- **`launcher` 를 주지 않으면 런처 버튼 자체가 렌더되지 않는다.** 옮겨 갈 서비스가 없는 앱은 이 슬롯을 비워 둔다 — 눌러도 아무것도 없는 버튼을 상단바에 남기지 않는다.
|
|
1583
|
-
- **서비스는 키(`service`)로 고른다 — 보이는 것은 아무것도 넘기지 않는다.** 쓸 수 있는 키는 `SLogo` 가 로고를 가진 서비스 전부(`'sellmate' | 'wms' | 'chat' | 'plm' | 'account' | 'people' | 'crm' | 'work' | 'pos'` — `SLauncherService`)이고, 서비스 로고(워드마크와 `WMS`·`Account`·`CRM` … 표기가 한 덩어리인 브랜드 로고)·아래 줄 한글 이름·상태 태그(`NEW`·`출시예정`)·출시 여부를 카탈로그(`LAUNCHER_SERVICE_CATALOG`)가 그 키로 정한다. 앱이 넘기게 하면 같은 서비스가 앱마다 다른 이름·색·문구로 선다. **서비스가 늘거나 이름·상태가 바뀌면 디자인 시스템 릴리스로 따라온다** — 앱은 고칠 것이 없다.
|
|
1602
|
+
- **서비스는 키(`service`)로 고른다 — 보이는 것은 아무것도 넘기지 않는다.** 쓸 수 있는 키는 `SLogo` 가 로고를 가진 서비스 전부(`'sellmate' | 'wms' | 'chat' | 'chatbot' | 'plm' | 'account' | 'people' | 'crm' | 'work' | 'pos'` — `SLauncherService`)이고, 서비스 로고(워드마크와 `WMS`·`Account`·`CRM` … 표기가 한 덩어리인 브랜드 로고)·아래 줄 한글 이름·상태 태그(`NEW`·`출시예정`)·출시 여부를 카탈로그(`LAUNCHER_SERVICE_CATALOG`)가 그 키로 정한다. 앱이 넘기게 하면 같은 서비스가 앱마다 다른 이름·색·문구로 선다. **서비스가 늘거나 이름·상태가 바뀌면 디자인 시스템 릴리스로 따라온다** — 앱은 고칠 것이 없다.
|
|
1584
1603
|
|
|
1585
1604
|
```tsx
|
|
1586
1605
|
✅ { service: 'wms', href: env.WMS_URL }
|
|
@@ -1733,6 +1752,11 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1733
1752
|
- 카드 안에 상태 배지를 넣으려면 `tag` 슬롯에 `STag` 를 준다. 라벨 문자열에 "(추천)" 처럼 섞어 쓰지 않는다.
|
|
1734
1753
|
- **카드처럼 생겼다고 `SCard`/`SSectionHeaderCard` 로 감싸지 않는다.** `SRadioCard` 자체가 완결된 요소이고, 나열 간격은 `SRadioCardGroup` 의 `direction` 이 맞춘다 (§2-2).
|
|
1735
1754
|
|
|
1755
|
+
##### 옵션을 서버에서 받는 동안 — `loading`
|
|
1756
|
+
|
|
1757
|
+
- **받는 동안 `loading` 을 켠다.** 트리거의 펼침 아이콘 자리에 스피너가, 드롭다운에 로딩 행이 뜬다. 첫 조회든 다음 페이지든 같은 prop 이다.
|
|
1758
|
+
- **셀렉트 옆에 따로 스피너를 두거나 `disabled` 로 잠가 로딩을 표현하지 않는다.** 로딩 중에도 열 수 있고, 열면 "표시할 항목이 없습니다." 대신 로딩 행이 나온다. `disabled` 와 `loading` 을 함께 주지 않는다 — 비활성이면 `loading` 을 무시해 스피너가 뜨지 않는다. 잠긴 셀렉트의 옵션은 잠금이 풀릴 때 받는다.
|
|
1759
|
+
|
|
1736
1760
|
##### 옵션이 수백~수천 개면 — `onReachEnd`
|
|
1737
1761
|
|
|
1738
1762
|
**렌더는 걱정하지 않아도 된다.** `SSelect` 는 언제나 보이는 범위의 행만 그린다 — 켜고 끄는 prop 이 없고, 옵션이 5개든 5,000개든 여는 비용이 같다. 행 높이가 균일하다고 가정하지도 않으므로 계층 목록이나 큰 글씨가 섞인 라벨도 그대로 넘기면 된다.
|
|
@@ -1993,6 +2017,17 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
1993
2017
|
|
|
1994
2018
|
`SCircleProgress` 는 `indeterminate` 로 두면 스피너가 된다. **다만 화면이나 영역을 막아야 하는 상황이면 progress 가 아니라 `SLoadingModal`·`SLoadingContainer` 다** (§3-2) — 진행 표시와 입력 차단은 다른 일이고, 막지 않으면 사용자가 로딩 중에 또 누른다.
|
|
1995
2019
|
|
|
2020
|
+
`type` 은 진행 **상태**로 고른다. 색이 아니라 상태가 기준이다.
|
|
2021
|
+
|
|
2022
|
+
| 상태 | `type` |
|
|
2023
|
+
| --- | --- |
|
|
2024
|
+
| 정상 진행 중 | `primary` |
|
|
2025
|
+
| 진행은 계속되지만 제품이 정한 기준에 미달해 주의가 필요하다 | `warning` |
|
|
2026
|
+
| 실패·중단 등 더 이상 정상적으로 진행되지 않는다 | `error` |
|
|
2027
|
+
| 완료 | `complete` |
|
|
2028
|
+
|
|
2029
|
+
**`warning` 과 `error` 는 "아직 진행 중인가" 로 가른다.** 기준 미달이어도 진행이 계속되면 `warning` 이다 — `error` 로 칠하면 멈춘 작업으로 읽힌다. 미달 기준은 디자인 시스템이 정하지 않으므로 제품 코드에서 판정해 `type` 을 넘긴다.
|
|
2030
|
+
|
|
1996
2031
|
#### 3-7-10. SPortal — 직접 쓸 일이 거의 없다
|
|
1997
2032
|
|
|
1998
2033
|
`STooltip`·`SPopover`·`SSelect`·날짜 피커가 내부에서 쓰는 저수준 레이어다. 앵커에 붙여 띄우는 동작이 필요하면 **먼저 §3-3 에서 대응 컴포넌트를 찾는다.** `SPortal` 을 직접 쓰는 것은 그 넷 중 어느 것도 아닌 새로운 부착형 레이어를 만들 때뿐이고, 그때도 모달 안에서 열릴 수 있다면 소속 컨테이너를 맞춰야 한다.
|
|
@@ -2046,7 +2081,7 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
2046
2081
|
|
|
2047
2082
|
셀메이트·서비스 로고는 `<svg>` 를 직접 붙이지 않고 `SLogo` 를 쓴다. **`SIcon` 과 같은 손맛(`name`·`size`·`color`)이지만 두 가지가 다르다.**
|
|
2048
2083
|
|
|
2049
|
-
- **`name` 에 서비스를 넣으면 그 서비스 로고가 나온다**(`sellmate` · `wms` · `chat` · `plm` · `account` · `people` · `crm` · `work` · `pos` · `console` — `SLogoName`). [셀메이트 워드마크 + 서비스 표기]가 한 덩어리다. **워드마크 옆에 표기를 앱이 따로 잇지 않는다** — 표기의 서체·자간·색이 원본과 어긋난다.
|
|
2084
|
+
- **`name` 에 서비스를 넣으면 그 서비스 로고가 나온다**(`sellmate` · `wms` · `chat` · `chatbot` · `plm` · `account` · `people` · `crm` · `work` · `pos` · `console` — `SLogoName`). [셀메이트 워드마크 + 서비스 표기]가 한 덩어리다. **워드마크 옆에 표기를 앱이 따로 잇지 않는다** — 표기의 서체·자간·색이 원본과 어긋난다.
|
|
2050
2085
|
- **배경에 맞춰 `mode` 를 준다.** 밝은 면은 `light`(기본), 어두운 면(dark GNB 등)은 `dark`. 워드마크 색과 서비스 표기 색을 모드가 정하므로 색을 직접 주지 않는다.
|
|
2051
2086
|
- **표기를 워드마크 아래에 두려면 `direction="vertical"` 을 준다**(로그인 카드처럼 폭이 좁고 로고가 가운데 서는 자리). 워드마크 아래에 표기를 앱이 따로 쌓지 않는다. 모든 서비스 로고가 세로를 가진다(`sellmate` 는 표기가 없어 어느 쪽이든 같다). 세로의 표기 색도 원본이 정하므로 색을 직접 맞추지 않는다.
|
|
2052
2087
|
- **`size` 는 높이다.** 로고는 가로로 길어 정사각으로 그리면 찌그러진다 — 폭은 비율에서 저절로 나오므로 주지 않는다. `vertical` 에서는 두 줄 전체의 높이라, 같은 `size` 면 워드마크가 가로보다 작다.
|
package/README.md
CHANGED
|
@@ -261,12 +261,12 @@ export default [
|
|
|
261
261
|
**폭은 등급 이름으로 줍니다** — `width="md"` 처럼 씁니다. 등급은 `--cmp-field-width-*` 토큰으로
|
|
262
262
|
풀리므로 토큰이 바뀌면 화면이 따라갑니다. 같은 값을 px 로 적으면 그 화면만 옛 값에 남습니다.
|
|
263
263
|
|
|
264
|
-
| 등급 | 쓰는 곳
|
|
265
|
-
| ---- |
|
|
264
|
+
| 등급 | 쓰는 곳 |
|
|
265
|
+
| ---- | -------------- |
|
|
266
266
|
| `xs` | 숫자 필드 전용 |
|
|
267
|
-
| `sm` |
|
|
268
|
-
| `md` |
|
|
269
|
-
| `lg` |
|
|
267
|
+
| `sm` | |
|
|
268
|
+
| `md` | |
|
|
269
|
+
| `lg` | |
|
|
270
270
|
| `xl` | 정책상 상한 |
|
|
271
271
|
|
|
272
272
|
```tsx
|
package/bin/sellmate-ds.mjs
CHANGED
|
@@ -12,23 +12,28 @@
|
|
|
12
12
|
* - 안전하게 고칠 수 없는 파일은 손대지 않고 붙여넣을 스니펫을 출력한다.
|
|
13
13
|
* - 무엇을 바꿨는지 전부 보고한다.
|
|
14
14
|
*/
|
|
15
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from
|
|
16
|
-
import { join, relative, dirname, sep } from
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
16
|
+
import { join, relative, dirname, sep } from 'node:path';
|
|
17
17
|
|
|
18
|
-
const PKG =
|
|
18
|
+
const PKG = 'sellmate-design-system-react';
|
|
19
19
|
const LLMS_PATH = `node_modules/${PKG}/dist/llms.txt`;
|
|
20
20
|
|
|
21
21
|
const args = process.argv.slice(2);
|
|
22
|
-
const command = args.find(
|
|
23
|
-
const dryRun = args.includes(
|
|
22
|
+
const command = args.find(a => !a.startsWith('-')) ?? 'init';
|
|
23
|
+
const dryRun = args.includes('--dry-run') || args.includes('-n');
|
|
24
24
|
|
|
25
25
|
const cwd = process.cwd();
|
|
26
26
|
|
|
27
27
|
/* ─────────────────────────── 출력 ─────────────────────────── */
|
|
28
28
|
|
|
29
29
|
const c = {
|
|
30
|
-
reset:
|
|
31
|
-
|
|
30
|
+
reset: '\x1b[0m',
|
|
31
|
+
bold: '\x1b[1m',
|
|
32
|
+
dim: '\x1b[2m',
|
|
33
|
+
green: '\x1b[32m',
|
|
34
|
+
yellow: '\x1b[33m',
|
|
35
|
+
cyan: '\x1b[36m',
|
|
36
|
+
red: '\x1b[31m',
|
|
32
37
|
};
|
|
33
38
|
const paint = (color, s) => (process.stdout.isTTY ? `${c[color]}${s}${c.reset}` : s);
|
|
34
39
|
|
|
@@ -38,7 +43,7 @@ const add = (status, file, detail, snippet) => results.push({ status, file, deta
|
|
|
38
43
|
|
|
39
44
|
/* ─────────────────────────── 유틸 ─────────────────────────── */
|
|
40
45
|
|
|
41
|
-
const read =
|
|
46
|
+
const read = p => readFileSync(p, 'utf8');
|
|
42
47
|
|
|
43
48
|
function write(path, content) {
|
|
44
49
|
if (dryRun) return;
|
|
@@ -47,14 +52,14 @@ function write(path, content) {
|
|
|
47
52
|
|
|
48
53
|
/** node_modules 가 있는 프로젝트 루트인지 확인 */
|
|
49
54
|
function assertProjectRoot() {
|
|
50
|
-
if (!existsSync(join(cwd,
|
|
51
|
-
console.error(paint(
|
|
55
|
+
if (!existsSync(join(cwd, 'package.json'))) {
|
|
56
|
+
console.error(paint('red', 'package.json 이 없습니다. 프로젝트 루트에서 실행하세요.'));
|
|
52
57
|
process.exit(1);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/** 후보 경로 중 처음 존재하는 것 */
|
|
57
|
-
const firstExisting =
|
|
62
|
+
const firstExisting = candidates => candidates.find(p => existsSync(join(cwd, p)));
|
|
58
63
|
|
|
59
64
|
/* ────────────────── 1. 에이전트 지침 파일 ────────────────── */
|
|
60
65
|
|
|
@@ -73,24 +78,24 @@ UI 작업 전 \`${LLMS_PATH}\` 를 **반드시 먼저 읽는다.**
|
|
|
73
78
|
`;
|
|
74
79
|
|
|
75
80
|
function stepAgentInstructions() {
|
|
76
|
-
const existing = [
|
|
77
|
-
const targets = existing.length ? existing : [
|
|
81
|
+
const existing = ['CLAUDE.md', 'AGENTS.md'].filter(f => existsSync(join(cwd, f)));
|
|
82
|
+
const targets = existing.length ? existing : ['CLAUDE.md'];
|
|
78
83
|
|
|
79
84
|
for (const file of targets) {
|
|
80
85
|
const path = join(cwd, file);
|
|
81
|
-
const current = existsSync(path) ? read(path) :
|
|
86
|
+
const current = existsSync(path) ? read(path) : '';
|
|
82
87
|
|
|
83
88
|
if (current.includes(LLMS_PATH)) {
|
|
84
|
-
add(
|
|
89
|
+
add('skipped', file, '이미 llms.txt 참조가 있습니다');
|
|
85
90
|
continue;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
const next = current
|
|
89
|
-
? `${current.replace(/\s*$/,
|
|
94
|
+
? `${current.replace(/\s*$/, '')}\n${AGENT_SECTION}`
|
|
90
95
|
: `# 프로젝트 지침\n${AGENT_SECTION}`;
|
|
91
96
|
|
|
92
97
|
write(path, next);
|
|
93
|
-
add(
|
|
98
|
+
add('added', file, existsSync(path) ? '디자인 시스템 지침 섹션 추가' : '생성 후 지침 추가');
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -105,8 +110,8 @@ function matchingBracket(src, openIndex) {
|
|
|
105
110
|
let depth = 0;
|
|
106
111
|
for (let i = openIndex; i < src.length; i++) {
|
|
107
112
|
const ch = src[i];
|
|
108
|
-
if (ch ===
|
|
109
|
-
else if (ch ===
|
|
113
|
+
if (ch === '[') depth++;
|
|
114
|
+
else if (ch === ']') {
|
|
110
115
|
depth--;
|
|
111
116
|
if (depth === 0) return i;
|
|
112
117
|
}
|
|
@@ -116,11 +121,19 @@ function matchingBracket(src, openIndex) {
|
|
|
116
121
|
|
|
117
122
|
function stepEslint() {
|
|
118
123
|
const file = firstExisting([
|
|
119
|
-
|
|
124
|
+
'eslint.config.mjs',
|
|
125
|
+
'eslint.config.js',
|
|
126
|
+
'eslint.config.ts',
|
|
127
|
+
'eslint.config.cjs',
|
|
120
128
|
]);
|
|
121
129
|
|
|
122
130
|
if (!file) {
|
|
123
|
-
add(
|
|
131
|
+
add(
|
|
132
|
+
'manual',
|
|
133
|
+
'eslint.config.mjs',
|
|
134
|
+
'설정 파일이 없습니다 — 아래 내용으로 만드세요',
|
|
135
|
+
ESLINT_SNIPPET,
|
|
136
|
+
);
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
126
139
|
|
|
@@ -128,7 +141,7 @@ function stepEslint() {
|
|
|
128
141
|
const src = read(path);
|
|
129
142
|
|
|
130
143
|
if (src.includes(`${PKG}/eslint`)) {
|
|
131
|
-
add(
|
|
144
|
+
add('skipped', file, '이미 ESLint 프리셋이 연결되어 있습니다');
|
|
132
145
|
return;
|
|
133
146
|
}
|
|
134
147
|
|
|
@@ -136,14 +149,14 @@ function stepEslint() {
|
|
|
136
149
|
// 배열 경계를 확신할 수 없으므로 손대지 않고 스니펫을 안내한다.
|
|
137
150
|
const exportMatch = src.match(/export\s+default\s*\[/);
|
|
138
151
|
if (!exportMatch) {
|
|
139
|
-
add(
|
|
152
|
+
add('manual', file, '자동 삽입이 어려운 형태입니다 — 아래를 직접 추가하세요', ESLINT_SNIPPET);
|
|
140
153
|
return;
|
|
141
154
|
}
|
|
142
155
|
|
|
143
|
-
const openIndex = src.indexOf(
|
|
156
|
+
const openIndex = src.indexOf('[', exportMatch.index);
|
|
144
157
|
const closeIndex = matchingBracket(src, openIndex);
|
|
145
158
|
if (closeIndex === -1) {
|
|
146
|
-
add(
|
|
159
|
+
add('manual', file, '배열 끝을 찾지 못했습니다 — 아래를 직접 추가하세요', ESLINT_SNIPPET);
|
|
147
160
|
return;
|
|
148
161
|
}
|
|
149
162
|
|
|
@@ -164,11 +177,13 @@ function stepEslint() {
|
|
|
164
177
|
const before = withImport.slice(0, close);
|
|
165
178
|
const needsComma = /[^[\s,]\s*$/.test(before);
|
|
166
179
|
const next =
|
|
167
|
-
before.replace(/\s*$/,
|
|
180
|
+
before.replace(/\s*$/, '') +
|
|
181
|
+
(needsComma ? ',' : '') +
|
|
182
|
+
`\n${ESLINT_SPREAD}\n` +
|
|
168
183
|
withImport.slice(close);
|
|
169
184
|
|
|
170
185
|
write(path, next);
|
|
171
|
-
add(
|
|
186
|
+
add('added', file, '프리셋(configs.recommended) 연결');
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
/* ────────────────── 3. 전역 CSS ────────────────── */
|
|
@@ -176,14 +191,19 @@ function stepEslint() {
|
|
|
176
191
|
/** 앱의 전역 CSS 를 찾는다 */
|
|
177
192
|
function findGlobalCss() {
|
|
178
193
|
const candidates = [
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
'app/globals.css',
|
|
195
|
+
'src/app/globals.css',
|
|
196
|
+
'src/styles/globals.css',
|
|
197
|
+
'styles/globals.css',
|
|
198
|
+
'src/index.css',
|
|
199
|
+
'src/main.css',
|
|
200
|
+
'src/global.css',
|
|
181
201
|
];
|
|
182
202
|
const found = firstExisting(candidates);
|
|
183
203
|
if (found) return found;
|
|
184
204
|
|
|
185
205
|
// 후보에 없으면 @import "tailwindcss" 가 있는 css 를 얕게 탐색
|
|
186
|
-
const roots = [
|
|
206
|
+
const roots = ['src', 'app', 'styles'].filter(d => existsSync(join(cwd, d)));
|
|
187
207
|
for (const root of roots) {
|
|
188
208
|
const hit = walkCss(join(cwd, root), 3);
|
|
189
209
|
if (hit) return relative(cwd, hit);
|
|
@@ -200,7 +220,7 @@ function walkCss(dir, depth) {
|
|
|
200
220
|
return null;
|
|
201
221
|
}
|
|
202
222
|
for (const name of entries) {
|
|
203
|
-
if (name ===
|
|
223
|
+
if (name === 'node_modules' || name.startsWith('.')) continue;
|
|
204
224
|
const p = join(dir, name);
|
|
205
225
|
let st;
|
|
206
226
|
try {
|
|
@@ -211,9 +231,9 @@ function walkCss(dir, depth) {
|
|
|
211
231
|
if (st.isDirectory()) {
|
|
212
232
|
const hit = walkCss(p, depth - 1);
|
|
213
233
|
if (hit) return hit;
|
|
214
|
-
} else if (name.endsWith(
|
|
234
|
+
} else if (name.endsWith('.css')) {
|
|
215
235
|
try {
|
|
216
|
-
if (read(p).includes(
|
|
236
|
+
if (read(p).includes('tailwindcss')) return p;
|
|
217
237
|
} catch {
|
|
218
238
|
/* 읽기 실패는 무시 */
|
|
219
239
|
}
|
|
@@ -227,9 +247,9 @@ function stepGlobalCss() {
|
|
|
227
247
|
|
|
228
248
|
if (!file) {
|
|
229
249
|
add(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
'manual',
|
|
251
|
+
'전역 CSS',
|
|
252
|
+
'전역 CSS 를 찾지 못했습니다 — 앱의 전역 CSS 에 아래를 추가하세요',
|
|
233
253
|
`@import 'tailwindcss';\n@import '${PKG}/theme.css';\n@source "<상대경로>/node_modules/${PKG}/dist";`,
|
|
234
254
|
);
|
|
235
255
|
return;
|
|
@@ -239,14 +259,14 @@ function stepGlobalCss() {
|
|
|
239
259
|
const src = read(path);
|
|
240
260
|
|
|
241
261
|
// CSS 파일 기준 상대경로로 @source 를 계산한다 (README 주의사항)
|
|
242
|
-
const toNodeModules = relative(dirname(path), join(cwd,
|
|
243
|
-
const sourcePath = toNodeModules.split(sep).join(
|
|
262
|
+
const toNodeModules = relative(dirname(path), join(cwd, 'node_modules', PKG, 'dist'));
|
|
263
|
+
const sourcePath = toNodeModules.split(sep).join('/');
|
|
244
264
|
|
|
245
265
|
const hasTheme = src.includes(`${PKG}/theme.css`);
|
|
246
266
|
const hasSource = src.includes(`node_modules/${PKG}/dist`);
|
|
247
267
|
|
|
248
268
|
if (hasTheme && hasSource) {
|
|
249
|
-
add(
|
|
269
|
+
add('skipped', file, 'theme.css · @source 가 이미 설정되어 있습니다');
|
|
250
270
|
return;
|
|
251
271
|
}
|
|
252
272
|
|
|
@@ -258,28 +278,28 @@ function stepGlobalCss() {
|
|
|
258
278
|
const tw = src.match(/@import\s+["']tailwindcss["'];?/);
|
|
259
279
|
if (!tw) {
|
|
260
280
|
add(
|
|
261
|
-
|
|
281
|
+
'manual',
|
|
262
282
|
file,
|
|
263
283
|
"@import 'tailwindcss' 를 찾지 못했습니다 — 아래를 직접 추가하세요",
|
|
264
|
-
lines.join(
|
|
284
|
+
lines.join('\n'),
|
|
265
285
|
);
|
|
266
286
|
return;
|
|
267
287
|
}
|
|
268
288
|
|
|
269
289
|
const insertAt = tw.index + tw[0].length;
|
|
270
|
-
const next = src.slice(0, insertAt) +
|
|
290
|
+
const next = src.slice(0, insertAt) + '\n' + lines.join('\n') + src.slice(insertAt);
|
|
271
291
|
write(path, next);
|
|
272
|
-
add(
|
|
292
|
+
add('added', file, lines.length === 2 ? 'theme.css import · @source 추가' : '누락분 추가');
|
|
273
293
|
}
|
|
274
294
|
|
|
275
295
|
/* ─────────────────────────── 실행 ─────────────────────────── */
|
|
276
296
|
|
|
277
297
|
function printHelp() {
|
|
278
298
|
console.log(`
|
|
279
|
-
${paint(
|
|
299
|
+
${paint('bold', 'sellmate-ds')} — ${PKG} 소비 앱 설정
|
|
280
300
|
|
|
281
|
-
${paint(
|
|
282
|
-
${paint(
|
|
301
|
+
${paint('cyan', 'npx sellmate-ds init')} 설정을 자동으로 연결합니다
|
|
302
|
+
${paint('cyan', 'npx sellmate-ds init --dry-run')} 무엇이 바뀔지만 보여줍니다
|
|
283
303
|
|
|
284
304
|
연결하는 것
|
|
285
305
|
· CLAUDE.md / AGENTS.md AI 에이전트가 규칙(llms.txt)을 읽도록 지침 추가
|
|
@@ -290,13 +310,13 @@ ${paint("bold", "sellmate-ds")} — ${PKG} 소비 앱 설정
|
|
|
290
310
|
`);
|
|
291
311
|
}
|
|
292
312
|
|
|
293
|
-
if (command ===
|
|
313
|
+
if (command === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
294
314
|
printHelp();
|
|
295
315
|
process.exit(0);
|
|
296
316
|
}
|
|
297
317
|
|
|
298
|
-
if (command !==
|
|
299
|
-
console.error(paint(
|
|
318
|
+
if (command !== 'init') {
|
|
319
|
+
console.error(paint('red', `알 수 없는 명령: ${command}`));
|
|
300
320
|
printHelp();
|
|
301
321
|
process.exit(1);
|
|
302
322
|
}
|
|
@@ -304,34 +324,46 @@ if (command !== "init") {
|
|
|
304
324
|
assertProjectRoot();
|
|
305
325
|
|
|
306
326
|
console.log(
|
|
307
|
-
`\n${paint(
|
|
327
|
+
`\n${paint('bold', `${PKG} 설정`)}${dryRun ? paint('yellow', ' (dry-run — 파일을 바꾸지 않습니다)') : ''}\n`,
|
|
308
328
|
);
|
|
309
329
|
|
|
310
330
|
stepAgentInstructions();
|
|
311
331
|
stepEslint();
|
|
312
332
|
stepGlobalCss();
|
|
313
333
|
|
|
314
|
-
const icon = {
|
|
334
|
+
const icon = {
|
|
335
|
+
added: paint('green', '✓'),
|
|
336
|
+
skipped: paint('dim', '·'),
|
|
337
|
+
manual: paint('yellow', '!'),
|
|
338
|
+
error: paint('red', '✗'),
|
|
339
|
+
};
|
|
315
340
|
|
|
316
341
|
for (const r of results) {
|
|
317
|
-
console.log(` ${icon[r.status]} ${paint(
|
|
342
|
+
console.log(` ${icon[r.status]} ${paint('bold', r.file)} ${paint('dim', r.detail)}`);
|
|
318
343
|
if (r.snippet) {
|
|
319
|
-
console.log(
|
|
344
|
+
console.log(
|
|
345
|
+
r.snippet
|
|
346
|
+
.split('\n')
|
|
347
|
+
.map(l => ` ${paint('cyan', l)}`)
|
|
348
|
+
.join('\n'),
|
|
349
|
+
);
|
|
320
350
|
}
|
|
321
351
|
}
|
|
322
352
|
|
|
323
|
-
const added = results.filter(
|
|
324
|
-
const manual = results.filter(
|
|
353
|
+
const added = results.filter(r => r.status === 'added').length;
|
|
354
|
+
const manual = results.filter(r => r.status === 'manual').length;
|
|
325
355
|
|
|
326
356
|
console.log();
|
|
327
357
|
if (dryRun) {
|
|
328
|
-
console.log(
|
|
358
|
+
console.log(
|
|
359
|
+
paint('yellow', ` ${added}개 항목이 변경됩니다. --dry-run 을 빼고 다시 실행하세요.`),
|
|
360
|
+
);
|
|
329
361
|
} else if (added) {
|
|
330
|
-
console.log(paint(
|
|
362
|
+
console.log(paint('green', ` ${added}개 항목을 설정했습니다.`));
|
|
331
363
|
} else if (!manual) {
|
|
332
|
-
console.log(paint(
|
|
364
|
+
console.log(paint('dim', ' 이미 모두 설정되어 있습니다.'));
|
|
333
365
|
}
|
|
334
366
|
if (manual) {
|
|
335
|
-
console.log(paint(
|
|
367
|
+
console.log(paint('yellow', ` ${manual}개 항목은 위 내용을 직접 추가해야 합니다.`));
|
|
336
368
|
}
|
|
337
369
|
console.log();
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
| Prop | Type | Default | Description |
|
|
10
10
|
|------|------|---------|-------------|
|
|
11
11
|
| `value?` | `number` | `0` | 진행률 (0–100) |
|
|
12
|
-
| `type?` | `SCircleProgressType` | `'primary'` |
|
|
12
|
+
| `type?` | `SCircleProgressType` | `'primary'` | 진행 상태 - `primary`: 정상 진행 중 - `warning`: 진행 중이지만 설정된 기준에 미달해 주의가 필요하다 (기준은 제품이 정한다) - `error`: 실패·중단 등 더 이상 정상적으로 진행되지 않는다 - `complete`: 완료 - `inverse`: 어두운 배경 위에 놓을 때 - `neutral`: 콘텐츠가 아직 없는 자리를 채울 때 (대기) |
|
|
13
13
|
| `indeterminate?` | `boolean` | `false` | 불확정(스피너) 모드 — value 무시 |
|
|
14
|
-
| `size?` | `number \| string` |
|
|
14
|
+
| `size?` | `number \| string` | — | 링의 지름. 숫자는 px 로 해석한다. 주지 않으면 `--cmp-progress-circular-size` 를 쓴다. 부모 크기에 비례시키려면 컨테이너 쿼리 단위(`'45cqh'`)를 쓴다 — 퍼센트는 부모가 inline-flex 라 기준 폭이 정해지지 않아 해석되지 않는다. |
|
|
15
15
|
| `label?` | `string` | — | 하단 레이블 |
|
|
16
16
|
| `innerValue?` | `boolean` | `false` | true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 |
|
|
17
17
|
| `className?` | `string` | — | |
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
### SCircleProgressType
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
|
-
export type SCircleProgressType =
|
|
25
|
+
export type SCircleProgressType =
|
|
26
|
+
'primary' | 'inverse' | 'warning' | 'error' | 'complete' | 'neutral';
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
## Dependencies
|
|
@@ -32,6 +33,7 @@ export type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete' |
|
|
|
32
33
|
- [SImage](../SImage)
|
|
33
34
|
- [SLoadingContainer](../SLoadingContainer)
|
|
34
35
|
- [SLoadingModal](../SLoadingModal)
|
|
36
|
+
- [SSelect](../SSelect)
|
|
35
37
|
- [STable](../STable)
|
|
36
38
|
|
|
37
39
|
### Graph
|
|
@@ -41,6 +43,7 @@ graph TD;
|
|
|
41
43
|
SImage --> SCircleProgress
|
|
42
44
|
SLoadingContainer --> SCircleProgress
|
|
43
45
|
SLoadingModal --> SCircleProgress
|
|
46
|
+
SSelect --> SCircleProgress
|
|
44
47
|
STable --> SCircleProgress
|
|
45
48
|
style SCircleProgress fill:#f9f,stroke:#333,stroke-width:4px
|
|
46
49
|
```
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { type CSSProperties } from 'react';
|
|
2
|
-
export type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete' | 'neutral';
|
|
2
|
+
export type SCircleProgressType = 'primary' | 'inverse' | 'warning' | 'error' | 'complete' | 'neutral';
|
|
3
3
|
export interface SCircleProgressProps {
|
|
4
4
|
/** 진행률 (0–100) */
|
|
5
5
|
value?: number;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* 진행 상태
|
|
8
|
+
* - `primary`: 정상 진행 중
|
|
9
|
+
* - `warning`: 진행 중이지만 설정된 기준에 미달해 주의가 필요하다 (기준은 제품이 정한다)
|
|
10
|
+
* - `error`: 실패·중단 등 더 이상 정상적으로 진행되지 않는다
|
|
11
|
+
* - `complete`: 완료
|
|
12
|
+
* - `inverse`: 어두운 배경 위에 놓을 때
|
|
13
|
+
* - `neutral`: 콘텐츠가 아직 없는 자리를 채울 때 (대기)
|
|
14
|
+
*/
|
|
7
15
|
type?: SCircleProgressType;
|
|
8
16
|
/** 불확정(스피너) 모드 — value 무시 */
|
|
9
17
|
indeterminate?: boolean;
|
|
10
18
|
/**
|
|
11
|
-
* 링의 지름. 숫자는 px 로 해석한다.
|
|
19
|
+
* 링의 지름. 숫자는 px 로 해석한다. 주지 않으면 `--cmp-progress-circular-size` 를 쓴다.
|
|
12
20
|
* 부모 크기에 비례시키려면 컨테이너 쿼리 단위(`'45cqh'`)를 쓴다 — 퍼센트는 부모가
|
|
13
21
|
* inline-flex 라 기준 폭이 정해지지 않아 해석되지 않는다.
|
|
14
22
|
*/
|
|
@@ -53,7 +53,7 @@ export interface SLauncherListBoxItem {
|
|
|
53
53
|
* 있는지 보이지 않는다). 대신 아래 `LOGO_COVERAGE` 가 로고 목록과 어긋나지 않는지 타입으로 지킨다.
|
|
54
54
|
*/
|
|
55
55
|
export type SLauncherService =
|
|
56
|
-
'sellmate' | 'wms' | 'chat' | 'plm' | 'account' | 'people' | 'crm' | 'work' | 'pos';
|
|
56
|
+
'sellmate' | 'wms' | 'chat' | 'chatbot' | 'plm' | 'account' | 'people' | 'crm' | 'work' | 'pos';
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
## Dependencies
|
|
@@ -38,12 +38,12 @@ export declare const LAUNCHER_LIST_BOX_TRAILING_ICON: SIconName;
|
|
|
38
38
|
* 문서에서 바로 읽히도록 키를 풀어 쓴다(`Exclude<SLogoName, …>` 로 두면 README·Storybook 에 무엇이
|
|
39
39
|
* 있는지 보이지 않는다). 대신 아래 `LOGO_COVERAGE` 가 로고 목록과 어긋나지 않는지 타입으로 지킨다.
|
|
40
40
|
*/
|
|
41
|
-
export type SLauncherService = 'sellmate' | 'wms' | 'chat' | 'plm' | 'account' | 'people' | 'crm' | 'work' | 'pos';
|
|
41
|
+
export type SLauncherService = 'sellmate' | 'wms' | 'chat' | 'chatbot' | 'plm' | 'account' | 'people' | 'crm' | 'work' | 'pos';
|
|
42
42
|
/**
|
|
43
43
|
* 런처에 설 수 있는 서비스 — **목록의 순서이기도 하다**(로고 순서와 같다).
|
|
44
44
|
* 앱이 넘긴 순서가 아니라 이 순서로 선다: 같은 런처가 앱마다 다른 순서로 뜨면 사용자가 매번 다시 찾는다.
|
|
45
45
|
*/
|
|
46
|
-
export declare const LAUNCHER_SERVICES: readonly ["sellmate", "wms", "chat", "plm", "account", "people", "crm", "work", "pos"];
|
|
46
|
+
export declare const LAUNCHER_SERVICES: readonly ["sellmate", "wms", "chat", "chatbot", "plm", "account", "people", "crm", "work", "pos"];
|
|
47
47
|
/**
|
|
48
48
|
* 서비스가 아닌 로고 — 런처의 서비스 줄로 세우지 않는다.
|
|
49
49
|
* `console` 은 통합 콘솔이라 목록 아래 "셀메이트 콘솔로 이동" 링크가 늘 맡는다. 줄로도 세우면 같은 곳이 두 번 선다.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| Prop | Type | Default | Description |
|
|
10
10
|
|------|------|---------|-------------|
|
|
11
11
|
| `value?` | `number` | `0` | 진행률 (0–100) |
|
|
12
|
-
| `type?` | `SLinearProgressType` | `'primary'` |
|
|
12
|
+
| `type?` | `SLinearProgressType` | `'primary'` | 진행 상태 - `primary`: 정상 진행 중 - `warning`: 진행 중이지만 설정된 기준에 미달해 주의가 필요하다 (기준은 제품이 정한다) - `error`: 실패·중단 등 더 이상 정상적으로 진행되지 않는다 - `complete`: 완료 |
|
|
13
13
|
| `size?` | `SLinearProgressSize` | `'sm'` | 바 높이. xs 는 바 안에 퍼센트 텍스트를 넣지 않는다 |
|
|
14
14
|
| `indeterminate?` | `boolean` | `false` | 진행률 없이 무한 애니메이션 |
|
|
15
15
|
| `label?` | `string` | — | 하단 레이블 |
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
### SLinearProgressType
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
export type SLinearProgressType = 'primary' | 'error' | 'complete';
|
|
24
|
+
export type SLinearProgressType = 'primary' | 'warning' | 'error' | 'complete';
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
### SLinearProgressSize
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { type CSSProperties } from 'react';
|
|
2
|
-
export type SLinearProgressType = 'primary' | 'error' | 'complete';
|
|
2
|
+
export type SLinearProgressType = 'primary' | 'warning' | 'error' | 'complete';
|
|
3
3
|
export type SLinearProgressSize = 'xs' | 'sm' | 'md';
|
|
4
4
|
export interface SLinearProgressProps {
|
|
5
5
|
/** 진행률 (0–100) */
|
|
6
6
|
value?: number;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* 진행 상태
|
|
9
|
+
* - `primary`: 정상 진행 중
|
|
10
|
+
* - `warning`: 진행 중이지만 설정된 기준에 미달해 주의가 필요하다 (기준은 제품이 정한다)
|
|
11
|
+
* - `error`: 실패·중단 등 더 이상 정상적으로 진행되지 않는다
|
|
12
|
+
* - `complete`: 완료
|
|
13
|
+
*/
|
|
8
14
|
type?: SLinearProgressType;
|
|
9
15
|
/** 바 높이. xs 는 바 안에 퍼센트 텍스트를 넣지 않는다 */
|
|
10
16
|
size?: SLinearProgressSize;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 담고 있는 로고 — 셀메이트 본체와 서비스들, 그리고 통합 콘솔. 로고 한 벌은 [셀메이트 워드마크 + 표기]다.
|
|
9
9
|
* 늘어나면 여기와 `logos.tsx` 의 맵에 함께 추가한다.
|
|
10
10
|
*/
|
|
11
|
-
export declare const LOGO_NAMES: readonly ["sellmate", "wms", "chat", "plm", "account", "people", "crm", "work", "pos", "console"];
|
|
11
|
+
export declare const LOGO_NAMES: readonly ["sellmate", "wms", "chat", "chatbot", "plm", "account", "people", "crm", "work", "pos", "console"];
|
|
12
12
|
export type SLogoName = (typeof LOGO_NAMES)[number];
|
|
13
13
|
/**
|
|
14
14
|
* 로고가 서는 배경. 밝은 면에는 `light`(남색 워드마크), 어두운 면에는 `dark`(흰 워드마크).
|